mirror of
https://gitlab.com/hibou-io/hibou-odoo/suite.git
synced 2025-01-20 12:37:31 +02:00
MIG hr_payroll_payment for Odoo 13 and improve
Pay your Payroll ================ Hibou's Payroll Payments modifies, and abstracts, the way that the accounting for payslips is generated. In stock Odoo 13, journal entries are grouped by account and name, but has no linking to partners. On the Payroll Journal, you can now select optional journal entry creation with the options: - Original: Stock Implementation - Grouped: Lines are grouped by account and partner. The slip line names will be comma separated in the line name. - Payslip: Lines are grouped by account and partner, as above, but a single journal entry will be created per payslip. Adds configuration on how you would pay your employees on the Payroll journal. e.g. You write a "check" from "Bank" Adds button on payslip and payslip batch to generate payment for the employee's payable portion. When paying on a batch, a "Batch Payment" will be generated and linked to the whole payslip run. Adds Accounting Date field on Batch to use when creating slips with the batch's date. Tested ------ Passes original Payroll Accounting tests and additional ones for gouping behavior.
This commit is contained in:
25
hr_payroll_payment/README.md
Normal file
25
hr_payroll_payment/README.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
Pay your Payroll
|
||||||
|
================
|
||||||
|
|
||||||
|
Hibou's Payroll Payments modifies, and abstracts, the way that the accounting for payslips is generated.
|
||||||
|
|
||||||
|
In stock Odoo 13, journal entries are grouped by account and name, but has no linking to partners.
|
||||||
|
|
||||||
|
On the Payroll Journal, you can now select optional journal entry creation with the options:
|
||||||
|
|
||||||
|
- Original: Stock Implementation
|
||||||
|
- Grouped: Lines are grouped by account and partner. The slip line names will be comma separated in the line name.
|
||||||
|
- Payslip: Lines are grouped by account and partner, as above, but a single journal entry will be created per payslip.
|
||||||
|
|
||||||
|
Adds configuration on how you would pay your employees on the Payroll journal. e.g. You write a "check" from "Bank"
|
||||||
|
|
||||||
|
Adds button on payslip and payslip batch to generate payment for the employee's payable portion.
|
||||||
|
|
||||||
|
When paying on a batch, a "Batch Payment" will be generated and linked to the whole payslip run.
|
||||||
|
|
||||||
|
Adds Accounting Date field on Batch to use when creating slips with the batch's date.
|
||||||
|
|
||||||
|
Tested
|
||||||
|
------
|
||||||
|
|
||||||
|
Passes original Payroll Accounting tests and additional ones for gouping behavior.
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||||
|
|
||||||
from . import hr_payroll_register_payment
|
from . import models
|
||||||
|
from . import wizard
|
||||||
|
|||||||
@@ -1,18 +1,50 @@
|
|||||||
|
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||||
|
|
||||||
{
|
{
|
||||||
'name': 'Payroll Payments',
|
'name': 'Payroll Payments',
|
||||||
'author': 'Hibou Corp. <hello@hibou.io>',
|
'author': 'Hibou Corp. <hello@hibou.io>',
|
||||||
'version': '12.0.1.0.0',
|
'version': '13.0.1.0.0',
|
||||||
'category': 'Human Resources',
|
'category': 'Human Resources',
|
||||||
'sequence': 95,
|
'sequence': 95,
|
||||||
'summary': 'Register payments for Payroll Payslips',
|
'summary': 'Register payments for Payroll Payslips',
|
||||||
'description': """
|
'description': """
|
||||||
Adds the ability to register a payment on a payslip.
|
Pay your Payroll
|
||||||
|
================
|
||||||
|
|
||||||
|
Hibou's Payroll Payments modifies, and abstracts, the way that the accounting for payslips is generated.
|
||||||
|
|
||||||
|
In stock Odoo 13, journal entries are grouped by account and name, but has no linking to partners.
|
||||||
|
|
||||||
|
On the Payroll Journal, you can now select optional journal entry creation with the options:
|
||||||
|
|
||||||
|
- Original: Stock Implementation
|
||||||
|
- Grouped: Lines are grouped by account and partner. The slip line names will be comma separated in the line name.
|
||||||
|
- Payslip: Lines are grouped by account and partner, as above, but a single journal entry will be created per payslip.
|
||||||
|
|
||||||
|
Adds configuration on how you would pay your employees on the Payroll journal. e.g. You write a "check" from "Bank"
|
||||||
|
|
||||||
|
Adds button on payslip and payslip batch to generate payment for the employee's payable portion.
|
||||||
|
|
||||||
|
When paying on a batch, a "Batch Payment" will be generated and linked to the whole payslip run.
|
||||||
|
|
||||||
|
Adds Accounting Date field on Batch to use when creating slips with the batch's date.
|
||||||
|
|
||||||
|
Tested
|
||||||
|
------
|
||||||
|
|
||||||
|
Passes original Payroll Accounting tests and additional ones for gouping behavior.
|
||||||
""",
|
""",
|
||||||
'website': 'https://hibou.io/',
|
'website': 'https://hibou.io/',
|
||||||
'depends': ['hr_payroll_account', 'payment'],
|
'depends': [
|
||||||
|
'hr_payroll_account',
|
||||||
|
'account_batch_payment',
|
||||||
|
],
|
||||||
'data': [
|
'data': [
|
||||||
'hr_payroll_register_payment.xml',
|
#'wizard/hr_payroll_register_payment_views.xml',
|
||||||
|
'views/account_views.xml',
|
||||||
|
'views/hr_payslip_views.xml',
|
||||||
],
|
],
|
||||||
'installable': True,
|
'installable': True,
|
||||||
'application': False,
|
'application': False,
|
||||||
|
'license': 'OPL-1',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,149 +0,0 @@
|
|||||||
from odoo import api, fields, models, _
|
|
||||||
from odoo.exceptions import ValidationError
|
|
||||||
|
|
||||||
|
|
||||||
class HrPayslip(models.Model):
|
|
||||||
_inherit = 'hr.payslip'
|
|
||||||
|
|
||||||
@api.multi
|
|
||||||
@api.depends('move_id', 'move_id.line_ids.full_reconcile_id')
|
|
||||||
def _is_paid(self):
|
|
||||||
for payslip in self:
|
|
||||||
payslip.is_paid = (
|
|
||||||
payslip.move_id and len(payslip.move_id.line_ids.filtered(lambda l: (
|
|
||||||
l.partner_id.id == payslip.employee_id.address_home_id.id and
|
|
||||||
l.account_id.internal_type == 'payable' and
|
|
||||||
not l.reconciled
|
|
||||||
))) == 0
|
|
||||||
)
|
|
||||||
|
|
||||||
is_paid = fields.Boolean(string="Has been Paid", compute='_is_paid', store=True)
|
|
||||||
|
|
||||||
|
|
||||||
class HrPayrollRegisterPaymentWizard(models.TransientModel):
|
|
||||||
|
|
||||||
_name = "hr.payroll.register.payment.wizard"
|
|
||||||
_description = "Hr Payroll Register Payment wizard"
|
|
||||||
|
|
||||||
@api.model
|
|
||||||
def _default_partner_id(self):
|
|
||||||
context = dict(self._context or {})
|
|
||||||
active_ids = context.get('active_ids', [])
|
|
||||||
payslip = self.env['hr.payslip'].browse(active_ids)
|
|
||||||
return payslip.employee_id.address_home_id.id
|
|
||||||
|
|
||||||
@api.model
|
|
||||||
def _default_amount(self):
|
|
||||||
context = dict(self._context or {})
|
|
||||||
active_ids = context.get('active_ids', [])
|
|
||||||
payslip = self.env['hr.payslip'].browse(active_ids)
|
|
||||||
amount = -sum(payslip.move_id.line_ids.filtered(lambda l: (
|
|
||||||
l.account_id.internal_type == 'payable'
|
|
||||||
and l.partner_id.id == payslip.employee_id.address_home_id.id
|
|
||||||
and not l.reconciled)
|
|
||||||
).mapped('balance'))
|
|
||||||
return amount
|
|
||||||
|
|
||||||
@api.model
|
|
||||||
def _default_communication(self):
|
|
||||||
context = dict(self._context or {})
|
|
||||||
active_ids = context.get('active_ids', [])
|
|
||||||
payslip = self.env['hr.payslip'].browse(active_ids)
|
|
||||||
return payslip.number
|
|
||||||
|
|
||||||
partner_id = fields.Many2one('res.partner', string='Partner', required=True, default=_default_partner_id)
|
|
||||||
journal_id = fields.Many2one('account.journal', string='Payment Method', required=True, domain=[('type', 'in', ('bank', 'cash'))])
|
|
||||||
company_id = fields.Many2one('res.company', related='journal_id.company_id', string='Company', readonly=True, required=True)
|
|
||||||
payment_method_id = fields.Many2one('account.payment.method', string='Payment Type', required=True)
|
|
||||||
payment_method_code = fields.Char(related='payment_method_id.code',
|
|
||||||
help="Technical field used to adapt the interface to the payment type selected.", readonly=True)
|
|
||||||
payment_transaction_id = fields.Many2one('payment.transaction', string="Payment Transaction")
|
|
||||||
payment_token_id = fields.Many2one('payment.token', string="Saved payment token",
|
|
||||||
domain=[('acquirer_id.capture_manually', '=', False)],
|
|
||||||
help="Note that tokens from acquirers set to only authorize transactions (instead of capturing the amount) are not available.")
|
|
||||||
amount = fields.Monetary(string='Payment Amount', required=True, default=_default_amount)
|
|
||||||
currency_id = fields.Many2one('res.currency', string='Currency', required=True, default=lambda self: self.env.user.company_id.currency_id)
|
|
||||||
payment_date = fields.Date(string='Payment Date', default=fields.Date.context_today, required=True)
|
|
||||||
communication = fields.Char(string='Memo', default=_default_communication)
|
|
||||||
hide_payment_method = fields.Boolean(compute='_compute_hide_payment_method',
|
|
||||||
help="Technical field used to hide the payment method if the selected journal has only one available which is 'manual'")
|
|
||||||
|
|
||||||
@api.onchange('partner_id')
|
|
||||||
def _onchange_partner_id(self):
|
|
||||||
res = {}
|
|
||||||
if self.partner_id:
|
|
||||||
partners = self.partner_id | self.partner_id.commercial_partner_id | self.partner_id.commercial_partner_id.child_ids
|
|
||||||
res['domain'] = {
|
|
||||||
'payment_token_id': [('partner_id', 'in', partners.ids), ('acquirer_id.capture_manually', '=', False)]}
|
|
||||||
|
|
||||||
return res
|
|
||||||
|
|
||||||
@api.onchange('payment_method_id', 'journal_id')
|
|
||||||
def _onchange_payment_method(self):
|
|
||||||
if self.payment_method_code == 'electronic':
|
|
||||||
self.payment_token_id = self.env['payment.token'].search(
|
|
||||||
[('partner_id', '=', self.partner_id.id), ('acquirer_id.capture_manually', '=', False)], limit=1)
|
|
||||||
else:
|
|
||||||
self.payment_token_id = False
|
|
||||||
|
|
||||||
@api.one
|
|
||||||
@api.constrains('amount')
|
|
||||||
def _check_amount(self):
|
|
||||||
if not self.amount > 0.0:
|
|
||||||
raise ValidationError('The payment amount must be strictly positive.')
|
|
||||||
|
|
||||||
@api.one
|
|
||||||
@api.depends('journal_id')
|
|
||||||
def _compute_hide_payment_method(self):
|
|
||||||
if not self.journal_id:
|
|
||||||
self.hide_payment_method = True
|
|
||||||
return
|
|
||||||
journal_payment_methods = self.journal_id.outbound_payment_method_ids
|
|
||||||
self.hide_payment_method = len(journal_payment_methods) == 1 and journal_payment_methods[0].code == 'manual'
|
|
||||||
|
|
||||||
@api.onchange('journal_id')
|
|
||||||
def _onchange_journal(self):
|
|
||||||
if self.journal_id:
|
|
||||||
# Set default payment method (we consider the first to be the default one)
|
|
||||||
payment_methods = self.journal_id.outbound_payment_method_ids
|
|
||||||
self.payment_method_id = payment_methods and payment_methods[0] or False
|
|
||||||
# Set payment method domain (restrict to methods enabled for the journal and to selected payment type)
|
|
||||||
return {'domain': {'payment_method_id': [('payment_type', '=', 'outbound'), ('id', 'in', payment_methods.ids)]}}
|
|
||||||
return {}
|
|
||||||
|
|
||||||
@api.multi
|
|
||||||
def payroll_post_payment(self):
|
|
||||||
self.ensure_one()
|
|
||||||
context = dict(self._context or {})
|
|
||||||
active_ids = context.get('active_ids', [])
|
|
||||||
payslip = self.env['hr.payslip'].browse(active_ids)
|
|
||||||
|
|
||||||
# Create payment and post it
|
|
||||||
payment = self.env['account.payment'].create({
|
|
||||||
'partner_type': 'supplier',
|
|
||||||
'payment_type': 'outbound',
|
|
||||||
'partner_id': self.partner_id.id,
|
|
||||||
'journal_id': self.journal_id.id,
|
|
||||||
'company_id': self.company_id.id,
|
|
||||||
'payment_method_id': self.payment_method_id.id,
|
|
||||||
'amount': self.amount,
|
|
||||||
'currency_id': self.currency_id.id,
|
|
||||||
'payment_date': self.payment_date,
|
|
||||||
'communication': self.communication,
|
|
||||||
'payment_transaction_id': self.payment_transaction_id.id if self.payment_transaction_id else False,
|
|
||||||
'payment_token_id': self.payment_token_id.id if self.payment_token_id else False,
|
|
||||||
})
|
|
||||||
payment.post()
|
|
||||||
|
|
||||||
# Reconcile the payment and the payroll, i.e. lookup on the payable account move lines
|
|
||||||
account_move_lines_to_reconcile = self.env['account.move.line']
|
|
||||||
for line in payment.move_line_ids:
|
|
||||||
if line.account_id.internal_type == 'payable':
|
|
||||||
account_move_lines_to_reconcile |= line
|
|
||||||
for line in payslip.move_id.line_ids:
|
|
||||||
if line.account_id.internal_type == 'payable' and line.partner_id.id == self.partner_id.id and not line.reconciled:
|
|
||||||
account_move_lines_to_reconcile |= line
|
|
||||||
|
|
||||||
account_move_lines_to_reconcile.reconcile()
|
|
||||||
|
|
||||||
return {'type': 'ir.actions.act_window_close'}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<odoo>
|
|
||||||
|
|
||||||
<!-- Add Filter for "Needs Payment" -->
|
|
||||||
<record id="payslip_filter_needs_payment" model="ir.ui.view">
|
|
||||||
<field name="name">hr.payslip.select</field>
|
|
||||||
<field name="model">hr.payslip</field>
|
|
||||||
<field name="priority">20</field>
|
|
||||||
<field name="inherit_id" ref="hr_payroll.view_hr_payslip_filter"/>
|
|
||||||
<field name="arch" type="xml">
|
|
||||||
<data>
|
|
||||||
<xpath expr="//filter[last()]" position="after">
|
|
||||||
<filter name="filter_needs_payment" string="Needs Payment" domain="[('is_paid', '=', False)]" help="Needs Payment Slip"/>
|
|
||||||
</xpath>
|
|
||||||
</data>
|
|
||||||
</field>
|
|
||||||
</record>
|
|
||||||
|
|
||||||
<!-- Wizard view to register payment -->
|
|
||||||
<record id="hr_payroll_register_payment_view_form" model="ir.ui.view">
|
|
||||||
<field name="name">hr.payroll.register.payment.wizard.form</field>
|
|
||||||
<field name="model">hr.payroll.register.payment.wizard</field>
|
|
||||||
<field name="arch" type="xml">
|
|
||||||
<form string="Register Payment">
|
|
||||||
<sheet>
|
|
||||||
<field name="id" invisible="1"/>
|
|
||||||
<div class="oe_title">
|
|
||||||
<h1>Draft Payment</h1>
|
|
||||||
</div>
|
|
||||||
<group>
|
|
||||||
<group>
|
|
||||||
<field name="partner_id" required="1" context="{'default_is_company': True, 'default_supplier': True}"/>
|
|
||||||
<field name="journal_id" widget="selection"/>
|
|
||||||
<field name="hide_payment_method" invisible="1"/>
|
|
||||||
<field name="payment_method_id" widget="radio" attrs="{'invisible': [('hide_payment_method', '=', True)]}"/>
|
|
||||||
<field name="payment_method_code" invisible="1"/>
|
|
||||||
<field name="payment_token_id" options="{'no_create': True}"
|
|
||||||
attrs="{'invisible': [('payment_method_code', '!=', 'electronic')],
|
|
||||||
'required': [('payment_method_code', '=', 'electronic')]}"/>
|
|
||||||
<label for="amount"/>
|
|
||||||
<div name="amount_div" class="o_row">
|
|
||||||
<field name="amount"/>
|
|
||||||
<field name="currency_id" options="{'no_create': True, 'no_open': True}" groups="base.group_multi_currency"/>
|
|
||||||
</div>
|
|
||||||
</group>
|
|
||||||
<group>
|
|
||||||
<field name="payment_date"/>
|
|
||||||
<field name="communication"/>
|
|
||||||
<field name="payment_transaction_id"/>
|
|
||||||
</group>
|
|
||||||
</group>
|
|
||||||
</sheet>
|
|
||||||
<footer>
|
|
||||||
<button string='Validate' name="payroll_post_payment" type="object" class="btn-primary"/>
|
|
||||||
<button string="Cancel" class="btn-default" special="cancel"/>
|
|
||||||
</footer>
|
|
||||||
</form>
|
|
||||||
</field>
|
|
||||||
</record>
|
|
||||||
|
|
||||||
<!-- Action to launch Wizard for register payment -->
|
|
||||||
<record id="hr_payroll_register_payment_wizard_action" model="ir.actions.act_window">
|
|
||||||
<field name="name">Register Payment</field>
|
|
||||||
<field name="res_model">hr.payroll.register.payment.wizard</field>
|
|
||||||
<field name="view_type">form</field>
|
|
||||||
<field name="view_mode">form</field>
|
|
||||||
<field name="view_id" ref="hr_payroll_register_payment_view_form"/>
|
|
||||||
<field name="target">new</field>
|
|
||||||
<field name="context">{'default_payment_type': 'inbound'}</field>
|
|
||||||
<field name="domain">[('partner_type', '=', 'customer')]</field>
|
|
||||||
</record>
|
|
||||||
|
|
||||||
<!-- Button on Payslip to launch Wizard for register payment -->
|
|
||||||
<record id="hr_payslip_form" model="ir.ui.view">
|
|
||||||
<field name="name">hr.payslip.form</field>
|
|
||||||
<field name="model">hr.payslip</field>
|
|
||||||
<field name="inherit_id" ref="hr_payroll.view_hr_payslip_form"/>
|
|
||||||
<field name="arch" type="xml">
|
|
||||||
<xpath expr="//button[@name='refund_sheet']" position="before">
|
|
||||||
<button name="%(hr_payroll_payment.hr_payroll_register_payment_wizard_action)d" type="action" string="Register Payment" class="oe_highlight" attrs="{'invisible': ['|', ('state', '!=', 'done'), ('is_paid', '=', True)]}" groups="account.group_account_user"/>
|
|
||||||
</xpath>
|
|
||||||
<xpath expr="//field[@name='credit_note']" position="after">
|
|
||||||
<field name="is_paid" readonly="1"/>
|
|
||||||
</xpath>
|
|
||||||
</field>
|
|
||||||
</record>
|
|
||||||
</odoo>
|
|
||||||
5
hr_payroll_payment/models/__init__.py
Normal file
5
hr_payroll_payment/models/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from . import account
|
||||||
|
from . import hr_payslip
|
||||||
|
from . import hr_payslip_patch
|
||||||
19
hr_payroll_payment/models/account.py
Normal file
19
hr_payroll_payment/models/account.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from odoo import fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class AccountJournal(models.Model):
|
||||||
|
_inherit = 'account.journal'
|
||||||
|
|
||||||
|
payroll_entry_type = fields.Selection([
|
||||||
|
('original', 'Original'),
|
||||||
|
('grouped', 'Grouped'),
|
||||||
|
('slip', 'Payslip'),
|
||||||
|
], string='Payroll Entry Type',
|
||||||
|
default='grouped',
|
||||||
|
help="Grouped and Payslip will add partner info and group by account and partner. "
|
||||||
|
"Payslip will generate a journal entry for every payslip.")
|
||||||
|
payroll_payment_journal_id = fields.Many2one('account.journal', string='Payroll Payment Journal')
|
||||||
|
payroll_payment_method_id = fields.Many2one('account.payment.method', string='Payroll Payment Method')
|
||||||
|
payroll_payment_method_refund_id = fields.Many2one('account.payment.method', string='Payroll Refund Method')
|
||||||
556
hr_payroll_payment/models/hr_payslip.py
Normal file
556
hr_payroll_payment/models/hr_payslip.py
Normal file
@@ -0,0 +1,556 @@
|
|||||||
|
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from odoo import api, fields, models, _
|
||||||
|
from odoo.exceptions import UserError, ValidationError
|
||||||
|
from odoo.tools import float_compare, float_is_zero
|
||||||
|
|
||||||
|
|
||||||
|
class HrPayslipRun(models.Model):
|
||||||
|
_inherit = 'hr.payslip.run'
|
||||||
|
|
||||||
|
@api.depends('slip_ids.is_paid')
|
||||||
|
def _is_paid(self):
|
||||||
|
for run in self:
|
||||||
|
run.is_paid = all(run.slip_ids.mapped('is_paid'))
|
||||||
|
|
||||||
|
is_paid = fields.Boolean(string="Payslips Paid", compute='_is_paid', store=True)
|
||||||
|
date = fields.Date('Date Account', states={'draft': [('readonly', False)], 'verify': [('readonly', False)]},
|
||||||
|
readonly=True,
|
||||||
|
help="Keep empty to use the period of the validation(Payslip) date.")
|
||||||
|
batch_payment_id = fields.Many2one('account.batch.payment', string='Payment Batch')
|
||||||
|
|
||||||
|
def action_register_payment(self):
|
||||||
|
action = self.mapped('slip_ids').action_register_payment()
|
||||||
|
payments = self.env['account.payment'].browse(action['res_ids'])
|
||||||
|
batch_action = payments.create_batch_payment()
|
||||||
|
self.write({'batch_payment_id': batch_action['res_id']})
|
||||||
|
return batch_action
|
||||||
|
|
||||||
|
def write(self, values):
|
||||||
|
if 'date' in values:
|
||||||
|
slips = self.mapped('slip_ids').filtered(lambda s: s.state in ('draft', 'verify'))
|
||||||
|
slips.write({'date': values['date']})
|
||||||
|
return super().write(values)
|
||||||
|
|
||||||
|
|
||||||
|
class HrPayslip(models.Model):
|
||||||
|
_inherit = 'hr.payslip'
|
||||||
|
|
||||||
|
@api.depends('move_id', 'move_id.line_ids.full_reconcile_id')
|
||||||
|
def _is_paid(self):
|
||||||
|
for payslip in self:
|
||||||
|
payslip.is_paid = (
|
||||||
|
payslip.move_id and len(payslip.move_id.line_ids.filtered(lambda l: (
|
||||||
|
l.partner_id == payslip.employee_id.address_home_id and
|
||||||
|
l.account_id.internal_type == 'payable' and
|
||||||
|
not l.reconciled
|
||||||
|
))) == 0
|
||||||
|
)
|
||||||
|
|
||||||
|
is_paid = fields.Boolean(string="Has been Paid", compute='_is_paid', store=True)
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def create(self, vals):
|
||||||
|
if 'date' in self.env.context:
|
||||||
|
vals['date'] = self.env.context.get('date')
|
||||||
|
return super(HrPayslip, self).create(vals)
|
||||||
|
|
||||||
|
def _payment_values(self, amount):
|
||||||
|
values = {
|
||||||
|
'payment_reference': self.number,
|
||||||
|
'communication': self.number + ' - ' + self.name,
|
||||||
|
'journal_id': self.move_id.journal_id.payroll_payment_journal_id.id,
|
||||||
|
'payment_method_id': self.move_id.journal_id.payroll_payment_method_id.id,
|
||||||
|
'partner_type': 'supplier',
|
||||||
|
'partner_id': self.employee_id.address_home_id.id,
|
||||||
|
'payment_type': 'outbound',
|
||||||
|
'amount': -amount,
|
||||||
|
}
|
||||||
|
if amount > 0.0:
|
||||||
|
values.update({
|
||||||
|
'payment_type': 'inbound',
|
||||||
|
'amount': amount,
|
||||||
|
'payment_method_id': self.move_id.journal_id.payroll_payment_method_refund_id.id,
|
||||||
|
})
|
||||||
|
return values
|
||||||
|
|
||||||
|
def action_register_payment(self):
|
||||||
|
if not all(slip.move_id.journal_id.payroll_payment_journal_id for slip in self):
|
||||||
|
raise UserError(_('Payroll Payment journal not configured on the existing entry\'s journal.'))
|
||||||
|
if not all(slip.move_id.journal_id.payroll_payment_method_id for slip in self):
|
||||||
|
raise UserError(_('Payroll Payment method not configured on the existing entry\'s journal.'))
|
||||||
|
|
||||||
|
payments = self.env['account.payment']
|
||||||
|
for slip in self.filtered(lambda s: s.move_id and not s.is_paid):
|
||||||
|
lines_to_pay = slip.move_id.line_ids.filtered(lambda l: l.partner_id == slip.employee_id.address_home_id
|
||||||
|
and l.account_id == slip.employee_id.address_home_id.property_account_payable_id)
|
||||||
|
amount = sum(lines_to_pay.mapped('amount_residual'))
|
||||||
|
payment_values = slip._payment_values(amount)
|
||||||
|
payment = payments.create(payment_values)
|
||||||
|
payment.post()
|
||||||
|
lines_paid = payment.move_line_ids.filtered(lambda l: l.account_id == slip.employee_id.address_home_id.property_account_payable_id)
|
||||||
|
lines_to_reconcile = lines_to_pay + lines_paid
|
||||||
|
lines_to_reconcile.reconcile()
|
||||||
|
payments += payment
|
||||||
|
action = self.env.ref('account.action_account_payments_payable').read()[0]
|
||||||
|
action.update({
|
||||||
|
'res_ids': payments.ids,
|
||||||
|
'domain': [('id', 'in', payments.ids)],
|
||||||
|
})
|
||||||
|
return action
|
||||||
|
|
||||||
|
def action_payslip_done(self):
|
||||||
|
res = super(HrPayslip, self).action_payslip_done()
|
||||||
|
self._generate_move()
|
||||||
|
return res
|
||||||
|
|
||||||
|
def _generate_move(self):
|
||||||
|
"""
|
||||||
|
Generate the accounting entries related to the selected payslips
|
||||||
|
A move is created for each journal and for each month.
|
||||||
|
"""
|
||||||
|
# Not needed after abstraction
|
||||||
|
|
||||||
|
#res = super(HrPayslip, self).action_payslip_done()
|
||||||
|
#precision = self.env['decimal.precision'].precision_get('Payroll')
|
||||||
|
|
||||||
|
# Add payslip without run
|
||||||
|
payslips_to_post = self.filtered(lambda slip: not slip.payslip_run_id)
|
||||||
|
|
||||||
|
# Adding pay slips from a batch and deleting pay slips with a batch that is not ready for validation.
|
||||||
|
payslip_runs = (self - payslips_to_post).mapped('payslip_run_id')
|
||||||
|
for run in payslip_runs:
|
||||||
|
if run._are_payslips_ready():
|
||||||
|
payslips_to_post |= run.slip_ids
|
||||||
|
|
||||||
|
# A payslip need to have a done state and not an accounting move.
|
||||||
|
payslips_to_post = payslips_to_post.filtered(lambda slip: slip.state == 'done' and not slip.move_id)
|
||||||
|
|
||||||
|
# Check that a journal exists on all the structures
|
||||||
|
if any(not payslip.struct_id for payslip in payslips_to_post):
|
||||||
|
raise ValidationError(_('One of the contract for these payslips has no structure type.'))
|
||||||
|
if any(not structure.journal_id for structure in payslips_to_post.mapped('struct_id')):
|
||||||
|
raise ValidationError(_('One of the payroll structures has no account journal defined on it.'))
|
||||||
|
|
||||||
|
# Map all payslips by structure journal and pay slips month.
|
||||||
|
# {'journal_id': {'month': [slip_ids]}}
|
||||||
|
# slip_mapped_data = {
|
||||||
|
# slip.struct_id.journal_id.id: {fields.Date().end_of(slip.date_to, 'month'): self.env['hr.payslip']} for slip
|
||||||
|
# in
|
||||||
|
# payslips_to_post}
|
||||||
|
# Hibou Customization: group with journal itself so that journal behavior can be derived.
|
||||||
|
slip_mapped_data = {
|
||||||
|
slip.struct_id.journal_id: {fields.Date().end_of(slip.date_to, 'month'): self.env['hr.payslip']} for slip
|
||||||
|
in
|
||||||
|
payslips_to_post}
|
||||||
|
for slip in payslips_to_post:
|
||||||
|
slip_mapped_data[slip.struct_id.journal_id][fields.Date().end_of(slip.date_to, 'month')] |= slip
|
||||||
|
|
||||||
|
for journal in slip_mapped_data: # For each journal_id.
|
||||||
|
"""
|
||||||
|
All methods to generate journal entry should generate one or more
|
||||||
|
journal entries given this format.
|
||||||
|
"""
|
||||||
|
for slip_date in slip_mapped_data[journal]: # For each month.
|
||||||
|
if hasattr(self, '_generate_move_' + str(journal.payroll_entry_type)):
|
||||||
|
getattr(self, '_generate_move_' + str(journal.payroll_entry_type))(slip_mapped_data, journal, slip_date)
|
||||||
|
else:
|
||||||
|
self._generate_move_original(slip_mapped_data, journal, slip_date)
|
||||||
|
|
||||||
|
def _check_slips_employee_home_address(self):
|
||||||
|
employees_missing_partner = self.mapped('employee_id').filtered(lambda e: not e.address_home_id)
|
||||||
|
if employees_missing_partner:
|
||||||
|
raise UserError(_('The following employees are missing private addresses. %s') % \
|
||||||
|
(', '.join(employees_missing_partner.mapped('name'))))
|
||||||
|
address_ap = self.mapped('employee_id.address_home_id.property_account_payable_id')
|
||||||
|
if len(address_ap) > 1:
|
||||||
|
raise UserError(_('Employee\'s private address account payable not the same for all addresses.'))
|
||||||
|
|
||||||
|
def _process_journal_lines_grouped(self, line_ids, date, precision):
|
||||||
|
slip = self
|
||||||
|
employee_partner_id = slip.employee_id.address_home_id.id
|
||||||
|
for line in slip.line_ids.filtered(lambda l: l.category_id):
|
||||||
|
amount = -line.total if slip.credit_note else line.total
|
||||||
|
if line.code == 'NET': # Check if the line is the 'Net Salary'.
|
||||||
|
for tmp_line in slip.line_ids.filtered(lambda l: l.category_id):
|
||||||
|
if tmp_line.salary_rule_id.not_computed_in_net: # Check if the rule must be computed in the 'Net Salary' or not.
|
||||||
|
if amount > 0:
|
||||||
|
amount -= abs(tmp_line.total)
|
||||||
|
elif amount < 0:
|
||||||
|
amount += abs(tmp_line.total)
|
||||||
|
if float_is_zero(amount, precision_digits=precision):
|
||||||
|
continue
|
||||||
|
debit_account_id = line.salary_rule_id.account_debit.id
|
||||||
|
credit_account_id = line.salary_rule_id.account_credit.id
|
||||||
|
partner_id = line.salary_rule_id.partner_id.id or employee_partner_id
|
||||||
|
|
||||||
|
if debit_account_id: # If the rule has a debit account.
|
||||||
|
debit = amount if amount > 0.0 else 0.0
|
||||||
|
credit = -amount if amount < 0.0 else 0.0
|
||||||
|
|
||||||
|
existing_debit_lines = (
|
||||||
|
line_id for line_id in line_ids if
|
||||||
|
# line_id['name'] == line.name
|
||||||
|
line_id['partner_id'] == partner_id
|
||||||
|
and line_id['account_id'] == debit_account_id
|
||||||
|
and ((line_id['debit'] > 0 and credit <= 0) or (line_id['credit'] > 0 and debit <= 0)))
|
||||||
|
debit_line = next(existing_debit_lines, False)
|
||||||
|
|
||||||
|
if not debit_line:
|
||||||
|
debit_line = {
|
||||||
|
'name': line.name,
|
||||||
|
'partner_id': partner_id,
|
||||||
|
'account_id': debit_account_id,
|
||||||
|
'journal_id': slip.struct_id.journal_id.id,
|
||||||
|
'date': date,
|
||||||
|
'debit': debit,
|
||||||
|
'credit': credit,
|
||||||
|
'analytic_account_id': line.salary_rule_id.analytic_account_id.id or slip.contract_id.analytic_account_id.id,
|
||||||
|
}
|
||||||
|
line_ids.append(debit_line)
|
||||||
|
else:
|
||||||
|
line_name_pieces = set(debit_line['name'].split(', '))
|
||||||
|
line_name_pieces.add(line.name)
|
||||||
|
debit_line['name'] = ', '.join(line_name_pieces)
|
||||||
|
debit_line['debit'] += debit
|
||||||
|
debit_line['credit'] += credit
|
||||||
|
|
||||||
|
if credit_account_id: # If the rule has a credit account.
|
||||||
|
debit = -amount if amount < 0.0 else 0.0
|
||||||
|
credit = amount if amount > 0.0 else 0.0
|
||||||
|
existing_credit_line = (
|
||||||
|
line_id for line_id in line_ids if
|
||||||
|
# line_id['name'] == line.name
|
||||||
|
line_id['partner_id'] == partner_id
|
||||||
|
and line_id['account_id'] == credit_account_id
|
||||||
|
and ((line_id['debit'] > 0 and credit <= 0) or (line_id['credit'] > 0 and debit <= 0))
|
||||||
|
)
|
||||||
|
credit_line = next(existing_credit_line, False)
|
||||||
|
|
||||||
|
if not credit_line:
|
||||||
|
credit_line = {
|
||||||
|
'name': line.name,
|
||||||
|
'partner_id': partner_id,
|
||||||
|
'account_id': credit_account_id,
|
||||||
|
'journal_id': slip.struct_id.journal_id.id,
|
||||||
|
'date': date,
|
||||||
|
'debit': debit,
|
||||||
|
'credit': credit,
|
||||||
|
'analytic_account_id': line.salary_rule_id.analytic_account_id.id or slip.contract_id.analytic_account_id.id,
|
||||||
|
}
|
||||||
|
line_ids.append(credit_line)
|
||||||
|
else:
|
||||||
|
line_name_pieces = set(credit_line['name'].split(', '))
|
||||||
|
line_name_pieces.add(line.name)
|
||||||
|
credit_line['name'] = ', '.join(line_name_pieces)
|
||||||
|
credit_line['debit'] += debit
|
||||||
|
credit_line['credit'] += credit
|
||||||
|
|
||||||
|
def _generate_move_grouped(self, slip_mapped_data, journal, slip_date):
|
||||||
|
slip_mapped_data[journal][slip_date]._check_slips_employee_home_address()
|
||||||
|
|
||||||
|
precision = self.env['decimal.precision'].precision_get('Payroll')
|
||||||
|
line_ids = []
|
||||||
|
debit_sum = 0.0
|
||||||
|
credit_sum = 0.0
|
||||||
|
date = slip_date
|
||||||
|
move_dict = {
|
||||||
|
'narration': '',
|
||||||
|
'ref': date.strftime('%B %Y'),
|
||||||
|
'journal_id': journal.id,
|
||||||
|
'date': date,
|
||||||
|
}
|
||||||
|
|
||||||
|
for slip in slip_mapped_data[journal][slip_date]:
|
||||||
|
move_dict['narration'] += slip.number or '' + ' - ' + slip.employee_id.name or ''
|
||||||
|
move_dict['narration'] += '\n'
|
||||||
|
slip._process_journal_lines_grouped(line_ids, date, precision)
|
||||||
|
|
||||||
|
for line_id in line_ids: # Get the debit and credit sum.
|
||||||
|
debit_sum += line_id['debit']
|
||||||
|
credit_sum += line_id['credit']
|
||||||
|
|
||||||
|
# The code below is called if there is an error in the balance between credit and debit sum.
|
||||||
|
if float_compare(credit_sum, debit_sum, precision_digits=precision) == -1:
|
||||||
|
acc_id = slip.journal_id.default_credit_account_id.id
|
||||||
|
if not acc_id:
|
||||||
|
raise UserError(
|
||||||
|
_('The Expense Journal "%s" has not properly configured the Credit Account!') % (
|
||||||
|
slip.journal_id.name))
|
||||||
|
existing_adjustment_line = (
|
||||||
|
line_id for line_id in line_ids if line_id['name'] == _('Adjustment Entry')
|
||||||
|
)
|
||||||
|
adjust_credit = next(existing_adjustment_line, False)
|
||||||
|
|
||||||
|
if not adjust_credit:
|
||||||
|
adjust_credit = {
|
||||||
|
'name': _('Adjustment Entry'),
|
||||||
|
'partner_id': False,
|
||||||
|
'account_id': acc_id,
|
||||||
|
'journal_id': slip.journal_id.id,
|
||||||
|
'date': date,
|
||||||
|
'debit': 0.0,
|
||||||
|
'credit': debit_sum - credit_sum,
|
||||||
|
}
|
||||||
|
line_ids.append(adjust_credit)
|
||||||
|
else:
|
||||||
|
adjust_credit['credit'] = debit_sum - credit_sum
|
||||||
|
|
||||||
|
elif float_compare(debit_sum, credit_sum, precision_digits=precision) == -1:
|
||||||
|
acc_id = slip.journal_id.default_debit_account_id.id
|
||||||
|
if not acc_id:
|
||||||
|
raise UserError(_('The Expense Journal "%s" has not properly configured the Debit Account!') % (
|
||||||
|
slip.journal_id.name))
|
||||||
|
existing_adjustment_line = (
|
||||||
|
line_id for line_id in line_ids if line_id['name'] == _('Adjustment Entry')
|
||||||
|
)
|
||||||
|
adjust_debit = next(existing_adjustment_line, False)
|
||||||
|
|
||||||
|
if not adjust_debit:
|
||||||
|
adjust_debit = {
|
||||||
|
'name': _('Adjustment Entry'),
|
||||||
|
'partner_id': False,
|
||||||
|
'account_id': acc_id,
|
||||||
|
'journal_id': slip.journal_id.id,
|
||||||
|
'date': date,
|
||||||
|
'debit': credit_sum - debit_sum,
|
||||||
|
'credit': 0.0,
|
||||||
|
}
|
||||||
|
line_ids.append(adjust_debit)
|
||||||
|
else:
|
||||||
|
adjust_debit['debit'] = credit_sum - debit_sum
|
||||||
|
|
||||||
|
# Add accounting lines in the move
|
||||||
|
move_dict['line_ids'] = [(0, 0, line_vals) for line_vals in line_ids]
|
||||||
|
move = self.env['account.move'].create(move_dict)
|
||||||
|
for slip in slip_mapped_data[journal][slip_date]:
|
||||||
|
slip.write({'move_id': move.id, 'date': date})
|
||||||
|
|
||||||
|
def _generate_move_slip(self, slip_mapped_data, journal, slip_date):
|
||||||
|
slip_mapped_data[journal][slip_date]._check_slips_employee_home_address()
|
||||||
|
|
||||||
|
precision = self.env['decimal.precision'].precision_get('Payroll')
|
||||||
|
|
||||||
|
for slip in slip_mapped_data[journal][slip_date]:
|
||||||
|
line_ids = []
|
||||||
|
debit_sum = 0.0
|
||||||
|
credit_sum = 0.0
|
||||||
|
date = slip_date
|
||||||
|
move_dict = {
|
||||||
|
'narration': '',
|
||||||
|
'ref': date.strftime('%B %Y'),
|
||||||
|
'journal_id': journal.id,
|
||||||
|
'date': date,
|
||||||
|
}
|
||||||
|
|
||||||
|
move_dict['narration'] += slip.number or '' + ' - ' + slip.employee_id.name or ''
|
||||||
|
move_dict['narration'] += '\n'
|
||||||
|
slip._process_journal_lines_grouped(line_ids, date, precision)
|
||||||
|
|
||||||
|
for line_id in line_ids: # Get the debit and credit sum.
|
||||||
|
debit_sum += line_id['debit']
|
||||||
|
credit_sum += line_id['credit']
|
||||||
|
|
||||||
|
# The code below is called if there is an error in the balance between credit and debit sum.
|
||||||
|
if float_compare(credit_sum, debit_sum, precision_digits=precision) == -1:
|
||||||
|
acc_id = slip.journal_id.default_credit_account_id.id
|
||||||
|
if not acc_id:
|
||||||
|
raise UserError(
|
||||||
|
_('The Expense Journal "%s" has not properly configured the Credit Account!') % (
|
||||||
|
slip.journal_id.name))
|
||||||
|
existing_adjustment_line = (
|
||||||
|
line_id for line_id in line_ids if line_id['name'] == _('Adjustment Entry')
|
||||||
|
)
|
||||||
|
adjust_credit = next(existing_adjustment_line, False)
|
||||||
|
|
||||||
|
if not adjust_credit:
|
||||||
|
adjust_credit = {
|
||||||
|
'name': _('Adjustment Entry'),
|
||||||
|
'partner_id': False,
|
||||||
|
'account_id': acc_id,
|
||||||
|
'journal_id': slip.journal_id.id,
|
||||||
|
'date': date,
|
||||||
|
'debit': 0.0,
|
||||||
|
'credit': debit_sum - credit_sum,
|
||||||
|
}
|
||||||
|
line_ids.append(adjust_credit)
|
||||||
|
else:
|
||||||
|
adjust_credit['credit'] = debit_sum - credit_sum
|
||||||
|
|
||||||
|
elif float_compare(debit_sum, credit_sum, precision_digits=precision) == -1:
|
||||||
|
acc_id = slip.journal_id.default_debit_account_id.id
|
||||||
|
if not acc_id:
|
||||||
|
raise UserError(_('The Expense Journal "%s" has not properly configured the Debit Account!') % (
|
||||||
|
slip.journal_id.name))
|
||||||
|
existing_adjustment_line = (
|
||||||
|
line_id for line_id in line_ids if line_id['name'] == _('Adjustment Entry')
|
||||||
|
)
|
||||||
|
adjust_debit = next(existing_adjustment_line, False)
|
||||||
|
|
||||||
|
if not adjust_debit:
|
||||||
|
adjust_debit = {
|
||||||
|
'name': _('Adjustment Entry'),
|
||||||
|
'partner_id': False,
|
||||||
|
'account_id': acc_id,
|
||||||
|
'journal_id': slip.journal_id.id,
|
||||||
|
'date': date,
|
||||||
|
'debit': credit_sum - debit_sum,
|
||||||
|
'credit': 0.0,
|
||||||
|
}
|
||||||
|
line_ids.append(adjust_debit)
|
||||||
|
else:
|
||||||
|
adjust_debit['debit'] = credit_sum - debit_sum
|
||||||
|
|
||||||
|
# Add accounting lines in the move
|
||||||
|
move_dict['line_ids'] = [(0, 0, line_vals) for line_vals in line_ids]
|
||||||
|
move = self.env['account.move'].create(move_dict)
|
||||||
|
slip.write({'move_id': move.id, 'date': date})
|
||||||
|
|
||||||
|
def _generate_move_original(self, slip_mapped_data, journal, slip_date):
|
||||||
|
"""
|
||||||
|
Odoo's original version.
|
||||||
|
Fixed bug with 'matching' credit line
|
||||||
|
"""
|
||||||
|
precision = self.env['decimal.precision'].precision_get('Payroll')
|
||||||
|
line_ids = []
|
||||||
|
debit_sum = 0.0
|
||||||
|
credit_sum = 0.0
|
||||||
|
date = slip_date
|
||||||
|
move_dict = {
|
||||||
|
'narration': '',
|
||||||
|
'ref': date.strftime('%B %Y'),
|
||||||
|
'journal_id': journal.id,
|
||||||
|
'date': date,
|
||||||
|
}
|
||||||
|
|
||||||
|
for slip in slip_mapped_data[journal][slip_date]:
|
||||||
|
move_dict['narration'] += slip.number or '' + ' - ' + slip.employee_id.name or ''
|
||||||
|
move_dict['narration'] += '\n'
|
||||||
|
for line in slip.line_ids.filtered(lambda l: l.category_id):
|
||||||
|
amount = -line.total if slip.credit_note else line.total
|
||||||
|
if line.code == 'NET': # Check if the line is the 'Net Salary'.
|
||||||
|
for tmp_line in slip.line_ids.filtered(lambda l: l.category_id):
|
||||||
|
if tmp_line.salary_rule_id.not_computed_in_net: # Check if the rule must be computed in the 'Net Salary' or not.
|
||||||
|
if amount > 0:
|
||||||
|
amount -= abs(tmp_line.total)
|
||||||
|
elif amount < 0:
|
||||||
|
amount += abs(tmp_line.total)
|
||||||
|
if float_is_zero(amount, precision_digits=precision):
|
||||||
|
continue
|
||||||
|
|
||||||
|
debit_account_id = line.salary_rule_id.account_debit.id
|
||||||
|
credit_account_id = line.salary_rule_id.account_credit.id
|
||||||
|
|
||||||
|
if debit_account_id: # If the rule has a debit account.
|
||||||
|
debit = amount if amount > 0.0 else 0.0
|
||||||
|
credit = -amount if amount < 0.0 else 0.0
|
||||||
|
|
||||||
|
existing_debit_lines = (
|
||||||
|
line_id for line_id in line_ids if
|
||||||
|
line_id['name'] == line.name
|
||||||
|
and line_id['account_id'] == debit_account_id
|
||||||
|
and ((line_id['debit'] > 0 and credit <= 0) or (line_id['credit'] > 0 and debit <= 0)))
|
||||||
|
debit_line = next(existing_debit_lines, False)
|
||||||
|
|
||||||
|
if not debit_line:
|
||||||
|
debit_line = {
|
||||||
|
'name': line.name,
|
||||||
|
'partner_id': False,
|
||||||
|
'account_id': debit_account_id,
|
||||||
|
'journal_id': slip.struct_id.journal_id.id,
|
||||||
|
'date': date,
|
||||||
|
'debit': debit,
|
||||||
|
'credit': credit,
|
||||||
|
'analytic_account_id': line.salary_rule_id.analytic_account_id.id or slip.contract_id.analytic_account_id.id,
|
||||||
|
}
|
||||||
|
line_ids.append(debit_line)
|
||||||
|
else:
|
||||||
|
debit_line['debit'] += debit
|
||||||
|
debit_line['credit'] += credit
|
||||||
|
|
||||||
|
if credit_account_id: # If the rule has a credit account.
|
||||||
|
debit = -amount if amount < 0.0 else 0.0
|
||||||
|
credit = amount if amount > 0.0 else 0.0
|
||||||
|
existing_credit_line = (
|
||||||
|
line_id for line_id in line_ids if
|
||||||
|
line_id['name'] == line.name
|
||||||
|
and line_id['account_id'] == credit_account_id
|
||||||
|
and ((line_id['debit'] > 0 and credit <= 0) or (line_id['credit'] > 0 and debit <= 0))
|
||||||
|
)
|
||||||
|
credit_line = next(existing_credit_line, False)
|
||||||
|
|
||||||
|
if not credit_line:
|
||||||
|
credit_line = {
|
||||||
|
'name': line.name,
|
||||||
|
'partner_id': False,
|
||||||
|
'account_id': credit_account_id,
|
||||||
|
'journal_id': slip.struct_id.journal_id.id,
|
||||||
|
'date': date,
|
||||||
|
'debit': debit,
|
||||||
|
'credit': credit,
|
||||||
|
'analytic_account_id': line.salary_rule_id.analytic_account_id.id or slip.contract_id.analytic_account_id.id,
|
||||||
|
}
|
||||||
|
line_ids.append(credit_line)
|
||||||
|
else:
|
||||||
|
credit_line['debit'] += debit
|
||||||
|
credit_line['credit'] += credit
|
||||||
|
|
||||||
|
for line_id in line_ids: # Get the debit and credit sum.
|
||||||
|
debit_sum += line_id['debit']
|
||||||
|
credit_sum += line_id['credit']
|
||||||
|
|
||||||
|
# The code below is called if there is an error in the balance between credit and debit sum.
|
||||||
|
if float_compare(credit_sum, debit_sum, precision_digits=precision) == -1:
|
||||||
|
acc_id = slip.journal_id.default_credit_account_id.id
|
||||||
|
if not acc_id:
|
||||||
|
raise UserError(
|
||||||
|
_('The Expense Journal "%s" has not properly configured the Credit Account!') % (
|
||||||
|
slip.journal_id.name))
|
||||||
|
existing_adjustment_line = (
|
||||||
|
line_id for line_id in line_ids if line_id['name'] == _('Adjustment Entry')
|
||||||
|
)
|
||||||
|
adjust_credit = next(existing_adjustment_line, False)
|
||||||
|
|
||||||
|
if not adjust_credit:
|
||||||
|
adjust_credit = {
|
||||||
|
'name': _('Adjustment Entry'),
|
||||||
|
'partner_id': False,
|
||||||
|
'account_id': acc_id,
|
||||||
|
'journal_id': slip.journal_id.id,
|
||||||
|
'date': date,
|
||||||
|
'debit': 0.0,
|
||||||
|
'credit': debit_sum - credit_sum,
|
||||||
|
}
|
||||||
|
line_ids.append(adjust_credit)
|
||||||
|
else:
|
||||||
|
adjust_credit['credit'] = debit_sum - credit_sum
|
||||||
|
|
||||||
|
elif float_compare(debit_sum, credit_sum, precision_digits=precision) == -1:
|
||||||
|
acc_id = slip.journal_id.default_debit_account_id.id
|
||||||
|
if not acc_id:
|
||||||
|
raise UserError(_('The Expense Journal "%s" has not properly configured the Debit Account!') % (
|
||||||
|
slip.journal_id.name))
|
||||||
|
existing_adjustment_line = (
|
||||||
|
line_id for line_id in line_ids if line_id['name'] == _('Adjustment Entry')
|
||||||
|
)
|
||||||
|
adjust_debit = next(existing_adjustment_line, False)
|
||||||
|
|
||||||
|
if not adjust_debit:
|
||||||
|
adjust_debit = {
|
||||||
|
'name': _('Adjustment Entry'),
|
||||||
|
'partner_id': False,
|
||||||
|
'account_id': acc_id,
|
||||||
|
'journal_id': slip.journal_id.id,
|
||||||
|
'date': date,
|
||||||
|
'debit': credit_sum - debit_sum,
|
||||||
|
'credit': 0.0,
|
||||||
|
}
|
||||||
|
line_ids.append(adjust_debit)
|
||||||
|
else:
|
||||||
|
adjust_debit['debit'] = credit_sum - debit_sum
|
||||||
|
|
||||||
|
# Add accounting lines in the move
|
||||||
|
move_dict['line_ids'] = [(0, 0, line_vals) for line_vals in line_ids]
|
||||||
|
move = self.env['account.move'].create(move_dict)
|
||||||
|
for slip in slip_mapped_data[journal][slip_date]:
|
||||||
|
slip.write({'move_id': move.id, 'date': date})
|
||||||
9
hr_payroll_payment/models/hr_payslip_patch.py
Normal file
9
hr_payroll_payment/models/hr_payslip_patch.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from odoo.addons.hr_payroll_account.models.hr_payroll_account import HrPayslip
|
||||||
|
|
||||||
|
# this is patched because this method is replaced in our implementation (and overridable via _generate_move())
|
||||||
|
def action_payslip_done(self):
|
||||||
|
return super(HrPayslip, self).action_payslip_done()
|
||||||
|
|
||||||
|
HrPayslip.action_payslip_done = action_payslip_done
|
||||||
BIN
hr_payroll_payment/static/description/icon.png
Normal file
BIN
hr_payroll_payment/static/description/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.8 KiB |
3
hr_payroll_payment/tests/__init__.py
Normal file
3
hr_payroll_payment/tests/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from . import test_hr_payroll_account
|
||||||
44
hr_payroll_payment/tests/test_hr_payroll_account.py
Normal file
44
hr_payroll_payment/tests/test_hr_payroll_account.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||||
|
|
||||||
|
|
||||||
|
import odoo.tests
|
||||||
|
from odoo.addons.hr_payroll_account.tests.test_hr_payroll_account import TestHrPayrollAccount as TestBase
|
||||||
|
|
||||||
|
@odoo.tests.tagged('post_install', '-at_install')
|
||||||
|
class TestHrPayrollAccount(TestBase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
# Two employees, but in stock tests they share the same partner...
|
||||||
|
self.hr_employee_mark.address_home_id = self.env['res.partner'].create({
|
||||||
|
'name': 'employee_mark',
|
||||||
|
})
|
||||||
|
# This rule has a partner, and is the only one with any accounting side effects.
|
||||||
|
# Remove partner to use the home address...
|
||||||
|
rule = self.env.ref('hr_payroll.hr_salary_rule_houserentallowance1')
|
||||||
|
rule.partner_id = False
|
||||||
|
|
||||||
|
def test_00_hr_payslip_run(self):
|
||||||
|
# Original method groups but has no partners.
|
||||||
|
self.account_journal.payroll_entry_type = 'original'
|
||||||
|
super().test_00_hr_payslip_run()
|
||||||
|
self.assertEqual(len(self.payslip_run.slip_ids), 3)
|
||||||
|
self.assertEqual(len(self.payslip_run.slip_ids.mapped('move_id')), 1)
|
||||||
|
self.assertEqual(len(self.payslip_run.slip_ids.mapped('move_id.line_ids.partner_id')), 0)
|
||||||
|
|
||||||
|
def test_01_hr_payslip_run(self):
|
||||||
|
# Grouped method groups but has partners.
|
||||||
|
self.account_journal.payroll_entry_type = 'grouped'
|
||||||
|
super().test_01_hr_payslip_run()
|
||||||
|
self.assertEqual(len(self.payslip_run.slip_ids), 3)
|
||||||
|
self.assertEqual(len(self.payslip_run.slip_ids.mapped('move_id')), 1)
|
||||||
|
self.assertEqual(len(self.payslip_run.slip_ids.mapped('move_id.line_ids.partner_id')), 2)
|
||||||
|
|
||||||
|
def test_01_2_hr_payslip_run(self):
|
||||||
|
# Payslip method makes an entry per payslip
|
||||||
|
self.account_journal.payroll_entry_type = 'slip'
|
||||||
|
# Call 'other' implementation.
|
||||||
|
super().test_01_hr_payslip_run()
|
||||||
|
self.assertEqual(len(self.payslip_run.slip_ids), 3)
|
||||||
|
self.assertEqual(len(self.payslip_run.slip_ids.mapped('move_id')), 3)
|
||||||
|
self.assertEqual(len(self.payslip_run.slip_ids.mapped('move_id.line_ids.partner_id')), 2)
|
||||||
18
hr_payroll_payment/views/account_views.xml
Normal file
18
hr_payroll_payment/views/account_views.xml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_account_journal_form_inherit" model="ir.ui.view">
|
||||||
|
<field name="name">account.journal.form.inherit</field>
|
||||||
|
<field name="model">account.journal</field>
|
||||||
|
<field name="inherit_id" ref="account.view_account_journal_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//page[@name='advanced_settings']/group" position="inside">
|
||||||
|
<group name="payroll_payment" string="Payroll Payments">
|
||||||
|
<field name="payroll_entry_type"/>
|
||||||
|
<field name="payroll_payment_journal_id" domain="[('type', 'in', ('bank', 'cash'))]"/>
|
||||||
|
<field name="payroll_payment_method_id" domain="[('payment_type', '=', 'outbound')]"/>
|
||||||
|
<field name="payroll_payment_method_refund_id" domain="[('payment_type', '=', 'inbound')]"/>
|
||||||
|
</group>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
63
hr_payroll_payment/views/hr_payslip_views.xml
Normal file
63
hr_payroll_payment/views/hr_payslip_views.xml
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<odoo>
|
||||||
|
<!-- Add Filter for "Needs Payment" -->
|
||||||
|
<record id="payslip_filter_payment" model="ir.ui.view">
|
||||||
|
<field name="name">hr.payslip.select.payment</field>
|
||||||
|
<field name="model">hr.payslip</field>
|
||||||
|
<field name="inherit_id" ref="hr_payroll.view_hr_payslip_filter"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//filter[last()]" position="after">
|
||||||
|
<filter name="filter_needs_payment" string="Needs Payment" domain="[('is_paid', '=', False)]"
|
||||||
|
help="Needs Payment or Reconciliation"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="payslip_run_filter_payment" model="ir.ui.view">
|
||||||
|
<field name="name">hr.payslip.run.select.payment</field>
|
||||||
|
<field name="model">hr.payslip.run</field>
|
||||||
|
<field name="inherit_id" ref="hr_payroll.hr_payslip_run_filter"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//filter[last()]" position="after">
|
||||||
|
<filter name="filter_needs_payment" string="Needs Payment" domain="[('is_paid', '=', False)]"
|
||||||
|
help="Slips needs Payment or Reconciliation"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Button on Payslip to launch Wizard for register payment -->
|
||||||
|
<record id="hr_payslip_form_payment" model="ir.ui.view">
|
||||||
|
<field name="name">hr.payslip.form.payment</field>
|
||||||
|
<field name="model">hr.payslip</field>
|
||||||
|
<field name="inherit_id" ref="hr_payroll.view_hr_payslip_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//button[@name='refund_sheet']" position="before">
|
||||||
|
<button name="action_register_payment" type="object" string="Register Payment" class="oe_highlight"
|
||||||
|
attrs="{'invisible': ['|', ('state', '!=', 'done'), ('is_paid', '=', True)]}"
|
||||||
|
groups="account.group_account_user"/>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//field[@name='credit_note']" position="after">
|
||||||
|
<field name="is_paid" readonly="1"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="hr_payslip_run_form_payment" model="ir.ui.view">
|
||||||
|
<field name="name">hr.payslip.run.form.payment</field>
|
||||||
|
<field name="model">hr.payslip.run</field>
|
||||||
|
<field name="inherit_id" ref="hr_payroll.hr_payslip_run_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//button[@name='action_validate']" position="after">
|
||||||
|
<button name="action_register_payment" type="object" string="Register Payment" class="oe_highlight"
|
||||||
|
attrs="{'invisible': ['|', ('state', '!=', 'close'), ('is_paid', '=', True)]}"
|
||||||
|
groups="account.group_account_user"/>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//field[@name='credit_note']" position="after">
|
||||||
|
<field name="is_paid" readonly="1"/>
|
||||||
|
<field name="date"/>
|
||||||
|
<field name="batch_payment_id" attrs="{'invisible': [('batch_payment_id', '=', False)]}"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
3
hr_payroll_payment/wizard/__init__.py
Normal file
3
hr_payroll_payment/wizard/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from . import hr_payroll_payslips_by_employees
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from odoo import api, models
|
||||||
|
|
||||||
|
|
||||||
|
class HrPayslipEmployees(models.TransientModel):
|
||||||
|
_inherit = 'hr.payslip.employees'
|
||||||
|
|
||||||
|
def compute_sheet(self):
|
||||||
|
date = False
|
||||||
|
if self.env.context.get('active_id'):
|
||||||
|
date = self.env['hr.payslip.run'].browse(self.env.context.get('active_id')).date
|
||||||
|
return super(HrPayslipEmployees, self.with_context(date=date)).compute_sheet()
|
||||||
Reference in New Issue
Block a user