# -*- coding: utf-8 -*-
# This file is a part of the AnyBlok / WMS Base project
#
# Copyright (C) 2018 Georges Racinet <gracinet@anybox.fr>
#
# This Source Code Form is subject to the terms of the Mozilla Public License,
# v. 2.0. If a copy of the MPL was not distributed with this file,You can
# obtain one at http://mozilla.org/MPL/2.0/.
from sqlalchemy import CheckConstraint
from anyblok import Declarations
from anyblok.column import Decimal
from anyblok_wms_base.constants import (
SPLIT_AGGREGATE_PHYSICAL_BEHAVIOUR
)
register = Declarations.register
Model = Declarations.Model
[docs]@register(Model.Wms)
class PhysObj:
"""Override to add the :attr:`quantity` field.
"""
quantity = Decimal(label="Quantity", default=1)
"""Quantity
Depending on the PhysObj Type, this represents in reality some physical
measure (length of wire, weight of wheat) for PhysObj stored and handled
in bulk, or a number of identical items, if goods are kept as individual
pieces.
There is no corresponding idea of a unit of measure for bulk PhysObj,
as we believe it to be enough to represent it in the PhysObj Type already
(which would be, e.g, respectively a meter of wire, a ton of wheat). Note
that bulk PhysObj can be the result of some :ref:`op_unpack`, with the
packaged version
being itself handled as an individual piece (imagine spindles of 100m for
the wire example) and further packable (pallets, containers…)
This field has been defined as Decimal to cover a broad scope of
applications. However, for performance reasons, applications are
free to overload this column with other numeric types (i.e.,
supporting transparently all the usual operations with the same
syntax, both in Python and SQL).
Examples :
+ applications not caring about fractional quantities
might want to overload this column with an Integer column for
extra performance (if that really makes a difference).
+ applications having to deal with fractional quantities not well
behaved in decimal notation (e.g., thirds of cherry pies)
may want to switch to a rational number type, such as ``mpq``
type on the PostgreSQL side), although it's probably a better idea
if there's an obvious common denominator to just use integers
(following on the example, simply have PhysObj Types representing
those thirds of pies alongside those representing the whole pies,
and represent the first cutting of a slice as an
Unpack)
"""
@classmethod
def define_table_args(cls):
return super(PhysObj, cls).define_table_args() + (
CheckConstraint('quantity > 0', name='positive_qty'),
)
def __str__(self):
return ("(id={self.id}, type={self.type}, "
"quantity={self.quantity})").format(self=self)
def __repr__(self):
return ("Wms.PhysObj(id={self.id}, type={self.type!r}, "
"quantity={self.quantity!r})".format(self=self))
[docs]@register(Model.Wms.PhysObj)
class Type:
"""Override to have behavorial tests for Split/Aggregate.
As a special case, the behaviours specify whether
:class:`Split <.operation.split.Split>` and
:class:`Aggregate <.operation.aggregate.Aggregate>` Operations are physical
(represent something happening in reality), and if that's the case if they
are reversible, using ``{"reversible": true}``, defaulting to ``false``,
in the ``split`` and ``aggregate`` behaviours, respectively.
We don't want to impose reversibility to be equal for both directions,
as we don't feel confident it would be true in all use cases (it is indeed
in the ones presented below).
Reality of Split and Aggregate and their reversibilitues can be queried
using :meth:`are_split_aggregate_physical`,
:meth:`is_split_reversible` and :meth:`is_aggregate_reversible`
Use cases:
* if the represented goods come as individual pieces in reality, then all
quantities are integers, and there's no difference in reality
between N>1 records of a given PhysObj Type with quantity=1 having
identical properties and locations on one hand, and a
record with quantity=N at the same location with the same properties, on
the other hand.
* if the represented goods are meters of wiring, then Splits are physical,
they mean cutting the wires, but Aggregates probably can't happen
in reality, and therefore Splits are irreversible.
* if the represented goods are kilograms of sand, kept in bulk,
then Splits mean in reality shoveling some out of, while Aggregates mean
shoveling some in (associated with Move operations, obviously).
Both operations are certainly reversible in reality.
"""
[docs] def are_split_aggregate_physical(self):
"""Tell if Split and Aggregate operations are physical.
By default, these operations are considered to be purely formal,
but a behaviour can be set to specify otherwise. This has impact
at least on reversibility.
Downstream libraries and applications should use
:const:`SPLIT_AGGREGATE_PHYSICAL_BEHAVIOUR` as this behaviour name.
:returns bool: the answer.
"""
return self.get_behaviour(SPLIT_AGGREGATE_PHYSICAL_BEHAVIOUR, False)
def _is_op_reversible(self, op_beh):
"""Common impl for question about reversibility of some operations.
:param op_beh: name of the behaviour for the given operation
"""
if not self.are_split_aggregate_physical():
return True
split = self.get_behaviour(op_beh)
if split is None:
return False
return split.get('reversible', False)
[docs] def is_split_reversible(self):
"""Tell whether :class:`Split <.operation.split.Split>` can be reverted
for this PhysObj Type.
By default, the Split Operation is considered to be formal,
hence the result is ``True``. Otherwise, that depends on the
``reversible`` flag in the ``split`` behaviour.
:returns bool: the answer.
"""
return self._is_op_reversible('split')
[docs] def is_aggregate_reversible(self):
"""Tell whether :class:`Aggregate <.operation.aggregate.Aggregate>`
can be reverted for this PhysObj Type.
By default, Aggregate is considered to be formal, hence the result is
``True``. Otherwise, that depends on the ``reversible`` flag in the
``aggregate`` behaviour.
:returns bool: the answer.
"""
return self._is_op_reversible('aggregate')