Skip to content

Commit 446a1c1

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 446a1c1

11 files changed

+383
-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
+73
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._has_commission_entry()
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.id not in commission_lines
28+
)
29+
)._create_commission_rule_lines('payment')
30+
return res
31+
32+
def _has_commission_entry(self):
33+
"""Check if a commission entry already exists for this invoice."""
34+
existing_entry = self.env['commission.rule.line'].search_read(
35+
[('move_id', 'in', self.ids)]
36+
)
37+
return {entry['move_id'][0] for entry in existing_entry}
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)
+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)