InvoicingRule.py 9.05 KB
##############################################################################
#
# Copyright (c) 2002-2005 Nexedi SARL and Contributors. All Rights Reserved.
#                    Sebastien Robin <seb@nexedi.com>
#                    Romain Courteaud <romain@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 Acquisition import aq_base, aq_parent, aq_inner, aq_acquire
from Products.CMFCore.utils import getToolByName

from Products.ERP5Type import Permissions, PropertySheet, Constraint, Interface
from Products.ERP5.Document.Rule import Rule
#from Products.ERP5Type.Base import TempBase

from zLOG import LOG

class InvoicingRule(Rule):
  """
    Invoicing Rule expand simulation created by a order or delivery rule.
  """

  # CMF Type Definition
  meta_type = 'ERP5 Invoicing Rule'
  portal_type = 'Invoicing Rule'
  add_permission = Permissions.AddPortalContent
  isPortalContent = 1
  isRADContent = 1

  # Declarative security
  security = ClassSecurityInfo()
  security.declareObjectProtected(Permissions.AccessContentsInformation)

  __implements__ = ( Interface.Predicate,
                     Interface.Rule )

  # Default Properties
  property_sheets = ( PropertySheet.Base
                    , PropertySheet.XMLObject
                    , PropertySheet.CategoryCore
                    , PropertySheet.DublinCore
                    )

  security.declareProtected(Permissions.AccessContentsInformation,
                            'isAccountable')
  def isAccountable(self, movement):
    """
    Tells wether 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

  def _test(self, movement):
    """
    Tests if the rule (still) applies
    """
    parent = movement.getParentValue()
    result = 0
    if (parent.getPortalType() == 'Applied Rule') and \
       (parent.getSpecialiseId() in ('default_order_rule',
                                     'default_delivery_rule' )):
      result = 1
    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')
  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, **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'
    # Source that movement from the next node / stock
    context_movement = applied_rule.getParentValue()

    # Do not invoice within the same entity or whenever entities are
    # not all defined
    # It is 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 Rule.expand(self, applied_rule, **kw)
    
    if context_movement.getSource() is not None:
      # XXX Please explain why ? Let us consider for
      # example a consumption movement of garbage which we
      # want to be invoiced (the cleanup company is working
      # within our premises)
      #
      # We should only expand movements if they have a source
      # otherwise, it creates infinite recursion
      # This happens for example whenever the source of a movement is 
      # acquired from an order which is deleted afterwards
      new_id = 'inv_mvt'
      if new_id in applied_rule.objectIds():
        invoice_line = applied_rule[new_id]
      else:
        invoice_line = applied_rule.newContent(
          type_name = delivery_line_type,
          id = new_id
        )
      # Edit movement
      invoice_line._edit(
        price = context_movement.getPrice(),
        quantity = context_movement.getQuantity(),
        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,
      )
    
    # Create one submovement which sources the transformation
    Rule.expand(self, applied_rule, **kw)