Implemented in OpenPLM 1.2

Alternate parts links

This page describes how alternate parts links will be implemented in OpenPLM. Any ideas are welcomed!

APL = Alternates parts links

"A <-> B" means "Part A is an alternative part of Part B"

Milestone 1.2

Definition

From  http://agkulkarni.wordpress.com/2011/08/06/alternate-and-substitute-parts/ :

An Alternate Part is a component that is a suitable replacement for another part in every assembly in which the original part occurs. Companies use alternate parts when multiple vendors can supply parts that serve the identical function and fit.

Properties

Symmetric relation
A <-> B => B <-> A
Transitive relation
A <-> B and B <-> C => A <-> C

Restrictions on APL creation

Is it forbidden to create an alternate parts relation between two revisions of the same part ? Yes

Creating a reflexive APL "A <-> A" is forbidden.

Permission

  • Owner of, at least, one part
  • At least, one part should be at draft status
  • In group ??
  • ...

Parents / Children

It is not possible to create an APL "A <-> B" if:

  • A is an ancestor of B
  • B is an ancestor of A
  • A is an ancestor of alternate parts of B's ancestors
  • B is an ancestor of alternate parts of A's ancestors
  • A is a sibling of B

Examples:

1) parents, ancestors (tested)

GraphViz image

Invalid APL:

  • Part1 <-> Part3
  • Part1 <-> Part4
  • Part1 <-> Part5
  • Part2 <-> Part3
  • Part2 <-> Part4
  • Part2 <-> Part5

Valid APL:

  • Part3 <-> Part5
  • Part4 <-> Part5

2) parents, ancestors (tested)

GraphViz image

Invalid APL:

  • Part1 <-> Part3
  • Part1 <-> Part4
  • Part1 <-> Part5
  • Part1 <-> Part6
  • Part2 <-> Part3
  • Part2 <-> Part4
  • Part2 <-> Part5
  • Part2 <-> Part6
  • Part3 <-> Part6
  • Part4 <-> Part6
  • Part5 <-> Part6

Valid APL:

  • Part3 <-> Part5
  • Part4 <-> Part5

3) siblings (tested)

GraphViz image

Invalid APL:

  • Part1 <-> Part2
  • Part1 <-> Part3
  • Part2 <-> Part3

Valid APL:

  • None

4) (tested)

GraphViz image

Invalid APL:

  • All

Valid APL:

  • None

5) (not yet tested)

GraphViz image

Invalid APL:

???

Valid APL:

???

Algorithm (done):

Variation of PartController.is_ancestor that also fetches alternates at each step of the loop.

Revisions

