Add modules commission_simple, commission_simple_agent, commission_simple_agent_purchase

Add demo data in sale_agent
This commit is contained in:
Alexis de Lattre
2024-11-30 01:19:31 +01:00
parent 7bdd579b1c
commit 5af6c895d0
42 changed files with 2262 additions and 0 deletions

View 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

View 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

View 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

View 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.')]

View 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 !')]

View 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')

View 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)