summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndrea Grazioso (agr-odoo) <agr@odoo.com>2020-12-22 17:20:09 +0000
committerVictor Feyens <vfe@odoo.com>2021-01-20 15:44:20 +0000
commit6532c6ba7acb2de53fc839d5a7c5d8b200716565 (patch)
tree79a33fcbb4e091f42edfd57a21a1eaf1fa5241ad
parent604c07edcd4da8cf2a663d06ba91f137eb74691b (diff)
[FIX] sale,sale_coupon: do not invoice only promotion lines
Create a promotion auto applied with 0 minimum amount on any product in catalog. Have a DEMO product invoiced on delivery Create a sale order with DEMO, apply the promotion. Confirm. Click on 'Create Invoice'. The invoice will only contain the promotion because the line for DEMO is to be invoiced only after delivery, while by default the promotion product has the invoice policy 'on order' This commit will: * avoid highlight the button 'create invoice' if the only invoiceable lines are promotion lines as well as raising an error if the invoice is created in the same scenario * refuse to invoice only promotion lines from a SO. * adds a test to cover the case opw-2414630 closes odoo/odoo#64721 X-original-commit: a3afb98b9a90b28454284528871dd805da9a95b1 Signed-off-by: Victor Feyens (vfe) <vfe@odoo.com>
-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)