GuideOdoo SalesMarch 13, 2026

Sales Teams & Territories in Odoo 19:
Multi-Region Operations

INTRODUCTION

Your CRM Is Only as Good as Your Territory Design

Most Odoo CRM deployments start with a single sales team called "Sales" and every rep pulling leads from the same pool. It works when you have three salespeople in one office. It collapses the moment you expand to a second region, open a European subsidiary, or hire a team that sells exclusively to enterprise accounts.

The symptoms show up fast. Reps in Toronto chase leads in Sydney. Two people call the same prospect on the same day. The VP of Sales can't see pipeline by region without exporting to a spreadsheet. Revenue targets exist in a slide deck but never connect to the CRM. The root cause is always the same: no territory structure.

Odoo 19's CRM module supports multi-team hierarchies, rule-based lead assignment, territory-aware pipelines, and per-team quotas—but none of it works out of the box. This guide walks you through the complete configuration: how to structure sales teams, map territories by geography/industry/account size, automate lead routing, set team quotas, control cross-team visibility, and handle multi-currency pricing per region.

01

Designing the Sales Team Hierarchy in Odoo 19

Before configuring anything in Settings, you need a team design that matches how your business actually sells. Odoo's crm.team model is flat by default—no parent-child relationships. But you can simulate hierarchy through naming conventions, team-specific domains, and dashboard grouping.

Common Multi-Region Team Structures

StructureBest ForOdoo Implementation
Geographic (NA / EMEA / APAC)B2B with regional pricing, local complianceOne crm.team per region, country tags on leads
Industry vertical (Healthcare / Finance / Retail)Complex sales requiring domain expertiseOne crm.team per vertical, industry field on contacts
Account size (SMB / Mid-Market / Enterprise)Different sales motions per segmentOne crm.team per tier, revenue-based assignment rules
Hybrid (Region × Segment)Large orgs with 50+ repsTeams like "NA Enterprise," "EMEA SMB" with combined rules

Creating Sales Teams with Territory Context

XML — data/crm_team_data.xml
<odoo>
  <!-- North America Sales Team -->
  <record id="team_na" model="crm.team">
    <field name="name">North America</field>
    <field name="use_leads">True</field>
    <field name="use_opportunities">True</field>
    <field name="company_id" ref="base.main_company"/>
    <field name="user_id" ref="base.user_admin"/>
    <field name="alias_name">sales-na</field>
  </record>

  <!-- EMEA Sales Team -->
  <record id="team_emea" model="crm.team">
    <field name="name">EMEA</field>
    <field name="use_leads">True</field>
    <field name="use_opportunities">True</field>
    <field name="company_id" ref="company_eu"/>
    <field name="user_id" ref="user_emea_manager"/>
    <field name="alias_name">sales-emea</field>
  </record>

  <!-- APAC Sales Team -->
  <record id="team_apac" model="crm.team">
    <field name="name">APAC</field>
    <field name="use_leads">True</field>
    <field name="use_opportunities">True</field>
    <field name="company_id" ref="company_apac"/>
    <field name="user_id" ref="user_apac_manager"/>
    <field name="alias_name">sales-apac</field>
  </record>
</odoo>
Email Aliases Matter

Each team's alias_name creates a dedicated email address (e.g., sales-na@yourcompany.com). Inbound emails to that alias automatically create leads assigned to the correct team. This is the simplest form of territory routing—configure it from day one.

02

Mapping Territories by Geography, Industry, and Account Size

A territory isn't just a region on a map. In Odoo 19, a territory is a set of rules that determine which team owns a lead based on one or more attributes: country, state/province, industry sector, estimated revenue, or any custom field on the contact or lead.

Step 1: Add Territory Fields to Leads and Contacts

Python — models/crm_lead.py
from odoo import api, fields, models


class CrmLead(models.Model):
    _inherit = "crm.lead"

    x_territory = fields.Selection(
        selection=[
            ("na", "North America"),
            ("emea", "EMEA"),
            ("apac", "APAC"),
            ("latam", "LATAM"),
        ],
        string="Territory",
        compute="_compute_territory",
        store=True,
        help="Auto-assigned based on country. "
             "Manual override allowed.",
    )
    x_account_tier = fields.Selection(
        selection=[
            ("smb", "SMB (<$1M revenue)"),
            ("mid", "Mid-Market ($1M-$50M)"),
            ("enterprise", "Enterprise ($50M+)"),
        ],
        string="Account Tier",
        help="Segment based on estimated annual revenue.",
    )
    x_industry_vertical = fields.Many2one(
        "res.partner.industry",
        string="Industry Vertical",
        related="partner_id.industry_id",
        store=True,
    )

    TERRITORY_MAP = {
        "na": ["US", "CA", "MX"],
        "emea": ["GB", "DE", "FR", "NL", "BE", "ES",
                 "IT", "SE", "NO", "DK", "CH", "AE"],
        "apac": ["AU", "NZ", "JP", "SG", "IN", "KR",
                 "TH", "MY", "PH", "ID"],
        "latam": ["BR", "AR", "CL", "CO", "PE"],
    }

    @api.depends("country_id")
    def _compute_territory(self):
        # Build reverse lookup: country_code -> territory
        code_map = {{}}
        for territory, codes in self.TERRITORY_MAP.items():
            for code in codes:
                code_map[code] = territory
        for lead in self:
            lead.x_territory = code_map.get(
                lead.country_id.code, False
            )

Step 2: Territory Assignment Rules via Odoo Data

Odoo 19's lead assignment engine (crm.team.rule) lets you define domain-based rules that automatically route leads to the correct team. Here's how to configure rules that combine geography with account tier:

XML — data/crm_team_rule_data.xml
<odoo>
  <!-- NA: all leads from US, CA, MX -->
  <record id="rule_territory_na" model="crm.team.rule">
    <field name="team_id" ref="team_na"/>
    <field name="name">North America Territory</field>
    <field name="domain">
      [("x_territory", "=", "na")]
    </field>
  </record>

  <!-- EMEA: European + Middle East leads -->
  <record id="rule_territory_emea" model="crm.team.rule">
    <field name="team_id" ref="team_emea"/>
    <field name="name">EMEA Territory</field>
    <field name="domain">
      [("x_territory", "=", "emea")]
    </field>
  </record>

  <!-- APAC: Asia-Pacific leads -->
  <record id="rule_territory_apac" model="crm.team.rule">
    <field name="team_id" ref="team_apac"/>
    <field name="name">APAC Territory</field>
    <field name="domain">
      [("x_territory", "=", "apac")]
    </field>
  </record>

  <!-- Enterprise tier override: all Enterprise
       leads go to Strategic Accounts team -->
  <record id="rule_enterprise_override"
          model="crm.team.rule">
    <field name="team_id" ref="team_strategic"/>
    <field name="name">Enterprise Override</field>
    <field name="sequence">5</field>
    <field name="domain">
      [("x_account_tier", "=", "enterprise")]
    </field>
  </record>
</odoo>
Rule Sequence Is Priority

Rules are evaluated in sequence order (lowest first). The Enterprise override rule has sequence=5, so it fires before geographic rules (default sequence is 10). This means a $100M enterprise lead from Germany goes to Strategic Accounts, not EMEA. Design your sequence numbers intentionally—they are your priority hierarchy.

03

Automating Lead and Opportunity Assignment by Territory

Territory rules decide which team gets the lead. But within a team of 8 reps, who gets it? Odoo 19 provides two assignment modes: round-robin and weighted assignment. Both are configured per-team.

Enabling Auto-Assignment

Navigate to CRM → Configuration → Settings and enable Rule-Based Assignment. This activates a scheduled action (crm.team.assign.leads) that runs periodically to distribute unassigned leads.

Python — Configuring assignment from code
# Enable lead assignment in CRM settings
env["ir.config_parameter"].sudo().set_param(
    "crm.lead.auto.assignment", "True"
)

# Set assignment frequency (in minutes)
env["ir.config_parameter"].sudo().set_param(
    "crm.lead.assignment.cron.interval", "30"
)

