Abstract classes

Fundamental Models

Model.Wms.Operation

class anyblok_wms_base.core.operation.base.Operation[source]

Base class for all Operations.

Warning

downstream applications and libraries should never issue insert() for Operations in their main code, and must use create() instead. See the documention of create() for more about this.

The Operation model encodes the common part of all precise operations, which themselves have dedicated models. This is implemented with the polymorphic features of SQLAlchemy and AnyBlok.

The main purpose of this separation is to simplify the representation of operational history: PhysObj Avatars and Operations can be linked together in full generality.

Downstream applications and libraries can add columns on the present model to satisfy their auditing needs (some notion of “user” or “operator” comes to mind).

Fields and their semantics

id = <anyblok.column.Integer object>

The primary key (serial integer)

its value is equal to that of the concrete Operation Model.

type = <anyblok.column.Selection object>

Polymorphic key to dispatch between the various Operation Models.

As such keys are supposed to be unique among all polymorphic cases in the whole application, those defined by WMS Base are prefixed with wms_.

state = <anyblok.column.Selection object>

The current state (lifecycle step) of the Operation.

See Lifecycle of operations for the meanings.

inputs

The Avatars records the Operation is working on.

This is a read-only pseudo field, initialized by create(). The backing data is actually stored in Model.Wms.Operation.HistoryInput.

outcomes

Return the outcomes of the present operation.

Outcomes are the PhysObj Avatars that the current Operation produces.

This is a Python property, because it might become a field at some point.

follows

Immediate predecessors in the Operation history,

These are the latest Operations that affected the inputs (and recorded themselves on them).

There may be zero, one or more of them.

Don’t assume it’d be a list, it could as well be a set or tuple, or any iterable.

Examples:

  • a move of a bottle of milk that follows the unpacking of a 6-pack, which itself follows a move from somewhere else
  • a parcel packing operation that follows exactly one move to the shipping area for each PhysObj to be packed. they themselves would follow more operations.
  • an Arrival typically doesn’t follow anything (but might be due to some kind of purchase order).

This is a read-only pseudo field, initialized by create(), The backing data is actually stored in Model.Wms.Operation.HistoryInput.

followers

The converse of follows

These are the Operations that are directly after the curent one. There may be also 0, 1 or more of them.

Don’t assume it’d be a list, it could as well be a set or tuple, or any iterable.

This is a read-only pseudo field, initialized by create(), The backing data is actually stored in Model.Wms.Operation.HistoryInput.

dt_execution = <anyblok.column.DateTime object>

Date and time of execution.

For Operations in state done, this represents the time at which the Operation has been completed, i.e., has reached that state.

For Operations in states planned and started, this represents the time at which the execution is supposed to complete. This has consequences on the dt_from and dt_until fields of the PhysObj Avatars affected by this Operation, to avoid summing up several Avatars of the same physical goods while peeking at quantities in the future, but has no other strong meaning within wms-core: if the end application does some serious time prediction, it can use it about freely. The actual execution can occur later at any time, be it sooner or later, as execute() will in particular correct the value of this field, and its consequences on the affected Avatars.

dt_start = <anyblok.column.DateTime object>

Date and time of start, if different than execution.

For all Operations, if the value of this field is None, this means that the Operation is considered to be instantaneous.

Overall, wms-core doesn’t do much about this field other than recording it: end applications that perform does serious time predictions can use it about freely.

Note

We will probably later on make use of this field in destructive Operations to update the dt_until field of their inputs, meaning that they won’t appear in present quantity queries anymore.

For Operations in states done and started this represents the time at which the Operation has been started, i.e., has reached the started state.

For Operations in state planned, this is as much theoretical as the field dt_execution is.

Main API

These are the methods end applications and downstream libraries are supposed to use.

classmethod create(state='planned', inputs=None, dt_execution=None, dt_start=None, **fields)[source]

Main method for creation of operations

In contrast with insert(), this class method performs some Wms specific logic, e.g, creation of PhysObj, but that’s up to the specific subclasses.

