@@ -483,7 +483,7 @@ def section_cut_xz(body: OpenSCADObject, y_cut_point:float=0) -> OpenSCADObject:
483483# =====================
484484# Any part defined in a method can be automatically counted using the
485485# `@bom_part()` decorator. After all parts have been created, call
486- # `bill_of_materials()`
486+ # `bill_of_materials(<SCAD_OBJ> )`
487487# to generate a report. See `examples/bom_scad.py` for usage
488488#
489489# 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:
493493# populate the new columns in order of their addition via bom_headers, or
494494# keyworded arguments can be used in any order.
495495
496- g_parts_dict = {}
497496g_bom_headers : List [str ] = []
498497
499498def set_bom_headers (* args ):
@@ -504,31 +503,53 @@ def bom_part(description: str='', per_unit_price:float=None, currency: str='US$'
504503 def wrap (f ):
505504 name = description if description else f .__name__
506505
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 }
509507 # This update also adds empty key value pairs to prevent key exceptions.
510508 elements .update (dict (zip_longest (g_bom_headers , args , fillvalue = '' )))
511509 elements .update (kwargs )
512510
513- g_parts_dict [name ] = elements
514-
515511 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
519515
520516 return wrapped_f
521517
522518 return wrap
523519
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 :
525545 field_names = ["Description" , "Count" , "Unit Price" , "Total Price" ]
526546 field_names += g_bom_headers
527547
528548 rows = []
529549
530550 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 = []
532553 count = elements ['Count' ]
533554 currency = elements ['currency' ]
534555 price = elements ['Unit Price' ]
0 commit comments