From d62b306b560dd954f71ddd1b5e361b0fed59d7d0 Mon Sep 17 00:00:00 2001 From: pilarvargas-tecnativa Date: Thu, 23 Nov 2023 12:10:55 +0100 Subject: [PATCH] [MIG] sale_coupon_multi_gift>sale_loyalty_multi_gift: Migration to 16.0 TT44348 --- sale_loyalty_multi_gift/README.rst | 19 +- sale_loyalty_multi_gift/__init__.py | 1 + sale_loyalty_multi_gift/__manifest__.py | 11 +- sale_loyalty_multi_gift/models/__init__.py | 2 +- .../models/coupon_program.py | 39 --- sale_loyalty_multi_gift/models/sale_order.py | 271 ++++++++---------- .../readme/CONTRIBUTORS.rst | 1 + .../security/ir.model.access.csv | 5 +- .../static/description/index.html | 15 +- sale_loyalty_multi_gift/tests/__init__.py | 2 +- ...ift.py => test_sale_loyalty_multi_gift.py} | 98 +++---- sale_loyalty_multi_gift/wizard/__init__.py | 1 + .../wizard/sale_loyalty_reward_wizard.py | 106 +++++++ .../sale_loyalty_reward_wizard_views.xml | 32 +++ .../odoo/addons/sale_coupon_multi_gift | 1 - .../odoo/addons/sale_loyalty_multi_gift | 1 + .../setup.py | 0 17 files changed, 336 insertions(+), 269 deletions(-) delete mode 100644 sale_loyalty_multi_gift/models/coupon_program.py rename sale_loyalty_multi_gift/tests/{test_sale_coupon_multi_gift.py => test_sale_loyalty_multi_gift.py} (53%) create mode 100644 sale_loyalty_multi_gift/wizard/__init__.py create mode 100644 sale_loyalty_multi_gift/wizard/sale_loyalty_reward_wizard.py create mode 100644 sale_loyalty_multi_gift/wizard/sale_loyalty_reward_wizard_views.xml delete mode 120000 setup/sale_coupon_multi_gift/odoo/addons/sale_coupon_multi_gift create mode 120000 setup/sale_loyalty_multi_gift/odoo/addons/sale_loyalty_multi_gift rename setup/{sale_coupon_multi_gift => sale_loyalty_multi_gift}/setup.py (100%) diff --git a/sale_loyalty_multi_gift/README.rst b/sale_loyalty_multi_gift/README.rst index c1fc069a5..9f9396728 100644 --- a/sale_loyalty_multi_gift/README.rst +++ b/sale_loyalty_multi_gift/README.rst @@ -1,13 +1,13 @@ -=========================== -Coupons multi gift in sales -=========================== +======================= +Sale Loyalty Multi Gift +======================= .. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:7f3497a24d35eeb0b15943041a8537f46b4eb32153e9451598ca64a9e5bf4e54 + !! source digest: sha256:214d59fbe6811aa1a74e43630c640fc5230b22769cefa87b7a3bba3eee355706 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png @@ -17,13 +17,13 @@ Coupons multi gift in sales :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/15.0/sale_coupon_multi_gift + :target: https://github.com/OCA/sale-promotion/tree/16.0/sale_loyalty_multi_gift :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-15-0/sale-promotion-15-0-sale_coupon_multi_gift + :target: https://translation.odoo-community.org/projects/sale-promotion-16-0/sale-promotion-16-0-sale_loyalty_multi_gift :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=15.0 + :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| @@ -56,7 +56,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. @@ -74,6 +74,7 @@ Contributors * `Tecnativa `_: * David Vidal + * Pilar Vargas * `Domatix `_: @@ -100,6 +101,6 @@ Current `maintainer `__: |maintainer-chienandalu| -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_multi_gift/__init__.py b/sale_loyalty_multi_gift/__init__.py index 0650744f6..9b4296142 100644 --- a/sale_loyalty_multi_gift/__init__.py +++ b/sale_loyalty_multi_gift/__init__.py @@ -1 +1,2 @@ from . import models +from . import wizard diff --git a/sale_loyalty_multi_gift/__manifest__.py b/sale_loyalty_multi_gift/__manifest__.py index a08745331..b57669d7d 100644 --- a/sale_loyalty_multi_gift/__manifest__.py +++ b/sale_loyalty_multi_gift/__manifest__.py @@ -1,15 +1,18 @@ # Copyright 2021 Tecnativa - David Vidal # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). { - "name": "Coupons multi gift in sales", + "name": "Sale Loyalty Multi Gift", "summary": "Allows to configure multiple gift rewards per promotion in sales", - "version": "15.0.1.0.0", + "version": "16.0.1.0.0", "development_status": "Beta", "category": "Sale", "website": "https://github.com/OCA/sale-promotion", "author": "Tecnativa, Odoo Community Association (OCA)", "maintainers": ["chienandalu"], "license": "AGPL-3", - "depends": ["sale_coupon_order_line_link", "coupon_multi_gift"], - "data": ["security/ir.model.access.csv"], + "depends": ["sale_loyalty_order_line_link", "loyalty_multi_gift"], + "data": [ + "security/ir.model.access.csv", + "wizard/sale_loyalty_reward_wizard_views.xml", + ], } diff --git a/sale_loyalty_multi_gift/models/__init__.py b/sale_loyalty_multi_gift/models/__init__.py index b1f0c3480..7578fe454 100644 --- a/sale_loyalty_multi_gift/models/__init__.py +++ b/sale_loyalty_multi_gift/models/__init__.py @@ -1,2 +1,2 @@ -from . import coupon_program +# from . import coupon_program from . import sale_order diff --git a/sale_loyalty_multi_gift/models/coupon_program.py b/sale_loyalty_multi_gift/models/coupon_program.py deleted file mode 100644 index 020dd6aad..000000000 --- a/sale_loyalty_multi_gift/models/coupon_program.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2021 Tecnativa - David Vidal -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -from odoo import models - - -class CouponProgram(models.Model): - _inherit = "coupon.program" - - def _compute_order_count(self): - """Relay on the order line link for these programs instead of the discount - products""" - multi_gift_programs = self.filtered(lambda x: x.reward_type == "multi_gift") - res = super(CouponProgram, self - multi_gift_programs)._compute_order_count() - for program in multi_gift_programs: - orders = self.env["sale.order.line"].read_group( - [ - ("state", "not in", ["draft", "sent", "cancel"]), - ("coupon_program_id", "=", program.id), - ], - ["order_id"], - ["order_id"], - ) - program.order_count = len(orders) - return res - - def action_view_sales_orders(self): - res = super().action_view_sales_orders() - if self.reward_type != "multi_gift": - return res - orders = ( - self.env["sale.order.line"] - .search([("coupon_program_id", "=", self.id)]) - .mapped("order_id") - ) - res["domain"] = [ - ("id", "in", orders.ids), - ("state", "not in", ["draft", "sent", "cancel"]), - ] - return res diff --git a/sale_loyalty_multi_gift/models/sale_order.py b/sale_loyalty_multi_gift/models/sale_order.py index 8ee67c712..150c5de75 100644 --- a/sale_loyalty_multi_gift/models/sale_order.py +++ b/sale_loyalty_multi_gift/models/sale_order.py @@ -1,186 +1,163 @@ # Copyright 2021 Tecnativa - David Vidal # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +import random + from odoo import _, fields, models -from odoo.fields import first +from odoo.exceptions import UserError +from odoo.fields import Command, first +from odoo.tools.float_utils import float_round class SaleOrder(models.Model): _inherit = "sale.order" - def _get_paid_order_lines(self): - """Add reward lines produced by multi gift promotions""" - lines = super()._get_paid_order_lines() - free_reward_products = ( - self.env["coupon.program"] - .search([("reward_type", "=", "multi_gift")]) - .mapped("coupon_multi_gift_ids.reward_product_ids") - ) - free_reward_product_lines = self.order_line.filtered( - lambda x: x.is_reward_line and x.product_id in free_reward_products - ) - return lines | free_reward_product_lines + def action_open_reward_wizard(self): + self.ensure_one() + self._update_programs_and_rewards() + claimable_rewards = self._get_claimable_rewards() + if ( + len(claimable_rewards) == 1 + and claimable_rewards.get(next(iter(claimable_rewards))).reward_type + == "multi_gift" + and any( + len(record.reward_product_ids) > 1 + for record in claimable_rewards.get( + next(iter(claimable_rewards)) + ).loyalty_multi_gift_ids + ) + ): + ctx = { + "default_selected_reward_id": claimable_rewards.get( + next(iter(claimable_rewards)) + ).id, + } + return { + "type": "ir.actions.act_window", + "view_mode": "form", + "res_model": "sale.loyalty.reward.wizard", + "target": "new", + "context": ctx, + } + else: + return super().action_open_reward_wizard() - def _get_reward_values_multi_gift_line(self, reward_line, program): + def _get_reward_values_multi_gift_line( + self, reward, coupon, cost, reward_line=False, product=False + ): """Multi Gift reward rules. For every gift reward rule, we'll create a new sale order line flagged as reward line with a 100% discount""" - - def _execute_onchanges(records, field_name): - """Helper methods that executes all onchanges associated to a field.""" - for onchange in records._onchange_methods.get(field_name, []): - for record in records: - onchange(record) - - # We could receive an optional product by context. Otherwise, the first product - # will apply. This feature can be used by modules like - # sale_coupon_selection_wizard. - optional_product = ( + self.ensure_one() + assert reward.reward_type == "multi_gift" + selected_product = ( self.env["product.product"].browse( self.env.context.get("reward_line_options", {}).get(reward_line.id) ) & reward_line.reward_product_ids ) - reward_product_id = optional_product or first(reward_line.reward_product_ids) - # We prepare a new line and trigger the proper onchanges to ensure we get the - # right line values (price unit according to the customer pricelist, taxes, ect) - order_line = self.order_line.new( - {"order_id": self.id, "product_id": reward_product_id.id} + reward_product_id = ( + selected_product or product or first(reward_line.reward_product_ids) ) - _execute_onchanges(order_line, "product_id") - order_line.update({"product_uom_qty": reward_line.reward_product_quantity}) - _execute_onchanges(order_line, "product_uom_qty") - vals = order_line._convert_to_write(order_line._cache) - vals.update( - { - "is_reward_line": True, - "name": _("Free Product") + " - " + reward_product_id.name, - "discount": 100, - "coupon_program_id": program.id, - "multi_gift_reward_line_id": reward_line.id, - "multi_gift_reward_line_id_option_product_id": reward_product_id.id, - } + if ( + not reward_product_id + or reward_product_id not in reward.loyalty_multi_gift_ids.reward_product_ids + ): + raise UserError(_("Invalid product to claim.")) + taxes = self.fiscal_position_id.map_tax( + reward_product_id.taxes_id.filtered( + lambda t: t.company_id == self.company_id + ) ) + vals = { + "order_id": self.id, + "is_reward_line": True, + "name": _( + "Free Product - %(product)s", + product=reward_product_id.with_context( + display_default_code=False + ).display_name, + ), + "product_id": reward_product_id.id, + "price_unit": reward_product_id.list_price, + "discount": 100, + "product_uom_qty": reward_line.reward_product_quantity, + "reward_id": reward.id, + "coupon_id": coupon.id, + "points_cost": cost, + "reward_identifier_code": str(random.getrandbits(32)), + "product_uom": reward_product_id.uom_id.id, + "sequence": max( + self.order_line.filtered(lambda x: not x.is_reward_line).mapped( + "sequence" + ), + default=10, + ) + + 1, + "tax_id": [(Command.CLEAR, 0, 0)] + + [(Command.LINK, tax.id, False) for tax in taxes], + "loyalty_program_id": reward.program_id.id, + "multi_gift_reward_line_id": reward_line.id, + "multi_gift_reward_line_id_option_product_id": reward_product_id.id, + } return vals - def _get_reward_values_multi_gift(self, program): + def _get_reward_values_multi_gift(self, reward, coupon, **kwargs): """Wrapper to create the reward lines for a multi gift promotion""" - return [ - self._get_reward_values_multi_gift_line(reward_line, program) - for reward_line in program.coupon_multi_gift_ids - ] - - def _get_reward_line_values(self, program): - """Hook into the core method considering multi gift rewards""" - self.ensure_one() - self = self.with_context(lang=self.partner_id.lang) - program = program.with_context(lang=self.partner_id.lang) - if program.reward_type == "multi_gift": - return self._get_reward_values_multi_gift(program) - return super()._get_reward_line_values(program) - - def _get_applicable_programs_multi_gift(self): - """Wrapper to avoid long method name limitations""" - programs = ( - self._get_applicable_programs() + self._get_valid_applied_coupon_program() - ) - programs = ( - programs._keep_only_most_interesting_auto_applied_global_discount_program() + points = self._get_real_points_for_coupon(coupon) + claimable_count = ( + float_round( + points / reward.required_points, + precision_rounding=1, + rounding_method="DOWN", + ) + if not reward.clear_wallet + else 1 ) - return programs - - def _remove_invalid_reward_lines(self): - """We have to put some logic redundancy here as the main method doesn't have - enough granularity to avoid deleting the lines belonging to the multi gift - programs when the promotions are updated. So the main module expects that the - promotion lines products match with the promotion discount product - (https://git.io/JWpoU) , which is not the approach in this module, where we add - extra lines with the reward products themselves and the proper price tag and - discount. So in this method override, we'll save those correct lines from the - pyre via context that the unlink method will properly catch. We also have to - remove the proper invalid lines that wouldn't be detected""" - self.ensure_one() - # This part is a repetition of the logic so we can get the right programs - applied_programs = self._get_applied_programs() - applicable_programs = self.env["coupon.program"] - if applied_programs: - applicable_programs = self._get_applicable_programs_multi_gift() - programs_to_remove = applied_programs - applicable_programs - # We're only interested in the Multi Gift programs - multi_gift_applied_programs = applied_programs.filtered( - lambda x: x.reward_type == "multi_gift" + total_cost = ( + points if reward.clear_wallet else claimable_count * reward.required_points ) - # These will be the ones to keep - valid_lines = self.order_line.filtered( + cost = total_cost / len(reward.loyalty_multi_gift_ids) + order_lines = self.order_line.filtered( lambda x: x.is_reward_line - and x.coupon_program_id in multi_gift_applied_programs - ) - multi_gift_programs_to_remove = programs_to_remove.filtered( - lambda x: x.reward_type == "multi_gift" + and x.reward_id.reward_type == "multi_gift" + and x.coupon_id == coupon ) - if multi_gift_programs_to_remove: - # Invalidate the generated coupons which we are not eligible anymore - self.generated_coupon_ids.filtered( - lambda x: x.program_id in multi_gift_programs_to_remove - ).write({"state": "expired"}) - # Detect and remove the proper unvalid program order lines - self.order_line.filtered( - lambda x: x.is_reward_line - and x.coupon_program_id in multi_gift_programs_to_remove - ).unlink() - # We'll catch the context in the subsequent unlink() method - return super( - SaleOrder, self.with_context(valid_multi_gift_lines=valid_lines.ids) - )._remove_invalid_reward_lines() + if order_lines: + return [ + self._get_reward_values_multi_gift_line( + reward, + coupon, + cost, + reward_line=reward_line.multi_gift_reward_line_id, + product=reward_line.multi_gift_reward_line_id_option_product_id, + ) + for reward_line in order_lines + ] + else: + return [ + self._get_reward_values_multi_gift_line( + reward, coupon, cost, reward_line=reward_line + ) + for reward_line in reward.loyalty_multi_gift_ids + ] - def _update_existing_reward_lines(self): - """We need to match `multi gift` programs with their discount product""" + def _get_reward_line_values(self, reward, coupon, **kwargs): + """Hook into the core method considering multi gift rewards""" self.ensure_one() - res = super( - SaleOrder, self.with_context(only_reward_lines=True) - )._update_existing_reward_lines() - applied_programs = self._get_applied_programs_with_rewards_on_current_order() - for program in applied_programs.filtered( - lambda x: x.reward_type == "multi_gift" - ): - for reward_line in program.coupon_multi_gift_ids: - lines = self.order_line.filtered( - lambda line: line.multi_gift_reward_line_id == reward_line - and line.is_reward_line - and line.coupon_program_id == program - ) - applied_product = lines.multi_gift_reward_line_id_option_product_id - for product in applied_product: - reward_line_options = {reward_line.id: product.id} - values = self.with_context( - reward_line_options=reward_line_options - )._get_reward_values_multi_gift_line(reward_line, program) - # Remove reward line if price or qty equal to 0 - if values.get("product_uom_qty") and values.get("price_unit"): - lines.write(values) - else: - lines.unlink() - return res + self = self.with_context(lang=self.partner_id.lang) + reward = reward.with_context(lang=self.partner_id.lang) + if reward.reward_type == "multi_gift": + return self._get_reward_values_multi_gift(reward, coupon, **kwargs) + return super()._get_reward_line_values(reward, coupon, **kwargs) class SaleOrderLine(models.Model): _inherit = "sale.order.line" multi_gift_reward_line_id = fields.Many2one( - comodel_name="coupon.reward.product_line", + comodel_name="loyalty.reward.product_line", readonly=True, ) multi_gift_reward_line_id_option_product_id = fields.Many2one( comodel_name="product.product", readonly=True, ) - - def unlink(self): - """Avoid unlinking valid multi gift lines since they aren't linked to the - discount product of the promotion program""" - if not self.env.context.get("valid_multi_gift_lines"): - return super().unlink() - return super( - SaleOrderLine, - self.filtered( - lambda x: x.id not in self.env.context.get("valid_multi_gift_lines") - ), - ).unlink() diff --git a/sale_loyalty_multi_gift/readme/CONTRIBUTORS.rst b/sale_loyalty_multi_gift/readme/CONTRIBUTORS.rst index 994869129..06c2608cd 100644 --- a/sale_loyalty_multi_gift/readme/CONTRIBUTORS.rst +++ b/sale_loyalty_multi_gift/readme/CONTRIBUTORS.rst @@ -1,6 +1,7 @@ * `Tecnativa `_: * David Vidal + * Pilar Vargas * `Domatix `_: diff --git a/sale_loyalty_multi_gift/security/ir.model.access.csv b/sale_loyalty_multi_gift/security/ir.model.access.csv index 502d309e9..24817d088 100644 --- a/sale_loyalty_multi_gift/security/ir.model.access.csv +++ b/sale_loyalty_multi_gift/security/ir.model.access.csv @@ -1,3 +1,4 @@ id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink -access_multi_gift_salesman,salesman,coupon_multi_gift.model_coupon_reward_product_line,sales_team.group_sale_salesman,1,0,0,0 -access_multi_gift_manager,multi_gift manager,coupon_multi_gift.model_coupon_reward_product_line,sales_team.group_sale_manager,1,1,1,1 +access_multi_gift_salesman,salesman,loyalty_multi_gift.model_loyalty_reward_product_line,sales_team.group_sale_salesman,1,0,0,0 +access_multi_gift_manager,multi_gift manager,loyalty_multi_gift.model_loyalty_reward_product_line,sales_team.group_sale_manager,1,1,1,1 +sale_loyalty_multi_gift.access_sale_loyalty_reward_product_line_wizard,access_sale_loyalty_reward_product_line_wizard,sale_loyalty_multi_gift.model_sale_loyalty_reward_product_line_wizard,base.group_user,1,1,1,1 diff --git a/sale_loyalty_multi_gift/static/description/index.html b/sale_loyalty_multi_gift/static/description/index.html index fc57a1328..4c17d5e01 100644 --- a/sale_loyalty_multi_gift/static/description/index.html +++ b/sale_loyalty_multi_gift/static/description/index.html @@ -4,7 +4,7 @@ -Coupons multi gift in sales +Sale Loyalty Multi Gift -
-

