Add modules commission_simple, commission_simple_agent, commission_simple_agent_purchase
Add demo data in sale_agent
This commit is contained in:
5
commission_simple/models/__init__.py
Normal file
5
commission_simple/models/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from . import commission_profile
|
||||
from . import commission_rule
|
||||
from . import commission_result
|
||||
from . import res_company
|
||||
from . import account_move_line
|
||||
90
commission_simple/models/account_move_line.py
Normal file
90
commission_simple/models/account_move_line.py
Normal file
@@ -0,0 +1,90 @@
|
||||
# Copyright 2019-2024 Akretion France (http://www.akretion.com/)
|
||||
# @author Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.tools import float_is_zero
|
||||
|
||||
|
||||
class AccountMoveLine(models.Model):
|
||||
_inherit = 'account.move.line'
|
||||
|
||||
commission_result_id = fields.Many2one(
|
||||
'commission.result', string='Commission Result', check_company=True)
|
||||
commission_rule_id = fields.Many2one(
|
||||
'commission.rule', 'Matched Commission Rule', ondelete='restrict', check_company=True)
|
||||
commission_base = fields.Monetary('Commission Base', currency_field='company_currency_id')
|
||||
commission_rate = fields.Float('Commission Rate', digits='Commission Rate')
|
||||
commission_amount = fields.Monetary(
|
||||
string='Commission Amount', currency_field='company_currency_id',
|
||||
readonly=True, compute='_compute_commission_amount', store=True)
|
||||
# to display on commission line
|
||||
product_categ_id = fields.Many2one(
|
||||
related='product_id.product_tmpl_id.categ_id')
|
||||
|
||||
@api.depends('commission_rate', 'commission_base')
|
||||
def _compute_commission_amount(self):
|
||||
for line in self:
|
||||
commission_amount = False
|
||||
if line.display_type == 'product':
|
||||
commission_amount = line.company_currency_id.round(
|
||||
line.commission_rate * line.commission_base / 100.0)
|
||||
line.commission_amount = commission_amount
|
||||
|
||||
def _match_commission_rule(self, rules):
|
||||
# commission rules are already in the right order
|
||||
self.ensure_one()
|
||||
for rule in rules:
|
||||
if rule['date_start'] and rule['date_start'] > self.date:
|
||||
continue
|
||||
if rule['date_end'] and rule['date_end'] < self.date:
|
||||
continue
|
||||
if rule['applied_on'] == '0_customer_product':
|
||||
if (
|
||||
self.partner_id.id in
|
||||
rule['partner_ids'] and
|
||||
self.product_id.id in rule['product_ids']):
|
||||
return rule
|
||||
elif rule['applied_on'] == '1_customer_product_category':
|
||||
if (
|
||||
self.partner_id.id in
|
||||
rule['partner_ids'] and
|
||||
self.product_categ_id.id in rule['product_categ_ids']):
|
||||
return rule
|
||||
elif rule['applied_on'] == '2_product':
|
||||
if self.product_id.id in rule['product_ids']:
|
||||
return rule
|
||||
elif rule['applied_on'] == '3_product_category':
|
||||
if self.product_categ_id.id in rule['product_categ_ids']:
|
||||
return rule
|
||||
elif rule['applied_on'] == '4_global':
|
||||
return rule
|
||||
return False
|
||||
|
||||
def _prepare_commission_data(self, rule):
|
||||
self.ensure_one()
|
||||
rate_prec = self.env['decimal.precision'].precision_get('Commission Rate')
|
||||
lvals = {
|
||||
'commission_rule_id': rule['id'],
|
||||
# company currency
|
||||
# inherit this method to change the value below if you want to base on margin
|
||||
# or something else
|
||||
'commission_rate': rule['rate'],
|
||||
}
|
||||
if rule['base'] == 'margin':
|
||||
# What do we do if it's negative ? For the moment, it adds a negative commission line
|
||||
cost = 0
|
||||
if self.product_id and self.product_uom_id:
|
||||
# if the module account_invoice_margin from akretion/odoo-usability is installed
|
||||
if hasattr(self, 'margin_company_currency'):
|
||||
cost = self.margin_company_currency
|
||||
else:
|
||||
sign = self.move_id.move_type == 'out_refund' and -1 or 1
|
||||
cost = self.product_id.standard_price * self.product_uom_id._compute_quantity(self.quantity, self.product_id.uom_id) * sign
|
||||
lvals['commission_base'] = self.balance * -1 - cost
|
||||
else:
|
||||
lvals['commission_base'] = self.balance * -1
|
||||
if float_is_zero(lvals['commission_rate'], precision_digits=rate_prec) or self.company_currency_id.is_zero(lvals['commission_base']):
|
||||
return False
|
||||
return lvals
|
||||
125
commission_simple/models/commission_profile.py
Normal file
125
commission_simple/models/commission_profile.py
Normal file
@@ -0,0 +1,125 @@
|
||||
# Copyright Akretion France (http://www.akretion.com/)
|
||||
# @author Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
|
||||
from odoo import fields, models, api, _
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class CommissionProfile(models.Model):
|
||||
_name = 'commission.profile'
|
||||
_description = 'Commission Profile'
|
||||
_order = 'sequence, id'
|
||||
|
||||
name = fields.Char(string='Name of the Profile', required=True)
|
||||
active = fields.Boolean(string='Active', default=True)
|
||||
sequence = fields.Integer()
|
||||
company_id = fields.Many2one(
|
||||
'res.company', string='Company', ondelete='cascade',
|
||||
required=False, default=lambda self: self.env.company)
|
||||
assign_ids = fields.One2many(
|
||||
'commission.profile.assignment', 'profile_id', string="Assignments")
|
||||
rule_ids = fields.One2many(
|
||||
'commission.rule', 'profile_id', string='Commission Rules')
|
||||
trigger_type = fields.Selection([
|
||||
('invoice', 'Invoiced'),
|
||||
('paid', 'Paid'),
|
||||
('in_payment', 'In Payment and Paid'),
|
||||
], default='paid', string='Trigger', required=True)
|
||||
|
||||
|
||||
class CommissionProfileAssignment(models.Model):
|
||||
_name = "commission.profile.assignment"
|
||||
_description = "Commission Profile Assignment"
|
||||
|
||||
profile_id = fields.Many2one('commission.profile', ondelete='cascade')
|
||||
company_id = fields.Many2one(
|
||||
'res.company', string='Company', ondelete='cascade',
|
||||
required=True, default=lambda self: self.env.company)
|
||||
assign_type = fields.Selection(
|
||||
'_assign_type_selection', default='user', required=True, string="Type")
|
||||
user_id = fields.Many2one(
|
||||
'res.users', compute="_compute_user_id", store=True, precompute=True, readonly=False,
|
||||
ondelete="restrict", string="Salesman",
|
||||
)
|
||||
|
||||
_sql_constraints = [
|
||||
(
|
||||
'company_user_uniq',
|
||||
'unique(user_id, company_id)',
|
||||
'This salesman already has an assignment in this company.')]
|
||||
|
||||
@api.model
|
||||
def _assign_type_selection(self):
|
||||
return [('user', _('Salesman'))]
|
||||
|
||||
@api.constrains('assign_type', 'user_id')
|
||||
def _check_user(self):
|
||||
for assignment in self:
|
||||
if assignment.assign_type == 'user' and not assignment.user_id:
|
||||
raise ValidationError(_("A salesman must be selected when the assignment type is 'Salesman'."))
|
||||
|
||||
@api.depends('assign_type')
|
||||
def _compute_user_id(self):
|
||||
for assign in self:
|
||||
if assign.assign_type != 'user':
|
||||
assign.user_id = False
|
||||
|
||||
def _get_partner(self):
|
||||
self.ensure_one()
|
||||
if self.assign_type == 'user':
|
||||
return self.user_id.partner_id
|
||||
return False
|
||||
|
||||
def _prepare_move_line_domain(self, date_range):
|
||||
self.ensure_one()
|
||||
domain = [
|
||||
('display_type', '=', 'product'),
|
||||
('move_id.move_type', 'in', ('out_invoice', 'out_refund')),
|
||||
('date', '<=', date_range.date_end),
|
||||
('company_id', '=', self.company_id.id),
|
||||
('commission_result_id', '=', False),
|
||||
('parent_state', '=', 'posted'),
|
||||
]
|
||||
if self.assign_type == 'user':
|
||||
domain.append(('move_id.invoice_user_id', '=', self.user_id.id))
|
||||
# TODO : for trigger 'paid' and 'in_payment', we would need to filter
|
||||
# out the invoices paid after the end date of the commission period
|
||||
if self.profile_id.trigger_type == 'paid':
|
||||
domain.append(('move_id.payment_state', 'in', ('paid', 'reversed')))
|
||||
elif self.profile_id.trigger_type == 'in_payment':
|
||||
domain.append(('move_id.payment_state', 'in', ('in_payment', 'paid', 'reversed')))
|
||||
elif self.profile_id.trigger_type == 'invoice':
|
||||
domain.append(('date', '>=', date_range.date_start))
|
||||
return domain
|
||||
|
||||
def _prepare_commission_result(self, date_range):
|
||||
vals = {
|
||||
'partner_id': self._get_partner().id,
|
||||
'profile_id': self.profile_id.id,
|
||||
'date_range_id': date_range.id,
|
||||
'assign_type': self.assign_type,
|
||||
'company_id': self.company_id.id,
|
||||
}
|
||||
return vals
|
||||
|
||||
def _generate_commission_result(self, date_range, rules):
|
||||
self.ensure_one()
|
||||
ilines = self.env['account.move.line'].search(
|
||||
self._prepare_move_line_domain(date_range), order='date, move_id, sequence, id')
|
||||
profile = self.profile_id
|
||||
ilines2write = {}
|
||||
for iline in ilines:
|
||||
rule = iline._match_commission_rule(rules[profile.id])
|
||||
if rule:
|
||||
lvals = iline._prepare_commission_data(rule)
|
||||
if lvals:
|
||||
ilines2write[iline] = lvals
|
||||
if ilines2write:
|
||||
com_result = self.env['commission.result'].create(self._prepare_commission_result(date_range))
|
||||
for iline, vals in ilines2write.items():
|
||||
iline.write(dict(vals, commission_result_id=com_result.id))
|
||||
return com_result
|
||||
else:
|
||||
return False
|
||||
77
commission_simple/models/commission_result.py
Normal file
77
commission_simple/models/commission_result.py
Normal file
@@ -0,0 +1,77 @@
|
||||
# Copyright Akretion France (http://www.akretion.com/)
|
||||
# @author Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import fields, models, api, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class CommissionResult(models.Model):
|
||||
_name = 'commission.result'
|
||||
_description = "Commission Result"
|
||||
_order = 'date_start desc'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
|
||||
partner_id = fields.Many2one(
|
||||
'res.partner', string='Salesman/Agent', required=True, ondelete='restrict',
|
||||
readonly=True, tracking=True)
|
||||
profile_id = fields.Many2one(
|
||||
'commission.profile', string='Commission Profile', readonly=True, tracking=True)
|
||||
assign_type = fields.Selection('_assign_type_selection', readonly=True, tracking=True)
|
||||
company_id = fields.Many2one(
|
||||
'res.company', string='Company', ondelete='cascade',
|
||||
required=True, readonly=True, default=lambda self: self.env.company, tracking=True)
|
||||
company_currency_id = fields.Many2one(
|
||||
related='company_id.currency_id', string='Company Currency', store=True)
|
||||
date_range_id = fields.Many2one(
|
||||
'date.range', required=True, string='Period', readonly=True, tracking=True)
|
||||
date_start = fields.Date(related='date_range_id.date_start', store=True)
|
||||
date_end = fields.Date(related='date_range_id.date_end', store=True)
|
||||
line_ids = fields.One2many(
|
||||
'account.move.line', 'commission_result_id', string='Commission Lines',
|
||||
states={'done': [('readonly', True)]})
|
||||
amount_total = fields.Monetary(
|
||||
string='Commission Total', currency_field='company_currency_id',
|
||||
compute='_compute_amount_total', store=True, tracking=True)
|
||||
state = fields.Selection([
|
||||
('draft', 'Draft'),
|
||||
('done', 'Done'),
|
||||
], default='draft', tracking=True)
|
||||
# TODO copy amount to another field
|
||||
# help='This is the total amount at the date of the computation of the commission')
|
||||
|
||||
@api.model
|
||||
def _assign_type_selection(self):
|
||||
return self.env['commission.profile.assignment']._assign_type_selection()
|
||||
|
||||
@api.depends('line_ids.commission_amount')
|
||||
def _compute_amount_total(self):
|
||||
rg_res = self.env['account.move.line'].read_group([('commission_result_id', 'in', self.ids)], ['commission_result_id', 'commission_amount:sum'], ['commission_result_id'])
|
||||
mapped_data = dict([(x['commission_result_id'][0], x['commission_amount']) for x in rg_res])
|
||||
for rec in self:
|
||||
rec.amount_total = mapped_data.get(rec.id, 0)
|
||||
|
||||
def unlink(self):
|
||||
for result in self:
|
||||
if result.state == 'done':
|
||||
raise UserError(_(
|
||||
"You cannot delete commission result %s because it is in done state.") % result.display_name)
|
||||
return super().unlink()
|
||||
|
||||
def draft2done(self):
|
||||
self.write({'state': 'done'})
|
||||
|
||||
def backtodraft(self):
|
||||
self.write({'state': 'draft'})
|
||||
|
||||
def name_get(self):
|
||||
res = []
|
||||
for result in self:
|
||||
name = '%s (%s)' % (result.partner_id.name, result.date_range_id.name)
|
||||
res.append((result.id, name))
|
||||
return res
|
||||
|
||||
_sql_constraints = [(
|
||||
'salesman_period_company_unique',
|
||||
'unique(company_id, partner_id, date_range_id)',
|
||||
'A commission result already exists for this salesman/agent for the same period.')]
|
||||
52
commission_simple/models/commission_rule.py
Normal file
52
commission_simple/models/commission_rule.py
Normal file
@@ -0,0 +1,52 @@
|
||||
# Copyright Akretion France (http://www.akretion.com/)
|
||||
# @author Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
|
||||
from odoo import fields, models, api
|
||||
|
||||
|
||||
class CommissionRule(models.Model):
|
||||
_name = 'commission.rule'
|
||||
_description = 'Commission Rule'
|
||||
_order = 'profile_id, applied_on'
|
||||
|
||||
partner_ids = fields.Many2many(
|
||||
'res.partner', string='Customers', domain=[('parent_id', '=', False)])
|
||||
product_categ_ids = fields.Many2many(
|
||||
'product.category', string="Product Categories")
|
||||
product_ids = fields.Many2many('product.product', string='Products')
|
||||
date_start = fields.Date('Start Date')
|
||||
date_end = fields.Date('End Date')
|
||||
profile_id = fields.Many2one(
|
||||
'commission.profile', string='Profile', ondelete='cascade')
|
||||
company_id = fields.Many2one(related='profile_id.company_id', store=True)
|
||||
rate = fields.Float('Commission Rate', digits="Commission Rate", copy=False)
|
||||
base = fields.Selection([
|
||||
('invoiced', 'Invoiced Amount'),
|
||||
('margin', 'Margin'),
|
||||
], default='invoiced', required=True, string="Commission Base")
|
||||
applied_on = fields.Selection([
|
||||
('0_customer_product', 'Products and Customers'),
|
||||
('1_customer_product_category', "Product Categories and Customers"),
|
||||
('2_product', "Products"),
|
||||
('3_product_category', "Product Categories"),
|
||||
('4_global', 'Global')],
|
||||
string='Apply On', default='4_global', required=True)
|
||||
active = fields.Boolean(string='Active', default=True)
|
||||
|
||||
@api.model
|
||||
def load_all_rules(self):
|
||||
rules = self.search_read([('profile_id', '!=', False)])
|
||||
res = {} # key = profile, value = [rule1 recordset, rule2]
|
||||
for rule in rules:
|
||||
if rule['profile_id'][0] not in res:
|
||||
res[rule['profile_id'][0]] = [rule]
|
||||
else:
|
||||
res[rule['profile_id'][0]].append(rule)
|
||||
return res
|
||||
|
||||
_sql_constraints = [(
|
||||
'rate_positive',
|
||||
'CHECK(rate >= 0)',
|
||||
'Rate must be positive !')]
|
||||
13
commission_simple/models/res_company.py
Normal file
13
commission_simple/models/res_company.py
Normal file
@@ -0,0 +1,13 @@
|
||||
# Copyright 2019-2024 Akretion France (https://www.akretion.com/)
|
||||
# @author Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResCompany(models.Model):
|
||||
_inherit = 'res.company'
|
||||
|
||||
commission_date_range_type_id = fields.Many2one(
|
||||
'date.range.type', string='Commission Periodicity', ondelete='restrict')
|
||||
20
commission_simple/models/res_users.py
Normal file
20
commission_simple/models/res_users.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# Copyright 2019-2024 Akretion France (https://www.akretion.com/)
|
||||
# @author Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResUsers(models.Model):
|
||||
_inherit = 'res.users'
|
||||
|
||||
# TODO mon idée : déplacer ça dans une table dédiée
|
||||
# company_id oblig
|
||||
# partner_id (filtré... sur lien vers user ou agent petit difficulté)
|
||||
# profile_id
|
||||
# type agent ou user => ça donne le champ de recherche
|
||||
|
||||
commission_profile_id = fields.Many2one(
|
||||
'commission.profile', string='Commission Profile',
|
||||
company_dependent=True)
|
||||
Reference in New Issue
Block a user