Exemple #1
0
    def aggregate(self):
        """Return a list of aggregated amounts

    Groups amounts with same price, efficiency, reference and categories, merge
    them by summing their quantities, and return the new amounts in a new list.
    """
        from Products.ERP5Type.Document import newTempAmount
        # XXX: Do we handle rounding correctly ?
        #      What to do if only total price is rounded ??
        aggregate_dict = {}
        result_list = self.__class__()
        for amount in self:
            key = (amount.getPrice(), amount.getEfficiency(),
                   amount.getReference(), amount.categories)
            aggregate = aggregate_dict.get(key)
            if aggregate is None:
                aggregate_dict[key] = [amount, amount.getQuantity()]
            else:
                aggregate[1] += amount.getQuantity()
        for amount, quantity in aggregate_dict.itervalues():
            # Before we ignore 'quantity==0' amount here for better performance,
            # but it is not a good idea, especially when the first expand causes
            # non-zero quantity and then quantity becomes zero.
            aggregate = newTempAmount(amount.aq_parent,
                                      '',
                                      notify_workflow=False)
            aggregate.__dict__.update(amount.aq_base.__dict__)
            aggregate._setQuantity(quantity)
            if isinstance(amount, RoundingProxy):
                aggregate = amount.getPortalObject(
                ).portal_roundings.getRoundingProxy(aggregate)
            else:
                del aggregate._base
            result_list.append(aggregate)
        return result_list
  def aggregate(self):
    """Return a list of aggregated amounts

    Groups amounts with same price, efficiency, reference and categories, merge
    them by summing their quantities, and return the new amounts in a new list.
    """
    from Products.ERP5Type.Document import newTempAmount
    # XXX: Do we handle rounding correctly ?
    #      What to do if only total price is rounded ??
    aggregate_dict = {}
    result_list = self.__class__()
    for amount in self:
      key = (amount.getPrice(), amount.getEfficiency(),
             amount.getReference(), amount.categories)
      aggregate = aggregate_dict.get(key)
      if aggregate is None:
        aggregate_dict[key] = [amount, amount.getQuantity()]
      else:
        aggregate[1] += amount.getQuantity()
    for amount, quantity in aggregate_dict.itervalues():
      # Before we ignore 'quantity==0' amount here for better performance,
      # but it is not a good idea, especially when the first expand causes
      # non-zero quantity and then quantity becomes zero.
      aggregate = newTempAmount(amount.aq_parent, '', notify_workflow=False)
      aggregate.__dict__.update(amount.aq_base.__dict__)
      aggregate._setQuantity(quantity)
      if isinstance(amount, RoundingProxy):
        aggregate = amount.getPortalObject().portal_roundings.getRoundingProxy(
          aggregate)
      else:
        del aggregate._base
      result_list.append(aggregate)
    return result_list
if selection_name is not None:

    reference_variation_category_list = context.portal_selections.getSelectionParamsFor(
        selection_name)['reference_variation_category_list']
    from Products.ERP5Type.Document import newTempAmount
    tmp_context = newTempAmount(
        context,
        "temp_context",
        quantity=1.0,
        variation_category_list=reference_variation_category_list,
        resource=context.getRelativeUrl())

    aal = context.getAggregatedAmountList(tmp_context)

    result = aal.getTotalDuration()
    return result

else:
    return None
if selection_name is not None:

  reference_variation_category_list = context.portal_selections.getSelectionParamsFor(selection_name)['reference_variation_category_list']
  from Products.ERP5Type.Document import newTempAmount
  tmp_context = newTempAmount(context, "temp_context",
                              quantity=1.0,
                              variation_category_list=reference_variation_category_list,
                              resource=context.getRelativeUrl()) 
   
  aal = context.getAggregatedAmountList(tmp_context)

  result = aal.getTotalDuration()
  return result


else:
  return None
