From 88f53b7315fe8699f7128800725fa4ad494a5abc Mon Sep 17 00:00:00 2001 From: "John Laterre (jol)" Date: Thu, 16 Nov 2023 16:03:09 +0100 Subject: [PATCH] [FIX] purchase_stock,stock_account: add analytic distribution to valuation lines Steps to reproduce: - Using a company whose currency is USD, make sure to have a rate change with EUR a week before Today. - Create an analytic plan with Default Applicability set to Mandatory. - Create an analytic account for the plan. - Create a Storable Product with a Product Category that has FIFO Costing Method and Automated Inventory Valuation. - Create a Request for Quotation in EUR using the new product and the new analytic account. - Confirm it, click on Receive Products, then Validate. - Go back to the RFQ and click on Create Bill. - Change the date to two weeks ago (in order to trigger an exchange difference). - Confirm it. Current Behavior: A `ValidationError` is raised in `_validate_distribution()`: "One or more lines require a 100% analytic distribution." Cause: This happens because the exchange difference journal items are created without `analytic_distribution` even though they should, since it was set as mandatory. Solution: Make sure the `analytic_distribution` is properly set when creating stock valuation lines. A test is also added to cover this use case. task-3582164 closes odoo/odoo#147387 X-original-commit: fafcf452c68aad27754a8c9bd7bde1742be61cb8 Signed-off-by: Habib Ayob (ayh) Signed-off-by: John Laterre (jol) --- .../tests/test_purchase_order_process.py | 91 +++++++++++++++++++ addons/stock_account/models/stock_move.py | 30 +++--- 2 files changed, 108 insertions(+), 13 deletions(-) diff --git a/addons/purchase_stock/tests/test_purchase_order_process.py b/addons/purchase_stock/tests/test_purchase_order_process.py index a3f35db8cf7..2968ba7c08d 100644 --- a/addons/purchase_stock/tests/test_purchase_order_process.py +++ b/addons/purchase_stock/tests/test_purchase_order_process.py @@ -1,6 +1,9 @@ +from odoo import Command, fields +from odoo.tests import tagged from .common import PurchaseTestCommon +@tagged('post_install', '-at_install') class TestPurchaseOrderProcess(PurchaseTestCommon): def test_00_cancel_purchase_order_flow(self): @@ -54,3 +57,91 @@ class TestPurchaseOrderProcess(PurchaseTestCommon): }) po.button_confirm() self.assertEqual(po.order_line.move_ids.product_packaging_id, packaging) + + def test_analytic_distribution_propagation_with_exchange_difference(self): + # Create 2 rates in order to generate an exchange difference later. + eur = self.env.ref('base.EUR') + eur.write({ + 'rate_ids': [ + Command.clear(), + Command.create({ + 'name': fields.Date.from_string('2023-01-01'), + 'company_rate': 2.0, + }), + Command.create({ + 'name': fields.Date.from_string('2023-12-01'), + 'company_rate': 3.0, + }), + ], + 'active': True, + }) + + # Create a mandatory analytic account. + analytic_plan = self.env['account.analytic.plan'].create({ + 'name': 'Analytic Plan', + 'default_applicability': 'mandatory', + }) + analytic_account = self.env['account.analytic.account'].create({ + 'name': 'Analytic Account', + 'plan_id': analytic_plan.id}, + ) + + # Create a storable product with FIFO costing method and automated inventory valuation. + analytic_product_category = self.env['product.category'].create({ + 'name': 'Analytic Product Category', + 'property_cost_method': 'fifo', + 'property_valuation': 'real_time', + }) + analytic_product = self.env['product.product'].create({ + 'name': 'Analytic Product', + 'detailed_type': 'product', + 'categ_id': analytic_product_category.id, + 'lst_price': 100.0, + 'standard_price': 25.0, + }) + + # Create and confirm a Purchase Order using aforementioned product and currency. + purchase_order = self.env['purchase.order'].create({ + 'date_order': fields.Date.from_string('2023-12-04'), + 'currency_id': eur.id, + 'partner_id': self.partner.id, + 'order_line': [ + Command.create({ + 'product_id': analytic_product.id, + 'product_qty': 10.0, + 'analytic_distribution': {analytic_account.id: 100}, + }), + ], + }) + purchase_order.button_confirm() + + # Make sure a stock move has been created to replenish the product. + self.assertEqual(len(purchase_order.picking_ids.move_ids), 1) + + stock_move = purchase_order.picking_ids.move_ids + stock_move.quantity = stock_move.product_uom_qty + + purchase_order.picking_ids.button_validate() + purchase_order.action_create_invoice() + + # Make sure a first Journal Entry has been created (to account for the stock move). + self.assertEqual(len(stock_move.account_move_ids), 1) + stock_account_move = stock_move.account_move_ids + + # Make sure the Vendor Bill has been created, + # and confirm it at an earlier date (to generate the exchange difference). + self.assertEqual(len(purchase_order.invoice_ids), 1) + + vendor_bill = purchase_order.invoice_ids + vendor_bill.invoice_date = fields.Date.from_string('2023-11-01') + vendor_bill.action_post() + + # Make sure a second Journal Entry has been created (to account for the exchange difference). + self.assertEqual(len(stock_move.account_move_ids), 2) + exchange_account_move = stock_move.account_move_ids - stock_account_move + + # Make sure both exchange Journal Items have the correct analytic distribution. + self.assertEqual(len(exchange_account_move.line_ids), 2) + for line in exchange_account_move.line_ids: + self.assertTrue(line.analytic_distribution) + self.assertEqual(line.analytic_distribution[str(analytic_account.id)], 100) diff --git a/addons/stock_account/models/stock_move.py b/addons/stock_account/models/stock_move.py index e973c5a7629..2ea4ad378df 100644 --- a/addons/stock_account/models/stock_move.py +++ b/addons/stock_account/models/stock_move.py @@ -463,29 +463,33 @@ class StockMove(models.Model): def _generate_valuation_lines_data(self, partner_id, qty, debit_value, credit_value, debit_account_id, credit_account_id, svl_id, description): # This method returns a dictionary to provide an easy extension hook to modify the valuation lines (see purchase for an example) self.ensure_one() - debit_line_vals = { + + line_vals = { 'name': description, 'product_id': self.product_id.id, 'quantity': qty, 'product_uom_id': self.product_id.uom_id.id, 'ref': description, 'partner_id': partner_id, - 'balance': debit_value, - 'account_id': debit_account_id, } - credit_line_vals = { - 'name': description, - 'product_id': self.product_id.id, - 'quantity': qty, - 'product_uom_id': self.product_id.uom_id.id, - 'ref': description, - 'partner_id': partner_id, - 'balance': -credit_value, - 'account_id': credit_account_id, + svl = self.env['stock.valuation.layer'].browse(svl_id) + if svl.account_move_line_id.analytic_distribution: + line_vals['analytic_distribution'] = svl.account_move_line_id.analytic_distribution + + rslt = { + 'credit_line_vals': { + **line_vals, + 'balance': -credit_value, + 'account_id': credit_account_id, + }, + 'debit_line_vals': { + **line_vals, + 'balance': debit_value, + 'account_id': debit_account_id, + }, } - rslt = {'credit_line_vals': credit_line_vals, 'debit_line_vals': debit_line_vals} if credit_value != debit_value: # for supplier returns of product in average costing method, in anglo saxon mode diff_amount = debit_value - credit_value