Compare commits

...

3 Commits

Author SHA1 Message Date
administrator 0f071e6dda Merge branch 'develop' 2025-01-29 18:21:05 +05:30
administrator dd9e87f3f5 Initial commit 2025-01-29 18:19:51 +05:30
Pranay 62edcad092 PayRoll, Leaves module deployment 2025-01-29 18:16:32 +05:30
213 changed files with 313607 additions and 24 deletions

View File

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

View File

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

View File

@ -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',
}

View File

@ -0,0 +1,4 @@
#-*- coding:utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import main

View File

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

View File

@ -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 &lt; 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) &gt; 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),
'|',
'&amp;',
('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', '&gt;=', start_month),
('date_start', '&lt;', 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) &gt; 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 &lt; 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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 &amp; 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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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.')

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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.")

View File

@ -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',
},
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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