Parameters:
  • state

    value of the state field right after creation. It has a strong influence on the consequences of the Operation: creating an Operation in the done state means executing it right away.

    Creating an Operation in the started state should make the relevant PhysObj and/or Avatar locked or destroyed right away (TODO not implemented)

  • fields – remaining fields, to be forwarded to insert and the various involved methods implemented in subclasses.
  • dt_execution – value of the attr:dt_execution right at creation. If state==planned, this is mandatory. Otherwise, it defaults to the current date and time (datetime.now())
Return Operation:
 

concrete instance of the appropriate Operation subclass.

In principle, downstream developers should never call insert().

As this is Python, nothing really forbids them of doing so, but they must then exactly know what they are doing, much like issuing INSERT statements to the database).

Keeping insert() as in vanilla SQLAlchemy has the advantage of making them easily usable in wms_core internal implementation without side effects.

On the other hand, downstream developers should feel free to insert() and update() in their unit or integration tests. The fact that they are inert should help reproduce weird situations (yes, the same could be achieved by forcing to use the Model class methods instead).

execute(dt_execution=None)[source]

Execute the operation.

Parameters:dt_execution (datetime) – the time at which execution happens. This parameter is meant for tests, or for callers doing bulk execution. If omitted, it defaults to the current date and time (datetime.now())

This is an idempotent call: if the operation is already done, nothing happens.

cancel()[source]

Cancel a planned operation and all its consequences.

This method will recursively cancel all follow-ups of self, before cancelling self itself.

The implementation is for now a simple recursion, and hence can lead to RecursionError on huge graphs. TODO rewrite using an accumulation logic rather than recursion.

plan_revert(dt_execution=None)[source]

Plan operations to revert the present one and its consequences.

Like cancel(), this method is recursive, but it applies only to operations that are in the ‘done’ state.

It is expected that some operations can’t be reverted, because they are destructive, and in that case an exception will be raised.

For now, time handling is rather dumb, as it will plan all the operations at the same date and time (this Blok has to idea of operations lead times), but that shouldn’t be a problem.

Parameters:dt_execution (datetime) – the time at which to plan the reversal operations. If not supplied, the current date and time will be used.
Return type:(Operation, list(Operation))
Returns:the operation reverting the present one, and the list of initial operations to be executed to actually start reversing the whole.
obliviate()[source]

Totally forget about an executed Operation and all its consequences.

This is intended for cases where an Operation has been recorded by mistake (bug or human error), but did not happen at all in reality.

We chose the word “obliviate” because it has a stronger feeling that simply “forget” and also sounds more specific.

This is not to be confused with reversals, which try and create a chain of Operations whose execution would revert the effect of some Operations.

If one reverts a Move that has been done by mistake, that means one performs a Move back (takes some time, can go wrong). If one obliviates a Move, that means one acknowledges that the Move never happened: its mere existence in the database is itself the mistake.

Also, some Operations cannot be reverted in reality, whereas oblivion in our sense have no effect on reality.

This method will recursively obliviate all follow-ups of self, before self itself.

The implementation is for now a simple recursion, and hence can lead to RecursionError on huge graphs. TODO rewrite using an accumulation logic rather than recursion. TODO it is also very much a duplication of cancel(). The recursion logic itself should probably be factorized in a common method.

TODO For the time being, the implementation insists on all Operations to be in the done state, but it should probably accept those that are in the planned state, and call cancel() on them, maybe this could become an option if we can’t decide.

alter_destination(destination)[source]

Change the destination of a planned Operation.

This is for Operations that are responsible for the location of their outcomes (see the destination_field class attribute)

The followers’ input_location_altered() will be called (will potentially recurse)

Mandatory API of subclasses

These are the methods the concrete Operation subclasses must implement.

Note

we provide helper Mixins to help reduce boilerplate and duplication.

after_insert()[source]

Perform specific logic after insert during creation process

To be implemented in subclasses.

execute_planned()[source]

Execute an operation that has been up to now in the ‘planned’ state.

To be implemented in subclasses.

This method does not have to care about the Operation state, which the base class has already checked.

This method must correct the dates and times on the affected Avatars or more broadly of any consequences of the theoretical execution date and time that has been set during planning. For that purpose, it can rely on the value of the dt_execution field to be now the final one (can be sooner or later than expected).

Normally, this method should not need either to perform any checks that the execution can, indeed, be done: such subclasses-specific tests are supposed to be done within check_execute_conditions().

