From 8f4dedc9a63d4a389c6aab478f601b1db2480754 Mon Sep 17 00:00:00 2001 From: "r.perez" Date: Mon, 21 Jul 2025 23:53:57 -0400 Subject: [PATCH] [ADD] crm_team_zip_assign: partner auto assign crm team by zip --- crm_team_zip_assign/README.rst | 220 +++++++ crm_team_zip_assign/__init__.py | 4 + crm_team_zip_assign/__manifest__.py | 17 + crm_team_zip_assign/models/__init__.py | 6 + crm_team_zip_assign/models/crm_team.py | 75 +++ .../models/crm_team_zip_pattern.py | 54 ++ crm_team_zip_assign/models/res_partner.py | 96 +++ crm_team_zip_assign/readme/CONTRIBUTORS.rst | 2 + crm_team_zip_assign/readme/DESCRIPTION.rst | 41 ++ crm_team_zip_assign/readme/USAGE.rst | 99 ++++ .../security/ir.model.access.csv | 4 + .../static/description/crm_team_form_view.png | Bin 0 -> 72136 bytes .../static/description/icon.png | Bin 0 -> 9455 bytes .../static/description/index.html | 558 ++++++++++++++++++ crm_team_zip_assign/tests/__init__.py | 3 + .../tests/test_zip_assignment.py | 459 ++++++++++++++ crm_team_zip_assign/views/crm_team_views.xml | 69 +++ .../views/res_partner_views.xml | 25 + .../odoo/addons/crm_team_zip_assign | 1 + setup/crm_team_zip_assign/setup.py | 3 + 20 files changed, 1736 insertions(+) create mode 100644 crm_team_zip_assign/README.rst create mode 100644 crm_team_zip_assign/__init__.py create mode 100644 crm_team_zip_assign/__manifest__.py create mode 100644 crm_team_zip_assign/models/__init__.py create mode 100644 crm_team_zip_assign/models/crm_team.py create mode 100644 crm_team_zip_assign/models/crm_team_zip_pattern.py create mode 100644 crm_team_zip_assign/models/res_partner.py create mode 100644 crm_team_zip_assign/readme/CONTRIBUTORS.rst create mode 100644 crm_team_zip_assign/readme/DESCRIPTION.rst create mode 100644 crm_team_zip_assign/readme/USAGE.rst create mode 100644 crm_team_zip_assign/security/ir.model.access.csv create mode 100644 crm_team_zip_assign/static/description/crm_team_form_view.png create mode 100644 crm_team_zip_assign/static/description/icon.png create mode 100644 crm_team_zip_assign/static/description/index.html create mode 100644 crm_team_zip_assign/tests/__init__.py create mode 100644 crm_team_zip_assign/tests/test_zip_assignment.py create mode 100644 crm_team_zip_assign/views/crm_team_views.xml create mode 100644 crm_team_zip_assign/views/res_partner_views.xml create mode 120000 setup/crm_team_zip_assign/odoo/addons/crm_team_zip_assign create mode 100644 setup/crm_team_zip_assign/setup.py diff --git a/crm_team_zip_assign/README.rst b/crm_team_zip_assign/README.rst new file mode 100644 index 00000000000..d22983d86cf --- /dev/null +++ b/crm_team_zip_assign/README.rst @@ -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 `_. +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 +~~~~~~~ + +* Binhex + +Contributors +~~~~~~~~~~~~ + +* Adasat Torres de León +* Rolando Pérez Rebollo + +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 `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/crm_team_zip_assign/__init__.py b/crm_team_zip_assign/__init__.py new file mode 100644 index 00000000000..efa66a975b9 --- /dev/null +++ b/crm_team_zip_assign/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2025 Binhex +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import models diff --git a/crm_team_zip_assign/__manifest__.py b/crm_team_zip_assign/__manifest__.py new file mode 100644 index 00000000000..e78de16c4e2 --- /dev/null +++ b/crm_team_zip_assign/__manifest__.py @@ -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", + ], +} diff --git a/crm_team_zip_assign/models/__init__.py b/crm_team_zip_assign/models/__init__.py new file mode 100644 index 00000000000..69694bf93d5 --- /dev/null +++ b/crm_team_zip_assign/models/__init__.py @@ -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 diff --git a/crm_team_zip_assign/models/crm_team.py b/crm_team_zip_assign/models/crm_team.py new file mode 100644 index 00000000000..5e2a8da913a --- /dev/null +++ b/crm_team_zip_assign/models/crm_team.py @@ -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." + ) + ) diff --git a/crm_team_zip_assign/models/crm_team_zip_pattern.py b/crm_team_zip_assign/models/crm_team_zip_pattern.py new file mode 100644 index 00000000000..2788f4caad4 --- /dev/null +++ b/crm_team_zip_assign/models/crm_team_zip_pattern.py @@ -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 diff --git a/crm_team_zip_assign/models/res_partner.py b/crm_team_zip_assign/models/res_partner.py new file mode 100644 index 00000000000..3c7eba40a67 --- /dev/null +++ b/crm_team_zip_assign/models/res_partner.py @@ -0,0 +1,96 @@ +# Copyright 2025 Binhex +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import ast +import logging +import re + +from odoo import api, fields, models +from odoo.tools import ustr + +_logger = logging.getLogger(__name__) + + +class ResPartner(models.Model): + _inherit = "res.partner" + + exclude_from_zip_assign = fields.Boolean( + string="Exclude from ZIP Assignment", + default=False, + help="If checked, this partner will not be automatically assigned " + "to a CRM team based on ZIP code patterns.", + ) + + @api.model_create_multi + def create(self, vals_list): + partners = super().create(vals_list) + partners._process_zip_assignment() + return partners + + def write(self, vals): + result = super().write(vals) + if "zip" in vals or "company_id" in vals or "exclude_from_zip_assign" in vals: + self._process_zip_assignment() + return result + + def _process_zip_assignment(self): + """Process ZIP assignment for multiple partners.""" + partners = self.filtered( + lambda partner: partner.zip + and partner.company_id + and not partner.exclude_from_zip_assign + and partner.country_id + and partner.state_id + and partner.type == "contact" + ) + if partners: + teams = self.env["crm.team"].search( + [ + ("enable_zip_auto_assignment", "=", True), + ("zip_regex_ids", "!=", False), + ("company_id", "!=", False), + ("country_ids", "!=", False), + ("state_ids", "!=", False), + ] + ) + for partner in partners: + selected_team = self._select_team_for_partner(partner, teams) + if selected_team: + _logger.info( + "Auto-assigning partner '%s' (ZIP: %s) to team '%s'", + partner.name, + partner.zip, + selected_team.name, + ) + partner.team_id = selected_team.id + return True + + def _select_team_for_partner(self, partner, teams): + """Select the best CRM team for a partner based on ZIP and location.""" + eligible_teams = teams.filtered( + lambda team: ( + team.company_id == partner.company_id + and team.country_ids & partner.country_id + and team.state_ids & partner.state_id + ) + ) + matching_teams = self.env["crm.team"] + for team in eligible_teams: + # Evaluate pre_zip_match_condition if present, else always match + if team.pre_zip_match_condition: + partner_domain = ast.literal_eval(ustr(team.pre_zip_match_condition)) + domain_match = bool(partner.filtered_domain(partner_domain)) + else: + domain_match = True + if domain_match: + if any( + re.match(pattern.pattern, partner.zip or "") + for pattern in team.zip_regex_ids + ): + matching_teams |= team + if matching_teams: + matching_teams = matching_teams.sorted( + key=lambda t: (-t.zip_assignment_priority, t.id) + ) + return matching_teams[:1] + return False diff --git a/crm_team_zip_assign/readme/CONTRIBUTORS.rst b/crm_team_zip_assign/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000000..7bf434767cb --- /dev/null +++ b/crm_team_zip_assign/readme/CONTRIBUTORS.rst @@ -0,0 +1,2 @@ +* Adasat Torres de León +* Rolando Pérez Rebollo diff --git a/crm_team_zip_assign/readme/DESCRIPTION.rst b/crm_team_zip_assign/readme/DESCRIPTION.rst new file mode 100644 index 00000000000..091211d19ae --- /dev/null +++ b/crm_team_zip_assign/readme/DESCRIPTION.rst @@ -0,0 +1,41 @@ +Auto-assign CRM teams to partners based on ZIP code patterns using regular expressions. + +.. image:: ../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 diff --git a/crm_team_zip_assign/readme/USAGE.rst b/crm_team_zip_assign/readme/USAGE.rst new file mode 100644 index 00000000000..68286690c29 --- /dev/null +++ b/crm_team_zip_assign/readme/USAGE.rst @@ -0,0 +1,99 @@ +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 diff --git a/crm_team_zip_assign/security/ir.model.access.csv b/crm_team_zip_assign/security/ir.model.access.csv new file mode 100644 index 00000000000..e680861458c --- /dev/null +++ b/crm_team_zip_assign/security/ir.model.access.csv @@ -0,0 +1,4 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_crm_team_zip_pattern_user,crm.team.zip.pattern.user,model_crm_team_zip_pattern,base.group_user,1,0,0,0 +access_crm_team_zip_pattern_manager,crm.team.zip.pattern.manager,model_crm_team_zip_pattern,sales_team.group_sale_manager,1,1,1,1 +access_crm_team_zip_pattern_salesman,crm.team.zip.pattern.salesman,model_crm_team_zip_pattern,sales_team.group_sale_salesman,1,1,1,0 diff --git a/crm_team_zip_assign/static/description/crm_team_form_view.png b/crm_team_zip_assign/static/description/crm_team_form_view.png new file mode 100644 index 0000000000000000000000000000000000000000..addf24708ab381cd769e837214e92cbbd91ad5fb GIT binary patch literal 72136 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 literal 0 HcmV?d00001 diff --git a/crm_team_zip_assign/static/description/icon.png b/crm_team_zip_assign/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3a0328b516c4980e8e44cdb63fd945757ddd132d GIT binary patch literal 9455 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 z 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 Xviia!U7SGha1wx#SCgwmn*{w2TRX*I literal 0 HcmV?d00001 diff --git a/crm_team_zip_assign/static/description/index.html b/crm_team_zip_assign/static/description/index.html new file mode 100644 index 00000000000..7cd7dda2961 --- /dev/null +++ b/crm_team_zip_assign/static/description/index.html @@ -0,0 +1,558 @@ + + + + + +CRM Team ZIP Assignment + + + +
+

