Skip to content

Commit 2955bfc

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 2955bfc

File tree

11 files changed

+383
-0
lines changed

11 files changed

+383
-0
lines changed

sale_commission/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import models

sale_commission/__manifest__.py

Lines changed: 16 additions & 0 deletions
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+
}
Lines changed: 44 additions & 0 deletions
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

Lines changed: 3 additions & 0 deletions
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
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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 = None
50+
team_rule = None
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+
applied_rules = [rule for rule in (person_rule, team_rule) if rule]
61+
commission_rule_lines = [{
62+
'date': move.invoice_date,
63+
'user_id': rule.user_id.id,
64+
'team_id': rule.team_id.id,
65+
'move_id': move.id,
66+
'amount': move.amount_total * (rule.rate),
67+
'currency_id': move.currency_id.id,
68+
'commission_rule_id' : rule.id,
69+
}
70+
for rule in applied_rules
71+
]
72+
if commission_rule_lines:
73+
self.env['commission.rule.line'].create(commission_rule_lines)
Lines changed: 67 additions & 0 deletions
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")
Lines changed: 14 additions & 0 deletions
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')
Lines changed: 85 additions & 0 deletions
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>
Lines changed: 3 additions & 0 deletions
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
Lines changed: 14 additions & 0 deletions
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>

0 commit comments

Comments
 (0)