Commit 18daa5fc authored by Gabriel Monnerat's avatar Gabriel Monnerat

erp5_accounting: Improve AccountingTransaction_roundDebitCredit to fix the rounding issue

parent e5a7950d
...@@ -52,9 +52,18 @@ ...@@ -52,9 +52,18 @@
<key> <string>_body</string> </key> <key> <string>_body</string> </key>
<value> <string encoding="cdata"><![CDATA[ <value> <string encoding="cdata"><![CDATA[
""" Rounds debit and credit lines on generated transactions, according to \n """ Rounds debit and credit lines on generated transactions, according to\n
precision of this transaction\'s resource.\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
\n
precision = context.getQuantityPrecisionFromResource(context.getResource())\n precision = context.getQuantityPrecisionFromResource(context.getResource())\n
resource = context.getResourceValue()\n resource = context.getResourceValue()\n
\n \n
...@@ -77,9 +86,65 @@ for line in line_list:\n ...@@ -77,9 +86,65 @@ for line in line_list:\n
if abs(round(total_quantity, precision)) > 2 * resource.getBaseUnitQuantity():\n if abs(round(total_quantity, precision)) > 2 * resource.getBaseUnitQuantity():\n
return\n return\n
\n \n
# if the difference is <= the base quantity unit, we round the last line.\n total_price = round(context.getTotalPrice(), precision)\n
if line is not None:\n account_type_dict = {}\n
line.setQuantity(round(line.getQuantity() - total_quantity, precision))\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> ]]></string> </value>
......
...@@ -2453,6 +2453,11 @@ class TestAccountingExport(AccountingTestCase): ...@@ -2453,6 +2453,11 @@ class TestAccountingExport(AccountingTestCase):
class TestTransactions(AccountingTestCase): class TestTransactions(AccountingTestCase):
"""Test behaviours and utility scripts for Accounting Transactions. """Test behaviours and utility scripts for Accounting Transactions.
""" """
def getBusinessTemplateList(self):
return AccountingTestCase.getBusinessTemplateList(self) + \
('erp5_invoicing', 'erp5_simplified_invoicing')
def _resetIdGenerator(self): def _resetIdGenerator(self):
# clear all existing ids in portal ids # clear all existing ids in portal ids
self.portal.portal_ids.clearGenerator(all=True) self.portal.portal_ids.clearGenerator(all=True)
...@@ -3193,6 +3198,124 @@ class TestTransactions(AccountingTestCase): ...@@ -3193,6 +3198,124 @@ class TestTransactions(AccountingTestCase):
for line in invoice.contentValues(): for line in invoice.contentValues():
self.assertTrue(line.getGroupingReference()) 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): def test_AccountingTransaction_getTotalDebitCredit(self):
# source view # source view
......
...@@ -2607,6 +2607,72 @@ class TestSaleInvoice(TestSaleInvoiceMixin, TestInvoice, ERP5TypeTestCase): ...@@ -2607,6 +2607,72 @@ class TestSaleInvoice(TestSaleInvoiceMixin, TestInvoice, ERP5TypeTestCase):
""") """)
sequence_list.play(self, quiet=quiet) 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): def test_02_TwoInvoicesFromTwoPackingList(self, quiet=quiet):
""" """
This test was created for the following bug: This test was created for the following bug:
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment