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).
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:
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.
These are the methods end applications and downstream libraries are supposed to use.
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: |
|
---|---|
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.
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: |
|
---|---|
Returns: | the planned reversal |
Return type: |
|
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
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.
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
inputs_number
, andOperation 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
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.
operation
= <anyblok.relationship.Many2One object>¶The Operation we are interested in.
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
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.
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.
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
.