Assembly 4 Bom Bug

I forked Assembly 4 and submitted a pull request on some easy bugs that I found with my test files.
https://github.com/Zolko-123/FreeCAD_Assembly4/compare/master...JonasThomas:FreeCAD_Assembly4:master

I ran into another here: https://github.com/JonasThomas/FreeCAD_Assembly4/blob/master/makeBomCmd.py#L303, that s easy enough to fix, but I don’t understand the complete picture at this point, and I thought better to ask before I start hacking away at the code.
PictureforForum.png
This is my situation:

BoltSpacerNutSubAssembly (Assembly) @ /home/jonasthomas/Documents/FreecadProjects/QuestionsReFreecad/Q_003-S.FCStd
 ├─ Constraints 
 ├─ Configurations 
 ├─ M8-Nut (Nut)
 ├─ M8-Washer (Washer)
 ├─ M8-Washer015 (Washer001)
 ├─ M8-Washer016 (Washer002)
 ├─ M8x40-Screw (Screw)
 ├─ M8-Washer014 (Washer003)
 └─ PartSpacer => Spacer @ /home/jonasthomas/Documents/FreecadProjects/QuestionsReFreecad/Q_002-P.FCStd
     └─ BodySpacer

In this test case, I have a customer spacer Q_003-S.FcStd that’s in a separate file for versioning control, that’s linked into the assembly.

    def listParts(self, obj, level=0, parent=None):

        file = open(ConfUserFilejson, 'r')
        self.infoKeysUser = json.load(file).copy()
        file.close()

        max_level = 100
        if self.follow_subassemblies == False:
            max_level = 2;

        if obj == None:
            return

        if self.PartsList == None:
            self.PartsList = {}

        #=======================
        # VISIBLE APP LINK
        #=======================

        if obj.TypeId == 'App::Link':
            if obj.Visibility == True:
                print("ASM4> {level}{obj_typeid} | {obj_name} | {obj_label}".format(level=self.indent(level), obj_label=obj.Label, obj_name=obj.FullName, obj_typeid=obj.TypeId))

                # self.Verbose += "> {level} | {type}: {label}, {fullname}".format(level=obj.Label, type="APP_LINK", label=obj.Label, fullname=obj.FullName)
                # try:
                #     self.Verbose += "- linked: {linked_obj}\n".format(linked_obj=obj.LinkedObject.Name)
                # except:
                #     self.Verbose += "- linked: {linked_obj}\n".format(linked_obj=obj.Name)
                # self.Verbose += '- not included\n\n'

                # Navigate on objects inside a App:Links (Groups of Fastners)
                if obj.ElementCount > 0:
                    for i in range(obj.ElementCount):
                        self.listParts(obj.LinkedObject, level, parent=obj)
                else:
                    self.listParts(obj.LinkedObject, level + 1, parent=obj)

        #==================================
        # MODEL_PART aka ASM4 SUB-ASSEMBLY
        #==================================
        elif obj.TypeId == 'App::Part' and Asm4.isAsm4Model(obj):
            if level > 0 and level <= max_level and self.follow_subassemblies == False:
                # Recover the record, if any
                try:
                    if self.infoKeysUser.get("Document").get('active'):
                        try:
                            doc_name = getattr(obj, self.infoKeysUser.get("Document").get('userData'))
                        except AttributeError:
                            doc_name = obj.Document.Name
                except:
                    doc_name = obj.Document.Name

                # Recover the record, if any
                if self.infoKeysUser.get("Part_Label").get('active'):
                    try:
                        obj_label = getattr(obj, self.infoKeysUser.get("Part_Label").get('userData'))
                    except AttributeError:
                        obj_label = obj.Label

                # The name cannot be Model othewise it will sum all other 'Model' names togueter
                if obj_label == "Model":
                   obj_label = obj.Document.Name

                if obj_label in self.PartsList:
                    if self.PartsList[obj_label]['Document'] == doc_name:
                        qtd = self.PartsList[obj_label]['Qty.'] + 1

                        print("ASM4> {level}| {qtd}x | {obj_typeid} | {obj_name} | {obj_label}".format(level=self.indent(level, tag=" "), obj_label=obj_label, obj_name=obj.FullName, obj_typeid=obj.TypeId, qtd=qtd))

                        self.Verbose += "> {level} | {type}: {label}, {fullname}\n".format(level=obj_label, type="ASM4_PART", label=obj_label, fullname=obj.FullName)
                        self.Verbose += "- object already added (" + str(qtd) + ")\n\n"
                        self.PartsList[obj_label]['Qty.'] = qtd

                else:
                    print("ASM4> {level}| 1x | {obj_typeid} | {obj_name} | {obj_label}".format(level=self.indent(level, tag=" "), obj_label=obj_label, obj_name=obj.FullName, obj_typeid=obj.TypeId))

                    self.Verbose += "> {level} | {type}: {label}, {fullname}\n".format(level=obj_label, type="ASM4_PART", label=obj_label, fullname=obj.FullName)
                    self.Verbose += "- adding object (1)\n"

                    self.PartsList[obj_label] = dict()
                    for prop in self.infoKeysUser:

                        if self.infoKeysUser.get(prop).get('active'):
                            try: # to get partInfo
                                getattr(obj, self.infoKeysUser.get(prop).get('userData'))
                                info = "(predefined)"
                            except AttributeError:
                                crea(self,obj)
                                fill(obj)
                                info = "(extracted)"

                            if self.infoKeysUser.get(prop).get('visible'):
                                data = getattr(obj, self.infoKeysUser.get(prop).get('userData'))
                            else:
                                data = "-"

                            if data == "":
                                data = "-"

                            if prop == "Part_Label":
                                data = obj_label

                            if data != "-":
                                self.Verbose += "- " + prop + ": " + data + " " + info + "\n"

                            self.PartsList[obj_label][self.infoKeysUser.get(prop).get('userData')] = data

                    self.PartsList[obj_label]['Qty.'] = 1
                    self.Verbose += '\n'


