Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions commission_product_fixed/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
===============================
Commission Product Fixed Amount
===============================

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

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

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

This module adds a new commission type, **Fixed amount per product**, on
top of the OCA commission framework.

Instead of computing the commission as a percentage of the sale/invoice
amount or the margin, the commission is a fixed amount defined per
product. The amount is independent from the selling price.

Because each agent is assigned its own commission, the same product can
grant a different fixed amount depending on the agent: just define a
separate commission per agent with its own per-product amounts.

**Table of contents**

.. contents::
:local:

Usage
=====

1. Go to *Invoicing > Configuration > Commissions > Commission types*.
2. Create a commission and set its *Type* to *Fixed amount per product*.
3. In the *Product amounts* table, add a line per product with the fixed
commission amount granted per unit. Optionally set:

- a *Min. Quantity* to grant a different amount above a quantity
threshold (add several lines for the same product; the highest
applicable threshold wins);
- a *Start Date* / *End Date* to limit the validity of the amount;
- a *Currency* (the amount is converted to the invoice currency when
they differ).

4. Assign this commission to an agent (*Contacts > the agent > Sales &
Purchase > Commission*). Use a different commission per agent to
grant different amounts for the same product.
5. On a customer invoice, the agent is added to the lines and the
commission is computed as ``fixed amount x quantity``, using the line
matching the invoice date and quantity.

If a product on a posted invoice line has no fixed amount defined in the
agent's commission, posting the invoice is blocked until the amount is
configured.

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

Bugs are tracked on `GitHub Issues <https://github.com/OCA/commission/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/OCA/commission/issues/new?body=module:%20commission_product_fixed%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.

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

Credits
=======

Authors
-------

* Quartile

Contributors
------------

- Quartile (https://www.quartile.co)

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/commission <https://github.com/OCA/commission/tree/18.0/commission_product_fixed>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
1 change: 1 addition & 0 deletions commission_product_fixed/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
18 changes: 18 additions & 0 deletions commission_product_fixed/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Copyright 2026 Quartile
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
{
"name": "Commission Product Fixed Amount",
"version": "19.0.1.0.0",
"author": "Quartile, Odoo Community Association (OCA)",
"category": "Sales Management",
"license": "AGPL-3",
"website": "https://github.com/OCA/commission",
"depends": [
"account_commission_oca",
],
"data": [
"security/ir.model.access.csv",
"views/commission_views.xml",
],
"installable": True,
}
3 changes: 3 additions & 0 deletions commission_product_fixed/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from . import commission
from . import commission_line_mixin
from . import account_move
71 changes: 71 additions & 0 deletions commission_product_fixed/models/account_move.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Copyright 2026 Quartile
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

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


class AccountInvoiceLineAgent(models.Model):
_inherit = "account.invoice.line.agent"

@api.depends("object_id.quantity", "invoice_date")
def _compute_amount(self):
# ``product_fixed`` amounts depend on the invoiced quantity and on the
# invoice date (for quantity tiers and validity windows), which are not
# part of the base dependencies. Add them so the commission amount is
# recomputed when they change.
return super()._compute_amount()


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

def action_post(self):
self._check_product_fixed_commission()
return super().action_post()

def _check_product_fixed_commission(self):
"""Block posting when a product/agent fixed commission is not defined.

For every commission of type ``product_fixed`` assigned on an invoice
line, the corresponding product must have an amount configured in the
commission. Otherwise the commission cannot be determined and the
invoice must not be posted.
"""
missing = []
for move in self:
if move.move_type[:3] != "out":
continue
for line in move.invoice_line_ids:
if line.commission_free:
continue
if not line.product_id:
continue
for agent in line.agent_ids:
commission = agent.commission_id
if commission.commission_type != "product_fixed":
continue
# Use the same effective date as ``_get_commission_amount``
# (the agent line's own ``invoice_date``) so the guard and
# the amount computation never disagree on line validity.
date = agent.invoice_date or fields.Date.context_today(agent)
if commission._get_product_fixed_line(
line.product_id, date, line.quantity
):
continue
missing.append(
self.env._(
"%(agent)s / %(product)s (commission: %(commission)s)",
agent=agent.agent_id.display_name,
product=line.product_id.display_name,
commission=commission.display_name,
)
)
if missing:
raise ValidationError(
self.env._(
"No fixed commission amount is defined for the following "
"agent / product combinations:\n%s",
"\n".join(f"- {m}" for m in missing),
)
)
93 changes: 93 additions & 0 deletions commission_product_fixed/models/commission.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Copyright 2026 Quartile
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

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


class Commission(models.Model):
_inherit = "commission"

commission_type = fields.Selection(
selection_add=[("product_fixed", "Fixed amount per product")],
ondelete={"product_fixed": "set default"},
)
product_line_ids = fields.One2many(
comodel_name="commission.product.line",
inverse_name="commission_id",
string="Product amounts",
copy=True,
)

def _get_product_fixed_line(self, product, date=None, quantity=0.0):
"""Return the applicable product amount line (or empty).

Lines are filtered by product, validity dates and minimum quantity.
The most specific match (highest applicable ``min_qty``) is selected
explicitly, so the result does not depend on the recordset order.
"""
self.ensure_one()
lines = self.product_line_ids.filtered(
lambda x: x.product_id == product and x.min_qty <= quantity
)
if date:
lines = lines.filtered(
lambda x: (not x.date_start or x.date_start <= date)
and (not x.date_end or x.date_end >= date)
)
return lines.sorted(key=lambda x: x.min_qty, reverse=True)[:1]


class CommissionProductLine(models.Model):
_name = "commission.product.line"
_description = "Fixed commission amount per product"
_order = "sequence, min_qty desc, id"
_rec_name = "product_id"

sequence = fields.Integer(default=10)
commission_id = fields.Many2one(
comodel_name="commission",
required=True,
ondelete="cascade",
)
product_id = fields.Many2one(
comodel_name="product.product",
string="Product",
required=True,
)
min_qty = fields.Float(
string="Min. Quantity",
default=0.0,
help="The minimum invoiced quantity for this amount to apply. When "
"several lines match a product, the one with the highest applicable "
"minimum quantity is used.",
)
amount = fields.Monetary(
string="Commission Amount",
required=True,
help="Fixed commission amount granted per unit of the product.",
)
currency_id = fields.Many2one(
comodel_name="res.currency",
default=lambda self: self.env.company.currency_id,
required=True,
)
date_start = fields.Date(
string="Start Date",
help="Start date for this amount (inclusive).",
)
date_end = fields.Date(
string="End Date",
help="End date for this amount (inclusive).",
)

@api.constrains("date_start", "date_end")
def _check_dates(self):
for line in self:
if not line.date_start or not line.date_end:
continue
if line.date_start <= line.date_end:
continue
raise ValidationError(
self.env._("The start date cannot be after the end date.")
)
41 changes: 41 additions & 0 deletions commission_product_fixed/models/commission_line_mixin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Copyright 2026 Quartile
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from odoo import fields, models


class CommissionLineMixin(models.AbstractModel):
_inherit = "commission.line.mixin"

def _get_commission_amount(self, commission, subtotal, product, quantity):
"""Compute the commission as a fixed amount per product unit.

The sale price, discount and margin are not taken into account: the
amount is the per-product fixed value (which differs by agent, as each
agent points to its own commission) multiplied by the quantity.
"""
self.ensure_one()
if commission and commission.commission_type == "product_fixed":
if product.commission_free:
return 0.0
date = getattr(self, "invoice_date", False) or (
fields.Date.context_today(self)
)
line = commission._get_product_fixed_line(product, date, quantity)
if not line:
# Missing configuration. The amount is left at 0 here and the
# posting of the invoice is blocked in account.move to force a
# proper setup (see _check_product_fixed_commission).
return 0.0
amount = line.amount * quantity
if (
line.currency_id
and self.currency_id
and (line.currency_id != self.currency_id)
):
company = getattr(self, "company_id", False) or self.env.company
amount = line.currency_id._convert(
amount, self.currency_id, company, date
)
return amount
return super()._get_commission_amount(commission, subtotal, product, quantity)
3 changes: 3 additions & 0 deletions commission_product_fixed/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[build-system]
requires = ["whool"]
build-backend = "whool.buildapi"
2 changes: 2 additions & 0 deletions commission_product_fixed/readme/CONTRIBUTORS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- Quartile (<https://www.quartile.co>)
- Tatsuki Kanda
10 changes: 10 additions & 0 deletions commission_product_fixed/readme/DESCRIPTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
This module adds a new commission type, **Fixed amount per product**, on top
of the OCA commission framework.

Instead of computing the commission as a percentage of the sale/invoice amount
or the margin, the commission is a fixed amount defined per product. The amount
is independent from the selling price.

Because each agent is assigned its own commission, the same product can grant a
different fixed amount depending on the agent: just define a separate commission
per agent with its own per-product amounts.
20 changes: 20 additions & 0 deletions commission_product_fixed/readme/USAGE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
1. Go to *Invoicing > Configuration > Commissions > Commission types*.
2. Create a commission and set its *Type* to *Fixed amount per product*.
3. In the *Product amounts* table, add a line per product with the fixed
commission amount granted per unit. Optionally set:
- a *Min. Quantity* to grant a different amount above a quantity threshold
(add several lines for the same product; the highest applicable
threshold wins);
- a *Start Date* / *End Date* to limit the validity of the amount;
- a *Currency* (the amount is converted to the invoice currency when they
differ).
4. Assign this commission to an agent (*Contacts > the agent > Sales &
Purchase > Commission*). Use a different commission per agent to grant
different amounts for the same product.
5. On a customer invoice, the agent is added to the lines and the commission
is computed as `fixed amount x quantity`, using the line matching the
invoice date and quantity.

If a product on a posted invoice line has no fixed amount defined in the
agent's commission, posting the invoice is blocked until the amount is
configured.
3 changes: 3 additions & 0 deletions commission_product_fixed/security/ir.model.access.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_commission_product_line_manager,access_commission_product_line_manager,model_commission_product_line,commission_oca.group_commission_manager,1,1,1,1
access_commission_product_line_user,access_commission_product_line_user,model_commission_product_line,commission_oca.group_commission_user,1,0,0,0
Loading
Loading