core.physobj

Model.Wms.PhysObj

class anyblok_wms_base.core.physobj.main.PhysObj[source]

Main data type to represent physical objects managed by the system.

The instances of this model are also the ultimate representation of the PhysObj “staying the same” or “becoming different” under the Operations, which is, ultimately, a subjective decision that has to be left to downstream libraires and applications, or even end users.

For instance, everybody agrees that moving something around does not make it different. Therefore, the Move Operation uses the same PhysObj record in its outcome as in its input. On the other hand, changing a property could be considered enough an alteration of the physical object to consider it different, or not (think of recording some measurement that had not be done earlier.)

Fields and their semantics

id = <anyblok.column.Integer object>

Primary key.

type = <anyblok.relationship.Many2One object>

The PhysObj Type

properties = <anyblok.relationship.Many2One object>

Link to Properties.

See also

Properties for functional aspects.

Warning

don’t ever mutate the contents of properties directly, unless what you want is precisely to affect all the PhysObj records that use them directly.

Besides their type and the fields meant for the Wms Base bloks logic, the PhysObj Model bears flexible data, called properties, that are to be manipulated as key/value pairs through the get_property() and set_property() methods.

As far as wms_core is concerned, values of properties can be of any type, yet downstream applications and libraries can choose to make them direct fields of the Properties model.

Properties can be shared among several PhysObj records, for efficiency. The set_property() implements the necessary Copy-on-Write mechanism to avoid unintentionnally modify the properties of many PhysObj records.

Technically, this data is deported into the Properties Model (see there on how to add additional properties). The properties column value can be None, so that we don’t pollute the database with empty lines of Property records, although this is subject to change in the future.

Type methods

has_type(goods_type)[source]

Tell whether self has the given type.

Parameters:goods_type (type.Type) –
Returns:True if the type attribute is goods_type or on of its descendants.
Rtype bool:
is_container()[source]

Tell whether the type is a container one.

Return type:bool

Property methods

get_property(k, default=None)[source]

Property getter, works like dict.get().

Actually I’d prefer to simply implement the dict API, but we can’t direcly inherit from UserDict yet. This is good enough to provide the abstraction needed for current internal wms_core calls.

merged_properties()[source]

Return all Properties, merged with the Type properties.

Return type:dict

To retrieve just one Property, prefer get_property(), which is meant to be more efficient.

has_property(name)[source]

Check if a Property with given name is present.

has_properties(names)[source]

Check in one shot if Properties with given names are present.

has_property_values(mapping)[source]

Check that all key/value pairs of mapping are in properties.

set_property(k, v)[source]

Property setter.

See remarks on get_property().

This method implements a simple Copy-on-Write mechanism. Namely, if the properties are referenced by other PhysObj records, it will duplicate them before actually setting the wished value.

update_properties(mapping)[source]

Update Properties in one shot, similar to dict.update()

Parameters:mapping – a dict like object, or an iterable of (key, value) pairs

This method implements a simple Copy-on-Write mechanism. Namely, if the properties are referenced by other PhysObj records, it will duplicate them before actually setting the wished value.

Avatar methods

current_avatar()[source]

The Avatar giving the current position of self in reality

Returns:the Avatar, or None, in case self is not yet or no more physically present.
eventual_avatar()[source]

The Avatar giving the latest foreseeable position of self.

Returns:the Avatar, or None, in case
  • self is planned to leave the system.
  • self has already left the system (only past avatars)

There are more complicated corner cases, but they shouldn’t arise in real operation and the results are considered to be dependent on the implementation of this method, hence not part of any stability promises. Simplest example: a single avatar, with state='present' and dt_until not None (normally a subsequent avatar, in the planned state should explain the bounded time range).

Containers methods

classmethod flatten_containers_subquery(top=None, additional_states=None, at_datetime=None)[source]

Return an SQL subquery flattening the containment graph.

Containing PhysObj can themselves be placed within a container through the standard mechanism: by having an Avatar whose location is the surrounding container. This default implementation issues a recursive CTE (WITH RECURSIVE) that climbs down along this, returning just the id column

