From 7a48d9b4440ad6ae0105b0c6852ddb4dae6dafdf Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Fri, 3 Jul 2020 08:36:09 -0700 Subject: [PATCH 1/7] [MOV] hr_commission: from Hibou Suite Enterprise for 13.0 --- hr_commission/__init__.py | 3 + hr_commission/__manifest__.py | 26 ++ hr_commission/models/__init__.py | 8 + hr_commission/models/account.py | 50 +++ hr_commission/models/commission.py | 329 ++++++++++++++++++ hr_commission/models/hr.py | 11 + hr_commission/models/partner.py | 9 + hr_commission/models/res_company.py | 18 + hr_commission/models/res_config_settings.py | 12 + .../security/commission_security.xml | 34 ++ hr_commission/security/ir.model.access.csv | 9 + hr_commission/static/description/icon.png | Bin 0 -> 5113 bytes hr_commission/tests/__init__.py | 3 + hr_commission/tests/test_commission.py | 227 ++++++++++++ hr_commission/views/account_views.xml | 18 + hr_commission/views/commission_views.xml | 264 ++++++++++++++ hr_commission/views/hr_views.xml | 16 + hr_commission/views/partner_views.xml | 15 + .../views/res_config_settings_views.xml | 46 +++ 19 files changed, 1098 insertions(+) create mode 100644 hr_commission/__init__.py create mode 100644 hr_commission/__manifest__.py create mode 100755 hr_commission/models/__init__.py create mode 100644 hr_commission/models/account.py create mode 100644 hr_commission/models/commission.py create mode 100644 hr_commission/models/hr.py create mode 100644 hr_commission/models/partner.py create mode 100644 hr_commission/models/res_company.py create mode 100644 hr_commission/models/res_config_settings.py create mode 100644 hr_commission/security/commission_security.xml create mode 100644 hr_commission/security/ir.model.access.csv create mode 100644 hr_commission/static/description/icon.png create mode 100755 hr_commission/tests/__init__.py create mode 100644 hr_commission/tests/test_commission.py create mode 100644 hr_commission/views/account_views.xml create mode 100644 hr_commission/views/commission_views.xml create mode 100644 hr_commission/views/hr_views.xml create mode 100644 hr_commission/views/partner_views.xml create mode 100644 hr_commission/views/res_config_settings_views.xml diff --git a/hr_commission/__init__.py b/hr_commission/__init__.py new file mode 100644 index 00000000..09434554 --- /dev/null +++ b/hr_commission/__init__.py @@ -0,0 +1,3 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from . import models diff --git a/hr_commission/__manifest__.py b/hr_commission/__manifest__.py new file mode 100644 index 00000000..52e7d5d7 --- /dev/null +++ b/hr_commission/__manifest__.py @@ -0,0 +1,26 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +{ + 'name': 'Hibou Commissions', + 'author': 'Hibou Corp. ', + 'version': '13.0.1.0.1', + 'category': 'Accounting/Commissions', + 'license': 'OPL-1', + 'website': 'https://hibou.io/', + 'depends': [ + # 'account_invoice_margin', # optional + 'hr_contract', + ], + 'data': [ + 'security/commission_security.xml', + 'security/ir.model.access.csv', + 'views/account_views.xml', + 'views/commission_views.xml', + 'views/hr_views.xml', + 'views/partner_views.xml', + 'views/res_config_settings_views.xml', + ], + 'installable': True, + 'application': True, + 'auto_install': False, + } diff --git a/hr_commission/models/__init__.py b/hr_commission/models/__init__.py new file mode 100755 index 00000000..d739b6ed --- /dev/null +++ b/hr_commission/models/__init__.py @@ -0,0 +1,8 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from . import account +from . import commission +from . import hr +from . import partner +from . import res_company +from . import res_config_settings diff --git a/hr_commission/models/account.py b/hr_commission/models/account.py new file mode 100644 index 00000000..9fc6c166 --- /dev/null +++ b/hr_commission/models/account.py @@ -0,0 +1,50 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from odoo import api, fields, models + + +class AccountMove(models.Model): + _inherit = 'account.move' + + commission_ids = fields.One2many(comodel_name='hr.commission', inverse_name='source_move_id', string='Commissions') + commission_count = fields.Integer(string='Number of Commissions', compute='_compute_commission_count') + + @api.depends('state', 'commission_ids') + def _compute_commission_count(self): + for move in self: + move.commission_count = len(move.commission_ids) + return True + + def open_commissions(self): + return { + 'type': 'ir.actions.act_window', + 'name': 'Invoice Commissions', + 'res_model': 'hr.commission', + 'view_mode': 'tree,form', + 'context': {'search_default_source_move_id': self[0].id} + } + + def action_post(self): + res = super(AccountMove, self).action_post() + invoices = self.filtered(lambda m: m.is_invoice()) + if invoices: + self.env['hr.commission'].invoice_validated(invoices) + return res + + def action_invoice_paid(self): + res = super(AccountMove, self).action_invoice_paid() + self.env['hr.commission'].invoice_paid(self) + return res + + def amount_for_commission(self): + # TODO Should toggle in Config Params + if hasattr(self, 'margin') and self.company_id.commission_amount_type == 'on_invoice_margin': + sign = -1 if self.type in ['in_refund', 'out_refund'] else 1 + return self.margin * sign + return self.amount_total_signed + + def action_cancel(self): + res = super(AccountMove, self).action_cancel() + for move in self: + move.sudo().commission_ids.unlink() + return res diff --git a/hr_commission/models/commission.py b/hr_commission/models/commission.py new file mode 100644 index 00000000..3a491080 --- /dev/null +++ b/hr_commission/models/commission.py @@ -0,0 +1,329 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from odoo import api, fields, models +from odoo.tools import float_is_zero +from odoo.exceptions import UserError + + +class Commission(models.Model): + _name = 'hr.commission' + _description = 'Commission' + _order = 'id desc' + + state = fields.Selection([ + ('draft', 'New'), + ('done', 'Confirmed'), + ('paid', 'Paid'), + ('cancel', 'Cancelled'), + ], 'Status', default='draft') + employee_id = fields.Many2one('hr.employee', required=1) + user_id = fields.Many2one('res.users', related='employee_id.user_id') + source_move_id = fields.Many2one('account.move') + contract_id = fields.Many2one('hr.contract') + structure_id = fields.Many2one('hr.commission.structure') + rate_type = fields.Selection([ + ('normal', 'Normal'), + ('structure', 'Structure'), + ('admin', 'Admin'), + ('manual', 'Manual'), + ], 'Rate Type', default='normal') + rate = fields.Float('Rate') + base_total = fields.Float('Base Total') + base_amount = fields.Float(string='Base Amount') + amount = fields.Float(string='Amount') + move_id = fields.Many2one('account.move', ondelete='set null') + move_date = fields.Date(related='move_id.date', store=True) + company_id = fields.Many2one('res.company', 'Company', required=True, + default=lambda s: s.env['res.company']._company_default_get('hr.commission')) + memo = fields.Char(string='Memo') + accounting_date = fields.Date('Force Accounting Date', + help="Choose the accounting date at which you want to value the commission " + "moves created by the commission instead of the default one.") + payment_id = fields.Many2one('hr.commission.payment', string='Commission Payment', ondelete='set null') + + @api.depends('employee_id', 'source_move_id') + def name_get(self): + res = [] + for commission in self: + name = '' + if commission.source_move_id: + name += commission.source_move_id.name + if commission.employee_id: + if name: + name += ' - ' + commission.employee_id.name + else: + name += commission.employee_id.name + res.append((commission.id, name)) + return res + + @api.onchange('rate_type') + def _onchange_rate_type(self): + for commission in self.filtered(lambda c: c.rate_type == 'manual'): + commission.rate = 100.0 + + @api.onchange('source_move_id', 'contract_id', 'rate_type', 'base_amount', 'rate') + def _compute_amount(self): + for commission in self: + # Determine rate (if needed) + if commission.structure_id and commission.rate_type == 'structure': + line = commission.structure_id.line_ids.filtered(lambda l: l.employee_id == commission.employee_id) + commission.rate = line.get_rate() + elif commission.contract_id and commission.rate_type != 'manual': + if commission.rate_type == 'normal': + commission.rate = commission.contract_id.commission_rate + else: + commission.rate = commission.contract_id.admin_commission_rate + + rounding = 2 + if commission.source_move_id: + rounding = commission.source_move_id.company_currency_id.rounding + commission.base_total = commission.source_move_id.amount_total_signed + commission.base_amount = commission.source_move_id.amount_for_commission() + + amount = (commission.base_amount * commission.rate) / 100.0 + if float_is_zero(amount, precision_rounding=rounding): + amount = 0.0 + commission.amount = amount + + + @api.model + def create(self, values): + res = super(Commission, self).create(values) + res._compute_amount() + if res.amount == 0.0 and res.state == 'draft': + res.state = 'done' + return res + + def unlink(self): + if self.filtered(lambda c: c.move_id): + raise UserError('You cannot delete a commission when it has an accounting entry.') + return super(Commission, self).unlink() + + def _filter_source_moves_for_creation(self, moves): + return moves.filtered(lambda i: i.user_id and not i.commission_ids) + + @api.model + def _commissions_to_confirm(self, moves): + commissions = moves.mapped('commission_ids') + return commissions.filtered(lambda c: c.state != 'cancel' and not c.move_id) + + @api.model + def invoice_validated(self, moves): + employee_obj = self.env['hr.employee'].sudo() + commission_obj = self.sudo() + for move in self._filter_source_moves_for_creation(moves): + move_amount = move.amount_for_commission() + + # Does the invoice have a commission structure? + partner = move.partner_id + commission_structure = partner.commission_structure_id + while not commission_structure and partner: + partner = partner.parent_id + commission_structure = partner.commission_structure_id + + if commission_structure: + commission_structure.create_for_source_move(move, move_amount) + else: + employee = employee_obj.search([('user_id', '=', move.user_id.id)], limit=1) + contract = employee.contract_id + if all((employee, contract)): + move.commission_ids += commission_obj.create({ + 'employee_id': employee.id, + 'contract_id': contract.id, + 'source_move_id': move.id, + 'base_amount': move_amount, + 'rate_type': 'normal', + 'company_id': move.company_id.id, + }) + + # Admin/Coach commission. + employee = employee.coach_id + contract = employee.contract_id + if all((employee, contract)): + move.commission_ids += commission_obj.create({ + 'employee_id': employee.id, + 'contract_id': contract.id, + 'source_move_id': move.id, + 'base_amount': move_amount, + 'rate_type': 'admin', + 'company_id': move.company_id.id, + }) + + if move.commission_ids and move.company_id.commission_type == 'on_invoice': + commissions = self._commissions_to_confirm(move) + commissions.sudo().action_confirm() + + return True + + @api.model + def invoice_paid(self, moves): + commissions = self._commissions_to_confirm(moves) + commissions.sudo().action_confirm() + return True + + def action_confirm(self): + move_obj = self.env['account.move'].sudo() + + for commission in self: + if commission.state == 'cancel': + continue + if commission.move_id or commission.amount == 0.0: + commission.write({'state': 'done'}) + continue + + journal = commission.company_id.commission_journal_id + if not journal or not journal.default_debit_account_id or not journal.default_credit_account_id: + raise UserError('Commission Journal not configured.') + + liability_account = commission.company_id.commission_liability_id + if not liability_account: + liability_account = commission.employee_id.address_home_id.property_account_payable_id + if not liability_account: + raise UserError('Commission liability account must be configured if employee\'s don\'t have AP setup.') + + date = commission.source_move_id.date if commission.source_move_id else fields.Date.context_today(commission) + + # Already paid. + payments = commission.source_move_id._get_reconciled_payments() + if payments: + date = max(payments.mapped('payment_date')) + if commission.accounting_date: + date = commission.accounting_date + + ref = 'Commission for ' + commission.name_get()[0][1] + if commission.memo: + ref += ' :: ' + commission.memo + + move = move_obj.create({ + 'date': date, + 'ref': ref, + 'journal_id': journal.id, + 'type': 'entry', + 'line_ids': [ + (0, 0, { + 'name': ref, + 'partner_id': commission.employee_id.address_home_id.id, + 'account_id': liability_account.id, + 'credit': commission.amount if commission.amount > 0.0 else 0.0, + 'debit': 0.0 if commission.amount > 0.0 else -commission.amount, + }), + (0, 0, { + 'name': ref, + 'partner_id': commission.employee_id.address_home_id.id, + 'account_id': journal.default_credit_account_id.id if commission.amount > 0.0 else journal.default_debit_account_id.id, + 'credit': 0.0 if commission.amount > 0.0 else -commission.amount, + 'debit': commission.amount if commission.amount > 0.0 else 0.0, + }), + ], + }) + move.post() + commission.write({'state': 'done', 'move_id': move.id}) + return True + + def action_mark_paid(self): + if self.filtered(lambda c: c.state != 'done'): + raise UserError('You cannot mark a commission "paid" if it is not already "done".') + if not self: + raise UserError('You must have at least one "done" commission.') + payments = self._mark_paid() + action = self.env.ref('hr_commission.action_hr_commission_payment').read()[0] + action['res_ids'] = payments.ids + return action + + def _mark_paid(self): + employees = self.mapped('employee_id') + payments = self.env['hr.commission.payment'] + for employee in employees: + commissions = self.filtered(lambda c: c.employee_id == employee) + min_date = False + max_date = False + for commission in commissions: + if not min_date or (commission.move_date and min_date > commission.move_date): + min_date = commission.move_date + if not max_date or (commission.move_date and max_date < commission.move_date): + max_date = commission.move_date + payment = payments.create({ + 'employee_id': employee.id, + 'name': ('Commissions %s - %s' % (min_date, max_date)), + 'date': fields.Date.today(), + }) + payments += payment + commissions.write({'state': 'paid', 'payment_id': payment.id}) + return payments + + def action_cancel(self): + for commission in self: + if commission.move_id: + commission.move_id.write({'state': 'draft'}) + commission.move_id.unlink() + commission.write({'state': 'cancel'}) + return True + + def action_draft(self): + for commission in self.filtered(lambda c: c.state == 'cancel'): + commission.write({'state': 'draft'}) + + +class CommissionPayment(models.Model): + _name = 'hr.commission.payment' + _description = 'Commission Payment' + _order = 'id desc' + + name = fields.Char(string='Name') + employee_id = fields.Many2one('hr.employee', required=1) + user_id = fields.Many2one('res.users', related='employee_id.user_id') + date = fields.Date(string='Date') + commission_ids = fields.One2many('hr.commission', 'payment_id', string='Paid Commissions', readonly=True) + commission_count = fields.Integer(string='Commission Count', compute='_compute_commission_stats', store=True) + commission_amount = fields.Float(string='Commission Amount', compute='_compute_commission_stats', store=True) + + @api.depends('commission_ids') + def _compute_commission_stats(self): + for payment in self: + payment.commission_count = len(payment.commission_ids) + payment.commission_amount = sum(payment.commission_ids.mapped('amount')) + + +class CommissionStructure(models.Model): + _name = 'hr.commission.structure' + _description = 'Commission Structure' + _order = 'id desc' + + name = fields.Char(string='Name') + line_ids = fields.One2many('hr.commission.structure.line', 'structure_id', string='Lines') + + def create_for_source_move(self, move, amount): + self.ensure_one() + commission_obj = self.env['hr.commission'].sudo() + + for line in self.line_ids: + employee = line.employee_id + rate = line.get_rate() + if all((employee, rate)): + contract = False + if not line.rate: + # The rate must have come from the contract. + contract = employee.contract_id + move.commission_ids += commission_obj.create({ + 'employee_id': employee.id, + 'structure_id': self.id, + 'source_move_id': move.id, + 'base_amount': amount, + 'rate_type': 'structure', + 'contract_id': contract.id if contract else False, + 'company_id': move.company_id.id, + }) + + +class CommissionStructureLine(models.Model): + _name = 'hr.commission.structure.line' + _description = 'Commission Structure Line' + + structure_id = fields.Many2one('hr.commission.structure', string='Structure', required=True) + employee_id = fields.Many2one('hr.employee', string='Employee', required=True) + rate = fields.Float(string='Commission %', default=0.0, help='Leave 0.0 to use the employee\'s current contract rate.') + + def get_rate(self): + if not self.rate: + return self.employee_id.contract_id.commission_rate + return self.rate diff --git a/hr_commission/models/hr.py b/hr_commission/models/hr.py new file mode 100644 index 00000000..4047ee34 --- /dev/null +++ b/hr_commission/models/hr.py @@ -0,0 +1,11 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from odoo import api, fields, models + + +class Contract(models.Model): + _inherit = 'hr.contract' + + commission_rate = fields.Float(string='Commission %', default=0.0) + admin_commission_rate = fields.Float(string='Admin Commission %', default=0.0) + diff --git a/hr_commission/models/partner.py b/hr_commission/models/partner.py new file mode 100644 index 00000000..b88149ad --- /dev/null +++ b/hr_commission/models/partner.py @@ -0,0 +1,9 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from odoo import fields, models + + +class Partner(models.Model): + _inherit = 'res.partner' + + commission_structure_id = fields.Many2one('hr.commission.structure', string='Commission Structure') diff --git a/hr_commission/models/res_company.py b/hr_commission/models/res_company.py new file mode 100644 index 00000000..26a2acb4 --- /dev/null +++ b/hr_commission/models/res_company.py @@ -0,0 +1,18 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from odoo import fields, models + + +class ResCompany(models.Model): + _inherit = 'res.company' + + commission_journal_id = fields.Many2one('account.journal', string='Commission Journal') + commission_liability_id = fields.Many2one('account.account', string='Commission Liability Account') + commission_type = fields.Selection([ + ('on_invoice', 'On Invoice Validation'), + ('on_invoice_paid', 'On Invoice Paid'), + ], string='Pay Commission', default='on_invoice_paid') + commission_amount_type = fields.Selection([ + ('on_invoice_margin', 'On Invoice Margin'), + ('on_invoice_total', 'On Invoice Total'), + ], string='Commission Base', default='on_invoice_margin') diff --git a/hr_commission/models/res_config_settings.py b/hr_commission/models/res_config_settings.py new file mode 100644 index 00000000..d09b5b38 --- /dev/null +++ b/hr_commission/models/res_config_settings.py @@ -0,0 +1,12 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = 'res.config.settings' + + commission_journal_id = fields.Many2one(related='company_id.commission_journal_id', readonly=False) + commission_liability_id = fields.Many2one(related='company_id.commission_liability_id', readonly=False) + commission_type = fields.Selection(related='company_id.commission_type', readonly=False) + commission_amount_type = fields.Selection(related='company_id.commission_amount_type', readonly=False) diff --git a/hr_commission/security/commission_security.xml b/hr_commission/security/commission_security.xml new file mode 100644 index 00000000..844471ff --- /dev/null +++ b/hr_commission/security/commission_security.xml @@ -0,0 +1,34 @@ + + + + + + Commission User + + [('user_id', '=', user.id)] + + + + + Commission Manager + + [(1, '=', 1)] + + + + + Commission Payment User + + [('user_id', '=', user.id)] + + + + + Commission Payment Manager + + [(1, '=', 1)] + + + + + diff --git a/hr_commission/security/ir.model.access.csv b/hr_commission/security/ir.model.access.csv new file mode 100644 index 00000000..59b621a1 --- /dev/null +++ b/hr_commission/security/ir.model.access.csv @@ -0,0 +1,9 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_commission_user,commission user,model_hr_commission,base.group_user,1,0,0,0 +access_commission_manager,commission manager,model_hr_commission,account.group_account_manager,1,1,1,1 +access_commission_payment_user,commission payment user,model_hr_commission_payment,base.group_user,1,0,0,0 +access_commission_payment_manager,commission payment manager,model_hr_commission_payment,account.group_account_manager,1,1,1,1 +access_commission_structure_user,commission structure user,model_hr_commission_structure,base.group_user,1,0,0,0 +access_commission_structure_manager,commission structure manager,model_hr_commission_structure,account.group_account_manager,1,1,1,1 +access_commission_structure_line_user,commission structure line user,model_hr_commission_structure_line,base.group_user,1,0,0,0 +access_commission_structure_line_manager,commission structure line manager,model_hr_commission_structure_line,account.group_account_manager,1,1,1,1 diff --git a/hr_commission/static/description/icon.png b/hr_commission/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..f4d8b5019a241e217641c64ad41f37ddf6d59ec5 GIT binary patch literal 5113 zcmX|FcQhQ_5??{GO7s#@cCB7l@4fDZ)dkU6z4uPAL??*edjwHJ^c51lixwoJZuAyJ ziRb&?JMWLVcjnIA-^|=I=bkwetEHj(82>3g004LlhbigY?^^#5@ZtTMM0!#S0ASHN z=oxw&BGe_U-CTGrk#1Hty#6lk_j&+8O4i@q(%Q+!8*F7`=in+0IqX0~zz#@hh=DMI z58=f%qz^x2a&-COL-z~C3KWj{&)Fal7`rOd%H_Op?-dTynceb zZk~2fesOVeD4zgSK!E4og2yYs)!Wja$JLAJABg{ADA{;fdpfv#JGi-m|G~7ha`W+) zhCrbIID!7p%YC@e|HpeT{6AwG*ZVX*@AG;3?A^}&GetNkDr$M@s49Tr%8DZV;vynE z0=)bH0Q+m7*Bxru@+7Zh*VCBE7$HMY;q|;pJX|rn`r%JYL^WAhj&}iQDQCIvm0lx>d%EDyAAe0Iz(^xSE$B~6-%vM)aK@8Q?||Kw#>QO z^3d{G`e)x`5yBD_-kNouwgL{u8ny*kIh$eP;@r5PI%OMDtAjMDVCA#ul=1%}^D(Ms zW2r7-vq7>i+}SH)C*D2h>Fb^4tEp^V4E7gdXoG9+g z7%oggL@xJ)_D3bRajK6|h7G)QiZ16}htpk(bAS+2A`&hqavG%B*xsJ>UdABgw|>ZM z3M(Q<$YHt+p7vkn#)VPG#(y>c74_L&`G@ZxcX$tx1ZVRqj=0b7*3dY?p{os#sxl`5 zjaWncDF``ojjqjZW!XtK-GyYPZ%!mQUh}Z8VLN0=LfDt)-?zWJrrWt$;GZKE zqtXM@4jzDNO7MqX6tg1%#LwNtILfQY73Sbo17&!OIfzkPhs&k;S~EM15@&LiUf|6^ z?%j?M3s~J$JL+JL4lO%C>OT(Sxb1nQQ;+Mf33FsYQ8Fs_>h)gKw2xf!ZPLWz;C}B4 zxK+WS#se%QXBshEKz@W@7nX;9XMYtc} zKHC*eEFWFQ%z0G%`#pBy6GeJ$EFECD91yEQ&R(I20HrLCQ7FO>XDri->Y-3zKih87 zkOL7GQHpUE6CvqSZSi<2Qg|?b4}9Lu58>IfagZ1H9Fyqu=%FIyfXYBctap&~EI=*< z6(Sn8>K>#y^bc0eX6oi%oWPVYkaNE#YfryvI&WQ z4?!fQM$-Nu8gezz4M9-=^+|h4EGQKSKgvY|pp#tXdn#H?2;XO;`O}1Sh93i0vX*O; zJ048nagZH;xu5TSpiY3B<%nMX)g{%cuGl_Gr^ThD0xI6i7IoWJnV%!qwy!_ zuCUFEV8taefT-J*Ev`GkOEsTR75#J?kYlp!a~K|^2wW2fuEu9kBy0_87s^#sDU+~Q zN{S5LYTIRSd-7y!n0u8GfS#In%eZUtc$_AKzEcHQiB1*;`wQIOG1vBZ2&_HlRcscR zH;sc(I0hw?Pxhs8CRnOq2z*Egu47cNF)AeC(aJs+4dM+p>WqS=FSP^twi9%l1P4sx zI1oe^3~WfHS7VYj54PJzE|(MZk;eG|oHLRM;jRIO34Uhs?aZA(khHYw5iTw8={_vl zW8~^3!({#Xbj7-_e##qZ@?!O6W-=h}X!g+9jKS(x0l<+8wzG5atDNt1Wj3jtlQu)E zEpc4#j^~078a$B``h$Ea0e4dlG#96mmj6JcI97Q(FtZz}rL-5L04K|@wn=W~>P9Y}S zk*6O;(9ewN7=xagj2*@d9cQDeFy)J<_|o64x2hoNo9tM zf3%*^l4+`r*2>qmYp9>r?n>rS-LiCYb;KKv-6z9)%bV&x&k5mgoNKOG!WQl}Fgs5m z9c(+>nmkK<8>68b2xdFIt-@c4ZsJzN~Z9#kS#pK+I*- zVim_nKe}G&BTQ=gBSe=!p`E*P4!T?X|DYnbWq-4}E)Aaept^4MR!^1E)K@5KLJryo z%%s`aICYzjliwJm7Agc-N`JPN3`>F7Q9l8S|cdw?! zNB256_au&Am;?p2eVI*(2ypeWU`MPhgjKI` zFTl_#Cne5eB9Tj*BFc$sf(bhEDSG~5Y8B68tRw)YtVNyVuAp7-$SKNeE*IJo&sI&` zRQonw(>z#4Bwl4VstK!pRzz?@0;CTY;y?V*$xpMgcQ}L83mi389aWclK&D`k%*oz` z1Xo8TSg)1*!t!*rQ#68tH~e45>@nH)*E4G$_-E&lK4A#`l{wAq`8NQ_ zVRSa&h_XDoDqun>gajWBb9TyraxXwEKPq(AJ~Rl8JoBiC%4`9ZjVz7K8;q+YqXHbR zv8_k2xr8`|p| z64GE*4iBKg9Wu{)C_tys-gva~o2HQ=Bx5?qHjY?yM%ze%Zy<4ox@X^)>}Kr+ot1@z zS$9l1ItrX--ot!0Y8fpdy&Pt1c&I4m_*8qT9gnv_ z(c6-xVhPU?&C-bOpq7UF_Y}9Ked~|4(4i2U&Av&Pq+s0v{ZJY&od4mV{AqLu(z(-H z_Q$$CDcz#k9*k2Vm^Rq>XNXpqWveA$X`9(>X1s|y-^*NwpCcaD(3DUzs>Je)@z;S% zZ(u9GgE$9EvAg%GTa}w*<^D#|@x>!WjLHch-S>t<@{At79yJpGJfVxc6sbeOPGVvf zs;|(F^VhKZ)u=&BME1h$I$MX>w}oiS93WP??kW4|uB?C{VOqsHy>|fF!G)aqz;BG8 zGpB~s2i#WQxyS1}6R@(pUdi_4>2LFJZ55&!x<3@(nx-g(_4#?Zx>D^N!!lF4+gdY> zTMk(6#*9#PFQ_i+*u(G_jWqO><)6ilOrbrW;Z;{_nVHB%OY=M0UayjbFcPb-#QwGG z?n3=K$mZC@okT8b7gX8+C=yjn<}2_BYWY{{5sp&nP5%f?+3L+ zn!VR1#-yToeqO_&`v1z6abk9)|3 zePW^u+G9W(2i-~0gGV+kRE$U!fE3EbC7r7GjRQ*E@pkz7IaUs$;qE zxGr?-rA8z;|LWa%by~>!{z|`MYuskD#@I+FSeS;&Zn_;37_;w^>wh>1vOknk+NJ~P zFrzD*8ZSAYr`OSFi@daZ`VNowSXXu=b!aUQpU|OkL|2m!o^T`7$9a@`BW_~uJC%LS zKcB(7IpNZ=5bSxEJ)-lvtS@90`4B{`XqC?=ATj z>&o0y)JN*Gj1ndxu%ge~8-kCvn`YZzAy}@D7b5t2Z~tZ+HMNC(L6>_M&|=dI;e4Br zBET`A;Mv$o{j%_=&cBAobXcu+$sgS$;y@3l#I%UDHtemn!rquRdm@I&&d-IWDQUb} zlCHk#Jj|lnf^HRNu1sjkw%UCT_U-FE-R$s})KMFxE@zr^18Mduh%~yzM<{L92 zsg!dQYM4fvNS?CNQLzzs--VReo^bs9h+6+Ejpz}`lI=YLp~k%@Ec$%#l`T;hJGI z%DDPqBL@c)2cUatamg*BGZ5R>RB|YXHysfPMnR@{iq=2WsH+DdHLqo%Rb3n5+xu^ z;=}u9x{!`zLIFr=uq9-Y9cjQ>_f1n=+WC`!wj^DZ`jud|DP}Xj{)J{J&GA<8}WVr + + + + account.move.form.inherit + account.move + + + + + + + + + \ No newline at end of file diff --git a/hr_commission/views/commission_views.xml b/hr_commission/views/commission_views.xml new file mode 100644 index 00000000..0e99a965 --- /dev/null +++ b/hr_commission/views/commission_views.xml @@ -0,0 +1,264 @@ + + + + hr.commission.form + hr.commission + +
+
+
+ +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + hr.commission.tree + hr.commission + + + + + + + + + + + + + + + + + + hr.commission.pivot + hr.commission + + + + + + + + + + + + hr.commission.graph + hr.commission + + + + + + + + + + + hr.commission.search + hr.commission + + + + + + + + + + + + + + + + + + + + + Commissions + hr.commission + tree,form,pivot,graph + + + + + + + + + + + + Mark Paid + ir.actions.server + code + + + +action = records.action_mark_paid() + + + + + + hr.commission.payment.form + hr.commission.payment + +
+
+ +
+

+ + + + + + + + + + + + + + + + + + + + + + + + hr.commission.payment.tree + hr.commission.payment + + + + + + + + + + + + + hr.commission.payment.search + hr.commission.payment + + + + + + + + + + + + + Commission Payments + hr.commission.payment + tree,form + + + + + + + + + hr.commission.structure.form + hr.commission.structure + +
+
+ +
+
+ +
+ + + + + + + + + + + + + + + diff --git a/hr_commission/views/hr_views.xml b/hr_commission/views/hr_views.xml new file mode 100644 index 00000000..6977afa2 --- /dev/null +++ b/hr_commission/views/hr_views.xml @@ -0,0 +1,16 @@ + + + + + hr.contract.form.inherit + hr.contract + + + + + + + + + + \ No newline at end of file diff --git a/hr_commission/views/partner_views.xml b/hr_commission/views/partner_views.xml new file mode 100644 index 00000000..23fcc43d --- /dev/null +++ b/hr_commission/views/partner_views.xml @@ -0,0 +1,15 @@ + + + + + res.parter.view.form.inherit + res.partner + + + + + + + + + \ No newline at end of file diff --git a/hr_commission/views/res_config_settings_views.xml b/hr_commission/views/res_config_settings_views.xml new file mode 100644 index 00000000..645c8f06 --- /dev/null +++ b/hr_commission/views/res_config_settings_views.xml @@ -0,0 +1,46 @@ + + + + + res.config.settings.view.form.inherit.account + res.config.settings + + + + +

Commissions

+
+
+
+
+ +
+ Commission journal default accounts can be thought of as the 'expense' side of the commission. If a Liability account + is not chosen, then the employee's home address partner's Account Payable will be used instead. +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + + \ No newline at end of file From ffc745321abf411bd311edf818a04779f73ce721 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Thu, 9 Jul 2020 18:46:37 -0700 Subject: [PATCH 2/7] [IMP] hr_commission: remove menus in Sales because we don't depend on `sale` --- hr_commission/tests/test_commission.py | 4 ++++ hr_commission/views/commission_views.xml | 21 --------------------- 2 files changed, 4 insertions(+), 21 deletions(-) diff --git a/hr_commission/tests/test_commission.py b/hr_commission/tests/test_commission.py index 688513d1..f921f30a 100644 --- a/hr_commission/tests/test_commission.py +++ b/hr_commission/tests/test_commission.py @@ -2,6 +2,10 @@ from odoo.tests import common +# TODO Tests won't pass without `sale` +# Tests should be refactored to not build sale orders +# to invoice, just create invoices directly. + class TestCommission(common.TransactionCase): diff --git a/hr_commission/views/commission_views.xml b/hr_commission/views/commission_views.xml index 0e99a965..62a695ca 100644 --- a/hr_commission/views/commission_views.xml +++ b/hr_commission/views/commission_views.xml @@ -116,20 +116,6 @@ tree,form,pivot,graph - - - - tree,form - - Date: Tue, 17 Nov 2020 15:06:01 -0500 Subject: [PATCH 3/7] [MIG] hr_commission: migrate to Odoo 14.0 --- hr_commission/__manifest__.py | 3 +- hr_commission/models/account.py | 2 +- hr_commission/models/commission.py | 12 ++--- hr_commission/tests/test_commission.py | 72 +++++++++++--------------- 4 files changed, 38 insertions(+), 51 deletions(-) diff --git a/hr_commission/__manifest__.py b/hr_commission/__manifest__.py index 52e7d5d7..83eea70e 100644 --- a/hr_commission/__manifest__.py +++ b/hr_commission/__manifest__.py @@ -3,12 +3,13 @@ { 'name': 'Hibou Commissions', 'author': 'Hibou Corp. ', - 'version': '13.0.1.0.1', + 'version': '14.0.1.0.0', 'category': 'Accounting/Commissions', 'license': 'OPL-1', 'website': 'https://hibou.io/', 'depends': [ # 'account_invoice_margin', # optional + 'account', 'hr_contract', ], 'data': [ diff --git a/hr_commission/models/account.py b/hr_commission/models/account.py index 9fc6c166..eb4d8350 100644 --- a/hr_commission/models/account.py +++ b/hr_commission/models/account.py @@ -39,7 +39,7 @@ class AccountMove(models.Model): def amount_for_commission(self): # TODO Should toggle in Config Params if hasattr(self, 'margin') and self.company_id.commission_amount_type == 'on_invoice_margin': - sign = -1 if self.type in ['in_refund', 'out_refund'] else 1 + sign = -1 if self.move_type in ['in_refund', 'out_refund'] else 1 return self.margin * sign return self.amount_total_signed diff --git a/hr_commission/models/commission.py b/hr_commission/models/commission.py index 3a491080..c6f3b25c 100644 --- a/hr_commission/models/commission.py +++ b/hr_commission/models/commission.py @@ -86,7 +86,6 @@ class Commission(models.Model): commission.amount = amount - @api.model def create(self, values): res = super(Commission, self).create(values) res._compute_amount() @@ -102,12 +101,10 @@ class Commission(models.Model): def _filter_source_moves_for_creation(self, moves): return moves.filtered(lambda i: i.user_id and not i.commission_ids) - @api.model def _commissions_to_confirm(self, moves): commissions = moves.mapped('commission_ids') return commissions.filtered(lambda c: c.state != 'cancel' and not c.move_id) - @api.model def invoice_validated(self, moves): employee_obj = self.env['hr.employee'].sudo() commission_obj = self.sudo() @@ -155,7 +152,6 @@ class Commission(models.Model): return True - @api.model def invoice_paid(self, moves): commissions = self._commissions_to_confirm(moves) commissions.sudo().action_confirm() @@ -172,7 +168,7 @@ class Commission(models.Model): continue journal = commission.company_id.commission_journal_id - if not journal or not journal.default_debit_account_id or not journal.default_credit_account_id: + if not journal or not journal.default_account_id: raise UserError('Commission Journal not configured.') liability_account = commission.company_id.commission_liability_id @@ -186,7 +182,7 @@ class Commission(models.Model): # Already paid. payments = commission.source_move_id._get_reconciled_payments() if payments: - date = max(payments.mapped('payment_date')) + date = max(payments.mapped('date')) if commission.accounting_date: date = commission.accounting_date @@ -198,7 +194,7 @@ class Commission(models.Model): 'date': date, 'ref': ref, 'journal_id': journal.id, - 'type': 'entry', + 'move_type': 'entry', 'line_ids': [ (0, 0, { 'name': ref, @@ -210,7 +206,7 @@ class Commission(models.Model): (0, 0, { 'name': ref, 'partner_id': commission.employee_id.address_home_id.id, - 'account_id': journal.default_credit_account_id.id if commission.amount > 0.0 else journal.default_debit_account_id.id, + 'account_id': journal.default_account_id.id, 'credit': 0.0 if commission.amount > 0.0 else -commission.amount, 'debit': commission.amount if commission.amount > 0.0 else 0.0, }), diff --git a/hr_commission/tests/test_commission.py b/hr_commission/tests/test_commission.py index f921f30a..86a84ebe 100644 --- a/hr_commission/tests/test_commission.py +++ b/hr_commission/tests/test_commission.py @@ -2,10 +2,6 @@ from odoo.tests import common -# TODO Tests won't pass without `sale` -# Tests should be refactored to not build sale orders -# to invoice, just create invoices directly. - class TestCommission(common.TransactionCase): @@ -44,34 +40,32 @@ class TestCommission(common.TransactionCase): 'admin_commission_rate': admin_commission_rate, 'state': 'open', # if not "Running" then no automatic selection when Payslip is created in 11.0 }) - - def _createInvoiceableSaleOrder(self, user): - product = self.env.ref('sale.advance_product_0') - partner = self.env.ref('base.res_partner_12') - sale = self.env['sale.order'].create({ - 'partner_id': partner.id, - 'user_id': user.id, - 'order_line': [(0, 0, { - 'name': 'test deposit', - 'product_id': product.id, - 'product_uom_qty': 1.0, - 'product_uom': product.uom_id.id, + + def _create_invoice(self, user): + # Create invoice + invoice = self.env['account.move'].create({ + 'move_type': 'out_invoice', + 'partner_id': self.env.ref("base.res_partner_2").id, + 'currency_id': self.env.ref('base.USD').id, + 'invoice_date': '2020-12-11', + 'invoice_user_id': user.id, + 'invoice_line_ids': [(0, 0, { + 'product_id': self.env.ref("product.product_product_4").id, + 'quantity': 1, 'price_unit': 5.0, - })] + })], }) - self.assertEqual(sale.user_id, user) - sale.action_confirm() - self.assertTrue(sale.state in ('sale', 'done')) - self.assertEqual(sale.invoice_status, 'to invoice') - return sale - + + self.assertEqual(invoice.invoice_user_id.id, user.id) + self.assertEqual(invoice.payment_state, 'not_paid') + return invoice + def test_commission(self): # find and configure company commissions journal commission_journal = self.env['account.journal'].search([('type', '=', 'general')], limit=1) self.assertTrue(commission_journal) expense_account = self.env.ref('l10n_generic_coa.1_expense') - commission_journal.default_debit_account_id = expense_account - commission_journal.default_credit_account_id = expense_account + commission_journal.default_account_id = expense_account self.env.user.company_id.commission_journal_id = commission_journal coach = self._createEmployee(self.browse_ref('base.user_root')) @@ -83,12 +77,12 @@ class TestCommission(common.TransactionCase): contract = self._createContract(emp, 5.0) - so = self._createInvoiceableSaleOrder(user) - inv = so._create_invoices() + inv = self._create_invoice(user) + self.assertFalse(inv.commission_ids, 'Commissions exist when invoice is created.') inv.action_post() # validate self.assertEqual(inv.state, 'posted') - self.assertEqual(inv.invoice_payment_state, 'not_paid') + self.assertEqual(inv.payment_state, 'not_paid') self.assertTrue(inv.commission_ids, 'Commissions not created when invoice is validated.') user_commission = inv.commission_ids.filtered(lambda c: c.employee_id.id == emp.id) @@ -114,11 +108,12 @@ class TestCommission(common.TransactionCase): 'currency_id': inv.currency_id.id, 'journal_id': pay_journal.id, }) - payment.post() + payment.action_post() + + receivable_line = payment.move_id.line_ids.filtered('credit') - receivable_line = payment.move_line_ids.filtered('credit') inv.js_assign_outstanding_line(receivable_line.id) - self.assertEqual(inv.invoice_payment_state, 'paid', 'Invoice is not paid.') + self.assertEqual(inv.payment_state, 'in_payment', 'Invoice is not paid.') user_commission = inv.commission_ids.filtered(lambda c: c.employee_id.id == emp.id) self.assertEqual(user_commission.state, 'done', 'Commission is not done.') @@ -150,8 +145,7 @@ class TestCommission(common.TransactionCase): commission_journal = self.env['account.journal'].search([('type', '=', 'general')], limit=1) self.assertTrue(commission_journal) expense_account = self.env.ref('l10n_generic_coa.1_expense') - commission_journal.default_debit_account_id = expense_account - commission_journal.default_credit_account_id = expense_account + commission_journal.default_account_id = expense_account self.env.user.company_id.commission_journal_id = commission_journal @@ -164,8 +158,7 @@ class TestCommission(common.TransactionCase): contract = self._createContract(emp, 5.0) - so = self._createInvoiceableSaleOrder(user) - inv = so._create_invoices() + inv = self._create_invoice(user) self.assertFalse(inv.commission_ids, 'Commissions exist when invoice is created.') inv.action_post() # validate self.assertEqual(inv.state, 'posted') @@ -188,8 +181,7 @@ class TestCommission(common.TransactionCase): commission_journal = self.env['account.journal'].search([('type', '=', 'general')], limit=1) self.assertTrue(commission_journal) expense_account = self.env.ref('l10n_generic_coa.1_expense') - commission_journal.default_debit_account_id = expense_account - commission_journal.default_credit_account_id = expense_account + commission_journal.default_account_id = expense_account self.env.user.company_id.commission_journal_id = commission_journal @@ -202,8 +194,6 @@ class TestCommission(common.TransactionCase): contract = self._createContract(emp, 5.0) - so = self._createInvoiceableSaleOrder(user) - # Create and set commission structure commission_structure = self.env['hr.commission.structure'].create({ 'name': 'Test Structure', @@ -212,9 +202,9 @@ class TestCommission(common.TransactionCase): (0, 0, {'employee_id': coach.id, 'rate': 0.0}), # This means it will use the coach's contract normal rate ], }) - so.partner_id.commission_structure_id = commission_structure - inv = so._create_invoices() + inv = self._create_invoice(user) + inv.partner_id.commission_structure_id = commission_structure self.assertFalse(inv.commission_ids, 'Commissions exist when invoice is created.') inv.action_post() # validate self.assertEqual(inv.state, 'posted') From 73d712b7166a472e0c049756bb833a0fc80051ca Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Mon, 24 May 2021 11:05:10 -0700 Subject: [PATCH 4/7] [FIX] hr_commission: creating in webclient --- hr_commission/models/commission.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hr_commission/models/commission.py b/hr_commission/models/commission.py index c6f3b25c..f6b891f0 100644 --- a/hr_commission/models/commission.py +++ b/hr_commission/models/commission.py @@ -85,7 +85,7 @@ class Commission(models.Model): amount = 0.0 commission.amount = amount - + @api.model def create(self, values): res = super(Commission, self).create(values) res._compute_amount() From 69469a7eecd305a26df7dc9737b0ad69f0df3d66 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Mon, 24 May 2021 11:53:14 -0700 Subject: [PATCH 5/7] [IMP] hr_commission: add option for tax excluded invoice total --- hr_commission/models/account.py | 3 ++- hr_commission/models/res_company.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/hr_commission/models/account.py b/hr_commission/models/account.py index eb4d8350..6180d014 100644 --- a/hr_commission/models/account.py +++ b/hr_commission/models/account.py @@ -37,10 +37,11 @@ class AccountMove(models.Model): return res def amount_for_commission(self): - # TODO Should toggle in Config Params if hasattr(self, 'margin') and self.company_id.commission_amount_type == 'on_invoice_margin': sign = -1 if self.move_type in ['in_refund', 'out_refund'] else 1 return self.margin * sign + elif self.company_id.commission_amount_type == 'on_invoice_untaxed': + return self.amount_untaxed_signed return self.amount_total_signed def action_cancel(self): diff --git a/hr_commission/models/res_company.py b/hr_commission/models/res_company.py index 26a2acb4..febdcaf9 100644 --- a/hr_commission/models/res_company.py +++ b/hr_commission/models/res_company.py @@ -15,4 +15,5 @@ class ResCompany(models.Model): commission_amount_type = fields.Selection([ ('on_invoice_margin', 'On Invoice Margin'), ('on_invoice_total', 'On Invoice Total'), + ('on_invoice_untaxed', 'On Invoice Total Tax Excluded'), ], string='Commission Base', default='on_invoice_margin') From c6310c514ac7da9c72db9eefe5c2a42fba528ccc Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Fri, 24 Sep 2021 16:11:55 -0700 Subject: [PATCH 6/7] [FIX] hr_commission: `account.move` soft deprecated method post --- hr_commission/models/commission.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hr_commission/models/commission.py b/hr_commission/models/commission.py index f6b891f0..2744dff4 100644 --- a/hr_commission/models/commission.py +++ b/hr_commission/models/commission.py @@ -212,7 +212,7 @@ class Commission(models.Model): }), ], }) - move.post() + move._post() commission.write({'state': 'done', 'move_id': move.id}) return True From 8a285b7b57c5f75038a2df4a046515dfb793ba7f Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Thu, 7 Oct 2021 11:50:35 -0700 Subject: [PATCH 7/7] [MIG] hr_commission: to Odoo 15.0 --- hr_commission/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hr_commission/__manifest__.py b/hr_commission/__manifest__.py index 83eea70e..491981fc 100644 --- a/hr_commission/__manifest__.py +++ b/hr_commission/__manifest__.py @@ -3,7 +3,7 @@ { 'name': 'Hibou Commissions', 'author': 'Hibou Corp. ', - 'version': '14.0.1.0.0', + 'version': '15.0.1.0.0', 'category': 'Accounting/Commissions', 'license': 'OPL-1', 'website': 'https://hibou.io/',