From 9e9d4fb599d74a8895d6d1c6979fbe5a41bcadd4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=81ukasz=20Nowak?= <luke@nexedi.com>
Date: Tue, 7 Jul 2009 12:51:00 +0000
Subject: [PATCH]  - BPM enabled rules, evaluation only

git-svn-id: https://svn.erp5.org/repos/public/erp5/trunk@27992 20353a03-c40f-0410-a6d1-a30d3c3de9de
---
 product/ERP5/Document/BPMDeliveryRule.py      | 161 +++++++++++
 .../Document/BPMInvoiceTransactionRule.py     | 262 ++++++++++++++++++
 product/ERP5/Document/BPMInvoicingRule.py     | 138 +++++++++
 product/ERP5/Document/BPMOrderRule.py         | 170 ++++++++++++
 product/ERP5/Document/BPMRule.py              | 244 ++++++++++++++++
 5 files changed, 975 insertions(+)
 create mode 100644 product/ERP5/Document/BPMDeliveryRule.py
 create mode 100644 product/ERP5/Document/BPMInvoiceTransactionRule.py
 create mode 100644 product/ERP5/Document/BPMInvoicingRule.py
 create mode 100644 product/ERP5/Document/BPMOrderRule.py
 create mode 100644 product/ERP5/Document/BPMRule.py