Exemple #5
0
    def test_transformedInventory(self):
        portal = self.getPortal()

        button_number = 3.0

        swimsuit = self.createResource(
            'Swimming Suit',
            self.swimsuit_variation_base_category_list,
            self.swimsuit_variation_category_list,
        )
        transformation = self.createTransformation()
        transformation.edit(title='Swimsuit Production',
                            variation_base_category_list=self.
                            swimsuit_variation_base_category_list)
        transformation.setResourceValue(swimsuit)
        self.commit()

        fabric = self.createResource(
            'Fabric',
            self.fabric_variation_base_category_list,
            self.fabric_variation_category_list,
        )
        fabric_line = self.createTransformedResource(transformation)
        fabric_line.setResourceValue(fabric)

        fabric_line.setVVariationBaseCategoryList(['colour'])
        for colour in self.colour_category_list:
            # For a blue swimming suit, we need blue fabric
            fabric_line.newCell(
                colour,
                categories=colour,
                membership_criterion_base_category=('colour', ),
                membership_criterion_category=(colour, ),
                base_id='variation')

        fabric_line.setQVariationBaseCategoryList(['size'])
        for i, size in enumerate(self.size_category_list):
            # Depending on the size, the quantity of Fabric is different.
            # arbitrarily, we fix the quantity for size s as:
            # self.size_category_list.index(s) + 1
            fabric_line.newCell(size,
                                quantity=i + 1,
                                mapped_value_property_list=('quantity', ),
                                membership_criterion_base_category=('size', ),
                                membership_criterion_category=(size, ),
                                base_id='quantity')

        button = self.createComponent()
        button.edit(
            title='Round Button',
            variation_base_category_list=self.
            button_variation_base_category_list,
        )
        button.setVariationCategoryList(self.button_variation_category_list)

        button_line = self.createTransformedResource(transformation)
        button_line.setResourceValue(button)
        button_line.setQuantity(button_number)

        button_line.setVVariationBaseCategoryList(['size'])
        for size in self.size_category_list:
            # The button used depends on the size
            button_line.newCell(size,
                                categories=size,
                                membership_criterion_base_category=('size', ),
                                membership_criterion_category=(size, ),
                                base_id='variation')

        sewing_line = transformation.newContent(
            portal_type=self.operation_line_portal_type)
        sewing_line.setResourceValue(
            portal.portal_categories.resolveCategory('operation/sewing'))

        sewing_line.setQVariationBaseCategoryList(['size', 'colour'])
        i = 1
        for size in self.size_category_list:
            for colour in self.colour_category_list:
                sewing_line.newCell(
                    size,
                    colour,
                    mapped_value_property_list=('quantity', ),
                    membership_criterion_base_category=('size', 'colour'),
                    membership_criterion_category=(size, colour),
                    quantity=i,
                    base_id='quantity')
                i += 1

        self.tic()

        self.assertEqual(
            swimsuit.getDefaultTransformationValue().getRelativeUrl(),
            transformation.getRelativeUrl())
        self.assertEqual(fabric.getDefaultTransformationValue(), None)
        self.assertEqual(button.getDefaultTransformationValue(), None)

        # Swimming Suit does not use ALL categories in Size category.
        # As a result, transformation lines should restrict their dimensions,
        # using the range induced by the resource, instead of naively
        # using the whole range directly from the variation categories.
        self.assertEqual(
            len(swimsuit.getVariationCategoryList(
                base_category_list=['size'])),
            len(fabric_line.getCellKeyList(base_id='quantity')))

        swimsuit_quantity = 4.0
        from Products.ERP5Type.Document import newTempAmount
        n = 1
        # Check that getAggregatedAmount returns the expected results, a.k.a.
        # that our Transformation is set up correctly.
        for i, size in enumerate(self.size_category_list):
            for colour in self.colour_category_list:
                # id does not matter, just make it unique
                temp_amount = newTempAmount(transformation,
                                            "foo_%s_%s" % (size, colour))
                temp_amount.edit(
                    quantity=swimsuit_quantity,
                    variation_category_list=[size, colour],
                    resource=swimsuit.getRelativeUrl(),
                )
                amount_list = transformation.getAggregatedAmountList(
                    temp_amount)

                # fabric + button + sewing
                self.assertEquals(len(amount_list), 3)
                for amount in amount_list:
                    resource = amount.getResource()
                    if resource == fabric.getRelativeUrl():
                        self.assertEquals(amount.getVariationCategoryList(),
                                          [colour])
                        self.assertEquals(amount.getQuantity(),
                                          (i + 1) * swimsuit_quantity)
                    elif resource == button.getRelativeUrl():
                        self.assertEquals(amount.getVariationCategoryList(),
                                          [size])
                        self.assertEquals(amount.getQuantity(),
                                          button_number * swimsuit_quantity)
                    elif resource == "operation/sewing":
                        self.assertEquals(amount.getQuantity(),
                                          n * swimsuit_quantity)
                    else:
                        self.fail("Invalid Resource: %s" % resource)
                n += 1

        for size in self.size_category_list:
            for colour in self.colour_category_list:
                self.makeMovement(swimsuit_quantity, swimsuit, size, colour)

        self.tic()

        inv = self.getSimulationTool().getInventoryList(
            node_uid=self.node.getUid(),
            transformed_resource=[
                fabric.getRelativeUrl(),
                button.getRelativeUrl(), "operation/sewing"
            ],
        )
        self.assertEquals(len(inv),
              len(transformation) * len(self.size_category_list) \
                * len(self.colour_category_list))

        self.assertEquals(
            len(self.getSimulationTool().getInventoryList(
                node_uid=self.node.getUid(),
                transformed_resource=[
                    fabric.getRelativeUrl(),
                    button.getRelativeUrl(), "operation/sewing"
                ],
                variation_text="something_not_existing",
            )), 0)

        n = 1
        for i, size in enumerate(self.size_category_list):
            for colour in self.colour_category_list:
                variation_text = '\n'.join([colour, size])
                inv = self.getSimulationTool().getInventoryList(
                    node_uid=self.mirror_node.getUid(),
                    transformed_resource=[
                        fabric.getRelativeUrl(),
                        button.getRelativeUrl(), "operation/sewing"
                    ],
                    variation_text=variation_text,
                )
                self.assertEquals(len(inv), len(transformation))
                for line in inv:
                    self.assertEquals(line.getVariationText(), variation_text)
                    self.assertEquals(line.getResource(),
                                      swimsuit.getRelativeUrl())
                    transformed_resource = line.transformed_resource_relative_url
                    if transformed_resource == fabric.getRelativeUrl():
                        self.assertEquals(line.transformed_variation_text,
                                          colour)
                        self.assertEquals(line.total_quantity,
                                          (i + 1) * swimsuit_quantity)
                    elif transformed_resource == button.getRelativeUrl():
                        self.assertEquals(line.transformed_variation_text,
                                          size)
                        self.assertEquals(line.total_quantity,
                                          button_number * swimsuit_quantity)
                    elif transformed_resource == "operation/sewing":
                        self.assertEquals(line.total_quantity,
                                          n * swimsuit_quantity)
                        self.assertEquals(line.transformed_variation_text, "")
                    else:
                        self.fail("Invalid Transformed Resource: %s" %
                                  transformed_resource)
                n += 1
    def getGeneratedAmountList(self,
                               amount_list=None,
                               rounding=False,
                               amount_generator_type_list=None,
                               generate_empty_amounts=True):
        """
    Implementation of a generic transformation algorithm which is
    applicable to payroll, tax generation and BOMs. Return the
    list of amounts without any aggregation.

    TODO:
    - is rounding really well supported (ie. before and after aggregation)
      very likely not - proxying before or after must be decided
    """
        # It is the only place where we can import this
        from Products.ERP5Type.Document import newTempAmount
        portal = self.getPortalObject()
        getRoundingProxy = portal.portal_roundings.getRoundingProxy
        amount_generator_line_type_list = \
          portal.getPortalAmountGeneratorLineTypeList()
        amount_generator_cell_type_list = \
          portal.getPortalAmountGeneratorCellTypeList()

        # Set empty result by default
        result = AggregatedAmountList()

        args = (getTransactionalVariable().setdefault(
            "amount_generator.BaseAmountDict", {}), dict(rounding=rounding))
        # If amount_list is None, then try to collect amount_list from
        # the current context
        default_target = None
        if amount_list is None:
            if self.providesIMovementCollection():
                default_target = 'isMovement'
                base_amount_list = BaseAmountDict(*args).__of__(self) \
                  .recurseMovementList(self.getMovementList())
            elif self.providesIAmount():
                base_amount_list = BaseAmountDict(*args).__of__(self),
            elif self.providesIAmountList():
                base_amount_list = (BaseAmountDict(*args).__of__(amount)
                                    for amount in self)
            else:
                raise ValueError(
                    "%r must implement IMovementCollection, IAmount or"
                    " IAmountList" % self)
        else:
            base_amount_list = (BaseAmountDict(*args).__of__(amount)
                                for amount in amount_list)

        def getLineSortKey(line):
            int_index = line.getIntIndex()
            return (line.getFloatIndex() if int_index is None else int_index,
                    random.random())

        is_mapped_value = isinstance(self, MappedValue)
        recurse_queue = deque()
        resolver = BaseAmountResolver(*args)

        for base_amount in base_amount_list:
            delivery_amount = base_amount.getObject()
            recurse_queue.append(
                self if is_mapped_value else delivery_amount.
                asComposedDocument(amount_generator_type_list))
            property_dict_list = []
            # If several amount generator lines have same reference, the first
            # (sorted by int_index or float_index) matching one will mask the others.
            reference_set = set()
            while recurse_queue:
                self = recurse_queue.popleft()
                amount_generator_line_list = self.objectValues(
                    portal_type=amount_generator_line_type_list)
                # Recursively feed base_amount
                if amount_generator_line_list:
                    # First sort so that a line can mask other of same reference.
                    # We will sort again later to satisfy dependencies between
                    # base_application & base_contribution.
                    amount_generator_line_list.sort(key=getLineSortKey)
                    recurse_queue += amount_generator_line_list
                    continue
                if self.getPortalType() not in amount_generator_line_type_list:
                    continue
                target_method = 'isDelivery' if self.isTargetDelivery() \
                  else default_target
                if target_method and not getattr(delivery_amount,
                                                 target_method)():
                    continue
                if not self.test(delivery_amount):
                    continue
                self = self.asPredicate()
                reference = self.getReference()
                if reference:
                    if reference in reference_set:
                        continue
                    reference_set.add(reference)
                # Try to collect cells and aggregate their mapped properties
                # using resource + variation as aggregation key or base_application
                # for intermediate lines.
                amount_generator_cell_list = [self] + self.objectValues(
                    portal_type=amount_generator_cell_type_list)
                cell_aggregate = {}  # aggregates final line information

                base_application_list = self.getBaseApplicationList()
                base_contribution_list = self.getBaseContributionList()
                for cell in amount_generator_cell_list:
                    if cell is not self:
                        if not cell.test(delivery_amount):
                            continue
                        cell = cell.asPredicate()
                    aggregate_key = cell.getCellAggregateKey()
                    try:
                        property_dict = cell_aggregate[aggregate_key]
                    except KeyError:
                        cell_aggregate[aggregate_key] = property_dict = {
                            None: self,
                            'base_application_set': set(base_application_list),
                            'base_contribution_set':
                            set(base_contribution_list),
                            'category_list': [],
                            'causality_value_list': [],
                            'efficiency': self.getEfficiency(),
                            'quantity_unit': self.getQuantityUnit(),
                            # The trade model rule often matches by reference and fails if
                            # getAggregatedAmountList returns amounts with same reference.
                            'reference': cell.getReference() or reference,
                        }
                    # Then collect the mapped values (quantity, price, trade_phase...)
                    for key in cell.getMappedValuePropertyList():
                        if key in ('net_converted_quantity', 'net_quantity',
                                   'converted_quantity'):
                            # XXX only 'quantity' is accepted and it is treated
                            #     as if it was 'converted_quantity'
                            raise NotImplementedError
                        # XXX-JPS Make sure handling of list properties can be handled
                        property_dict[key] = cell.getProperty(key)
                    category_list = cell.getAcquiredCategoryMembershipList(
                        cell.getMappedValueBaseCategoryList(), base=1)
                    property_dict['category_list'] += category_list
                    property_dict['resource'] = cell.getResource()
                    if cell is self:
                        self_key = aggregate_key
                    else:
                        # cells inherit base_application and base_contribution from line
                        property_dict['base_application_set'].update(
                            cell.getBaseApplicationList())
                        property_dict['base_contribution_set'].update(
                            cell.getBaseContributionList())
                    property_dict['causality_value_list'].append(cell)

                # Ignore line (i.e. self) if cells produce unrelated amounts.
                # With Transformed Resource (Transformation), line is considered in
                # order to gather common properties and cells are used to describe
                # variated properties: only 1 amount is produced.
                # In cases like trade, payroll or assorted resources,
                # we want to ignore the line if they are cells.
                # See also implementations of 'getCellAggregateKey'
                if len(cell_aggregate) > 1 and \
                   len(cell_aggregate[self_key]['causality_value_list']) == 1:
                    del cell_aggregate[self_key]

                # Allow base_application & base_contribution to be variated.
                for property_dict in cell_aggregate.itervalues():
                    base_amount_set = property_dict['base_application_set']
                    variation_list = tuple(
                        sorted(x for x in base_amount_set
                               if not x.startswith('base_amount/')))
                    base_amount_set.difference_update(variation_list)
                    # Before we ignored 'quantity=0' amount here for better performance,
                    # but it makes expand unstable (e.g. when the first expand causes
                    # non-zero quantity and then quantity becomes zero).
                    # Ignore only if there's no base_application.
                    if not base_amount_set:
                        continue
                    property_dict['_application'] = [(x, variation_list)
                                                     for x in base_amount_set]
                    base_amount_set = property_dict['base_contribution_set']
                    variation_list = tuple(
                        sorted(x for x in base_amount_set
                               if not x.startswith('base_amount/')))
                    property_dict['_contribution'] = [
                        (x, variation_list)
                        for x in base_amount_set.difference(variation_list)
                    ]
                    property_dict_list.append(property_dict)

            # Sort amount generators according to
            # base_application & base_contribution dependencies.
            resolver(delivery_amount, property_dict_list)

            # Accumulate applicable values.
            for property_dict in property_dict_list:
                self = property_dict.pop(None)
                base_amount.setAmountGeneratorLine(self)
                contribution_list = property_dict.pop('_contribution')
                # property_dict may include
                #   resource - VAT service or a Component in MRP
                #              (if unset, the amount will only be used for reporting)
                #   variation params - color, size, employer share, etc.
                #   one of (net_)(converted_)quantity - used as a multiplier
                #     -> in MRP, quantity in component
                #     -> for trade, it provides a way to configure a fixed quantity
                #   price -  empty (like in Transformation) price of a product
                #            (ex. a Stamp) or tax ratio (ie. price per value units)
                #   base_contribution_list - needed to produce reports with
                #                            getTotalPrice
                # 'efficiency' is stored separately in the generated amount,
                # for future simulation of efficiencies.
                # If no quantity is provided, we consider that the value is 1.0
                # (XXX is it OK ?) XXX-JPS Need careful review with taxes
                quantity = float(
                    sum(
                        base_amount.getGeneratedAmountQuantity(*x)
                        for x in property_dict.pop('_application')))
                for key in 'quantity', 'price', 'efficiency':
                    if property_dict.get(key, 0) in (None, ''):
                        del property_dict[key]
                quantity *= property_dict.pop('quantity', 1)

                # Backward compatibility
                if getattr(self.aq_base, 'create_line', None) == 0:
                    property_dict['resource'] = None
                # Create an Amount object
                amount = newTempAmount(
                    portal,
                    # we only want the id to be unique so we pick a random causality
                    property_dict['causality_value_list']
                    [-1].getRelativeUrl().replace('/', '_'),
                    notify_workflow=False)
                amount._setCategoryList(property_dict.pop('category_list', ()))
                if amount.getQuantityUnit():
                    del property_dict['quantity_unit']
                amount._edit(
                    quantity=quantity,
                    # XXX Are title, int_index and description useful ??
                    title=self.getTitle(),
                    int_index=self.getIntIndex(),
                    description=self.getDescription(),
                    **property_dict)
                # convert to default management unit if possible
                amount._setQuantity(amount.getConvertedQuantity())
                amount._setQuantityUnit(
                    amount.getResourceDefaultQuantityUnit())
                if rounding:
                    # We hope here that rounding is sufficient at line level
                    amount = getRoundingProxy(amount, context=self)
                result.append(amount)
                # Contribute
                quantity *= property_dict.get('price', 1)
                try:
                    quantity /= property_dict.get('efficiency', 1)
                except ZeroDivisionError:
                    quantity *= float('inf')
                for base_contribution, variation_category_list in contribution_list:
                    base_amount.contribute(base_contribution,
                                           variation_category_list, quantity)

        return result
