Skip to content

Commit 7ad850e

Browse files
committed
[FIX] repair,sale_stock,stock_account: repair with auto FIFO component
When using an auto fifo product as part of the repair, the COGS won't be based on the used product To reproduce the issue: 1. Setup auto fifo product 2. Receive 1@10 and 1@20 3. Process a RO: - Invoice method: After repair - Parts: - Add 1 x fifo product 4. Create and post the invoice 5. Open its journal items Error: Cogs are $20 instead of $10 When posting the invoice, we generate the COGS: https://github.com/odoo/odoo/blob/4df156164cf1d2764ba23682beee588777457fd6/addons/stock_account/models/account_move.py#L47-L48 We therefore compute the "anglo saxon unit price": https://github.com/odoo/odoo/blob/4df156164cf1d2764ba23682beee588777457fd6/addons/stock_account/models/account_move.py#L133 However, there isn't any override to handle the RO case, so it leads to the default mechanism, i.e. the standard price of the product: https://github.com/odoo/odoo/blob/4df156164cf1d2764ba23682beee588777457fd6/addons/stock_account/models/account_move.py#L294-L295 https://github.com/odoo/odoo/blob/4df156164cf1d2764ba23682beee588777457fd6/addons/stock_account/models/account_move.py#L294-L295 https://github.com/odoo/odoo/blob/7cd7563f6708331bb6baf0e06d07a9f9ee329e38/addons/stock_account/models/product.py#L753-L757 And, since the first product is out, its standard price is now based on the next candidate: $20 About the `sudo`: an accountman has not any access to `repair`, so posting such an invoice would raise an error. Since this diff is specific to Odoo 16, the idea is not to impact any security rules and rather minimize the changes. Note: Indeed, `repair` does not depend on `stock_account`, so this commit could lead to a traceback if the bridge is removed. I delegate this issue to the error of dependencies. Anyway, removing the bridge would lead to other bugs. Hopefully, this has been fixed on master [1]. [1] f7dbdec OPW-4166570 closes odoo#193076 Signed-off-by: William Henrotin (whe) <[email protected]>
1 parent 4bde914 commit 7ad850e

File tree

5 files changed

+148
-45
lines changed

5 files changed

+148
-45
lines changed

addons/repair/models/account_move.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,12 @@ class AccountMoveLine(models.Model):
2020

2121
repair_line_ids = fields.One2many('repair.line', 'invoice_line_id', readonly=True, copy=False)
2222
repair_fee_ids = fields.One2many('repair.fee', 'invoice_line_id', readonly=True, copy=False)
23+
24+
def _stock_account_get_anglo_saxon_price_unit(self):
25+
price_unit = super()._stock_account_get_anglo_saxon_price_unit()
26+
ro_line = self.sudo().repair_line_ids
27+
if ro_line:
28+
am = ro_line.invoice_line_id.move_id.sudo(False)
29+
sm = ro_line.move_id.sudo(False)
30+
price_unit = self._deduce_anglo_saxon_unit_price(am, sm)
31+
return price_unit

addons/repair/tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# -*- coding: utf-8 -*-
22
# Part of Odoo. See LICENSE file for full copyright and licensing details.
33

