============================ Bill of materials -- BOM ============================ Definition ========== In openPLM, all parts have a bill of materials (BOM) which lists the children parts that compose a part. A BOM lists for each child part how much is needed to build or assembly the parent part. openPLM manages single-line BOM (first level of children) and indented BOM (all levels of children). How the BOM is stored ====================== openPLM stores each row of a BOM but it only stores the first level. Indented BOM are dynamically built. For example, the following (simplified) BOM is stored with 3 rows. ===== ======= ======== Level Element Quantity ===== ======= ======== --- P0 --- 1 P1 2 2 P2 4 1 P2 5 ===== ======= ======== The rows are (notation: parent, child, quantity): * P0, P1, 2 * P1, P2, 4 * P0, P2, 5 The model behind these rows is :class:`.ParentChildLink`. It has the following fields: parent id of a part child id of a part quantity amount of child unit unit of the quantity (like kg, m) order field to order (sort) the BOM ctime date of creation of the row end_time date of deletion of the row (null value means no deletion) The *ctime* and *end_time* fields allow openPLM to track changes of a BOM. For example, if the quantity changes, the row is not deleted but, its *end_time* field is set and a new row is created. For example, the following BOM (date of creation: 2012/01/01) is ===== ======= ======== ==== ===== Level Element Quantity Unit Order ===== ======= ======== ==== ===== --- P0 --- -- --- 1 P1 2 m 100 ===== ======= ======== ==== ===== is stored as one row: parent -> P0, child -> P1, quantity -> 2, unit -> m, order -> 100, ctime -> 2012/01/01, end_time -> null. If an user changes the unit to km and the quantity to 3, the BOM will be ===== ======= ======== ==== ===== Level Element Quantity Unit Order ===== ======= ======== ==== ===== --- P0 --- -- --- 1 P1 3 km 100 ===== ======= ======== ==== ===== and the database will contains two rows (date of modification: 2012/01/02): * parent -> P0, child -> P1, quantity -> 2, unit -> m, order -> 100, ctime -> 2012/01/01, end_time -> 2012/01/02 * parent -> P0, child -> P1, quantity -> 3, unit -> km, order -> 100, ctime -> 2012/01/02, end_time -> null If he adds a new child (P2, date of addition: 2012/01/03): ===== ======= ======== ==== ===== Level Element Quantity Unit Order ===== ======= ======== ==== ===== --- P0 --- -- --- 1 P1 2 km 100 1 P2 120 m 200 ===== ======= ======== ==== ===== The database will contains 3 rows: * parent -> P0, child -> P1, quantity -> 2, unit -> m, order -> 100, ctime -> 2012/01/01, end_time -> 2012/01/02 * parent -> P0, child -> P1, quantity -> 3, unit -> km, order -> 100, ctime -> 2012/01/02, end_time -> null * parent -> P0, child -> P3, quantity -> 120, unit -> m, order -> 200, ctime -> 2012/01/03, end_time -> null So previous rows are not altered since they have not been modified. .. _bom_extensions: Extensions ========== Purpose -------- Some applications need to store additional data on each row. These data should or should not be displayed or editable. For example, an electronic CAD application adds a *referenced designator* field which tells where a component is put on a board. This field must be displayed and is editable. Another application can add several location fields per row which tell where each child element is located. These location field would be invisible but are useful to generate a STEP document from the BOM. Implementation -------------- openPLM has a model named :class:`.ParentChildLinkExtension` to store these extensions. An application can add a model that extends :class:`.ParentChildLinkExtension` and register it. The application only has to override some methods and openPLM will handle the display and edition of the BOM. .. admonition:: PCLE and pcle Starting from here, PCLE means a model that extends a ParentChildLinkExtension and pcle means an instance of a ParentChildLinkExtension A PCLE has one mandatory field, *link* which is a foreign key to a :class:`.ParentChildLink`. When a :class:`.ParentChildLink` is duplicated (after a modification from an user), its bound extensions are duplicated by calling their :meth:`.ParentChildLinkExtension.clone` method. Registration ------------ By default, a PCLE will not be used by openPLM and must be registered. A PCLE can be registered by calling the :func:`.registered_pcle` function. By default, a registered PCLE applies to all BOM. It is possible to change this behaviour by overriding the method :meth:`.ParentChildLinkExtension.apply_to`. This method takes one argument, the parent which will have a child, so it is possible to test its type or some of its attributes. Visible and invisible fields ---------------------------- A PCLE can have as many fields as needed. Added fields are hidden by default. The classmethod :meth:`.ParentChildLinkExtension.get_visible_fields` can be overridden to return a list of visible fields. The classmethod :meth:`.ParentChildLinkExtension.get_editable_fields` is called to get the list of editable fields that are added to the "add child" form and to the "edit bom" form. By default, it returns the list of visible fields. .. warning:: If a PCLE has at least one visible field, openPLM can create at most one pcle per link and it will not be able to display several columns for the same field. This behaviour is intended to keep the BOM readable and easily editable. If you really need several visible pcles per link, you can create your own models.Field and play with its :meth:`formfield` method. Cloning -------- :class:`.ParentChildLink` are never deleted but they are cloned. And since a pcle is bound to a link, it must be clonable. A PCLE must define the :meth:`.ParentChildLinkExtension.clone` method. By default, it raises a :exc:`NotImplementedError` exception. Examples -------- Reference designator field ************************** .. code-block:: python class ReferenceDesignator(ParentChildLinkExtension): reference_designator = models.CharField(max_length=200, blank=True) def __unicode__(self): return u"ReferenceDesignator<%s>" % self.reference_designator @classmethod def get_visible_fields(cls): return ("reference_designator", ) @classmethod def apply_to(cls, parent): # only apply to mother boards return isinstance(parent, MotherBoard) def clone(self, link, save, **data): ref = data.get("reference_designator", self.reference_designator) clone = ReferenceDesignator(link=link, reference_designator=ref) if save: clone.save() return clone register(ReferenceDesignator) register_PCLE(ReferenceDesignator) Hidden location fields ********************** Since all fields are hidden, no Location objects will be by openPLM created. A custom controller can create them but it would not have to handle their cloning. .. code-block:: python class Location(ParentChildLinkExtension): x = models.FloatField(default=lambda: 1) y = models.FloatField(default=lambda: 1) z = models.FloatField(default=lambda: 1) def __unicode__(self): return u"" % (self.x, self.y, self.z) @classmethod def apply_to(cls, parent): # only apply to all parts return True def clone(self, link, save, **data): x = data.get("x", self.x) y = data.get("y", self.y) z = data.get("z", self.z) clone = Location(link=link, x=x, y=y, z=z) if save: clone.save() return clone register(Location) register_PCLE(Location) .. _bom_comparison: .. versionadded:: 1.2 BOMs comparison ================ Since the version 1.2, it's possible to compare two BOMs at two different dates and renders a diff view showing differences side by side (like in trac). The method :meth:`.PartController.cmp_bom` performs the comparison and the view :func:`.compare_bom` renders the result. This comparison relies :meth:`.difflib.SequenceMatcher.get_opcodes`. The standard :mod:`.difflib` module can compare strings and any types of finite sequences. Using this module is only a matter of input formats. In OpenPLM, BOMs are flatten (see :func:`.flatten_bom`) and three lines of code do the comparison.