From 626f45acf12bdf93baa5e99ad31ed3bd69da3528 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=A9rome=20Perrin?= <jerome@nexedi.com>
Date: Tue, 18 Dec 2007 09:42:19 +0000
Subject: [PATCH] enable constraints for accounting transaction validation.

git-svn-id: https://svn.erp5.org/repos/public/erp5/trunk@18381 20353a03-c40f-0410-a6d1-a30d3c3de9de
---
 .../AccountingTransactionBalance.py           |  79 +++++++++++++
 .../ERP5/Document/AccountingTransaction.py    |  32 ++++++
 .../Document/AccountingTransactionLine.py     |   1 +
 product/ERP5/Document/BalanceTransaction.py   |   1 +
 .../PropertySheet/AccountingTransaction.py    |  71 ++++++++++++
 .../AccountingTransactionLine.py              | 107 ++++++++++++++++++
 6 files changed, 291 insertions(+)
 create mode 100644 product/ERP5/Constraint/AccountingTransactionBalance.py
 create mode 100644 product/ERP5/PropertySheet/AccountingTransaction.py
 create mode 100644 product/ERP5/PropertySheet/AccountingTransactionLine.py

diff --git a/product/ERP5/Constraint/AccountingTransactionBalance.py b/product/ERP5/Constraint/AccountingTransactionBalance.py
new file mode 100644
index 0000000000..2dbe0c3dfc
--- /dev/null
+++ b/product/ERP5/Constraint/AccountingTransactionBalance.py
@@ -0,0 +1,79 @@
+##############################################################################
+#
+# Copyright (c) 2007 Nexedi SA and Contributors. All Rights Reserved.
+#          Jerome Perrin <jerome@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 Products.ERP5Type.Constraint import Constraint
+from Products.ERP5Type.Message import Message
+N_ = lambda msg: msg  # just to extract messages
+
+class AccountingTransactionBalance(Constraint):
+  """Check that accounting transaction total debit and total credit are equals.
+  """
+  
+  _message_id_list = [ 'message_transaction_not_balanced_for_source',
+                       'message_transaction_not_balanced_for_destination' ]
+  
+  message_transaction_not_balanced_for_source = N_(
+       'Transaction is not Balanced')
+  message_transaction_not_balanced_for_destination = N_(
+       'Transaction is not Balanced')
+
+  def checkConsistency(self, obj, fixit=0):
+    """Implement here the consistency checker
+    """
+    error_list = []
+    source_sum = 0
+    destination_sum = 0
+    for line in obj.getMovementList(
+          portal_type=obj.getPortalAccountingMovementTypeList()):
+      source_sum += line.getSourceInventoriatedTotalAssetPrice() or 0
+      destination_sum += \
+          line.getDestinationInventoriatedTotalAssetPrice() or 0
+    
+    source_section = obj.getSourceSectionValue()
+    destination_section = obj.getDestinationSectionValue()
+    source_precision = destination_precision = 2
+
+    if source_section is not None and\
+                 source_section.getPortalType() == 'Organisation':
+      source_currency = source_section.getPriceCurrencyValue()
+      if source_currency is not None:
+        source_precision = source_currency.getQuantityPrecision()
+    if round(source_sum, source_precision) != 0:
+      error_list.append(self._generateError(obj, self._getMessage(
+            'message_transaction_not_balanced_for_source')))
+
+    if destination_section is not None and\
+                 destination_section.getPortalType() == 'Organisation':
+      destination_currency = destination_section.getPriceCurrencyValue()
+      if destination_currency is not None:
+        destination_precision = destination_currency.getQuantityPrecision()
+    if round(destination_sum, destination_precision) != 0:
+      error_list.append(self._generateError(obj, self._getMessage(
+              'message_transaction_not_balanced_for_destination')))
+    
+    return error_list
diff --git a/product/ERP5/Document/AccountingTransaction.py b/product/ERP5/Document/AccountingTransaction.py
index c9ae251d23..6f3a2f4771 100644
--- a/product/ERP5/Document/AccountingTransaction.py
+++ b/product/ERP5/Document/AccountingTransaction.py
@@ -63,7 +63,39 @@ class AccountingTransaction(Delivery):
                       , PropertySheet.Amount
                       , PropertySheet.Reference
                       , PropertySheet.PaymentCondition
