GuideMarch 13, 2026

Sales Commission Management:
Rules, Tiers & Automated Payouts

INTRODUCTION

Spreadsheet Commissions Are Costing You More Than You Think

Most companies running Odoo still calculate sales commissions in spreadsheets. A finance analyst exports confirmed invoices, cross-references them against a PDF commission policy document, manually calculates each rep's payout, and pastes the results into a payroll adjustment. The process takes 2–3 days every month and nobody trusts the numbers.

The problems compound fast. Reps dispute calculations they can't verify. Managers can't see commission accruals mid-month. Finance discovers errors after payroll has already run. When the commission structure changes—a new product tier, a shifted quota threshold, a territory reassignment—the spreadsheet becomes a liability that only one person knows how to maintain.

Odoo 19 has the building blocks for a fully automated commission system: computed fields, server actions, scheduled automation, and deep integration with payroll and accounting. This guide walks you through the complete architecture: how to design commission plans with tiered rules, track performance at the team and individual level, automate payout calculations, and integrate the results with payroll and journal entries.

01

Designing Commission Plan Structures in Odoo 19

Before writing any code, you need a data model that separates the commission plan (the policy) from the commission line (the individual calculation result). This separation is critical because plans change over time, but historical payouts must remain auditable against the rules that were active when the sale was made.

The Commission Plan Model

Python — models/commission_plan.py
from odoo import fields, models


class CommissionPlan(models.Model):
    _name = "commission.plan"
    _description = "Sales Commission Plan"

    name = fields.Char(required=True)
    active = fields.Boolean(default=True)
    date_from = fields.Date(
        string="Effective From",
        required=True,
    )
    date_to = fields.Date(string="Effective Until")
    company_id = fields.Many2one(
        "res.company",
        default=lambda self: self.env.company,
    )
    rule_ids = fields.One2many(
        "commission.plan.rule",
        "plan_id",
        string="Commission Rules",
    )
    salesperson_ids = fields.Many2many(
        "res.users",
        string="Assigned Salespeople",
        help="Leave empty to apply to all salespeople.",
    )
    team_ids = fields.Many2many(
        "crm.team",
        string="Assigned Sales Teams",
    )

The Commission Rule Model

Each plan contains one or more rules that define how commissions are calculated. Rules support three calculation types: flat amount per invoice, percentage of revenue, and graduated tiers based on cumulative sales volume.

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


class CommissionPlanRule(models.Model):
    _name = "commission.plan.rule"
    _description = "Commission Plan Rule"
    _order = "threshold_min asc"

    plan_id = fields.Many2one(
        "commission.plan",
        required=True,
        ondelete="cascade",
    )
    rule_type = fields.Selection(
        [
            ("flat", "Flat Amount per Invoice"),
            ("percentage", "Percentage of Revenue"),
            ("graduated", "Graduated Tier"),
        ],
        required=True,
        default="percentage",
    )
    product_category_id = fields.Many2one(
        "product.category",
        string="Product Category",
        help="Leave empty to apply to all categories.",
    )
    threshold_min = fields.Float(
        string="Minimum Revenue Threshold",
        help="Cumulative revenue required to activate "
             "this tier (graduated rules only).",
    )
    threshold_max = fields.Float(
        string="Maximum Revenue Threshold",
    )
    rate = fields.Float(
        string="Commission Rate (%)",
        help="Percentage rate for percentage/graduated rules.",
    )
    flat_amount = fields.Float(
        string="Flat Commission Amount",
        help="Fixed amount per qualifying invoice.",
    )
Effective Date Ranges

Always scope commission plans to a date range. When the sales director changes the rate from 5% to 7% mid-quarter, you create a new plan version—you don't edit the existing one. Historical commission lines reference the plan version that was active at the time of the sale. This prevents disputes and audit issues.

02

Implementing Tiered Commission Rules: Flat, Percentage, and Graduated

The real complexity in commission management isn't the base calculation—it's handling graduated tiers where the rate changes as cumulative revenue crosses thresholds. A rep earning 5% on the first $50,000 and 8% on everything above $50,000 in a quarter needs the calculation engine to split the revenue across tiers correctly.

The Commission Calculation Engine

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


class CommissionLine(models.Model):
    _name = "commission.line"
    _description = "Commission Calculation Line"

    salesperson_id = fields.Many2one("res.users", required=True)
    plan_id = fields.Many2one("commission.plan", required=True)
    rule_id = fields.Many2one("commission.plan.rule")
    invoice_id = fields.Many2one("account.move")
    date = fields.Date(default=fields.Date.today)
    base_amount = fields.Float(
        string="Revenue Base",
        help="Invoice amount used for calculation.",
    )
    commission_amount = fields.Float(
        string="Commission Earned",
        compute="_compute_commission",
        store=True,
    )
    state = fields.Selection(
        [
            ("draft", "Draft"),
            ("confirmed", "Confirmed"),
            ("paid", "Paid"),
        ],
        default="draft",
    )

    @api.depends("base_amount", "rule_id", "rule_id.rule_type",
                 "rule_id.rate", "rule_id.flat_amount")
    def _compute_commission(self):
        for line in self:
            rule = line.rule_id
            if not rule:
                line.commission_amount = 0.0
                continue

            if rule.rule_type == "flat":
                line.commission_amount = rule.flat_amount

            elif rule.rule_type == "percentage":
                line.commission_amount = (
                    line.base_amount * rule.rate / 100.0
                )

            elif rule.rule_type == "graduated":
                line.commission_amount = (
                    line._calculate_graduated(rule)
                )

    def _calculate_graduated(self, rule):
        """Split revenue across graduated tiers."""
        plan = rule.plan_id
        tiers = plan.rule_ids.filtered(
            lambda r: r.rule_type == "graduated"
        ).sorted("threshold_min")

        # Get cumulative revenue before this invoice
        previous = self.env["commission.line"].search([
            ("salesperson_id", "=", self.salesperson_id.id),
            ("plan_id", "=", plan.id),
            ("id", "!=", self.id or 0),
            ("date", ">=", plan.date_from),
            ("date", "<=", plan.date_to or "2099-12-31"),
        ])
        cum_before = sum(previous.mapped("base_amount"))
        cum_after = cum_before + self.base_amount

        total_commission = 0.0
        for tier in tiers:
            t_min = tier.threshold_min
            t_max = tier.threshold_max or float("inf")

            # Portion of this invoice in this tier
            tier_start = max(cum_before, t_min)
            tier_end = min(cum_after, t_max)

            if tier_start < tier_end:
                taxable = tier_end - tier_start
                total_commission += taxable * tier.rate / 100.0

        return total_commission

Example: Graduated Tier Configuration

Here's how a typical quarterly graduated commission plan looks when configured in Odoo:

TierRevenue RangeCommission RateExample Payout on $120,000 Quarterly Sales
Base$0 – $50,0005%$2,500
Accelerator$50,001 – $100,0008%$4,000
Overachiever$100,001+12%$2,400
Total Quarterly Commission$8,900
Graduated vs. Marginal

Make sure your sales team understands the difference between graduated (each dollar is taxed at the rate of its tier) and flat-tier (all revenue retroactively switches to the highest tier reached). The graduated approach above pays $8,900 on $120K. A flat-tier approach at 12% would pay $14,400. This distinction causes more commission disputes than any other structural decision.

03

Team vs. Individual Commission Tracking and Sales Attribution

Not every sale is a solo effort. When a sales engineer assists on a deal, when a manager's team hits a collective target, or when territory-based bonuses apply alongside individual commissions, you need attribution rules that split credit accurately without double-counting.

Multi-Level Attribution Model

Python — models/commission_attribution.py
from odoo import fields, models


class CommissionAttribution(models.Model):
    _name = "commission.attribution"
    _description = "Commission Credit Attribution"

    invoice_id = fields.Many2one(
        "account.move", required=True, ondelete="cascade",
    )
    salesperson_id = fields.Many2one("res.users", required=True)
    role = fields.Selection(
        [
            ("primary", "Primary Rep"),
            ("overlay", "Sales Overlay / Engineer"),
            ("manager", "Team Manager"),
        ],
        required=True,
        default="primary",
    )
    split_pct = fields.Float(
        string="Credit Split %",
        default=100.0,
        help="Percentage of invoice revenue credited "
             "to this person.",
    )
    team_id = fields.Many2one(
        "crm.team",
        related="salesperson_id.sale_team_id",
        store=True,
    )

Automated Attribution via Server Action

When an invoice is confirmed, a server action automatically creates attribution records based on the sales order's team structure:

Python — models/account_move.py
from odoo import models


class AccountMove(models.Model):
    _inherit = "account.move"

    def action_post(self):
        """Override to create commission attributions."""
        res = super().action_post()
        for move in self.filtered(
            lambda m: m.move_type == "out_invoice"
        ):
            move._create_commission_attributions()
        return res

    def _create_commission_attributions(self):
        Attribution = self.env["commission.attribution"]
        so = self.invoice_line_ids.sale_line_ids.order_id[:1]

        # Primary rep from the sales order
        if so and so.user_id:
            Attribution.create({
                "invoice_id": self.id,
                "salesperson_id": so.user_id.id,
                "role": "primary",
                "split_pct": 80.0,
            })

        # Team manager gets override commission
        if so and so.team_id and so.team_id.user_id:
            Attribution.create({
                "invoice_id": self.id,
                "salesperson_id": so.team_id.user_id.id,
                "role": "manager",
                "split_pct": 20.0,
            })
Prevent Double-Counting

Splits don't have to add up to 100%. A common pattern is 100% credit to the rep plus a 10% override to the manager (110% total attribution). This is intentional—the manager override comes from a separate cost center. What you must prevent is creating duplicate attribution records when an invoice is reset to draft and re-posted. Add a check for existing attributions before creating new ones.

04

Automating Commission Payouts: Scheduled Calculations, Payroll Integration, and Journal Entries

The calculation engine means nothing if payouts still require manual intervention. The final piece is automation: a scheduled action that runs commission calculations, creates payroll inputs or journal entries, and posts the results for review and approval.

Scheduled Commission Calculation

Python — models/commission_cron.py
from odoo import fields, models
from dateutil.relativedelta import relativedelta


class CommissionCron(models.Model):
    _inherit = "commission.plan"

    def _cron_calculate_commissions(self):
        """Monthly cron: calculate commissions for
        all confirmed invoices without commission lines."""
        today = fields.Date.today()
        last_month_start = (
            today - relativedelta(months=1)
        ).replace(day=1)
        last_month_end = today.replace(day=1) - relativedelta(
            days=1
        )

        invoices = self.env["account.move"].search([
            ("move_type", "=", "out_invoice"),
            ("state", "=", "posted"),
            ("invoice_date", ">=", last_month_start),
            ("invoice_date", "<=", last_month_end),
            ("commission_line_ids", "=", False),
        ])

        for invoice in invoices:
            attributions = self.env[
                "commission.attribution"
            ].search([
                ("invoice_id", "=", invoice.id),
            ])
            for attr in attributions:
                plan = self._find_active_plan(
                    attr.salesperson_id, invoice.invoice_date
                )
                if not plan:
                    continue

                base = (
                    invoice.amount_untaxed
                    * attr.split_pct / 100.0
                )
                rule = plan._match_rule(
                    base, attr.salesperson_id,
                    invoice.invoice_date,
                )
                if rule:
                    self.env["commission.line"].create({
                        "salesperson_id": attr.salesperson_id.id,
                        "plan_id": plan.id,
                        "rule_id": rule.id,
                        "invoice_id": invoice.id,
                        "base_amount": base,
                        "state": "draft",
                    })

Cron Job XML Registration

XML — data/commission_cron.xml
<odoo>
  <record id="ir_cron_commission_calculate"
          model="ir.cron">
    <field name="name">
      Calculate Monthly Commissions
    </field>
    <field name="model_id"
           ref="model_commission_plan"/>
    <field name="state">code</field>
    <field name="code">
      model._cron_calculate_commissions()
    </field>
    <field name="interval_number">1</field>
    <field name="interval_type">months</field>
    <field name="numbercall">-1</field>
    <field name="doall" eval="False"/>
  </record>
</odoo>

Accounting Integration: Commission Journal Entries

Once commission lines are confirmed, they need to flow into your accounting. The cleanest approach is creating journal entries that debit a commission expense account and credit a payable account for each salesperson:

Python — models/commission_line.py (payout method)
def action_create_journal_entry(self):
    """Create accounting entries for confirmed
    commission lines."""
    lines_by_person = {}
    for line in self.filtered(
        lambda l: l.state == "confirmed"
    ):
        key = line.salesperson_id.id
        lines_by_person.setdefault(key, []).append(line)

    journal = self.env.ref(
        "my_commission.commission_journal"
    )
    expense_account = self.env.ref(
        "my_commission.account_commission_expense"
    )
    payable_account = self.env.ref(
        "my_commission.account_commission_payable"
    )

    for user_id, comm_lines in lines_by_person.items():
        user = self.env["res.users"].browse(user_id)
        total = sum(l.commission_amount for l in comm_lines)

        move = self.env["account.move"].create({
            "journal_id": journal.id,
            "ref": f"Commission - {{user.name}}",
            "line_ids": [
                (0, 0, {
                    "account_id": expense_account.id,
                    "debit": total,
                    "name": f"Commission expense: "
                            f"{{user.name}}",
                }),
                (0, 0, {
                    "account_id": payable_account.id,
                    "credit": total,
                    "partner_id":
                        user.partner_id.id,
                    "name": f"Commission payable: "
                            f"{{user.name}}",
                }),
            ],
        })
        move.action_post()

        for cl in comm_lines:
            cl.state = "paid"

Reporting Dashboard: Pivot and Graph Views

Give sales managers and finance a real-time view of commission accruals, payouts, and performance against targets:

XML — views/commission_line_views.xml
<odoo>
  <!-- Pivot view: commissions by salesperson -->
  <record id="commission_line_pivot"
          model="ir.ui.view">
    <field name="name">commission.line.pivot</field>
    <field name="model">commission.line</field>
    <field name="arch" type="xml">
      <pivot string="Commission Analysis">
        <field name="salesperson_id" type="row"/>
        <field name="date" type="col"
               interval="month"/>
        <field name="commission_amount"
               type="measure"/>
        <field name="base_amount"
               type="measure"/>
      </pivot>
    </field>
  </record>

  <!-- Graph view: monthly commission trends -->
  <record id="commission_line_graph"
          model="ir.ui.view">
    <field name="name">commission.line.graph</field>
    <field name="model">commission.line</field>
    <field name="arch" type="xml">
      <graph string="Commission Trends"
             type="bar" stacked="True">
        <field name="date" type="row"
               interval="month"/>
        <field name="salesperson_id" type="col"/>
        <field name="commission_amount"
               type="measure"/>
      </graph>
    </field>
  </record>
</odoo>
Payroll Integration Alternative

If you use Odoo Payroll, create commission lines as payslip input lines instead of journal entries. Define a salary rule with code COMMISSION that reads from commission.line records. This keeps commissions inside the payroll workflow—subject to tax calculations, deductions, and payslip approval—rather than as standalone journal entries that bypass payroll entirely.

05

3 Commission Management Mistakes That Create Disputes and Accounting Errors

1

Calculating Commissions on Invoices Instead of Payments

You configure the commission engine to trigger when an invoice is posted. The rep earns 8% of a $50,000 deal and gets paid $4,000 in commission. Two months later, the customer goes silent—the invoice becomes a bad debt. You've paid commission on revenue you never collected. This is the most expensive commission architecture mistake because it compounds across every uncollectable invoice.

Our Fix

Create commission lines in draft state when the invoice is posted, but only move them to confirmed when the corresponding payment is reconciled. Use the reconcile event on account.move.line as the trigger. For partial payments, confirm a proportional commission amount. This way, commissions track actual cash collection, not just booked revenue.

2

Not Handling Credit Notes and Invoice Reversals

A customer returns $15,000 worth of product. The credit note is posted against the original invoice. But the commission system doesn't see credit notes—it only processes out_invoice records. The rep keeps the full commission on revenue that was clawed back. Over a quarter, unhandled credit notes can inflate commission payouts by 5–15% depending on your return rate.

Our Fix

Extend the commission engine to process out_refund moves. When a credit note is posted, create a negative commission line linked to the original invoice's commission record. The rep's next payout automatically deducts the clawback. For graduated tiers, recalculate the cumulative revenue to potentially shift the rep down a tier—this requires reprocessing all commission lines for the affected period.

3

Hardcoding Commission Rates Instead of Using Date-Scoped Plan Versions

The sales director changes the commission rate from 5% to 7% starting next quarter. Someone updates the rule record directly. Now every historical commission line recalculates because the stored computed field references the current rule rate, not the rate that was active when the sale was made. The January payouts that were already disbursed at 5% now show 7% in the system. Your audit trail is destroyed.

Our Fix

Never edit an active commission plan. Archive the old plan, create a new version with the updated rates, and set the date_from to the new effective date. Commission lines reference a specific plan_id and rule_id—once paid, those records are immutable. Add a rate_snapshot field on commission.line that stores the rate at calculation time, so even if someone accidentally edits a rule, the historical payout amount is preserved.

BUSINESS ROI

What Automated Commission Management Saves Your Business

Commission automation isn't an HR convenience project. It's a finance accuracy and sales retention initiative:

90%Less Manual Calculation Time

A 20-person sales team generating 200+ invoices per month means 2–3 days of spreadsheet work every pay cycle. Automated calculations reduce this to a 15-minute review and approval step.

ZeroCommission Disputes

Reps can see their commission accruals in real time through their Odoo portal. Every line traces back to a specific invoice, rule, and rate. Transparency eliminates the "I think I was shorted" conversations.

3–5%Payout Accuracy Improvement

Manual commission calculations typically have a 3–5% error rate from missed credit notes, wrong tier assignments, and split calculation mistakes. Automated systems eliminate these entirely.

For a company with $5M in annual commissioned sales at an average 7% rate, a 4% calculation error means $14,000 in annual overpayments or underpayments. Overpayments erode margins. Underpayments erode trust and drive rep turnover—which costs 50–200% of a sales rep's annual compensation to backfill.

SEO NOTES

Optimization Metadata

Meta Desc

Complete guide to building sales commission management in Odoo 19. Commission plans, tiered rules, team attribution, automated payouts, payroll integration, and reporting dashboards.

H2 Keywords

1. "Designing Commission Plan Structures in Odoo 19"
2. "Implementing Tiered Commission Rules: Flat, Percentage, and Graduated"
3. "Team vs. Individual Commission Tracking and Sales Attribution"
4. "Automating Commission Payouts: Scheduled Calculations, Payroll Integration, and Journal Entries"

Your Sales Team Deserves a Commission System They Can Trust

Commission disputes are a symptom of a broken process, not a people problem. When reps can't see how their payout was calculated, when managers can't project commission costs mid-quarter, and when finance spends days reconciling spreadsheets that don't match payroll—the problem is architectural. The data exists in Odoo. It just needs a system that connects invoices to rules to payouts without manual intervention.

If your commission process still involves spreadsheets, we should automate it. We design and implement complete commission management systems in Odoo 19—from plan configuration to graduated tier calculations to payroll integration. The typical project takes 2–4 weeks and pays for itself within the first quarter through reduced errors and eliminated manual work.

Book a Free Commission Audit