This subquery cannot be used directly: it is meant to be used as part of a wider query; see unit tests) for nice examples with or without joins.

Note

This subquery itself does not restrict its results to actually be containers! Only its use in joins as locations of Avatars will, and that’s considered good enough, as filtering on actual containers would be more complicated (resolving behaviour inheritance) and is useless for quantity queries.

Applicative code relying on this method for other reasons than quantity counting should therefore add its own ways to restrict to actual containers if needed.

Parameters:top – if specified, the query starts at this Location (inclusive)

For some applications with a large and complicated containing hierarchy, joining on this CTE can become a performance problem. Quoting PostgreSQL documentation on CTEs:

However, the other side of this coin is that the optimizer is less able to push restrictions from the parent query down into a WITH query than an ordinary subquery. The WITH query will generally be evaluated as written, without suppression of rows that the parent query might discard afterwards.

If that becomes a problem, it is still possible to override the present method: any subquery whose results have the same columns can be used by callers instead of the recursive CTE.

Examples:

  1. one might design a flat Location hierarchy using prefixing on code to express inclusion instead of the standard Avatar mechanism. parent. See this test for a proof of this concept.
  2. one might make a materialized view out of the present recursive CTE, refreshing as soon as needed.

Model.Wms.PhysObj.Type

class anyblok_wms_base.core.physobj.type.Type[source]

Types of PhysObj.

For a full functional discussion, see Physical Object Types.

Fields and their semantics

id = <anyblok.column.Integer object>

Primary key

code = <anyblok.column.Text object>

Uniquely identifying code.

As a convenience, and for sharing with other applications.

behaviours = <anyblok_postgres.column.Jsonb object>

Flexible field to encode how represented objects interact with the system.

Notably, PhysObj Types specify with this flexible field how various Operations will treat the represented physical object.

See also

Unpack for a complex example.

But behaviours are in no means in one to one correspondence with Operation classes, nor do they need to be related to Operations. Any useful information that depends on the Type only is admissible to encode as a behaviour.

The value is a key/value mapping (behaviour name/value).

Warning

direct read access to a behaviour is to be avoided in favour of get_behaviour() (see Goods Type hierarchy and behaviour inheritance).

This field is also open for downstream libraries and applications to make use of it to define some of their specific logic, but care must be taken not to conflict with the keys used by wms-core and other bloks (TODO introduce namespacing, then ? at least make a list available by using constants from an autodocumented module)

Methods

is_sub_type(gt)[source]

True if self is a sub type of gt, inclusively.

TODO PERF the current implementation recurses over ancestors. A subsequent implementation could add caching and/or recursive SQL queries.

classmethod query_subtypes(ancestors, as_cte=False)[source]

Return a recursive SQL query for Type hierarchy.

Parameters:ancestors – the Type to start with (inclusive)

The resulting query or CTE matches all Types that are descendents (inclusive) of those in ancestors.

get_behaviour(name, default=None)[source]

Get the value of the behaviour with given name.

This method is the preferred way to access a given behaviour. It resolves the wished behaviour by looking it up within the behaviours dict, and recursively on its parent.

It also takes care of corner cases, such as when behaviours is None as a whole.

classmethod query_behaviour(behaviour, as_cte=False)[source]

Return a recursive SQL query for behaviour presence.

Parameters:
  • behaviour – Types having this behaviour will be returned. The value of the behaviours don’t matter.
  • as_cte (bool) – if True, instead of a directly usable query, a CTE (WITH statement) is returned, that the called may in turn use as they wish

The resulting query or CTE matches all Types that are descendents (inclusive) of those in ancestors.

Model.Wms.PhysObj.Properties

class anyblok_wms_base.core.physobj.main.Properties[source]

Properties of PhysObj.

This is kept in a separate Model (and SQL table) to provide sharing among several PhysObj instances, as they can turn out to be identical for a large number of them.

Use-case: receive a truckload of milk bottles that all have the same expiration date, and unpack everything down to the bottles. The expiration date would be stored in a single Properties instance, assuming there aren’t also non-uniform properties to store, of course.