Coupons multi gift in sales

+
+

Sale Loyalty Multi Gift

-

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

+

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

This module allows to define multiple reward products on promotions on sale orders.

Table of contents

@@ -405,7 +405,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.

@@ -421,6 +421,7 @@

Contributors

diff --git a/sale_loyalty_multi_gift/tests/__init__.py b/sale_loyalty_multi_gift/tests/__init__.py index e9e2e2373..34744a87c 100644 --- a/sale_loyalty_multi_gift/tests/__init__.py +++ b/sale_loyalty_multi_gift/tests/__init__.py @@ -1 +1 @@ -from . import test_sale_coupon_multi_gift +from . import test_sale_loyalty_multi_gift diff --git a/sale_loyalty_multi_gift/tests/test_sale_coupon_multi_gift.py b/sale_loyalty_multi_gift/tests/test_sale_loyalty_multi_gift.py similarity index 53% rename from sale_loyalty_multi_gift/tests/test_sale_coupon_multi_gift.py rename to sale_loyalty_multi_gift/tests/test_sale_loyalty_multi_gift.py index 1a453a7f5..8be3e05b4 100644 --- a/sale_loyalty_multi_gift/tests/test_sale_coupon_multi_gift.py +++ b/sale_loyalty_multi_gift/tests/test_sale_loyalty_multi_gift.py @@ -1,13 +1,14 @@ # Copyright 2021 Tecnativa - David Vidal +# Copyright 2023 Tecnativa - David Vidal # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from odoo.tests import Form -from odoo.addons.coupon_multi_gift.tests.coupon_multi_gift_case import ( - CouponMultiGiftCase, +from odoo.addons.loyalty_multi_gift.tests.test_loyalty_multi_gift_case import ( + LoyaltyMultiGiftCase, ) -class TestSaleCouponMultiGift(CouponMultiGiftCase): +class TestSaleLoyaltyMultiGift(LoyaltyMultiGiftCase): @classmethod def setUpClass(cls): super().setUpClass() @@ -18,10 +19,21 @@ def setUpClass(cls): line_form.product_uom_qty = 2 cls.sale = sale_form.save() - def test_sale_coupon_test_multi_gift(self): + def _action_apply_program(self, sale, program, reward_line_options=False): + sale._update_programs_and_rewards() + wizard = ( + self.env["sale.loyalty.reward.wizard"] + .with_context(active_id=sale) + .create({"selected_reward_id": program.reward_ids[0].id}) + ) + if reward_line_options: + for line in wizard.loyalty_gift_line_ids: + line.selected_gift_id = reward_line_options.get(line.line_id.id) + wizard.action_apply() + + def test_01_sale_coupon_test_multi_gift(self): """As we fulfill the proper product qties, we get the proper free product""" - line = self.sale.order_line - self.sale.recompute_coupon_lines() + self._action_apply_program(self.sale, self.loyalty_program_form) # As set up, we should one discount line for every reward line in the promotion discount_line_product_2 = self.sale.order_line.filtered( lambda x: x.product_id == self.product_2 and x.is_reward_line @@ -35,10 +47,13 @@ def test_sale_coupon_test_multi_gift(self): self.assertEqual(0, discount_line_product_3.price_reduce) self.assertEqual(60, discount_line_product_2.price_unit) self.assertEqual(70, discount_line_product_3.price_unit) + + def test_02_test_sale_coupon_test_multi_gift(self): + line = self.sale.order_line line_form = Form(line, view="sale.view_order_line_tree") line_form.product_uom_qty = 7 line_form.save() - self.sale.recompute_coupon_lines() + self._action_apply_program(self.sale, self.loyalty_program_form) discount_line_product_2 = self.sale.order_line.filtered( lambda x: x.product_id == self.product_2 and x.is_reward_line ) @@ -50,22 +65,33 @@ def test_sale_coupon_test_multi_gift(self): self.assertEqual(3, discount_line_product_3.product_uom_qty) self.assertEqual(0, discount_line_product_2.price_reduce) self.assertEqual(0, discount_line_product_3.price_reduce) + + def test_03_test_sale_coupon_test_multi_gift(self): + line = self.sale.order_line + line_form = Form(line, view="sale.view_order_line_tree") # Now it can't be applied anymore so the discount lines will dissapear line_form.product_uom_qty = 1 line_form.save() - self.sale.recompute_coupon_lines() + self.sale._update_programs_and_rewards() discount_line = self.sale.order_line.filtered("is_reward_line") # The discount goes away self.assertFalse(bool(discount_line)) + + def test_04_test_sale_coupon_test_multi_gift(self): # Optional rewards - line_form.product_uom_qty = 2 - line_form.save() reward_line_options = { - self.coupon_program.coupon_multi_gift_ids[1].id: self.product_4.id + self.loyalty_program_form.reward_ids[0] + .loyalty_multi_gift_ids[0] + .id: self.product_2.id, + self.loyalty_program_form.reward_ids[0] + .loyalty_multi_gift_ids[1] + .id: self.product_4.id, } - self.sale.with_context( - reward_line_options=reward_line_options - ).recompute_coupon_lines() + self._action_apply_program( + self.sale, + self.loyalty_program_form, + reward_line_options=reward_line_options, + ) discount_line_product_2 = self.sale.order_line.filtered( lambda x: x.product_id == self.product_2 and x.is_reward_line ) @@ -78,47 +104,3 @@ def test_sale_coupon_test_multi_gift(self): self.assertEqual(2, discount_line_product_2.product_uom_qty) self.assertEqual(3, discount_line_product_4.product_uom_qty) self.assertFalse(discount_line_product_3) - # The original gift options are kept - self.sale.recompute_coupon_lines() - discount_line_product_4 = self.sale.order_line.filtered( - lambda x: x.product_id == self.product_4 and x.is_reward_line - ) - self.assertEqual(3, discount_line_product_4.product_uom_qty) - self.assertEqual( - discount_line_product_4.multi_gift_reward_line_id_option_product_id, - self.product_4, - "The product should be kept after updating recomputing the promotions", - ) - # The regular options are ok as well - discount_line_product_2 = self.sale.order_line.filtered( - lambda x: x.product_id == self.product_2 and x.is_reward_line - ) - discount_line_product_3 = self.sale.order_line.filtered( - lambda x: x.product_id == self.product_3 and x.is_reward_line - ) - self.assertEqual(2, discount_line_product_2.product_uom_qty) - self.assertEqual(3, discount_line_product_4.product_uom_qty) - - def test_sale_coupon_multi_gift_count(self): - """We have to count the orders in a different manner than the core method""" - self.assertEqual(self.coupon_program.order_count, 0) - self.sale.recompute_coupon_lines() - self.sale.action_confirm() - self.coupon_program._compute_order_count() - self.assertEqual(self.coupon_program.order_count, 1) - # Let's place a second order - sale_form = Form(self.env["sale.order"]) - sale_form.partner_id = self.partner - with sale_form.order_line.new() as line_form: - line_form.product_id = self.product_1 - line_form.product_uom_qty = 2 - sale_2 = sale_form.save() - sale_2.recompute_coupon_lines() - sale_2.action_confirm() - self.coupon_program._compute_order_count() - self.assertEqual(self.coupon_program.order_count, 2) - # We should get our order when we click in the orders smart button - action_domain = self.coupon_program.action_view_sales_orders()["domain"] - self.assertEqual( - self.env["sale.order"].search(action_domain), self.sale + sale_2 - ) diff --git a/sale_loyalty_multi_gift/wizard/__init__.py b/sale_loyalty_multi_gift/wizard/__init__.py new file mode 100644 index 000000000..1ca11a9d4 --- /dev/null +++ b/sale_loyalty_multi_gift/wizard/__init__.py @@ -0,0 +1 @@ +from . import sale_loyalty_reward_wizard diff --git a/sale_loyalty_multi_gift/wizard/sale_loyalty_reward_wizard.py b/sale_loyalty_multi_gift/wizard/sale_loyalty_reward_wizard.py new file mode 100644 index 000000000..b29947ef8 --- /dev/null +++ b/sale_loyalty_multi_gift/wizard/sale_loyalty_reward_wizard.py @@ -0,0 +1,106 @@ +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class SaleLoyaltyRewardWizard(models.TransientModel): + _inherit = "sale.loyalty.reward.wizard" + + # In case of multi_gift reward + multi_gift_reward = fields.Boolean(related="selected_reward_id.multi_gift") + loyalty_multi_gift_ids = fields.One2many( + related="selected_reward_id.loyalty_multi_gift_ids" + ) + loyalty_gift_line_ids = fields.One2many( + comodel_name="sale.loyalty.reward.product_line.wizard", + inverse_name="wizard_id", + compute="_compute_loyalty_gift_line_ids", + store=True, + readonly=False, + string="Loyalty Gift Lines", + ) + + @api.depends("selected_reward_id") + def _compute_loyalty_gift_line_ids(self): + lines_vals = [] + if self.selected_reward_id.reward_type != "multi_gift": + self.loyalty_gift_line_ids = None + else: + for line in self.loyalty_multi_gift_ids: + lines_vals.append( + ( + 0, + 0, + { + "wizard_id": self.id, + "reward_id": self.selected_reward_id.id, + "line_id": line.id, + }, + ) + ) + self.loyalty_gift_line_ids = lines_vals + + def action_apply(self): + self.ensure_one() + if not self.selected_reward_id: + raise ValidationError(_("No reward selected.")) + claimable_rewards = self.order_id._get_claimable_rewards() + selected_coupon = False + for coupon, rewards in claimable_rewards.items(): + if self.selected_reward_id in rewards: + selected_coupon = coupon + break + if not selected_coupon: + raise ValidationError( + _( + "Coupon not found while trying to add the following reward: %s", + self.selected_reward_id.description, + ) + ) + if self.selected_reward_id.reward_type == "multi_gift": + reward_line_options = {} + for line in self.loyalty_gift_line_ids: + reward_line_options.update({line.line_id.id: line.selected_gift_id.id}) + order = self.env["sale.order"].browse(self.order_id.id) + order.with_context( + reward_line_options=reward_line_options + )._apply_program_reward(self.selected_reward_id, coupon) + order.with_context( + reward_line_options=reward_line_options + )._update_programs_and_rewards() + else: + return super().action_apply() + return True + + +class SaleLoyaltyRewardProductLineWizard(models.TransientModel): + _name = "sale.loyalty.reward.product_line.wizard" + _description = "Wizard for Managing Loyalty Reward Product Options" + + wizard_id = fields.Many2one(comodel_name="sale.loyalty.reward.wizard") + reward_id = fields.Many2one(related="wizard_id.selected_reward_id", store=True) + line_id = fields.Many2one(comodel_name="loyalty.reward.product_line") + gift_ids = fields.Many2many(related="line_id.reward_product_ids") + gift_options_description = fields.Text( + compute="_compute_gift_options_description", string="Gift Options" + ) + selected_gift_id = fields.Many2one( + comodel_name="product.product", + domain="[('id', 'in', gift_ids)]", + compute="_compute_selected_gift_id", + readonly=False, + store=True, + ) + + @api.depends("gift_ids") + def _compute_gift_options_description(self): + for record in self: + products = ", ".join(record.gift_ids._origin.mapped("display_name")) + record.gift_options_description = f"Choose between: {products}" + + @api.depends("gift_ids", "reward_id") + def _compute_selected_gift_id(self): + for wizard in self: + if not wizard.wizard_id.selected_reward_id.reward_type == "multi_gift": + wizard.selected_gift_id = None + else: + wizard.selected_gift_id = wizard.gift_ids[:1] diff --git a/sale_loyalty_multi_gift/wizard/sale_loyalty_reward_wizard_views.xml b/sale_loyalty_multi_gift/wizard/sale_loyalty_reward_wizard_views.xml new file mode 100644 index 000000000..bab709bd8 --- /dev/null +++ b/sale_loyalty_multi_gift/wizard/sale_loyalty_reward_wizard_views.xml @@ -0,0 +1,32 @@ + + + + sale.loyalty.reward.wizard + + + + + + + + + + + + + + + + + diff --git a/setup/sale_coupon_multi_gift/odoo/addons/sale_coupon_multi_gift b/setup/sale_coupon_multi_gift/odoo/addons/sale_coupon_multi_gift deleted file mode 120000 index e283a9421..000000000 --- a/setup/sale_coupon_multi_gift/odoo/addons/sale_coupon_multi_gift +++ /dev/null @@ -1 +0,0 @@ -../../../../sale_coupon_multi_gift \ No newline at end of file diff --git a/setup/sale_loyalty_multi_gift/odoo/addons/sale_loyalty_multi_gift b/setup/sale_loyalty_multi_gift/odoo/addons/sale_loyalty_multi_gift new file mode 120000 index 000000000..7a23f9b7f --- /dev/null +++ b/setup/sale_loyalty_multi_gift/odoo/addons/sale_loyalty_multi_gift @@ -0,0 +1 @@ +../../../../sale_loyalty_multi_gift \ No newline at end of file diff --git a/setup/sale_coupon_multi_gift/setup.py b/setup/sale_loyalty_multi_gift/setup.py similarity index 100% rename from setup/sale_coupon_multi_gift/setup.py rename to setup/sale_loyalty_multi_gift/setup.py