4+
from . import test_anglo_saxon_valuation
45
from . import test_repair
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# -*- coding: utf-8 -*-
2+
# Part of Odoo. See LICENSE file for full copyright and licensing details.
3+
4+
from odoo.tests import Form, tagged
5+
from odoo.addons.stock_account.tests.test_anglo_saxon_valuation_reconciliation_common import ValuationReconciliationTestCommon
6+
from odoo.exceptions import UserError
7+
8+
9+
@tagged('post_install', '-at_install')
10+
class TestAngloSaxonValuation(ValuationReconciliationTestCommon):
11+
12+
@classmethod
13+
def setUpClass(cls, chart_template_ref=None):
14+
super().setUpClass(chart_template_ref=chart_template_ref)
15+
16+
cls.env.user.company_id.anglo_saxon_accounting = True
17+
18+
cls.fifo_product = cls.env['product.product'].create({
19+
'name': 'product',
20+
'type': 'product',
21+
'categ_id': cls.stock_account_product_categ.id,
22+
})
23+
24+
cls.basic_accountman = cls.env['res.users'].create({
25+
'name': 'Basic Accountman',
26+
'login': 'basic_accountman',
27+
'password': 'basic_accountman',
28+
'groups_id': [(6, 0, cls.env.ref('account.group_account_invoice').ids)],
29+
})
30+
31+
def _make_in_move(self, product, quantity=1, unit_cost=None):
32+
unit_cost = unit_cost or product.standard_price
33+
move = self.env['stock.move'].create({
34+
'name': product.name,
35+
'product_id': product.id,
36+
'location_id': self.env.ref('stock.stock_location_suppliers').id,
37+
'location_dest_id': self.company_data['default_warehouse'].lot_stock_id.id,
38+
'product_uom': product.uom_id.id,
39+
'product_uom_qty': quantity,
40+
'price_unit': unit_cost,
41+
})
42+
move._action_confirm()
43+
move.quantity_done = quantity
44+
move._action_done()
45+
return move
46+
47+
def test_inv_ro_with_auto_fifo_part(self):
48+
self.fifo_product.standard_price = 100
49+
50+
self._make_in_move(self.fifo_product, unit_cost=10)
51+
self._make_in_move(self.fifo_product, unit_cost=25)
52+
53+
ro = self.env['repair.order'].create({
54+
'product_id': self.product_a.id,
55+
'partner_id': self.partner_a.id,
56+
'invoice_method': 'after_repair',
57+
'operations': [(0, 0, {
58+
'name': self.fifo_product.name,
59+
'type': 'add',
60+
'product_id': self.fifo_product.id,
61+
'price_unit': 1,
62+
})],
63+
})
64+
ro.action_repair_confirm()
65+
ro.action_repair_start()
66+
ro.action_repair_end()
67+
68+
wizard_ctx = {
69+
"active_model": 'repair_order',
70+
"active_ids": [ro.id],
71+
"active_id": ro.id
72+
}
73+
self.env['repair.order.make_invoice'].with_context(wizard_ctx).create({}).make_invoices()
74+
invoice = ro.invoice_id
75+
76+
self.env.invalidate_all()
77+
invoice.with_user(self.basic_accountman).action_post()
78+
79+
self.assertRecordValues(invoice.line_ids, [
80+
{'debit': 0, 'credit': 1, 'account_id': self.company_data['default_account_revenue'].id},
81+
{'debit': 1, 'credit': 0, 'account_id': self.company_data['default_account_receivable'].id},
82+
{'debit': 0, 'credit': 10, 'account_id': self.company_data['default_account_stock_out'].id},
83+
{'debit': 10, 'credit': 0, 'account_id': self.company_data['default_account_expense'].id},
84+
])

addons/sale_stock/models/account_move.py

Lines changed: 2 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -132,49 +132,7 @@ def _stock_account_get_anglo_saxon_price_unit(self):
132132
price_unit = super(AccountMoveLine, self)._stock_account_get_anglo_saxon_price_unit()
133133

134134
so_line = self.sale_line_ids and self.sale_line_ids[-1] or False
135-
move_is_downpayment = self.env.context.get("move_is_downpayment")
136-
if move_is_downpayment is None:
137-
move_is_downpayment = self.move_id.invoice_line_ids.filtered(
138-
lambda line: any(line.sale_line_ids.mapped("is_downpayment"))
139-
)
140135
if so_line:
141-
is_line_reversing = False
142-
if self.move_id.move_type == 'out_refund' and not move_is_downpayment:
143-
is_line_reversing = True
144-
qty_to_invoice = self.product_uom_id._compute_quantity(self.quantity, self.product_id.uom_id)
145-
if self.move_id.move_type == 'out_refund' and move_is_downpayment:
146-
qty_to_invoice = -qty_to_invoice
147-
account_moves = so_line.invoice_lines.move_id.filtered(lambda m: m.state == 'posted' and bool(m.reversed_entry_id) == is_line_reversing)
148-
149-
posted_cogs = self.env['account.move.line'].search([
150-
('move_id', 'in', account_moves.ids),
151-
('display_type', '=', 'cogs'),
152-
('product_id', '=', self.product_id.id),
153-
('balance', '>', 0),
154-
])
155-
qty_invoiced = 0
156-
product_uom = self.product_id.uom_id
157-
for line in posted_cogs:
158-
if float_compare(line.quantity, 0, precision_rounding=product_uom.rounding) and line.move_id.move_type == 'out_refund' and any(line.move_id.invoice_line_ids.sale_line_ids.mapped('is_downpayment')):
159-
qty_invoiced += line.product_uom_id._compute_quantity(abs(line.quantity), line.product_id.uom_id)
160-
else:
161-
qty_invoiced += line.product_uom_id._compute_quantity(line.quantity, line.product_id.uom_id)
162-
value_invoiced = sum(posted_cogs.mapped('balance'))
163-
reversal_moves = self.env['account.move']._search([('reversed_entry_id', 'in', posted_cogs.move_id.ids)])
164-
reversal_cogs = self.env['account.move.line'].search([
165-
('move_id', 'in', reversal_moves),
166-
('display_type', '=', 'cogs'),
167-
('product_id', '=', self.product_id.id),
168-
('balance', '>', 0)
169-
])
170-
for line in reversal_cogs:
171-
if float_compare(line.quantity, 0, precision_rounding=product_uom.rounding) and line.move_id.move_type == 'out_refund' and any(line.move_id.invoice_line_ids.sale_line_ids.mapped('is_downpayment')):
172-
qty_invoiced -= line.product_uom_id._compute_quantity(abs(line.quantity), line.product_id.uom_id)
173-
else:
174-
qty_invoiced -= line.product_uom_id._compute_quantity(line.quantity, line.product_id.uom_id)
175-
value_invoiced -= sum(reversal_cogs.mapped('balance'))
176-
177-
product = self.product_id.with_company(self.company_id).with_context(value_invoiced=value_invoiced)
178-
average_price_unit = product._compute_average_price(qty_invoiced, qty_to_invoice, so_line.move_ids, is_returned=is_line_reversing)
179-
price_unit = self.product_id.uom_id.with_company(self.company_id)._compute_price(average_price_unit, self.product_uom_id)
136+
price_unit = self._deduce_anglo_saxon_unit_price(so_line.invoice_lines.move_id, so_line.move_ids)
137+
180138
return price_unit

