From e9bc364cf508a8bfdbc052810ceaf411960f1ae7 Mon Sep 17 00:00:00 2001 From: kukor-odoo Date: Fri, 13 Feb 2026 11:54:40 +0530 Subject: [PATCH 1/3] [ADD] pre_commissions: Initial setup of Sales Commission module structure Established the base structure for the Sales Commission module. Added core models and initial configuration setup. Integrated menu items under Reporting and Configuration. --- pre_commissions | 1 + 1 file changed, 1 insertion(+) create mode 160000 pre_commissions diff --git a/pre_commissions b/pre_commissions new file mode 160000 index 00000000000..8a0e435d0cb --- /dev/null +++ b/pre_commissions @@ -0,0 +1 @@ +Subproject commit 8a0e435d0cb5aa8f09dfe4674917f114906e6bee From 4d81b3ee5c4f6a26994b22ac6b3132b1429023d1 Mon Sep 17 00:00:00 2001 From: kukor-odoo Date: Fri, 13 Feb 2026 12:21:27 +0530 Subject: [PATCH 2/3] Add pre_commissions as regular directory --- pre_commissions | 1 - pre_commissions/__init__.py | 1 + pre_commissions/__manifest__.py | 13 ++++ pre_commissions/models/__init__.py | 3 + pre_commissions/models/account_move.py | 21 +++++++ pre_commissions/models/commission_rule.py | 52 ++++++++++++++++ pre_commissions/models/sale_commission.py | 56 +++++++++++++++++ pre_commissions/security/ir.model.access.csv | 3 + pre_commissions/views/commision_rule.xml | 65 ++++++++++++++++++++ pre_commissions/views/menus.xml | 16 +++++ pre_commissions/views/sale_commission.xml | 53 ++++++++++++++++ 11 files changed, 283 insertions(+), 1 deletion(-) delete mode 160000 pre_commissions create mode 100644 pre_commissions/__init__.py create mode 100644 pre_commissions/__manifest__.py create mode 100644 pre_commissions/models/__init__.py create mode 100644 pre_commissions/models/account_move.py create mode 100644 pre_commissions/models/commission_rule.py create mode 100644 pre_commissions/models/sale_commission.py create mode 100644 pre_commissions/security/ir.model.access.csv create mode 100644 pre_commissions/views/commision_rule.xml create mode 100644 pre_commissions/views/menus.xml create mode 100644 pre_commissions/views/sale_commission.xml diff --git a/pre_commissions b/pre_commissions deleted file mode 160000 index 8a0e435d0cb..00000000000 --- a/pre_commissions +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 8a0e435d0cb5aa8f09dfe4674917f114906e6bee diff --git a/pre_commissions/__init__.py b/pre_commissions/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/pre_commissions/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/pre_commissions/__manifest__.py b/pre_commissions/__manifest__.py new file mode 100644 index 00000000000..3c7339935e0 --- /dev/null +++ b/pre_commissions/__manifest__.py @@ -0,0 +1,13 @@ +{ + 'name': "Sale Commission", + 'author': "Kunj Koradiya", + 'description': "This is the description", + 'license': "LGPL-3", + 'depends': ['base', 'account', 'sale'], + 'data': [ + 'security/ir.model.access.csv', + 'views/commision_rule.xml', + 'views/sale_commission.xml', + 'views/menus.xml' + ] +} diff --git a/pre_commissions/models/__init__.py b/pre_commissions/models/__init__.py new file mode 100644 index 00000000000..eb2b588db60 --- /dev/null +++ b/pre_commissions/models/__init__.py @@ -0,0 +1,3 @@ +from . import commission_rule +from . import sale_commission +from . import account_move diff --git a/pre_commissions/models/account_move.py b/pre_commissions/models/account_move.py new file mode 100644 index 00000000000..86d1d49ba80 --- /dev/null +++ b/pre_commissions/models/account_move.py @@ -0,0 +1,21 @@ +from odoo import models + + +class AccountMove(models.Model): + _inherit = 'account.move' + + def action_post(self): + res = super().action_post() + # breakpoint() + for move in self: + if move.move_type != 'out_invoice': + continue + + sale_orders = move.invoice_line_ids.sale_line_ids.order_id + sale_orders = sale_orders.exists() + + if not sale_orders: + continue + + self.env['sale.commission'].sudo().check_commission_rules(sale_orders, move) + return res diff --git a/pre_commissions/models/commission_rule.py b/pre_commissions/models/commission_rule.py new file mode 100644 index 00000000000..04c5ca67aac --- /dev/null +++ b/pre_commissions/models/commission_rule.py @@ -0,0 +1,52 @@ +from odoo import fields, models, api + + +class CommisionRule(models.Model): + _name = 'commission.rule' + _desc = "Commission Rule" + _order = 'sequence' + + sequence = fields.Integer() + active = fields.Boolean(default=True) + commission_rate = fields.Float(required=True) + due_at = fields.Selection([ + ('invoice', "Invoice Posted"), + ('payment', "Payment Received") + ], + default='invoice', + required=True + ) + commission_for = fields.Selection([ + ('salesperson', "Salesperson"), + ('team', "Sales Team") + ]) + product_category_id = fields.Many2one('product.category') + product_id = fields.Many2one('product.product') + product_expired = fields.Selection([ + ('no_impact', 'No Impact'), + ('yes', "Expired Only"), + ('no', 'Not Expired') + ], default='no_impact' + ) + max_discount = fields.Float(string="Max Discount %") + fast_payment = fields.Boolean() + fast_payment_days = fields.Integer() + salesperson_id = fields.Many2one('res.users') + team_id = fields.Many2one('crm.team', string="Sales Team") + condition_display = fields.Char(store=True, compute='_compute_condition_display') + + @api.depends('product_category_id', 'product_id', 'salesperson_id', 'team_id', 'max_discount') + def _compute_condition_display(self): + for rec in self: + parts = [] + if rec.product_category_id: + parts.append(f"Category: {rec.product_category_id.name}") + if rec.product_id: + parts.append(f"Product: {rec.product_id.name}") + if rec.salesperson_id: + parts.append(f"Salesperson: {rec.salesperson_id.name}") + if rec.team_id: + parts.append(f"Team: {rec.team_id.name}") + if rec.max_discount: + parts.append(f"Max Discount: {rec.max_discount}%") + rec.condition_display = " AND ".join(parts) or "All" diff --git a/pre_commissions/models/sale_commission.py b/pre_commissions/models/sale_commission.py new file mode 100644 index 00000000000..e69a18f0d62 --- /dev/null +++ b/pre_commissions/models/sale_commission.py @@ -0,0 +1,56 @@ +from odoo import fields, models, api + + +class SaleCommission(models.Model): + _name = 'sale.commission' + _description = "Sale Commission" + + date = fields.Date(required=True) + user_id = fields.Many2one('res.users', string="Salesperson") + team_id = fields.Many2one('crm.team', string="Sales Team") + invoice_id = fields.Many2one('account.move') + partner_id = fields.Many2one(related='invoice_id.partner_id', store=True) + + amount = fields.Monetary() + currency_id = fields.Many2one( + 'res.currency', + default=lambda self: self.env.company.currency_id.id + ) + + rule_id = fields.Many2one('commission.rule') + sale_order_id = fields.Many2one('sale.order') + + def check_commission_rules(self, sale_orders, invoice): + # breakpoint() + if not sale_orders: + return + rules = self.env['commission.rule'].search([ + ('active', '=', True) + ]) + for order in sale_orders: + for rule in rules: + condition_ok = True + if rule.salesperson_id and order.user_id != rule.salesperson_id: + condition_ok = False + if rule.team_id and order.team_id != rule.team_id: + condition_ok = False + + if not condition_ok: + continue + # existing = self.search([ + # ('invoice_id', '=', invoice.id), + # ('rule_id', '=', rule.id), + # ('sale_order_id', '=', order.id) + # ], limit=1) + # if existing: + # continue + commission_amount = invoice.amount_total * rule.commission_rate + self.create({ + 'date': invoice.invoice_date, + 'invoice_id': invoice.id, + 'user_id': order.user_id.id, + 'team_id': order.team_id.id, + 'amount': commission_amount, + 'rule_id': rule.id, + 'sale_order_id': order.id, + }) diff --git a/pre_commissions/security/ir.model.access.csv b/pre_commissions/security/ir.model.access.csv new file mode 100644 index 00000000000..835acb20bf8 --- /dev/null +++ b/pre_commissions/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink +access_commission_rules,access_commission_rules,model_commission_rule,base.group_user,1,1,1,1 +access_sale_commissions,access_sale_commissions,model_sale_commission,base.group_user,1,1,1,1 diff --git a/pre_commissions/views/commision_rule.xml b/pre_commissions/views/commision_rule.xml new file mode 100644 index 00000000000..a514f77d8b9 --- /dev/null +++ b/pre_commissions/views/commision_rule.xml @@ -0,0 +1,65 @@ + + + + + Commission Rules + commission.rule + list,form + + + + + commission.rule.view.form + commission.rule + +
+ + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + + commission.rule.view.list + commission.rule + + + + + + + + + + + +
\ No newline at end of file diff --git a/pre_commissions/views/menus.xml b/pre_commissions/views/menus.xml new file mode 100644 index 00000000000..10b9e753f28 --- /dev/null +++ b/pre_commissions/views/menus.xml @@ -0,0 +1,16 @@ + + + + + + + + \ No newline at end of file diff --git a/pre_commissions/views/sale_commission.xml b/pre_commissions/views/sale_commission.xml new file mode 100644 index 00000000000..3211adcbc51 --- /dev/null +++ b/pre_commissions/views/sale_commission.xml @@ -0,0 +1,53 @@ + + + + + Sale Commission + sale.commission + list,form,pivot,graph + + + + + + sale.commission.view.list + sale.commission + + + + + + + + + + + + + + + sale.commission.view.pivot + sale.commission + + + + + + + + + + + + + sale.commission.view.graph + sale.commission + + + + + + + + + From 982155635070fd51f8488ac863a88c3904acc910 Mon Sep 17 00:00:00 2001 From: kukor-odoo Date: Tue, 17 Feb 2026 17:22:55 +0530 Subject: [PATCH 3/3] Implement Sales Commission module end-to-end - Added Sales Commission menus under Sales, Reporting, and Configuration - Implemented Commission Rules with form and list views - Supported rule conditions for product, category, salesperson, sales team, discounts, expiration, and fast payment - Enabled rule priority handling using sequence and drag-and-drop ordering - Automated commission creation on invoice confirmation or payment - Applied first matching rule for salesperson, then sales team - Built commission list, search, graph, and pivot views with grouping and filters - Ensured amounts are converted to company currency - Improved UX with conditional field visibility, placeholders, and computed condition labels - Added complete demo data for rules, users, teams, invoices, and commissions --- pre_commissions/__manifest__.py | 6 +- pre_commissions/data/demo.xml | 336 ++++++++++++++++++++++ pre_commissions/models/commission_rule.py | 59 +++- pre_commissions/models/sale_commission.py | 134 ++++++--- pre_commissions/views/commision_rule.xml | 2 +- pre_commissions/views/sale_commission.xml | 37 ++- 6 files changed, 513 insertions(+), 61 deletions(-) create mode 100644 pre_commissions/data/demo.xml diff --git a/pre_commissions/__manifest__.py b/pre_commissions/__manifest__.py index 3c7339935e0..4ac676c1d76 100644 --- a/pre_commissions/__manifest__.py +++ b/pre_commissions/__manifest__.py @@ -3,11 +3,15 @@ 'author': "Kunj Koradiya", 'description': "This is the description", 'license': "LGPL-3", - 'depends': ['base', 'account', 'sale'], + 'depends': ['base', 'account', 'sale_management'], + 'auto_install': True, 'data': [ 'security/ir.model.access.csv', 'views/commision_rule.xml', 'views/sale_commission.xml', 'views/menus.xml' + ], + 'demo': [ + 'data/demo.xml' ] } diff --git a/pre_commissions/data/demo.xml b/pre_commissions/data/demo.xml new file mode 100644 index 00000000000..8a9b620fa8c --- /dev/null +++ b/pre_commissions/data/demo.xml @@ -0,0 +1,336 @@ + + + + Alice + alice + + + Bob + bob + + + Charlie + charlie + + + Diana + diana + + + Ellen + ellen + + + + + Alpha Team + + + + Beta Team + + + + Gamma Team + + + + + + Electronics + + + Furniture + + + + Laptop + + + + Phone + + + + Table + + + + Chair + + + + Monitor + + + + + + 1 + True + 0.05 + salesperson + + + + + 2 + True + 0.03 + team + + + + + 3 + True + 0.07 + salesperson + + + + + 4 + True + 0.04 + team + + + + + + 5 + True + 0.06 + salesperson + 10.0 + + + + + ACME Corp + + + Globex Inc + + + + + + + + + + + + + + + + + + + + + + + + 2 + 1000.0 + 5.0 + + + + + 1 + 300.0 + 0.0 + + + + + 4 + 50.0 + 0.0 + + + + + out_invoice + + 2026-02-17 + + + + + out_invoice + + 2026-02-17 + + + + + out_invoice + + 2026-02-17 + + + + + + + + 2026-02-17 + + + + 110.0 + + + + + + 2026-02-17 + + + + 60.0 + + + + + + 2026-02-17 + + + + 35.0 + + + + + + 2026-02-17 + + + + 15.0 + + + + + + + 2026-02-17 + + + + 9.0 + + + + + + + 2026-02-17 + + + + 12.0 + + + + + + + 2026-02-17 + + + + 14.0 + + + + + + + 2026-02-17 + + + + 21.0 + + + + + + + 2026-02-17 + + + + 6.0 + + + + + + 2026-02-17 + + + + 10.5 + + + + + + 2026-02-17 + + + + 8.0 + + + + + + 2026-02-17 + + + + 5.0 + + + + + + + 2025-02-17 + + + + 710.0 + + + + + diff --git a/pre_commissions/models/commission_rule.py b/pre_commissions/models/commission_rule.py index 04c5ca67aac..2d7bba6c23d 100644 --- a/pre_commissions/models/commission_rule.py +++ b/pre_commissions/models/commission_rule.py @@ -3,7 +3,7 @@ class CommisionRule(models.Model): _name = 'commission.rule' - _desc = "Commission Rule" + _description = "Commission Rule" _order = 'sequence' sequence = fields.Integer() @@ -12,41 +12,74 @@ class CommisionRule(models.Model): due_at = fields.Selection([ ('invoice', "Invoice Posted"), ('payment', "Payment Received") - ], - default='invoice', - required=True - ) + ], default='invoice', required=True) commission_for = fields.Selection([ ('salesperson', "Salesperson"), ('team', "Sales Team") ]) + product_id = fields.Many2one( + 'product.product') product_category_id = fields.Many2one('product.category') - product_id = fields.Many2one('product.product') product_expired = fields.Selection([ ('no_impact', 'No Impact'), ('yes', "Expired Only"), ('no', 'Not Expired') - ], default='no_impact' - ) + ], default='no_impact') max_discount = fields.Float(string="Max Discount %") fast_payment = fields.Boolean() fast_payment_days = fields.Integer() salesperson_id = fields.Many2one('res.users') team_id = fields.Many2one('crm.team', string="Sales Team") - condition_display = fields.Char(store=True, compute='_compute_condition_display') + condition_display = fields.Char( + compute='_compute_condition_display', + store=True + ) + + @api.onchange('commission_for') + def _onchange_commission_for(self): + for rec in self: + if rec.commission_for == 'salesperson': + rec.team_id = False + elif rec.commission_for == 'team': + rec.salesperson_id = False + else: + rec.salesperson_id = False + rec.team_id = False - @api.depends('product_category_id', 'product_id', 'salesperson_id', 'team_id', 'max_discount') + @api.depends( + 'product_category_id', + 'product_id', + 'salesperson_id', + 'team_id', + 'max_discount', + 'product_expired' + ) def _compute_condition_display(self): for rec in self: parts = [] - if rec.product_category_id: - parts.append(f"Category: {rec.product_category_id.name}") + if rec.product_id: parts.append(f"Product: {rec.product_id.name}") + rec.product_category_id = rec.product_category_id or rec.product_id.categ_id + + if rec.product_category_id: + parts.append(f"Category: {rec.product_category_id.name}") + if rec.salesperson_id: parts.append(f"Salesperson: {rec.salesperson_id.name}") + if rec.team_id: parts.append(f"Team: {rec.team_id.name}") + if rec.max_discount: parts.append(f"Max Discount: {rec.max_discount}%") - rec.condition_display = " AND ".join(parts) or "All" + + if rec.product_expired != 'no_impact': + selection = dict( + rec.fields_get(['product_expired'])[ + 'product_expired']['selection'] + ) + label = selection.get(rec.product_expired) + parts.append(f"Product Expiry: {label}") + + rec.condition_display = " AND ".join(parts) if parts else "All" diff --git a/pre_commissions/models/sale_commission.py b/pre_commissions/models/sale_commission.py index e69a18f0d62..8f707f90dcf 100644 --- a/pre_commissions/models/sale_commission.py +++ b/pre_commissions/models/sale_commission.py @@ -1,56 +1,108 @@ -from odoo import fields, models, api +from odoo import models, fields class SaleCommission(models.Model): _name = 'sale.commission' _description = "Sale Commission" + _order = 'date desc' - date = fields.Date(required=True) - user_id = fields.Many2one('res.users', string="Salesperson") - team_id = fields.Many2one('crm.team', string="Sales Team") - invoice_id = fields.Many2one('account.move') - partner_id = fields.Many2one(related='invoice_id.partner_id', store=True) + date = fields.Date(required=True, default=fields.Date.context_today) - amount = fields.Monetary() + user_id = fields.Many2one( + 'res.users', string="Salesperson", required=True, index=True) + team_id = fields.Many2one('crm.team', string="Sales Team", index=True) + invoice_id = fields.Many2one('account.move', string="Invoice", index=True) + partner_id = fields.Many2one( + related='invoice_id.partner_id', store=True, string="Customer") + amount = fields.Monetary(required=True) currency_id = fields.Many2one( - 'res.currency', - default=lambda self: self.env.company.currency_id.id - ) - - rule_id = fields.Many2one('commission.rule') - sale_order_id = fields.Many2one('sale.order') + 'res.currency', default=lambda self: self.env.company.currency_id, required=True) + rule_id = fields.Many2one( + 'commission.rule', string="Commission Rule", required=True) + sale_order_id = fields.Many2one('sale.order', string="Sale Order") def check_commission_rules(self, sale_orders, invoice): - # breakpoint() - if not sale_orders: + if not sale_orders or not invoice: return - rules = self.env['commission.rule'].search([ - ('active', '=', True) - ]) + + rules = self.env['commission.rule'].search( + [('active', '=', True)], + order='sequence asc' + ) + + commission_vals = [] + for order in sale_orders: for rule in rules: - condition_ok = True - if rule.salesperson_id and order.user_id != rule.salesperson_id: - condition_ok = False - if rule.team_id and order.team_id != rule.team_id: - condition_ok = False + if not self._rule_applies(rule, order, invoice): + continue - if not condition_ok: + users, team = self._get_commission_owners(rule, order) + if not users: continue - # existing = self.search([ - # ('invoice_id', '=', invoice.id), - # ('rule_id', '=', rule.id), - # ('sale_order_id', '=', order.id) - # ], limit=1) - # if existing: - # continue - commission_amount = invoice.amount_total * rule.commission_rate - self.create({ - 'date': invoice.invoice_date, - 'invoice_id': invoice.id, - 'user_id': order.user_id.id, - 'team_id': order.team_id.id, - 'amount': commission_amount, - 'rule_id': rule.id, - 'sale_order_id': order.id, - }) + + total_amount = invoice.amount_total * rule.commission_rate + per_user_amount = ( + total_amount / len(users) + if rule.commission_for == 'team' + else total_amount + ) + + for user in users: + commission_vals.append({ + 'date': invoice.invoice_date, + 'invoice_id': invoice.id, + 'user_id': user.id, + 'team_id': team.id if team else user.sale_team_id.id, + 'amount': per_user_amount, + 'rule_id': rule.id, + 'sale_order_id': order.id, + }) + + break # first valid rule wins + + if commission_vals: + self.create(commission_vals) + + def _get_commission_owners(self, rule, order): + if rule.commission_for == 'team' and order.team_id: + users = order.team_id.member_ids.filtered(lambda u: u.active) + return users, order.team_id + + if order.user_id: + return order.user_id, False + + return self.env['res.users'], False + + def _rule_applies(self, rule, order, invoice): + return all([ + self._rule_salesperson(rule, order), + self._rule_team(rule, order), + self._rule_products(rule, order), + self._rule_discount(rule, order), + self._rule_due_at(rule, invoice), + ]) + + def _rule_salesperson(self, rule, order): + return not rule.salesperson_id or order.user_id == rule.salesperson_id + + def _rule_team(self, rule, order): + return not rule.team_id or order.team_id == rule.team_id + + def _rule_products(self, rule, order): + for line in order.order_line: + if rule.product_id and line.product_id != rule.product_id: + return False + if rule.product_category_id and line.product_id.categ_id != rule.product_category_id: + return False + return True + + def _rule_discount(self, rule, order): + if not rule.max_discount: + return True + return all(line.discount <= rule.max_discount for line in order.order_line) + + def _rule_due_at(self, rule, invoice): + if rule.due_at == 'payment': + return invoice.payment_state == 'paid' + return True diff --git a/pre_commissions/views/commision_rule.xml b/pre_commissions/views/commision_rule.xml index a514f77d8b9..fa11715be68 100644 --- a/pre_commissions/views/commision_rule.xml +++ b/pre_commissions/views/commision_rule.xml @@ -55,7 +55,7 @@ - + diff --git a/pre_commissions/views/sale_commission.xml b/pre_commissions/views/sale_commission.xml index 3211adcbc51..fa17e09953d 100644 --- a/pre_commissions/views/sale_commission.xml +++ b/pre_commissions/views/sale_commission.xml @@ -1,10 +1,10 @@ - Sale Commission sale.commission list,form,pivot,graph + {'group_by': ['date','team_id','user_id']} @@ -23,7 +23,6 @@ - sale.commission.view.pivot @@ -39,15 +38,43 @@ - + sale.commission.view.graph sale.commission - + - + + + + + sale.commission.view.search + sale.commission + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file