[FIX] sale_mrp, sale_stock: compute is_reverse based on move type

Very similar to the usecase in commit dafe732e6874fbdd15dfd213b5c5c0cde93e534e

Steps to reproduce:

- Install Accounting, Sales, Stock
- Create a product category PC:
	- Costing method: FIFO
	- Inventory Valuation: Automated
- Create a product P:
    - Type: Storable
    - Product Category: PC
- Create a Sales Order SO with 1 x P at cost 10
- Confirm delivery
- SO > Create Invoice > Post
- SO > Return the delivery
- Change product price from 10 to 6
- SO > Create Invoice (!!)
- The invoice will be marked as a credit note automatically
- Post the invoice

Issue:

The product is listed in the journal items, which is correct,
but its value is 6 while it should be 10.

If we create the invoice from the Sales Order instead of adding a
credit note, then the move lines will not be considered reversed,
since they do not have an associated invoice.

Solution:

Consider the move type when deciding if the move line is reversed or not.

opw-3077772

closes odoo/odoo#112673

X-original-commit: c1e8e4fbbe369615733298a1dfe18ead566eb310
Signed-off-by: Stefan-Calin Crainiciuc (stcc) <stcc@odoo.com>
Signed-off-by: Adrien Widart <awt@odoo.com>
This commit is contained in:
Stefan-Calin Crainiciuc (stcc)
2023-02-15 09:11:27 +01:00
parent 3897dfcf0f
commit 77fcc10bb1
4 changed files with 198 additions and 2 deletions
+1 -1
View File
@@ -13,7 +13,7 @@ class AccountMoveLine(models.Model):
if so_line:
bom = self.env['mrp.bom']._bom_find(products=so_line.product_id, company_id=so_line.company_id.id, bom_type='phantom')[so_line.product_id]
if bom:
is_line_reversing = bool(self.move_id.reversed_entry_id)
is_line_reversing = self.move_id.move_type == 'out_refund'
qty_to_invoice = self.product_uom_id._compute_quantity(self.quantity, self.product_id.uom_id)
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)
qty_invoiced = sum([x.product_uom_id._compute_quantity(x.quantity, x.product_id.uom_id) for x in posted_invoice_lines])
@@ -320,6 +320,110 @@ class TestSaleMRPAngloSaxonValuation(ValuationReconciliationTestCommon):
self.assertEqual(cogs_aml.debit, 0)
self.assertEqual(cogs_aml.credit, 20, 'Should be to the value of the returned component')
def test_anglo_saxo_return_and_create_invoice(self):
"""
When creating an invoice for a returned kit, the value of the anglo-saxo lines
should be based on the returned component's value
"""
self.stock_account_product_categ.property_cost_method = 'fifo'
kit = self._create_product('Simple Kit', 'product', 0)
component = self._create_product('Compo A', 'product', 0)
(kit + component).invoice_policy = 'delivery'
kit.property_account_expense_id = self.company_data['default_account_expense']
self.env['mrp.bom'].create({
'product_tmpl_id': kit.product_tmpl_id.id,
'product_qty': 1.0,
'type': 'phantom',
'bom_line_ids': [(0, 0, {'product_id': component.id, 'product_qty': 1.0})]
})
# Receive 3 components: one @10, one @20 and one @60
in_moves = self.env['stock.move'].create([{
'name': 'IN move @%s' % p,
'product_id': component.id,
'location_id': self.env.ref('stock.stock_location_suppliers').id,
'location_dest_id': self.company_data['default_warehouse'].lot_stock_id.id,
'product_uom': component.uom_id.id,
'product_uom_qty': 1,
'price_unit': p,
} for p in [10, 20, 60]])
in_moves._action_confirm()
in_moves.quantity_done = 1
in_moves._action_done()
# Sell 3 kits
so = self.env['sale.order'].create({
'partner_id': self.env.ref('base.res_partner_1').id,
'order_line': [
(0, 0, {
'name': kit.name,
'product_id': kit.id,
'product_uom_qty': 3.0,
'product_uom': kit.uom_id.id,
'price_unit': 100,
'tax_id': False,
})],
})
so.action_confirm()
# Deliver the components: 1@10, then 1@20 and then 1@60
pickings = []
picking = so.picking_ids
while picking:
pickings.append(picking)
picking.move_ids.quantity_done = 1
action = picking.button_validate()
if isinstance(action, dict):
wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
wizard.process()
picking = picking.backorder_ids
invoice = so._create_invoices()
invoice.action_post()
# Receive one @100
in_moves = self.env['stock.move'].create({
'name': 'IN move @100',
'product_id': component.id,
'location_id': self.env.ref('stock.stock_location_suppliers').id,
'location_dest_id': self.company_data['default_warehouse'].lot_stock_id.id,
'product_uom': component.uom_id.id,
'product_uom_qty': 1,
'price_unit': 100,
})
in_moves._action_confirm()
in_moves.quantity_done = 1
in_moves._action_done()
# Return the second picking (i.e. one component @20)
ctx = {'active_id': pickings[1].id, 'active_model': 'stock.picking'}
return_wizard = Form(self.env['stock.return.picking'].with_context(ctx)).save()
return_picking_id, dummy = return_wizard._create_returns()
return_picking = self.env['stock.picking'].browse(return_picking_id)
return_picking.move_ids.quantity_done = 1
return_picking.button_validate()
# Create a new invoice for the returned kit
ctx = {'active_model': 'sale.order', 'active_ids': so.ids}
create_invoice_wizard = self.env['sale.advance.payment.inv'].with_context(ctx).create(
{'advance_payment_method': 'delivered'})
create_invoice_wizard.create_invoices()
reverse_invoice = so.invoice_ids[-1]
with Form(reverse_invoice) as reverse_invoice_form:
with reverse_invoice_form.invoice_line_ids.edit(0) as line:
line.quantity = 1
reverse_invoice.action_post()
amls = reverse_invoice.line_ids
stock_out_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_stock_out'])
self.assertEqual(stock_out_aml.debit, 20, 'Should be to the value of the returned component')
self.assertEqual(stock_out_aml.credit, 0)
cogs_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_expense'])
self.assertEqual(cogs_aml.debit, 0)
self.assertEqual(cogs_aml.credit, 20, 'Should be to the value of the returned component')
def test_kit_avco_fully_owned_and_delivered_invoice_post_delivery(self):
self.stock_account_product_categ.property_cost_method = 'average'
+1 -1
View File
@@ -121,7 +121,7 @@ class AccountMoveLine(models.Model):
so_line = self.sale_line_ids and self.sale_line_ids[-1] or False
if so_line:
is_line_reversing = bool(self.move_id.reversed_entry_id)
is_line_reversing = self.move_id.move_type == 'out_refund'
qty_to_invoice = self.product_uom_id._compute_quantity(self.quantity, self.product_id.uom_id)
account_moves = so_line.invoice_lines.move_id.filtered(lambda m: m.state == 'posted' and bool(m.reversed_entry_id) == is_line_reversing)
posted_cogs = account_moves.line_ids.filtered(lambda l: l.display_type == 'cogs' and l.product_id == self.product_id and l.balance > 0)
@@ -1459,6 +1459,98 @@ class TestAngloSaxonValuation(ValuationReconciliationTestCommon):
self.assertEqual(cogs_aml.debit, 0)
self.assertEqual(cogs_aml.credit, 20, 'Should be to the value of the returned product')
def test_fifo_return_and_create_invoice(self):
"""
When creating an invoice for a returned product, the value of the anglo-saxo lines
should be based on the returned product's value
"""
self.product.categ_id.property_cost_method = 'fifo'
self.product.invoice_policy = 'delivery'
# Receive one @10, one @20 and one @60
in_moves = self.env['stock.move'].create([{
'name': 'IN move @%s' % p,
'product_id': self.product.id,
'location_id': self.env.ref('stock.stock_location_suppliers').id,
'location_dest_id': self.company_data['default_warehouse'].lot_stock_id.id,
'product_uom': self.product.uom_id.id,
'product_uom_qty': 1,
'price_unit': p,
} for p in [10, 20, 60]])
in_moves._action_confirm()
in_moves.quantity_done = 1
in_moves._action_done()
# Sell 3 units
so = self.env['sale.order'].create({
'partner_id': self.partner_a.id,
'order_line': [
(0, 0, {
'name': self.product.name,
'product_id': self.product.id,
'product_uom_qty': 3.0,
'product_uom': self.product.uom_id.id,
'price_unit': 100,
'tax_id': False,
})],
})
so.action_confirm()
# Deliver 1@10, then 1@20 and then 1@60
pickings = []
picking = so.picking_ids
while picking:
pickings.append(picking)
picking.move_ids.quantity_done = 1
action = picking.button_validate()
if isinstance(action, dict):
wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
wizard.process()
picking = picking.backorder_ids
invoice = so._create_invoices()
invoice.action_post()
# Receive one @100
in_moves = self.env['stock.move'].create({
'name': 'IN move @100',
'product_id': self.product.id,
'location_id': self.env.ref('stock.stock_location_suppliers').id,
'location_dest_id': self.company_data['default_warehouse'].lot_stock_id.id,
'product_uom': self.product.uom_id.id,
'product_uom_qty': 1,
'price_unit': 100,
})
in_moves._action_confirm()
in_moves.quantity_done = 1
in_moves._action_done()
# Return the second picking (i.e. 1@20)
ctx = {'active_id': pickings[1].id, 'active_model': 'stock.picking'}
return_wizard = Form(self.env['stock.return.picking'].with_context(ctx)).save()
return_picking_id, dummy = return_wizard._create_returns()
return_picking = self.env['stock.picking'].browse(return_picking_id)
return_picking.move_ids.quantity_done = 1
return_picking.button_validate()
# Create a new invoice for the returned product
ctx = {'active_model': 'sale.order', 'active_ids': so.ids}
create_invoice_wizard = self.env['sale.advance.payment.inv'].with_context(ctx).create({'advance_payment_method': 'delivered'})
create_invoice_wizard.create_invoices()
reverse_invoice = so.invoice_ids[-1]
with Form(reverse_invoice) as reverse_invoice_form:
with reverse_invoice_form.invoice_line_ids.edit(0) as line:
line.quantity = 1
reverse_invoice.action_post()
amls = reverse_invoice.line_ids
stock_out_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_stock_out'])
self.assertEqual(stock_out_aml.debit, 20, 'Should be to the value of the returned product')
self.assertEqual(stock_out_aml.credit, 0)
cogs_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_expense'])
self.assertEqual(cogs_aml.debit, 0)
self.assertEqual(cogs_aml.credit, 20, 'Should be to the value of the returned product')
def test_fifo_several_invoices_reset_repost(self):
self.product.categ_id.property_cost_method = 'fifo'
self.product.invoice_policy = 'delivery'