add new module

This commit is contained in:
sonal arora
2020-07-21 10:09:46 +00:00
parent 800e7fdceb
commit c0d10e3fbe
1063 changed files with 243750 additions and 5 deletions

View File

@@ -0,0 +1,5 @@
#-*- coding:utf-8 -*-
from . import models
from . import report
from . import wizard

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

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

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

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,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

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

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

View 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()),
}

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

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

View File

@@ -0,0 +1,4 @@
#-*- coding:utf-8 -*-
from . import report_payslip_details
from . import report_contribution_register

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

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

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

View 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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_hr_payroll_community_structure hr.payroll.structure model_hr_payroll_structure hr_payroll_community.group_hr_payroll_community_user 1 1 1 1
3 access_hr_payroll_community_structure_hr_user hr.payroll.structure.hr.user model_hr_payroll_structure hr.group_hr_user 1 0 0 0
4 access_hr_contribution_register hr.contribution.register model_hr_contribution_register hr_payroll_community.group_hr_payroll_community_user 1 1 1 1
5 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
6 access_hr_payslip hr.payslip model_hr_payslip hr_payroll_community.group_hr_payroll_community_user 1 1 1 1
7 access_hr_payslip_line hr.payslip.line model_hr_payslip_line hr_payroll_community.group_hr_payroll_community_user 1 1 1 1
8 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
9 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
10 access_hr_payslip_run hr.payslip.run model_hr_payslip_run hr_payroll_community.group_hr_payroll_community_manager 1 1 1 1
11 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
12 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
13 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
14 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Some files were not shown because too many files have changed in this diff Show More