Commit 6b696094 authored by Romain Courteaud's avatar Romain Courteaud

Start to implement the new simulation rules.

http://erp5.org/Discussion/SimulationRules

By Alexandre Boeglin and Rafael Monnerat.
Verified by Romain Courteaud.

Currently, the rule 18 is not implemented (we don't copy yet order 
movement's properties to the simulation movement)

The behavior of the simulation should not be changed by this commit.



git-svn-id: https://svn.erp5.org/repos/public/erp5/trunk@9783 20353a03-c40f-0410-a6d1-a30d3c3de9de
parent c4b84da0
...@@ -86,7 +86,7 @@ class AmortisationRule(Rule): ...@@ -86,7 +86,7 @@ class AmortisationRule(Rule):
'correction': 'correction' 'correction': 'correction'
} }
def test(self, movement): def _test(self, movement):
""" """
Tests if the rule (still) applies Tests if the rule (still) applies
""" """
......
...@@ -28,7 +28,6 @@ ...@@ -28,7 +28,6 @@
from AccessControl import ClassSecurityInfo from AccessControl import ClassSecurityInfo
from Products.CMFCore.utils import getToolByName from Products.CMFCore.utils import getToolByName
from Products.CMFCore.WorkflowCore import WorkflowMethod
from Products.ERP5Type import Permissions, PropertySheet, Constraint, Interface from Products.ERP5Type import Permissions, PropertySheet, Constraint, Interface
from Products.ERP5Type.XMLObject import XMLObject from Products.ERP5Type.XMLObject import XMLObject
from Products.ERP5Type.PsycoWrapper import psyco from Products.ERP5Type.PsycoWrapper import psyco
...@@ -37,20 +36,21 @@ from zLOG import LOG ...@@ -37,20 +36,21 @@ from zLOG import LOG
class AppliedRule(XMLObject): class AppliedRule(XMLObject):
""" """
An applied rule holds a list of simulation movements An applied rule holds a list of simulation movements.
An applied rule points to an instance of Rule An applied rule points to an instance of Rule (which defines the actual
(which defines the actual rule to apply with its parameters) rule to apply with its parameters) through the specialise relation.
An applied rule can expand itself (look at its direct parent An applied rule can expand itself (look at its direct parent and take
and take conclusions on what should be inside). This is similar conclusions on what should be inside).
to the base_fix_consistency mechanism
An applied rule can "solve" or "backtrack". In this case An applied rule can tell if it is stable (if its children are consistent
it looks at its children, looks at the difference between with what would be expanded from its direct parent).
target and actual, and takes conclusions on its parent
All algorithms are implemented by the rule An applied rule can tell if any of his direct children is divergent (not
consistent with the delivery).
All algorithms are implemented by the rule.
""" """
# CMF Type Definition # CMF Type Definition
...@@ -78,17 +78,17 @@ class AppliedRule(XMLObject): ...@@ -78,17 +78,17 @@ class AppliedRule(XMLObject):
""" """
Tests if the rule (still) applies Tests if the rule (still) applies
""" """
my_parent = self.aq_parent if self.isRootAppliedRule():
if my_parent is None: # Should be is portal_simulation
return 1 return 1
else: else:
parent_value = self.aq_parent
rule = self.getSpecialiseValue() rule = self.getSpecialiseValue()
return rule.test(my_parent) return rule.test(parent_value)
security.declareProtected(Permissions.AccessContentsInformation, security.declareProtected(Permissions.AccessContentsInformation,
'isAccountable') 'isAccountable')
def isAccountable(self, movement): def isAccountable(self, movement):
"""Tells wether generated movement needs to be accounted or not.""" """Tells whether generated movement needs to be accounted or not."""
return self.getSpecialiseValue().isAccountable(movement) return self.getSpecialiseValue().isAccountable(movement)
security.declareProtected(Permissions.ModifyPortalContent, 'expand') security.declareProtected(Permissions.ModifyPortalContent, 'expand')
...@@ -114,8 +114,6 @@ class AppliedRule(XMLObject): ...@@ -114,8 +114,6 @@ class AppliedRule(XMLObject):
# "recursiveImmediateReindexObject"]).\ # "recursiveImmediateReindexObject"]).\
# notifySimulationChange(rule._v_notify_dict) # notifySimulationChange(rule._v_notify_dict)
#expand = WorkflowMethod(expand)
security.declareProtected(Permissions.ModifyPortalContent, 'solve') security.declareProtected(Permissions.ModifyPortalContent, 'solve')
def solve(self, solution_list): def solve(self, solution_list):
""" """
...@@ -133,8 +131,6 @@ class AppliedRule(XMLObject): ...@@ -133,8 +131,6 @@ class AppliedRule(XMLObject):
if rule is not None: if rule is not None:
rule.solve(self) rule.solve(self)
#solve = WorkflowMethod(solve)
security.declareProtected(Permissions.ModifyPortalContent, 'diverge') security.declareProtected(Permissions.ModifyPortalContent, 'diverge')
def diverge(self): def diverge(self):
""" """
...@@ -147,38 +143,38 @@ class AppliedRule(XMLObject): ...@@ -147,38 +143,38 @@ class AppliedRule(XMLObject):
if rule is not None: if rule is not None:
rule.diverge(self) rule.diverge(self)
#diverge = WorkflowMethod(diverge)
# Solvers # Solvers
security.declareProtected(Permissions.View, 'isDivergent') security.declareProtected(Permissions.AccessContentsInformation,
def isDivergent(self): 'isStable')
def isStable(self):
""" """
Returns 1 if divergent rule Tells whether the rule is stable or not.
""" """
rule = self.getSpecialiseValue() return self.getSpecialiseValue().isStable(self)
if rule is not None:
return rule.isDivergent(self) security.declareProtected(Permissions.AccessContentsInformation,
return 0 'isDivergent')
def isDivergent(self, movement):
"""
Tells whether generated movement is divergent or not.
"""
return self.getSpecialiseValue().isDivergent(movement)
security.declareProtected(Permissions.View, 'getDivergenceList') security.declareProtected(Permissions.AccessContentsInformation,
def getDivergenceList(self): 'getDivergenceList')
def getDivergenceList(self, movement):
""" """
Returns a list Divergence descriptors Returns a list Divergence descriptors
""" """
rule = self.getSpecialiseValue() return self.getSpecialiseValue().getDivergenceList(movement)
if rule is not None:
return rule.getDivergenceList(self)
return ()
security.declareProtected(Permissions.View, 'getSolverList') security.declareProtected(Permissions.AccessContentsInformation,
def getSolverList(self): 'getSolverList')
def getSolverList(self, movement):
""" """
Returns a list Divergence solvers Returns a list Divergence solvers
""" """
rule = self.getSpecialiseValue() return self.getSpecialiseValue().getSolverList(movement)
if rule is not None:
return rule.getSolverList(self)
return ()
security.declareProtected(Permissions.AccessContentsInformation, security.declareProtected(Permissions.AccessContentsInformation,
'isRootAppliedRule') 'isRootAppliedRule')
......
...@@ -58,8 +58,9 @@ class DeliveryRule(Rule): ...@@ -58,8 +58,9 @@ class DeliveryRule(Rule):
, PropertySheet.DublinCore , PropertySheet.DublinCore
) )
def test(self, movement): def _test(self, movement):
""" """
Default behaviour of DeliveryRule.test
Tests if the rule (still) applies Tests if the rule (still) applies
""" """
# A delivery rule never applies # A delivery rule never applies
...@@ -68,91 +69,93 @@ class DeliveryRule(Rule): ...@@ -68,91 +69,93 @@ class DeliveryRule(Rule):
# Simulation workflow # Simulation workflow
security.declareProtected(Permissions.ModifyPortalContent, 'expand') security.declareProtected(Permissions.ModifyPortalContent, 'expand')
def expand(self, applied_rule, def expand(self, applied_rule, **kw):
movement_type_method='getPortalOrderMovementTypeList', **kw): """
""" Expands the additional Delivery movements to a new simulation tree.
Expands the current movement downwards. Expand is only allowed to create or modify simulation movements for
-> new status -> expanded delivery lines which are not already linked to another simulation
An applied rule can be expanded only if its parent movement movement.
is expanded.
""" If the movement is not in current state, has no delivered child, and not
delivery_line_type = 'Simulation Movement' in delivery movements, it can be deleted.
# Get the delivery where we come from Else if the movement is not in current state, it can be modified.
# Causality is a kind of Delivery (ex. Packing List) Else, it cannot be modified.
my_delivery = applied_rule.getDefaultCausalityValue() """
# Only expand if my_delivery is not None movement_type = 'Simulation Movement'
if my_delivery is not None: existing_movement_list = []
#if my_delivery.getSimulationState() not in ('delivered', ): immutable_movement_list = []
# Even if delivered, we should always calculate consequences delivery = applied_rule.getDefaultCausalityValue()
if delivery is not None:
# First, check each contained movement and make delivery_movement_list = delivery.getMovementList()
# a list of delivery uids which do not need to be copied # Check existing movements
# eventually delete movement which do not exist anylonger for movement in applied_rule.contentValues(portal_type=movement_type):
existing_uid_list = [] if movement.getLastExpandSimulationState() not in \
existing_uid_list_append = existing_uid_list.append delivery.getPortalCurrentInventoryStateList():
order_movement_type_list = getattr(applied_rule,
movement_type_method)() movement_delivery = movement.getDeliveryValue()
if not self._isTreeDelivered([movement], ignore_first=1) and \
for movement in applied_rule.objectValues() : movement_delivery not in delivery_movement_list:
delivery_value = movement.getDeliveryValue(
portal_type=order_movement_type_list)
if (delivery_value is None) or\
(delivery_value.hasCellContent()) or\
(len(delivery_value.getDeliveryRelatedValueList()) > 1):
# Our delivery_value is already related
# to another simulation movement
# Delete ourselve
# XXX Make sure this is not deleted if already in delivery
applied_rule._delObject(movement.getId()) applied_rule._delObject(movement.getId())
else: else:
existing_uid_list_append(delivery_value.getUid()) existing_movement_list.append(movement)
else:
# Copy each movement (line or cell) from the delivery is that existing_movement_list.append(movement)
for delivery_movement in my_delivery.getMovementList(): immutable_movement_list.append(movement)
simulation_movement_to_update_list = delivery_movement.\
getOrderRelatedValueList(portal_type = 'Simulation Movement') # Create or modify movements
try: for movement in delivery.getMovementList():
if len(delivery_movement.getDeliveryRelatedValueList()) == 0: related_delivery = movement.getDeliveryRelatedValue()
# Only create if orphaned movement if related_delivery is None:
if delivery_movement.getUid() not in existing_uid_list: # create a new movement
# Generate a nicer ID if movement.getParentUid() == movement.getExplanationUid():
if delivery_movement.getParentUid() ==\
delivery_movement.getExplanationUid():
# We are on a line # We are on a line
new_id = delivery_movement.getId() new_id = movement.getId()
else: else:
# On a cell # Weare on a cell
new_id = "%s_%s" % (delivery_movement.getParentId(), new_id = "%s_%s" % (movement.getParentId(), movement.getId())
delivery_movement.getId())
# Generate the simulation movement # Generate the simulation movement
new_sim_mvt = applied_rule.newContent( new_sim_mvt = applied_rule.newContent(
id = new_id, portal_type=movement_type,
portal_type = delivery_line_type, id=new_id,
order_value = delivery_movement) order_value=movement,
simulation_movement_to_update_list.append(new_sim_mvt) order_ratio=1,
delivery_value=movement,
for simulation_movement in simulation_movement_to_update_list : delivery_ratio=1,
simulation_movement.edit( deliverable=1,
delivery_value=delivery_movement, source=movement.getSource(),
# XXX Do we need to copy the quantity source_section=movement.getSourceSection(),
# Why not the resource, the variation,... destination=movement.getDestination(),
# force_update is required in order destination_section=movement.getDestinationSection(),
# to make sure the quantity is stored quantity=movement.getQuantity(),
# on the movement resource=movement.getResource(),
quantity=delivery_movement.getQuantity(), variation_category_list=movement.getVariationCategoryList(),
variation_category_list=\ variation_property_dict=movement.getVariationPropertyDict(),
delivery_movement.getVariationCategoryList(), start_date=movement.getStartDate(),
stop_date=movement.getStopDate())
elif related_delivery in existing_movement_list:
if related_delivery not in immutable_movement_list:
# modification allowed
related_delivery.edit(
delivery_value=movement,
delivery_ratio=1, delivery_ratio=1,
deliverable=1, deliverable=1,
source=movement.getSource(),
source_section=movement.getSourceSection(),
destination=movement.getDestination(),
destination_section=movement.getDestinationSection(),
quantity=movement.getQuantity(),
resource=movement.getResource(),
variation_category_list=movement.getVariationCategoryList(),
variation_property_dict=movement.getVariationPropertyDict(),
start_date=movement.getStartDate(),
stop_date=movement.getStopDate(),
force_update=1) force_update=1)
else:
# modification disallowed, must compensate
pass
except AttributeError: # Now we can set the last expand simulation state to the current state
LOG('ERP5: WARNING', 0, applied_rule.setLastExpandSimulationState(delivery.getSimulationState())
'AttributeError during expand on delivery line %s'\
% delivery_movement.absolute_url())
# Pass to base class # Pass to base class
Rule.expand(self, applied_rule, **kw) Rule.expand(self, applied_rule, **kw)
...@@ -180,29 +183,38 @@ class DeliveryRule(Rule): ...@@ -180,29 +183,38 @@ class DeliveryRule(Rule):
""" """
# Solvers # Solvers
security.declareProtected(Permissions.View, 'isDivergent') security.declareProtected(Permissions.AccessContentsInformation, 'isStable')
def isDivergent(self, applied_rule): def isStable(self, movement):
""" """
Returns 1 if divergent rule Checks that the applied_rule is stable
""" """
return 0
security.declareProtected(Permissions.View, 'getDivergenceList') security.declareProtected(Permissions.AccessContentsInformation, 'isDivergent')
def isDivergent(self, movement):
"""
Checks that the movement is divergent
"""
return Rule.isDivergent(self, movement)
security.declareProtected(Permissions.AccessContentsInformation, 'getDivergenceList')
def getDivergenceList(self, applied_rule): def getDivergenceList(self, applied_rule):
""" """
Returns a list Divergence descriptors Returns a list Divergence descriptors
""" """
security.declareProtected(Permissions.View, 'getSolverList') security.declareProtected(Permissions.AccessContentsInformation, 'getSolverList')
def getSolverList(self, applied_rule): def getSolverList(self, applied_rule):
""" """
Returns a list Divergence solvers Returns a list Divergence solvers
""" """
# Deliverability / orderability # Deliverability / orderability
def isOrderable(self, m): def isOrderable(self, movement):
return 1 return 1
def isDeliverable(self, m): def isDeliverable(self, movement):
if m.getSimulationState() in m.getPortalDraftOrderStateList(): if movement.getSimulationState() in movement.getPortalDraftOrderStateList():
return 0 return 0
return 1 return 1
...@@ -39,8 +39,6 @@ class InvoiceTransactionRule(Rule, PredicateMatrix): ...@@ -39,8 +39,6 @@ class InvoiceTransactionRule(Rule, PredicateMatrix):
for each invoice movement based on category membership and for each invoice movement based on category membership and
other predicated. Template accounting movements are stored other predicated. Template accounting movements are stored
in cells inside an instance of the InvoiceTransactionRule. in cells inside an instance of the InvoiceTransactionRule.
WARNING: what to do with movement split ?
""" """
# CMF Type Definition # CMF Type Definition
...@@ -64,7 +62,7 @@ class InvoiceTransactionRule(Rule, PredicateMatrix): ...@@ -64,7 +62,7 @@ class InvoiceTransactionRule(Rule, PredicateMatrix):
, PropertySheet.DublinCore , PropertySheet.DublinCore
) )
def test(self, movement): def _test(self, movement):
""" """
Tests if the rule (still) applies Tests if the rule (still) applies
""" """
...@@ -80,12 +78,118 @@ class InvoiceTransactionRule(Rule, PredicateMatrix): ...@@ -80,12 +78,118 @@ class InvoiceTransactionRule(Rule, PredicateMatrix):
return 1 return 1
return 0 return 0
#### Helper method for expand
def _generatePrevisionList(self, applied_rule, **kw):
"""
Generate a list of movements, that should be children of this rule,
based on its context (parent movement, delivery, configuration ...)
These previsions are acrually returned as dictionaries.
"""
prevision_list = []
context_movement = applied_rule.getParentValue()
# Find a matching cell
cell = self._getMatchingCell(context_movement)
if cell is not None : # else, we do nothing
for transaction_line in cell.objectValues() :
# get the resource (in that order):
# * resource from the invoice (using deliveryValue)
# * price_currency from the invoice
# * price_currency from the parents simulation movement's
# deliveryValue
# * price_currency from the top level simulation movement's
# orderValue
resource = None
invoice_line = context_movement.getDeliveryValue()
if invoice_line is not None :
invoice = invoice_line.getExplanationValue()
resource = invoice.getProperty('resource',
invoice.getProperty('price_currency', None))
if resource is None :
# search the resource on parents simulation movement's deliveries
simulation_movement = applied_rule.getParentValue()
portal_simulation = self.getPortalObject().portal_simulation
while resource is None and \
simulation_movement != portal_simulation :
delivery = simulation_movement.getDeliveryValue()
if delivery is not None:
resource = delivery.getProperty('price_currency', None)
if simulation_movement.getParentValue().getParentValue() \
== portal_simulation :
# we are on the first simulation movement, we'll try
# to get the resource from it's order price currency.
order = simulation_movement.getOrderValue()
resource = order.getProperty('price_currency', None)
simulation_movement = simulation_movement\
.getParentValue().getParentValue()
if resource is None :
# last resort : get the resource from the rule
resource = transaction_line.getResource() or cell.getResource()
if resource in (None, '') :
# XXX this happen in many order, so this log is probably useless
LOG("InvoiceTransactionRule", PROBLEM,
"expanding %s: without resource" % applied_rule.getPath())
prevision_line = {}
prevision_line.update(
id = transaction_line.getId(),
source = transaction_line.getSource(),
destination = transaction_line.getDestination(),
source_section = context_movement.getSourceSection(),
destination_section = context_movement.getDestinationSection(),
resource = resource,
# calculate (quantity * price) * cell_quantity
quantity = (context_movement.getCorrectedQuantity() *
context_movement.getPrice()) * transaction_line.getQuantity(),
start_date = context_movement.getStartDate(),
stop_date = context_movement.getStopDate(),
force_update = 1)
prevision_list.append(prevision_line)
return prevision_list
security.declareProtected(Permissions.ModifyPortalContent, 'expand') security.declareProtected(Permissions.ModifyPortalContent, 'expand')
def expand(self, applied_rule, force=0, **kw): def expand(self, applied_rule, force=0, **kw):
""" Expands the current movement downward.
""" """
invoice_transaction_line_type = 'Simulation Movement' Expands the rule:
- generate a list of previsions
- compare the prevision with existing children
- get the list of existing movements (immutable, mutable, deletable)
- compute the difference between prevision and existing (add,
modify, remove)
- add/modify/remove child movements to match prevision
"""
add_list, modify_dict, \
delete_list = self._getCompensatedMovementList(applied_rule,
matching_property_list=['resource', 'source', 'destination'], **kw)
if len(add_list) or len(modify_dict):
pass#import pdb; pdb.set_trace()
for movement_id in delete_list:
applied_rule._delObject(movement_id)
for movement, prop_dict in modify_dict.items():
applied_rule[movement].edit(**prop_dict)
for movement_dict in add_list:
if 'id' in movement_dict.keys():
mvmt_id = applied_rule._get_id(movement_dict.pop('id'))
new_mvmt = applied_rule.newContent(id=mvmt_id,
portal_type=self.movement_type)
else:
new_mvmt = applied_rule.newContent(portal_type=self.movement_type)
new_mvmt.edit(**movement_dict)
# Pass to base class
Rule.expand(self, applied_rule, force=force, **kw)
#### old expand method kept for reference
def old_expand(self, applied_rule, force=0, **kw):
""" Expands the current movement downward.
"""
# First, get the simulation movement we were expanded from # First, get the simulation movement we were expanded from
my_invoice_line_simulation = applied_rule.getParentValue() my_invoice_line_simulation = applied_rule.getParentValue()
...@@ -118,7 +222,7 @@ class InvoiceTransactionRule(Rule, PredicateMatrix): ...@@ -118,7 +222,7 @@ class InvoiceTransactionRule(Rule, PredicateMatrix):
else : else :
my_simulation_movement = applied_rule.newContent( my_simulation_movement = applied_rule.newContent(
id = transaction_line.getId() id = transaction_line.getId()
, portal_type=invoice_transaction_line_type) , portal_type=self.movement_type)
# get the resource (in that order): # get the resource (in that order):
# * resource from the invoice (using deliveryValue) # * resource from the invoice (using deliveryValue)
......
...@@ -33,12 +33,13 @@ from Products.CMFCore.utils import getToolByName ...@@ -33,12 +33,13 @@ from Products.CMFCore.utils import getToolByName
from Products.ERP5Type import Permissions, PropertySheet, Constraint, Interface from Products.ERP5Type import Permissions, PropertySheet, Constraint, Interface
from Products.ERP5.Document.Rule import Rule from Products.ERP5.Document.Rule import Rule
#from Products.ERP5Type.Base import TempBase
from zLOG import LOG from zLOG import LOG
class InvoicingRule(Rule): class InvoicingRule(Rule):
""" """
Invoicing Rule expand simulation created by a order rule. Invoicing Rule expand simulation created by a order or delivery rule.
""" """
# CMF Type Definition # CMF Type Definition
...@@ -65,15 +66,15 @@ class InvoicingRule(Rule): ...@@ -65,15 +66,15 @@ class InvoicingRule(Rule):
security.declareProtected(Permissions.AccessContentsInformation, security.declareProtected(Permissions.AccessContentsInformation,
'isAccountable') 'isAccountable')
def isAccountable(self, movement): def isAccountable(self, movement):
"""Tells wether generated movement needs to be accounted or not. """
Tells wether generated movement needs to be accounted or not.
Invoice movement are never accountable, so simulation movement for Invoice movement are never accountable, so simulation movement for
invoice movements should not be accountable either. invoice movements should not be accountable either.
""" """
return 0 return 0
security.declareProtected(Permissions.AccessContentsInformation, 'test') def _test(self, movement):
def test(self, movement):
""" """
Tests if the rule (still) applies Tests if the rule (still) applies
""" """
...@@ -85,26 +86,108 @@ class InvoicingRule(Rule): ...@@ -85,26 +86,108 @@ class InvoicingRule(Rule):
result = 1 result = 1
return result return result
#### Helper method for expand
def _generatePrevisionList(self, applied_rule, **kw):
"""
Generate a list of movements, that should be children of this rule,
based on its context (parent movement, delivery, configuration ...)
These previsions are acrually returned as dictionaries.
"""
# XXX Isn't it better to share the code with expand method
context_movement = applied_rule.getParentValue()
# Do not invoice within the same entity or whenever entities are not all
# defined.
# It could be OK to invoice within different entities of the same
# company if we wish to get some internal analytical accounting but that
# requires some processing to produce a balance sheet.
source_section = context_movement.getSourceSection()
destination_section = context_movement.getDestinationSection()
if source_section == destination_section or source_section is None \
or destination_section is None:
return []
invoice_line = {}
invoice_line.update(
price=context_movement.getPrice(),
quantity=context_movement.getCorrectedQuantity(),
quantity_unit=context_movement.getQuantityUnit(),
efficiency=context_movement.getEfficiency(),
resource=context_movement.getResource(),
variation_category_list=context_movement.getVariationCategoryList(),
variation_property_dict=context_movement.getVariationPropertyDict(),
start_date=context_movement.getStartDate(),
stop_date=context_movement.getStopDate(),
source=context_movement.getSource(), source_section=source_section,
destination=context_movement.getDestination(),
destination_section=destination_section,
# We do need to collect invoice lines to build invoices
deliverable=1
)
return [invoice_line]
security.declareProtected(Permissions.ModifyPortalContent, 'expand') security.declareProtected(Permissions.ModifyPortalContent, 'expand')
def expand(self, applied_rule, **kw): def expand(self, applied_rule, force=0, **kw):
""" Expands the current movement downward. """
Expands the rule:
- generate a list of previsions
- compare the prevision with existing children
- get the list of existing movements (immutable, mutable, deletable)
- compute the difference between prevision and existing (add,
modify, remove)
- add/modify/remove child movements to match prevision
"""
add_list, modify_dict, \
delete_list = self._getCompensatedMovementList(applied_rule, **kw)
for movement_id in delete_list:
applied_rule._delObject(movement_id)
for movement, prop_dict in modify_dict.items():
#XXX ignore start_date and stop_date if the difference is smaller than a
# rule defined value
for prop in ('start_date', 'stop_date'):
if prop in prop_dict.keys():
prop_dict.pop(prop)
applied_rule[movement].edit(**prop_dict)
for movement_dict in add_list:
if 'id' in movement_dict.keys():
mvmt_id = applied_rule._get_id(movement_dict.pop('id'))
new_mvmt = applied_rule.newContent(id=mvmt_id,
portal_type=self.movement_type)
else:
new_mvmt = applied_rule.newContent(portal_type=self.movement_type)
new_mvmt.edit(**movement_dict)
# Pass to base class
Rule.expand(self, applied_rule, force=force, **kw)
def isDeliverable(self, movement):
return movement.getResource() is not None
#### old expand method kept for reference
def old_expand(self, applied_rule, **kw):
"""
Expands the current movement downward.
""" """
delivery_line_type = 'Simulation Movement' delivery_line_type = 'Simulation Movement'
# Source that movement from the next node / stock # Source that movement from the next node / stock
my_context_movement = applied_rule.getParentValue() context_movement = applied_rule.getParentValue()
# Do not invoice within the same entity or whenever entities are # Do not invoice within the same entity or whenever entities are
# not all defined # not all defined
# It is OK to invoice within different entities of the same company # It is OK to invoice within different entities of the same company
# if we wish to get some internal analytical accounting # if we wish to get some internal analytical accounting
# but that requires some processing to produce a balance sheet # but that requires some processing to produce a balance sheet
source_section = my_context_movement.getSourceSection() source_section = context_movement.getSourceSection()
destination_section = my_context_movement.getDestinationSection() destination_section = context_movement.getDestinationSection()
if source_section == destination_section or source_section is None \ if source_section == destination_section or source_section is None \
or destination_section is None: or destination_section is None:
return Rule.expand(self, applied_rule, **kw) return Rule.expand(self, applied_rule, **kw)
if my_context_movement.getSource() is not None: if context_movement.getSource() is not None:
# XXX Please explain why ? Let us consider for # XXX Please explain why ? Let us consider for
# example a consumption movement of garbage which we # example a consumption movement of garbage which we
# want to be invoiced (the cleanup company is working # want to be invoiced (the cleanup company is working
...@@ -124,20 +207,20 @@ class InvoicingRule(Rule): ...@@ -124,20 +207,20 @@ class InvoicingRule(Rule):
) )
# Edit movement # Edit movement
invoice_line._edit( invoice_line._edit(
price = my_context_movement.getPrice(), price = context_movement.getPrice(),
quantity = my_context_movement.getQuantity(), quantity = context_movement.getQuantity(),
quantity_unit = my_context_movement.getQuantityUnit(), quantity_unit = context_movement.getQuantityUnit(),
efficiency = my_context_movement.getEfficiency(), efficiency = context_movement.getEfficiency(),
resource = my_context_movement.getResource(), resource = context_movement.getResource(),
variation_category_list = my_context_movement.\ variation_category_list = context_movement.\
getVariationCategoryList(), getVariationCategoryList(),
variation_property_dict = my_context_movement.\ variation_property_dict = context_movement.\
getVariationPropertyDict(), getVariationPropertyDict(),
start_date = my_context_movement.getStartDate(), start_date = context_movement.getStartDate(),
stop_date = my_context_movement.getStopDate(), stop_date = context_movement.getStopDate(),
source = my_context_movement.getSource(), source = context_movement.getSource(),
source_section = source_section, source_section = source_section,
destination = my_context_movement.getDestination(), destination = context_movement.getDestination(),
destination_section = destination_section, destination_section = destination_section,
# We do need to collect invoice lines to build invoices # We do need to collect invoice lines to build invoices
deliverable = 1, deliverable = 1,
...@@ -146,6 +229,3 @@ class InvoicingRule(Rule): ...@@ -146,6 +229,3 @@ class InvoicingRule(Rule):
# Create one submovement which sources the transformation # Create one submovement which sources the transformation
Rule.expand(self, applied_rule, **kw) Rule.expand(self, applied_rule, **kw)
def isDeliverable(self, m):
return m.getResource() is not None
...@@ -35,7 +35,7 @@ from Products.ERP5Type.XMLObject import XMLObject ...@@ -35,7 +35,7 @@ from Products.ERP5Type.XMLObject import XMLObject
from Products.ERP5.Document.Amount import Amount from Products.ERP5.Document.Amount import Amount
from zLOG import LOG from zLOG import LOG, WARNING
class Movement(XMLObject, Amount): class Movement(XMLObject, Amount):
""" """
...@@ -394,6 +394,25 @@ class Movement(XMLObject, Amount): ...@@ -394,6 +394,25 @@ class Movement(XMLObject, Amount):
return 1 return 1
return 0 return 0
security.declareProtected(Permissions.AccessContentsInformation,
'isFrozen')
def isFrozen(self):
"""
Returns the frozen status of this movemnt.
a movement in started, stopped, delivered is automatically frozen.
If frozen is locally set to '0', we must check for a parent set to '1', in
which case, we want the children to be frozen as well.
"""
# XXX Hardcoded
# Maybe, we should use getPortalCurrentInventoryStateList
# and another portal method for cancelled (and deleted)
LOG("Movement, isFrozen", WARNING, "Hardcoded state list")
if self.getSimulationState() in ('stopped', 'delivered', 'cancelled'):
return 1
if self._baseIsFrozen() == 0:
self._baseSetFrozen(None)
return self._baseGetFrozen() or 0
security.declareProtected( Permissions.AccessContentsInformation, security.declareProtected( Permissions.AccessContentsInformation,
'getExplanation') 'getExplanation')
def getExplanation(self): def getExplanation(self):
......
...@@ -31,7 +31,7 @@ from AccessControl import ClassSecurityInfo ...@@ -31,7 +31,7 @@ from AccessControl import ClassSecurityInfo
from Products.ERP5Type import Permissions, PropertySheet, Constraint, Interface from Products.ERP5Type import Permissions, PropertySheet, Constraint, Interface
from Products.ERP5.Document.Rule import Rule from Products.ERP5.Document.Rule import Rule
from Products.ERP5.Document.DeliveryRule import DeliveryRule from Products.ERP5.Document.DeliveryRule import DeliveryRule
from zLOG import LOG from zLOG import LOG, WARNING
class OrderRule(DeliveryRule): class OrderRule(DeliveryRule):
""" """
...@@ -40,7 +40,6 @@ class OrderRule(DeliveryRule): ...@@ -40,7 +40,6 @@ class OrderRule(DeliveryRule):
WARNING: what to do with movement split ? WARNING: what to do with movement split ?
""" """
# CMF Type Definition # CMF Type Definition
meta_type = 'ERP5 Order Rule' meta_type = 'ERP5 Order Rule'
portal_type = 'Order Rule' portal_type = 'Order Rule'
...@@ -63,73 +62,110 @@ class OrderRule(DeliveryRule): ...@@ -63,73 +62,110 @@ class OrderRule(DeliveryRule):
security.declareProtected(Permissions.ModifyPortalContent, 'expand') security.declareProtected(Permissions.ModifyPortalContent, 'expand')
def expand(self, applied_rule, force=0, **kw): def expand(self, applied_rule, force=0, **kw):
""" """
Expands the current movement downward. Expands the Order to a new simulation tree.
-> new status -> expanded expand is only allowed to modify a simulation movement if it doesn't
An applied rule can be expanded only if its parent movement have a delivery relation yet.
is expanded.
If the movement is in ordered or planned state, has no delivered
child, and is not in order, it can be deleted.
Else, if the movement is in ordered or planned state, has no
delivered child, and is in order, it can be modified.
Else, it cannot be modified.
""" """
delivery_line_type = 'Simulation Movement'
# Get the order when we come from movement_type = 'Simulation Movement'
my_order = applied_rule.getDefaultCausalityValue() existing_movement_list = []
# Only expand if my_order is not None and state is not 'confirmed' immutable_movement_list = []
if my_order is not None: order = applied_rule.getDefaultCausalityValue()
# Only expand order rule if order not yet confirmed (This is consistent if order is not None:
# with the fact that once simulation is launched, we stick to it) order_movement_list = order.getMovementList()
state = applied_rule.getLastExpandSimulationState() # check existing movements
if force or \ for movement in applied_rule.contentValues(portal_type=movement_type):
(state not in applied_rule.getPortalReservedInventoryStateList()\ if (not movement.getLastExpandSimulationState() in
and state not in applied_rule.getPortalCurrentInventoryStateList()): order.getPortalReservedInventoryStateList() and
# First, check each contained movement and make not movement.getLastExpandSimulationState() in
# a list of order ids which do not need to be copied order.getPortalCurrentInventoryStateList()) and \
# eventually delete movement which do not exist anylonger not self._isTreeDelivered([movement]):
existing_uid_list = []
existing_uid_list_append = existing_uid_list.append movement_order = movement.getOrderValue()
movement_type_list = applied_rule.getPortalMovementTypeList() if movement_order in order_movement_list:
order_movement_type_list = \ existing_movement_list.append(movement)
applied_rule.getPortalOrderMovementTypeList() else:
# Calculate existing simulation movement to delete
for movement in applied_rule.contentValues(
filter={'portal_type': movement_type_list}):
order_value = movement.getOrderValue(\
portal_type=order_movement_type_list)
if (order_value is None) or\
(order_value.hasCellContent()):
# XXX Make sure this is not deleted if already in delivery
applied_rule._delObject(movement.getId()) applied_rule._delObject(movement.getId())
else: else:
existing_uid_list_append(order_value.getUid()) existing_movement_list.append(movement)
# Build simulation movement if necessary immutable_movement_list.append(movement)
for order_movement in my_order.getMovementList():
try: # Create or modify movements
if order_movement.getUid() not in existing_uid_list: for movement in order_movement_list:
# Generate a nicer ID related_order = movement.getOrderRelatedValue()
if order_movement.getParentUid() ==\ if related_order is None:
order_movement.getExplanationUid(): if movement.getParentUid() == movement.getExplanationUid():
# We are on a line # We are on a line
new_id = order_movement.getId() new_id = movement.getId()
else: else:
# On a cell # We are on a cell
new_id = "%s_%s" % (order_movement.getParentId(), new_id = "%s_%s" % (movement.getParentId(), movement.getId())
order_movement.getId()) # Generate a simulation movement
# Generate the simulation movement LOG("OrderRule, expand", WARNING, "Hardcoded state list")
# Source, Destination, Quantity, Date, etc. are applied_rule.newContent(
# acquired from the order and need not to be copied. portal_type=movement_type,
new_sim_mvt = applied_rule.newContent(
portal_type=delivery_line_type,
id=new_id, id=new_id,
order_value=order_movement, order_value=movement,
order_ratio=1,
delivery_ratio=1, delivery_ratio=1,
deliverable=1, deliverable=1,
# source=movement.getSource(),
# source_section=movement.getSourceSection(),
# destination=movement.getDestination(),
# destination_section=movement.getDestinationSection(),
# quantity=movement.getQuantity(),
# resource=movement.getResource(),
# variation_category_list=movement.getVariationCategoryList(),
# variation_property_dict=movement.getVariationPropertyDict(),
# start_date=movement.getStartDate(),
# stop_date=movement.getStopDate(),
**kw)
elif related_order in existing_movement_list:
if related_order not in immutable_movement_list:
# modification allowed
related_order.edit(
order_value=movement,
# source=movement.getSource(),
# source_section=movement.getSourceSection(),
# destination=movement.getDestination(),
# destination_section=movement.getDestinationSection(),
# quantity=movement.getQuantity(),
# resource=movement.getResource(),
# variation_category_list=movement.getVariationCategoryList(),
# variation_property_dict=movement.getVariationPropertyDict(),
# start_date=movement.getStartDate(),
# stop_date=movement.getStopDate(),
**kw) **kw)
# No acquisition on variation_category_list #related_order.setLastExpandSimulationState(order.getSimulationState())
# in this case to prevent user failure
except AttributeError: else:
LOG('ERP5: WARNING', 0,\ # modification disallowed, must compensate
'AttributeError during expand on order movement %s'\ pass
% order_movement.absolute_url())
# Now we can set the last expand simulation state # Now we can set the last expand simulation state to the current state
# to the current state applied_rule.setLastExpandSimulationState(order.getSimulationState())
applied_rule.setLastExpandSimulationState(\
my_order.getSimulationState())
# Pass to base class # Pass to base class
Rule.expand(self, applied_rule, force=force, **kw) Rule.expand(self, applied_rule, force=force, **kw)
security.declareProtected(Permissions.AccessContentsInformation, 'isStable')
def isStable(self, applied_rule):
"""
Checks that the applied_rule is stable
"""
LOG('OrderRule.isStable', WARNING, 'Not Implemented')
return 1
security.declareProtected(Permissions.AccessContentsInformation,
'isDivergent')
def isDivergent(self, movement):
"""
Checks that the movement is divergent
"""
return Rule.isDivergent(self, movement)
...@@ -64,8 +64,7 @@ class PaymentRule(Rule): ...@@ -64,8 +64,7 @@ class PaymentRule(Rule):
, PropertySheet.DublinCore , PropertySheet.DublinCore
) )
security.declareProtected(Permissions.AccessContentsInformation, 'test') def _test(self, movement):
def test(self, movement):
""" """
Tests if the rule (still) applies Tests if the rule (still) applies
""" """
......
This diff is collapsed.
...@@ -29,10 +29,8 @@ ...@@ -29,10 +29,8 @@
from Globals import InitializeClass from Globals import InitializeClass
from AccessControl import ClassSecurityInfo from AccessControl import ClassSecurityInfo
from Products.CMFCore.utils import getToolByName from Products.CMFCore.utils import getToolByName
from Products.CMFCore.WorkflowCore import WorkflowMethod
from Products.ERP5Type import Permissions, PropertySheet, Constraint, Interface from Products.ERP5Type import Permissions, PropertySheet, Constraint, Interface
from Products.ERP5.Core import MetaNode, MetaResource
from Products.ERP5.Document.Movement import Movement from Products.ERP5.Document.Movement import Movement
...@@ -250,8 +248,6 @@ class SimulationMovement(Movement): ...@@ -250,8 +248,6 @@ class SimulationMovement(Movement):
""" """
self.setCausalityState('diverged') self.setCausalityState('diverged')
#diverge = WorkflowMethod(diverge) USELESS NOW
security.declareProtected( Permissions.AccessContentsInformation, security.declareProtected( Permissions.AccessContentsInformation,
'getExplanation') 'getExplanation')
def getExplanation(self): def getExplanation(self):
...@@ -295,15 +291,6 @@ class SimulationMovement(Movement): ...@@ -295,15 +291,6 @@ class SimulationMovement(Movement):
if explanation_value != self.getPortalObject(): if explanation_value != self.getPortalObject():
return explanation_value return explanation_value
def isFrozen(self):
"""
A frozen simulation movement can not change its target anylonger
Also, once a movement is frozen, we do not calculate anylonger
its direct consequences. (ex. we do not calculate again a transformation)
"""
return 0
# Deliverability / orderability # Deliverability / orderability
security.declareProtected( Permissions.AccessContentsInformation, security.declareProtected( Permissions.AccessContentsInformation,
'isOrderable') 'isOrderable')
...@@ -382,7 +369,7 @@ class SimulationMovement(Movement): ...@@ -382,7 +369,7 @@ class SimulationMovement(Movement):
'isConvergent') 'isConvergent')
def isConvergent(self): def isConvergent(self):
""" """
Returns true if the Simulation Movement is convergent comparing to Returns true if the Simulation Movement is convergent with the
the delivery value the delivery value
""" """
return not self.isDivergent() return not self.isDivergent()
...@@ -391,60 +378,26 @@ class SimulationMovement(Movement): ...@@ -391,60 +378,26 @@ class SimulationMovement(Movement):
'isDivergent') 'isDivergent')
def isDivergent(self): def isDivergent(self):
""" """
Returns true if the Simulation Movement is divergent comparing to Returns true if the Simulation Movement is divergent from the
the delivery value the delivery value
""" """
delivery = self.getDeliveryValue() return self.getParentValue().isDivergent(self)
if delivery is None:
return 0 security.declareProtected( Permissions.AccessContentsInformation,
# XXX Those properties are the same than defined in DeliveryBuilder. 'getDivergenceList')
# We need to defined it only 1 time. def getDivergenceList(self):
#LOG('SimulationMovement.isDivergent',0,delivery.getPath()) """
#LOG('SimulationMovement.isDivergent self.getStartDate()',0,self.getStartDate()) Returns detailed information about the divergence
#LOG('SimulationMovement.isDivergent delivery.getStartDate()',0,delivery.getStartDate()) """
if self.getSourceSection() != delivery.getSourceSection() or \ return self.getParentValue().getDivergenceList(self)
self.getDestinationSection() != delivery.getDestinationSection() or \
self.getSource() != delivery.getSource() or \ security.declareProtected( Permissions.AccessContentsInformation,
self.getDestination() != delivery.getDestination() or \ 'getSolverList')
self.getResource() != delivery.getResource() or \ def getSolverList(self):
self.getVariationCategoryList() != delivery.getVariationCategoryList()\ """
or \ Returns solvers that can fix the current divergence
self.getAggregateList() != delivery.getAggregateList() or \ """
self.getStartDate() != delivery.getStartDate() or \ return self.getParentValue().getSolverList(self)
self.getStopDate() != delivery.getStopDate():
# for method in ["getSourceSection",
# "getDestinationSection",
# "getSource",
# "getDestination",
# "getResource",
# "getVariationCategoryList",
# "getStartDate",
# "getStopDate"]:
# LOG("SimulationMovement, isDivergent", 0,
# "method: %s, self: %s , delivery: %s" % \
# tuple([method]+[str(getattr(x,method)()) for x in (self, delivery)]))
return 1
d_quantity = delivery.getQuantity()
quantity = self.getCorrectedQuantity()
d_error = self.getDeliveryError()
if quantity is None:
if d_quantity is None:
return 0
return 1
if d_quantity is None:
d_quantity = 0
if d_error is None:
d_error = 0
delivery_ratio = self.getDeliveryRatio()
# if the delivery_ratio is None, make sure that we are
# divergent even if the delivery quantity is 0
if delivery_ratio is not None:
d_quantity *= delivery_ratio
if delivery_ratio == 0 and quantity >0:
return 1
if d_quantity != quantity + d_error:
return 1
return 0
security.declareProtected( Permissions.ModifyPortalContent, security.declareProtected( Permissions.ModifyPortalContent,
'setDefaultDeliveryProperties') 'setDefaultDeliveryProperties')
......
...@@ -62,8 +62,7 @@ class TransformationRule(Rule): ...@@ -62,8 +62,7 @@ class TransformationRule(Rule):
# Class variable # Class variable
simulation_movement_portal_type = "Simulation Movement" simulation_movement_portal_type = "Simulation Movement"
security.declareProtected(Permissions.AccessContentsInformation, 'test') def _test(self, movement):
def test(self, movement):
""" """
Tests if the rule (still) applies Tests if the rule (still) applies
""" """
......
...@@ -145,8 +145,7 @@ class TransformationSourcingRule(Rule): ...@@ -145,8 +145,7 @@ class TransformationSourcingRule(Rule):
# Class variable # Class variable
simulation_movement_portal_type = "Simulation Movement" simulation_movement_portal_type = "Simulation Movement"
security.declareProtected(Permissions.AccessContentsInformation, 'test') def _test(self, movement):
def test(self, movement):
""" """
Tests if the rule (still) applies Tests if the rule (still) applies
""" """
......
...@@ -39,6 +39,15 @@ class AppliedRule: ...@@ -39,6 +39,15 @@ class AppliedRule:
'description' : 'Contains the id of the simulation state when the '\ 'description' : 'Contains the id of the simulation state when the '\
'object was last expanded (in order to avoid '\ 'object was last expanded (in order to avoid '\
'recalculation)', 'recalculation)',
'acquisition_base_category' : ( 'parent',),
'acquisition_portal_type' : ('Applied Rule', ),
'acquisition_copy_value' : 0,
'acquisition_mask_value' : 1,
'acquisition_accessor_id' : 'getLastExpandSimulationState',
'acquisition_depends' : None,
'alt_accessor_id' : ('getLastExpandSimulationState', ),
'type' : 'string', 'type' : 'string',
'mode' : 'w' }, 'mode' : 'w' },
) )
......
...@@ -71,3 +71,4 @@ class Task: ...@@ -71,3 +71,4 @@ class Task:
'mode' : 'w' }, 'mode' : 'w' },
) )
_categories = ( 'source_project', 'destination_project' )
...@@ -31,9 +31,16 @@ from TargetSolver import TargetSolver ...@@ -31,9 +31,16 @@ from TargetSolver import TargetSolver
class CopyToTarget(TargetSolver): class CopyToTarget(TargetSolver):
""" """
Copy values simulation movement as target. This is This solver calculates the ratio between the new (delivery) and old
only acceptable for root movements. The meaning of (simulation) quantity and applies this ratio to the simulation movement
this solver of other movements is far from certain. and to its parent, until a stable one is found
XXX: This solver's name is not good, and it tries too many things.
Once the new isDivergent engine is implemented, this solver can be
splitted in smaller ones (one for profit and loss, one for backtracking)
Backtracking alone is not enough to solve completely, it must be used with
another solver (profit and loss, or creating a compensation branch ...)
""" """
def _generateValueDeltaDict(self, simulation_movement): def _generateValueDeltaDict(self, simulation_movement):
""" """
...@@ -94,10 +101,21 @@ class CopyToTarget(TargetSolver): ...@@ -94,10 +101,21 @@ class CopyToTarget(TargetSolver):
""" """
Get parent movement, and its value delta dict. Get parent movement, and its value delta dict.
""" """
#XXX max_allowed_delta is the maximum number of days we want not to
# account as a divergence. It should be configurable through a Rule
max_allowed_delta = 15
applied_rule = simulation_movement.getParentValue() applied_rule = simulation_movement.getParentValue()
parent_movement = applied_rule.getParentValue() parent_movement = applied_rule.getParentValue()
if parent_movement.getPortalType() != "Simulation Movement": if parent_movement.getPortalType() != "Simulation Movement":
parent_movement = None parent_movement = None
for date_delta in ('start_date_delta', 'stop_date_delta'):
if date_delta in value_delta_dict.keys():
if abs(value_delta_dict[date_delta]) <= \
applied_rule.getProperty('max_allowed_delta', max_allowed_delta):
value_delta_dict.pop(date_delta)
return parent_movement, value_delta_dict return parent_movement, value_delta_dict
def _recursivelySolve(self, simulation_movement, is_last_movement=1, **value_delta_dict): def _recursivelySolve(self, simulation_movement, is_last_movement=1, **value_delta_dict):
...@@ -106,12 +124,34 @@ class CopyToTarget(TargetSolver): ...@@ -106,12 +124,34 @@ class CopyToTarget(TargetSolver):
his parent movement. his parent movement.
""" """
value_dict = self._generateValueDict(simulation_movement, **value_delta_dict) value_dict = self._generateValueDict(simulation_movement, **value_delta_dict)
simulation_movement.edit(**value_dict)
if is_last_movement:
delivery_quantity = simulation_movement.getDeliveryValue().getQuantity()
simulation_movement.setDeliveryError(delivery_quantity - value_dict['quantity'])
parent_movement, parent_value_delta_dict = \ parent_movement, parent_value_delta_dict = \
self._getParentParameters(simulation_movement, **value_delta_dict) self._getParentParameters(simulation_movement, **value_delta_dict)
if parent_movement is not None and parent_movement.isFrozen():
# If backtraxcking is not possible, we have to make sure that the
# divergence is solved locally by using profit and loss
sm_quantity = simulation_movement.getQuantity()
delivery_quantity = \
simulation_movement.getDeliveryValue().getQuantity()
# simulation_movement.edit(
# profit_quantity=sm_quantity - delivery_quantity)
else:
# fix foating point rounding error
if is_last_movement:
delivery_quantity = \
simulation_movement.getDeliveryValue().getQuantity()
simulation_movement.setDeliveryError(delivery_quantity -
value_dict['quantity'])
delivery = simulation_movement.getDeliveryValue()
simulation_movement.setDestination(delivery.getDestination())
simulation_movement.setSource(delivery.getSource())
simulation_movement.setDestinationSection(delivery.getDestinationSection())
simulation_movement.setSourceSection(delivery.getSourceSection())
simulation_movement.edit(**value_dict)
if parent_movement is not None: if parent_movement is not None:
# Modify the parent movement # backtrack to the parent movement only if it is not frozen
self._recursivelySolve(parent_movement, is_last_movement=0, **parent_value_delta_dict) self._recursivelySolve(parent_movement, is_last_movement=0,
**parent_value_delta_dict)
...@@ -76,6 +76,10 @@ class SplitAndDefer(CopyToTarget): ...@@ -76,6 +76,10 @@ class SplitAndDefer(CopyToTarget):
**self.additional_parameters **self.additional_parameters
) )
new_movement.activate(**self.additional_parameters).expand() new_movement.activate(**self.additional_parameters).expand()
# adopt new quantity on original simulation movement
simulation_movement.edit(quantity=new_movement_quantity)
simulation_movement._v_activate_kw = self.activate_kw simulation_movement._v_activate_kw = self.activate_kw
simulation_movement.activate(**self.additional_parameters).expand() simulation_movement.activate(**self.additional_parameters).expand()
CopyToTarget.solve(self, simulation_movement)
# SplitAndDefer solves the divergence at the current level, no need to
# backtrack.
...@@ -1266,6 +1266,7 @@ class TestAccountingRules(TestAccountingRulesMixin, ERP5TypeTestCase): ...@@ -1266,6 +1266,7 @@ class TestAccountingRules(TestAccountingRulesMixin, ERP5TypeTestCase):
invoice_transaction_line.getSourceCredit()) invoice_transaction_line.getSourceCredit())
self.assertEquals(simulation_movement.getSourceDebit(), self.assertEquals(simulation_movement.getSourceDebit(),
invoice_transaction_line.getSourceDebit()) invoice_transaction_line.getSourceDebit())
self.assertEquals(simulation_movement.getDelivery(), self.assertEquals(simulation_movement.getDelivery(),
invoice_transaction_line.getRelativeUrl()) invoice_transaction_line.getRelativeUrl())
self.assert_(len(simulation_movement_found.keys()), 3) self.assert_(len(simulation_movement_found.keys()), 3)
......
This diff is collapsed.
This diff is collapsed.
...@@ -59,6 +59,7 @@ class TestPackingListMixin(TestOrderMixin): ...@@ -59,6 +59,7 @@ class TestPackingListMixin(TestOrderMixin):
""" """
Test business template erp5_trade Test business template erp5_trade
""" """
packable_packing_list_portal_type_list = ['Sale Packing List']
container_portal_type = 'Container' container_portal_type = 'Container'
container_line_portal_type = 'Container Line' container_line_portal_type = 'Container Line'
container_cell_portal_type = 'Container Cell' container_cell_portal_type = 'Container Cell'
...@@ -81,7 +82,8 @@ class TestPackingListMixin(TestOrderMixin): ...@@ -81,7 +82,8 @@ class TestPackingListMixin(TestOrderMixin):
stepCheckDeliveryBuilding \ stepCheckDeliveryBuilding \
stepCheckPackingListIsNotDivergent ' stepCheckPackingListIsNotDivergent '
default_sequence_with_two_lines = 'stepCreateOrganisation1 \ default_sequence_with_two_lines = '\
stepCreateOrganisation1 \
stepCreateOrganisation2 \ stepCreateOrganisation2 \
stepCreateOrganisation3 \ stepCreateOrganisation3 \
stepCreateOrder \ stepCreateOrder \
...@@ -104,7 +106,8 @@ class TestPackingListMixin(TestOrderMixin): ...@@ -104,7 +106,8 @@ class TestPackingListMixin(TestOrderMixin):
stepCheckDeliveryBuilding \ stepCheckDeliveryBuilding \
stepCheckPackingListIsNotDivergent ' stepCheckPackingListIsNotDivergent '
variated_default_sequence = 'stepCreateOrganisation1 \ variated_default_sequence = '\
stepCreateOrganisation1 \
stepCreateOrganisation2 \ stepCreateOrganisation2 \
stepCreateOrganisation3 \ stepCreateOrganisation3 \
stepCreateOrder \ stepCreateOrder \
...@@ -178,7 +181,7 @@ class TestPackingListMixin(TestOrderMixin): ...@@ -178,7 +181,7 @@ class TestPackingListMixin(TestOrderMixin):
Test if packing list is divergent Test if packing list is divergent
""" """
packing_list = sequence.get('packing_list') packing_list = sequence.get('packing_list')
self.assertEquals('diverged',packing_list.getCausalityState()) self.assertEquals('diverged', packing_list.getCausalityState())
def stepCheckPackingListIsNotDivergent(self, sequence=None, sequence_list=None, **kw): def stepCheckPackingListIsNotDivergent(self, sequence=None, sequence_list=None, **kw):
""" """
...@@ -205,11 +208,12 @@ class TestPackingListMixin(TestOrderMixin): ...@@ -205,11 +208,12 @@ class TestPackingListMixin(TestOrderMixin):
""" """
packing_list = sequence.get('packing_list') packing_list = sequence.get('packing_list')
quantity = sequence.get('line_quantity',default=self.default_quantity) quantity = sequence.get('line_quantity',default=self.default_quantity)
quantity = quantity -1 quantity = quantity - 1
sequence.edit(line_quantity=quantity) sequence.edit(line_quantity=quantity)
for packing_list_line in packing_list.objectValues( for packing_list_line in packing_list.objectValues(
portal_type=self.packing_list_line_portal_type): portal_type=self.packing_list_line_portal_type):
packing_list_line.edit(quantity=quantity) packing_list_line.edit(quantity=quantity)
sequence.edit(last_delta = sequence.get('last_delta', 0.0) - 1.0)
def stepIncreasePackingListLineQuantity(self, sequence=None, def stepIncreasePackingListLineQuantity(self, sequence=None,
sequence_list=None, **kw): sequence_list=None, **kw):
...@@ -217,9 +221,13 @@ class TestPackingListMixin(TestOrderMixin): ...@@ -217,9 +221,13 @@ class TestPackingListMixin(TestOrderMixin):
Set a increased quantity on packing list lines Set a increased quantity on packing list lines
""" """
packing_list = sequence.get('packing_list') packing_list = sequence.get('packing_list')
quantity = sequence.get('line_quantity',default=self.default_quantity)
quantity = quantity + 1
sequence.edit(line_quantity=quantity)
for packing_list_line in packing_list.objectValues( for packing_list_line in packing_list.objectValues(
portal_type=self.packing_list_line_portal_type): portal_type=self.packing_list_line_portal_type):
packing_list_line.edit(quantity=self.default_quantity+1) packing_list_line.edit(quantity=quantity)
sequence.edit(last_delta = sequence.get('last_delta', 0.0) + 1.0)
def stepSplitAndDeferPackingList(self, sequence=None, sequence_list=None, **kw): def stepSplitAndDeferPackingList(self, sequence=None, sequence_list=None, **kw):
""" """
...@@ -285,14 +293,41 @@ class TestPackingListMixin(TestOrderMixin): ...@@ -285,14 +293,41 @@ class TestPackingListMixin(TestOrderMixin):
portal_type=self.packing_list_portal_type) portal_type=self.packing_list_portal_type)
self.assertEquals(1,len(packing_list_list)) self.assertEquals(1,len(packing_list_list))
packing_list1 = sequence.get('packing_list') packing_list1 = sequence.get('packing_list')
last_delta = sequence.get('last_delta', 0.0)
for line in packing_list1.objectValues( for line in packing_list1.objectValues(
portal_type= self.packing_list_line_portal_type): portal_type= self.packing_list_line_portal_type):
self.assertEquals(self.default_quantity+1,line.getQuantity()) self.assertEquals(self.default_quantity + last_delta,
line.getQuantity())
simulation_list = line.getDeliveryRelatedValueList( simulation_list = line.getDeliveryRelatedValueList(
portal_type='Simulation Movement') portal_type='Simulation Movement')
self.assertEquals(len(simulation_list),1) self.assertEquals(len(simulation_list),1)
simulation_movement = simulation_list[0] simulation_movement = simulation_list[0]
self.assertEquals(simulation_movement.getQuantity(),self.default_quantity+1) self.assertEquals(self.default_quantity + last_delta,
simulation_movement.getCorrectedQuantity())
def stepCheckPackingListNotSolved(self, sequence=None, sequence_list=None, **kw):
"""
This step is specific to test_10 : the incorrectly used solver didn't
solve anything.
"""
order = sequence.get('order')
packing_list_list = order.getCausalityRelatedValueList(
portal_type=self.packing_list_portal_type)
self.assertEquals(1,len(packing_list_list))
packing_list1 = sequence.get('packing_list')
last_delta = sequence.get('last_delta', 0.0)
for line in packing_list1.objectValues(
portal_type= self.packing_list_line_portal_type):
self.assertEquals(self.default_quantity + last_delta,
line.getQuantity())
simulation_list = line.getDeliveryRelatedValueList(
portal_type='Simulation Movement')
self.assertEquals(len(simulation_list),1)
simulation_movement = simulation_list[0]
# Here we don't add last_delta, as the solver didn't do its work.
self.assertEquals(self.default_quantity,
simulation_movement.getCorrectedQuantity())
def stepChangePackingListDestination(self, sequence=None, def stepChangePackingListDestination(self, sequence=None,
sequence_list=None, **kw): sequence_list=None, **kw):
...@@ -429,7 +464,7 @@ class TestPackingListMixin(TestOrderMixin): ...@@ -429,7 +464,7 @@ class TestPackingListMixin(TestOrderMixin):
packing_list = sequence.get('new_packing_list') packing_list = sequence.get('new_packing_list')
self.stepAdoptPrevision(sequence=sequence,packing_list=packing_list) self.stepAdoptPrevision(sequence=sequence,packing_list=packing_list)
def stepAcceptDecision(self,sequence=None, sequence_list=None, **kw): def stepAcceptDecisionPackingList(self,sequence=None, sequence_list=None, **kw):
""" """
Check if simulation movement are disconnected Check if simulation movement are disconnected
""" """
...@@ -561,6 +596,8 @@ class TestPackingListMixin(TestOrderMixin): ...@@ -561,6 +596,8 @@ class TestPackingListMixin(TestOrderMixin):
not equals to the quantity of the packing list not equals to the quantity of the packing list
""" """
packing_list = sequence.get('packing_list') packing_list = sequence.get('packing_list')
if packing_list.getPortalType() not in \
self.packable_packing_list_portal_type_list: return
self.assertEquals(0,packing_list.isPacked()) self.assertEquals(0,packing_list.isPacked())
self.assertEquals('missing',packing_list.getContainerState()) self.assertEquals('missing',packing_list.getContainerState())
...@@ -572,6 +609,8 @@ class TestPackingListMixin(TestOrderMixin): ...@@ -572,6 +609,8 @@ class TestPackingListMixin(TestOrderMixin):
""" """
if packing_list is None: if packing_list is None:
packing_list = sequence.get('packing_list') packing_list = sequence.get('packing_list')
if packing_list.getPortalType() not in \
self.packable_packing_list_portal_type_list: return
get_transaction().commit() get_transaction().commit()
self.assertEquals(1,packing_list.isPacked()) self.assertEquals(1,packing_list.isPacked())
self.assertEquals('packed',packing_list.getContainerState()) self.assertEquals('packed',packing_list.getContainerState())
...@@ -582,6 +621,8 @@ class TestPackingListMixin(TestOrderMixin): ...@@ -582,6 +621,8 @@ class TestPackingListMixin(TestOrderMixin):
equals to the quantity of the packing list equals to the quantity of the packing list
""" """
packing_list = sequence.get('new_packing_list') packing_list = sequence.get('new_packing_list')
if packing_list.getPortalType() not in \
self.packable_packing_list_portal_type_list: return
self.stepCheckPackingListIsPacked(sequence=sequence, self.stepCheckPackingListIsPacked(sequence=sequence,
packing_list=packing_list) packing_list=packing_list)
...@@ -621,8 +662,11 @@ class TestPackingList(TestPackingListMixin, ERP5TypeTestCase) : ...@@ -621,8 +662,11 @@ class TestPackingList(TestPackingListMixin, ERP5TypeTestCase) :
# Test with a simply order without cell # Test with a simply order without cell
sequence_string = self.default_sequence + '\ sequence_string = self.default_sequence + '\
stepChangePackingListDestination \ stepChangePackingListDestination \
stepCheckPackingListIsDivergent \ stepCheckPackingListIsCalculating \
stepAcceptDecisionPackingList \
stepTic \ stepTic \
stepCheckPackingListIsSolved \
stepCheckPackingListIsNotDivergent \
stepCheckSimulationDestinationUpdated \ stepCheckSimulationDestinationUpdated \
' '
sequence_list.addSequenceString(sequence_string) sequence_list.addSequenceString(sequence_string)
...@@ -640,7 +684,7 @@ class TestPackingList(TestPackingListMixin, ERP5TypeTestCase) : ...@@ -640,7 +684,7 @@ class TestPackingList(TestPackingListMixin, ERP5TypeTestCase) :
sequence_string = self.default_sequence + '\ sequence_string = self.default_sequence + '\
stepChangePackingListStartDate \ stepChangePackingListStartDate \
stepCheckPackingListIsCalculating \ stepCheckPackingListIsCalculating \
stepAcceptDecision \ stepAcceptDecisionPackingList \
stepTic \ stepTic \
stepCheckPackingListIsSolved \ stepCheckPackingListIsSolved \
stepCheckPackingListIsNotDivergent \ stepCheckPackingListIsNotDivergent \
...@@ -783,9 +827,14 @@ class TestPackingList(TestPackingListMixin, ERP5TypeTestCase) : ...@@ -783,9 +827,14 @@ class TestPackingList(TestPackingListMixin, ERP5TypeTestCase) :
def test_10_PackingListIncreaseQuantity(self, quiet=0, run=run_all_test): def test_10_PackingListIncreaseQuantity(self, quiet=0, run=run_all_test):
""" """
Change the quantity on an delivery line, then - Increase the quantity on an delivery line
see if the packing list is divergent and then - check if the packing list is divergent
split and defer the packing list - Apply the "split and defer" solver to the packing list
- check that nothing was splitted and the packing list is still divergent
(reset the delta before, as we don't expect a modification)
Basically, when we apply "split and defer" to a packing list, we don't
want it to modify lines which have been increased.
""" """
if not run: return if not run: return
sequence_list = SequenceList() sequence_list = SequenceList()
...@@ -796,8 +845,9 @@ class TestPackingList(TestPackingListMixin, ERP5TypeTestCase) : ...@@ -796,8 +845,9 @@ class TestPackingList(TestPackingListMixin, ERP5TypeTestCase) :
stepCheckPackingListIsCalculating \ stepCheckPackingListIsCalculating \
stepSplitAndDeferPackingList \ stepSplitAndDeferPackingList \
stepTic \ stepTic \
stepCheckPackingListIsSolved \ stepCheckPackingListIsDiverged \
stepCheckPackingListNotSplitted \ stepCheckPackingListIsDivergent \
stepCheckPackingListNotSolved \
' '
sequence_list.addSequenceString(sequence_string) 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