From c687a89b5c280a8a733959849127ae04ff1c8e17 Mon Sep 17 00:00:00 2001 From: Ahmed Khalaf Date: Thu, 10 Aug 2023 14:27:51 +0200 Subject: [PATCH] [FIX] mrp_subcontracting_purchase: create svl on price edit When a product is purchased and received, valuation layers are created. if we created a bill for the PO and change the product price, when the account move is posted, `_apply_price_difference` creates new SVLs to correct the product value. Before this commit, the above scenario did not work with subcontracted products, mainly because the valuation layer is linked to the finished product move of the MO and not the incoming move of the PO. This is fixed in `_get_valued_in_moves` where if the move is_subcontract the orig move is the one linked to the valuation layer. Now that we have the valuation layer, another issue arises, in the aml `_generate_price_difference_vals` when the difference is calculated. the `unit_valuation_difference` is not properly calculated because the value in the layer includes the components value, so its incorrect to compare it to the AML price, so we use `_get_layer_price_unit` to get the layer_price_unit relevant to the AML, which in this case is the price of subcontracting (price on the PO) Also for automated inventory valuation, when the inventory correction entry is created it must credit the stock input account since the value of the subcontracted product is corrected. closes odoo/odoo#131565 Taskid: 3449931 Signed-off-by: William Henrotin (whe) --- .../models/stock_move.py | 4 ++ .../models/__init__.py | 1 + .../models/account_move_line.py | 6 +++ .../models/stock_valuation_layer.py | 20 +++++++++ .../tests/test_mrp_subcontracting_purchase.py | 41 +++++++++++++++++++ addons/purchase_stock/models/__init__.py | 1 + .../models/account_move_line.py | 4 +- .../models/stock_valuation_layer.py | 12 ++++++ 8 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 addons/mrp_subcontracting_purchase/models/stock_valuation_layer.py create mode 100644 addons/purchase_stock/models/stock_valuation_layer.py diff --git a/addons/mrp_subcontracting_account/models/stock_move.py b/addons/mrp_subcontracting_account/models/stock_move.py index 744f3f993ca..2b92ecc7fce 100644 --- a/addons/mrp_subcontracting_account/models/stock_move.py +++ b/addons/mrp_subcontracting_account/models/stock_move.py @@ -51,6 +51,10 @@ class StockMove(models.Model): 'balance': -component_cost, 'account_id': credit_account_id, } + # if svl passed is not linked to the move in self, the valuation is a correction and should always credit the + # `stock_input` account as it adds directly to the value of the subcontracted product + elif svl_id and self.stock_valuation_layer_ids.ids and svl_id not in self.stock_valuation_layer_ids.ids: + rslt['credit_line_vals']['account_id'] = self.product_id.product_tmpl_id.get_product_accounts()['stock_input'].id return rslt def _get_dest_account(self, account_data): diff --git a/addons/mrp_subcontracting_purchase/models/__init__.py b/addons/mrp_subcontracting_purchase/models/__init__.py index 7952bc4194f..d72772bf68b 100644 --- a/addons/mrp_subcontracting_purchase/models/__init__.py +++ b/addons/mrp_subcontracting_purchase/models/__init__.py @@ -3,4 +3,5 @@ from . import account_move_line from . import stock_picking +from . import stock_valuation_layer from . import purchase_order diff --git a/addons/mrp_subcontracting_purchase/models/account_move_line.py b/addons/mrp_subcontracting_purchase/models/account_move_line.py index de7bf8a86c3..51d3eeede80 100644 --- a/addons/mrp_subcontracting_purchase/models/account_move_line.py +++ b/addons/mrp_subcontracting_purchase/models/account_move_line.py @@ -14,3 +14,9 @@ class AccountMoveLine(models.Model): components_cost -= sum(subcontract_production.move_raw_ids.stock_valuation_layer_ids.mapped('value')) price_unit_val_dif = price_unit_val_dif + components_cost / relevant_qty return price_unit_val_dif, relevant_qty + + def _get_valued_in_moves(self): + res = super()._get_valued_in_moves() + # subcontracted move valuations are not linked to the PO move but its orig move (the MO finished move) + res |= res.filtered(lambda m: m.is_subcontract).move_orig_ids + return res diff --git a/addons/mrp_subcontracting_purchase/models/stock_valuation_layer.py b/addons/mrp_subcontracting_purchase/models/stock_valuation_layer.py new file mode 100644 index 00000000000..2e305cb27d2 --- /dev/null +++ b/addons/mrp_subcontracting_purchase/models/stock_valuation_layer.py @@ -0,0 +1,20 @@ + + +from odoo import models + + +class StockValuationLayer(models.Model): + _inherit = 'stock.valuation.layer' + + def _get_layer_price_unit(self): + """ For a subcontracted product, we want a way to get the subcontracting cost (the price on the PO) + This override deducts the value of subcomponents from the layer price. + """ + components_price = 0 + production = self.stock_move_id.production_id + if production.subcontractor_id and production.state == 'done': + # each layer has a quantity and price for each move, to get the correct component price for each move + # we need to get the components used for each quantity + for move in production.move_raw_ids: + components_price += sum(move.sudo().stock_valuation_layer_ids.mapped('value')) / production.product_uom_qty + return super()._get_layer_price_unit() - components_price diff --git a/addons/mrp_subcontracting_purchase/tests/test_mrp_subcontracting_purchase.py b/addons/mrp_subcontracting_purchase/tests/test_mrp_subcontracting_purchase.py index 97b4cf41694..e42fb5d8989 100644 --- a/addons/mrp_subcontracting_purchase/tests/test_mrp_subcontracting_purchase.py +++ b/addons/mrp_subcontracting_purchase/tests/test_mrp_subcontracting_purchase.py @@ -292,3 +292,44 @@ class MrpSubcontractingPurchaseTest(TestMrpSubcontractingCommon): # price diff line should be 100 - 50 - 10 - 20 price_diff_line = invoice.line_ids.filtered(lambda m: m.account_id == stock_price_diff_acc_id) self.assertEqual(price_diff_line.credit, 20) + + def test_subcontract_product_price_change(self): + """ Create a PO for subcontracted product, receive the product (finish MO), + create vendor bill and edit the product price, confirm the bill. + An extra SVL should be created to correct the valuation of the product + Also check account move data for real time inventory + """ + product_category_all = self.env.ref('product.product_category_all') + product_category_all.property_cost_method = 'fifo' + product_category_all.property_valuation = 'real_time' + stock_in_acc_id = product_category_all.property_stock_account_input_categ_id.id + purchase = self.env['purchase.order'].create({ + 'partner_id': self.subcontractor_partner1.id, + 'order_line': [Command.create({ + 'name': self.finished.name, + 'product_id': self.finished.id, + 'product_uom_qty': 10, + 'product_uom': self.finished.uom_id.id, + 'price_unit': 1, + })], + }) + + purchase.button_confirm() + # receive product + receipt = purchase.picking_ids + receipt.move_ids.quantity_done = 10 + receipt.button_validate() + # create bill + purchase.action_create_invoice() + aml = self.env['account.move.line'].search([('purchase_line_id', '=', purchase.order_line.id)]) + # add 0.5 per unit ( 0.5 x 10 ) = 5 extra valuation + aml.price_unit = 1.5 + aml.move_id.invoice_date = Date.today() + aml.move_id.action_post() + svl = aml.stock_valuation_layer_ids + self.assertEqual(len(svl), 1) + self.assertEqual(svl.value, 5) + # check for the automated inventory valuation + account_move_credit_line = svl.account_move_id.line_ids.filtered(lambda l: l.credit > 0) + self.assertEqual(account_move_credit_line.account_id.id, stock_in_acc_id) + self.assertEqual(account_move_credit_line.credit, 5) diff --git a/addons/purchase_stock/models/__init__.py b/addons/purchase_stock/models/__init__.py index deda82d5df6..fc159a2d26e 100644 --- a/addons/purchase_stock/models/__init__.py +++ b/addons/purchase_stock/models/__init__.py @@ -11,3 +11,4 @@ from . import res_company from . import stock from . import stock_move from . import stock_rule +from . import stock_valuation_layer diff --git a/addons/purchase_stock/models/account_move_line.py b/addons/purchase_stock/models/account_move_line.py index 708aa77939d..99888bcded6 100644 --- a/addons/purchase_stock/models/account_move_line.py +++ b/addons/purchase_stock/models/account_move_line.py @@ -191,7 +191,7 @@ class AccountMoveLine(models.Model): out_qty_to_invoice = 0 aml = initial_pdiff_svl.account_move_line_id parent_layer = initial_pdiff_svl.stock_valuation_layer_id - layer_price_unit = parent_layer.value / parent_layer.quantity + layer_price_unit = parent_layer._get_layer_price_unit() else: sign = 1 # get the invoiced qty of the layer without considering `self` @@ -199,7 +199,7 @@ class AccountMoveLine(models.Model): remaining_out_qty_to_invoice = max(0, out_layer_qty - invoiced_layer_qty) out_qty_to_invoice = min(remaining_out_qty_to_invoice, invoicing_layer_qty) qty_to_correct = invoicing_layer_qty - out_qty_to_invoice - layer_price_unit = layer.value / layer.quantity + layer_price_unit = layer._get_layer_price_unit() aml = self aml_gross_price_unit = aml._get_gross_unit_price() diff --git a/addons/purchase_stock/models/stock_valuation_layer.py b/addons/purchase_stock/models/stock_valuation_layer.py new file mode 100644 index 00000000000..3959806cb0f --- /dev/null +++ b/addons/purchase_stock/models/stock_valuation_layer.py @@ -0,0 +1,12 @@ +from odoo import models + + +class StockValuationLayer(models.Model): + _inherit = 'stock.valuation.layer' + + def _get_layer_price_unit(self): + """ This function returns the value of product in a layer per unit, relative to the aml + the function is designed to be overriden to add logic to price unit calculation + :param layer: the layer the price unit is derived from + """ + return self.value / self.quantity