diff --git a/sale_loyalty_limit/README.rst b/sale_loyalty_limit/README.rst new file mode 100644 index 00000000..8c4a0ad9 --- /dev/null +++ b/sale_loyalty_limit/README.rst @@ -0,0 +1,126 @@ +============ +Coupon Limit +============ + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:7ee02c35cf9a86ded5ad53c98fdab3dc5f7b78725d4177bfc9918188e8c7351f + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png + :target: https://odoo-community.org/page/development-status + :alt: Production/Stable +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fsale--promotion-lightgray.png?logo=github + :target: https://github.com/OCA/sale-promotion/tree/16.0/sale_loyalty_limit + :alt: OCA/sale-promotion +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/sale-promotion-16-0/sale-promotion-16-0-sale_loyalty_limit + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/sale-promotion&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module allows to configure a limit on the times a promotion can be applied. Two +limits can be configured: customer and salesman. Those limits apply to either programs +or coupons. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +To configure customer limits: + +#. Go to *Sales > Products > Discount & Loyalty* and select or create a new one. +#. Set the *Maximum Customer Applications* to the number of times a program can be used + by a customer. + +NOTE: The customer limit is applied at commercial entity level, not for each contact +inside the entity. + +To configure salesmen limits: + +#. Go to *Sales > Products > Discount & Loyalty* and select or create a new one. +#. Add salesmen to the *Salesmen Limits* list and their maximum number of applications. +#. You can add different limits to different salesmen groups. +#. If you want to constrain the use of the promotion to the salesmen list, set the + option *Strict limit* on, so any other salesman won't be able to apply the promotion. + +Usage +===== + +Once the program limits are configured, apply the programs as usual in your sale orders. + +Once the limit for a customer or a salesman is reached, if we try to apply a promotion: + +- A code promotion will raise an error. +- A program with no code won't be applied. +- A coupon belonging to a limited program will raise an error. +- A promotion applied on the next order won't generate the coupon. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Tecnativa + +Contributors +~~~~~~~~~~~~ + +* `Tecnativa `_: + + * Pedro M. Baeza + * David Vidal + * Pilar Vargas + +* `Akretion `_: + + * Florian Mounier + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-chienandalu| image:: https://github.com/chienandalu.png?size=40px + :target: https://github.com/chienandalu + :alt: chienandalu + +Current `maintainer `__: + +|maintainer-chienandalu| + +This module is part of the `OCA/sale-promotion `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/sale_loyalty_limit/__init__.py b/sale_loyalty_limit/__init__.py new file mode 100644 index 00000000..0650744f --- /dev/null +++ b/sale_loyalty_limit/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/sale_loyalty_limit/__manifest__.py b/sale_loyalty_limit/__manifest__.py new file mode 100644 index 00000000..f1e3dee1 --- /dev/null +++ b/sale_loyalty_limit/__manifest__.py @@ -0,0 +1,19 @@ +# Copyright 2021 Tecnativa - David Vidal +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +{ + "name": "Coupon Limit", + "summary": "Restrict number of promotions per customer or salesman", + "version": "16.0.1.0.0", + "development_status": "Production/Stable", + "category": "Sale", + "website": "https://github.com/OCA/sale-promotion", + "author": "Tecnativa, Odoo Community Association (OCA)", + "maintainers": ["chienandalu"], + "license": "AGPL-3", + "depends": [ + "loyalty_limit", + "sale_commercial_partner", + "sale_loyalty_order_line_link", + ], + "data": ["security/ir.model.access.csv"], +} diff --git a/sale_loyalty_limit/i18n/it.po b/sale_loyalty_limit/i18n/it.po new file mode 100644 index 00000000..f22dc285 --- /dev/null +++ b/sale_loyalty_limit/i18n/it.po @@ -0,0 +1,85 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * sale_coupon_limit +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 15.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2023-07-10 10:09+0000\n" +"Last-Translator: Francesco Foresti \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.17\n" + +#. module: sale_coupon_limit +#: model:ir.model,name:sale_coupon_limit.model_coupon_coupon +msgid "Coupon" +msgstr "Buono sconto" + +#. module: sale_coupon_limit +#: model:ir.model,name:sale_coupon_limit.model_coupon_rule +msgid "Coupon Rule" +msgstr "Regola buono sconto" + +#. module: sale_coupon_limit +#: model:ir.model,name:sale_coupon_limit.model_coupon_rule_salesmen_limit +msgid "Coupon Rule Salesmen limits" +msgstr "Limiti regola buono sconto addetto alle vendite" + +#. module: sale_coupon_limit +#: model:ir.model,name:sale_coupon_limit.model_coupon_program +msgid "Coupon display on a website" +msgstr "Mostra buono sconto su sito web" + +#. module: sale_coupon_limit +#: code:addons/sale_coupon_limit/models/coupon_program.py:0 +#, python-format +msgid "" +"This promo code was already applied %(count)s times for this customer and " +"there's an stablished limit of %(max)s for this promotion." +msgstr "" +"Questo buono sconto è già stato applicato %(count)s volte per questo cliente " +"e il limite massimo è di %(max)s per questa promozione." + +#. module: sale_coupon_limit +#: code:addons/sale_coupon_limit/models/coupon_program.py:0 +#, python-format +msgid "" +"This promo code was already applied %(times)s times for this salesman and " +"there's an stablished limit of %(max)s for this promotion." +msgstr "" +"Questo buono sconto è già stato applicato %(times)s volte per questo addetto " +"alle vendite e il limite massimo è di %(max)s per questa promozione." + +#. module: sale_coupon_limit +#: code:addons/sale_coupon_limit/models/coupon_coupon.py:0 +#: code:addons/sale_coupon_limit/models/coupon_program.py:0 +#, python-format +msgid "This promotion is restricted to the listed salesmen." +msgstr "Questa promozione è riservata all'addetto alle vendite indicato." + +#. module: sale_coupon_limit +#: code:addons/sale_coupon_limit/models/coupon_coupon.py:0 +#: code:addons/sale_coupon_limit/models/coupon_program.py:0 +#, python-format +msgid "" +"This promotion was already applied %(count)s times for this customer and " +"there's an stablished limit of %(max)s." +msgstr "" +"Questa promozione è già stata applicata %(count)s volte per questo cliente e " +"il limite massimo è di %(max)s." + +#. module: sale_coupon_limit +#: code:addons/sale_coupon_limit/models/coupon_coupon.py:0 +#, python-format +msgid "" +"This promotion was already applied %(times)s times for this salesman and " +"there's an stablished limit of %(max)s." +msgstr "" +"Questa promozione è già stata applicata %(times)s volte per questo addetto " +"alle vendite e il limite massimo è di %(max)s." diff --git a/sale_loyalty_limit/i18n/sale_coupon_limit.pot b/sale_loyalty_limit/i18n/sale_coupon_limit.pot new file mode 100644 index 00000000..af1fa336 --- /dev/null +++ b/sale_loyalty_limit/i18n/sale_coupon_limit.pot @@ -0,0 +1,74 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * sale_coupon_limit +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 15.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: sale_coupon_limit +#: model:ir.model,name:sale_coupon_limit.model_coupon_coupon +msgid "Coupon" +msgstr "" + +#. module: sale_coupon_limit +#: model:ir.model,name:sale_coupon_limit.model_coupon_rule +msgid "Coupon Rule" +msgstr "" + +#. module: sale_coupon_limit +#: model:ir.model,name:sale_coupon_limit.model_coupon_rule_salesmen_limit +msgid "Coupon Rule Salesmen limits" +msgstr "" + +#. module: sale_coupon_limit +#: model:ir.model,name:sale_coupon_limit.model_coupon_program +msgid "Coupon display on a website" +msgstr "" + +#. module: sale_coupon_limit +#: code:addons/sale_coupon_limit/models/coupon_program.py:0 +#, python-format +msgid "" +"This promo code was already applied %(count)s times for this customer and " +"there's an stablished limit of %(max)s for this promotion." +msgstr "" + +#. module: sale_coupon_limit +#: code:addons/sale_coupon_limit/models/coupon_program.py:0 +#, python-format +msgid "" +"This promo code was already applied %(times)s times for this salesman and " +"there's an stablished limit of %(max)s for this promotion." +msgstr "" + +#. module: sale_coupon_limit +#: code:addons/sale_coupon_limit/models/coupon_coupon.py:0 +#: code:addons/sale_coupon_limit/models/coupon_program.py:0 +#, python-format +msgid "This promotion is restricted to the listed salesmen." +msgstr "" + +#. module: sale_coupon_limit +#: code:addons/sale_coupon_limit/models/coupon_coupon.py:0 +#: code:addons/sale_coupon_limit/models/coupon_program.py:0 +#, python-format +msgid "" +"This promotion was already applied %(count)s times for this customer and " +"there's an stablished limit of %(max)s." +msgstr "" + +#. module: sale_coupon_limit +#: code:addons/sale_coupon_limit/models/coupon_coupon.py:0 +#, python-format +msgid "" +"This promotion was already applied %(times)s times for this salesman and " +"there's an stablished limit of %(max)s." +msgstr "" diff --git a/sale_loyalty_limit/models/__init__.py b/sale_loyalty_limit/models/__init__.py new file mode 100644 index 00000000..f668597f --- /dev/null +++ b/sale_loyalty_limit/models/__init__.py @@ -0,0 +1,2 @@ +from . import loyalty_program +from . import sale_order diff --git a/sale_loyalty_limit/models/loyalty_program.py b/sale_loyalty_limit/models/loyalty_program.py new file mode 100644 index 00000000..bf1168e1 --- /dev/null +++ b/sale_loyalty_limit/models/loyalty_program.py @@ -0,0 +1,51 @@ +# Copyright 2021 Tecnativa - David Vidal +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo import models + + +class LoyaltyProgram(models.Model): + _inherit = "loyalty.program" + + def _compute_salesmen_limit_count(self): + """This count is merely informative""" + res = super()._compute_salesmen_limit_count() + for program in self: + program.salesmen_limit_count = sum( + program.salesmen_limit_ids.mapped("max_salesman_application") + ) + program.salesmen_limit_used_count = sum( + program.salesmen_limit_ids.mapped("times_used") + ) + return res + + +class LoyaltySalesmenLimit(models.Model): + _inherit = "loyalty.salesmen.limit" + + def _compute_times_used(self): + res = super()._compute_times_used() + programs = self.env["loyalty.program"].search_read( + [("id", "in", self.mapped("program_id").ids)], + ["id", "program_type", "coupon_ids", "salesmen_limit_ids"], + ) + for program in programs: + salesmen_limits = self.filtered( + lambda x: x._origin in program["salesmen_limit_ids"] + ) + for salesman_limit in salesmen_limits: + salesman_limit.times_used = len( + self.env["sale.order.line"].read_group( + [ + ("loyalty_program_id", "=", program["id"]), + ( + "order_id.user_id", + "=", + salesman_limit.user_id.id, + ), + ("order_id.state", "!=", "cancel"), + ], + ["order_id"], + ["order_id"], + ) + ) + return res diff --git a/sale_loyalty_limit/models/sale_order.py b/sale_loyalty_limit/models/sale_order.py new file mode 100644 index 00000000..768ad042 --- /dev/null +++ b/sale_loyalty_limit/models/sale_order.py @@ -0,0 +1,92 @@ +from odoo import _, models + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + def _program_check_compute_points(self, programs): + res = super()._program_check_compute_points(programs) + # Iterate through the programs that initially have no errors + for program, result in res.items(): + if result.get("error", False): + continue + # Customer limit rules + if program.max_customer_application: + customer_domain = [ + ("order_line.loyalty_program_id", "=", program.id), + ("id", "!=", self.id), + ( + "commercial_partner_id", + "=", + self.commercial_partner_id.id, + ), + ] + order_count = self.env["sale.order"].search_count(customer_domain) + limit_reached = order_count >= program.max_customer_application + if limit_reached and self.applied_coupon_ids: + res[program] = { + "error": _( + "This promo code was already applied %(count)s times for this " + "customer and there's an stablished limit of %(max)s for this " + "promotion." + ) + % { + "count": order_count, + "max": program.max_customer_application, + } + } + if limit_reached and not self.applied_coupon_ids: + res[program] = { + "error": _( + "This promotion was already applied %(count)s times " + "for this customer and there's an established limit " + "of %(max)s." + ) + % { + "count": order_count, + "max": program.max_customer_application, + } + } + # Salesmen limit rules + salesman_rule = program.salesmen_limit_ids.filtered( + lambda x: x.user_id.id == self.user_id.id + ) + if salesman_rule: + max_rule = salesman_rule.max_salesman_application + # It is necessary to recalculate the number of times it has been used + # omitting the current sell order because when a sell order is confirmed, + # the "_update_programs_and_rewards" method is re-executed which triggers + # the compute method again and in this method the current sell order is + # not excluded. + times_used = len( + self.env["sale.order.line"].read_group( + [ + ("loyalty_program_id", "=", program.id), + ( + "order_id.user_id", + "=", + salesman_rule.user_id.id, + ), + ("order_id.state", "!=", "cancel"), + ("order_id", "!=", self.id), + ], + ["order_id"], + ["order_id"], + ) + ) + if times_used == 0 or times_used < max_rule: + continue + if times_used >= max_rule: + res[program] = { + "error": _( + "This promo code was already applied %(times)s times for this " + "salesman and there's an stablished limit of %(max)s for this " + "promotion." + ) + % {"times": times_used, "max": max_rule} + } + if program.salesmen_strict_limit and not salesman_rule: + res[program] = { + "error": _("This promotion is restricted to the listed salesmen.") + } + return res diff --git a/sale_loyalty_limit/readme/CONFIGURE.rst b/sale_loyalty_limit/readme/CONFIGURE.rst new file mode 100644 index 00000000..df3cbb1f --- /dev/null +++ b/sale_loyalty_limit/readme/CONFIGURE.rst @@ -0,0 +1,16 @@ +To configure customer limits: + +#. Go to *Sales > Products > Discount & Loyalty* and select or create a new one. +#. Set the *Maximum Customer Applications* to the number of times a program can be used + by a customer. + +NOTE: The customer limit is applied at commercial entity level, not for each contact +inside the entity. + +To configure salesmen limits: + +#. Go to *Sales > Products > Discount & Loyalty* and select or create a new one. +#. Add salesmen to the *Salesmen Limits* list and their maximum number of applications. +#. You can add different limits to different salesmen groups. +#. If you want to constrain the use of the promotion to the salesmen list, set the + option *Strict limit* on, so any other salesman won't be able to apply the promotion. diff --git a/sale_loyalty_limit/readme/CONTRIBUTORS.rst b/sale_loyalty_limit/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000..7d6f8b1a --- /dev/null +++ b/sale_loyalty_limit/readme/CONTRIBUTORS.rst @@ -0,0 +1,9 @@ +* `Tecnativa `_: + + * Pedro M. Baeza + * David Vidal + * Pilar Vargas + +* `Akretion `_: + + * Florian Mounier diff --git a/sale_loyalty_limit/readme/DESCRIPTION.rst b/sale_loyalty_limit/readme/DESCRIPTION.rst new file mode 100644 index 00000000..eb1b17bb --- /dev/null +++ b/sale_loyalty_limit/readme/DESCRIPTION.rst @@ -0,0 +1,3 @@ +This module allows to configure a limit on the times a promotion can be applied. Two +limits can be configured: customer and salesman. Those limits apply to either programs +or coupons. diff --git a/sale_loyalty_limit/readme/USAGE.rst b/sale_loyalty_limit/readme/USAGE.rst new file mode 100644 index 00000000..ba5203e5 --- /dev/null +++ b/sale_loyalty_limit/readme/USAGE.rst @@ -0,0 +1,8 @@ +Once the program limits are configured, apply the programs as usual in your sale orders. + +Once the limit for a customer or a salesman is reached, if we try to apply a promotion: + +- A code promotion will raise an error. +- A program with no code won't be applied. +- A coupon belonging to a limited program will raise an error. +- A promotion applied on the next order won't generate the coupon. diff --git a/sale_loyalty_limit/security/ir.model.access.csv b/sale_loyalty_limit/security/ir.model.access.csv new file mode 100644 index 00000000..8b2b58cc --- /dev/null +++ b/sale_loyalty_limit/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_salesmen_limit_salesman,salesman,model_loyalty_salesmen_limit,sales_team.group_sale_salesman,1,0,0,0 +access_salesmen_limit_manager,salesmen_limit manager,model_loyalty_salesmen_limit,sales_team.group_sale_manager,1,1,1,1 diff --git a/sale_loyalty_limit/static/description/icon.png b/sale_loyalty_limit/static/description/icon.png new file mode 100644 index 00000000..07f27584 Binary files /dev/null and b/sale_loyalty_limit/static/description/icon.png differ diff --git a/sale_loyalty_limit/static/description/icon.svg b/sale_loyalty_limit/static/description/icon.svg new file mode 100644 index 00000000..2f5df826 --- /dev/null +++ b/sale_loyalty_limit/static/description/icon.svg @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sale_loyalty_limit/static/description/index.html b/sale_loyalty_limit/static/description/index.html new file mode 100644 index 00000000..27075257 --- /dev/null +++ b/sale_loyalty_limit/static/description/index.html @@ -0,0 +1,466 @@ + + + + + + +Coupon Limit + + + +
+

Coupon Limit

+ + +

Production/Stable License: AGPL-3 OCA/sale-promotion Translate me on Weblate Try me on Runboat

+

This module allows to configure a limit on the times a promotion can be applied. Two +limits can be configured: customer and salesman. Those limits apply to either programs +or coupons.

+

Table of contents

+ +
+

Configuration

+

To configure customer limits:

+
    +
  1. Go to Sales > Products > Discount & Loyalty and select or create a new one.
  2. +
  3. Set the Maximum Customer Applications to the number of times a program can be used +by a customer.
  4. +
+

NOTE: The customer limit is applied at commercial entity level, not for each contact +inside the entity.

+

To configure salesmen limits:

+
    +
  1. Go to Sales > Products > Discount & Loyalty and select or create a new one.
  2. +
  3. Add salesmen to the Salesmen Limits list and their maximum number of applications.
  4. +
  5. You can add different limits to different salesmen groups.
  6. +
  7. If you want to constrain the use of the promotion to the salesmen list, set the +option Strict limit on, so any other salesman won’t be able to apply the promotion.
  8. +
+
+
+

Usage

+

Once the program limits are configured, apply the programs as usual in your sale orders.

+

Once the limit for a customer or a salesman is reached, if we try to apply a promotion:

+
    +
  • A code promotion will raise an error.
  • +
  • A program with no code won’t be applied.
  • +
  • A coupon belonging to a limited program will raise an error.
  • +
  • A promotion applied on the next order won’t generate the coupon.
  • +
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Tecnativa
  • +
