Skip to content
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
220 changes: 220 additions & 0 deletions crm_team_zip_assign/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
=======================
CRM Team ZIP Assignment
=======================

..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:3cd3d416a5d908751e8e2a3d3342a40b61a84843c0d71e9f78477b67ca5a13ff
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

.. |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%2Fcrm-lightgray.png?logo=github
:target: https://github.com/OCA/crm/tree/16.0/crm_team_zip_assign
:alt: OCA/crm
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/crm-16-0/crm-16-0-crm_team_zip_assign
: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/crm&target_branch=16.0
:alt: Try me on Runboat

|badge1| |badge2| |badge3| |badge4| |badge5|

Auto-assign CRM teams to partners based on ZIP code patterns using regular expressions.

.. image:: https://raw.githubusercontent.com/OCA/crm/16.0/crm_team_zip_assign/static/description/crm_team_form_view.png

Features
--------

- Auto-assign CRM teams to partners based on ZIP code patterns
- Support for Python regular expressions with validation constraints
- Multi-company support
- Geographic filtering by countries and states
- Priority-based assignment when multiple teams match
- Exclusion flag for partners who should not be auto-assigned
- Real-time regex pattern validation prevents invalid patterns
- Contextual action for manual assignment from partner views
- Conditional assignment using pre-zip match conditions: teams can specify a domain expression (Odoo domain syntax) that must be satisfied by the partner before ZIP regex matching is performed. This allows for advanced filtering, e.g., only assign if the partner is a company or meets other criteria.

Assignment Logic
~~~~~~~~~~~~~~~~

The assignment is triggered on partner create/write when ZIP, company, country, state, or exclusion flag changes. A contextual action is also available from partner views for manual assignment. The system:

1. Finds all active teams with ZIP assignment enabled in the partner's company
2. Filters teams by matching countries and states (if specified)
3. For each eligible team, evaluates the optional pre-zip match condition (Odoo domain expression). If the partner does not satisfy the condition, the team is skipped.
4. Tests each remaining team's regex patterns against the partner's ZIP code
5. Selects the team with highest priority if multiple matches exist
6. Logs assignment activity for audit purposes

Assignment Rules
~~~~~~~~~~~~~~~~

1. Only active teams with "Active ZIP Assignment" enabled are considered
2. Only teams in the same company as the partner are considered
3. Teams must have matching countries (partner's country must be in team's countries)
4. Teams must have matching states (partner's state must be in team's states, if team has states defined)
5. If a team has a pre-zip match condition, the partner must satisfy the condition (Odoo domain) before ZIP regex matching is performed. If not set, all partners are considered.
6. Partners without a company, ZIP code, country, or state are not assigned
7. Partners with "Exclude from ZIP Assignment" checked are not assigned
8. When multiple teams match, the team with highest priority is selected
9. Invalid regex patterns are prevented by validation constraints at input time

**Table of contents**

.. contents::
:local:

Usage
=====

Setting up CRM Teams
---------------------

1. Go to CRM → Configuration → Sales Teams
2. Edit or create a CRM team
3. Enable "Active ZIP Assignment" checkbox
4. Set "ZIP Assignment Priority" (higher number = higher priority)
5. Configure geographic coverage:
- Select "Countries" where this team operates (required)
- Select "States" within those countries (required)
6. Add ZIP patterns in the "ZIP Patterns" tab
7. (Optional) Set a "Pre-Zip Match Condition" using Odoo domain syntax to restrict assignment to partners matching specific criteria (e.g., only companies, only certain types, etc.)

Geographic Coverage
~~~~~~~~~~~~~~~~~~~

- **Countries**: Teams will only be considered for partners located in the selected countries
- **States**: Teams will only be considered for partners in the selected states
- **Domain Filtering**: State selection is automatically filtered based on selected countries

Pre-Zip Match Condition
~~~~~~~~~~~~~~~~~~~~~~~

You can further restrict team assignment by specifying a domain condition in the "Pre-Zip Match Condition" field. This uses Odoo's domain syntax (e.g., `[('is_company', '=', True)]`). Only partners matching this condition will be considered for ZIP pattern matching for this team.

Pre-Zip Match Condition Examples
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Some example domain conditions:

- `[('is_company', '=', True)]` — Only assign to partners that are companies
- `[('type', '=', 'contact')]` — Only assign to contacts
- `[('industry_id', '!=', False)]` — Only assign to partners with an industry set

You can combine multiple conditions, e.g. `[('is_company', '=', True), ('country_id', '=', ref('base.us'))]`

ZIP Pattern Examples
~~~~~~~~~~~~~~~~~~~~

All patterns are validated in real-time to ensure they are valid Python regular expressions:

- ``^1[0-5].*`` - ZIP codes starting with 10-15
- ``^2[0-9].*`` - ZIP codes starting with 20-29
- ``^751.*`` - ZIP codes starting with 751
- ``.*123$`` - ZIP codes ending with 123
- ``^[1-3].*`` - ZIP codes starting with 1, 2, or 3
- ``^(10|20|30).*`` - ZIP codes starting with 10, 20, or 30

Pattern Validation
~~~~~~~~~~~~~~~~~~

The system validates regex patterns when they are entered:

- Invalid patterns will show an error message immediately
- Error messages include the specific regex error for debugging
- Only valid patterns can be saved to the database

Partner Configuration
---------------------

Partners have an "Exclude from ZIP Assignment" checkbox to prevent automatic assignment.

Teams can have a "Pre-Zip Match Condition" to restrict assignment to partners matching specific criteria before ZIP pattern matching is performed.

Usage
-----

Automatic Assignment
~~~~~~~~~~~~~~~~~~~~


Partners are automatically assigned to CRM teams when:

- A partner is created with complete geographic information (ZIP, country, state)
- A partner's ZIP code is modified
- A partner's country or state is changed
- A partner's company is changed
- The exclusion flag is modified

For each team, if a "Pre-Zip Match Condition" is set, the partner must match this condition before ZIP pattern matching is performed. If not set, all partners are considered for ZIP matching.

Manual Assignment
~~~~~~~~~~~~~~~~~

You can also trigger assignment manually:

- Use the contextual action available in partner views
- This is useful for reassigning existing partners after updating team configurations or after addon installation

Assignment Requirements
~~~~~~~~~~~~~~~~~~~~~~~

For automatic assignment to work, partners must have:

- A ZIP code
- A country
- A state
- A company
- "Exclude from ZIP Assignment" must be unchecked

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

Bugs are tracked on `GitHub Issues <https://github.com/OCA/crm/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 <https://github.com/OCA/crm/issues/new?body=module:%20crm_team_zip_assign%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.

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

Credits
=======

Authors
~~~~~~~

* Binhex

Contributors
~~~~~~~~~~~~

* Adasat Torres de León <a.torres@binhex.cloud>
* Rolando Pérez Rebollo <r.perez@binhex.cloud>

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/crm <https://github.com/OCA/crm/tree/16.0/crm_team_zip_assign>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
4 changes: 4 additions & 0 deletions crm_team_zip_assign/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Copyright 2025 Binhex
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from . import models
17 changes: 17 additions & 0 deletions crm_team_zip_assign/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Copyright 2025 Binhex
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

{
"name": "CRM Team ZIP Assignment",
"summary": "Auto-assign CRM teams to partners based on ZIP code patterns",
"version": "16.0.1.0.0",
"license": "AGPL-3",
"author": "Binhex, Odoo Community Association (OCA)",
"website": "https://github.com/OCA/crm",
"depends": ["crm"],
"data": [
"security/ir.model.access.csv",
"views/crm_team_views.xml",
"views/res_partner_views.xml",
],
}
6 changes: 6 additions & 0 deletions crm_team_zip_assign/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Copyright 2025 Binhex
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from . import crm_team
from . import crm_team_zip_pattern
from . import res_partner
75 changes: 75 additions & 0 deletions crm_team_zip_assign/models/crm_team.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Copyright 2025 Binhex
# License AGPL-3.0 or later (http://www

from odoo import Command, _, api, fields, models
from odoo.exceptions import ValidationError


class CrmTeam(models.Model):
_inherit = "crm.team"

enable_zip_auto_assignment = fields.Boolean(
string="Enable Auto ZIP Assignment",
default=False,
help="If checked, this team will be considered for automatic assignment "
"based on ZIP code patterns.",
)

country_ids = fields.Many2many(
"res.country",
string="Countries",
help="Countries where this team operates",
default=lambda self: self.env.company.country_id.ids,
)
state_ids = fields.Many2many(
"res.country.state", string="States", help="States where this team operates"
)

zip_regex_ids = fields.One2many(
comodel_name="crm.team.zip.pattern",
inverse_name="team_id",
string="ZIP Regex",
help="Regular expression patterns to match partner ZIP codes",
)

zip_assignment_priority = fields.Integer(
string="Assignment Priority",
default=0,
help="Higher priority teams will be preferred when multiple teams "
"match the same ZIP code. Higher number = higher priority.",
)

pre_zip_match_condition = fields.Char(
string="Pre-Zip Match Condition",
help=(
"If present, this condition must be satisfied before "
"matching the zip regular expression."
),
)

@api.onchange("country_ids")
def _onchange_countries(self):
if self.country_ids:
domain = [("country_id", "in", self.country_ids.ids)]
self.state_ids = [Command.clear()]
else:
domain = []
return {"domain": {"state_ids": domain}}

@api.onchange("company_id")
def _onchange_company_id(self):
self.country_ids = [Command.clear()]
self.state_ids = [Command.clear()]
if self.company_id:
self.country_ids = [Command.set([self.company_id.country_id.id])]

@api.constrains("enable_zip_auto_assignment", "company_id")
def _check_enable_zip_auto_assignment(self):
for team in self:
if team.enable_zip_auto_assignment and not team.company_id:
raise ValidationError(
_(
"A company must be set if "
"'Enable Auto ZIP Assignment' is checked."
)
)
54 changes: 54 additions & 0 deletions crm_team_zip_assign/models/crm_team_zip_pattern.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Copyright 2025 Binhex
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

import re

from odoo import _, api, fields, models
from odoo.exceptions import ValidationError


class CrmTeamZipPattern(models.Model):
_name = "crm.team.zip.pattern"
_description = "CRM Team ZIP Pattern"
_order = "team_id, id"

name = fields.Char(compute="_compute_name", store=True)

team_id = fields.Many2one(
comodel_name="crm.team",
string="CRM Team",
required=True,
ondelete="cascade",
)
pattern = fields.Char(
string="ZIP Pattern",
required=True,
help="Python regular expression pattern to match ZIP codes. "
"Example: '^1[0-5].*' for ZIP codes starting with 10-15",
)

_sql_constraints = [
(
"unique_team_id_pattern_combination",
"UNIQUE(team_id, pattern)",
_("The pattern has already been assigned to this team."),
)
]

@api.depends("team_id", "pattern")
def _compute_name(self):
for record in self:
name = f"{record.team_id.name} - {record.pattern}"
record.name = name

@api.constrains("pattern")
def _check_pattern_validity(self):
"""Validate that the pattern is a valid Python regex."""
for record in self:
if record.pattern:
try:
re.compile(record.pattern)
except re.error as e:
raise ValidationError(
f"Invalid regex pattern '{record.pattern}': {str(e)}"
) from e
Loading