# Configure per-member capacity on the team
na_team = env.ref("my_module.team_na")
for member in na_team.crm_team_member_ids:
    # Max 20 active leads per rep
    member.assignment_max = 20
    # Domain filter: only assign leads in
    # this rep's sub-territory (e.g., West Coast)
    member.assignment_domain = (
        '[("state_id.code", "in", '
        '["CA", "WA", "OR", "NV", "AZ"])]'
    )

Round-Robin vs. Weighted Assignment

ModeHow It WorksBest For
Round-RobinLeads distributed evenly across team members in orderEqual territories, similar rep experience levels
WeightedEach member has a capacity weight; higher weight = more leadsSenior reps handle more volume, junior reps ramp up

Server Action for Real-Time Assignment

The default cron job runs every 30 minutes. For high-velocity sales teams, that's too slow. Here's a server action that triggers assignment immediately when a lead is created or its territory changes:

Python — models/crm_lead.py (continued)
class CrmLead(models.Model):
    _inherit = "crm.lead"

    @api.model_create_multi
    def create(self, vals_list):
        leads = super().create(vals_list)
        # Trigger immediate assignment for new leads
        leads._handle_salesmen_assignment(
            auto_commit=False
        )
        return leads

    def write(self, vals):
        res = super().write(vals)
        if "x_territory" in vals or "country_id" in vals:
            # Re-assign when territory changes
            unassigned = self.filtered(
                lambda l: not l.user_id
            )
            if unassigned:
                unassigned._handle_salesmen_assignment(
                    auto_commit=False
                )
        return res
Respect the Capacity Limit

The assignment_max field on crm.team.member is not a suggestion—it's a hard cap. Once a rep hits their max active leads, the assignment engine skips them. If all reps are at capacity, leads queue up unassigned. Monitor the "Unassigned Leads" dashboard weekly—a growing queue means your team is under-staffed or over-targeted.

04

Setting Team Quotas and Revenue Targets per Territory

Territory assignment is the plumbing. Quotas are the accountability. Odoo 19 provides crm.team invoicing targets out of the box, but most companies need more granularity: monthly targets, per-rep quotas, and pipeline coverage ratios.

Configuring Team-Level Targets

Python — Setting targets via code
# Set invoicing target on the sales team
na_team = env.ref("my_module.team_na")
na_team.invoiced_target = 500000.00  # $500K/month

emea_team = env.ref("my_module.team_emea")
emea_team.invoiced_target = 350000.00  # EUR 350K/month

apac_team = env.ref("my_module.team_apac")
apac_team.invoiced_target = 200000.00  # AUD 200K/month

Custom Per-Rep Quota Tracking

Odoo's built-in target is team-level only. For per-rep quotas, extend the crm.team.member model:

Python — models/crm_team_member.py
from odoo import api, fields, models


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

    x_monthly_quota = fields.Monetary(
        string="Monthly Quota",
        currency_field="x_currency_id",
        help="Individual revenue target for this rep.",
    )
    x_currency_id = fields.Many2one(
        "res.currency",
        related="crm_team_id.company_id.currency_id",
    )
    x_quota_achieved = fields.Monetary(
        string="Achieved (MTD)",
        compute="_compute_quota_achieved",
        currency_field="x_currency_id",
    )
    x_quota_pct = fields.Float(
        string="Quota %",
        compute="_compute_quota_achieved",
    )

    @api.depends("x_monthly_quota")
    def _compute_quota_achieved(self):
        today = fields.Date.today()
        month_start = today.replace(day=1)
        for member in self:
            won_opps = self.env["crm.lead"].search([
                ("user_id", "=", member.user_id.id),
                ("team_id", "=", member.crm_team_id.id),
                ("stage_id.is_won", "=", True),
                ("date_closed", ">=", month_start),
                ("date_closed", "<=", today),
            ])
            achieved = sum(won_opps.mapped(
                "expected_revenue"
            ))
            member.x_quota_achieved = achieved
            member.x_quota_pct = (
                (achieved / member.x_monthly_quota * 100)
                if member.x_monthly_quota else 0.0
            )