Exemple #7
0
  def test_transformedInventory(self):
    portal = self.getPortal()

    button_number = 3.0

    swimsuit = self.createResource(
        'Swimming Suit',
        self.swimsuit_variation_base_category_list,
        self.swimsuit_variation_category_list,
    )
    transformation = self.createTransformation()
    transformation.edit(
        title = 'Swimsuit Production',
        variation_base_category_list = self.swimsuit_variation_base_category_list
    )
    transformation.setResourceValue(swimsuit)
    transaction.commit()

    fabric = self.createResource(
        'Fabric',
        self.fabric_variation_base_category_list,
        self.fabric_variation_category_list,
    )
    fabric_line = self.createTransformedResource(transformation)
    fabric_line.setResourceValue(fabric)

    fabric_line.setVVariationBaseCategoryList(['colour'])
    for colour in self.colour_category_list:
      # For a blue swimming suit, we need blue fabric
      fabric_line.newCell(colour,
                          categories = colour,
                          membership_criterion_base_category= ('colour',),
                          membership_criterion_category= (colour,),
                          base_id = 'variation')

    fabric_line.setQVariationBaseCategoryList(['size'])
    for i, size in enumerate(self.size_category_list):
      # Depending on the size, the quantity of Fabric is different.
      # arbitrarily, we fix the quantity for size s as:
      # self.size_category_list.index(s) + 1
      fabric_line.newCell(size,
                          quantity = i+1,
                          mapped_value_property_list = ('quantity',),
                          membership_criterion_base_category= ('size',),
                          membership_criterion_category= (size,),
                          base_id = 'quantity')

    button = self.createComponent()
    button.edit(
      title = 'Round Button',
      variation_base_category_list = self.button_variation_base_category_list,
    )
    button.setVariationCategoryList(self.button_variation_category_list)

    button_line = self.createTransformedResource(transformation)
    button_line.setResourceValue(button)
    button_line.setQuantity(button_number)

    button_line.setVVariationBaseCategoryList(['size'])
    for size in self.size_category_list:
      # The button used depends on the size
      button_line.newCell(size,
                          categories = size,
                          membership_criterion_base_category= ('size',),
                          membership_criterion_category= (size,),
                          base_id = 'variation')

    sewing_line = transformation.newContent(
        portal_type = self.operation_line_portal_type)
    sewing_line.setResourceValue(
        portal.portal_categories.resolveCategory('operation/sewing'))

    sewing_line.setQVariationBaseCategoryList(['size', 'colour'])
    i = 1
    for size in self.size_category_list:
      for colour in self.colour_category_list:
        sewing_line.newCell(size,
                            colour,
                            mapped_value_property_list = ('quantity',),
                            membership_criterion_base_category= ('size', 'colour'),
                            membership_criterion_category= (size, colour),
                            quantity = i,
                            base_id = 'quantity')
        i += 1

    transaction.commit()
    self.tic()

    self.assertEqual(swimsuit.getDefaultTransformationValue().getRelativeUrl(),
                     transformation.getRelativeUrl())
    self.assertEqual(fabric.getDefaultTransformationValue(), None)
    self.assertEqual(button.getDefaultTransformationValue(), None)

    # Swimming Suit does not use ALL categories in Size category.
    # As a result, transformation lines should restrict their dimensions,
    # using the range induced by the resource, instead of naively
    # using the whole range directly from the variation categories.
    self.assertEqual(
        len(swimsuit.getVariationCategoryList(base_category_list=['size'])),
        len(fabric_line.getCellKeyList(base_id='quantity'))
    )

    swimsuit_quantity = 4.0
    from Products.ERP5Type.Document import newTempAmount
    n = 1
    # Check that getAggregatedAmount returns the expected results, a.k.a.
    # that our Transformation is set up correctly.
    for i, size in enumerate(self.size_category_list):
      for colour in self.colour_category_list:
        # id does not matter, just make it unique
        temp_amount = newTempAmount(transformation, "foo_%s_%s" % (size, colour))
        temp_amount.edit(
            quantity = swimsuit_quantity,
            variation_category_list = [size, colour],
            resource = swimsuit.getRelativeUrl(),
        )
        amount_list = transformation.getAggregatedAmountList(temp_amount)

        # fabric + button + sewing
        self.assertEquals(len(amount_list), 3)
        for amount in amount_list:
          resource = amount.getResource()
          if resource == fabric.getRelativeUrl():
            self.assertEquals(amount.getVariationCategoryList(), [colour])
            self.assertEquals(amount.getQuantity(), (i+1)*swimsuit_quantity)
          elif resource == button.getRelativeUrl():
            self.assertEquals(amount.getVariationCategoryList(), [size])
            self.assertEquals(amount.getQuantity(), button_number*swimsuit_quantity)
          elif resource == "operation/sewing":
            self.assertEquals(amount.getQuantity(), n*swimsuit_quantity)
          else:
            self.fail("Invalid Resource: %s" % resource)
        n += 1


    for size in self.size_category_list:
      for colour in self.colour_category_list:
        self.makeMovement(swimsuit_quantity, swimsuit, size, colour)

    transaction.commit()
    self.tic()

    inv = self.getSimulationTool().getInventoryList(
            node_uid=self.node.getUid(),
            transformed_resource=[fabric.getRelativeUrl(),
                                  button.getRelativeUrl(),
                                  "operation/sewing"],
            )
    self.assertEquals(len(inv),
          len(transformation) * len(self.size_category_list) \
            * len(self.colour_category_list))

    self.assertEquals(len(self.getSimulationTool().getInventoryList(
            node_uid=self.node.getUid(),
            transformed_resource=[fabric.getRelativeUrl(),
                                  button.getRelativeUrl(),
                                  "operation/sewing"],
            variation_text="something_not_existing",
            )), 0)

    n = 1
    for i, size in enumerate(self.size_category_list):
      for colour in self.colour_category_list:
        variation_text = '\n'.join([colour, size])
        inv = self.getSimulationTool().getInventoryList(
                node_uid=self.mirror_node.getUid(),
                transformed_resource=[fabric.getRelativeUrl(),
                                      button.getRelativeUrl(),
                                      "operation/sewing"],
                variation_text=variation_text,
              )
        self.assertEquals(len(inv), len(transformation))
        for line in inv:
          self.assertEquals(line.getVariationText(), variation_text)
          self.assertEquals(line.getResource(), swimsuit.getRelativeUrl())
          transformed_resource = line.transformed_resource_relative_url
          if transformed_resource == fabric.getRelativeUrl():
            self.assertEquals(line.transformed_variation_text, colour)
            self.assertEquals(line.total_quantity, (i+1)*swimsuit_quantity)
          elif transformed_resource == button.getRelativeUrl():
            self.assertEquals(line.transformed_variation_text, size)
            self.assertEquals(line.total_quantity, button_number*swimsuit_quantity)
          elif transformed_resource == "operation/sewing":
            self.assertEquals(line.total_quantity, n*swimsuit_quantity)
            self.assertEquals(line.transformed_variation_text, "")
          else:
            self.fail("Invalid Transformed Resource: %s" % transformed_resource)
        n += 1
