This commit is contained in:
parent
bff1c557ef
commit
97d61d3076
93
addons/payroll/README.rst
Normal file
93
addons/payroll/README.rst
Normal file
@ -0,0 +1,93 @@
|
||||
=======
|
||||
Payroll
|
||||
=======
|
||||
|
||||
..
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! This file is generated by oca-gen-addon-readme !!
|
||||
!! changes will be overwritten. !!
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! source digest: sha256:def4f708faf4767d5767c515426c572391d63f2a30ffde152f86d3a019c0b920
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
|
||||
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
|
||||
:target: https://odoo-community.org/page/development-status
|
||||
:alt: Beta
|
||||
.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png
|
||||
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
|
||||
:alt: License: LGPL-3
|
||||
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fpayroll-lightgray.png?logo=github
|
||||
:target: https://github.com/OCA/payroll/tree/17.0/payroll
|
||||
:alt: OCA/payroll
|
||||
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
|
||||
:target: https://translation.odoo-community.org/projects/payroll-17-0/payroll-17-0-payroll
|
||||
:alt: Translate me on Weblate
|
||||
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
|
||||
:target: https://runboat.odoo-community.org/builds?repo=OCA/payroll&target_branch=17.0
|
||||
:alt: Try me on Runboat
|
||||
|
||||
|badge1| |badge2| |badge3| |badge4| |badge5|
|
||||
|
||||
Manage your employee payroll records.
|
||||
|
||||
This module is a backport from Odoo SA and as such, it is not included in the OCA CLA. That means we do not have a copy of the copyright on it like all other OCA modules.
|
||||
|
||||
**Table of contents**
|
||||
|
||||
.. contents::
|
||||
:local:
|
||||
|
||||
Bug Tracker
|
||||
===========
|
||||
|
||||
Bugs are tracked on `GitHub Issues <https://github.com/OCA/payroll/issues>`_.
|
||||
In case of trouble, please check there if your issue has already been reported.
|
||||
If you spotted it first, help us to smash it by providing a detailed and welcomed
|
||||
`feedback <https://github.com/OCA/payroll/issues/new?body=module:%20payroll%0Aversion:%2017.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
|
||||
|
||||
Do not contact contributors directly about support or help with technical issues.
|
||||
|
||||
Credits
|
||||
=======
|
||||
|
||||
Authors
|
||||
~~~~~~~
|
||||
|
||||
* Odoo SA
|
||||
|
||||
Contributors
|
||||
~~~~~~~~~~~~
|
||||
|
||||
* Odoo SA <info@odoo.com>
|
||||
* David James <david@djdc.net.au>
|
||||
* Hilar AK <hilarak@gmail.com>
|
||||
* Nimarosa (Nicolas Rodriguez) <nicolarsande@gmail.com>
|
||||
* Henrik Norlin (@appstogrow)
|
||||
|
||||
Maintainers
|
||||
~~~~~~~~~~~
|
||||
|
||||
This module is maintained by the OCA.
|
||||
|
||||
.. image:: https://odoo-community.org/logo.png
|
||||
:alt: Odoo Community Association
|
||||
:target: https://odoo-community.org
|
||||
|
||||
OCA, or the Odoo Community Association, is a nonprofit organization whose
|
||||
mission is to support the collaborative development of Odoo features and
|
||||
promote its widespread use.
|
||||
|
||||
.. |maintainer-appstogrow| image:: https://github.com/appstogrow.png?size=40px
|
||||
:target: https://github.com/appstogrow
|
||||
:alt: appstogrow
|
||||
.. |maintainer-nimarosa| image:: https://github.com/nimarosa.png?size=40px
|
||||
:target: https://github.com/nimarosa
|
||||
:alt: nimarosa
|
||||
|
||||
Current `maintainers <https://odoo-community.org/page/maintainer-role>`__:
|
||||
|
||||
|maintainer-appstogrow| |maintainer-nimarosa|
|
||||
|
||||
This module is part of the `OCA/payroll <https://github.com/OCA/payroll/tree/17.0/payroll>`_ project on GitHub.
|
||||
|
||||
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
|
5
addons/payroll/__init__.py
Normal file
5
addons/payroll/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import models
|
||||
from . import report
|
||||
from . import wizard
|
45
addons/payroll/__manifest__.py
Normal file
45
addons/payroll/__manifest__.py
Normal file
@ -0,0 +1,45 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
"name": "Payroll",
|
||||
"version": "17.0.1.0.0",
|
||||
"category": "Payroll",
|
||||
"website": "https://github.com/OCA/payroll",
|
||||
"sequence": 38,
|
||||
"summary": "Manage your employee payroll records",
|
||||
"license": "LGPL-3",
|
||||
"author": "Odoo SA, Odoo Community Association (OCA)",
|
||||
"depends": [
|
||||
"hr_contract",
|
||||
"hr_holidays",
|
||||
"mail",
|
||||
],
|
||||
"data": [
|
||||
"security/hr_payroll_security.xml",
|
||||
"security/ir.model.access.csv",
|
||||
"data/hr_payroll_sequence.xml",
|
||||
"data/hr_payroll_data.xml",
|
||||
"wizard/hr_payroll_contribution_register_report_views.xml",
|
||||
"wizard/hr_payroll_payslips_by_employees_views.xml",
|
||||
"views/menus.xml",
|
||||
"views/hr_contract_views.xml",
|
||||
"views/hr_payroll_structure_views.xml",
|
||||
"views/hr_salary_rule_category_views.xml",
|
||||
"views/hr_contribution_register_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_employee_views.xml",
|
||||
"views/report_contributionregister.xml",
|
||||
"views/report_payslip.xml",
|
||||
"views/report_payslipdetails.xml",
|
||||
"report/report.xml",
|
||||
"views/res_config_settings_views.xml",
|
||||
"wizard/hr_payroll_send_email.xml",
|
||||
"wizard/hr_payslip_change_state_view.xml",
|
||||
],
|
||||
"demo": ["demo/hr_payroll_demo.xml"],
|
||||
"application": True,
|
||||
"maintainers": ["appstogrow", "nimarosa"],
|
||||
}
|
12
addons/payroll/data/hr_payroll_data.xml
Normal file
12
addons/payroll/data/hr_payroll_data.xml
Normal file
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo noupdate="1">
|
||||
<!-- 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>
|
||||
</odoo>
|
9
addons/payroll/data/hr_payroll_sequence.xml
Normal file
9
addons/payroll/data/hr_payroll_sequence.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo 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>
|
||||
</record>
|
||||
</odoo>
|
221
addons/payroll/demo/hr_payroll_demo.xml
Normal file
221
addons/payroll/demo/hr_payroll_demo.xml
Normal file
@ -0,0 +1,221 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<record id="contrib_register_employees" model="hr.contribution.register">
|
||||
<field name="name">Employees</field>
|
||||
<field name="partner_id" eval="False" />
|
||||
</record>
|
||||
<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">Gross</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>
|
||||
<record id="hr_rule_basic" model="hr.salary.rule">
|
||||
<field name="name">Basic Salary</field>
|
||||
<field name="sequence" eval="1" />
|
||||
<field name="code">BASIC</field>
|
||||
<field name="category_id" ref="payroll.BASIC" />
|
||||
<field name="condition_select">none</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="amount_python_compute">
|
||||
result = contract.wage
|
||||
</field>
|
||||
</record>
|
||||
<record id="hr_rule_taxable" model="hr.salary.rule">
|
||||
<field name="name">Gross</field>
|
||||
<field name="sequence" eval="100" />
|
||||
<field name="code">GROSS</field>
|
||||
<field name="category_id" ref="payroll.GROSS" />
|
||||
<field name="condition_select">none</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="amount_python_compute">
|
||||
result = categories.BASIC + categories.ALW
|
||||
</field>
|
||||
</record>
|
||||
<record id="hr_rule_net" model="hr.salary.rule">
|
||||
<field name="name">Net Salary</field>
|
||||
<field name="sequence" eval="200" />
|
||||
<field name="code">NET</field>
|
||||
<field name="category_id" ref="payroll.NET" />
|
||||
<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="register_id" ref="contrib_register_employees" />
|
||||
</record>
|
||||
<!-- Salary Structure -->
|
||||
<record id="structure_base" model="hr.payroll.structure">
|
||||
<field name="code">BASE</field>
|
||||
<field name="name">Base for new structures</field>
|
||||
<field
|
||||
eval="[(6, 0, [ref('hr_rule_basic'), ref('hr_rule_taxable'),ref('hr_rule_net')])]"
|
||||
name="rule_ids"
|
||||
/>
|
||||
<field name="company_id" ref="base.main_company" />
|
||||
</record>
|
||||
<!-- Contribution Register -->
|
||||
<record id="hr_houserent_register" model="hr.contribution.register">
|
||||
<field name="name">House Rent Allowance Register</field>
|
||||
</record>
|
||||
<record id="hr_provident_fund_register" model="hr.contribution.register">
|
||||
<field name="name">Provident Fund Register</field>
|
||||
</record>
|
||||
<record id="hr_professional_tax_register" model="hr.contribution.register">
|
||||
<field name="name">Professional Tax Register</field>
|
||||
</record>
|
||||
<record id="hr_meal_voucher_register" model="hr.contribution.register">
|
||||
<field name="name">Meal Voucher Register</field>
|
||||
</record>
|
||||
<!-- Salary Rules -->
|
||||
<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="payroll.ALW" />
|
||||
<field name="register_id" ref="hr_houserent_register" />
|
||||
<field name="name">House Rent Allowance</field>
|
||||
<field name="sequence" eval="5" />
|
||||
</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="payroll.ALW" />
|
||||
<field name="name">Conveyance Allowance</field>
|
||||
<field name="sequence" eval="10" />
|
||||
</record>
|
||||
<record id="hr_salary_rule_professionaltax1" model="hr.salary.rule">
|
||||
<field name="amount_select">fix</field>
|
||||
<field eval="150" name="sequence" />
|
||||
<field eval="-200.0" name="amount_fix" />
|
||||
<field name="code">PT</field>
|
||||
<field name="category_id" ref="payroll.DED" />
|
||||
<field name="register_id" ref="hr_professional_tax_register" />
|
||||
<field name="name">Professional Tax</field>
|
||||
</record>
|
||||
<record id="hr_salary_rule_providentfund1" model="hr.salary.rule">
|
||||
<field name="amount_select">percentage</field>
|
||||
<field eval="120" name="sequence" />
|
||||
<field eval="-12.5" name="amount_percentage" />
|
||||
<field name="amount_percentage_base">contract.wage</field>
|
||||
<field name="code">PF</field>
|
||||
<field name="category_id" ref="payroll.DED" />
|
||||
<field name="register_id" ref="hr_provident_fund_register" />
|
||||
<field name="name">Provident Fund</field>
|
||||
</record>
|
||||
<record id="hr_salary_rule_ca_demo" model="hr.salary.rule">
|
||||
<field name="amount_select">fix</field>
|
||||
<field eval="600.0" name="amount_fix" />
|
||||
<field name="code">CAMD</field>
|
||||
<field name="category_id" ref="payroll.ALW" />
|
||||
<field name="name">Conveyance Allowance For Marc Demo</field>
|
||||
<field name="sequence" eval="15" />
|
||||
</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">
|
||||
worked_days.WORK100 and worked_days.WORK100.number_of_days
|
||||
</field>
|
||||
<field name="code">MA</field>
|
||||
<field name="category_id" ref="payroll.ALW" />
|
||||
<field name="register_id" ref="hr_meal_voucher_register" />
|
||||
<field name="name">Meal Voucher</field>
|
||||
<field name="sequence" eval="16" />
|
||||
</record>
|
||||
<record id="hr_salary_rule_sales_commission" model="hr.salary.rule">
|
||||
<field name="amount_select">code</field>
|
||||
<field name="code">SALE</field>
|
||||
<field name="category_id" ref="payroll.ALW" />
|
||||
<field name="name">Get 1% of sales</field>
|
||||
<field name="sequence" eval="17" />
|
||||
<field name="amount_python_compute">
|
||||
result = ((inputs.SALEURO and inputs.SALEURO.amount) + (inputs.SALASIA and
|
||||
inputs.SALASIA.amount)) * 0.01
|
||||
</field>
|
||||
</record>
|
||||
<!-- Rule Inputs -->
|
||||
<record id="hr_rule_input_sale_a" model="hr.rule.input">
|
||||
<field name="code">SALEURO</field>
|
||||
<field name="name">Sales to Europe</field>
|
||||
<field name="input_id" ref="hr_salary_rule_sales_commission" />
|
||||
</record>
|
||||
<record id="hr_rule_input_sale_b" model="hr.rule.input">
|
||||
<field name="code">SALASIA</field>
|
||||
<field name="name">Sales to Asia</field>
|
||||
<field name="input_id" ref="hr_salary_rule_sales_commission" />
|
||||
</record>
|
||||
<!-- Salary Structure -->
|
||||
<record id="structure_001" model="hr.payroll.structure">
|
||||
<field name="code">ME</field>
|
||||
<field name="name">Marketing Executive</field>
|
||||
<field
|
||||
eval="[(6, 0, [ref('hr_salary_rule_houserentallowance1'),
|
||||
ref('hr_salary_rule_convanceallowance1'),ref('hr_salary_rule_professionaltax1'),ref('hr_salary_rule_providentfund1')])]"
|
||||
name="rule_ids"
|
||||
/>
|
||||
<field name="company_id" ref="base.main_company" />
|
||||
<field name="parent_id" ref="structure_base" />
|
||||
</record>
|
||||
<record id="structure_002" model="hr.payroll.structure">
|
||||
<field name="code">MEMD</field>
|
||||
<field name="name">Marketing Executive for Marc Demo</field>
|
||||
<field
|
||||
eval="[(6, 0, [ref('hr_salary_rule_ca_demo'), ref('hr_salary_rule_meal_voucher')])]"
|
||||
name="rule_ids"
|
||||
/>
|
||||
<field name="company_id" ref="base.main_company" />
|
||||
<field name="parent_id" ref="structure_001" />
|
||||
</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="work_phone">+3282823500</field>
|
||||
<field
|
||||
name="image_1920"
|
||||
type="base64"
|
||||
file="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="struct_id" ref="payroll.structure_001" />
|
||||
<field name="employee_id" ref="hr_employee_payroll" />
|
||||
<field name="notes">Default contract for marketing executives</field>
|
||||
<field eval="4000.0" name="wage" />
|
||||
</record>
|
||||
<record id="hr_contract_marc_demo" 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="struct_id" ref="payroll.structure_002" />
|
||||
<field name="employee_id" ref="hr.employee_qdp" />
|
||||
<field name="notes">This is Marc Demo's contract</field>
|
||||
<field eval="5000.0" name="wage" />
|
||||
</record>
|
||||
</odoo>
|
2288
addons/payroll/i18n/af.po
Normal file
2288
addons/payroll/i18n/af.po
Normal file
File diff suppressed because it is too large
Load Diff
2281
addons/payroll/i18n/am.po
Normal file
2281
addons/payroll/i18n/am.po
Normal file
File diff suppressed because it is too large
Load Diff
2453
addons/payroll/i18n/ar.po
Normal file
2453
addons/payroll/i18n/ar.po
Normal file
File diff suppressed because it is too large
Load Diff
2282
addons/payroll/i18n/az.po
Normal file
2282
addons/payroll/i18n/az.po
Normal file
File diff suppressed because it is too large
Load Diff
2395
addons/payroll/i18n/bg.po
Normal file
2395
addons/payroll/i18n/bg.po
Normal file
File diff suppressed because it is too large
Load Diff
2287
addons/payroll/i18n/bn.po
Normal file
2287
addons/payroll/i18n/bn.po
Normal file
File diff suppressed because it is too large
Load Diff
2400
addons/payroll/i18n/bs.po
Normal file
2400
addons/payroll/i18n/bs.po
Normal file
File diff suppressed because it is too large
Load Diff
2510
addons/payroll/i18n/ca.po
Normal file
2510
addons/payroll/i18n/ca.po
Normal file
File diff suppressed because it is too large
Load Diff
2366
addons/payroll/i18n/cs.po
Normal file
2366
addons/payroll/i18n/cs.po
Normal file
File diff suppressed because it is too large
Load Diff
2314
addons/payroll/i18n/da.po
Normal file
2314
addons/payroll/i18n/da.po
Normal file
File diff suppressed because it is too large
Load Diff
2430
addons/payroll/i18n/de.po
Normal file
2430
addons/payroll/i18n/de.po
Normal file
File diff suppressed because it is too large
Load Diff
2303
addons/payroll/i18n/el.po
Normal file
2303
addons/payroll/i18n/el.po
Normal file
File diff suppressed because it is too large
Load Diff
2285
addons/payroll/i18n/en_GB.po
Normal file
2285
addons/payroll/i18n/en_GB.po
Normal file
File diff suppressed because it is too large
Load Diff
2770
addons/payroll/i18n/es.po
Normal file
2770
addons/payroll/i18n/es.po
Normal file
File diff suppressed because it is too large
Load Diff
2916
addons/payroll/i18n/es_AR.po
Normal file
2916
addons/payroll/i18n/es_AR.po
Normal file
File diff suppressed because it is too large
Load Diff
2282
addons/payroll/i18n/es_BO.po
Normal file
2282
addons/payroll/i18n/es_BO.po
Normal file
File diff suppressed because it is too large
Load Diff
2285
addons/payroll/i18n/es_CL.po
Normal file
2285
addons/payroll/i18n/es_CL.po
Normal file
File diff suppressed because it is too large
Load Diff
2285
addons/payroll/i18n/es_CO.po
Normal file
2285
addons/payroll/i18n/es_CO.po
Normal file
File diff suppressed because it is too large
Load Diff
2282
addons/payroll/i18n/es_CR.po
Normal file
2282
addons/payroll/i18n/es_CR.po
Normal file
File diff suppressed because it is too large
Load Diff
2285
addons/payroll/i18n/es_DO.po
Normal file
2285
addons/payroll/i18n/es_DO.po
Normal file
File diff suppressed because it is too large
Load Diff
2285
addons/payroll/i18n/es_EC.po
Normal file
2285
addons/payroll/i18n/es_EC.po
Normal file
File diff suppressed because it is too large
Load Diff
2285
addons/payroll/i18n/es_PE.po
Normal file
2285
addons/payroll/i18n/es_PE.po
Normal file
File diff suppressed because it is too large
Load Diff
2282
addons/payroll/i18n/es_PY.po
Normal file
2282
addons/payroll/i18n/es_PY.po
Normal file
File diff suppressed because it is too large
Load Diff
2285
addons/payroll/i18n/es_VE.po
Normal file
2285
addons/payroll/i18n/es_VE.po
Normal file
File diff suppressed because it is too large
Load Diff
2427
addons/payroll/i18n/et.po
Normal file
2427
addons/payroll/i18n/et.po
Normal file
File diff suppressed because it is too large
Load Diff
2311
addons/payroll/i18n/eu.po
Normal file
2311
addons/payroll/i18n/eu.po
Normal file
File diff suppressed because it is too large
Load Diff
2449
addons/payroll/i18n/fa.po
Normal file
2449
addons/payroll/i18n/fa.po
Normal file
File diff suppressed because it is too large
Load Diff
2277
addons/payroll/i18n/fa_IR.po
Normal file
2277
addons/payroll/i18n/fa_IR.po
Normal file
File diff suppressed because it is too large
Load Diff
2329
addons/payroll/i18n/fi.po
Normal file
2329
addons/payroll/i18n/fi.po
Normal file
File diff suppressed because it is too large
Load Diff
2279
addons/payroll/i18n/fil.po
Normal file
2279
addons/payroll/i18n/fil.po
Normal file
File diff suppressed because it is too large
Load Diff
2284
addons/payroll/i18n/fo.po
Normal file
2284
addons/payroll/i18n/fo.po
Normal file
File diff suppressed because it is too large
Load Diff
2456
addons/payroll/i18n/fr.po
Normal file
2456
addons/payroll/i18n/fr.po
Normal file
File diff suppressed because it is too large
Load Diff
2288
addons/payroll/i18n/fr_BE.po
Normal file
2288
addons/payroll/i18n/fr_BE.po
Normal file
File diff suppressed because it is too large
Load Diff
2285
addons/payroll/i18n/fr_CA.po
Normal file
2285
addons/payroll/i18n/fr_CA.po
Normal file
File diff suppressed because it is too large
Load Diff
2281
addons/payroll/i18n/gl.po
Normal file
2281
addons/payroll/i18n/gl.po
Normal file
File diff suppressed because it is too large
Load Diff
2292
addons/payroll/i18n/gu.po
Normal file
2292
addons/payroll/i18n/gu.po
Normal file
File diff suppressed because it is too large
Load Diff
2310
addons/payroll/i18n/he.po
Normal file
2310
addons/payroll/i18n/he.po
Normal file
File diff suppressed because it is too large
Load Diff
2281
addons/payroll/i18n/hi.po
Normal file
2281
addons/payroll/i18n/hi.po
Normal file
File diff suppressed because it is too large
Load Diff
2324
addons/payroll/i18n/hr.po
Normal file
2324
addons/payroll/i18n/hr.po
Normal file
File diff suppressed because it is too large
Load Diff
2401
addons/payroll/i18n/hu.po
Normal file
2401
addons/payroll/i18n/hu.po
Normal file
File diff suppressed because it is too large
Load Diff
2459
addons/payroll/i18n/id.po
Normal file
2459
addons/payroll/i18n/id.po
Normal file
File diff suppressed because it is too large
Load Diff
2299
addons/payroll/i18n/is.po
Normal file
2299
addons/payroll/i18n/is.po
Normal file
File diff suppressed because it is too large
Load Diff
2610
addons/payroll/i18n/it.po
Normal file
2610
addons/payroll/i18n/it.po
Normal file
File diff suppressed because it is too large
Load Diff
2382
addons/payroll/i18n/ja.po
Normal file
2382
addons/payroll/i18n/ja.po
Normal file
File diff suppressed because it is too large
Load Diff
2293
addons/payroll/i18n/ka.po
Normal file
2293
addons/payroll/i18n/ka.po
Normal file
File diff suppressed because it is too large
Load Diff
2338
addons/payroll/i18n/kab.po
Normal file
2338
addons/payroll/i18n/kab.po
Normal file
File diff suppressed because it is too large
Load Diff
2446
addons/payroll/i18n/km.po
Normal file
2446
addons/payroll/i18n/km.po
Normal file
File diff suppressed because it is too large
Load Diff
2442
addons/payroll/i18n/ko.po
Normal file
2442
addons/payroll/i18n/ko.po
Normal file
File diff suppressed because it is too large
Load Diff
2281
addons/payroll/i18n/lo.po
Normal file
2281
addons/payroll/i18n/lo.po
Normal file
File diff suppressed because it is too large
Load Diff
2464
addons/payroll/i18n/lt.po
Normal file
2464
addons/payroll/i18n/lt.po
Normal file
File diff suppressed because it is too large
Load Diff
2325
addons/payroll/i18n/lv.po
Normal file
2325
addons/payroll/i18n/lv.po
Normal file
File diff suppressed because it is too large
Load Diff
2284
addons/payroll/i18n/mk.po
Normal file
2284
addons/payroll/i18n/mk.po
Normal file
File diff suppressed because it is too large
Load Diff
2445
addons/payroll/i18n/mn.po
Normal file
2445
addons/payroll/i18n/mn.po
Normal file
File diff suppressed because it is too large
Load Diff
2325
addons/payroll/i18n/nb.po
Normal file
2325
addons/payroll/i18n/nb.po
Normal file
File diff suppressed because it is too large
Load Diff
2278
addons/payroll/i18n/ne.po
Normal file
2278
addons/payroll/i18n/ne.po
Normal file
File diff suppressed because it is too large
Load Diff
2459
addons/payroll/i18n/nl.po
Normal file
2459
addons/payroll/i18n/nl.po
Normal file
File diff suppressed because it is too large
Load Diff
2285
addons/payroll/i18n/nl_BE.po
Normal file
2285
addons/payroll/i18n/nl_BE.po
Normal file
File diff suppressed because it is too large
Load Diff
2369
addons/payroll/i18n/pl.po
Normal file
2369
addons/payroll/i18n/pl.po
Normal file
File diff suppressed because it is too large
Load Diff
2377
addons/payroll/i18n/pt.po
Normal file
2377
addons/payroll/i18n/pt.po
Normal file
File diff suppressed because it is too large
Load Diff
2397
addons/payroll/i18n/pt_BR.po
Normal file
2397
addons/payroll/i18n/pt_BR.po
Normal file
File diff suppressed because it is too large
Load Diff
2401
addons/payroll/i18n/ro.po
Normal file
2401
addons/payroll/i18n/ro.po
Normal file
File diff suppressed because it is too large
Load Diff
2465
addons/payroll/i18n/ru.po
Normal file
2465
addons/payroll/i18n/ru.po
Normal file
File diff suppressed because it is too large
Load Diff
2370
addons/payroll/i18n/sk.po
Normal file
2370
addons/payroll/i18n/sk.po
Normal file
File diff suppressed because it is too large
Load Diff
2369
addons/payroll/i18n/sl.po
Normal file
2369
addons/payroll/i18n/sl.po
Normal file
File diff suppressed because it is too large
Load Diff
2278
addons/payroll/i18n/so.po
Normal file
2278
addons/payroll/i18n/so.po
Normal file
File diff suppressed because it is too large
Load Diff
2284
addons/payroll/i18n/sq.po
Normal file
2284
addons/payroll/i18n/sq.po
Normal file
File diff suppressed because it is too large
Load Diff
2292
addons/payroll/i18n/sr.po
Normal file
2292
addons/payroll/i18n/sr.po
Normal file
File diff suppressed because it is too large
Load Diff
2295
addons/payroll/i18n/sr@latin.po
Normal file
2295
addons/payroll/i18n/sr@latin.po
Normal file
File diff suppressed because it is too large
Load Diff
2505
addons/payroll/i18n/sv.po
Normal file
2505
addons/payroll/i18n/sv.po
Normal file
File diff suppressed because it is too large
Load Diff
2291
addons/payroll/i18n/ta.po
Normal file
2291
addons/payroll/i18n/ta.po
Normal file
File diff suppressed because it is too large
Load Diff
2320
addons/payroll/i18n/th.po
Normal file
2320
addons/payroll/i18n/th.po
Normal file
File diff suppressed because it is too large
Load Diff
2442
addons/payroll/i18n/tr.po
Normal file
2442
addons/payroll/i18n/tr.po
Normal file
File diff suppressed because it is too large
Load Diff
2461
addons/payroll/i18n/uk.po
Normal file
2461
addons/payroll/i18n/uk.po
Normal file
File diff suppressed because it is too large
Load Diff
2450
addons/payroll/i18n/vi.po
Normal file
2450
addons/payroll/i18n/vi.po
Normal file
File diff suppressed because it is too large
Load Diff
2436
addons/payroll/i18n/zh_CN.po
Normal file
2436
addons/payroll/i18n/zh_CN.po
Normal file
File diff suppressed because it is too large
Load Diff
2426
addons/payroll/i18n/zh_TW.po
Normal file
2426
addons/payroll/i18n/zh_TW.po
Normal file
File diff suppressed because it is too large
Load Diff
27
addons/payroll/migrations/16.0.1.1.0/noupdate_changes.xml
Normal file
27
addons/payroll/migrations/16.0.1.1.0/noupdate_changes.xml
Normal file
@ -0,0 +1,27 @@
|
||||
<?xml version='1.0' encoding='utf-8' ?>
|
||||
<odoo>
|
||||
<record id="mail_template_hr_payslip" model="mail.template">
|
||||
<field name="email_from">{{user.employee_id.work_email}}</field>
|
||||
<field name="email_to">{{object.employee_id.work_contact_id.email}}</field>
|
||||
<field name="reply_to">{{user.employee_id.work_email}}</field>
|
||||
<field
|
||||
name="subject"
|
||||
>Payslip for {{object.payslip_run_id.name}} from {{object.company_id.name}}</field>
|
||||
<field name="lang">{{object.employee_id.work_contact_id.lang}}</field>
|
||||
<field name="body_html" type="html">
|
||||
<div style="margin: 0px; padding: 0px;">
|
||||
<p style="margin: 0px; padding: 0px; font-size: 13px;">
|
||||
Dear <t t-out="object.employee_id.name" />,
|
||||
<br /><br />
|
||||
Please find attached the payslip for <t
|
||||
t-out="object.payslip_run_id.name"
|
||||
/>.
|
||||
<br /><br />
|
||||
Do not hesitate to contact us if you have any questions.
|
||||
<br /><br />
|
||||
<t t-out="user.signature" />
|
||||
</p>
|
||||
</div>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
11
addons/payroll/migrations/16.0.1.1.0/post-migration.py
Normal file
11
addons/payroll/migrations/16.0.1.1.0/post-migration.py
Normal file
@ -0,0 +1,11 @@
|
||||
# Copyright 2023 Tecnativa - Víctor Martínez
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from openupgradelib import openupgrade
|
||||
|
||||
|
||||
@openupgrade.migrate()
|
||||
def migrate(env, version):
|
||||
openupgrade.load_data(
|
||||
env.cr, "payroll", "migrations/16.0.1.1.0/noupdate_changes.xml"
|
||||
)
|
17
addons/payroll/models/__init__.py
Normal file
17
addons/payroll/models/__init__.py
Normal file
@ -0,0 +1,17 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import hr_contract
|
||||
from . import hr_employee
|
||||
from . import hr_leave_type
|
||||
from . import hr_payroll_structure
|
||||
from . import hr_salary_rule
|
||||
from . import hr_salary_rule_category
|
||||
from . import hr_rule_input
|
||||
from . import hr_contribution_register
|
||||
from . import base_browsable
|
||||
from . import hr_payslip
|
||||
from . import hr_payslip_line
|
||||
from . import hr_payslip_input
|
||||
from . import hr_payslip_worked_days
|
||||
from . import hr_payslip_run
|
||||
from . import res_config_settings
|
101
addons/payroll/models/base_browsable.py
Normal file
101
addons/payroll/models/base_browsable.py
Normal file
@ -0,0 +1,101 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import logging
|
||||
|
||||
from odoo import fields
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseBrowsableObject:
|
||||
def __init__(self, vals_dict):
|
||||
self.__dict__["base_fields"] = ["base_fields", "dict"]
|
||||
self.dict = vals_dict
|
||||
|
||||
def __getattr__(self, attr):
|
||||
return attr in self.dict and self.dict.__getitem__(attr) or 0.0
|
||||
|
||||
def __setattr__(self, attr, value):
|
||||
_fields = self.__dict__["base_fields"]
|
||||
if attr in _fields:
|
||||
return super().__setattr__(attr, value)
|
||||
self.__dict__["dict"][attr] = value
|
||||
|
||||
def __str__(self):
|
||||
return str(self.__dict__)
|
||||
|
||||
|
||||
# These classes are used in the _get_payslip_lines() method
|
||||
class BrowsableObject(BaseBrowsableObject):
|
||||
def __init__(self, employee_id, vals_dict, env):
|
||||
super().__init__(vals_dict)
|
||||
self.base_fields += ["employee_id", "env"]
|
||||
self.employee_id = employee_id
|
||||
self.env = env
|
||||
|
||||
|
||||
class InputLine(BrowsableObject):
|
||||
"""a class that will be used into the python code, mainly for
|
||||
usability purposes"""
|
||||
|
||||
def sum(self, code, from_date, to_date=None):
|
||||
if to_date is None:
|
||||
to_date = fields.Date.today()
|
||||
self.env.cr.execute(
|
||||
"""
|
||||
SELECT sum(amount) as sum
|
||||
FROM hr_payslip as hp, hr_payslip_input as pi
|
||||
WHERE hp.employee_id = %s AND hp.state = 'done'
|
||||
AND hp.date_from >= %s AND hp.date_to <= %s
|
||||
AND hp.id = pi.payslip_id AND pi.code = %s""",
|
||||
(self.employee_id, from_date, to_date, code),
|
||||
)
|
||||
return self.env.cr.fetchone()[0] or 0.0
|
||||
|
||||
|
||||
class WorkedDays(BrowsableObject):
|
||||
"""a class that will be used into the python code, mainly for
|
||||
usability purposes"""
|
||||
|
||||
def _sum(self, code, from_date, to_date=None):
|
||||
if to_date is None:
|
||||
to_date = fields.Date.today()
|
||||
self.env.cr.execute(
|
||||
"""
|
||||
SELECT sum(number_of_days) as number_of_days,
|
||||
sum(number_of_hours) as number_of_hours
|
||||
FROM hr_payslip as hp, hr_payslip_worked_days as pi
|
||||
WHERE hp.employee_id = %s AND hp.state = 'done'
|
||||
AND hp.date_from >= %s AND hp.date_to <= %s
|
||||
AND hp.id = pi.payslip_id AND pi.code = %s""",
|
||||
(self.employee_id, from_date, to_date, code),
|
||||
)
|
||||
return self.env.cr.fetchone()
|
||||
|
||||
def sum(self, code, from_date, to_date=None):
|
||||
res = self._sum(code, from_date, to_date)
|
||||
return res and res[0] or 0.0
|
||||
|
||||
def sum_hours(self, code, from_date, to_date=None):
|
||||
res = self._sum(code, from_date, to_date)
|
||||
return res and res[1] or 0.0
|
||||
|
||||
|
||||
class Payslips(BrowsableObject):
|
||||
"""a class that will be used into the python code, mainly for
|
||||
usability purposes"""
|
||||
|
||||
def sum(self, code, from_date, to_date=None):
|
||||
if to_date is None:
|
||||
to_date = fields.Date.today()
|
||||
self.env.cr.execute(
|
||||
"""SELECT sum(case when hp.credit_note = False then
|
||||
(pl.total) else (-pl.total) end)
|
||||
FROM hr_payslip as hp, hr_payslip_line as pl
|
||||
WHERE hp.employee_id = %s AND hp.state = 'done'
|
||||
AND hp.date_from >= %s AND hp.date_to <= %s AND
|
||||
hp.id = pl.slip_id AND pl.code = %s""",
|
||||
(self.employee_id, from_date, to_date, code),
|
||||
)
|
||||
res = self.env.cr.fetchone()
|
||||
return res and res[0] or 0.0
|
45
addons/payroll/models/hr_contract.py
Normal file
45
addons/payroll/models/hr_contract.py
Normal file
@ -0,0 +1,45 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class HrContract(models.Model):
|
||||
"""
|
||||
Employee contract based on the visa, work permits
|
||||
allows to configure different Salary structure
|
||||
"""
|
||||
|
||||
_inherit = "hr.contract"
|
||||
_description = "Employee Contract"
|
||||
|
||||
struct_id = fields.Many2one("hr.payroll.structure", string="Salary Structure")
|
||||
schedule_pay = fields.Selection(
|
||||
[
|
||||
("monthly", "Monthly"),
|
||||
("quarterly", "Quarterly"),
|
||||
("semi-annually", "Semi-annually"),
|
||||
("annually", "Annually"),
|
||||
("weekly", "Weekly"),
|
||||
("bi-weekly", "Bi-weekly"),
|
||||
("bi-monthly", "Bi-monthly"),
|
||||
],
|
||||
string="Scheduled Pay",
|
||||
index=True,
|
||||
default="monthly",
|
||||
help="Defines the frequency of the wage payment.",
|
||||
)
|
||||
resource_calendar_id = fields.Many2one(
|
||||
required=True, help="Employee's working schedule."
|
||||
)
|
||||
|
||||
def get_all_structures(self):
|
||||
"""
|
||||
@return: the structures linked to the given contracts, ordered by
|
||||
hierachy (parent=False first, then first level children and
|
||||
so on) and without duplicates
|
||||
"""
|
||||
structures = self.mapped("struct_id")
|
||||
if not structures:
|
||||
return []
|
||||
# YTI TODO return browse records
|
||||
return list(set(structures._get_parent_structure().ids))
|
20
addons/payroll/models/hr_contribution_register.py
Normal file
20
addons/payroll/models/hr_contribution_register.py
Normal file
@ -0,0 +1,20 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class HrContributionRegister(models.Model):
|
||||
_name = "hr.contribution.register"
|
||||
_description = "Contribution Register"
|
||||
|
||||
company_id = fields.Many2one(
|
||||
"res.company",
|
||||
string="Company",
|
||||
default=lambda self: self.env.company,
|
||||
)
|
||||
partner_id = fields.Many2one("res.partner", string="Partner")
|
||||
name = fields.Char(required=True)
|
||||
register_line_ids = fields.One2many(
|
||||
"hr.payslip.line", "register_id", string="Register Line", readonly=True
|
||||
)
|
||||
note = fields.Text(string="Description")
|
20
addons/payroll/models/hr_employee.py
Normal file
20
addons/payroll/models/hr_employee.py
Normal file
@ -0,0 +1,20 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class HrEmployee(models.Model):
|
||||
_inherit = "hr.employee"
|
||||
_description = "Employee"
|
||||
|
||||
slip_ids = fields.One2many(
|
||||
"hr.payslip", "employee_id", string="Payslips", readonly=True
|
||||
)
|
||||
payslip_count = fields.Integer(
|
||||
compute="_compute_payslip_count",
|
||||
groups="payroll.group_payroll_user",
|
||||
)
|
||||
|
||||
def _compute_payslip_count(self):
|
||||
for employee in self:
|
||||
employee.payslip_count = len(employee.slip_ids)
|
10
addons/payroll/models/hr_leave_type.py
Normal file
10
addons/payroll/models/hr_leave_type.py
Normal file
@ -0,0 +1,10 @@
|
||||
# Copyright (C) 2022 Trevi Software (https://trevi.et)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class HrLeaveType(models.Model):
|
||||
_inherit = "hr.leave.type"
|
||||
|
||||
code = fields.Char(string="Payroll Code")
|
85
addons/payroll/models/hr_payroll_structure.py
Normal file
85
addons/payroll/models/hr_payroll_structure.py
Normal file
@ -0,0 +1,85 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class HrPayrollStructure(models.Model):
|
||||
"""
|
||||
Salary structure used to defined
|
||||
- Basic
|
||||
- Allowances
|
||||
- Deductions
|
||||
"""
|
||||
|
||||
_name = "hr.payroll.structure"
|
||||
_description = "Salary Structure"
|
||||
|
||||
@api.model
|
||||
def _get_parent(self):
|
||||
return self.env.ref("hr_payroll.structure_base", False)
|
||||
|
||||
name = fields.Char(required=True)
|
||||
code = fields.Char(string="Reference")
|
||||
company_id = fields.Many2one(
|
||||
"res.company",
|
||||
string="Company",
|
||||
required=True,
|
||||
copy=False,
|
||||
default=lambda self: self.env.company,
|
||||
)
|
||||
note = fields.Text(string="Description")
|
||||
parent_id = fields.Many2one(
|
||||
"hr.payroll.structure", string="Parent", default=_get_parent
|
||||
)
|
||||
children_ids = fields.One2many(
|
||||
"hr.payroll.structure", "parent_id", string="Children", copy=True
|
||||
)
|
||||
rule_ids = fields.Many2many(
|
||||
"hr.salary.rule",
|
||||
"hr_structure_salary_rule_rel",
|
||||
"struct_id",
|
||||
"rule_id",
|
||||
string="Salary Rules",
|
||||
)
|
||||
require_code = fields.Boolean(
|
||||
"Require code",
|
||||
compute="_compute_require_code",
|
||||
default=lambda self: self._compute_require_code(),
|
||||
)
|
||||
|
||||
def _compute_require_code(self):
|
||||
require = (
|
||||
self.env["ir.config_parameter"]
|
||||
.sudo()
|
||||
.get_param("payroll.require_code_and_category")
|
||||
)
|
||||
self.require_code = require
|
||||
return require
|
||||
|
||||
@api.constrains("parent_id")
|
||||
def _check_parent_id(self):
|
||||
if not self._check_recursion():
|
||||
raise ValidationError(_("You cannot create a recursive salary structure."))
|
||||
|
||||
@api.returns("self", lambda value: value.id)
|
||||
def copy(self, default=None):
|
||||
self.ensure_one()
|
||||
default = dict(default or {}, code=_("%s (copy)") % self.code)
|
||||
return super().copy(default)
|
||||
|
||||
def get_all_rules(self):
|
||||
"""
|
||||
@return: returns a list of tuple (id, sequence) of rules that are maybe
|
||||
to apply
|
||||
"""
|
||||
all_rules = []
|
||||
for struct in self:
|
||||
all_rules += struct.rule_ids._recursive_search_of_rules()
|
||||
return all_rules
|
||||
|
||||
def _get_parent_structure(self):
|
||||
parent = self.mapped("parent_id")
|
||||
if parent:
|
||||
parent = parent._get_parent_structure()
|
||||
return parent + self
|
783
addons/payroll/models/hr_payslip.py
Normal file
783
addons/payroll/models/hr_payslip.py
Normal file
@ -0,0 +1,783 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import logging
|
||||
import math
|
||||
from datetime import date, datetime, time
|
||||
|
||||
import babel
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from pytz import timezone
|
||||
|
||||
from odoo import _, api, fields, models, tools
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.tools.safe_eval import safe_eval
|
||||
|
||||
from .base_browsable import (
|
||||
BaseBrowsableObject,
|
||||
BrowsableObject,
|
||||
InputLine,
|
||||
Payslips,
|
||||
WorkedDays,
|
||||
)
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HrPayslip(models.Model):
|
||||
_name = "hr.payslip"
|
||||
_inherit = ["mail.thread", "mail.activity.mixin"]
|
||||
_description = "Payslip"
|
||||
_order = "id desc"
|
||||
|
||||
struct_id = fields.Many2one(
|
||||
"hr.payroll.structure",
|
||||
string="Structure",
|
||||
readonly=True,
|
||||
help="Defines the rules that have to be applied to this payslip, "
|
||||
"accordingly to the contract chosen. If you let empty the field "
|
||||
"contract, this field isn't mandatory anymore and thus the rules "
|
||||
"applied will be all the rules set on the structure of all contracts "
|
||||
"of the employee valid for the chosen period",
|
||||
)
|
||||
name = fields.Char(string="Payslip Name", readonly=True)
|
||||
number = fields.Char(
|
||||
string="Reference",
|
||||
readonly=True,
|
||||
copy=False,
|
||||
)
|
||||
employee_id = fields.Many2one(
|
||||
"hr.employee",
|
||||
string="Employee",
|
||||
required=True,
|
||||
readonly=True,
|
||||
)
|
||||
date_from = fields.Date(
|
||||
readonly=True,
|
||||
required=True,
|
||||
default=lambda self: fields.Date.to_string(date.today().replace(day=1)),
|
||||
tracking=True,
|
||||
)
|
||||
date_to = fields.Date(
|
||||
readonly=True,
|
||||
required=True,
|
||||
default=lambda self: fields.Date.to_string(
|
||||
(datetime.now() + relativedelta(months=+1, day=1, days=-1)).date()
|
||||
),
|
||||
tracking=True,
|
||||
)
|
||||
state = fields.Selection(
|
||||
[
|
||||
("draft", "Draft"),
|
||||
("verify", "Waiting"),
|
||||
("done", "Done"),
|
||||
("cancel", "Rejected"),
|
||||
],
|
||||
string="Status",
|
||||
index=True,
|
||||
readonly=True,
|
||||
copy=False,
|
||||
default="draft",
|
||||
tracking=True,
|
||||
help="""* When the payslip is created the status is \'Draft\'
|
||||
\n* If the payslip is under verification, the status is \'Waiting\'.
|
||||
\n* If the payslip is confirmed then status is set to \'Done\'.
|
||||
\n* When user cancel payslip the status is \'Rejected\'.""",
|
||||
)
|
||||
line_ids = fields.One2many(
|
||||
"hr.payslip.line",
|
||||
"slip_id",
|
||||
string="Payslip Lines",
|
||||
readonly=True,
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
"res.company",
|
||||
string="Company",
|
||||
readonly=True,
|
||||
copy=False,
|
||||
default=lambda self: self.env.company,
|
||||
)
|
||||
worked_days_line_ids = fields.One2many(
|
||||
"hr.payslip.worked_days",
|
||||
"payslip_id",
|
||||
string="Payslip Worked Days",
|
||||
copy=True,
|
||||
readonly=True,
|
||||
)
|
||||
input_line_ids = fields.One2many(
|
||||
"hr.payslip.input",
|
||||
"payslip_id",
|
||||
string="Payslip Inputs",
|
||||
readonly=True,
|
||||
)
|
||||
paid = fields.Boolean(
|
||||
string="Made Payment Order ? ",
|
||||
readonly=True,
|
||||
copy=False,
|
||||
)
|
||||
note = fields.Text(
|
||||
string="Internal Note",
|
||||
readonly=True,
|
||||
tracking=True,
|
||||
)
|
||||
contract_id = fields.Many2one(
|
||||
"hr.contract",
|
||||
string="Contract",
|
||||
readonly=True,
|
||||
tracking=True,
|
||||
)
|
||||
dynamic_filtered_payslip_lines = fields.One2many(
|
||||
"hr.payslip.line",
|
||||
compute="_compute_dynamic_filtered_payslip_lines",
|
||||
)
|
||||
credit_note = fields.Boolean(
|
||||
readonly=True,
|
||||
help="Indicates this payslip has a refund of another",
|
||||
)
|
||||
payslip_run_id = fields.Many2one(
|
||||
"hr.payslip.run",
|
||||
string="Payslip Batches",
|
||||
readonly=True,
|
||||
copy=False,
|
||||
tracking=True,
|
||||
)
|
||||
payslip_count = fields.Integer(
|
||||
compute="_compute_payslip_count", string="Payslip Computation Details"
|
||||
)
|
||||
hide_child_lines = fields.Boolean(default=False)
|
||||
hide_invisible_lines = fields.Boolean(
|
||||
string="Show only lines that appear on payslip", default=False
|
||||
)
|
||||
compute_date = fields.Date()
|
||||
refunded_id = fields.Many2one(
|
||||
"hr.payslip", string="Refunded Payslip", readonly=True
|
||||
)
|
||||
allow_cancel_payslips = fields.Boolean(
|
||||
"Allow Canceling Payslips", compute="_compute_allow_cancel_payslips"
|
||||
)
|
||||
prevent_compute_on_confirm = fields.Boolean(
|
||||
"Prevent Compute on Confirm", compute="_compute_prevent_compute_on_confirm"
|
||||
)
|
||||
|
||||
def _compute_allow_cancel_payslips(self):
|
||||
self.allow_cancel_payslips = (
|
||||
self.env["ir.config_parameter"]
|
||||
.sudo()
|
||||
.get_param("payroll.allow_cancel_payslips")
|
||||
)
|
||||
|
||||
def _compute_prevent_compute_on_confirm(self):
|
||||
self.prevent_compute_on_confirm = (
|
||||
self.env["ir.config_parameter"]
|
||||
.sudo()
|
||||
.get_param("payroll.prevent_compute_on_confirm")
|
||||
)
|
||||
|
||||
@api.depends("line_ids", "hide_child_lines", "hide_invisible_lines")
|
||||
def _compute_dynamic_filtered_payslip_lines(self):
|
||||
for payslip in self:
|
||||
lines = payslip.line_ids
|
||||
if payslip.hide_child_lines:
|
||||
lines = lines.filtered(lambda line: not line.parent_rule_id)
|
||||
if payslip.hide_invisible_lines:
|
||||
lines = lines.filtered(lambda line: line.appears_on_payslip)
|
||||
payslip.dynamic_filtered_payslip_lines = lines
|
||||
|
||||
def _compute_payslip_count(self):
|
||||
for payslip in self:
|
||||
payslip.payslip_count = len(payslip.line_ids)
|
||||
|
||||
@api.constrains("date_from", "date_to")
|
||||
def _check_dates(self):
|
||||
if any(self.filtered(lambda payslip: payslip.date_from > payslip.date_to)):
|
||||
raise ValidationError(
|
||||
_("Payslip 'Date From' must be earlier than 'Date To'.")
|
||||
)
|
||||
|
||||
def copy(self, default=None):
|
||||
rec = super().copy(default)
|
||||
for line in self.input_line_ids:
|
||||
line.copy({"payslip_id": rec.id})
|
||||
for line in self.line_ids:
|
||||
line.copy({"slip_id": rec.id, "input_ids": []})
|
||||
return rec
|
||||
|
||||
def action_payslip_draft(self):
|
||||
return self.write({"state": "draft"})
|
||||
|
||||
def action_payslip_done(self):
|
||||
if (
|
||||
not self.env.context.get("without_compute_sheet")
|
||||
and not self.prevent_compute_on_confirm
|
||||
):
|
||||
self.compute_sheet()
|
||||
return self.write({"state": "done"})
|
||||
|
||||
def action_payslip_cancel(self):
|
||||
for payslip in self:
|
||||
if payslip.allow_cancel_payslips:
|
||||
if payslip.refunded_id and payslip.refunded_id.state != "cancel":
|
||||
raise ValidationError(
|
||||
_(
|
||||
"""To cancel the Original Payslip the
|
||||
Refunded Payslip needs to be canceled first!"""
|
||||
)
|
||||
)
|
||||
else:
|
||||
if self.filtered(lambda slip: slip.state == "done"):
|
||||
raise UserError(_("Cannot cancel a payslip that is done."))
|
||||
return self.write({"state": "cancel"})
|
||||
|
||||
def refund_sheet(self):
|
||||
for payslip in self:
|
||||
copied_payslip = payslip.copy(
|
||||
{"credit_note": True, "name": _("Refund: %s") % payslip.name}
|
||||
)
|
||||
number = copied_payslip.number or self.env["ir.sequence"].next_by_code(
|
||||
"salary.slip"
|
||||
)
|
||||
copied_payslip.write({"number": number})
|
||||
copied_payslip.with_context(
|
||||
without_compute_sheet=True
|
||||
).action_payslip_done()
|
||||
formview_ref = self.env.ref("payroll.hr_payslip_view_form", False)
|
||||
treeview_ref = self.env.ref("payroll.hr_payslip_view_tree", False)
|
||||
res = {
|
||||
"name": _("Refund Payslip"),
|
||||
"view_mode": "tree, form",
|
||||
"view_id": False,
|
||||
"res_model": "hr.payslip",
|
||||
"type": "ir.actions.act_window",
|
||||
"target": "current",
|
||||
"domain": "[('id', 'in', %s)]" % copied_payslip.ids,
|
||||
"views": [
|
||||
(treeview_ref and treeview_ref.id or False, "tree"),
|
||||
(formview_ref and formview_ref.id or False, "form"),
|
||||
],
|
||||
"context": {},
|
||||
}
|
||||
payslip.write({"refunded_id": safe_eval(res["domain"])[0][2][0] or False})
|
||||
return res
|
||||
|
||||
def unlink(self):
|
||||
if any(self.filtered(lambda payslip: payslip.state not in ("draft", "cancel"))):
|
||||
raise UserError(
|
||||
_("You cannot delete a payslip which is not draft or cancelled")
|
||||
)
|
||||
return super().unlink()
|
||||
|
||||
def compute_sheet(self):
|
||||
for payslip in self:
|
||||
# delete old payslip lines
|
||||
payslip.line_ids.unlink()
|
||||
# write payslip lines
|
||||
number = payslip.number or self.env["ir.sequence"].next_by_code(
|
||||
"salary.slip"
|
||||
)
|
||||
lines = [(0, 0, line) for line in list(payslip.get_lines_dict().values())]
|
||||
payslip.write(
|
||||
{
|
||||
"line_ids": lines,
|
||||
"number": number,
|
||||
"state": "verify",
|
||||
"compute_date": fields.Date.today(),
|
||||
}
|
||||
)
|
||||
return True
|
||||
|
||||
@api.model
|
||||
def get_worked_day_lines(self, contracts, date_from, date_to):
|
||||
"""
|
||||
@param contracts: Browse record of contracts
|
||||
@return: returns a list of dict containing the input that should be
|
||||
applied for the given contract between date_from and date_to
|
||||
"""
|
||||
res = []
|
||||
for contract in contracts.filtered(
|
||||
lambda contract: contract.resource_calendar_id
|
||||
):
|
||||
day_from = datetime.combine(date_from, time.min)
|
||||
day_to = datetime.combine(date_to, time.max)
|
||||
day_contract_start = datetime.combine(contract.date_start, time.min)
|
||||
# Support for the hr_public_holidays module.
|
||||
contract = contract.with_context(
|
||||
employee_id=self.employee_id.id, exclude_public_holidays=True
|
||||
)
|
||||
# only use payslip day_from if it's greather than contract start date
|
||||
if day_from < day_contract_start:
|
||||
day_from = day_contract_start
|
||||
# == compute leave days == #
|
||||
leaves = self._compute_leave_days(contract, day_from, day_to)
|
||||
res.extend(leaves)
|
||||
# == compute worked days == #
|
||||
attendances = self._compute_worked_days(contract, day_from, day_to)
|
||||
res.append(attendances)
|
||||
return res
|
||||
|
||||
def _compute_leave_days(self, contract, day_from, day_to):
|
||||
"""
|
||||
Leave days computation
|
||||
@return: returns a list containing the leave inputs for the period
|
||||
of the payslip. One record per leave type.
|
||||
"""
|
||||
leaves_positive = (
|
||||
self.env["ir.config_parameter"].sudo().get_param("payroll.leaves_positive")
|
||||
)
|
||||
leaves = {}
|
||||
calendar = contract.resource_calendar_id
|
||||
tz = timezone(calendar.tz)
|
||||
day_leave_intervals = contract.employee_id.list_leaves(
|
||||
day_from, day_to, calendar=contract.resource_calendar_id
|
||||
)
|
||||
for day, hours, leave in day_leave_intervals:
|
||||
holiday = leave[:1].holiday_id
|
||||
current_leave_struct = leaves.setdefault(
|
||||
holiday.holiday_status_id,
|
||||
{
|
||||
"name": holiday.holiday_status_id.name or _("Global Leaves"),
|
||||
"sequence": 5,
|
||||
"code": holiday.holiday_status_id.code or "GLOBAL",
|
||||
"number_of_days": 0.0,
|
||||
"number_of_hours": 0.0,
|
||||
"contract_id": contract.id,
|
||||
},
|
||||
)
|
||||
if leaves_positive:
|
||||
current_leave_struct["number_of_hours"] += hours
|
||||
else:
|
||||
current_leave_struct["number_of_hours"] -= hours
|
||||
work_hours = calendar.get_work_hours_count(
|
||||
tz.localize(datetime.combine(day, time.min)),
|
||||
tz.localize(datetime.combine(day, time.max)),
|
||||
compute_leaves=False,
|
||||
)
|
||||
if work_hours:
|
||||
if leaves_positive:
|
||||
current_leave_struct["number_of_days"] += hours / work_hours
|
||||
else:
|
||||
current_leave_struct["number_of_days"] -= hours / work_hours
|
||||
return leaves.values()
|
||||
|
||||
def _compute_worked_days(self, contract, day_from, day_to):
|
||||
"""
|
||||
Worked days computation
|
||||
@return: returns a list containing the total worked_days for the period
|
||||
of the payslip. This returns the FULL work days expected for the resource
|
||||
calendar selected for the employee (it don't substract leaves by default).
|
||||
"""
|
||||
work_data = contract.employee_id._get_work_days_data_batch(
|
||||
day_from,
|
||||
day_to,
|
||||
calendar=contract.resource_calendar_id,
|
||||
compute_leaves=False,
|
||||
)
|
||||
return {
|
||||
"name": _("Normal Working Days paid at 100%"),
|
||||
"sequence": 1,
|
||||
"code": "WORK100",
|
||||
"number_of_days": work_data[contract.employee_id.id]["days"],
|
||||
"number_of_hours": work_data[contract.employee_id.id]["hours"],
|
||||
"contract_id": contract.id,
|
||||
}
|
||||
|
||||
@api.model
|
||||
def get_inputs(self, contracts, date_from, date_to):
|
||||
# TODO: We leave date_from and date_to params here for backwards
|
||||
# compatibility reasons for the ones who inherit this function
|
||||
# in another modules, but they are not used.
|
||||
# Will be removed in next versions.
|
||||
"""
|
||||
Inputs computation.
|
||||
@returns: Returns a dict with the inputs that are fetched from the salary_structure
|
||||
associated rules for the given contracts.
|
||||
""" # noqa: E501
|
||||
res = []
|
||||
current_structure = self.struct_id
|
||||
structure_ids = contracts.get_all_structures()
|
||||
if current_structure:
|
||||
structure_ids = list(set(current_structure._get_parent_structure().ids))
|
||||
rule_ids = (
|
||||
self.env["hr.payroll.structure"].browse(structure_ids).get_all_rules()
|
||||
)
|
||||
sorted_rule_ids = [id for id, sequence in sorted(rule_ids, key=lambda x: x[1])]
|
||||
payslip_inputs = (
|
||||
self.env["hr.salary.rule"].browse(sorted_rule_ids).mapped("input_ids")
|
||||
)
|
||||
for contract in contracts:
|
||||
for payslip_input in payslip_inputs:
|
||||
res.append(
|
||||
{
|
||||
"name": payslip_input.name,
|
||||
"code": payslip_input.code,
|
||||
"contract_id": contract.id,
|
||||
}
|
||||
)
|
||||
return res
|
||||
|
||||
def _init_payroll_dict_contracts(self):
|
||||
return {
|
||||
"count": 0,
|
||||
}
|
||||
|
||||
def get_payroll_dict(self, contracts):
|
||||
"""Setup miscellaneous dictionary values.
|
||||
Other modules may overload this method to inject discreet values into
|
||||
the salary rules. Such values will be available to the salary rule
|
||||
under the `payroll.` prefix.
|
||||
|
||||
This method is evaluated once per payslip.
|
||||
:param contracts: Recordset of all hr.contract records in this payslip
|
||||
:return: a dictionary of discreet values and/or Browsable Objects
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
res = {
|
||||
# In salary rules refer to this as: payroll.contracts.count
|
||||
"contracts": BaseBrowsableObject(self._init_payroll_dict_contracts()),
|
||||
}
|
||||
res["contracts"].count = len(contracts)
|
||||
|
||||
return res
|
||||
|
||||
def get_current_contract_dict(self, contract, contracts):
|
||||
"""Contract dependent dictionary values.
|
||||
This method is called just before the salary rules are evaluated for
|
||||
contract.
|
||||
|
||||
This method is evaluated once for every contract in the payslip.
|
||||
|
||||
:param contract: The current hr.contract being processed
|
||||
:param contracts: Recordset of all hr.contract records in this payslip
|
||||
:return: a dictionary of discreet values and/or Browsable Objects
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
# res = super().get_current_contract_dict(contract, contracts)
|
||||
# res.update({
|
||||
# # In salary rules refer to these as:
|
||||
# # current_contract.foo
|
||||
# # current_contract.foo.bar.baz
|
||||
# "foo": 0,
|
||||
# "bar": BaseBrowsableObject(
|
||||
# {
|
||||
# "baz": 0
|
||||
# }
|
||||
# )
|
||||
# })
|
||||
# <do something to update values in res>
|
||||
# return res
|
||||
|
||||
return {}
|
||||
|
||||
def _get_tools_dict(self):
|
||||
# _get_tools_dict() is intended to be inherited by other private modules
|
||||
# to add tools or python libraries available in localdict
|
||||
return {"math": math} # "math" object is useful for doing calculations
|
||||
|
||||
def _get_baselocaldict(self, contracts):
|
||||
self.ensure_one()
|
||||
worked_days_dict = {
|
||||
line.code: line for line in self.worked_days_line_ids if line.code
|
||||
}
|
||||
input_lines_dict = {
|
||||
line.code: line for line in self.input_line_ids if line.code
|
||||
}
|
||||
localdict = {
|
||||
"payslips": Payslips(self.employee_id.id, self, self.env),
|
||||
"worked_days": WorkedDays(self.employee_id.id, worked_days_dict, self.env),
|
||||
"inputs": InputLine(self.employee_id.id, input_lines_dict, self.env),
|
||||
"payroll": BrowsableObject(
|
||||
self.employee_id.id, self.get_payroll_dict(contracts), self.env
|
||||
),
|
||||
"current_contract": BrowsableObject(self.employee_id.id, {}, self.env),
|
||||
"categories": BrowsableObject(self.employee_id.id, {}, self.env),
|
||||
"rules": BrowsableObject(self.employee_id.id, {}, self.env),
|
||||
"result_rules": BrowsableObject(self.employee_id.id, {}, self.env),
|
||||
"tools": BrowsableObject(
|
||||
self.employee_id.id, self._get_tools_dict(), self.env
|
||||
),
|
||||
}
|
||||
return localdict
|
||||
|
||||
def _get_salary_rules(self):
|
||||
rule_obj = self.env["hr.salary.rule"]
|
||||
sorted_rules = rule_obj
|
||||
for payslip in self:
|
||||
contracts = payslip._get_employee_contracts()
|
||||
if len(contracts) == 1 and payslip.struct_id:
|
||||
structure_ids = list(set(payslip.struct_id._get_parent_structure().ids))
|
||||
else:
|
||||
structure_ids = contracts.get_all_structures()
|
||||
rule_ids = (
|
||||
self.env["hr.payroll.structure"].browse(structure_ids).get_all_rules()
|
||||
)
|
||||
sorted_rule_ids = [
|
||||
id for id, sequence in sorted(rule_ids, key=lambda x: x[1])
|
||||
]
|
||||
sorted_rules |= rule_obj.browse(sorted_rule_ids)
|
||||
return sorted_rules
|
||||
|
||||
def _compute_payslip_line(self, rule, localdict, lines_dict):
|
||||
self.ensure_one()
|
||||
# check if there is already a rule computed with that code
|
||||
previous_amount = rule.code in localdict and localdict[rule.code] or 0.0
|
||||
# compute the rule to get some values for the payslip line
|
||||
values = rule._compute_rule(localdict)
|
||||
key = (rule.code or "id" + str(rule.id)) + "-" + str(localdict["contract"].id)
|
||||
return self._get_lines_dict(
|
||||
rule, localdict, lines_dict, key, values, previous_amount
|
||||
)
|
||||
|
||||
def _get_lines_dict(
|
||||
self, rule, localdict, lines_dict, key, values, previous_amount
|
||||
):
|
||||
total = values["quantity"] * values["rate"] * values["amount"] / 100.0
|
||||
values["total"] = total
|
||||
# set/overwrite the amount computed for this rule in the localdict
|
||||
if rule.code:
|
||||
localdict[rule.code] = total
|
||||
localdict["rules"].dict[rule.code] = rule
|
||||
localdict["result_rules"].dict[rule.code] = BaseBrowsableObject(values)
|
||||
# sum the amount for its salary category
|
||||
localdict = self._sum_salary_rule_category(
|
||||
localdict, rule.category_id, total - previous_amount
|
||||
)
|
||||
# create/overwrite the line in the temporary results
|
||||
line_dict = {
|
||||
"salary_rule_id": rule.id,
|
||||
"employee_id": localdict["employee"].id,
|
||||
"contract_id": localdict["contract"].id,
|
||||
"code": rule.code,
|
||||
"category_id": rule.category_id.id,
|
||||
"sequence": rule.sequence,
|
||||
"appears_on_payslip": rule.appears_on_payslip,
|
||||
"parent_rule_id": rule.parent_rule_id.id,
|
||||
"condition_select": rule.condition_select,
|
||||
"condition_python": rule.condition_python,
|
||||
"condition_range": rule.condition_range,
|
||||
"condition_range_min": rule.condition_range_min,
|
||||
"condition_range_max": rule.condition_range_max,
|
||||
"amount_select": rule.amount_select,
|
||||
"amount_fix": rule.amount_fix,
|
||||
"amount_python_compute": rule.amount_python_compute,
|
||||
"amount_percentage": rule.amount_percentage,
|
||||
"amount_percentage_base": rule.amount_percentage_base,
|
||||
"register_id": rule.register_id.id,
|
||||
}
|
||||
line_dict.update(values)
|
||||
lines_dict[key] = line_dict
|
||||
return localdict, lines_dict
|
||||
|
||||
@api.model
|
||||
def _get_payslip_lines(self, _contract_ids, payslip_id):
|
||||
_logger.warning(
|
||||
"Use of _get_payslip_lines() is deprecated. "
|
||||
"Use get_lines_dict() instead."
|
||||
)
|
||||
return self.browse(payslip_id).get_lines_dict()
|
||||
|
||||
def get_lines_dict(self):
|
||||
lines_dict = {}
|
||||
blacklist = []
|
||||
for payslip in self:
|
||||
contracts = payslip._get_employee_contracts()
|
||||
baselocaldict = payslip._get_baselocaldict(contracts)
|
||||
for contract in contracts:
|
||||
# assign "current_contract" dict
|
||||
baselocaldict["current_contract"] = BrowsableObject(
|
||||
payslip.employee_id.id,
|
||||
payslip.get_current_contract_dict(contract, contracts),
|
||||
payslip.env,
|
||||
)
|
||||
# set up localdict with current contract and employee values
|
||||
localdict = dict(
|
||||
baselocaldict,
|
||||
employee=contract.employee_id,
|
||||
contract=contract,
|
||||
payslip=payslip,
|
||||
)
|
||||
for rule in payslip._get_salary_rules():
|
||||
localdict = rule._reset_localdict_values(localdict)
|
||||
# check if the rule can be applied
|
||||
if rule._satisfy_condition(localdict) and rule.id not in blacklist:
|
||||
localdict, _dict = payslip._compute_payslip_line(
|
||||
rule, localdict, lines_dict
|
||||
)
|
||||
lines_dict.update(_dict)
|
||||
else:
|
||||
# blacklist this rule and its children
|
||||
blacklist += [
|
||||
id for id, seq in rule._recursive_search_of_rules()
|
||||
]
|
||||
# call localdict_hook
|
||||
localdict = payslip.localdict_hook(localdict)
|
||||
# reset "current_contract" dict
|
||||
baselocaldict["current_contract"] = {}
|
||||
return lines_dict
|
||||
|
||||
def localdict_hook(self, localdict):
|
||||
# This hook is called when the function _get_lines_dict ends the loop
|
||||
# and before its returns. This method by itself don't add any functionality
|
||||
# and is intedend to be inherited to access localdict from other functions.
|
||||
return localdict
|
||||
|
||||
def get_payslip_vals(
|
||||
self, date_from, date_to, employee_id=False, contract_id=False, struct_id=False
|
||||
):
|
||||
# Initial default values for generated payslips
|
||||
employee = self.env["hr.employee"].browse(employee_id)
|
||||
res = {
|
||||
"value": {
|
||||
"line_ids": [],
|
||||
"input_line_ids": [(2, x) for x in self.input_line_ids.ids],
|
||||
"worked_days_line_ids": [(2, x) for x in self.worked_days_line_ids.ids],
|
||||
"name": "",
|
||||
"contract_id": False,
|
||||
"struct_id": False,
|
||||
}
|
||||
}
|
||||
# If we don't have employee or date data, we return.
|
||||
if (not employee_id) or (not date_from) or (not date_to):
|
||||
return res
|
||||
# We check if contract_id is present, if not we fill with the
|
||||
# first contract of the employee. If not contract present, we return.
|
||||
if not self.env.context.get("contract"):
|
||||
contract_ids = employee.contract_id.ids
|
||||
else:
|
||||
if contract_id:
|
||||
contract_ids = [contract_id]
|
||||
else:
|
||||
contract_ids = employee._get_contracts(
|
||||
date_from=date_from, date_to=date_to
|
||||
).ids
|
||||
if not contract_ids:
|
||||
return res
|
||||
contract = self.env["hr.contract"].browse(contract_ids[0])
|
||||
res["value"].update({"contract_id": contract.id})
|
||||
# We check if struct_id is already filled, otherwise we assign the contract struct. # noqa: E501
|
||||
# If contract don't have a struct, we return.
|
||||
if struct_id:
|
||||
res["value"].update({"struct_id": struct_id[0]})
|
||||
else:
|
||||
struct = contract.struct_id
|
||||
if not struct:
|
||||
return res
|
||||
res["value"].update({"struct_id": struct.id})
|
||||
# Computation of the salary input and worked_day_lines
|
||||
contracts = self.env["hr.contract"].browse(contract_ids)
|
||||
worked_days_line_ids = self.get_worked_day_lines(contracts, date_from, date_to)
|
||||
input_line_ids = self.get_inputs(contracts, date_from, date_to)
|
||||
res["value"].update(
|
||||
{
|
||||
"worked_days_line_ids": worked_days_line_ids,
|
||||
"input_line_ids": input_line_ids,
|
||||
}
|
||||
)
|
||||
return res
|
||||
|
||||
def _sum_salary_rule_category(self, localdict, category, amount):
|
||||
self.ensure_one()
|
||||
if category.parent_id:
|
||||
localdict = self._sum_salary_rule_category(
|
||||
localdict, category.parent_id, amount
|
||||
)
|
||||
if category.code:
|
||||
localdict["categories"].dict[category.code] = (
|
||||
localdict["categories"].dict.get(category.code, 0) + amount
|
||||
)
|
||||
return localdict
|
||||
|
||||
def _get_employee_contracts(self):
|
||||
contracts = self.env["hr.contract"]
|
||||
for payslip in self:
|
||||
if payslip.contract_id.ids:
|
||||
contracts |= payslip.contract_id
|
||||
else:
|
||||
contracts |= payslip.employee_id._get_contracts(
|
||||
date_from=payslip.date_from, date_to=payslip.date_to
|
||||
)
|
||||
return contracts
|
||||
|
||||
@api.onchange("struct_id")
|
||||
def onchange_struct_id(self):
|
||||
for payslip in self:
|
||||
if not payslip.struct_id:
|
||||
payslip.input_line_ids.unlink()
|
||||
return
|
||||
input_lines = payslip.input_line_ids.browse([])
|
||||
input_line_ids = payslip.get_inputs(
|
||||
payslip._get_employee_contracts(), payslip.date_from, payslip.date_to
|
||||
)
|
||||
for r in input_line_ids:
|
||||
input_lines += input_lines.new(r)
|
||||
payslip.input_line_ids = input_lines
|
||||
|
||||
@api.onchange("date_from", "date_to")
|
||||
def onchange_dates(self):
|
||||
for payslip in self:
|
||||
if not payslip.date_from or not payslip.date_to:
|
||||
return
|
||||
worked_days_lines = payslip.worked_days_line_ids.browse([])
|
||||
worked_days_line_ids = payslip.get_worked_day_lines(
|
||||
payslip._get_employee_contracts(), payslip.date_from, payslip.date_to
|
||||
)
|
||||
for line in worked_days_line_ids:
|
||||
worked_days_lines += worked_days_lines.new(line)
|
||||
payslip.worked_days_line_ids = worked_days_lines
|
||||
|
||||
@api.onchange("employee_id", "date_from", "date_to")
|
||||
def onchange_employee(self):
|
||||
for payslip in self:
|
||||
# Return if required values are not present.
|
||||
if (
|
||||
(not payslip.employee_id)
|
||||
or (not payslip.date_from)
|
||||
or (not payslip.date_to)
|
||||
):
|
||||
continue
|
||||
# Assign contract_id automatically when the user don't selected one.
|
||||
if not payslip.env.context.get("contract") or not payslip.contract_id:
|
||||
contract_ids = payslip._get_employee_contracts().ids
|
||||
if not contract_ids:
|
||||
continue
|
||||
payslip.contract_id = payslip.env["hr.contract"].browse(contract_ids[0])
|
||||
# Assign struct_id automatically when the user don't selected one.
|
||||
if not payslip.struct_id and not payslip.env.context.get("struct_id"):
|
||||
if not payslip.contract_id.struct_id:
|
||||
continue
|
||||
payslip.struct_id = payslip.contract_id.struct_id
|
||||
# Compute payslip name
|
||||
payslip._compute_name()
|
||||
# Call worked_days_lines computation when employee is changed.
|
||||
payslip.onchange_dates()
|
||||
# Call input_lines computation when employee is changed.
|
||||
payslip.onchange_struct_id()
|
||||
# Assign company_id automatically based on employee selected.
|
||||
payslip.company_id = payslip.employee_id.company_id
|
||||
|
||||
def _compute_name(self):
|
||||
for record in self:
|
||||
record.name = _("Salary Slip of %(name)s for %(dt)s") % {
|
||||
"name": record.employee_id.name,
|
||||
"dt": tools.ustr(
|
||||
babel.dates.format_date(
|
||||
date=datetime.combine(record.date_from, time.min),
|
||||
format="MMMM-y",
|
||||
locale=record.env.context.get("lang") or "en_US",
|
||||
)
|
||||
),
|
||||
}
|
||||
|
||||
@api.onchange("contract_id")
|
||||
def onchange_contract(self):
|
||||
if not self.contract_id:
|
||||
self.struct_id = False
|
||||
self.with_context(contract=True).onchange_employee()
|
||||
return
|
||||
|
||||
def get_salary_line_total(self, code):
|
||||
self.ensure_one()
|
||||
line = self.line_ids.filtered(lambda line: line.code == code)
|
||||
if line:
|
||||
return line[0].total
|
||||
else:
|
||||
return 0.0
|
32
addons/payroll/models/hr_payslip_input.py
Normal file
32
addons/payroll/models/hr_payslip_input.py
Normal file
@ -0,0 +1,32 @@
|
||||
# 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", required=True)
|
||||
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(
|
||||
required=True, help="The code that can be used in the salary rules"
|
||||
)
|
||||
amount = fields.Float(
|
||||
help="It is used in computation. For e.g. A rule for sales having "
|
||||
"1% commission of basic salary for per product can defined in "
|
||||
"expression like result = inputs.SALEURO.amount * contract.wage*0.01."
|
||||
)
|
||||
amount_qty = fields.Float(
|
||||
"Amount Quantity", help="It can be used in computation for other inputs"
|
||||
)
|
||||
contract_id = fields.Many2one(
|
||||
"hr.contract",
|
||||
string="Contract",
|
||||
required=True,
|
||||
help="The contract for which applied this input",
|
||||
)
|
96
addons/payroll/models/hr_payslip_line.py
Normal file
96
addons/payroll/models/hr_payslip_line.py
Normal file
@ -0,0 +1,96 @@
|
||||
# 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"
|
||||
_inherit = "hr.salary.rule"
|
||||
_description = "Payslip Line"
|
||||
_order = "contract_id, sequence"
|
||||
|
||||
slip_id = fields.Many2one(
|
||||
"hr.payslip", string="Pay Slip", required=True, ondelete="cascade"
|
||||
)
|
||||
date_from = fields.Date("Date From", related="slip_id.date_from", store=True)
|
||||
payslip_run_id = fields.Many2one(
|
||||
"hr.payslip.run", related="slip_id.payslip_run_id", string="Payslip Batch"
|
||||
)
|
||||
child_ids = fields.One2many(
|
||||
"hr.payslip.line", "parent_line_id", string="Child Payslip Lines"
|
||||
)
|
||||
parent_line_id = fields.Many2one(
|
||||
"hr.payslip.line",
|
||||
string="Parent Payslip Line",
|
||||
compute="_compute_parent_line_id",
|
||||
store=True,
|
||||
)
|
||||
salary_rule_id = fields.Many2one("hr.salary.rule", string="Rule", required=True)
|
||||
employee_id = fields.Many2one("hr.employee", string="Employee", required=True)
|
||||
contract_id = fields.Many2one(
|
||||
"hr.contract", string="Contract", required=True, index=True
|
||||
)
|
||||
rate = fields.Float(string="Rate (%)", digits="Payroll Rate", default=100.0)
|
||||
amount = fields.Float(digits="Payroll")
|
||||
quantity = fields.Float(digits="Payroll", default=1.0)
|
||||
total = fields.Float(
|
||||
compute="_compute_total",
|
||||
digits="Payroll",
|
||||
store=True,
|
||||
)
|
||||
allow_edit_payslip_lines = fields.Boolean(
|
||||
"Allow editing", compute="_compute_allow_edit_payslip_lines"
|
||||
)
|
||||
|
||||
def _compute_allow_edit_payslip_lines(self):
|
||||
self.allow_edit_payslip_lines = (
|
||||
self.env["ir.config_parameter"]
|
||||
.sudo()
|
||||
.get_param("payroll.allow_edit_payslip_lines")
|
||||
)
|
||||
|
||||
@api.depends("parent_rule_id", "contract_id", "slip_id")
|
||||
def _compute_parent_line_id(self):
|
||||
for line in self:
|
||||
if line.parent_rule_id:
|
||||
parent_line = line.slip_id.line_ids.filtered(
|
||||
lambda record, line=line: record.salary_rule_id
|
||||
== line.parent_rule_id
|
||||
and record.contract_id == line.contract_id
|
||||
and record.slip_id == line.slip_id
|
||||
)
|
||||
if parent_line and len(parent_line) > 1:
|
||||
raise UserError(
|
||||
_("Recursion error. Only one line should be parent of %s")
|
||||
% line.parent_rule_id.name
|
||||
)
|
||||
line.parent_line_id = (
|
||||
parent_line[0].id if len(parent_line) == 1 else False
|
||||
)
|
||||
else:
|
||||
line.parent_line_id = False
|
||||
|
||||
@api.depends("quantity", "amount", "rate")
|
||||
def _compute_total(self):
|
||||
for line in self:
|
||||
line.total = float(line.quantity) * line.amount * line.rate / 100
|
||||
|
||||
@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().create(vals_list)
|
70
addons/payroll/models/hr_payslip_run.py
Normal file
70
addons/payroll/models/hr_payslip_run.py
Normal file
@ -0,0 +1,70 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class HrPayslipRun(models.Model):
|
||||
_name = "hr.payslip.run"
|
||||
_inherit = ["mail.thread", "mail.activity.mixin"]
|
||||
_description = "Payslip Batches"
|
||||
_order = "id desc"
|
||||
|
||||
name = fields.Char(required=True, readonly=True)
|
||||
slip_ids = fields.One2many(
|
||||
"hr.payslip",
|
||||
"payslip_run_id",
|
||||
string="Payslips",
|
||||
readonly=True,
|
||||
)
|
||||
state = fields.Selection(
|
||||
[("draft", "Draft"), ("close", "Close")],
|
||||
string="Status",
|
||||
index=True,
|
||||
readonly=True,
|
||||
copy=False,
|
||||
tracking=1,
|
||||
default="draft",
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
"res.company",
|
||||
string="Company",
|
||||
required=True,
|
||||
copy=False,
|
||||
default=lambda self: self.env.company,
|
||||
)
|
||||
date_start = fields.Date(
|
||||
string="Date From",
|
||||
required=True,
|
||||
readonly=True,
|
||||
default=lambda self: fields.Date.today().replace(day=1),
|
||||
)
|
||||
date_end = fields.Date(
|
||||
string="Date To",
|
||||
required=True,
|
||||
readonly=True,
|
||||
default=lambda self: fields.Date.today().replace(day=1)
|
||||
+ relativedelta(months=+1, day=1, days=-1),
|
||||
)
|
||||
credit_note = fields.Boolean(
|
||||
readonly=True,
|
||||
help="If its checked, indicates that all payslips generated from here "
|
||||
"are refund payslips.",
|
||||
)
|
||||
struct_id = fields.Many2one(
|
||||
"hr.payroll.structure",
|
||||
string="Structure",
|
||||
readonly=True,
|
||||
help="Defines the rules that have to be applied to this payslip batch, "
|
||||
"accordingly to the contract chosen. If you let empty the field "
|
||||
"contract, this field isn't mandatory anymore and thus the rules "
|
||||
"applied will be all the rules set on the structure of all contracts "
|
||||
"of the employee valid for the chosen period",
|
||||
)
|
||||
|
||||
def draft_payslip_run(self):
|
||||
return self.write({"state": "draft"})
|
||||
|
||||
def close_payslip_run(self):
|
||||
return self.write({"state": "close"})
|
26
addons/payroll/models/hr_payslip_worked_days.py
Normal file
26
addons/payroll/models/hr_payslip_worked_days.py
Normal file
@ -0,0 +1,26 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class HrPayslipWorkedDays(models.Model):
|
||||
_name = "hr.payslip.worked_days"
|
||||
_description = "Payslip Worked Days"
|
||||
_order = "payslip_id, sequence"
|
||||
|
||||
name = fields.Char(string="Description", required=True)
|
||||
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(
|
||||
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")
|
||||
contract_id = fields.Many2one(
|
||||
"hr.contract",
|
||||
string="Contract",
|
||||
required=True,
|
||||
help="The contract for which applied this input",
|
||||
)
|
16
addons/payroll/models/hr_rule_input.py
Normal file
16
addons/payroll/models/hr_rule_input.py
Normal file
@ -0,0 +1,16 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class HrRuleInput(models.Model):
|
||||
_name = "hr.rule.input"
|
||||
_description = "Salary Rule Input"
|
||||
|
||||
name = fields.Char(string="Description", required=True)
|
||||
code = fields.Char(
|
||||
required=True, help="The code that can be used in the salary rules"
|
||||
)
|
||||
input_id = fields.Many2one(
|
||||
"hr.salary.rule", string="Salary Rule Input", required=True
|
||||
)
|
333
addons/payroll/models/hr_salary_rule.py
Normal file
333
addons/payroll/models/hr_salary_rule.py
Normal file
@ -0,0 +1,333 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
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(
|
||||
help="The code of salary rules can be used as reference in computation "
|
||||
"of other rules. In that case, it is case sensitive.",
|
||||
)
|
||||
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. "
|
||||
"For e.g. A rule for Meal Voucher having fixed amount of "
|
||||
"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")
|
||||
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.",
|
||||
)
|
||||
parent_rule_id = fields.Many2one(
|
||||
"hr.salary.rule", string="Parent Salary Rule", index=True
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
"res.company",
|
||||
string="Company",
|
||||
default=lambda self: self.env.company,
|
||||
)
|
||||
condition_select = fields.Selection(
|
||||
[("none", "Always True"), ("range", "Range"), ("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_python = fields.Text(
|
||||
string="Python Condition",
|
||||
required=True,
|
||||
default="""
|
||||
# Available variables:
|
||||
#-------------------------------
|
||||
# payslip: hr.payslip object
|
||||
# payslips: object containing payslips (browsable)
|
||||
# employee: hr.employee object
|
||||
# contract: hr.contract object
|
||||
# rules: object containing the rules code (previously computed)
|
||||
# categories: object containing the computed salary rule categories
|
||||
# (sum of amount of all rules belonging to that category).
|
||||
# worked_days: object containing the computed worked days.
|
||||
# inputs: object containing the computed inputs.
|
||||
# payroll: object containing miscellaneous values related to payroll
|
||||
# current_contract: object with values calculated from the current contract
|
||||
# result_rules: object with a dict of qty, rate, amount an total of calculated rules
|
||||
# tools: object that contain libraries and tools that can be used in calculations
|
||||
|
||||
# Available compute variables:
|
||||
#-------------------------------
|
||||
# result: returned value have to be set in the variable 'result'
|
||||
|
||||
# Example:
|
||||
#-------------------------------
|
||||
# result = worked_days.WORK0 and worked_days.WORK0.number_of_days > 0
|
||||
|
||||
""", # noqa: E501
|
||||
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"),
|
||||
("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_python_compute = fields.Text(
|
||||
string="Python Code",
|
||||
default="""
|
||||
# Available variables:
|
||||
#-------------------------------
|
||||
# payslip: hr.payslip object
|
||||
# payslips: object containing payslips (browsable)
|
||||
# employee: hr.employee object
|
||||
# contract: hr.contract object
|
||||
# rules: object containing the rules code (previously computed)
|
||||
# categories: object containing the computed salary rule categories
|
||||
# (sum of amount of all rules belonging to that category).
|
||||
# worked_days: object containing the computed worked days.
|
||||
# inputs: object containing the computed inputs.
|
||||
# payroll: object containing miscellaneous values related to payroll
|
||||
# current_contract: object with values calculated from the current contract
|
||||
# result_rules: object with a dict of qty, rate, amount an total of calculated rules
|
||||
# tools: object that contain libraries and tools that can be used in calculations
|
||||
|
||||
# Available compute variables:
|
||||
#-------------------------------
|
||||
# result: returned value have to be set in the variable 'result'
|
||||
# result_rate: the rate that will be applied to "result".
|
||||
# result_qty: the quantity of units that will be multiplied to "result".
|
||||
# result_name: if this variable is computed, it will contain the name of the line.
|
||||
|
||||
# Example:
|
||||
#-------------------------------
|
||||
# result = contract.wage * 0.10
|
||||
|
||||
""", # noqa: E501
|
||||
)
|
||||
amount_percentage_base = fields.Char(
|
||||
string="Percentage based on", help="result will be affected to a variable"
|
||||
)
|
||||
child_ids = fields.One2many(
|
||||
"hr.salary.rule", "parent_rule_id", string="Child Salary Rule", copy=True
|
||||
)
|
||||
register_id = fields.Many2one(
|
||||
"hr.contribution.register",
|
||||
string="Contribution Register",
|
||||
help="Eventual third party involved in the salary payment of the employees.",
|
||||
)
|
||||
input_ids = fields.One2many("hr.rule.input", "input_id", string="Inputs", copy=True)
|
||||
note = fields.Text(string="Description")
|
||||
require_code_and_category = fields.Boolean(
|
||||
"Require code and category",
|
||||
compute="_compute_require_code_and_category",
|
||||
default=lambda self: self._compute_require_code_and_category(),
|
||||
)
|
||||
|
||||
@api.constrains("parent_rule_id")
|
||||
def _check_parent_rule_id(self):
|
||||
if not self._check_recursion(parent="parent_rule_id"):
|
||||
raise ValidationError(
|
||||
_("Error! You cannot create recursive hierarchy of Salary Rules.")
|
||||
)
|
||||
|
||||
def _recursive_search_of_rules(self):
|
||||
"""
|
||||
@return: returns a list of tuple (id, sequence) which are all the
|
||||
children of the passed rule_ids
|
||||
"""
|
||||
children_rules = []
|
||||
for rule in self.filtered(lambda rule: rule.child_ids):
|
||||
children_rules += rule.child_ids._recursive_search_of_rules()
|
||||
return [(rule.id, rule.sequence) for rule in self] + children_rules
|
||||
|
||||
def _reset_localdict_values(self, localdict):
|
||||
localdict["result_name"] = None
|
||||
localdict["result_qty"] = 1.0
|
||||
localdict["result_rate"] = 100
|
||||
localdict["result"] = None
|
||||
return localdict
|
||||
|
||||
def _compute_require_code_and_category(self):
|
||||
require = (
|
||||
self.env["ir.config_parameter"]
|
||||
.sudo()
|
||||
.get_param("payroll.require_code_and_category")
|
||||
)
|
||||
self.require_code_and_category = require
|
||||
return require
|
||||
|
||||
# TODO should add some checks on the type of result (should be float)
|
||||
def _compute_rule(self, localdict):
|
||||
"""
|
||||
:param localdict: dictionary containing the environement in which to
|
||||
compute the rule
|
||||
:return: returns a dict with values for the payslip line.
|
||||
The dict should minimum have "name", "quantity", "rate" and "amount".
|
||||
:rtype: {"name": string, "quantity": float, "rate": float, "amount": float}
|
||||
"""
|
||||
self.ensure_one()
|
||||
method = f"_compute_rule_{self.amount_select}"
|
||||
return api.call_kw(self, method, [self.ids, localdict], {})
|
||||
|
||||
def _compute_rule_fix(self, localdict):
|
||||
try:
|
||||
return {
|
||||
"name": self.name,
|
||||
"quantity": float(safe_eval(self.quantity, localdict)),
|
||||
"rate": 100.0,
|
||||
"amount": self.amount_fix,
|
||||
}
|
||||
except Exception as ex:
|
||||
raise UserError(
|
||||
_(
|
||||
"Wrong quantity defined for salary rule "
|
||||
"%(nm)s (%(code)s) for employee %(ee)s."
|
||||
)
|
||||
% {"nm": self.name, "code": self.code, "ee": localdict["employee"].name}
|
||||
) from ex
|
||||
|
||||
def _compute_rule_percentage(self, localdict):
|
||||
try:
|
||||
return {
|
||||
"name": self.name,
|
||||
"quantity": float(safe_eval(self.quantity, localdict)),
|
||||
"rate": self.amount_percentage,
|
||||
"amount": float(safe_eval(self.amount_percentage_base, localdict)),
|
||||
}
|
||||
except Exception as ex:
|
||||
raise UserError(
|
||||
_(
|
||||
"Wrong percentage base or quantity defined for salary "
|
||||
"rule %(nm)s (%(code)s) for employee %(ee)s."
|
||||
)
|
||||
% {"nm": self.name, "code": self.code, "ee": localdict["employee"].name}
|
||||
) from ex
|
||||
|
||||
def _compute_rule_code(self, localdict):
|
||||
try:
|
||||
safe_eval(self.amount_python_compute, localdict, mode="exec", nocopy=True)
|
||||
return self._get_rule_dict(localdict)
|
||||
except Exception as ex:
|
||||
raise UserError(
|
||||
_(
|
||||
"""
|
||||
Wrong python code defined for salary rule %(nm)s (%(code)s) for employee %(ee)s.
|
||||
Here is the error received:
|
||||
|
||||
%(err)s
|
||||
"""
|
||||
)
|
||||
% {
|
||||
"nm": self.name,
|
||||
"code": self.code,
|
||||
"ee": localdict["employee"].name,
|
||||
"err": repr(ex),
|
||||
}
|
||||
) from ex
|
||||
|
||||
def _get_rule_dict(self, localdict):
|
||||
name = localdict.get("result_name") or self.name
|
||||
quantity = float(localdict["result_qty"]) if "result_qty" in localdict else 1.0
|
||||
rate = float(localdict["result_rate"]) if "result_rate" in localdict else 100.0
|
||||
return {
|
||||
"name": name,
|
||||
"quantity": quantity,
|
||||
"rate": rate,
|
||||
"amount": float(localdict["result"]),
|
||||
}
|
||||
|
||||
def _satisfy_condition(self, localdict):
|
||||
"""
|
||||
@param contract_id: id of hr.contract to be tested
|
||||
@return: returns True if the given rule match the condition for the
|
||||
given contract. Return False otherwise.
|
||||
"""
|
||||
self.ensure_one()
|
||||
method = f"_satisfy_condition_{self.condition_select}"
|
||||
if self.parent_rule_id:
|
||||
current_result = api.call_kw(self, method, [self.ids, localdict], {})
|
||||
parent_result = self.parent_rule_id._satisfy_condition(localdict)
|
||||
return current_result and parent_result
|
||||
return api.call_kw(self, method, [self.ids, localdict], {})
|
||||
|
||||
def _satisfy_condition_none(self, localdict):
|
||||
return True
|
||||
|
||||
def _satisfy_condition_range(self, localdict):
|
||||
try:
|
||||
result = safe_eval(self.condition_range, localdict)
|
||||
return (
|
||||
self.condition_range_min <= result <= self.condition_range_max or False
|
||||
)
|
||||
except Exception as ex:
|
||||
raise UserError(
|
||||
_(
|
||||
"Wrong range condition defined for salary rule "
|
||||
"%(nm)s (%(code)s) for employee %(ee)s."
|
||||
)
|
||||
% {"nm": self.name, "code": self.code, "ee": localdict["employee"].name}
|
||||
) from ex
|
||||
|
||||
def _satisfy_condition_python(self, localdict):
|
||||
try:
|
||||
safe_eval(self.condition_python, localdict, mode="exec", nocopy=True)
|
||||
return "result" in localdict and localdict["result"] or False
|
||||
except Exception as ex:
|
||||
raise UserError(
|
||||
_(
|
||||
"""
|
||||
Wrong python condition defined for salary rule %(nm)s (%(code)s) for employee %(ee)s.
|
||||
Here is the error received:
|
||||
|
||||
%(err)s
|
||||
"""
|
||||
)
|
||||
% {
|
||||
"nm": self.name,
|
||||
"code": self.code,
|
||||
"ee": localdict["employee"].name,
|
||||
"err": repr(ex),
|
||||
}
|
||||
) from ex
|
54
addons/payroll/models/hr_salary_rule_category.py
Normal file
54
addons/payroll/models/hr_salary_rule_category.py
Normal file
@ -0,0 +1,54 @@
|
||||
# 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()
|
||||
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"
|
||||
)
|
||||
salary_rules_ids = fields.One2many(
|
||||
"hr.salary.rule", "category_id", string="Salary Rule Categories"
|
||||
)
|
||||
note = fields.Text(string="Description")
|
||||
company_id = fields.Many2one(
|
||||
"res.company",
|
||||
string="Company",
|
||||
default=lambda self: self.env.company,
|
||||
)
|
||||
require_code = fields.Boolean(
|
||||
"Require code",
|
||||
compute="_compute_require_code",
|
||||
default=lambda self: self._compute_require_code(),
|
||||
)
|
||||
|
||||
def _compute_require_code(self):
|
||||
require = (
|
||||
self.env["ir.config_parameter"]
|
||||
.sudo()
|
||||
.get_param("payroll.require_code_and_category")
|
||||
)
|
||||
self.require_code = require
|
||||
return require
|
||||
|
||||
@api.constrains("parent_id")
|
||||
def _check_parent_id(self):
|
||||
if not self._check_recursion():
|
||||
raise ValidationError(
|
||||
_(
|
||||
"Error! You cannot create recursive hierarchy of Salary "
|
||||
"Rule Category."
|
||||
)
|
||||
)
|
37
addons/payroll/models/res_config_settings.py
Normal file
37
addons/payroll/models/res_config_settings.py
Normal file
@ -0,0 +1,37 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = "res.config.settings"
|
||||
|
||||
module_payroll_account = fields.Boolean(string="Payroll Accounting")
|
||||
leaves_positive = fields.Boolean(
|
||||
config_parameter="payroll.leaves_positive",
|
||||
string="Leaves with positive values",
|
||||
help="In payslip worked days, leave days/hours have positive values",
|
||||
)
|
||||
allow_cancel_payslips = fields.Boolean(
|
||||
config_parameter="payroll.allow_cancel_payslips",
|
||||
string="Allow canceling confirmed payslips",
|
||||
help="Allow users to cancel confirmed payslips.",
|
||||
)
|
||||
prevent_compute_on_confirm = fields.Boolean(
|
||||
config_parameter="payroll.prevent_compute_on_confirm",
|
||||
string="Confirm payslips without recomputing",
|
||||
help="Prevent payslips from being recomputed when confirming them",
|
||||
default=True,
|
||||
)
|
||||
allow_edit_payslip_lines = fields.Boolean(
|
||||
config_parameter="payroll.allow_edit_payslip_lines",
|
||||
string="Allow editing payslip lines",
|
||||
help="Allow users to edit some payslip line fields manually",
|
||||
default=False,
|
||||
)
|
||||
require_code_and_category = fields.Boolean(
|
||||
config_parameter="payroll.require_code_and_category",
|
||||
string="Require code/category on rules, categories and structures",
|
||||
help="Require rule.code, rule.category, category.code, structure.code",
|
||||
default=False,
|
||||
)
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user