CRM Team ZIP Assignment

+ + +

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

+

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

+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. +
  3. Filters teams by matching countries and states (if specified)
  4. +
  5. 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.
  6. +
  7. Tests each remaining team’s regex patterns against the partner’s ZIP code
  8. +
  9. Selects the team with highest priority if multiple matches exist
  10. +
  11. Logs assignment activity for audit purposes
  12. +
+
+
+

Assignment Rules

+
    +
  1. Only active teams with “Active ZIP Assignment” enabled are considered
  2. +
  3. Only teams in the same company as the partner are considered
  4. +
  5. Teams must have matching countries (partner’s country must be in team’s countries)
  6. +
  7. Teams must have matching states (partner’s state must be in team’s states, if team has states defined)
  8. +
  9. 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.
  10. +
  11. Partners without a company, ZIP code, country, or state are not assigned
  12. +
  13. Partners with “Exclude from ZIP Assignment” checked are not assigned
  14. +
  15. When multiple teams match, the team with highest priority is selected
  16. +
  17. Invalid regex patterns are prevented by validation constraints at input time
  18. +
+

Table of contents

+
+ +
+
+

Usage

+
+
+
+
+

Setting up CRM Teams

+
    +
  1. Go to CRM → Configuration → Sales Teams
  2. +
  3. Edit or create a CRM team
  4. +
  5. Enable “Active ZIP Assignment” checkbox
  6. +
  7. Set “ZIP Assignment Priority” (higher number = higher priority)
  8. +
  9. Configure geographic coverage: +- Select “Countries” where this team operates (required) +- Select “States” within those countries (required)
  10. +
  11. Add ZIP patterns in the “ZIP Patterns” tab
  12. +
  13. (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.)
  14. +