Exemple #8
0
  def getGeneratedAmountList(self, amount_list=None, rounding=False,
                             amount_generator_type_list=None,
                             generate_empty_amounts=True):
    """
    Implementation of a generic transformation algorithm which is
    applicable to payroll, tax generation and BOMs. Return the
    list of amounts without any aggregation.

    TODO:
    - is rounding really well supported (ie. before and after aggregation)
      very likely not - proxying before or after must be decided
    """
    # It is the only place where we can import this
    from Products.ERP5Type.Document import newTempAmount
    portal = self.getPortalObject()
    getRoundingProxy = portal.portal_roundings.getRoundingProxy
    amount_generator_line_type_list = \
      portal.getPortalAmountGeneratorLineTypeList()
    amount_generator_cell_type_list = \
      portal.getPortalAmountGeneratorCellTypeList()

    # Set empty result by default
    result = AggregatedAmountList()

    args = (getTransactionalVariable().setdefault(
              "amount_generator.BaseAmountDict", {}),
            dict(rounding=rounding))
    # If amount_list is None, then try to collect amount_list from
    # the current context
    default_target = None
    if amount_list is None:
      if self.providesIMovementCollection():
        default_target = 'isMovement'
        base_amount_list = BaseAmountDict(*args).__of__(self) \
          .recurseMovementList(self.getMovementList())
      elif self.providesIAmount():
        base_amount_list = BaseAmountDict(*args).__of__(self),
      elif self.providesIAmountList():
        base_amount_list = (BaseAmountDict(*args).__of__(amount)
                            for amount in self)
      else:
        raise ValueError("%r must implement IMovementCollection, IAmount or"
                         " IAmountList" % self)
    else:
      base_amount_list = (BaseAmountDict(*args).__of__(amount)
                          for amount in amount_list)

    def getLineSortKey(line):
      int_index = line.getIntIndex()
      return (line.getFloatIndex() if int_index is None else int_index,
              random.random())

    is_mapped_value = isinstance(self, MappedValue)
    recurse_queue = deque()
    resolver = BaseAmountResolver(*args)

    for base_amount in base_amount_list:
      delivery_amount = base_amount.getObject()
      recurse_queue.append(self if is_mapped_value
        else delivery_amount.asComposedDocument(amount_generator_type_list))
      property_dict_list = []
      # If several amount generator lines have same reference, the first
      # (sorted by int_index or float_index) matching one will mask the others.
      reference_set = set()
      while recurse_queue:
        self = recurse_queue.popleft()
        amount_generator_line_list = self.objectValues(
          portal_type=amount_generator_line_type_list)
        # Recursively feed base_amount
        if amount_generator_line_list:
          # First sort so that a line can mask other of same reference.
          # We will sort again later to satisfy dependencies between
          # base_application & base_contribution.
          amount_generator_line_list.sort(key=getLineSortKey)
          recurse_queue += amount_generator_line_list
          continue
        if self.getPortalType() not in amount_generator_line_type_list:
          continue
        target_method = 'isDelivery' if self.isTargetDelivery() \
          else default_target
        if target_method and not getattr(delivery_amount, target_method)():
          continue
        if not self.test(delivery_amount):
          continue
        self = self.asPredicate()
        reference = self.getReference()
        if reference:
          if reference in reference_set:
            continue
          reference_set.add(reference)
        # Try to collect cells and aggregate their mapped properties
        # using resource + variation as aggregation key or base_application
        # for intermediate lines.
        amount_generator_cell_list = [self] + self.objectValues(
          portal_type=amount_generator_cell_type_list)
        cell_aggregate = {} # aggregates final line information

        base_application_list = self.getBaseApplicationList()
        base_contribution_list = self.getBaseContributionList()
        for cell in amount_generator_cell_list:
          if cell is not self:
            if not cell.test(delivery_amount):
              continue
            cell = cell.asPredicate()
          aggregate_key = cell.getCellAggregateKey()
          try:
            property_dict = cell_aggregate[aggregate_key]
          except KeyError:
            cell_aggregate[aggregate_key] = property_dict = {
              None: self,
              'base_application_set': set(base_application_list),
              'base_contribution_set': set(base_contribution_list),
              'category_list': [],
              'causality_value_list': [],
              'efficiency': self.getEfficiency(),
              'quantity_unit': self.getQuantityUnit(),
              # XXX If they are several cells, we have duplicate references.
              'reference': reference,
            }
          # Then collect the mapped values (quantity, price, trade_phase...)
          for key in cell.getMappedValuePropertyList():
            if key in ('net_converted_quantity',
                       'net_quantity', 'converted_quantity'):
              # XXX only 'quantity' is accepted and it is treated
              #     as if it was 'converted_quantity'
              raise NotImplementedError
            # XXX-JPS Make sure handling of list properties can be handled
            property_dict[key] = cell.getProperty(key)
          category_list = cell.getAcquiredCategoryMembershipList(
            cell.getMappedValueBaseCategoryList(), base=1)
          property_dict['category_list'] += category_list
          property_dict['resource'] = cell.getResource()
          if cell is self:
            self_key = aggregate_key
          else:
            # cells inherit base_application and base_contribution from line
            property_dict['base_application_set'].update(
              cell.getBaseApplicationList())
            property_dict['base_contribution_set'].update(
              cell.getBaseContributionList())
          property_dict['causality_value_list'].append(cell)

        # Ignore line (i.e. self) if cells produce unrelated amounts.
        # With Transformed Resource (Transformation), line is considered in
        # order to gather common properties and cells are used to describe
        # variated properties: only 1 amount is produced.
        # In cases like trade, payroll or assorted resources,
        # we want to ignore the line if they are cells.
        # See also implementations of 'getCellAggregateKey'
        if len(cell_aggregate) > 1 and \
           len(cell_aggregate[self_key]['causality_value_list']) == 1:
          del cell_aggregate[self_key]

        # Allow base_application & base_contribution to be variated.
        for property_dict in cell_aggregate.itervalues():
          base_amount_set = property_dict['base_application_set']
          variation_list = tuple(sorted(x for x in base_amount_set
                                          if not x.startswith('base_amount/')))
          base_amount_set.difference_update(variation_list)
          # Before we ignored 'quantity=0' amount here for better performance,
          # but it makes expand unstable (e.g. when the first expand causes
          # non-zero quantity and then quantity becomes zero).
          # Ignore only if there's no base_application.
          if not base_amount_set:
            continue
          property_dict['_application'] = [(x, variation_list)
            for x in base_amount_set]
          base_amount_set = property_dict['base_contribution_set']
          variation_list = tuple(sorted(x for x in base_amount_set
                                          if not x.startswith('base_amount/')))
          property_dict['_contribution'] = [(x, variation_list)
            for x in base_amount_set.difference(variation_list)]
          property_dict_list.append(property_dict)

      # Sort amount generators according to
      # base_application & base_contribution dependencies.
      resolver(delivery_amount, property_dict_list)

      # Accumulate applicable values.
      for property_dict in property_dict_list:
        self = property_dict.pop(None)
        base_amount.setAmountGeneratorLine(self)
        contribution_list = property_dict.pop('_contribution')
        # property_dict may include
        #   resource - VAT service or a Component in MRP
        #              (if unset, the amount will only be used for reporting)
        #   variation params - color, size, employer share, etc.
        #   one of (net_)(converted_)quantity - used as a multiplier
        #     -> in MRP, quantity in component
        #     -> for trade, it provides a way to configure a fixed quantity
        #   price -  empty (like in Transformation) price of a product
        #            (ex. a Stamp) or tax ratio (ie. price per value units)
        #   base_contribution_list - needed to produce reports with
        #                            getTotalPrice
        # 'efficiency' is stored separately in the generated amount,
        # for future simulation of efficiencies.
        # If no quantity is provided, we consider that the value is 1.0
        # (XXX is it OK ?) XXX-JPS Need careful review with taxes
        quantity = float(sum(base_amount.getGeneratedAmountQuantity(*x)
                             for x in property_dict.pop('_application')))
        for key in 'quantity', 'price', 'efficiency':
          if property_dict.get(key, 0) in (None, ''):
            del property_dict[key]
        quantity *= property_dict.pop('quantity', 1)

        # Backward compatibility
        if getattr(self.aq_base, 'create_line', None) == 0:
          property_dict['resource'] = None
        # Create an Amount object
        amount = newTempAmount(portal,
          # we only want the id to be unique so we pick a random causality
          property_dict['causality_value_list'][-1]
            .getRelativeUrl().replace('/', '_'),
          notify_workflow=False)
        amount._setCategoryList(property_dict.pop('category_list', ()))
        if amount.getQuantityUnit():
          del property_dict['quantity_unit']
        amount._edit(
          quantity=quantity,
          # XXX Are title, int_index and description useful ??
          title=self.getTitle(),
          int_index=self.getIntIndex(),
          description=self.getDescription(),
          **property_dict)
        # convert to default management unit if possible
        amount._setQuantity(amount.getConvertedQuantity())
        amount._setQuantityUnit(amount.getResourceDefaultQuantityUnit())
        if rounding:
          # We hope here that rounding is sufficient at line level
          amount = getRoundingProxy(amount, context=self)
        result.append(amount)
        # Contribute
        quantity *= property_dict.get('price', 1)
        try:
          quantity /= property_dict.get('efficiency', 1)
        except ZeroDivisionError:
          quantity *= float('inf')
        for base_contribution, variation_category_list in contribution_list:
          base_amount.contribute(base_contribution, variation_category_list,
                                 quantity)

    return result