Downstream applications and libraries are not supposed to call this method: they should use execute(), which takes care of all the above-mentioned preparations.

plan_revert_single(dt_execution, follows=())[source]

Create a planned operation to revert the present one.

This method assumes that reversals have already been issued for follow-up Operations, and takes them as input.

Parameters:
  • dt_execution (datetime) – the date and time at which to plan the reversal operations.
  • follows (list(Operation)) – the Operations that the reversal will have to follow. In other words, these are the reversals of self.followers (can be empty).
Returns:

the planned reversal

Return type:

Operation

Downstream applications and libraries are not supposed to call this method: they should use plan_revert(), which takes care of the necessary recursivity.

To be implemented in sublasses

input_location_altered()[source]

Callback to notify the Operation that location changed on an input.

This is useful in replanifications: when a change in the Operations DAG leads to a direct change of some Avatar locations, the Operations that have these as inputs must be notified with this method (it’s quite possible that several inputs of a given Operation have changed locations).

Here, “change of location” means that some Avatar’s location field value has changed, it doesn’t mean some physical objects have been moved.

Optional API of subclasses

These methods have a default implementation in the base class, and are meant for the concrete Operation subclasses to override them if needed.

destination_field = None

For Operations that are responsible for the location of their outcomes.

For instance, an Unpack happens in place, so this class attribute will remain None. On the other hand, Moves and Arrivals will have their destination fields here.

This is useful for methods such as alter_destination() to know if they are relevant to that Operation and how to update it.

is_reversible()[source]

Tell whether the current operation can be in principle reverted.

This does not check that actual conditions to plan a revert are met (which would need to plan reversals for all followers first), but only that, in principle, it is possible.

Returns:the answer
Return type:bool

As there are many irreversible operations, and besides, reversibility has to be implemented for each subclass, the default implementation returns False. Subclasses implementing reversibility have to override this.

classmethod check_create_conditions(state, dt_execution, inputs=None, **kwargs)[source]

Check that the conditions are met for the creation.

This is done before calling insert().

In this default implementation, we check

  • that the number of inputs is correct, by comparing with inputs_number, and
  • that they are all in the proper state for the wished Operation state.

Subclasses are welcome to override this, and will probably want to call it back, using super.

check_execute_conditions()[source]

Used during execution to check that the Operation is indeed doable.

In this default implementation, we check that all the inputs are in the present state.

Subclasses are welcome to override this, and will probably want to call it back, using super.

cancel_single()[source]

Cancel just the current operation.

This method assumes that follow-up Operations have already been taken care of. It removes all planned consequences of the current one, but dos not delete it.

The default implementation calls reset_inputs_original_values(), then delete_outcomes() (all outcomes should be in the future state). Subclasses should override this if there’s more to be done.

Downstream applications and libraries are not supposed to call this method: they should use cancel(), which takes care of the necessary recursivity and the final deletion.

obliviate_single()[source]

Oblivate just the current operation.

This method assumes that follow-up Operations are already been taken care of. It removes all consequences of the current one, but does not delete it.

The default implementation calls reset_inputs_original_values(), then delete_outcomes().

Subclasses should override this if there’s more to be done.

Downstream applications and libraries are not supposed to call this method: they should use obliviate(), which takes care of the necessary recursivity and the final deletion.

classmethod before_insert(inputs=None, **fields)[source]

Model.Wms.Operation.HistoryInput

class anyblok_wms_base.core.operation.base.HistoryInput[source]

Internal Model linking Operations with their inputs and together.

The main purpose of this model is to represent the Direct Acyclic Graph (DAG) of Operations history, and its “weighing”: the links to Operation inputs.

Additionally, some things to keep track of for cancel and oblivion are also stored there.

Fields and their semantics

operation = <anyblok.relationship.Many2One object>

The Operation we are interested in.

avatar = <anyblok.relationship.Many2One object>

One of the inputs of the operation.

orig_dt_until = <anyblok.column.DateTime object>

Saving the original dt_until value of the Avatar

This is needed for cancel and oblivion

Helper Mixin classes

These implement a subset of the mandatory API for subclasses of Operation.

