Skip to content

Commit e3dafa3

Browse files
committed
[ADD] sale_commission: Automate sales commission calculation
This module automates the calculation of commissions for salespersons and sales teams based on predefined rules. Commissions can be applied at invoicing or payment and linked to specific products, categories, or general sales. Features: - Define commission rules for salespersons and sales teams. - Apply commissions at invoicing or payment stages. - Support product and category-based commission rules. - Automatically generate commission lines linked to invoices.
1 parent 4c650f3 commit e3dafa3

11 files changed

+382
-0
lines changed

sale_commission/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import models

sale_commission/__manifest__.py

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
'name': 'Sale Commission',
3+
'version': '1.0',
4+
'category': 'Sales/Commission',
5+
'summary': "Manage your salespersons' commissions",
6+
'depends': ['sale_management'],
7+
'data': [
8+
'demo/commission_rule.xml',
9+
'views/commission_rule_views.xml',
10+
'report/commission_report.xml',
11+
'views/commission_menu.xml',
12+
'security/ir.model.access.csv',
13+
],
14+
'installable': True,
15+
'license': 'AGPL-3',
16+
}
+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<odoo>
3+
<record id="commission_rule_1" model="commission.rule">
4+
<field name="rate">0.5</field>
5+
<field name="due_at">invoicing</field>
6+
<field name="commission_for">person</field>
7+
<field name="user_id" ref="base.user_admin"/>
8+
<field name="product_expired">no_impact</field>
9+
</record>
10+
11+
<record id="commission_rule_2" model="commission.rule">
12+
<field name="rate">0.3</field>
13+
<field name="due_at">payment</field>
14+
<field name="commission_for">team</field>
15+
<field name="team_id" ref="sales_team.team_sales_department"/>
16+
<field name="product_expired">no_impact</field>
17+
</record>
18+
19+
<record id="commission_rule_3" model="commission.rule">
20+
<field name="rate">0.7</field>
21+
<field name="due_at">invoicing</field>
22+
<field name="commission_for">person</field>
23+
<field name="product_category_id" ref="product.product_category_all"/>
24+
<field name="user_id" ref="base.user_admin"/>
25+
<field name="product_expired">no_impact</field>
26+
</record>
27+
28+
<record id="commission_rule_4" model="commission.rule">
29+
<field name="product_category_id" ref="product.product_category_consumable"/>
30+
<field name="rate">0.4</field>
31+
<field name="due_at">payment</field>
32+
<field name="commission_for">team</field>
33+
<field name="team_id" ref="sales_team.team_sales_department"/>
34+
<field name="product_expired">no_impact</field>
35+
</record>
36+
37+
<record id="commission_rule_5" model="commission.rule">
38+
<field name="rate">0.2</field>
39+
<field name="due_at">invoicing</field>
40+
<field name="commission_for">person</field>
41+
<field name="user_id" ref="base.user_admin"/>
42+
<field name="product_expired">no_impact</field>
43+
</record>
44+
</odoo>

