[REM] hr_payroll_payment: available in professional

H14528
This commit is contained in:
Mayank Patel
2024-09-11 05:47:51 +00:00
parent eb02b8e6b9
commit 9053e1677c
15 changed files with 0 additions and 1177 deletions

View File

@@ -1,27 +0,0 @@
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.
Adds fiscal position mappings to set a fiscal position on the contract and have payslips map their accounts.
Tested
------
Passes original Payroll Accounting tests and additional ones for gouping behavior.

View File

@@ -1,4 +0,0 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from . import models
from . import wizard

View File

@@ -1,52 +0,0 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
{
'name': 'Payroll Payments',
'author': 'Hibou Corp. <hello@hibou.io>',
'version': '15.0.1.0.0',
'category': 'Human Resources',
'sequence': 95,
'summary': 'Register payments for Payroll Payslips',
'description': """
Pay your Payroll
================
Hibou's Payroll Payments modifies, and abstracts, the way that the accounting for payslips is generated.
In stock Odoo 15, 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.
Adds fiscal position mappings to set a fiscal position on the contract and have payslips map their accounts.
Tested
------
Passes original Payroll Accounting tests and additional ones for gouping behavior.
""",
'website': 'https://hibou.io/',
'depends': [
'hr_payroll_account',
'account_batch_payment',
'hibou_professional',
],
'data': [
'views/account_views.xml',
'views/hr_payslip_views.xml',
],
'installable': True,
'application': False,
'license': 'OPL-1',
}

View File

