[FIX] sale_coupon(_delivery): fix some flows (mostly on next order)

This commit is the continuity of a42c27f9 that fixed some bugs.
One of the bug was that a discount was only applied on price without taxes.
E.G: A $100 product with 20% excluded tax would cost $120 but a 50% discount
     would give the user only $50 discount and not $60 as expected.
     User would lose $10 discount.
With these fixes, the discount line would correctly be set to $60 but since the
discount line would not have tax, the merchant would lose the discount amount
on the tax, in this case 50% of $20.

This commit will improve this case by adding a tax on a discount line.
Thus, since a global discount can be applied to multiple products having
different taxes or even multiple taxes on the same product, a discount line
should be split for every product taxes the discount applies on.
Obviously, this is a big behavior change, even if the discount is now finally
correct: the discount is applied on the total price and the merchant does not
lose any taxes.

To make it more clear, the discount lines description will mention on which
taxes it is applied.

There won't be any changes in the e-commerce since we will group the discount
lines so it will appear as before even if there are multiple lines. This is
only possible because we don't show taxes on line on the e-commerce.

This commit also fixes more complex flows/use cases.
Note: Fixes & flows will be illustrated with a SO but the behavior is the
      same with a cart on front-end.

== Fixes ==
===========================================================================
The biggest flow that needed to be fixed is when a promotion program give a
reward on the next order.
Let's illustrate with a promotion program giving a free `product_B` on next
order (so it will generate a coupon) if the SO meets the requirements.
The requirements are: buy a `product_A` and buy for at least $600.
(This program needs a code: 'free_b_on_next')

1. If the SO has a `product_A` and over $600, applying the promotion code
   `free_b_on_next` will error saying that `product_B` is not in the cart.
   This error should not appear, since the `product_B` is the reward for
   the next sale order that will use the generated coupon.

Anyway, if you add `product_B` and apply the promotion code `free_b_on_next`
to generate the coupon to check the rest of the flow. It will generate the
coupon and send email to the customer.

2. Every time you will apply the promotion code `free_b_on_next`, it will
   regenerate a new coupon and resend a new email to the client.

Then, create a second SO, add 2 `product_B` and try to apply the generated
coupon from the first SO.

3. It will error saying that the $600 requirement is not met.
   It should not since the requirement was only required to generate the
   coupon on the first SO. User has now a coupon to enjoy a free `product_B`
   and should not buy $600.

Anyway, let's add some products to meet the 600$ requirement to check the rest
of the flow.

4. It will now error saying that `product_A` is not in the cart.
   As bug 3., it should not since this requirement was also required to
   generate the coupon on the first SO.

Anyway, let's add `product_A` to meet the requirement to check the rest of the
flow.

5. Updating the promotions will remove the reward and you won't be able to
   reapply the coupon since it was `used`.
   This bug was even more critical in front-end since any action on the cart
   will recompute the coupons and automatically delete the reward after adding
   it. The customer never saw the reward on his cart.
   This bug is related to 3. and 4. Indeed, when recomputing coupons, it will
   remove programs that are not applicable anymore. The code wrongly think it is
   the case with this coupon since we don't have $600 or a `product_A`.
   `_remove_invalid_reward_lines` should treat coupon coming from a promotion
   program set on `next_order` differently and not filter on requirements.
===========================================================================

6. (extension of 2.) Every time an user would meet the requirements of a
   promotion program generating a coupon for a next order, it would generate a
   coupon and send an email.
   E.g: If buy 2 iPads get 1 for free on next order. If user add and remove iPad
        for some reason, every time he has 2 iPads in his cart it will receive
        an email with a new coupon.

