Skip to content

Commit 1f376a1

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 1f376a1

11 files changed

+363
-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+
}
+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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+
</record>
9+
10+
<record id="commission_rule_2" model="commission.rule">
11+
<field name="rate">0.3</field>
12+
<field name="due_at">payment</field>
13+
<field name="commission_for">team</field>
14+
<field name="team_id" ref="sales_team.team_sales_department"/>
15+
</record>
16+
17+
<record id="commission_rule_3" model="commission.rule">
18+
<field name="rate">0.7</field>
19+
<field name="due_at">invoicing</field>
20+
<field name="commission_for">person</field>
21+
<field name="product_category_id" ref="product.product_category_all"/>
22+
<field name="user_id" ref="base.user_admin"/>
23+
</record>
24+
25+
<record id="commission_rule_4" model="commission.rule">
26+
<field name="product_category_id" ref="product.product_category_consumable"/>
27+
<field name="rate">0.4</field>
28+
<field name="due_at">payment</field>
29+
<field name="commission_for">team</field>
30+
<field name="team_id" ref="sales_team.team_sales_department"/>
31+
</record>
32+
33+
<record id="commission_rule_5" model="commission.rule">
34+
<field name="rate">0.2</field>
35+
<field name="due_at">invoicing</field>
36+
<field name="commission_for">person</field>
37+
<field name="user_id" ref="base.user_admin"/>
38+
</record>
39+
</odoo>

sale_commission/models/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from . import commission_rule
2+
from . import commission_line
3+
from . import account_move
+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
from odoo import models
2+
3+
4+
class AccountMove(models.Model):
5+
_inherit = 'account.move'
6+
7+
def action_post(self):
8+
"""Overrides invoice confirmation to trigger commision calculation."""
9+
res = super().action_post()
10+
self._create_commission_lines('invoicing')
11+
return res
12+
13+
def action_register_payment(self):
14+
"""Overrides payment registration to trigger commision calculation."""
15+
res = super().action_register_payment()
16+
self._create_commission_lines('payment')
17+
return res
18+
19+
def _create_commission_lines(self, trigger_stage):
20+
for invoice in self:
21+
commission_rules = self.env['commission.rule'].search([])
22+
23+
applied_rules = {'person': None, 'team': None}
24+
25+
for rule in commission_rules:
26+
if rule.due_at != trigger_stage:
27+
continue
28+
matches_product = not rule.product_id or any(
29+
line.product_id == rule.product_id for line in invoice.invoice_line_ids
30+
)
31+
matches_category = not rule.product_category_id or any(
32+
not line.product_id.categ_id or line.product_id.categ_id == rule.product_category_id
33+
for line in invoice.invoice_line_ids
34+
)
35+
matches_user = not rule.user_id or rule.user_id == invoice.invoice_user_id
36+
matches_team = not rule.team_id or rule.team_id == invoice.invoice_user_id.sale_team_id
37+
38+
if matches_product and matches_category and matches_user and matches_team:
39+
if rule.commission_for == 'person' and not applied_rules['person']:
40+
applied_rules['person'] = rule
41+
if rule.commission_for == 'team' and not applied_rules['team']:
42+
applied_rules['team'] = rule
43+
44+
if applied_rules['person'] and applied_rules['team']:
45+
break
46+
47+
commission_lines = []
48+
for rule in applied_rules.values():
49+
if rule:
50+
commission_lines.append({
51+
'date': invoice.invoice_date,
52+
'user_id': rule.user_id.id if rule.commission_for == 'person' else False,
53+
'team_id': rule.team_id.id if rule.commission_for == 'team' else False,
54+
'invoice_id': invoice.id,
55+
'amount': invoice.amount_total * (rule.rate / 100),
56+
'currency_id': invoice.currency_id.id,
57+
'commission_rule_id' : rule.id,
58+
})
59+
if commission_lines:
60+
self.env['commission.line'].create(commission_lines)
+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from odoo import fields, models
2+
3+
4+
class CommissionLine(models.Model):
5+
_name = 'commission.line'
6+
_description = "Commission 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+
invoice_id = fields.Many2one('account.move', string="Invoice")
12+
currency_id = fields.Many2one('res.currency', default=lambda self: self.env.company.currency_id)
13+
commission_rule_id = fields.Many2one('commission.rule', string="Rule")
14+
amount = fields.Monetary("Amount", required=True, currency_field='currency_id')
+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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+
default='invoicing'
27+
)
28+
product_expired = fields.Selection(
29+
selection=[
30+
('no_impact', "No Impact"),
31+
('yes', "Yes"),
32+
('no', "No")
33+
],
34+
string="Product Expired",
35+
default="no_impact"
36+
)
37+
max_discount = fields.Float(string="Max Discount")
38+
on_fast_payment = fields.Boolean(string="On Fast Payment")
39+
fast_payment_days = fields.Integer(string="Before Days")
40+
display_name = fields.Char(string="Condition", compute="_compute_display_name", store=True)
41+
42+
product_category_id = fields.Many2one('product.category', string="Product Category")
43+
product_id = fields.Many2one('product.product', string="Product")
44+
user_id = fields.Many2one('res.users', string="Salesperson")
45+
team_id = fields.Many2one('crm.team', string="Sales Team")
46+
47+
@api.depends('product_category_id', 'product_id', 'team_id', 'user_id')
48+
def _compute_display_name(self):
49+
"""Computes the display name based on selected fields in 'Apply On'."""
50+
for rule in self:
51+
conditions = []
52+
if rule.product_category_id:
53+
conditions.append(f"Category: {rule.product_category_id.name}")
54+
if rule.product_id:
55+
conditions.append(f"Product: {rule.product_id.name}")
56+
if rule.team_id:
57+
conditions.append(f"Team: {rule.team_id.name}")
58+
if rule.user_id:
59+
conditions.append(f"Salesperson: {rule.user_id.name}")
60+
if rule.max_discount:
61+
conditions.append(f"Max Discount: {rule.max_discount}")
62+
if rule.product_expired:
63+
conditions.append(f"Product Expired: {rule.product_expired}")
64+
65+
rule.display_name = " AND ".join(conditions) if conditions else "No Conditions"
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.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="invoice_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.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.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.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.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_line,access.commission.line,sale_commission.model_commission_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)