+
+
+

Contributors

+
    +
  • Tecnativa:
      +
    • Pedro M. Baeza
    • +
    • David Vidal
    • +
    • Pilar Vargas
    • +
    +
  • +
  • Akretion:
      +
    • Florian Mounier
    • +
    +
  • +
+
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainer:

+

chienandalu

+

This module is part of the OCA/sale-promotion project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/sale_loyalty_limit/tests/__init__.py b/sale_loyalty_limit/tests/__init__.py new file mode 100644 index 00000000..3492c36d --- /dev/null +++ b/sale_loyalty_limit/tests/__init__.py @@ -0,0 +1 @@ +from . import test_sale_loyalty_limit diff --git a/sale_loyalty_limit/tests/test_sale_loyalty_limit.py b/sale_loyalty_limit/tests/test_sale_loyalty_limit.py new file mode 100644 index 00000000..77dba29b --- /dev/null +++ b/sale_loyalty_limit/tests/test_sale_loyalty_limit.py @@ -0,0 +1,280 @@ +# Copyright 2021 Tecnativa - David Vidal +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo.exceptions import UserError, ValidationError +from odoo.tests import Form + +from odoo.addons.loyalty_limit.tests.test_loyalty_limit import LoyaltyLimitCase + + +class TestSaleCouponLimit(LoyaltyLimitCase): + def _action_apply_program(self, sale, program): + sale._update_programs_and_rewards() + wizard = ( + self.env["sale.loyalty.reward.wizard"] + .with_context(active_id=sale) + .create({"selected_reward_id": program.reward_ids.id}) + ) + wizard.action_apply() + + def _create_sale(self, partner, salesman=False): + """Helper method to create sales in the test cases""" + sale_form = Form(self.env["sale.order"]) + sale_form.partner_id = partner + if salesman: + sale_form.user_id = salesman + with sale_form.order_line.new() as line_form: + line_form.product_id = self.product_a + line_form.product_uom_qty = 1 + return sale_form.save() + + def _apply_promo_code(self, order, code, no_reward_fail=True): + status = order._try_apply_code(code) + if "error" in status: + raise ValidationError(status["error"]) + coupons = self.env["loyalty.card"] + rewards = self.env["loyalty.reward"] + for coupon, coupon_rewards in status.items(): + coupons |= coupon + rewards |= coupon_rewards + if len(coupons) == 1 and len(rewards) == 1: + status = order._apply_program_reward(rewards, coupons) + + def test_01_program_no_code_customer_limit(self): + """A program with no code and customer application limit won't be applied + once the limit is reached""" + sale_1 = self._create_sale(self.partner_1) + # In the case definition the program is no code, so there's nothing else to + # setup. + self._action_apply_program(sale_1, self.promotion_with_customer_limit) + self.assertTrue(bool(sale_1.order_line.filtered("is_reward_line"))) + # The limit is 2, so the promotion can be placed in a second order + sale_2 = self._create_sale(self.partner_1) + self._action_apply_program(sale_2, self.promotion_with_customer_limit) + self.assertTrue(bool(sale_2.order_line.filtered("is_reward_line"))) + # As we reach the limit, no discount will be applied + sale_3 = self._create_sale(self.partner_1) + with self.assertRaises(ValidationError): + self._action_apply_program(sale_3, self.promotion_with_customer_limit) + self.assertFalse(bool(sale_3.order_line.filtered("is_reward_line"))) + # However other partners can still enjoy the promotion + sale_4 = self._create_sale(self.partner_2) + self._action_apply_program(sale_4, self.promotion_with_customer_limit) + self.assertTrue(bool(sale_4.order_line.filtered("is_reward_line"))) + + def test_02_program_promo_code_customer_limit(self): + """A program with code and customer application limit will raise an error when + # such limit is reached for a customer""" + self.promotion_with_customer_limit.program_type = "promo_code" + self.promotion_with_customer_limit.rule_ids.code = "TEST-SALE-COUPON-LIMIT" + self.promotion_with_customer_limit.reward_ids.discount_product_ids = ( + self.product_a + ) + # We apply it once for partner 1... + sale_1 = self._create_sale(self.partner_1) + self._apply_promo_code(sale_1, "TEST-SALE-COUPON-LIMIT") + self.assertTrue(bool(sale_1.order_line.filtered("is_reward_line"))) + # We apply it twice for partner 1... + sale_2 = self._create_sale(self.partner_1) + self._apply_promo_code(sale_2, "TEST-SALE-COUPON-LIMIT") + self.assertTrue(bool(sale_2.order_line.filtered("is_reward_line"))) + # As we reach the limit we can't apply this code anymore + sale_3 = self._create_sale(self.partner_1) + with self.assertRaises(ValidationError): + self._apply_promo_code(sale_3, "TEST-SALE-COUPON-LIMIT") + # We can still apply the promotion to other partners + sale_4 = self._create_sale(self.partner_2) + self._apply_promo_code(sale_4, "TEST-SALE-COUPON-LIMIT") + self.assertTrue(bool(sale_4.order_line.filtered("is_reward_line"))) + + def test_03_coupon_code_customer_limit(self): + """When a coupon of a customer limited program is applied, an error will raise + when the limit is reached for a given customer.""" + # Create coupons for the coupon program + self.env["loyalty.generate.wizard"].with_context( + active_id=self.coupon_program_with_customer_limit.id + ).create({"coupon_qty": 3}).generate_coupons() + coupon1, coupon2, coupon3 = self.coupon_program_with_customer_limit.coupon_ids + sale_1 = self._create_sale(self.partner_1) + self._apply_promo_code(sale_1, coupon1.code) + self.assertTrue(bool(sale_1.order_line.filtered("is_reward_line"))) + # We apply another coupon for partner 1... + sale_2 = self._create_sale(self.partner_1) + self._apply_promo_code(sale_2, coupon2.code) + self.assertTrue(bool(sale_2.order_line.filtered("is_reward_line"))) + # No coupon is applied. In Backend UI a Warning popup is raised + sale_3 = self._create_sale(self.partner_1) + with self.assertRaises(ValidationError): + self._apply_promo_code(sale_3, coupon3.code) + # We can still apply the coupon to other partners + sale_4 = self._create_sale(self.partner_2) + self._apply_promo_code(sale_4, coupon3.code) + self.assertTrue(bool(sale_4.order_line.filtered("is_reward_line"))) + + def test_04_coupon_code_next_order_customer_limit(self): + """Coupons should not be generated for next orders above the customer limit""" + # The first order generates the coupon for the next one + sale_1 = self._create_sale(self.partner_1) + sale_1._update_programs_and_rewards() + sale_1.action_confirm() + coupon_1 = self.next_order_coupon_with_customer_limit.coupon_ids + self.assertTrue(bool(coupon_1.points > 0), "A valid coupon must be generated") + # Apply it and generate another coupon in a second sale and apply it again + self._apply_promo_code(self._create_sale(self.partner_1), coupon_1.code) + sale_2 = self._create_sale(self.partner_1) + sale_2._update_programs_and_rewards() + sale_2.action_confirm() + coupon_2 = self.next_order_coupon_with_customer_limit.coupon_ids[1] + self.assertTrue( + bool(coupon_2.points > 0), "A second valid coupon must be generated" + ) + self._apply_promo_code(self._create_sale(self.partner_1), coupon_2.code) + # Finally, we can't generate more coupons from this promotion for this partner + sale_3 = self._create_sale(self.partner_1) + sale_3._update_programs_and_rewards() + sale_3.action_confirm() + self.assertFalse( + bool(len(self.next_order_coupon_with_customer_limit.coupon_ids) > 2), + "No more coupons should be generated for this customer and program", + ) + # Other customers can still use the program + sale_4 = self._create_sale(self.partner_2) + sale_4._update_programs_and_rewards() + sale_4.action_confirm() + coupon_4 = self.next_order_coupon_with_customer_limit.coupon_ids[2] + self.assertTrue( + bool(coupon_4.points > 0), + "A valid coupon should be generated for this customer", + ) + + def test_05_program_no_code_salesman_limit(self): + """A program with no code and salesman application limit won't be applied + once the limit is reached""" + # Avoid other salesmen using this program + self.promotion_with_salesman_limit.salesmen_strict_limit = True + # Place the first order of salesman 1 + sale_1 = self._create_sale(self.partner_1, self.salesman_1) + self._action_apply_program(sale_1, self.promotion_with_salesman_limit) + self.assertTrue(bool(sale_1.order_line.filtered("is_reward_line"))) + # The limit is 2, so the promotion can be placed in a second order + sale_2 = self._create_sale(self.partner_1, self.salesman_1) + self._action_apply_program(sale_2, self.promotion_with_salesman_limit) + self.assertTrue(bool(sale_2.order_line.filtered("is_reward_line"))) + # As we reach the limit, no discount will be applied + sale_3 = self._create_sale(self.partner_1, self.salesman_1) + with self.assertRaises(ValidationError): + self._action_apply_program(sale_3, self.promotion_with_salesman_limit) + self.assertFalse(bool(sale_3.order_line.filtered("is_reward_line"))) + # However the other salesman can still enjoy the promotion + sale_4 = self._create_sale(self.partner_1, self.salesman_2) + self._action_apply_program(sale_4, self.promotion_with_salesman_limit) + self.assertTrue(bool(sale_4.order_line.filtered("is_reward_line"))) + # As only the salesmen in the list can use the promotion, no other can apply it + sale_5 = self._create_sale(self.partner_1) + with self.assertRaises(ValidationError): + self._action_apply_program(sale_5, self.promotion_with_salesman_limit) + self.assertFalse(bool(sale_5.order_line.filtered("is_reward_line"))) + + def test_06_program_promo_code_salesman_limit(self): + """A program with code and salesman application limit will raise an error when + such limit is reached for a salesman in the list""" + self.promotion_with_salesman_limit.program_type = "promo_code" + self.promotion_with_salesman_limit.trigger = "with_code" + self.promotion_with_salesman_limit.applies_on = "current" + self.promotion_with_salesman_limit.salesmen_strict_limit = True + self.promotion_with_salesman_limit.rule_ids.mode = "with_code" + self.promotion_with_salesman_limit.rule_ids.code = "TEST-SALE-COUPON-LIMIT" + self.promotion_with_salesman_limit.reward_ids.discount_product_ids = ( + self.product_a + ) + # First salesman_1 order... + sale_1 = self._create_sale(self.partner_1, self.salesman_1) + self._apply_promo_code(sale_1, "TEST-SALE-COUPON-LIMIT") + self.assertTrue(bool(sale_1.order_line.filtered("is_reward_line"))) + # Second salesman_1 order... + sale_2 = self._create_sale(self.partner_1, self.salesman_1) + self._apply_promo_code(sale_2, "TEST-SALE-COUPON-LIMIT") + self.assertTrue(bool(sale_2.order_line.filtered("is_reward_line"))) + # As we reach the limit we can't apply this code anymore + sale_3 = self._create_sale(self.partner_1, self.salesman_1) + with self.assertRaises(ValidationError): + self._apply_promo_code(sale_3, "TEST-SALE-COUPON-LIMIT") + # We can still apply the promotion with the other salesman + sale_4 = self._create_sale(self.partner_1, self.salesman_2) + self._apply_promo_code(sale_4, "TEST-SALE-COUPON-LIMIT") + self.assertTrue(bool(sale_4.order_line.filtered("is_reward_line"))) + # But only the salesmen in the list can use the promotion, no other can apply it + sale_5 = self._create_sale(self.partner_1) + with self.assertRaises(ValidationError): + self._apply_promo_code(sale_5, "TEST-SALE-COUPON-LIMIT") + + def test_07_coupon_code_salesman_limit(self): + """When a coupon of a salesmen limited program is applied, an error will raise + when the limit is reached for a given salesman.""" + # Avoid other salesmen using this program + self.coupon_program_with_salesman_limit.salesmen_strict_limit = True + # Create coupons for the coupon program + self.env["loyalty.generate.wizard"].with_context( + active_id=self.coupon_program_with_salesman_limit.id + ).create({"coupon_qty": 3}).generate_coupons() + coupon1, coupon2, coupon3 = self.coupon_program_with_salesman_limit.coupon_ids + # We apply one coupon with salesman_1... + sale_1 = self._create_sale(self.partner_1, self.salesman_1) + self._apply_promo_code(sale_1, coupon1.code) + self.assertTrue(bool(sale_1.order_line.filtered("is_reward_line"))) + # We apply another coupon with salesman_1... + sale_2 = self._create_sale(self.partner_1, self.salesman_1) + self._apply_promo_code(sale_2, coupon2.code) + self.assertTrue(bool(sale_2.order_line.filtered("is_reward_line"))) + # An error raises as we reach the limit + sale_3 = self._create_sale(self.partner_1, self.salesman_1) + with self.assertRaises(UserError): + self._apply_promo_code(sale_3, coupon3.code) + # We can't apply with salesmen not in the list either + sale_4 = self._create_sale(self.partner_1) + with self.assertRaises(UserError): + self._apply_promo_code(sale_4, coupon3.code) + # We can still apply the coupon with salesman_2 + sale_5 = self._create_sale(self.partner_1, self.salesman_2) + self._apply_promo_code(sale_5, coupon3.code) + self.assertTrue(bool(sale_5.order_line.filtered("is_reward_line"))) + + def test_08_coupon_code_next_order_salesmen_limit(self): + """Coupons should not be generated for next orders above the salesman limit""" + # Avoid other salesmen using this program + self.next_order_coupon_with_salesman_limit.salesmen_strict_limit = True + # The first order generates the coupon for the next one + sale_1 = self._create_sale(self.partner_1, self.salesman_1) + sale_1._update_programs_and_rewards() + sale_1.action_confirm() + coupon_1 = self.next_order_coupon_with_salesman_limit.coupon_ids[0] + self.assertTrue(bool(coupon_1.points > 0), "A valid coupon must be generated") + # Apply it and generate another coupon in a second sale and apply it again + self._apply_promo_code( + self._create_sale(self.partner_1, self.salesman_1), coupon_1.code + ) + sale_2 = self._create_sale(self.partner_1, self.salesman_1) + sale_2._update_programs_and_rewards() + sale_2.action_confirm() + coupon_2 = self.next_order_coupon_with_salesman_limit.coupon_ids[1] + self.assertTrue( + bool(coupon_2.points > 0), "A second valid coupon must be generated" + ) + self._apply_promo_code( + self._create_sale(self.partner_1, self.salesman_1), coupon_2.code + ) + # Finally, we can't generate more coupons from this promotion for this partner + sale_3 = self._create_sale(self.partner_1, self.salesman_1) + sale_3._update_programs_and_rewards() + self.assertFalse( + bool(len(self.next_order_coupon_with_salesman_limit.coupon_ids) > 2), + "No more coupons should be generated for this salesman and program", + ) + # Other customers can still use the program + sale_4 = self._create_sale(self.partner_1, self.salesman_2) + sale_4._update_programs_and_rewards() + sale_4.action_confirm() + coupon_4 = self.next_order_coupon_with_salesman_limit.coupon_ids[2] + self.assertTrue( + bool(coupon_4.points > 0), + "A valid coupon should be generated for this customer", + ) diff --git a/setup/sale_loyalty_limit/odoo/addons/sale_loyalty_limit b/setup/sale_loyalty_limit/odoo/addons/sale_loyalty_limit new file mode 120000 index 00000000..411e0e26 --- /dev/null +++ b/setup/sale_loyalty_limit/odoo/addons/sale_loyalty_limit @@ -0,0 +1 @@ +../../../../sale_loyalty_limit \ No newline at end of file diff --git a/setup/sale_loyalty_limit/setup.py b/setup/sale_loyalty_limit/setup.py new file mode 100644 index 00000000..28c57bb6 --- /dev/null +++ b/setup/sale_loyalty_limit/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)