diff options
| -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) |