[b]        #============================
        # STANDALONE MODEL_PART
        #============================[/b]

        elif obj.TypeId == 'App::Part' and not Asm4.isAsm4Model(obj):
            if level > 0 and level <= max_level:
                obj_label = ""
                # Recover the record, if any
                try:
                    if self.infoKeysUser.get("Document").get('active'):
                        try:
                            doc_name = getattr(obj, self.infoKeysUser.get("Document").get('userData'))
                        except AttributeError:
                            doc_name = obj.Document.Name
                except:
                    doc_name = obj.Document.Name

                # Recover the record, if any

                try:
                    if self.infoKeysUser.get("Part_Label").get('active'):
                        try:
                            obj_label = getattr(obj, self.infoKeysUser.get("Part_Label").get('userData'))
                        except AttributeError:
                            obj_label = obj.Label
                except:
                    doc_name = obj.Label

                # The name cannot be model othewise it will sum all other 'Model' names togueter
                if obj_label == "Model":
                   obj_label = obj.Document.Name

                if obj_label in self.PartsList:
                    if self.PartsList[obj_label]['Document'] == doc_name:
                        qtd = self.PartsList[obj_label]['Qty.'] + 1
                        print("ASM4> {level}| {qtd}x | {obj_typeid} | {obj_name} | {obj_label}".format(level=self.indent(level, tag=" "), obj_label=obj_label, obj_name=obj.FullName, obj_typeid=obj.TypeId, qtd=qtd))
                        self.Verbose += "> {level} | {type}: {label}, {fullname}\n".format(level=obj_label, type="PART", label=obj_label, fullname=obj.FullName)
                        self.Verbose += "- object already added (" + str(qtd) + ")\n\n"
                        self.PartsList[obj_label]['Qty.'] = qtd

                else:
                    print("ASM4> {level}| 1x | {obj_typeid} | {obj_name} | {obj_label}".format(level=self.indent(level, tag=" "), obj_label=obj_label, obj_name=obj.FullName, obj_typeid=obj.TypeId))
                    self.Verbose += "> {level} | {type}: {label}, {fullname}\n".format(level=obj_label, type="PART", label=obj_label, fullname=obj.FullName)
                    self.Verbose += "- adding object (1)\n"
                    self.PartsList[obj_label] = dict()
                    for prop in self.infoKeysUser:
                        if self.infoKeysUser.get(prop).get('active'):
                            try: # to get partInfo
                                getattr(obj, self.infoKeysUser.get(prop).get('userData'))
                                info = "(predefined)"
                            except AttributeError:
                                crea(self,obj)
                                fill(obj)
                                info = "(extracted)"

                            if self.infoKeysUser.get(prop).get('visible'):
                                data = getattr(obj, self.infoKeysUser.get(prop).get('userData'))
                            else:
                                data = "-"

                            if data == "":
                                data = "-"

                            if prop == "Part_Label":
                                data = obj_label

                            if data != "-":
                               [b] self.Verbose += "- " + prop + ": " + data + " " + info + "\n"[/b]

                            self.PartsList[obj_label][self.infoKeysUser.get(prop).get('userData')] = data

                    self.PartsList[obj_label]['Qty.'] = 1
                    self.Verbose += '\n'

        #============================
        # STANDALONE MODEL_PARTDESIGN
        #============================

        elif obj.TypeId == 'PartDesign::Body':

            # if level > 0 and level <= max_level and Asm4.isAsm4Model(parent):
            if level > 0 and level <= max_level :

                ##### This try/except isn't working right - Document field can't be set, but is active by default.
                ##### This results in a blank document name and the Quantity never increments on any part.
                ## Recover the record, if any
                #try:
                #    if self.infoKeysUser.get("Document").get('active'):
                #        try:
                #            doc_name = getattr(obj, self.infoKeysUser.get("Document").get('userData'))
                #        except AttributeError:
                #            doc_name = obj.Document.Name
                #except:
                #    doc_name = obj.Document.Name
                doc_name = obj.Document.Name

                if obj.Label in self.PartsList:
                    if self.PartsList[obj.Label]['Document'] == doc_name:
                        qtd = self.PartsList[obj.Label]['Qty.'] + 1
                        print("ASM4> {level}{qtd}x | {obj_typeid} | {obj_name} | {obj_label}".format(level=self.indent(level, tag=" "), obj_label=obj.Label, obj_name=obj.FullName, obj_typeid=obj.TypeId, qtd=qtd))
                        self.Verbose += "> {level} | {type}: {label}, {fullname}\n".format(level=obj.Label, type="PART", label=obj.Label, fullname=obj.FullName)
                        self.Verbose += "- object already added (" + str(qtd) + ")\n\n"
                        self.PartsList[obj.Label]['Qty.'] = qtd

                else:
                    print("ASM4> {level}1x | {obj_typeid} | {obj_name} | {obj_label}".format(level=self.indent(level, tag=" "), obj_label=obj.Label, obj_name=obj.FullName, obj_typeid=obj.TypeId))
                    self.Verbose += "> {level} | {type}: {label}, {fullname}\n".format(level=obj.Label, type="PARTDESIGN", label=obj.Label, fullname=obj.FullName)
                    self.Verbose += "- adding object (1)\n"
                    self.PartsList[obj.Label] = dict()
                    for prop in self.infoKeysUser:
                        self.Verbose +=  "- " + prop + ': '
                        if prop == 'Document':
                            data = obj.Document.Label
                        elif prop == 'PartName':
                            data = obj.PartName
                        elif prop == 'PartLength':
                            data = obj.PartLength
                        elif prop == 'PartWidth':
                            data = obj.PartWidth
                        elif prop == 'PartHeight':
                            data = obj.PartHeight
                        else:
                            data = "-"

                        if data != "-":
                            self.Verbose += data + '\n'

                        self.PartsList[obj.Label][self.infoKeysUser.get(prop).get('userData')] = data

                    self.PartsList[obj.Label]['Qty.'] = 1
                    self.Verbose += '\n'


        #============================
        # FASTENERS AND ARRAYS
        #============================

        elif obj.TypeId == 'Part::FeaturePython' and (obj.Content.find("FastenersCmd") or (obj.Content.find("PCBStandoff")) > -1):
            if level > 0 and level <= max_level:
                doc_name = os.path.splitext(os.path.basename(obj.Document.FileName))[0]
                obj_label = re.sub(r'[0-9]+$', '', obj.Label)

                # if array
                if obj.Content.find("Orthogonal array")>-1:
                  # count up the objects in the array
                  x_count = obj.NumberX
                  y_count = obj.NumberY
                  z_count = obj.NumberZ
                  total = x_count * y_count * z_count
                  # identify the linked object
                  subobj = obj.Base.LinkedObject
                  # count each instance of linked object
                  for i in range(0, total):
                    print("    ", i, "...")
                    self.listParts(subobj, level, parent=obj)

                elif obj_label in self.PartsList:
                    if self.PartsList[obj_label]['Document'] == doc_name:
                        qtd = self.PartsList[obj_label]['Qty.'] + 1
                        print("ASM4> {level}| {qtd}x | {obj_typeid} | {obj_name} | {obj_label}".format(level=self.indent(level, tag=" "), obj_label=obj_label, obj_name=obj.FullName, obj_typeid=obj.TypeId, qtd=qtd))
                        self.Verbose += "> {level} | {type}: {label}, {fullname}\n".format(level=obj_label, type="FASTENER", label=obj_label, fullname=obj.FullName)
                        self.Verbose += "- object already added (" + str(qtd) + ")\n\n"
                        self.PartsList[obj_label]['Qty.'] = qtd

                else: # if the part is a was not added already
                    print("ASM4> {level}| 1x | {obj_typeid} | {obj_name} | {obj_label}".format(level=self.indent(level, tag=" "), obj_label=obj_label, obj_name=obj.FullName, obj_typeid=obj.TypeId))

                    self.Verbose += "> {level} | {type}: {label}, {fullname}\n".format(level=obj_label, type="FASTENER", label=obj_label, fullname=obj.FullName)
                    self.Verbose += "- adding object (1)\n"

                    self.PartsList[obj_label] = dict()
                    for prop in self.infoKeysUser:
                        if prop == 'Document':
                            data = doc_name
                        elif prop == 'Part_Label':
                            data = obj_label
                        elif prop == "Fastener_Diameter":
                            data = obj.diameter
                        elif prop == "Fastener_Type":
                            data = obj.type
                        elif prop == "Fastener_Length":
                            try:
                                data = str(obj.length).strip("mm")
                            except:
                                data = ""
                        else:
                            data = "-"

                        if data != "-":
                            self.Verbose += "- " + prop + ': ' + data + '\n'

                        self.PartsList[obj_label][self.infoKeysUser.get(prop).get('userData')] = data

                    self.PartsList[obj_label]['Qty.'] = 1
                    self.Verbose += '\n'


        # else:
            # print("@", obj.TypeId)

        #===================================
        # Continue walking inside the groups
        #===================================

        # Navigate on objects inide a folders
        if obj.TypeId == 'App::DocumentObjectGroup':
            for objname in obj.getSubObjects():
                subobj = obj.Document.getObject(objname[0:-1])
                self.listParts(subobj, level, parent=obj)

        # Navigate on objects inide a ASM4 Part (Links and Folders)
        if obj.TypeId == 'App::Part':
            for objname in obj.getSubObjects():
                subobj = obj.Document.getObject(objname[0:-1])
                # if subobj.TypeId == 'App::Link' or subobj.TypeId == 'App::DocumentObjectGroup':
                self.listParts(subobj, level+1, parent=obj)

        return

        self.Verbose += '\nBOM creation is done\n'

The issue is in this line.
self.Verbose += "- " + prop + ": " + data + " " + info + “\n”
The problem is that it was a document object and it was expecting text.
As I mentioned, easy enough get past the error, but it feels like there is more code handling that needs to take place and I’m just starting understand what’s going on here. If someone with more familiarity with whats going on here could chime it. It would be appreciated.

Edit.
I added this to get past the error. But I just feels it feels like there should be more code here:

                            if prop == "Part_Label":
                                data = obj_label

                            if data != "-":
                                try:
                                    self.Verbose += "- " + prop + ": " + data + " " + info + "\n"
                                e[b]xcept TypeError  as e:
                                     self.Verbose += "- Error: " + str(e) + "\n"
                                     #todo Not sure if this is the best solution.[/b]
                            self.PartsList[obj_label][self.infoKeysUser.get(prop).get('userData')] = data

                    self.PartsList[obj_label]['Qty.'] = 1
                    self.Verbose += '\n'

Edit #2 After fixing? this error, the next error seems to be recursion related.. Need to step away and walk the dog for a while.

I spent most of the day hacking away at the code. I managed to generate a parts list with a bunch of missing items.
It’s not really what I had in mind. The MakeBomCommand.py is a bit misleading IMHO.. The BOM spreadsheet is really a parts list…
Even so, it something I can use but not what I had in mind. I haven’t done, any commits on the code, because it’s really pretty dirty at the moment.

One thing that I’m unsure of is the part name, in Q_003-S I have 4 identical washers, but freecad wants different names for them. I’m not sure how that’s going to work populating the part name column.
PictureforForum2.png
Edit #1
Some minor progress
FixFastenerTypeSimpleSub.png
FixFastenerTypeFullAssy.png
Edit #2 fixed fastener length. Corrected type, you’ll need to delete the Json and go into edit part info to get it to regenerate.
FixFastenerLength.png
Edit #3 Fixed name and Diameter
FixFastenerNameAndLength.png

The “Parts list all” feature is pretty broken in the Assembly4 master branch.
I forked it and hacked it up to get it work for my use case and proof of concept. Looking at the code, appears to me that the MakeBomCmd was setup for a very specialized us case. I tried to fixed it to generate flat partlist showing usage for a general use case. I think there should also be an option for an indented bill of material as well.
I have worked extensively with bill of materials extracted from ERP systems. I think it would be really cool if Freecad could be setup to feed a erp system.
Thinking about it, in ERP system, you usually have a drawing number, drawing revision, partID, Part Description. I think the Drawing and Revision could be extracted easily from a freecad file. The revision of the drawing should be embedded in the filename. Currently in the assembly4 workbench you can name a part. It would be really cool if somehow we could get a PartID and description in there.

