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