From 6624cbf75bc9169735cd197a47fada38d29b24f2 Mon Sep 17 00:00:00 2001 From: yhu-odoo Date: Wed, 19 Apr 2023 13:16:48 +0000 Subject: [PATCH] [FIX] mrp_subcontracting_purchase, purchase_stock: price difference entry wrong when resupply subcontractor In case product is using standard price and automated inventory valuation. We post account entries in price difference account when the price on purchase order is different from the cost of product. However, when we do subcontracting and resupply our subcontractor, the cost is not just the price on purchase order, but also the cost of the components we sent to the subcontractor. Currently, we didn't take the components cost in to account when post price difference entries. Fix it in this commit. Task-3223451 closes odoo/odoo#132282 X-original-commit: e771535b487e76f4e5a23b767d220d7f640292a6 Signed-off-by: William Henrotin (whe) Signed-off-by: Yuchen Huang (yhu) --- .../models/__init__.py | 1 + .../models/account_move_line.py | 16 ++++++ .../tests/test_mrp_subcontracting_purchase.py | 53 +++++++++++++++++++ .../purchase_stock/models/account_invoice.py | 37 +------------ .../models/account_move_line.py | 42 +++++++++++++++ 5 files changed, 113 insertions(+), 36 deletions(-) create mode 100644 addons/mrp_subcontracting_purchase/models/account_move_line.py diff --git a/addons/mrp_subcontracting_purchase/models/__init__.py b/addons/mrp_subcontracting_purchase/models/__init__.py index eee25d6fe21..7952bc4194f 100644 --- a/addons/mrp_subcontracting_purchase/models/__init__.py +++ b/addons/mrp_subcontracting_purchase/models/__init__.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from . import account_move_line from . import stock_picking 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 new file mode 100644 index 00000000000..de7bf8a86c3 --- /dev/null +++ b/addons/mrp_subcontracting_purchase/models/account_move_line.py @@ -0,0 +1,16 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models + + +class AccountMoveLine(models.Model): + _inherit = 'account.move.line' + + def _get_price_unit_val_dif_and_relevant_qty(self): + price_unit_val_dif, relevant_qty = super()._get_price_unit_val_dif_and_relevant_qty() + if self.product_id.cost_method == 'standard' and self.purchase_line_id: + components_cost = 0 + subcontract_production = self.purchase_line_id.move_ids._get_subcontract_production() + 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 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 592107d31e4..a24d7008f79 100644 --- a/addons/mrp_subcontracting_purchase/tests/test_mrp_subcontracting_purchase.py +++ b/addons/mrp_subcontracting_purchase/tests/test_mrp_subcontracting_purchase.py @@ -3,6 +3,7 @@ from odoo import Command from odoo.exceptions import UserError +from odoo.fields import Date from odoo.tests import Form from odoo.addons.mrp_subcontracting.tests.common import TestMrpSubcontractingCommon @@ -234,3 +235,55 @@ class MrpSubcontractingPurchaseTest(TestMrpSubcontractingCommon): self.env['stock.warehouse.orderpoint']._get_orderpoint_action() self.assertTrue(self.env['stock.warehouse.orderpoint'].search([('product_id', '=', component.id)])) + + def test_subcontracting_resupply_price_diff(self): + """Test that the price difference is correctly computed when a subcontracted + product is resupplied. + """ + resupply_sub_on_order_route = self.env['stock.route'].search([('name', '=', 'Resupply Subcontractor on Order')]) + (self.comp1 + self.comp2).write({'route_ids': [(6, None, [resupply_sub_on_order_route.id])]}) + product_category_all = self.env.ref('product.product_category_all') + product_category_all.property_cost_method = 'standard' + product_category_all.property_valuation = 'real_time' + + stock_price_diff_acc_id = self.env['account.account'].create({ + 'name': 'default_account_stock_price_diff', + 'code': 'STOCKDIFF', + 'reconcile': True, + 'account_type': 'asset_current', + 'company_id': self.env.company.id, + }) + product_category_all.property_account_creditor_price_difference_categ = stock_price_diff_acc_id + + self.comp1.standard_price = 10.0 + self.comp2.standard_price = 20.0 + self.finished.standard_price = 100 + + # Create a PO for 1 finished product. + po_form = Form(self.env['purchase.order']) + po_form.partner_id = self.subcontractor_partner1 + with po_form.order_line.new() as po_line: + po_line.product_id = self.finished + po_line.product_qty = 1 + po_line.price_unit = 50 # should be 70 + po = po_form.save() + po.button_confirm() + + action = po.action_view_subcontracting_resupply() + resupply_picking = self.env[action['res_model']].browse(action['res_id']) + resupply_picking.move_ids.quantity_done = 1 + resupply_picking.button_validate() + + action = po.action_view_picking() + final_picking = self.env[action['res_model']].browse(action['res_id']) + final_picking.move_ids.quantity_done = 1 + final_picking.button_validate() + + action = po.action_create_invoice() + invoice = self.env['account.move'].browse(action['res_id']) + invoice.invoice_date = Date.today() + invoice.action_post() + + # 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) diff --git a/addons/purchase_stock/models/account_invoice.py b/addons/purchase_stock/models/account_invoice.py index 8c2c635abde..60598a95b16 100644 --- a/addons/purchase_stock/models/account_invoice.py +++ b/addons/purchase_stock/models/account_invoice.py @@ -53,43 +53,8 @@ class AccountMove(models.Model): debit_pdiff_account = line.product_id.product_tmpl_id.get_product_accounts(fiscal_pos=move.fiscal_position_id)['expense'] if not debit_pdiff_account: continue - # Retrieve stock valuation moves. - valuation_stock_moves = self.env['stock.move'].search([ - ('purchase_line_id', '=', line.purchase_line_id.id), - ('state', '=', 'done'), - ('product_qty', '!=', 0.0), - ]) if line.purchase_line_id else self.env['stock.move'] - if line.product_id.cost_method != 'standard' and line.purchase_line_id: - if move.move_type == 'in_refund': - valuation_stock_moves = valuation_stock_moves.filtered(lambda stock_move: stock_move._is_out()) - else: - valuation_stock_moves = valuation_stock_moves.filtered(lambda stock_move: stock_move._is_in()) - - if not valuation_stock_moves: - continue - - valuation_price_unit_total, valuation_total_qty = valuation_stock_moves._get_valuation_price_and_qty(line, move.currency_id) - valuation_price_unit = valuation_price_unit_total / valuation_total_qty - valuation_price_unit = line.product_id.uom_id._compute_price(valuation_price_unit, line.product_uom_id) - else: - # Valuation_price unit is always expressed in invoice currency, so that it can always be computed with the good rate - price_unit = line.product_id.uom_id._compute_price(line.product_id.standard_price, line.product_uom_id) - price_unit = -price_unit if line.move_id.move_type == 'in_refund' else price_unit - valuation_date = valuation_stock_moves and max(valuation_stock_moves.mapped('date')) or move.date - valuation_price_unit = line.company_currency_id._convert( - price_unit, move.currency_id, - move.company_id, valuation_date, round=False - ) - - price_unit = line._get_gross_unit_price() - - price_unit_val_dif = price_unit - valuation_price_unit - # If there are some valued moves, we only consider their quantity already used - if line.product_id.cost_method == 'standard': - relevant_qty = line.quantity - else: - relevant_qty = line._get_out_and_not_invoiced_qty(valuation_stock_moves) + price_unit_val_dif, relevant_qty = line._get_price_unit_val_dif_and_relevant_qty() price_subtotal = relevant_qty * price_unit_val_dif # We consider there is a price difference if the subtotal is not zero. In case a diff --git a/addons/purchase_stock/models/account_move_line.py b/addons/purchase_stock/models/account_move_line.py index 6a9c702a616..708aa77939d 100644 --- a/addons/purchase_stock/models/account_move_line.py +++ b/addons/purchase_stock/models/account_move_line.py @@ -278,3 +278,45 @@ class AccountMoveLine(models.Model): 'stock_valuation_layer_id': corrected_layer.id, 'price_diff_value': self.currency_id.round(pdiff * quantity), } + + def _get_price_unit_val_dif_and_relevant_qty(self): + self.ensure_one() + # Retrieve stock valuation moves. + valuation_stock_moves = self.env['stock.move'].search([ + ('purchase_line_id', '=', self.purchase_line_id.id), + ('state', '=', 'done'), + ('product_qty', '!=', 0.0), + ]) if self.purchase_line_id else self.env['stock.move'] + + if self.product_id.cost_method != 'standard' and self.purchase_line_id: + if self.move_type == 'in_refund': + valuation_stock_moves = valuation_stock_moves.filtered(lambda stock_move: stock_move._is_out()) + else: + valuation_stock_moves = valuation_stock_moves.filtered(lambda stock_move: stock_move._is_in()) + + if not valuation_stock_moves: + return 0, 0 + + valuation_price_unit_total, valuation_total_qty = valuation_stock_moves._get_valuation_price_and_qty(self, self.move_id.currency_id) + valuation_price_unit = valuation_price_unit_total / valuation_total_qty + valuation_price_unit = self.product_id.uom_id._compute_price(valuation_price_unit, self.product_uom_id) + else: + # Valuation_price unit is always expressed in invoice currency, so that it can always be computed with the good rate + price_unit = self.product_id.uom_id._compute_price(self.product_id.standard_price, self.product_uom_id) + price_unit = -price_unit if self.move_id.move_type == 'in_refund' else price_unit + valuation_date = valuation_stock_moves and max(valuation_stock_moves.mapped('date')) or self.date + valuation_price_unit = self.company_currency_id._convert( + price_unit, self.currency_id, + self.company_id, valuation_date, round=False + ) + + price_unit = self._get_gross_unit_price() + + price_unit_val_dif = price_unit - valuation_price_unit + # If there are some valued moves, we only consider their quantity already used + if self.product_id.cost_method == 'standard': + relevant_qty = self.quantity + else: + relevant_qty = self._get_out_and_not_invoiced_qty(valuation_stock_moves) + + return price_unit_val_dif, relevant_qty