As far as raw material, I have some thoughts, In the project that I’m working on I have purchased components, 3d printed components and parts made with subtractive machining. Thinking about it, It would be cool if there was something for raw material, using fasteners as an example. If there was a special body class, say for bar stock or plate or tube, it could be added as a “raw material body” to an Assembly 4 part, and then the part cut way from that. In this way it would be possible to not only to extract raw material information but used for process planning as well.
As I mentioned I forked Zolko ASM4 workbench and pushed the changes to github. It’s really rough but I got it working(I think) for my use case. Looking at my output, I think I need to Add the spec number to the BomKey for the fastener. That needs to be a primary key.
As I mentioned, I need this for my business that I’m starting up. I plan to clean this up a bit more when I have some time

BoltSpacerNutSubAssembly (Assembly) @ /home/jonasthomas/Documents/FreecadProjects/QuestionsReFreecad/Q_003-S.FCStd
 ├─ Constraints 
 ├─ Configurations 
 ├─ M8-Nut (Nut)
 ├─ M8-Washer (Washer)
 ├─ M8-Washer051 (Washer001)
 ├─ M8-Washer052 (Washer002)
 ├─ M8x40-Screw (Screw)
 ├─ M8-Washer050 (Washer003)
 └─ PartSpacerTom => Spacer @ /home/jonasthomas/Documents/FreecadProjects/QuestionsReFreecad/Q_002-P.FCStd
     └─ BodySpacer

FlatBomSummarySimple.png

Assembly  @ /home/jonasthomas/Documents/FreecadProjects/QuestionsReFreecad/Q_004-A.FCStd
 ├─ Constraints 
 ├─ Configurations 
 ├─ PartWithCommonFeatures => PartWithCommonFeatures @ /home/jonasthomas/Documents/FreecadProjects/QuestionsReFreecad/Q_001-C.FCStd
 │   └─ BodyWithCommonFeatures (Body001)
 ├─ BoltSpacerNutSubAssembly => Assembly @ /home/jonasthomas/Documents/FreecadProjects/QuestionsReFreecad/Q_003-S.FCStd
 │   ├─ Constraints 
 │   ├─ Configurations 
 │   ├─ M8-Nut (Nut)
 │   ├─ M8-Washer (Washer)
 │   ├─ M8-Washer048 (Washer001)
 │   ├─ M8-Washer049 (Washer002)
 │   ├─ M8x40-Screw (Screw)
 │   ├─ M8-Washer047 (Washer003)
 │   └─ PartSpacerTom => Spacer @ /home/jonasthomas/Documents/FreecadProjects/QuestionsReFreecad/Q_002-P.FCStd
 │       └─ BodySpacer 
 ├─ BoltSpacerNutSubAssembly001 => Assembly @ /home/jonasthomas/Documents/FreecadProjects/QuestionsReFreecad/Q_003-S.FCStd
 │   ├─ Constraints 
 │   ├─ Configurations 
 │   ├─ M8-Nut (Nut)
 │   ├─ M8-Washer (Washer)
 │   ├─ M8-Washer048 (Washer001)
 │   ├─ M8-Washer049 (Washer002)
 │   ├─ M8x40-Screw (Screw)
 │   ├─ M8-Washer047 (Washer003)
 │   └─ PartSpacerTom => Spacer @ /home/jonasthomas/Documents/FreecadProjects/QuestionsReFreecad/Q_002-P.FCStd
 │       └─ BodySpacer 
 ├─ BoltSpacerNutSubAssembly002 => Assembly @ /home/jonasthomas/Documents/FreecadProjects/QuestionsReFreecad/Q_003-S.FCStd
 │   ├─ Constraints 
 │   ├─ Configurations 
 │   ├─ M8-Nut (Nut)
 │   ├─ M8-Washer (Washer)
 │   ├─ M8-Washer048 (Washer001)
 │   ├─ M8-Washer049 (Washer002)
 │   ├─ M8x40-Screw (Screw)
 │   ├─ M8-Washer047 (Washer003)
 │   └─ PartSpacerTom => Spacer @ /home/jonasthomas/Documents/FreecadProjects/QuestionsReFreecad/Q_002-P.FCStd
 │       └─ BodySpacer 
 ├─ BoltSpacerNutSubAssembly003 => Assembly @ /home/jonasthomas/Documents/FreecadProjects/QuestionsReFreecad/Q_003-S.FCStd
 │   ├─ Constraints 
 │   ├─ Configurations 
 │   ├─ M8-Nut (Nut)
 │   ├─ M8-Washer (Washer)
 │   ├─ M8-Washer048 (Washer001)
 │   ├─ M8-Washer049 (Washer002)
 │   ├─ M8x40-Screw (Screw)
 │   ├─ M8-Washer047 (Washer003)
 │   └─ PartSpacerTom => Spacer @ /home/jonasthomas/Documents/FreecadProjects/QuestionsReFreecad/Q_002-P.FCStd
 │       └─ BodySpacer 
 └─ PartClonedWithAddedFeature => PartClonedWithAddedFeature @ /home/jonasthomas/Documents/FreecadProjects/QuestionsReFreecad/Q_001-C.FCStd
     └─ ClonedBodyWithAddedFeature (Body)

FlatBomSummaryComplex.png

it is broken because everybody does that: “fix” it for his own use-case. Therefor sorry but I won’t take your PR since it doesn’t solve anything generally.

Geeeeeeeeeeeze, isn’t there anyone able to code this properly ?

it is broken because everybody does that: “fix” it for his own use-case. Therefor sorry but I won’t take your PR since it doesn’t solve anything generally.

Geeeeeeeeeeeze, isn’t there anyone able to code this properly ?

@zolko
I needed a solution so I hacked one up. That, being said, it was my intent to make it as generic as possible to make a solution that could be built on..

Good engineering versus hacking, is to defining a problem before trying to solve it. (I really didn’t do that in this case, because I was in a hurry)
Sooooo.
Defining the problem
Customization will be inevitable. The workflow for someone making custom gears for custom gearboxes , is significantly different that someone in need of a parametric solution for designing a standardized product.

There is currently no generic base solution that works that can be built upon.

Customization is currently intertwined with core functionality in the code, which will make your maintenance really difficult.

There are basic elements that are basically universal to all ERP systems:
PartId, PartIdDescription Drawing number, Drawing Revision. It seems that ASM4 design, The ASM4 workbench Part definition, appears to be more of a Description than a PartID. The PartId should be unique and occur only once in a drawing in an assembly. (There can be more than one part as in my use case to my drawing) Because there is not PartID in a ASM4 drawing, there is no way to check for uniqueness.

For a bill of material, there can be either a flat solution, (Like the one I did) or an indented, which shows the parent child relationships between parts.
Flat files, are used for manual use, you would need an indented, which could be used for importation into an ERP/MES system. A base system should offer both options.

The current solution in Assembly4 master is to use specific (IMHO), It assumes that raw material length can be extracted from a body. This makes it complex to provide a general solution.

For parts made with subtractive manufacturing, FreeCAD lacks a standardized way to define what the raw material. This make it difficult to provide a generic Bill of material.

Parts cut from sheet wood, or plastic and steel plate laser cut metal and plastic, need some type of standardized definition.

Parts made from bar, versus sawed slugs or forgings, would need some type of definition.


Questions/Comments/Proposed solutions.

PartID
I would suggest that PartID and Description needs to be integral to the “Part” Definition in the ASM4 workbench IMHO.
(It took me, a while, to understand the ASM4 structure of Parts and Assemblies. Once I understood the workflow, it really is a nice solution.)
Thinking about this, there should be some toggles on the partID. For people who don’t care about a Part_id, perhaps it an be an auto-generated GUID, hidden in the background. Checking for Part_ID uniqueness withing a fcStd->Asm4 part in an Assembly should be fairly simple.
@zolko What are your thoughts on this?

Raw Material
It seems like there should be a standardized raw material body, similar to what was done, with Fastener, I think could be very useful parts manufactured with subtractive manufacturing.
I know there is talk about adding material into FreeCAD. Perhaps this outside the scope of ASM4.. What are your thoughts?

Proposed solution
If you want, I can clean up the code a bit.
There are fields that (IMHO) should be hard-coded into a solution:
For flat Parts list.
BomKey (which should be unique)
Document
PartID
PartIDDescription
Qty