Exemple #9
0
        def accumulateAmountList(self):
            amount_generator_line_list = self.contentValues(
                portal_type=amount_generator_line_type_list)
            # Recursively feed base_amount
            if amount_generator_line_list:
                amount_generator_line_list.sort(
                    key=lambda x: (x.getIntIndex(), random.random()))
                for amount_generator_line in amount_generator_line_list:
                    accumulateAmountList(amount_generator_line)
                return
            elif (self.getPortalType() not in amount_generator_line_type_list):
                return
            target_method = self.isTargetDelivery(
            ) and 'isDelivery' or default_target
            if target_method and not getattr(delivery_amount, target_method)():
                return
            # Try to collect cells and aggregate their mapped properties
            # using resource + variation as aggregation key or base_application
            # for intermediate lines.
            amount_generator_cell_list = [self] + self.contentValues(
                portal_type=amount_generator_cell_type_list)
            cell_aggregate = {}  # aggregates final line information

            base_application_list = self.getBaseApplicationList()
            base_contribution_list = self.getBaseContributionList()
            for cell in amount_generator_cell_list:
                if not cell.test(delivery_amount):
                    if cell is self:
                        return
                    continue
                key = cell.getCellAggregateKey()
                try:
                    property_dict = cell_aggregate[key]
                except KeyError:
                    cell_aggregate[key] = property_dict = {
                        'base_application_set': set(base_application_list),
                        'base_contribution_set': set(base_contribution_list),
                        'category_list': [],
                        'causality_value_list': [],
                        'efficiency': self.getEfficiency(),
                        'quantity_unit': self.getQuantityUnit(),
                        # XXX If they are several cells, we have duplicate references.
                        'reference': self.getReference(),
                    }
                # Then collect the mapped values (quantity, price, trade_phase...)
                for key in cell.getMappedValuePropertyList():
                    if key in ('net_converted_quantity', 'net_quantity',
                               'converted_quantity'):
                        # XXX only 'quantity' is accepted and it is treated
                        #     as if it was 'converted_quantity'
                        raise NotImplementedError
                    # XXX-JPS Make sure handling of list properties can be handled
                    property_dict[key] = cell.getProperty(key)
                category_list = cell.getAcquiredCategoryMembershipList(
                    cell.getMappedValueBaseCategoryList(), base=1)
                property_dict['category_list'] += category_list
                property_dict['resource'] = cell.getResource()
                if cell is not self:
                    # cells inherit base_application and base_contribution from line
                    property_dict['base_application_set'].update(
                        cell.getBaseApplicationList())
                    property_dict['base_contribution_set'].update(
                        cell.getBaseContributionList())
                property_dict['causality_value_list'].append(cell)

            base_amount.setAmountGeneratorLine(self)
            for property_dict in cell_aggregate.itervalues():
                # Ignore line (i.e. self) if cells produce unrelated amounts.
                # With Transformed Resource (Transformation), line is considered in
                # order to gather common properties and cells are used to describe
                # varianted properties: only 1 amount is produced.
                # In cases like trade, payroll or assorted resources,
                # we want to ignore the line if they are cells.
                # See also implementations of 'getCellAggregateKey'
                causality_value = property_dict['causality_value_list'][-1]
                if causality_value is self and len(cell_aggregate) > 1:
                    continue
                base_application_set = property_dict['base_application_set']
                # allow a single base_application to be variated
                variation_category_list = tuple(
                    sorted([
                        x for x in base_application_set
                        if x[:12] != 'base_amount/'
                    ]))
                if variation_category_list:
                    base_application_set.difference_update(
                        variation_category_list)
                # property_dict may include
                #   resource - VAT service or a Component in MRP
                #              (if unset, the amount will only be used for reporting)
                #   variation params - color, size, employer share, etc.
                #   one of (net_)(converted_)quantity - used as a multiplier
                #     -> in MRP, quantity in component
                #     -> for trade, it provides a way to configure a fixed quantity
                #   price -  empty (like in Transformation) price of a product
                #            (ex. a Stamp) or tax ratio (ie. price per value units)
                #   base_contribution_list - needed to produce reports with
                #                            getTotalPrice
                # 'efficiency' is stored separately in the generated amount,
                # for future simulation of efficiencies.
                # If no quantity is provided, we consider that the value is 1.0
                # (XXX is it OK ?) XXX-JPS Need careful review with taxes
                quantity = float(
                    sum(
                        base_amount.getGeneratedAmountQuantity(
                            base_application, variation_category_list)
                        for base_application in base_application_set))
                for key in 'quantity', 'price', 'efficiency':
                    if property_dict.get(key, 0) in (None, ''):
                        del property_dict[key]
                quantity *= property_dict.pop('quantity', 1)

                # Before we ignore 'quantity==0' amount here for better
                # performance, but it is not a good idea, especially when the
                # first expand causes non-zero quantity and then quantity
                # becomes zero.
                # if not (quantity or generate_empty_amounts):
                #   continue

                # Backward compatibility
                if getattr(self.aq_base, 'create_line', None) == 0:
                    property_dict['resource'] = None
                # Create an Amount object
                amount = newTempAmount(
                    portal,
                    # we only want the id to be unique so we pick a random causality
                    causality_value.getRelativeUrl().replace('/', '_'),
                    notify_workflow=False)
                amount._setCategoryList(property_dict.pop('category_list', ()))
                if amount.getQuantityUnit():
                    del property_dict['quantity_unit']
                amount._edit(
                    quantity=quantity,
                    # XXX Are title, int_index and description useful ??
                    title=self.getTitle(),
                    int_index=self.getIntIndex(),
                    description=self.getDescription(),
                    **property_dict)
                # convert to default management unit if possible
                amount._setQuantity(amount.getConvertedQuantity())
                amount._setQuantityUnit(
                    amount.getResourceDefaultQuantityUnit())
                if rounding:
                    # We hope here that rounding is sufficient at line level
                    amount = getRoundingProxy(amount, context=self)
                result.append(amount)
                # Contribute
                quantity *= property_dict.get('price', 1)
                try:
                    quantity /= property_dict.get('efficiency', 1)
                except ZeroDivisionError:
                    quantity *= float('inf')
                base_contribution_set = property_dict['base_contribution_set']
                # allow a single base_contribution to be variated
                variation_category_list = tuple(
                    sorted([
                        x for x in base_contribution_set
                        if x[:12] != 'base_amount/'
                    ]))
                if variation_category_list:
                    base_contribution_set.difference_update(
                        variation_category_list)
                for base_contribution in base_contribution_set:
                    base_amount.contribute(base_contribution,
                                           variation_category_list, quantity)
