@@ -483,7 +483,7 @@ def section_cut_xz(body: OpenSCADObject, y_cut_point:float=0) -> OpenSCADObject:
483
483
# =====================
484
484
# Any part defined in a method can be automatically counted using the
485
485
# `@bom_part()` decorator. After all parts have been created, call
486
- # `bill_of_materials()`
486
+ # `bill_of_materials(<SCAD_OBJ> )`
487
487
# to generate a report. See `examples/bom_scad.py` for usage
488
488
#
489
489
# Additional columns can be added (such as leftover material or URL to part)
@@ -493,7 +493,6 @@ def section_cut_xz(body: OpenSCADObject, y_cut_point:float=0) -> OpenSCADObject:
493
493
# populate the new columns in order of their addition via bom_headers, or
494
494
# keyworded arguments can be used in any order.
495
495
496
- g_parts_dict = {}
497
496
g_bom_headers : List [str ] = []
498
497
499
498
def set_bom_headers (* args ):
@@ -504,31 +503,53 @@ def bom_part(description: str='', per_unit_price:float=None, currency: str='US$'
504
503
def wrap (f ):
505
504
name = description if description else f .__name__
506
505
507
- elements = {}
508
- elements .update ({'Count' :0 , 'currency' :currency , 'Unit Price' :per_unit_price })
506
+ elements = {'name' : name , 'Count' :0 , 'currency' :currency , 'Unit Price' :per_unit_price }
509
507
# This update also adds empty key value pairs to prevent key exceptions.
510
508
elements .update (dict (zip_longest (g_bom_headers , args , fillvalue = '' )))
511
509
elements .update (kwargs )
512
510
513
- g_parts_dict [name ] = elements
514
-
515
511
def wrapped_f (* wargs , ** wkwargs ):
516
- name = description if description else f . __name__
517
- g_parts_dict [ name ][ 'Count' ] += 1
518
- return f ( * wargs , ** wkwargs )
512
+ scad_obj = f ( * wargs , ** wkwargs )
513
+ scad_obj . add_trait ( 'BOM' , elements )
514
+ return scad_obj
519
515
520
516
return wrapped_f
521
517
522
518
return wrap
523
519
524
- def bill_of_materials (csv :bool = False ) -> str :
520
+ def bill_of_materials (root_obj :OpenSCADObject , csv :bool = False ) -> str :
521
+ traits_dicts = _traits_bom_dicts (root_obj )
522
+ # Build a single dictionary from the ones stored on each child object
523
+ # (This is an adaptation of an earlier version, and probably not the most
524
+ # direct way to accomplish this)
525
+ all_bom_traits = {}
526
+ for traits_dict in traits_dicts :
527
+ name = traits_dict ['name' ]
528
+ if name in all_bom_traits :
529
+ all_bom_traits [name ]['Count' ] += 1
530
+ else :
531
+ all_bom_traits [name ] = traits_dict
532
+ all_bom_traits [name ]['Count' ] = 1
533
+ bom = _make_bom (all_bom_traits , csv )
534
+ return bom
535
+
536
+ def _traits_bom_dicts (root_obj :OpenSCADObject ) -> List [Dict [str , float ]]:
537
+ all_child_traits = [_traits_bom_dicts (c ) for c in root_obj .children ]
538
+ child_traits = [item for subl in all_child_traits for item in subl if item ]
539
+ bom_trait = root_obj .get_trait ('BOM' )
540
+ if bom_trait :
541
+ child_traits .append (bom_trait )
542
+ return child_traits
543
+
544
+ def _make_bom (bom_parts_dict : Dict [str , float ], csv :bool = False , ) -> str :
525
545
field_names = ["Description" , "Count" , "Unit Price" , "Total Price" ]
526
546
field_names += g_bom_headers
527
547
528
548
rows = []
529
549
530
550
all_costs : Dict [str , float ] = {}
531
- for desc , elements in g_parts_dict .items ():
551
+ for desc , elements in bom_parts_dict .items ():
552
+ row = []
532
553
count = elements ['Count' ]
533
554
currency = elements ['currency' ]
534
555
price = elements ['Unit Price' ]
0 commit comments