From a782d43e67f4091f44bd9018817e7263e2944477 Mon Sep 17 00:00:00 2001 From: Chris Palmer Date: Fri, 4 Jun 2021 17:47:29 +0100 Subject: [PATCH] bom.py now generates bom.csv to allow costed BOMs to be made using a spreadsheet. --- docs/usage.md | 9 +++++++ examples/MainsBreakOutBox/bom/bom.csv | 16 ++++++++++++ scripts/bom.py | 35 +++++++++++++++++++++++++++ 3 files changed, 60 insertions(+) create mode 100644 examples/MainsBreakOutBox/bom/bom.csv diff --git a/docs/usage.md b/docs/usage.md index ca305db..eac4868 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -277,6 +277,15 @@ The top level assembly instructions and assembly contents could also be differen If the top level module is just a shell wrapper that simply includes one other assembly, with no additional parts, then it is removed from the build instructions and the assembly it calls becomes the top level. This allows a different project description for each target but only one set of top level instructions without repeating them. +### Costed BOMs + +A costed bill of materials can be made by opening the generated file `bom/bom.csv` in a spreadsheet program using a single quote as the string delimiter and comma as the field separator. +That gets a list of part descriptions and quantities to which prices can be added to get the total cost and perhaps a URL of where to buy each part. + +If a Python file called `parts.py` is found then `bom.py` will attempt to call functions for each part to get a price and URL. +Any functions not found are printed, so you can see the format expected. +The function are passed the quantity to allow them to calculate volume discounts, etc. + ### Other libraries The build scripts need to be able to locate the source files where the modules to generate the STL files and assemblies reside. They will search all the scad files diff --git a/examples/MainsBreakOutBox/bom/bom.csv b/examples/MainsBreakOutBox/bom/bom.csv new file mode 100644 index 0000000..f75bbf9 --- /dev/null +++ b/examples/MainsBreakOutBox/bom/bom.csv @@ -0,0 +1,16 @@ +'Ferrule for 1.5mm^2 wire - not shown', 3 +'Wire blue 30/0.25mm strands, length 150mm - not shown', 2 +'Wire brown 30/0.25mm strands, length 150mm - not shown', 2 +'Wire green & yellow 30/0.25mm strands, length 150mm - not shown', 2 +'IEC inlet for ATX', 1 +'Heatfit insert M3', 2 +'4mm shielded jack socket blue', 2 +'4mm shielded jack socket brown', 1 +'4mm shielded jack socket green', 2 +'Mains socket 13A', 1 +'Nut M3 x 2.4mm nyloc', 6 +'Screw M3 cs cap x 12mm', 2 +'Screw M3 cs cap x 20mm', 2 +'Screw M3 dome x 10mm', 4 +'Heatshrink sleeving ID 3.2mm x 15mm - not shown', 8 +'Washer M3 x 7mm x 0.5mm', 10 diff --git a/scripts/bom.py b/scripts/bom.py index 48267ba..9ec05bc 100755 --- a/scripts/bom.py +++ b/scripts/bom.py @@ -31,6 +31,12 @@ from set_config import * import json import re +try: + import parts + got_parts_py = True +except: + got_parts_py = False + def find_scad_file(mname): for filename in os.listdir(source_dir): if filename[-5:] == ".scad": @@ -129,6 +135,33 @@ class BOM: return ass return ass.replace("assembly", "assemblies") + def print_CSV(self, file = None): + i = 0 + for part in sorted(self.vitamins): + i += 1 + if ': ' in part: + part_no, description = part.split(': ') + else: + part_no, description = "", part + qty = self.vitamins[part].count + if got_parts_py: + match = re.match(r'^.*\((.*?)[,\)].*$', part_no) + if match and not match.group(1).startswith('"'): + part_no = part_no.replace('(' + match.group(1), '_' + match.group(1) + '(').replace('(, ', '(') + func = 'parts.' + part_no.replace('(', '(%d, ' % qty).replace(', )', ')') + func = func.replace('true', 'True').replace('false', 'False').replace('undef', 'None') + try: + price, url = eval(func) + print("'%s',%3d,%.2f,'=B%d*C%d',%s" % (description, qty, price, i, i, url), file=file) + except: + if part_no: + print("%s not found in parts.py" % func) + print("'%s',%3d" % (description, qty), file=file) + else: + print("'%s',%3d" % (description, qty), file=file) + if got_parts_py: + print(",'=SUM(B1:B%d)',,'=SUM(D1:D%d)'" %(i, i), file=file) + def print_bom(self, breakdown, file = None): if self.vitamins: print("Vitamins:", file=file) @@ -265,6 +298,8 @@ def boms(target = None): main.print_bom(True, open(bom_dir + "/bom.txt","wt")) + main.print_CSV(open(bom_dir + "/bom.csv","wt")) + for ass in main.assemblies: with open(bom_dir + "/" + ass + ".txt", "wt") as f: bom = main.assemblies[ass]