add new module
5
hr_payroll_community/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
#-*- coding:utf-8 -*-
|
||||
|
||||
from . import models
|
||||
from . import report
|
||||
from . import wizard
|
||||
38
hr_payroll_community/__manifest__.py
Normal file
@@ -0,0 +1,38 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
{
|
||||
'name': 'Odoo13 Payroll',
|
||||
'category': 'Generic Modules/Human Resources',
|
||||
'version': '13.0.1.5.3',
|
||||
'author': 'Odoo SA,Cybrosys Techno Solutions',
|
||||
'company': 'Cybrosys Techno Solutions',
|
||||
'maintainer': 'Cybrosys Techno Solutions',
|
||||
'website': 'https://www.cybrosys.com',
|
||||
'summary': 'Manage your employee payroll records',
|
||||
'images': ['static/description/banner.gif'],
|
||||
'description': "Odoo 13 Payroll, Payroll, Odoo 13,Odoo Payroll, Odoo Community Payroll",
|
||||
'depends': [
|
||||
'hr_contract',
|
||||
'hr_holidays',
|
||||
'hr_contract_types',
|
||||
],
|
||||
'data': [
|
||||
'security/hr_payroll_security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'wizard/hr_payroll_payslips_by_employees_views.xml',
|
||||
'views/hr_contract_views.xml',
|
||||
'views/hr_salary_rule_views.xml',
|
||||
'views/hr_payslip_views.xml',
|
||||
'views/hr_employee_views.xml',
|
||||
'data/hr_payroll_sequence.xml',
|
||||
'views/hr_payroll_report.xml',
|
||||
'data/hr_payroll_data.xml',
|
||||
'wizard/hr_payroll_contribution_register_report_views.xml',
|
||||
'views/res_config_settings_views.xml',
|
||||
'views/report_contributionregister_templates.xml',
|
||||
'views/report_payslip_templates.xml',
|
||||
'views/report_payslipdetails_templates.xml',
|
||||
],
|
||||
'license': 'AGPL-3',
|
||||
# 'demo': ['data/hr_payroll_demo.xml'],
|
||||
}
|
||||
192
hr_payroll_community/data/hr_payroll_data.xml
Normal file
@@ -0,0 +1,192 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="0">
|
||||
<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="HRA" model="hr.salary.rule.category">
|
||||
<field name="name">House Rent Allowance</field>
|
||||
<field name="code">HRA</field>
|
||||
</record>
|
||||
|
||||
<record id="DA" model="hr.salary.rule.category">
|
||||
<field name="name">Dearness Allowance</field>
|
||||
<field name="code">DA</field>
|
||||
</record>
|
||||
|
||||
<record id="Travel" model="hr.salary.rule.category">
|
||||
<field name="name">Travel Allowance</field>
|
||||
<field name="code">Travel</field>
|
||||
</record>
|
||||
|
||||
<record id="Meal" model="hr.salary.rule.category">
|
||||
<field name="name">Meal Allowance</field>
|
||||
<field name="code">Meal</field>
|
||||
</record>
|
||||
|
||||
<record id="Medical" model="hr.salary.rule.category">
|
||||
<field name="name">Medical Allowance</field>
|
||||
<field name="code">Medical</field>
|
||||
</record>
|
||||
|
||||
<record id="Other" model="hr.salary.rule.category">
|
||||
<field name="name">Other Allowance</field>
|
||||
<field name="code">Other</field>
|
||||
</record>
|
||||
|
||||
|
||||
<!-- <record id="DEDUCTION" model="hr.salary.rule.category">-->
|
||||
<!-- <field name="name">Deduction</field>-->
|
||||
<!-- <field name="code">DED</field>-->
|
||||
<!-- <field name="parent_id" eval="False"/>-->
|
||||
<!-- </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="hr_payroll_community.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="hr_payroll_community.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="hr_payroll_community.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>
|
||||
|
||||
<record id="hr_rule_hra" model="hr.salary.rule">
|
||||
<field name="name">House Rent Allowance</field>
|
||||
<field name="sequence" eval="1"/>
|
||||
<field name="code">HRA</field>
|
||||
<field name="category_id" ref="hr_payroll_community.HRA"/>
|
||||
<field name="condition_select">none</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="amount_python_compute">result = contract.hra</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_rule_da" model="hr.salary.rule">
|
||||
<field name="name">Dearness Allowance</field>
|
||||
<field name="sequence" eval="1"/>
|
||||
<field name="code">DA</field>
|
||||
<field name="category_id" ref="hr_payroll_community.DA"/>
|
||||
<field name="condition_select">none</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="amount_python_compute">result = contract.da</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_rule_travel" model="hr.salary.rule">
|
||||
<field name="name">Travel Allowance</field>
|
||||
<field name="sequence" eval="1"/>
|
||||
<field name="code">Travel</field>
|
||||
<field name="category_id" ref="hr_payroll_community.Travel"/>
|
||||
<field name="condition_select">none</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="amount_python_compute">result = contract.travel_allowance</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_rule_meal" model="hr.salary.rule">
|
||||
<field name="name">Meal Allowance</field>
|
||||
<field name="sequence" eval="1"/>
|
||||
<field name="code">Meal</field>
|
||||
<field name="category_id" ref="hr_payroll_community.Meal"/>
|
||||
<field name="condition_select">none</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="amount_python_compute">result = contract.meal_allowance</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_rule_medical" model="hr.salary.rule">
|
||||
<field name="name">Medical Allowance</field>
|
||||
<field name="sequence" eval="1"/>
|
||||
<field name="code">Medical</field>
|
||||
<field name="category_id" ref="hr_payroll_community.Medical"/>
|
||||
<field name="condition_select">none</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="amount_python_compute">result = contract.medical_allowance</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_rule_other" model="hr.salary.rule">
|
||||
<field name="name">Other Allowance</field>
|
||||
<field name="sequence" eval="1"/>
|
||||
<field name="code">Other</field>
|
||||
<field name="category_id" ref="hr_payroll_community.Other"/>
|
||||
<field name="condition_select">none</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="amount_python_compute">result = contract.other_allowance</field>
|
||||
</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>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
162
hr_payroll_community/data/hr_payroll_demo.xml
Normal file
@@ -0,0 +1,162 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- 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="hr_payroll_community.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="hr_payroll_community.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="hr_payroll_community.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="hr_payroll_community.DED"/>
|
||||
<field name="register_id" ref="hr_provident_fund_register"/>
|
||||
<field name="name">Provident Fund</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_salary_rule_ca_gravie" model="hr.salary.rule">
|
||||
<field name="amount_select">fix</field>
|
||||
<field eval="600.0" name="amount_fix"/>
|
||||
<field name="code">CAGG</field>
|
||||
<field name="category_id" ref="hr_payroll_community.ALW"/>
|
||||
<field name="name">Conveyance Allowance For Gravie</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="hr_payroll_community.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="hr_payroll_community.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">MEGG</field>
|
||||
<field name="name">Marketing Executive for Gilles Gravie</field>
|
||||
<field eval="[(6, 0, [ref('hr_salary_rule_ca_gravie'), 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 eval="0" name="manager"/>
|
||||
<field name="company_id" ref="base.main_company"/>
|
||||
<field eval="1" name="active"/>
|
||||
<field name="name">Roger Scott</field>
|
||||
<field name="work_location">Building 1, Second Floor</field>
|
||||
<field name="work_phone">+3282823500</field>
|
||||
<field name="image" type="base64" file="hr_payroll_community/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="type_id" ref="hr_contract.hr_contract_type_emp"/>
|
||||
<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="hr_payroll_community.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_gilles_gravie" model="hr.contract">
|
||||
<field name="name">Contract For Gilles Gravie</field>
|
||||
<field name="type_id" ref="hr_contract.hr_contract_type_emp"/>
|
||||
<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="hr_payroll_community.structure_002"/>
|
||||
<field name="employee_id" ref="hr.employee_qdp"/>
|
||||
<field name="notes">This is Gilles Gravie's contract</field>
|
||||
<field eval="5000.0" name="wage"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
13
hr_payroll_community/data/hr_payroll_sequence.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
|
||||
<record id="seq_salary_slip" model="ir.sequence">
|
||||
<field name="name">Salary Slip</field>
|
||||
<field name="code">salary.slip</field>
|
||||
<field name="prefix">SLIP/</field>
|
||||
<field name="padding">3</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
1509
hr_payroll_community/i18n/af.po
Normal file
1508
hr_payroll_community/i18n/am.po
Normal file
1650
hr_payroll_community/i18n/ar.po
Normal file
1608
hr_payroll_community/i18n/az.po
Normal file
1508
hr_payroll_community/i18n/bg.po
Normal file
1609
hr_payroll_community/i18n/bn.po
Normal file
1620
hr_payroll_community/i18n/bs.po
Normal file
1651
hr_payroll_community/i18n/ca.po
Normal file
1617
hr_payroll_community/i18n/cs.po
Normal file
1619
hr_payroll_community/i18n/da.po
Normal file
1647
hr_payroll_community/i18n/de.po
Normal file
1611
hr_payroll_community/i18n/el.po
Normal file
1508
hr_payroll_community/i18n/en_GB.po
Normal file
1647
hr_payroll_community/i18n/es.po
Normal file
1508
hr_payroll_community/i18n/es_AR.po
Normal file
1508
hr_payroll_community/i18n/es_BO.po
Normal file
1508
hr_payroll_community/i18n/es_CL.po
Normal file
1508
hr_payroll_community/i18n/es_CO.po
Normal file
1508
hr_payroll_community/i18n/es_CR.po
Normal file
1508
hr_payroll_community/i18n/es_DO.po
Normal file
1508
hr_payroll_community/i18n/es_EC.po
Normal file
1508
hr_payroll_community/i18n/es_PE.po
Normal file
1508
hr_payroll_community/i18n/es_PY.po
Normal file
1508
hr_payroll_community/i18n/es_VE.po
Normal file
1645
hr_payroll_community/i18n/et.po
Normal file
1508
hr_payroll_community/i18n/eu.po
Normal file
1613
hr_payroll_community/i18n/fa.po
Normal file
1624
hr_payroll_community/i18n/fi.po
Normal file
1604
hr_payroll_community/i18n/fil.po
Normal file
1508
hr_payroll_community/i18n/fo.po
Normal file
1661
hr_payroll_community/i18n/fr.po
Normal file
1431
hr_payroll_community/i18n/fr_BE.po
Normal file
1508
hr_payroll_community/i18n/fr_CA.po
Normal file
1508
hr_payroll_community/i18n/gl.po
Normal file
1612
hr_payroll_community/i18n/gu.po
Normal file
1615
hr_payroll_community/i18n/he.po
Normal file
1430
hr_payroll_community/i18n/hi.po
Normal file
1617
hr_payroll_community/i18n/hr.po
Normal file
1653
hr_payroll_community/i18n/hu.po
Normal file
1651
hr_payroll_community/i18n/id.po
Normal file
1610
hr_payroll_community/i18n/is.po
Normal file
1633
hr_payroll_community/i18n/it.po
Normal file
1628
hr_payroll_community/i18n/ja.po
Normal file
1508
hr_payroll_community/i18n/ka.po
Normal file
1611
hr_payroll_community/i18n/kab.po
Normal file
1612
hr_payroll_community/i18n/km.po
Normal file
1611
hr_payroll_community/i18n/ko.po
Normal file
1508
hr_payroll_community/i18n/lo.po
Normal file
1662
hr_payroll_community/i18n/lt.po
Normal file
1617
hr_payroll_community/i18n/lv.po
Normal file
1508
hr_payroll_community/i18n/mk.po
Normal file
1639
hr_payroll_community/i18n/mn.po
Normal file
1610
hr_payroll_community/i18n/nb.po
Normal file
1505
hr_payroll_community/i18n/ne.po
Normal file
1659
hr_payroll_community/i18n/nl.po
Normal file
1508
hr_payroll_community/i18n/nl_BE.po
Normal file
1632
hr_payroll_community/i18n/pl.po
Normal file
1642
hr_payroll_community/i18n/pt.po
Normal file
1651
hr_payroll_community/i18n/pt_BR.po
Normal file
1640
hr_payroll_community/i18n/ro.po
Normal file
1653
hr_payroll_community/i18n/ru.po
Normal file
1634
hr_payroll_community/i18n/sk.po
Normal file
1633
hr_payroll_community/i18n/sl.po
Normal file
1508
hr_payroll_community/i18n/sq.po
Normal file
1610
hr_payroll_community/i18n/sr.po
Normal file
1511
hr_payroll_community/i18n/sr@latin.po
Normal file
1635
hr_payroll_community/i18n/sv.po
Normal file
1611
hr_payroll_community/i18n/ta.po
Normal file
1612
hr_payroll_community/i18n/th.po
Normal file
1641
hr_payroll_community/i18n/tr.po
Normal file
1654
hr_payroll_community/i18n/uk.po
Normal file
1644
hr_payroll_community/i18n/vi.po
Normal file
1629
hr_payroll_community/i18n/zh_CN.po
Normal file
1622
hr_payroll_community/i18n/zh_TW.po
Normal file
7
hr_payroll_community/models/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
#-*- coding:utf-8 -*-
|
||||
|
||||
from . import hr_contract
|
||||
from . import hr_employee
|
||||
from . import res_config_settings
|
||||
from . import hr_salary_rule
|
||||
from . import hr_payslip
|
||||
64
hr_payroll_community/models/hr_contract.py
Normal file
@@ -0,0 +1,64 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
from odoo import api, 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.")
|
||||
hra = fields.Monetary(string='HRA', tracking=True, help="House rent allowance.")
|
||||
travel_allowance = fields.Monetary(string="Travel Allowance", help="Travel allowance")
|
||||
da = fields.Monetary(string="DA", help="Dearness allowance")
|
||||
meal_allowance = fields.Monetary(string="Meal Allowance", help="Meal allowance")
|
||||
medical_allowance = fields.Monetary(string="Medical Allowance", help="Medical allowance")
|
||||
other_allowance = fields.Monetary(string="Other Allowance", help="Other allowances")
|
||||
|
||||
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 duplicata
|
||||
"""
|
||||
structures = self.mapped('struct_id')
|
||||
if not structures:
|
||||
return []
|
||||
# YTI TODO return browse records
|
||||
return list(set(structures._get_parent_structure().ids))
|
||||
|
||||
def get_attribute(self, code, attribute):
|
||||
return self.env['hr.contract.advantage.template'].search([('code', '=', code)], limit=1)[attribute]
|
||||
|
||||
def set_attribute_value(self, code, active):
|
||||
for contract in self:
|
||||
if active:
|
||||
value = self.env['hr.contract.advantage.template'].search([('code', '=', code)], limit=1).default_value
|
||||
contract[code] = value
|
||||
else:
|
||||
contract[code] = 0.0
|
||||
|
||||
|
||||
class HrContractAdvandageTemplate(models.Model):
|
||||
_name = 'hr.contract.advantage.template'
|
||||
_description = "Employee's Advantage on Contract"
|
||||
|
||||
name = fields.Char('Name', required=True)
|
||||
code = fields.Char('Code', required=True)
|
||||
lower_bound = fields.Float('Lower Bound', help="Lower bound authorized by the employer for this advantage")
|
||||
upper_bound = fields.Float('Upper Bound', help="Upper bound authorized by the employer for this advantage")
|
||||
default_value = fields.Float('Default value for this advantage')
|
||||
20
hr_payroll_community/models/hr_employee.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class HrEmployee(models.Model):
|
||||
_inherit = 'hr.employee'
|
||||
_description = 'Employee'
|
||||
|
||||
slip_ids = fields.One2many('hr.payslip', 'employee_id', string='Payslips', readonly=True, help="payslip")
|
||||
payslip_count = fields.Integer(compute='_compute_payslip_count', string='Payslip Count')
|
||||
|
||||
def _compute_payslip_count(self):
|
||||
payslip_data = self.env['hr.payslip'].sudo().read_group([('employee_id', 'in', self.ids)],
|
||||
['employee_id'], ['employee_id'])
|
||||
result = dict((data['employee_id'][0], data['employee_id_count']) for data in payslip_data)
|
||||
for employee in self:
|
||||
employee.payslip_count = result.get(employee.id, 0)
|
||||
|
||||
|
||||
650
hr_payroll_community/models/hr_payslip.py
Normal file
@@ -0,0 +1,650 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
import babel
|
||||
from collections import defaultdict
|
||||
from datetime import date, datetime, time
|
||||
from datetime import timedelta
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from pytz import timezone
|
||||
from pytz import utc
|
||||
|
||||
from odoo import api, fields, models, tools, _
|
||||
from odoo.addons import decimal_precision as dp
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.tools import float_utils
|
||||
|
||||
# This will generate 16th of days
|
||||
ROUNDING_FACTOR = 16
|
||||
|
||||
|
||||
class HrPayslip(models.Model):
|
||||
_name = 'hr.payslip'
|
||||
_description = 'Pay Slip'
|
||||
|
||||
struct_id = fields.Many2one('hr.payroll.structure', string='Structure',
|
||||
readonly=True, states={'draft': [('readonly', False)]},
|
||||
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,
|
||||
states={'draft': [('readonly', False)]})
|
||||
number = fields.Char(string='Reference', readonly=True, copy=False, help="References",
|
||||
states={'draft': [('readonly', False)]})
|
||||
employee_id = fields.Many2one('hr.employee', string='Employee', required=True, readonly=True, help="Employee",
|
||||
states={'draft': [('readonly', False)]})
|
||||
date_from = fields.Date(string='Date From', readonly=True, required=True, help="Start date",
|
||||
default=lambda self: fields.Date.to_string(date.today().replace(day=1)),
|
||||
states={'draft': [('readonly', False)]})
|
||||
date_to = fields.Date(string='Date To', readonly=True, required=True, help="End date",
|
||||
default=lambda self: fields.Date.to_string(
|
||||
(datetime.now() + relativedelta(months=+1, day=1, days=-1)).date()),
|
||||
states={'draft': [('readonly', False)]})
|
||||
# this is chaos: 4 states are defined, 3 are used ('verify' isn't) and 5 exist ('confirm' seems to have existed)
|
||||
state = fields.Selection([
|
||||
('draft', 'Draft'),
|
||||
('verify', 'Waiting'),
|
||||
('done', 'Done'),
|
||||
('cancel', 'Rejected'),
|
||||
], string='Status', index=True, readonly=True, copy=False, default='draft',
|
||||
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,
|
||||
states={'draft': [('readonly', False)]})
|
||||
company_id = fields.Many2one('res.company', string='Company', readonly=True, copy=False, help="Company",
|
||||
default=lambda self: self.env['res.company']._company_default_get(),
|
||||
states={'draft': [('readonly', False)]})
|
||||
worked_days_line_ids = fields.One2many('hr.payslip.worked_days', 'payslip_id',
|
||||
string='Payslip Worked Days', copy=True, readonly=True,
|
||||
help="Payslip worked days",
|
||||
states={'draft': [('readonly', False)]})
|
||||
input_line_ids = fields.One2many('hr.payslip.input', 'payslip_id', string='Payslip Inputs',
|
||||
readonly=True, states={'draft': [('readonly', False)]})
|
||||
paid = fields.Boolean(string='Made Payment Order ? ', readonly=True, copy=False,
|
||||
states={'draft': [('readonly', False)]})
|
||||
note = fields.Text(string='Internal Note', readonly=True, states={'draft': [('readonly', False)]})
|
||||
contract_id = fields.Many2one('hr.contract', string='Contract', readonly=True, help="Contract",
|
||||
states={'draft': [('readonly', False)]})
|
||||
details_by_salary_rule_category = fields.One2many('hr.payslip.line',
|
||||
compute='_compute_details_by_salary_rule_category',
|
||||
string='Details by Salary Rule Category', help="Details from the salary rule category")
|
||||
credit_note = fields.Boolean(string='Credit Note', readonly=True,
|
||||
states={'draft': [('readonly', False)]},
|
||||
help="Indicates this payslip has a refund of another")
|
||||
payslip_run_id = fields.Many2one('hr.payslip.run', string='Payslip Batches', readonly=True,
|
||||
copy=False, states={'draft': [('readonly', False)]})
|
||||
payslip_count = fields.Integer(compute='_compute_payslip_count', string="Payslip Computation Details")
|
||||
|
||||
def _compute_details_by_salary_rule_category(self):
|
||||
for payslip in self:
|
||||
payslip.details_by_salary_rule_category = payslip.mapped('line_ids').filtered(lambda line: line.category_id)
|
||||
|
||||
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 'Date To'."))
|
||||
|
||||
def action_payslip_draft(self):
|
||||
return self.write({'state': 'draft'})
|
||||
|
||||
def action_payslip_done(self):
|
||||
self.compute_sheet()
|
||||
return self.write({'state': 'done'})
|
||||
|
||||
def action_payslip_cancel(self):
|
||||
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: ') + payslip.name})
|
||||
copied_payslip.compute_sheet()
|
||||
copied_payslip.action_payslip_done()
|
||||
formview_ref = self.env.ref('hr_payroll_community.view_hr_payslip_form', False)
|
||||
treeview_ref = self.env.ref('hr_payroll_community.view_hr_payslip_tree', False)
|
||||
return {
|
||||
'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': {}
|
||||
}
|
||||
|
||||
def check_done(self):
|
||||
return True
|
||||
|
||||
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(HrPayslip, self).unlink()
|
||||
|
||||
# TODO move this function into hr_contract module, on hr.employee object
|
||||
@api.model
|
||||
def get_contract(self, employee, date_from, date_to):
|
||||
"""
|
||||
@param employee: recordset of employee
|
||||
@param date_from: date field
|
||||
@param date_to: date field
|
||||
@return: returns the ids of all the contracts for the given employee that need to be considered for the given dates
|
||||
"""
|
||||
# a contract is valid if it ends between the given dates
|
||||
clause_1 = ['&', ('date_end', '<=', date_to), ('date_end', '>=', date_from)]
|
||||
# OR if it starts between the given dates
|
||||
clause_2 = ['&', ('date_start', '<=', date_to), ('date_start', '>=', date_from)]
|
||||
# OR if it starts before the date_from and finish after the date_end (or never finish)
|
||||
clause_3 = ['&', ('date_start', '<=', date_from), '|', ('date_end', '=', False), ('date_end', '>=', date_to)]
|
||||
clause_final = [('employee_id', '=', employee.id), ('state', '=', 'open'), '|',
|
||||
'|'] + clause_1 + clause_2 + clause_3
|
||||
return self.env['hr.contract'].search(clause_final).ids
|
||||
|
||||
def compute_sheet(self):
|
||||
for payslip in self:
|
||||
number = payslip.number or self.env['ir.sequence'].next_by_code('salary.slip')
|
||||
# delete old payslip lines
|
||||
payslip.line_ids.unlink()
|
||||
# set the list of contract for which the rules have to be applied
|
||||
# if we don't give the contract, then the rules to apply should be for all current contracts of the employee
|
||||
contract_ids = payslip.contract_id.ids or \
|
||||
self.get_contract(payslip.employee_id, payslip.date_from, payslip.date_to)
|
||||
lines = [(0, 0, line) for line in self._get_payslip_lines(contract_ids, payslip.id)]
|
||||
payslip.write({'line_ids': lines, 'number': number})
|
||||
return True
|
||||
|
||||
@api.model
|
||||
def get_worked_day_lines(self, contracts, date_from, date_to):
|
||||
"""
|
||||
@param contract: 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 = []
|
||||
# fill only if the contract as a working schedule linked
|
||||
for contract in contracts.filtered(lambda contract: contract.resource_calendar_id):
|
||||
day_from = datetime.combine(fields.Date.from_string(date_from), time.min)
|
||||
day_to = datetime.combine(fields.Date.from_string(date_to), time.max)
|
||||
|
||||
# compute leave days
|
||||
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.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,
|
||||
})
|
||||
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:
|
||||
current_leave_struct['number_of_days'] += hours / work_hours
|
||||
|
||||
# compute worked days
|
||||
work_data = contract.employee_id.get_work_days_data(day_from, day_to,
|
||||
calendar=contract.resource_calendar_id)
|
||||
attendances = {
|
||||
'name': _("Normal Working Days paid at 100%"),
|
||||
'sequence': 1,
|
||||
'code': 'WORK100',
|
||||
'number_of_days': work_data['days'],
|
||||
'number_of_hours': work_data['hours'],
|
||||
'contract_id': contract.id,
|
||||
}
|
||||
|
||||
res.append(attendances)
|
||||
res.extend(leaves.values())
|
||||
return res
|
||||
|
||||
@api.model
|
||||
def get_inputs(self, contracts, date_from, date_to):
|
||||
res = []
|
||||
|
||||
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])]
|
||||
inputs = self.env['hr.salary.rule'].browse(sorted_rule_ids).mapped('input_ids')
|
||||
|
||||
for contract in contracts:
|
||||
for input in inputs:
|
||||
input_data = {
|
||||
'name': input.name,
|
||||
'code': input.code,
|
||||
'contract_id': contract.id,
|
||||
}
|
||||
res += [input_data]
|
||||
return res
|
||||
|
||||
@api.model
|
||||
def _get_payslip_lines(self, contract_ids, payslip_id):
|
||||
def _sum_salary_rule_category(localdict, category, amount):
|
||||
if category.parent_id:
|
||||
localdict = _sum_salary_rule_category(localdict, category.parent_id, amount)
|
||||
localdict['categories'].dict[category.code] = category.code in localdict['categories'].dict and \
|
||||
localdict['categories'].dict[category.code] + amount or amount
|
||||
return localdict
|
||||
|
||||
class BrowsableObject(object):
|
||||
def __init__(self, employee_id, dict, env):
|
||||
self.employee_id = employee_id
|
||||
self.dict = dict
|
||||
self.env = env
|
||||
|
||||
def __getattr__(self, attr):
|
||||
return attr in self.dict and self.dict.__getitem__(attr) or 0.0
|
||||
|
||||
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
|
||||
|
||||
# we keep a dict with the result because a value can be overwritten by another rule with the same code
|
||||
result_dict = {}
|
||||
rules_dict = {}
|
||||
worked_days_dict = {}
|
||||
inputs_dict = {}
|
||||
blacklist = []
|
||||
payslip = self.env['hr.payslip'].browse(payslip_id)
|
||||
for worked_days_line in payslip.worked_days_line_ids:
|
||||
worked_days_dict[worked_days_line.code] = worked_days_line
|
||||
for input_line in payslip.input_line_ids:
|
||||
inputs_dict[input_line.code] = input_line
|
||||
|
||||
categories = BrowsableObject(payslip.employee_id.id, {}, self.env)
|
||||
inputs = InputLine(payslip.employee_id.id, inputs_dict, self.env)
|
||||
worked_days = WorkedDays(payslip.employee_id.id, worked_days_dict, self.env)
|
||||
payslips = Payslips(payslip.employee_id.id, payslip, self.env)
|
||||
rules = BrowsableObject(payslip.employee_id.id, rules_dict, self.env)
|
||||
|
||||
baselocaldict = {'categories': categories, 'rules': rules, 'payslip': payslips, 'worked_days': worked_days,
|
||||
'inputs': inputs}
|
||||
# get the ids of the structures on the contracts and their parent id as well
|
||||
contracts = self.env['hr.contract'].browse(contract_ids)
|
||||
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()
|
||||
# get the rules of the structure and thier children
|
||||
rule_ids = self.env['hr.payroll.structure'].browse(structure_ids).get_all_rules()
|
||||
# run the rules by sequence
|
||||
sorted_rule_ids = [id for id, sequence in sorted(rule_ids, key=lambda x: x[1])]
|
||||
sorted_rules = self.env['hr.salary.rule'].browse(sorted_rule_ids)
|
||||
|
||||
for contract in contracts:
|
||||
employee = contract.employee_id
|
||||
localdict = dict(baselocaldict, employee=employee, contract=contract)
|
||||
for rule in sorted_rules:
|
||||
key = rule.code + '-' + str(contract.id)
|
||||
localdict['result'] = None
|
||||
localdict['result_qty'] = 1.0
|
||||
localdict['result_rate'] = 100
|
||||
# check if the rule can be applied
|
||||
if rule._satisfy_condition(localdict) and rule.id not in blacklist:
|
||||
# compute the amount of the rule
|
||||
amount, qty, rate = rule._compute_rule(localdict)
|
||||
# check if there is already a rule computed with that code
|
||||
previous_amount = rule.code in localdict and localdict[rule.code] or 0.0
|
||||
# set/overwrite the amount computed for this rule in the localdict
|
||||
tot_rule = amount * qty * rate / 100.0
|
||||
localdict[rule.code] = tot_rule
|
||||
rules_dict[rule.code] = rule
|
||||
# sum the amount for its salary category
|
||||
localdict = _sum_salary_rule_category(localdict, rule.category_id, tot_rule - previous_amount)
|
||||
# create/overwrite the rule in the temporary results
|
||||
result_dict[key] = {
|
||||
'salary_rule_id': rule.id,
|
||||
'contract_id': contract.id,
|
||||
'name': rule.name,
|
||||
'code': rule.code,
|
||||
'category_id': rule.category_id.id,
|
||||
'sequence': rule.sequence,
|
||||
'appears_on_payslip': rule.appears_on_payslip,
|
||||
'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,
|
||||
'amount': amount,
|
||||
'employee_id': contract.employee_id.id,
|
||||
'quantity': qty,
|
||||
'rate': rate,
|
||||
}
|
||||
else:
|
||||
# blacklist this rule and its children
|
||||
blacklist += [id for id, seq in rule._recursive_search_of_rules()]
|
||||
|
||||
return list(result_dict.values())
|
||||
|
||||
# YTI TODO To rename. This method is not really an onchange, as it is not in any view
|
||||
# employee_id and contract_id could be browse records
|
||||
def onchange_employee_id(self, date_from, date_to, employee_id=False, contract_id=False):
|
||||
# defaults
|
||||
res = {
|
||||
'value': {
|
||||
'line_ids': [],
|
||||
# delete old input lines
|
||||
'input_line_ids': [(2, x,) for x in self.input_line_ids.ids],
|
||||
# delete old worked days lines
|
||||
'worked_days_line_ids': [(2, x,) for x in self.worked_days_line_ids.ids],
|
||||
# 'details_by_salary_head':[], TODO put me back
|
||||
'name': '',
|
||||
'contract_id': False,
|
||||
'struct_id': False,
|
||||
}
|
||||
}
|
||||
if (not employee_id) or (not date_from) or (not date_to):
|
||||
return res
|
||||
ttyme = datetime.combine(fields.Date.from_string(date_from), time.min)
|
||||
employee = self.env['hr.employee'].browse(employee_id)
|
||||
locale = self.env.context.get('lang') or 'en_US'
|
||||
res['value'].update({
|
||||
'name': _('Salary Slip of %s for %s') % (
|
||||
employee.name, tools.ustr(babel.dates.format_date(date=ttyme, format='MMMM-y', locale=locale))),
|
||||
'company_id': employee.company_id.id,
|
||||
})
|
||||
|
||||
if not self.env.context.get('contract'):
|
||||
# fill with the first contract of the employee
|
||||
contract_ids = self.get_contract(employee, date_from, date_to)
|
||||
else:
|
||||
if contract_id:
|
||||
# set the list of contract for which the input have to be filled
|
||||
contract_ids = [contract_id]
|
||||
else:
|
||||
# if we don't give the contract, then the input to fill should be for all current contracts of the employee
|
||||
contract_ids = self.get_contract(employee, date_from, date_to)
|
||||
|
||||
if not contract_ids:
|
||||
return res
|
||||
contract = self.env['hr.contract'].browse(contract_ids[0])
|
||||
res['value'].update({
|
||||
'contract_id': contract.id
|
||||
})
|
||||
struct = contract.struct_id
|
||||
if not struct:
|
||||
return res
|
||||
res['value'].update({
|
||||
'struct_id': struct.id,
|
||||
})
|
||||
# computation of the salary input
|
||||
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
|
||||
|
||||
@api.onchange('employee_id', 'date_from', 'date_to')
|
||||
def onchange_employee(self):
|
||||
|
||||
if (not self.employee_id) or (not self.date_from) or (not self.date_to):
|
||||
return
|
||||
|
||||
employee = self.employee_id
|
||||
date_from = self.date_from
|
||||
date_to = self.date_to
|
||||
contract_ids = []
|
||||
|
||||
ttyme = datetime.combine(fields.Date.from_string(date_from), time.min)
|
||||
locale = self.env.context.get('lang') or 'en_US'
|
||||
self.name = _('Salary Slip of %s for %s') % (
|
||||
employee.name, tools.ustr(babel.dates.format_date(date=ttyme, format='MMMM-y', locale=locale)))
|
||||
self.company_id = employee.company_id
|
||||
|
||||
if not self.env.context.get('contract') or not self.contract_id:
|
||||
contract_ids = self.get_contract(employee, date_from, date_to)
|
||||
if not contract_ids:
|
||||
return
|
||||
self.contract_id = self.env['hr.contract'].browse(contract_ids[0])
|
||||
|
||||
if not self.contract_id.struct_id:
|
||||
return
|
||||
self.struct_id = self.contract_id.struct_id
|
||||
if self.contract_id:
|
||||
contract_ids = self.contract_id.ids
|
||||
# computation of the salary input
|
||||
contracts = self.env['hr.contract'].browse(contract_ids)
|
||||
worked_days_line_ids = self.get_worked_day_lines(contracts, date_from, date_to)
|
||||
worked_days_lines = self.worked_days_line_ids.browse([])
|
||||
for r in worked_days_line_ids:
|
||||
worked_days_lines += worked_days_lines.new(r)
|
||||
self.worked_days_line_ids = worked_days_lines
|
||||
|
||||
input_line_ids = self.get_inputs(contracts, date_from, date_to)
|
||||
input_lines = self.input_line_ids.browse([])
|
||||
for r in input_line_ids:
|
||||
input_lines += input_lines.new(r)
|
||||
self.input_line_ids = input_lines
|
||||
return
|
||||
|
||||
@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
|
||||
|
||||
|
||||
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', help="Payslip")
|
||||
salary_rule_id = fields.Many2one('hr.salary.rule', string='Rule', required=True, help="salary rule")
|
||||
employee_id = fields.Many2one('hr.employee', string='Employee', required=True, help="Employee")
|
||||
contract_id = fields.Many2one('hr.contract', string='Contract', required=True, index=True, help="Contract")
|
||||
rate = fields.Float(string='Rate (%)', digits=dp.get_precision('Payroll Rate'), default=100.0)
|
||||
amount = fields.Float(digits=dp.get_precision('Payroll'))
|
||||
quantity = fields.Float(digits=dp.get_precision('Payroll'), default=1.0)
|
||||
total = fields.Float(compute='_compute_total', string='Total', help="Total", digits=dp.get_precision('Payroll'), store=True)
|
||||
|
||||
@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(HrPayslipLine, self).create(vals_list)
|
||||
|
||||
|
||||
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, help="Payslip")
|
||||
sequence = fields.Integer(required=True, index=True, default=10, help="Sequence")
|
||||
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', help="Number of days worked")
|
||||
number_of_hours = fields.Float(string='Number of Hours', help="Number of hours worked")
|
||||
contract_id = fields.Many2one('hr.contract', string='Contract', required=True,
|
||||
help="The contract for which applied this input")
|
||||
|
||||
|
||||
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', help="Payslip", index=True)
|
||||
sequence = fields.Integer(required=True, index=True, default=10, help="Sequence")
|
||||
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.")
|
||||
contract_id = fields.Many2one('hr.contract', string='Contract', required=True,
|
||||
help="The contract for which applied this input")
|
||||
|
||||
|
||||
class HrPayslipRun(models.Model):
|
||||
_name = 'hr.payslip.run'
|
||||
_description = 'Payslip Batches'
|
||||
|
||||
name = fields.Char(required=True, readonly=True, states={'draft': [('readonly', False)]})
|
||||
slip_ids = fields.One2many('hr.payslip', 'payslip_run_id', string='Payslips', readonly=True,
|
||||
states={'draft': [('readonly', False)]})
|
||||
state = fields.Selection([
|
||||
('draft', 'Draft'),
|
||||
('close', 'Close'),
|
||||
], string='Status', index=True, readonly=True, copy=False, default='draft')
|
||||
date_start = fields.Date(string='Date From', required=True, readonly=True, help="start date",
|
||||
states={'draft': [('readonly', False)]},
|
||||
default=lambda self: fields.Date.to_string(date.today().replace(day=1)))
|
||||
date_end = fields.Date(string='Date To', required=True, readonly=True, help="End date",
|
||||
states={'draft': [('readonly', False)]},
|
||||
default=lambda self: fields.Date.to_string(
|
||||
(datetime.now() + relativedelta(months=+1, day=1, days=-1)).date()))
|
||||
credit_note = fields.Boolean(string='Credit Note', readonly=True,
|
||||
states={'draft': [('readonly', False)]},
|
||||
help="If its checked, indicates that all payslips generated from here are refund "
|
||||
"payslips.")
|
||||
|
||||
def draft_payslip_run(self):
|
||||
return self.write({'state': 'draft'})
|
||||
|
||||
def close_payslip_run(self):
|
||||
return self.write({'state': 'close'})
|
||||
|
||||
|
||||
class ResourceMixin(models.AbstractModel):
|
||||
_inherit = "resource.mixin"
|
||||
|
||||
def get_work_days_data(self, from_datetime, to_datetime, compute_leaves=True, calendar=None, domain=None):
|
||||
"""
|
||||
By default the resource calendar is used, but it can be
|
||||
changed using the `calendar` argument.
|
||||
|
||||
`domain` is used in order to recognise the leaves to take,
|
||||
None means default value ('time_type', '=', 'leave')
|
||||
|
||||
Returns a dict {'days': n, 'hours': h} containing the
|
||||
quantity of working time expressed as days and as hours.
|
||||
"""
|
||||
resource = self.resource_id
|
||||
calendar = calendar or self.resource_calendar_id
|
||||
|
||||
# naive datetimes are made explicit in UTC
|
||||
if not from_datetime.tzinfo:
|
||||
from_datetime = from_datetime.replace(tzinfo=utc)
|
||||
if not to_datetime.tzinfo:
|
||||
to_datetime = to_datetime.replace(tzinfo=utc)
|
||||
|
||||
# total hours per day: retrieve attendances with one extra day margin,
|
||||
# in order to compute the total hours on the first and last days
|
||||
from_full = from_datetime - timedelta(days=1)
|
||||
to_full = to_datetime + timedelta(days=1)
|
||||
intervals = calendar._attendance_intervals(from_full, to_full, resource)
|
||||
day_total = defaultdict(float)
|
||||
for start, stop, meta in intervals:
|
||||
day_total[start.date()] += (stop - start).total_seconds() / 3600
|
||||
|
||||
# actual hours per day
|
||||
if compute_leaves:
|
||||
intervals = calendar._work_intervals(from_datetime, to_datetime, resource, domain)
|
||||
else:
|
||||
intervals = calendar._attendance_intervals(from_datetime, to_datetime, resource)
|
||||
day_hours = defaultdict(float)
|
||||
for start, stop, meta in intervals:
|
||||
day_hours[start.date()] += (stop - start).total_seconds() / 3600
|
||||
|
||||
# compute number of days as quarters
|
||||
days = sum(
|
||||
float_utils.round(ROUNDING_FACTOR * day_hours[day] / day_total[day]) / ROUNDING_FACTOR
|
||||
for day in day_hours
|
||||
)
|
||||
return {
|
||||
'days': days,
|
||||
'hours': sum(day_hours.values()),
|
||||
}
|
||||
242
hr_payroll_community/models/hr_salary_rule.py
Normal file
@@ -0,0 +1,242 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.tools.safe_eval import safe_eval
|
||||
|
||||
from odoo.addons import decimal_precision as dp
|
||||
|
||||
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_community.structure_base', False)
|
||||
|
||||
name = fields.Char(required=True)
|
||||
code = fields.Char(string='Reference', required=True)
|
||||
company_id = fields.Many2one('res.company', string='Company', required=True,
|
||||
copy=False, default=lambda self: self.env['res.company']._company_default_get())
|
||||
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')
|
||||
|
||||
@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(HrPayrollStructure, self).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
|
||||
|
||||
|
||||
class HrContributionRegister(models.Model):
|
||||
_name = 'hr.contribution.register'
|
||||
_description = 'Contribution Register'
|
||||
|
||||
company_id = fields.Many2one('res.company', string='Company',
|
||||
default=lambda self: self.env['res.company']._company_default_get())
|
||||
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')
|
||||
|
||||
|
||||
class HrSalaryRuleCategory(models.Model):
|
||||
_name = 'hr.salary.rule.category'
|
||||
_description = 'Salary Rule Category'
|
||||
|
||||
name = fields.Char(required=True, translate=True)
|
||||
code = fields.Char(required=True)
|
||||
parent_id = fields.Many2one('hr.salary.rule.category', string='Parent',
|
||||
help="Linking a salary category to its parent is used only for the reporting purpose.")
|
||||
children_ids = fields.One2many('hr.salary.rule.category', 'parent_id', string='Children')
|
||||
note = fields.Text(string='Description')
|
||||
company_id = fields.Many2one('res.company', string='Company',
|
||||
default=lambda self: self.env['res.company']._company_default_get())
|
||||
|
||||
@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.'))
|
||||
|
||||
|
||||
class HrSalaryRule(models.Model):
|
||||
_name = 'hr.salary.rule'
|
||||
_order = 'sequence, id'
|
||||
_description = 'Salary Rule'
|
||||
|
||||
name = fields.Char(required=True, translate=True)
|
||||
code = fields.Char(required=True,
|
||||
help="The code of salary rules can be used as reference in computation of other rules. "
|
||||
"In that case, it is case sensitive.")
|
||||
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 "
|
||||
u"1€ per worked day can have its quantity defined in expression "
|
||||
"like worked_days.WORK100.number_of_days.")
|
||||
category_id = fields.Many2one('hr.salary.rule.category', string='Category', required=True)
|
||||
active = fields.Boolean(default=True,
|
||||
help="If the active field is set to false, it will allow you to hide the salary rule without removing it.")
|
||||
appears_on_payslip = fields.Boolean(string='Appears on Payslip', default=True,
|
||||
help="Used to display the salary rule on payslip.")
|
||||
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['res.company']._company_default_get())
|
||||
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: object containing the payslips
|
||||
# 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
|
||||
|
||||
# Note: returned value have to be set in the variable 'result'
|
||||
|
||||
result = rules.NET > categories.NET * 0.10''',
|
||||
help='Applied this rule for calculation if condition is true. You can specify condition like basic > 1000.')
|
||||
condition_range_min = fields.Float(string='Minimum Range', help="The minimum amount, applied for this rule.")
|
||||
condition_range_max = fields.Float(string='Maximum Range', help="The maximum amount, applied for this rule.")
|
||||
amount_select = fields.Selection([
|
||||
('percentage', 'Percentage (%)'),
|
||||
('fix', 'Fixed Amount'),
|
||||
('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=dp.get_precision('Payroll'))
|
||||
amount_percentage = fields.Float(string='Percentage (%)', digits=dp.get_precision('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: object containing the payslips
|
||||
# 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.
|
||||
|
||||
# Note: returned value have to be set in the variable 'result'
|
||||
|
||||
result = contract.wage * 0.10''')
|
||||
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')
|
||||
|
||||
@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
|
||||
|
||||
#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 tuple build as the base/amount computed, the quantity and the rate
|
||||
:rtype: (float, float, float)
|
||||
"""
|
||||
self.ensure_one()
|
||||
if self.amount_select == 'fix':
|
||||
try:
|
||||
return self.amount_fix, float(safe_eval(self.quantity, localdict)), 100.0
|
||||
except:
|
||||
raise UserError(_('Wrong quantity defined for salary rule %s (%s).') % (self.name, self.code))
|
||||
elif self.amount_select == 'percentage':
|
||||
try:
|
||||
return (float(safe_eval(self.amount_percentage_base, localdict)),
|
||||
float(safe_eval(self.quantity, localdict)),
|
||||
self.amount_percentage)
|
||||
except:
|
||||
raise UserError(_('Wrong percentage base or quantity defined for salary rule %s (%s).') % (self.name, self.code))
|
||||
else:
|
||||
try:
|
||||
safe_eval(self.amount_python_compute, localdict, mode='exec', nocopy=True)
|
||||
return float(localdict['result']), 'result_qty' in localdict and localdict['result_qty'] or 1.0, 'result_rate' in localdict and localdict['result_rate'] or 100.0
|
||||
except:
|
||||
raise UserError(_('Wrong python code defined for salary rule %s (%s).') % (self.name, self.code))
|
||||
|
||||
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()
|
||||
|
||||
if self.condition_select == 'none':
|
||||
return True
|
||||
elif self.condition_select == 'range':
|
||||
try:
|
||||
result = safe_eval(self.condition_range, localdict)
|
||||
return self.condition_range_min <= result and result <= self.condition_range_max or False
|
||||
except:
|
||||
raise UserError(_('Wrong range condition defined for salary rule %s (%s).') % (self.name, self.code))
|
||||
else: # python code
|
||||
try:
|
||||
safe_eval(self.condition_python, localdict, mode='exec', nocopy=True)
|
||||
return 'result' in localdict and localdict['result'] or False
|
||||
except:
|
||||
raise UserError(_('Wrong python condition defined for salary rule %s (%s).') % (self.name, self.code))
|
||||
|
||||
|
||||
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)
|
||||
12
hr_payroll_community/models/res_config_settings.py
Normal file
@@ -0,0 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
module_account_accountant = fields.Boolean(string='Account Accountant')
|
||||
module_l10n_fr_hr_payroll = fields.Boolean(string='French Payroll')
|
||||
module_l10n_be_hr_payroll = fields.Boolean(string='Belgium Payroll')
|
||||
module_l10n_in_hr_payroll = fields.Boolean(string='Indian Payroll')
|
||||
4
hr_payroll_community/report/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
#-*- coding:utf-8 -*-
|
||||
|
||||
from . import report_payslip_details
|
||||
from . import report_contribution_register
|
||||
51
hr_payroll_community/report/report_contribution_register.py
Normal file
@@ -0,0 +1,51 @@
|
||||
#-*- coding:utf-8 -*-
|
||||
|
||||
from datetime import datetime
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class ContributionRegisterReport(models.AbstractModel):
|
||||
_name = 'report.hr_payroll_community.report_contributionregister'
|
||||
_description = 'Payroll Contribution Register Report'
|
||||
|
||||
def _get_payslip_lines(self, register_ids, date_from, date_to):
|
||||
result = {}
|
||||
self.env.cr.execute("""
|
||||
SELECT pl.id from hr_payslip_line as pl
|
||||
LEFT JOIN hr_payslip AS hp on (pl.slip_id = hp.id)
|
||||
WHERE (hp.date_from >= %s) AND (hp.date_to <= %s)
|
||||
AND pl.register_id in %s
|
||||
AND hp.state = 'done'
|
||||
ORDER BY pl.slip_id, pl.sequence""",
|
||||
(date_from, date_to, tuple(register_ids)))
|
||||
line_ids = [x[0] for x in self.env.cr.fetchall()]
|
||||
for line in self.env['hr.payslip.line'].browse(line_ids):
|
||||
result.setdefault(line.register_id.id, self.env['hr.payslip.line'])
|
||||
result[line.register_id.id] += line
|
||||
return result
|
||||
|
||||
@api.model
|
||||
def _get_report_values(self, docids, data=None):
|
||||
if not data.get('form'):
|
||||
raise UserError(_("Form content is missing, this report cannot be printed."))
|
||||
|
||||
register_ids = self.env.context.get('active_ids', [])
|
||||
contrib_registers = self.env['hr.contribution.register'].browse(register_ids)
|
||||
date_from = data['form'].get('date_from', fields.Date.today())
|
||||
date_to = data['form'].get('date_to', str(datetime.now() + relativedelta(months=+1, day=1, days=-1))[:10])
|
||||
lines_data = self._get_payslip_lines(register_ids, date_from, date_to)
|
||||
lines_total = {}
|
||||
for register in contrib_registers:
|
||||
lines = lines_data.get(register.id)
|
||||
lines_total[register.id] = lines and sum(lines.mapped('total')) or 0.0
|
||||
return {
|
||||
'doc_ids': register_ids,
|
||||
'doc_model': 'hr.contribution.register',
|
||||
'docs': contrib_registers,
|
||||
'data': data,
|
||||
'lines_data': lines_data,
|
||||
'lines_total': lines_total
|
||||
}
|
||||
98
hr_payroll_community/report/report_payslip_details.py
Normal file
@@ -0,0 +1,98 @@
|
||||
#-*- coding:utf-8 -*-
|
||||
|
||||
from odoo import api, models
|
||||
|
||||
class PayslipDetailsReport(models.AbstractModel):
|
||||
_name = 'report.hr_payroll_community.report_payslipdetails'
|
||||
_description = 'Payslip Details Report'
|
||||
|
||||
def get_details_by_rule_category(self, payslip_lines):
|
||||
PayslipLine = self.env['hr.payslip.line']
|
||||
RuleCateg = self.env['hr.salary.rule.category']
|
||||
|
||||
def get_recursive_parent(current_rule_category, rule_categories=None):
|
||||
if rule_categories:
|
||||
rule_categories = current_rule_category | rule_categories
|
||||
else:
|
||||
rule_categories = current_rule_category
|
||||
|
||||
if current_rule_category.parent_id:
|
||||
return get_recursive_parent(current_rule_category.parent_id, rule_categories)
|
||||
else:
|
||||
return rule_categories
|
||||
|
||||
res = {}
|
||||
result = {}
|
||||
|
||||
if payslip_lines:
|
||||
self.env.cr.execute("""
|
||||
SELECT pl.id, pl.category_id, pl.slip_id FROM hr_payslip_line as pl
|
||||
LEFT JOIN hr_salary_rule_category AS rc on (pl.category_id = rc.id)
|
||||
WHERE pl.id in %s
|
||||
GROUP BY rc.parent_id, pl.sequence, pl.id, pl.category_id
|
||||
ORDER BY pl.sequence, rc.parent_id""",
|
||||
(tuple(payslip_lines.ids),))
|
||||
for x in self.env.cr.fetchall():
|
||||
result.setdefault(x[2], {})
|
||||
result[x[2]].setdefault(x[1], [])
|
||||
result[x[2]][x[1]].append(x[0])
|
||||
for payslip_id, lines_dict in result.items():
|
||||
res.setdefault(payslip_id, [])
|
||||
for rule_categ_id, line_ids in lines_dict.items():
|
||||
rule_categories = RuleCateg.browse(rule_categ_id)
|
||||
lines = PayslipLine.browse(line_ids)
|
||||
level = 0
|
||||
for parent in get_recursive_parent(rule_categories):
|
||||
res[payslip_id].append({
|
||||
'rule_category': parent.name,
|
||||
'name': parent.name,
|
||||
'code': parent.code,
|
||||
'level': level,
|
||||
'total': sum(lines.mapped('total')),
|
||||
})
|
||||
level += 1
|
||||
for line in lines:
|
||||
res[payslip_id].append({
|
||||
'rule_category': line.name,
|
||||
'name': line.name,
|
||||
'code': line.code,
|
||||
'total': line.total,
|
||||
'level': level
|
||||
})
|
||||
return res
|
||||
|
||||
def get_lines_by_contribution_register(self, payslip_lines):
|
||||
result = {}
|
||||
res = {}
|
||||
for line in payslip_lines.filtered('register_id'):
|
||||
result.setdefault(line.slip_id.id, {})
|
||||
result[line.slip_id.id].setdefault(line.register_id, line)
|
||||
result[line.slip_id.id][line.register_id] |= line
|
||||
for payslip_id, lines_dict in result.items():
|
||||
res.setdefault(payslip_id, [])
|
||||
for register, lines in lines_dict.items():
|
||||
res[payslip_id].append({
|
||||
'register_name': register.name,
|
||||
'total': sum(lines.mapped('total')),
|
||||
})
|
||||
for line in lines:
|
||||
res[payslip_id].append({
|
||||
'name': line.name,
|
||||
'code': line.code,
|
||||
'quantity': line.quantity,
|
||||
'amount': line.amount,
|
||||
'total': line.total,
|
||||
})
|
||||
return res
|
||||
|
||||
@api.model
|
||||
def _get_report_values(self, docids, data=None):
|
||||
payslips = self.env['hr.payslip'].browse(docids)
|
||||
return {
|
||||
'doc_ids': docids,
|
||||
'doc_model': 'hr.payslip',
|
||||
'docs': payslips,
|
||||
'data': data,
|
||||
'get_details_by_rule_category': self.get_details_by_rule_category(payslips.mapped('details_by_salary_rule_category').filtered(lambda r: r.appears_on_payslip)),
|
||||
'get_lines_by_contribution_register': self.get_lines_by_contribution_register(payslips.mapped('line_ids').filtered(lambda r: r.appears_on_payslip)),
|
||||
}
|
||||
49
hr_payroll_community/security/hr_payroll_security.xml
Normal file
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
|
||||
<record model="ir.module.category" id="module_category_hr_payroll_community">
|
||||
<field name="name">Payroll</field>
|
||||
<field name="description">Helps you manage your payrolls.</field>
|
||||
<field name="sequence">16</field>
|
||||
</record>
|
||||
|
||||
<record id="group_hr_payroll_community_user" model="res.groups">
|
||||
<field name="name">Officer</field>
|
||||
<field name="category_id" ref="hr_payroll_community.module_category_hr_payroll_community"/>
|
||||
<field name="implied_ids" eval="[(4, ref('hr.group_hr_user')), (4, ref('hr_contract.group_hr_contract_manager'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="group_hr_payroll_community_manager" model="res.groups">
|
||||
<field name="name">Manager</field>
|
||||
<field name="category_id" ref="hr_payroll_community.module_category_hr_payroll_community"/>
|
||||
<field name="implied_ids" eval="[(4, ref('hr_payroll_community.group_hr_payroll_community_user'))]"/>
|
||||
<field name="users" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="base.default_user" model="res.users">
|
||||
<field name="groups_id" eval="[(4,ref('hr_payroll_community.group_hr_payroll_community_manager'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="hr_payroll_rule_officer" model="ir.rule">
|
||||
<field name="name">Officer and subordinates Payslip</field>
|
||||
<field name="model_id" ref="model_hr_payslip"/>
|
||||
<field name="domain_force">['|','|', ('employee_id.user_id', '=', user.id), ('employee_id.department_id', '=', False), ('employee_id.department_id.manager_id.user_id', '=', user.id)]</field>
|
||||
<field name="groups" eval="[(4, ref('hr_payroll_community.group_hr_payroll_community_user'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="hr_payslip_rule_manager" model="ir.rule">
|
||||
<field name="name">All Payslip</field>
|
||||
<field name="model_id" ref="model_hr_payslip"/>
|
||||
<field name="domain_force">[(1,'=',1)]</field>
|
||||
<field name="groups" eval="[(4, ref('hr_payroll_community.group_hr_payroll_community_manager'))]"/>
|
||||
</record>
|
||||
<record model="ir.rule" id="payroll_multi_company_rule">
|
||||
<field name="name">Payroll multi company</field>
|
||||
<field name="model_id" ref="model_hr_payslip"/>
|
||||
<field name="global" eval="True"/>
|
||||
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
14
hr_payroll_community/security/ir.model.access.csv
Normal file
@@ -0,0 +1,14 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_hr_payroll_community_structure,hr.payroll.structure,model_hr_payroll_structure,hr_payroll_community.group_hr_payroll_community_user,1,1,1,1
|
||||
access_hr_payroll_community_structure_hr_user,hr.payroll.structure.hr.user,model_hr_payroll_structure,hr.group_hr_user,1,0,0,0
|
||||
access_hr_contribution_register,hr.contribution.register,model_hr_contribution_register,hr_payroll_community.group_hr_payroll_community_user,1,1,1,1
|
||||
access_hr_salary_rule_category,hr.salary.rule.category,model_hr_salary_rule_category,hr_payroll_community.group_hr_payroll_community_user,1,1,1,1
|
||||
access_hr_payslip,hr.payslip,model_hr_payslip,hr_payroll_community.group_hr_payroll_community_user,1,1,1,1
|
||||
access_hr_payslip_line,hr.payslip.line,model_hr_payslip_line,hr_payroll_community.group_hr_payroll_community_user,1,1,1,1
|
||||
access_hr_payslip_input_user,hr.payslip.input.user,model_hr_payslip_input,hr_payroll_community.group_hr_payroll_community_user,1,1,1,1
|
||||
access_hr_payslip_worked_days_officer,hr.payslip.worked_days.officer,model_hr_payslip_worked_days,hr_payroll_community.group_hr_payroll_community_user,1,1,1,1
|
||||
access_hr_payslip_run,hr.payslip.run,model_hr_payslip_run,hr_payroll_community.group_hr_payroll_community_manager,1,1,1,1
|
||||
access_hr_rule_input_officer,hr.rule.input.office,model_hr_rule_input,hr_payroll_community.group_hr_payroll_community_user,1,1,1,1
|
||||
access_hr_salary_rule_user,hr.salary.rule.user,model_hr_salary_rule,hr_payroll_community.group_hr_payroll_community_user,1,1,1,1
|
||||
access_hr_contract_advantage_template,hr.contract.advantage.template.user,model_hr_contract_advantage_template,hr_payroll_community.group_hr_payroll_community_user,1,1,1,1
|
||||
access_hr_contract_advantage_template_hr_user,hr.contract.advantage.template.hr.user,model_hr_contract_advantage_template,hr.group_hr_user,1,0,0,0
|
||||
|
BIN
hr_payroll_community/static/description/banner.gif
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
hr_payroll_community/static/description/icon.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
hr_payroll_community/static/description/images/01payroll.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
hr_payroll_community/static/description/images/02payroll.png
Normal file
|
After Width: | Height: | Size: 75 KiB |
BIN
hr_payroll_community/static/description/images/03payroll.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
hr_payroll_community/static/description/images/04payroll.png
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
hr_payroll_community/static/description/images/05payroll.png
Normal file
|
After Width: | Height: | Size: 120 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 91 KiB |
|
After Width: | Height: | Size: 86 KiB |