diff --git a/product/ERP5/Document/InvoiceRule.py b/product/ERP5/Document/InvoiceRule.py
new file mode 100755
index 0000000000000000000000000000000000000000..2b0f07aa49d6147df729d765dbadd6e3f0e57faa
--- /dev/null
+++ b/product/ERP5/Document/InvoiceRule.py
@@ -0,0 +1,243 @@
+##############################################################################
+#
+# Copyright (c) 2002 Nexedi SARL and Contributors. All Rights Reserved.
+#                    Jean-Paul Smets-Solanes <jp@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, Constraint, Interface
+from Products.ERP5.Document.Rule import Rule
+
+from zLOG import LOG
+
+class InvoiceRule(Rule):
+    """
+      Invoice Rule object make sure an Invoice in the similation
+      is consistent with the real invoice
+
+      WARNING: what to do with movement split ?
+    """
+
+    # CMF Type Definition
+    meta_type = 'ERP5 Invoice Rule'
+    portal_type = 'Invoice Rule'
+    add_permission = Permissions.AddPortalContent
+    isPortalContent = 1
+    isRADContent = 1
+
+    # Declarative security
+    security = ClassSecurityInfo()
+    security.declareObjectProtected(Permissions.View)
+
+    # Default Properties
+    property_sheets = ( PropertySheet.Base
+                      , PropertySheet.XMLObject
+                      , PropertySheet.CategoryCore
+                      , PropertySheet.DublinCore
+                      )
+
+    def test(self, movement):
+      """
+        Tests if the rule (still) applies
+      """
+      # An invoice rule never applies since it is always explicitely instanciated
+      # This will change in the near future : invoice will be generated following a delivery
+      return 0
+
+    # Simulation workflow
+    security.declareProtected(Permissions.ModifyPortalContent, 'expand')
+    def expand(self, applied_rule, force=0, **kw):
+      """
+        Expands the current movement downward.
+
+        -> new status -> expanded
+
+        An applied rule can be expanded only if its parent movement
+        is expanded.
+      """
+      invoice_line_type = 'Simulation Movement'
+
+      # Get the invoice where we come from
+      my_invoice = applied_rule.getDefaultCausalityValue()
+
+      # Only expand if my_invoice is not None and state is not 'confirmed'
+      if my_invoice is not None:
+        # Only expand invoice rule if invoice not yet confirmed (This is consistent
+        # with the fact that once simulation is launched, we stick to it)
+        if force or \
+           (applied_rule.getLastExpandSimulationState() not in applied_rule.getPortalReservedInventoryStateList() and \
+           applied_rule.getLastExpandSimulationState() not in applied_rule.getPortalCurrentInventoryStateList()):
+          # First, check each contained movement and make
+          # a list of invoice_line ids which do not need to be copied
+          # eventually delete movement which do not exist anylonger
+          existing_uid_list = []
+          for movement in applied_rule.contentValues(filter={'portal_type':applied_rule.getPortalMovementTypeList()}):
+            invoice_element = movement.getDeliveryValue(portal_type=applied_rule.getPortalInvoiceMovementTypeList())
+            if invoice_element is None:
+              # Does not exist any longer
+              movement.flushActivity(invoke=0)
+              applied_rule._delObject(movement.getId())  # XXXX Make sur this is not deleted if already in delivery
+            else:
+              if getattr(invoice_element, 'isCell', 0):
+                # It is a Cell
+                existing_uid_list += [invoice_element.getUid()]
+              elif invoice_element.hasCellContent():
+                # Do not keep head of cells
+                invoice_element.flushActivity(invoke=0)
+                applied_rule._delObject(movement.getId())  # XXXX Make sur this is not deleted if already in delivery
+              else:
+                # It is a Line
+                existing_uid_list += [invoice_element.getUid()]
+
+          # Copy each movement (line or cell) from the invoice
+          for invoice_line_object in my_invoice.contentValues(filter={'portal_type':applied_rule.getPortalInvoiceMovementTypeList()}):
+            try:
+              if invoice_line_object.hasCellContent():
+                for c in invoice_line_object.getCellValueList():
+                  #LOG('Cell  in', 0, '%s %s' % (c.getUid(), existing_uid_list))
+                  if c.getUid() not in existing_uid_list:
+                    new_id = invoice_line_object.getId() + '_' + c.getId()
+                    #LOG('Create Cell', 0, str(new_id))
+                    my_invoice.portal_types.constructContent(type_name=invoice_line_type,
+                        container=applied_rule,
+                        id=new_id,
+                        delivery_value = c,
+                        deliverable = 1
+                    )
+                    #LOG('After Create Cell', 0, str(new_id))
+              else:
+                if invoice_line_object.getUid() not in existing_uid_list:
+                  new_id = invoice_line_object.getId()
+                  #LOG('Line', 0, str(new_id))
+                  my_invoice.portal_types.constructContent(type_name=invoice_line_type,
+                      container=applied_rule,
+                      id=new_id,
+                      delivery_value = invoice_line_object,
+                      deliverable = 1,
+                  )
+                  #LOG('After Create Cell', 0, str(new_id))
+                  # Source, Destination, Quantity, Date, etc. are
+                  # acquired from the invoice and need not to be copied.
+            except AttributeError:
+              LOG('ERP5: WARNING', 0, 'AttributeError during expand on invoice line %s'
+                                                      % invoice_line_object.absolute_url())
+
+          # Now we can set the last expand simulation state to the current state
+          applied_rule.setLastExpandSimulationState(my_invoice.getSimulationState())
+
+      # Pass to base class
+      Rule.expand(self, applied_rule, force=force, **kw)
+
+    security.declareProtected(Permissions.ModifyPortalContent, 'solve')
+    def solve(self, applied_rule, solution_list):
+      """
+        Solve inconsitency according to a certain number of solutions
+        templates. This updates the
+
+        -> new status -> solved
+
+        This applies a solution to an applied rule. Once
+        the solution is applied, the parent movement is checked.
+        If it does not diverge, the rule is reexpanded. If not,
+        diverge is called on the parent movement.
+      """
+
+    security.declareProtected(Permissions.ModifyPortalContent, 'diverge')
+    def diverge(self, applied_rule):
+      """
+        -> new status -> diverged
+
+        This basically sets the rule to "diverged"
+        and blocks expansion process
+      """
+
+    # Solvers
+    security.declareProtected(Permissions.View, 'isDivergent')
+    def isDivergent(self, applied_rule):
+      """
+        Returns 1 if divergent rule
+      """
+
+    security.declareProtected(Permissions.View, 'getDivergenceList')
+    def getDivergenceList(self, applied_rule):
+      """
+        Returns a list Divergence descriptors
+      """
+
+    security.declareProtected(Permissions.View, 'getSolverList')
+    def getSolverList(self, applied_rule):
+      """
+        Returns a list Divergence solvers
+      """
+
+    # Deliverability / orderability
+    def isOrderable(self, m):
+      return 1
+
+    def isDeliverable(self, m):
+      if m.getSimulationState() in draft_order_state:
+        return 0
+      return 1
+
+    def collectSimulationMovements(self, applied_rule):
+      LOG("invoiceRule", 0, "collected")
+
+      # get every movement we want to group
+      movement_list = []
+      for simulation_movement in applied_rule.contentValues() :
+        for rule in simulation_movement() :
+          for sub_simulation_movement in rule.contentValues() :
+            movement_list += [sub_simulation_movement ]
+
+      # group movements
+      root_group = self.portal_simulation.collectMovement(movement_list=movement_list, class_list=[CategoryMovementGroup])
+
+      invoice = applied_rule.getCausalityValue()
+      existing_transaction_line_id_list = invoice.contentIds()
+      # sum quantities and add lines to invoice
+      for group in root_group.group_list :
+        orig_group_id = group.movement_list[0].getId()
+        quantity = 0
+        for movement in group.movement_list :
+          quantity += movement.getQuantity()
+        # Guess an unused name for the new movement
+        if orig_group_id in existing_transaction_line_id_list :
+          n = 1
+          while '%s_%s' % (orig_group_id, n) in existing_transaction_line_id_list :
+            n += 1
+          group_id = '%s_%s' % (orig_group_id, n)
+        else :
+          group_id = orig_group_id
+        existing_transaction_line_id_list.append(group_id)
+
+        # add sum of movements to invoice
+        invoice.newContent(portal_type = 'Accounting Transaction Line'
+          , id = group_id
+          , source = group.movement_list[0].getSource()
+          , destination = group.movement_list[0].getDestination()
+          , quantity = quantity
+          )
+
+      return