Merge branch 'mig/15.0/hr_payroll_payment' into '15.0'

mig/15.0/hr_payroll_payment into 15.0

See merge request hibou-io/hibou-odoo/suite!1108
This commit is contained in:
Jared Kipe
2021-10-07 13:07:28 +00:00
14 changed files with 949 additions and 0 deletions

View File

@@ -0,0 +1,27 @@
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.

4
hr_payroll_payment/__init__.py Executable file
View File

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

View File

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

@@ -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

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

View File

@@ -0,0 +1,593 @@
# 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):
values = {
'payment_reference': self.number,
'ref': 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):
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

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

View 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

View File

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

@@ -0,0 +1,21 @@
<?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

@@ -0,0 +1,75 @@
<?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

@@ -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

View File

@@ -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()