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