From 1a29aa7092cbc33ba2b6c12af9a184c68dc2d2e3 Mon Sep 17 00:00:00 2001 From: Mmequignon Date: Fri, 25 Oct 2024 12:36:41 +0200 Subject: [PATCH] sale_loyalty_criteria_order_based: Adapt to 17.0 odoo changes --- sale_loyalty_criteria_order_based/README.rst | 31 ++++--- .../__manifest__.py | 7 +- sale_loyalty_criteria_order_based/i18n/es.po | 58 ------------- sale_loyalty_criteria_order_based/i18n/it.po | 69 ---------------- .../i18n/sale_coupon_criteria_order_based.pot | 53 ------------ .../models/__init__.py | 6 +- .../models/coupon_coupon.py | 15 ---- .../models/coupon_program.py | 59 ------------- .../models/coupon_rule.py | 13 --- .../models/loyalty_program.py | 34 ++++++++ .../models/loyalty_rule.py | 35 ++++++++ .../models/sale_order.py | 28 +++++++ .../readme/CONTRIBUTORS.md | 1 + .../readme/DESCRIPTION.md | 5 +- .../readme/USAGE.md | 12 +-- .../static/description/index.html | 28 ++++--- .../tests/__init__.py | 2 +- .../tests/common.py | 48 +++++++++++ .../tests/test_methods.py | 74 +++++++++++++++++ .../tests/test_reward_order_criteria.py | 76 +++++++++++++++++ .../test_sale_coupon_criteria_order_based.py | 82 ------------------- .../views/coupon_program_view.xml | 20 ----- .../views/loyalty_program.xml | 21 +++++ .../views/loyalty_rule.xml | 21 +++++ 24 files changed, 391 insertions(+), 407 deletions(-) delete mode 100644 sale_loyalty_criteria_order_based/i18n/es.po delete mode 100644 sale_loyalty_criteria_order_based/i18n/it.po delete mode 100644 sale_loyalty_criteria_order_based/i18n/sale_coupon_criteria_order_based.pot delete mode 100644 sale_loyalty_criteria_order_based/models/coupon_coupon.py delete mode 100644 sale_loyalty_criteria_order_based/models/coupon_program.py delete mode 100644 sale_loyalty_criteria_order_based/models/coupon_rule.py create mode 100644 sale_loyalty_criteria_order_based/models/loyalty_program.py create mode 100644 sale_loyalty_criteria_order_based/models/loyalty_rule.py create mode 100644 sale_loyalty_criteria_order_based/models/sale_order.py create mode 100644 sale_loyalty_criteria_order_based/tests/common.py create mode 100644 sale_loyalty_criteria_order_based/tests/test_methods.py create mode 100644 sale_loyalty_criteria_order_based/tests/test_reward_order_criteria.py delete mode 100644 sale_loyalty_criteria_order_based/tests/test_sale_coupon_criteria_order_based.py delete mode 100644 sale_loyalty_criteria_order_based/views/coupon_program_view.xml create mode 100644 sale_loyalty_criteria_order_based/views/loyalty_program.xml create mode 100644 sale_loyalty_criteria_order_based/views/loyalty_rule.xml diff --git a/sale_loyalty_criteria_order_based/README.rst b/sale_loyalty_criteria_order_based/README.rst index 533396f63..177c4050f 100644 --- a/sale_loyalty_criteria_order_based/README.rst +++ b/sale_loyalty_criteria_order_based/README.rst @@ -17,10 +17,10 @@ Sales Coupon based on Sales Order values :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/17.0/sale_coupon_criteria_order_based + :target: https://github.com/OCA/sale-promotion/tree/17.0/sale_loyalty_criteria_order_based :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-17-0/sale-promotion-17-0-sale_coupon_criteria_order_based + :target: https://translation.odoo-community.org/projects/sale-promotion-17-0/sale-promotion-17-0-sale_loyalty_criteria_order_based :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=17.0 @@ -28,10 +28,10 @@ Sales Coupon based on Sales Order values |badge1| |badge2| |badge3| |badge4| |badge5| -This module adds an option to configure your coupon conditions based on -the Sales Order values. +This module adds an option to configure your **Loyalty Program** and add +additional constraints based on **Sales Order** values. -Example: "If Sales Team is 'Europe' apply the following discount' +Example: "If Sales Team is 'Europe' grant one point for the program" **Table of contents** @@ -41,13 +41,18 @@ Example: "If Sales Team is 'Europe' apply the following discount' Usage ===== -Open or create a new Promotion Program in the **Sales/Products/Promotion -Programs** menu. +Open or create a new Loyalty Program in the **Sales/Products/Discount & +Loyalty** menu. -In *Conditions* section the following new option is available: +There's two ways to add a condition based on orders: -- **Based on Order** configure an order-based domain to apply the - promotion +- On the program itself, allowing to define ``global`` conditions (valid + for all rules) + → Find and edit the **Based on Order** field on the **Loyalty + Program** form view. +- On the rule, allowing to define conditions valid for a given rule only + → Open or create a new **Loyalty Rule**, and edit the **Based on + Order** field. Bug Tracker =========== @@ -55,7 +60,7 @@ 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 `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -77,6 +82,8 @@ Contributors - Pilar Vargas +- MmeQuignon + Maintainers ----------- @@ -90,6 +97,6 @@ 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. -This module is part of the `OCA/sale-promotion `_ project on GitHub. +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_criteria_order_based/__manifest__.py b/sale_loyalty_criteria_order_based/__manifest__.py index a6ad0f3e4..40541992c 100644 --- a/sale_loyalty_criteria_order_based/__manifest__.py +++ b/sale_loyalty_criteria_order_based/__manifest__.py @@ -10,6 +10,9 @@ "license": "AGPL-3", "category": "Sale", "website": "https://github.com/OCA/sale-promotion", - "depends": ["sale_coupon"], - "data": ["views/coupon_program_view.xml"], + "depends": ["sale_loyalty"], + "data": [ + "views/loyalty_program.xml", + "views/loyalty_rule.xml", + ], } diff --git a/sale_loyalty_criteria_order_based/i18n/es.po b/sale_loyalty_criteria_order_based/i18n/es.po deleted file mode 100644 index 5044a0b3b..000000000 --- a/sale_loyalty_criteria_order_based/i18n/es.po +++ /dev/null @@ -1,58 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * sale_coupon_criteria_order_based -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server 15.0\n" -"Report-Msgid-Bugs-To: \n" -"PO-Revision-Date: 2023-11-02 19:36+0000\n" -"Last-Translator: Ivorra78 \n" -"Language-Team: none\n" -"Language: es\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_criteria_order_based -#: model:ir.model.fields,field_description:sale_coupon_criteria_order_based.field_coupon_program__rule_order_domain -#: model:ir.model.fields,field_description:sale_coupon_criteria_order_based.field_coupon_rule__rule_order_domain -msgid "Based on Order" -msgstr "Basado en el Pedido" - -#. module: sale_coupon_criteria_order_based -#: model:ir.model,name:sale_coupon_criteria_order_based.model_coupon_coupon -msgid "Coupon" -msgstr "Cupón" - -#. module: sale_coupon_criteria_order_based -#: model:ir.model,name:sale_coupon_criteria_order_based.model_coupon_rule -msgid "Coupon Rule" -msgstr "Regla de Cupón" - -#. module: sale_coupon_criteria_order_based -#: model:ir.model,name:sale_coupon_criteria_order_based.model_coupon_program -msgid "Coupon display on a website" -msgstr "Cupón mostrado en la web" - -#. module: sale_coupon_criteria_order_based -#: model:ir.model.fields,help:sale_coupon_criteria_order_based.field_coupon_program__rule_order_domain -#: model:ir.model.fields,help:sale_coupon_criteria_order_based.field_coupon_rule__rule_order_domain -msgid "Coupon program will work for the order with selected domain only" -msgstr "" -"El programa de cupones sólo funcionará para el pedido con el dominio " -"seleccionado" - -#. module: sale_coupon_criteria_order_based -#: model_terms:ir.ui.view,arch_db:sale_coupon_criteria_order_based.coupon_program_view_promo_program_form -msgid "Select order" -msgstr "Seleccionar pedido" - -#. module: sale_coupon_criteria_order_based -#: code:addons/sale_coupon_criteria_order_based/models/coupon_coupon.py:0 -#: code:addons/sale_coupon_criteria_order_based/models/coupon_program.py:0 -#, python-format -msgid "The order doesn't have access to this reward." -msgstr "La orden no tiene acceso a esta recompensa." diff --git a/sale_loyalty_criteria_order_based/i18n/it.po b/sale_loyalty_criteria_order_based/i18n/it.po deleted file mode 100644 index 57ae9758c..000000000 --- a/sale_loyalty_criteria_order_based/i18n/it.po +++ /dev/null @@ -1,69 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * sale_coupon_criteria_order_based -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server 14.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_criteria_order_based -#: model:ir.model.fields,field_description:sale_coupon_criteria_order_based.field_coupon_program__rule_order_domain -#: model:ir.model.fields,field_description:sale_coupon_criteria_order_based.field_coupon_rule__rule_order_domain -msgid "Based on Order" -msgstr "Basato su Ordine" - -#. module: sale_coupon_criteria_order_based -#: model:ir.model,name:sale_coupon_criteria_order_based.model_coupon_coupon -msgid "Coupon" -msgstr "Buono sconto" - -#. module: sale_coupon_criteria_order_based -#: model:ir.model,name:sale_coupon_criteria_order_based.model_coupon_rule -msgid "Coupon Rule" -msgstr "Regola buono sconto" - -#. module: sale_coupon_criteria_order_based -#: model:ir.model,name:sale_coupon_criteria_order_based.model_coupon_program -msgid "Coupon display on a website" -msgstr "" - -#. module: sale_coupon_criteria_order_based -#: model:ir.model.fields,help:sale_coupon_criteria_order_based.field_coupon_program__rule_order_domain -#: model:ir.model.fields,help:sale_coupon_criteria_order_based.field_coupon_rule__rule_order_domain -msgid "Coupon program will work for the order with selected domain only" -msgstr "" -"Questo coupon sarà applicabile solo per gli ordini con il dominio selezionato" - -#. module: sale_coupon_criteria_order_based -#: model_terms:ir.ui.view,arch_db:sale_coupon_criteria_order_based.coupon_program_view_promo_program_form -msgid "Select order" -msgstr "Seleziona ordini" - -#. module: sale_coupon_criteria_order_based -#: code:addons/sale_coupon_criteria_order_based/models/coupon_coupon.py:0 -#: code:addons/sale_coupon_criteria_order_based/models/coupon_program.py:0 -#, python-format -msgid "The order doesn't have access to this reward." -msgstr "L'ordine non ha accesso a questa ricompensa." - -#~ msgid "Coupon Program" -#~ msgstr "Programma Coupon" - -#~ msgid "Display Name" -#~ msgstr "Nome visualizzato" - -#~ msgid "ID" -#~ msgstr "ID" - -#~ msgid "Last Modified on" -#~ msgstr "Ultima modifica il" diff --git a/sale_loyalty_criteria_order_based/i18n/sale_coupon_criteria_order_based.pot b/sale_loyalty_criteria_order_based/i18n/sale_coupon_criteria_order_based.pot deleted file mode 100644 index bdd1bce40..000000000 --- a/sale_loyalty_criteria_order_based/i18n/sale_coupon_criteria_order_based.pot +++ /dev/null @@ -1,53 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * sale_coupon_criteria_order_based -# -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_criteria_order_based -#: model:ir.model.fields,field_description:sale_coupon_criteria_order_based.field_coupon_program__rule_order_domain -#: model:ir.model.fields,field_description:sale_coupon_criteria_order_based.field_coupon_rule__rule_order_domain -msgid "Based on Order" -msgstr "" - -#. module: sale_coupon_criteria_order_based -#: model:ir.model,name:sale_coupon_criteria_order_based.model_coupon_coupon -msgid "Coupon" -msgstr "" - -#. module: sale_coupon_criteria_order_based -#: model:ir.model,name:sale_coupon_criteria_order_based.model_coupon_rule -msgid "Coupon Rule" -msgstr "" - -#. module: sale_coupon_criteria_order_based -#: model:ir.model,name:sale_coupon_criteria_order_based.model_coupon_program -msgid "Coupon display on a website" -msgstr "" - -#. module: sale_coupon_criteria_order_based -#: model:ir.model.fields,help:sale_coupon_criteria_order_based.field_coupon_program__rule_order_domain -#: model:ir.model.fields,help:sale_coupon_criteria_order_based.field_coupon_rule__rule_order_domain -msgid "Coupon program will work for the order with selected domain only" -msgstr "" - -#. module: sale_coupon_criteria_order_based -#: model_terms:ir.ui.view,arch_db:sale_coupon_criteria_order_based.coupon_program_view_promo_program_form -msgid "Select order" -msgstr "" - -#. module: sale_coupon_criteria_order_based -#: code:addons/sale_coupon_criteria_order_based/models/coupon_coupon.py:0 -#: code:addons/sale_coupon_criteria_order_based/models/coupon_program.py:0 -#, python-format -msgid "The order doesn't have access to this reward." -msgstr "" diff --git a/sale_loyalty_criteria_order_based/models/__init__.py b/sale_loyalty_criteria_order_based/models/__init__.py index 1f966a6a5..2d2d304db 100644 --- a/sale_loyalty_criteria_order_based/models/__init__.py +++ b/sale_loyalty_criteria_order_based/models/__init__.py @@ -1,3 +1,3 @@ -from . import coupon_rule -from . import coupon_program -from . import coupon_coupon +from . import sale_order +from . import loyalty_rule +from . import loyalty_program diff --git a/sale_loyalty_criteria_order_based/models/coupon_coupon.py b/sale_loyalty_criteria_order_based/models/coupon_coupon.py deleted file mode 100644 index a956fa4a3..000000000 --- a/sale_loyalty_criteria_order_based/models/coupon_coupon.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright 2022 Ooops404 -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). - -from odoo import _, models - - -class CouponCoupon(models.Model): - _inherit = "coupon.coupon" - - def _check_coupon_code(self, order_date, partner_id, **kwargs): - message = super()._check_coupon_code(order_date, partner_id, **kwargs) - order = kwargs.get("order") - if not self.program_id._is_valid_order(order): - message = {"error": _("The order doesn't have access to this reward.")} - return message diff --git a/sale_loyalty_criteria_order_based/models/coupon_program.py b/sale_loyalty_criteria_order_based/models/coupon_program.py deleted file mode 100644 index 961a95f46..000000000 --- a/sale_loyalty_criteria_order_based/models/coupon_program.py +++ /dev/null @@ -1,59 +0,0 @@ -# Copyright 2022 Ooops404 -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). - -import ast - -from odoo import _, api, fields, models - - -class CouponProgram(models.Model): - _inherit = "coupon.program" - - rule_order_domain = fields.Char( - string="Based on Order", - help="Coupon program will work for the order with selected domain only", - ) - - def _is_valid_order(self, order): - """ - Check that we can apply the coupon for current order - """ - if self.rule_order_domain and self.rule_order_domain != "[]": - domain = ast.literal_eval(self.rule_order_domain) - return bool(order.filtered_domain(domain)) - else: - return True - - def _check_promo_code(self, order, coupon_code): - message = super()._check_promo_code(order, coupon_code) - if not self._is_valid_order(order): - message = {"error": _("The order doesn't have access to this reward.")} - return message - - @api.model - def _filter_programs_from_common_rules(self, order, next_order=False): - programs = super()._filter_programs_from_common_rules( - order, next_order=next_order - ) - - # Order 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 and programs: - programs = programs._filter_programs_on_order(order) - - programs_curr_order = programs.filtered( - lambda p: p.promo_applicability == "on_current_order" - ) - programs = programs.filtered(lambda p: p.promo_applicability == "on_next_order") - if programs_curr_order: - # Checking if rewards are in the SO should not be performed for - # rewards on_next_order - programs += programs_curr_order._filter_not_ordered_reward_programs(order) - return programs - - def _filter_programs_on_order(self, order): - """ - Filter all programs by order based domain - """ - return self.filtered(lambda program: program._is_valid_order(order)) diff --git a/sale_loyalty_criteria_order_based/models/coupon_rule.py b/sale_loyalty_criteria_order_based/models/coupon_rule.py deleted file mode 100644 index 491bb82a0..000000000 --- a/sale_loyalty_criteria_order_based/models/coupon_rule.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright 2022 Ooops404 -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). - -from odoo import fields, models - - -class CouponRule(models.Model): - _inherit = "coupon.rule" - - rule_order_domain = fields.Char( - string="Based on Order", - help="Coupon program will work for the order with selected domain only", - ) diff --git a/sale_loyalty_criteria_order_based/models/loyalty_program.py b/sale_loyalty_criteria_order_based/models/loyalty_program.py new file mode 100644 index 000000000..34c224990 --- /dev/null +++ b/sale_loyalty_criteria_order_based/models/loyalty_program.py @@ -0,0 +1,34 @@ +# Copyright 2022 Ooops404 +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import ast + +from odoo import _, api, fields, models +from odoo.exceptions import UserError +from odoo.osv.expression import expression + + +class LoyaltyProgram(models.Model): + _inherit = "loyalty.program" + + rule_order_domain = fields.Char(string="Based on Order", default="[]") + + @api.constrains("rule_order_domain") + def _constrain_rule_order_domain(self): + model = self.env["sale.order"] + for program in self: + try: + domain = ast.literal_eval(program.rule_order_domain) + # Ensuring that domain is valid for sale.order + expression(domain, model) + except Exception as e: + raise UserError(_("Invalid domain on program")) from e + + def _is_valid_order(self, order): + """Check that we can apply the program on current order""" + self.ensure_one() + order.ensure_one() + domain = ast.literal_eval(self.rule_order_domain) + if domain: + return bool(order.filtered_domain(domain)) + return True diff --git a/sale_loyalty_criteria_order_based/models/loyalty_rule.py b/sale_loyalty_criteria_order_based/models/loyalty_rule.py new file mode 100644 index 000000000..23f3200a0 --- /dev/null +++ b/sale_loyalty_criteria_order_based/models/loyalty_rule.py @@ -0,0 +1,35 @@ +# Copyright 2022 Ooops404 +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +# Copyright 2024 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) +import ast + +from odoo import _, api, fields, models +from odoo.exceptions import UserError +from odoo.osv.expression import expression + + +class LoyaltyRule(models.Model): + _inherit = "loyalty.rule" + + rule_order_domain = fields.Char(string="Based on Order", default="[]") + + @api.constrains("rule_order_domain") + def _constrain_rule_order_domain(self): + model = self.env["sale.order"] + for rule in self: + try: + domain = ast.literal_eval(rule.rule_order_domain) + # Ensuring that domain is valid for sale.order + expression(domain, model) + except Exception as e: + raise UserError(_("Invalid domain on rule")) from e + + def _is_valid_order(self, order): + """Check that we can apply the rule on current order""" + self.ensure_one() + order.ensure_one() + domain = ast.literal_eval(self.rule_order_domain) + if domain: + return bool(order.filtered_domain(domain)) + return True diff --git a/sale_loyalty_criteria_order_based/models/sale_order.py b/sale_loyalty_criteria_order_based/models/sale_order.py new file mode 100644 index 000000000..af13a0468 --- /dev/null +++ b/sale_loyalty_criteria_order_based/models/sale_order.py @@ -0,0 +1,28 @@ +# Copyright 2024 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import _, models +from odoo.osv.expression import AND + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + def _try_apply_code(self, code): + self.ensure_one() + base_domain = self._get_trigger_domain() + domain = AND([base_domain, [("mode", "=", "with_code"), ("code", "=", code)]]) + rule = self.env["loyalty.rule"].search(domain) + if rule and rule._is_valid_order(self): + return super()._try_apply_code(code) + return { + "error": _( + "This code (%(code)s) is not available for this order.", code=code + ) + } + + def _try_apply_program(self, program, coupon): + self.ensure_one() + if program and program._is_valid_order(self): + return super()._try_apply_program(program, coupon) + return {"error": _("The program is not available for this order.")} diff --git a/sale_loyalty_criteria_order_based/readme/CONTRIBUTORS.md b/sale_loyalty_criteria_order_based/readme/CONTRIBUTORS.md index 7d1a3d174..b1016ec39 100644 --- a/sale_loyalty_criteria_order_based/readme/CONTRIBUTORS.md +++ b/sale_loyalty_criteria_order_based/readme/CONTRIBUTORS.md @@ -2,3 +2,4 @@ - Cetmix \<\> - [Tecnativa](https://www.tecnativa.com) - Pilar Vargas +- MmeQuignon \ diff --git a/sale_loyalty_criteria_order_based/readme/DESCRIPTION.md b/sale_loyalty_criteria_order_based/readme/DESCRIPTION.md index 458019a5f..977b3ca24 100644 --- a/sale_loyalty_criteria_order_based/readme/DESCRIPTION.md +++ b/sale_loyalty_criteria_order_based/readme/DESCRIPTION.md @@ -1,4 +1,3 @@ -This module adds an option to configure your coupon conditions based on -the Sales Order values. +This module adds an option to configure your **Loyalty Program** and add additional constraints based on **Sales Order** values. -Example: "If Sales Team is 'Europe' apply the following discount' +Example: "If Sales Team is 'Europe' grant one point for the program" diff --git a/sale_loyalty_criteria_order_based/readme/USAGE.md b/sale_loyalty_criteria_order_based/readme/USAGE.md index 06c20bb4a..3b43105d5 100644 --- a/sale_loyalty_criteria_order_based/readme/USAGE.md +++ b/sale_loyalty_criteria_order_based/readme/USAGE.md @@ -1,7 +1,7 @@ -Open or create a new Promotion Program in the **Sales/Products/Promotion -Programs** menu. +Open or create a new Loyalty Program in the **Sales/Products/Discount & Loyalty** menu. -In *Conditions* section the following new option is available: - -- **Based on Order** configure an order-based domain to apply the - promotion +There's two ways to add a condition based on orders: +- On the program itself, allowing to define `global` conditions (valid for all rules)\ + → Find and edit the **Based on Order** field on the **Loyalty Program** form view. +- On the rule, allowing to define conditions valid for a given rule only\ + → Open or create a new **Loyalty Rule**, and edit the **Based on Order** field. diff --git a/sale_loyalty_criteria_order_based/static/description/index.html b/sale_loyalty_criteria_order_based/static/description/index.html index be1a6caec..71b043f91 100644 --- a/sale_loyalty_criteria_order_based/static/description/index.html +++ b/sale_loyalty_criteria_order_based/static/description/index.html @@ -369,10 +369,10 @@

