Skip to content

Commit e46d685

Browse files
committed
[FIX] sale_{stock,mrp}: repost inv with cogs
To reproduce the issue: (Also need: account_accountant,purchase) 1. Setup a product P - Storable - Category: - FIFO + Auto 2. Process a PO with 1 x P at $10 3. Process a PO with 1 x P at $50 4. Create and confirm a SO with 1 x P - Because of FIFO, the value of the delivered product is $10 5. Process the delivery 6. Post the invoice 7. Add credit note - Credit Method: Full refund and new draft invoice 8. Post the second invoice Error: The cogs are based on the second received product ($50), they should rather be based on the delivered one When computing the anglo-saxon unit price of the product, we consume the outgoing SVLs, and we consider the already-invoiced quantity. Here is the issue: we pretend that this quantity is 1, because of the first invoice, but this should be balanced with the quantity of the credit note OPW-3109789 closes odoo#114455 X-original-commit: cee2ae2 Signed-off-by: William Henrotin (whe) <whe@odoo.com> Signed-off-by: Adrien Widart <awt@odoo.com>
1 parent 2631c8a commit e46d685

File tree

4 files changed

+146
-1
lines changed

4 files changed

+146
-1
lines changed

addons/sale_mrp/models/account_move.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,12 @@ def _stock_account_get_anglo_saxon_price_unit(self):
1515
if bom:
1616
is_line_reversing = self.move_id.move_type == 'out_refund'
1717
qty_to_invoice = self.product_uom_id._compute_quantity(self.quantity, self.product_id.uom_id)
18-
posted_invoice_lines = so_line.invoice_lines.filtered(lambda l: l.move_id.state == 'posted' and bool(l.move_id.reversed_entry_id) == is_line_reversing)
18+
account_moves = so_line.invoice_lines.move_id.filtered(lambda m: m.state == 'posted' and bool(m.reversed_entry_id) == is_line_reversing)
19+
posted_invoice_lines = account_moves.line_ids.filtered(lambda l: l.display_type == 'cogs' and l.product_id == self.product_id and l.balance > 0)
1920
qty_invoiced = sum([x.product_uom_id._compute_quantity(x.quantity, x.product_id.uom_id) for x in posted_invoice_lines])
21+
reversal_cogs = posted_invoice_lines.move_id.reversal_move_id.line_ids.filtered(lambda l: l.display_type == 'cogs' and l.product_id == self.product_id and l.balance > 0)
22+
qty_invoiced -= sum([line.product_uom_id._compute_quantity(line.quantity, line.product_id.uom_id) for line in reversal_cogs])
23+
2024
moves = so_line.move_ids
2125
average_price_unit = 0
2226
components_qty = so_line._get_bom_component_qty(bom)

addons/sale_mrp/tests/test_sale_mrp_flow.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2163,3 +2163,78 @@ def test_kit_return_and_decrease_sol_qty_to_zero(self):
21632163
line.product_uom_qty = 0
21642164

21652165
self.assertEqual(so.picking_ids, delivery | return_picking)
2166+
2167+
def test_fifo_reverse_and_create_new_invoice(self):
2168+
"""
2169+
FIFO automated
2170+
Kit with one component
2171+
Receive the component: 1@10, 1@50
2172+
Deliver 1 kit
2173+
Post the invoice, add a credit note with option 'new draft inv'
2174+
Post the second invoice
2175+
COGS should be based on the delivered kit
2176+
"""
2177+
kit = self._create_product('Simple Kit', self.uom_unit)
2178+
categ_form = Form(self.env['product.category'])
2179+
categ_form.name = 'Super Fifo'
2180+
categ_form.property_cost_method = 'fifo'
2181+
categ_form.property_valuation = 'real_time'
2182+
categ = categ_form.save()
2183+
(kit + self.component_a).categ_id = categ
2184+
2185+
self.env['mrp.bom'].create({
2186+
'product_tmpl_id': kit.product_tmpl_id.id,
2187+
'product_qty': 1.0,
2188+
'type': 'phantom',
2189+
'bom_line_ids': [(0, 0, {'product_id': self.component_a.id, 'product_qty': 1.0})]
2190+
})
2191+
2192+
in_moves = self.env['stock.move'].create([{
2193+
'name': 'IN move @%s' % p,
2194+
'product_id': self.component_a.id,
2195+
'location_id': self.env.ref('stock.stock_location_suppliers').id,
2196+
'location_dest_id': self.company_data['default_warehouse'].lot_stock_id.id,
2197+
'product_uom': self.component_a.uom_id.id,
2198+
'product_uom_qty': 1,
2199+
'price_unit': p,
2200+
} for p in [10, 50]])
2201+
in_moves._action_confirm()
2202+
in_moves.quantity_done = 1
2203+
in_moves._action_done()
2204+
2205+
so = self.env['sale.order'].create({
2206+
'partner_id': self.env.ref('base.res_partner_1').id,
2207+
'order_line': [
2208+
(0, 0, {
2209+
'name': kit.name,
2210+
'product_id': kit.id,
2211+
'product_uom_qty': 1.0,
2212+
'product_uom': kit.uom_id.id,
2213+
'price_unit': 100,
2214+
'tax_id': False,
2215+
})],
2216+
})
2217+
so.action_confirm()
2218+
2219+
picking = so.picking_ids
2220+
picking.move_ids.quantity_done = 1.0
2221+
picking.button_validate()
2222+
2223+
invoice01 = so._create_invoices()
2224+
invoice01.action_post()
2225+
2226+
move_reversal = self.env['account.move.reversal'].with_context(active_model="account.move", active_ids=invoice01.ids).create({
2227+
'refund_method': 'modify',
2228+
'journal_id': invoice01.journal_id.id,
2229+
})
2230+
reversal = move_reversal.reverse_moves()
2231+
invoice02 = self.env['account.move'].browse(reversal['res_id'])
2232+
invoice02.action_post()
2233+
2234+
amls = invoice02.line_ids
2235+
stock_out_aml = amls.filtered(lambda aml: aml.account_id == categ.property_stock_account_output_categ_id)
2236+
self.assertEqual(stock_out_aml.debit, 0)
2237+
self.assertEqual(stock_out_aml.credit, 10)
2238+
cogs_aml = amls.filtered(lambda aml: aml.account_id == categ.property_account_expense_categ_id)
2239+
self.assertEqual(cogs_aml.debit, 10)
2240+
self.assertEqual(cogs_aml.credit, 0)

