From bf95b1f2ade01a79263461987e62d3b5b1b09483 Mon Sep 17 00:00:00 2001 From: shag-odoo Date: Mon, 3 Jul 2023 12:02:13 +0530 Subject: [PATCH] [ADD] sale: new wizard to apply discounts on sale orders Replace the magic js logic applying the discount of the first line to all the lines by a dedicated wizard, providing three kind of discounts: * Discount on each sale order lines * Discount on the global order amount * Fixed discount amount task-3369680 closes odoo/odoo#129087 Signed-off-by: Victor Feyens (vfe) Co-authored-by: Victor Feyens --- addons/delivery/views/sale_order_views.xml | 3 - addons/sale/__manifest__.py | 2 +- addons/sale/models/res_company.py | 10 ++ addons/sale/models/sale_order.py | 10 ++ addons/sale/security/ir.model.access.csv | 1 + .../static/src/js/product_discount_field.js | 68 --------- .../static/src/xml/product_discount_field.xml | 9 -- addons/sale/tests/__init__.py | 3 +- addons/sale/tests/test_sale_order_discount.py | 85 +++++++++++ addons/sale/views/sale_order_views.xml | 8 +- addons/sale/wizard/__init__.py | 3 +- addons/sale/wizard/sale_order_discount.py | 133 ++++++++++++++++++ .../sale/wizard/sale_order_discount_views.xml | 39 +++++ .../sale_loyalty/views/sale_order_views.xml | 7 +- 14 files changed, 291 insertions(+), 90 deletions(-) delete mode 100644 addons/sale/static/src/js/product_discount_field.js delete mode 100644 addons/sale/static/src/xml/product_discount_field.xml create mode 100644 addons/sale/tests/test_sale_order_discount.py create mode 100644 addons/sale/wizard/sale_order_discount.py create mode 100644 addons/sale/wizard/sale_order_discount_views.xml diff --git a/addons/delivery/views/sale_order_views.xml b/addons/delivery/views/sale_order_views.xml index 9777bb51c6a..5c6c20658d5 100644 --- a/addons/delivery/views/sale_order_views.xml +++ b/addons/delivery/views/sale_order_views.xml @@ -11,9 +11,6 @@ -
- 0 -
diff --git a/addons/sale/wizard/__init__.py b/addons/sale/wizard/__init__.py index 246f28cce38..169ce646f41 100644 --- a/addons/sale/wizard/__init__.py +++ b/addons/sale/wizard/__init__.py @@ -2,9 +2,10 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from . import account_payment_register +from . import mass_cancel_orders from . import payment_link_wizard from . import payment_provider_onboarding_wizard from . import res_config_settings from . import sale_make_invoice_advance from . import sale_order_cancel -from . import mass_cancel_orders +from . import sale_order_discount diff --git a/addons/sale/wizard/sale_order_discount.py b/addons/sale/wizard/sale_order_discount.py new file mode 100644 index 00000000000..5d607ba0b94 --- /dev/null +++ b/addons/sale/wizard/sale_order_discount.py @@ -0,0 +1,133 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from collections import defaultdict + +from odoo import Command, _, api, fields, models +from odoo.exceptions import ValidationError + + +class SaleOrderDiscount(models.TransientModel): + _name = 'sale.order.discount' + _description = "Discount Wizard" + + sale_order_id = fields.Many2one( + 'sale.order', default=lambda self: self.env.context.get('active_id'), required=True) + company_id = fields.Many2one(related='sale_order_id.company_id') + currency_id = fields.Many2one(related='sale_order_id.currency_id') + discount_amount = fields.Monetary(string="Amount") + discount_percentage = fields.Float(string="Percentage") + discount_type = fields.Selection( + selection=[ + ('sol_discount', "On All Order Lines"), + ('so_discount', "Global Discount"), + ('amount', "Fixed Amount"), + ], + default='sol_discount', + ) + + # CONSTRAINT METHODS # + + @api.constrains('discount_type', 'discount_percentage') + def _check_discount_amount(self): + for wizard in self: + if wizard.discount_type in ('sol_discount', 'so_discount') and ( + wizard.discount_percentage <= 0.0 + or wizard.discount_percentage > 1.0 + ): + raise ValidationError(_("Invalid discount amount")) + + def _prepare_discount_product_values(self): + self.ensure_one() + return { + 'name': _('Discount'), + 'type': 'service', + 'invoice_policy': 'order', + 'list_price': 0.0, + 'company_id': self.company_id.id, + 'taxes_id': None, + } + + def _prepare_discount_line_values(self, product, amount, taxes, description=None): + self.ensure_one() + + vals = { + 'order_id': self.sale_order_id.id, + 'product_id': product.id, + 'sequence': 999, + 'price_unit': -amount, + 'tax_id': [Command.set(taxes.ids)], + } + if description: + # If not given, name will fallback on the standard SOL logic (cf. _compute_name) + vals['name'] = description + + return vals + + def _create_discount_lines(self): + """Create SOline(s) according to wizard configuration""" + self.ensure_one() + + discount_product = self.company_id.sale_discount_product_id + if not discount_product: + self.company_id.sale_discount_product_id = self.env['product.product'].create( + self._prepare_discount_product_values() + ) + discount_product = self.company_id.sale_discount_product_id + + if self.discount_type == 'amount': + vals_list = [ + self._prepare_discount_line_values( + product=discount_product, + amount=self.discount_amount, + taxes=self.env['account.tax'], + ) + ] + else: # so_discount + total_price_per_tax_groups = defaultdict(float) + for line in self.sale_order_id.order_line: + if not line.product_uom_qty or not line.price_unit: + continue + + total_price_per_tax_groups[line.tax_id] += line.price_subtotal + + if not total_price_per_tax_groups: + # No valid lines on which the discount can be applied + return + elif len(total_price_per_tax_groups) == 1: + # No taxes, or all lines have the exact same taxes + taxes = next(iter(total_price_per_tax_groups.keys())) + subtotal = total_price_per_tax_groups[taxes] + vals_list = [{ + **self._prepare_discount_line_values( + product=discount_product, + amount=subtotal * self.discount_percentage, + taxes=taxes, + description=_( + "Discount: %(percent)s%%", + percent=self.discount_percentage*100 + ), + ), + }] + else: + vals_list = [ + self._prepare_discount_line_values( + product=discount_product, + amount=subtotal * self.discount_percentage, + taxes=taxes, + description=_( + "Discount: %(percent)s%%" + "- On products with the following taxes %(taxes)s", + percent=self.discount_percentage*100, + taxes=", ".join(taxes.mapped('name')) + ), + ) for taxes, subtotal in total_price_per_tax_groups.items() + ] + return self.env['sale.order.line'].create(vals_list) + + def action_apply_discount(self): + self.ensure_one() + self = self.with_company(self.company_id) + if self.discount_type == 'sol_discount': + self.sale_order_id.order_line.write({'discount': self.discount_percentage*100}) + else: + self._create_discount_lines() diff --git a/addons/sale/wizard/sale_order_discount_views.xml b/addons/sale/wizard/sale_order_discount_views.xml new file mode 100644 index 00000000000..8358f4498ec --- /dev/null +++ b/addons/sale/wizard/sale_order_discount_views.xml @@ -0,0 +1,39 @@ + + + + + sale.order.line.wizard.form + sale.order.discount + +
+ + + + +
+
+ + +
+
+ +
+
+
+
+
+
+
+
+ +
diff --git a/addons/sale_loyalty/views/sale_order_views.xml b/addons/sale_loyalty/views/sale_order_views.xml index 6ff5fc4dfd7..385bb1f2998 100644 --- a/addons/sale_loyalty/views/sale_order_views.xml +++ b/addons/sale_loyalty/views/sale_order_views.xml @@ -7,10 +7,7 @@ 10 -
- 0 -
-
+
+