From e6228402e75e1982b742d0fb51262dacfbd67a90 Mon Sep 17 00:00:00 2001 From: Olivier Colson Date: Thu, 25 Oct 2018 17:57:52 +0200 Subject: [PATCH] [FIX] purchase: fix anglosaxon valuation Before that, when receiving, then invoicing with price diff, the value in interim account corresponded to the price on the invoice instead of the original valuation one. Scenario to reproduce the issue: * create PO with 1 product, FIFO valuation, price=10 * receive PO: the entry in stock interim account has debit=credit=10 * create invoice, with price=15 instead of 10 * the invoice entry should be: payable: C=15 ; price diff: D=5 ; interim account: D=10 ==> Instead, we had payable: C=20 ; price diff: D=5 ; interim account: D=15 => this was wrong Also added a test case for the same scenario, verifying the amounts written in the different accounts (and refactored a bit ValuationReconciliationTestCase for easiness) Closes #27394 --- addons/purchase/models/account_invoice.py | 8 +- ...st_anglo_saxon_valuation_reconciliation.py | 73 ++++++++++--------- addons/purchase/tests/test_stockvaluation.py | 50 +++++++++++++ ...st_anglo_saxon_valuation_reconciliation.py | 70 +++++++++++++----- ...o_saxon_valuation_reconciliation_common.py | 18 +++-- 5 files changed, 161 insertions(+), 58 deletions(-) diff --git a/addons/purchase/models/account_invoice.py b/addons/purchase/models/account_invoice.py index f8c36aeed09..91ed1678086 100644 --- a/addons/purchase/models/account_invoice.py +++ b/addons/purchase/models/account_invoice.py @@ -227,7 +227,7 @@ class AccountInvoice(models.Model): # valuation difference, it means this difference is due to exchange rates, # so we don't create anything, the exchange rate entries will # be processed automatically by the rest of the code. - diff_res.append({ + diff_line = { 'type': 'src', 'name': i_line.name[:64], 'price_unit': inv.currency_id.round(price_unit_val_dif), @@ -238,7 +238,11 @@ class AccountInvoice(models.Model): 'uom_id': line['uom_id'], 'account_analytic_id': line['account_analytic_id'], 'tax_ids': tax_ids, - }) + } + # We update the original line accordingly + line['price_unit'] = inv.currency_id.round(line['price_unit'] - diff_line['price_unit']) + line['price'] = inv.currency_id.round(line['quantity'] * line['price_unit']) + diff_res.append(diff_line) return diff_res return [] diff --git a/addons/purchase/tests/test_anglo_saxon_valuation_reconciliation.py b/addons/purchase/tests/test_anglo_saxon_valuation_reconciliation.py index 6443d2bf9ea..3058713d190 100644 --- a/addons/purchase/tests/test_anglo_saxon_valuation_reconciliation.py +++ b/addons/purchase/tests/test_anglo_saxon_valuation_reconciliation.py @@ -25,7 +25,7 @@ class TestValuationReconciliation(ValuationReconciliationTestCase): }) self.test_product_category.property_account_creditor_price_difference_categ = self.price_dif_account.id - def _create_purchase(self, product, quantity=1.0): + def _create_purchase(self, product, date, quantity=1.0): rslt = self.env['purchase.order'].create({ 'partner_id': self.test_partner.id, 'currency_id': self.currency_two.id, @@ -36,14 +36,14 @@ class TestValuationReconciliation(ValuationReconciliationTestCase): 'product_qty': quantity, 'product_uom': product.uom_po_id.id, 'price_unit': self.product_price_unit, - 'date_planned': '2017-10-07', + 'date_planned': date, })], - 'date_order': '2017-10-01', + 'date_order': date, }) rslt.button_confirm() return rslt - def _create_invoice_for_po(self, purchase_order): + def _create_invoice_for_po(self, purchase_order, date): account_receivable = self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_receivable').id)], limit=1) rslt = self.env['account.invoice'].create({ 'purchase_id': purchase_order.id, @@ -52,8 +52,8 @@ class TestValuationReconciliation(ValuationReconciliationTestCase): 'currency_id': self.currency_two.id, 'name': 'vendor bill', 'type': 'in_invoice', - 'date_invoice': '2017-12-31', - 'date': '2017-12-31', + 'date_invoice': date, + 'date': date, 'account_id': account_receivable.id, }) rslt.purchase_order_change() @@ -63,11 +63,16 @@ class TestValuationReconciliation(ValuationReconciliationTestCase): """ Tests the case into which we receive the goods first, and then make the invoice. """ test_product = self.test_product_delivery - purchase_order = self._create_purchase(test_product) + purchase_order = self._create_purchase(test_product, '2018-01-01') self._process_pickings(purchase_order.picking_ids) - invoice = self._create_invoice_for_po(purchase_order) - self.currency_rate.rate = 7.76435463 + invoice = self._create_invoice_for_po(purchase_order, '2018-02-02') + self.env['res.currency.rate'].create({ + 'currency_id': self.currency_one.id, + 'company_id': self.company.id, + 'rate': 7.76435463, + 'name': '2018-02-01', + }) invoice.action_invoice_open() picking = self.env['stock.picking'].search([('purchase_id','=',purchase_order.id)]) self.check_reconciliation(invoice, picking) @@ -78,29 +83,16 @@ class TestValuationReconciliation(ValuationReconciliationTestCase): def test_invoice_shipment(self): """ Tests the case into which we make the invoice first, and then receive the goods. """ - # First, we delete the currency rate defined in demo data for USD on June 6th. - # This is mandatory to ensure consistency of the data generated by the test, - # as stock pickings created from a PO are by design always created for - # the current date (there is no way forcing this), meaning that they - # always use today's exchange rate for their valuation. - # Despite the fact we rewrite the date of the valuation moves artificially, - # we cannot correct the debit and credit values, since the anglosaxon - # entries get automatically reconciled (and you cannot modify a reconciled entry). - # So, we have to make sure that "today"'s rate will always be the last rate we - # created in order to ensure complete control of the test. - self.env.ref('base.rateUSDbis').unlink() - - # Create a PO and an invoice for it test_product = self.test_product_order - purchase_order = self._create_purchase(test_product) + purchase_order = self._create_purchase(test_product, '2017-12-01') - invoice = self._create_invoice_for_po(purchase_order) + invoice = self._create_invoice_for_po(purchase_order, '2017-12-23') invoice_line = self.env['account.invoice.line'].search([('invoice_id', '=', invoice.id)]) invoice_line.quantity = 1 # The currency rate changes - self.currency_rate = self.env['res.currency.rate'].create({ + self.env['res.currency.rate'].create({ 'currency_id': self.currency_one.id, 'company_id': self.company.id, 'rate': 13.834739702, @@ -114,7 +106,7 @@ class TestValuationReconciliation(ValuationReconciliationTestCase): self.check_reconciliation(invoice, picking) # The currency rate changes again - self.currency_rate = self.env['res.currency.rate'].create({ + self.env['res.currency.rate'].create({ 'currency_id': self.currency_one.id, 'company_id': self.company.id, 'rate': 10.54739702, @@ -133,7 +125,7 @@ class TestValuationReconciliation(ValuationReconciliationTestCase): self._change_pickings_date(return_pick, '2018-01-13') # The currency rate changes again - self.currency_rate = self.env['res.currency.rate'].create({ + self.env['res.currency.rate'].create({ 'currency_id': self.currency_one.id, 'company_id': self.company.id, 'rate': 9.56564564, @@ -158,25 +150,40 @@ class TestValuationReconciliation(ValuationReconciliationTestCase): """ Tests the case into which we receive part of the goods first, then 2 invoices at different rates, and finally the remaining quantities """ test_product = self.test_product_delivery - purchase_order = self._create_purchase(test_product, quantity=5.0) + purchase_order = self._create_purchase(test_product, '2017-01-01', quantity=5.0) self._process_pickings(purchase_order.picking_ids, quantity=2.0) picking = self.env['stock.picking'].search([('purchase_id', '=', purchase_order.id)], order="id asc", limit=1) - invoice = self._create_invoice_for_po(purchase_order) + invoice = self._create_invoice_for_po(purchase_order, '2017-01-15') invoice_line = self.env['account.invoice.line'].search([('invoice_id', '=', invoice.id)]) invoice_line.quantity = 3 - self.currency_rate.rate = 7.76435463 + self.env['res.currency.rate'].create({ + 'currency_id': self.currency_one.id, + 'company_id': self.company.id, + 'rate': 7.76435463, + 'name': '2017-02-01', + }) invoice.action_invoice_open() self.check_reconciliation(invoice, picking, full_reconcile=False) - invoice2 = self._create_invoice_for_po(purchase_order) + invoice2 = self._create_invoice_for_po(purchase_order, '2017-02-15') invoice_line = self.env['account.invoice.line'].search([('invoice_id', '=', invoice2.id)]) invoice_line.quantity = 2 - self.currency_rate.rate = 13.834739702 + self.env['res.currency.rate'].create({ + 'currency_id': self.currency_one.id, + 'company_id': self.company.id, + 'rate': 13.834739702, + 'name': '2017-03-01', + }) invoice2.action_invoice_open() self.check_reconciliation(invoice2, picking, full_reconcile=False) - self.currency_rate.rate = 12.195747002 + self.env['res.currency.rate'].create({ + 'currency_id': self.currency_one.id, + 'company_id': self.company.id, + 'rate': 12.195747002, + 'name': '2017-04-01', + }) self._process_pickings(purchase_order.picking_ids.filtered(lambda x: x.state != 'done'), quantity=3.0) picking = self.env['stock.picking'].search([('purchase_id', '=', purchase_order.id)], order='id desc', limit=1) self.check_reconciliation(invoice2, picking) diff --git a/addons/purchase/tests/test_stockvaluation.py b/addons/purchase/tests/test_stockvaluation.py index e515bd32c82..e03a455f115 100644 --- a/addons/purchase/tests/test_stockvaluation.py +++ b/addons/purchase/tests/test_stockvaluation.py @@ -4,6 +4,7 @@ import time from datetime import datetime +from odoo.tests import Form from odoo.tests.common import TransactionCase, tagged from odoo.addons.account.tests.account_test_classes import AccountingTestCase from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT @@ -412,3 +413,52 @@ class TestStockValuationWithCOA(AccountingTestCase): # check the anglo saxon entries price_diff_entry = self.env['account.move.line'].search([('account_id', '=', price_diff_account.id)]) self.assertEqual(price_diff_entry.credit, 100) + + def test_anglosaxon_valuation(self): + self.env.user.company_id.anglo_saxon_accounting = True + self.product1.product_tmpl_id.cost_method = 'fifo' + self.product1.product_tmpl_id.valuation = 'real_time' + self.product1.product_tmpl_id.invoice_policy = 'delivery' + price_diff_account = self.env['account.account'].create({ + 'name': 'price diff account', + 'code': 'price diff account', + 'user_type_id': self.env.ref('account.data_account_type_current_assets').id, + }) + self.product1.property_account_creditor_price_difference = price_diff_account + + # Create PO + po_form = Form(self.env['purchase.order']) + po_form.partner_id = self.partner_id + with po_form.order_line.new() as po_line: + po_line.product_id = self.product1 + po_line.product_qty = 1 + po_line.price_unit = 10.0 + order = po_form.save() + order.button_confirm() + + # Receive the goods + receipt = order.picking_ids[0] + receipt.move_lines.quantity_done = 1 + receipt.button_validate() + + # Create an invoice with a different price + invoice = self.env['account.invoice'].create({ + 'partner_id': order.partner_id.id, + 'purchase_id': order.id, + 'account_id': order.partner_id.property_account_payable_id.id, + 'type': 'in_invoice', + }) + invoice.purchase_order_change() + invoice.invoice_line_ids[0].price_unit = 15.0 + invoice.action_invoice_open() + + # Check what was posted in the price difference account + price_diff_aml = self.env['account.move.line'].search([('account_id','=',price_diff_account.id)]) + self.assertEquals(len(price_diff_aml), 1, "Only one line should have been generated in the price difference account.") + self.assertAlmostEquals(price_diff_aml.debit, 5, "Price difference should be equal to 5 (15-10)") + + # Check what was posted in stock input account + input_aml = self.env['account.move.line'].search([('account_id','=',self.stock_input_account.id)]) + self.assertEquals(len(input_aml), 2, "Only two lines should have been generated in stock input account: one when receiving the product, one when making the invoice.") + self.assertAlmostEquals(sum(input_aml.mapped('debit')), 10, "Total debit value on stock input account should be equal to the original PO price of the product.") + self.assertAlmostEquals(sum(input_aml.mapped('credit')), 10, "Total credit value on stock input account should be equal to the original PO price of the product.") diff --git a/addons/sale_stock/tests/test_anglo_saxon_valuation_reconciliation.py b/addons/sale_stock/tests/test_anglo_saxon_valuation_reconciliation.py index 37c77993954..3d84f710ca3 100644 --- a/addons/sale_stock/tests/test_anglo_saxon_valuation_reconciliation.py +++ b/addons/sale_stock/tests/test_anglo_saxon_valuation_reconciliation.py @@ -30,7 +30,7 @@ class TestValuationReconciliation(ValuationReconciliationTestCase): #set the invoice_policy to delivery to have an accurate COGS entry self.test_product_delivery.invoice_policy = "delivery" - def _create_sale(self, product, quantity=1.0): + def _create_sale(self, product, date, quantity=1.0): rslt = self.env['sale.order'].create({ 'partner_id': self.test_partner.id, 'currency_id': self.currency_two.id, @@ -42,18 +42,19 @@ class TestValuationReconciliation(ValuationReconciliationTestCase): 'product_uom': product.uom_po_id.id, 'price_unit': self.product_price_unit, })], + 'date_order': date, }) rslt.action_confirm() return rslt - def _create_invoice_for_so(self, sale_order, product): + def _create_invoice_for_so(self, sale_order, product, date): rslt = self.env['account.invoice'].create({ 'partner_id': self.test_partner.id, 'reference_type': 'none', 'currency_id': self.currency_two.id, 'name': 'customer invoice', 'type': 'out_invoice', - 'date_invoice': time.strftime('%Y') + '-12-22', + 'date_invoice': date, 'account_id': self.account_receivable.id, 'invoice_line_ids': [(0, 0, { 'name': 'test line', @@ -93,11 +94,16 @@ class TestValuationReconciliation(ValuationReconciliationTestCase): test_product = self.test_product_delivery self._set_initial_stock_for_product(test_product) - sale_order = self._create_sale(test_product) + sale_order = self._create_sale(test_product, '2108-01-01') self._process_pickings(sale_order.picking_ids) - invoice = self._create_invoice_for_so(sale_order, test_product) - self.currency_rate.rate = 9.87366352 + invoice = self._create_invoice_for_so(sale_order, test_product, '2018-02-12') + self.env['res.currency.rate'].create({ + 'currency_id': self.currency_one.id, + 'company_id': self.company.id, + 'rate': 9.87366352, + 'name': '2018-02-01', + }) invoice.action_invoice_open() picking = self.env['stock.picking'].search([('sale_id', '=', sale_order.id)]) self.check_reconciliation(invoice, picking, operation='sale') @@ -111,10 +117,15 @@ class TestValuationReconciliation(ValuationReconciliationTestCase): self.test_product_delivery.standard_price = 13 self._set_initial_stock_for_product(test_product) - sale_order = self._create_sale(test_product) + sale_order = self._create_sale(test_product, '2018-01-01') - invoice = self._create_invoice_for_so(sale_order, test_product) - self.currency_rate.rate = 0.974784 + invoice = self._create_invoice_for_so(sale_order, test_product, '2018-02-03') + self.env['res.currency.rate'].create({ + 'currency_id': self.currency_one.id, + 'company_id': self.company.id, + 'rate': 0.974784, + 'name': '2018-02-01', + }) invoice.action_invoice_open() self._process_pickings(sale_order.picking_ids) @@ -123,7 +134,12 @@ class TestValuationReconciliation(ValuationReconciliationTestCase): self.check_reconciliation(invoice, picking, operation='sale') #return the goods and refund the invoice - self.currency_rate.rate = 10.54739702 + self.env['res.currency.rate'].create({ + 'currency_id': self.currency_one.id, + 'company_id': self.company.id, + 'rate': 10.54739702, + 'name': '2018-03-01', + }) stock_return_picking = self.env['stock.return.picking']\ .with_context(active_ids=[picking.id], active_id=picking.id).create({}) stock_return_picking.product_return_moves.quantity = 1.0 @@ -132,7 +148,12 @@ class TestValuationReconciliation(ValuationReconciliationTestCase): return_pick.action_assign() return_pick.move_lines.quantity_done = 1 return_pick.action_done() - self.currency_rate.rate = 9.56564564 + self.env['res.currency.rate'].create({ + 'currency_id': self.currency_one.id, + 'company_id': self.company.id, + 'rate': 9.56564564, + 'name': '2018-04-01', + }) refund_invoice_wiz = self.env['account.invoice.refund'].with_context(active_ids=[invoice.id]).create({ 'description': 'test_invoice_shipment_refund', 'filter_refund': 'cancel', @@ -148,26 +169,41 @@ class TestValuationReconciliation(ValuationReconciliationTestCase): test_product = self.test_product_delivery self._set_initial_stock_for_product(test_product) - sale_order = self._create_sale(test_product, quantity=5) + sale_order = self._create_sale(test_product, '2018-01-01', quantity=5) self._process_pickings(sale_order.picking_ids, quantity=2.0) picking = self.env['stock.picking'].search([('sale_id', '=', sale_order.id)], order="id asc", limit=1) - invoice = self._create_invoice_for_so(sale_order, test_product) + invoice = self._create_invoice_for_so(sale_order, test_product, '2018-02-03') invoice_line = self.env['account.invoice.line'].search([('invoice_id', '=', invoice.id)]) invoice_line.quantity = 3 - self.currency_rate.rate = 7.76435463 + self.env['res.currency.rate'].create({ + 'currency_id': self.currency_one.id, + 'company_id': self.company.id, + 'rate': 7.76435463, + 'name': '2018-02-01', + }) invoice.action_invoice_open() self.check_reconciliation(invoice, picking, full_reconcile=False, operation='sale') - invoice2 = self._create_invoice_for_so(sale_order, test_product) + invoice2 = self._create_invoice_for_so(sale_order, test_product, '2018-03-12') invoice_line = self.env['account.invoice.line'].search([('invoice_id', '=', invoice2.id)]) invoice_line.quantity = 2 - self.currency_rate.rate = 13.834739702 + self.env['res.currency.rate'].create({ + 'currency_id': self.currency_one.id, + 'company_id': self.company.id, + 'rate': 13.834739702, + 'name': '2018-03-01', + }) invoice2.action_invoice_open() self.check_reconciliation(invoice2, picking, full_reconcile=False, operation='sale') - self.currency_rate.rate = 12.195747002 + self.env['res.currency.rate'].create({ + 'currency_id': self.currency_one.id, + 'company_id': self.company.id, + 'rate': 12.195747002, + 'name': '2018-04-01', + }) self._process_pickings(sale_order.picking_ids.filtered(lambda x: x.state != 'done'), quantity=3.0) picking = self.env['stock.picking'].search([('sale_id', '=', sale_order.id)], order='id desc', limit=1) self.check_reconciliation(invoice2, picking, operation='sale') diff --git a/addons/stock_account/tests/test_anglo_saxon_valuation_reconciliation_common.py b/addons/stock_account/tests/test_anglo_saxon_valuation_reconciliation_common.py index b2e3bd0d095..cb254a392ab 100644 --- a/addons/stock_account/tests/test_anglo_saxon_valuation_reconciliation_common.py +++ b/addons/stock_account/tests/test_anglo_saxon_valuation_reconciliation_common.py @@ -58,12 +58,6 @@ class ValuationReconciliationTestCase(AccountingTestCase): self.currency_one = self.company.currency_id currency_two_name = 'USD' if self.currency_one.name != 'USD' else 'EUR' self.currency_two = self.env['res.currency'].search([('name', '=', currency_two_name)]) - self.currency_rate = self.env['res.currency.rate'].create({ - 'currency_id': self.currency_one.id, - 'company_id': self.company.id, - 'rate': 1.234343354, # Totally arbitratry value - 'name': '2017-01-01', - }) self.input_account = self.env['account.account'].create({ 'name': 'Test stock in', @@ -127,3 +121,15 @@ class ValuationReconciliationTestCase(AccountingTestCase): }) self.product_price_unit = 66.0 + + # We delete the currency rate defined in demo data for USD on June 6th. + # This is mandatory to ensure consistency of the data generated by the test, + # as stock pickings created from a PO are by design always created for + # the current date (there is no way forcing this), meaning that they + # always use today's exchange rate for their valuation. + # Despite the fact we rewrite the date of the valuation moves artificially, + # we cannot correct the debit and credit values, since the anglosaxon + # entries get automatically reconciled (and you cannot modify a reconciled entry). + # So, we have to make sure that "today"'s rate will always be the last rate we + # created in order to ensure complete control of the test. + self.env.ref('base.rateUSDbis').unlink()