Sales Coupon based on Sales Order values

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !! source digest: sha256:979b0c094347147f5111cfc8288b9cc8e5881d909d64fb41c068cc4f2ab4f187 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! --> -

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

-

This module adds an option to configure your coupon conditions based on -the Sales Order values.

-

Example: “If Sales Team is ‘Europe’ apply the following discount’

+

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

+

This module adds an option to configure your Loyalty Program and add +additional constraints based on Sales Order values.

+

Example: “If Sales Team is ‘Europe’ grant one point for the program”

Table of contents

    @@ -388,12 +388,17 @@

    Sales Coupon based on Sales Order values

Usage

-

Open or create a new Promotion Program in the Sales/Products/Promotion -Programs menu.

-

In Conditions section the following new option is available:

+

Open or create a new Loyalty Program in the Sales/Products/Discount & +Loyalty menu.

+

There’s two ways to add a condition based on orders:

    -
  • Based on Order configure an order-based domain to apply the -promotion
  • +
  • On the program itself, allowing to define global conditions (valid +for all rules) +→ Find and edit the Based on Order field on the Loyalty +Program form view.
  • +
  • On the rule, allowing to define conditions valid for a given rule only +→ Open or create a new Loyalty Rule, and edit the Based on +Order field.
@@ -401,7 +406,7 @@

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.

+feedback.

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

@@ -422,6 +427,7 @@

Contributors

  • Pilar Vargas
  • +
  • MmeQuignon <matthieu.mequignon@camptocamp.com>
  • @@ -433,7 +439,7 @@

    Maintainers

    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.

    -

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

    +

    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_criteria_order_based/tests/__init__.py b/sale_loyalty_criteria_order_based/tests/__init__.py index 1c5acf048..887ee09b1 100644 --- a/sale_loyalty_criteria_order_based/tests/__init__.py +++ b/sale_loyalty_criteria_order_based/tests/__init__.py @@ -1 +1 @@ -from . import test_sale_coupon_criteria_order_based +from . import test_methods diff --git a/sale_loyalty_criteria_order_based/tests/common.py b/sale_loyalty_criteria_order_based/tests/common.py new file mode 100644 index 000000000..a6fe07b57 --- /dev/null +++ b/sale_loyalty_criteria_order_based/tests/common.py @@ -0,0 +1,48 @@ +# Copyright 2024 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo.tests import Form, tagged +from odoo.tests.common import TransactionCase + + +@tagged("-at_install", "post_install") +class Common(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + + @classmethod + def create_order(cls, add_line=True): + # A simple order with 1 ordered product + cls.partner = cls.env["res.partner"].create({"name": "Test Customer"}) + cls.product = cls.env["product.product"].create({"name": "service"}) + with Form(cls.env["sale.order"]) as order_form: + order_form.partner_id = cls.partner + if add_line: + with order_form.order_line.new() as order_line: + order_line.product_id = cls.product + order_line.name = cls.product.name + order_line.product_uom_qty = 1 + order_line.product_uom = cls.product.uom_id + order_line.price_unit = 42.0 + return order_form.record + + @classmethod + def create_program(cls, rule_values=None, **extra_vals): + default_rule_values = {} + if rule_values: + default_rule_values = rule_values + values = { + "name": "Test Program", + "program_type": "promo_code", + "rule_ids": [(0, 0, default_rule_values)], + } + if extra_vals: + values.update(extra_vals) + return cls.env["loyalty.program"].create(values) + + @classmethod + def create_rule(cls, **extra_vals): + program = cls.create_program(rule_values=extra_vals) + return program.rule_ids diff --git a/sale_loyalty_criteria_order_based/tests/test_methods.py b/sale_loyalty_criteria_order_based/tests/test_methods.py new file mode 100644 index 000000000..a151af6b0 --- /dev/null +++ b/sale_loyalty_criteria_order_based/tests/test_methods.py @@ -0,0 +1,74 @@ +# Copyright 2024 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo.exceptions import UserError + +from .common import Common + + +class TestMethods(Common): + def test_constrain_rule(self): + # Trying to set rule_order_domain as an invalid domain should + # raise a UserError + regex = r"Invalid domain on rule" + # invalid expression + domain = '{"wrong": "wrong"}' + with self.assertRaisesRegex(UserError, regex): + self.create_rule(rule_order_domain=domain) + # invalid expression + domain = "wrong" + with self.assertRaisesRegex(UserError, regex): + self.create_rule(rule_order_domain=domain) + # valid expression, wrong field + domain = '[("wrong_field", "=", False)]' + with self.assertRaisesRegex(UserError, regex): + self.create_rule(rule_order_domain=domain) + # Valid domains shouldn't raise an exception + self.create_rule(rule_order_domain='[("partner_id", "=", 42)]') + self.create_rule(rule_order_domain="[]") + + def test_constrain_program(self): + # Trying to set rule_order_domain as an invalid domain should + # raise a UserError + regex = r"Invalid domain on program" + # invalid expression + domain = '{"wrong": "wrong"}' + with self.assertRaisesRegex(UserError, regex): + self.create_program(rule_order_domain=domain) + # invalid expression + domain = "wrong" + with self.assertRaisesRegex(UserError, regex): + self.create_program(rule_order_domain=domain) + # valid expression, wrong field + domain = '[("wrong_field", "=", False)]' + with self.assertRaisesRegex(UserError, regex): + self.create_program(rule_order_domain=domain) + # Valid domains shouldn't raise an exception + self.create_program(rule_order_domain='[("partner_id", "=", 42)]') + self.create_program(rule_order_domain="[]") + + def test_rule_domain(self): + order = self.create_order() + all_orders_rule = self.create_rule() + self.assertTrue(all_orders_rule._is_valid_order(order)) + other_order_rule = self.create_rule( + rule_order_domain=f"[('id', '!=', {order.id})]" + ) + self.assertFalse(other_order_rule._is_valid_order(order)) + this_order_rule = self.create_rule( + rule_order_domain=f"[('id', '=', {order.id})]" + ) + self.assertTrue(this_order_rule._is_valid_order(order)) + + def test_program_domain(self): + order = self.create_order() + all_orders_program = self.create_program() + self.assertTrue(all_orders_program._is_valid_order(order)) + other_order_program = self.create_program( + rule_order_domain=f"[('id', '!=', {order.id})]" + ) + self.assertFalse(other_order_program._is_valid_order(order)) + this_order_program = self.create_program( + rule_order_domain=f"[('id', '=', {order.id})]" + ) + self.assertTrue(this_order_program._is_valid_order(order)) diff --git a/sale_loyalty_criteria_order_based/tests/test_reward_order_criteria.py b/sale_loyalty_criteria_order_based/tests/test_reward_order_criteria.py new file mode 100644 index 000000000..adf95795f --- /dev/null +++ b/sale_loyalty_criteria_order_based/tests/test_reward_order_criteria.py @@ -0,0 +1,76 @@ +# Copyright 2024 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from .common import Common + + +class TestRewardOrderCriteria(Common): + def test_apply_code_no_domain(self): + self.create_rule(code="TEST", minimum_qty=1) + # Rule is valid when 1 product is sold, with code TEST + # There's no line in the order, it should return an error + order_no_line = self.create_order(add_line=False) + result = order_no_line._try_apply_code("TEST") + self.assertEqual( + result.get("error"), + "You don't have the required product quantities on your sales order.", + ) + # If we apply it on an order with a line, no error should be returned + order_with_line = self.create_order() + result = order_with_line._try_apply_code("TEST") + self.assertNotIn("error", result) + + def test_apply_rule_with_domain(self): + # A rule only valid for draft orders with 1 qty sold at least + self.create_rule( + code="TEST_WITH_DOMAIN", rule_order_domain="[('state', '=', 'draft')]" + ) + # Order is draft but doesn't have a line + order = self.create_order(add_line=False) + result = order._try_apply_code("TEST_WITH_DOMAIN") + self.assertEqual( + result.get("error"), + "You don't have the required product quantities on your sales order.", + ) + # Order is draft and have a line -> no error + order = self.create_order() + result = order._try_apply_code("TEST_WITH_DOMAIN") + self.assertNotIn("error", result) + # Order is not draft -> Should return an error + order = self.create_order() + order.action_confirm() + result = order._try_apply_code("TEST_WITH_DOMAIN") + self.assertEqual( + result.get("error"), + "This code (TEST_WITH_DOMAIN) is not available for this order.", + ) + + def test_apply_program_with_domain(self): + # Very similar test then `test_apply_code_no_domain`, except domain is on + # the program. + # A program only valid for draft orders, with a rule allowing + # this code to be used only when there's a product sold at least + self.create_program( + rule_values={ + "code": "TEST_WITH_DOMAIN", + }, + rule_order_domain="[('state', '=', 'draft')]", + ) + # Order is draft but doesn't have a line + order = self.create_order(add_line=False) + result = order._try_apply_code("TEST_WITH_DOMAIN") + self.assertEqual( + result.get("error"), + "You don't have the required product quantities on your sales order.", + ) + # Order is draft and have a line -> no error + order = self.create_order() + result = order._try_apply_code("TEST_WITH_DOMAIN") + self.assertNotIn("error", result) + # Order is not draft -> Should return an error + order = self.create_order() + order.action_confirm() + result = order._try_apply_code("TEST_WITH_DOMAIN") + self.assertEqual( + result.get("error"), "The program is not available for this order." + ) diff --git a/sale_loyalty_criteria_order_based/tests/test_sale_coupon_criteria_order_based.py b/sale_loyalty_criteria_order_based/tests/test_sale_coupon_criteria_order_based.py deleted file mode 100644 index fc21bb8ed..000000000 --- a/sale_loyalty_criteria_order_based/tests/test_sale_coupon_criteria_order_based.py +++ /dev/null @@ -1,82 +0,0 @@ -# Copyright 2022 Ooops404 -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). - -from odoo.tests import tagged - -from odoo.addons.sale_coupon.tests.common import TestSaleCouponCommon - - -@tagged("post_install", "-at_install") -class TestSaleCouponCriteriaOrderBased(TestSaleCouponCommon): - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) - - def test_program_rules_order_based(self): - """ - Check program rules based on order domain - """ - partner_test = self.env["res.partner"].create( - { - "name": "Test", - "email": "test@example.com", - } - ) - user_test = ( - self.env["res.users"] - .with_context(no_reset_password=True) - .create( - { - "login": "test", - "password": "test", - "partner_id": partner_test.id, - "groups_id": [(6, 0, [self.env.ref("base.group_user").id])], - } - ) - ) - # Update program order domain. The program should be applied for order - # if Salesperson of the order is 'Test' user - self.immediate_promotion_program.write( - {"rule_order_domain": "[('user_id.id', '=', %s)]" % (user_test.id)} - ) - order = self.empty_order - order.write( - { - "order_line": [ - ( - 0, - False, - { - "product_id": self.product_A.id, - "name": "1 Product A", - "product_uom": self.uom_unit.id, - "product_uom_qty": 1.0, - }, - ), - ( - 0, - False, - { - "product_id": self.product_B.id, - "name": "2 Product B", - "product_uom": self.uom_unit.id, - "product_uom_qty": 1.0, - }, - ), - ] - } - ) - order.recompute_coupon_lines() - self.assertEqual( - len(order.order_line.ids), - 2, - "The promo offer shouldn't have been applied.", - ) - order.user_id = user_test.id - order.recompute_coupon_lines() - self.assertEqual( - len(order.order_line.ids), - 3, - "The promo offer should have been applied.", - ) diff --git a/sale_loyalty_criteria_order_based/views/coupon_program_view.xml b/sale_loyalty_criteria_order_based/views/coupon_program_view.xml deleted file mode 100644 index f21fe4239..000000000 --- a/sale_loyalty_criteria_order_based/views/coupon_program_view.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - coupon.program.view.form - coupon.program - - - - - - - - - diff --git a/sale_loyalty_criteria_order_based/views/loyalty_program.xml b/sale_loyalty_criteria_order_based/views/loyalty_program.xml new file mode 100644 index 000000000..f6d789c18 --- /dev/null +++ b/sale_loyalty_criteria_order_based/views/loyalty_program.xml @@ -0,0 +1,21 @@ + + + + + + loyalty.program.form.inherit + loyalty.program + + + + + + + + + diff --git a/sale_loyalty_criteria_order_based/views/loyalty_rule.xml b/sale_loyalty_criteria_order_based/views/loyalty_rule.xml new file mode 100644 index 000000000..76ea93194 --- /dev/null +++ b/sale_loyalty_criteria_order_based/views/loyalty_rule.xml @@ -0,0 +1,21 @@ + + + + + + loyalty.rule.form.inherit + loyalty.rule + + + + + + + + +