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()