@@ -1,222 +0,0 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * hr_payroll_payment
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 15.0+e\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-10-12 01:27+0000\n"
"PO-Revision-Date: 2021-10-12 01:27+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: hr_payroll_payment
#: code:addons/hr_payroll_payment/models/hr_payslip.py:0
#: code:addons/hr_payroll_payment/models/hr_payslip.py:0
#: code:addons/hr_payroll_payment/models/hr_payslip.py:0
#: code:addons/hr_payroll_payment/models/hr_payslip.py:0
#: code:addons/hr_payroll_payment/models/hr_payslip.py:0
#: code:addons/hr_payroll_payment/models/hr_payslip.py:0
#: code:addons/hr_payroll_payment/models/hr_payslip.py:0
#: code:addons/hr_payroll_payment/models/hr_payslip.py:0
#: code:addons/hr_payroll_payment/models/hr_payslip.py:0
#: code:addons/hr_payroll_payment/models/hr_payslip.py:0
#: code:addons/hr_payroll_payment/models/hr_payslip.py:0
#: code:addons/hr_payroll_payment/models/hr_payslip.py:0
#, python-format
msgid "Adjustment Entry"
msgstr "Entrada de Ajuste"
#. module: hr_payroll_payment
#: model:ir.model.fields,field_description:hr_payroll_payment.field_hr_payslip_run__date
msgid "Date Account"
msgstr "Cuenta de Fecha"
#. module: hr_payroll_payment
#: model_terms:ir.ui.view,arch_db:hr_payroll_payment.view_account_journal_form_inherit
msgid "Default Account"
msgstr "Cuenta Predeterminada"
#. module: hr_payroll_payment
#: model:ir.model,name:hr_payroll_payment.model_hr_contract
msgid "Employee Contract"
msgstr "Contrato del empleado"
#. module: hr_payroll_payment
#: code:addons/hr_payroll_payment/models/hr_payslip.py:0
#, python-format
msgid ""
"Employee's private address account payable not the same for all addresses."
msgstr "La cuenta a pagar de la dirección privada del empleado no es la misma para todas las direcciones."
#. module: hr_payroll_payment
#: model:ir.model,name:hr_payroll_payment.model_hr_payslip_employees
msgid "Generate payslips for all selected employees"
msgstr "Generar los recibos de nómina para todos los empleados seleccionados"
#. module: hr_payroll_payment
#: model:ir.model.fields.selection,name:hr_payroll_payment.selection__account_journal__payroll_entry_type__grouped
msgid "Grouped"
msgstr "Agrupado"
#. module: hr_payroll_payment
#: model:ir.model.fields,help:hr_payroll_payment.field_account_bank_statement_import_journal_creation__payroll_entry_type
#: model:ir.model.fields,help:hr_payroll_payment.field_account_journal__payroll_entry_type
msgid ""
"Grouped and Payslip will add partner info and group by account and partner. "
"Payslip will generate a journal entry for every payslip."
msgstr "El recibo de nómina generará un asiento contable para cada recibo de nómina"
#. module: hr_payroll_payment
#: model:ir.model.fields,field_description:hr_payroll_payment.field_hr_payslip__is_paid
msgid "Has been Paid"
msgstr "Ha sido Pagado"
#. module: hr_payroll_payment
#: model:ir.model,name:hr_payroll_payment.model_account_journal
msgid "Journal"
msgstr "Diario"
#. module: hr_payroll_payment
#: model:ir.model.fields,help:hr_payroll_payment.field_hr_payslip_run__date
msgid "Keep empty to use the period of the validation(Payslip) date."
msgstr "Mantener vacio para utilizar el periodo de la fecha de validación (recibo de nómina)"
#. module: hr_payroll_payment
#: model_terms:ir.ui.view,arch_db:hr_payroll_payment.payslip_filter_payment
#: model_terms:ir.ui.view,arch_db:hr_payroll_payment.payslip_run_filter_payment
msgid "Needs Payment"
msgstr "Requiere Pagar"
#. module: hr_payroll_payment
#: model_terms:ir.ui.view,arch_db:hr_payroll_payment.payslip_filter_payment
msgid "Needs Payment or Reconciliation"
msgstr "Requiere pagar o reconciliar"
#. module: hr_payroll_payment
#: code:addons/hr_payroll_payment/models/hr_payslip.py:0
#, python-format
msgid "One of the contract for these payslips has no structure type."
msgstr "Uno de los contratos para estas recibos de nómina no tiene ningún tipo de estructura"
#. module: hr_payroll_payment
#: code:addons/hr_payroll_payment/models/hr_payslip.py:0
#, python-format
msgid "One of the payroll structures has no account journal defined on it."
msgstr "Uno de las estructuras de nomina no tienen un diario de cuenta definido"
#. module: hr_payroll_payment
#: model:ir.model.fields.selection,name:hr_payroll_payment.selection__account_journal__payroll_entry_type__original
msgid "Original"
msgstr "Original"
#. module: hr_payroll_payment
#: model:ir.model,name:hr_payroll_payment.model_hr_payslip
msgid "Pay Slip"
msgstr "Recibo de nómina"
#. module: hr_payroll_payment
#: model:ir.model.fields,field_description:hr_payroll_payment.field_hr_payslip_run__batch_payment_id
msgid "Payment Batch"
msgstr "Lote de Pagos"
#. module: hr_payroll_payment
#: model:ir.model.fields,field_description:hr_payroll_payment.field_account_bank_statement_import_journal_creation__payroll_entry_type
#: model:ir.model.fields,field_description:hr_payroll_payment.field_account_journal__payroll_entry_type
msgid "Payroll Entry Type"
msgstr "Tipo de entrada de nómina"
#. module: hr_payroll_payment
#: model:ir.model.fields,field_description:hr_payroll_payment.field_hr_contract__payroll_fiscal_position_id
msgid "Payroll Fiscal Position"
msgstr "Posición Fiscal de la Nómina"
#. module: hr_payroll_payment
#: model:ir.model.fields,field_description:hr_payroll_payment.field_account_bank_statement_import_journal_creation__payroll_payment_journal_id
#: model:ir.model.fields,field_description:hr_payroll_payment.field_account_journal__payroll_payment_journal_id
msgid "Payroll Payment Journal"
msgstr "Diario de Pago de la Nómina"
#. module: hr_payroll_payment
#: model:ir.model.fields,field_description:hr_payroll_payment.field_account_bank_statement_import_journal_creation__payroll_payment_method_id
#: model:ir.model.fields,field_description:hr_payroll_payment.field_account_journal__payroll_payment_method_id
msgid "Payroll Payment Method"
msgstr "Método de pago de la nómina"
#. module: hr_payroll_payment
#: code:addons/hr_payroll_payment/models/hr_payslip.py:0
#, python-format
msgid ""
"Payroll Payment journal not configured on the existing entry's journal."
msgstr "El diario de pago de la nómina no ha sido configurado en algún diario de una entrada existente"
#. module: hr_payroll_payment
#: code:addons/hr_payroll_payment/models/hr_payslip.py:0
#, python-format
msgid "Payroll Payment method not configured on the existing entry's journal."
msgstr "El método de pago de la nómina no ha sido configurado en algún diario de una entrada existente"
#. module: hr_payroll_payment
#: model_terms:ir.ui.view,arch_db:hr_payroll_payment.view_account_journal_form_inherit
msgid "Payroll Payments"
msgstr "Pagos de Nómina"
#. module: hr_payroll_payment
#: model:ir.model.fields,field_description:hr_payroll_payment.field_account_bank_statement_import_journal_creation__payroll_payment_method_refund_id
#: model:ir.model.fields,field_description:hr_payroll_payment.field_account_journal__payroll_payment_method_refund_id
msgid "Payroll Refund Method"
msgstr "Método de Reembolso de la Nómina"
#. module: hr_payroll_payment
#: model:ir.model.fields.selection,name:hr_payroll_payment.selection__account_journal__payroll_entry_type__slip
msgid "Payslip"
msgstr "Recibo de Nómina"
#. module: hr_payroll_payment
#: model:ir.model,name:hr_payroll_payment.model_hr_payslip_run
msgid "Payslip Batches"
msgstr "Lotes de recibos de nómina"
#. module: hr_payroll_payment
#: model:ir.model.fields,field_description:hr_payroll_payment.field_hr_payslip_run__is_paid
msgid "Payslips Paid"
msgstr "Recibos de nóminas pagadas"
#. module: hr_payroll_payment
#: model_terms:ir.ui.view,arch_db:hr_payroll_payment.hr_payslip_form_payment
#: model_terms:ir.ui.view,arch_db:hr_payroll_payment.hr_payslip_run_form_payment
msgid "Register Payment"
msgstr "Registrar Pago"
#. module: hr_payroll_payment
#: model_terms:ir.ui.view,arch_db:hr_payroll_payment.payslip_run_filter_payment
msgid "Slips needs Payment or Reconciliation"
msgstr "Recibos de nómina requieren ser pagadas o reconciliadas"
#. module: hr_payroll_payment
#: code:addons/hr_payroll_payment/models/hr_payslip.py:0
#: code:addons/hr_payroll_payment/models/hr_payslip.py:0
#: code:addons/hr_payroll_payment/models/hr_payslip.py:0
#: code:addons/hr_payroll_payment/models/hr_payslip.py:0
#: code:addons/hr_payroll_payment/models/hr_payslip.py:0
#: code:addons/hr_payroll_payment/models/hr_payslip.py:0
#, python-format
msgid ""
"The Expense Journal \"%s\" has not properly configured the Default Account!"
msgstr "¡El diario de gastos \"%s\" no ha configurado apropiadamente la cuenta predeterminada!"
#. module: hr_payroll_payment
#: code:addons/hr_payroll_payment/models/hr_payslip.py:0
#, python-format
msgid "The following employees are missing private addresses. %s"
msgstr "A los siguientes empleados les faltan direcciones privadas. %s"
#. module: hr_payroll_payment
#: model:ir.model.fields,help:hr_payroll_payment.field_hr_contract__payroll_fiscal_position_id
msgid "Used for mapping accounts when processing payslip journal entries."
msgstr "Se utilizará para asignar cuentas cuando se procesa asientos contables del diario de nominas"

