From 18daa5fcb259b33b016a66dc4ee715e495a6dbc1 Mon Sep 17 00:00:00 2001
From: Gabriel Monnerat <gabriel@tiolive.com>
Date: Tue, 25 Nov 2014 16:36:42 +0100
Subject: [PATCH] erp5_accounting: Improve
 AccountingTransaction_roundDebitCredit to fix the rounding issue

---
 ...AccountingTransaction_roundDebitCredit.xml |  75 ++++++++++-
 product/ERP5/tests/testAccounting.py          | 123 ++++++++++++++++++
 product/ERP5/tests/testInvoice.py             |  66 ++++++++++
 3 files changed, 259 insertions(+), 5 deletions(-)

diff --git a/bt5/erp5_accounting/SkinTemplateItem/portal_skins/erp5_accounting/AccountingTransaction_roundDebitCredit.xml b/bt5/erp5_accounting/SkinTemplateItem/portal_skins/erp5_accounting/AccountingTransaction_roundDebitCredit.xml
index 49d6a9625c..5380570a25 100644
--- a/bt5/erp5_accounting/SkinTemplateItem/portal_skins/erp5_accounting/AccountingTransaction_roundDebitCredit.xml
+++ b/bt5/erp5_accounting/SkinTemplateItem/portal_skins/erp5_accounting/AccountingTransaction_roundDebitCredit.xml
@@ -52,9 +52,18 @@
             <key> <string>_body</string> </key>
             <value> <string encoding="cdata"><![CDATA[
 
-""" Rounds debit and credit lines on generated transactions, according to \n
-precision of this transaction\'s resource.\n
+""" Rounds debit and credit lines on generated transactions, according to\n
+precision of this transaction\'s resource. \n
+\n
+What is expected with this script:\n
+\n
+  - All lines are rounded to the currency precision\n
+  - Amount on the receivable accounting line match invoice total price\n
+  - total debit == total credit\n
+  - In reality we probably also want that amount on vat line match invoice vat\n
+  amount, but we have ignored this.\n
 """\n
+\n
 precision = context.getQuantityPrecisionFromResource(context.getResource())\n
 resource = context.getResourceValue()\n
 \n
@@ -77,9 +86,65 @@ for line in line_list:\n
 if abs(round(total_quantity, precision)) > 2 * resource.getBaseUnitQuantity():\n
   return\n
 \n
-# if the difference is <= the base quantity unit, we round the last line.\n
-if line is not None:\n
-  line.setQuantity(round(line.getQuantity() - total_quantity, precision))\n
+total_price = round(context.getTotalPrice(), precision)\n
+account_type_dict = {}\n
+\n
+for line in line_list:\n
+  for account in (line.getSourceValue(portal_type=\'Account\'),\n
+      line.getDestinationValue(portal_type=\'Account\'),):\n
+    account_type_dict.setdefault(line, set()).add(\n
+      account is not None and account.getAccountTypeValue() or None)\n
+\n
+account_type = context.getPortalObject().portal_categories.account_type\n
+receivable_type = account_type.asset.receivable\n
+payable_type = account_type.liability.payable\n
+abs_total_quantity = abs(round(total_quantity, precision))\n
+line_to_adjust = None\n
+\n
+asset_line = None\n
+for line, account_type_list in account_type_dict.iteritems():\n
+  if receivable_type in account_type_list or payable_type in account_type_list:\n
+    asset_line = line\n
+    break\n
+\n
+if not asset_line:\n
+  assert total_price == 0.0 and total_quantity == 0.0, \\\n
+    \'receivable or payable line not found.\'\n
+  return\n
+\n
+# If we have a difference between total credit and total debit, one line is \n
+# chosen to add or remove this difference. The payable or receivable is chosen \n
+# only if this line is not matching with invoice total price, because total price\n
+# comes from all invoice lines (quantity * price) and it is what should be payed.\n
+# And payable or receivable line is the record in the accounting of what has \n
+# to be payed. Then, we must not touch it when it already matches.\n
+# If is not a payable or receivable, vat or other line (ie. income) is used.\n
+if abs_total_quantity != 0:\n
+  if round(abs(asset_line.getQuantity()), precision) != round(abs(context.getTotalPrice()), precision):\n
+    # adjust payable or receivable\n
+    for line in line_list:\n
+      if receivable_type in account_type_dict[line] or \\\n
+          payable_type in account_type_dict[line]:\n
+        line_to_adjust = line\n
+        break\n
+  if line_to_adjust is None:\n
+    # VAT\n
+    for line in line_list:\n
+      if receivable_type.refundable_vat in account_type_dict[line] or \\\n
+          payable_type.collected_vat in account_type_dict[line]:\n
+        line_to_adjust = line\n
+        break\n
+  if line_to_adjust is None:\n
+    # adjust anything except payable or receivable\n
+    for line in line_list:\n
+      if receivable_type not in account_type_dict[line] and \\\n
+          payable_type not in account_type_dict[line]:\n
+        line_to_adjust = line\n
+        break\n
+\n
+if line_to_adjust is not None:\n
+  line_to_adjust.setQuantity(\n
+    round(line_to_adjust.getQuantity() - total_quantity, precision))\n
 
 
 ]]></string> </value>
diff --git a/product/ERP5/tests/testAccounting.py b/product/ERP5/tests/testAccounting.py
index 2ca0d4a24d..9a45704a49 100644
--- a/product/ERP5/tests/testAccounting.py
+++ b/product/ERP5/tests/testAccounting.py
@@ -2453,6 +2453,11 @@ class TestAccountingExport(AccountingTestCase):
 class TestTransactions(AccountingTestCase):
   """Test behaviours and utility scripts for Accounting Transactions.
   """
+
+  def getBusinessTemplateList(self):
+    return AccountingTestCase.getBusinessTemplateList(self) + \
+        ('erp5_invoicing', 'erp5_simplified_invoicing')
+
   def _resetIdGenerator(self):
     # clear all existing ids in portal ids
       self.portal.portal_ids.clearGenerator(all=True)
@@ -3193,6 +3198,124 @@ class TestTransactions(AccountingTestCase):
     for line in invoice.contentValues():
       self.assertTrue(line.getGroupingReference())
 
+  def test_roundDebitCredit_raises_if_big_difference(self):
+    invoice = self._makeOne(
+      portal_type='Sale Invoice Transaction',
+      lines=(dict(source_value=self.account_module.goods_sales,
+                source_debit=100.032345),
+           dict(source_value=self.account_module.receivable,
+                source_credit=100.000001)))
+    precision = invoice.getQuantityPrecisionFromResource(invoice.getResource())
+    invoice.newContent(portal_type='Invoice Line', quantity=1, price=100)
+    self.assertRaises(invoice.AccountingTransaction_roundDebitCredit)
+
+  def test_roundDebitCredit_when_payable_is_different_total_price(self):
+    invoice = self._makeOne(
+      portal_type='Purchase Invoice Transaction',
+      stop_date=DateTime(),
+      destination_section_value=self.section,
+      source_section_value=self.organisation_module.supplier,
+      lines=(dict(source_value=self.account_module.goods_purchase,
+                id="expense",
+                destination_debit=100.000001),
+           dict(source_value=self.account_module.payable,
+                id="payable",
+                destination_credit=100.012345)))
+    precision = invoice.getQuantityPrecisionFromResource(invoice.getResource())
+    invoice.newContent(portal_type='Invoice Line', quantity=1, price=100)
+    line_list = invoice.getMovementList(
+                    portal_type=invoice.getPortalAccountingMovementTypeList())
+    self.assertNotEqual(0.0,
+      sum([round(g.getQuantity(), precision) for g in line_list]))
+    invoice.AccountingTransaction_roundDebitCredit()
+    line_list = invoice.getMovementList(
+                 portal_type=invoice.getPortalAccountingMovementTypeList())
+    self.assertEqual(0.0,
+      sum([round(g.getQuantity(), precision) for g in line_list]))
+    self.assertEqual(100.00, invoice.payable.getDestinationCredit())
+    self.assertEqual(100.00, invoice.expense.getDestinationDebit())
+    self.assertEqual([], invoice.checkConsistency())
+
+  def test_roundDebitCredit_when_payable_is_equal_total_price(self):
+    invoice = self._makeOne(
+      portal_type='Purchase Invoice Transaction',
+      stop_date=DateTime(),
+      destination_section_value=self.section,
+      source_section_value=self.organisation_module.supplier,
+      lines=(dict(source_value=self.account_module.goods_purchase,
+                id="expense",
+                destination_debit=100.012345),
+           dict(source_value=self.account_module.payable,
+                id="payable",
+               destination_credit=100.000001)))
+    precision = invoice.getQuantityPrecisionFromResource(invoice.getResource())
+    invoice.newContent(portal_type='Invoice Line', quantity=1, price=100)
+    line_list = invoice.getMovementList(
+                    portal_type=invoice.getPortalAccountingMovementTypeList())
+    self.assertNotEqual(0.0,
+      sum([round(g.getQuantity(), precision) for g in line_list]))
+    invoice.AccountingTransaction_roundDebitCredit()
+    line_list = invoice.getMovementList(
+                 portal_type=invoice.getPortalAccountingMovementTypeList())
+    self.assertEqual(0.0,
+      sum([round(g.getQuantity(), precision) for g in line_list]))
+    self.assertEqual(100.00, invoice.payable.getDestinationCredit())
+    self.assertEqual(100.00, invoice.expense.getDestinationDebit())
+    self.assertEqual([], invoice.checkConsistency())
+
+  def test_roundDebitCredit_when_receivable_is_equal_total_price(self):
+    invoice = self._makeOne(
+      portal_type='Sale Invoice Transaction',
+      stop_date=DateTime(),
+      destination_section_value=self.section,
+      source_section_value=self.section,
+      lines=(dict(source_value=self.account_module.goods_sales,
+                id="income",
+                source_credit=100.012345),
+           dict(source_value=self.account_module.receivable,
+                id="receivable",
+                source_debit=100.000001)))
+    precision = invoice.getQuantityPrecisionFromResource(invoice.getResource())
+    invoice.newContent(portal_type='Invoice Line', quantity=1, price=100)
+    line_list = invoice.getMovementList(
+                    portal_type=invoice.getPortalAccountingMovementTypeList())
+    self.assertNotEqual(sum([round(g.getQuantity(), precision) for g in line_list]),
+      0.0)
+    invoice.AccountingTransaction_roundDebitCredit()
+    line_list = invoice.getMovementList(
+                 portal_type=invoice.getPortalAccountingMovementTypeList())
+    self.assertEqual(sum([round(g.getQuantity(), precision) for g in line_list]),
+      0.0)
+    self.assertEqual(100.00, invoice.income.getSourceCredit())
+    self.assertEqual(100.00, invoice.receivable.getSourceDebit())
+    self.assertEqual([], invoice.checkConsistency())
+
+  def test_roundDebitCredit_when_receivable_is_different_total_price(self):
+    invoice = self._makeOne(
+      portal_type='Sale Invoice Transaction',
+      stop_date=DateTime(),
+      destination_section_value=self.section,
+      source_section_value=self.section,
+      lines=(dict(source_value=self.account_module.goods_sales,
+                id="income",
+                source_credit=100.000001),
+           dict(source_value=self.account_module.receivable,
+                id="receivable",
+                source_debit=100.012345)))
+    precision = invoice.getQuantityPrecisionFromResource(invoice.getResource())
+    invoice.newContent(portal_type='Invoice Line', quantity=1, price=100)
+    line_list = invoice.getMovementList(
+                    portal_type=invoice.getPortalAccountingMovementTypeList())
+    self.assertNotEqual(sum([round(g.getQuantity(), precision) for g in line_list]),
+      0.0)
+    invoice.AccountingTransaction_roundDebitCredit()
+    line_list = invoice.getMovementList(
+                 portal_type=invoice.getPortalAccountingMovementTypeList())
+    self.assertEqual(sum([round(g.getQuantity(), precision) for g in line_list]),
+      0.0)
+    self.assertEqual(100.00, invoice.income.getSourceCredit())
+    self.assertEqual(100.00, invoice.receivable.getSourceDebit())
+    self.assertEqual([], invoice.checkConsistency())
 
   def test_AccountingTransaction_getTotalDebitCredit(self):
     # source view
diff --git a/product/ERP5/tests/testInvoice.py b/product/ERP5/tests/testInvoice.py
index b5a6b460e6..883f30e309 100644
--- a/product/ERP5/tests/testInvoice.py
+++ b/product/ERP5/tests/testInvoice.py
@@ -2607,6 +2607,72 @@ class TestSaleInvoice(TestSaleInvoiceMixin, TestInvoice, ERP5TypeTestCase):
       """)
     sequence_list.play(self, quiet=quiet)
 
+  def stepCreateCurrency(self, sequence):
+    currency = self.portal.currency_module.newContent(
+      portal_type="Currency", title="Currency",
+      base_unit_quantity=0.01)
+    sequence.edit(currency=currency)
+
+  def stepCheckInvoiceWithBadPrecision(self, sequence):
+    portal = self.portal
+    vendor = sequence.get('vendor')
+    invoice = portal.accounting_module.newContent(
+      portal_type="Sale Invoice Transaction",
+      specialise=self.business_process,
+      source_section_value=vendor,
+      start_date=self.datetime,
+      price_currency_value=sequence.get('currency'),
+      destination_section_value=sequence.get('client1'),
+      source_value=vendor)
+    resource = self.portal.getDefaultModule(
+        self.resource_portal_type).newContent(
+                    portal_type=self.resource_portal_type,
+                    title='Resource',
+                    sale_supply_line_source_account="account_module/sale",
+                    product_line='apparel')
+    product_line = invoice.newContent(portal_type="Invoice Line",
+      resource_value=resource, quantity=1, price=0.014)
+    product_line = invoice.newContent(portal_type="Invoice Line",
+      resource_value=resource, quantity=1, price=0.014)
+    self.tic()
+    invoice.plan()
+    invoice.confirm()
+    self.tic()
+    invoice.start()
+    self.tic()
+    movement_list = invoice.getMovementList(
+        portal_type=invoice.getPortalAccountingMovementTypeList())
+    receivable_line = [m for m in movement_list \
+      if m.getSourceValue().getAccountType() == \
+        "asset/receivable"][0]
+    self.assertEquals(0.03, receivable_line.getSourceDebit())
+    data = invoice.Invoice_getODTDataDict()
+    precision = invoice.getQuantityPrecisionFromResource(
+      invoice.getResource())
+    self.assertEquals(round(data['total_price'], precision),
+      receivable_line.getSourceDebit())
+    vat_line = [m for m in movement_list \
+      if m.getSourceValue().getAccountType() == \
+        "liability/payable/collected_vat"][0]
+    self.assertEquals(0.0, vat_line.getSourceDebit())
+    income_line = [m for m in movement_list \
+      if m.getSourceValue().getAccountType() == \
+        "income"][0]
+    self.assertEquals(0.03, income_line.getSourceCredit())
+
+  def test_AccountingTransaction_roundDebitCredit(self):
+    """
+      Check that with two invoice lines with total price equal 0.14,
+      the receivable line will be 0.03 and vat line 0
+    """
+    sequence_list = SequenceList()
+    sequence_list.addSequenceString("""
+      stepCreateCurrency
+      stepCreateEntities
+      stepCheckInvoiceWithBadPrecision
+    """)
+    sequence_list.play(self)
+
   def test_02_TwoInvoicesFromTwoPackingList(self, quiet=quiet):
     """
     This test was created for the following bug:
-- 
2.30.9