[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:
@@ -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'
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user