diff --git a/product/ERP5/Document/BPMDeliveryRule.py b/product/ERP5/Document/BPMDeliveryRule.py
new file mode 100644
index 0000000000..c6628635b1
--- /dev/null
+++ b/product/ERP5/Document/BPMDeliveryRule.py
@@ -0,0 +1,161 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Copyright (c) 2002-2009 Nexedi SA and Contributors. All Rights Reserved.
+#                    Jean-Paul Smets-Solanes <jp@nexedi.com>
+#                    Romain Courteaud <romain@nexedi.com>
+#                    Łukasz Nowak <luke@nexedi.com>
+#
+# WARNING: This program as such is intended to be used by professional
+# programmers who take the whole responsability of assessing all potential
+# consequences resulting from its eventual inadequacies and bugs
+# End users who are looking for a ready-to-use solution with commercial
+# garantees and support are strongly adviced to contract a Free Software
+# Service Company
+#
+# This program is Free Software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+#
+##############################################################################
+
+from AccessControl import ClassSecurityInfo
+from Products.ERP5Type import Permissions, PropertySheet
+from Products.ERP5.Document.BPMRule import BPMRule
+
+class BPMDeliveryRule(BPMRule):
+  """
+    DISCLAIMER: Refer to BPMRule docstring disclaimer.
+
+    This is BPM enabled Delivery Rule.
+  """
+
+  # CMF Type Definition
+  meta_type = 'ERP5 BPM Delivery Rule'
+  portal_type = 'BPM Delivery Rule'
+
+  # Declarative security
+  security = ClassSecurityInfo()
+  security.declareObjectProtected(Permissions.AccessContentsInformation)
+
+  # Default Properties
+  property_sheets = ( PropertySheet.Base
+                    , PropertySheet.XMLObject
+                    , PropertySheet.CategoryCore
+                    , PropertySheet.DublinCore
+                    , PropertySheet.Task
+                    , PropertySheet.AppliedRule
+                    )
+
+  # Simulation workflow
+  security.declareProtected(Permissions.ModifyPortalContent, 'expand')
+  def expand(self, applied_rule, delivery_movement_type_list=None, **kw):
+    """
+    Expands the additional Delivery movements to a new simulation tree.
+    Expand is only allowed to create or modify simulation movements for
+    delivery lines which are not already linked to another simulation
+    movement.
+
+    If the movement is not in current state, has no delivered child, and not
+    in delivery movements, it can be deleted.
+    Else if the movement is not in current state, it can be modified.
+    Else, it cannot be modified.
+    """
+    existing_movement_list = []
+    immutable_movement_list = []
+    delivery = applied_rule.getDefaultCausalityValue()
+    if delivery_movement_type_list is None:
+      delivery_movement_type_list = self.getPortalDeliveryMovementTypeList()
+    if delivery is not None:
+      delivery_movement_list = delivery.getMovementList(
+                                 portal_type=delivery_movement_type_list)
+      # Check existing movements
+      for movement in applied_rule.contentValues(
+          portal_type=self.movement_type):
+        if movement.getLastExpandSimulationState() not in \
+          self.getPortalCurrentInventoryStateList():
+          # XXX: This condition is quick and dirty hack - knowing if
+          #      Simulation Movement is frozen shall not be ever hardcoded,
+          #      this is BPM configuration
+          movement_delivery = movement.getDeliveryValue()
+          if not movement._isTreeDelivered(ignore_first=1) and \
+              movement_delivery not in delivery_movement_list:
+            applied_rule._delObject(movement.getId())
+          else:
+            existing_movement_list.append(movement)
+        else:
+          existing_movement_list.append(movement)
+          immutable_movement_list.append(movement)
+
+      # Create or modify movements
+      for deliv_mvt in delivery_movement_list:
+        sim_mvt = deliv_mvt.getDeliveryRelatedValue()
+        if sim_mvt is None:
+          # create a new deliv_mvt
+          if deliv_mvt.getParentUid() == deliv_mvt.getExplanationUid():
+            # We are on a line
+            new_id = deliv_mvt.getId()
+          else:
+            # We are on a cell
+            new_id = "%s_%s" % (deliv_mvt.getParentId(), deliv_mvt.getId())
+          # Generate the simulation deliv_mvt
+          property_dict = self.self._getExpandablePropertyDict(applied_rule,
+              deliv_mvt)
+          new_sim_mvt = applied_rule.newContent(
+              portal_type=self.movement_type,
+              id=new_id,
+              order_value=deliv_mvt,
+              order_ratio=1,
+              delivery_value=deliv_mvt,
+              delivery_ratio=1,
+              deliverable=1,
+
+              **property_dict
+          )
+        elif sim_mvt in existing_movement_list:
+          if sim_mvt not in immutable_movement_list:
+            # modification allowed
+            # XXX Hardcoded value
+            sim_mvt.edit(
+                delivery_value=deliv_mvt,
+                delivery_ratio=1,
+                deliverable=1,
+                force_update=1,
+                **property_dict
+                )
+          else:
+            # modification disallowed, must compensate
+            raise NotImplementedError('BPM *have* to support')
+
+      # Now we can set the last expand simulation state to the current state
+      applied_rule.setLastExpandSimulationState(delivery.getSimulationState())
+    # Pass to base class
+    BPMRule.expand(self, applied_rule, **kw)
+
+  security.declareProtected(Permissions.AccessContentsInformation, 'isStable')
+  def isStable(self, applied_rule):
+    """
+    Checks that the applied_rule is stable
+    """
+    return 0
+
+  # Deliverability / orderability
+  def isOrderable(self, movement):
+    return 1
+
+  def isDeliverable(self, movement):
+    if movement.getSimulationState() in movement \
+        .getPortalDraftOrderStateList():
+      return 0
+    return 1
+
diff --git a/product/ERP5/Document/BPMInvoiceTransactionRule.py b/product/ERP5/Document/BPMInvoiceTransactionRule.py
new file mode 100644
index 0000000000..6cdbf8ac6e
--- /dev/null
+++ b/product/ERP5/Document/BPMInvoiceTransactionRule.py
@@ -0,0 +1,262 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Copyright (c) 2002-2009 Nexedi SA and Contributors. All Rights Reserved.
+#                    Jean-Paul Smets-Solanes <jp@nexedi.com>
+#                    Łukasz Nowak <luke@nexedi.com>
+#
+# WARNING: This program as such is intended to be used by professional
+# programmers who take the whole responsability of assessing all potential
+# consequences resulting from its eventual inadequacies and bugs
+# End users who are looking for a ready-to-use solution with commercial
+# garantees and support are strongly adviced to contract a Free Software
+# Service Company
+#
+# This program is Free Software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+#
+##############################################################################
+
+from AccessControl import ClassSecurityInfo
+from Products.ERP5Type import Permissions, PropertySheet
+from Products.ERP5.Document.BPMRule import BPMRule
+from Products.ERP5.Document.PredicateMatrix import PredicateMatrix
+
+class BPMInvoiceTransactionRule(BPMRule, PredicateMatrix):
+  """
+    DISCLAIMER: Refer to BPMRule docstring disclaimer.
+
+    This is BPM enabled Invoice Transaction Rule.
+  """
+
+  # CMF Type Definition
+  meta_type = 'ERP5 BPM Invoice Transaction Rule'
+  portal_type = 'BPM Invoice Transaction Rule'
+  add_permission = Permissions.AddPortalContent
+  isPortalContent = 1
+  isRADContent = 1
+  
+  # Declarative security
+  security = ClassSecurityInfo()
+  security.declareObjectProtected(Permissions.AccessContentsInformation)
+
+  # Default Properties
+  property_sheets = ( PropertySheet.Base
+                    , PropertySheet.XMLObject
+                    , PropertySheet.CategoryCore
+                    , PropertySheet.DublinCore
+                    , PropertySheet.Task
+                    , PropertySheet.AppliedRule
+                    )
+
+#### 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 actually returned as dictionaries.
+    """
+    prevision_list = []
+    context_movement = applied_rule.getParentValue()
+
+    business_process = applied_rule.getBusinessProcessValue()
+
+    movement_and_path_list = []
+    for business_path in business_process.getPathValueList(
+                        self.getProperty('trade_phase_list'),
+                        context_movement):
+      movement_and_path_list.append((context_movement, business_path))
+
+    if len(movement_and_path_list) > 1:
+      raise NotImplementedError
+
+    # Find a matching cell
+    cell = self._getMatchingCell(context_movement)
+
+    if cell is not None : # else, we do nothing
+      for accounting_rule_cell_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 (resource is None) and \
+               (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()
+              if order is not None:
+                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 = accounting_rule_cell_line.getResource() \
+              or cell.getResource()
+        # XXX Harcoded list
+        prevision_line = {
+            'source': accounting_rule_cell_line.getSource(),
+            'source_section': context_movement.getSourceSection(),
+            'source_decision': context_movement.getSourceDecision(),
+            'source_administration': context_movement \
+                .getSourceAdministration(),
+            'source_project': context_movement.getSourceProject(),
+            'source_function': context_movement.getSourceFunction(),
+            'source_payment': context_movement.getSourcePayment(),
+            'destination': accounting_rule_cell_line.getDestination(),
+            'destination_section': context_movement.getDestinationSection(),
+            'destination_decision': context_movement.getDestinationDecision(),
+            'destination_administration': context_movement \
+                .getDestinationAdministration(),
+            'destination_project': context_movement.getDestinationProject(),
+            'destination_function': context_movement.getDestinationFunction(),
+            'destination_payment': context_movement.getDestinationPayment(),
+            'start_date': context_movement.getStartDate(),
+            'stop_date': context_movement.getStopDate(),
+            'resource': resource,
+            'quantity': (context_movement.getCorrectedQuantity() *
+              context_movement.getPrice(0.0)) *
+              accounting_rule_cell_line.getQuantity(),
+            'price': 1,
+            'force_update': 1,
+            'causality_value': business_path,
+            }
+
+        if accounting_rule_cell_line.hasProperty(
+            'generate_prevision_script_id'):
+          generate_prevision_script_id = \
+                accounting_rule_cell_line.getGeneratePrevisionScriptId()
+          prevision_line.update(getattr(context_movement,
+                              generate_prevision_script_id)(prevision_line))
+        prevision_list.append(prevision_line)
+    return prevision_list
+
+  security.declareProtected(Permissions.ModifyPortalContent, 'expand')
+  def expand(self, applied_rule, force=0, **kw):
+    """
+    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','destination_total_asset_price',
+              'source_total_asset_price'],**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)
+      #set asset_price on movement when resource is different from price
+      #currency of the source/destination section
+      currency = new_mvmt.getResourceValue()
+      if currency is not None:
+        currency_url = currency.getRelativeUrl()
+        dest_section = new_mvmt.getDestinationSectionValue()
+        if dest_section is not None:
+          dest_currency_url = dest_section.getProperty('price_currency', None)
+        else:
+          dest_currency_url = None
+        if dest_currency_url is not None \
+            and currency_url != dest_currency_url:
+          precision = dest_section.getPriceCurrencyValue() \
+              .getQuantityPrecision()
+          dest_exchange_ratio = currency.getPrice(context=new_mvmt.asContext(
+            categories=['price_currency/%s' % dest_currency_url,
+                        'resource/%s' % currency_url],
+            start_date=new_mvmt.getStartDate()))
+          if dest_exchange_ratio is not None:
+            new_mvmt.edit(destination_total_asset_price=round(
+             (dest_exchange_ratio*
+              applied_rule.getParentValue().getTotalPrice()),precision))
+
+        source_section = new_mvmt.getSourceSectionValue()
+        if source_section is not None:
+          source_currency_url = source_section.getProperty(
+              'price_currency', None)
+        else:
+          source_currency_url = None
+        if source_currency_url is not None \
+            and currency_url != source_currency_url:
+          precision = source_section.getPriceCurrencyValue() \
+              .getQuantityPrecision()
+          source_exchange_ratio = currency.getPrice(context=new_mvmt\
+              .asContext(
+            categories=['price_currency/%s' % source_currency_url,
+                        'resource/%s' % currency_url],
+            start_date=new_mvmt.getStartDate()))
+          if source_exchange_ratio is not None:
+            new_mvmt.setSourceTotalAssetPrice(round(
+       source_exchange_ratio*applied_rule.getParentValue().getTotalPrice(),
+            precision))
+
+    # Pass to base class
+    BPMRule.expand(self, applied_rule, force=force, **kw)
+
+  # Matrix related
+  security.declareProtected( Permissions.ModifyPortalContent,
+                              'newCellContent' )
+  def newCellContent(self, id, portal_type='Accounting Rule Cell', **kw):
+    """
+      Creates a new Cell.
+    """
+    self.invokeFactory(type_name=portal_type, id=id)
+    new_cell = self.get(id)
+    return new_cell
+
+  # Deliverability / orderability
+  def isOrderable(self, m):
+    return 1
+
+  def isDeliverable(self, m):
+    if m.getSimulationState() in self.getPortalDraftOrderStateList():
+      return 0
+    return 1
diff --git a/product/ERP5/Document/BPMInvoicingRule.py b/product/ERP5/Document/BPMInvoicingRule.py
new file mode 100644
index 0000000000..305f97b739
--- /dev/null
+++ b/product/ERP5/Document/BPMInvoicingRule.py
@@ -0,0 +1,138 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Copyright (c) 2002-2009 Nexedi SA and Contributors. All Rights Reserved.
+#                    Sebastien Robin <seb@nexedi.com>
+#                    Romain Courteaud <romain@nexedi.com>
+#                    Łukasz Nowak <luke@nexedi.com>
+#
+# WARNING: This program as such is intended to be used by professional
+# programmers who take the whole responsibility of assessing all potential
+# consequences resulting from its eventual inadequacies and bugs
+# End users who are looking for a ready-to-use solution with commercial
+# guarantees and support are strongly adviced to contract a Free Software
+# Service Company
+#
+# This program is Free Software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301,
+# USA.
+#
+##############################################################################
+
+from AccessControl import ClassSecurityInfo
+from Products.ERP5Type import Permissions, PropertySheet
+from Products.ERP5.Document.BPMRule import BPMRule
+
+class BPMInvoicingRule(BPMRule):
+  """
+    DISCLAIMER: Refer to BPMRule docstring disclaimer.
+
+    This is BPM enabled Invoicing Rule
+  """
+
+  # CMF Type Definition
+  meta_type = 'ERP5 BPM Invoicing Rule'
+  portal_type = 'BPM Invoicing Rule'
+  add_permission = Permissions.AddPortalContent
+  isPortalContent = 1
+  isRADContent = 1
+
+  # Declarative security
+  security = ClassSecurityInfo()
+  security.declareObjectProtected(Permissions.AccessContentsInformation)
+
+  # Default Properties
+  property_sheets = ( PropertySheet.Base
+                    , PropertySheet.XMLObject
+                    , PropertySheet.CategoryCore
+                    , PropertySheet.DublinCore
+                    , PropertySheet.Task
+                    , PropertySheet.AppliedRule
+                    )
+
+  security.declareProtected(Permissions.AccessContentsInformation,
+                            'isAccountable')
+  def isAccountable(self, movement):
+    """
+    Tells whether generated movement needs to be accounted or not.
+
+    Invoice movement are never accountable, so simulation movement for
+    invoice movements should not be accountable either.
+    """
+    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 returned as dictionaries.
+    """
+    # XXX Isn't it better to share the code with expand method
+    context_movement = applied_rule.getParentValue()
+    business_process = applied_rule.getBusinessProcessValue()
+
+    movement_and_path_list = []
+    for business_path in business_process.getPathValueList(
+                        self.getProperty('trade_phase_list'),
+                        context_movement):
+      movement_and_path_list.append((context_movement, business_path))
+
+    if len(movement_and_path_list) > 1:
+      raise NotImplementedError
+
+    for movement, business_path in movement_and_path_list:
+      property_dict = self._getExpandablePropertyDict(
+                                     applied_rule, movement, business_path)
+      property_dict['deliverable'] = 1
+    return [property_dict]
+
+  security.declareProtected(Permissions.ModifyPortalContent, 'expand')
+  def expand(self, applied_rule, force=0, **kw):
+    """
+    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
+    """
+    parent_movement = applied_rule.getParentValue()
+    if parent_movement is not None:
+      if not parent_movement.isFrozen():
+        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():
+          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
+    BPMRule.expand(self, applied_rule, force=force, **kw)
+
+  def isDeliverable(self, movement):
+    return movement.getResource() is not None
+
diff --git a/product/ERP5/Document/BPMOrderRule.py b/product/ERP5/Document/BPMOrderRule.py
new file mode 100644
index 0000000000..733855600e
--- /dev/null
+++ b/product/ERP5/Document/BPMOrderRule.py
@@ -0,0 +1,170 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Copyright (c) 2002-2009 Nexedi SA and Contributors. All Rights Reserved.
+#                    Jean-Paul Smets-Solanes <jp@nexedi.com>
+#                    Romain Courteaud <romain@nexedi.com>
+#                    Łukasz Nowak <luke@nexedi.com>
+#
+# WARNING: This program as such is intended to be used by professional
+# programmers who take the whole responsability of assessing all potential
+# consequences resulting from its eventual inadequacies and bugs
+# End users who are looking for a ready-to-use solution with commercial
+# garantees and support are strongly adviced to contract a Free Software
+# Service Company
+#
+# This program is Free Software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+#
+##############################################################################
+
+from AccessControl import ClassSecurityInfo
+from Products.ERP5Type import Permissions, PropertySheet
+from Products.ERP5.Document.BPMRule import BPMRule
+from Products.ERP5.Document.BPMDeliveryRule import BPMDeliveryRule
+from zLOG import LOG, WARNING
+
+class BPMOrderRule(BPMDeliveryRule):
+  """
+    DISCLAIMER: Refer to BPMRule docstring disclaimer.
+
+    This is BPM enabled Order Rule.
+  """
+  # CMF Type Definition
+  meta_type = 'ERP5 BPM Order Rule'
+  portal_type = 'BPM Order Rule'
+
+  # Declarative security
+  security = ClassSecurityInfo()
+  security.declareObjectProtected(Permissions.AccessContentsInformation)
+
+  # Default Properties
+  property_sheets = ( PropertySheet.Base
+                    , PropertySheet.XMLObject
+                    , PropertySheet.CategoryCore
+                    , PropertySheet.DublinCore
+                    , PropertySheet.Task
+                    , PropertySheet.AppliedRule
+                    )
+
+  # Simulation workflow
+  security.declareProtected(Permissions.ModifyPortalContent, 'expand')
+  def expand(self, applied_rule, force=0, **kw):
+    """
+      Expands the Order to a new simulation tree.
+      expand is only allowed to modify a simulation movement if it doesn't
+      have a delivery relation yet.
+
+      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.
+    """
+
+    existing_movement_list = []
+    immutable_movement_list = []
+    order = applied_rule.getDefaultCausalityValue()
+    business_process = applied_rule.getBusinessProcessValue()
+    if order is not None:
+      order_movement_list = order.getMovementList(
+                     portal_type=order.getPortalOrderMovementTypeList())
+      # check existing movements
+      for simulation_movement in applied_rule.contentValues(
+          portal_type=self.movement_type):
+        if (not simulation_movement.getLastExpandSimulationState() in
+            order.getPortalReservedInventoryStateList() and
+            not simulation_movement.getLastExpandSimulationState() in
+            order.getPortalCurrentInventoryStateList()) and \
+            not simulation_movement._isTreeDelivered():
+
+          movement_order = simulation_movement.getOrderValue()
+          if movement_order in order_movement_list:
+            existing_movement_list.append(simulation_movement)
+          else:
+            applied_rule._delObject(simulation_movement.getId())
+        else:
+          existing_movement_list.append(simulation_movement)
+          immutable_movement_list.append(simulation_movement)
+
+      # this dict simulates getOrderRelatedValue, but it will not work if an
+      # order was generated from multiple applied rules
+      order_movement_dict = {}
+      for s_m in applied_rule.objectValues():
+        order_movement = s_m.getOrderValue()
+        if order_movement is not None:
+          order_movement_dict[order_movement.getPath()] = s_m
+
+      # Create or modify movements
+      for order_movement in order_movement_list:
+        related_order = order_movement_dict.get(order_movement.getPath(),
+            None)
+        if related_order is None:
+          related_order = order_movement.getOrderRelatedValue()
+
+        movement_and_path_list = []
+        for business_path in business_process.getPathValueList(
+                            self.getProperty('trade_phase_list'),
+                            order_movement):
+          movement_and_path_list.append((order_movement, business_path))
+
+        if len(movement_and_path_list) > 1:
+          raise NotImplementedError
+
+        for movement, business_path in movement_and_path_list:
+          property_dict = self._getExpandablePropertyDict(
+                                         applied_rule, movement,
+                                         business_path)
+          property_dict.update(order_value=order_movement)
+        if related_order is None:
+          # Generate a simulation movement
+          # Do not try to create meaningfull IDs, as order movement can be
+          # hierarchical
+          applied_rule.newContent(
+              portal_type=self.movement_type,
+              order_ratio=1,
+              delivery_ratio=1,
+              deliverable=1,
+              **property_dict)
+        elif related_order in existing_movement_list:
+          if related_order not in immutable_movement_list:
+            # modification allowed
+            related_order.edit(
+                **property_dict)
+          else:
+            # modification disallowed, must compensate
+            raise NotImplementedError('BPM *have* to support compensation')
+
+          # Now we can set the last expand simulation state to the current
+          # state
+          applied_rule.setLastExpandSimulationState(
+              order.getSimulationState())
+    # Pass to base class
+    BPMRule.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 BPMRule.isDivergent(self, movement)
diff --git a/product/ERP5/Document/BPMRule.py b/product/ERP5/Document/BPMRule.py
new file mode 100644
index 0000000000..f55cd8729a
--- /dev/null
+++ b/product/ERP5/Document/BPMRule.py
@@ -0,0 +1,244 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Copyright (c) 2002-2009 Nexedi SA and Contributors. All Rights Reserved.
+#                    Jean-Paul Smets-Solanes <jp@nexedi.com>
+#                    Łukasz Nowak <luke@nexedi.com>
+#
+# WARNING: This program as such is intended to be used by professional
+# programmers who take the whole responsability of assessing all potential
+# consequences resulting from its eventual inadequacies and bugs
+# End users who are looking for a ready-to-use solution with commercial
+# garantees and support are strongly adviced to contract a Free Software
+# Service Company
+#
+# This program is Free Software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+#
+##############################################################################
+
+import zope.interface
+from AccessControl import ClassSecurityInfo
+from Products.ERP5Type import Permissions, PropertySheet, interfaces
+from Products.ERP5.Document.Rule import Rule
+
+class BPMRule(Rule):
+  """
+    DISCLAIMER: Do not use this in any production system.
+                This is only proof of concept and evaluation of new system
+                design. Implementation and API can change without any
+                further warning.
+
+                *DO NOT USE IN PRODUCTION SYSTEM*
+
+    This is BPM enabled Rule system Base class.
+  """
+
+  # CMF Type Definition
+  meta_type = 'ERP5 BPM Rule'
+  portal_type = 'BPM Rule'
+  add_permission = Permissions.AddPortalContent
+  isPortalContent = 1
+  isRADContent = 1
+  isPredicate = 1
+
+  # Declarative security
+  security = ClassSecurityInfo()
+  security.declareObjectProtected(Permissions.AccessContentsInformation)
+
+  zope.interface.implements( interfaces.IPredicate,
+                     interfaces.IRule )
+
+  # Default Properties
+  property_sheets = ( PropertySheet.Base
+                    , PropertySheet.XMLObject
+                    , PropertySheet.CategoryCore
+                    , PropertySheet.DublinCore
+                    , PropertySheet.Task
+                    , PropertySheet.Predicate
+                    , PropertySheet.Reference
+                    , PropertySheet.Version
+                    )
+
+#### Helpers
+  def _getCurrentMovementList(self, applied_rule, **kw):
+    """
+    Returns the list of current children of the applied rule, sorted in 3
+    groups : immutables/mutables/deletable
+
+     * immutable is frozen
+     * mutable is not frozen, but delivered
+     * deletable is not frozen and not delivered
+
+    Delivered means movement is delivered or any of its children is delivered.
+    """
+    immutable_movement_list = []
+    mutable_movement_list = []
+    deletable_movement_list = []
+
+    for movement in applied_rule.contentValues(
+        portal_type=self.movement_type):
+      if movement.isFrozen():
+        immutable_movement_list.append(movement)
+      else:
+        if movement._isTreeDelivered():
+          mutable_movement_list.append(movement)
+        else:
+          deletable_movement_list.append(movement)
+
+    return (immutable_movement_list, mutable_movement_list,
+            deletable_movement_list)
+
+  def _getCompensatedMovementList(self, applied_rule,
+                                  matching_property_list=(
+                                  'resource',
+                                  'variation_category_list',
+                                  'variation_property_dict',), **kw):
+    """
+    Compute the difference between prevision and existing movements
+
+    immutable movements need compensation, mutables needs to be modified
+
+    XXX For now, this implementation is too simple. It could be improved by
+    using MovementGroups
+    """
+    add_list = [] # list of movements to be added
+    modify_dict = {} # dict of movements to be modified
+    delete_list = [] # list of movements to be deleted
+
+    prevision_list = self._generatePrevisionList(applied_rule, **kw)
+    immutable_movement_list, mutable_movement_list, \
+        deletable_movement_list = self._getCurrentMovementList(applied_rule,
+                                                               **kw)
+    movement_list = immutable_movement_list + mutable_movement_list \
+                    + deletable_movement_list
+    non_matched_list = movement_list[:] # list of remaining movements
+
+    for prevision in prevision_list:
+      p_matched_list = []
+      for movement in non_matched_list:
+        for prop in matching_property_list:
+          if prevision.get(prop) != movement.getProperty(prop):
+            break
+        else:
+          p_matched_list.append(movement)
+
+      # Movements exist, we'll try to make them match the prevision
+      if p_matched_list != []:
+        # Check the quantity
+        m_quantity = 0.0
+        for movement in p_matched_list:
+          m_quantity += movement.getQuantity()#getCorrectedQuantity()
+        if m_quantity != prevision.get('quantity'):
+          q_diff = prevision.get('quantity') - m_quantity
+          # try to find a movement that can be edited
+          for movement in p_matched_list:
+            if movement in (mutable_movement_list \
+                + deletable_movement_list):
+              # mark as requiring modification
+              prop_dict = modify_dict.setdefault(movement.getId(), {})
+              #prop_dict['quantity'] = movement.getCorrectedQuantity() + \
+              prop_dict['quantity'] = movement.getQuantity() + \
+                  q_diff
+              break
+          # no modifiable movement was found, need to create one
+          else:
+            prevision['quantity'] = q_diff
+            add_list.append(prevision)
+
+        # Check the date
+        for movement in p_matched_list:
+          if movement in (mutable_movement_list \
+              + deletable_movement_list):
+            prop_dict = modify_dict.setdefault(movement.getId(), {})
+            for prop in ('start_date', 'stop_date'):
+              #XXX should be >= 15
+              if prevision.get(prop) != movement.getProperty(prop):
+                prop_dict[prop] = prevision.get(prop)
+                break
+
+            for k, v in prevision.items():
+              if k not in ('quantity', 'start_date', 'stop_date') and \
+                      v != movement.getProperty(k):
+                prop_dict.setdefault(k, v)
+
+        # update movement lists
+        for movement in p_matched_list:
+          non_matched_list.remove(movement)
+
+      # No movement matched, we need to create one
+      else:
+        add_list.append(prevision)
+
+    # delete non matched movements
+    for movement in non_matched_list:
+      if movement in deletable_movement_list:
+        # delete movement
+        delete_list.append(movement.getId())
+      elif movement in mutable_movement_list:
+        # set movement quantity to 0 to make it "void"
+        prop_dict = modify_dict.setdefault(movement.getId(), {})
+        prop_dict['quantity'] = 0.0
+      else:
+        # movement not modifiable, we can decide to create a compensation
+        # with negative quantity
+        raise NotImplementedError(
+                "Can not create a compensation movement for %s" % \
+                movement.getRelativeUrl())
+    return (add_list, modify_dict, delete_list)
+
+  security.declareProtected(Permissions.AccessContentsInformation,
+                            '_getExpandablePropertyDict')
+  def _getExpandablePropertyDict(self, applied_rule, movement, business_path,
+      **kw):
+    """
+    Return a Dictionary with the Properties used to edit 
+    the simulation movement. A supply path can be passed to assign more
+    information
+    """
+    property_dict = {}
+
+    default_property_list = self.getExpandablePropertyList()
+    for prop in default_property_list:
+      property_dict[prop] = movement.getProperty(prop)
+
+    # Arrow
+    for base_category in \
+        business_path.getSourceBaseCategoryList() +\
+        business_path.getDestinationBaseCategoryList():
+      property_dict[base_category] = business_path\
+                .getDefaultAcquiredCategoryMembership(base_category,
+                    context=movement)
+    # Amount
+    if business_path.getQuantity():
+      property_dict['quantity'] = business_path.getQuantity()
+    elif business_path.getEfficiency():
+      property_dict['quantity'] = movement.getQuantity() *\
+        business_path.getEfficiency()
+    else:
+      property_dict['quantity'] = movement.getQuantity()
+
+    if movement.getStartDate() == movement.getStopDate():
+      property_dict['start_date'] = business_path.getExpectedStartDate(
+          movement)
+      property_dict['stop_date'] = business_path.getExpectedStopDate(movement)
+    else: # XXX shall not be used, but business_path.getExpectedStart/StopDate
+          # do not works on second path...
+      property_dict['start_date'] = movement.getStartDate()
+      property_dict['stop_date'] = movement.getStopDate()
+
+    # save a relation to supply path for builders XXX which category
+    property_dict['causality_value'] = business_path
+
+    return property_dict
-- 
2.30.9