Table Of Contents

Previous topic

7. Documenting OpenPLM

Next topic

9. Publication

This Page


Previous versions


8. Bill of materials – BOM

8.1. 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).

8.2. 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
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 ParentChildLink. It has the following fields:

id of a part
id of a part
amount of child
unit of the quantity (like kg, m)
field to order (sort) the BOM
date of creation of the row
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
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
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
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.

8.3. Extensions

8.3.1. 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.

8.3.2. Implementation

openPLM has a model named ParentChildLinkExtension to store these extensions. An application can add a model that extends ParentChildLinkExtension and register it. The application only has to override some methods and openPLM will handle the display and edition of the BOM.

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 ParentChildLink.

When a ParentChildLink is duplicated (after a modification from an user), its bound extensions are duplicated by calling their ParentChildLinkExtension.clone() method.

8.3.3. Registration

By default, a PCLE will not be used by openPLM and must be registered. A PCLE can be registered by calling the registered_pcle() function.

By default, a registered PCLE applies to all BOM. It is possible to change this behaviour by overriding the method 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.

8.3.4. Visible and invisible fields

A PCLE can have as many fields as needed. Added fields are hidden by default. The classmethod ParentChildLinkExtension.get_visible_fields() can be overridden to return a list of visible fields.

The classmethod 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.


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 formfield() method.

8.3.5. Cloning

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 ParentChildLinkExtension.clone() method. By default, it raises a NotImplementedError exception.

8.3.6. Examples Reference designator field

class ReferenceDesignator(ParentChildLinkExtension):

    reference_designator = models.CharField(max_length=200, blank=True)

    def __unicode__(self):
        return u"ReferenceDesignator<%s>" % self.reference_designator

    def get_visible_fields(cls):
        return ("reference_designator", )

    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:
        return clone

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.

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"<Location<%f, %f, %f>" % (self.x, self.y, self.z)

    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:
        return clone


New in version 1.2.

8.4. 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 PartController.cmp_bom() performs the comparison and the view compare_bom() renders the result.

This comparison relies difflib.SequenceMatcher.get_opcodes(). The standard 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 flatten_bom()) and three lines of code do the comparison.