diff --git a/sale_coupon_promotion_generate_coupon/README.rst b/sale_coupon_promotion_generate_coupon/README.rst new file mode 100644 index 000000000..513efc568 --- /dev/null +++ b/sale_coupon_promotion_generate_coupon/README.rst @@ -0,0 +1,91 @@ +========================================== +Generate coupons in another coupon program +========================================== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |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/13.0/sale_coupon_promotion_generate_coupon + :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-13-0/sale-promotion-13-0-sale_coupon_promotion_generate_coupon + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/296/13.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module allows to generate pending coupons from a promotion to another one so the +rules to apply the coupon could be different from the ones to generate it. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +To use this module, you need to have configured: + +#. A coupon program which will be the one where the coupons will be generated. +#. A promotion program with applicability on the next order and the first program + selected as next order program. + +Then, when a user meets the promotion program requisites, a pending coupon will be +generated in the coupon program. + +The user will be able to enjoy it in a later order as usual but the rules will be those +configured in that promotion. + +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 smashing 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 `_: + + * David Vidal + +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. + +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_coupon_promotion_generate_coupon/__init__.py b/sale_coupon_promotion_generate_coupon/__init__.py new file mode 100644 index 000000000..0650744f6 --- /dev/null +++ b/sale_coupon_promotion_generate_coupon/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/sale_coupon_promotion_generate_coupon/__manifest__.py b/sale_coupon_promotion_generate_coupon/__manifest__.py new file mode 100644 index 000000000..0af891f2f --- /dev/null +++ b/sale_coupon_promotion_generate_coupon/__manifest__.py @@ -0,0 +1,13 @@ +# Copyright 2023 Tecnativa - David Vidal +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +{ + "name": "Generate coupons in another coupon program", + "summary": "Allows to generate pending coupons in a coupon program", + "version": "13.0.1.0.0", + "author": "Tecnativa, Odoo Community Association (OCA)", + "license": "AGPL-3", + "category": "Sales Management", + "website": "https://github.com/OCA/sale-promotion", + "depends": ["sale_coupon"], + "data": ["views/coupon_program_views.xml"], +} diff --git a/sale_coupon_promotion_generate_coupon/models/__init__.py b/sale_coupon_promotion_generate_coupon/models/__init__.py new file mode 100644 index 000000000..b1f0c3480 --- /dev/null +++ b/sale_coupon_promotion_generate_coupon/models/__init__.py @@ -0,0 +1,2 @@ +from . import coupon_program +from . import sale_order diff --git a/sale_coupon_promotion_generate_coupon/models/coupon_program.py b/sale_coupon_promotion_generate_coupon/models/coupon_program.py new file mode 100644 index 000000000..680c47b17 --- /dev/null +++ b/sale_coupon_promotion_generate_coupon/models/coupon_program.py @@ -0,0 +1,46 @@ +# Copyright 2023 Tecnativa - David Vidal +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo import api, fields, models + + +class SaleCouponProgram(models.Model): + _inherit = "sale.coupon.program" + + next_order_program_id = fields.Many2one( + comodel_name="sale.coupon.program", + domain=[("program_type", "=", "coupon_program")], + ) + + @api.onchange("promo_applicability") + def _onchange_promo_applicability(self): + """Remove the destination program on as all would behave in unexpected ways""" + if self.promo_applicability == "on_current_order": + self.next_order_program_id = False + + def _compute_coupon_count(self): + """Hook the destination program coupon counter""" + next_programs = self.filtered("next_order_program_id") + for program in next_programs: + program.coupon_count = program.next_order_program_id.coupon_count + return super(SaleCouponProgram, (self - next_programs))._compute_coupon_count() + + def _compute_order_count(self): + """Hook the destination program sales counter""" + next_programs = self.filtered("next_order_program_id") + for program in next_programs: + program.order_count = program.next_order_program_id.order_count + return super(SaleCouponProgram, (self - next_programs))._compute_order_count() + + def action_next_order_program_coupons(self): + """Hook the destination program coupon action""" + action = self.env["ir.actions.act_window"].for_xml_id( + "sale_coupon", "sale_coupon_action" + ) + action["domain"] = [("program_id", "=", self.next_order_program_id.id)] + return action + + def action_view_sales_orders(self): + """Hook the destination program sales action""" + if self.next_order_program_id: + return self.next_order_program_id.action_view_sales_orders() + return super().action_view_sales_orders() diff --git a/sale_coupon_promotion_generate_coupon/models/sale_order.py b/sale_coupon_promotion_generate_coupon/models/sale_order.py new file mode 100644 index 000000000..5fc769321 --- /dev/null +++ b/sale_coupon_promotion_generate_coupon/models/sale_order.py @@ -0,0 +1,32 @@ +# Copyright 2023 Tecnativa - David Vidal +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo import models + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + def _create_reward_coupon(self, program): + """Check if the program is set to generate the coupon in other promotion""" + if program.next_order_program_id: + program = program.next_order_program_id + return super()._create_reward_coupon(program) + + def _remove_invalid_reward_lines(self): + """Ensure that the generated coupons for the next program are expired. The + original method relies on the original promo discount product ids so we + have to take an alternative approach expiring those remaining generated + coupons with no applied promo""" + res = super()._remove_invalid_reward_lines() + applied_programs = ( + self.no_code_promo_program_ids + + self.code_promo_program_id + + self.applied_coupon_ids.mapped("program_id") + ) + applied_programs += applied_programs.next_order_program_id + not_applicable_generated_coupons = self.generated_coupon_ids.filtered( + lambda x: x.program_id not in applied_programs + ) + not_applicable_generated_coupons.write({"state": "expired"}) + self.generated_coupon_ids -= not_applicable_generated_coupons + return res diff --git a/sale_coupon_promotion_generate_coupon/readme/CONTRIBUTORS.rst b/sale_coupon_promotion_generate_coupon/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..94b6ba953 --- /dev/null +++ b/sale_coupon_promotion_generate_coupon/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +* `Tecnativa `_: + + * David Vidal diff --git a/sale_coupon_promotion_generate_coupon/readme/DESCRIPTION.rst b/sale_coupon_promotion_generate_coupon/readme/DESCRIPTION.rst new file mode 100644 index 000000000..29b11fcd4 --- /dev/null +++ b/sale_coupon_promotion_generate_coupon/readme/DESCRIPTION.rst @@ -0,0 +1,2 @@ +This module allows to generate pending coupons from a promotion to another one so the +rules to apply the coupon could be different from the ones to generate it. diff --git a/sale_coupon_promotion_generate_coupon/readme/USAGE.rst b/sale_coupon_promotion_generate_coupon/readme/USAGE.rst new file mode 100644 index 000000000..9601ff42a --- /dev/null +++ b/sale_coupon_promotion_generate_coupon/readme/USAGE.rst @@ -0,0 +1,11 @@ +To use this module, you need to have configured: + +#. A coupon program which will be the one where the coupons will be generated. +#. A promotion program with applicability on the next order and the first program + selected as next order program. + +Then, when a user meets the promotion program requisites, a pending coupon will be +generated in the coupon program. + +The user will be able to enjoy it in a later order as usual but the rules will be those +configured in that promotion. diff --git a/sale_coupon_promotion_generate_coupon/static/description/icon.png b/sale_coupon_promotion_generate_coupon/static/description/icon.png new file mode 100644 index 000000000..e0eb4b125 Binary files /dev/null and b/sale_coupon_promotion_generate_coupon/static/description/icon.png differ diff --git a/sale_coupon_promotion_generate_coupon/static/description/index.html b/sale_coupon_promotion_generate_coupon/static/description/index.html new file mode 100644 index 000000000..cbae4add7 --- /dev/null +++ b/sale_coupon_promotion_generate_coupon/static/description/index.html @@ -0,0 +1,437 @@ + + + + + + +Generate coupons in another coupon program + + + +
+