+                      , PropertySheet.AccountingTransaction
                       )
+    
+    def hasSourceSectionAccounting(self):
+      """Return true if we should take into account accounting for source
+      section.
+      """
+      section = self.getSourceSectionValue()
+      if section is not None:
+        preference_tool = getToolByName(self, 'portal_preferences')
+        preferred_section_category = preference_tool.\
+                getPreferredAccountingTransactionSectionCategory()
+        if preferred_section_category:
+          if section.getPortalType() == 'Person':
+            return 0
+          return section.isMemberOf(preferred_section_category)
+      return 0
+
+    def hasDestinationSectionAccounting(self):
+      """Return true if we should take into account accounting for destination
+      section.
+      """
+      section = self.getDestinationSectionValue()
+      if section is not None:
+        preference_tool = getToolByName(self, 'portal_preferences')
+        preferred_section_category = preference_tool.\
+                getPreferredAccountingTransactionSectionCategory()
+        if preferred_section_category:
+          if section.getPortalType() == 'Person':
+            return 0
+          return section.isMemberOf(preferred_section_category)
+      return 0
+    
 
 # Compatibility
 # It may be necessary to create an alias after removing the Transaction class
diff --git a/product/ERP5/Document/AccountingTransactionLine.py b/product/ERP5/Document/AccountingTransactionLine.py
index 4dc3884d70..c38f559378 100644
--- a/product/ERP5/Document/AccountingTransactionLine.py
+++ b/product/ERP5/Document/AccountingTransactionLine.py
@@ -57,6 +57,7 @@ class AccountingTransactionLine(DeliveryLine):
                     , PropertySheet.Task
                     , PropertySheet.Arrow
                     , PropertySheet.Price
+                    , PropertySheet.AccountingTransactionLine
                     )
 
   # Declarative interfaces
diff --git a/product/ERP5/Document/BalanceTransaction.py b/product/ERP5/Document/BalanceTransaction.py
index 373f4993f1..1714b9b80d 100644
--- a/product/ERP5/Document/BalanceTransaction.py
+++ b/product/ERP5/Document/BalanceTransaction.py
@@ -106,6 +106,7 @@ class BalanceTransaction(AccountingTransaction, Inventory):
                     , PropertySheet.Amount
                     , PropertySheet.Reference
                     , PropertySheet.PaymentCondition
+                    , PropertySheet.AccountingTransaction
                     )
   
 
