Esempio n. 1
0
 def getAggregatedAmountList(
     self, amount_list=None, rounding=False, amount_generator_type_list=None, generate_empty_amounts=True
 ):
     """
 Implementation of a generic transformation algorith which is
 applicable to payroll, tax generation and BOMs. Return the
 list of amounts with aggregation.
 """
     generated_amount_list = self.getGeneratedAmountList(
         amount_list=amount_list, rounding=rounding, amount_generator_type_list=amount_generator_type_list
     )
     # XXX: Do we handle rounding correctly ?
     #      What to do if only total price is rounded ??
     aggregate_dict = {}
     result_list = AggregatedAmountList()
     for amount in generated_amount_list:
         key = (amount.getPrice(), amount.getEfficiency(), amount.getReference(), amount.categories)
         aggregate = aggregate_dict.get(key)
         if aggregate is None:
             aggregate_dict[key] = [amount, amount.getQuantity()]
             result_list.append(amount)
         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.
         amount._setQuantity(quantity)
     if 0:
         print "getAggregatedAmountList(%r) -> (%s)" % (
             self.getRelativeUrl(),
             ", ".join("(%s, %s, %s)" % (x.getResourceTitle(), x.getQuantity(), x.getPrice()) for x in result_list),
         )
     return result_list
Esempio n. 2
0
 def getAggregatedAmountList(self,
                             amount_list=None,
                             rounding=False,
                             amount_generator_type_list=None,
                             generate_empty_amounts=True):
     """
 Implementation of a generic transformation algorith which is
 applicable to payroll, tax generation and BOMs. Return the
 list of amounts with aggregation.
 """
     generated_amount_list = self.getGeneratedAmountList(
         amount_list=amount_list,
         rounding=rounding,
         amount_generator_type_list=amount_generator_type_list)
     # XXX: Do we handle rounding correctly ?
     #      What to do if only total price is rounded ??
     aggregate_dict = {}
     result_list = AggregatedAmountList()
     for amount in generated_amount_list:
         key = (amount.getPrice(), amount.getEfficiency(),
                amount.getReference(), amount.categories)
         aggregate = aggregate_dict.get(key)
         if aggregate is None:
             aggregate_dict[key] = [amount, amount.getQuantity()]
             result_list.append(amount)
         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.
         # if quantity or generate_empty_amounts:
         #   amount._setQuantity(quantity)
         # else:
         #   result_list.remove(amount)
         amount._setQuantity(quantity)
     if 0:
         print 'getAggregatedAmountList(%r) -> (%s)' % (self.getRelativeUrl(
         ), ', '.join('(%s, %s, %s)' %
                      (x.getResourceTitle(), x.getQuantity(), x.getPrice())
                      for x in result_list))
     return result_list
Esempio n. 3
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(),
                            # 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
Esempio n. 4
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