Initial commit
This commit is contained in:
		
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| /target | ||||
| .~* | ||||
| *.ods | ||||
							
								
								
									
										1489
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										1489
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										17
									
								
								Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								Cargo.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| [package] | ||||
| name = "bom-combine" | ||||
| version = "0.1.0" | ||||
| edition = "2021" | ||||
|  | ||||
| # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||||
|  | ||||
| [dependencies] | ||||
|  | ||||
| spreadsheet-ods = "0.12" | ||||
| csv = "1.2" | ||||
| clap = { version = "4.3", features = ["derive"] } | ||||
| color-eyre = "0.6" | ||||
| log = "0.4" | ||||
| stderrlog = "0.5" | ||||
| serde = { version = "1.0", features = ["derive"] } | ||||
| icu_locid = "1.2" | ||||
							
								
								
									
										179
									
								
								src/main.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										179
									
								
								src/main.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,179 @@ | ||||
| use std::{ | ||||
|     cmp::Ordering, | ||||
|     collections::{hash_map::Entry, HashMap}, | ||||
|     path::Path, | ||||
| }; | ||||
|  | ||||
| use clap::Parser; | ||||
| use color_eyre::eyre::Result; | ||||
| use icu_locid::locale; | ||||
| use log::{debug, error, info}; | ||||
| use serde::Deserialize; | ||||
| use spreadsheet_ods::{ | ||||
|     formula::{fcellref, fcellrefa, fcellrefa_table}, | ||||
|     write_ods, CellStyle, CellStyleRef, Sheet, ValueFormatNumber, WorkBook, | ||||
| }; | ||||
|  | ||||
| #[derive(Parser, Debug)] | ||||
| struct Args { | ||||
|     #[arg(required = true)] | ||||
|     input: Vec<String>, | ||||
|     output: String, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Deserialize, Clone)] | ||||
| struct Record { | ||||
|     #[serde(rename(deserialize = "Reference"))] | ||||
|     reference: String, | ||||
|     #[serde(rename(deserialize = "Qty"))] | ||||
|     qty: u64, | ||||
|     #[serde(rename(deserialize = "Value"))] | ||||
|     value: String, | ||||
|     #[serde(rename(deserialize = "Footprint"))] | ||||
|     footprint: String, | ||||
|     #[serde(rename(deserialize = "WE Part"))] | ||||
|     we_part: Option<String>, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone)] | ||||
| struct Bom { | ||||
|     name: String, | ||||
|     entries: Vec<Record>, | ||||
| } | ||||
|  | ||||
| fn main() -> Result<()> { | ||||
|     color_eyre::install()?; | ||||
|     stderrlog::new() | ||||
|         .module(module_path!()) | ||||
|         .verbosity(3) | ||||
|         .init()?; | ||||
|     let args = Args::parse(); | ||||
|  | ||||
|     let mut boms = vec![]; | ||||
|  | ||||
|     for path in args.input { | ||||
|         let path = Path::new(&path); | ||||
|         if !path.exists() { | ||||
|             error!("{:?} does not exist", path); | ||||
|             continue; | ||||
|         } | ||||
|         info!("Opening {:?} as input", path); | ||||
|         let mut reader = csv::Reader::from_path(path)?; | ||||
|         let bom = Bom { | ||||
|             name: path.file_name().unwrap().to_str().unwrap().to_owned(), | ||||
|             entries: reader.deserialize().collect::<Result<Vec<_>, _>>()?, | ||||
|         }; | ||||
|         boms.push(bom); | ||||
|     } | ||||
|     let mut wb = WorkBook::new(locale!("en_US")); | ||||
|  | ||||
|     let mut summary_sheet = Sheet::new("Summary"); | ||||
|     summary_sheet.set_value(0, 0, "#"); | ||||
|     summary_sheet.set_value(0, 1, "Qty"); | ||||
|     summary_sheet.set_value(0, 2, "Value"); | ||||
|     summary_sheet.set_value(0, 3, "Footprint"); | ||||
|     summary_sheet.set_value(0, 4, "WE Part"); | ||||
|     summary_sheet.set_value(0, 5, "Price per Unit"); | ||||
|     summary_sheet.set_value(0, 6, "Total Price"); | ||||
|     summary_sheet.set_value(0, 7, "Link"); | ||||
|     summary_sheet.set_value(0, 10, "Name"); | ||||
|     summary_sheet.set_value(0, 11, "Amount"); | ||||
|     wb.push_sheet(summary_sheet); | ||||
|  | ||||
|     let mut parts: Vec<(_, _, Option<_>, _)> = vec![]; | ||||
|  | ||||
|     for bom in boms { | ||||
|         info!("Coppying {} to output file", bom.name); | ||||
|         let mut sheet = Sheet::new(&bom.name); | ||||
|         sheet.set_value(0, 0, "#"); | ||||
|         sheet.set_value(0, 1, "Reference"); | ||||
|         sheet.set_value(0, 2, "Qty"); | ||||
|         sheet.set_value(0, 3, "Value"); | ||||
|         sheet.set_value(0, 4, "Footprint"); | ||||
|         sheet.set_value(0, 5, "WE Part"); | ||||
|  | ||||
|         for (i, entry) in bom.entries.iter().enumerate() { | ||||
|             let row = u32::try_from(i)? + 1; | ||||
|             sheet.set_value(row, 0, row); | ||||
|             sheet.set_value(row, 1, &entry.reference); | ||||
|             sheet.set_value(row, 2, entry.qty); | ||||
|             sheet.set_value(row, 3, &entry.value); | ||||
|             sheet.set_value(row, 4, &entry.footprint); | ||||
|             if let Some(we_part) = entry.we_part.to_owned() { | ||||
|                 sheet.set_value(row, 5, we_part); | ||||
|             } | ||||
|             let Some((_, _, _, refs)) = parts.iter_mut().find(|(value, footprint, we_part, _)| { | ||||
|                 value == &entry.value | ||||
|                     && footprint == &entry.footprint | ||||
|                     && (we_part.is_none() || entry.we_part.is_none() || *we_part == entry.we_part) | ||||
|             }) else { | ||||
|                 parts.push((entry.value.to_owned(), entry.footprint.to_owned(), entry.we_part.to_owned(), vec![(bom.name.to_owned(), row)])); | ||||
|                 continue; | ||||
|             }; | ||||
|             refs.push((bom.name.to_owned(), row)); | ||||
|         } | ||||
|  | ||||
|         wb.push_sheet(sheet); | ||||
|     } | ||||
|  | ||||
|     info!("Sorting parts"); | ||||
|     parts.sort_unstable_by( | ||||
|         |(value_a, footprint_a, _, _), (value_b, footprint_b, _, _)| match footprint_a | ||||
|             .cmp(footprint_b) | ||||
|         { | ||||
|             Ordering::Less => Ordering::Less, | ||||
|             Ordering::Equal => value_a.cmp(value_b), | ||||
|             Ordering::Greater => Ordering::Greater, | ||||
|         }, | ||||
|     ); | ||||
|  | ||||
|     info!("Creating summary sheet"); | ||||
|     let mut boards = vec![]; | ||||
|     let summary_sheet = wb.sheet_mut(0); | ||||
|     for (i, (value, footprint, we_part, refs)) in parts.iter().enumerate() { | ||||
|         let row = u32::try_from(i)? + 1; | ||||
|         summary_sheet.set_value(row, 0, row); | ||||
|         summary_sheet.set_value(row, 2, value); | ||||
|         summary_sheet.set_value(row, 3, footprint); | ||||
|         if let Some(we_part) = we_part { | ||||
|             summary_sheet.set_value(row, 4, we_part); | ||||
|         } | ||||
|         summary_sheet.set_formula(row, 6, format!("{}*{}", fcellref(row, 5), fcellref(row, 1))); | ||||
|  | ||||
|         let mut qty_formula = "".to_owned(); | ||||
|         for (board, row) in refs { | ||||
|             let board_index = boards | ||||
|                 .iter() | ||||
|                 .find_map(|(b, i)| if *b == board { Some(*i) } else { None }) | ||||
|                 .unwrap_or_else(|| { | ||||
|                     let index = u32::try_from(boards.len()).unwrap() + 1; | ||||
|                     boards.push((board, index)); | ||||
|                     summary_sheet.set_value(index, 10, board); | ||||
|                     summary_sheet.set_value(index, 11, 1); | ||||
|                     index | ||||
|                 }); | ||||
|             if qty_formula == "" { | ||||
|                 qty_formula = format!( | ||||
|                     "{}*{}", | ||||
|                     fcellrefa_table(board, *row, 2), | ||||
|                     fcellrefa(board_index, 11) | ||||
|                 ); | ||||
|             } else { | ||||
|                 qty_formula.push_str( | ||||
|                     format!( | ||||
|                         "+{}*{}", | ||||
|                         fcellrefa_table(board, *row, 2), | ||||
|                         fcellrefa(board_index, 11) | ||||
|                     ) | ||||
|                     .as_str(), | ||||
|                 ); | ||||
|             } | ||||
|         } | ||||
|         summary_sheet.set_formula(row, 1, qty_formula); | ||||
|     } | ||||
|  | ||||
|     info!("Saving to {:?}", args.output); | ||||
|     write_ods(&mut wb, args.output)?; | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
		Reference in New Issue
	
	Block a user