Compare commits
2 Commits
56fd53e470
...
dd9e87f3f5
| Author | SHA1 | Date |
|---|---|---|
|
|
dd9e87f3f5 | |
|
|
62edcad092 |
|
|
@ -14,6 +14,18 @@ class HRLeave(models.Model):
|
|||
date_from = datetime.fromisoformat(from_date).replace(hour=0, minute=0, second=0)
|
||||
date_to = datetime.fromisoformat(to_date).replace(hour=23, minute=59, second=59)
|
||||
employee_id = int(employee_id)
|
||||
all_leaves = self.search([
|
||||
('date_from', '<', date_to),
|
||||
('date_to', '>', date_from),
|
||||
('employee_id', 'in', [employee_id]),
|
||||
('state', 'not in', ['cancel', 'refuse']),
|
||||
])
|
||||
domain = [
|
||||
('employee_id', '=', employee_id),
|
||||
('date_from', '<', date_to),
|
||||
('date_to', '>', date_from),
|
||||
('state', 'not in', ['cancel', 'refuse']),
|
||||
]
|
||||
if fetched_leave_id and fetched_leave_id > 0:
|
||||
all_leaves = self.search([
|
||||
('date_from', '<', date_to),
|
||||
|
|
@ -30,19 +42,6 @@ class HRLeave(models.Model):
|
|||
('id', '!=', fetched_leave_id),
|
||||
('state', 'not in', ['cancel', 'refuse']),
|
||||
]
|
||||
else:
|
||||
all_leaves = self.search([
|
||||
('date_from', '<', date_to),
|
||||
('date_to', '>', date_from),
|
||||
('employee_id', 'in', [employee_id]),
|
||||
('state', 'not in', ['cancel', 'refuse']),
|
||||
])
|
||||
domain = [
|
||||
('employee_id', '=', employee_id),
|
||||
('date_from', '<', date_to),
|
||||
('date_to', '>', date_from),
|
||||
('state', 'not in', ['cancel', 'refuse']),
|
||||
]
|
||||
|
||||
|
||||
conflicting_holidays = all_leaves.filtered_domain(domain)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
#-*- coding:utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import controllers
|
||||
from . import models
|
||||
from . import wizard
|
||||
from . import report
|
||||
|
||||
|
||||
def _auto_install_l10n_hr_payroll(env):
|
||||
"""Installs l10n_**_hr_payroll modules automatically if such exists for the countries that companies are in"""
|
||||
country_codes = env['res.company'].search([]).country_id.mapped('code')
|
||||
if not country_codes:
|
||||
return
|
||||
possible_module_names = [f'l10n_{country_code.lower()}_hr_payroll' for country_code in country_codes]
|
||||
modules = env['ir.module.module'].search([('name', 'in', possible_module_names), ('state', '=', 'uninstalled')])
|
||||
if modules:
|
||||
modules.sudo().button_install()
|
||||
|
||||
|
||||
def _post_init_hook(env):
|
||||
env['res.company'].search([])._create_dashboard_notes()
|
||||
_auto_install_l10n_hr_payroll(env)
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
'name': 'Payroll',
|
||||
'category': 'Human Resources/Payroll',
|
||||
'sequence': 290,
|
||||
'summary': 'Manage your employee payroll records',
|
||||
'installable': True,
|
||||
'application': True,
|
||||
'depends': [
|
||||
# 'hr_work_entry_contract_enterprise',
|
||||
'mail',
|
||||
'web_editor',
|
||||
'hr_work_entry_contract',
|
||||
# 'hr_gantt'
|
||||
],
|
||||
'data': [
|
||||
'security/hr_payroll_security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'wizard/hr_payroll_payslips_by_employees_views.xml',
|
||||
'wizard/hr_payroll_index_wizard_views.xml',
|
||||
'wizard/hr_payroll_edit_payslip_lines_wizard_views.xml',
|
||||
'views/hr_contract_views.xml',
|
||||
'views/hr_payroll_structure_views.xml',
|
||||
'views/hr_payroll_structure_type_views.xml',
|
||||
'views/hr_salary_rule_category_views.xml',
|
||||
'views/hr_salary_rule_views.xml',
|
||||
'views/hr_payslip_line_views.xml',
|
||||
'views/hr_payslip_views.xml',
|
||||
'views/hr_payslip_run_views.xml',
|
||||
'views/hr_payslip_input_type_views.xml',
|
||||
'views/hr_salary_attachment_views.xml',
|
||||
'views/hr_employee_views.xml',
|
||||
'views/res_users_views.xml',
|
||||
'views/hr_payroll_employee_declaration_views.xml',
|
||||
'data/hr_payroll_dashboard_warning_data.xml',
|
||||
'data/hr_payroll_sequence.xml',
|
||||
'data/report_paperformat_data.xml',
|
||||
'views/hr_payroll_report.xml',
|
||||
'data/hr_payroll_data.xml',
|
||||
'data/mail_activity_type_data.xml',
|
||||
'data/mail_template_data.xml',
|
||||
'data/ir_cron_data.xml',
|
||||
'data/ir_actions_server_data.xml',
|
||||
'data/note_data.xml',
|
||||
'data/hr_payroll_tour.xml',
|
||||
'views/res_config_settings_views.xml',
|
||||
'views/report_contributionregister_templates.xml',
|
||||
'views/report_payslip_templates.xml',
|
||||
'views/report_light_payslip_templates.xml',
|
||||
'views/hr_work_entry_type_views.xml',
|
||||
'views/resource_calendar_views.xml',
|
||||
'views/hr_rule_parameter_views.xml',
|
||||
'views/hr_payroll_report_views.xml',
|
||||
'views/hr_work_entry_report_views.xml',
|
||||
'views/hr_payroll_dashboard_views.xml',
|
||||
'views/hr_payroll_dashboard_warning_views.xml',
|
||||
'views/hr_payroll_headcount_views.xml',
|
||||
'views/hr_payroll_menu.xml',
|
||||
# 'views/hr_work_entry_views.xml',
|
||||
'views/hr_work_entry_export_mixin_views.xml',
|
||||
'report/hr_contract_history_report_views.xml',
|
||||
'wizard/hr_payroll_payment_report_wizard.xml',
|
||||
],
|
||||
# 'demo': ['data/hr_payroll_demo.xml'],
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
'hr_payroll/static/src/components/add_payslips/**',
|
||||
'hr_payroll/static/src/views/add_payslips_hook.js',
|
||||
# 'hr_payroll/static/src/**/*',
|
||||
# ('remove', 'hr_payroll/static/src/js/hr_payroll_report_graph_view.js'),
|
||||
# ('remove', 'hr_payroll/static/src/js/hr_payroll_report_pivot_*'),
|
||||
# ('remove', 'hr_payroll/static/src/js/hr_work_entries_gantt.*'),
|
||||
],
|
||||
'web.assets_backend_lazy': [
|
||||
# 'hr_payroll/static/src/js/hr_payroll_report_graph_view.js',
|
||||
# 'hr_payroll/static/src/js/hr_payroll_report_pivot_*',
|
||||
# 'hr_payroll/static/src/js/hr_work_entries_gantt.*',
|
||||
# 'hr_payroll/static/**/*'
|
||||
],
|
||||
'web.assets_tests': [
|
||||
# 'hr_payroll/static/tests/**/*.js',
|
||||
],
|
||||
},
|
||||
'license': 'OEEL-1',
|
||||
'post_init_hook': '_post_init_hook',
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
#-*- coding:utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import main
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import io
|
||||
import re
|
||||
|
||||
from odoo.http import request, route, Controller, content_disposition
|
||||
from odoo.tools.pdf import PdfFileReader, PdfFileWriter
|
||||
from odoo.tools.safe_eval import safe_eval
|
||||
|
||||
|
||||
class HrPayroll(Controller):
|
||||
|
||||
@route(["/print/payslips"], type='http', auth='user')
|
||||
def get_payroll_report_print(self, list_ids='', **post):
|
||||
if not request.env.user.has_group('hr_payroll.group_hr_payroll_user') or not list_ids or re.search("[^0-9|,]", list_ids):
|
||||
return request.not_found()
|
||||
|
||||
ids = [int(s) for s in list_ids.split(',')]
|
||||
payslips = request.env['hr.payslip'].browse(ids)
|
||||
|
||||
pdf_writer = PdfFileWriter()
|
||||
payslip_reports = payslips._get_pdf_reports()
|
||||
|
||||
for report, slips in payslip_reports.items():
|
||||
for payslip in slips:
|
||||
pdf_content, _ = request.env['ir.actions.report'].\
|
||||
with_context(lang=payslip.employee_id.lang or payslip.env.lang).\
|
||||
sudo().\
|
||||
_render_qweb_pdf(report, payslip.id, data={'company_id': payslip.company_id})
|
||||
reader = PdfFileReader(io.BytesIO(pdf_content), strict=False, overwriteWarnings=False)
|
||||
|
||||
for page in range(reader.getNumPages()):
|
||||
pdf_writer.addPage(reader.getPage(page))
|
||||
|
||||
_buffer = io.BytesIO()
|
||||
pdf_writer.write(_buffer)
|
||||
merged_pdf = _buffer.getvalue()
|
||||
_buffer.close()
|
||||
|
||||
if len(payslip_reports) == 1 and len(payslips) == 1 and payslips.struct_id.report_id.print_report_name:
|
||||
report_name = safe_eval(payslips.struct_id.report_id.print_report_name, {'object': payslips})
|
||||
else:
|
||||
report_name = ' - '.join(r.name for r in list(payslip_reports.keys()))
|
||||
employees = payslips.employee_id.mapped('name')
|
||||
if len(employees) == 1:
|
||||
report_name = '%s - %s' % (report_name, employees[0])
|
||||
|
||||
pdfhttpheaders = [
|
||||
('Content-Type', 'application/pdf'),
|
||||
('Content-Length', len(merged_pdf)),
|
||||
('Content-Disposition', content_disposition(report_name + '.pdf'))
|
||||
]
|
||||
|
||||
return request.make_response(merged_pdf, headers=pdfhttpheaders)
|
||||
|
|
@ -0,0 +1,265 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
|
||||
<record id="hr_payroll_dashboard_warning_employee_without_contract" model="hr.payroll.dashboard.warning">
|
||||
<field name="name">Employees Without Running Contracts</field>
|
||||
<field name="country_id" eval="False"/>
|
||||
<field name="evaluation_code">
|
||||
# Retrieve employees:
|
||||
# - with no open contract, and date_end in the past
|
||||
# - with no contract, and not green draft contract
|
||||
employees_without_contracts = self.env['hr.employee']
|
||||
all_employees = self.env['hr.employee'].search([
|
||||
('employee_type', '=', 'employee'),
|
||||
('company_id', 'in', self.env.companies.ids),
|
||||
])
|
||||
today = date.today()
|
||||
for employee in all_employees:
|
||||
contract = employee.contract_id.sudo()
|
||||
if contract and contract.date_end and contract.date_end < today:
|
||||
employees_without_contracts += employee
|
||||
elif not contract:
|
||||
existing_draft_contract = self.env['hr.contract'].search([
|
||||
('employee_id', '=', employee.id),
|
||||
('company_id', '=', employee.company_id.id),
|
||||
('state', '=', 'draft'),
|
||||
('kanban_state', '=', 'done'),
|
||||
])
|
||||
if not existing_draft_contract:
|
||||
employees_without_contracts += employee
|
||||
if employees_without_contracts:
|
||||
warning_count = len(employees_without_contracts)
|
||||
warning_records = employees_without_contracts
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_payroll_dashboard_warning_employee_with_different_company_on_contract" model="hr.payroll.dashboard.warning">
|
||||
<field name="name">Employee whose contracts and company are differents</field>
|
||||
<field name="country_id" eval="False"/>
|
||||
<field name="evaluation_code">
|
||||
employee_with_different_company_on_contract = self.env['hr.employee']
|
||||
all_employees = self.env['hr.employee'].search([
|
||||
('employee_type', '=', 'employee'),
|
||||
('company_id', 'in', self.env.companies.ids),
|
||||
])
|
||||
contracts = self.sudo().env['hr.contract'].search([
|
||||
('state', 'in', ['draft', 'open']),
|
||||
('employee_id', 'in', all_employees.ids),
|
||||
])
|
||||
for contract in contracts:
|
||||
if contract.employee_id.company_id != contract.company_id:
|
||||
employee_with_different_company_on_contract |= contract.employee_id
|
||||
if employee_with_different_company_on_contract:
|
||||
warning_count = len(employee_with_different_company_on_contract)
|
||||
warning_records = employee_with_different_company_on_contract
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_payroll_dashboard_warning_employees_multiple_payslips" model="hr.payroll.dashboard.warning">
|
||||
<field name="name">Employees With Multiple Open Payslips of Same Type</field>
|
||||
<field name="country_id" eval="False"/>
|
||||
<field name="evaluation_code">
|
||||
employee_payslips = defaultdict(lambda: defaultdict(lambda: self.env['hr.payslip']))
|
||||
for slip in last_batches.slip_ids:
|
||||
if slip.state == 'cancel':
|
||||
continue
|
||||
employee = slip.employee_id
|
||||
struct = slip.struct_id
|
||||
|
||||
employee_payslips[struct][employee] |= slip
|
||||
|
||||
|
||||
employees_multiple_payslips = self.env['hr.payslip']
|
||||
for dummy, employee_slips in employee_payslips.items():
|
||||
for employee, payslips in employee_slips.items():
|
||||
if len(payslips) > 1:
|
||||
employees_multiple_payslips |= payslips
|
||||
if employees_multiple_payslips:
|
||||
warning_count = len(employees_multiple_payslips.employee_id)
|
||||
warning_records = employees_multiple_payslips
|
||||
additional_context = {'search_default_group_by_employee_id': 1}
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_payroll_dashboard_warning_employee_missing_from_open_batch" model="hr.payroll.dashboard.warning">
|
||||
<field name="name">Employees (With Running Contracts) missing from open batches</field>
|
||||
<field name="country_id" eval="False"/>
|
||||
<field name="evaluation_code">
|
||||
employees_missing_payslip = self.env['hr.employee'].search([
|
||||
('company_id', 'in', last_batches.company_id.ids),
|
||||
('id', 'not in', last_batches.slip_ids.employee_id.ids),
|
||||
('contract_warning', '=', False)])
|
||||
if employees_missing_payslip:
|
||||
warning_count = len(employees_missing_payslip)
|
||||
warning_records = employees_missing_payslip.contract_id
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_payroll_dashboard_warning_employee_ambiguous_contract" model="hr.payroll.dashboard.warning">
|
||||
<field name="name">Employees With Both New And Running Contracts</field>
|
||||
<field name="country_id" eval="False"/>
|
||||
<field name="evaluation_code">
|
||||
# Retrieve employees with both draft and running contracts
|
||||
ambiguous_domain = [
|
||||
('company_id', 'in', self.env.companies.ids),
|
||||
('employee_id', '!=', False),
|
||||
'|',
|
||||
'&',
|
||||
('state', '=', 'draft'),
|
||||
('kanban_state', '!=', 'done'),
|
||||
('state', '=', 'open')]
|
||||
employee_contract_groups = self.env['hr.contract']._read_group(
|
||||
ambiguous_domain,
|
||||
groupby=['employee_id'],
|
||||
having=[('state:count_distinct', '=', 2)])
|
||||
ambiguous_employee_ids = [employee.id for [employee] in employee_contract_groups]
|
||||
if ambiguous_employee_ids:
|
||||
ambiguous_contracts = self.env['hr.contract'].search(
|
||||
ambiguous_domain + [('employee_id', 'in', ambiguous_employee_ids)])
|
||||
warning_count = len(ambiguous_employee_ids)
|
||||
warning_records = ambiguous_contracts
|
||||
additional_context = {'search_default_group_by_employee': 1}
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_payroll_dashboard_warning_work_entries_in_conflict" model="hr.payroll.dashboard.warning">
|
||||
<field name="name">Conflicts</field>
|
||||
<field name="country_id" eval="False"/>
|
||||
<field name="evaluation_code">
|
||||
start_month = date.today().replace(day=1)
|
||||
next_month = start_month + relativedelta(months=1)
|
||||
work_entries_in_conflict = self.env['hr.work.entry'].search_count([
|
||||
('state', '=', 'conflict'),
|
||||
('date_stop', '>=', start_month),
|
||||
('date_start', '<', next_month)])
|
||||
if work_entries_in_conflict:
|
||||
warning_count = work_entries_in_conflict
|
||||
warning_action = 'hr_work_entry.hr_work_entry_action_conflict'
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_payroll_dashboard_warning_working_schedule_change" model="hr.payroll.dashboard.warning">
|
||||
<field name="name">Working Schedule Changes</field>
|
||||
<field name="country_id" eval="False"/>
|
||||
<field name="evaluation_code">
|
||||
employee_calendar_contracts = defaultdict(lambda: defaultdict(lambda: self.env['hr.contract']))
|
||||
for slip in last_batches.slip_ids:
|
||||
if slip.state == 'cancel':
|
||||
continue
|
||||
employee = slip.employee_id
|
||||
contract = slip.contract_id
|
||||
calendar = contract.resource_calendar_id
|
||||
employee_calendar_contracts[employee][calendar] |= contract
|
||||
|
||||
multiple_schedule_contracts = self.env['hr.contract']
|
||||
for employee, calendar_contracts in employee_calendar_contracts.items():
|
||||
if len(calendar_contracts) > 1:
|
||||
for dummy, contracts in calendar_contracts.items():
|
||||
multiple_schedule_contracts |= contracts
|
||||
|
||||
if multiple_schedule_contracts:
|
||||
warning_count = len(multiple_schedule_contracts.employee_id)
|
||||
warning_records = multiple_schedule_contracts
|
||||
additional_context = {'search_default_group_by_employee': 1}
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_payroll_dashboard_warning_nearly_expired_contracts" model="hr.payroll.dashboard.warning">
|
||||
<field name="name">Employees with running contracts coming to an end</field>
|
||||
<field name="country_id" eval="False"/>
|
||||
<field name="evaluation_code">
|
||||
# Nearly expired contracts
|
||||
nearly_expired_contracts = self.env['hr.contract']
|
||||
for company in self.env.companies:
|
||||
outdated_days = date.today() + relativedelta(days=company.contract_expiration_notice_period)
|
||||
nearly_expired_contracts += self.env['hr.contract']._get_nearly_expired_contracts(outdated_days, company.id)
|
||||
if nearly_expired_contracts:
|
||||
warning_count = len(nearly_expired_contracts)
|
||||
warning_records = nearly_expired_contracts
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_payroll_dashboard_warning_payslips_previous_contract" model="hr.payroll.dashboard.warning">
|
||||
<field name="name">Payslips Generated On Previous Contract</field>
|
||||
<field name="country_id" eval="False"/>
|
||||
<field name="evaluation_code">
|
||||
employee_payslip_contracts = defaultdict(lambda: self.env['hr.contract'])
|
||||
for slip in last_batches.slip_ids:
|
||||
if slip.state == 'cancel':
|
||||
continue
|
||||
employee = slip.employee_id
|
||||
contract = slip.contract_id
|
||||
employee_payslip_contracts[employee] |= contract
|
||||
|
||||
employees_previous_contract = self.env['hr.employee']
|
||||
for employee, used_contracts in employee_payslip_contracts.items():
|
||||
if employee.contract_id not in used_contracts:
|
||||
employees_previous_contract |= employee
|
||||
|
||||
if employees_previous_contract:
|
||||
warning_count = len(employees_previous_contract)
|
||||
warning_records = employees_previous_contract
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_payroll_dashboard_warning_payslips_negative_net" model="hr.payroll.dashboard.warning">
|
||||
<field name="name">Payslips With Negative NET</field>
|
||||
<field name="country_id" eval="False"/>
|
||||
<field name="evaluation_code">
|
||||
payslips_with_negative_net = self.env['hr.payslip']
|
||||
|
||||
for slip in last_batches.slip_ids:
|
||||
if slip.state == 'cancel':
|
||||
continue
|
||||
if slip.net_wage < 0:
|
||||
payslips_with_negative_net |= slip
|
||||
|
||||
if payslips_with_negative_net:
|
||||
warning_count = len(payslips_with_negative_net)
|
||||
warning_records = payslips_with_negative_net
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_payroll_dashboard_warning_new_contracts" model="hr.payroll.dashboard.warning">
|
||||
<field name="name">New Contracts</field>
|
||||
<field name="country_id" eval="False"/>
|
||||
<field name="evaluation_code">
|
||||
new_contracts = self.env['hr.contract'].search([
|
||||
('state', '=', 'draft'),
|
||||
('employee_id', '!=', False),
|
||||
('kanban_state', '=', 'normal')])
|
||||
if new_contracts:
|
||||
warning_count = len(new_contracts)
|
||||
warning_records = new_contracts
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_payroll_dashboard_warning_employee_without_identification" model="hr.payroll.dashboard.warning">
|
||||
<field name="name">Employees Without Identification Number</field>
|
||||
<field name="country_id" eval="False"/>
|
||||
<field name="evaluation_code">
|
||||
employees_wo_id = self.env['hr.employee'].search([
|
||||
('identification_id', '=', False),
|
||||
])
|
||||
if employees_wo_id:
|
||||
warning_count = len(employees_wo_id)
|
||||
warning_records = employees_wo_id
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_payroll_dashboard_warning_employee_without_bank_account" model="hr.payroll.dashboard.warning">
|
||||
<field name="name">Employees Without Bank account Number</field>
|
||||
<field name="country_id" eval="False"/>
|
||||
<field name="evaluation_code">
|
||||
employees_wo_bnk_acc = self.env['hr.employee'].search([
|
||||
('bank_account_id', '=', False),
|
||||
('contract_id', '!=', False),
|
||||
])
|
||||
if employees_wo_bnk_acc:
|
||||
warning_count = len(employees_wo_bnk_acc)
|
||||
warning_records = employees_wo_bnk_acc
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,296 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="0">
|
||||
|
||||
<record id="BASIC" model="hr.salary.rule.category">
|
||||
<field name="name">Basic</field>
|
||||
<field name="code">BASIC</field>
|
||||
</record>
|
||||
|
||||
<record id="ALW" model="hr.salary.rule.category">
|
||||
<field name="name">Allowance</field>
|
||||
<field name="code">ALW</field>
|
||||
</record>
|
||||
|
||||
<record id="GROSS" model="hr.salary.rule.category">
|
||||
<field name="name">Taxable Salary</field>
|
||||
<field name="code">GROSS</field>
|
||||
</record>
|
||||
|
||||
<record id="DED" model="hr.salary.rule.category">
|
||||
<field name="name">Deduction</field>
|
||||
<field name="code">DED</field>
|
||||
</record>
|
||||
|
||||
<record id="NET" model="hr.salary.rule.category">
|
||||
<field name="name">Net</field>
|
||||
<field name="code">NET</field>
|
||||
</record>
|
||||
|
||||
<record id="COMP" model="hr.salary.rule.category">
|
||||
<field name="name">Company Contribution</field>
|
||||
<field name="code">COMP</field>
|
||||
</record>
|
||||
</data>
|
||||
|
||||
<data noupdate="1">
|
||||
<!--Default structure -->
|
||||
<record id="default_structure" model="hr.payroll.structure">
|
||||
<field name="name">Default Structure Rules Set</field>
|
||||
<field name="type_id" ref="hr_contract.structure_type_employee"/>
|
||||
<field name="country_id" eval="False"/>
|
||||
<field name="active">False</field>
|
||||
</record>
|
||||
|
||||
<!--default salary rules -->
|
||||
<record id="default_basic_salary_rule" model="hr.salary.rule">
|
||||
<field name="category_id" ref="hr_payroll.BASIC"/>
|
||||
<field name="name">Basic Salary</field>
|
||||
<field name="sequence">1</field>
|
||||
<field name="code">BASIC</field>
|
||||
<field name="condition_select">none</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="amount_python_compute">
|
||||
result = payslip.paid_amount
|
||||
</field>
|
||||
<field name="struct_id" ref="hr_payroll.default_structure"/>
|
||||
</record>
|
||||
|
||||
<record id="default_gross_salary_rule" model="hr.salary.rule">
|
||||
<field name="category_id" ref="hr_payroll.GROSS"/>
|
||||
<field name="name">Taxable Salary</field>
|
||||
<field name="sequence">100 </field>
|
||||
<field name="code">GROSS</field>
|
||||
<field name="condition_select">none</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="amount_python_compute">
|
||||
result = categories['BASIC'] + categories['ALW']
|
||||
</field>
|
||||
<field name="struct_id" ref="hr_payroll.default_structure"/>
|
||||
</record>
|
||||
|
||||
<record id="default_deduction_salary_rule" model="hr.salary.rule">
|
||||
<field name="category_id" ref="hr_payroll.DED"/>
|
||||
<field name="name">Deduction</field>
|
||||
<field name="sequence">198</field>
|
||||
<field name="code">DEDUCTION</field>
|
||||
<field name="condition_select">python</field>
|
||||
<field name="condition_python">result = 'DEDUCTION' in inputs</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="amount_python_compute">
|
||||
result = -inputs['DEDUCTION'].amount
|
||||
result_name = inputs['DEDUCTION'].name
|
||||
</field>
|
||||
<field name="struct_id" ref="hr_payroll.default_structure"/>
|
||||
</record>
|
||||
|
||||
<record id="default_attachment_of_salary_rule" model="hr.salary.rule">
|
||||
<field name="category_id" ref="hr_payroll.DED"/>
|
||||
<field name="name">Attachment of Salary</field>
|
||||
<field name="sequence">174</field>
|
||||
<field name="code">ATTACH_SALARY</field>
|
||||
<field name="condition_select">python</field>
|
||||
<field name="condition_python">result = 'ATTACH_SALARY' in inputs</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="amount_python_compute">
|
||||
result = -inputs['ATTACH_SALARY'].amount
|
||||
result_name = inputs['ATTACH_SALARY'].name
|
||||
</field>
|
||||
<field name="struct_id" ref="hr_payroll.default_structure"/>
|
||||
</record>
|
||||
|
||||
<record id="default_assignment_of_salary_rule" model="hr.salary.rule">
|
||||
<field name="category_id" ref="hr_payroll.DED"/>
|
||||
<field name="name">Assignment of Salary</field>
|
||||
<field name="sequence">174</field>
|
||||
<field name="code">ASSIG_SALARY</field>
|
||||
<field name="condition_select">python</field>
|
||||
<field name="condition_python">result = 'ASSIG_SALARY' in inputs</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="amount_python_compute">
|
||||
result = -inputs['ASSIG_SALARY'].amount
|
||||
result_name = inputs['ASSIG_SALARY'].name
|
||||
</field>
|
||||
<field name="struct_id" ref="hr_payroll.default_structure"/>
|
||||
</record>
|
||||
|
||||
<record id="default_child_support" model="hr.salary.rule">
|
||||
<field name="category_id" ref="hr_payroll.DED"/>
|
||||
<field name="name">Child Support</field>
|
||||
<field name="code">CHILD_SUPPORT</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="sequence">174</field>
|
||||
<field name="condition_select">python</field>
|
||||
<field name="condition_python">result = 'CHILD_SUPPORT' in inputs</field>
|
||||
<field name="amount_python_compute">
|
||||
result = -inputs['CHILD_SUPPORT'].amount
|
||||
result_name = inputs['CHILD_SUPPORT'].name
|
||||
</field>
|
||||
<field name="struct_id" ref="hr_payroll.default_structure"/>
|
||||
</record>
|
||||
|
||||
<record id="default_reimbursement_salary_rule" model="hr.salary.rule">
|
||||
<field name="category_id" ref="hr_payroll.ALW"/>
|
||||
<field name="name">Reimbursement</field>
|
||||
<field name="sequence">199</field>
|
||||
<field name="code">REIMBURSEMENT</field>
|
||||
<field name="condition_select">python</field>
|
||||
<field name="condition_python">result = 'REIMBURSEMENT' in inputs</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="amount_python_compute">
|
||||
result = inputs['REIMBURSEMENT'].amount
|
||||
result_name = inputs['REIMBURSEMENT'].name
|
||||
</field>
|
||||
<field name="struct_id" ref="hr_payroll.default_structure"/>
|
||||
</record>
|
||||
|
||||
<record id="default_net_salary" model="hr.salary.rule">
|
||||
<field name="category_id" ref="hr_payroll.NET"/>
|
||||
<field name="name">Net Salary</field>
|
||||
<field name="sequence">200</field>
|
||||
<field name="code">NET</field>
|
||||
<field name="appears_on_employee_cost_dashboard">True</field>
|
||||
<field name="condition_select">none</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="amount_python_compute">
|
||||
result = categories['BASIC'] + categories['ALW'] + categories['DED']
|
||||
</field>
|
||||
<field name="struct_id" ref="hr_payroll.default_structure"/>
|
||||
</record>
|
||||
</data>
|
||||
|
||||
<data noupdate="0">
|
||||
<!-- Salary Structure -->
|
||||
<record id="structure_002" model="hr.payroll.structure">
|
||||
<field name="name">Regular Pay</field>
|
||||
<field name="type_id" ref="hr_contract.structure_type_employee"/>
|
||||
<field name="unpaid_work_entry_type_ids" eval="[(4, ref('hr_work_entry_contract.work_entry_type_unpaid_leave'))]"/>
|
||||
<field name="country_id" eval="False"/>
|
||||
</record>
|
||||
<record id="hr_contract.structure_type_employee" model="hr.payroll.structure.type">
|
||||
<field name="default_struct_id" ref="structure_002"/>
|
||||
</record>
|
||||
|
||||
<record id="structure_worker_001" model="hr.payroll.structure">
|
||||
<field name="name">Worker Pay</field>
|
||||
<field name="type_id" ref="hr_contract.structure_type_worker"/>
|
||||
<field name="country_id" eval="False"/>
|
||||
</record>
|
||||
<record id="hr_contract.structure_type_worker" model="hr.payroll.structure.type">
|
||||
<field name="default_struct_id" ref="structure_worker_001"/>
|
||||
</record>
|
||||
|
||||
<!-- Decimal Precision -->
|
||||
<record forcecreate="True" id="decimal_payroll" model="decimal.precision">
|
||||
<field name="name">Payroll</field>
|
||||
<field name="digits">2</field>
|
||||
</record>
|
||||
|
||||
<record forcecreate="True" id="decimal_payroll_rate" model="decimal.precision">
|
||||
<field name="name">Payroll Rate</field>
|
||||
<field name="digits">4</field>
|
||||
</record>
|
||||
|
||||
<!-- Work Entry Type -->
|
||||
<record id="hr_work_entry_contract.work_entry_type_leave" model="hr.work.entry.type">
|
||||
<field name="round_days">HALF</field>
|
||||
<field name="round_days_type">DOWN</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_work_entry_contract.work_entry_type_compensatory" model="hr.work.entry.type">
|
||||
<field name="round_days">HALF</field>
|
||||
<field name="round_days_type">DOWN</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_work_entry_contract.work_entry_type_home_working" model="hr.work.entry.type">
|
||||
<field name="round_days">HALF</field>
|
||||
<field name="round_days_type">DOWN</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_work_entry_contract.work_entry_type_unpaid_leave" model="hr.work.entry.type">
|
||||
<field name="round_days">HALF</field>
|
||||
<field name="round_days_type">DOWN</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_work_entry_contract.work_entry_type_sick_leave" model="hr.work.entry.type">
|
||||
<field name="round_days">HALF</field>
|
||||
<field name="round_days_type">DOWN</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_work_entry_contract.work_entry_type_legal_leave" model="hr.work.entry.type">
|
||||
<field name="round_days">HALF</field>
|
||||
<field name="round_days_type">DOWN</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_work_entry_type_out_of_contract" model="hr.work.entry.type">
|
||||
<field name="name">Out of Contract</field>
|
||||
<field name="color">0</field>
|
||||
<field name="code">OUT</field>
|
||||
<field name="is_leave" eval="False"/>
|
||||
<field name="is_unforeseen" eval="False"/>
|
||||
<field name="round_days">HALF</field>
|
||||
<field name="round_days_type">HALF-UP</field>
|
||||
</record>
|
||||
|
||||
<record id="input_deduction" model="hr.payslip.input.type">
|
||||
<field name="name">Deduction</field>
|
||||
<field name="code">DEDUCTION</field>
|
||||
<field name="country_id" eval="False"/>
|
||||
</record>
|
||||
|
||||
<record id="input_reimbursement" model="hr.payslip.input.type">
|
||||
<field name="name">Reimbursement</field>
|
||||
<field name="code">REIMBURSEMENT</field>
|
||||
<field name="country_id" eval="False"/>
|
||||
</record>
|
||||
|
||||
<record id="input_attachment_salary" model="hr.payslip.input.type">
|
||||
<field name="name">Attachment of Salary</field>
|
||||
<field name="code">ATTACH_SALARY</field>
|
||||
<field name="country_id" eval="False"/>
|
||||
<field name="available_in_attachments" eval="True"/>
|
||||
</record>
|
||||
|
||||
<record id="input_assignment_salary" model="hr.payslip.input.type">
|
||||
<field name="name">Assignment of Salary</field>
|
||||
<field name="code">ASSIG_SALARY</field>
|
||||
<field name="country_id" eval="False"/>
|
||||
<field name="available_in_attachments" eval="True"/>
|
||||
</record>
|
||||
|
||||
<record id="input_child_support" model="hr.payslip.input.type">
|
||||
<field name="name">Child Support</field>
|
||||
<field name="code">CHILD_SUPPORT</field>
|
||||
<field name="country_id" eval="False"/>
|
||||
<field name="available_in_attachments" eval="True"/>
|
||||
<field name="default_no_end_date" eval="True"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.actions.server" id="action_reset_work_entries">
|
||||
<field name="name">Payroll - Technical: Reset Work Entries</field>
|
||||
<field name="model_id" ref="hr_work_entry.model_hr_work_entry"/>
|
||||
<field name="state">code</field>
|
||||
<field name="groups_id" eval="[(4,ref('base.group_system'))]"/>
|
||||
<field name="code">
|
||||
# Don't call this server action if you don't want to loose all your work entries
|
||||
env['hr.work.entry'].search([]).unlink()
|
||||
now = datetime.datetime.now()
|
||||
env['hr.contract'].write({
|
||||
'date_generated_from': now,
|
||||
'date_generated_to': now
|
||||
})
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_edit_payslip_lines" model="ir.actions.server">
|
||||
<field name="name">Edit Payslip Lines</field>
|
||||
<field name="model_id" ref="hr_payroll.model_hr_payslip"/>
|
||||
<field name="binding_model_id" ref="hr_payroll.model_hr_payslip"/>
|
||||
<field name="binding_view_types">form</field>
|
||||
<field name="state">code</field>
|
||||
<field name="code">
|
||||
action = records.action_edit_payslip_lines()
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,417 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<record id="base.user_demo" model="res.users">
|
||||
<field name="groups_id" eval="[
|
||||
(3, ref('hr.group_hr_user')),
|
||||
(3, ref('hr_contract.group_hr_contract_manager')),
|
||||
(3, ref('hr_payroll.group_hr_payroll_user')),
|
||||
(3, ref('hr_payroll.group_hr_payroll_manager')),
|
||||
(3, ref('hr_contract.group_hr_contract_employee_manager'))]"/>
|
||||
</record>
|
||||
</data>
|
||||
|
||||
<!-- Structure Type -->
|
||||
<record id="hr_contract.structure_type_worker" model="hr.payroll.structure.type">
|
||||
<field name="wage_type">hourly</field>
|
||||
</record>
|
||||
|
||||
<!-- Contribution Register -->
|
||||
<record id="hr_houserent_register" model="res.partner">
|
||||
<field name="name">House Rent Allowance Register</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_provident_fund_register" model="res.partner">
|
||||
<field name="name">Provident Fund Register</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_professional_tax_register" model="res.partner">
|
||||
<field name="name">Professional Tax Register</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_meal_voucher_register" model="res.partner">
|
||||
<field name="name">Meal Voucher Register</field>
|
||||
<field name="company_type">company</field>
|
||||
</record>
|
||||
|
||||
<!-- Salary Rules for Regular Pay-->
|
||||
|
||||
<record id="structure_003" model="hr.payroll.structure">
|
||||
<field name="name">13th month - End of the year bonus</field>
|
||||
<field name="type_id" ref="hr_contract.structure_type_employee"/>
|
||||
<field name="rule_ids" eval="[]"/>
|
||||
</record>
|
||||
|
||||
<record id="hr_salary_rule_houserentallowance1" model="hr.salary.rule">
|
||||
<field name="amount_select">percentage</field>
|
||||
<field eval="40.0" name="amount_percentage"/>
|
||||
<field name="amount_percentage_base">contract.wage</field>
|
||||
<field name="code">HRA</field>
|
||||
<field name="category_id" ref="hr_payroll.ALW"/>
|
||||
<field name="partner_id" ref="hr_houserent_register"/>
|
||||
<field name="name">House Rent Allowance</field>
|
||||
<field name="sequence" eval="5"/>
|
||||
<field name="struct_id" ref="structure_002"/>
|
||||
</record>
|
||||
|
||||
<record id="hr_salary_rule_convanceallowance1" model="hr.salary.rule">
|
||||
<field name="amount_select">fix</field>
|
||||
<field eval="800.0" name="amount_fix"/>
|
||||
<field name="code">CA</field>
|
||||
<field name="category_id" ref="hr_payroll.ALW"/>
|
||||
<field name="name">Conveyance Allowance</field>
|
||||
<field name="sequence" eval="10"/>
|
||||
<field name="struct_id" ref="structure_002"/>
|
||||
</record>
|
||||
|
||||
<record id="hr_salary_rule_ca_gravie" model="hr.salary.rule">
|
||||
<field name="amount_select">fix</field>
|
||||
<field name="amount_fix" eval="600.0"/>
|
||||
<field name="code">CAGG</field>
|
||||
<field name="category_id" ref="hr_payroll.ALW"/>
|
||||
<field name="name">Conveyance Allowance For Gravie</field>
|
||||
<field name="sequence" eval="15"/>
|
||||
<field name="struct_id" ref="structure_002"/>
|
||||
</record>
|
||||
|
||||
<record id="hr_salary_rule_sum_alw_category" model="hr.salary.rule">
|
||||
<field name="amount_select">code</field>
|
||||
<field name="code">SUMALW</field>
|
||||
<field name="category_id" ref="hr_payroll.ALW"/>
|
||||
<field name="name">Sum of Allowance category</field>
|
||||
<field name="sequence" eval="99"/>
|
||||
<field name="amount_python_compute">result = payslip._sum_category('ALW', payslip.date_from, to_date=payslip.date_to)</field>
|
||||
<field name="struct_id" ref="structure_002"/>
|
||||
</record>
|
||||
|
||||
<record id="hr_salary_rule_meal_voucher" model="hr.salary.rule">
|
||||
<field name="amount_select">fix</field>
|
||||
<field eval="10" name="amount_fix"/>
|
||||
<field name="quantity">'WORK100' in worked_days and worked_days['WORK100'].number_of_days</field>
|
||||
<field name="code">MA</field>
|
||||
<field name="category_id" ref="hr_payroll.ALW"/>
|
||||
<field name="partner_id" ref="hr_meal_voucher_register"/>
|
||||
<field name="name">Meal Voucher</field>
|
||||
<field name="sequence" eval="16"/>
|
||||
<field name="struct_id" ref="structure_002"/>
|
||||
</record>
|
||||
|
||||
<record id="hr_salary_rule_providentfund1" model="hr.salary.rule">
|
||||
<field name="amount_select">percentage</field>
|
||||
<field name="sequence" eval="120"/>
|
||||
<field name="amount_percentage" eval="-12.5"/>
|
||||
<field name="amount_percentage_base">contract.wage</field>
|
||||
<field name="code">PF</field>
|
||||
<field name="category_id" ref="hr_payroll.DED"/>
|
||||
<field name="partner_id" ref="hr_provident_fund_register"/>
|
||||
<field name="name">Provident Fund</field>
|
||||
<field name="struct_id" ref="structure_002"/>
|
||||
</record>
|
||||
|
||||
<record id="hr_salary_rule_professionaltax1" model="hr.salary.rule">
|
||||
<field name="amount_select">fix</field>
|
||||
<field name="sequence" eval="150"/>
|
||||
<field name="amount_fix" eval="-200.0"/>
|
||||
<field name="code">PT</field>
|
||||
<field name="category_id" ref="hr_payroll.DED"/>
|
||||
<field name="partner_id" ref="hr_professional_tax_register"/>
|
||||
<field name="name">Professional Tax</field>
|
||||
<field name="struct_id" ref="structure_002"/>
|
||||
</record>
|
||||
|
||||
<record id="hr_salary_rule_13th_month_salary" model="hr.salary.rule">
|
||||
<field name="amount_select">percentage</field>
|
||||
<field name="amount_percentage" eval="10.0"/>
|
||||
<field name="amount_percentage_base">contract.wage</field>
|
||||
<field name="code">13th pay</field>
|
||||
<field name="category_id" ref="hr_payroll.ALW"/>
|
||||
<field name="name">13th pay salary</field>
|
||||
<field name="sequence" eval="5"/>
|
||||
<field name="struct_id" ref="structure_003"/>
|
||||
</record>
|
||||
|
||||
<!-- Employee -->
|
||||
|
||||
<record id="hr_employee_payroll" model="hr.employee">
|
||||
<field name="company_id" ref="base.main_company"/>
|
||||
<field eval="1" name="active"/>
|
||||
<field name="name">Roger Scott</field>
|
||||
<field name="parent_id" ref="hr.employee_vad"/>
|
||||
<field name="work_location_id" ref="hr.work_location_1"/>
|
||||
<field name="work_phone">+3282823500</field>
|
||||
<field name="image_1920" type="base64" file="hr_payroll/static/img/hr_employee_payroll-image.jpg"/>
|
||||
</record>
|
||||
|
||||
<!-- Employee Contract -->
|
||||
|
||||
<record id="hr_contract_firstcontract1" model="hr.contract">
|
||||
<field name="name">Marketing Executive Contract</field>
|
||||
<field name="date_start" eval="time.strftime('%Y-%m')+'-1'"/>
|
||||
<field name="date_end" eval="time.strftime('%Y')+'-12-31'"/>
|
||||
<field name="structure_type_id" ref="hr_contract.structure_type_employee"/>
|
||||
<field name="employee_id" ref="hr_employee_payroll"/>
|
||||
<field name="notes">Default contract for marketing executives</field>
|
||||
<field eval="4000.0" name="wage"/>
|
||||
<field name="state">open</field>
|
||||
<field name="hr_responsible_id" ref="base.user_admin"/>
|
||||
</record>
|
||||
|
||||
<record id="hr_contract_gilles_gravie" model="hr.contract">
|
||||
<field name="name">Contract For Marc Demo</field>
|
||||
<field name="date_start" eval="time.strftime('%Y-%m')+'-1'"/>
|
||||
<field name="date_end" eval="time.strftime('%Y')+'-12-31'"/>
|
||||
<field name="structure_type_id" ref="hr_contract.structure_type_employee"/>
|
||||
<field name="employee_id" ref="hr.employee_qdp"/>
|
||||
<field name="notes">This is Marc Demo's contract</field>
|
||||
<field eval="5000.0" name="wage"/>
|
||||
<field name="hr_responsible_id" ref="base.user_admin"/>
|
||||
</record>
|
||||
|
||||
<record id="hr_contract.hr_contract_admin" model="hr.contract">
|
||||
<field name="structure_type_id" ref="hr_contract.structure_type_employee"/>
|
||||
</record>
|
||||
|
||||
<record id="hr_contract.hr_contract_al" model="hr.contract">
|
||||
<field name="structure_type_id" ref="hr_contract.structure_type_employee"/>
|
||||
</record>
|
||||
|
||||
<record id="hr_contract.hr_contract_mit" model="hr.contract">
|
||||
<field name="structure_type_id" ref="hr_contract.structure_type_employee"/>
|
||||
</record>
|
||||
|
||||
<record id="hr_contract.hr_contract_stw" model="hr.contract">
|
||||
<field name="structure_type_id" ref="hr_contract.structure_type_employee"/>
|
||||
</record>
|
||||
|
||||
<record id="hr_contract.hr_contract_qdp" model="hr.contract">
|
||||
<field name="structure_type_id" ref="hr_contract.structure_type_employee"/>
|
||||
</record>
|
||||
|
||||
<record id="hr_contract.hr_contract_han" model="hr.contract">
|
||||
<field name="structure_type_id" ref="hr_contract.structure_type_employee"/>
|
||||
</record>
|
||||
|
||||
<record id="hr_contract.hr_contract_niv" model="hr.contract">
|
||||
<field name="structure_type_id" ref="hr_contract.structure_type_employee"/>
|
||||
</record>
|
||||
|
||||
<record id="hr_contract.hr_contract_jth" model="hr.contract">
|
||||
<field name="structure_type_id" ref="hr_contract.structure_type_employee"/>
|
||||
</record>
|
||||
|
||||
<record id="hr_contract.hr_contract_chs" model="hr.contract">
|
||||
<field name="structure_type_id" ref="hr_contract.structure_type_employee"/>
|
||||
</record>
|
||||
|
||||
<record id="hr_contract.hr_contract_jve" model="hr.contract">
|
||||
<field name="structure_type_id" ref="hr_contract.structure_type_employee"/>
|
||||
</record>
|
||||
|
||||
<record id="hr_contract.hr_contract_fme" model="hr.contract">
|
||||
<field name="structure_type_id" ref="hr_contract.structure_type_employee"/>
|
||||
</record>
|
||||
|
||||
<record id="hr_contract.hr_contract_fpi" model="hr.contract">
|
||||
<field name="structure_type_id" ref="hr_contract.structure_type_employee"/>
|
||||
</record>
|
||||
|
||||
<record id="hr_contract.hr_contract_vad" model="hr.contract">
|
||||
<field name="structure_type_id" ref="hr_contract.structure_type_employee"/>
|
||||
</record>
|
||||
|
||||
<!-- Salary Attachments -->
|
||||
<record id="child_support" model="hr.salary.attachment">
|
||||
<field name="employee_ids" eval="[(4, ref('hr.employee_qdp'))]"/>
|
||||
<field name="monthly_amount">7500</field>
|
||||
<field name="other_input_type_id" ref="hr_payroll.input_child_support"/>
|
||||
<field name="date_start" eval="DateTime.today()"/>
|
||||
<field name="description">Child Support</field>
|
||||
</record>
|
||||
|
||||
<!-- Work entries -->
|
||||
<record id="hr_work_entry_contract.work_entry_type_long_leave" model="hr.work.entry.type">
|
||||
<field name="round_days">FULL</field>
|
||||
<field name="round_days_type">DOWN</field>
|
||||
</record>
|
||||
|
||||
<function model="hr.employee" name="generate_work_entries">
|
||||
<value model="hr.employee" eval="obj().search([]).ids"/>
|
||||
<value eval="(DateTime.today() + relativedelta(day=1, months=-1))"/>
|
||||
<value eval="(DateTime.today() + relativedelta(day=31))"/>
|
||||
</function>
|
||||
|
||||
<!-- Payslip batch Year -1 -->
|
||||
<record id="hr_payslip_batch_year_1" model="hr.payslip.run">
|
||||
<field name="name" eval="'Late batch salary ' + (DateTime.today() - relativedelta(years=1, month=1, day=1)).strftime('%B %Y')"/>
|
||||
<field name="date_start" eval="(DateTime.today() - relativedelta(years=1, month=1, day=1)).strftime('%Y-%m-%d')"/>
|
||||
<field name="date_end" eval="(DateTime.today() - relativedelta(years=1, month=2, day=1)).strftime('%Y-%m-%d')"/>
|
||||
<field name="company_id" ref="base.main_company"/>
|
||||
</record>
|
||||
|
||||
<!-- Payslip batch Year -2 -->
|
||||
<record id="hr_payslip_batch_year_2" model="hr.payslip.run">
|
||||
<field name="name" eval="(DateTime.today() - relativedelta(years=2, month=1, day=1)).strftime('%B %Y') + ' batch'"/>
|
||||
<field name="date_start" eval="(DateTime.today() - relativedelta(years=2) + relativedelta(day=1)).strftime('%Y-%m-%d')"/>
|
||||
<field name="date_end" eval="(DateTime.today() - relativedelta(years=2) + relativedelta(months=12, day=1)).strftime('%Y-%m-%d')"/>
|
||||
<field name="company_id" ref="base.main_company"/>
|
||||
</record>
|
||||
|
||||
<!-- Payslip batch Month 0-->
|
||||
<record id="hr_payslip_batch_latest" model="hr.payslip.run">
|
||||
<field name="name" eval="'Batch for ' + time.strftime('%B %Y')"/>
|
||||
<field name="date_start" eval="time.strftime('%Y-%m-1')"/>
|
||||
<field name="date_end" eval="(DateTime.today() + relativedelta(months=1, day=1) - relativedelta(days=1)).strftime('%Y-%m-%d')"/>
|
||||
<field name="company_id" ref="base.main_company"/>
|
||||
</record>
|
||||
|
||||
<record id="hr_payslip_batch_new_batch" model="hr.payslip.run">
|
||||
<field name="name" eval="'Correction batch for ' + (DateTime.today() - relativedelta(day=1)).strftime('%B %Y')"></field>
|
||||
<field name="date_start" eval="(DateTime.today() - relativedelta(day=1)).strftime('%Y-%m-%d')"/>
|
||||
<field name="date_end" eval="(DateTime.today() + relativedelta(months=1, day=1)).strftime('%Y-%m-%d')"/>
|
||||
<field name="company_id" ref="base.main_company"/>
|
||||
</record>
|
||||
|
||||
<!-- Payslips Batch Month - 1 -->
|
||||
<record id="hr_payslip_batch_month_1" model="hr.payslip.run">
|
||||
<field name="name" eval="(DateTime.today() - relativedelta(months=1, day=1)).strftime('%B %Y')"/>
|
||||
<field name="date_start" eval="(DateTime.today() - relativedelta(months=1, day=1)).strftime('%Y-%m-%d')"/>
|
||||
<field name="date_end" eval="(DateTime.today() - relativedelta(day=1)).strftime('%Y-%m-%d')"/>
|
||||
<field name="company_id" ref="base.main_company"/>
|
||||
</record>
|
||||
|
||||
<!-- Payslip Batch Month - 2 -->
|
||||
<record id="hr_payslip_batch_month_2" model="hr.payslip.run">
|
||||
<field name="name" eval="'Pay of the month ' + (DateTime.today() - relativedelta(months=2, day=1)).strftime('%B %Y')"/>
|
||||
<field name="date_start" eval="(DateTime.today() - relativedelta(months=2, day=1)).strftime('%Y-%m-%d')"/>
|
||||
<field name="date_end" eval="(DateTime.today() - relativedelta(months=1, day=1)).strftime('%Y-%m-%d')"/>
|
||||
<field name="company_id" ref="base.main_company"/>
|
||||
</record>
|
||||
|
||||
<!-- Payslip Admin -->
|
||||
<record id="hr_payslip_admin_latest_0" model="hr.payslip">
|
||||
<field name="name" eval="'Mitchell Admin 1 ' + time.strftime('%B %Y')"/>
|
||||
<field name="contract_id" ref="hr_contract.hr_contract_admin"/>
|
||||
<field name="date_from" eval="time.strftime('%Y-%m-1')"/>
|
||||
<field name="date_to" eval="time.strftime('%Y-%m-15')"/>
|
||||
<field name="employee_id" ref="hr.employee_admin"/>
|
||||
<field name="struct_id" ref="hr_payroll.structure_002"/>
|
||||
<field name="number">SLIP1001</field>
|
||||
<field name="payslip_run_id" ref="hr_payslip_batch_latest"/>
|
||||
<field name="company_id" ref="base.main_company"/>
|
||||
</record>
|
||||
|
||||
<record id="hr_payslip_admin_latest_1" model="hr.payslip">
|
||||
<field name="name" eval="'Mitchell Admin 2 ' + time.strftime('%B %Y')"/>
|
||||
<field name="contract_id" ref="hr_contract.hr_contract_admin_new"/>
|
||||
<field name="date_from" eval="time.strftime('%Y-%m-16')"/>
|
||||
<field name="date_to" eval="(DateTime.today() + relativedelta(months=1, day=1) - relativedelta(days=1)).strftime('%Y-%m-%d')"/>
|
||||
<field name="employee_id" ref="hr.employee_admin"/>
|
||||
<field name="struct_id" ref="hr_payroll.structure_002"/>
|
||||
<field name="number">SLIP1002</field>
|
||||
<field name="payslip_run_id" ref="hr_payslip_batch_latest"/>
|
||||
<field name="company_id" ref="base.main_company"/>
|
||||
</record>
|
||||
|
||||
<record id="hr_payslip_admin_month_1" model="hr.payslip">
|
||||
<field name="name" eval="'Mitchell Admin ' + (DateTime.today() - relativedelta(months=1, day=1)).strftime('%B %Y')"/>
|
||||
<field name="contract_id" ref="hr_contract.hr_contract_qdp"/>
|
||||
<field name="date_from" eval="(DateTime.today() - relativedelta(months=1, day=1)).strftime('%Y-%m-%d')"/>
|
||||
<field name="date_to" eval="(DateTime.today() - relativedelta(day=1)).strftime('%Y-%m-%d')"/>
|
||||
<field name="employee_id" ref="hr.employee_admin"/>
|
||||
<field name="struct_id" ref="hr_payroll.structure_002"/>
|
||||
<field name="number">SLIP1004</field>
|
||||
<field name="payslip_run_id" ref="hr_payslip_batch_month_1"/>
|
||||
<field name="company_id" ref="base.main_company"/>
|
||||
</record>
|
||||
|
||||
<record id="hr_payslip_admin_month_1_1" model="hr.payslip">
|
||||
<field name="name" eval="'Mitchell Admin ' + (DateTime.today() - relativedelta(months=1, day=1)).strftime('%B %Y')"/>
|
||||
<field name="contract_id" ref="hr_contract.hr_contract_qdp"/>
|
||||
<field name="date_from" eval="(DateTime.today() - relativedelta(months=1, day=1)).strftime('%Y-%m-%d')"/>
|
||||
<field name="date_to" eval="(DateTime.today() - relativedelta(day=1)).strftime('%Y-%m-%d')"/>
|
||||
<field name="employee_id" ref="hr.employee_admin"/>
|
||||
<field name="struct_id" ref="hr_payroll.structure_002"/>
|
||||
<field name="number">SLIP1005</field>
|
||||
<field name="payslip_run_id" ref="hr_payslip_batch_month_1"/>
|
||||
<field name="company_id" ref="base.main_company"/>
|
||||
</record>
|
||||
|
||||
<record id="hr_payslip_admin_month_2" model="hr.payslip">
|
||||
<field name="name" eval="'Mitchell Admin ' + (DateTime.today() - relativedelta(months=2, day=1)).strftime('%B %Y')"/>
|
||||
<field name="contract_id" ref="hr_contract.hr_contract_qdp"/>
|
||||
<field name="date_from" eval="(DateTime.today() - relativedelta(months=2, day=1)).strftime('%Y-%m-%d')"/>
|
||||
<field name="date_to" eval="(DateTime.today() - relativedelta(months=1, day=1)).strftime('%Y-%m-%d')"/>
|
||||
<field name="employee_id" ref="hr.employee_admin"/>
|
||||
<field name="struct_id" ref="hr_payroll.structure_002"/>
|
||||
<field name="number">SLIP1006</field>
|
||||
<field name="payslip_run_id" ref="hr_payslip_batch_month_2"/>
|
||||
<field name="company_id" ref="base.main_company"/>
|
||||
</record>
|
||||
|
||||
<!-- Payslip Demo -->
|
||||
<record id="hr_payslip_demo_latest" model="hr.payslip">
|
||||
<field name="name" eval="'Marc Demo ' + time.strftime('%B %Y')"/>
|
||||
<field name="contract_id" ref="hr_contract.hr_contract_qdp"/>
|
||||
<field name="date_from" eval="time.strftime('%Y-%m-1')"/>
|
||||
<field name="date_to" eval="(DateTime.today() + relativedelta(months=1, day=1) - relativedelta(days=1)).strftime('%Y-%m-%d')"/>
|
||||
<field name="employee_id" ref="hr.employee_qdp"/>
|
||||
<field name="struct_id" ref="hr_payroll.structure_002"/>
|
||||
<field name="number">SLIP1003</field>
|
||||
<field name="payslip_run_id" ref="hr_payslip_batch_latest"/>
|
||||
<field name="company_id" ref="base.main_company"/>
|
||||
</record>
|
||||
|
||||
<!-- Year Payslips -->
|
||||
<record id="hr_payslip_admin_year_1" model="hr.payslip">
|
||||
<field name="name">Mitchell Admin Long Payslip</field>
|
||||
<field name="contract_id" ref="hr_contract.hr_contract_admin_new"/>
|
||||
<field name="date_from" eval="(DateTime.today() - relativedelta(years=1, month=1, day=1)).strftime('%Y-%m-%d')"/>
|
||||
<field name="date_to" eval="(DateTime.today() - relativedelta(years=1, month=2, day=1)).strftime('%Y-%m-%d')"/>
|
||||
<field name="employee_id" ref="hr.employee_admin"/>
|
||||
<field name="struct_id" ref="hr_payroll.structure_002"/>
|
||||
<field name="number">SLIP1007</field>
|
||||
<field name="payslip_run_id" ref="hr_payslip_batch_year_1"/>
|
||||
<field name="company_id" ref="base.main_company"/>
|
||||
</record>
|
||||
|
||||
<record id="hr_payslip_demo_year_2" model="hr.payslip">
|
||||
<field name="name">Marc Demo Long Payslip</field>
|
||||
<field name="contract_id" ref="hr_contract.hr_contract_admin_new"/>
|
||||
<field name="date_from" eval="(DateTime.today() - relativedelta(years=2) + relativedelta(day=1)).strftime('%Y-%m-%d')"/>
|
||||
<field name="date_to" eval="(DateTime.today() - relativedelta(years=2) + relativedelta(months=1, day=1)).strftime('%Y-%m-%d')"/>
|
||||
<field name="employee_id" ref="hr.employee_qdp"/>
|
||||
<field name="struct_id" ref="hr_payroll.structure_002"/>
|
||||
<field name="number">SLIP1008</field>
|
||||
<field name="payslip_run_id" ref="hr_payslip_batch_year_2"/>
|
||||
<field name="company_id" ref="base.main_company"/>
|
||||
</record>
|
||||
|
||||
<!-- Compute Sheets -->
|
||||
<function model="hr.payslip" name="compute_sheet">
|
||||
<value model="hr.payslip" eval="[
|
||||
ref('hr_payslip_admin_latest_0'),
|
||||
ref('hr_payslip_admin_latest_1'),
|
||||
ref('hr_payslip_demo_latest'),
|
||||
ref('hr_payslip_admin_month_1'),
|
||||
ref('hr_payslip_admin_month_2'),
|
||||
ref('hr_payslip_admin_month_1_1'),
|
||||
ref('hr_payslip_admin_year_1'),
|
||||
ref('hr_payslip_demo_year_2')
|
||||
]"/>
|
||||
</function>
|
||||
|
||||
<!-- Validate Batch -->
|
||||
<function model="hr.payslip.run" name="action_open">
|
||||
<value model="hr.payslip.run" eval="[ref('hr_payslip_batch_latest'), ref('hr_payslip_batch_month_1'), ref('hr_payslip_batch_month_2')]"/>
|
||||
</function>
|
||||
|
||||
<function model="hr.payslip.run" name="action_validate">
|
||||
<value model="hr.payslip.run" eval="[ref('hr_payslip_batch_month_1'), ref('hr_payslip_batch_month_2')]"/>
|
||||
</function>
|
||||
|
||||
<!-- Set batch to paid -->
|
||||
<function model="hr.payslip.run" name="action_paid">
|
||||
<value model="hr.payslip.run" eval="[ref('hr_payslip_batch_month_2')]"/>
|
||||
</function>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<record id="seq_salary_slip" model="ir.sequence">
|
||||
<field name="name">Salary Slip</field>
|
||||
<field name="code">salary.slip</field>
|
||||
<field name="prefix">SLIP/</field>
|
||||
<field name="padding">3</field>
|
||||
<field name="company_id" eval="False"/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="payroll_tours" model="web_tour.tour">
|
||||
<field name="name">payroll_tours</field>
|
||||
<field name="sequence">80</field>
|
||||
<field name="rainbow_man_message"><![CDATA[
|
||||
<strong>Congrats, Your first payslip is now finished. It's time for you to explore the Payroll app by yourself.</strong>
|
||||
]]></field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version='1.0' encoding='UTF-8' ?>
|
||||
<odoo>
|
||||
<record id="ir_actions_server_action_open_reporting" model="ir.actions.server">
|
||||
<field name="name">Open Payroll Reporting</field>
|
||||
<field name="model_id" ref="hr_payroll.model_hr_payroll_report"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">
|
||||
action = env['hr.payroll.report']._get_action()
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="ir_cron_update_payroll_data" model="ir.cron">
|
||||
<field name="name">Payroll: Update data</field>
|
||||
<field name="model_id" ref="hr_payroll.model_hr_payslip"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model._update_payroll_data()</field>
|
||||
<field name="active" eval="True"/>
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">months</field>
|
||||
<field name="nextcall" eval="DateTime.now().replace(day=20, hour=3, minute=0)"/>
|
||||
</record>
|
||||
|
||||
<record id="ir_cron_generate_payslip_pdfs" model="ir.cron">
|
||||
<field name="name">Payroll: Generate pdfs</field>
|
||||
<field name="model_id" ref="hr_payroll.model_hr_payslip"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model._cron_generate_pdf()</field>
|
||||
<field name="active" eval="True"/>
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">hours</field>
|
||||
<field name="nextcall" eval="(DateTime.now() + timedelta(hours=1))"/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo><data noupdate="1">
|
||||
|
||||
<record id="mail_activity_data_hr_payslip_negative_net" model="mail.activity.type">
|
||||
<field name="name">Negative Net to Report</field>
|
||||
<field name="icon">fa-usd</field>
|
||||
<field name="sequence">100</field>
|
||||
<field name="res_model">hr.payslip</field>
|
||||
</record>
|
||||
|
||||
</data></odoo>
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo><data noupdate="1">
|
||||
|
||||
<record id="mail_template_new_payslip" model="mail.template">
|
||||
<field name="name">Payroll: New Payslip</field>
|
||||
<field name="model_id" ref="hr_payroll.model_hr_payslip"/>
|
||||
<field name="subject">{{ object.employee_id.name }}, a new payslip is available for you</field>
|
||||
<field name="email_from">{{ user.email_formatted }}</field>
|
||||
<field name="partner_to">{{ object.employee_id.work_contact_id.id }}</field>
|
||||
<field name="description">Sent to employee to notify them about their new payslip</field>
|
||||
<field name="body_html" type="html">
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="width:100%; margin:0px auto;"><tbody>
|
||||
<tr><td valign="top" style="text-align: left; font-size: 14px;">
|
||||
Dear <t t-esc="object.employee_id.name"></t>, a new payslip is available for you.<br/><br/>
|
||||
Please find the PDF in your employee portal.<br/><br/>
|
||||
Have a nice day,<br/>
|
||||
The HR Team
|
||||
</td></tr>
|
||||
</tbody></table>
|
||||
</field>
|
||||
<field name="lang">{{ object.employee_id.lang }}</field>
|
||||
<field name="auto_delete" eval="True"/>
|
||||
</record>
|
||||
|
||||
</data></odoo>
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<template id="hr_payroll_note_demo_content">
|
||||
<p>Board meeting summary:</p>
|
||||
<p>We have to improve our Payroll flow with the new Odoo process</p>
|
||||
<p><br/></p>
|
||||
<p>On the <t t-out="date_today"/></p>
|
||||
<p>1. Save documents to terminated employees</p>
|
||||
<p>2. Index salaries for Marketing department</p>
|
||||
<p>3. Create a new contract for Marc Demo with his new position</p>
|
||||
<p><br/></p>
|
||||
<ul class="o_checklist">
|
||||
<li id="checkId-130162183830">Give insurance card to new registered employees</li>
|
||||
</ul>
|
||||
<p>Links:</p>
|
||||
<p>
|
||||
Payroll tips & tricks: <a href="https://www.odoo.com/fr_FR/slides/slide/manage-payroll-1002" target="_blank">
|
||||
https://www.odoo.com/fr_FR/slides/slide/manage-payroll-1002
|
||||
</a>
|
||||
</p>
|
||||
</template>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
<?xml version="1.0"?>
|
||||
<odoo>
|
||||
<record id="paperformat_euro_light" model="report.paperformat">
|
||||
<field name="name">A4</field>
|
||||
<field name="default" eval="True" />
|
||||
<field name="format">A4</field>
|
||||
<field name="page_height">0</field>
|
||||
<field name="page_width">0</field>
|
||||
<field name="orientation">Portrait</field>
|
||||
<field name="margin_top">32</field>
|
||||
<field name="margin_bottom">0</field>
|
||||
<field name="margin_left">0</field>
|
||||
<field name="margin_right">0</field>
|
||||
<field name="header_line" eval="False" />
|
||||
<field name="header_spacing">32</field>
|
||||
<field name="dpi">90</field>
|
||||
<field name="css_margins" eval="True" />
|
||||
</record>
|
||||
|
||||
<record id="paperformat_us_light" model="report.paperformat">
|
||||
<field name="name">US Letter</field>
|
||||
<field name="default" eval="True" />
|
||||
<field name="format">Letter</field>
|
||||
<field name="page_height">0</field>
|
||||
<field name="page_width">0</field>
|
||||
<field name="orientation">Portrait</field>
|
||||
<field name="margin_top">32</field>
|
||||
<field name="margin_bottom">0</field>
|
||||
<field name="margin_left">0</field>
|
||||
<field name="margin_right">0</field>
|
||||
<field name="header_line" eval="False" />
|
||||
<field name="header_spacing">32</field>
|
||||
<field name="dpi">90</field>
|
||||
<field name="css_margins" eval="True" />
|
||||
</record>
|
||||
</odoo>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,29 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import hr_contract
|
||||
from . import hr_contract_history
|
||||
from . import hr_employee
|
||||
from . import res_config_settings
|
||||
from . import hr_payroll_dashboard_warning
|
||||
from . import hr_payroll_structure
|
||||
from . import hr_payroll_structure_type
|
||||
from . import hr_salary_rule
|
||||
from . import hr_salary_rule_category
|
||||
from . import hr_payslip
|
||||
from . import hr_payslip_line
|
||||
from . import hr_payslip_worked_days
|
||||
from . import hr_payslip_input
|
||||
from . import hr_payslip_input_type
|
||||
from . import hr_payslip_run
|
||||
from . import resource_calendar
|
||||
from . import res_company
|
||||
from . import res_users
|
||||
from . import hr_rule_parameter
|
||||
from . import hr_work_entry_type
|
||||
from . import hr_work_entry
|
||||
from . import hr_salary_attachment
|
||||
from . import note
|
||||
from . import hr_payroll_employee_declaration
|
||||
from . import hr_payroll_declaration_mixin
|
||||
from . import hr_payroll_headcount
|
||||
from . import hr_work_entry_export_mixin
|
||||
|
|
@ -0,0 +1,471 @@
|
|||
# -*- coding:utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import date, datetime
|
||||
from collections import defaultdict
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.osv import expression
|
||||
|
||||
import pytz
|
||||
|
||||
class HrContract(models.Model):
|
||||
_inherit = 'hr.contract'
|
||||
_description = 'Employee Contract'
|
||||
|
||||
schedule_pay = fields.Selection([
|
||||
('annually', 'Annually'),
|
||||
('semi-annually', 'Semi-annually'),
|
||||
('quarterly', 'Quarterly'),
|
||||
('bi-monthly', 'Bi-monthly'),
|
||||
('monthly', 'Monthly'),
|
||||
('semi-monthly', 'Semi-monthly'),
|
||||
('bi-weekly', 'Bi-weekly'),
|
||||
('weekly', 'Weekly'),
|
||||
('daily', 'Daily')],
|
||||
compute='_compute_schedule_pay', store=True, readonly=False)
|
||||
resource_calendar_id = fields.Many2one(default=lambda self: self.env.company.resource_calendar_id,
|
||||
help='''Employee's working schedule.
|
||||
When left empty, the employee is considered to have a fully flexible schedule, allowing them to work without any time limit, anytime of the week.
|
||||
'''
|
||||
)
|
||||
hours_per_week = fields.Float(related='resource_calendar_id.hours_per_week')
|
||||
full_time_required_hours = fields.Float(related='resource_calendar_id.full_time_required_hours')
|
||||
is_fulltime = fields.Boolean(related='resource_calendar_id.is_fulltime')
|
||||
wage_type = fields.Selection([
|
||||
('monthly', 'Fixed Wage'),
|
||||
('hourly', 'Hourly Wage')
|
||||
], compute='_compute_wage_type', store=True, readonly=False)
|
||||
hourly_wage = fields.Monetary('Hourly Wage', tracking=True, help="Employee's hourly gross wage.")
|
||||
payslips_count = fields.Integer("# Payslips", compute='_compute_payslips_count', groups="hr_payroll.group_hr_payroll_user")
|
||||
calendar_changed = fields.Boolean(help="Whether the previous or next contract has a different schedule or not")
|
||||
|
||||
time_credit = fields.Boolean('Part Time', readonly=False)
|
||||
work_time_rate = fields.Float(
|
||||
compute='_compute_work_time_rate', store=True, readonly=True,
|
||||
string='Work time rate', help='Work time rate versus full time working schedule.')
|
||||
standard_calendar_id = fields.Many2one(
|
||||
'resource.calendar', default=lambda self: self.env.company.resource_calendar_id, readonly=True,
|
||||
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]")
|
||||
time_credit_type_id = fields.Many2one(
|
||||
'hr.work.entry.type', string='Part Time Work Entry Type',
|
||||
domain=[('is_leave', '=', True)],
|
||||
help="The work entry type used when generating work entries to fit full time working schedule.")
|
||||
salary_attachments_count = fields.Integer(related='employee_id.salary_attachment_count')
|
||||
|
||||
@api.depends('structure_type_id')
|
||||
def _compute_schedule_pay(self):
|
||||
for contract in self:
|
||||
contract.schedule_pay = contract.structure_type_id.default_schedule_pay
|
||||
|
||||
@api.depends('structure_type_id')
|
||||
def _compute_wage_type(self):
|
||||
for contract in self:
|
||||
contract.wage_type = contract.structure_type_id.wage_type
|
||||
|
||||
@api.depends('time_credit', 'resource_calendar_id.hours_per_week', 'standard_calendar_id.hours_per_week')
|
||||
def _compute_work_time_rate(self):
|
||||
for contract in self:
|
||||
if contract.time_credit and contract.structure_type_id.default_resource_calendar_id:
|
||||
hours_per_week = contract.resource_calendar_id.hours_per_week
|
||||
hours_per_week_ref = contract.structure_type_id.default_resource_calendar_id.hours_per_week
|
||||
else:
|
||||
hours_per_week = contract.resource_calendar_id.hours_per_week
|
||||
hours_per_week_ref = contract.company_id.resource_calendar_id.hours_per_week
|
||||
if not hours_per_week and not hours_per_week_ref:
|
||||
contract.work_time_rate = 1
|
||||
else:
|
||||
contract.work_time_rate = hours_per_week / (hours_per_week_ref or hours_per_week)
|
||||
|
||||
def _compute_payslips_count(self):
|
||||
count_data = self.env['hr.payslip']._read_group(
|
||||
[('contract_id', 'in', self.ids)],
|
||||
['contract_id'],
|
||||
['__count'])
|
||||
mapped_counts = {contract.id: count for contract, count in count_data}
|
||||
for contract in self:
|
||||
contract.payslips_count = mapped_counts.get(contract.id, 0)
|
||||
|
||||
def copy_data(self, default=None):
|
||||
vals_list = super().copy_data(default=default)
|
||||
return [dict(vals, name=self.env._("%s (copy)", contract.name)) for contract, vals in zip(self, vals_list)]
|
||||
|
||||
def _get_salary_costs_factor(self):
|
||||
self.ensure_one()
|
||||
factors = {
|
||||
"annually": 1,
|
||||
"semi-annually": 2,
|
||||
"quarterly": 4,
|
||||
"bi-monthly": 6,
|
||||
"monthly": 12,
|
||||
"semi-monthly": 24,
|
||||
"bi-weekly": 26,
|
||||
"weekly": 52,
|
||||
"daily": 52 * (self.resource_calendar_id._get_days_per_week() if self.resource_calendar_id else 5),
|
||||
}
|
||||
return factors.get(self.schedule_pay, super()._get_salary_costs_factor())
|
||||
|
||||
def _is_same_occupation(self, contract):
|
||||
self.ensure_one()
|
||||
contract_type = self.contract_type_id
|
||||
work_time_rate = self.resource_calendar_id.work_time_rate
|
||||
same_type = contract_type == contract.contract_type_id and work_time_rate == contract.resource_calendar_id.work_time_rate
|
||||
return same_type
|
||||
|
||||
def _get_occupation_dates(self, include_future_contracts=False):
|
||||
# Takes several contracts and returns all the contracts under the same occupation (i.e. the same
|
||||
# work rate + the date_from and date_to)
|
||||
# include_futur_contracts will use draft contracts if the start_date is posterior compared to current date
|
||||
# NOTE: this does not take kanban_state in account
|
||||
result = []
|
||||
done_contracts = self.env['hr.contract']
|
||||
date_today = fields.Date.today()
|
||||
|
||||
def remove_gap(contract, other_contracts, before=False):
|
||||
# We do not consider a gap of more than 4 days to be a same occupation
|
||||
# other_contracts is considered to be ordered correctly in function of `before`
|
||||
current_date = contract.date_start if before else contract.date_end
|
||||
for i, other_contract in enumerate(other_contracts):
|
||||
if not current_date:
|
||||
return other_contracts[0:i]
|
||||
if before:
|
||||
# Consider contract.date_end being false as an error and cut the loop
|
||||
gap = (current_date - (other_contract.date_end or date(2100, 1, 1))).days
|
||||
current_date = other_contract.date_start
|
||||
else:
|
||||
gap = (other_contract.date_start - current_date).days
|
||||
current_date = other_contract.date_end
|
||||
if gap >= 4:
|
||||
return other_contracts[0:i]
|
||||
return other_contracts
|
||||
|
||||
for contract in self:
|
||||
if contract in done_contracts:
|
||||
continue
|
||||
contracts = contract # hr.contract(38,)
|
||||
date_from = contract.date_start
|
||||
date_to = contract.date_end
|
||||
history = self.env['hr.contract.history'].search([('employee_id', '=', contract.employee_id.id)], limit=1)
|
||||
all_contracts = history.contract_ids.filtered(
|
||||
lambda c: (
|
||||
c.active and c != contract and
|
||||
(c.state in ['open', 'close'] or (include_future_contracts and c.state == 'draft' and c.date_start >= date_today))
|
||||
)
|
||||
) # hr.contract(29, 37, 38, 39, 41) -> hr.contract(29, 37, 39, 41)
|
||||
before_contracts = all_contracts.filtered(lambda c: c.date_start < contract.date_start) # hr.contract(39, 41)
|
||||
before_contracts = remove_gap(contract, before_contracts, before=True)
|
||||
after_contracts = all_contracts.filtered(lambda c: c.date_start > contract.date_start).sorted(key='date_start') # hr.contract(37, 29)
|
||||
after_contracts = remove_gap(contract, after_contracts)
|
||||
|
||||
for before_contract in before_contracts:
|
||||
if contract._is_same_occupation(before_contract):
|
||||
date_from = before_contract.date_start
|
||||
contracts |= before_contract
|
||||
else:
|
||||
break
|
||||
|
||||
for after_contract in after_contracts:
|
||||
if contract._is_same_occupation(after_contract):
|
||||
date_to = after_contract.date_end
|
||||
contracts |= after_contract
|
||||
else:
|
||||
break
|
||||
result.append((contracts, date_from, date_to))
|
||||
done_contracts |= contracts
|
||||
return result
|
||||
|
||||
def _compute_calendar_changed(self):
|
||||
date_today = fields.Date.today()
|
||||
contract_resets = self.filtered(
|
||||
lambda c: (
|
||||
not c.date_start or not c.employee_id or not c.resource_calendar_id or not c.active or
|
||||
not (c.state in ('open', 'close') or (c.state == 'draft' and c.date_start >= date_today)) # make sure to include futur contracts
|
||||
)
|
||||
)
|
||||
contract_resets.filtered(lambda c: c.calendar_changed).write({'calendar_changed': False})
|
||||
self -= contract_resets
|
||||
occupation_dates = self._get_occupation_dates(include_future_contracts=True)
|
||||
occupation_by_employee = defaultdict(list)
|
||||
for row in occupation_dates:
|
||||
occupation_by_employee[row[0][0].employee_id.id].append(row)
|
||||
contract_changed = self.env['hr.contract']
|
||||
for occupations in occupation_by_employee.values():
|
||||
if len(occupations) == 1:
|
||||
continue
|
||||
for i in range(len(occupations) - 1):
|
||||
current_row = occupations[i]
|
||||
next_row = occupations[i + 1]
|
||||
contract_changed |= current_row[0][-1]
|
||||
contract_changed |= next_row[0][0]
|
||||
contract_changed.filtered(lambda c: not c.calendar_changed).write({'calendar_changed': True})
|
||||
(self - contract_changed).filtered(lambda c: c.calendar_changed).write({'calendar_changed': False})
|
||||
|
||||
def _get_contract_work_entries_values(self, date_start, date_stop):
|
||||
contract_vals = super()._get_contract_work_entries_values(date_start, date_stop)
|
||||
contract_vals += self._get_contract_credit_time_values(date_start, date_stop)
|
||||
return contract_vals
|
||||
|
||||
def _get_contract_credit_time_values(self, date_start, date_stop):
|
||||
contract_vals = []
|
||||
for contract in self:
|
||||
if not contract.time_credit or not contract.time_credit_type_id:
|
||||
continue
|
||||
|
||||
employee = contract.employee_id
|
||||
resource = employee.resource_id
|
||||
calendar = contract.resource_calendar_id
|
||||
standard_calendar = contract.standard_calendar_id
|
||||
|
||||
standard_attendances = standard_calendar._work_intervals_batch(
|
||||
pytz.utc.localize(date_start) if not date_start.tzinfo else date_start,
|
||||
pytz.utc.localize(date_stop) if not date_stop.tzinfo else date_stop,
|
||||
resources=resource,
|
||||
compute_leaves=False)[resource.id]
|
||||
|
||||
attendances = calendar._work_intervals_batch(
|
||||
pytz.utc.localize(date_start) if not date_start.tzinfo else date_start,
|
||||
pytz.utc.localize(date_stop) if not date_stop.tzinfo else date_stop,
|
||||
resources=resource,
|
||||
compute_leaves=False)[resource.id]
|
||||
|
||||
credit_time_intervals = standard_attendances - attendances
|
||||
|
||||
for interval in credit_time_intervals:
|
||||
work_entry_type_id = contract.time_credit_type_id
|
||||
new_vals = {
|
||||
'name': "%s: %s" % (work_entry_type_id.name, employee.name),
|
||||
'date_start': interval[0].astimezone(pytz.utc).replace(tzinfo=None),
|
||||
'date_stop': interval[1].astimezone(pytz.utc).replace(tzinfo=None),
|
||||
'work_entry_type_id': work_entry_type_id.id,
|
||||
'employee_id': employee.id,
|
||||
'contract_id': contract.id,
|
||||
'company_id': contract.company_id.id,
|
||||
'state': 'draft',
|
||||
'is_credit_time': True,
|
||||
}
|
||||
contract_vals.append(new_vals)
|
||||
return contract_vals
|
||||
|
||||
def _get_work_time_rate(self):
|
||||
self.ensure_one()
|
||||
return self.work_time_rate if self.time_credit else 1.0
|
||||
|
||||
def _get_contract_wage_field(self):
|
||||
self.ensure_one()
|
||||
if self.wage_type == 'hourly':
|
||||
return 'hourly_wage'
|
||||
return super()._get_contract_wage_field()
|
||||
|
||||
@api.model
|
||||
def _recompute_calendar_changed(self, employee_ids):
|
||||
contract_ids = self.search([('employee_id', 'in', employee_ids.ids)], order='date_start asc')
|
||||
if not contract_ids:
|
||||
return
|
||||
contract_ids._compute_calendar_changed()
|
||||
|
||||
def action_open_payslips(self):
|
||||
self.ensure_one()
|
||||
action = self.env["ir.actions.actions"]._for_xml_id("hr_payroll.action_view_hr_payslip_month_form")
|
||||
action.update({'domain': [('contract_id', '=', self.id)]})
|
||||
return action
|
||||
|
||||
def _index_contracts(self):
|
||||
action = self.env["ir.actions.actions"]._for_xml_id("hr_payroll.action_hr_payroll_index")
|
||||
action['context'] = repr(self.env.context)
|
||||
return action
|
||||
|
||||
def _get_work_hours_domain(self, date_from, date_to, domain=None, inside=True):
|
||||
if domain is None:
|
||||
domain = []
|
||||
domain = expression.AND([domain, [
|
||||
('state', 'in', ['validated', 'draft']),
|
||||
('contract_id', 'in', self.ids),
|
||||
]])
|
||||
if inside:
|
||||
domain = expression.AND([domain, [
|
||||
('date_start', '>=', date_from),
|
||||
('date_stop', '<=', date_to)]])
|
||||
else:
|
||||
domain = expression.AND([domain, [
|
||||
'|', '|',
|
||||
'&', '&',
|
||||
('date_start', '>=', date_from),
|
||||
('date_start', '<', date_to),
|
||||
('date_stop', '>', date_to),
|
||||
'&', '&',
|
||||
('date_start', '<', date_from),
|
||||
('date_stop', '<=', date_to),
|
||||
('date_stop', '>', date_from),
|
||||
'&',
|
||||
('date_start', '<', date_from),
|
||||
('date_stop', '>', date_to)]])
|
||||
return domain
|
||||
|
||||
def _preprocess_work_hours_data(self, work_data, date_from, date_to):
|
||||
"""
|
||||
Method is meant to be overriden, see hr_payroll_attendance
|
||||
"""
|
||||
return
|
||||
|
||||
def get_work_hours(self, date_from, date_to, domain=None):
|
||||
# Get work hours between 2 dates (datetime.date)
|
||||
# To correctly englobe the period, the start and end periods are converted
|
||||
# using the calendar timezone.
|
||||
assert not isinstance(date_from, datetime)
|
||||
assert not isinstance(date_to, datetime)
|
||||
|
||||
date_from = datetime.combine(fields.Datetime.to_datetime(date_from), datetime.min.time())
|
||||
date_to = datetime.combine(fields.Datetime.to_datetime(date_to), datetime.max.time())
|
||||
work_data = defaultdict(int)
|
||||
|
||||
contracts_by_company_tz = defaultdict(lambda: self.env['hr.contract'])
|
||||
for contract in self:
|
||||
contracts_by_company_tz[(
|
||||
contract.company_id,
|
||||
(contract.resource_calendar_id or contract.employee_id.resource_calendar_id).tz
|
||||
)] += contract
|
||||
utc = pytz.timezone('UTC')
|
||||
|
||||
for (company, contract_tz), contracts in contracts_by_company_tz.items():
|
||||
tz = pytz.timezone(contract_tz) if contract_tz else pytz.utc
|
||||
date_from_tz = tz.localize(date_from).astimezone(utc).replace(tzinfo=None)
|
||||
date_to_tz = tz.localize(date_to).astimezone(utc).replace(tzinfo=None)
|
||||
work_data_tz = contracts.with_company(company).sudo()._get_work_hours(
|
||||
date_from_tz, date_to_tz, domain=domain)
|
||||
for work_entry_type_id, hours in work_data_tz.items():
|
||||
work_data[work_entry_type_id] += hours
|
||||
return work_data
|
||||
|
||||
def _get_work_hours(self, date_from, date_to, domain=None):
|
||||
"""
|
||||
Returns the amount (expressed in hours) of work
|
||||
for a contract between two dates.
|
||||
If called on multiple contracts, sum work amounts of each contract.
|
||||
:param date_from: The start date
|
||||
:param date_to: The end date
|
||||
:returns: a dictionary {work_entry_id: hours_1, work_entry_2: hours_2}
|
||||
"""
|
||||
assert isinstance(date_from, datetime)
|
||||
assert isinstance(date_to, datetime)
|
||||
|
||||
# First, found work entry that didn't exceed interval.
|
||||
work_entries = self.env['hr.work.entry']._read_group(
|
||||
self._get_work_hours_domain(date_from, date_to, domain=domain, inside=True),
|
||||
['work_entry_type_id'],
|
||||
['duration:sum']
|
||||
)
|
||||
work_data = defaultdict(int)
|
||||
work_data.update({work_entry_type.id: duration_sum for work_entry_type, duration_sum in work_entries})
|
||||
self._preprocess_work_hours_data(work_data, date_from, date_to)
|
||||
|
||||
# Second, find work entry that exceeds interval and compute right duration.
|
||||
work_entries = self.env['hr.work.entry'].search(self._get_work_hours_domain(date_from, date_to, domain=domain, inside=False))
|
||||
|
||||
for work_entry in work_entries:
|
||||
date_start = max(date_from, work_entry.date_start)
|
||||
date_stop = min(date_to, work_entry.date_stop)
|
||||
if work_entry.work_entry_type_id.is_leave:
|
||||
contract = work_entry.contract_id
|
||||
calendar = contract.resource_calendar_id
|
||||
employee = contract.employee_id
|
||||
contract_data = employee._get_work_days_data_batch(
|
||||
date_start, date_stop, compute_leaves=False, calendar=calendar
|
||||
)[employee.id]
|
||||
|
||||
work_data[work_entry.work_entry_type_id.id] += contract_data.get('hours', 0)
|
||||
else:
|
||||
work_data[work_entry.work_entry_type_id.id] += work_entry._get_work_duration(date_start, date_stop) # Number of hours
|
||||
return work_data
|
||||
|
||||
def _get_default_work_entry_type_id(self):
|
||||
return self.structure_type_id.default_work_entry_type_id.id or super()._get_default_work_entry_type_id()
|
||||
|
||||
def _get_fields_that_recompute_payslip(self):
|
||||
# Returns the fields that should recompute the payslip
|
||||
return [self._get_contract_wage]
|
||||
|
||||
def _get_nearly_expired_contracts(self, outdated_days, company_id=False):
|
||||
today = fields.Date.today()
|
||||
nearly_expired_contracts = self.search([
|
||||
('company_id', '=', company_id or self.env.company.id),
|
||||
('state', '=', 'open'),
|
||||
('date_end', '>=', today),
|
||||
('date_end', '<', outdated_days)])
|
||||
|
||||
# Check if no new contracts starting after the end of the expiring one
|
||||
nearly_expired_contracts_without_new_contracts = self.env['hr.contract']
|
||||
new_contracts_grouped_by_employee = {
|
||||
employee.id
|
||||
for [employee] in self._read_group([
|
||||
('company_id', '=', company_id or self.env.company.id),
|
||||
('state', '=', 'draft'),
|
||||
('date_start', '>=', outdated_days),
|
||||
('employee_id', 'in', nearly_expired_contracts.employee_id.ids)
|
||||
], groupby=['employee_id'])
|
||||
}
|
||||
|
||||
for expired_contract in nearly_expired_contracts:
|
||||
if expired_contract.employee_id.id not in new_contracts_grouped_by_employee:
|
||||
nearly_expired_contracts_without_new_contracts |= expired_contract
|
||||
return nearly_expired_contracts_without_new_contracts
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
res = super().create(vals_list)
|
||||
self._recompute_calendar_changed(res.mapped('employee_id'))
|
||||
return res
|
||||
|
||||
def unlink(self):
|
||||
employee_ids = self.mapped('employee_id')
|
||||
res = super().unlink()
|
||||
self._recompute_calendar_changed(employee_ids)
|
||||
return res
|
||||
|
||||
def write(self, vals):
|
||||
if 'state' in vals and vals['state'] == 'cancel':
|
||||
self.env['hr.payslip'].search([
|
||||
('contract_id', 'in', self.filtered(lambda c: c.state != 'cancel').ids),
|
||||
('state', 'in', ['draft', 'verify']),
|
||||
]).action_payslip_cancel()
|
||||
res = super().write(vals)
|
||||
dependendant_fields = self._get_fields_that_recompute_payslip()
|
||||
if any(key in dependendant_fields for key in vals.keys()):
|
||||
for contract in self:
|
||||
contract._recompute_payslips(self.date_start, self.date_end or date.max)
|
||||
if any(key in vals for key in ('state', 'date_start', 'resource_calendar_id', 'employee_id')):
|
||||
self._recompute_calendar_changed(self.mapped('employee_id'))
|
||||
return res
|
||||
|
||||
def _recompute_work_entries(self, date_from, date_to):
|
||||
self.ensure_one()
|
||||
super()._recompute_work_entries(date_from, date_to)
|
||||
self._recompute_payslips(date_from, date_to)
|
||||
|
||||
def _recompute_payslips(self, date_from, date_to):
|
||||
self.ensure_one()
|
||||
all_payslips = self.env['hr.payslip'].sudo().search([
|
||||
('contract_id', '=', self.id),
|
||||
('state', 'in', ['draft', 'verify']),
|
||||
('date_from', '<=', date_to),
|
||||
('date_to', '>=', date_from),
|
||||
('company_id', '=', self.env.company.id),
|
||||
]).filtered(lambda p: p.is_regular)
|
||||
if all_payslips:
|
||||
all_payslips.action_refresh_from_work_entries()
|
||||
|
||||
def action_new_salary_attachment(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': _('Salary Attachment'),
|
||||
'res_model': 'hr.salary.attachment',
|
||||
'view_mode': 'form',
|
||||
'target': 'new',
|
||||
'context': {'default_employee_ids': self.employee_id.ids}
|
||||
}
|
||||
|
||||
def action_open_salary_attachments(self):
|
||||
self.ensure_one()
|
||||
action = self.env["ir.actions.actions"]._for_xml_id("hr_payroll.hr_salary_attachment_action")
|
||||
action.update({'domain': [('employee_ids', 'in', self.employee_id.ids)],
|
||||
'context': {'default_employee_ids': self.employee_id.ids}})
|
||||
return action
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
class ContractHistory(models.Model):
|
||||
_inherit = 'hr.contract.history'
|
||||
|
||||
time_credit = fields.Boolean('Credit time', readonly=True, help='This is a credit time contract.')
|
||||
work_time_rate = fields.Float(string='Work time rate', help='Work time rate versus full time working schedule.')
|
||||
standard_calendar_id = fields.Many2one('resource.calendar', readonly=True)
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class HrEmployee(models.Model):
|
||||
_inherit = 'hr.employee'
|
||||
_description = 'Employee'
|
||||
|
||||
currency_id = fields.Many2one(
|
||||
"res.currency",
|
||||
string='Currency',
|
||||
related='company_id.currency_id')
|
||||
slip_ids = fields.One2many('hr.payslip', 'employee_id', string='Payslips', readonly=True, groups="hr_payroll.group_hr_payroll_user")
|
||||
payslip_count = fields.Integer(compute='_compute_payslip_count', string='Payslip Count', groups="hr_payroll.group_hr_payroll_user")
|
||||
registration_number = fields.Char('Registration Number of the Employee', groups="hr.group_hr_user", copy=False)
|
||||
salary_attachment_ids = fields.Many2many(
|
||||
'hr.salary.attachment',
|
||||
string='Salary Attachments',
|
||||
groups="hr_payroll.group_hr_payroll_user")
|
||||
salary_attachment_count = fields.Integer(
|
||||
compute='_compute_salary_attachment_count', string="Salary Attachment Count",
|
||||
groups="hr_payroll.group_hr_payroll_user")
|
||||
mobile_invoice = fields.Binary(string="Mobile Subscription Invoice", groups="hr_contract.group_hr_contract_manager")
|
||||
sim_card = fields.Binary(string="SIM Card Copy", groups="hr_contract.group_hr_contract_manager")
|
||||
internet_invoice = fields.Binary(string="Internet Subscription Invoice", groups="hr_contract.group_hr_contract_manager")
|
||||
is_non_resident = fields.Boolean(string='Non-resident', help='If recipient lives in a foreign country', groups="hr.group_hr_user")
|
||||
disabled = fields.Boolean(string="Disabled", help="If the employee is declared disabled by law", groups="hr.group_hr_user", tracking=True)
|
||||
structure_type_id = fields.Many2one(string="Salary Structure Type", related="contract_ids.structure_type_id", groups="hr.group_hr_user")
|
||||
|
||||
_sql_constraints = [
|
||||
('unique_registration_number', 'UNIQUE(registration_number, company_id)', 'No duplication of registration numbers is allowed')
|
||||
]
|
||||
|
||||
def _compute_payslip_count(self):
|
||||
for employee in self:
|
||||
employee.payslip_count = len(employee.slip_ids)
|
||||
|
||||
def _compute_salary_attachment_count(self):
|
||||
for employee in self:
|
||||
employee.salary_attachment_count = len(employee.salary_attachment_ids)
|
||||
|
||||
@api.model
|
||||
def _get_account_holder_employees_data(self):
|
||||
# as acc_type isn't stored we can not use a domain to retrieve the employees
|
||||
# bypass orm for performance, we only care about the employee id anyway
|
||||
|
||||
# return nothing if user has no right to either employee or bank partner
|
||||
if (not self.browse().has_access('read') or
|
||||
not self.env['res.partner.bank'].has_access('read')):
|
||||
return []
|
||||
|
||||
self.env.cr.execute('''
|
||||
SELECT emp.id,
|
||||
acc.acc_number,
|
||||
acc.allow_out_payment
|
||||
FROM hr_employee emp
|
||||
LEFT JOIN res_partner_bank acc
|
||||
ON acc.id=emp.bank_account_id
|
||||
JOIN hr_contract con
|
||||
ON con.employee_id=emp.id
|
||||
WHERE emp.company_id IN %s
|
||||
AND emp.active=TRUE
|
||||
AND con.state='open'
|
||||
AND emp.bank_account_id is not NULL
|
||||
''', (tuple(self.env.companies.ids),))
|
||||
|
||||
return self.env.cr.dictfetchall()
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
#-*- coding:utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class HrPayrollDashboardWarning(models.Model):
|
||||
_name = 'hr.payroll.dashboard.warning'
|
||||
_description = 'Payroll Dashboard Warning'
|
||||
_order = 'sequence, name'
|
||||
|
||||
name = fields.Char(required=True, translate=True)
|
||||
active = fields.Boolean(default=True)
|
||||
country_id = fields.Many2one(
|
||||
'res.country',
|
||||
string='Country',
|
||||
default=lambda self: self.env.company.country_id,
|
||||
domain=lambda self: [('id', 'in', self.env.companies.country_id.ids)])
|
||||
evaluation_code = fields.Text(string='Python Code',
|
||||
default='''
|
||||
# Available variables:
|
||||
#----------------------
|
||||
# - warning_count: Number of warnings.
|
||||
# - warning_records: Records containing warnings.
|
||||
# - warning_action: Action to perform in response to warnings.
|
||||
# - additional_context: Additional context to include with the action.''')
|
||||
sequence = fields.Integer(default=10)
|
||||
color = fields.Integer(string='Warning Color', help='Tag color. No color means black.')
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import base64
|
||||
import logging
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HrPayrollDeclarationMixin(models.AbstractModel):
|
||||
_name = 'hr.payroll.declaration.mixin'
|
||||
_description = 'Payroll Declaration Mixin'
|
||||
|
||||
@api.model
|
||||
def default_get(self, field_list=None):
|
||||
country_restriction = self._country_restriction()
|
||||
if country_restriction and self.env.company.country_id.code != country_restriction:
|
||||
raise UserError(_('You must be logged in a %s company to use this feature', country_restriction))
|
||||
return super().default_get(field_list)
|
||||
|
||||
def _get_year_selection(self):
|
||||
current_year = datetime.now().year
|
||||
return [(str(i), i) for i in range(1990, current_year + 1)]
|
||||
|
||||
year = fields.Selection(
|
||||
selection='_get_year_selection', string='Year', required=True,
|
||||
default=lambda x: str(datetime.now().year - 1))
|
||||
line_ids = fields.One2many(
|
||||
'hr.payroll.employee.declaration', 'res_id', string='Declarations')
|
||||
lines_count = fields.Integer(compute='_compute_lines_count')
|
||||
company_id = fields.Many2one('res.company', default=lambda self: self.env.company)
|
||||
pdf_error = fields.Text('PDF Error Message')
|
||||
|
||||
def unlink(self):
|
||||
self.line_ids.unlink() # We need to unlink the child line_ids as well to prevent orphan records
|
||||
return super().unlink()
|
||||
|
||||
def action_generate_declarations(self):
|
||||
for sheet in self:
|
||||
if not sheet.line_ids:
|
||||
raise UserError(_('There is no declaration to generate for the given period'))
|
||||
return self.action_generate_pdf()
|
||||
|
||||
@api.depends('line_ids')
|
||||
def _compute_lines_count(self):
|
||||
for sheet in self:
|
||||
sheet.lines_count = len(sheet.line_ids)
|
||||
|
||||
def action_open_declarations(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'name': _('Employee Declarations'),
|
||||
'res_model': 'hr.payroll.employee.declaration',
|
||||
'type': 'ir.actions.act_window',
|
||||
'views': [(False, 'list'), (False, 'form')],
|
||||
'view_mode': 'list,form',
|
||||
'domain': [('res_id', '=', self.id), ('res_model', '=', self._name)],
|
||||
'context': {'default_res_model': self._name, 'default_res_id': self.id},
|
||||
}
|
||||
|
||||
def _country_restriction(self):
|
||||
return False
|
||||
|
||||
def action_generate_pdf(self):
|
||||
return self.line_ids.action_generate_pdf()
|
||||
|
||||
def _post_process_rendering_data_pdf(self, rendering_data):
|
||||
return rendering_data
|
||||
|
||||
def _get_rendering_data(self, employees):
|
||||
return {}
|
||||
|
||||
def _process_files(self, files):
|
||||
self.ensure_one()
|
||||
self.pdf_error = False
|
||||
for employee, filename, data in files:
|
||||
line = self.line_ids.filtered(lambda l: l.employee_id == employee)
|
||||
line.write({
|
||||
'pdf_file': base64.encodebytes(data),
|
||||
'pdf_filename': filename,
|
||||
})
|
||||
|
||||
def _get_pdf_report(self):
|
||||
return False
|
||||
|
||||
def _get_pdf_filename(self, employee):
|
||||
self.ensure_one()
|
||||
return _('%(employee_name)s-declaration-%(year)s', employee_name=employee.legal_name, year=self.year)
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import logging
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
from odoo import models, fields, api, _
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HrPayrollEmployeeDeclaration(models.Model):
|
||||
_name = 'hr.payroll.employee.declaration'
|
||||
_description = 'Payroll Employee Declaration'
|
||||
_rec_name = 'employee_id'
|
||||
|
||||
res_model = fields.Char(
|
||||
'Declaration Model Name', required=True, index=True)
|
||||
res_id = fields.Many2oneReference(
|
||||
'Declaration Model Id', index=True, model_field='res_model', required=True)
|
||||
employee_id = fields.Many2one('hr.employee', domain="['|', ('active', '=', True), ('active', '=', False)]")
|
||||
company_id = fields.Many2one('res.company', default=lambda self: self.env.company, required=True)
|
||||
pdf_file = fields.Binary('PDF File', readonly=True, attachment=False)
|
||||
pdf_filename = fields.Char()
|
||||
pdf_to_generate = fields.Boolean()
|
||||
state = fields.Selection([
|
||||
('draft', 'Draft'),
|
||||
('pdf_to_generate', 'Queued PDF generation'),
|
||||
('pdf_generated', 'Generated PDF'),
|
||||
], compute='_compute_state', store=True)
|
||||
|
||||
_sql_constraints = [
|
||||
('unique_employee_sheet', 'unique(employee_id, res_model, res_id)', 'An employee can only have one declaration per sheet.'),
|
||||
]
|
||||
|
||||
@api.depends('pdf_to_generate', 'pdf_file')
|
||||
def _compute_state(self):
|
||||
for declaration in self:
|
||||
if declaration.pdf_to_generate:
|
||||
declaration.state = 'pdf_to_generate'
|
||||
elif declaration.pdf_file:
|
||||
declaration.state = 'pdf_generated'
|
||||
else:
|
||||
declaration.state = 'draft'
|
||||
|
||||
def _generate_pdf(self):
|
||||
report_sudo = self.env["ir.actions.report"].sudo()
|
||||
declarations_by_sheet = defaultdict(lambda: self.env['hr.payroll.employee.declaration'])
|
||||
for declaration in self:
|
||||
declarations_by_sheet[(declaration.res_model, declaration.res_id)] += declaration
|
||||
|
||||
|
||||
for (res_model, res_id), declarations in declarations_by_sheet.items():
|
||||
sheet = self.env[res_model].browse(res_id)
|
||||
if not sheet.exists():
|
||||
_logger.warning('Sheet %s %s does not exist', res_model, res_id)
|
||||
continue
|
||||
report_id = sheet._get_pdf_report().id
|
||||
rendering_data = sheet._get_rendering_data(declarations.employee_id)
|
||||
if 'error' in rendering_data:
|
||||
sheet.pdf_error = rendering_data['error']
|
||||
continue
|
||||
rendering_data = sheet._post_process_rendering_data_pdf(rendering_data)
|
||||
|
||||
pdf_files = []
|
||||
sheet_count = len(rendering_data)
|
||||
counter = 1
|
||||
for employee, employee_data in rendering_data.items():
|
||||
_logger.info('Printing %s (%s/%s)', sheet._description, counter, sheet_count)
|
||||
counter += 1
|
||||
sheet_filename = sheet._get_pdf_filename(employee)
|
||||
sheet_file, dummy = report_sudo.with_context(lang=employee.lang or self.env.lang)._render_qweb_pdf(
|
||||
report_id,
|
||||
[employee.id], data={'report_data': employee_data, 'employee': employee, 'company_id': employee.company_id})
|
||||
pdf_files.append((employee, sheet_filename, sheet_file))
|
||||
if pdf_files:
|
||||
sheet._process_files(pdf_files)
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
declarations = super().create(vals_list)
|
||||
if any(declaration.pdf_to_generate for declaration in declarations):
|
||||
self.env.ref('hr_payroll.ir_cron_generate_payslip_pdfs')._trigger()
|
||||
return declarations
|
||||
|
||||
def write(self, vals):
|
||||
res = super().write(vals)
|
||||
if vals.get('pdf_to_generate'):
|
||||
self.env.ref('hr_payroll.ir_cron_generate_payslip_pdfs')._trigger()
|
||||
return res
|
||||
|
||||
def action_generate_pdf(self):
|
||||
if self:
|
||||
self.write({'pdf_to_generate': True})
|
||||
self.env.ref('hr_payroll.ir_cron_generate_payslip_pdfs')._trigger()
|
||||
message = _("PDF generation started. It will be available shortly.")
|
||||
else:
|
||||
message = _("Please select the declarations for which you want to generate a PDF.")
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'type': 'success',
|
||||
'message': message,
|
||||
'next': {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'reload'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@api.autovacuum
|
||||
def _gc_orphan_declarations(self):
|
||||
orphans = self.env['hr.payroll.employee.declaration']
|
||||
grouped_declarations = self.read_group([], ['ids:array_agg(id)', 'res_ids:array_agg(res_id)'], ['res_model'])
|
||||
for gd in grouped_declarations:
|
||||
sheet_ids = self.env[gd['res_model']].browse(set(gd['res_ids'])).exists().ids
|
||||
for declaration in self.browse(gd['ids']):
|
||||
if declaration.res_id not in sheet_ids:
|
||||
orphans += declaration
|
||||
if orphans:
|
||||
orphans.unlink()
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from collections import defaultdict
|
||||
from random import randint
|
||||
|
||||
from odoo import fields, models, api, _
|
||||
|
||||
|
||||
class HrPayrollHeadcount(models.Model):
|
||||
_name = 'hr.payroll.headcount'
|
||||
_description = 'Payroll Headcount'
|
||||
|
||||
name = fields.Char(string='Name', compute='_compute_name', store=True)
|
||||
is_name_custom = fields.Boolean(string='Custom Name', compute="_compute_is_name_custom")
|
||||
company_id = fields.Many2one('res.company', default=lambda self: self.env.company.id)
|
||||
line_ids = fields.One2many('hr.payroll.headcount.line', 'headcount_id')
|
||||
employee_count = fields.Integer(string='Employee Count')
|
||||
date_from = fields.Date(string='From', default=lambda self: fields.date.today(), required=True)
|
||||
date_to = fields.Date(string='To')
|
||||
|
||||
_sql_constraints = [
|
||||
('date_range', 'CHECK (date_from <= date_to)', 'The start date must be anterior to the end date.'),
|
||||
]
|
||||
|
||||
@api.depends('date_from', 'date_to', 'company_id')
|
||||
def _compute_name(self):
|
||||
for headcount in self:
|
||||
if not headcount.is_name_custom:
|
||||
headcount.name = headcount.get_default_name()
|
||||
|
||||
@api.depends('name')
|
||||
def _compute_is_name_custom(self):
|
||||
for headcount in self:
|
||||
if headcount.name and headcount.name != headcount.get_default_name():
|
||||
headcount.is_name_custom = True
|
||||
else:
|
||||
headcount.is_name_custom = False
|
||||
|
||||
def get_default_name(self):
|
||||
self.ensure_one()
|
||||
if self.date_from == self.date_to or not self.date_to:
|
||||
return _(
|
||||
'Headcount for %(company_name)s on the %(date)s',
|
||||
company_name=self.company_id.name,
|
||||
date=self.date_from)
|
||||
return _(
|
||||
'Headcount for %(company_name)s from %(date_from)s to %(date_to)s',
|
||||
company_name=self.company_id.name,
|
||||
date_from=self.date_from,
|
||||
date_to=self.date_to)
|
||||
|
||||
def action_populate(self):
|
||||
self.ensure_one()
|
||||
if not self.date_to:
|
||||
self.date_to = self.date_from
|
||||
contracts = self.env['hr.contract'].search([
|
||||
('company_id', '=', self.company_id.id),
|
||||
'|',
|
||||
('date_end', '=', False),
|
||||
('date_end', '>=', self.date_from),
|
||||
('date_start', '<=', self.date_to),
|
||||
'|',
|
||||
('state', 'in', ['open', 'close']),
|
||||
'&',
|
||||
('state', '=', 'draft'),
|
||||
('kanban_state', '=', 'done'),
|
||||
], order='employee_id, date_start DESC')
|
||||
|
||||
contracts_by_employee_id = defaultdict(lambda: self.env['hr.contract'])
|
||||
working_rates = set()
|
||||
for contract in contracts:
|
||||
contracts_by_employee_id[contract.employee_id.id] |= contract
|
||||
working_rates.add(round(contract.hours_per_week, 2))
|
||||
|
||||
existing_working_rates = self.env['hr.payroll.headcount.working.rate']\
|
||||
.search([('rate', 'in', list(working_rates))])
|
||||
working_rate_to_create = working_rates - set(existing_working_rates.mapped('rate'))
|
||||
if working_rate_to_create:
|
||||
created_working_rate = self.env['hr.payroll.headcount.working.rate']\
|
||||
.create([{'rate': rate} for rate in working_rate_to_create])
|
||||
existing_working_rates |= created_working_rate
|
||||
|
||||
working_rate_id_by_value = {}
|
||||
for working_rate in existing_working_rates:
|
||||
working_rate_id_by_value[working_rate.rate] = working_rate.id
|
||||
|
||||
lines = [
|
||||
(0, 0, {
|
||||
'contract_id': contracts[0].id,
|
||||
'working_rate_ids': [
|
||||
(6, 0, [working_rate_id_by_value[round(contract.hours_per_week, 2)] for contract in contracts]),
|
||||
],
|
||||
'contract_names': ', '.join(contract.name for contract in contracts),
|
||||
})
|
||||
for contracts in contracts_by_employee_id.values()]
|
||||
self.line_ids = [(5, 0, 0)] + lines
|
||||
self.employee_count = len(self.line_ids)
|
||||
|
||||
def action_open_lines(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'name': _("Headcount's employees"),
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'hr.payroll.headcount.line',
|
||||
'view_mode': 'list',
|
||||
'domain': [('headcount_id', '=', self.id)],
|
||||
'target': 'current',
|
||||
'context': {
|
||||
'search_default_group_by_department': True,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class HrPayrollHeadcountLine(models.Model):
|
||||
_name = 'hr.payroll.headcount.line'
|
||||
_description = 'Headcount Line'
|
||||
|
||||
headcount_id = fields.Many2one('hr.payroll.headcount', string='headcount_id', required=True, ondelete='cascade')
|
||||
working_rate_ids = fields.Many2many('hr.payroll.headcount.working.rate', required=True, string='Working Rate')
|
||||
contract_names = fields.Char(string='Contract Names', required=True, readonly=True)
|
||||
contract_id = fields.Many2one('hr.contract', string='Contract', required=True, readonly=True)
|
||||
department_id = fields.Many2one(related='contract_id.department_id', string='Department')
|
||||
job_id = fields.Many2one(related='contract_id.job_id', string='Job Title')
|
||||
currency_id = fields.Many2one(related='contract_id.currency_id', string='Currency')
|
||||
wage_on_payroll = fields.Monetary(string='Wage On Payroll', currency_field='currency_id', compute='_compute_wage_on_payroll')
|
||||
employee_id = fields.Many2one(related="contract_id.employee_id", required=True, readonly=True)
|
||||
employee_type = fields.Selection(related='employee_id.employee_type', string='Employee Type')
|
||||
|
||||
@api.depends('contract_id')
|
||||
def _compute_wage_on_payroll(self):
|
||||
for line in self:
|
||||
line.wage_on_payroll = line.contract_id._get_contract_wage()
|
||||
|
||||
|
||||
class HrPayrollHeadcountWorkingRate(models.Model):
|
||||
_name = 'hr.payroll.headcount.working.rate'
|
||||
_description = 'Working Rate'
|
||||
|
||||
rate = fields.Float(string='Rate')
|
||||
color = fields.Integer(string='Color', default=lambda self: randint(1, 11))
|
||||
|
||||
@api.depends('rate')
|
||||
def _compute_display_name(self):
|
||||
for working_rate in self:
|
||||
working_rate.display_name = _('%(rate)s Hours/week', rate=working_rate.rate)
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class HrPayrollStructure(models.Model):
|
||||
_name = 'hr.payroll.structure'
|
||||
_description = 'Salary Structure'
|
||||
|
||||
@api.model
|
||||
def _get_default_report_id(self):
|
||||
return self.env.ref('hr_payroll.action_report_payslip', False)
|
||||
|
||||
@api.model
|
||||
def _get_default_rule_ids(self):
|
||||
default_structure = self.env.ref('hr_payroll.default_structure', False)
|
||||
if not default_structure or not default_structure.rule_ids:
|
||||
return []
|
||||
vals = [
|
||||
(0, 0, {
|
||||
'name': rule.name,
|
||||
'sequence': rule.sequence,
|
||||
'code': rule.code,
|
||||
'category_id': rule.category_id,
|
||||
'condition_select': rule.condition_select,
|
||||
'condition_python': rule.condition_python,
|
||||
'amount_select': rule.amount_select,
|
||||
'amount_python_compute': rule.amount_python_compute,
|
||||
'appears_on_employee_cost_dashboard': rule.appears_on_employee_cost_dashboard,
|
||||
}) for rule in default_structure.rule_ids]
|
||||
return vals
|
||||
|
||||
def _get_domain_report(self):
|
||||
if self.env.company.country_code:
|
||||
return [
|
||||
('model', '=', 'hr.payslip'),
|
||||
('report_type', '=', 'qweb-pdf'),
|
||||
'|',
|
||||
('report_name', 'ilike', 'l10n_' + self.env.company.country_code.lower()),
|
||||
'&',
|
||||
('report_name', 'ilike', 'hr_payroll'),
|
||||
('report_name', 'not ilike', 'l10n')
|
||||
]
|
||||
else:
|
||||
return [
|
||||
('model', '=', 'hr.payslip'),
|
||||
('report_type', '=', 'qweb-pdf'),
|
||||
('report_name', 'ilike', 'hr_payroll'),
|
||||
('report_name', 'not ilike', 'l10n')
|
||||
]
|
||||
|
||||
name = fields.Char(required=True)
|
||||
code = fields.Char()
|
||||
active = fields.Boolean(default=True)
|
||||
type_id = fields.Many2one(
|
||||
'hr.payroll.structure.type', required=True)
|
||||
country_id = fields.Many2one('res.country', string='Country', default=lambda self: self.env.company.country_id)
|
||||
note = fields.Html(string='Description')
|
||||
rule_ids = fields.One2many(
|
||||
'hr.salary.rule', 'struct_id', copy=True,
|
||||
string='Salary Rules', default=_get_default_rule_ids)
|
||||
report_id = fields.Many2one('ir.actions.report',
|
||||
string="Template", domain=_get_domain_report, default=_get_default_report_id)
|
||||
payslip_name = fields.Char(string="Payslip Name", translate=True,
|
||||
help="Name to be set on a payslip. Example: 'End of the year bonus'. If not set, the default value is 'Salary Slip'")
|
||||
hide_basic_on_pdf = fields.Boolean(help="Enable this option if you don't want to display the Basic Salary on the printed pdf.")
|
||||
unpaid_work_entry_type_ids = fields.Many2many(
|
||||
'hr.work.entry.type', 'hr_payroll_structure_hr_work_entry_type_rel')
|
||||
use_worked_day_lines = fields.Boolean(default=True, help="Worked days won't be computed/displayed in payslips.")
|
||||
schedule_pay = fields.Selection(related='type_id.default_schedule_pay')
|
||||
input_line_type_ids = fields.Many2many('hr.payslip.input.type', string='Other Input Line')
|
||||
ytd_computation = fields.Boolean(default=False, string='Year to Date Computation',
|
||||
help="Adds a column in the payslip that shows the accumulated amount paid for different rules during the year")
|
||||
|
||||
def copy_data(self, default=None):
|
||||
vals_list = super().copy_data(default=default)
|
||||
return [dict(vals, name=self.env._("%s (copy)", structure.name)) for structure, vals in zip(self, vals_list)]
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
# -*- coding:utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class HrPayrollStructureType(models.Model):
|
||||
_inherit = 'hr.payroll.structure.type'
|
||||
_description = 'Salary Structure Type'
|
||||
_order = 'sequence, id'
|
||||
|
||||
sequence = fields.Integer(default=10)
|
||||
name = fields.Char('Structure Type', required=True)
|
||||
default_schedule_pay = fields.Selection([
|
||||
('annually', 'Annually'),
|
||||
('semi-annually', 'Semi-annually'),
|
||||
('quarterly', 'Quarterly'),
|
||||
('bi-monthly', 'Bi-monthly'),
|
||||
('monthly', 'Monthly'),
|
||||
('semi-monthly', 'Semi-monthly'),
|
||||
('bi-weekly', 'Bi-weekly'),
|
||||
('weekly', 'Weekly'),
|
||||
('daily', 'Daily'),
|
||||
], string='Default Scheduled Pay', default='monthly',
|
||||
help="Defines the frequency of the wage payment.")
|
||||
struct_ids = fields.One2many('hr.payroll.structure', 'type_id', string="Structures")
|
||||
default_struct_id = fields.Many2one('hr.payroll.structure', string="Regular Pay Structure")
|
||||
default_work_entry_type_id = fields.Many2one('hr.work.entry.type', help="Work entry type for regular attendances.", required=True,
|
||||
default=lambda self: self.env.ref('hr_work_entry.work_entry_type_attendance', raise_if_not_found=False))
|
||||
wage_type = fields.Selection([
|
||||
('monthly', 'Fixed Wage'),
|
||||
('hourly', 'Hourly Wage')
|
||||
], string="Default Wage Type", default='monthly', required=True)
|
||||
struct_type_count = fields.Integer(compute='_compute_struct_type_count', string='Structure Type Count')
|
||||
|
||||
def _compute_struct_type_count(self):
|
||||
for structure_type in self:
|
||||
structure_type.struct_type_count = len(structure_type.struct_ids)
|
||||
|
||||
def _check_country(self, vals):
|
||||
country_id = vals.get('country_id')
|
||||
if country_id and country_id not in self.env.companies.mapped('country_id').ids:
|
||||
raise UserError(_('You should also be logged into a company in %s to set this country.', self.env['res.country'].browse(country_id).name))
|
||||
|
||||
def write(self, vals):
|
||||
if self.env.context.get('payroll_check_country'):
|
||||
self._check_country(vals)
|
||||
return super().write(vals)
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
if self.env.context.get('payroll_check_country'):
|
||||
for vals in vals_list:
|
||||
self._check_country(vals)
|
||||
return super().create(vals_list)
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,23 @@
|
|||
# -*- coding:utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class HrPayslipInput(models.Model):
|
||||
_name = 'hr.payslip.input'
|
||||
_description = 'Payslip Input'
|
||||
_order = 'payslip_id, sequence'
|
||||
|
||||
name = fields.Char(string="Description")
|
||||
payslip_id = fields.Many2one('hr.payslip', string='Pay Slip', required=True, ondelete='cascade', index=True)
|
||||
sequence = fields.Integer(required=True, index=True, default=10)
|
||||
input_type_id = fields.Many2one('hr.payslip.input.type', string='Type', required=True, domain="['|', ('id', 'in', _allowed_input_type_ids), ('struct_ids', '=', False)]")
|
||||
_allowed_input_type_ids = fields.Many2many('hr.payslip.input.type', related='payslip_id.struct_id.input_line_type_ids')
|
||||
code = fields.Char(related='input_type_id.code', required=True, help="The code that can be used in the salary rules")
|
||||
amount = fields.Float(
|
||||
string="Count",
|
||||
help="It is used in computation. E.g. a rule for salesmen having 1%% commission of basic salary per product can defined in expression like: result = inputs['SALEURO'].amount * contract.wage * 0.01.")
|
||||
contract_id = fields.Many2one(
|
||||
related='payslip_id.contract_id', string='Contract', required=True,
|
||||
help="The contract this input should be applied to")
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
# -*- coding:utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models, api, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class HrPayslipInputType(models.Model):
|
||||
_name = 'hr.payslip.input.type'
|
||||
_description = 'Payslip Input Type'
|
||||
|
||||
name = fields.Char(string='Description', required=True)
|
||||
code = fields.Char(required=True, help="The code that can be used in the salary rules")
|
||||
struct_ids = fields.Many2many('hr.payroll.structure', string='Availability in Structure', help='This input will be only available in those structure. If empty, it will be available in all payslip.')
|
||||
country_id = fields.Many2one('res.country', string='Country', default=lambda self: self.env.company.country_id)
|
||||
country_code = fields.Char(related='country_id.code')
|
||||
active = fields.Boolean('Active', default=True)
|
||||
available_in_attachments = fields.Boolean(string="Available in attachments")
|
||||
is_quantity = fields.Boolean(default=False, string="Is quantity?", help="If set, hide currency and consider the manual input as a quantity for every rule computation using this input.")
|
||||
default_no_end_date = fields.Boolean("No end date by default")
|
||||
|
||||
@api.ondelete(at_uninstall=False)
|
||||
def _unlink_except_master_data(self):
|
||||
external_ids = self.get_external_id()
|
||||
for input_type in self:
|
||||
external_id = external_ids[input_type.id]
|
||||
if external_id and not external_id.startswith('__export__'):
|
||||
raise UserError(_("You cannot delete %s as it is used in another module but you can archive it instead.", input_type.name))
|
||||
|
||||
@api.constrains('active')
|
||||
def _check_salary_attachment_type_active(self):
|
||||
if self.env['hr.salary.attachment'].search_count([('other_input_type_id', 'in', self.ids), ('state', 'not in', ('close', 'cancel'))], limit=1):
|
||||
raise UserError("You cannot archive an input type if there exists a running salary attachment of this type.")
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
# -*- coding:utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class HrPayslipLine(models.Model):
|
||||
_name = 'hr.payslip.line'
|
||||
_description = 'Payslip Line'
|
||||
_order = 'contract_id, sequence, code'
|
||||
|
||||
name = fields.Char(required=True)
|
||||
sequence = fields.Integer(required=True, index=True, default=5,
|
||||
help='Use to arrange calculation sequence')
|
||||
code = fields.Char(required=True,
|
||||
help="The code of salary rules can be used as reference in computation of other rules. "
|
||||
"In that case, it is case sensitive.")
|
||||
slip_id = fields.Many2one('hr.payslip', string='Pay Slip', required=True, ondelete='cascade')
|
||||
salary_rule_id = fields.Many2one('hr.salary.rule', string='Rule', required=True)
|
||||
contract_id = fields.Many2one('hr.contract', string='Contract', required=True, index=True)
|
||||
employee_id = fields.Many2one('hr.employee', string='Employee', required=True)
|
||||
rate = fields.Float(string='Rate (%)', digits='Payroll Rate', default=100.0)
|
||||
amount = fields.Monetary()
|
||||
quantity = fields.Float(digits='Payroll', default=1.0)
|
||||
total = fields.Monetary(string='Total')
|
||||
ytd = fields.Monetary(string='YTD')
|
||||
|
||||
amount_select = fields.Selection(related='salary_rule_id.amount_select', readonly=True)
|
||||
amount_fix = fields.Float(related='salary_rule_id.amount_fix', readonly=True)
|
||||
amount_percentage = fields.Float(related='salary_rule_id.amount_percentage', readonly=True)
|
||||
appears_on_payslip = fields.Boolean(related='salary_rule_id.appears_on_payslip', readonly=True)
|
||||
category_id = fields.Many2one(related='salary_rule_id.category_id', readonly=True)
|
||||
partner_id = fields.Many2one(related='salary_rule_id.partner_id', readonly=True)
|
||||
|
||||
date_from = fields.Date(string='From', related="slip_id.date_from", store=True)
|
||||
date_to = fields.Date(string='To', related="slip_id.date_to", store=True)
|
||||
company_id = fields.Many2one(related='slip_id.company_id')
|
||||
currency_id = fields.Many2one('res.currency', related='slip_id.currency_id')
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for values in vals_list:
|
||||
if 'employee_id' not in values or 'contract_id' not in values:
|
||||
payslip = self.env['hr.payslip'].browse(values.get('slip_id'))
|
||||
values['employee_id'] = values.get('employee_id') or payslip.employee_id.id
|
||||
values['contract_id'] = values.get('contract_id') or payslip.contract_id and payslip.contract_id.id
|
||||
if not values['contract_id']:
|
||||
raise UserError(_('You must set a contract to create a payslip line.'))
|
||||
return super(HrPayslipLine, self).create(vals_list)
|
||||
|
||||
def get_payslip_styling_dict(self):
|
||||
return {
|
||||
'NET': {
|
||||
'line_style': 'color:#875A7B;',
|
||||
'line_class': 'o_total o_border_bottom fw-bold',
|
||||
},
|
||||
'GROSS': {
|
||||
'line_style': 'color:#00A09D;',
|
||||
'line_class': 'o_subtotal o_border_bottom',
|
||||
},
|
||||
'BASIC': {
|
||||
'line_style': 'color:#00A09D;',
|
||||
'line_class': 'o_subtotal o_border_bottom',
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import date, datetime
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
|
||||
|
||||
class HrPayslipRun(models.Model):
|
||||
_name = 'hr.payslip.run'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
_description = 'Payslip Batches'
|
||||
_order = 'date_end desc'
|
||||
|
||||
name = fields.Char(required=True)
|
||||
slip_ids = fields.One2many('hr.payslip', 'payslip_run_id', string='Payslips')
|
||||
state = fields.Selection([
|
||||
('draft', 'New'),
|
||||
('verify', 'Confirmed'),
|
||||
('close', 'Done'),
|
||||
('paid', 'Paid'),
|
||||
], string='Status', index=True, readonly=True, copy=False, default='draft', store=True, compute='_compute_state_change')
|
||||
date_start = fields.Date(string='Date From', required=True, default=lambda self: fields.Date.to_string(date.today().replace(day=1)))
|
||||
date_end = fields.Date(string='Date To', required=True,
|
||||
default=lambda self: fields.Date.to_string((datetime.now() + relativedelta(months=+1, day=1, days=-1)).date()))
|
||||
payslip_count = fields.Integer(compute='_compute_payslip_count')
|
||||
company_id = fields.Many2one('res.company', string='Company', readonly=True, required=True,
|
||||
default=lambda self: self.env.company)
|
||||
country_id = fields.Many2one(
|
||||
'res.country', string='Country',
|
||||
related='company_id.country_id', readonly=True
|
||||
)
|
||||
country_code = fields.Char(related='country_id.code', depends=['country_id'], readonly=True)
|
||||
currency_id = fields.Many2one(related="company_id.currency_id")
|
||||
payment_report = fields.Binary(
|
||||
string='Payment Report',
|
||||
help="Export .csv file related to this batch",
|
||||
readonly=True)
|
||||
payment_report_filename = fields.Char(readonly=True)
|
||||
payment_report_date = fields.Date(readonly=True)
|
||||
|
||||
def _compute_payslip_count(self):
|
||||
for payslip_run in self:
|
||||
payslip_run.payslip_count = len(payslip_run.slip_ids)
|
||||
|
||||
@api.depends('slip_ids', 'state')
|
||||
def _compute_state_change(self):
|
||||
for payslip_run in self:
|
||||
if payslip_run.state == 'draft' and payslip_run.slip_ids:
|
||||
payslip_run.update({'state': 'verify'})
|
||||
|
||||
def action_draft(self):
|
||||
if self.slip_ids.filtered(lambda s: s.state == 'paid'):
|
||||
raise ValidationError(_('You cannot reset a batch to draft if some of the payslips have already been paid.'))
|
||||
self.write({'state': 'draft'})
|
||||
self.slip_ids.write({'state': 'draft'})
|
||||
|
||||
def action_open(self):
|
||||
self.write({'state': 'verify'})
|
||||
|
||||
def action_close(self):
|
||||
if self._are_payslips_ready():
|
||||
self.write({'state' : 'close'})
|
||||
|
||||
def action_payment_report(self, export_format='csv'):
|
||||
self.ensure_one()
|
||||
self.env['hr.payroll.payment.report.wizard'].create({
|
||||
'payslip_ids': self.slip_ids.ids,
|
||||
'payslip_run_id': self.id,
|
||||
'export_format': export_format
|
||||
}).generate_payment_report()
|
||||
|
||||
def action_paid(self):
|
||||
self.mapped('slip_ids').action_payslip_paid()
|
||||
self.write({'state': 'paid'})
|
||||
|
||||
def action_validate(self):
|
||||
payslip_done_result = self.mapped('slip_ids').filtered(lambda slip: slip.state not in ['draft', 'cancel']).action_payslip_done()
|
||||
self.action_close()
|
||||
return payslip_done_result
|
||||
|
||||
def action_confirm(self):
|
||||
self.slip_ids.write({'state': 'verify'})
|
||||
self.write({'state': 'verify'})
|
||||
|
||||
def action_open_payslips(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "hr.payslip",
|
||||
"views": [[False, "list"], [False, "form"]],
|
||||
"domain": [['id', 'in', self.slip_ids.ids]],
|
||||
"context": {'default_payslip_run_id': self.id},
|
||||
"name": "Payslips",
|
||||
}
|
||||
|
||||
def action_open_payslip_run_form(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'hr.payslip.run',
|
||||
'views': [[False, 'form']],
|
||||
'res_id': self.id,
|
||||
}
|
||||
|
||||
def _generate_payslips(self):
|
||||
action = self.env["ir.actions.actions"]._for_xml_id("hr_payroll.action_hr_payslip_by_employees")
|
||||
action['context'] = repr(self.env.context)
|
||||
return action
|
||||
|
||||
@api.ondelete(at_uninstall=False)
|
||||
def _unlink_if_draft_or_cancel(self):
|
||||
if any(self.mapped('slip_ids').filtered(lambda payslip: payslip.state not in ('draft', 'cancel'))):
|
||||
raise UserError(_("You can't delete a batch with payslips if they are not draft or cancelled."))
|
||||
|
||||
def _are_payslips_ready(self):
|
||||
return all(slip.state in ['done', 'cancel'] for slip in self.mapped('slip_ids'))
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
# -*- coding:utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.tools import float_round
|
||||
|
||||
|
||||
class HrPayslipWorkedDays(models.Model):
|
||||
_name = 'hr.payslip.worked_days'
|
||||
_description = 'Payslip Worked Days'
|
||||
_order = 'payslip_id, sequence'
|
||||
|
||||
name = fields.Char(compute='_compute_name', store=True, string='Description', readonly=False)
|
||||
payslip_id = fields.Many2one('hr.payslip', string='Pay Slip', required=True, ondelete='cascade', index=True)
|
||||
sequence = fields.Integer(required=True, index=True, default=10)
|
||||
code = fields.Char(string='Code', related='work_entry_type_id.code')
|
||||
work_entry_type_id = fields.Many2one('hr.work.entry.type', string='Type', required=True, help="The code that can be used in the salary rules")
|
||||
number_of_days = fields.Float(string='Number of Days')
|
||||
number_of_hours = fields.Float(string='Number of Hours')
|
||||
is_paid = fields.Boolean(compute='_compute_is_paid', store=True)
|
||||
amount = fields.Monetary(string='Amount', compute='_compute_amount', store=True, copy=True)
|
||||
contract_id = fields.Many2one(related='payslip_id.contract_id', string='Contract',
|
||||
help="The contract this worked days should be applied to")
|
||||
currency_id = fields.Many2one('res.currency', related='payslip_id.currency_id')
|
||||
is_credit_time = fields.Boolean(string='Credit Time')
|
||||
ytd = fields.Monetary(string='YTD')
|
||||
|
||||
@api.depends(
|
||||
'work_entry_type_id', 'payslip_id', 'payslip_id.struct_id',
|
||||
'payslip_id.employee_id', 'payslip_id.contract_id', 'payslip_id.struct_id', 'payslip_id.date_from', 'payslip_id.date_to')
|
||||
def _compute_is_paid(self):
|
||||
unpaid = {struct.id: struct.unpaid_work_entry_type_ids.ids for struct in self.mapped('payslip_id.struct_id')}
|
||||
for worked_days in self:
|
||||
worked_days.is_paid = (worked_days.work_entry_type_id.id not in unpaid[worked_days.payslip_id.struct_id.id]) if worked_days.payslip_id.struct_id.id in unpaid else False
|
||||
|
||||
@api.depends('is_paid', 'is_credit_time', 'number_of_hours', 'payslip_id', 'contract_id.wage', 'payslip_id.sum_worked_hours')
|
||||
def _compute_amount(self):
|
||||
for worked_days in self:
|
||||
if worked_days.payslip_id.edited or worked_days.payslip_id.state not in ['draft', 'verify']:
|
||||
continue
|
||||
if not worked_days.contract_id or worked_days.code == 'OUT' or worked_days.is_credit_time:
|
||||
worked_days.amount = 0
|
||||
continue
|
||||
if worked_days.payslip_id.wage_type == "hourly":
|
||||
worked_days.amount = worked_days.payslip_id.contract_id.hourly_wage * worked_days.number_of_hours if worked_days.is_paid else 0
|
||||
else:
|
||||
worked_days.amount = worked_days.payslip_id.contract_id.contract_wage * worked_days.number_of_hours / (worked_days.payslip_id.sum_worked_hours or 1) if worked_days.is_paid else 0
|
||||
|
||||
def _is_half_day(self):
|
||||
self.ensure_one()
|
||||
work_hours = self.payslip_id._get_worked_day_lines_hours_per_day()
|
||||
# For refunds number of days is negative
|
||||
return abs(self.number_of_days) < 1 or float_round(self.number_of_hours / self.number_of_days, 2) < work_hours
|
||||
|
||||
@api.depends('work_entry_type_id', 'number_of_days', 'number_of_hours', 'payslip_id')
|
||||
def _compute_name(self):
|
||||
to_check_public_holiday = {
|
||||
res[0]: res[1]
|
||||
for res in self.env['resource.calendar.leaves']._read_group(
|
||||
[
|
||||
('resource_id', '=', False),
|
||||
('work_entry_type_id', 'in', self.mapped('work_entry_type_id').ids),
|
||||
('date_from', '<=', max(self.payslip_id.mapped('date_to'))),
|
||||
('date_to', '>=', min(self.payslip_id.mapped('date_from'))),
|
||||
],
|
||||
['work_entry_type_id'],
|
||||
['id:recordset']
|
||||
)
|
||||
}
|
||||
for worked_days in self:
|
||||
public_holidays = to_check_public_holiday.get(worked_days.work_entry_type_id, '')
|
||||
holidays = public_holidays and public_holidays.filtered(lambda p:
|
||||
(p.calendar_id.id == worked_days.payslip_id.contract_id.resource_calendar_id.id or not p.calendar_id.id)
|
||||
and p.date_from.date() <= worked_days.payslip_id.date_to
|
||||
and p.date_to.date() >= worked_days.payslip_id.date_from
|
||||
and p.company_id == worked_days.payslip_id.company_id)
|
||||
half_day = worked_days._is_half_day()
|
||||
if holidays:
|
||||
name = (', '.join(holidays.mapped('name')))
|
||||
else:
|
||||
name = worked_days.work_entry_type_id.name
|
||||
worked_days.name = name + (_(' (Half-Day)') if half_day else '')
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
# -*- coding:utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.tools.safe_eval import safe_eval
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.tools import ormcache
|
||||
from odoo.tools.misc import format_date
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class HrSalaryRuleParameterValue(models.Model):
|
||||
_name = 'hr.rule.parameter.value'
|
||||
_description = 'Salary Rule Parameter Value'
|
||||
_order = 'date_from desc'
|
||||
|
||||
rule_parameter_id = fields.Many2one('hr.rule.parameter', required=True, ondelete='cascade', default=lambda self: self.env.context.get('active_id'))
|
||||
rule_parameter_name = fields.Char(related="rule_parameter_id.name", readonly=True)
|
||||
code = fields.Char(related="rule_parameter_id.code", index=True, store=True, readonly=True)
|
||||
date_from = fields.Date(string="From", index=True, required=True)
|
||||
parameter_value = fields.Text(help="Python data structure")
|
||||
country_id = fields.Many2one(related="rule_parameter_id.country_id")
|
||||
|
||||
_sql_constraints = [
|
||||
('_unique', 'unique (rule_parameter_id, date_from)', "Two rules with the same code cannot start the same day"),
|
||||
]
|
||||
|
||||
@api.constrains('parameter_value')
|
||||
def _check_parameter_value(self):
|
||||
for value in self:
|
||||
try:
|
||||
safe_eval(value.parameter_value)
|
||||
except Exception as e:
|
||||
raise UserError(_('Wrong rule parameter value for %(rule_parameter_name)s at date %(date)s.\n%(error)s', rule_parameter_name=value.rule_parameter_name, date=format_date(self.env, value.date_from), error=str(e)))
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
self.env.registry.clear_cache()
|
||||
return super().create(vals_list)
|
||||
|
||||
def write(self, vals):
|
||||
if 'date_from' in vals or 'parameter_value' in vals:
|
||||
self.env.registry.clear_cache()
|
||||
return super().write(vals)
|
||||
|
||||
def unlink(self):
|
||||
self.env.registry.clear_cache()
|
||||
return super().unlink()
|
||||
|
||||
|
||||
class HrSalaryRuleParameter(models.Model):
|
||||
_name = 'hr.rule.parameter'
|
||||
_description = 'Salary Rule Parameter'
|
||||
|
||||
name = fields.Char(required=True)
|
||||
code = fields.Char(required=True, help="This code is used in salary rules to refer to this parameter.")
|
||||
description = fields.Html()
|
||||
country_id = fields.Many2one('res.country', string='Country', default=lambda self: self.env.company.country_id)
|
||||
parameter_version_ids = fields.One2many('hr.rule.parameter.value', 'rule_parameter_id', string='Versions')
|
||||
|
||||
_sql_constraints = [
|
||||
('_unique', 'unique (code)', "Two rule parameters cannot have the same code."),
|
||||
]
|
||||
|
||||
@api.model
|
||||
@ormcache('code', 'date', 'tuple(self.env.context.get("allowed_company_ids", []))')
|
||||
def _get_parameter_from_code(self, code, date=None, raise_if_not_found=True):
|
||||
if not date:
|
||||
date = fields.Date.today()
|
||||
# This should be quite fast as it uses a limit and fields are indexed
|
||||
# moreover the method is cached
|
||||
rule_parameter = self.env['hr.rule.parameter.value'].search([
|
||||
('code', '=', code),
|
||||
('date_from', '<=', date)], limit=1)
|
||||
if rule_parameter:
|
||||
return safe_eval(rule_parameter.parameter_value)
|
||||
if raise_if_not_found:
|
||||
raise UserError(_('No rule parameter with code "%(code)s" was found for %(date)s', code=code, date=date))
|
||||
else:
|
||||
return None
|
||||
|
|
@ -0,0 +1,298 @@
|
|||
# -*- coding:utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools.date_utils import start_of
|
||||
from odoo.tools.misc import formatLang
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from math import ceil
|
||||
|
||||
|
||||
class HrSalaryAttachment(models.Model):
|
||||
_name = 'hr.salary.attachment'
|
||||
_description = 'Salary Attachment'
|
||||
_inherit = ['mail.thread']
|
||||
_rec_name = 'description'
|
||||
|
||||
_sql_constraints = [
|
||||
(
|
||||
'check_monthly_amount', 'CHECK (monthly_amount > 0)',
|
||||
'Payslip amount must be strictly positive.'
|
||||
),
|
||||
(
|
||||
'check_total_amount',
|
||||
'CHECK ((total_amount > 0 AND total_amount >= monthly_amount) OR no_end_date = True)',
|
||||
'Total amount must be strictly positive and greater than or equal to the payslip amount.'
|
||||
),
|
||||
('check_remaining_amount', 'CHECK (remaining_amount >= 0)', 'Remaining amount must be positive.'),
|
||||
('check_dates', 'CHECK (date_start <= date_end)', 'End date may not be before the starting date.'),
|
||||
]
|
||||
|
||||
employee_ids = fields.Many2many('hr.employee', string='Employees', required=True,
|
||||
domain=lambda self: [('company_id', 'in', self.env.companies.ids)])
|
||||
employee_count = fields.Integer(compute='_compute_employee_count')
|
||||
company_id = fields.Many2one(
|
||||
'res.company', string='Company', required=True,
|
||||
default=lambda self: self.env.company)
|
||||
currency_id = fields.Many2one('res.currency', related='company_id.currency_id')
|
||||
description = fields.Char(required=True)
|
||||
other_input_type_id = fields.Many2one(
|
||||
'hr.payslip.input.type',
|
||||
string="Type",
|
||||
required=True,
|
||||
tracking=True,
|
||||
domain=[('available_in_attachments', '=', True)]
|
||||
)
|
||||
no_end_date = fields.Boolean(compute='_compute_no_end_date', store=True, readonly=False)
|
||||
monthly_amount = fields.Monetary('Payslip Amount', required=True, tracking=True, help='Amount to pay each payslip.')
|
||||
occurrences = fields.Integer(
|
||||
compute='_compute_occurrences',
|
||||
help='Number of times the salary attachment will appear on the payslip.',
|
||||
)
|
||||
active_amount = fields.Monetary(
|
||||
'Active Amount', compute='_compute_active_amount',
|
||||
help='Amount to pay for this payslip, Payslip Amount or less depending on the Remaining Amount.',
|
||||
)
|
||||
total_amount = fields.Monetary(
|
||||
'Total Amount',
|
||||
tracking=True,
|
||||
help='Total amount to be paid.',
|
||||
)
|
||||
has_total_amount = fields.Boolean('Has Total Amount', compute='_compute_has_total_amount')
|
||||
paid_amount = fields.Monetary('Paid Amount', tracking=True)
|
||||
remaining_amount = fields.Monetary(
|
||||
'Remaining Amount', compute='_compute_remaining_amount', store=True,
|
||||
help='Remaining amount to be paid.',
|
||||
)
|
||||
is_quantity = fields.Boolean(related='other_input_type_id.is_quantity')
|
||||
is_refund = fields.Boolean(
|
||||
string='Negative Value',
|
||||
help='Check if the value of the salary attachment must be taken into account as negative (-X)')
|
||||
date_start = fields.Date('Start Date', required=True, default=lambda r: start_of(fields.Date.today(), 'month'), tracking=True)
|
||||
date_estimated_end = fields.Date(
|
||||
'Estimated End Date', compute='_compute_estimated_end',
|
||||
help='Approximated end date.',
|
||||
)
|
||||
date_end = fields.Date(
|
||||
'End Date', default=False, tracking=True,
|
||||
help='Date at which this assignment has been set as completed or cancelled.',
|
||||
)
|
||||
state = fields.Selection(
|
||||
selection=[
|
||||
('open', 'Running'),
|
||||
('close', 'Completed'),
|
||||
('cancel', 'Cancelled'),
|
||||
],
|
||||
string='Status',
|
||||
default='open',
|
||||
required=True,
|
||||
tracking=True,
|
||||
copy=False,
|
||||
)
|
||||
payslip_ids = fields.Many2many('hr.payslip', relation='hr_payslip_hr_salary_attachment_rel', string='Payslips', copy=False)
|
||||
payslip_count = fields.Integer('# Payslips', compute='_compute_payslip_count')
|
||||
has_done_payslip = fields.Boolean(compute="_compute_has_done_payslip")
|
||||
|
||||
attachment = fields.Binary('Document', copy=False)
|
||||
attachment_name = fields.Char()
|
||||
|
||||
has_similar_attachment = fields.Boolean(compute='_compute_has_similar_attachment')
|
||||
has_similar_attachment_warning = fields.Char(compute='_compute_has_similar_attachment')
|
||||
|
||||
@api.depends('other_input_type_id')
|
||||
def _compute_no_end_date(self):
|
||||
for attachment in self:
|
||||
attachment.no_end_date = attachment.other_input_type_id.default_no_end_date
|
||||
|
||||
@api.depends('employee_ids')
|
||||
def _compute_employee_count(self):
|
||||
for attachment in self:
|
||||
attachment.employee_count = len(attachment.employee_ids)
|
||||
|
||||
@api.depends('monthly_amount', 'date_start', 'date_end')
|
||||
def _compute_total_amount(self):
|
||||
for record in self:
|
||||
if record.has_total_amount:
|
||||
date_start = record.date_start if record.date_start else fields.Date.today()
|
||||
date_end = record.date_end if record.date_end else fields.Date.today()
|
||||
month_difference = (date_end.year - date_start.year) * 12 + (date_end.month - date_start.month)
|
||||
record.total_amount = max(0, month_difference + 1) * record.monthly_amount
|
||||
else:
|
||||
record.total_amount = record.paid_amount
|
||||
|
||||
@api.depends('monthly_amount', 'total_amount')
|
||||
def _compute_occurrences(self):
|
||||
self.occurrences = 0
|
||||
for attachment in self:
|
||||
if not attachment.total_amount or not attachment.monthly_amount:
|
||||
continue
|
||||
attachment.occurrences = ceil(attachment.total_amount / attachment.monthly_amount)
|
||||
|
||||
@api.depends('no_end_date', 'date_end')
|
||||
def _compute_has_total_amount(self):
|
||||
for record in self:
|
||||
if record.no_end_date and not record.date_end:
|
||||
record.has_total_amount = False
|
||||
else:
|
||||
record.has_total_amount = True
|
||||
|
||||
@api.depends('total_amount', 'paid_amount', 'monthly_amount')
|
||||
def _compute_remaining_amount(self):
|
||||
for record in self:
|
||||
if record.has_total_amount:
|
||||
record.remaining_amount = max(0, record.total_amount - record.paid_amount)
|
||||
else:
|
||||
record.remaining_amount = record.monthly_amount
|
||||
|
||||
@api.depends('state', 'total_amount', 'monthly_amount', 'date_start')
|
||||
def _compute_estimated_end(self):
|
||||
for record in self:
|
||||
if record.state not in ['close', 'cancel'] and record.has_total_amount and record.monthly_amount:
|
||||
record.date_estimated_end = start_of(record.date_start + relativedelta(months=ceil(record.remaining_amount / record.monthly_amount)), 'month')
|
||||
else:
|
||||
record.date_estimated_end = False
|
||||
|
||||
@api.depends('payslip_ids')
|
||||
def _compute_payslip_count(self):
|
||||
for record in self:
|
||||
record.payslip_count = len(record.payslip_ids)
|
||||
|
||||
@api.depends('total_amount', 'paid_amount', 'monthly_amount')
|
||||
def _compute_active_amount(self):
|
||||
for record in self:
|
||||
record.active_amount = min(record.monthly_amount, record.remaining_amount)
|
||||
|
||||
@api.depends('employee_ids', 'description', 'monthly_amount', 'date_start')
|
||||
def _compute_has_similar_attachment(self):
|
||||
date_min = min(self.mapped('date_start'))
|
||||
possible_matches = self.search([
|
||||
('state', '=', 'open'),
|
||||
('employee_ids', 'in', self.employee_ids.ids),
|
||||
('monthly_amount', 'in', self.mapped('monthly_amount')),
|
||||
('date_start', '<=', date_min),
|
||||
])
|
||||
for record in self:
|
||||
similar = []
|
||||
if record.employee_count == 1 and record.date_start and record.state == 'open':
|
||||
similar = possible_matches.filtered_domain([
|
||||
('id', '!=', record.id),
|
||||
('employee_ids', 'in', record.employee_ids.ids),
|
||||
('monthly_amount', '=', record.monthly_amount),
|
||||
('date_start', '<=', record.date_start),
|
||||
('other_input_type_id', '=', record.other_input_type_id.id),
|
||||
])
|
||||
similar = similar.filtered(lambda s: s.employee_count == 1)
|
||||
record.has_similar_attachment = similar if record.state == 'open' else False
|
||||
record.has_similar_attachment_warning = similar and _('Warning, a similar attachment has been found.')
|
||||
|
||||
@api.depends("payslip_ids.state")
|
||||
def _compute_has_done_payslip(self):
|
||||
for record in self:
|
||||
record.has_done_payslip = any(payslip.state in ['done', 'paid'] for payslip in record.payslip_ids)
|
||||
|
||||
def action_done(self):
|
||||
self.ensure_one()
|
||||
if self.employee_count > 1:
|
||||
description = self.description
|
||||
self.env['hr.salary.attachment'].create([{
|
||||
'employee_ids': [(4, employee.id)],
|
||||
'company_id': self.company_id.id,
|
||||
'description': self.description,
|
||||
'other_input_type_id': self.other_input_type_id.id,
|
||||
'monthly_amount': self.monthly_amount,
|
||||
'total_amount': self.total_amount,
|
||||
'paid_amount': self.paid_amount,
|
||||
'date_start': self.date_start,
|
||||
'date_end': self.date_end,
|
||||
'state': 'open',
|
||||
'attachment': self.attachment,
|
||||
'attachment_name': self.attachment_name,
|
||||
} for employee in self.employee_ids])
|
||||
self.write({'state': 'close'})
|
||||
self.unlink()
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': _('Salary Attachments'),
|
||||
'res_model': 'hr.salary.attachment',
|
||||
'view_mode': 'list,form',
|
||||
'context': {'search_default_description': description},
|
||||
}
|
||||
self.write({
|
||||
'state': 'close',
|
||||
'date_end': fields.Date.today(),
|
||||
})
|
||||
|
||||
def action_open(self):
|
||||
self.ensure_one()
|
||||
self.write({
|
||||
'state': 'open',
|
||||
'date_end': False,
|
||||
})
|
||||
|
||||
def action_cancel(self):
|
||||
self.ensure_one()
|
||||
self.write({
|
||||
'state': 'cancel',
|
||||
'date_end': fields.Date.today(),
|
||||
})
|
||||
|
||||
def action_open_payslips(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': _('Payslips'),
|
||||
'res_model': 'hr.payslip',
|
||||
'view_mode': 'list,form',
|
||||
'domain': [('id', 'in', self.payslip_ids.ids)],
|
||||
}
|
||||
|
||||
@api.ondelete(at_uninstall=False)
|
||||
def _unlink_if_not_running(self):
|
||||
if any(assignment.state == 'open' for assignment in self):
|
||||
raise UserError(_('You cannot delete a running salary attachment!'))
|
||||
|
||||
def record_payment(self, total_amount):
|
||||
''' Record a new payment for this attachment, if the total has been reached the attachment will be closed.
|
||||
|
||||
:param amount: amount to register for this payment
|
||||
computed using the payslip_amount and the total if not given
|
||||
|
||||
Note that paid_amount can never be higher than total_amount
|
||||
'''
|
||||
def _record_payment(attachment, amount):
|
||||
attachment.message_post(
|
||||
body=_('Recorded a new payment of %s.', formatLang(self.env, amount, currency_obj=attachment.currency_id))
|
||||
)
|
||||
attachment.paid_amount += amount
|
||||
if attachment.remaining_amount == 0:
|
||||
attachment.action_done()
|
||||
|
||||
if any(len(a.employee_ids) > 1 for a in self):
|
||||
raise UserError(_('You cannot record a payment on multi employees attachments.'))
|
||||
|
||||
remaining = total_amount
|
||||
# It is necessary to sort attachments to pay monthly payments (child_support) first
|
||||
attachments_sorted = self.sorted(key=lambda a: a.has_total_amount)
|
||||
# For all types of attachments, we must pay the payslip_amount without exceeding the total amount
|
||||
for attachment in attachments_sorted:
|
||||
amount = min(attachment.monthly_amount, attachment.remaining_amount, remaining)
|
||||
if not amount:
|
||||
continue
|
||||
remaining -= amount
|
||||
_record_payment(attachment, amount)
|
||||
# If we still have remaining, balance the attachments (running) that have a total amount
|
||||
# in the chronology of estimated end dates.
|
||||
if remaining:
|
||||
fixed_total_attachments = self.filtered(lambda a: a.state == 'open' and a.has_total_amount)
|
||||
fixed_total_attachments_sorted = fixed_total_attachments.sorted(lambda a: a.date_estimated_end)
|
||||
for attachment in fixed_total_attachments_sorted:
|
||||
amount = min(attachment.remaining_amount, attachment.remaining_amount, remaining)
|
||||
if not amount:
|
||||
continue
|
||||
remaining -= amount
|
||||
_record_payment(attachment, amount)
|
||||
|
||||
def _get_active_amount(self):
|
||||
return sum(a.active_amount * (-1 if a.is_refund else 1) for a in self)
|
||||
|
|
@ -0,0 +1,233 @@
|
|||
# -*- coding:utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools.safe_eval import safe_eval
|
||||
|
||||
|
||||
class HrSalaryRule(models.Model):
|
||||
_name = 'hr.salary.rule'
|
||||
_order = 'sequence, id'
|
||||
_description = 'Salary Rule'
|
||||
|
||||
name = fields.Char(required=True, translate=True)
|
||||
code = fields.Char(required=True,
|
||||
help="The code of salary rules can be used as reference in computation of other rules. "
|
||||
"In that case, it is case sensitive.")
|
||||
struct_id = fields.Many2one('hr.payroll.structure', string="Salary Structure", required=True)
|
||||
sequence = fields.Integer(required=True, index=True, default=5,
|
||||
help='Use to arrange calculation sequence')
|
||||
quantity = fields.Char(default='1.0',
|
||||
help="It is used in computation for percentage and fixed amount. "
|
||||
"E.g. a rule for Meal Voucher having fixed amount of "
|
||||
u"1€ per worked day can have its quantity defined in expression "
|
||||
"like worked_days['WORK100'].number_of_days.")
|
||||
category_id = fields.Many2one('hr.salary.rule.category', string='Category', required=True)
|
||||
active = fields.Boolean(default=True,
|
||||
help="If the active field is set to false, it will allow you to hide the salary rule without removing it.")
|
||||
appears_on_payslip = fields.Boolean(string='Appears on Payslip', default=True,
|
||||
help="Used to display the salary rule on payslip.")
|
||||
appears_on_employee_cost_dashboard = fields.Boolean(string='View on Employer Cost Dashboard', default=False,
|
||||
help="Used to display the value in the employer cost dashboard.")
|
||||
appears_on_payroll_report = fields.Boolean(string="View on Payroll Reporting", default=False)
|
||||
condition_select = fields.Selection([
|
||||
('none', 'Always True'),
|
||||
('range', 'Range'),
|
||||
('input', 'Other Input'),
|
||||
('python', 'Python Expression')
|
||||
], string="Condition Based on", default='none', required=True)
|
||||
condition_range = fields.Char(string='Range Based on', default='contract.wage',
|
||||
help='This will be used to compute the % fields values; in general it is on basic, '
|
||||
'but you can also use categories code fields in lowercase as a variable names '
|
||||
'(hra, ma, lta, etc.) and the variable basic.')
|
||||
condition_other_input_id = fields.Many2one('hr.payslip.input.type', domain=[('is_quantity', '=', False)])
|
||||
condition_python = fields.Text(string='Python Condition', required=True,
|
||||
default='''
|
||||
# Available variables:
|
||||
#----------------------
|
||||
# payslip: hr.payslip object
|
||||
# employee: hr.employee object
|
||||
# contract: hr.contract object
|
||||
# rules: dict containing the rules code (previously computed)
|
||||
# categories: dict containing the computed salary rule categories (sum of amount of all rules belonging to that category).
|
||||
# worked_days: dict containing the computed worked days
|
||||
# inputs: dict containing the computed inputs.
|
||||
|
||||
# Output:
|
||||
#----------------------
|
||||
# result: boolean True if the rule should be calculated, False otherwise
|
||||
|
||||
result = rules['NET']['total'] > categories['NET'] * 0.10''',
|
||||
help='Applied this rule for calculation if condition is true. You can specify condition like basic > 1000.')
|
||||
condition_range_min = fields.Float(string='Minimum Range', help="The minimum amount, applied for this rule.")
|
||||
condition_range_max = fields.Float(string='Maximum Range', help="The maximum amount, applied for this rule.")
|
||||
amount_select = fields.Selection([
|
||||
('percentage', 'Percentage (%)'),
|
||||
('fix', 'Fixed Amount'),
|
||||
('input', 'Other Input'),
|
||||
('code', 'Python Code'),
|
||||
], string='Amount Type', index=True, required=True, default='fix', help="The computation method for the rule amount.")
|
||||
amount_fix = fields.Float(string='Fixed Amount', digits='Payroll')
|
||||
amount_percentage = fields.Float(string='Percentage (%)', digits='Payroll Rate',
|
||||
help='For example, enter 50.0 to apply a percentage of 50%')
|
||||
amount_other_input_id = fields.Many2one('hr.payslip.input.type', domain=[('is_quantity', '=', False)])
|
||||
amount_python_compute = fields.Text(string='Python Code',
|
||||
default='''
|
||||
# Available variables:
|
||||
#----------------------
|
||||
# payslip: hr.payslip object
|
||||
# employee: hr.employee object
|
||||
# contract: hr.contract object
|
||||
# rules: dict containing the rules code (previously computed)
|
||||
# categories: dict containing the computed salary rule categories (sum of amount of all rules belonging to that category).
|
||||
# worked_days: dict containing the computed worked days
|
||||
# inputs: dict containing the computed inputs.
|
||||
|
||||
# Output:
|
||||
#----------------------
|
||||
# result: float, base amount of the rule
|
||||
# result_rate: float, rate between -100.0 and 100.0, which defaults to 100.0 (%).
|
||||
# result_qty: float, quantity, which defaults to 1.
|
||||
# result_name: string, name of the line, which defaults to the name field of the salary rule.
|
||||
This is useful if the name depends should depend on something computed in the rule.
|
||||
# The total returned by the salary rule is calculated as:
|
||||
# total = result * result_rate * result_qty
|
||||
|
||||
result = contract.wage * 0.10''')
|
||||
amount_percentage_base = fields.Char(string='Percentage based on', help='result will be affected to a variable')
|
||||
partner_id = fields.Many2one('res.partner', string='Partner',
|
||||
help="Eventual third party involved in the salary payment of the employees.")
|
||||
note = fields.Html(string='Description', translate=True)
|
||||
|
||||
def _raise_error(self, localdict, error_type, e):
|
||||
raise UserError(_("""%(error_type)s
|
||||
- Employee: %(employee)s
|
||||
- Contract: %(contract)s
|
||||
- Payslip: %(payslip)s
|
||||
- Salary rule: %(name)s (%(code)s)
|
||||
- Error: %(error_message)s""",
|
||||
error_type=error_type,
|
||||
employee=localdict['employee'].name,
|
||||
contract=localdict['contract'].name,
|
||||
payslip=localdict['payslip'].name,
|
||||
name=self.name,
|
||||
code=self.code,
|
||||
error_message=e))
|
||||
|
||||
def _compute_rule(self, localdict):
|
||||
|
||||
"""
|
||||
:param localdict: dictionary containing the current computation environment
|
||||
:return: returns a tuple (amount, qty, rate)
|
||||
:rtype: (float, float, float)
|
||||
"""
|
||||
self.ensure_one()
|
||||
localdict['localdict'] = localdict
|
||||
if self.amount_select == 'fix':
|
||||
try:
|
||||
return self.amount_fix or 0.0, float(safe_eval(self.quantity, localdict)), 100.0
|
||||
except Exception as e:
|
||||
self._raise_error(localdict, _("Wrong quantity defined for:"), e)
|
||||
if self.amount_select == 'percentage':
|
||||
try:
|
||||
return (float(safe_eval(self.amount_percentage_base, localdict)),
|
||||
float(safe_eval(self.quantity, localdict)),
|
||||
self.amount_percentage or 0.0)
|
||||
except Exception as e:
|
||||
self._raise_error(localdict, _("Wrong percentage base or quantity defined for:"), e)
|
||||
if self.amount_select == 'input':
|
||||
if self.amount_other_input_id.code not in localdict['inputs']:
|
||||
return 0.0, 1.0, 100.0
|
||||
return localdict['inputs'][self.amount_other_input_id.code].amount, 1.0, 100.0
|
||||
# python code
|
||||
try:
|
||||
safe_eval(self.amount_python_compute or 0.0, localdict, mode='exec', nocopy=True)
|
||||
return float(localdict['result']), localdict.get('result_qty', 1.0), localdict.get('result_rate', 100.0)
|
||||
except Exception as e:
|
||||
self._raise_error(localdict, _("Wrong python code defined for:"), e)
|
||||
|
||||
def _satisfy_condition(self, localdict):
|
||||
self.ensure_one()
|
||||
localdict['localdict'] = localdict
|
||||
if self.condition_select == 'none':
|
||||
return True
|
||||
if self.condition_select == 'range':
|
||||
try:
|
||||
result = safe_eval(self.condition_range, localdict)
|
||||
return self.condition_range_min <= result <= self.condition_range_max
|
||||
except Exception as e:
|
||||
self._raise_error(localdict, _("Wrong range condition defined for:"), e)
|
||||
if self.condition_select == 'input':
|
||||
return self.condition_other_input_id.code in localdict['inputs']
|
||||
# python code
|
||||
try:
|
||||
safe_eval(self.condition_python, localdict, mode='exec', nocopy=True)
|
||||
return localdict.get('result', False)
|
||||
except Exception as e:
|
||||
self._raise_error(localdict, _("Wrong python condition defined for:"), e)
|
||||
|
||||
def _get_report_field_name(self):
|
||||
self.ensure_one()
|
||||
return 'x_l10n_%s_%s' % (
|
||||
self.struct_id.country_id.code.lower() if self.struct_id.country_id.code else 'xx',
|
||||
self.code.lower().replace('.', '_').replace('-', '_').replace(' ', '_'),
|
||||
)
|
||||
|
||||
def _generate_payroll_report_fields(self):
|
||||
fields_vals_list = []
|
||||
for rule in self:
|
||||
field_name = rule._get_report_field_name()
|
||||
model = self.env.ref('hr_payroll.model_hr_payroll_report').sudo().read(['id', 'name'])[0]
|
||||
if rule.appears_on_payroll_report and field_name not in self.env['hr.payroll.report']:
|
||||
fields_vals_list.append({
|
||||
'name': field_name,
|
||||
'model': model['name'],
|
||||
'model_id': model['id'],
|
||||
'field_description': '%s: %s' % (rule.struct_id.country_id.code or 'XX', rule.name),
|
||||
'ttype': 'float',
|
||||
})
|
||||
if fields_vals_list:
|
||||
self.env['ir.model.fields'].sudo().create(fields_vals_list)
|
||||
self.env['hr.payroll.report'].init()
|
||||
|
||||
def _remove_payroll_report_fields(self):
|
||||
# Note: should be called after the value is changed, aka after the
|
||||
# super call of the write method
|
||||
remaining_rules = self.env['hr.salary.rule'].search([('appears_on_payroll_report', '=', True)])
|
||||
all_remaining_field_names = [rule._get_report_field_name() for rule in remaining_rules]
|
||||
field_names = [rule._get_report_field_name() for rule in self]
|
||||
# Avoid to unlink a field if another rule request it (example: ONSSEMPLOYER)
|
||||
field_names = [field_name for field_name in field_names if field_name not in all_remaining_field_names]
|
||||
model = self.env.ref('hr_payroll.model_hr_payroll_report')
|
||||
fields_to_unlink = self.env['ir.model.fields'].sudo().search([
|
||||
('name', 'in', field_names),
|
||||
('model_id', '=', model.id),
|
||||
('ttype', '=', 'float'),
|
||||
])
|
||||
if fields_to_unlink:
|
||||
fields_to_unlink.unlink()
|
||||
self.env['hr.payroll.report'].init()
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
rules = super().create(vals_list)
|
||||
rules._generate_payroll_report_fields()
|
||||
return rules
|
||||
|
||||
def copy_data(self, default=None):
|
||||
vals_list = super().copy_data(default=default)
|
||||
return [dict(vals, name=self.env._("%s (copy)", rule.name)) for rule, vals in zip(self, vals_list)]
|
||||
|
||||
def write(self, vals):
|
||||
res = super().write(vals)
|
||||
if 'appears_on_payroll_report' in vals:
|
||||
if vals['appears_on_payroll_report']:
|
||||
self._generate_payroll_report_fields()
|
||||
else:
|
||||
self._remove_payroll_report_fields()
|
||||
return res
|
||||
|
||||
def unlink(self):
|
||||
self.write({'appears_on_payroll_report': False})
|
||||
return super().unlink()
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
# -*- coding:utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class HrSalaryRuleCategory(models.Model):
|
||||
_name = 'hr.salary.rule.category'
|
||||
_description = 'Salary Rule Category'
|
||||
|
||||
name = fields.Char(required=True, translate=True)
|
||||
code = fields.Char(required=True)
|
||||
parent_id = fields.Many2one('hr.salary.rule.category', string='Parent',
|
||||
help="Linking a salary category to its parent is used only for the reporting purpose.")
|
||||
children_ids = fields.One2many('hr.salary.rule.category', 'parent_id', string='Children')
|
||||
note = fields.Html(string='Description')
|
||||
|
||||
@api.constrains('parent_id')
|
||||
def _check_parent_id(self):
|
||||
if self._has_cycle():
|
||||
raise ValidationError(_('Error! You cannot create recursive hierarchy of Salary Rule Category.'))
|
||||
|
||||
def _sum_salary_rule_category(self, localdict, amount):
|
||||
self.ensure_one()
|
||||
if self.parent_id:
|
||||
localdict = self.parent_id._sum_salary_rule_category(localdict, amount)
|
||||
localdict['categories'][self.code] = localdict['categories'][self.code] + amount
|
||||
return localdict
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue