[FIX] sale_stock: partial downpayment doesn't create COGS

Usecase to reproduce:
- Product wiht a real time valuation
- Product with invoice on delivered quantity
- Create a SO for 5 units and 1000$ each
- Do a full downpayment of 100% of quotation
- Deliver 3 out of 5 units and create a backorder
- Create an invoice
- Validate the invoice

Expected behavior:
The cogs entries are there

Current behavior:
No cogs

It only happens with partial downpayment. When the downpayment amount
equals the quotation amount. An invoice is created instead of a credit
note and the process works correctly.

It happens because it creates a credit note with a negative quantity to
invoice so the system doesn't understand it has to create the cogs at
that point.

closes odoo/odoo#161768

X-original-commit: d2a365c2ee9af6a9272d83183fc75fa6914bc560
Signed-off-by: Adrien Widart (awt) <awt@odoo.com>
Signed-off-by: Arnold Moyaux (arm) <arm@odoo.com>
This commit is contained in:
Arnold Moyaux
2024-04-24 14:48:56 +00:00
parent 3aa2d05213
commit 100b252b7a
2 changed files with 109 additions and 9 deletions
+29 -9
View File
@@ -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)
@@ -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},
])