def forward_props(self, spec, outcome): """Handle the properties for a given outcome (Goods record) :param spec: the relevant part of behaviour for this outcome :param outcome: just-created Goods instance """ packs = self.input.goods fwd_props = spec.get('forward_properties', ()) req_props = spec.get('required_properties') if req_props and not packs.properties: raise OperationInputsError( self, "Packs {inputs[0]} have no properties, yet their type {type} " "requires these for Unpack operation: {req_props}", type=packs.type, req_props=req_props) if not fwd_props: return for pname in fwd_props: pvalue = packs.get_property(pname) if pvalue is None: if pname not in req_props: continue raise OperationInputsError( self, "Packs {inputs[0]} lacks the property {prop}" "required by their type for Unpack operation", prop=pname) outcome.set_property(pname, pvalue)
def create_unpacked_goods(self, fields, spec): """Create just a record, bearing the total quantity. See also this method :meth:`in the base class <anyblok_wms_base.core.operation.Unpack.create_unpacked_goods>` TODO: introduce a behaviour (available in spec) to create as many records as specified. Even if ``wms-quantity`` is installed, it might be more efficient for some PhysObj types. Use-case: some bulk handling alongside packed goods by the unit in the same facility. """ PhysObj = self.registry.Wms.PhysObj target_qty = fields['quantity'] = spec['quantity'] * self.quantity existing_ids = spec.get('local_goods_ids') if existing_ids is not None: goods = [PhysObj.query().get(eid for eid in existing_ids)] if sum(g.quantity for g in goods) != target_qty: raise OperationInputsError( self, "final outcome specification {spec!r} has " "'local_goods_ids' parameter, but they don't provide " "the wished total quantity {target_qty} " "Detailed input: {inputs[0]!r}", spec=spec, target_qty=target_qty) return goods return [PhysObj.insert(**fields)]
def check_create_conditions(cls, state, dt_execution, inputs=None, outcome_type=None, name=None, **kwargs): super(Assembly, cls).check_create_conditions(state, dt_execution, inputs=inputs, **kwargs) behaviour = outcome_type.behaviours.get('assembly') if behaviour is None: raise OperationError( cls, "No assembly specified for type {outcome_type!r}", outcome_type=outcome_type) spec = behaviour.get(name) if spec is None: raise OperationError( cls, "No such assembly: {name!r} for type {outcome_type!r}", name=name, outcome_type=outcome_type) loc = inputs[0].location if any(inp.location != loc for inp in inputs[1:]): raise OperationInputsError( cls, "Inputs {inputs} are in different Locations: {locations!r}", inputs=inputs, # in the passing case, building a set would have been # useless overhead locations=set(inp.location for inp in inputs))
def check_create_conditions(cls, state, dt_execution, inputs=None, **kwargs): """Check that the inputs to aggregate are indeed indistinguishable. This performs the check from superclasses, and then compares all fields from :attr:`UNIFROM_AVATAR_FIELDS` on the inputs (Avatars) and :attr:`UNIFORM_GOODS_FIELDS` (on the underlying Goods). """ super(Aggregate, cls).check_create_conditions( state, dt_execution, inputs=inputs, **kwargs) first = inputs[0] first_goods = first.goods for avatar in inputs: goods = avatar.goods diff = {} # field name -> (first value, second value) for field in cls.UNIFORM_GOODS_FIELDS: if not cls.field_is_equal(field, first_goods, goods): diff[field] = (getattr(first_goods, field), getattr(goods, field)) for field in cls.UNIFORM_AVATAR_FIELDS: first_value = getattr(first, field) second_value = getattr(avatar, field) if first_value != second_value: diff[field] = (first_value, second_value) if diff: raise OperationInputsError( cls, "Can't create Aggregate with inputs {inputs} " "because of discrepancy in field {field!r}: " "Here's a mapping giving by field the differing " "values between the record with id {first.id} " "and the one with id {second.id}: {diff!r} ", inputs=inputs, field=field, first=first, second=avatar, diff=diff)
def check_create_conditions(cls, state, dt_execution, inputs=None, quantity=None, **kwargs): # TODO quantity is now irrelevant in wms-core super(Unpack, cls).check_create_conditions( state, dt_execution, inputs=inputs, quantity=quantity, **kwargs) goods_type = inputs[0].obj.type if 'unpack' not in goods_type.behaviours: raise OperationInputsError( cls, "Can't create an Unpack for {inputs} " "because their type {type} doesn't have the 'unpack' " "behaviour", inputs=inputs, type=goods_type)
def check_create_conditions(cls, state, dt_execution, inputs=None, **kwargs): """Check that the conditions are met for the creation. This is done before calling ``insert()``. In this default implementation, we check - that the number of inputs is correct, by comparing with :attr:`inputs_number`, and - that they are all in the proper state for the wished :attr:`Operation state <state>`. Subclasses are welcome to override this, and will probably want to call it back, using ``super``. """ expected = cls.inputs_number if not inputs: # to include None if expected: raise OperationMissingInputsError( cls, "The 'inputs' keyword argument must be passed to the " "create() method, and must not be empty " "got {inputs})", inputs=inputs) elif len(inputs) != expected: raise OperationInputsError( cls, "Expecting exactly {exp} inputs, got {nb} of them: " "{inputs}", exp=expected, nb=len(inputs), inputs=inputs) if state == 'done' and inputs: for record in inputs: if record.state != 'present': raise OperationInputWrongState( cls, record, 'present', prelude="Can't create in state 'done' " "for inputs {inputs}", inputs=inputs)
def check_inputs_locations(cls, inputs, **kwargs): """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. """ loc = inputs[0].location if any(inp.location != loc for inp in inputs[1:]): raise OperationInputsError( cls, "Inputs {inputs} are in different Locations: {locations!r}", inputs=inputs, # in the passing case, building a set would have been # useless overhead locations=set(inp.location for inp in inputs))
def create_unpacked_goods(self, fields, spec): """Create PhysObj record according to given specification. This singled out method is meant for easy subclassing (see, e.g, in :ref:`wms-quantity Blok <blok_wms_quantity>`). :param 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. :param 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). :return: 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. """ PhysObj = self.registry.Wms.PhysObj existing_ids = spec.get('local_physobj_ids') target_qty = spec['quantity'] if existing_ids is not None: if len(existing_ids) != target_qty: raise OperationInputsError( self, "final outcome specification {spec!r} has " "'local_physobj_ids' parameter, but they don't provide " "the wished total quantity {target_qty} " "Detailed input: {inputs[0]!r}", spec=spec, target_qty=target_qty) return [PhysObj.query().get(eid) for eid in existing_ids] return [PhysObj.insert(**fields) for _ in range(spec['quantity'])]
def get_outcome_specs(self): """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 :attr:`behaviours <anyblok_wms_base.core.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 """ # TODO PERF playing safe by performing a copy, in order not # to propagate mutability to the DB. Not sure how much of it # is necessary. packs = self.input goods_type = packs.obj.type behaviour = goods_type.get_behaviour('unpack') specs = behaviour.get('outcomes', [])[:] if behaviour.get('uniform_outcomes', False): for outcome in specs: outcome['forward_properties'] = 'clone' return specs specific_outcomes = packs.get_property(CONTENTS_PROPERTY, ()) specs.extend(specific_outcomes) if not specs: raise OperationInputsError( self, "unpacking {inputs[0]} yields no outcomes. " "Type {type} 'unpack' behaviour: {behaviour}, " "specific outcomes from PhysObj properties: " "{specific}", type=goods_type, behaviour=behaviour, specific=specific_outcomes) global_fwd = behaviour.get('forward_properties', ()) global_req = behaviour.get('required_properties', ()) for outcome in specs: if outcome.get('forward_properties') == 'clone': continue outcome.setdefault('forward_properties', []).extend(global_fwd) outcome.setdefault('required_properties', []).extend(global_req) return specs
def outcome_props_update(self, spec): """Handle the properties for a given outcome (PhysObj record) This is actually a bit more that just forwarding. :param dict spec: the relevant specification for this outcome, as produced by :meth:`get_outcome_specs` (see below for the contents). :param outcome: the just created PhysObj instance :return: the properties to update, as a :class:`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 :class:`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. """ props_upd = {} direct_props = spec.get('properties') if direct_props is not None and 'local_physobj_ids' not in spec: props_upd.update(direct_props) packs = self.input.obj fwd_props = spec.get('forward_properties', ()) req_props = spec.get('required_properties') if req_props and not packs.properties: raise OperationInputsError( self, "Packs {inputs[0]} have no properties, yet their type {type} " "requires these for Unpack operation: {req_props}", type=packs.type, req_props=req_props) if not fwd_props: return props_upd for pname in fwd_props: pvalue = packs.get_property(pname) if pvalue is None: if pname not in req_props: continue raise OperationInputsError( self, "Packs {inputs[0]} lacks the property {prop}" "required by their type for Unpack operation", prop=pname) props_upd[pname] = pvalue return props_upd
def test_op_inputs_err_cls_missing_inputs(self): with self.assertRaises(ValueError): OperationInputsError(self.Arrival, "bogus")