Commit b0d269b1 authored by Sebastien Robin's avatar Sebastien Robin

simulation: fully review the way to merge deliveries and enable merge delivery action

Fully rewrite portal_simulation.mergeDeliveryList to use builders to reconstruct new
merged delivery.

Add parameter "merge_delivery" to builder. This parameter is used when
merge should be done in such a way that movement group at delivery
level are ignored
parent 2ee779a4
......@@ -54,7 +54,7 @@
</item>
<item>
<key> <string>id</string> </key>
<value> <string>Delivery_mergeDeliveryList</string> </value>
<value> <string>DeliveryModule_mergeDeliveryList</string> </value>
</item>
<item>
<key> <string>title</string> </key>
......
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="PythonScript" module="Products.PythonScripts.PythonScript"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>Script_magic</string> </key>
<value> <int>3</int> </value>
</item>
<item>
<key> <string>_bind_names</string> </key>
<value>
<object>
<klass>
<global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
</klass>
<tuple/>
<state>
<dictionary>
<item>
<key> <string>_asgns</string> </key>
<value>
<dictionary>
<item>
<key> <string>name_container</string> </key>
<value> <string>container</string> </value>
</item>
<item>
<key> <string>name_context</string> </key>
<value> <string>context</string> </value>
</item>
<item>
<key> <string>name_m_self</string> </key>
<value> <string>script</string> </value>
</item>
<item>
<key> <string>name_subpath</string> </key>
<value> <string>traverse_subpath</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</state>
</object>
</value>
</item>
<item>
<key> <string>_params</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>_proxy_roles</string> </key>
<value>
<tuple>
<string>Manager</string>
</tuple>
</value>
</item>
<item>
<key> <string>guard</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>Delivery_cleanDeliveryAfterMerge</string> </value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="Guard" module="Products.DCWorkflow.Guard"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>permissions</string> </key>
<value>
<tuple>
<string>View</string>
</tuple>
</value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ActionInformation" module="Products.CMFCore.ActionInformation"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>action</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>categories</string> </key>
<value>
<tuple>
<string>action_type/object_action</string>
</tuple>
</value>
</item>
<item>
<key> <string>category</string> </key>
<value> <string>object_action</string> </value>
</item>
<item>
<key> <string>condition</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>icon</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>merge_delivery_list</string> </value>
</item>
<item>
<key> <string>permissions</string> </key>
<value>
<tuple>
<string>View</string>
</tuple>
</value>
</item>
<item>
<key> <string>priority</string> </key>
<value> <float>1.0</float> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Merge Internal Packing Lists</string> </value>
</item>
<item>
<key> <string>visible</string> </key>
<value> <int>1</int> </value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="Expression" module="Products.CMFCore.Expression"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>text</string> </key>
<value> <string>string:${object_url}/DeliveryModule_mergeDeliveryList</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ActionInformation" module="Products.CMFCore.ActionInformation"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>action</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>categories</string> </key>
<value>
<tuple>
<string>action_type/object_action</string>
</tuple>
</value>
</item>
<item>
<key> <string>category</string> </key>
<value> <string>object_action</string> </value>
</item>
<item>
<key> <string>condition</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>icon</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>merge_delivery_list</string> </value>
</item>
<item>
<key> <string>permissions</string> </key>
<value>
<tuple>
<string>View</string>
</tuple>
</value>
</item>
<item>
<key> <string>priority</string> </key>
<value> <float>1.0</float> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Merge Purchase Packing Lists</string> </value>
</item>
<item>
<key> <string>visible</string> </key>
<value> <int>1</int> </value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="Expression" module="Products.CMFCore.Expression"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>text</string> </key>
<value> <string>string:${object_url}/DeliveryModule_mergeDeliveryList</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ActionInformation" module="Products.CMFCore.ActionInformation"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>action</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>categories</string> </key>
<value>
<tuple>
<string>action_type/object_action</string>
</tuple>
</value>
</item>
<item>
<key> <string>category</string> </key>
<value> <string>object_action</string> </value>
</item>
<item>
<key> <string>condition</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>icon</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>merge_delivery_list</string> </value>
</item>
<item>
<key> <string>permissions</string> </key>
<value>
<tuple>
<string>View</string>
</tuple>
</value>
</item>
<item>
<key> <string>priority</string> </key>
<value> <float>1.0</float> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Merge Sale Packing Lists</string> </value>
</item>
<item>
<key> <string>visible</string> </key>
<value> <int>1</int> </value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="Expression" module="Products.CMFCore.Expression"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>text</string> </key>
<value> <string>string:${object_url}/DeliveryModule_mergeDeliveryList</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ERP5 Form" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_bind_names</string> </key>
<value>
<object>
<klass>
<global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
</klass>
<tuple/>
<state>
<dictionary>
<item>
<key> <string>_asgns</string> </key>
<value>
<dictionary/>
</value>
</item>
</dictionary>
</state>
</object>
</value>
</item>
<item>
<key> <string>_objects</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>action</string> </key>
<value> <string>Delivery_mergeDeliveryList</string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>edit_order</string> </key>
<value>
<list/>
</value>
</item>
<item>
<key> <string>encoding</string> </key>
<value> <string>UTF-8</string> </value>
</item>
<item>
<key> <string>enctype</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>group_list</string> </key>
<value>
<list>
<string>left</string>
<string>right</string>
<string>center</string>
<string>bottom</string>
<string>hidden</string>
</list>
</value>
</item>
<item>
<key> <string>groups</string> </key>
<value>
<dictionary>
<item>
<key> <string>bottom</string> </key>
<value>
<list>
<string>listbox</string>
</list>
</value>
</item>
<item>
<key> <string>center</string> </key>
<value>
<list/>
</value>
</item>
<item>
<key> <string>hidden</string> </key>
<value>
<list/>
</value>
</item>
<item>
<key> <string>left</string> </key>
<value>
<list/>
</value>
</item>
<item>
<key> <string>right</string> </key>
<value>
<list/>
</value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>Delivery_viewMergedDeliveryList</string> </value>
</item>
<item>
<key> <string>method</string> </key>
<value> <string>POST</string> </value>
</item>
<item>
<key> <string>name</string> </key>
<value> <string>Delivery_viewMergedDeliveryList</string> </value>
</item>
<item>
<key> <string>pt</string> </key>
<value> <string>form_dialog</string> </value>
</item>
<item>
<key> <string>row_length</string> </key>
<value> <int>4</int> </value>
</item>
<item>
<key> <string>stored_encoding</string> </key>
<value> <string>UTF-8</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Merge Deliveries</string> </value>
</item>
<item>
<key> <string>unicode_mode</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>update_action</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ProxyField" module="Products.ERP5Form.ProxyField"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>delegated_list</string> </key>
<value>
<list>
<string>sort</string>
<string>title</string>
<string>lines</string>
<string>list_action</string>
<string>selection_name</string>
<string>columns</string>
<string>list_method</string>
</list>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>listbox</string> </value>
</item>
<item>
<key> <string>message_values</string> </key>
<value>
<dictionary>
<item>
<key> <string>external_validator_failed</string> </key>
<value> <string>The input failed the external validator.</string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>overrides</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>target</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>tales</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>list_method</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>selection_name</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent>
</value>
</item>
<item>
<key> <string>target</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>values</string> </key>
<value>
<dictionary>
<item>
<key> <string>columns</string> </key>
<value>
<list>
<tuple>
<string>id</string>
<string>ID</string>
</tuple>
<tuple>
<string>portal_type</string>
<string>Type</string>
</tuple>
<tuple>
<string>title</string>
<string>Title</string>
</tuple>
<tuple>
<string>relative_url</string>
<string>Path</string>
</tuple>
<tuple>
<string>description</string>
<string>Description</string>
</tuple>
</list>
</value>
</item>
<item>
<key> <string>field_id</string> </key>
<value> <string>my_view_mode_listbox</string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string>Base_viewTradeFieldLibrary</string> </value>
</item>
<item>
<key> <string>lines</string> </key>
<value> <int>30</int> </value>
</item>
<item>
<key> <string>list_action</string> </key>
<value> <string>Folder_viewContentList</string> </value>
</item>
<item>
<key> <string>selection_name</string> </key>
<value> <string>delivery_selection</string> </value>
</item>
<item>
<key> <string>sort</string> </key>
<value>
<list>
<tuple>
<string>relative_url</string>
<string>relative_url</string>
</tuple>
</list>
</value>
</item>
<item>
<key> <string>target</string> </key>
<value> <string>Click to edit the target</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Merged Deliveries</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="TALESMethod" module="Products.Formulator.TALESField"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_text</string> </key>
<value> <string>python:None</string> </value>
</item>
</dictionary>
</pickle>
</record>
<record id="3" aka="AAAAAAAAAAM=">
<pickle>
<global name="TALESMethod" module="Products.Formulator.TALESField"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_text</string> </key>
<value> <string>here/REQUEST/form/selection_name</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
erp5_core (>= 5.4.3)
erp5_base
erp5_pdm
\ No newline at end of file
erp5_pdm
erp5_simulation
\ No newline at end of file
......@@ -35,6 +35,7 @@ Internal Packing List Line | quantity_view
Internal Packing List Line | view
Internal Packing List Line | view_price
Internal Packing List Module | delivery_line_report
Internal Packing List Module | merge_delivery_list
Internal Packing List Module | view
Internal Packing List Module | workflow_report
Internal Packing List | details
......@@ -105,6 +106,7 @@ Purchase Packing List Line | quantity_view
Purchase Packing List Line | view
Purchase Packing List Line | view_price
Purchase Packing List Module | delivery_line_report
Purchase Packing List Module | merge_delivery_list
Purchase Packing List Module | packing_list_export
Purchase Packing List Module | packing_list_report
Purchase Packing List Module | view
......@@ -231,6 +233,7 @@ Sale Packing List Line | profile_view
Sale Packing List Line | quantity_view
Sale Packing List Line | view
Sale Packing List Module | delivery_line_report
Sale Packing List Module | merge_delivery_list
Sale Packing List Module | packing_list_export
Sale Packing List Module | packing_list_report
Sale Packing List Module | shipment_report
......
......@@ -41,7 +41,7 @@ class CausalityAssignmentMovementGroup(MovementGroup):
def _getPropertyDict(self, movement, **kw):
return self._addCausalityToEdit(movement)
def _separate(self, movement_list):
def _separate(self, movement_list, **kw):
if not movement_list:
return []
property_dict = {}
......
......@@ -81,12 +81,19 @@ class MovementGroup(XMLObject):
# This method should be defined in sub classes.
raise NotImplementedError
def _separate(self, movement_list):
def _separate(self, movement_list, merge_delivery=False, **kw):
# By default, we separate movements by _getPropertyDict() values.
# You can override this method in each MovementGroup class.
tmp_dict = {}
first_property_dict = None
collect_order_group_id = self.getCollectOrderGroupId()
for movement in movement_list:
property_dict = self._getPropertyDict(movement)
# We are in the case of merging of deliveries, thus if the movement
# is configured to not split, just take properties of the first movement
if merge_delivery and collect_order_group_id == "delivery":
property_dict = {}
else:
property_dict = self._getPropertyDict(movement, **kw)
# XXX it can be wrong. we need a good way to get hash value, or
# we should compare for all pairs.
key = repr(property_dict)
......@@ -96,11 +103,11 @@ class MovementGroup(XMLObject):
tmp_dict[key] = [[movement], property_dict]
return tmp_dict.values()
def separate(self, movement_list):
def separate(self, movement_list, **kw):
# We sort group of simulation movements by their IDs.
# DO NOT OVERRIDE THIS METHOD. Override _separate() instead.
return sorted([[sorted(x[0], key=lambda x: x.getId()), x[1]] \
for x in self._separate(movement_list)],
for x in self._separate(movement_list, **kw)],
key=lambda x: x[0][0].getId())
def isBranch(self):
......
......@@ -51,7 +51,7 @@ class PropertyAssignmentMovementGroup(MovementGroup):
# We can always update.
return True, property_dict
def _separate(self, movement_list):
def _separate(self, movement_list, **kw):
if not movement_list:
return []
......
......@@ -44,7 +44,7 @@ class PropertyGroupingMovementGroup(MovementGroup):
return True, {}
return False, {}
def _separate(self, movement_list):
def _separate(self, movement_list, **kw):
if not movement_list:
return []
......
......@@ -61,7 +61,7 @@ class QuantitySignMovementGroup(MovementGroup):
property_dict['quantity_sign'] = cmp(quantity, 0)
return property_dict
def _separate(self, movement_list):
def _separate(self, movement_list, **kw):
if not movement_list:
return []
......
......@@ -57,5 +57,5 @@ class SplitMovementGroup(MovementGroup):
def test(self, document, property_dict, **kw):
return True, property_dict
def _separate(self, movement_list):
def _separate(self, movement_list, **kw):
return [[[movement], {}] for movement in movement_list]
......@@ -41,13 +41,15 @@ class MovementGroupNode:
# a separate method requests separating movements.
def __init__(self, movement_group_list=None, movement_list=None,
last_line_movement_group=None,
separate_method_name_list=[], movement_group=None):
separate_method_name_list=[], movement_group=None,
merge_delivery=None):
self._movement_list = []
self._group_list = []
self._movement_group = movement_group
self._movement_group_list = movement_group_list
self._last_line_movement_group = last_line_movement_group
self._separate_method_name_list = separate_method_name_list
self._merge_delivery = merge_delivery
if movement_list is not None :
self.append(movement_list)
......@@ -56,22 +58,24 @@ class MovementGroupNode:
movement_group=self._movement_group_list[0],
movement_group_list=self._movement_group_list[1:],
last_line_movement_group=self._last_line_movement_group,
separate_method_name_list=self._separate_method_name_list)
separate_method_name_list=self._separate_method_name_list,
merge_delivery=self._merge_delivery)
nested_instance.setGroupEdit(**property_dict)
split_movement_list = nested_instance.append(movement_list)
self._group_list.append(nested_instance)
return split_movement_list
def append(self, movement_list):
def append(self, movement_list, **kw):
all_split_movement_list = []
if len(self._movement_group_list):
for separate_movement_list, property_dict in \
self._movement_group_list[0].separate(movement_list):
self._movement_group_list[0].separate(movement_list,
merge_delivery=self._merge_delivery):
split_movement_list = self._appendGroup(separate_movement_list,
property_dict)
if len(split_movement_list):
if self._movement_group == self._last_line_movement_group:
self.append(split_movement_list)
self.append(split_movement_list, **kw)
else:
all_split_movement_list.extend(split_movement_list)
else:
......
......@@ -33,6 +33,7 @@ from AccessControl import ClassSecurityInfo
from Products.ERP5Type.Globals import InitializeClass, DTMLFile
from Products.ERP5Type import Permissions
from Products.ERP5Type.Tool.BaseTool import BaseTool
from Products.ERP5Type.UnrestrictedMethod import UnrestrictedMethod
from Products.ERP5 import _dtmldir
......@@ -2646,8 +2647,20 @@ class SimulationTool(BaseTool):
return result
# Used for mergeDeliveryList.
class MergeDeliveryListError(Exception): pass
def _findBuilderForDelivery(self, delivery):
"""
Find out the builder corresponding to a delivery by looking at the business process
"""
builder = None
portal_type = delivery.getPortalType()
for business_link in delivery.asComposedDocument().objectValues(portal_type="Business Link"):
for business_link_builder in business_link.getDeliveryBuilderValueList():
if business_link_builder.getDeliveryPortalType() == portal_type:
builder = business_link_builder
break
if builder is not None:
break
return builder
security.declareProtected( Permissions.ModifyPortalContent, 'mergeDeliveryList' )
def mergeDeliveryList(self, delivery_list):
......@@ -2656,246 +2669,102 @@ class SimulationTool(BaseTool):
All delivery lines are merged into the first one.
The first one is therefore called main_delivery here.
The others are cancelled.
Return the main delivery.
"""
# Sanity checks.
if len(delivery_list) == 0:
raise self.MergeDeliveryListError, "No delivery is passed"
elif len(delivery_list) == 1:
raise self.MergeDeliveryListError, "Only one delivery is passed"
main_delivery = delivery_list[0]
delivery_list = delivery_list[1:]
# Another sanity check. It is necessary for them to be identical in some attributes.
for delivery in delivery_list:
for attr in ('portal_type', 'simulation_state',
'source', 'destination',
'source_section', 'destination_section',
'source_decision', 'destination_decision',
'source_administration', 'destination_administration',
'source_payment', 'destination_payment'):
main_value = main_delivery.getProperty(attr)
value = delivery.getProperty(attr)
if main_value != value:
raise self.MergeDeliveryListError, \
"%s is not the same between %s and %s (%s and %s)" % (attr, delivery.getId(), main_delivery.getId(), value, main_value)
# One more sanity check. Check if discounts are the same, if any.
main_discount_list = main_delivery.contentValues(filter = {'portal_type': self.getPortalDiscountTypeList()})
for delivery in delivery_list:
discount_list = delivery.contentValues(filter = {'portal_type': self.getPortalDiscountTypeList()})
if len(main_discount_list) != len(discount_list):
raise self.MergeDeliveryListError, "Discount is not the same between %s and %s" % (delivery.getId(), main_delivery.getId())
for discount in discount_list:
for main_discount in main_discount_list:
if discount.getDiscount() == main_discount.getDiscount() \
and discount.getDiscountRatio() == main_discount.getDiscountRatio() \
and discount.getDiscountType() == main_discount.getDiscountType() \
and discount.getImmediateDiscount() == main_discount.getImmediateDiscount():
break
else:
raise self.MergeDeliveryListError, "Discount is not the same between %s and %s" % (delivery.getId(), main_delivery.getId())
# One more sanity check. Check if payment conditions are the same, if any.
main_payment_condition_list = main_delivery.contentValues(filter = {'portal_type': self.getPortalPaymentConditionTypeList()})
for delivery in delivery_list:
payment_condition_list = delivery.contentValues(filter = {'portal_type': self.getPortalPaymentConditionTypeList()})
if len(main_payment_condition_list) != len(payment_condition_list):
raise self.MergeDeliveryListError, "Payment Condition is not the same between %s and %s" % (delivery.getId(), main_delivery.getId())
for condition in payment_condition_list:
for main_condition in main_payment_condition_list:
if condition.getPaymentMode() == main_condition.getPaymentMode() \
and condition.getPaymentAdditionalTerm() == main_condition.getPaymentAdditionalTerm() \
and condition.getPaymentAmount() == main_condition.getPaymentAmount() \
and condition.getPaymentEndOfMonth() == main_condition.getPaymentEndOfMonth() \
and condition.getPaymentRatio() == main_condition.getPaymentRatio() \
and condition.getPaymentTerm() == main_condition.getPaymentTerm():
break
if not(len(delivery_list) >=2):
raise ValueError("Please select at least 2 deliveries")
portal= self.getPortalObject()
translateString = portal.Base_translateString
error_list = []
if len(delivery_list) > 1:
portal_type_set = set([x.getPortalType() for x in delivery_list])
if len(portal_type_set) != 1:
error_list.append(translateString("Please select only deliveries of same type"))
else:
portal_type, = portal_type_set
allowed_state_set = set(portal.getPortalReservedInventoryStateList() + \
portal.getPortalFutureInventoryStateList())
found_state_set = set([x.getSimulationState() for x in delivery_list])
if found_state_set.difference(allowed_state_set):
error_list.append(translateString("Found delivery having unexpected status for merge"))
else:
raise self.MergeDeliveryListError, "Payment Condition is not the same between %s and %s" % (delivery.getId(), main_delivery.getId())
# Make sure that all activities are flushed, to get simulation movements from delivery cells.
for delivery in delivery_list:
for order in delivery.getCausalityValueList(portal_type = self.getPortalOrderTypeList()):
for applied_rule in order.getCausalityRelatedValueList(portal_type = 'Applied Rule'):
applied_rule.flushActivity(invoke = 1)
for causality_related_delivery in delivery.getCausalityValueList(portal_type = self.getPortalDeliveryTypeList()):
for applied_rule in causality_related_delivery.getCausalityRelatedValueList(portal_type = 'Applied Rule'):
applied_rule.flushActivity(invoke = 1)
# Get a list of simulated movements and invoice movements.
main_simulated_movement_list = main_delivery.getSimulatedMovementList()
main_invoice_movement_list = main_delivery.getInvoiceMovementList()
simulated_movement_list = main_simulated_movement_list[:]
invoice_movement_list = main_invoice_movement_list[:]
for delivery in delivery_list:
simulated_movement_list.extend(delivery.getSimulatedMovementList())
invoice_movement_list.extend(delivery.getInvoiceMovementList())
#for movement in simulated_movement_list + invoice_movement_list:
# parent = movement.aq_parent
# LOG('mergeDeliveryList', 0, 'movement = %s, parent = %s, movement.getPortalType() = %s, parent.getPortalType() = %s' % (repr(movement), repr(parent), repr(movement.getPortalType()), repr(parent.getPortalType())))
LOG('mergeDeliveryList', 0, 'simulated_movement_list = %s, invoice_movement_list = %s' % (str(simulated_movement_list), str(invoice_movement_list)))
for main_movement_list, movement_list in \
((main_simulated_movement_list, simulated_movement_list),
(main_invoice_movement_list, invoice_movement_list)):
root_group = self.collectMovement(movement_list,
check_order = 0,
check_path = 0,
check_date = 0,
check_criterion = 1,
check_resource = 1,
check_base_variant = 1,
check_variant = 1)
for criterion_group in root_group.group_list:
LOG('mergeDeliveryList dump tree', 0, 'criterion = %s, movement_list = %s, group_list = %s' % (repr(criterion_group.criterion), repr(criterion_group.movement_list), repr(criterion_group.group_list)))
for resource_group in criterion_group.group_list:
LOG('mergeDeliveryList dump tree', 0, 'resource = %s, movement_list = %s, group_list = %s' % (repr(resource_group.resource), repr(resource_group.movement_list), repr(resource_group.group_list)))
for base_variant_group in resource_group.group_list:
LOG('mergeDeliveryList dump tree', 0, 'base_category_list = %s, movement_list = %s, group_list = %s' % (repr(base_variant_group.base_category_list), repr(base_variant_group.movement_list), repr(base_variant_group.group_list)))
for variant_group in base_variant_group.group_list:
LOG('mergeDeliveryList dump tree', 0, 'category_list = %s, movement_list = %s, group_list = %s' % (repr(variant_group.category_list), repr(variant_group.movement_list), repr(variant_group.group_list)))
for criterion_group in root_group.group_list:
for resource_group in criterion_group.group_list:
for base_variant_group in resource_group.group_list:
# Get a list of categories.
category_dict = {}
for variant_group in base_variant_group.group_list:
for category in variant_group.category_list:
category_dict[category] = 1
category_list = category_dict.keys()
# Try to find a delivery line.
delivery_line = None
for movement in base_variant_group.movement_list:
if movement in main_movement_list:
if movement.aq_parent.getPortalType() in self.getPortalSimulatedMovementTypeList() \
or movement.aq_parent.getPortalType() in self.getPortalInvoiceMovementTypeList():
delivery_line = movement.aq_parent
else:
delivery_line = movement
LOG('mergeDeliveryList', 0, 'delivery_line %s is found: criterion = %s, resource = %s, base_category_list = %s' % (repr(delivery_line), repr(criterion_group.criterion), repr(resource_group.resource), repr(base_variant_group.base_category_list)))
break
if delivery_line is None:
# Not found. So create a new delivery line.
movement = base_variant_group.movement_list[0]
if movement.aq_parent.getPortalType() in self.getPortalSimulatedMovementTypeList() \
or movement.aq_parent.getPortalType() in self.getPortalInvoiceMovementTypeList():
delivery_line_type = movement.aq_parent.getPortalType()
else:
delivery_line_type = movement.getPortalType()
delivery_line = main_delivery.newContent(portal_type = delivery_line_type,
resource = resource_group.resource)
LOG('mergeDeliveryList', 0, 'New delivery_line %s is created: criterion = %s, resource = %s, base_category_list = %s' % (repr(delivery_line), repr(criterion_group.criterion), repr(resource_group.resource), repr(base_variant_group.base_category_list)))
# Update the base categories and categories.
#LOG('mergeDeliveryList', 0, 'base_category_list = %s, category_list = %s' % (repr(base_category_list), repr(category_list)))
delivery_line.setVariationBaseCategoryList(base_variant_group.base_category_list)
delivery_line.setVariationCategoryList(category_list)
object_to_update = None
for variant_group in base_variant_group.group_list:
if len(variant_group.category_list) == 0:
object_to_update = delivery_line
else:
for delivery_cell in delivery_line.contentValues():
predicate_value_list = delivery_cell.getPredicateValueList()
LOG('mergeDeliveryList', 0, 'delivery_cell = %s, predicate_value_list = %s, variant_group.category_list = %s' % (repr(delivery_cell), repr(predicate_value_list), repr(variant_group.category_list)))
if len(predicate_value_list) == len(variant_group.category_list):
for category in variant_group.category_list:
if category not in predicate_value_list:
break
else:
object_to_update = delivery_cell
break
#LOG('mergeDeliveryList', 0, 'object_to_update = %s' % repr(object_to_update))
if object_to_update is not None:
cell_price = object_to_update.getPrice() or 0.0
cell_quantity = object_to_update.getQuantity() or 0.0
cell_target_quantity = object_to_update.getNetConvertedTargetQuantity() or 0.0 # XXX What to do ?
cell_total_price = cell_target_quantity * cell_price
cell_category_list = list(object_to_update.getCategoryList())
for movement in variant_group.movement_list:
if movement in main_movement_list:
continue
LOG('mergeDeliveryList', 0, 'movement = %s' % repr(movement))
cell_quantity += movement.getQuantity()
cell_target_quantity += movement.getNetConvertedTargetQuantity()
try:
# XXX WARNING - ADD PRICED QUANTITY
cell_price = movement.getPrice()
cell_total_price += movement.getNetConvertedTargetQuantity() * cell_price
except TypeError:
cell_total_price = None
for category in movement.getCategoryList():
if category not in cell_category_list:
cell_category_list.append(category)
# Make sure that simulation movements point to an appropriate delivery line or
# delivery cell.
if hasattr(movement, 'getDeliveryRelatedValueList'):
for simulation_movement in \
movement.getDeliveryRelatedValueList(portal_type = 'Simulation Movement'):
simulation_movement.setDeliveryValue(object_to_update)
#simulation_movement.reindexObject()
if hasattr(movement, 'getOrderRelatedValueList'):
for simulation_movement in \
movement.getOrderRelatedValueList(portal_type = 'Simulation Movement'):
simulation_movement.setOrderValue(object_to_update)
#simulation_movement.reindexObject()
if cell_target_quantity != 0 and cell_total_price is not None:
average_price = cell_total_price / cell_target_quantity
else:
average_price = 0
LOG('mergeDeliveryList', 0, 'object_to_update = %s, cell_category_list = %s, cell_target_quantity = %s, cell_quantity = %s, average_price = %s' % (repr(object_to_update), repr(cell_category_list), repr(cell_target_quantity), repr(cell_quantity), repr(average_price)))
object_to_update.setCategoryList(cell_category_list)
if object_to_update.getPortalType() in self.getPortalSimulatedMovementTypeList():
object_to_update.edit(target_quantity = cell_target_quantity,
quantity = cell_quantity,
price = average_price,
)
elif object_to_update.getPortalType() in self.getPortalInvoiceMovementTypeList():
# Invoices do not have target quantities, and the price never change.
object_to_update.edit(quantity = cell_quantity,
price = cell_price,
)
else:
raise self.MergeDeliveryListError, "Unknown portal type %s" % str(object_to_update.getPortalType())
else:
raise self.MergeDeliveryListError, "No object to update"
# Merge containers. Just copy them from other deliveries into the main.
for delivery in delivery_list:
container_id_list = delivery.contentIds(filter = {'portal_type': self.getPortalContainerTypeList()})
if len(container_id_list) > 0:
copy_data = delivery.manage_copyObjects(ids = container_id_list)
new_id_list = main_delivery.manage_pasteObjects(copy_data)
# Unify the list of causality.
causality_list = main_delivery.getCausalityValueList()
for delivery in delivery_list:
for causality in delivery.getCausalityValueList():
if causality not in causality_list:
causality_list.append(causality)
LOG("mergeDeliveryList", 0, "causality_list = %s" % str(causality_list))
main_delivery.setCausalityValueList(causality_list)
# Cancel deliveries.
for delivery in delivery_list:
LOG("mergeDeliveryList", 0, "cancelling %s" % repr(delivery))
delivery.cancel()
# Reindex the main delivery.
main_delivery.reindexObject()
return main_delivery
# Allow to call a script to do custom checking conditions before merge
main_delivery = delivery_list[0]
check_merge_condition_method = main_delivery._getTypeBasedMethod("checkMergeConditionOnDeliveryList")
if check_merge_condition_method is not None:
error_list.extend(check_merge_condition_method(delivery_list=delivery_list))
if len(error_list) == 0:
# so far so good
# in delivery_list we have list of delivery to merge
simulation_movement_list = []
to_copy_delivery_line_list = [] # for lines not coming from upper simulation, thus
# created by hand should be manually added to main
# delivery since they are not coming from builder
for delivery in delivery_list:
line_id_to_delete_list = []
for movement in delivery.getMovementList():
related_simulation_movement_list = movement.getDeliveryRelatedValueList()
for simulation_movement in related_simulation_movement_list:
# if we are on a root applied rule directly, so in the case of
# a manually added line, we have to copy
# the simulation movement into to main delivery
if simulation_movement.getParentValue().getParentValue().getId() == "portal_simulation":
# For manually added lines, make sure we have only one simulation movement
assert len(related_simulation_movement_list) == 1
if not(delivery is main_delivery):
to_copy_delivery_line_list.append(movement)
else:
simulation_movement.setDeliveryValue(None)
simulation_movement_list.append(simulation_movement)
# Since we keep the main delivery, we remove existing lines already
# coming from builder to let builder recreate them in the same time
# as other ones (to possibly merge lines also)
movement_id = movement.getId()
if delivery is main_delivery and not(movement_id in line_id_to_delete_list):
line_id_to_delete_list.append(movement.getId())
if line_id_to_delete_list:
delivery.manage_delObjects(ids=line_id_to_delete_list)
# It is required to expand again simulation movement, because
# we unlinked them from delivery, so it is possible that some
# properties will change on simulation movement (mostly categories).
# By expanding again, we will avoid having many deliveries instead
# of one when doing "merge"
for simulation_movement in simulation_movement_list:
simulation_movement.expand(expand_policy='immediate')
# activate builder
merged_builder = self._findBuilderForDelivery(main_delivery)
if merged_builder is None:
error_list.append(translateString("Unable to find builder"))
else:
merged_builder.build(movement_relative_url_list=[q.getRelativeUrl() for q in \
simulation_movement_list], merge_delivery=True,
delivery_relative_url_list=[main_delivery.getRelativeUrl()])
# Finally, copy all lines that were created manually on all deliveries except
# the main one
@UnrestrictedMethod
def setMainDeliveryModifiable(delivery):
# set causality state in such way we can modify delivery
delivery.diverge()
setMainDeliveryModifiable(main_delivery)
delivery_type_list = portal.getPortalDeliveryTypeList()
for delivery_line in to_copy_delivery_line_list:
delivery = delivery_line.getParentValue()
if not(delivery.getPortalType() in delivery_type_list):
raise NotImplementedError("Merge of deliveries doe not yet handle case of cells")
copy_data = delivery.manage_copyObjects(ids=[delivery_line.getId()])
main_delivery.manage_pasteObjects(copy_data)
main_delivery.updateCausalityState()
# Finally do cleanup
for delivery in delivery_list[1:]:
# cancel, delete - to disallow any user related operations on those deliveries
after_merge_method = delivery._getTypeBasedMethod('cleanDeliveryAfterMerge')
if after_merge_method is not None:
after_merge_method()
else:
error_list.append(translateString("Please select at least two deliveries"))
return error_list
#######################################################
# Sequence
......
......@@ -101,7 +101,8 @@ class BuilderMixin(XMLObject, Amount, Predicate):
security.declarePublic('build')
def build(self, applied_rule_uid=None, movement_relative_url_list=None,
delivery_relative_url_list=None, movement_list=None,
explanation=None, business_link=None, activate_kw=None, **kw):
explanation=None, business_link=None, activate_kw=None,
merge_delivery=None, **kw):
"""
Build deliveries from a list of movements
......@@ -136,13 +137,13 @@ class BuilderMixin(XMLObject, Amount, Predicate):
if not movement_list:
return []
# Collect
root_group_node = self.collectMovement(movement_list)
root_group_node = self.collectMovement(movement_list, merge_delivery=merge_delivery)
# Build
delivery_list = self.buildDeliveryList(
root_group_node,
delivery_relative_url_list=delivery_relative_url_list,
movement_list=movement_list, activate_kw=activate_kw,
**kw)
merge_delivery=merge_delivery, **kw)
# Call a script after building
self.callAfterBuildingScript(delivery_list, movement_list, **kw)
return delivery_list
......@@ -297,7 +298,7 @@ class BuilderMixin(XMLObject, Amount, Predicate):
searchMovementList = UnrestrictedMethod(_searchMovementList)
security.declarePrivate('collectMovement')
def collectMovement(self, movement_list):
def collectMovement(self, movement_list, merge_delivery=False):
"""
group movements in the way we want. Thanks to this method, we are able
to retrieve movement classed by order, resource, criterion,....
......@@ -314,7 +315,8 @@ class BuilderMixin(XMLObject, Amount, Predicate):
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)
last_line_movement_group=last_line_movement_group,
merge_delivery=merge_delivery)
root_group_node.append(movement_list)
return root_group_node
......@@ -396,14 +398,16 @@ class BuilderMixin(XMLObject, Amount, Predicate):
if update:
delivery_to_update_list = [portal.restrictedTraverse(relative_url) for \
relative_url in delivery_relative_url_list]
# Deliveries we are trying to update
delivery_select_method_id = self.getDeliverySelectMethodId()
if delivery_select_method_id not in ["", None]:
to_update_delivery_sql_list = getattr(self, delivery_select_method_id) \
(movement_list=movement_list)
delivery_to_update_list.extend([sql_delivery.getObject() \
for sql_delivery \
in to_update_delivery_sql_list])
# Only use select method when the list of delivery is not already provided
if len(delivery_to_update_list) == 0:
# Deliveries we are trying to update
delivery_select_method_id = self.getDeliverySelectMethodId()
if delivery_select_method_id not in ["", None]:
to_update_delivery_sql_list = getattr(self, delivery_select_method_id) \
(movement_list=movement_list)
delivery_to_update_list.extend([sql_delivery.getObject() \
for sql_delivery \
in to_update_delivery_sql_list])
else:
delivery_to_update_list = []
# We do not want to update the same object more than twice in one
......@@ -431,7 +435,8 @@ class BuilderMixin(XMLObject, Amount, Predicate):
collect_order_list, movement_group_node_list=None,
delivery_to_update_list=None,
divergence_list=None,
activate_kw=None, force_update=0, **kw):
activate_kw=None, force_update=0,
merge_delivery=None, **kw):
"""
Build delivery from a list of movement
"""
......@@ -457,7 +462,8 @@ class BuilderMixin(XMLObject, Amount, Predicate):
delivery_to_update_list=delivery_to_update_list,
divergence_list=divergence_list,
activate_kw=activate_kw,
force_update=force_update)
force_update=force_update,
merge_delivery=merge_delivery)
delivery_list.extend(new_delivery_list)
force_update = 0
else:
......@@ -467,9 +473,14 @@ class BuilderMixin(XMLObject, Amount, Predicate):
x for x in delivery_to_update_list \
if x.getPortalType() == self.getDeliveryPortalType() and \
not self._isUpdated(x, 'delivery')]
delivery, property_dict = self._findUpdatableObject(
delivery_to_update_list, movement_group_node_list,
divergence_list)
if merge_delivery:
# We must have only one delivery to update in the case of merge
delivery, = delivery_to_update_list
property_dict = {}
else:
delivery, property_dict = self._findUpdatableObject(
delivery_to_update_list, movement_group_node_list,
divergence_list)
# if all deliveries are rejected in case of update, we update the
# first one.
......
......@@ -1010,6 +1010,48 @@ class TestPackingListMixin(TestOrderMixin):
after_tag=after_tag,
).build(explanation_uid=packing_list.getCausalityValue().getUid())
def stepMergeSplittedPackingList(self, sequence=None):
"""
Invoke the merge of the two sales packing list and check the merged packing list
Then also try to create a packing list not coming from order, and then
tro to merge it with the merged packing list
"""
# Merge the two existing packing list
packing_list1 = sequence.get('packing_list')
packing_list2 = sequence.get('new_packing_list')
self.portal.portal_simulation.mergeDeliveryList([packing_list1, packing_list2])
self.tic()
self.assertEqual('confirmed', packing_list1.getSimulationState())
self.assertEqual('cancelled', packing_list2.getSimulationState())
line, = packing_list1.objectValues(
portal_type= self.packing_list_line_portal_type)
self.assertEqual(self.default_quantity,line.getQuantity())
self.assertTrue(packing_list1.getStartDate() is not None)
self.assertTrue(packing_list1.getStopDate() is not None)
# Now clone the merged packing list, so that we will have :
# - one packing list coming from order (merged_packing_list)
# - one not coming from order (the cloned one)
cloned_packing_list = packing_list1.Base_createCloneDocument(batch_mode=True)
cloned_packing_list.setStartDate(cloned_packing_list.getStartDate() + 1)
cloned_packing_list.setStopDate(cloned_packing_list.getStopDate() + 1)
cloned_line, = cloned_packing_list.objectValues()
cloned_line.setQuantity(self.default_quantity+1)
self.portal.portal_workflow.doActionFor(cloned_packing_list, "confirm_action")
self.tic()
self.portal.portal_simulation.mergeDeliveryList([packing_list1, cloned_packing_list])
self.tic()
self.assertEqual('confirmed', packing_list1.getSimulationState())
self.assertEqual('cancelled', cloned_packing_list.getSimulationState())
resource = sequence.get('resource').getRelativeUrl()
def checkLineSet(delivery, expected_set):
line_list = delivery.getMovementList()
self.assertEqual(len(line_list), len(expected_set))
found_set = set([(x.getResource(), x.getQuantity(), x.getPrice()) for x in line_list])
expected_set = set([(resource, self.default_quantity, 555),
(resource, self.default_quantity+1, 555)])
checkLineSet(packing_list1, expected_set)
class TestPackingList(TestPackingListMixin, ERP5TypeTestCase) :
run_all_test = 1
......@@ -1020,6 +1062,8 @@ class TestPackingList(TestPackingListMixin, ERP5TypeTestCase) :
Change the quantity on an delivery line, then
see if the packing list is divergent and then
split and defer the packing list
Finally, check we can merge if needed
"""
if not run: return
sequence_list = SequenceList()
......@@ -1033,6 +1077,7 @@ class TestPackingList(TestPackingListMixin, ERP5TypeTestCase) :
Tic
CheckPackingListIsSolved
CheckPackingListSplitted
MergeSplittedPackingList
"""
sequence_list.addSequenceString(sequence_string)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment