From ba1edb6934f5fd0afcfbb2e62f7ea1a6d61b875c Mon Sep 17 00:00:00 2001 From: Yusuke Muraoka <yusuke@nexedi.com> Date: Tue, 10 Feb 2009 11:21:58 +0000 Subject: [PATCH] Add support for nested lines by Order/Delivery Builder. Move getTotal* and related methods of OrderLine to DeliveryLine, because nested lines would be used in delivery lines as well as order lines. git-svn-id: https://svn.erp5.org/repos/public/erp5/trunk@25505 20353a03-c40f-0410-a6d1-a30d3c3de9de --- product/ERP5/Document/DeliveryBuilder.py | 6 +- product/ERP5/Document/DeliveryLine.py | 43 +- product/ERP5/Document/MovementGroup.py | 4 + .../ERP5/Document/NestedLineMovementGroup.py | 54 +++ product/ERP5/Document/OrderBuilder.py | 190 +++++---- product/ERP5/Document/OrderLine.py | 51 --- product/ERP5/MovementGroup.py | 6 +- ...stDeliveryBuilderToSupportMultipleLines.py | 381 ++++++++++++++++++ 8 files changed, 584 insertions(+), 151 deletions(-) create mode 100644 product/ERP5/Document/NestedLineMovementGroup.py create mode 100644 product/ERP5/tests/testDeliveryBuilderToSupportMultipleLines.py diff --git a/product/ERP5/Document/DeliveryBuilder.py b/product/ERP5/Document/DeliveryBuilder.py index c0fb39b80b..5cc514ebbe 100644 --- a/product/ERP5/Document/DeliveryBuilder.py +++ b/product/ERP5/Document/DeliveryBuilder.py @@ -263,16 +263,16 @@ class DeliveryBuilder(OrderBuilder): simulation_movement_list.append(simulation_movement) # Collect - root_group = self.collectMovement(simulation_movement_list) + root_group_node = self.collectMovement(simulation_movement_list) # Build portal = self.getPortalObject() delivery_module = getattr(portal, self.getDeliveryModule()) delivery_to_update_list = [delivery] self._resetUpdated() - delivery_list = self._deliveryGroupProcessing( + delivery_list = self._processDeliveryGroup( delivery_module, - root_group, + root_group_node, self.getDeliveryMovementGroupList(), delivery_to_update_list=delivery_to_update_list, divergence_list=divergence_to_adopt_list, diff --git a/product/ERP5/Document/DeliveryLine.py b/product/ERP5/Document/DeliveryLine.py index de6c9e14e8..2bc0931e54 100644 --- a/product/ERP5/Document/DeliveryLine.py +++ b/product/ERP5/Document/DeliveryLine.py @@ -115,8 +115,19 @@ class DeliveryLine(Movement, XMLObject, XMLMatrix, Variated, return self.getParentValue().isAccountable() and (not self.hasCellContent()) def _getTotalPrice(self, default=0.0, context=None, fast=0): - """ Returns the total price for this line or the cells it contains. """ - if not self.hasCellContent(base_id='movement'): + """ + Returns the total price for this line, this line contains, or the cells it contains. + + if hasLineContent: return sum of lines total price + if hasCellContent: return sum of cells total price + else: return quantity * price + if fast is argument true, then a SQL method will be used. + """ + if self.hasLineContent(): + meta_type = self.meta_type + return sum(l.getTotalPrice(context=context) + for l in self.objectValues() if l.meta_type==meta_type) + elif not self.hasCellContent(base_id='movement'): return Movement._getTotalPrice(self, default=default, context=context) elif fast: # Use MySQL return self.DeliveryLine_zGetTotal()[0].total_price or 0.0 @@ -129,18 +140,34 @@ class DeliveryLine(Movement, XMLObject, XMLMatrix, Variated, """ Returns the quantity if no cell or the total quantity if cells - If fast is equal to 0, we returns the right quantity even - if there is nothing into the catalog or the catalog is not - up to date + if hasLineContent: return sum of lines total quantity + if hasCellContent: return sum of cells total quantity + else: return quantity + if fast argument is true, then a SQL method will be used. """ base_id = 'movement' - if not self.hasCellContent(base_id=base_id): - return self.getQuantity() - else: + if self.hasLineContent(): + meta_type = self.meta_type + return sum(l.getTotalQuantity() for l in + self.objectValues() if l.meta_type==meta_type) + elif self.hasCellContent(base_id=base_id): if fast : # Use MySQL aggregate = self.DeliveryLine_zGetTotal()[0] return aggregate.total_quantity or 0.0 return sum([cell.getQuantity() for cell in self.getCellValueList()]) + else: + return self.getQuantity() + + security.declareProtected(Permissions.AccessContentsInformation, + 'hasLineContent') + def hasLineContent(self): + """Return true if the object contains lines. + + This method only checks the first sub line because all sub + lines should be same meta type in reality if we have line + inside line. + """ + return len(self) != 0 and self.objectValues()[0].meta_type == self.meta_type security.declareProtected(Permissions.AccessContentsInformation, 'hasCellContent') diff --git a/product/ERP5/Document/MovementGroup.py b/product/ERP5/Document/MovementGroup.py index e6f22b109f..9d427a2cde 100644 --- a/product/ERP5/Document/MovementGroup.py +++ b/product/ERP5/Document/MovementGroup.py @@ -79,3 +79,7 @@ class MovementGroup(XMLObject): return sorted([[sorted(x[0], key=lambda x: x.getId()), x[1]] \ for x in self._separate(movement_list)], key=lambda x: x[0][0].getId()) + + def isBranch(self): + # self is taken as branch point by the builder if returned value is True. + return False diff --git a/product/ERP5/Document/NestedLineMovementGroup.py b/product/ERP5/Document/NestedLineMovementGroup.py new file mode 100644 index 0000000000..51cf637abe --- /dev/null +++ b/product/ERP5/Document/NestedLineMovementGroup.py @@ -0,0 +1,54 @@ +############################################################################## +# +# Copyright (c) 2009 Nexedi SA and Contributors. All Rights Reserved. +# +# WARNING: This program as such is intended to be used by professional +# programmers who take the whole responsibility of assessing all potential +# consequences resulting from its eventual inadequacies and bugs +# End users who are looking for a ready-to-use solution with commercial +# guarantees and support are strongly adviced to contract a Free Software +# Service Company +# +# This program is Free Software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. +# +############################################################################## + +from Products.ERP5.Document.MovementGroup import MovementGroup + +class NestedLineMovementGroup(MovementGroup): + """ + This MovementGroup is only used to multiple lines control. + No more effect. + """ + + meta_type = 'ERP5 Nested Line Movement Group' + portal_type = 'Nested Line Movement Group' + + def _getPropertyDict(self, movement, **kw): + return {} + + def test(self, object, property_dict, property_list=None, **kw): + if property_list not in (None, []): + target_property_list = [x for x in self.getTestedPropertyList() \ + if x in property_list] + else: + target_property_list = self.getTestedPropertyList() + for prop in target_property_list: + if property_dict[prop] != object.getProperty(prop, None): + return False, property_dict + return True, property_dict + + def isBranch(self): + return True diff --git a/product/ERP5/Document/OrderBuilder.py b/product/ERP5/Document/OrderBuilder.py index 39f6b22848..16222604d9 100644 --- a/product/ERP5/Document/OrderBuilder.py +++ b/product/ERP5/Document/OrderBuilder.py @@ -118,10 +118,10 @@ class OrderBuilder(XMLObject, Amount, Predicate): movement_list = [self.restrictedTraverse(relative_url) for relative_url \ in movement_relative_url_list] # Collect - root_group = self.collectMovement(movement_list) + root_group_node = self.collectMovement(movement_list) # Build delivery_list = self.buildDeliveryList( - root_group, + root_group_node, delivery_relative_url_list=delivery_relative_url_list, movement_list=movement_list,**kw) # Call a script after building @@ -229,45 +229,48 @@ class OrderBuilder(XMLObject, Amount, Predicate): movement_group_list = self.getMovementGroupList() last_line_movement_group = self.getDeliveryMovementGroupList()[-1] separate_method_name_list = self.getDeliveryCellSeparateOrderList([]) - my_root_group = MovementGroupNode( + root_group_node = MovementGroupNode( separate_method_name_list=separate_method_name_list, movement_group_list=movement_group_list, last_line_movement_group=last_line_movement_group) - my_root_group.append(movement_list) - return my_root_group + root_group_node.append(movement_list) + return root_group_node - def _test(self, instance, movement_group_list, + def _test(self, instance, movement_group_node_list, divergence_list): result = True new_property_dict = {} - for movement_group in movement_group_list: - tmp_result, tmp_property_dict = movement_group.test( + for movement_group_node in movement_group_node_list: + tmp_result, tmp_property_dict = movement_group_node.test( instance, divergence_list) if not tmp_result: result = tmp_result new_property_dict.update(tmp_property_dict) return result, new_property_dict - def _findUpdatableObject(self, instance_list, movement_group_list, + def _findUpdatableObject(self, instance_list, movement_group_node_list, divergence_list): instance = None property_dict = {} if not len(instance_list): - for movement_group in movement_group_list: - property_dict.update(movement_group.getGroupEditDict()) + for movement_group_node in movement_group_node_list: + property_dict.update(movement_group_node.getGroupEditDict()) else: - # we want to check the original first. - # the original is the delivery of the last (bottom) movement group. + # we want to check the original delivery first. + # so sort instance_list by that current is exists or not. try: - original = movement_group_list[-1].getMovementList()[0].getDeliveryValue() + current = movement_group_node_list[-1].getMovementList()[0].getDeliveryValue() + portal = self.getPortalObject() + while current != portal: + if current in instance_list: + instance_list.sort(key=lambda x: x != current and 1 or 0) + break + current = current.getParentValue() except AttributeError: - original = None - if original is not None: - original_id = original.getId() - instance_list.sort(key=lambda x: x.getId() != original_id and 1 or 0) + pass for instance_to_update in instance_list: result, property_dict = self._test( - instance_to_update, movement_group_list, divergence_list) + instance_to_update, movement_group_node_list, divergence_list) if result == True: instance = instance_to_update break @@ -280,7 +283,7 @@ class OrderBuilder(XMLObject, Amount, Predicate): buildDeliveryList = UnrestrictedMethod(self._buildDeliveryList) return buildDeliveryList(*args, **kw) - def _buildDeliveryList(self, movement_group, delivery_relative_url_list=None, + def _buildDeliveryList(self, movement_group_node, delivery_relative_url_list=None, movement_list=None,**kw): """This method is wrapped by UnrestrictedMethod.""" # Parameter initialization @@ -304,33 +307,26 @@ class OrderBuilder(XMLObject, Amount, Predicate): # We do not want to update the same object more than twice in one # _deliveryGroupProcessing(). self._resetUpdated() - delivery_list = self._deliveryGroupProcessing( + delivery_list = self._processDeliveryGroup( delivery_module, - movement_group, + movement_group_node, self.getDeliveryMovementGroupList(), delivery_to_update_list=delivery_to_update_list, **kw) return delivery_list - def _deliveryGroupProcessing(self, *args, **kw): - """ - Build empty delivery from a list of movement - """ - deliveryGroupProcessing = UnrestrictedMethod(self.__deliveryGroupProcessing) - return deliveryGroupProcessing(*args, **kw) - - def __deliveryGroupProcessing(self, delivery_module, movement_group, - collect_order_list, movement_group_list=None, - delivery_to_update_list=None, - divergence_list=None, - activate_kw=None, force_update=0, **kw): + def _processDeliveryGroup(self, delivery_module, movement_group_node, + collect_order_list, movement_group_node_list=None, + delivery_to_update_list=None, + divergence_list=None, + activate_kw=None, force_update=0, **kw): """This method is wrapped by UnrestrictedMethod.""" - if movement_group_list is None: - movement_group_list = [] + if movement_group_node_list is None: + movement_group_node_list = [] if divergence_list is None: divergence_list = [] # do not use 'append' or '+=' because they are destructive. - movement_group_list = movement_group_list + [movement_group] + movement_group_node_list = movement_group_node_list + [movement_group_node] # Parameter initialization if delivery_to_update_list is None: delivery_to_update_list = [] @@ -338,12 +334,12 @@ class OrderBuilder(XMLObject, Amount, Predicate): if len(collect_order_list): # Get sorted movement for each delivery - for group in movement_group.getGroupList(): - new_delivery_list = self._deliveryGroupProcessing( + for grouped_node in movement_group_node.getGroupList(): + new_delivery_list = self._processDeliveryGroup( delivery_module, - group, + grouped_node, collect_order_list[1:], - movement_group_list=movement_group_list, + movement_group_node_list=movement_group_node_list, delivery_to_update_list=delivery_to_update_list, divergence_list=divergence_list, activate_kw=activate_kw, @@ -358,7 +354,7 @@ class OrderBuilder(XMLObject, Amount, Predicate): if x.getPortalType() == self.getDeliveryPortalType() and \ not self._isUpdated(x, 'delivery')] delivery, property_dict = self._findUpdatableObject( - delivery_to_update_list, movement_group_list, + delivery_to_update_list, movement_group_node_list, divergence_list) # if all deliveries are rejected in case of update, we update the @@ -370,7 +366,7 @@ class OrderBuilder(XMLObject, Amount, Predicate): # Create delivery try: old_delivery = self._searchUpByPortalType( - movement_group.getMovementList()[0].getDeliveryValue(), + movement_group_node.getMovementList()[0].getDeliveryValue(), self.getDeliveryPortalType()) except AttributeError: old_delivery = None @@ -392,7 +388,7 @@ class OrderBuilder(XMLObject, Amount, Predicate): delivery = delivery_module[cp['new_id']] # delete non-split movements keep_id_list = [y.getDeliveryValue().getId() for y in \ - movement_group.getMovementList()] + movement_group_node.getMovementList()] delete_id_list = [x.getId() for x in delivery.contentValues() \ if x.getId() not in keep_id_list] delivery.deleteContent(delete_id_list) @@ -402,10 +398,10 @@ class OrderBuilder(XMLObject, Amount, Predicate): delivery.edit(**property_dict) # Then, create delivery line - for group in movement_group.getGroupList(): - self._deliveryLineGroupProcessing( + for grouped_node in movement_group_node.getGroupList(): + self._processDeliveryLineGroup( delivery, - group, + grouped_node, self.getDeliveryLineMovementGroupList()[1:], divergence_list=divergence_list, activate_kw=activate_kw, @@ -413,28 +409,31 @@ class OrderBuilder(XMLObject, Amount, Predicate): delivery_list.append(delivery) return delivery_list - def _deliveryLineGroupProcessing(self, delivery, movement_group, - collect_order_list, movement_group_list=None, - divergence_list=None, - activate_kw=None, force_update=0, **kw): + def _processDeliveryLineGroup(self, delivery, movement_group_node, + collect_order_list, movement_group_node_list=None, + divergence_list=None, + activate_kw=None, force_update=0, **kw): """ Build delivery line from a list of movement on a delivery """ - if movement_group_list is None: - movement_group_list = [] + if movement_group_node_list is None: + movement_group_node_list = [] if divergence_list is None: divergence_list = [] # do not use 'append' or '+=' because they are destructive. - movement_group_list = movement_group_list + [movement_group] + movement_group_node_list = movement_group_node_list + [movement_group_node] - if len(collect_order_list): + if len(collect_order_list) and not movement_group_node.getCurrentMovementGroup().isBranch(): # Get sorted movement for each delivery line - for group in movement_group.getGroupList(): - self._deliveryLineGroupProcessing( - delivery, group, collect_order_list[1:], - movement_group_list=movement_group_list, + for grouped_node in movement_group_node.getGroupList(): + self._processDeliveryLineGroup( + delivery, + grouped_node, + collect_order_list[1:], + movement_group_node_list=movement_group_node_list, divergence_list=divergence_list, - activate_kw=activate_kw, force_update=force_update) + activate_kw=activate_kw, + force_update=force_update) else: # Test if we can update an existing line, or if we need to create a new # one @@ -442,7 +441,7 @@ class OrderBuilder(XMLObject, Amount, Predicate): portal_type=self.getDeliveryLinePortalType()) if \ not self._isUpdated(x, 'line')] delivery_line, property_dict = self._findUpdatableObject( - delivery_line_to_update_list, movement_group_list, + delivery_line_to_update_list, movement_group_node_list, divergence_list) if delivery_line is not None: update_existing_line = 1 @@ -451,7 +450,7 @@ class OrderBuilder(XMLObject, Amount, Predicate): update_existing_line = 0 try: old_delivery_line = self._searchUpByPortalType( - movement_group.getMovementList()[0].getDeliveryValue(), + movement_group_node.getMovementList()[0].getDeliveryValue(), self.getDeliveryLinePortalType()) except AttributeError: old_delivery_line = None @@ -475,7 +474,7 @@ class OrderBuilder(XMLObject, Amount, Predicate): delivery_line.setVariationCategoryList([]) # delete non-split movements keep_id_list = [y.getDeliveryValue().getId() for y in \ - movement_group.getMovementList()] + movement_group_node.getMovementList()] delete_id_list = [x.getId() for x in delivery_line.contentValues() \ if x.getId() not in keep_id_list] delivery_line.deleteContent(delete_id_list) @@ -484,34 +483,46 @@ class OrderBuilder(XMLObject, Amount, Predicate): if property_dict: delivery_line.edit(**property_dict) + if movement_group_node.getCurrentMovementGroup().isBranch(): + for grouped_node in movement_group_node.getGroupList(): + self._processDeliveryLineGroup( + delivery_line, + grouped_node, + collect_order_list[1:], + movement_group_node_list=movement_group_node_list, + divergence_list=divergence_list, + activate_kw=activate_kw, + force_update=force_update) + return + # Update variation category list on line variation_category_dict = dict([(variation_category, True) for variation_category in delivery_line.getVariationCategoryList()]) - for movement in movement_group.getMovementList(): + for movement in movement_group_node.getMovementList(): for category in movement.getVariationCategoryList(): variation_category_dict[category] = True variation_category_list = sorted(variation_category_dict.keys()) delivery_line.setVariationCategoryList(variation_category_list) # Then, create delivery movement (delivery cell or complete delivery # line) - group_list = movement_group.getGroupList() + grouped_node_list = movement_group_node.getGroupList() # If no group is defined for cell, we need to continue, in order to # save the quantity value - if len(group_list): - for group in group_list: - self._deliveryCellGroupProcessing( + if len(grouped_node_list): + for grouped_node in grouped_node_list: + self._processDeliveryCellGroup( delivery_line, - group, + grouped_node, self.getDeliveryCellMovementGroupList()[1:], update_existing_line=update_existing_line, divergence_list=divergence_list, activate_kw=activate_kw, force_update=force_update) else: - self._deliveryCellGroupProcessing( + self._processDeliveryCellGroup( delivery_line, - movement_group, + movement_group_node, [], update_existing_line=update_existing_line, divergence_list=divergence_list, @@ -519,36 +530,36 @@ class OrderBuilder(XMLObject, Amount, Predicate): force_update=force_update) - def _deliveryCellGroupProcessing(self, delivery_line, movement_group, - collect_order_list, movement_group_list=None, - update_existing_line=0, - divergence_list=None, - activate_kw=None, force_update=0): + def _processDeliveryCellGroup(self, delivery_line, movement_group_node, + collect_order_list, movement_group_node_list=None, + update_existing_line=0, + divergence_list=None, + activate_kw=None, force_update=0): """ Build delivery cell from a list of movement on a delivery line or complete delivery line """ - if movement_group_list is None: - movement_group_list = [] + if movement_group_node_list is None: + movement_group_node_list = [] if divergence_list is None: divergence_list = [] # do not use 'append' or '+=' because they are destructive. - movement_group_list = movement_group_list + [movement_group] + movement_group_node_list = movement_group_node_list + [movement_group_node] if len(collect_order_list): # Get sorted movement for each delivery line - for group in movement_group.getGroupList(): - self._deliveryCellGroupProcessing( + for grouped_node in movement_group_node.getGroupList(): + self._processDeliveryCellGroup( delivery_line, - group, + grouped_node, collect_order_list[1:], - movement_group_list=movement_group_list, + movement_group_node_list=movement_group_node_list, update_existing_line=update_existing_line, divergence_list=divergence_list, activate_kw=activate_kw, force_update=force_update) else: - movement_list = movement_group.getMovementList() + movement_list = movement_group_node.getMovementList() if len(movement_list) != 1: raise CollectError, "DeliveryBuilder: %s unable to distinct those\ movements: %s" % (self.getId(), str(movement_list)) @@ -574,7 +585,7 @@ class OrderBuilder(XMLObject, Amount, Predicate): else: object_to_update_list = [] object_to_update, property_dict = self._findUpdatableObject( - object_to_update_list, movement_group_list, + object_to_update_list, movement_group_node_list, divergence_list) if object_to_update is not None: update_existing_movement = 1 @@ -586,7 +597,7 @@ class OrderBuilder(XMLObject, Amount, Predicate): delivery_line.getCellKeyList(base_id=base_id) \ if delivery_line.hasCell(base_id=base_id, *cell_key)] object_to_update, property_dict = self._findUpdatableObject( - object_to_update_list, movement_group_list, + object_to_update_list, movement_group_node_list, divergence_list) if object_to_update is not None: # We update a existing cell @@ -600,7 +611,7 @@ class OrderBuilder(XMLObject, Amount, Predicate): omit_optional_variation=1) if not delivery_line.hasCell(base_id=base_id, *cell_key): try: - old_cell = movement_group.getMovementList()[0].getDeliveryValue() + old_cell = movement_group_node.getMovementList()[0].getDeliveryValue() except AttributeError: old_cell = None if old_cell is None: @@ -756,3 +767,8 @@ class OrderBuilder(XMLObject, Amount, Predicate): def _resetUpdated(self): tv = getTransactionalVariable(self) tv['builder_processed_list'] = {} + + # for backward compatibilities. + _deliveryGroupProcessing = _processDeliveryGroup + _deliveryLineGroupProcessing = _processDeliveryLineGroup + _deliveryCellGroupProcessing = _processDeliveryCellGroup diff --git a/product/ERP5/Document/OrderLine.py b/product/ERP5/Document/OrderLine.py index 526e603032..f01a956336 100644 --- a/product/ERP5/Document/OrderLine.py +++ b/product/ERP5/Document/OrderLine.py @@ -64,57 +64,6 @@ class OrderLine(DeliveryLine): # Declarative interfaces __implements__ = ( Interface.Variated, ) - security.declareProtected(Permissions.AccessContentsInformation, - 'hasLineContent') - def hasLineContent(self): - """Return true if the object contains lines. - - This method only checks the first sub document because all sub - documents should be Order Line in reality if we have Order Line - inside Order Line. - """ - return len(self) != 0 and self.objectValues()[0].meta_type == self.meta_type - - def _getTotalPrice(self, default=0.0, context=None, fast=0): - """Returns the total price for this order line. - - if hasLineContent: return sum of lines total price - if hasCellContent: return sum of cells total price - else: return quantity * price - if fast is argument true, then a SQL method will be used. - """ - if self.hasLineContent(): - meta_type = self.meta_type - return sum(l.getTotalPrice(context=context) - for l in self.objectValues() if l.meta_type==meta_type) - return DeliveryLine._getTotalPrice(self, - default=default, - context=context, - fast=fast) - - security.declareProtected(Permissions.AccessContentsInformation, - 'getTotalQuantity') - def getTotalQuantity(self, fast=0): - """Returns the total quantity of this order line. - - if hasLineContent: return sum of lines total quantity - if hasCellContent: return sum of cells total quantity - else: return quantity - if fast argument is true, then a SQL method will be used. - """ - base_id = 'movement' - if self.hasLineContent(): - meta_type = self.meta_type - return sum(l.getTotalQuantity() for l in - self.objectValues() if l.meta_type==meta_type) - elif self.hasCellContent(base_id=base_id): - if fast : # Use MySQL - aggregate = self.DeliveryLine_zGetTotal()[0] - return aggregate.total_quantity or 0.0 - return sum([cell.getQuantity() for cell in self.getCellValueList()]) - else: - return self.getQuantity() - def applyToOrderLineRelatedMovement(self, portal_type='Simulation Movement', method_id = 'expand'): """ diff --git a/product/ERP5/MovementGroup.py b/product/ERP5/MovementGroup.py index 8b6cd4e2f4..50dfda74d0 100644 --- a/product/ERP5/MovementGroup.py +++ b/product/ERP5/MovementGroup.py @@ -76,8 +76,7 @@ class MovementGroupNode: for movement in movement_list[1:]: # We have a conflict here, because it is forbidden to have # 2 movements on the same node group - tmp_result = self._separate(movement) - self._movement_list, split_movement = tmp_result + self._movement_list, split_movement = self._separate(movement) if split_movement is not None: # We rejected a movement, we need to put it on another line # Or to create a new one @@ -109,6 +108,9 @@ class MovementGroupNode: del(property_dict[key]) return property_dict + def getCurrentMovementGroup(self): + return self._movement_group + def getMovementList(self): """ Return movement list in the current group diff --git a/product/ERP5/tests/testDeliveryBuilderToSupportMultipleLines.py b/product/ERP5/tests/testDeliveryBuilderToSupportMultipleLines.py new file mode 100644 index 0000000000..47d887d58e --- /dev/null +++ b/product/ERP5/tests/testDeliveryBuilderToSupportMultipleLines.py @@ -0,0 +1,381 @@ +############################################################################## +# -*- coding: utf8 -*- +# +# Copyright (c) 2009 Nexedi SA and Contributors. All Rights Reserved. +# +# WARNING: This program as such is intended to be used by professional +# programmers who take the whole responsibility of assessing all potential +# consequences resulting from its eventual inadequacies and bugs +# End users who are looking for a ready-to-use solution with commercial +# guarantees and support are strongly adviced to contract a Free Software +# Service Company +# +# This program is Free Software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +############################################################################## + +import unittest + +from Products.ERP5Type.UnrestrictedMethod import UnrestrictedMethod +from Products.ERP5Type.tests.ERP5TypeTestCase import ERP5TypeTestCase +from Products.ERP5Type.tests.Sequence import SequenceList +from Products.ERP5Type.tests.utils import createZODBPythonScript +from Products.ERP5.tests.testInvoice import TestSaleInvoiceMixin + +class TestNestedLineMixin(TestSaleInvoiceMixin): + + """ + NestedLineMovementGroup is a mark only for controlling multiple lines in DeliveryBuilder. + We need this feature to make multi-level "Invoice Line"s. + """ + + DEFAULT_SEQUENCE = TestSaleInvoiceMixin.PACKING_LIST_DEFAULT_SEQUENCE + \ + """ + stepSetReadyPackingList + stepTic + stepUpdateBuilderForMultipleLineList + stepSetPythonScriptForDeliveryBuilder + stepStartPackingList + stepCheckInvoicingRule + stepTic + stepGetRelatedInvoiceFromPackingList + """ + delivery_builder_id = 'sale_invoice_builder' + default_quantity = TestSaleInvoiceMixin.default_quantity + new_order_quantity = TestSaleInvoiceMixin.default_quantity * 3 + new_packing_list_quantity = TestSaleInvoiceMixin.default_quantity * 5 + new_invoice_quantity = TestSaleInvoiceMixin.default_quantity * 2 + + def afterSetUp(self): + TestSaleInvoiceMixin.afterSetUp(self) + # Necessary to allow Invoice Line to be included in Invoice Line. + self.allowInvoiceLineContentTypeInInvoiceLine() + + def allowInvoiceLineContentTypeInInvoiceLine(self): + return UnrestrictedMethod(self._allowInvoiceLineContentTypeInInvoiceLine)() + + def _allowInvoiceLineContentTypeInInvoiceLine(self): + invoice_line_type = self.portal.portal_types['Invoice Line'] + if 'Invoice Line' not in invoice_line_type.allowed_content_types: + invoice_line_type.allowed_content_types += ('Invoice Line',) + + def stepGetRelatedInvoiceFromPackingList(self, sequence, **kw): + packing_list = sequence.get('packing_list') + related_invoice_list = packing_list \ + .getCausalityRelatedValueList(portal_type=self.invoice_portal_type) + invoice = related_invoice_list[0].getObject() + sequence.edit(invoice=invoice) + + def stepUpdateBuilderForMultipleLineList(self, **kw): + self.updateBuilderForMultipleLineList() + + def updateBuilderForMultipleLineList(self): + return UnrestrictedMethod(self._updateBuilderForMultipleLineList)() + + def _updateBuilderForMultipleLineList(self): + delivery_builder = getattr(self.portal.portal_deliveries, self.delivery_builder_id) + + delivery_builder.deleteContent(delivery_builder.contentIds()) + delivery_builder.newContent( + portal_type='Property Movement Group', + collect_order_group='delivery', + divergence_scope='property', + tested_property_list=('start_date', 'stop_date'), + int_index=1) + delivery_builder.newContent( + portal_type='Category Movement Group', + collect_order_group='delivery', + divergence_scope='category', + tested_property_list=('delivery_mode', + 'incoterm', + 'source', + 'destination', + 'source_section', + 'destination_section', + 'destination_function', + 'source_function', + 'source_decision', + 'destination_decision', + 'source_administration', + 'destination_administration', + 'price_currency'), + int_index=2) + delivery_builder.newContent( + portal_type='Delivery Causality Assignment Movement Group', + collect_order_group='delivery', + int_index=3) + delivery_builder.newContent( + portal_type='Property Movement Group', + collect_order_group='line', + divergence_scope='property', + tested_property_list=('start_date', 'stop_date'), + int_index=1) + # *** test this *** + delivery_builder.newContent( + portal_type='Nested Line Movement Group', + collect_order_group='line', + int_index=2) + delivery_builder.newContent( + portal_type='Category Movement Group', + collect_order_group='line', + divergence_scope='category', + tested_property_list=('resource', 'aggregate', 'base_contribution'), + int_index=3) + delivery_builder.newContent( + portal_type='Base Variant Movement Group', + collect_order_group='line', + int_index=4) + delivery_builder.newContent( + portal_type='Property Movement Group', + collect_order_group='line', + divergence_scope='property', + tested_property_list=('description'), + int_index=5) + delivery_builder.newContent( + portal_type='Variant Movement Group', + collect_order_group='cell', + divergence_scope='category', + int_index=1) + + def stepSetExistDeliveriesToSequence(self, sequence=None, **kw): + order = self.portal.sale_order_module.contentValues(portal_type='Sale Order')[0] + packing_list = self.portal.sale_packing_list_module \ + .contentValues(portal_type='Sale Packing List')[0] + invoice = self.portal.accounting_module \ + .contentValues(portal_type='Sale Invoice Transaction')[0] + sequence.edit(order=order, packing_list=packing_list, invoice=invoice) + + def stepUpdateOrder(self, sequence=None, **kw): + movement = sequence.get('order').getMovementList(portal_type='Sale Order Line')[0] + movement.edit(quantity=self.new_order_quantity, + price=self.default_price) + + def stepUpdatePackingList(self, sequence=None, **kw): + movement = sequence.get('packing_list') \ + .getMovementList(portal_type='Sale Packing List Line')[0] + movement.edit(quantity=self.new_packing_list_quantity) + + def stepSetFillContainerLine(self, sequence=None, **kw): + movement = sequence.get('container_line') + movement.edit(quantity=self.new_order_quantity) + + def stepUpdateInvoice(self, sequence=None, **kw): + movement = sequence.get('invoice') \ + .getMovementList(portal_type='Invoice Line')[0] + movement.edit(quantity=self.new_invoice_quantity) + + def stepSetPythonScriptForDeliveryBuilder(self, **kw): + """ + Make a script which returns existing Sale Invoice Transactions, + so that all movements are merged into existing ones. + """ + delivery_select_method_id = 'Test_selectDelivery' + createZODBPythonScript( + self.portal.portal_skins.custom, + delivery_select_method_id, + 'movement_list=None', + """ +return context.getPortalObject().portal_catalog(portal_type='Sale Invoice Transaction') +""") + delivery_builder = getattr(self.portal.portal_deliveries, self.delivery_builder_id) + delivery_builder.delivery_select_method_id = delivery_select_method_id + + def stepSetSeparateMethodToDeliveryBuilder(self, **kw): + """ + Merge multiple simulation movements into one movement. + """ + delivery_builder = getattr(self.portal.portal_deliveries, self.delivery_builder_id) + delivery_builder.delivery_cell_separate_order = ('calculateAddQuantity',) + + def stepAdoptPrevisionPackingListQuantity(self,sequence=None, sequence_list=None): + document = sequence.get('packing_list') + self._solveDivergence(document, 'quantity', 'adopt') + + def stepAcceptDecisionPackingListQuantity(self,sequence=None, sequence_list=None): + document = sequence.get('packing_list') + self._solveDivergence(document, 'quantity', 'accept') + + def stepAdoptPrevisionInvoiceQuantity(self,sequence=None, sequence_list=None): + document = sequence.get('invoice') + self._solveDivergence(document, 'quantity', 'adopt') + + def stepAcceptDecisionInvoiceQuantity(self,sequence=None, sequence_list=None): + document = sequence.get('invoice') + self._solveDivergence(document, 'quantity', 'accept') + + +class TestNestedLine(TestNestedLineMixin, ERP5TypeTestCase): + + quiet = 0 + + def test_01_IfNested(self, quiet=quiet): + sequence_list = SequenceList() + sequence = sequence_list.addSequenceString(self.DEFAULT_SEQUENCE) + sequence_list.play(self, quiet=quiet) + + # order = sequence.get('order') + # packing_list = sequence.get('packing_list') + document = sequence.get('invoice') + self.assertEquals('Sale Invoice Transaction', document.getPortalType()) + self.assertEquals(1, len(document)) + + line = document.objectValues()[0] + self.assertEquals('Invoice Line', line.getPortalType()) + self.assertEquals(None, line.getQuantity(None)) + self.assertEquals(1, len(line)) + + line_line = line.objectValues()[0] + self.assertEquals('Invoice Line', line_line.getPortalType()) + + self.assertEquals(self.default_price * self.default_quantity, document.getTotalPrice()) + self.assertEquals(self.default_quantity, document.getTotalQuantity()) + self.assertEquals(self.default_price, line_line.getPrice()) + self.assertEquals(self.default_quantity, line_line.getQuantity()) + + + def test_02_AdoptingPrevision(self, quiet=quiet): + sequence_list = SequenceList() + sequence = sequence_list.addSequenceString(self.DEFAULT_SEQUENCE + \ + """ + stepUpdatePackingList + stepTic + + stepAcceptDecisionPackingListQuantity + stepTic + + stepCheckInvoiceIsDivergent + stepCheckInvoiceIsDiverged + stepAdoptPrevisionInvoiceQuantity + stepTic + """ + ) + sequence_list.play(self, quiet=quiet) + + document = sequence.get('invoice') + self.assertEquals('solved', document.getCausalityState()) + self.assertEquals(1, len(document)) + + line = document.objectValues()[0] + self.assertEquals('Invoice Line', line.getPortalType()) + self.assertEquals(None, line.getQuantity(None)) + self.assertEquals(1, len(line)) + + line_line = line.objectValues()[0] + self.assertEquals('Invoice Line', line_line.getPortalType()) + + self.assertEquals(self.default_price * self.new_packing_list_quantity, document.getTotalPrice()) + self.assertEquals(self.new_packing_list_quantity, document.getTotalQuantity()) + self.assertEquals(self.new_packing_list_quantity, line_line.getQuantity()) + + def test_03_AcceptingDecision(self, quiet=quiet): + sequence_list = SequenceList() + sequence = sequence_list.addSequenceString(self.DEFAULT_SEQUENCE + \ + """ + stepUpdateInvoice + stepTic + + stepCheckInvoiceIsDivergent + stepAcceptDecisionInvoiceQuantity + stepTic + + stepCheckInvoiceIsNotDivergent + stepCheckPackingListIsDivergent + stepAdoptPrevisionPackingListQuantity + stepTic + """ + ) + sequence_list.play(self, quiet=quiet) + + document = sequence.get('invoice') + + self.assertEquals('solved', document.getCausalityState()) + self.assertEquals(1, len(document)) + + line = document.objectValues()[0] + self.assertEquals('Invoice Line', line.getPortalType()) + self.assertEquals(None, line.getQuantity(None)) + self.assertEquals(1, len(line)) + + line_line = line.objectValues()[0] + self.assertEquals('Invoice Line', line_line.getPortalType()) + + self.assertEquals(self.default_price * self.new_invoice_quantity, document.getTotalPrice()) + self.assertEquals(self.new_invoice_quantity, document.getTotalQuantity()) + self.assertEquals(self.new_invoice_quantity, line_line.getQuantity()) + + def test_04_MergingMultipleSaleOrders(self, quiet=quiet): + sequence_list = SequenceList() + sequence = sequence_list.addSequenceString(self.DEFAULT_SEQUENCE + \ + """ + stepCreateOrder + stepSetOrderProfile + stepSetOrderPriceCurrency + stepTic + stepCreateOrderLine + stepSetOrderLineResource + stepUpdateOrder + stepOrderOrder + stepTic + stepCheckDeliveryBuilding + stepConfirmOrder + stepTic + stepCheckOrderRule + stepCheckOrderSimulation + stepCheckDeliveryBuilding + stepAddPackingListContainer + stepAddPackingListContainerLine + stepSetFillContainerLine + stepTic + + stepSetReadyPackingList + stepTic + + stepStartPackingList + stepCheckInvoicingRule + stepTic + + stepCheckInvoiceIsDivergent + stepAdoptPrevisionInvoiceQuantity + stepTic + """ + ) + sequence_list.play(self, quiet=quiet) + + self.assertEquals(1, len(self.portal.accounting_module)) + + document = self.portal.accounting_module.objectValues()[0] + self.assertEquals('solved', document.getCausalityState()) + self.assertEquals(1, len(document)) + + line = document.objectValues()[0] + self.assertEquals('Invoice Line', line.getPortalType()) + self.assertEquals(None, line.getQuantity(None)) + self.assertEquals(1, len(line)) + + line_line = line.objectValues()[0] + self.assertEquals('Invoice Line', line_line.getPortalType()) + + # The sale invoice summed up from two sale orders. + # The quantity of a sale order is self.default_quantity, and + # that of the other one is self.new_order_quantity. + self.assertEquals(self.default_price * (self.default_quantity + self.new_order_quantity), document.getTotalPrice()) + self.assertEquals(self.default_quantity + self.new_order_quantity, document.getTotalQuantity()) + self.assertEquals(self.default_quantity + self.new_order_quantity, line_line.getQuantity()) + + +def test_suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(TestNestedLine)) + return suite -- 2.30.9