Handling and transforming Operations

Model.Wms.Operation.Move

This Operation Model inherits from Mixin.WmsSingleInput

class anyblok_wms_base.core.operation.move.Move[source]

A stock move

Fields and their semantics

id = <anyblok.column.Integer object>
destination = <anyblok.relationship.Many2One object>

Specific members

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.

Mandatory methods of Operation subclasses

after_insert()[source]
execute_planned()[source]
is_reversible()[source]

Moves are always reversible.

See the base class for what reversibility exactly means.

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

Overridden methods of Operation

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

Ensure that destination is indeed a container.

Model.Wms.Operation.Teleportation

This Operation Model inherits from Mixin.WmsSingleInput

class 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.

Fields and their semantics

id = <anyblok.column.Integer object>

Primary key.

new_location = <anyblok.relationship.Many2One object>

Where the PhysObj record showed up.

Mandatory methods of Operation subclasses

classmethod 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.

  • the state of input is set to past,
  • a new present Avatar gets created at new_location,
  • care is taken of date & time fields.

Overridden methods of Operation

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

Check that new_location is a container.

Model.Wms.Operation.Observation

This Operation Model inherits from Mixin.WmsSingleInput

class 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.

Fields and their semantics

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.

Overridden methods of Operation

obliviate_single()[source]

Restore the Properties as they were before execution.

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.

Mandatory methods of Operation subclasses

after_insert()[source]
execute_planned()[source]

Specific members

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().

Model.Wms.Operation.Unpack

This Operation Model inherits from Mixin.WmsSingleInput

class 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.

Fields and their semantics

id = <anyblok.column.Integer object>

Specific members

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:

  • fixed outcomes:
    a 6-pack of orange juice bottles gets unpacked as 6 bottles
  • fully variable outcomes:
    a parcel with described contents
  • variable outcomes:
    a packaging with parts always present and some varying.

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:

  • at toplevel of the behaviour (uniform_outcomes=True)
  • in each outcome of the behaviour (outcomes key)
  • in each outcome of the PhysObj record (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:
  • spec (dict) – the relevant specification for this outcome, as produced by get_outcome_specs() (see below for the contents).
  • outcome – the just created PhysObj instance
Returns:

the properties to update, as a dict

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:
  • fields – pre-baked fields, prepared by the base class. In the current implementation, they are fully derived from spec, hence one may think of them as redundant, but the point is that they are outside the responsibility of this method.
  • spec – specification for these PhysObj, should be used minimally in subclasses, typically for quantity related adjustments. Also, if the special local_physobj_ids is provided, this method should attempt to reuse the PhysObj record with that id (interplay with quantity might depend on the implementation).
Returns:

the list of created PhysObj records. In wms-core, there will be as many as the wished quantity, but in wms-quantity, this maybe a single record bearing the total quantity.

classmethod 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:
  • inputs – should be made of only one element, an Avatar of the physical object to be unpacked, yet it’s convenient to get it as an iterable (also for the caller).
  • outcomes – candidate Avatars to reinterpret as outcomes of the newly created Unpack. It is possible that the Unpack produces some extra ones, and conversely that some of them are not produced by the Unpack.
Returns:

a pair made of

  • the created Unpack
  • the sublist of outcomes that have been attached.

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…)

Overridden methods of Operation

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

Remove the newly created PhysObj, not only their Avatars.

Mandatory methods of Operation subclasses

after_insert()[source]
execute_planned()[source]

Model.Wms.Operation.Assembly

class 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.

Fields and their semantics

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.

Specific members

specification

The Assembly specification

The Assembly specification is merged from two sources:

  • within the assembly part of the behaviour field of outcome_type, the subdict associated with name;
  • optionally, the instance specific 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:
    a dict whose keys are Assembly states, and values are dicts of Properties to set on the outcome; the values are pairs (TYPE, EXPRESSION), evaluated by passing as positional arguments to eval_typed_expr().
  • inputs_properties:
    a dict whose keys are Assembly states, and values are themselves dicts with key/values:
    • required:
      list of properties that must be present on all inputs while reaching or passing through the given Assembly state, whatever their values
    • required_values:
      dict of Property key/value pairs that all inputs must bear while reaching or passing through the given Assembly state.
    • forward:
      list of properties to forward to the outcome while reaching or passing through the given Assembly state.

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':
    all inputs will be listed
  • 'extra':
    only the actual inputs that aren’t specified in the behaviour will be listed. This is useful in cases where the Unpack behaviour already takes the specified ones into account. Hence, the variable parts of Assembly and Unpack are consistent.

for_contents: possible values of second element:

  • 'descriptions':
    include PhysObj’ Types, those Properties that aren’t recoverable by an Unpack from the Assembly outcome, together with appropriate forward_properties for those who are (TODO except those that come from a global forward in the Assembly specification)
  • 'records':
    same as 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

SPEC_LIST_MERGE and dict_merge.

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)}}}
classmethod 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:
  • state – The Assembly state that we are reaching.
  • for_creation (bool) – if True, means that this is part of the creation process, i.e, there’s no previous state.
Return type:

Model.Wms.PhysObj.Properties

Raises:

AssemblyInputNotMatched if one of the input specifications is not matched by self.inputs, AssemblyPropertyConflict in case of conflicting values for the outcome.

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:
  • expr – the expression to evaluate
  • etype – the type or expr.

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:
  • state – The Assembly state that we are reaching.
  • assembled_props (dict) – a dict of already prepared Properties for this state.
  • for_creation (bool) – if True, means that this is part of the creation process, i.e, there’s no previous state.
Returns:

the properties to set or update

Return type:

any iterable that can be passed to dict.update().

props_hook_fmt = 'outcome_properties_{name}'

Mandatory methods of Operation subclasses

classmethod check_create_conditions(state, dt_execution, inputs=None, outcome_type=None, name=None, **kwargs)[source]
after_insert()[source]
execute_planned()[source]

Check or rematch inputs, update properties and states.