Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[16.0] improves loyalty_partner_applicability #265

Open
wants to merge 4 commits into
base: 16.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 20 additions & 10 deletions loyalty_mass_mailing/models/loyalty_program.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).


from ast import literal_eval

from odoo import api, fields, models
from odoo.osv import expression

Expand All @@ -25,16 +23,28 @@
compute="_compute_partner_applicability_domain",
)

@api.depends("rule_ids.rule_partners_domain")
@api.depends(
"partner_domain",
"partner_ids",
"rule_ids.partner_domain",
"rule_ids.partner_ids",
)
def _compute_partner_applicability_domain(self):
for program in self:
partner_domains = [
literal_eval(domain)
for domain in program.rule_ids.mapped("rule_partners_domain")
if domain
]
if all(partner_domains):
program.partner_applicability_domain = expression.OR(partner_domains)
programs_domain = []
program_domain = program._get_eval_partner_domain()
if program_domain:
programs_domain.append(program_domain)

Check warning on line 37 in loyalty_mass_mailing/models/loyalty_program.py

View check run for this annotation

Codecov / codecov/patch

loyalty_mass_mailing/models/loyalty_program.py#L37

Added line #L37 was not covered by tests
rules_domain = []
for rule in program.rule_ids:
rule_domain = rule._get_eval_partner_domain()
rules_domain.append(rule_domain)
if all(rules_domain):
# If one of the rules has no domain, we don't want to apply any domain
rules_domain = expression.OR(rules_domain)
programs_domain.append(rules_domain)
if programs_domain:
program.partner_applicability_domain = expression.AND(programs_domain)
else:
program.partner_applicability_domain = "[]"

Expand Down
10 changes: 5 additions & 5 deletions loyalty_mass_mailing/tests/test_loyalty_mass_mailing.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def setUpClass(cls):
0,
0,
{
"rule_partners_domain": [("id", "=", cls.partner1.id)],
"partner_domain": [("id", "=", cls.partner1.id)],
"reward_point_mode": "order",
"minimum_qty": 1,
},
Expand Down Expand Up @@ -95,7 +95,7 @@ def setUpClass(cls):
0,
0,
{
"rule_partners_domain": [("id", "=", cls.partner1.id)],
"partner_domain": [("id", "=", cls.partner1.id)],
"reward_point_mode": "order",
"minimum_qty": 1,
},
Expand All @@ -104,7 +104,7 @@ def setUpClass(cls):
0,
0,
{
"rule_partners_domain": [("id", "=", cls.partner2.id)],
"partner_domain": [("id", "=", cls.partner2.id)],
"reward_point_mode": "order",
"minimum_qty": 1,
},
Expand Down Expand Up @@ -148,8 +148,8 @@ def test_program_all_partners(self):
self.assertEqual(self.program_all_partners.mailing_count, 1)

def test_program_all_partners_2(self):
# Cuando hay varias reglas y en alguna no está definido el dominio para
# partners, entonces el mailing_domain será = [ ]
# When there are several rules and in some of them the domain for
# partners is not defined, then the mailing_domain will be = [ ].
self.assertEqual(self.program_all_partners_2.mailing_count, 0)
self.program_all_partners_2.action_mailing_count()
self.assertEqual(self.program_all_partners_2.mailing_count, 1)
Expand Down
30 changes: 30 additions & 0 deletions loyalty_partner_applicability/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,36 @@ Loyalty Partner Applicability
This module extends the Loyalty functionality by allowing the definition of a customer
filter for promotion rules. This module is a base to be extended.

It allows to restrict the applicability of a promotion and its rules to a subset of customers.
This is useful when you want to target a specific group of customers with a promotion.
The restriction can be defined at 2 levels:

- The program
- The rule

When a restriction is defined at the program level, the promotion will only be available to
customers that match the filter. When a restriction is defined at the rule level, the rule
will only be applied to customers that match the filter. If a restriction is defined at both
levels, the program's rules will only be applied to customers that match both filters.

**Table of contents**

.. contents::
:local:

Usage
=====

Restriction on partners can be expressed in the following ways:

- By selecting a list of specific partners
- By providing a domain to filter partners

If both are provided, the condition will be a logical OR.

When you define a domain the datetime object is available as a variable `datetime`
to allow for dynamic filtering based on the current datetime.

Bug Tracker
===========

Expand All @@ -53,6 +78,7 @@ Authors
~~~~~~~

* Tecnativa
* ACSONE SA/NV

Contributors
~~~~~~~~~~~~
Expand All @@ -61,6 +87,10 @@ Contributors

