diff --git a/sale_commission/__init__.py b/sale_commission/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/sale_commission/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/sale_commission/__manifest__.py b/sale_commission/__manifest__.py new file mode 100644 index 00000000000..433de7cc131 --- /dev/null +++ b/sale_commission/__manifest__.py @@ -0,0 +1,16 @@ +{ + 'name': 'Sale Commission', + 'version': '1.0', + 'category': 'Sales/Commission', + 'summary': "Manage your salespersons' commissions", + 'depends': ['sale_management'], + 'data': [ + 'demo/commission_rule.xml', + 'views/commission_rule_views.xml', + 'report/commission_report.xml', + 'views/commission_menu.xml', + 'security/ir.model.access.csv', + ], + 'installable': True, + 'license': 'AGPL-3', +} diff --git a/sale_commission/demo/commission_rule.xml b/sale_commission/demo/commission_rule.xml new file mode 100644 index 00000000000..c31488e128a --- /dev/null +++ b/sale_commission/demo/commission_rule.xml @@ -0,0 +1,44 @@ + + + + 0.5 + invoicing + person + + no_impact + + + + 0.3 + payment + team + + no_impact + + + + 0.7 + invoicing + person + + + no_impact + + + + + 0.4 + payment + team + + no_impact + + + + 0.2 + invoicing + person + + no_impact + + diff --git a/sale_commission/models/__init__.py b/sale_commission/models/__init__.py new file mode 100644 index 00000000000..ee80d0b632a --- /dev/null +++ b/sale_commission/models/__init__.py @@ -0,0 +1,3 @@ +from . import account_move +from . import commission_rule +from . import commission_rule_line diff --git a/sale_commission/models/account_move.py b/sale_commission/models/account_move.py new file mode 100644 index 00000000000..03cf489bff7 --- /dev/null +++ b/sale_commission/models/account_move.py @@ -0,0 +1,72 @@ +from odoo import models + + +class AccountMove(models.Model): + _inherit = 'account.move' + + def _post(self, soft=True): + """Overrides invoice posting to trigger commission calculation.""" + res = super()._post(soft) + + self.filtered( + lambda m: + m.move_type == 'out_invoice' + and (m.invoice_user_id or m.team_id) + )._create_commission_rule_lines('invoicing') + + return res + + def action_register_payment(self): + """Overrides payment registration to trigger commision calculation.""" + res = super().action_register_payment() + commission_lines = self._get_applicable_commission() + self.filtered( + lambda m: ( + m.move_type == 'out_invoice' + and (m.invoice_user_id or m.team_id) + and m not in commission_lines.move_id + ) + )._create_commission_rule_lines('payment') + return res + + def _get_applicable_commission(self): + """Check if a commission entry already exists for recordset.""" + return self.env['commission.rule.line'].search_fetch( + [('move_id', 'in', self.ids)], + ['move_id'] + ) + + def _create_commission_rule_lines(self, trigger_stage): + for move in self: + commission_rules = self.env['commission.rule'].search([ + ('due_at', '=', trigger_stage), + ('product_id', 'in', move.invoice_line_ids.product_id.ids + [False]), + ('product_category_id', 'in', move.invoice_line_ids.product_id.categ_id.ids + [False]), + ('user_id', 'in', [move.invoice_user_id.id ,False]), + ('team_id', 'in', [move.invoice_user_id.sale_team_id.id, False]), + ]) + + person_rule = self.env['commission.rule'] + team_rule = self.env['commission.rule'] + + for rule in commission_rules: + if rule.commission_for == 'person' and not person_rule: + person_rule = rule + if rule.commission_for == 'team' and not team_rule: + team_rule = rule + if person_rule and team_rule: + break + + commission_rule_lines = [{ + 'date': move.invoice_date, + 'user_id': rule.user_id.id, + 'team_id': rule.team_id.id, + 'move_id': move.id, + 'amount': move.amount_total * (rule.rate), + 'currency_id': move.currency_id.id, + 'commission_rule_id' : rule.id, + } + for rule in (person_rule, team_rule) + ] + if commission_rule_lines: + self.env['commission.rule.line'].create(commission_rule_lines) diff --git a/sale_commission/models/commission_rule.py b/sale_commission/models/commission_rule.py new file mode 100644 index 00000000000..b742f154c66 --- /dev/null +++ b/sale_commission/models/commission_rule.py @@ -0,0 +1,67 @@ +from odoo import _, api, fields, models + + +class CommissionRule(models.Model): + _name = 'commission.rule' + _description = "Commission Rule" + + sequence = fields.Integer('Sequence', default=1, help="Used to order commission rule.") + rate = fields.Float(string="Commission Rate", required=True) + commission_for = fields.Selection( + selection=[ + ('person', "Salesperson"), + ('team', "Sales Team") + ], + string="Commission for", + required=True, + default='person' + ) + due_at = fields.Selection( + selection=[ + ('invoicing', "Invoicing"), + ('payment', "Payment"), + ], + string="Due at", + required=True, + ) + product_expired = fields.Selection( + selection=[ + ('no_impact', "No Impact"), + ('yes', "Yes"), + ('no', "No") + ], + string="Product Expired", + required=True + ) + max_discount = fields.Float(string="Max Discount") + on_fast_payment = fields.Boolean(string="On Fast Payment") + fast_payment_days = fields.Integer(string="Before Days") + display_name = fields.Char(string="Condition", compute="_compute_display_name", store=True) + + product_category_id = fields.Many2one('product.category', string="Product Category") + product_id = fields.Many2one('product.product', string="Product") + user_id = fields.Many2one('res.users', string="Salesperson") + team_id = fields.Many2one('crm.team', string="Sales Team") + + @api.depends('product_category_id', 'product_id', 'team_id', 'user_id') + def _compute_display_name(self): + """Computes the display name based on selected fields in 'Apply On'.""" + _ = self.env._ + fields_mapping = ( + (_("Category"), 'product_category_id'), + (_("Product"), 'product_id'), + (_("Salesperson"), 'user_id'), + (_("Sales Team"), 'team_id') + ) + for rule in self: + conditions = [ + _("%(display_name)s: %(value)s", display_name=display_name, value=rule[fname].name) + for display_name, fname in fields_mapping + if rule[fname] + ] + if rule.max_discount: + conditions.append(_("Max Discount: %s", rule.max_discount)) + if rule.product_expired: + conditions.append(_("Product Expired: %s", rule.product_expired)) + + rule.display_name = _(" AND ").join(conditions) if conditions else _("No Conditions") diff --git a/sale_commission/models/commission_rule_line.py b/sale_commission/models/commission_rule_line.py new file mode 100644 index 00000000000..a1ba2327a63 --- /dev/null +++ b/sale_commission/models/commission_rule_line.py @@ -0,0 +1,14 @@ +from odoo import fields, models + + +class CommissionRuleLine(models.Model): + _name = 'commission.rule.line' + _description = "Commission Rule Line" + + date = fields.Date(string="Date", default=fields.Date.today) + user_id = fields.Many2one('res.users', string="User") + team_id = fields.Many2one('crm.team', string="Sales Team") + move_id = fields.Many2one('account.move', string="Invoice") + currency_id = fields.Many2one('res.currency', default=lambda self: self.move_id.company_id.currency_id) + commission_rule_id = fields.Many2one('commission.rule', string="Rule") + amount = fields.Monetary("Amount", required=True, currency_field='currency_id') diff --git a/sale_commission/report/commission_report.xml b/sale_commission/report/commission_report.xml new file mode 100644 index 00000000000..6978e0a4377 --- /dev/null +++ b/sale_commission/report/commission_report.xml @@ -0,0 +1,85 @@ + + + + sale.commission.report.view.list + commission.rule.line + + + + + + + + + + + + + + sale.commission.report.view.graph + commission.rule.line + + + + + + + + + + + sale.commission.report.view.pivot + commission.rule.line + + + + + + + + + + + + sale.commission.report.view.search + commission.rule.line + + + + + + + + + + + + + + + + + + + Commissions + commission.rule.line + list,graph,pivot + {'search_default_my': True} + + + Unfortunately, there are no commissions for you + + + Ensure you are assigned to a commission rule and have made sales that qualify for commissions + + + + diff --git a/sale_commission/security/ir.model.access.csv b/sale_commission/security/ir.model.access.csv new file mode 100644 index 00000000000..822ea9d01b1 --- /dev/null +++ b/sale_commission/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_rule,access.commission.rule,sale_commission.model_commission_rule,sales_team.group_sale_manager,1,1,1,1 +access_commission_rule_line,access.commission.rule.line,sale_commission.model_commission_rule_line,sales_team.group_sale_manager,1,1,1,1 diff --git a/sale_commission/views/commission_menu.xml b/sale_commission/views/commission_menu.xml new file mode 100644 index 00000000000..d8d6a8befbc --- /dev/null +++ b/sale_commission/views/commission_menu.xml @@ -0,0 +1,14 @@ + + + + + diff --git a/sale_commission/views/commission_rule_views.xml b/sale_commission/views/commission_rule_views.xml new file mode 100644 index 00000000000..3283080b9b8 --- /dev/null +++ b/sale_commission/views/commission_rule_views.xml @@ -0,0 +1,63 @@ + + + + sale.commission.rule.view.form + commission.rule + + + + + + + + + + + + + + + + + + + + + + + + + + + Days + + + + + + + + + + + + + sale.commission.rule.view.list + commission.rule + + + + + + + + + + + + + Commission Rules + commission.rule + list,form + +
+ Unfortunately, there are no commissions for you +
+ Ensure you are assigned to a commission rule and have made sales that qualify for commissions +