[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) <whe@odoo.com>
Signed-off-by: Yuchen Huang (yhu) <yhu@odoo.com>
This commit is contained in:
yhu-odoo
2023-08-19 11:01:00 +02:00
parent 77a8616263
commit 6624cbf75b
5 changed files with 113 additions and 36 deletions
@@ -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
@@ -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
@@ -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)
@@ -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
@@ -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