diff --git a/sale_loyalty_order_suggestion/README.rst b/sale_loyalty_order_suggestion/README.rst new file mode 100644 index 000000000..19900a19e --- /dev/null +++ b/sale_loyalty_order_suggestion/README.rst @@ -0,0 +1,104 @@ +======================== +Sale Loyalty Suggestions +======================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:bd9a02553f79e2effc733436a6ddc50682c22b0ba1e99c739a0798d5619ba27a + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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_order_suggestion + :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_order_suggestion + :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 extends the functionality of the `sale_loyalty` wizard by giving hints of +available promotions and products needed to apply them to the seller placing the sales +order. A product added to the lines of a sales order will be marked with a gift icon if +there is a promotion containing that product as part of its rules. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +To use this module: + +* Configure or create a promotion and set in its rules products and a required quantity. +* Create a sales order and add to the order lines one of the products that were part of + the promotion rules. This line will then be marked with an icon (🎁) which will appear + at the end on the right. +* Click on the icon and the wizard will open with the available promotions of which the + product is part of its rules. +* Select the promotion and the products needed to apply it. + +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 + +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_order_suggestion/__init__.py b/sale_loyalty_order_suggestion/__init__.py new file mode 100644 index 000000000..9b4296142 --- /dev/null +++ b/sale_loyalty_order_suggestion/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizard diff --git a/sale_loyalty_order_suggestion/__manifest__.py b/sale_loyalty_order_suggestion/__manifest__.py new file mode 100644 index 000000000..6c361efc6 --- /dev/null +++ b/sale_loyalty_order_suggestion/__manifest__.py @@ -0,0 +1,26 @@ +# Copyright 2021 Tecnativa - David Vidal +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +{ + "name": "Sale Loyalty Suggestions", + "summary": "Suggest promotions in the sale order line", + "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": ["sale_loyalty", "sale_loyalty_initial_date_validity"], + "data": [ + "security/ir.model.access.csv", + "views/sale_order_views.xml", + "wizard/sale_loyalty_reward_wizard_views.xml", + ], + "assets": { + "web.assets_backend": [ + "/sale_loyalty_order_suggestion/static/src/js/suggest_promotion_widget.esm.js", + "/sale_loyalty_order_suggestion/static/src/xml/suggest_promotion.xml", + ], + }, + "installable": True, +} diff --git a/sale_loyalty_order_suggestion/i18n/sale_coupon_order_suggestion.pot b/sale_loyalty_order_suggestion/i18n/sale_coupon_order_suggestion.pot new file mode 100644 index 000000000..8fae2fde5 --- /dev/null +++ b/sale_loyalty_order_suggestion/i18n/sale_coupon_order_suggestion.pot @@ -0,0 +1,31 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * sale_loyalty_order_suggestion +# +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_loyalty_order_suggestion +#. openerp-web +#: code:addons/sale_loyalty_order_suggestion/static/src/xml/suggest_promotion.xml:0 +#, python-format +msgid "Available promotions" +msgstr "" + +#. module: sale_loyalty_order_suggestion +#: model:ir.model,name:sale_loyalty_order_suggestion.model_sale_order_line +msgid "Sales Order Line" +msgstr "" + +#. module: sale_loyalty_order_suggestion +#: model:ir.model.fields,field_description:sale_loyalty_order_suggestion.field_sale_order_line__suggested_promotion_ids +msgid "Suggested Promotion" +msgstr "" diff --git a/sale_loyalty_order_suggestion/models/__init__.py b/sale_loyalty_order_suggestion/models/__init__.py new file mode 100644 index 000000000..6aacb7531 --- /dev/null +++ b/sale_loyalty_order_suggestion/models/__init__.py @@ -0,0 +1 @@ +from . import sale_order diff --git a/sale_loyalty_order_suggestion/models/sale_order.py b/sale_loyalty_order_suggestion/models/sale_order.py new file mode 100644 index 000000000..9757a2a4c --- /dev/null +++ b/sale_loyalty_order_suggestion/models/sale_order.py @@ -0,0 +1,118 @@ +# Copyright 2021 Tecnativa - David Vidal +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo import api, fields, models +from odoo.osv import expression + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + def _get_available_programs_domain(self): + domain = [ + ("active", "=", True), + ("trigger", "=", "auto"), + ("program_type", "not in", ("promo_code", "next_order_coupons")), + "|", + ("date_from", "=", False), + ("date_from", "<=", fields.Date.today()), + "|", + ("date_to", "=", False), + ("date_to", ">=", fields.Date.today()), + "|", + ("applies_on", "=", "current"), + ("applies_on", "=", "both"), + ] + return domain + + def _get_available_programs(self): + programs = self.env["loyalty.program"] + self._update_programs_and_rewards() + programs_applied = self._get_reward_programs() + domain = expression.AND( + [ + self._get_available_programs_domain(), + [("id", "not in", programs_applied.ids)], + ] + ) + programs = ( + self.env["loyalty.program"] + .search(domain) + .filtered(lambda p: not p.limit_usage or p.total_order_count < p.max_usage) + ) + # If the same reward has already been applied from another program, the reward + # and the program to which it belongs must be deleted from the list of + # suggestions. For example, if a 10% discount has already been applied, the same + # discount cannot be applied even if it belongs to another program. + order_rewards = self.order_line.reward_id + programs_to_delete = [] + if order_rewards: + programs_to_delete = [ + program.id + for program in programs + if any( + all( + getattr(program_reward, field) == getattr(order_reward, field) + for field in [ + "reward_type", + "discount", + "discount_mode", + "discount_applicability", + "discount_product_domain", + "discount_product_ids", + "discount_product_category_id", + "discount_max_amount", + "reward_product_id", + "reward_product_tag_id", + "reward_product_qty", + "required_points", + ] + ) + for program_reward in program.reward_ids + for order_reward in order_rewards + ) + ] + return programs.filtered(lambda p: p.id not in programs_to_delete) + + def _available_programs(self): + self.ensure_one() + valid_programs = self._get_available_programs() + # Filters programs that have rules with minimum_qty > 0 + programs_with_minimum_qty = valid_programs.filtered( + lambda x: any(rule.minimum_qty > 0 for rule in x.rule_ids) + ) + programs = self.env["loyalty.program"] + if programs_with_minimum_qty: + product_id = self.env.context.get("product_id") + programs += programs_with_minimum_qty.filtered( + lambda x: any( + product_id in rule._get_valid_products().ids for rule in x.rule_ids + ) + ) + return programs + + +class SaleOrderLine(models.Model): + _inherit = "sale.order.line" + + suggested_promotion_ids = fields.Many2many( + comodel_name="loyalty.program", + compute="_compute_suggested_promotion_ids", + ) + suggested_promotions = fields.Boolean( + compute="_compute_suggested_promotion_ids", default=False + ) + suggested_reward_ids = fields.Many2many( + comodel_name="loyalty.reward", + compute="_compute_suggested_promotion_ids", + ) + + @api.depends("product_id") + def _compute_suggested_promotion_ids(self): + self.suggested_promotion_ids = False + self.suggested_reward_ids = False + for line in self.filtered("product_id"): + line.suggested_promotion_ids = line.order_id.with_context( + product_id=line.product_id.id + )._available_programs() + line.suggested_promotions = bool(line.suggested_promotion_ids) + line.suggested_reward_ids = line.suggested_promotion_ids.reward_ids diff --git a/sale_loyalty_order_suggestion/readme/CONTRIBUTORS.rst b/sale_loyalty_order_suggestion/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..07e8c598b --- /dev/null +++ b/sale_loyalty_order_suggestion/readme/CONTRIBUTORS.rst @@ -0,0 +1,5 @@ +* `Tecnativa `_: + + * Pedro M. Baeza + * David Vidal + * Pilar Vargas diff --git a/sale_loyalty_order_suggestion/readme/DESCRIPTION.rst b/sale_loyalty_order_suggestion/readme/DESCRIPTION.rst new file mode 100644 index 000000000..942eb466b --- /dev/null +++ b/sale_loyalty_order_suggestion/readme/DESCRIPTION.rst @@ -0,0 +1,4 @@ +This module extends the functionality of the `sale_loyalty` wizard by giving hints of +available promotions and products needed to apply them to the seller placing the sales +order. A product added to the lines of a sales order will be marked with a gift icon if +there is a promotion containing that product as part of its rules. diff --git a/sale_loyalty_order_suggestion/readme/USAGE.rst b/sale_loyalty_order_suggestion/readme/USAGE.rst new file mode 100644 index 000000000..f8adf79e6 --- /dev/null +++ b/sale_loyalty_order_suggestion/readme/USAGE.rst @@ -0,0 +1,9 @@ +To use this module: + +* Configure or create a promotion and set in its rules products and a required quantity. +* Create a sales order and add to the order lines one of the products that were part of + the promotion rules. This line will then be marked with an icon (🎁) which will appear + at the end on the right. +* Click on the icon and the wizard will open with the available promotions of which the + product is part of its rules. +* Select the promotion and the products needed to apply it. diff --git a/sale_loyalty_order_suggestion/security/ir.model.access.csv b/sale_loyalty_order_suggestion/security/ir.model.access.csv new file mode 100644 index 000000000..50a8d8686 --- /dev/null +++ b/sale_loyalty_order_suggestion/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +sale_loyalty_order_suggestion.access_sale_loyalty_rule_product_line_wizard,access_sale_loyalty_rule_product_line_wizard,sale_loyalty_order_suggestion.model_sale_loyalty_rule_product_line_wizard,base.group_user,1,1,1,1 diff --git a/sale_loyalty_order_suggestion/static/description/icon.png b/sale_loyalty_order_suggestion/static/description/icon.png new file mode 100644 index 000000000..f8d442615 Binary files /dev/null and b/sale_loyalty_order_suggestion/static/description/icon.png differ diff --git a/sale_loyalty_order_suggestion/static/description/index.html b/sale_loyalty_order_suggestion/static/description/index.html new file mode 100644 index 000000000..34fb3c269 --- /dev/null +++ b/sale_loyalty_order_suggestion/static/description/index.html @@ -0,0 +1,445 @@ + + + + + + +Sale Loyalty Suggestions + + + +
+