Pipeline Coverage Ratio

A healthy sales team needs 3x pipeline coverage—$3 in active pipeline for every $1 of quota. If your NA team has a $500K monthly target, they need $1.5M in active opportunities. Build a dashboard widget that shows this ratio per team. When it drops below 2.5x, it's time to increase marketing spend or adjust targets.

05

Cross-Team Visibility Rules and Multi-Currency Per Region

In a multi-region setup, not every rep should see every deal. The EMEA team doesn't need to see North America's pipeline cluttering their Kanban view. But the VP of Sales needs to see everything. And each region prices in its local currency.

Record Rules for Team-Based Visibility

XML — security/crm_lead_rules.xml
<odoo>
  <!-- Sales reps see only their own team's leads -->
  <record id="rule_lead_team_visibility"
          model="ir.rule">
    <field name="name">
      CRM Lead: Team Visibility
    </field>
    <field name="model_id"
           ref="crm.model_crm_lead"/>
    <field name="domain_force">
      ['|',
        ('team_id', 'in',
          user.sale_team_id.ids),
        ('team_id', '=', False)]
    </field>
    <field name="groups"
           eval="[(4, ref('sales_team.group_sale_salesman'))]"/>
    <field name="perm_read" eval="True"/>
    <field name="perm_write" eval="True"/>
    <field name="perm_create" eval="True"/>
    <field name="perm_unlink" eval="False"/>
  </record>

  <!-- Sales managers see all teams they manage -->
  <record id="rule_lead_manager_visibility"
          model="ir.rule">
    <field name="name">
      CRM Lead: Manager Cross-Team
    </field>
    <field name="model_id"
           ref="crm.model_crm_lead"/>
    <field name="domain_force">[(1, '=', 1)]</field>
    <field name="groups"
           eval="[(4, ref('sales_team.group_sale_manager'))]"/>
  </record>
</odoo>

Multi-Currency Configuration Per Region

Each sales team is linked to a company via company_id. The company's currency determines how revenue is displayed on dashboards and reports. For multi-currency to work properly, you need:

  • Currency rate updates — enable the ECB or Open Exchange Rates provider under Accounting → Settings → Currencies. Rates update daily.
  • Pricelists per region — create a pricelist in EUR for EMEA, AUD for APAC, and USD for NA. Link them to the customer's country via fiscal position or pricelist rules.
  • Pipeline amounts in team currency — opportunity expected_revenue is stored in the company currency. When a Singapore rep logs a deal in SGD, it converts automatically using the day's rate.
Python — Multi-currency pricelist assignment
# Assign pricelists based on lead territory
class CrmLead(models.Model):
    _inherit = "crm.lead"

    def _prepare_customer_values(self, partner_name,
                                  is_company=False):
        """Override to assign regional pricelist
        when converting lead to contact."""
        values = super()._prepare_customer_values(
            partner_name, is_company=is_company
        )
        pricelist_map = {{
            "na": self.env.ref(
                "my_module.pricelist_usd"),
            "emea": self.env.ref(
                "my_module.pricelist_eur"),
            "apac": self.env.ref(
                "my_module.pricelist_aud"),
        }}
        pricelist = pricelist_map.get(self.x_territory)
        if pricelist:
            values["property_product_pricelist"] = (
                pricelist.id
            )
        return values
Dashboard Currency Trap

The CRM pipeline dashboard shows amounts in the current user's company currency, not the opportunity's currency. If your VP of Sales sits in the US company but views the EMEA pipeline, all amounts appear in USD at the current conversion rate. This is correct behavior—but confuse it with "wrong numbers" and you'll waste a week debugging. Add a note to your sales playbook explaining this.

06

3 Territory Configuration Mistakes That Sabotage Multi-Region Sales

1

Assigning Leads to Teams Without a Fallback Rule

You set up territory rules for NA, EMEA, and APAC. A lead comes in from Brazil. No rule matches. The lead sits unassigned with no team, invisible on every team's Kanban board. Nobody follows up. The prospect buys from your competitor three weeks later.