sale_commission/models/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from . import account_move
2+
from . import commission_rule
3+
from . import commission_rule_line
+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
from odoo import models
2+
3+
4+
class AccountMove(models.Model):
5+
_inherit = 'account.move'
6+
7+
def _post(self, soft=True):
8+
"""Overrides invoice posting to trigger commission calculation."""
9+
res = super()._post(soft)
10+
11+
self.filtered(
12+
lambda m:
13+
m.move_type == 'out_invoice'
14+
and (m.invoice_user_id or m.team_id)
15+
)._create_commission_rule_lines('invoicing')
16+
17+
return res
18+
19+
def action_register_payment(self):
20+
"""Overrides payment registration to trigger commision calculation."""
21+
res = super().action_register_payment()
22+
commission_lines = self._get_applicable_commission()
23+
self.filtered(
24+
lambda m: (
25+
m.move_type == 'out_invoice'
26+
and (m.invoice_user_id or m.team_id)
27+
and m not in commission_lines.move_id
28+
)
29+
)._create_commission_rule_lines('payment')
30+
return res
31+
32+
def _get_applicable_commission(self):
33+
"""Check if a commission entry already exists for recordset."""
34+
return self.env['commission.rule.line'].search_fetch(
35+
[('move_id', 'in', self.ids)],
36+
['move_id']
37+
)
38+
39+
def _create_commission_rule_lines(self, trigger_stage):
40+
for move in self:
41+
commission_rules = self.env['commission.rule'].search([
42+
('due_at', '=', trigger_stage),
43+
('product_id', 'in', move.invoice_line_ids.product_id.ids + [False]),
44+
('product_category_id', 'in', move.invoice_line_ids.product_id.categ_id.ids + [False]),
45+
('user_id', 'in', [move.invoice_user_id.id ,False]),
46+
('team_id', 'in', [move.invoice_user_id.sale_team_id.id, False]),
47+
])
48+
49+
person_rule = self.env['commission.rule']
50+
team_rule = self.env['commission.rule']
51+
52+
for rule in commission_rules:
53+
if rule.commission_for == 'person' and not person_rule:
54+
person_rule = rule
55+
if rule.commission_for == 'team' and not team_rule:
56+
team_rule = rule
57+
if person_rule and team_rule:
58+
break
59+
60+
commission_rule_lines = [{
61+
'date': move.invoice_date,
62+
'user_id': rule.user_id.id,
63+
'team_id': rule.team_id.id,
64+
'move_id': move.id,
65+
'amount': move.amount_total * (rule.rate),
66+
'currency_id': move.currency_id.id,
67+
'commission_rule_id' : rule.id,
68+
}
69+
for rule in (person_rule, team_rule)
70+
]
71+
if commission_rule_lines:
72+
self.env['commission.rule.line'].create(commission_rule_lines)
+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
from odoo import _, api, fields, models
2+
3+
4+
class CommissionRule(models.Model):
5+
_name = 'commission.rule'
6+
_description = "Commission Rule"
7+
8+
sequence = fields.Integer('Sequence', default=1, help="Used to order commission rule.")
9+
rate = fields.Float(string="Commission Rate", required=True)
10+
commission_for = fields.Selection(
11+
selection=[
12+
('person', "Salesperson"),
13+
('team', "Sales Team")
14+
],
15+
string="Commission for",
16+
required=True,
17+
default='person'
18+
)
19+
due_at = fields.Selection(
20+
selection=[
21+
('invoicing', "Invoicing"),
22+
('payment', "Payment"),
23+
],
24+
string="Due at",
25+
required=True,
26+
)
27+
product_expired = fields.Selection(
28+
selection=[
29+
('no_impact', "No Impact"),
30+
('yes', "Yes"),
31+
('no', "No")
32+
],
33+
string="Product Expired",
34+
required=True
35+
)
36+
max_discount = fields.Float(string="Max Discount")
37+
on_fast_payment = fields.Boolean(string="On Fast Payment")
38+
fast_payment_days = fields.Integer(string="Before Days")
39+
display_name = fields.Char(string="Condition", compute="_compute_display_name", store=True)
40+
41+
product_category_id = fields.Many2one('product.category', string="Product Category")
42+
product_id = fields.Many2one('product.product', string="Product")
43+
user_id = fields.Many2one('res.users', string="Salesperson")
44+
team_id = fields.Many2one('crm.team', string="Sales Team")
45+
46+
@api.depends('product_category_id', 'product_id', 'team_id', 'user_id')
47+
def _compute_display_name(self):
48+
"""Computes the display name based on selected fields in 'Apply On'."""
49+
_ = self.env._
50+
fields_mapping = (
51+
(_("Category"), 'product_category_id'),
52+
(_("Product"), 'product_id'),
53+
(_("Salesperson"), 'user_id'),
54+
(_("Sales Team"), 'team_id')
55+
)
56+
for rule in self:
57+
conditions = [
58+
_("%(display_name)s: %(value)s", display_name=display_name, value=rule[fname].name)
59+
for display_name, fname in fields_mapping
60+
if rule[fname]
61+
]
62+
if rule.max_discount:
63+
conditions.append(_("Max Discount: %s", rule.max_discount))
64+
if rule.product_expired:
65+
conditions.append(_("Product Expired: %s", rule.product_expired))
66+
67+
rule.display_name = _(" AND ").join(conditions) if conditions else _("No Conditions")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from odoo import fields, models
2+
3+
4+
class CommissionRuleLine(models.Model):
5+
_name = 'commission.rule.line'
6+
_description = "Commission Rule Line"
7+
8+
date = fields.Date(string="Date", default=fields.Date.today)
9+
user_id = fields.Many2one('res.users', string="User")
10+
team_id = fields.Many2one('crm.team', string="Sales Team")
11+
move_id = fields.Many2one('account.move', string="Invoice")
12+
currency_id = fields.Many2one('res.currency', default=lambda self: self.move_id.company_id.currency_id)
13+
commission_rule_id = fields.Many2one('commission.rule', string="Rule")
14+
amount = fields.Monetary("Amount", required=True, currency_field='currency_id')
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<odoo>
3+
<record id="sale_commission_report_view_list" model="ir.ui.view">
4+
<field name="name">sale.commission.report.view.list</field>
5+
<field name="model">commission.rule.line</field>
6+
<field name="arch" type="xml">
7+
<list create="0" editable="top">
8+
<field name="date"/>
9+
<field name="user_id"/>
10+
<field name="team_id" string="Team"/>
11+
<field name="move_id"/>
12+
<field name="amount"/>
13+
<field name="currency_id" column_invisible="True"/>
14+
</list>
15+
</field>
16+
</record>
17+
18+
<record id="sale_commission_report_view_graph" model="ir.ui.view">
19+
<field name="name">sale.commission.report.view.graph</field>
20+
<field name="model">commission.rule.line</field>
21+
<field name="arch" type="xml">
22+
<graph>
23+
<field name="date" type="col"/>
24+
<field name="user_id" type="row"/>
25+
<field name="amount" type="measure"/>
26+
</graph>
27+
</field>
28+
</record>
29+
30+
<record id="sale_commission_report_view_pivot" model="ir.ui.view">
31+
<field name="name">sale.commission.report.view.pivot</field>
32+
<field name="model">commission.rule.line</field>
33+
<field name="arch" type="xml">
34+
<pivot>
35+
<field name="date" type="col"/>
36+
<field name="user_id" type="row"/>
37+
<field name="team_id" type="row"/>
38+
<field name="amount" type="measure"/>
39+
</pivot>
40+
</field>
41+
</record>
42+
43+
<record id="sale_commission_report_view_search" model="ir.ui.view">
44+
<field name="name">sale.commission.report.view.search</field>
45+
<field name="model">commission.rule.line</field>
46+
<field name="arch" type="xml">
47+
<search string="Commission Report">
48+
<field name="user_id"/>
49+
<field name="team_id"/>
50+
<field name="commission_rule_id"/>
51+
<filter string="My commission" name="my"
52+
domain="[('user_id', '=', uid)]"/>
53+
<filter string="Date" name="filter_date"
54+
date="date"/>
55+
56+
<group string="Group By">
57+
<filter string="Salesperson"
58+
name="group_by_salesperson"
59+
context="{'group_by':'user_id'}"/>
60+
<filter string="Sales Team"
61+
name="group_by_salesteam"
62+
context="{'group_by':'team_id'}"/>
63+
<filter string="Date"
64+
name="group_by_date"
65+
context="{'group_by':'date'}"/>
66+
</group>
67+
</search>
68+
</field>
69+
</record>
70+
71+
<record id="sale_commission_action_report" model="ir.actions.act_window">
72+
<field name="name">Commissions</field>
73+
<field name="res_model">commission.rule.line</field>
74+
<field name="view_mode">list,graph,pivot</field>
75+
<field name="context">{'search_default_my': True}</field>
76+
<field name="help" type="html">
77+
<p class="o_view_nocontent_neutral_face">
78+
Unfortunately, there are no commissions for you
79+
</p>
80+
<p>
81+
Ensure you are assigned to a commission rule and have made sales that qualify for commissions
82+
</p>
83+
</field>
84+
</record>
85+
</odoo>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
2+
access_commission_rule,access.commission.rule,sale_commission.model_commission_rule,sales_team.group_sale_manager,1,1,1,1
3+
access_commission_rule_line,access.commission.rule.line,sale_commission.model_commission_rule_line,sales_team.group_sale_manager,1,1,1,1
+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<odoo>
3+
<menuitem
4+
id="menu_sale_commission_rule"
5+
name="Commission Rules"
6+
parent="sale.menu_sale_config"
7+
sequence="20"
8+
action="sale_commission.sale_commission_rule"/>
9+
<menuitem
10+
id="menu_sale_commission_report"
11+
name="Sales Commissions"
12+
parent="sale.menu_sale_report"
13+
action="sale_commission.sale_commission_action_report"/>
14+
</odoo>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<odoo>
3+
<record id="sale_commission_rule_view_form" model="ir.ui.view">
4+
<field name="name">sale.commission.rule.view.form</field>
5+
<field name="model">commission.rule</field>
6+
<field name="arch" type="xml">
7+
<form string="Commission Rule">
8+
<sheet>
9+
<group>
10+
<group>
11+
<field name="rate" widget="percentage"/>
12+
<field name="due_at"/>
13+
</group>
14+
<group>
15+
<field name="commission_for"/>
16+
</group>
17+
</group>
18+
<group string="Apply on">
19+
<group>
20+
<field name="product_category_id" placeholder="All"/>
21+
<field name="product_id" placeholder="All"/>
22+
<field name="product_expired"/>
23+
<field name="max_discount" widget="percentage"/>
24+
</group>
25+
<group>
26+
<label for="on_fast_payment" string="On Fast Payment"/>
27+
<div>
28+
<field name="on_fast_payment" class="oe_inline"/>
29+
<span invisible="not on_fast_payment">
30+
<label for="fast_payment_days" string="Before" class="o_light_label"/>
31+
<field name="fast_payment_days" class="oe_inline" nolabel="1"/>
32+
Days
33+
</span>
34+
</div>
35+
<field name="user_id" placeholder="All" invisible="commission_for != 'person'"/>
36+
<field name="team_id" placeholder="All" invisible="commission_for != 'team'"/>
37+
</group>
38+
</group>
39+
</sheet>
40+
</form>
41+
</field>
42+
</record>
43+
44+
<record id="sale_commission_rule_view_list" model="ir.ui.view">
45+
<field name="name">sale.commission.rule.view.list</field>
46+
<field name="model">commission.rule</field>
47+
<field name="arch" type="xml">
48+
<list string="Commission Rules">
49+
<field name="sequence" widget="handle"/>
50+
<field name="due_at"/>
51+
<field name="rate" string="Rate" widget="percentage"/>
52+
<field name="commission_for"/>
53+
<field name="display_name"/>
54+
</list>
55+
</field>
56+
</record>
57+
58+
<record id="sale_commission_rule" model="ir.actions.act_window">
59+
<field name="name">Commission Rules</field>
60+
<field name="res_model">commission.rule</field>
61+
<field name="view_mode">list,form</field>
62+
</record>
63+
</odoo>

0 commit comments

Comments
 (0)