A stocks and logistics engine with Python 3, SQLAlchemy, PostgreSQL and AnyBlok.
Let's be honest
What do they have in common ?
Before diving in, a bit more about motivation
history tracking: speak of stock levels
which Blok is active depends on the database
Made for this series of talks, and available at https://github.com/gracinet/awb-pyconfr-2018
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]:
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.
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 Out[4]: [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'))]
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.
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() 0
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!
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] Out[14]: [('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'))
Motivation de la séparation entre PhysObj et PhysObj.Avatar :
Motivation de la séparation entre PhysObj et PhysObj.Avatar :
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', input=Wms.PhysObj.Avatar(...), destination=Wms.PhysObj(id=4, 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'
This makes for a bit of indirection…
AWB does provide high level methods to compute stock quantities
- canceled: cancel()
- executed : execute()
- started: start()
Up to now, all we've seen is provided by the wms-core Blok. We also have:
General ideas page: https://anyblok-wms-base.readthedocs.io/en/latest/improvements.html
Lots of interesting things remain to be done:
Let's rephrase the goals I stated near the beginning
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) 81 In [26]: set((avatar.state, avatar.obj.type.code, avatar.location.code) ...: for avatar in unpack.outcomes) Out[26]: {('present', 'GR-DUST-WIND-VOL1/CRATE', 'SALLE1'), ('present', 'PALLET WOOD', 'SALLE1')}
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'] Out[28]: {'outcomes': [{'forward_properties': ['lot'], 'quantity': 80, 'required_properties': [], 'type': 'GR-DUST-WIND-VOL1/CRATE'}, {'forward_properties': [], 'quantity': 1, 'required_properties': [], 'type': 'PALLET WOOD'}]}}
Space | Forward |
---|---|
Right, Down, Page Down | Next slide |
Left, Up, Page Up | Previous slide |
G | Go to slide number |
P | Open presenter console |
H | Toggle this help |