[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:
@@ -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},
|
||||
])
|
||||
|
||||
Reference in New Issue
Block a user