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
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
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
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