Sale Loyalty Suggestions

+ + +

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

+

This module extends the functionality of the sale_loyalty wizard by giving hints of +available promotions and products needed to apply them to the seller placing the sales +order. A product added to the lines of a sales order will be marked with a gift icon if +there is a promotion containing that product as part of its rules.

+

Table of contents

+ +
+

Usage

+

To use this module:

+
    +
  • Configure or create a promotion and set in its rules products and a required quantity.
  • +
  • Create a sales order and add to the order lines one of the products that were part of +the promotion rules. This line will then be marked with an icon (🎁) which will appear +at the end on the right.
  • +
  • Click on the icon and the wizard will open with the available promotions of which the +product is part of its rules.
  • +
  • Select the promotion and the products needed to apply it.
  • +
+
+
+

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
    • +
    +
  • +
+
+
+

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_order_suggestion/static/src/js/suggest_promotion_widget.esm.js b/sale_loyalty_order_suggestion/static/src/js/suggest_promotion_widget.esm.js new file mode 100644 index 000000000..ed45019fd --- /dev/null +++ b/sale_loyalty_order_suggestion/static/src/js/suggest_promotion_widget.esm.js @@ -0,0 +1,35 @@ +/** @odoo-module **/ +const {Component} = owl; +import {registry} from "@web/core/registry"; +import {standardFieldProps} from "@web/views/fields/standard_field_props"; +import {useService} from "@web/core/utils/hooks"; + +export class SuggestPromotionWidget extends Component { + setup() { + super.setup(); + this.actionService = useService("action"); + } + getSuggestedPromotions() { + return this.props.record.data.suggested_reward_ids.records.map( + (object) => object.data.id + ); + } + + async viewPromotionsWizard() { + const SuggestedPromotions = this.getSuggestedPromotions(); + const record = this.__owl__.parent.parent.parent.props.record; + await record.save(); + this.actionService.doAction("sale_loyalty.sale_loyalty_reward_wizard_action", { + additionalContext: { + default_active_id: record.data.id, + default_order_id: record.data.id, + default_reward_ids: SuggestedPromotions, + }, + }); + } +} + +SuggestPromotionWidget.template = "sale_loyalty_order_suggestion.suggestPromotion"; +SuggestPromotionWidget.props = standardFieldProps; + +registry.category("fields").add("suggest_promotion_widget", SuggestPromotionWidget); diff --git a/sale_loyalty_order_suggestion/static/src/xml/suggest_promotion.xml b/sale_loyalty_order_suggestion/static/src/xml/suggest_promotion.xml new file mode 100644 index 000000000..41dd0a031 --- /dev/null +++ b/sale_loyalty_order_suggestion/static/src/xml/suggest_promotion.xml @@ -0,0 +1,13 @@ + + + +
+ +
+
+
diff --git a/sale_loyalty_order_suggestion/tests/__init__.py b/sale_loyalty_order_suggestion/tests/__init__.py new file mode 100644 index 000000000..6235cf4bc --- /dev/null +++ b/sale_loyalty_order_suggestion/tests/__init__.py @@ -0,0 +1,2 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from . import test_sale_loyalty_order_suggestion diff --git a/sale_loyalty_order_suggestion/tests/test_sale_loyalty_order_suggestion.py b/sale_loyalty_order_suggestion/tests/test_sale_loyalty_order_suggestion.py new file mode 100644 index 000000000..fbce727a9 --- /dev/null +++ b/sale_loyalty_order_suggestion/tests/test_sale_loyalty_order_suggestion.py @@ -0,0 +1,150 @@ +# Copyright 2023 Tecnativa - Pilar Vargas +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo.tests.common import Form, TransactionCase + + +class TestSaleLoyaltyOrderSuggestion(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + product_obj = cls.env["product.product"] + cls.pricelist = cls.env["product.pricelist"].create( + { + "name": "Test pricelist", + "item_ids": [ + ( + 0, + 0, + { + "applied_on": "3_global", + "compute_price": "formula", + "base": "list_price", + }, + ) + ], + } + ) + cls.partner = cls.env["res.partner"].create( + {"name": "Mr. Odoo", "property_product_pricelist": cls.pricelist.id} + ) + cls.product_a = product_obj.create({"name": "Product A", "list_price": 50}) + cls.product_b = product_obj.create({"name": "Product B", "list_price": 10}) + cls.product_c = product_obj.create({"name": "Product C", "list_price": 70}) + cls.loyalty_program = cls.env["loyalty.program"].create( + { + "name": "Test Loyalty Order Suggestion", + "program_type": "promotion", + "trigger": "auto", + "applies_on": "current", + "rule_ids": [ + ( + 0, + 0, + { + "reward_point_mode": "order", + "minimum_qty": 3, + "product_ids": [ + ( + 6, + 0, + [ + cls.product_a.id, + cls.product_b.id, + cls.product_c.id, + ], + ) + ], + }, + ) + ], + "reward_ids": [ + ( + 0, + 0, + { + "reward_type": "discount", + "required_points": 1, + "discount": 10, + "discount_mode": "percent", + "discount_applicability": "order", + }, + ) + ], + } + ) + sale_form = Form(cls.env["sale.order"]) + sale_form.partner_id = cls.partner + with sale_form.order_line.new() as line_form: + line_form.product_id = cls.product_a + line_form.product_uom_qty = 1 + cls.sale = sale_form.save() + + def _open_suggested_promotion_wizard(self, suggested_reward_ids): + self.sale._update_programs_and_rewards() + wizard = ( + self.env["sale.loyalty.reward.wizard"] + .with_context(active_id=self.sale) + .create({"order_id": self.sale.id}) + ) + wizard.reward_ids = suggested_reward_ids + return wizard + + def test_01_suggested_promotion_for_product_a_no_applicable(self): + # In this test a suggestion is made for a promotion that contains in its rules + # the product added to the lines of the order but does not meet all the + # requirements to be applied so it will be necessary to configure the + # products in the wizard. + line_a = self.sale.order_line.filtered(lambda x: x.product_id == self.product_a) + wizard = self._open_suggested_promotion_wizard(line_a.suggested_reward_ids) + self.assertEqual(wizard.reward_ids, line_a.suggested_promotion_ids.reward_ids) + # Select promotion to apply + wizard.selected_reward_id = self.loyalty_program.reward_ids[0].id + # The promotion is not directly applicable as it does not comply with all the rules. + self.assertFalse(wizard.applicable_program) + # The wizard contains 3 lines, one for each product configured in the rules + self.assertEqual(len(wizard.loyalty_rule_line_ids), 3) + wiz_line_a = wizard.loyalty_rule_line_ids.filtered( + lambda x: x.product_id == self.product_a + ) + self.assertTrue(wiz_line_a) + self.assertEqual(wiz_line_a.units_included, 1) + self.assertEqual(wiz_line_a.units_required, 3) + self.assertEqual(wiz_line_a.units_to_include, 0) + wiz_line_b = wizard.loyalty_rule_line_ids.filtered( + lambda x: x.product_id == self.product_b + ) + self.assertTrue(wiz_line_b) + self.assertEqual(wiz_line_b.units_included, 0) + self.assertEqual(wiz_line_b.units_required, 3) + self.assertEqual(wiz_line_b.units_to_include, 0) + wiz_line_c = wizard.loyalty_rule_line_ids.filtered( + lambda x: x.product_id == self.product_c + ) + self.assertTrue(wiz_line_c) + self.assertEqual(wiz_line_c.units_included, 0) + self.assertEqual(wiz_line_c.units_required, 3) + self.assertEqual(wiz_line_c.units_to_include, 0) + # More units are added to make the promotion compliant and applicable. + wiz_line_c.units_to_include = 2 + wizard.action_apply() + line_c = self.sale.order_line.filtered(lambda x: x.product_id == self.product_c) + self.assertTrue(line_c) + self.assertEqual(line_c.product_uom_qty, 2) + self.assertTrue(self.sale.order_line.filtered(lambda x: x.is_reward_line)) + + def test_02_suggested_promotion_for_product_a_auto_applicable(self): + # In this test a suggestion is made for a promotion that contains in its rules + # the product added to the order lines and that meets all the requirements to be + # applied. + line_a = self.sale.order_line.filtered(lambda x: x.product_id == self.product_a) + line_a.product_uom_qty = 3 + wizard = self._open_suggested_promotion_wizard(line_a.suggested_reward_ids) + self.assertEqual(wizard.reward_ids, line_a.suggested_promotion_ids.reward_ids) + # Select promotion to apply + wizard.selected_reward_id = self.loyalty_program.reward_ids[0].id + # The promotion is directly applicable as it does not comply with all the rules. + self.assertTrue(wizard.applicable_program) + # The wizard contains 0 lines + self.assertEqual(len(wizard.loyalty_rule_line_ids), 0) + wizard.action_apply() + self.assertTrue(self.sale.order_line.filtered(lambda x: x.is_reward_line)) diff --git a/sale_loyalty_order_suggestion/views/sale_order_views.xml b/sale_loyalty_order_suggestion/views/sale_order_views.xml new file mode 100644 index 000000000..295818932 --- /dev/null +++ b/sale_loyalty_order_suggestion/views/sale_order_views.xml @@ -0,0 +1,22 @@ + + + + + sale.order + + + + + + + + + + + diff --git a/sale_loyalty_order_suggestion/wizard/__init__.py b/sale_loyalty_order_suggestion/wizard/__init__.py new file mode 100644 index 000000000..1ca11a9d4 --- /dev/null +++ b/sale_loyalty_order_suggestion/wizard/__init__.py @@ -0,0 +1 @@ +from . import sale_loyalty_reward_wizard diff --git a/sale_loyalty_order_suggestion/wizard/sale_loyalty_reward_wizard.py b/sale_loyalty_order_suggestion/wizard/sale_loyalty_reward_wizard.py new file mode 100644 index 000000000..0b2d4029e --- /dev/null +++ b/sale_loyalty_order_suggestion/wizard/sale_loyalty_reward_wizard.py @@ -0,0 +1,166 @@ +from odoo import _, api, fields, models + + +class SaleLoyaltyRewardWizard(models.TransientModel): + _inherit = "sale.loyalty.reward.wizard" + _description = "Sale Loyalty - Reward Selection Wizard" + + # To ensure whether the selected promotion satisfies your rules and is directly + # applicable or needs to satisfy your rules to be applied. + applicable_program = fields.Boolean( + compute="_compute_applicable_promotion", + default="True", + ) + loyalty_rule_line_ids = fields.One2many( + comodel_name="sale.loyalty.rule.product_line.wizard", + inverse_name="wizard_id", + compute="_compute_loyalty_rule_line_ids", + store=True, + readonly=False, + string="Loyalty Rule Lines", + ) + loyalty_rule_line_description = fields.Html( + compute="_compute_loyalty_rule_line_description" + ) + + @api.depends("reward_ids", "selected_reward_id") + def _compute_applicable_promotion(self): + self.order_id._update_programs_and_rewards() + claimable_rewards = self.order_id._get_claimable_rewards() + if self.selected_reward_id in claimable_rewards.values(): + self.applicable_program = True + else: + self.applicable_program = False + + @api.depends("selected_reward_id") + def _compute_loyalty_rule_line_ids(self): + units_required = min( + self.selected_reward_id.program_id.rule_ids.mapped("minimum_qty"), default=0 + ) + if ( + self.selected_reward_id + and not self.applicable_program + and units_required > 0 + ): + lines_vals = [] + products = [ + product + for rule in self.selected_reward_id.program_id.rule_ids + for product in rule._get_valid_products() + ] + for record in products: + units_included = self.order_id.order_line.filtered( + lambda x: x.product_id == record and not x.is_reward_line + ).product_uom_qty + lines_vals.append( + ( + 0, + 0, + { + "wizard_id": self.id, + "product_id": record.id, + "units_required": units_required, + "units_included": units_included or 0, + }, + ) + ) + self.loyalty_rule_line_ids = lines_vals + else: + self.loyalty_rule_line_ids = None + + @api.depends_context("lang") + @api.depends("selected_reward_id") + def _compute_loyalty_rule_line_description(self): + self.loyalty_rule_line_description = False + units_required = min( + self.selected_reward_id.program_id.rule_ids.mapped("minimum_qty"), default=0 + ) + if ( + self.selected_reward_id + and not self.applicable_program + and units_required > 0 + ): + products = self.loyalty_rule_line_ids.mapped("product_id.name") + if len(products) > 1: + products_str = f"{', '.join(products[:-1])} {_('or')} {products[-1]}" + else: + products_str = products[0] if products else "" + self.loyalty_rule_line_description = f"* {_('Required quantity')}: {units_required} {_('units of')} {products_str}" # noqa: B950 + + def _update_order_line_with_units(self, order_line, units): + """Updates an existing order line with the provided units.""" + order_line.write({"product_uom_qty": order_line.product_uom_qty + units}) + + def _create_new_order_line(self, product, units): + """Creates a new order line with the product and units provided.""" + self.order_id.order_line.create( + { + "order_id": self.order_id.id, + "product_id": product.id, + "product_uom_qty": units, + } + ) + + def action_apply(self): + for line in self.loyalty_rule_line_ids.filtered( + lambda x: x.units_to_include > 0 + ): + # Filter existing order lines based on the product of the loyalty rule line + order_line = self.order_id.order_line.filtered( + lambda x: x.product_id == line.product_id + ) + if order_line: + self._update_order_line_with_units(order_line, line.units_to_include) + else: + self._create_new_order_line(line.product_id, line.units_to_include) + self.order_id._update_programs_and_rewards() + super().action_apply() + return { + "type": "ir.actions.client", + "tag": "reload", + } + + +class SaleLoyaltyRuleProductLineWizard(models.TransientModel): + _name = "sale.loyalty.rule.product_line.wizard" + _description = "Sale Loyalty Rule Product Line Wizard" + + wizard_id = fields.Many2one(comodel_name="sale.loyalty.reward.wizard") + order_id = fields.Many2one(related="wizard_id.order_id", store=True) + company_id = fields.Many2one(related="order_id.company_id") + currency_id = fields.Many2one(related="order_id.currency_id") + product_id = fields.Many2one(comodel_name="product.product") + price_unit = fields.Float(string="Unit Price", compute="_compute_price_unit") + pricelist_id = fields.Many2one(related="wizard_id.order_id.pricelist_id") + units_included = fields.Float(string="Included units") + units_required = fields.Float(string="Required units") + units_to_include = fields.Float(string="Units to include") + + @api.depends("wizard_id") + def _compute_price_unit(self): + for record in self.filtered(lambda x: x.product_id): + pricelist_rule_id = record.pricelist_id._get_product_rule( + record.product_id, + 1.0, + uom=record.product_id.uom_id, + date=record.order_id.date_order, + ) + pricelist_rule = record.env["product.pricelist.item"].browse( + pricelist_rule_id + ) + price_rule = pricelist_rule._compute_price( + record.product_id, + record.units_included, + record.product_id.uom_id, + record.order_id.date_order, + currency=record.currency_id, + ) + record.price_unit = record.product_id._get_tax_included_unit_price( + record.order_id.company_id, + record.currency_id, + record.order_id.date_order, + "sale", + fiscal_position=record.order_id.fiscal_position_id, + product_price_unit=price_rule, + product_currency=record.currency_id, + ) diff --git a/sale_loyalty_order_suggestion/wizard/sale_loyalty_reward_wizard_views.xml b/sale_loyalty_order_suggestion/wizard/sale_loyalty_reward_wizard_views.xml new file mode 100644 index 000000000..1a8f941b2 --- /dev/null +++ b/sale_loyalty_order_suggestion/wizard/sale_loyalty_reward_wizard_views.xml @@ -0,0 +1,39 @@ + + + + sale.loyalty.reward.wizard + + + + + + + + + + + + + + + + + + + + + + diff --git a/setup/sale_loyalty_order_suggestion/odoo/addons/sale_loyalty_order_suggestion b/setup/sale_loyalty_order_suggestion/odoo/addons/sale_loyalty_order_suggestion new file mode 120000 index 000000000..c394394a7 --- /dev/null +++ b/setup/sale_loyalty_order_suggestion/odoo/addons/sale_loyalty_order_suggestion @@ -0,0 +1 @@ +../../../../sale_loyalty_order_suggestion \ No newline at end of file diff --git a/setup/sale_loyalty_order_suggestion/setup.py b/setup/sale_loyalty_order_suggestion/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/sale_loyalty_order_suggestion/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)