Generate coupons in another coupon program

+ + +

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

+

This module allows to generate pending coupons from a promotion to another one so the +rules to apply the coupon could be different from the ones to generate it.

+

Table of contents

+ +
+

Usage

+

To use this module, you need to have configured:

+
    +
  1. A coupon program which will be the one where the coupons will be generated.
  2. +
  3. A promotion program with applicability on the next order and the first program +selected as next order program.
  4. +
+

Then, when a user meets the promotion program requisites, a pending coupon will be +generated in the coupon program.

+

The user will be able to enjoy it in a later order as usual but the rules will be those +configured in that promotion.

+
+
+

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 smashing it by providing a detailed and welcomed +feedback.

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • Tecnativa
  • +
+
+
+

Contributors

+ +
+
+

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.

+

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_coupon_promotion_generate_coupon/tests/__init__.py b/sale_coupon_promotion_generate_coupon/tests/__init__.py new file mode 100644 index 000000000..c964e22ef --- /dev/null +++ b/sale_coupon_promotion_generate_coupon/tests/__init__.py @@ -0,0 +1 @@ +from . import test_sale_coupon_promotion_generate_coupon diff --git a/sale_coupon_promotion_generate_coupon/tests/test_sale_coupon_promotion_generate_coupon.py b/sale_coupon_promotion_generate_coupon/tests/test_sale_coupon_promotion_generate_coupon.py new file mode 100644 index 000000000..cb1adead8 --- /dev/null +++ b/sale_coupon_promotion_generate_coupon/tests/test_sale_coupon_promotion_generate_coupon.py @@ -0,0 +1,88 @@ +# Copyright 2023 Tecnativa - David Vidal +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo.exceptions import UserError +from odoo.tests import Form, SavepointCase + + +class TestSaleCouponGenerateCoupon(SavepointCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.partner = cls.env["res.partner"].create({"name": "Mr. Odoo"}) + cls.other_partner = cls.partner.copy({"name": "Mr. OCA"}) + cls.product_1 = cls.env["product.product"].create( + {"name": "Test 1", "sale_ok": True, "list_price": 50} + ) + cls.product_2 = cls.env["product.product"].create( + {"name": "Test 2", "sale_ok": True, "list_price": 50} + ) + coupon_program_form = Form( + cls.env["sale.coupon.program"], + view="sale_coupon.sale_coupon_program_view_form", + ) + coupon_program_form.name = "Test coupon program with generated coupons" + coupon_program_form.rule_products_domain = [("id", "=", cls.product_2.id)] + cls.coupon_program = coupon_program_form.save() + promotion_program_form = Form( + cls.env["sale.coupon.program"], + view="sale_coupon.sale_coupon_program_view_promo_program_form", + ) + promotion_program_form.name = "Test program with coupon generation conditions" + promotion_program_form.promo_applicability = "on_next_order" + promotion_program_form.rule_products_domain = [("id", "=", cls.product_1.id)] + promotion_program_form.next_order_program_id = cls.coupon_program + promotion_program_form.promo_code_usage = "no_code_needed" + cls.promotion_program = promotion_program_form.save() + 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_1 + line_form.product_uom_qty = 2 + cls.sale = sale_form.save() + cls.sale.recompute_coupon_lines() + + def test_sale_coupon_promotion_generate_coupon(self): + self.assertEqual( + self.coupon_program.coupon_ids, + self.sale.generated_coupon_ids, + "A coupon should be generated in the coupon program and linked in the sale", + ) + self.assertEqual( + self.promotion_program, + self.sale.no_code_promo_program_ids, + "The coupon generator program should be linked to the sales order", + ) + self.assertEqual( + 1, + self.promotion_program.coupon_count, + "The coupon counter should be updated in the coupon generator program", + ) + self.sale.action_confirm() + # Let's use the generate coupon, which can have a different rules set + 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() + apply_coupon = ( + self.env["sale.coupon.apply.code"] + .with_context(active_id=sale_2.id) + .create({"coupon_code": self.sale.generated_coupon_ids.code}) + ) + # The rules of the coupon program don't fit this sale order + with self.assertRaises(UserError): + apply_coupon.process_coupon() + sale_2.order_line.product_id = self.product_2 + # Now we're talkin :) + apply_coupon.process_coupon() + self.assertEqual( + sale_2.applied_coupon_ids, + self.sale.generated_coupon_ids, + "The applied coupon should be linked to the sales order", + ) + self.assertEqual( + 1, + self.promotion_program.order_count, + "We should get the order count in the coupon generator promotion program", + ) diff --git a/sale_coupon_promotion_generate_coupon/views/coupon_program_views.xml b/sale_coupon_promotion_generate_coupon/views/coupon_program_views.xml new file mode 100644 index 000000000..75e03d272 --- /dev/null +++ b/sale_coupon_promotion_generate_coupon/views/coupon_program_views.xml @@ -0,0 +1,69 @@ + + + + sale.coupon.program + + + + + + + + + The generated coupons will follow the rules defined in the + Next Order Program + + + + + {'invisible': [('next_order_program_id', '!=', False)]} + + + + + + + + + sale.coupon.program + + + + + + + {'invisible': [('next_order_program_id', '!=', False)]} + + + {'invisible': [('next_order_program_id', '!=', False)]} + + + + diff --git a/setup/sale_coupon_promotion_generate_coupon/odoo/addons/sale_coupon_promotion_generate_coupon b/setup/sale_coupon_promotion_generate_coupon/odoo/addons/sale_coupon_promotion_generate_coupon new file mode 120000 index 000000000..14a8d443e --- /dev/null +++ b/setup/sale_coupon_promotion_generate_coupon/odoo/addons/sale_coupon_promotion_generate_coupon @@ -0,0 +1 @@ +../../../../sale_coupon_promotion_generate_coupon \ No newline at end of file diff --git a/setup/sale_coupon_promotion_generate_coupon/setup.py b/setup/sale_coupon_promotion_generate_coupon/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/sale_coupon_promotion_generate_coupon/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)