summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--addons/sale/models/sale.py86
-rw-r--r--addons/sale_coupon/models/sale_order.py27
-rw-r--r--addons/sale_coupon/tests/__init__.py1
-rw-r--r--addons/sale_coupon/tests/test_sale_invoicing.py54
4 files changed, 132 insertions, 36 deletions
diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py
index 0d3b42f5e20..2aee0194d8d 100644
--- a/addons/sale/models/sale.py
+++ b/addons/sale/models/sale.py
@@ -625,6 +625,33 @@ Reason(s) of this behavior could be:
""")
return UserError(msg)
+ def _get_invoiceable_lines(self, final=False):
+ """Return the invoiceable lines for order `self`."""
+ down_payment_line_ids = []
+ invoiceable_line_ids = []
+ pending_section = None
+ precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
+
+ for line in self.order_line:
+ if line.display_type == 'line_section':
+ # Only invoice the section if one of its lines is invoiceable
+ pending_section = line
+ continue
+ if line.display_type != 'line_note' and float_is_zero(line.qty_to_invoice, precision_digits=precision):
+ continue
+ if line.qty_to_invoice > 0 or (line.qty_to_invoice < 0 and final) or line.display_type == 'line_note':
+ if line.is_downpayment:
+ # Keep down payment lines separately, to put them together
+ # at the end of the invoice, in a specific dedicated section.
+ down_payment_line_ids.append(line.id)
+ continue
+ if pending_section:
+ invoiceable_line_ids.append(pending_section.id)
+ pending_section = None
+ invoiceable_line_ids.append(line.id)
+
+ return self.env['sale.order.line'].browse(invoiceable_line_ids + down_payment_line_ids)
+
def _create_invoices(self, grouped=False, final=False, date=None):
"""
Create the invoice associated to the SO.
@@ -640,54 +667,41 @@ Reason(s) of this behavior could be:
except AccessError:
return self.env['account.move']
- precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
-
# 1) Create invoices.
invoice_vals_list = []
- invoice_item_sequence = 0
+ invoice_item_sequence = 0 # Incremental sequencing to keep the lines order on the invoice.
for order in self:
order = order.with_company(order.company_id)
current_section_vals = None
down_payments = order.env['sale.order.line']
- # Invoice values.
invoice_vals = order._prepare_invoice()
+ invoiceable_lines = order._get_invoiceable_lines(final)
- # Invoice line values (keep only necessary sections).
- invoice_lines_vals = []
- for line in order.order_line:
- if line.display_type == 'line_section':
- current_section_vals = line._prepare_invoice_line(sequence=invoice_item_sequence + 1)
- continue
- if line.display_type != 'line_note' and float_is_zero(line.qty_to_invoice, precision_digits=precision):
- continue
- if line.qty_to_invoice > 0 or (line.qty_to_invoice < 0 and final) or line.display_type == 'line_note':
- if line.is_downpayment:
- down_payments += line
- continue
- if current_section_vals:
- invoice_item_sequence += 1
- invoice_lines_vals.append(current_section_vals)
- current_section_vals = None
- invoice_item_sequence += 1
- prepared_line = line._prepare_invoice_line(sequence=invoice_item_sequence)
- invoice_lines_vals.append(prepared_line)
-
- # If down payments are present in SO, group them under common section
- if down_payments:
- invoice_item_sequence += 1
- down_payments_section = order._prepare_down_payment_section_line(sequence=invoice_item_sequence)
- invoice_lines_vals.append(down_payments_section)
- for down_payment in down_payments:
- invoice_item_sequence += 1
- invoice_down_payment_vals = down_payment._prepare_invoice_line(sequence=invoice_item_sequence)
- invoice_lines_vals.append(invoice_down_payment_vals)
-
- if not any(new_line['display_type'] is False for new_line in invoice_lines_vals):
+ if not any(not line.display_type for line in invoiceable_lines):
raise self._nothing_to_invoice_error()
- invoice_vals['invoice_line_ids'] = [(0, 0, invoice_line_id) for invoice_line_id in invoice_lines_vals]
+ invoice_line_vals = []
+ down_payment_section_added = False
+ for line in invoiceable_lines:
+ if not down_payment_section_added and line.is_downpayment:
+ # Create a dedicated section for the down payments
+ # (put at the end of the invoiceable_lines)
+ invoice_line_vals.append(
+ (0, 0, order._prepare_down_payment_section_line(
+ sequence=invoice_item_sequence,
+ )),
+ )
+ dp_section = True
+ invoice_item_sequence += 1
+ invoice_line_vals.append(
+ (0, 0, line._prepare_invoice_line(
+ sequence=invoice_item_sequence,
+ )),
+ )
+ invoice_item_sequence += 1
+ invoice_vals['invoice_line_ids'] = invoice_line_vals
invoice_vals_list.append(invoice_vals)
if not invoice_vals_list:
diff --git a/addons/sale_coupon/models/sale_order.py b/addons/sale_coupon/models/sale_order.py
index 031a13a2bd6..61af502619b 100644
--- a/addons/sale_coupon/models/sale_order.py
+++ b/addons/sale_coupon/models/sale_order.py
@@ -427,6 +427,33 @@ class SaleOrder(models.Model):
"""
return self.code_promo_program_id + self.no_code_promo_program_ids + self.applied_coupon_ids.mapped('program_id')
+ def _get_invoice_status(self):
+ # Handling of a specific situation: an order contains
+ # a product invoiced on delivery and a promo line invoiced
+ # on order. We would avoid having the invoice status 'to_invoice'
+ # if the created invoice will only contain the promotion line
+ super()._get_invoice_status()
+ for order in self.filtered(lambda order: order.invoice_status == 'to invoice'):
+ paid_lines = order._get_paid_order_lines()
+ if not any(line.invoice_status == 'to invoice' for line in paid_lines):
+ order.invoice_status = 'no'
+
+ def _get_invoiceable_lines(self, final=False):
+ """ Ensures we cannot invoice only reward lines.
+
+ Since promotion lines are specified with service products,
+ those lines are directly invoiceable when the order is confirmed
+ which can result in invoices containing only promotion lines.
+
+ To avoid those cases, we allow the invoicing of promotion lines
+ iff at least another 'basic' lines is also invoiceable.
+ """
+ invoiceable_lines = super()._get_invoiceable_lines(final)
+ reward_lines = self._get_reward_lines()
+ if invoiceable_lines <= reward_lines:
+ return self.env['sale.order.line'].browse()
+ return invoiceable_lines
+
class SaleOrderLine(models.Model):
_inherit = "sale.order.line"
diff --git a/addons/sale_coupon/tests/__init__.py b/addons/sale_coupon/tests/__init__.py
index 96f8d513813..7ec9d2c0a7a 100644
--- a/addons/sale_coupon/tests/__init__.py
+++ b/addons/sale_coupon/tests/__init__.py
@@ -6,3 +6,4 @@ from . import test_program_with_code_operations
from . import test_program_rules
from . import test_program_numbers
from . import test_program_multi_company
+from . import test_sale_invoicing
diff --git a/addons/sale_coupon/tests/test_sale_invoicing.py b/addons/sale_coupon/tests/test_sale_invoicing.py
new file mode 100644
index 00000000000..773df314920
--- /dev/null
+++ b/addons/sale_coupon/tests/test_sale_invoicing.py
@@ -0,0 +1,54 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from odoo.addons.sale_coupon.tests.common import TestSaleCouponCommon
+from odoo.exceptions import UserError
+from odoo.tests import tagged
+
+
+@tagged('post_install', '-at_install')
+class TestSaleInvoicing(TestSaleCouponCommon):
+
+ def test_invoicing_order_with_promotions(self):
+ discount_coupon_program = self.env['coupon.program'].create({
+ 'name': '10% Discount', # Default behavior
+ 'program_type': 'coupon_program',
+ 'reward_type': 'discount',
+ 'discount_apply_on': 'on_order',
+ 'promo_code_usage': 'no_code_needed',
+ })
+ # Override the default invoice_policy on products
+ discount_coupon_program.discount_line_product_id.invoice_policy = 'order'
+ product = self.env['product.product'].create({
+ 'invoice_policy': 'delivery',
+ 'name': 'Product invoiced on delivery',
+ 'lst_price': 500,
+ })
+
+ order = self.empty_order
+ order.write({
+ 'order_line': [
+ (0, 0, {
+ 'product_id': product.id,
+ })
+ ]
+ })
+
+ order.recompute_coupon_lines()
+ # Order is not confirmed, there shouldn't be any invoiceable line
+ invoiceable_lines = order._get_invoiceable_lines()
+ self.assertEqual(len(invoiceable_lines), 0)
+
+ order.action_confirm()
+ invoiceable_lines = order._get_invoiceable_lines()
+ # Product was not delivered, we cannot invoice
+ # the product line nor the promotion line
+ self.assertEqual(len(invoiceable_lines), 0)
+ with self.assertRaises(UserError):
+ order._create_invoices()
+
+ order.order_line[0].qty_delivered = 1
+ # Product is delivered, the two lines can be invoiced.
+ invoiceable_lines = order._get_invoiceable_lines()
+ self.assertEqual(order.order_line, invoiceable_lines)
+ account_move = order._create_invoices()
+ self.assertEqual(len(account_move.invoice_line_ids), 2)