diff options
| author | Andrea Grazioso (agr-odoo) <agr@odoo.com> | 2020-12-22 17:20:09 +0000 |
|---|---|---|
| committer | Victor Feyens <vfe@odoo.com> | 2021-01-20 15:44:20 +0000 |
| commit | 6532c6ba7acb2de53fc839d5a7c5d8b200716565 (patch) | |
| tree | 79a33fcbb4e091f42edfd57a21a1eaf1fa5241ad | |
| parent | 604c07edcd4da8cf2a663d06ba91f137eb74691b (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.py | 86 | ||||
| -rw-r--r-- | addons/sale_coupon/models/sale_order.py | 27 | ||||
| -rw-r--r-- | addons/sale_coupon/tests/__init__.py | 1 | ||||
| -rw-r--r-- | addons/sale_coupon/tests/test_sale_invoicing.py | 54 |
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) |