Our Fix

Always create a catch-all rule with the highest sequence number (lowest priority) and a domain of [(1, '=', 1)]. Assign it to a "Global / Unassigned" team that a sales ops person monitors daily. No lead should ever be teamless. Also set up an automated action that sends a Slack/email alert when leads land in the catch-all team.

2

Using Company-Level Multi-Company Rules When You Only Need Team Separation

You create separate Odoo companies for each region to "keep things clean." Now every cross-region report requires inter-company transactions, transfer pricing records, and a finance team that understands multi-company consolidation in Odoo. Your $50K CRM project just became a $200K ERP implementation because you used company separation where team separation would have sufficed.

Our Fix

Only use multi-company when you have separate legal entities that need independent chart of accounts, tax filings, and financial statements. For sales territory separation, use crm.team with record rules. Teams give you pipeline segmentation, assignment rules, and separate dashboards without the accounting complexity of multi-company.

3

Hardcoding Territory Logic in Python Instead of Using Configurable Rules

Your developer writes a create override with 50 lines of if/elif statements mapping countries to teams. It works. Then you expand to Latin America. The developer adds another elif. Then you split EMEA into "Western Europe" and "DACH." More elif statements. Six months later, every territory change requires a code deployment, a module upgrade, and a prayer that the logic doesn't conflict with the CRM assignment engine.

Our Fix

Use crm.team.rule records with domain expressions. These are data, not code. Sales ops can modify them through the UI (CRM → Configuration → Sales Teams → Assignment Rules) without developer involvement. Keep your Python limited to computed fields (like x_territory) that derive metadata. Let the assignment engine handle the routing logic.

BUSINESS ROI

What Proper Territory Configuration Delivers to Your Bottom Line

Territory design isn't a CRM admin task. It's a revenue architecture decision:

15-25%Higher Lead Conversion

Leads routed to the right rep within minutes convert at significantly higher rates than leads that sit in a shared pool for hours. Speed-to-contact is the strongest predictor of conversion.

ZeroTerritory Conflicts

Automated assignment eliminates the "I was working that account" arguments. Clear territory boundaries mean reps focus on selling instead of defending turf.

30 minTerritory Rebalancing

When you hire a new rep or expand to a new region, reconfiguring rules takes 30 minutes instead of a sprint of developer work. Your sales ops team controls the map.

For a company with 200 inbound leads per month and a $25,000 average deal size, improving lead-to-opportunity conversion by just 5 percentage points (e.g., from 20% to 25%) adds $250,000 in pipeline per month. At a 30% close rate, that's $75,000 in additional monthly revenue—from a one-time CRM configuration project.

SEO NOTES

Optimization Metadata

Meta Desc

Complete guide to configuring Odoo 19 sales teams and territories for multi-region operations. Covers territory mapping, automated lead assignment, team quotas, cross-team visibility, and multi-currency setup.

H2 Keywords

1. "Designing the Sales Team Hierarchy in Odoo 19"
2. "Mapping Territories by Geography, Industry, and Account Size"
3. "Automating Lead and Opportunity Assignment by Territory"
4. "Setting Team Quotas and Revenue Targets per Territory"
5. "3 Territory Configuration Mistakes That Sabotage Multi-Region Sales"

Your Territory Design Is Your Revenue Architecture

A well-configured territory structure does more than organize your CRM. It ensures every lead reaches the right rep in the right region within minutes, not days. It gives managers real-time visibility into pipeline by territory. It connects quotas to actual CRM data instead of living in a disconnected spreadsheet. And it scales—adding a new region or splitting a territory is a configuration change, not a development project.

If your sales teams are still fighting over leads or your pipeline reports require manual territory tagging, let's fix the foundation. We design and implement territory structures in Odoo 19 that match how your business actually sells—by region, by vertical, by account size, or any combination. The project typically takes 1-2 weeks and the ROI shows up in your very first month of clean pipeline data.

Book a Free Territory Audit