Is it possible to create an APL between two revisions ("PART_001/a" <-> PART_001/b") ??? No, invalid

(done)

States / lifecycle

  • A or B is cancelled -> invalid (done)
  • A or B is deprecated -> invalid (done)
  • A or B is draft -> valid
  • A or B go to deprecated -> Break alternate links (todo)

Groups

Is it possible to create an APL between two parts of two different groups?

  • yes, if readable

Merge of two alternate partsets

Is it forbidden ??? Yes (may be evolve once all is tested/approved)

Warnings

Alternate parts greatly increase the risk of obtaining circular BOMs.

Promotion

not yet implemented

Rules

  • When Part_001/a/official is replace by Part_001/a : Inform user that alternate links will switch from a to b
  • When Part_001/a official -> obsolete : Inform user that alternate links will be dropped

Case 1

Level part state
0 PART_1 draft
1 PART_2 draft
---- alternate PART_3 official

PART_2 <-> PART_3, is PART_1 (the parent) promotable ?

  • Yes

Case 2

Level part state
0 PART_1 draft
1 PART_2 official
---- alternate PART_3 official

PART_2 <-> PART_3, is PART_1 (the parent) promotable ?

  • Yes

Case 3

Level part state
0 PART_1 draft
1 PART_2 official
---- alternate PART_3 draft

PART_2 <-> PART_3, is PART_1 (the parent) promotable ?

  • Yes

Case 4

Level part state
0 PART_1 draft
1 PART_2 draft
---- alternate PART_3 draft

PART_2 <-> PART_3, is PART_1 (the parent) promotable ?

  • No

Case 5

Level part state
0 PART_1 draft
---- alternate PART_2 draft

PART_1<-> PART_2, is PART_1promotable ?

  • Yes if an official document is attached to PART_1

Case 6

Level part state
0 PART_1 draft
---- alternate PART_2 draft
1 PART_3 official

PART_1<-> PART_2, is PART_1 (the parent) promotable ?

  • Yes

Revisions

case 1

PART_001/a <-> PART_002/a

When PART_001/a is revised (to PART_001/b/draft) should we get:

  • PART_001/a <-> PART_002/a : Yes (done, no required code)
  • or PART_001/b <-> PART_002/a : No
  • or PART_001/a <-> PART_002/a <-> PART_001/b : No

When PART_001/a/obsolete is revised (to PART_001/b/official) should we get:

  • PART_001/a <-> PART_002/a : No
  • or PART_001/b <-> PART_002/a : Yes (todo)
  • or PART_001/a <-> PART_002/a <-> PART_001/b : No

User interface

New pages

Parts: new Alternate tab

  • lists alternate parts -> a table or a grid of identity cards (like the browse feature) ???
  • a toolbar: 3 buttons : add / create and add / edit (table) or 2 buttons : add / create and add + a button on each cards to remove it (-> popup to confirm) (grid)
-------------------------------------------------------
[Add an alternate part] [Create and add an alternate part] [Edit]

|Type | Reference | Revision | name|
|Part | PART_001  | a        | p1  |
|Part | PART_002  | d        | x2  |
-------------------------------------------------------

or

-------------------------------------------------------
[Add an alternate part] [Create and add an alternate part]

/----------------------\  /---------------------\
| PART_00 1         [X]|  | PART_002         [X]| <-- a button to remove the part
| p1                   |  | x2                  |
\----------------------/  \---------------------/
-------------------------------------------------------

Add alternate page

Displays a form to select a part.

Search panel: add an "Add" button on each valid candidate

Page changes

BOM view

A new filter is available to display alternate parts of the children.

By default, this filter is disabled (alternate parts not visible).

Children of alternate parts are not displayed.

A new filter is available to display alternate parts of the current part.

Should alternate parts of children be displayed ???

GraphViz image

or

GraphViz image

Implementation details

Database schema

Solution 5 is implemented.

Solution 1

A new model AlternatePartLink?:

Attributes:

part1
a foreign key
part2
a foreign key
ctime
date of creation of the link
end_time
date of deletion of the link (default: None)

To create A <-> B:

Creation of

Then to create A <-> C

Creation of

Number of rows if there are N parts in the same 'alternate' group: N*(N-1)

Request to get alternate parts of A: AlternatePartLink.objects.filter(part1=A) (and time stuff)

Request to remove A: AlternatePartLink.objects.filter(part1=A, end_time__isnull=True).update(end_time=now())

Number of request to add a part if there are N exiting parts: 2*N + one request to get existing alternate parts

Solution 2

Variation of solution 1:

Only create one of APL(part1=A, part2=B) / APL(part1=A, part2=B)

Number of rows if there are N parts in the same 'alternate' group: N*(N-1)/2

Request to get alternate parts of A: AlternatePartLink.objects.filter(Q(part1=A )|Q(part2=A)) (and time stuff)

Request to remove A: AlternatePartLink.objects.filter(Q(part1=A )|Q(part2=A)).filter(end_time__isnull=True).update(end_time=now())

Number of request to add a part if there are N exiting parts: N + one request to get existing alternate parts

Solution 3

A new model AlternateGroup?:

Attributes:

parts
a many to many field
ctime
same as solution 1
end_time
same as solution 1

and a new Part's attribute: alternate_group, a foreign key, None by default

To create A <-> B:

Creation of

  • a new g = AlternateGroup?(ctime=t1, parts=[A,B])
  • A.alternate_group = B.alternate_group = g

Then to create A <-> C

Creation of

  • a new g2 = AlternateGroup?(ctime=t2, parts=[A,B, C])
  • A.alternate_group.end_time = t2
  • A.alternate_group = B.alternate_group = C.alternate_group = g2 ( g2.parts.update(alternate_group=g2 )

Request to get alternate parts of A:

g = A.alternate_group
if g: g.parts.exclude(id=A)
else: []

at date t: A.alternategroup_set.filter(time stuff)[0].parts

Request to remove A:

g = A.alternate_group
A.alternate_group = None
g.end_time = t
g2 = AlternateGroup(ctime=t2, parts=g.parts.exclude(id=A))
g2.parts.update(alternate_group=A)

Suggestion: use the word "set" instead of "group"

Solution 4

Variation of solution 3

A new model: PartSet?(Link)

  • required attribute: type a positive smal integer field
  • many to many fields : parts (related name: partsets)
  • time stuff (from Link)

type : constants: 1 -> alternate, 2 -> synchronized, 3 -> ....

To create A <-> B:

Creation of

  • a new g = PartSet?(ctime=t1, type=1)
  • g.parts.add(A,B)

Then to create A <-> C

Creation of

  • a new g2 = PartSet?(ctime=t2, parts=[A,B, C]) N +1 requests, possibility to write a raw sql request
  • A.partsets.filter(id__ne=g2.id, type=1).end()

Request to get alternate parts of A:

try:
   g = A.partsets.now().get(type=1)
   parts = g.parts
except DoesNotExists:
   parts = []

at date t: A.partsets.at(t).get(type=1)

Number of rows if there are N parts in the same 'alternate' group: N*(N-1)/2 rows for the m2m table + N rows for the partsets table

Request to remove A:

 * a new g2 = PartSet(ctime=t2, parts=[B, C])
 * A.partsets.filter(id__ne=g2.id, type=1).end()

This solution is reusable: LifecyclesSynchronization may use it.

It may also be extending to a set of plmobjects.

Note: django 1.4 calls bulk_create to add items to a m2m field (one request)

Solution 5

Variation of solution 4:

PartSet is abstract.

type field is deleted.

Creation of a subclass per type (AlternatePartSet, SynchronizePartSet)

PartSet? related to a list of parts : ps = XXXPartSet.objects.now().filter(parts__in=list_of_parts).distinct()

Part ids and PartSet? ids: Part.objects.filter(xxxpartsets__in=ps).extra(select={"psid":"synchronizedpartset_id"}).values("id", "psid")

PartController?

New methods:

add_alternate(self, alternate_part)
add an alternate part
returns created APL(s?)
get_alternates(self, date=None)
Returns the list of alternate parts at date time
is_alternate(self, other_part)
Return True if other_part is an alternate part of self
check_add_alternate(self, other_part)
Raises a ValueError? exception if it is not possible to call self.add_alternate(other_part)
can_add_alternate(self, other_part)
Returns False if it is not possible to call self.add_alternate(other_part))
delete_alternate(self, other_part)
Deletes the APL between self and other_part
What to do if there are other alternate parts

Modified methods:

check_add_child
  • raises a ValueError? if the child part is an alternate part of self
  • raises a ValueError? if the child part is an ancestor of one of the alternate parts of self
  • raises a ValueError? if an alternate part of the child part is an alternate part of self
  • raises a ValueError? if an alternate part the child part is an ancestor of one of the alternate parts of self

TODO: test, graph, examples

Promotion

If

  • PART_001 contains PART_002,
  • PART_001 is a draft
  • PART_002 is official
  • PART_003 is an alternate part of PART_002
  • PART_0003 is a draft

is PART_001 promotable to an official state ?

Cancellation

Is is possible to cancel a part if it has one or more alternate part ?

Parent Child Link Extension (PCLE)

?

Test cases

This page contains a lot of examples ;-)