Exemple #10
0
    def accumulateAmountList(self):
      amount_generator_line_list = self.contentValues(
        portal_type=amount_generator_line_type_list)
      # Recursively feed base_amount
      if amount_generator_line_list:
        amount_generator_line_list.sort(key=lambda x: (x.getIntIndex(),
                                                       random.random()))
        for amount_generator_line in amount_generator_line_list:
          accumulateAmountList(amount_generator_line)
        return
      elif (self.getPortalType() not in amount_generator_line_type_list):
        return
      target_method = self.isTargetDelivery() and 'isDelivery' or default_target
      if target_method and not getattr(delivery_amount, target_method)():
        return
      # Try to collect cells and aggregate their mapped properties
      # using resource + variation as aggregation key or base_application
      # for intermediate lines.
      amount_generator_cell_list = [self] + self.contentValues(
        portal_type=amount_generator_cell_type_list)
      cell_aggregate = {} # aggregates final line information

      base_application_list = self.getBaseApplicationList()
      base_contribution_list = self.getBaseContributionList()
      for cell in amount_generator_cell_list:
        if not cell.test(delivery_amount):
          if cell is self:
            return
          continue
        key = cell.getCellAggregateKey()
        try:
          property_dict = cell_aggregate[key]
        except KeyError:
          cell_aggregate[key] = property_dict = {
            'base_application_set': set(base_application_list),
            'base_contribution_set': set(base_contribution_list),
            'category_list': [],
            'causality_value_list': [],
            'efficiency': self.getEfficiency(),
            'quantity_unit': self.getQuantityUnit(),
            # XXX If they are several cells, we have duplicate references.
            'reference': self.getReference(),
          }
        # Then collect the mapped values (quantity, price, trade_phase...)
        for key in cell.getMappedValuePropertyList():
          if key in ('net_converted_quantity',
                     'net_quantity', 'converted_quantity'):
            # XXX only 'quantity' is accepted and it is treated
            #     as if it was 'converted_quantity'
            raise NotImplementedError
          # XXX-JPS Make sure handling of list properties can be handled
          property_dict[key] = cell.getProperty(key)
        category_list = cell.getAcquiredCategoryMembershipList(
          cell.getMappedValueBaseCategoryList(), base=1)
        property_dict['category_list'] += category_list
        property_dict['resource'] = cell.getResource()
        if cell is not self:
          # cells inherit base_application and base_contribution from line
          property_dict['base_application_set'].update(
            cell.getBaseApplicationList())
          property_dict['base_contribution_set'].update(
            cell.getBaseContributionList())
        property_dict['causality_value_list'].append(cell)

      base_amount.setAmountGeneratorLine(self)
      for property_dict in cell_aggregate.itervalues():
        # Ignore line (i.e. self) if cells produce unrelated amounts.
        # With Transformed Resource (Transformation), line is considered in
        # order to gather common properties and cells are used to describe
        # varianted properties: only 1 amount is produced.
        # In cases like trade, payroll or assorted resources,
        # we want to ignore the line if they are cells.
        # See also implementations of 'getCellAggregateKey'
        causality_value = property_dict['causality_value_list'][-1]
        if causality_value is self and len(cell_aggregate) > 1:
          continue
        base_application_set = property_dict['base_application_set']
        # allow a single base_application to be variated
        variation_category_list = tuple(sorted([x for x in base_application_set
                                                  if x[:12] != 'base_amount/']))
        if variation_category_list:
          base_application_set.difference_update(variation_category_list)
          assert len(base_application_set) == 1
        # property_dict may include
        #   resource - VAT service or a Component in MRP
        #              (if unset, the amount will only be used for reporting)
        #   variation params - color, size, employer share, etc.
        #   one of (net_)(converted_)quantity - used as a multiplier
        #     -> in MRP, quantity in component
        #     -> for trade, it provides a way to configure a fixed quantity
        #   price -  empty (like in Transformation) price of a product
        #            (ex. a Stamp) or tax ratio (ie. price per value units)
        #   base_contribution_list - needed to produce reports with
        #                            getTotalPrice
        # 'efficiency' is stored separately in the generated amount,
        # for future simulation of efficiencies.
        # If no quantity is provided, we consider that the value is 1.0
        # (XXX is it OK ?) XXX-JPS Need careful review with taxes
        quantity = float(sum(base_amount.getGeneratedAmountQuantity(
                               base_application, variation_category_list)
                             for base_application in base_application_set))
        for key in 'quantity', 'price', 'efficiency':
          if property_dict.get(key, 0) in (None, ''):
            del property_dict[key]
        quantity *= property_dict.pop('quantity', 1)

        # Before we ignore 'quantity==0' amount here for better
        # performance, but it is not a good idea, especially when the
        # first expand causes non-zero quantity and then quantity
        # becomes zero.
        # if not (quantity or generate_empty_amounts):
        #   continue

        # Backward compatibility
        if getattr(self.aq_base, 'create_line', None) == 0:
          property_dict['resource'] = None
        # Create an Amount object
        amount = newTempAmount(portal,
          # we only want the id to be unique so we pick a random causality
          causality_value.getRelativeUrl().replace('/', '_'))
        amount._setCategoryList(property_dict.pop('category_list', ()))
        if amount.getQuantityUnit():
          del property_dict['quantity_unit']
        amount._edit(
          quantity=quantity,
          # XXX Are title, int_index and description useful ??
          title=self.getTitle(),
          int_index=self.getIntIndex(),
          description=self.getDescription(),
          **property_dict)
        # convert to default management unit if possible
        amount._setQuantity(amount.getConvertedQuantity())
        amount._setQuantityUnit(amount.getResourceDefaultQuantityUnit())
        if rounding:
          # We hope here that rounding is sufficient at line level
          amount = getRoundingProxy(amount, context=self)
        result.append(amount)
        # Contribute
        quantity *= property_dict.get('price', 1)
        try:
          quantity /= property_dict.get('efficiency', 1)
        except ZeroDivisionError:
          quantity *= float('inf')
        base_contribution_set = property_dict['base_contribution_set']
        # allow a single base_contribution to be variated
        variation_category_list = tuple(sorted([x for x in base_contribution_set
                                                  if x[:12] != 'base_amount/']))
        if variation_category_list:
          base_contribution_set.difference_update(variation_category_list)
          assert len(base_contribution_set) == 1
        for base_contribution in base_contribution_set:
          base_amount.contribute(base_contribution, variation_category_list,
                                 quantity)