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..4ac676c1d76 --- /dev/null +++ b/pre_commissions/__manifest__.py @@ -0,0 +1,17 @@ +{ + 'name': "Sale Commission", + 'author': "Kunj Koradiya", + 'description': "This is the description", + 'license': "LGPL-3", + '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/__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..2d7bba6c23d --- /dev/null +++ b/pre_commissions/models/commission_rule.py @@ -0,0 +1,85 @@ +from odoo import fields, models, api + + +class CommisionRule(models.Model): + _name = 'commission.rule' + _description = "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_id = fields.Many2one( + 'product.product') + product_category_id = fields.Many2one('product.category') + 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( + 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', + 'product_expired' + ) + def _compute_condition_display(self): + for rec in self: + parts = [] + + 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}%") + + 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 new file mode 100644 index 00000000000..8f707f90dcf --- /dev/null +++ b/pre_commissions/models/sale_commission.py @@ -0,0 +1,108 @@ +from odoo import models, fields + + +class SaleCommission(models.Model): + _name = 'sale.commission' + _description = "Sale Commission" + _order = 'date desc' + + date = fields.Date(required=True, default=fields.Date.context_today) + + 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, 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): + if not sale_orders or not invoice: + return + + rules = self.env['commission.rule'].search( + [('active', '=', True)], + order='sequence asc' + ) + + commission_vals = [] + + for order in sale_orders: + for rule in rules: + if not self._rule_applies(rule, order, invoice): + continue + + users, team = self._get_commission_owners(rule, order) + if not users: + continue + + 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/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..fa11715be68 --- /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..fa17e09953d --- /dev/null +++ b/pre_commissions/views/sale_commission.xml @@ -0,0 +1,80 @@ + + + + Sale Commission + sale.commission + list,form,pivot,graph + {'group_by': ['date','team_id','user_id']} + + + + + + sale.commission.view.list + sale.commission + + + + + + + + + + + + + + sale.commission.view.pivot + sale.commission + + + + + + + + + + + + + sale.commission.view.graph + sale.commission + + + + + + + + + + + + sale.commission.view.search + sale.commission + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file