Additional Hardcoded field for a indented BOM
ParentPartID
Bom Level

The flat option, I think is what people will want to customize to suit there needs.
If this can be done in such a way that can make code maintenance easier? Any thoughts?

I chose your Assembly workbench because, I thought it was an elegant simple solution. I didn’t realize that the Bom feature(which I really need) was so badly broken.

I would like to separate PartInfo and BoM. First make a PartInfo that is really usable, and only then try to make a generic BoM function.

Your PartId idea is a good one, but nothing will insure that it’s unique. It should be possible to test for uniqueness using Document Path/Name and inside there the PartName.

So as a beginning it could be mandatory that PartId (I have called that PartRef) is filled-in by the user, else it raises a warning.


For a bill of material, there can be either a flat solution, (Like the one I did) or an indented, which shows the parent child relationships between parts. Flat files, are used for manual use, you would need an indented, which could be used for importation into an ERP/MES system. A base system should offer both options.

a BoM is always a flat list/table. We’re not aiming (yet) for a full ERP system. Also, if you want a hierarchical tree view of the model you can have it today.


The current solution in Assembly4 master is to use specific (IMHO), It assumes that raw material length can be extracted from a body. This makes it complex to provide a general solution.

yes, that’s wrong. What I had in mind for cut-length is to take the maximum of BoundingBox.X/Y/Z of the part’s shape, which would work in 99.99% of the cases (but not for diagonal beams, tough !)


Questions/Comments/Proposed solutions.

PartID
I would suggest that PartID and Description needs to be integral to the “Part” Definition in the ASM4 workbench IMHO.
(It took me, a while, to understand the ASM4 structure of Parts and Assemblies. Once I understood the workflow, it really is a nice solution.)
Thinking about this, there should be some toggles on the partID. For people who don’t care about a Part_id, perhaps it an be an auto-generated GUID, hidden in the background. Checking for Part_ID uniqueness withing a fcStd->Asm4 part in an Assembly should be fairly simple.
@zolko What are your thoughts on this?

people who don’t need PartInfo and BoM should simply leave all this empty. No automagic solution.

What I had imagined is that once a part is created, it is possible to call the PartInfo function, and there be presented with default fields. If necessary, the user can add/remove fields, but also load the fields from a template so a project/company can create custom templates for its parts. Of course, it should be so that it doesn’t make an error if no template is present !


Proposed solution
If you want, I can clean up the code a bit.
There are fields that (IMHO) should be hard-coded into a solution:
For flat Parts list.
BomKey (which should be unique)
Document
PartID
PartIDDescription
Qty

Additional Hardcoded field for a indented BOM
ParentPartID
Bom Level

for PartInfo : PartRef (or PartId), PartName, PartDescription, Revision, ProjectName, ProjectRef (or ProjectId), Author, Material,… all user-input strings. Can be hard-coded into PartInfo.py to begin-with, and use templates if present. See screenshot from SolidWorks part properties:

for BoM: add Quantity, cut-length (for each). Allow to specify which assembly depth to parse.


PartInfo_SW.jpg

IMHO calling something reference means that it is not required. I reference dimension on a sketch is not required.
I would suggest a different name. For many people, a part number is for a computer system, and description if for the humans. It would be nice to be able nice to see either or/both displayed in the Assembly tree.

Oth..I’m not sure what the proper term is, but I’ve seen “Bubble numbers” on assembly drawing that refer to a specfic part in a parts list.
Do you have built into ASM4? for that?

a BoM is always a flat list/table. We’re not aiming (yet) for a full ERP system. Also, if you want a hierarchical tree view of the model you can have it today.

I probably should have been more precise in my description. What I described as a flat list, I meant as a non-indented Bill of Material. An indented bill of material represents a the hierarchical nature of an assembly on a flat list. Both have there uses.

I’m not suggesting that we turn FreeCAD into an ERP system, but I would like to have FreeCAD be able to communicate with an ERP system.
To that extent, with in a full assembly, there should be a utility to export a Part list as well as the Part/Child relations that are contained in a BOM

I really like the graphical representation of the hierarchical tree view that was done. It is very understandable. The downside is it’s data is pretty much single use. (I have found a indented bom in a spreadsheet to be very useful) (This is on my wish list for the future)

The current solution in Assembly4 master is to use specific (IMHO), It assumes that raw material length can be extracted from a body. This makes it complex to provide a general solution.