* Pilar Vargas

* `ACSONE SA/NV <https://acsone.eu>`_:

* Laurent Mignon <laurent.mignon@@acsone.eu>

Maintainers
~~~~~~~~~~~

Expand Down
6 changes: 3 additions & 3 deletions loyalty_partner_applicability/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
"name": "Loyalty Partner Applicability",
"summary": "Enables the definition of a customer filter for promotion rules that will "
"only be applied to customers who meet the specified conditions in the filter.",
"version": "16.0.2.0.2",
"version": "16.0.3.0.0",
"category": "Sale",
"website": "https://github.com/OCA/sale-promotion",
"author": "Tecnativa, Odoo Community Association (OCA)",
"author": "Tecnativa,ACSONE SA/NV,Odoo Community Association (OCA)",
"license": "AGPL-3",
"depends": ["loyalty"],
"data": ["views/loyalty_rule_view_form.xml"],
"data": ["views/loyalty_rule_view_form.xml", "views/loyalty_program_view_form.xml"],
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import ast
import logging

from odoo import SUPERUSER_ID, api
from odoo.osv import expression

_logger = logging.getLogger(__name__)


def migrate(cr, version):
_logger.info(
"Migrating loyalty partners applicability domain from rules to programs"
)
env = api.Environment(cr, SUPERUSER_ID, {})
programs = env["loyalty.program"].search([])
for program in programs:
program_partner_domains = []
for rule in program.rule_ids:
domain = rule.partner_domain
py_domain = ast.literal_eval(domain)
if py_domain and py_domain not in program_partner_domains:
program_partner_domains.append(py_domain)
_logger.info(
f"Adding domain {py_domain} to program {program.name} "
"from rule {rule.display_name}"
)
rule.write({"partner_domain": "[]"})
if program_partner_domains:
program.write(
{"partner_domain": str(expression.OR(program_partner_domains))}
)
_logger.info(
f"Set domain {program.partner_domain} to program {program.name}"
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import logging

_logger = logging.getLogger(__name__)


def migrate(cr, version):
_logger.info("Rename rule_partners_domain to partner_domain in loyalty rule")
cr.execute(
"ALTER TABLE loyalty_rule RENAME COLUMN rule_partners_domain TO partner_domain"
)
2 changes: 2 additions & 0 deletions loyalty_partner_applicability/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
from . import loyalty_partner_applicability_mixin
from . import loyalty_program
from . import loyalty_rule
from . import res_config_settings
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# Copyright 2023 Tecnativa - Pilar Vargas
# Copyright 2025 ACSONE SA/NV
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).


from odoo import api, fields, models
from odoo.osv import expression
from odoo.tools.safe_eval import datetime, safe_eval


class LoyaltyPartnerApplicabilityMixin(models.AbstractModel):

_name = "loyalty.partner.applicability.mixin"
_description = "Manage the applicability of loyalty elements to partners"

partner_ids = fields.Many2many(
string="Specific customers",
comodel_name="res.partner",
help="This field allow to restrict the applicability to a specific set of partners",
default=lambda p: p.env.context.get("default_partner_ids"),
)

partner_domain = fields.Char(
string="Specific customers domain",
help="Applicable to the customers that match the domain",
default="[]",
)

@api.model_create_multi
def create(self, vals_list):
res = super().create(vals_list)
for vals in vals_list:
if not vals.get("partner_domain", False):
vals["partner_domain"] = "[]"
return res

def _get_eval_partner_domain(self):
self.ensure_one()
return safe_eval(
self.partner_domain,
{"datetime": datetime, "context_today": datetime.datetime.now},
)

def _is_coupon_sharing_allowed(self):
allow_sharing = (
self.env["ir.config_parameter"].sudo().get_param("allow_coupon_sharing")
)
return allow_sharing and (
allow_sharing.lower() == "true" or allow_sharing == "1"
)

def _get_partner_domain(self, partner):
self.ensure_one()
domain = []
if (self.partner_domain and self.partner_domain != "[]") or self.partner_ids:
if self._is_coupon_sharing_allowed():
domain = [
("commercial_partner_id", "=", partner.commercial_partner_id.id)
]

else:
domain = [("id", "=", partner.id)]

partner_domain = []
if self.partner_ids:
partner_domain = [("id", "in", self.partner_ids.ids)]
if self.partner_domain and self.partner_domain != "[]":
partner_domain = expression.OR(
[partner_domain, self._get_eval_partner_domain()]
)

domain = expression.AND([domain, partner_domain])
return domain