diff --git a/product/ERP5/PropertySheet/AccountingTransaction.py b/product/ERP5/PropertySheet/AccountingTransaction.py
new file mode 100644
index 0000000000..3abfa0961c
--- /dev/null
+++ b/product/ERP5/PropertySheet/AccountingTransaction.py
@@ -0,0 +1,71 @@
+##############################################################################
+#
+# Copyright (c) 2007 Nexedi SA and Contributors. All Rights Reserved.
+#                            Jerome Perrin <jerome@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.
+#
+##############################################################################
+
+class AccountingTransaction:
+  """Constraints for Accounting Transactions
+  """
+
+  _constraints = (
+    { 'id': 'section_existence',
+      'description': 'Both sections must be defined for invoices',
+      'type': 'CategoryExistence',
+      'destination_section' : 1,
+      'source_section' : 1,
+      'portal_type': ('Person', 'Organisation'),
+      'condition' : 'python: object.getPortalType() in'
+                    ' portal.getPortalInvoiceTypeList()',
+    },
+
+    { 'id': 'date_existence',
+      'description': 'Date must be defined',
+      'message_property_not_set': 'Date must be defined',
+      'condition' : 'python: object.getSimulationState() not'
+                     ' in ("cancelled", "deleted")',
+      'type': 'PropertyExistence',
+      'start_date' : 1,
+    },
+
+    { 'id': 'currency_existence',
+      'description': 'Currency must be defined',
+      'message_category_not_set':
+           'Currency must be defined',
+      'portal_type': ('Currency',),
+      'condition' : 'python: object.getSimulationState() not'
+                     ' in ("cancelled", "deleted")',
+      'type': 'CategoryExistence',
+      'resource' : 1,
+    },
+
+    { 'id': 'debit_credit_balance',
+      'description': 'Total Debit must equal Total Credit',
+      'condition' : 'python: object.getSimulationState() not'
+                     ' in ("cancelled", "deleted")',
+      'type': 'AccountingTransactionBalance',
+    },
+
+ )
diff --git a/product/ERP5/PropertySheet/AccountingTransactionLine.py b/product/ERP5/PropertySheet/AccountingTransactionLine.py
new file mode 100644
index 0000000000..0b6673779a
--- /dev/null
+++ b/product/ERP5/PropertySheet/AccountingTransactionLine.py
@@ -0,0 +1,107 @@
+##############################################################################
+#
+# Copyright (c) 2006 Nexedi SA and Contributors. All Rights Reserved.
+#                            Jerome Perrin <jerome@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 Products.CMFCore.Expression import Expression
+
+class AccountingTransactionLine:
+  """Constraints for Accounting Transaction Lines
+  """
+
+  _constraints = (
+      # We need an account if we have a quantity for this side
+    { 'id': 'source_existence',
+      'description': 'Accounting Transaction Lines must use an account',
+      'condition' : 'python: object.getSourceInventoriatedTotalAssetPrice()'\
+                    ' and object.hasSourceSectionAccounting()'\
+                    ' and not object.getDestination(portal_type="Account")',
+      'type': 'CategoryExistence',
+      'source' : 1,
+      'portal_type': 'Account',
+      'message_category_not_set': 'Account must be defined on lines',
+    },
+    { 'id': 'destination_existence',
+      'description': 'Accounting Transaction Lines must use an account',
+      'condition' :
+        'python: object.getDestinationInventoriatedTotalAssetPrice()'\
+        ' and object.hasDestinationSectionAccounting()'\
+        ' and not object.getSource(portal_type="Account")',
+      'type': 'CategoryExistence',
+      'destination' : 1,
+      'portal_type': 'Account',
+      'message_category_not_set': 'Account must be defined on lines',
+    },
+
+      # We need a mirror section for recievable / payable accounts
+    { 'id': 'destination_section_existence',
+      'condition' :
+        'python: object.getSourceValue(portal_type="Account") is not None'\
+        ' and object.getSourceValue(portal_type="Account").getAccountTypeId()'\
+        ' in ("receivable", "payable")',
+      'type': 'CategoryAcquiredExistence',
+      'destination_section' : 1,
+      'portal_type': ('Person', 'Organisation'),
+      'message_category_not_set': 'Third party must be defined for '\
+          'payable or receivable accounts.'
+    },
+    { 'id': 'source_section_existence',
+      'condition' :
+  'python: object.getDestinationValue(portal_type="Account") is not None'\
+  ' and object.getDestinationValue(portal_type="Account").getAccountTypeId()'\
+  ' in ("receivable", "payable")',
+      'type': 'CategoryAcquiredExistence',
+      'source_section' : 1,
+      'portal_type': ('Person', 'Organisation'),
+      'message_category_not_set': 'Third party must be defined for '\
+          'payable or receivable accounts.'
+    },
+
+       # We need a payment for bank accounts
+    { 'id': 'source_payment_existence',
+      'condition' : 'python: object.hasSourceSectionAccounting() and'\
+      ' object.getSourceValue(portal_type="Account") is not None'\
+      ' and object.getSourceValue(portal_type="Account").getAccountType()'\
+      ' == "asset/cash/bank"',
+      'type': 'CategoryAcquiredExistence',
+      'source_payment' : 1,
+      'portal_type': Expression('portal/getPortalPaymentNodeTypeList'),
+      'message_category_not_set': 'Bank account must be defined for '\
+          'bank type accounts'
+    },
+    { 'id': 'destination_payment_existence',
+      'condition' : 'python: object.hasDestinationSectionAccounting()'\
+    ' and object.getDestinationValue(portal_type="Account") is not None'\
+    ' and object.getDestinationValue(portal_type="Account").getAccountType()'\
+    ' == "asset/cash/bank"',
+      'type': 'CategoryAcquiredExistence',
+      'destination_payment' : 1,
+      'portal_type': Expression('portal/getPortalPaymentNodeTypeList'),
+      'message_category_not_set': 'Bank account must be defined for '\
+          'bank type accounts'
+    },
+ )
+
-- 
2.30.9