Example #1
0
    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)
Example #2
0
    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)]
Example #3
0
    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))
Example #4
0
    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)
Example #5
0
    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)
Example #6
0
    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)
Example #7
0
    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))
Example #8
0
    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'])]
Example #9
0
    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
Example #10
0
    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")