AnyBlok / WMS Base (AWB)

A stocks and logistics engine with Python 3, SQLAlchemy, PostgreSQL and AnyBlok.

Stocks and Logistics ?

State of the project

Goal of this talk

Let's be honest

Use cases

What do they have in common ?

Common required features

Before diving in, a bit more about motivation

history tracking: speak of stock levels


Key aspects

An example scenario

Made for this series of talks, and available at

Let's start an IPython interpreter on the example project:

$ venv/bin/anyblok_interpreter -c demo.cfg
Loading config file '/etc/xdg/AnyBlok/conf.cfg'
Loading config file '/home/gracinet/.config/AnyBlok/conf.cfg'
Loading config file '/home/gracinet/anyblok/awb-pyconfr-2018/demo.cfg'
Python 3.5.3 (default, Sep 27 2018, 17:25:39)
Type 'copyright', 'credits' or 'license' for more information
IPython 7.0.1 -- An enhanced Interactive Python. Type '?' for help.

In [1]:

Physical objects

Let's fetch one of our example Types, then the physical objects of that type.

Take your time on this screen, it's the first with actual code examples.

  • explain use of registry and Models
  • comment the PhysObj naming choice
In [1]: PhysObj = registry.Wms.PhysObj
In [2]: book_type = PhysObj.Type.query().filter_by(code='GR-DUST-WIND-VOL2').one()
In [3]: units = PhysObj.query().filter_by(type=book_type).all()
In [4]: units
[Wms.PhysObj(id=18, type=Wms.PhysObj.Type(id=7, code='GR-DUST-WIND-VOL2')),
Wms.PhysObj(id=19, type=Wms.PhysObj.Type(id=7, code='GR-DUST-WIND-VOL2')),
Wms.PhysObj(id=20, type=Wms.PhysObj.Type(id=7, code='GR-DUST-WIND-VOL2')),
Wms.PhysObj(id=21, type=Wms.PhysObj.Type(id=7, code='GR-DUST-WIND-VOL2')),
Wms.PhysObj(id=22, type=Wms.PhysObj.Type(id=7, code='GR-DUST-WIND-VOL2'))]

PhysObj: Properties

Physical Objects also sport a flexible properties system

In [5]: units[0]
Out[5]: Wms.PhysObj(id=18, type=Wms.PhysObj.Type(id=7, code='GR-DUST-WIND-VOL2')

In [6]: units[0].merged_properties()
Out[6]:{'lot': '12A345'}

In [7]: unit[0].set_property('used-on-display', True)

In [8]: units[0].get_property('used-on-display')
Out[9]: True

Under the hood, within the flexible JSONB field, or separate table columns.

PhysObj: more about Types

If handling differs, PhysObj Type must differ

Ex: a crate of 50 must be represented by another Type than 50 units:

In [8]: crate = PhysObj.Type.query().filter_by(code='GR-DUST-WIND-VOL1/CRATE').one()

In [9]: PhysObj.query().filter_by(type=crate).count()

And a pallet of 80 crates is again something else than 80 crates:

In [10]: pallet = PhysObj.Type.query().filter_by(code='GR-DUST-WIND-VOL1/PALLET').one()

In [11]: PhysObj.query().filter_by(type=pallet).all()
Out[11]: [Wms.PhysObj(id=20, type=Wms.PhysObj.Type(id=6, code='GR-DUST-WIND-VOL1/PALLET'))]

Up to now, we've seen how to answer the first question: "what?", time to speak of the others!

PhysObj.Avatar: when and where

We use a distinct model, Wms.PhysObj.Avatar to encode time and place information about the physical objects.

In [12]: Avatar = PhysObj.Avatar

In [13]: avatars = Avatar.query().filter_by(obj=units[0]).order_by(Avatar.dt_from).all()

In [14]: [(av.state, av.location.code, str(av.dt_from)) for av in avatars]
[('past', 'IN PLATFORM', '2018-10-06 01:00:40.366405+02:00'),
('past', 'BIN #3', '2018-10-06 01:00:40.397054+02:00'),
('present', 'PACKING AREA', '2018-10-06 01:00:40.416139+02:00'),
('future', 'OUT PLATFORM', '2018-10-07 13:00:40.416139+02:00')]

Locations are nothing but instances of Wms.PhysObj (!)

In [15]: avatars[0].location
Out[15]: Wms.PhysObj(id=2, code='IN PLATFORM',
                     type=Wms.PhysObj.Type(id=1, code='EMPLACEMENT FIXE'))

PhysObj.Avatar: where and when

Motivation de la séparation entre PhysObj et PhysObj.Avatar :

  • hygiène de base de données
  • réservation

PhysObj.Avatar: where and when

Motivation de la séparation entre PhysObj et PhysObj.Avatar :

  • hygiène de base de données
  • réservation

Operations: how and why

In [16]: op = avatars[-1].reason  # will be outcome_of from 0.9 onwards

In [17]: op
Out[17]: Model.Wms.Operation.Move(id=17, state='planned',
                                                          code='OUT PLATFORM',

In [18]: op.execute()

In [19]: avatars[-1].state
Out[19]: 'present'

To conclude, let's ship!

In [20]: registry.Wms.Operation.Departure.create(input=avatars[-1], state='done')

In [21]: avatars[-1].state
Out[21]: 'past'

No separate Location Model ?

This makes for a bit of indirection…

AWB does provide high level methods to compute stock quantities


Operations: lifecycle

Opérations: lifecycle

Available Operations

Other AWB components

Up to now, all we've seen is provided by the wms-core Blok. We also have:

Future developments

General ideas page:

Lots of interesting things remain to be done:

Presentation goals

Let's rephrase the goals I stated near the beginning

Questions, suggestions?

Complements: unpacking

Let's unpack a pallet:

In [22]: pallet
Out[22]: Wms.PhysObj.Type(id=7, code='GR-DUST-WIND-VOL1/PALLET')

In [23]: pallet_av = Avatar.query().join(Avatar.obj).filter_by(type=pallet).one()

In [24]: pallet_av.state, pallet_av.location.code
Out[24]: ('present', 'SALLE1')

In [25]:unpack = registry.Wms.Operation.Unpack.create(input=pallet_av, state='done')
Out[25]: len(unpack.outcomes)

In [26]: set((avatar.state, avatar.obj.type.code, avatar.location.code)
    ...:     for avatar in unpack.outcomes)
{('present', 'GR-DUST-WIND-VOL1/CRATE', 'SALLE1'),
('present', 'PALLET WOOD', 'SALLE1')}

Unpacking declaration

Let's introspect it:

In [27]: pallet
Out[27]: Wms.PhysObj.Type(id=7, code='GR-DUST-WIND-VOL1/PALLET')

In [28] pallet.behaviours['unpack']
{'outcomes': [{'forward_properties': ['lot'],
               'quantity': 80,
               'required_properties': [],
               'type': 'GR-DUST-WIND-VOL1/CRATE'},
              {'forward_properties': [],
              'quantity': 1,
              'required_properties': [],
              'type': 'PALLET WOOD'}]}}
Right, Down, Page DownNext slide
Left, Up, Page UpPrevious slide
GGo to slide number
POpen presenter console
HToggle this help