6'. When applying a promotion code that will reward next order, it will not be
    set in the SO `code_promo_program_id` (I did not change that behavior since
    it could make sense to be able to apply another coupon that will give -20%
    on current order).
    This will allow applying multiple promotion rewarding a next order and/or a
    promotion rewarding current order. (Which would not be possible if was
    stored on `code_promo_program_id` since you can only apply one code on an
    order)
    But this cause a bug: since the promotion program is not stored on the order,
    every time you will apply the coupon it will generate a new coupon and send
    an email.
    So if you work on a draft sale order and do some tests, it will send many
    emails to customer..

7. Having multiple global discounts (on the whole SO) was forbidden: you can't
   apply a coupon promotion or a program promotion if there is already a global
   coupon discount or a global promotion discount with code. But still you could
   apply a global discount if there were a global promotion discount automatically
   applied (without a code).

8. On the generate coupon wizard, if you select `Number of Selected Customers`,
   by default it is set to 'Match all records (X records)'.
   But when you click on generate, it won't generate anything even if visually
   it selected every partner by default.
   This is because the domain default value is an empty string. Thus, the domain
   widget is selecting everything but once it is sent back to the server, since
   it's an empty string it won't get through the `if` condition.
   It was even weirder when you add something to the domain and remove it, you
   then visually come back to where you were at the beginning but the domain got
   changed from '' to '[]' so in this case it will generate the coupon for every
   partner.

== More like improvements ==

1. `action_cancel` and `action_confirm` would handle correctly the states of the
   applied and generated coupons (valid, consumed, expired..).
   But `action_draft` would not handle the states at all.

2. The generated coupon on a SO are now sent by email only when the SO is validated.
   This is part of the fix n°2 on coupon being generated multiple times and sent by
   email multiple times.
   The coupon sent by email was not usable anyway since it would error saying the
   SO that generated the coupon was not validated.

3. Discount line limited to a max amount (set in the program) will mention on
   the line description that the discount is limited.
   This will avoid cases where you don't understand why 50% discount of $2000
   SO shows $40.

4. If you archive a program and there is still some SO containing this program's
   reward line, these line will be 'floating free' and will never be updated or
   removed. Indeed, the `_remove_invalid_reward_lines` is basically checking
   which programs could be applied on the order and which one are applied on the
   order. If a program is applied on the order but is not supposed to, his
   reward line will be removed.
   Of course, since the program got archived, it won't get retrieve and the line
   will never get removed. Even if the case of a free iPad: if you buy an iPad
   and you removed the paid iPad from the SO. The free iPad will remain forever.
   Note: We can't remove these discount line directly when archiving a program
         because there might be quotations that we don't want to modify since it
         is waiting for customer confirmation.

Improved behavior:
  - Archiving a program archives its related reward product but unarchiving
    the program does not unarchived the product.

task-1866977

opw-1832967
opw-1857843

Closes #2224
Comes with https://github.com/odoo/odoo/pull/24971
This commit is contained in:
Romain Derie
2018-09-13 15:16:57 +02:00
parent 0a565abfad
commit 034fb31c40
5 changed files with 205 additions and 98 deletions
+4 -2
View File
@@ -57,7 +57,8 @@ class SaleCoupon(models.Model):
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 not self.program_id._filter_on_mimimum_amount(order):
# Minimum requirement should not be checked if the coupon got generated by a promotion program (the requirement should have only be checked to generate the coupon)
elif self.program_id.program_type == 'coupon_program' and 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)}
@@ -71,7 +72,8 @@ class SaleCoupon(models.Model):
message = {'error': _('The reward products should be in the sales order lines to apply the discount.')}
elif not self.program_id._is_valid_partner(order.partner_id):
message = {'error': _("The customer doesn't have access to this reward.")}
elif not self.program_id._filter_programs_on_products(order):
# Product requirement should not be checked if the coupon got generated by a promotion program (the requirement should have only be checked to generate the coupon)
elif self.program_id.program_type == 'coupon_program' and not self.program_id._filter_programs_on_products(order):
message = {'error': _("You don't have the required product quantities on your sales order. All the products should be recorded on the sales order. (Example: You need to have 3 T-shirts on your sales order if the promotion is 'Buy 2, Get 1 Free'.")}
else:
if self.program_id not in applicable_programs and self.program_id.promo_applicability == 'on_current_order':
+14 -6
View File
@@ -114,7 +114,7 @@ class SaleCouponProgram(models.Model):
if not self.active:
coupons = self.filtered(lambda p: p.promo_code_usage == 'code_needed').mapped('coupon_ids')
coupons.filtered(lambda x: x.state != 'used').write({'state': 'expired'})
self.mapped('discount_line_product_id').write({'active': False})
self.mapped('discount_line_product_id').write({'active': self.active})
def action_view_sales_orders(self):
self.ensure_one()
@@ -151,7 +151,7 @@ class SaleCouponProgram(models.Model):
message = {'error': _('Promo code is expired')}
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()):
elif self._is_global_discount_program() and order._is_global_discount_already_applied():
message = {'error': _('Global discounts are not cumulable.')}
elif self.promo_applicability == 'on_current_order' and self.reward_type == 'product' and not order._is_reward_in_order_lines(self):
message = {'error': _('The reward products should be in the sales order lines to apply the discount.')}
@@ -160,7 +160,7 @@ class SaleCouponProgram(models.Model):
elif not self._filter_programs_on_products(order):
message = {'error': _("You don't have the required product quantities on your sales order. If the reward is same product quantity, please make sure that all the products are recorded on the sales order (Example: You need to have 3 T-shirts on your sales order if the promotion is 'Buy 2, Get 1 Free'.")}
else:
if self not in applicable_programs:
if self not in applicable_programs and self.promo_applicability == 'on_current_order':
message = {'error': _('At least one of the required conditions is not met to get the reward!')}
return message
@@ -240,12 +240,20 @@ class SaleCouponProgram(models.Model):
return programs
@api.model
def _filter_programs_from_common_rules(self, order):
programs = self._filter_on_mimimum_amount(order)
def _filter_programs_from_common_rules(self, order, next_order=False):
""" Return the programs if every conditions is met
:param bool next_order: is the reward given from a previous order
"""
programs = self
# Minimum requirement should not be checked if the coupon got generated by a promotion program (the requirement should have only be checked to generate the coupon)
if not next_order:
programs = programs and programs._filter_on_mimimum_amount(order)
programs = programs and programs._filter_on_validity_dates(order)
programs = programs and programs._filter_unexpired_programs(order)
programs = programs and programs._filter_programs_on_partners(order)
programs = programs and programs._filter_programs_on_products(order)
# Product requirement should not be checked if the coupon got generated by a promotion program (the requirement should have only be checked to generate the coupon)
if not next_order:
programs = programs and programs._filter_programs_on_products(order)
programs = programs and programs._filter_not_ordered_reward_programs(order)
return programs
+178 -83
View File
@@ -2,6 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.tools.misc import formatLang
class SaleOrder(models.Model):
@@ -39,12 +40,14 @@ class SaleOrder(models.Model):
def copy(self, default=None):
order = super(SaleOrder, self).copy(default)
order._get_reward_lines().unlink()
order.with_context(sale_coupon_no_loop=False)._create_new_no_code_promo_reward_lines()
order._create_new_no_code_promo_reward_lines()
return order
def action_confirm(self):
self.recompute_coupon_lines()
self.generated_coupon_ids.write({'state': 'new'})
self.applied_coupon_ids.write({'state': 'used'})
self._send_reward_coupon_mail()
return super(SaleOrder, self).action_confirm()
def action_cancel(self):
@@ -53,6 +56,11 @@ class SaleOrder(models.Model):
self.applied_coupon_ids.write({'state': 'new'})
return res
def action_draft(self):
res = super(SaleOrder, self).action_draft()
self.generated_coupon_ids.write({'state': 'reserved'})
return res
def _get_reward_lines(self):
self.ensure_one()
return self.order_line.filtered(lambda line: line.is_reward_line)
@@ -95,11 +103,11 @@ class SaleOrder(models.Model):
'tax_id': [(4, tax.id, False) for tax in taxes],
}
def _get_order_lines_tax_included_amount(self):
""" Returns the taxes included sale order total amount without the rewards amount"""
def _get_paid_order_lines(self):
""" Returns the sale order lines that are not reward lines.
It will also return reward lines being free product lines. """
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)])
return 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'))
@@ -109,51 +117,93 @@ class SaleOrder(models.Model):
else:
return fixed_amount
def _get_lines_unit_prices(self):
def _get_cheapest_line(self):
# Unit prices tax included
return [x.price_total / x.product_uom_qty for x in self.order_line.filtered(lambda x: not x.is_reward_line)]
return min(self.order_line.filtered(lambda x: not x.is_reward_line and x.price_unit > 0), key=lambda x: x['price_unit'])
def _get_reward_values_discount_percentage(self, program):
discount_amount = 0
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_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_total * (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
def _get_reward_values_discount_percentage_per_line(self, program, line):
discount_amount = line.product_uom_qty * line.price_unit * (program.discount_percentage / 100)
return discount_amount
def _get_reward_values_discount(self, program):
if program.discount_type == 'percentage':
discount_amount = self._get_reward_values_discount_percentage(program)
elif program.discount_type == 'fixed_amount':
discount_amount = self._get_reward_values_discount_fixed_amount(program)
return {
'name': _("Discount: ") + program.name,
'product_id': program.discount_line_product_id.id,
'price_unit': - discount_amount,
'product_uom_qty': 1.0,
'product_uom': program.discount_line_product_id.uom_id.id,
'is_reward_line': True
}
reward_dict = {}
lines = self._get_paid_order_lines()
if program.discount_apply_on == 'cheapest_product':
line = self._get_cheapest_line()
if line:
discount_line_amount = line.price_unit * (program.discount_percentage / 100)
if discount_line_amount:
taxes = line.tax_id
if self.fiscal_position_id:
taxes = self.fiscal_position_id.map_tax(taxes)
reward_dict[line.tax_id] = {
'name': _("Discount: ") + program.name,
'product_id': program.discount_line_product_id.id,
'price_unit': - discount_line_amount,
'product_uom_qty': 1.0,
'product_uom': program.discount_line_product_id.uom_id.id,
'is_reward_line': True,
'tax_id': [(4, tax.id, False) for tax in taxes],
}
elif program.discount_apply_on in ['specific_product', 'on_order']:
if program.discount_apply_on == 'specific_product':
# We should not exclude reward line that offer this product since we need to offer only the discount on the real paid product (regular product - free product)
free_product_lines = self.env['sale.coupon.program'].search([('reward_type', '=', 'product'), ('reward_product_id', '=', program.discount_specific_product_id.id)]).mapped('discount_line_product_id')
lines = lines.filtered(lambda x: x.product_id == program.discount_specific_product_id or x.product_id in free_product_lines)
for line in lines:
discount_line_amount = self._get_reward_values_discount_percentage_per_line(program, line)
if discount_line_amount:
if line.tax_id in reward_dict:
reward_dict[line.tax_id]['price_unit'] -= discount_line_amount
else:
taxes = line.tax_id
if self.fiscal_position_id:
taxes = self.fiscal_position_id.map_tax(taxes)
tax_name = ""
if len(taxes) == 1:
tax_name = " - " + _("On product with following tax: ") + ', '.join(taxes.mapped('name'))
elif len(taxes) > 1:
tax_name = " - " + _("On product with following taxes: ") + ', '.join(taxes.mapped('name'))
reward_dict[line.tax_id] = {
'name': _("Discount: ") + program.name + tax_name,
'product_id': program.discount_line_product_id.id,
'price_unit': - discount_line_amount,
'product_uom_qty': 1.0,
'product_uom': program.discount_line_product_id.uom_id.id,
'is_reward_line': True,
'tax_id': [(4, tax.id, False) for tax in taxes],
}
# If there is a max amount for discount, we might have to limit some discount lines or completely remove some lines
max_amount = program._compute_program_amount('discount_max_amount', self.currency_id)
if max_amount > 0:
amount_already_given = 0
for val in list(reward_dict):
amount_to_discount = amount_already_given + reward_dict[val]["price_unit"]
if abs(amount_to_discount) > max_amount:
reward_dict[val]["price_unit"] = - (max_amount - abs(amount_already_given))
add_name = formatLang(self.env, max_amount, currency_obj=self.currency_id)
reward_dict[val]["name"] += "( " + _("limited to ") + add_name + ")"
amount_already_given += reward_dict[val]["price_unit"]
if reward_dict[val]["price_unit"] == 0:
del reward_dict[val]
return reward_dict.values()
def _get_reward_line_values(self, program):
self.ensure_one()
if program.reward_type == 'discount':
return self._get_reward_values_discount(program)
elif program.reward_type == 'product':
return self._get_reward_values_product(program)
return [self._get_reward_values_product(program)]
def _create_reward_line(self, programs):
self.ensure_one()
for program in programs:
self.write({'order_line': [(0, False, self._get_reward_line_values(program))]})
def _create_reward_line(self, program):
self.write({'order_line': [(0, False, value) for value in self._get_reward_line_values(program)]})
def _create_reward_coupon(self, program):
coupon = self.env['sale.coupon'].create({
@@ -164,17 +214,21 @@ class SaleOrder(models.Model):
'discount_line_product_id': program.discount_line_product_id.id
})
self.generated_coupon_ids |= coupon
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
})
self.message_post(body=body, subject=subject, subtype='mail.mt_comment', partner_ids=[(4, self.partner_id.id)])
return coupon
def _send_reward_coupon_mail(self):
self.ensure_one()
for coupon in self.generated_coupon_ids:
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
})
self.message_post(body=body, subject=subject, subtype='mail.mt_comment', partner_ids=[(4, self.partner_id.id)])
def _get_applicable_programs(self):
"""
This method is used to return the valid applicable programs on given order.
@@ -182,7 +236,6 @@ class SaleOrder(models.Model):
"""
self.ensure_one()
programs = self.env['sale.coupon.program'].search([
('active', '=', True)
])._filter_programs_from_common_rules(self)
if self.promo_code:
programs._filter_promo_programs_with_code(self)
@@ -191,12 +244,15 @@ class SaleOrder(models.Model):
def _get_applicable_no_code_promo_program(self):
self.ensure_one()
programs = self.env['sale.coupon.program'].search([
('active', '=', True),
('promo_code_usage', '=', 'no_code_needed'),
# ('promo_applicability', '=', 'on_current_order')
])._filter_programs_from_common_rules(self)
return programs
def _get_applied_coupon_program_coming_from_another_so(self):
self.ensure_one()
programs = self.applied_coupon_ids.mapped('program_id')._filter_programs_from_common_rules(self, True)
return programs
def _create_new_no_code_promo_reward_lines(self):
'''Apply new programs that are applicable'''
self.ensure_one()
@@ -204,47 +260,85 @@ class SaleOrder(models.Model):
programs = order._get_applicable_no_code_promo_program()
for program in programs:
error_status = program._check_promo_code(order, False)
if not error_status.get('error') and program.promo_applicability == 'on_next_order':
order._create_reward_coupon(program)
order.no_code_promo_program_ids |= program
elif not error_status.get('error') and program.discount_line_product_id.id not in self.order_line.mapped('product_id').ids:
self.write({'order_line': [(0, False, order._get_reward_line_values(program))]})
if not error_status.get('error'):
if program.promo_applicability == 'on_next_order':
order._create_reward_coupon(program)
elif program.discount_line_product_id.id not in self.order_line.mapped('product_id').ids:
self.write({'order_line': [(0, False, value) for value in self._get_reward_line_values(program)]})
order.no_code_promo_program_ids |= program
def _update_existing_reward_lines(self):
'''Update values for already applied rewards'''
self.ensure_one()
order = self
applied_programs = order.no_code_promo_program_ids + \
order.applied_coupon_ids.mapped('program_id') + \
order.code_promo_program_id
applied_programs = order._get_applied_programs_with_rewards_on_current_order()
for program in applied_programs:
values = order._get_reward_line_values(program)
lines = order.order_line.filtered(lambda line: line.product_id == program.discount_line_product_id)
# Remove reward line if price or qty equal to 0
if values['product_uom_qty'] and values['price_unit']:
order.write({'order_line': [(1, line.id, values) for line in lines]})
else:
if program.reward_type != 'free_shipping':
order.write({'order_line': [(2, line.id, False) for line in lines]})
else:
values.update(price_unit=0.0)
order.write({'order_line': [(1, line.id, values) for line in lines]})
lines_to_remove = lines
# Values is what discount lines should really be, lines is what we got in the SO at the moment
# 1. If values & lines match, we should update the line (or delete it if no qty or price?)
# 2. If the value is not in the lines, we should add it
# 3. if the lines contains a tax not in value, we should remove it
for value in values:
value_found = False
for line in lines:
# Case 1.
if not len(set(line.tax_id.mapped('id')).symmetric_difference(set([v[1] for v in value['tax_id']]))):
value_found = True
# Working on Case 3.
lines_to_remove -= line
# Check commit 6bb42904a03 for next if/else
# Remove reward line if price or qty equal to 0
if value['product_uom_qty'] and value['price_unit']:
order.write({'order_line': [(1, line.id, value)]})
else:
if program.reward_type != 'free_shipping':
lines_to_remove += line
else:
value.update(price_unit=0.0)
order.write({'order_line': [(1, line.id, value)]})
continue
# Case 2.
if not value_found:
order.write({'order_line': [(0, False, value)]})
# Case 3.
lines_to_remove.unlink()
def _remove_invalid_reward_lines(self):
'''Unlink reward order lines that are not applicable anymore'''
invalid_lines = self.env['sale.order.line']
for order in self:
new_applicable_programs = order._get_applicable_no_code_promo_program() + order._get_applicable_programs()
old_applicable_programs = order.no_code_promo_program_ids + order.applied_coupon_ids.mapped('program_id') + order.code_promo_program_id
programs_to_remove = old_applicable_programs - new_applicable_programs
products_to_remove = (programs_to_remove).mapped('discount_line_product_id')
self.generated_coupon_ids.filtered(lambda coupon: coupon.program_id.discount_line_product_id.id in products_to_remove.ids).write({'state': 'expired'})
order.no_code_promo_program_ids -= programs_to_remove
order.code_promo_program_id -= programs_to_remove
order.applied_coupon_ids -= order.applied_coupon_ids.filtered(lambda coupon: coupon.program_id in programs_to_remove)
invalid_lines |= order.order_line.filtered(lambda line: line.product_id.id in products_to_remove.ids)
order.write({'order_line': [(2, line.id, False) for line in invalid_lines]})
""" Find programs & coupons that are not applicable anymore.
It will then unlink the related reward order lines.
It will also unset the order's fields that are storing
the applied coupons & programs.
Note: It will also remove a reward line coming from an archive program.
"""
self.ensure_one()
order = self
applicable_programs = order._get_applicable_no_code_promo_program() + order._get_applicable_programs() + order._get_applied_coupon_program_coming_from_another_so()
applied_programs = order._get_applied_programs_with_rewards_on_current_order()
programs_to_remove = applied_programs - applicable_programs
products_to_remove = programs_to_remove.mapped('discount_line_product_id')
# delete reward line coming from an archived coupon (it will never be updated/removed when recomputing the order)
invalid_lines = order.order_line.filtered(lambda line: line.is_reward_line and line.product_id.id not in (applied_programs).mapped('discount_line_product_id').ids)
self.generated_coupon_ids.filtered(lambda coupon: coupon.program_id.discount_line_product_id.id in products_to_remove.ids).write({'state': 'expired'})
order.no_code_promo_program_ids -= programs_to_remove
order.code_promo_program_id -= programs_to_remove
order.applied_coupon_ids -= order.applied_coupon_ids.filtered(lambda coupon: coupon.program_id in programs_to_remove)
invalid_lines += order.order_line.filtered(lambda line: line.product_id.id in products_to_remove.ids)
order.write({'order_line': [(2, line.id, False) for line in invalid_lines]})
def _get_applied_programs_with_rewards_on_current_order(self):
# Need to add filter on current order. Indeed, it has always been calculating reward line even if on next order (which is useless and do calculation for nothing)
# This problem could not be noticed since it would only update or delete existing lines related to that program, it would not find the line to update since not in the order
# But now if we dont find the reward line in the order, we add it (since we can now have multiple line per program in case of discount on different vat), thus the bug
# mentionned ahead will be seen now
return self.no_code_promo_program_ids.filtered(lambda p: p.promo_applicability == 'on_current_order') + \
self.applied_coupon_ids.mapped('program_id') + \
self.code_promo_program_id.filtered(lambda p: p.promo_applicability == 'on_current_order')
class SaleOrderLine(models.Model):
_inherit = "sale.order.line"
@@ -259,10 +353,11 @@ class SaleOrderLine(models.Model):
)
coupons_to_reactivate.write({'state': 'new'})
line.order_id.applied_coupon_ids -= coupons_to_reactivate
# Remove the program from the order in the case it's still valid
for line in self:
# Remove the program from the order if the deleted line is the reward line of the program
# And if there is no other lines from this program (It's the case when discount is split per different taxes)
related_program = self.env['sale.coupon.program'].search([('discount_line_product_id', '=', line.product_id.id)])
if related_program:
related_program_lines = line.order_id.order_line.filtered(lambda l: l.product_id.id == related_program.discount_line_product_id.id) - line
if related_program and not related_program_lines:
line.order_id.no_code_promo_program_ids -= related_program
line.order_id.code_promo_program_id -= related_program
return super(SaleOrderLine, self).unlink()
+8 -6
View File
@@ -29,13 +29,15 @@ class SaleCouponApplyCode(models.TransientModel):
error_status = program._check_promo_code(order, coupon_code)
if not error_status:
if program.promo_applicability == 'on_next_order':
coupon = order._create_reward_coupon(program)
return {
'generated_coupon': {
'reward': coupon.program_id.discount_line_product_id.name,
'code': coupon.code,
# Avoid creating the coupon if it already exist
if program.discount_line_product_id.id not in order.generated_coupon_ids.filtered(lambda coupon: coupon.state in ['new', 'reserved']).mapped('discount_line_product_id').ids:
coupon = order._create_reward_coupon(program)
return {
'generated_coupon': {
'reward': coupon.program_id.discount_line_product_id.name,
'code': coupon.code,
}
}
}
else: # The program is applied on this order
order._create_reward_line(program)
order.code_promo_program_id = program
+1 -1
View File
@@ -11,7 +11,7 @@ class SaleCouponGenerate(models.TransientModel):
('nbr_coupon', 'Number of Coupons'),
('nbr_customer', 'Number of Selected Customers')
], default='nbr_coupon')
partners_domain = fields.Char(string="Customer")
partners_domain = fields.Char(string="Customer", default='[]')
partner_ids = fields.Many2many('res.partner', string="Related Partners", compute='_compute_partner_ids')
@api.multi