Applications are welcome to overload this model to add new fields rather than storing their meaningful information in the flexible field, if it has added value for performance or programmming tightness reasons. This has the obvious drawback of defining some properties for all PhysObj, regardless of their Types, so it should not be abused.

This model implements a subset of the dict API, treating direct fields and top-level keys of flexible uniformely, so that, as long as all pieces of code use only this API to handle properties, flexible keys can be replaced with proper fields transparently at any time in the development of downstream applications and libraries (assuming of course that any existing data is properly migrated to the new schema).

Fields and their semantics

id = <anyblok.column.Integer object>

Primary key.

flexible = <anyblok_postgres.column.Jsonb object>

Flexible properties.

The value is expected to be a mapping, and all property handling operations defined in the wms-core will handle the properties by key, while being indifferent of the values.

Note

the core also makes use of a few special properties, such as contents. TODO make a list, in the form of constants in a module

Methods

classmethod create(**props)[source]

Direct creation.

The caller doesn’t have to care about which properties get stored as direct fields or in the flexible field.

This method is a better alternative than insertion followed by calls to set(), because it guarantees that only one SQL INSERT will be issued.

If no props are given, then nothing is created and None gets returned, thus avoiding a needless row in the database. This may seem trivial, but it spares a test for callers that would pass a dict, using the ** syntax, which could turn out to be empty.

duplicate()[source]

Insert a copy of self and return its id.

get(k, *default)[source]

Similar to dict.get().

__getitem__(k)[source]

Support for reading with the [] syntax.

Raises:KeyError
__setitem__(k, v)[source]

Support for writing with the [] notation.

pop(k, *default)[source]

Similar to dict.pop().

update(*args, **kwargs)[source]

Similar to dict.update()

This current implementation doesn’t attempt to be smarter that setting the values one after the other, which means in particular going through all the checks for each key. A future implementation might try and be more efficient.

as_dict()[source]

Return the properties as a dict.

This is not to be confused with the generic to_dict() method of all Models. The present method abstracts over the flexible field and the regular ones. It also strips id and doesn’t attempt to follow relationships.

__contains__(k)[source]

Support for the ‘in’ operator.

Field properties are always present. Since one could say that the database uses None to mark absence, it could be relevant to return False if the value is None (TODO STABILIZATION).

Model.Wms.PhysObj.Avatar

class anyblok_wms_base.core.physobj.main.Avatar[source]

PhysObj Avatar.

See in Core Concepts for a functional description.

Fields and their semantics

outcome_of = <anyblok.relationship.Many2One object>

The Operation that created this Avatar.

location = <anyblok.relationship.Many2One object>

Where the PhysObj are/will be/were.

See Location for a discussion of what this should actually mean.

state = <anyblok.column.Selection object>

State of existence in the premises.

see anyblok_wms_base.constants.

This may become an ENUM once Anyblok supports them.

dt_from = <anyblok.column.DateTime object>

Date and time from which the Avatar is meaningful, inclusively.

Functionally, even though the default in creating Operations will be to use the current date and time, this is not to be confused with the time of creation in the database, which we don’t care much about.

The actual meaning really depends on the value of the state field:

  • In the past and present states, this is supposed to be a faithful representation of reality.
  • In the future state, this is completely theoretical, and wms-core doesn’t do much about it, besides using it to avoid counting several Avatars of the same physical goods while peeking at quantities in the future. If the end application does serious time prediction, it can use it freely.

In all cases, this doesn’t mean that the very same PhysObj aren’t present at an earlier time with the same state, location, etc. That earlier time range would simply be another Avatar (use case: moving back and forth).

dt_until = <anyblok.column.DateTime object>

Date and time until which the Avatar record is meaningful, exclusively.

Like dt_from, the meaning varies according to the value of state:

  • In the past state, this is supposed to be a faithful representation of reality: apart from the special case of formal Splits and Aggregates, the goods really left this location at these date and time.
  • In the present and future states, this is purely theoretical, and the same remarks as for the dt_from field apply readily.

In all cases, this doesn’t mean that the very same goods aren’t present at an later time with the same state, location, etc. That later time range would simply be another Avatar (use case: moving back and forth).

id = <anyblok.column.Integer object>

Primary key.