View File

@@ -1,5 +0,0 @@
# 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

View File

@@ -1,19 +0,0 @@
# 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')

View File

@@ -1,599 +0,0 @@
# 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 HrContract(models.Model):
_inherit = 'hr.contract'
payroll_fiscal_position_id = fields.Many2one('account.fiscal.position', 'Payroll Fiscal Position', domain="[('company_id', '=', company_id)]",
help="Used for mapping accounts when processing payslip journal entries.")
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):
method = self.move_id.journal_id.payroll_payment_method_id
if amount > 0.0:
method = self.move_id.journal_id.payroll_payment_method_refund_id
method_line = self.move_id.journal_id.payroll_payment_journal_id.outbound_payment_method_line_ids.filtered(
lambda l: l.payment_method_id == method
)
values = {
'payment_reference': self.number,
'ref': self.number + ' - ' + self.name,
'journal_id': self.move_id.journal_id.payroll_payment_journal_id.id,
'payment_method_line_id': method_line.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,
})
return values
def action_register_payment(self):
slips = self.filtered(lambda s: s.move_id.state in ('draft', 'posted') and not s.is_paid)
if not all(slip.move_id.journal_id.payroll_payment_journal_id for slip in slips):
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 slips):
raise UserError(_('Payroll Payment method not configured on the existing entry\'s journal.'))
# as of 14, you cannot reconcile to un-posted moves
# so if you are paying it, we must assume you want to post any draft entries
slip_moves = slips.mapped('move_id')
unposted_moves = slip_moves.filtered(lambda m: m.state == 'draft')
unposted_moves._post(soft=False)
payments = self.env['account.payment']
for slip in slips:
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'))
if not amount:
continue
payment_values = slip._payment_values(amount)
payment = payments.create(payment_values)
payment.action_post()
lines_paid = payment.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.
# Hibou Customization: prefer slip's `date` over end of month
# Hibou Customization: add an account mapping based on fiscal position
slip_mapped_data = {
slip.struct_id.journal_id: {slip.date or 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][slip.date or fields.Date().end_of(slip.date_to, 'month')] |= slip
account_map = payslips_to_post._generate_account_map()
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, account_map)
else:
self._generate_move_original(slip_mapped_data, journal, slip_date, account_map)
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 _generate_account_map(self):
account_map = {}
rules = self.mapped('line_ids.salary_rule_id')
base_account_map = {a: a for a in (rules.mapped('account_debit') | rules.mapped('account_credit'))}
account_map[self.env['account.fiscal.position']] = base_account_map
fiscal_positions = self.mapped('contract_id.payroll_fiscal_position_id')
for fp in fiscal_positions:
account_map[fp] = fp.map_accounts(base_account_map.copy())
return account_map
def _process_journal_lines_grouped(self, line_ids, date, precision, account_map):
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 = line.salary_rule_id.account_debit
debit_account_id = account_map[debit_account].id if debit_account else False
credit_account = line.salary_rule_id.account_credit
credit_account_id = account_map[credit_account].id if credit_account else False
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, account_map):
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]:
slip_accounts = account_map[slip.contract_id.payroll_fiscal_position_id]
move_dict['narration'] += slip.number or '' + ' - ' + slip.employee_id.name or ''
move_dict['narration'] += '\n'
slip._process_journal_lines_grouped(line_ids, date, precision, slip_accounts)
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_account_id.id
if not acc_id:
raise UserError(
_('The Expense Journal "%s" has not properly configured the Default 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_account_id.id
if not acc_id:
raise UserError(_('The Expense Journal "%s" has not properly configured the Default 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, account_map):
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]:
slip_accounts = account_map[slip.contract_id.payroll_fiscal_position_id]
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, slip_accounts)
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_account_id.id
if not acc_id:
raise UserError(
_('The Expense Journal "%s" has not properly configured the Default 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_account_id.id
if not acc_id:
raise UserError(_('The Expense Journal "%s" has not properly configured the Default 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, account_map):
"""
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]:
slip_accounts = account_map[slip.contract_id.payroll_fiscal_position_id]
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 = line.salary_rule_id.account_debit
debit_account_id = slip_accounts[debit_account].id if debit_account else False
credit_account = line.salary_rule_id.account_credit
credit_account_id = slip_accounts[credit_account].id if credit_account else False
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_account_id.id
if not acc_id:
raise UserError(
_('The Expense Journal "%s" has not properly configured the Default 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_account_id.id
if not acc_id:
raise UserError(_('The Expense Journal "%s" has not properly configured the Default 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})

View File

@@ -1,9 +0,0 @@
# 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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

View File

@@ -1,3 +0,0 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from . import test_hr_payroll_account

View File

@@ -1,125 +0,0 @@
# 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()
# upstream code no-longer sets the journal, though it does create it....
self.hr_structure_softwaredeveloper.journal_id = self.account_journal
# upstream code no-longer has any accounts (just makes journal entries without any lines)
demo_account = self.env.ref('hr_payroll_account.demo_account')
self.hr_structure_softwaredeveloper.rule_ids.filtered(lambda r: r.code == 'HRA').account_debit = demo_account
# Need a default account as there will be adjustment lines equal and opposite to the above PT rule...
self.account_journal.default_account_id = demo_account
# 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...
self.rule = self.hr_structure_softwaredeveloper.rule_ids.filtered(lambda r: r.code == 'HRA')
self.rule.partner_id = False
# configure journal to be able to make payments
ap = self.hr_employee_mark.address_home_id.property_account_payable_id
self.assertTrue(ap)
# note there is no NET rule, so I just use a random allowance with fixed 800.0 amount
net_rule = self.hr_structure_softwaredeveloper.rule_ids.filtered(lambda r: r.code == 'CA')
self.assertTrue(net_rule)
net_rule.account_credit = ap
bank_journal = self.env['account.journal'].search([('type', '=', 'bank')], limit=1)
self.account_journal.payroll_payment_journal_id = bank_journal
self.account_journal.payroll_payment_method_id = bank_journal.outbound_payment_method_line_ids[0].payment_method_id
def _setup_fiscal_position(self):
account_rule_debit = self.rule.account_debit
self.assertTrue(account_rule_debit)
account_other = self.env['account.account'].search([('id', '!=', account_rule_debit.id)], limit=1)
self.assertTrue(account_other)
fp = self.env['account.fiscal.position'].create({
'name': 'Salary Remap 1',
'account_ids': [(0, 0, {
'account_src_id': account_rule_debit.id,
'account_dest_id': account_other.id,
})]
})
self.hr_contract_john.payroll_fiscal_position_id = fp
def _setup_fiscal_position_empty(self):
self._setup_fiscal_position()
self.hr_contract_john.payroll_fiscal_position_id.write({'account_ids': [(5, 0, 0)]})
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), 2)
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_00_fiscal_position(self):
self._setup_fiscal_position()
self.test_00_hr_payslip_run()
self.assertEqual(len(self.payslip_run.slip_ids.mapped('move_id.line_ids.account_id')), 3)
def test_00_fiscal_position_empty(self):
self._setup_fiscal_position_empty()
self.test_00_hr_payslip_run()
self.assertEqual(len(self.payslip_run.slip_ids.mapped('move_id.line_ids.account_id')), 2)
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), 2)
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)
# what is going on with the 3rd one?!
slips_to_pay = self.payslip_run.slip_ids
action = slips_to_pay.action_register_payment()
payment_ids = action['res_ids']
self.assertEqual(len(payment_ids), 2)
def test_01_fiscal_position(self):
self._setup_fiscal_position()
self.test_01_hr_payslip_run()
self.assertEqual(len(self.payslip_run.slip_ids.mapped('move_id.line_ids.account_id')), 3)
def test_01_fiscal_position_empty(self):
self._setup_fiscal_position_empty()
self.test_01_hr_payslip_run()
self.assertEqual(len(self.payslip_run.slip_ids.mapped('move_id.line_ids.account_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), 2)
self.assertEqual(len(self.payslip_run.slip_ids.mapped('move_id')), 2)
self.assertEqual(len(self.payslip_run.slip_ids.mapped('move_id.line_ids.partner_id')), 2)
slips_to_pay = self.payslip_run.slip_ids
# what is going on with the 3rd one?!
# it is possible to filter it out, but it doesn't change it
self.assertEqual(len(slips_to_pay), 2)
action = slips_to_pay.action_register_payment()
payment_ids = action['res_ids']
self.assertEqual(len(payment_ids), 2)
def test_01_2_fiscal_position(self):
self._setup_fiscal_position()
self.test_01_2_hr_payslip_run()
self.assertEqual(len(self.payslip_run.slip_ids.mapped('move_id.line_ids.account_id')), 3)
def test_01_2_fiscal_position_empty(self):
self._setup_fiscal_position_empty()
self.test_01_2_hr_payslip_run()
self.assertEqual(len(self.payslip_run.slip_ids.mapped('move_id.line_ids.account_id')), 2)

View File

@@ -1,21 +0,0 @@
<?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>
<xpath expr="//page[@name='bank_account']//field[@name='code']" position="before">
<field name="default_account_id" string="Default Account" attrs="{'invisible': [('type', '!=', 'general')]}" />
</xpath>
</field>
</record>
</odoo>

View File

@@ -1,75 +0,0 @@
<?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>
<!-- contract -->
<record id="hr_contract_view_form_inherit" model="ir.ui.view">
<field name="name">hr.contract.view.form.inherit</field>
<field name="model">hr.contract</field>
<field name="inherit_id" ref="hr_payroll_account.hr_contract_view_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='analytic_account_id']" position="after">
<field name="payroll_fiscal_position_id"/>
</xpath>
</field>
</record>
</odoo>

View File

@@ -1,3 +0,0 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from . import hr_payroll_payslips_by_employees

View File

@@ -1,13 +0,0 @@
# 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()