def _is_partner_valid(self, partner):
"""
Check if the partner is valid for the loyalty element

If no restriction is set on partner, the partner is always valid
If restrictions are set, the partner must match one of them.
The matching varies depending on the coupon sharing setting:
- If coupon sharing is not allowed, the partner must match one
- If coupon sharing is allowed, the partner must match one or have
the same commercial parent as a partner matching one restriction
:param partner: res.partner record
:return: bool
"""
self.ensure_one()
# If no restriction is set, the partner is always valid
if not self.partner_ids and not self.partner_domain:
return True

Check warning on line 91 in loyalty_partner_applicability/models/loyalty_partner_applicability_mixin.py

View check run for this annotation

Codecov / codecov/patch

loyalty_partner_applicability/models/loyalty_partner_applicability_mixin.py#L91

Added line #L91 was not covered by tests
coupon_sharing_allowed = self._is_coupon_sharing_allowed()
# In any case, if restrictions are set and the partner matches them, it is valid
partner_valid = True
if self.partner_ids:
partner_valid = partner in self.partner_ids
if self.partner_domain and self.partner_domain != "[]":
# (partner_valid and self.partner_ids) is required since we assume that if the
# partner_ids is not set but the partner_domain is set, the partner must match the
# partner_domain (IOW before we check the domain we assume that the partner is not
# valid if partner_ids is not set and partner_domain is set)
partner_valid = (partner_valid and bool(self.partner_ids)) or (
partner.filtered_domain(self._get_eval_partner_domain()) == partner
)
# if the partner is not valid but coupon sharing is allowed, check if the partner has
# the same commercial parent as the partner in the restrictions
if not partner_valid and coupon_sharing_allowed:
partner_domain = self._get_partner_domain(partner)
partner_valid = self.env["res.partner"].search_count(partner_domain) > 0
return partner_valid
9 changes: 9 additions & 0 deletions loyalty_partner_applicability/models/loyalty_program.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Copyright 2025 ACSONE SA/NV
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from odoo import models


class LoyaltyProgram(models.Model):
_name = "loyalty.program"
_inherit = ["loyalty.program", "loyalty.partner.applicability.mixin"]
27 changes: 10 additions & 17 deletions loyalty_partner_applicability/models/loyalty_rule.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,15 @@
# Copyright 2023 Tecnativa - Pilar Vargas
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import api, fields, models

from odoo import models

class LoyaltyRule(models.Model):
_inherit = "loyalty.rule"
_description = "Loyalty Rule"

rule_partners_domain = fields.Char(
string="Based on Customers",
help="Loyalty program will work for selected customers only",
default="[]",
)

@api.model_create_multi
def create(self, vals_list):
res = super().create(vals_list)
for vals in vals_list:
if not vals.get("rule_partners_domain", False):
vals["rule_partners_domain"] = "[]"
return res
class LoyaltyRule(models.Model):
_name = "loyalty.rule"
_inherit = ["loyalty.rule", "loyalty.partner.applicability.mixin"]

def _is_partner_valid(self, partner):
self.ensure_one()
return super()._is_partner_valid(partner) and self.program_id._is_partner_valid(
partner
)
4 changes: 4 additions & 0 deletions loyalty_partner_applicability/readme/CONTRIBUTORS.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
* `Tecnativa <https://www.tecnativa.com>`_:

* Pilar Vargas

* `ACSONE SA/NV <https://acsone.eu>`_:

* Laurent Mignon <laurent.mignon@@acsone.eu>
12 changes: 12 additions & 0 deletions loyalty_partner_applicability/readme/DESCRIPTION.rst
Original file line number Diff line number Diff line change
@@ -1,2 +1,14 @@
This module extends the Loyalty functionality by allowing the definition of a customer
filter for promotion rules. This module is a base to be extended.

It allows to restrict the applicability of a promotion and its rules to a subset of customers.
This is useful when you want to target a specific group of customers with a promotion.
The restriction can be defined at 2 levels:

- The program
- The rule

When a restriction is defined at the program level, the promotion will only be available to
customers that match the filter. When a restriction is defined at the rule level, the rule
will only be applied to customers that match the filter. If a restriction is defined at both
levels, the program's rules will only be applied to customers that match both filters.
9 changes: 9 additions & 0 deletions loyalty_partner_applicability/readme/USAGE.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Restriction on partners can be expressed in the following ways:

- By selecting a list of specific partners
- By providing a domain to filter partners

If both are provided, the condition will be a logical OR.

When you define a domain the datetime object is available as a variable `datetime`
to allow for dynamic filtering based on the current datetime.
Loading