yes, that’s wrong. What I had in mind for cut-length is to take the maximum of BoundingBox.X/Y/Z of the part’s shape, which would work in 99.99% of the cases (but not for diagonal beams, tough !)

Words like stones once cast can’t be recalled. I want to walk that one back a bit. ASM4 is a design tool. Lets just say I was looking at this from the manufacturing engineering perspective making a process plan. That code was misbehaving for me and I didn’t understand it so I blew it away in my fork(In hindsite, I sort of regret doing that)

Questions/Comments/Proposed solutions.

PartID
I would suggest that PartID and Description needs to be integral to the “Part” Definition in the ASM4 workbench IMHO.
(It took me, a while, to understand the ASM4 structure of Parts and Assemblies. Once I understood the workflow, it really is a nice solution.)
Thinking about this, there should be some toggles on the partID. For people who don’t care about a Part_id, perhaps it an be an auto-generated GUID, hidden in the background. Checking for Part_ID uniqueness withing a fcStd->Asm4 part in an Assembly should be fairly simple.
@zolko What are your thoughts on this?

people who don’t need PartInfo and BoM should simply leave all this empty. No automagic solution.

What I had imagined is that once a part is created, it is possible to call the PartInfo function, and there be presented with default fields. If necessary, the user can add/remove fields, but also load the fields from a template so a project/company can create custom templates for its parts. Of course, it should be so that it doesn’t make an error if no template is present !

It sounds like we are on the same page in requiring a distinct part number. I may be we might be thinking differently on the Part Description.
I’m trying to develop a drawing numbering system that works for me. (I’m not trying to force this on anyone. It’s my convention that’s working for me)
Using

Q_001_C.fcStd  as an example
Q position represents the project. It will vary from project to project
    001 is an incremental counter for a project.
             C is a designation for what kind of drawing it is. (I'm still working on this)
                 At the moment I have the following meanings.
                    A) Full Assembly
                    S) Sub Assembly
                    C) Drawing with multiple parts
                    P) Purchased custom component
Eventually I want to had revision which will look like this
Q_001_C_0.00.Fstd
In this use case for this drawing I have 2 parts in the drawing.
I plan to designate them as 
Q_001_C-1   Plate
Q_001_C-2   Plate with Hole
If I name that part Q_001_C-1 as the part, It will be very hard to understand whats going on.
That's why I was hoping that we could have PartNumber and Part Number description be viewable in the combo view display.

ScreenShotForZolko.png
I just realized that I could add my Partnumber convention manually to the current part ID and it could work for now.
I do think that in the long term there should be a separate partID and description in a ASM4 part definition.

Zolko
I was looking at the structure you suggested and refined it a bit.

ProjectId
ProjectName,

PartId
PartName
PartDescription,
Material
DrawnBy Author,
DrawnDate
CheckedBy
CheckDate:
Revision

A couple of thoughts/questions
One FcStd can contain multiple parts(Non-linked), and one assembly(with Many linked parts.) Do you agree?

The ProjectID/ProjectName should be defined once on the FcStd level(outside of PartInfo.Py, but referenced by it (At the moment I don’t know how to do that. Do you agree with that I’m saying?

You had mentioned wanted PartName and Part Description. Shouldn’t this just be one? What would the use case be for 2?
I noticed when you create a part there is something called BASE?
ScreenShotForZolko3.png
Is ID= PartID and is this where Material info should go?

Finally, I got it working the way that I want.
I refactored the code a bit and pushed it to my fork.
Basically, if there are no customizations done in infoKeys.py clicking on part lists all will work.
I have some heavy personal customization that’s not for everyone but it should be easily not to use them if you don’t want it
Json will need to delete and reset fields and Configure fields in Edit Part information need to be used on individual parts /assemblies to get this to populate correct. (I changed some names around so this will need to be done)
Lots of debugging statements need to be removed to clean up before merging.

I have some customization,that i did that I think are sort of cool..
the FCsctd, Contains a Drawing + Drawing Revision.
The Drawing # is the basis of the Part_id
The part description is basically the name of the body within the part (Assuming a part with a single body)
When the Bom if being build it auto populates the fields and checks to see if File names and part names conform to the naming conventions.

I think everyone is going to want there own customization. I refactored so that if there is no customization, there is a hard coded solution that should work.
(Which is not the case on the current master)
ScreenShotForZolko4.png
[Edit.. Code fails on groups at the moment. :frowning: ]
Edit 2. Groups appears to be not an issue. I need a warning message the user fields where not yet created.