They tend to have long names because Anyblok does not have namespaces for Mixins.

Being Mixins, they can themselves be overridden in concrete applications, but this is not recommended except for quick bug fixing.

Mixin.WmsSingleInputOperation: working on a single input

class anyblok_wms_base.core.operation.single_input.WmsSingleInputOperation[source]

Mixin for Operations that apply to a single record of PhysObj.

This is synctactical sugar, allowing to work with such Operations as if Operations weren’t meant in general for multiple inputs.

Fields and their semantics

inputs_number = 1

Tell the base class that, indeed, we expect a single input.

input

Convenience attribute to refer to the single element of inputs.

Overrides of the base class.

classmethod create(input=None, inputs=None, **kwargs)[source]

Accept the alternative input arg and call back the base class.

This override is for convenience in a case of a single input.

Specific methods.

refine_with_leading_move(stopover)[source]

Split the current Operation in two, the first one being a Move

Parameters:stopover – this is the location of the intermediate Avatar that’s been introduced (destination of the Move).
Returns:the new Move

This doesn’t change anything for the Operations that the current Operation follows, and in fact, it is guaranteed that their outcomes are untouched by this method.

This may recurse for consequences of the fact that the location of self.input changes in the process (and in fact it’s a new Avatar), but it is expected that this should be used mostly for Operations for which the location of the inputs don’t matter, such as Move or Departure.

Example use case: Rather than planning a Move from stock to a shipping area, followed by a Departure, one may wish to just plan a Departure directly from the stock location, and later on, refine this as Move, then Departure. This is especially useful if the shipping area can’t be determined at the time of the original planning, or simply to follow the general principle of sober planning.

Mixin.WmsSingleOutcomeOperation: producing only one outcome

class anyblok_wms_base.core.operation.single_outcome.WmsSingleOutcomeOperation[source]

Mixin for Operations that produce a single outcome.

This is synctactical sugar, allowing to work with such Operations as if Operations couldn’t in general produce several outcomes.

Fields and their semantics

outcome

Convenience attribute to return the unique outcome.

Specific methods.

refine_with_trailing_move(stopover)[source]

Split the current Operation in two, the last one being a Move

This is for Operations that are responsible for the location of their outcome (see the destination_field class attribute)

Parameters:stopover – this is the location of the intermediate Avatar that’s been introduced (starting point of the Move).
Returns:the new Move instance

This doesn’t change anything for the followers of the current Operation, and in fact, it is guaranteed that their inputs are untouched by this method.

Example use case: Rather than planning an Arrival followed by a Move to stock location, One may wish to just plan an Arrival into some the final stock destination, and later on, refine this as an Arrival in a landing area, followed by a Move to the stock destination. This is especially useful if the landing area can’t be determined at the time of the original planning, or simply to follow the general principle of sober planning.

Mixin.WmsInPlaceOperation: staying in the same location or container

class anyblok_wms_base.core.operation.in_place.WmsInPlaceOperation[source]

Mixin for Operations that happen in place.

For these Operations, all inputs are in the same location, and the locations of their outcomes is the one of the inputs.

Specific methods.

classmethod unique_inputs_location(inputs)[source]

Return the unique location of the given Avatars.

Parameters:inputs – the Avatars to check and extract from.
Returns:the unique location
Raises:OperationInputsError in case location is not unique among inputs

Overrides of the base class.

classmethod check_create_conditions(state, dt_execution, inputs=None, **fields)[source]

Check that inputs locations are all the same.

input_location_altered()[source]

An in place Operation must propagate change of locations.

This checks that the inputs locations are still all the same, updates the location of the outcomes and notifies the followers.

Raises:OperationInputsError if the inputs locations now differ

Mixin.WmsInventoryOperation: common logic of inventory Operations

class anyblok_wms_base.core.operation.inventory.WmsInventoryOperation[source]

Mixin for Inventory Operations.

Inventory Operations have some common features / traits, such as not supporting the planned state.

Also, this Mixin allows dependent Bloks to add more functionality to all of them in one shot.

Optional API of subclasses

classmethod check_create_conditions(state, dt_execution, **kwargs)[source]

Forbid creation with wrong states.

Raises:OperationForbiddenState if state is not 'done'