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