# 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 > 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 _get_discount_product(self): """Return product.product used for discount line""" 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 return discount_product def _create_discount_lines(self): """Create SOline(s) according to wizard configuration""" self.ensure_one() discount_product = self._get_discount_product() 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()