mirror of
https://gitlab.com/hibou-io/hibou-odoo/suite.git
synced 2025-01-20 12:37:31 +02:00
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:
27
hr_payroll_payment/README.md
Normal file
27
hr_payroll_payment/README.md
Normal 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
4
hr_payroll_payment/__init__.py
Executable 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
|
||||||
52
hr_payroll_payment/__manifest__.py
Executable file
52
hr_payroll_payment/__manifest__.py
Executable 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',
|
||||||
|
}
|
||||||
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')
|
||||||
593
hr_payroll_payment/models/hr_payslip.py
Normal file
593
hr_payroll_payment/models/hr_payslip.py
Normal 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})
|
||||||
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
|
||||||
125
hr_payroll_payment/tests/test_hr_payroll_account.py
Normal file
125
hr_payroll_payment/tests/test_hr_payroll_account.py
Normal 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)
|
||||||
21
hr_payroll_payment/views/account_views.xml
Normal file
21
hr_payroll_payment/views/account_views.xml
Normal 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>
|
||||||
75
hr_payroll_payment/views/hr_payslip_views.xml
Normal file
75
hr_payroll_payment/views/hr_payslip_views.xml
Normal 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>
|
||||||
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