inventory: the wms-inventory Blok

This package provides the wms-inventory Blok.

Model.Wms.Inventory

class anyblok_wms_base.inventory.order.Inventory[source]

This model represents the decision of making an Inventory.

It expresses a global specification for the inventory process to be made as well as human level additional information.

Applicative code is welcomed and actually supposed to override this to add more columns as needed (dates, creator, reason, comments…)

Instances of Wms.Inventory are linked to a tree of processing Nodes, which is reachable with the convenience root attribute.

TODO structural Properties to use throughout the whole hierarchy for Physical Object identification

This tree is designed for distribution of the assessment and reconciliation work, but it’s possible to compute all reconciliations and apply them on an Inventory for testing purposes as follows (assuming that all related Nodes are in the full state):

inventory.root.recurse_compute_push_actions()
inventory.reconcile_all()

Fields and their semantics

id = <anyblok.column.Integer object>

Primary key.

root

Root Node of the Inventory.

Methods

classmethod create(location, **fields)[source]

Insert a new Inventory together with its root Node.

Returns:the new Inventory
reconcile_all()[source]

Convenience method to apply all Actions linked to this Inventory.

This is a straightforward yet non scalable implementation of the final reconciliation (see below). Don’t use it on large installations.

To run it, it is required that the root Node has reached the pushed state.

Raises:NodeStateError if root Node is not ready.

This method does everything in one shot, therefore leading to huge database transactions on full inventories of large installations.

For large inventories, a more progressive way of doing is required, perhaps Node per Node plus batching for each Node. Nodes wouldn’t have to be taken in order, but care must be taken while updating their state to ‘reconciled’ in out of order executions with several batches per Node.

Model.Wms.Inventory.Node

class anyblok_wms_base.inventory.node.Node(parent=None, from_split=False, **fields)[source]

Representation of the inventory of a subtree of containment hierarchy.

For each Inventory, there’s a tree of Inventory Nodes, each Node having one-to-many relationships to:

  • Inventory Lines that together with its descendants’, form the whole assessment of the contents of the Node’s location
  • Inventory Actions that encode the primary Operations that have to be executed to reconcile the database with the assessment.

Each Node has a location under which the locations of its children should be directly placed, but that doesn’t mean each container visited by the inventory process has to be represented by a Node: instead, for each Inventory, the locations of its leaf Nodes would ideally balance the amount of assessment work that can be done by one person in a continuous manner while keeping the size of the tree reasonible.

Applications may want to override this Model to add user fields, representing who’s in charge of a given node. The user would then either optionally take care of splitting (issuing children) the Node and perform assesments that are not covered by children Nodes.

This whole structure is designed so that assessment work can be distributed and reconciliation can be performed in parallel.

Fields and their semantics

state = <anyblok.column.Selection object>

Node lifecycle

  • draft:
    the Node has been created, could still be split, but its lines don’t represent the full contents yet.
  • assessment:
    (TODO not there yet, do we need it?) assessment work has started.
  • full:
    all Physical Objects relevant to the Inventory that are below location are accounted for in the lines of its Nodes or of its descendants. This implies in particular that none of the children Nodes is in prior states.
  • computed:
    all Actions to reconcile the database with the assessment have been issued. It is still possible to simplify them
  • pushed:
    attached Actions have been simplified, and the remaining ones have been pushed to the parent for further simplification
  • reconciled:
    all relevant Operations have been issued.
inventory = <anyblok.relationship.Many2One object>

The Inventory for which this Node has been created

parent = <anyblok.relationship.Many2One object>
location = <anyblok.relationship.Many2One object>
is_leaf

(bool): True if and only if the Node has no children.

Methods

split()[source]

Create a child Node for each container in location.

compute_actions(recompute=False)[source]

Create actions to reconcile database with assessment.

Parameters:recompute (bool) – if True, can be applied even if state is already computed but no later

This Node’s Lines are compared with the Avatars in the present state that are under location. Unmatched quantities in Lines give rise to Actions with type='app', whereas unmatched quantities in Avatars give rise to Actions with type='disp'.

