Initial commit

This commit is contained in:
Max Känner 2023-07-30 14:03:01 +02:00
commit 95516d9405
4 changed files with 1688 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/target
.~*
*.ods

1489
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

17
Cargo.toml Normal file
View 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
View 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(())
}