addons/sale_stock/models/account_move.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,10 +124,15 @@ def _stock_account_get_anglo_saxon_price_unit(self):
124124
is_line_reversing = self.move_id.move_type == 'out_refund'
125125
qty_to_invoice = self.product_uom_id._compute_quantity(self.quantity, self.product_id.uom_id)
126126
account_moves = so_line.invoice_lines.move_id.filtered(lambda m: m.state == 'posted' and bool(m.reversed_entry_id) == is_line_reversing)
127+
127128
posted_cogs = account_moves.line_ids.filtered(lambda l: l.display_type == 'cogs' and l.product_id == self.product_id and l.balance > 0)
128129
qty_invoiced = sum([line.product_uom_id._compute_quantity(line.quantity, line.product_id.uom_id) for line in posted_cogs])
129130
value_invoiced = sum(posted_cogs.mapped('balance'))
130131

132+
reversal_cogs = posted_cogs.move_id.reversal_move_id.line_ids.filtered(lambda l: l.display_type == 'cogs' and l.product_id == self.product_id and l.balance > 0)
133+
qty_invoiced -= sum([line.product_uom_id._compute_quantity(line.quantity, line.product_id.uom_id) for line in reversal_cogs])
134+
value_invoiced -= sum(reversal_cogs.mapped('balance'))
135+
131136
product = self.product_id.with_company(self.company_id).with_context(value_invoiced=value_invoiced)
132137
average_price_unit = product._compute_average_price(qty_invoiced, qty_to_invoice, so_line.move_ids, is_returned=is_line_reversing)
133138
price_unit = self.product_id.uom_id.with_company(self.company_id)._compute_price(average_price_unit, self.product_uom_id)

addons/sale_stock/tests/test_anglo_saxon_valuation.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1629,3 +1629,64 @@ def test_fifo_several_invoices_reset_repost(self):
16291629
(invoice01 | invoice03).action_post()
16301630
cogs = invoices.line_ids.filtered(lambda l: l.account_id == out_account)
16311631
self.assertEqual(sum(cogs.mapped('credit')), total_value)
1632+
1633+
def test_fifo_reverse_and_create_new_invoice(self):
1634+
"""
1635+
FIFO automated
1636+
Receive 1@10, 1@50
1637+
Deliver 1
1638+
Post the invoice, add a credit note with option 'new draft inv'
1639+
Post the second invoice
1640+
COGS should be based on the delivered product
1641+
"""
1642+
self.product.categ_id.property_cost_method = 'fifo'
1643+
1644+
in_moves = self.env['stock.move'].create([{
1645+
'name': 'IN move @%s' % p,
1646+
'product_id': self.product.id,
1647+
'location_id': self.env.ref('stock.stock_location_suppliers').id,
1648+
'location_dest_id': self.company_data['default_warehouse'].lot_stock_id.id,
1649+
'product_uom': self.product.uom_id.id,
1650+
'product_uom_qty': 1,
1651+
'price_unit': p,
1652+
} for p in [10, 50]])
1653+
in_moves._action_confirm()
1654+
in_moves.quantity_done = 1
1655+
in_moves._action_done()
1656+
1657+
so = self.env['sale.order'].create({
1658+
'partner_id': self.partner_a.id,
1659+
'order_line': [
1660+
(0, 0, {
1661+
'name': self.product.name,
1662+
'product_id': self.product.id,
1663+
'product_uom_qty': 1.0,
1664+
'product_uom': self.product.uom_id.id,
1665+
'price_unit': 100,
1666+
'tax_id': False,
1667+
})],
1668+
})
1669+
so.action_confirm()
1670+
1671+
picking = so.picking_ids
1672+
picking.move_ids.quantity_done = 1.0
1673+
picking.button_validate()
1674+
1675+
invoice01 = so._create_invoices()
1676+
invoice01.action_post()
1677+
1678+
move_reversal = self.env['account.move.reversal'].with_context(active_model="account.move", active_ids=invoice01.ids).create({
1679+
'refund_method': 'modify',
1680+
'journal_id': invoice01.journal_id.id,
1681+
})
1682+
reversal = move_reversal.reverse_moves()
1683+
invoice02 = self.env['account.move'].browse(reversal['res_id'])
1684+
invoice02.action_post()
1685+
1686+
amls = invoice02.line_ids
1687+
stock_out_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_stock_out'])
1688+
self.assertEqual(stock_out_aml.debit, 0)
1689+
self.assertEqual(stock_out_aml.credit, 10)
1690+
cogs_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_expense'])
1691+
self.assertEqual(cogs_aml.debit, 10)
1692+
self.assertEqual(cogs_aml.credit, 0)

0 commit comments

Comments
 (0)