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: Goods 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 Goods Avatars that the current Operation produces, unless another Operation has been executed afterwards, becoming their reason. If no Operation is downstream, one can think of outcomes as the results of the current Operation.

This default implementation considers that the inputs of the current Operation never are outcomes.

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 Goods 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 Goods 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 Goods, 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 Goods 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 sound more specific.

This is not to be confused with reversals, which try and create a chain of Operations to perform to 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.

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

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.

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.

To be implemented in sublasses

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.

latest_previous_op = <anyblok.relationship.Many2One object>

The latest operation that affected avatars before operation.

This is both the fundamental data structure suporting history (DAG) aspects of Operations, as is exposed in the Operation.follows and Operation.followers attributes and, on the other hand, the preservation of reason for restore if needed, even after the current operation is done.

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 Goods record

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

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

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.