+
+

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

+
    +
  • Binhex
  • +
+
+
+

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/crm project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/crm_team_zip_assign/tests/__init__.py b/crm_team_zip_assign/tests/__init__.py new file mode 100644 index 00000000000..cdcddd53fd7 --- /dev/null +++ b/crm_team_zip_assign/tests/__init__.py @@ -0,0 +1,3 @@ +# Copyright 2025 Binhex +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from . import test_zip_assignment diff --git a/crm_team_zip_assign/tests/test_zip_assignment.py b/crm_team_zip_assign/tests/test_zip_assignment.py new file mode 100644 index 00000000000..eb2cc5fe997 --- /dev/null +++ b/crm_team_zip_assign/tests/test_zip_assignment.py @@ -0,0 +1,459 @@ +# Copyright 2025 Binhex +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from psycopg2.errors import UniqueViolation + +from odoo.exceptions import ValidationError + +from odoo.addons.base.tests.common import BaseCommon + + +class TestZipAssignment(BaseCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + + # Create countries and states + + cls.state_ny = cls.env.ref("base.state_us_27") # New York + cls.state_ca = cls.env.ref("base.state_us_5") # California + cls.country_us = cls.state_ny.country_id # USA + cls.state_on = cls.env.ref("base.state_ca_on") # Ontario + cls.country_ca = cls.state_on.country_id # Canada + + # Create companies + cls.company_a = cls.env["res.company"].create( + { + "name": "Company A", + "country_id": cls.country_us.id, + } + ) + cls.company_b = cls.env["res.company"].create( + { + "name": "Company B", + "country_id": cls.country_us.id, + } + ) + + # Create CRM teams + cls.team_north = cls.env["crm.team"].create( + { + "name": "North Team", + "company_id": cls.company_a.id, + "enable_zip_auto_assignment": True, + "zip_assignment_priority": 10, # Higher priority + "country_ids": [(6, 0, [cls.country_us.id])], + "state_ids": [(6, 0, [cls.state_ny.id])], + } + ) + + cls.team_north_lesser_priority = cls.env["crm.team"].create( + { + "name": "North Team Lesser Priority", + "company_id": cls.company_a.id, + "enable_zip_auto_assignment": True, + "zip_assignment_priority": 4, # Lower priority + "country_ids": [(6, 0, [cls.country_us.id])], + "state_ids": [(6, 0, [cls.state_ny.id])], + } + ) + + cls.team_south = cls.env["crm.team"].create( + { + "name": "South Team", + "company_id": cls.company_a.id, + "enable_zip_auto_assignment": True, + "zip_assignment_priority": 5, + "country_ids": [(6, 0, [cls.country_us.id])], + "state_ids": [(6, 0, [cls.state_ca.id])], + } + ) + + cls.team_company_b = cls.env["crm.team"].create( + { + "name": "Company B Team", + "company_id": cls.company_b.id, + "enable_zip_auto_assignment": True, + "zip_assignment_priority": 1, + "country_ids": [(6, 0, [cls.country_us.id])], + "state_ids": [(6, 0, [cls.state_ny.id])], + } + ) + + cls.team_inactive = cls.env["crm.team"].create( + { + "name": "Inactive Team", + "company_id": cls.company_a.id, + "enable_zip_auto_assignment": False, + "zip_assignment_priority": 20, + "country_ids": [(6, 0, [cls.country_us.id])], + "state_ids": [(6, 0, [cls.state_ny.id])], + } + ) + + cls.team_company = cls.env["crm.team"].create( + { + "name": "Team Company", + "company_id": cls.company_a.id, + "enable_zip_auto_assignment": True, + "zip_assignment_priority": 10, + "country_ids": [(6, 0, [cls.country_us.id])], + "state_ids": [(6, 0, [cls.state_ny.id])], + "pre_zip_match_condition": "[('is_company', '=', True)]", + } + ) + + cls.team_person = cls.env["crm.team"].create( + { + "name": "Team Person", + "company_id": cls.company_a.id, + "enable_zip_auto_assignment": True, + "zip_assignment_priority": 10, + "country_ids": [(6, 0, [cls.country_us.id])], + "state_ids": [(6, 0, [cls.state_ny.id])], + "pre_zip_match_condition": "[('is_company', '=', False)]", + } + ) + + # Create ZIP patterns + cls.env["crm.team.zip.pattern"].create( + [ + { + "team_id": cls.team_north.id, + "pattern": r"^1[0-5].*", # ZIP starting with 10-15 + }, + { + "team_id": cls.team_north_lesser_priority.id, + "pattern": r"^1[0-5].*", # Same pattern to test priority + }, + { + "team_id": cls.team_south.id, + "pattern": r"^2[0-9].*", # ZIP starting with 20-29 + }, + { + "team_id": cls.team_south.id, + "pattern": r"^1[6-9].*", # ZIP starting with 16-19 + }, + { + "team_id": cls.team_company_b.id, + "pattern": r"^1[0-5].*", # Same pattern as north team + }, + { + "team_id": cls.team_inactive.id, + "pattern": r"^3[0-9].*", # ZIP starting with 30-39 + }, + { + "team_id": cls.team_company.id, + "pattern": r"^9[6-8].*", # ZIP starting with 96-98 + }, + { + "team_id": cls.team_person.id, + "pattern": r"^9[6-8].*", # ZIP starting with 96-98 + }, + ] + ) + + def test_single_match(self): + """Test partner assignment with single team match.""" + partner = self.env["res.partner"].create( + { + "name": "Test Partner North", + "zip": "12345", + "company_id": self.company_a.id, + "country_id": self.country_us.id, + "state_id": self.state_ny.id, + "is_company": False, + } + ) + self.assertEqual(partner.team_id, self.team_north) + + def test_multiple_matches_priority(self): + """Test priority when multiple teams match.""" + # Both teams have patterns for ZIP starting with 10-15 + # North team has higher priority (10 vs 4) + partner = self.env["res.partner"].create( + { + "name": "Test Partner Priority", + "zip": "15789", + "company_id": self.company_a.id, + "country_id": self.country_us.id, + "state_id": self.state_ny.id, + "is_company": False, + } + ) + self.assertEqual(partner.team_id, self.team_north) + + def test_no_match(self): + """Test no assignment when no team matches.""" + partner = self.env["res.partner"].create( + { + "name": "Test Partner No Match", + "zip": "99999", + "company_id": self.company_a.id, + "country_id": self.country_us.id, + "state_id": self.state_ny.id, + "is_company": False, + } + ) + self.assertFalse(partner.team_id) + + def test_exclusion_flag(self): + """Test that excluded partners are not assigned.""" + partner = self.env["res.partner"].create( + { + "name": "Test Partner Excluded", + "zip": "12345", + "company_id": self.company_a.id, + "country_id": self.country_us.id, + "state_id": self.state_ny.id, + "exclude_from_zip_assign": True, + "is_company": False, + } + ) + self.assertFalse(partner.team_id) + + def test_multi_company(self): + """Test multi-company isolation.""" + # Partner in company A should get team_north + partner_a = self.env["res.partner"].create( + { + "name": "Test Partner Company A", + "zip": "12345", + "company_id": self.company_a.id, + "country_id": self.country_us.id, + "state_id": self.state_ny.id, + "is_company": False, + } + ) + self.assertEqual(partner_a.team_id, self.team_north) + + # Partner in company B should get team_company_b + partner_b = self.env["res.partner"].create( + { + "name": "Test Partner Company B", + "zip": "12345", + "company_id": self.company_b.id, + "country_id": self.country_us.id, + "state_id": self.state_ny.id, + "is_company": False, + } + ) + self.assertEqual(partner_b.team_id, self.team_company_b) + + def test_inactive_team_ignored(self): + """Test that inactive teams are ignored.""" + partner = self.env["res.partner"].create( + { + "name": "Test Partner Inactive", + "zip": "30123", + "company_id": self.company_a.id, + "country_id": self.country_us.id, + "state_id": self.state_ny.id, + "is_company": False, + } + ) + self.assertFalse(partner.team_id) + + def test_invalid_regex(self): + """Test that invalid regex patterns are prevented by constraints.""" + # Test constraint validation prevents invalid patterns + with self.assertRaises(ValidationError): + self.env["crm.team.zip.pattern"].create( + { + "team_id": self.team_north.id, + "pattern": r"[invalid(regex", # Invalid pattern + } + ) + + def test_repeated_pattern_in_team(self): + """Test unique team_id pattern combination in crm team.""" + with self.assertRaises(UniqueViolation): + self.env["crm.team.zip.pattern"].create( + { + "team_id": self.team_north.id, + "pattern": r"^1[0-5].*", # ZIP starting with 10-15 + } + ) + + def test_enable_zip_auto_assignment_requires_company(self): + """ + Test that enabling zip auto assignment without company + raises ValidationError. + """ + with self.assertRaises(ValidationError): + self.env["crm.team"].create( + { + "name": "No Company Team", + "enable_zip_auto_assignment": True, + "company_id": None, + } + ) + + def test_geographic_filtering(self): + """Test that geographic filtering works correctly.""" + # Partner in wrong state should not be assigned + partner_wrong_state = self.env["res.partner"].create( + { + "name": "Test Partner Wrong State", + "zip": "12345", + "company_id": self.company_a.id, + "country_id": self.country_us.id, + "state_id": self.state_ca.id, # California, but team_north is for NY + "is_company": False, + } + ) + self.assertFalse(partner_wrong_state.team_id) + + # Partner in wrong country should not be assigned + partner_wrong_country = self.env["res.partner"].create( + { + "name": "Test Partner Wrong Country", + "zip": "12345", + "company_id": self.company_a.id, + "country_id": self.country_ca.id, # Canada + "state_id": self.state_on.id, + "is_company": False, + } + ) + self.assertFalse(partner_wrong_country.team_id) + + def test_no_company_no_assignment(self): + """Test that partners without company are not assigned.""" + partner = self.env["res.partner"].create( + { + "name": "Test Partner No Company", + "zip": "12345", + "country_id": self.country_us.id, + "state_id": self.state_ny.id, + "is_company": False, + } + ) + self.assertFalse(partner.team_id) + + def test_no_zip_no_assignment(self): + """Test that partners without ZIP are not assigned.""" + partner = self.env["res.partner"].create( + { + "name": "Test Partner No ZIP", + "company_id": self.company_a.id, + "country_id": self.country_us.id, + "state_id": self.state_ny.id, + "is_company": False, + } + ) + self.assertFalse(partner.team_id) + + def test_no_country_no_assignment(self): + """Test that partners without country are not assigned.""" + partner = self.env["res.partner"].create( + { + "name": "Test Partner No Country", + "zip": "12345", + "company_id": self.company_a.id, + "country_id": False, + "is_company": False, + } + ) + self.assertFalse(partner.team_id) + + def test_no_state_no_assignment(self): + """Test that partners without state are not assigned.""" + partner = self.env["res.partner"].create( + { + "name": "Test Partner No State", + "zip": "12345", + "company_id": self.company_a.id, + "country_id": self.country_us.id, + "is_company": False, + } + ) + self.assertFalse(partner.team_id) + + def test_write_zip_triggers_assignment(self): + """Test that changing ZIP triggers reassignment.""" + partner = self.env["res.partner"].create( + { + "name": "Test Partner Write", + "zip": "99999", # No match + "company_id": self.company_a.id, + "country_id": self.country_us.id, + "state_id": self.state_ny.id, + "is_company": False, + } + ) + self.assertFalse(partner.team_id) + + # Change ZIP to matching pattern + partner.write({"zip": "12345"}) + self.assertEqual(partner.team_id, self.team_north) + + def test_write_company_triggers_assignment(self): + """Test that changing company triggers reassignment.""" + partner = self.env["res.partner"].create( + { + "name": "Test Partner Write Company", + "zip": "12345", + "company_id": self.company_b.id, + "country_id": self.country_us.id, + "state_id": self.state_ny.id, + "is_company": False, + } + ) + self.assertEqual(partner.team_id, self.team_company_b) + + # Change company + partner.write({"company_id": self.company_a.id}) + self.assertEqual(partner.team_id, self.team_north) + + def test_write_exclusion_flag(self): + """Test that changing exclusion flag works correctly.""" + partner = self.env["res.partner"].create( + { + "name": "Test Partner Exclusion", + "zip": "12345", + "company_id": self.company_a.id, + "country_id": self.country_us.id, + "state_id": self.state_ny.id, + "is_company": False, + } + ) + self.assertEqual(partner.team_id, self.team_north) + + # Exclude from assignment + partner.write({"exclude_from_zip_assign": True}) + # Team should remain (exclusion doesn't clear existing assignment) + self.assertEqual(partner.team_id, self.team_north) + + # Include again + partner.write({"exclude_from_zip_assign": False}) + # Should still be assigned to same team + self.assertEqual(partner.team_id, self.team_north) + + def test_pre_zip_match_condition(self): + """Test that pre_zip_match_condition filters partners as expected.""" + # Setup: two teams, same country/state/zip/priority, different pre_zip_match_condition + # Partner is a company + partner_company = self.env["res.partner"].create( + { + "name": "Partner Company", + "zip": "96765", + "company_id": self.company_a.id, + "country_id": self.country_us.id, + "state_id": self.state_ny.id, + "is_company": True, + } + ) + self.assertEqual(partner_company.team_id, self.team_company) + + # Partner is a person + partner_person = self.env["res.partner"].create( + { + "name": "Partner Person", + "zip": "96765", + "company_id": self.company_a.id, + "country_id": self.country_us.id, + "state_id": self.state_ny.id, + "is_company": False, + } + ) + self.assertEqual(partner_person.team_id, self.team_person) diff --git a/crm_team_zip_assign/views/crm_team_views.xml b/crm_team_zip_assign/views/crm_team_views.xml new file mode 100644 index 00000000000..2ff1083e758 --- /dev/null +++ b/crm_team_zip_assign/views/crm_team_views.xml @@ -0,0 +1,69 @@ + + + + + + crm.team.form.zip.assign + crm.team + + + + + + + + + + + + + + + + + + + + + + + + + + + crm.team.tree.zip.assign + crm.team + + + + + + + + + diff --git a/crm_team_zip_assign/views/res_partner_views.xml b/crm_team_zip_assign/views/res_partner_views.xml new file mode 100644 index 00000000000..5fceb52d56b --- /dev/null +++ b/crm_team_zip_assign/views/res_partner_views.xml @@ -0,0 +1,25 @@ + + + + + + res.partner.form.zip.assign + res.partner + + + + + + + + + + Assign CRM Team By Zip + + + form,list + code + records._process_zip_assignment() + + diff --git a/setup/crm_team_zip_assign/odoo/addons/crm_team_zip_assign b/setup/crm_team_zip_assign/odoo/addons/crm_team_zip_assign new file mode 120000 index 00000000000..442bb612e80 --- /dev/null +++ b/setup/crm_team_zip_assign/odoo/addons/crm_team_zip_assign @@ -0,0 +1 @@ +../../../../crm_team_zip_assign \ No newline at end of file diff --git a/setup/crm_team_zip_assign/setup.py b/setup/crm_team_zip_assign/setup.py new file mode 100644 index 00000000000..b908cbe55cb --- /dev/null +++ b/setup/crm_team_zip_assign/setup.py @@ -0,0 +1,3 @@ +import setuptools + +setuptools.setup()