addons/stock_account/models/account_move.py

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# -*- coding: utf-8 -*-
22

33
from odoo import fields, models, api
4-
from odoo.tools import float_is_zero
4+
from odoo.tools import float_compare, float_is_zero
55

66

77
class AccountMove(models.Model):
@@ -297,3 +297,54 @@ def _stock_account_get_anglo_saxon_price_unit(self):
297297
@api.onchange('product_id')
298298
def _inverse_product_id(self):
299299
super(AccountMoveLine, self.filtered(lambda l: l.display_type != 'cogs'))._inverse_product_id()
300+
301+
def _deduce_anglo_saxon_unit_price(self, account_moves, stock_moves):
302+
self.ensure_one()
303+
304+
move_is_downpayment = self.env.context.get("move_is_downpayment")
305+
if move_is_downpayment is None:
306+
move_is_downpayment = self.move_id.invoice_line_ids.filtered(
307+
lambda line: any(line.sale_line_ids.mapped("is_downpayment"))
308+
)
309+
310+
is_line_reversing = False
311+
if self.move_id.move_type == 'out_refund' and not move_is_downpayment:
312+
is_line_reversing = True
313+
qty_to_invoice = self.product_uom_id._compute_quantity(self.quantity, self.product_id.uom_id)
314+
if self.move_id.move_type == 'out_refund' and move_is_downpayment:
315+
qty_to_invoice = -qty_to_invoice
316+
account_moves = account_moves.filtered(lambda m: m.state == 'posted' and bool(m.reversed_entry_id) == is_line_reversing)
317+
318+
posted_cogs = self.env['account.move.line'].search([
319+
('move_id', 'in', account_moves.ids),
320+
('display_type', '=', 'cogs'),
321+
('product_id', '=', self.product_id.id),
322+
('balance', '>', 0),
323+
])
324+
qty_invoiced = 0
325+
product_uom = self.product_id.uom_id
326+
for line in posted_cogs:
327+
if float_compare(line.quantity, 0, precision_rounding=product_uom.rounding) and line.move_id.move_type == 'out_refund' and any(line.move_id.invoice_line_ids.sale_line_ids.mapped('is_downpayment')):
328+
qty_invoiced += line.product_uom_id._compute_quantity(abs(line.quantity), line.product_id.uom_id)
329+
else:
330+
qty_invoiced += line.product_uom_id._compute_quantity(line.quantity, line.product_id.uom_id)
331+
value_invoiced = sum(posted_cogs.mapped('balance'))
332+
reversal_moves = self.env['account.move']._search([('reversed_entry_id', 'in', posted_cogs.move_id.ids)])
333+
reversal_cogs = self.env['account.move.line'].search([
334+
('move_id', 'in', reversal_moves),
335+
('display_type', '=', 'cogs'),
336+
('product_id', '=', self.product_id.id),
337+
('balance', '>', 0)
338+
])
339+
for line in reversal_cogs:
340+
if float_compare(line.quantity, 0, precision_rounding=product_uom.rounding) and line.move_id.move_type == 'out_refund' and any(line.move_id.invoice_line_ids.sale_line_ids.mapped('is_downpayment')):
341+
qty_invoiced -= line.product_uom_id._compute_quantity(abs(line.quantity), line.product_id.uom_id)
342+
else:
343+
qty_invoiced -= line.product_uom_id._compute_quantity(line.quantity, line.product_id.uom_id)
344+
value_invoiced -= sum(reversal_cogs.mapped('balance'))
345+
346+
product = self.product_id.with_company(self.company_id).with_context(value_invoiced=value_invoiced)
347+
average_price_unit = product._compute_average_price(qty_invoiced, qty_to_invoice, stock_moves, is_returned=is_line_reversing)
348+
price_unit = self.product_id.uom_id.with_company(self.company_id)._compute_price(average_price_unit, self.product_uom_id)
349+
350+
return price_unit

0 commit comments

Comments
 (0)