Nodes are assumed to be either leafs or fully split: if the Node is not a leaf, then only Avatars directly pointing to its location will be considered. By contrast, the whole subhierarchy of container objects under location will be considered for leaf Nodes.

Finally, this method doesn’t issue Actions with type='telep': this is done in subsequent simplification steps, see for instance compute_push_actions().

Implementation and performance details:

Internally, this uses an SQL query that’s quite heavy:

  • recursive CTE for the sublocations
  • that’s joined with Avatar and PhysObj to extract quantities and information (type, code, properties)
  • on top of that, full outer join with Inventory.Line

but it has advantages:

  • works uniformely in the three cases:
    • no Inventory.Line matching a given Avatar
    • no Avatar matching a given Inventory.Line
    • a given Inventory.Line has matching Avatars, but the counts don’t match
  • minimizes round-trip to the database
  • minimizes Python side processing
clear_actions()[source]
compute_push_actions()[source]

Compute and simplify actions, push unsimplifable ones to the parent

The actions needed for reconcilation are first computed, then simplified by matching apparitions with disparitions to issue teleportations. The remaining apparitions and disparitions are pushed to the parent node for further simplification until we reach the top.

Pushing up to the parent may seem heavy, but it allows to split the whole reconciliation (with simplification) work into separate steps.

For big inventories, the caller of this method would typically commit for each Node. For really big inventories, the work could be split up between different processes.

recurse_compute_push_actions()[source]

Recursion along the whole tree in one shot.

This is not recommended for big inventories, as it will lead to one huge transaction.

Model.Wms.Inventory.Line

class anyblok_wms_base.inventory.node.Line[source]

Represent an assessment for a Node instance.

This is an inert model, meant to be filled through some user interface.

If the corresponding Node is a leaf, then location could be any container under the Node’s location.

But if the Node is split, then the location must be identical to the Node’s location, otherwise the simplification of reconciliation Actions can’t work properly.

Fields and their semantics

node = <anyblok.relationship.Many2One object>
location = <anyblok.relationship.Many2One object>
type = <anyblok.relationship.Many2One object>
code = <anyblok.column.Text object>
properties = <anyblok_postgres.column.Jsonb object>
quantity = <anyblok.column.Integer object>

Model.Wms.Inventory.Action

class anyblok_wms_base.inventory.action.Action[source]

Represent a reconciliation Action for a Node instance.

Fields and their semantics

node = <anyblok.relationship.Many2One object>
OPERATIONS = (('app', 'wms_inventory_action_app'), ('disp', 'wms_inventory_action_disp'), ('telep', 'wms_inventory_action_telep'))
quantity = <anyblok.column.Integer object>
location = <anyblok.relationship.Many2One object>
destination = <anyblok.relationship.Many2One object>

Optional destination container.

This is useful if type is telep only.

physobj_type = <anyblok.relationship.Many2One object>
physobj_code = <anyblok.column.Text object>
physobj_properties = <anyblok_postgres.column.Jsonb object>

Methods

classmethod simplify(node)[source]
apply()[source]

Perform Inventory Operations for the current Action.

Returns:tuple of the newly created Operations

The new Operations will all point to the related Inventory.

choose_affected()[source]

Choose Physical Objects to be taken for Disparition/Teleportation.

if physobj_code is None, we match only Physical Objects whose code is also None. That’s because the code should come directly from existing PhysObj records (that weren’t reflected in Inventory Lines).

Same remark would go for Properties, but: TODO implement Properties TODO adapt to wms-quantity

customize_operation_fields(operation_fields)[source]

Hook to modify fields of Operations spawned by apply()

This is meant for easy override by applications.

Parameters:operation_fields (dict) – prefilled by apply() with the minimal required values in the generic case. This methods mutates it in place
Returns:None

The typical customization would consist of putting additional fields that make sense for the local business logic, but this method isn’t limited to that.