diff --git a/addons/sale_mrp/models/account_move.py b/addons/sale_mrp/models/account_move.py index c3f2a9ae229..1b89099e557 100644 --- a/addons/sale_mrp/models/account_move.py +++ b/addons/sale_mrp/models/account_move.py @@ -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]) diff --git a/addons/sale_mrp/tests/test_sale_mrp_anglo_saxon_valuation.py b/addons/sale_mrp/tests/test_sale_mrp_anglo_saxon_valuation.py index f33559991d1..a2a9838d085 100644 --- a/addons/sale_mrp/tests/test_sale_mrp_anglo_saxon_valuation.py +++ b/addons/sale_mrp/tests/test_sale_mrp_anglo_saxon_valuation.py @@ -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' diff --git a/addons/sale_stock/models/account_move.py b/addons/sale_stock/models/account_move.py index 42267af8a96..d42de9ff83b 100644 --- a/addons/sale_stock/models/account_move.py +++ b/addons/sale_stock/models/account_move.py @@ -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) diff --git a/addons/sale_stock/tests/test_anglo_saxon_valuation.py b/addons/sale_stock/tests/test_anglo_saxon_valuation.py index e7767ecc0d1..0fdde851d7d 100644 --- a/addons/sale_stock/tests/test_anglo_saxon_valuation.py +++ b/addons/sale_stock/tests/test_anglo_saxon_valuation.py @@ -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'