send payroll OCA module
All checks were successful
ci / main (push) Successful in 3m55s

This commit is contained in:
Tokiniaina 2024-11-13 14:54:26 +03:00
parent bff1c557ef
commit 97d61d3076
143 changed files with 186439 additions and 0 deletions

93
addons/payroll/README.rst Normal file
View 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.

View 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

View 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"],
}

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

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

View 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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

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

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

View 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

View 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

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

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

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

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

View 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

View 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

View 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",
)

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

View 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"})

View 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",
)

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

View 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

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

View 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