diff --git a/addons/sale_stock/models/account_move.py b/addons/sale_stock/models/account_move.py index b9c12d93f20..7ce6e3994d3 100644 --- a/addons/sale_stock/models/account_move.py +++ b/addons/sale_stock/models/account_move.py @@ -15,12 +15,17 @@ class AccountMove(models.Model): """ Overridden from stock_account. Returns the stock moves associated to this invoice.""" rslt = super(AccountMove, self)._stock_account_get_last_step_stock_moves() - for invoice in self.filtered(lambda x: x.move_type == 'out_invoice'): - rslt += invoice.mapped('invoice_line_ids.sale_line_ids.move_ids').filtered(lambda x: x.state == 'done' and x.location_dest_id.usage == 'customer') - for invoice in self.filtered(lambda x: x.move_type == 'out_refund'): - rslt += invoice.mapped('reversed_entry_id.invoice_line_ids.sale_line_ids.move_ids').filtered(lambda x: x.state == 'done' and x.location_id.usage == 'customer') - # Add refunds generated from the SO - rslt += invoice.mapped('invoice_line_ids.sale_line_ids.move_ids').filtered(lambda x: x.state == 'done' and x.location_id.usage == 'customer') + for invoice in self: + if invoice.move_type not in ['out_invoice', 'out_refund']: + continue + if (invoice.move_type == 'out_invoice' or ( + invoice.move_type == 'out_refund' and any(invoice.invoice_line_ids.sale_line_ids.mapped('is_downpayment'))) + ): + rslt += invoice.mapped('invoice_line_ids.sale_line_ids.move_ids').filtered(lambda x: x.state == 'done' and x.location_dest_id.usage == 'customer') + else: + rslt += invoice.mapped('reversed_entry_id.invoice_line_ids.sale_line_ids.move_ids').filtered(lambda x: x.state == 'done' and x.location_id.usage == 'customer') + # Add refunds generated from the SO + rslt += invoice.mapped('invoice_line_ids.sale_line_ids.move_ids').filtered(lambda x: x.state == 'done' and x.location_id.usage == 'customer') return rslt def _get_invoiced_lot_values(self): @@ -140,17 +145,32 @@ class AccountMoveLine(models.Model): price_unit = super(AccountMoveLine, self)._stock_account_get_anglo_saxon_price_unit() so_line = self.sale_line_ids and self.sale_line_ids[-1] or False + down_payment = self.move_id.invoice_line_ids.filtered(lambda line: line.sale_line_ids.is_downpayment) if so_line: - is_line_reversing = self.move_id.move_type == 'out_refund' + is_line_reversing = False + if self.move_id.move_type == 'out_refund' and not down_payment: + is_line_reversing = True qty_to_invoice = self.product_uom_id._compute_quantity(self.quantity, self.product_id.uom_id) + if self.move_id.move_type == 'out_refund' and down_payment: + qty_to_invoice = -qty_to_invoice 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) - qty_invoiced = sum([line.product_uom_id._compute_quantity(line.quantity, line.product_id.uom_id) for line in posted_cogs]) + qty_invoiced = 0 + product_uom = self.product_id.uom_id + for line in posted_cogs: + 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')): + qty_invoiced += line.product_uom_id._compute_quantity(abs(line.quantity), line.product_id.uom_id) + else: + qty_invoiced += line.product_uom_id._compute_quantity(line.quantity, line.product_id.uom_id) value_invoiced = sum(posted_cogs.mapped('balance')) 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) - qty_invoiced -= sum([line.product_uom_id._compute_quantity(line.quantity, line.product_id.uom_id) for line in reversal_cogs]) + for line in reversal_cogs: + 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')): + qty_invoiced -= line.product_uom_id._compute_quantity(abs(line.quantity), line.product_id.uom_id) + else: + qty_invoiced -= line.product_uom_id._compute_quantity(line.quantity, line.product_id.uom_id) value_invoiced -= sum(reversal_cogs.mapped('balance')) product = self.product_id.with_company(self.company_id).with_context(value_invoiced=value_invoiced) diff --git a/addons/sale_stock/tests/test_anglo_saxon_valuation.py b/addons/sale_stock/tests/test_anglo_saxon_valuation.py index 7e34b85954e..32a74733157 100644 --- a/addons/sale_stock/tests/test_anglo_saxon_valuation.py +++ b/addons/sale_stock/tests/test_anglo_saxon_valuation.py @@ -1727,3 +1727,83 @@ class TestAngloSaxonValuation(ValuationReconciliationTestCommon): # Check no reinvoice line addded to the sale order self.assertEqual(len(sale_order.order_line), 1) self.assertEqual(sale_order.order_line.product_uom_qty, 2.0) + + def test_anglo_saxon_cogs_with_down_payment(self): + """Create a SO with a product invoiced on delivered quantity. + Do a 100% down payment, deliver a part of it with a backorder + then invoice the delivered part from the down payment. + Deliver the remaining part and invoice it.""" + self.product.invoice_policy = 'delivery' + self.product.standard_price = 10 + self.env['stock.quant'].with_context(inventory_mode=True).create({ + 'product_id': self.product.id, # tracking serial + 'inventory_quantity': 20, + 'location_id': self.company_data['default_warehouse'].lot_stock_id.id, + }).action_apply_inventory() + + # Create a SO with a product invoiced on delivered quantity + 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': 10.0, + 'product_uom': self.product.uom_id.id, + 'price_unit': 100, + 'tax_id': False, + })], + }) + so.action_confirm() + + # Do a 100% down payment + down_payment = self.env['sale.advance.payment.inv'].create({ + 'advance_payment_method': 'percentage', + 'amount': 100, + 'sale_order_ids': so.ids, + }) + down_payment.create_invoices() + # Invoice the delivered part from the down payment + down_payment_invoices = so.invoice_ids + down_payment_invoices.action_post() + + # Deliver a part of it with a backorder + so.picking_ids.move_ids.quantity = 4 + so.picking_ids.move_ids.picked = True + backorder_wizard_dict = so.picking_ids.button_validate() + backorder_wizard = Form(self.env[backorder_wizard_dict['res_model']].with_context(backorder_wizard_dict['context'])).save() + backorder_wizard.process() + + invoice_wizard = self.env['sale.advance.payment.inv'].with_context(active_ids=so.ids, open_invoices=True).create({}) + invoice_wizard.create_invoices() + credit_note = so.invoice_ids.filtered(lambda i: i.state != 'posted') + self.assertEqual(len(credit_note), 1) + self.assertEqual(len(credit_note.invoice_line_ids.filtered(lambda line: line.display_type == 'product')), 2) + down_payment_line = credit_note.invoice_line_ids.filtered(lambda line: line.sale_line_ids.is_downpayment) + down_payment_line.quantity = 0.4 + credit_note.action_post() + # Deliver the remaining part and invoice itµ + backorder = so.picking_ids.filtered(lambda p: p.state != 'done') + backorder.move_ids.quantity = 6 + backorder.move_ids.picked = True + backorder.button_validate() + + invoice_wizard = self.env['sale.advance.payment.inv'].with_context(active_ids=so.ids, open_invoices=True).create({}) + invoice_wizard.create_invoices() + + invoice = so.invoice_ids.filtered(lambda i: i.state != 'posted') + invoice.action_post() + + # Check the resulting accounting entries + account_stock_out = self.company_data['default_account_stock_out'] + account_expense = self.company_data['default_account_expense'] + invoice_1_cogs = credit_note.line_ids.filtered(lambda l: l.display_type == 'cogs') + invoice_2_cogs = invoice.line_ids.filtered(lambda l: l.display_type == 'cogs') + self.assertRecordValues(invoice_1_cogs, [ + {'debit': 0, 'credit': 40, 'account_id': account_stock_out.id, 'reconciled': True}, + {'debit': 40, 'credit': 0, 'account_id': account_expense.id, 'reconciled': False}, + ]) + self.assertRecordValues(invoice_2_cogs, [ + {'debit': 0, 'credit': 60, 'account_id': account_stock_out.id, 'reconciled': True}, + {'debit': 60, 'credit': 0, 'account_id': account_expense.id, 'reconciled': False}, + ])