diff --git a/models/sale_coupon.py b/models/sale_coupon.py index 10bdc6f97d5..e60873df677 100644 --- a/models/sale_coupon.py +++ b/models/sale_coupon.py @@ -52,15 +52,12 @@ class SaleCoupon(models.Model): def _check_coupon_code(self, order): message = {} applicable_programs = order._get_applicable_programs() - amount_total = order.amount_untaxed + order.reward_amount - if self.program_id.rule_minimum_amount_tax_inclusion == 'tax_included': - amount_total += order.amount_tax if self.state in ('used', 'expired') or \ (self.expiration_date and self.expiration_date < order.date_order): message = {'error': _('This coupon %s has been used or is expired.') % (self.code)} elif self.state == 'reserved': message = {'error': _('This coupon %s exists but the origin sales order is not validated yet.') % (self.code)} - elif self.program_id._compute_program_amount('rule_minimum_amount', order.currency_id) > amount_total: + elif not self.program_id._filter_on_mimimum_amount(order): message = {'error': _('A minimum of %s %s should be purchased to get the reward') % (self.program_id.rule_minimum_amount, self.program_id.currency_id.name)} elif not self.program_id.active: message = {'error': _('The coupon program for %s is in draft or closed state') % (self.code)} diff --git a/models/sale_coupon_program.py b/models/sale_coupon_program.py index b6d43713cec..6c487f7f281 100644 --- a/models/sale_coupon_program.py +++ b/models/sale_coupon_program.py @@ -10,7 +10,10 @@ class SaleCouponProgram(models.Model): _name = 'sale.coupon.program' _description = "Sales Coupon Program" _inherits = {'sale.coupon.rule': 'rule_id', 'sale.coupon.reward': 'reward_id'} - _order = "sequence" + # We should apply 'discount' promotion first to avoid offering free product when we should not. + # Eg: If the discount lower the SO total below the required threshold + # Note: This is only revelant when programs have the same sequence (which they have by default) + _order = "sequence, reward_type" name = fields.Char(required=True, translate=True) active = fields.Boolean('Active', default=True, help="A program is available for the customers when active") @@ -129,12 +132,9 @@ class SaleCouponProgram(models.Model): def _check_promo_code(self, order, coupon_code): message = {} applicable_programs = order._get_applicable_programs() - amount_total = order.amount_untaxed + order.reward_amount - if self.rule_minimum_amount_tax_inclusion == 'tax_included': - amount_total += order.amount_tax if self.maximum_use_number != 0 and self.order_count >= self.maximum_use_number: message = {'error': _('Promo code %s has been expired.') % (coupon_code)} - elif self._compute_program_amount('rule_minimum_amount', order.currency_id) > amount_total: + elif not self._filter_on_mimimum_amount(order): message = {'error': _('A minimum of %s %s should be purchased to get the reward') % (self.rule_minimum_amount, self.currency_id.name)} elif self.promo_code and self.promo_code == order.promo_code: message = {'error': _('The promo code is already applied on this order')} @@ -144,7 +144,7 @@ class SaleCouponProgram(models.Model): message = {'error': _('Promo code is invalid')} elif self.rule_date_from and self.rule_date_from > order.date_order or self.rule_date_to and order.date_order > self.rule_date_to: message = {'error': _('Promo code is expired')} - elif order.promo_code: + elif order.promo_code and self.promo_code_usage == 'code_needed': message = {'error': _('Promotionals codes are not cumulable.')} elif self._is_global_discount_program() and order.applied_coupon_ids.mapped('program_id').filtered(lambda program: program._is_global_discount_program()): message = {'error': _('Global discounts are not cumulable.')} @@ -165,12 +165,16 @@ class SaleCouponProgram(models.Model): @api.model def _filter_on_mimimum_amount(self, order): - # To get actual total amount of SO without any reward or discount - discounted_amount = sum(order.order_line.filtered(lambda line: line.is_reward_line).mapped('price_total')) - untaxed_amount = order.amount_untaxed - discounted_amount + untaxed_amount = order.amount_untaxed + tax_amount = order.amount_tax + + # Some lines should not be considered when checking if threshold is met like delivery + untaxed_amount -= sum([line.price_subtotal for line in order._get_no_effect_on_threshold_lines()]) + tax_amount -= sum([line.price_tax for line in order._get_no_effect_on_threshold_lines()]) + return self.filtered(lambda program: program.rule_minimum_amount_tax_inclusion == 'tax_included' and - program._compute_program_amount('rule_minimum_amount', order.currency_id) <= untaxed_amount + order.amount_tax or + program._compute_program_amount('rule_minimum_amount', order.currency_id) <= untaxed_amount + tax_amount or program.rule_minimum_amount_tax_inclusion == 'tax_excluded' and program._compute_program_amount('rule_minimum_amount', order.currency_id) <= untaxed_amount) diff --git a/models/sale_order.py b/models/sale_order.py index 9ce09c452e2..12876e46c4c 100644 --- a/models/sale_order.py +++ b/models/sale_order.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import api, fields, models +from odoo import api, fields, models, _ class SaleOrder(models.Model): @@ -19,7 +19,15 @@ class SaleOrder(models.Model): @api.depends('order_line') def _compute_reward_total(self): for order in self: - order.reward_amount = sum([line.price_subtotal for line in order.order_line.filtered(lambda line: line.is_reward_line)]) + order.reward_amount = sum([line.price_subtotal for line in order._get_reward_lines()]) + + def _get_no_effect_on_threshold_lines(self): + self.ensure_one() + lines = self.env['sale.order.line'] + # Do not count already applied promo_code discount; Do not substract itself + if self.code_promo_program_id and self.code_promo_program_id.reward_type == 'discount': + lines = self.order_line.filtered(lambda l: l.product_id == self.code_promo_program_id.discount_line_product_id) + return lines @api.multi def recompute_coupon_lines(self): @@ -30,7 +38,7 @@ class SaleOrder(models.Model): def copy(self, default=None): order = super(SaleOrder, self).copy(dict(default or {}, order_line=False)) - for line in self.order_line.filtered(lambda line: not line.is_reward_line): + for line in self._get_reward_lines(): line.copy({'order_id': order.id}) order.with_context(sale_coupon_no_loop=False)._create_new_no_code_promo_reward_lines() return order @@ -72,7 +80,7 @@ class SaleOrder(models.Model): if program._is_valid_product(program.reward_product_id): reward_product_qty = max_product_qty // (program.rule_min_quantity + program.reward_product_quantity) else: - reward_product_qty = max_product_qty + reward_product_qty = min(max_product_qty, self.order_line.filtered(lambda x: x.product_id == program.reward_product_id).product_uom_qty) reward_qty = min(int(int(max_product_qty / program.rule_min_quantity) * program.reward_product_quantity), reward_product_qty) # Take the default taxes on the reward product, mapped with the fiscal position @@ -84,14 +92,16 @@ class SaleOrder(models.Model): 'price_unit': - price_unit, 'product_uom_qty': reward_qty, 'is_reward_line': True, - 'name': "Free Product - " + program.reward_product_id.name, + 'name': _("Free Product") + " - " + program.reward_product_id.name, 'product_uom': program.reward_product_id.uom_id.id, 'tax_id': [(4, tax.id, False) for tax in taxes], } - def _get_order_lines_untaxed_amount(self): - """ Returns the untaxed sale order total amount without the rewards amount""" - return sum([x.price_subtotal for x in self.order_line.filtered(lambda x: not x.is_reward_line)]) + def _get_order_lines_tax_included_amount(self): + """ Returns the taxes included sale order total amount without the rewards amount""" + free_reward_product = self.env['sale.coupon.program'].search([('reward_type', '=', 'product')]).mapped('discount_line_product_id') + return sum([x.price_total for x in self.order_line.filtered(lambda x: not x.is_reward_line or x.product_id in free_reward_product)]) + def _get_reward_values_discount_fixed_amount(self, program): total_amount = sum(self.order_line.filtered(lambda line: not line.is_reward_line).mapped('price_total')) @@ -106,15 +116,15 @@ class SaleOrder(models.Model): def _get_reward_values_discount_percentage(self, program): discount_amount = 0 - amount_untaxed = self._get_order_lines_untaxed_amount() + amount_total = self._get_order_lines_tax_included_amount() max_amount = program._compute_program_amount('discount_max_amount', self.currency_id) if program.discount_apply_on == 'on_order': - discount_amount = amount_untaxed * (program.discount_percentage / 100) + discount_amount = amount_total * (program.discount_percentage / 100) if program.discount_apply_on == 'cheapest_product': unit_prices = self._get_lines_unit_prices() discount_amount = (min(unit_prices) * (program.discount_percentage) / 100) if program.discount_apply_on == 'specific_product': - discount_amount = sum([(x.price_unit * x.product_uom_qty) * (program.discount_percentage / 100) for x in self.order_line.filtered( + discount_amount = sum([(x.price_total * x.product_uom_qty) * (program.discount_percentage / 100) for x in self.order_line.filtered( lambda x: x.product_id == program.discount_specific_product_id)]) if program.discount_max_amount and discount_amount > max_amount: discount_amount = max_amount @@ -126,7 +136,7 @@ class SaleOrder(models.Model): elif program.discount_type == 'fixed_amount': discount_amount = self._get_reward_values_discount_fixed_amount(program) return { - 'name': "Discount: %s" % (program.name), + 'name': _("Discount: ") + program.name, 'product_id': program.discount_line_product_id.id, 'price_unit': - discount_amount, 'product_uom_qty': 1.0, @@ -155,7 +165,10 @@ class SaleOrder(models.Model): 'discount_line_product_id': program.discount_line_product_id.id }) self.generated_coupon_ids |= coupon - subject = '%s, a coupon has been generated from your order %s' % (self.partner_id.name, self.name) + subject = _('%(partner_name)s, a coupon has been generated from your order %(order_name)s') % { + 'partner_name': self.partner_id.name, + 'order_name': self.name, + } body = self.env.ref('sale_coupon.sale_coupon_created_coupon_email_template').render({ 'code': coupon.code, 'reward_description': coupon.program_id.discount_line_product_id.name @@ -183,7 +196,7 @@ class SaleOrder(models.Model): ('promo_code_usage', '=', 'no_code_needed'), # ('promo_applicability', '=', 'on_current_order') ])._filter_programs_from_common_rules(self) - return programs.sorted('sequence') + return programs def _create_new_no_code_promo_reward_lines(self): '''Apply new programs that are applicable'''