[IMP] sale: untaxed amount to invoice/invoiced on sale line
Similar fields were introduced at d2e33ba57c
but their computation were not totally exact, so someone removed them.
Also, for performance sake, they were replaced by a (too) simpler
calculation at 39d0613187d36f16c541734e80925e9d.
Turns out, those fields were usefull !
Neither of those 2 implementations were 100% correct, so this commit
reintroduced thoses 2 fields on sale.order.line object:
1/ untaxed_amount_invoiced: will contain the amount (money without
taxes) invoiced
from the sale line, taking refund linked to the same SO line into
account. The formula is
SUM(inv_line.price_subtotal) - SUM(ref_line.price_subtotal)
where
`inv_line` is a customer invoice line linked to the SO line
`ref_line` is a customer credit note (refund) line linked to the SO
line
2/ untaxed_amount_to_invoice: will contain the amount (taxes excluded)
of the money to invoice from this sale line. The formula is
SOL.total - SOL.untaxed_amount_invoiced
Those numbers are interesting in reporting, and to manage the billing
part for salespersons.
Task #1857652
This commit is contained in:
@@ -933,6 +933,9 @@ class SaleOrderLine(models.Model):
|
||||
compute='_get_invoice_qty', string='Invoiced', store=True, readonly=True,
|
||||
digits=dp.get_precision('Product Unit of Measure'))
|
||||
|
||||
untaxed_amount_invoiced = fields.Monetary("Untaxed Invoiced Amount", compute='_compute_untaxed_amount_invoiced', compute_sudo=True, store=True)
|
||||
untaxed_amount_to_invoice = fields.Monetary("Untaxed Amount To Invoice", compute='_compute_untaxed_amount_to_invoice', compute_sudo=True, store=True)
|
||||
|
||||
salesman_id = fields.Many2one(related='order_id.user_id', store=True, string='Salesperson', readonly=True)
|
||||
currency_id = fields.Many2one(related='order_id.currency_id', depends=['order_id'], store=True, string='Currency', readonly=True)
|
||||
company_id = fields.Many2one(related='order_id.company_id', string='Company', store=True, readonly=True)
|
||||
@@ -1047,6 +1050,52 @@ class SaleOrderLine(models.Model):
|
||||
else:
|
||||
line.qty_delivered_manual = 0.0
|
||||
|
||||
@api.depends('invoice_lines', 'invoice_lines.price_total', 'invoice_lines.invoice_id.state', 'invoice_lines.invoice_id.type')
|
||||
def _compute_untaxed_amount_invoiced(self):
|
||||
""" Compute the untaxed amount already invoiced from the sale order line, taking the refund attached
|
||||
the so line into account. This amount is computed as
|
||||
SUM(inv_line.price_subtotal) - SUM(ref_line.price_subtotal)
|
||||
where
|
||||
`inv_line` is a customer invoice line linked to the SO line
|
||||
`ref_line` is a customer credit note (refund) line linked to the SO line
|
||||
"""
|
||||
for line in self:
|
||||
amount_invoiced = 0.0
|
||||
for invoice_line in line.invoice_lines:
|
||||
if invoice_line.invoice_id.state in ['open', 'paid']:
|
||||
invoice_date = invoice_line.invoice_id.date_invoice or fields.Date.today()
|
||||
if invoice_line.invoice_id.type == 'out_invoice':
|
||||
amount_invoiced += invoice_line.currency_id._convert(invoice_line.price_subtotal, line.currency_id, line.company_id, invoice_date)
|
||||
elif invoice_line.invoice_id.type == 'out_refund':
|
||||
amount_invoiced -= invoice_line.currency_id._convert(invoice_line.price_subtotal, line.currency_id, line.company_id, invoice_date)
|
||||
line.untaxed_amount_invoiced = amount_invoiced
|
||||
|
||||
@api.depends('state', 'product_id', 'untaxed_amount_invoiced', 'qty_delivered')
|
||||
def _compute_untaxed_amount_to_invoice(self):
|
||||
""" Total of remaining amount to invoice on the sale order line (taxes excl.) as
|
||||
total_sol - amount already invoiced
|
||||
where Total_sol depends on the invoice policy of the product.
|
||||
|
||||
Note: Draft invoice are ignored on purpose, the 'to invoice' amount should
|
||||
come only from the SO lines.
|
||||
"""
|
||||
for line in self:
|
||||
amount_to_invoice = 0.0
|
||||
if line.state in ['sale', 'done']:
|
||||
# Note: do not use price_subtotal field as it returns zero when the ordered quantity is
|
||||
# zero. It causes problem for expense line (e.i.: ordered qty = 0, deli qty = 4,
|
||||
# price_unit = 20 ; subtotal is zero), but when you can invoice the line, you see an
|
||||
# amount and not zero. Since we compute untaxed amount, we can use directly the price
|
||||
# unit without using `compute_all()` method on taxes.
|
||||
price_subtotal = 0.0
|
||||
if line.product_id.invoice_policy == 'delivery':
|
||||
price_subtotal = line.price_unit * line.qty_delivered
|
||||
else:
|
||||
price_subtotal = line.price_unit * line.product_uom_qty
|
||||
|
||||
amount_to_invoice = price_subtotal - line.untaxed_amount_invoiced
|
||||
line.untaxed_amount_to_invoice = amount_to_invoice
|
||||
|
||||
@api.multi
|
||||
def _prepare_invoice_line(self, qty):
|
||||
"""
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.tools import pycompat
|
||||
from odoo.tools import pycompat, float_is_zero
|
||||
from .test_sale_common import TestCommonSaleNoChart
|
||||
|
||||
|
||||
@@ -115,6 +115,11 @@ class TestSaleToInvoice(TestCommonSaleNoChart):
|
||||
product_price = line.price_unit * line.product_uom_qty
|
||||
self.assertEquals(line.discount, (product_price - line.price_subtotal) / product_price * 100, 'Discount should be applied on order line')
|
||||
|
||||
# lines are in draft
|
||||
for line in self.sale_order.order_line:
|
||||
self.assertTrue(float_is_zero(line.untaxed_amount_to_invoice, precision_digits=2), "The amount to invoice should be zero, as the line is in draf state")
|
||||
self.assertTrue(float_is_zero(line.untaxed_amount_invoiced, precision_digits=2), "The invoiced amount should be zero, as the line is in draft state")
|
||||
|
||||
self.sale_order.action_confirm()
|
||||
|
||||
# Let's do an invoice with invoiceable lines
|
||||
@@ -131,14 +136,26 @@ class TestSaleToInvoice(TestCommonSaleNoChart):
|
||||
|
||||
def test_invoice_refund(self):
|
||||
""" Test invoice with a refund and check customer invoices credit note is created from respective invoice """
|
||||
# lines are in draft
|
||||
for line in self.sale_order.order_line:
|
||||
self.assertTrue(float_is_zero(line.untaxed_amount_to_invoice, precision_digits=2), "The amount to invoice should be zero, as the line is in draf state")
|
||||
self.assertTrue(float_is_zero(line.untaxed_amount_invoiced, precision_digits=2), "The invoiced amount should be zero, as the line is in draft state")
|
||||
|
||||
# Confirm the SO
|
||||
self.sale_order.action_confirm()
|
||||
# Take only invoicable line
|
||||
order_line = self.sale_order.order_line.filtered(lambda l: l.product_id.invoice_policy == 'order')
|
||||
|
||||
# Check ordered quantity, quantity to invoice and invoiced quantity of SO lines
|
||||
for line in order_line:
|
||||
self.assertEquals(line.qty_to_invoice, line.product_uom_qty, 'Quantity to invoice should be same as ordered quantity')
|
||||
self.assertEquals(line.qty_invoiced, 0.0, 'Invoiced quantity should be zero as no any invoice created for SO')
|
||||
for line in self.sale_order.order_line:
|
||||
if line.product_id.invoice_policy == 'delivery':
|
||||
self.assertEquals(line.qty_to_invoice, 0.0, 'Quantity to invoice should be same as ordered quantity')
|
||||
self.assertEquals(line.qty_invoiced, 0.0, 'Invoiced quantity should be zero as no any invoice created for SO')
|
||||
self.assertEquals(line.untaxed_amount_to_invoice, 0.0, "The amount to invoice should be zero, as the line based on delivered quantity")
|
||||
self.assertEquals(line.untaxed_amount_invoiced, 0.0, "The invoiced amount should be zero, as the line based on delivered quantity")
|
||||
else:
|
||||
self.assertEquals(line.qty_to_invoice, line.product_uom_qty, 'Quantity to invoice should be same as ordered quantity')
|
||||
self.assertEquals(line.qty_invoiced, 0.0, 'Invoiced quantity should be zero as no any invoice created for SO')
|
||||
self.assertEquals(line.untaxed_amount_to_invoice, line.product_uom_qty * line.price_unit, "The amount to invoice should the total of the line, as the line is confirmed")
|
||||
self.assertEquals(line.untaxed_amount_invoiced, 0.0, "The invoiced amount should be zero, as the line is confirmed")
|
||||
|
||||
# Let's do an invoice with invoiceable lines
|
||||
payment = self.env['sale.advance.payment.inv'].with_context(self.context).create({
|
||||
@@ -149,22 +166,98 @@ class TestSaleToInvoice(TestCommonSaleNoChart):
|
||||
invoice = self.sale_order.invoice_ids[0]
|
||||
|
||||
# Update quantity of an invoice lines
|
||||
invoice.invoice_line_ids[0].write({'quantity': 3.0})
|
||||
invoice.invoice_line_ids[1].write({'quantity': 2.0})
|
||||
invoice.invoice_line_ids[0].write({'quantity': 3.0}) # product ordered: from 5 to 3
|
||||
invoice.invoice_line_ids[1].write({'quantity': 2.0}) # service ordered: from 3 to 2
|
||||
|
||||
# amount to invoice / invoiced should not have changed (amounts take only confirmed invoice into account)
|
||||
for line in self.sale_order.order_line:
|
||||
if line.product_id.invoice_policy == 'delivery':
|
||||
self.assertEquals(line.qty_to_invoice, 0.0, "Quantity to invoice should be zero")
|
||||
self.assertEquals(line.qty_invoiced, 0.0, "Invoiced quantity should be zero as delivered lines are not delivered yet")
|
||||
self.assertEquals(line.untaxed_amount_to_invoice, 0.0, "The amount to invoice should be zero, as the line based on delivered quantity (no confirmed invoice)")
|
||||
self.assertEquals(line.untaxed_amount_invoiced, 0.0, "The invoiced amount should be zero, as no invoice are validated for now")
|
||||
else:
|
||||
if line == self.sol_prod_order:
|
||||
self.assertEquals(self.sol_prod_order.qty_to_invoice, 2.0, "Changing the quantity on draft invoice update the qty to invoice on SO lines")
|
||||
self.assertEquals(self.sol_prod_order.qty_invoiced, 3.0, "Changing the quantity on draft invoice update the invoiced qty on SO lines")
|
||||
else:
|
||||
self.assertEquals(self.sol_serv_order.qty_to_invoice, 1.0, "Changing the quantity on draft invoice update the qty to invoice on SO lines")
|
||||
self.assertEquals(self.sol_serv_order.qty_invoiced, 2.0, "Changing the quantity on draft invoice update the invoiced qty on SO lines")
|
||||
self.assertEquals(line.untaxed_amount_to_invoice, line.product_uom_qty * line.price_unit, "The amount to invoice should the total of the line, as the line is confirmed (no confirmed invoice)")
|
||||
self.assertEquals(line.untaxed_amount_invoiced, 0.0, "The invoiced amount should be zero, as no invoice are validated for now")
|
||||
|
||||
invoice.action_invoice_open()
|
||||
|
||||
# Check quantity to invoice on SO lines
|
||||
for line in order_line:
|
||||
self.assertEquals(line.qty_to_invoice, line.product_uom_qty - line.qty_invoiced, 'Quantity to invoice should be a difference between ordered quantity and invoiced quantity')
|
||||
for line in self.sale_order.order_line:
|
||||
if line.product_id.invoice_policy == 'delivery':
|
||||
self.assertEquals(line.qty_to_invoice, 0.0, "Quantity to invoice should be same as ordered quantity")
|
||||
self.assertEquals(line.qty_invoiced, 0.0, "Invoiced quantity should be zero as no any invoice created for SO")
|
||||
self.assertEquals(line.untaxed_amount_to_invoice, 0.0, "The amount to invoice should be zero, as the line based on delivered quantity")
|
||||
self.assertEquals(line.untaxed_amount_invoiced, 0.0, "The invoiced amount should be zero, as the line based on delivered quantity")
|
||||
else:
|
||||
if line == self.sol_prod_order:
|
||||
self.assertEquals(line.qty_to_invoice, 2.0, "The ordered sale line are totally invoiced (qty to invoice is zero)")
|
||||
self.assertEquals(line.qty_invoiced, 3.0, "The ordered (prod) sale line are totally invoiced (qty invoiced come from the invoice lines)")
|
||||
else:
|
||||
self.assertEquals(line.qty_to_invoice, 1.0, "The ordered sale line are totally invoiced (qty to invoice is zero)")
|
||||
self.assertEquals(line.qty_invoiced, 2.0, "The ordered (serv) sale line are totally invoiced (qty invoiced = the invoice lines)")
|
||||
self.assertEquals(line.untaxed_amount_to_invoice, line.price_unit * line.qty_to_invoice, "Amount to invoice is now set as qty to invoice * unit price since no price change on invoice, for ordered products")
|
||||
self.assertEquals(line.untaxed_amount_invoiced, line.price_unit * line.qty_invoiced, "Amount invoiced is now set as qty invoiced * unit price since no price change on invoice, for ordered products")
|
||||
|
||||
# Make a credit note
|
||||
credit_note = self.env['account.invoice.refund'].with_context({'active_ids': [invoice.id], 'active_id': invoice.id}).create({
|
||||
'filter_refund': 'refund',
|
||||
'description': 'test'
|
||||
credit_note_wizard = self.env['account.invoice.refund'].with_context({'active_ids': [invoice.id], 'active_id': invoice.id}).create({
|
||||
'filter_refund': 'modify', # this is the only mode for which the SO line is linked to the refund (https://github.com/odoo/odoo/commit/e680f29560ac20133c7af0c6364c6ef494662eac)
|
||||
'description': 'reason test',
|
||||
})
|
||||
credit_note.invoice_refund()
|
||||
invoice_credit_note = self.sale_order.invoice_ids[1]
|
||||
credit_note_wizard.invoice_refund()
|
||||
invoice_2 = self.sale_order.invoice_ids.sorted(key=lambda inv: inv.id, reverse=False)[-1] # the first invoice, its refund, and the new invoice
|
||||
|
||||
# Check invoice's type and number
|
||||
self.assertEquals(invoice_credit_note.type, 'out_refund', 'Invoice type should be a customer credit note')
|
||||
self.assertEquals(invoice.number, invoice_credit_note.origin, 'Customer invoices credit note should be created from respective invoice')
|
||||
self.assertEquals(invoice_2.type, 'out_invoice', 'The last created invoiced should be a customer invoice')
|
||||
self.assertEquals(invoice_2.state, 'draft', 'Last Customer invoices should be in draft')
|
||||
|
||||
# At this time, the invoice 1 and its refund are confirmed, so the amounts invoiced are zero. The third invoice
|
||||
# (2nd customer inv) is in draft state.
|
||||
for line in self.sale_order.order_line:
|
||||
if line.product_id.invoice_policy == 'delivery':
|
||||
self.assertEquals(line.qty_to_invoice, 0.0, "Quantity to invoice should be same as ordered quantity")
|
||||
self.assertEquals(line.qty_invoiced, 0.0, "Invoiced quantity should be zero as no any invoice created for SO")
|
||||
self.assertEquals(line.untaxed_amount_to_invoice, 0.0, "The amount to invoice should be zero, as the line based on delivered quantity")
|
||||
self.assertEquals(line.untaxed_amount_invoiced, 0.0, "The invoiced amount should be zero, as the line based on delivered quantity")
|
||||
else:
|
||||
if line == self.sol_prod_order:
|
||||
self.assertEquals(line.qty_to_invoice, 2.0, "The qty to invoice does not change when confirming the new invoice (2)")
|
||||
self.assertEquals(line.qty_invoiced, 3.0, "The ordered (prod) sale line does not change on invoice 2 confirmation")
|
||||
self.assertEquals(line.untaxed_amount_to_invoice, line.price_unit * 5, "Amount to invoice is now set as qty to invoice * unit price since no price change on invoice, for ordered products")
|
||||
self.assertEquals(line.untaxed_amount_invoiced, 0.0, "Amount invoiced is zero as the invoice 1 and its refund are reconcilied")
|
||||
else:
|
||||
self.assertEquals(line.qty_to_invoice, 1.0, "The qty to invoice does not change when confirming the new invoice (2)")
|
||||
self.assertEquals(line.qty_invoiced, 2.0, "The ordered (serv) sale line does not change on invoice 2 confirmation")
|
||||
self.assertEquals(line.untaxed_amount_to_invoice, line.price_unit * 3, "Amount to invoice is now set as unit price * ordered qty - refund qty) even if the ")
|
||||
self.assertEquals(line.untaxed_amount_invoiced, 0.0, "Amount invoiced is zero as the invoice 1 and its refund are reconcilied")
|
||||
|
||||
# Change unit of ordered product on refund lines
|
||||
invoice_2.invoice_line_ids.filtered(lambda invl: invl.product_id == self.sol_prod_order.product_id).write({'price_unit': 100})
|
||||
invoice_2.invoice_line_ids.filtered(lambda invl: invl.product_id == self.sol_serv_order.product_id).write({'price_unit': 50})
|
||||
|
||||
# Validate the refund
|
||||
invoice_2.action_invoice_open()
|
||||
|
||||
for line in self.sale_order.order_line:
|
||||
if line.product_id.invoice_policy == 'delivery':
|
||||
self.assertEquals(line.qty_to_invoice, 0.0, "Quantity to invoice should be same as ordered quantity")
|
||||
self.assertEquals(line.qty_invoiced, 0.0, "Invoiced quantity should be zero as no any invoice created for SO")
|
||||
self.assertEquals(line.untaxed_amount_to_invoice, 0.0, "The amount to invoice should be zero, as the line based on delivered quantity")
|
||||
self.assertEquals(line.untaxed_amount_invoiced, 0.0, "The invoiced amount should be zero, as the line based on delivered quantity")
|
||||
else:
|
||||
if line == self.sol_prod_order:
|
||||
self.assertEquals(line.qty_to_invoice, 2.0, "The qty to invoice does not change when confirming the new invoice (2)")
|
||||
self.assertEquals(line.qty_invoiced, 3.0, "The ordered sale line are totally invoiced (qty invoiced = ordered qty)")
|
||||
self.assertEquals(line.untaxed_amount_to_invoice, 1100.0, "")
|
||||
self.assertEquals(line.untaxed_amount_invoiced, 300.0, "")
|
||||
else:
|
||||
self.assertEquals(line.qty_to_invoice, 1.0, "The qty to invoice does not change when confirming the new invoice (2)")
|
||||
self.assertEquals(line.qty_invoiced, 2.0, "The ordered sale line are totally invoiced (qty invoiced = ordered qty)")
|
||||
self.assertEquals(line.untaxed_amount_to_invoice, 170.0, "")
|
||||
self.assertEquals(line.untaxed_amount_invoiced, 100.0, "")
|
||||
|
||||
Reference in New Issue
Block a user