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 -
-
+
+