This Operation Model inherits from Mixin.WmsSingleInput
anyblok_wms_base.core.operation.move.
Move
[source]¶A stock move
id
= <anyblok.column.Integer object>¶destination
= <anyblok.relationship.Many2One object>¶revert_extra_fields
()[source]¶Extra fields to take into account in plan_revert_single()
.
Singled out for easy subclassing, e.g., by the wms-quantity Blok.
is_reversible
()[source]¶Moves are always reversible.
See the base class
for what
reversibility exactly means.
This Operation Model inherits from Mixin.WmsSingleInput
anyblok_wms_base.core.operation.teleportation.
Teleportation
[source]¶Inventory Operation to record unexpected change of location for PhysObj.
This is similar to Move, but has a distinct functional meaning: the change of location is only witnessed after the fact, and it has no known explanation.
Teleportations can exist only in the done
state.
id
= <anyblok.column.Integer object>¶Primary key.
new_location
= <anyblok.relationship.Many2One object>¶Where the PhysObj record showed up.
check_create_conditions
(state, dt_execution, new_location=None, **kwargs)[source]¶Check that new_location is a container.
after_insert
()[source]¶Update input
Avatar and create a new one.
input
is set to past
,present
Avatar gets created at new_location
,check_create_conditions
(state, dt_execution, new_location=None, **kwargs)[source]Check that new_location is a container.
This Operation Model inherits from Mixin.WmsSingleInput
anyblok_wms_base.core.operation.observation.
Observation
[source]¶Operation to change PhysObj Properties.
Besides being commonly associated with some measurement or assessment being done in reality, this Operation is the preferred way to alter the Properties of a physical object (PhysObj), in a traceable, reversible way.
For now, only whole Property values are supported, i.e., for
dict
-valued Properties, we can’t observe the value of just a
subkey.
Observations support oblivion in the standard way, by reverting the Properties of the physical object to their prior values. This is consistent with the general rule that oblivion is to be used in cases where the database values themselves are irrelevant (for instance if the Observation was for the wrong physical object).
On the other hand, reverting an Observation is semantically more
complicated. See plan_revert_single()
for more details.
name
= <anyblok.column.Text object>¶The name of the observation, to identity quickly an observation
This field is optional and depends on the developer’s needs.
observed_properties
= <anyblok_postgres.column.Jsonb object>¶Result of the Observation.
It is forbidden to fill this field for a planned Observation: this is thought to be contradictory with the idea of actually observing something. In the case of planned Observations, this field should be updated right before execution.
TODO: rethink this, wouldn’t it make sense actually to record some expected results, so that dependent Operations could be themselves planned ? This doesn’t seem to be that useful though, since e.g., Assemblies can check different Properties during their different states. On the other hand, it could make sense in cases where the result is very often the same to prefill it.
Another case would be for reversals: prefill the result.
required_properties
= <anyblok_postgres.column.Jsonb object>¶List of Properties that must be present in observed_properties
In other words, these are Properties the Observation must update.
At execution time, the contents of observed_properties
is examined
and an error is raised if one of these properties is missing.
previous_properties
= <anyblok_postgres.column.Jsonb object>¶Used in particular during oblivion.
This records key/value pairs of direct properties before execution of the Observation TODO and maybe reversal
id
= <anyblok.column.Integer object>¶Primary key.
is_reversible
()[source]¶Observations are always reversible.
See plan_revert_single()
for a full discussion of this.
plan_revert_single
(dt_execution, follows=())[source]¶Reverting an Observation is a no-op.
For the time being, we find it sufficient to consider that Observations are really meant to find some information about the physical object (e.g a weight, a working condition). Therefore, reverting them doesn’t make sense, while we don’t want to consider them fully irreversible, so that a chain of Operations involving an Operation can still be reversed.
The solution to this dilemma for the time being is that reverting an Observation does nothing. For instance, if an Observation follows some other Operation and has itself a follower, the outcome of the reversal of the follower is fed directly to the reversal of the previous operation.
We may add more variants (reversal via a prefilled Observation etc.) in the future.
apply_properties
()[source]¶Save previous properties, then apply observed_properties`
The previous direct properties of the physical object get saved in
previous_properties
, then the
key/value pairs of observed_properties
are applied.
In case an observed value is a new one, ie, there wasn’t any direct key of that name before, it ends up simply to be absent from the :previous_properties dict (even if there was an inherited one).
This allows for easy restoration of previous values in
obliviate_single()
.
This Operation Model inherits from Mixin.WmsSingleInput
anyblok_wms_base.core.operation.unpack.
Unpack
[source]¶Unpacking some goods, creating new PhysObj and Avatar records.
This is a destructive Operation, in the usual mild sense: once it’s done,
the input PhysObj Avatars is in the past
state, and their underlying
PhysObj have no new Avatars.
It is conditionally reversible through appropriate Assembly Operations.
Which PhysObj will get created and which Properties they will bear is
specified in the unpack
behaviour of the Type of the PhysObj being
unpacked, together with their contents
optional Properties.
See get_outcome_specs()
and forward_props()
for details
about these and how to achieve the wished functionality.
Unpacks happen in place: the newly created Avatar appear in the location where the input was. It is thus the caller’s responsibility to prepend moves to unpacking areas, and/or append moves to final destinations.
id
= <anyblok.column.Integer object>¶get_outcome_specs
()[source]¶Produce a complete specification for outcomes and their properties.
In what follows “the behaviour” means the value associated with the
unpack
key in the PhysObj Type behaviours
.
Unless uniform_outcomes
is set to True
in the behaviour,
the outcomes of the Unpack are obtained by merging those defined in
the behaviour (under the outcomes
key) and in the
packs (self.input
) contents
Property.
This accomodates various use cases:
The properties on outcomes are set from those of self.input
according to the forward_properties
and required_properties
of the outcomes, unless again if uniform_outcomes
is set to
True
, in which case the properties of the packs (self.input
)
aren’t even read, but simply
cloned (referenced again) in the outcomes. This should be better
for performance in high volume operation.
The same can be achieved on a given outcome by specifying the
special 'clone'
value for forward_properties
.
Otherwise, the forward_properties
and required_properties
unpack behaviour from the PhysObj Type of the packs (self.input
)
are merged with those of the outcomes, so that, for instance
forward_properties
have three key/value sources:
uniform_outcomes=True
)outcomes
key)contents
property)Here’s a use-case: imagine the some purchase order reference is
tracked as property po_ref
(could be important for accounting).
A PhysObj Type representing an incoming package holding various PhysObj
could specify that po_ref
must be forwarded upon Unpack in all
cases. For instance, a PhysObj record with that type could then
specify that its outcomes are a phone with a given color
property (to be forwarded upon Unpack)
and a power adapter (whose colour is not tracked).
Both the phone and the power adapter would get the po_ref
forwarded, with no need to specify it on each in the incoming pack
properties.
TODO DOC move a lot to global doc
outcome_props_update
(spec)[source]¶Handle the properties for a given outcome (PhysObj record)
This is actually a bit more that just forwarding.
Parameters: |
|
---|---|
Returns: | the properties to update, as a |
Specification contents
properties
:A direct mapping of properties to set on the outcome. These have
the lowest precedence, meaning that they will
be overridden by properties forwarded from self.input
.
Also, if spec has the local_physobj_id
key, properties
is
ignored. The rationale for this is that normally, there are no
present or future Avatar for these PhysObj, and therefore the
Properties of outcome should not have diverged from the contents
of properties
since the spec (which must itself not come from
the behaviour, but instead from contents
) has been
created (typically by an Assembly).
required_properties
:list (or iterable) of properties that are required on
self.input
. If one is missing, then
OperationInputsError
gets raised.
forward_properties
.
forward_properties
:list (or iterable) of properties to copy if present from
self.input
to outcome
.
Required properties aren’t automatically forwarded, so that it’s
possible to require one for checking purposes without polluting the
Properties of outcome
. To forward and require a property, it has
thus to be in both lists.
create_unpacked_goods
(fields, spec)[source]¶Create PhysObj record according to given specification.
This singled out method is meant for easy subclassing (see, e.g, in wms-quantity Blok).
Parameters: |
|
---|---|
Returns: | the list of created PhysObj records. In |
plan_for_outcomes
(inputs, outcomes, dt_execution=None)[source]¶Create a planned Unpack of which some outcomes are already given.
This is useful for planning refinements, in cases the given future
outcomes already exist in the database, typically because they are
from Arrivals that are in the process of being superseded
Parameters: |
|
---|---|
Returns: | a pair made of
|
This method ensures that the newly created Unpack instance produces at least the same properties as already present on the given outcomes, and actually uses the properties as a match criteria to perform the attachments. It is on the other hand perfectly acceptable that the Unpack adds more properties, for instance because they were previously unplannable or irrelevant for the planning (use cases: serial and batch numbers, expiry dates…)
check_create_conditions
(state, dt_execution, inputs=None, quantity=None, **kwargs)[source]¶anyblok_wms_base.core.operation.assembly.
Assembly
[source]¶Assembly/Pack Operation.
This operation covers simple packing and assembly needs : those for which a single outcome is produced from the inputs, which must also be in the same Location.
The behaviour is specified on the outcome's PhysObj Type
(see Assembly specification
);
it amounts to describe the expected inputs,
and how to build the Properties of the outcome (see
outcome_properties()
). All Property related parameters in the
specification are bound to the state to be reached or passed through.
A given Type can be assembled in different ways: the
Assembly specification
is chosen within
the assembly
Type behaviour according to the value of the name
field.
Specific hooks
are available for
use-cases that aren’t covered by the specification format (example: to
forward Properties with non uniform values from the inputs to the
outcome).
The name
is the main dispatch key for these hooks, which don’t
depend on the outcome's Good Type
.
id
= <anyblok.column.Integer object>¶outcome_type
= <anyblok.relationship.Many2One object>¶The PhysObj Type
to produce.
name
= <anyblok.column.Text object>¶The name of the assembly, to be looked up in behaviour.
This field has a default value to accomodate the common case where there’s
only one assembly for the given outcome_type
.
Note
the default value is not enforced before flush, this can
prove out to be really inconvenient for downstream code.
TODO apply the default value in check_create_conditions()
for convenience ?
parameters
= <anyblok_postgres.column.Jsonb object>¶Extra parameters specific to this instance.
This dict
is merged with the parameters from the
outcome_type
behaviour to build the final specification
.
match
= <anyblok_postgres.column.Jsonb object>¶Field use to store the result of inputs matching
Assembly Operations match their actual inputs (set at creation)
with the inputs
part of specification
.
This field is used to store the
result, so that it’s available for further logic (for instance in
the property setting hooks
).
This field’s value is either None
(before matching) or a list
of lists: for each of the inputs specification, respecting
ordering, the list of ids of the matching Avatars.
specification
¶The Assembly specification
The Assembly specification is merged from two sources:
assembly
part of the behaviour field of
outcome_type
, the subdict associated with name
;parameters
.Here’s an example, for an Assembly whose name
is
'soldering'
, also displaying most standard parameters.
Individual aspects of these parameters are discussed in detail
afterwards, as well as the merging logic.
On the outcome_type
:
behaviours = {
…
'assembly': {
'soldering': {
'outcome_properties': {
'planned': {'built_here': ['const', True]},
'started': {'spam': ['const', 'eggs']},
'done': {'serial': ['sequence', 'SOLDERINGS']},
},
'inputs': [
{'type': 'GT1',
'quantity': 1,
'properties': {
'planned': {
'required': ['x'],
},
'started': {
'required': ['foo'],
'required_values': {'x': True},
'requirements': 'match', # default is 'check'
},
'done': {
'forward': ['foo', 'bar'],
'requirements': 'check',
}
},
{'type': 'GT2',
'quantity': 2
},
{'type': 'GT3',
'quantity': 1,
}
],
'inputs_spec_type': {
'planned': 'check', # default is 'match'
'started': 'match', # default is 'check' for
# 'started' and 'done' states
},
'for_contents': ['all', 'descriptions'],
'allow_extra_inputs': True,
'inputs_properties': {
'planned': {
'required': …
'required_values': …
'forward': …
},
'started': …
'done': …
}
}
…
}
}
On the Assembly instance:
parameters = {
'outcome_properties': {
'started': {'life': ['const', 'brian']}
},
'inputs': [
{},
{'code': 'ABC'},
{'id': 1234},
]
'inputs_properties': {
'planned': {
'forward': ['foo', 'bar'],
},
},
}
Note
Non standard parameters can be specified, for use in
Specific hooks
.
Inputs
The inputs
part of the specification is primarily a list of
expected inputs, with various criteria (PhysObj Type, quantity,
PhysObj code and Properties).
Besides requiring them in the first place, these criteria are also
used to qualify (match) the inputs
(note that Operation inputs are unordered in general,
while this inputs
parameter is). This spares the
calling code the need to keep track of that qualification after
selecting the goods in the first place. The result of that
matching is stored in the match
field, is kept for later
Assembly state changes and can be used by application
code, e.g., for operator display purposes.
Assemblies can also have extra inputs,
according to the value of the allow_extra_inputs
boolean
parameter. This is especially useful for generic packing scenarios.
Having both specified and extra inputs is supported (imagine packing client parcels with specified wrapping, a greetings card plus variable contents).
The type
criterion applies the PhysObj Type hierarchy, hence it’s
possible to create a generic packing Assembly for a whole family of
PhysObj Types (e.g., adult trekking shoes).
Similarly, all Property requirements take the properties inherited from the PhysObj Types into account.
Global Property specifications
The Assembly specification
can have the following
key/value pairs:
outcome_properties
:(TYPE, EXPRESSION)
, evaluated by passing as
positional arguments to eval_typed_expr()
.inputs_properties
:Per input Property checking, matching and forwarding
The same parameters as in inputs_properties
can also be specified
inside each dict
that form
the inputs
list of the Assembly specification
),
as the properties
sub parameter.
In that case, the Property requirements are used either as
matching criteria on the inputs, or as a check on already matched
PhysObj, according to the value of the inputs_spec_type
parameter
(default is 'match'
in the planned
Assembly state,
and 'check'
in the other states).
Example:
'inputs_spec_type': {
'started': 'match', # default is 'check' for
# 'started' and 'done' states
},
'inputs': [
{'type': 'GT1',
'quantity': 1,
'properties': {
'planned': {'required': ['x']},
'started': {
'required_values': {'x': True},
},
'done': {
'forward': ['foo', 'bar'],
},
…
]
During matching, per input specifications are applied in order,
but remember that
the ordering of self.inputs
itself is to be considered random.
In case inputs_spec_type
is 'check'
, the checking is done
on the PhysObj matched by previous states, thus avoiding a potentially
costly rematching. In the above example, matching will be performed
in the 'planned'
and 'started'
states, but a simple check
will be done if going from the started
to the done
state.
It is therefore possible to plan an Assembly with partial information
about its inputs (waiting for some Observation, or a previous Assembly
to be done), and to
refine that information, which can be displayed to operators, or have
consequences on the Properties of the outcome, at each state change.
In many cases, rematching the inputs for all state changes is
unnecessary. That’s why, to avoid paying the computational cost
three times, the default value is 'check'
for the done
and
started
states.
The result of matching is stored in the match
field.
In all cases, if a given Property is to be forwarded from several
inputs to the outcome and its values on these inputs aren’t equal,
AssemblyPropertyConflict
will be raised.
Passing through states
Following the general expectations about states of Operations, if
an Assembly is created directly in the done
state, it will apply
the outcome_properties
for the planned
, started
and
done
states.
Also, the matching and checks of input Properties for the planned
,
started
and done
state will be performed, in that order.
In other words, it behaves exactly as if it had been first planned, then started, and finally executed.
Similarly, if a planned Assembly is executed (without being started
first), then outcome Properties, matches and checks related to the
started
state are performed before those of the done
state.
for_contents: building the contents Property
The outcome of the Assembly bears the special contents property
, also
used by Operation.Unpack
.
This makes the reversal of Assemblies by Unpacks possible (with care in the behaviour specifications), and also can be used by applicative code to use information about the inputs even after the Assembly is done.
The building of the contents Property is controlled by the
for_contents
parameter, which itself is either None
or a
pair of strings, whose first element indicates which inputs to list,
and the second how to list them.
The default value of for_contents
is DEFAULT_FOR_CONTENTS
.
If for_contents
is None
, no contents Property will be set
on the outcome. Use this if it’s unnecessary pollution, for instance
if it is custom set by specific hooks anyway, or if no Unpack for
disassembly is ever to be wished.
for_contents: possible values of first element:
'all'
:'extra'
:for_contents: possible values of second element:
'descriptions'
:forward_properties
for those who are (TODO except those that
come from a global forward
in the Assembly specification)'records'
:descriptions
, but also includes the record ids, so
that an Unpack following the Assembly would not give rise to new
PhysObj records, but would reuse the existing ones, hence keep the
promise that the PhysObj records are meant to track the “sameness”
of the physical objects.Merging logic
All sub parameters are merged according to the expected type. For
instance, required
and forward
in the various Property
parameters are merged as a set
.
As displayed in the example above, if there’s an inputs
part
in parameters
, it must be made of exactly the same number
of dicts
as within the outcome_type
behaviour. More
precisely, these lists are merged using the zip()
Python
builtin, which results in a truncation to the shortest. Of course,
not having an inputs
part in parameters
does not
result in empty inputs
.
See also
Specific hooks
While already powerful, the Property manipulations described above are not expected to fit all situations. This is obviously true for the rule forbidding the forwarding of values that aren’t equal for all relevant inputs: in some use cases, one would want to take the minimum of theses values, sum them, keep them as a list, or all of these at once… On the other hand, the specification is already complicated enough as it is.
Therefore, the core will stick to these still
relatively simple primitives, but will also provide the means
to perform custom logic, through assembly-specific hooks
DEFAULT_FOR_CONTENTS
= ('extra', 'records')¶Default value of the for_contents
part of specification.
See outcome_properties()
for the meaning of the values.
SPEC_LIST_MERGE
= {'inputs': ('zip', {'*': {'properties': {'*': {'required': ('set', None), 'forward': ('set', None)}}}}), 'inputs_properties': {'*': {'required': ('set', None), 'forward': ('set', None)}}}¶check_inputs_locations
(inputs, **kwargs)[source]¶Check consistency of inputs locations.
This method is singled out for easy override by applicative code.
Indeed applicative code can consider that the inputs may be in
a bunch of related locations, with a well defined output location.
In particular, it receives keyword arguments kwargs
that we
don’t need in this default implementation.
outcome_properties
(state, for_creation=False)[source]¶Method responsible for properties on the outcome.
For the given state that is been reached, this method returns a dict of Properties to apply on the outcome.
Parameters: |
|
---|---|
Return type: |
|
Raises: |
|
The specific hook
gets called at the very end of the process, giving it higher
precedence than any other source of Properties.
outcome_location
()[source]¶Find where the new assembled physical object should appear.
In this default implementation, we insist on the inputs being in
a common location (see check_inputs_locations()
and we
decide this is the location of the outcome.
Applicative code is welcomed to refine this by overriding this method.
eval_typed_expr
(etype, expr)[source]¶Evaluate a typed expression.
Parameters: |
|
---|
Possible values for etype
'const'
:expr
is considered to be a constant and gets returned
directly. Any Python value that is JSON serializable is admissible.'sequence'
:expr
must be the code of a
Model.System.Sequence
instance. The return value is
the formatted value of that sequence, after incrementation.specific_outcome_properties
(assembled_props, state, for_creation=False)[source]¶Hook for per-name specific update of Properties on outcome.
At the time of Operation creation or execution,
this calls a specific method whose name is derived from the
name
field, by this format
, if that
method exists.
Applicative code is meant to override the present Model to provide the specific method. The signature to implement is identical to the present one:
Parameters: |
|
---|---|
Returns: | the properties to set or update |
Return type: | any iterable that can be passed to |
props_hook_fmt
= 'outcome_properties_{name}'¶