migration and fix issues
This commit is contained in:
parent
9dbfa06368
commit
c863ff5785
|
|
@ -0,0 +1,5 @@
|
|||
# -*- coding:utf-8 -*-
|
||||
|
||||
from . import wizard
|
||||
from . import models
|
||||
from . import report
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
# -*- coding:utf-8 -*-
|
||||
|
||||
{
|
||||
'name': 'Payroll',
|
||||
'category': 'Odex25-HR/Odex25-HR',
|
||||
'sequence': 38,
|
||||
'summary': 'Manage your employee payroll records',
|
||||
'description': "",
|
||||
'website': 'https://www.odoo.com/page/employees',
|
||||
'depends': [
|
||||
'hr_contract',
|
||||
'hr_holidays_community',
|
||||
],
|
||||
'data': [
|
||||
'security/hr_payroll_security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'data/hr_payroll_sequence.xml',
|
||||
'wizard/hr_payroll_payslips_by_employees_views.xml',
|
||||
'wizard/hr_payroll_contribution_register_report_views.xml',
|
||||
'views/hr_contract_views.xml',
|
||||
'views/hr_salary_rule_views.xml',
|
||||
'views/hr_payslip_views.xml',
|
||||
'views/hr_employee_views.xml',
|
||||
'views/hr_payroll_report.xml',
|
||||
'views/res_config_settings_views.xml',
|
||||
'views/report_contribution_register_templates.xml',
|
||||
'views/report_payslip_templates.xml',
|
||||
'views/report_payslip_details_templates.xml',
|
||||
'data/hr_payroll_data.xml',
|
||||
],
|
||||
'images': ['static/description/banner.png'],
|
||||
'application': True,
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<data noupdate="1">
|
||||
<record id="contrib_register_employees" model="hr.contribution.register">
|
||||
<field name="name">Employees</field>
|
||||
<field name="partner_id" eval="False"/>
|
||||
</record>
|
||||
|
||||
|
||||
<record id="ALW" model="hr.salary.rule.category">
|
||||
<field name="name">Allowance</field>
|
||||
<field name="code">ALW</field>
|
||||
<field name="rule_type">allowance</field>
|
||||
</record>
|
||||
|
||||
|
||||
<record id="DED" model="hr.salary.rule.category">
|
||||
<field name="name">Deduction</field>
|
||||
<field name="code">DED</field>
|
||||
<field name="rule_type">deduction</field>
|
||||
</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>
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
<?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="exp_hr_payroll.ALW"/>
|
||||
<field name="register_id" ref="hr_houserent_register"/>
|
||||
<field name="name">House Rent Allowance</field>
|
||||
<field name="sequence" eval="5"/>
|
||||
</record>
|
||||
|
||||
<record id="hr_salary_rule_convanceallowance1" model="hr.salary.rule">
|
||||
<field name="amount_select">fix</field>
|
||||
<field eval="800.0" name="amount_fix"/>
|
||||
<field name="code">CA</field>
|
||||
<field name="category_id" ref="exp_hr_payroll.ALW"/>
|
||||
<field name="name">Conveyance Allowance</field>
|
||||
<field name="sequence" eval="10"/>
|
||||
</record>
|
||||
|
||||
<record id="hr_salary_rule_professionaltax1" model="hr.salary.rule">
|
||||
<field name="amount_select">fix</field>
|
||||
<field eval="150" name="sequence"/>
|
||||
<field eval="-200.0" name="amount_fix"/>
|
||||
<field name="code">PT</field>
|
||||
<field name="category_id" ref="exp_hr_payroll.DED"/>
|
||||
<field name="register_id" ref="hr_professional_tax_register"/>
|
||||
<field name="name">Professional Tax</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_salary_rule_providentfund1" model="hr.salary.rule">
|
||||
<field name="amount_select">percentage</field>
|
||||
<field eval="120" name="sequence"/>
|
||||
<field eval="-12.5" name="amount_percentage"/>
|
||||
<field name="amount_percentage_base">contract.wage</field>
|
||||
<field name="code">PF</field>
|
||||
<field name="category_id" ref="exp_hr_payroll.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="exp_hr_payroll.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="exp_hr_payroll.ALW"/>
|
||||
<field name="register_id" ref="hr_meal_voucher_register"/>
|
||||
<field name="name">Meal Voucher</field>
|
||||
<field name="sequence" eval="16"/>
|
||||
</record>
|
||||
|
||||
<record id="hr_salary_rule_sales_commission" model="hr.salary.rule">
|
||||
<field name="amount_select">code</field>
|
||||
<field name="code">SALE</field>
|
||||
<field name="category_id" ref="exp_hr_payroll.ALW"/>
|
||||
<field name="name">Get 1% of sales</field>
|
||||
<field name="sequence" eval="17"/>
|
||||
<field name="amount_python_compute">result = ((inputs.SALEURO and inputs.SALEURO.amount) + (inputs.SALASIA and inputs.SALASIA.amount)) * 0.01</field>
|
||||
</record>
|
||||
|
||||
<!-- Rule Inputs -->
|
||||
|
||||
<record id="hr_rule_input_sale_a" model="hr.rule.input">
|
||||
<field name="code">SALEURO</field>
|
||||
<field name="name">Sales to Europe</field>
|
||||
<field name="input_id" ref="hr_salary_rule_sales_commission"/>
|
||||
</record>
|
||||
|
||||
<record id="hr_rule_input_sale_b" model="hr.rule.input">
|
||||
<field name="code">SALASIA</field>
|
||||
<field name="name">Sales to Asia</field>
|
||||
<field name="input_id" ref="hr_salary_rule_sales_commission"/>
|
||||
</record>
|
||||
|
||||
<!-- Salary Structure -->
|
||||
|
||||
<record id="structure_001" model="hr.payroll.structure">
|
||||
<field name="code">ME</field>
|
||||
<field name="name">Marketing Executive</field>
|
||||
<field eval="[(6, 0, [ref('hr_salary_rule_houserentallowance1'), ref('hr_salary_rule_convanceallowance1'),ref('hr_salary_rule_professionaltax1'),ref('hr_salary_rule_providentfund1')])]" name="rule_ids"/>
|
||||
<field name="company_id" ref="base.main_company"/>
|
||||
<field name="parent_id" ref="structure_base"/>
|
||||
</record>
|
||||
|
||||
<record id="structure_002" model="hr.payroll.structure">
|
||||
<field name="code">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="address_home_id" ref="base.res_partner_main2"/>
|
||||
<field name="image" type="base64" file="exp_hr_payroll/static/img/hr_employee_payroll-image.jpg"/>
|
||||
</record>
|
||||
|
||||
<!-- Employee Contract -->
|
||||
|
||||
<record id="hr_contract_firstcontract1" model="hr.contract">
|
||||
<field name="name">Marketing Executive Contract</field>
|
||||
<field name="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="exp_hr_payroll.structure_001"/>
|
||||
<field name="employee_id" ref="hr_employee_payroll"/>
|
||||
<field name="notes">Default contract for marketing executives</field>
|
||||
<field eval="4000.0" name="wage"/>
|
||||
</record>
|
||||
|
||||
<record id="hr_contract_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="exp_hr_payroll.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>
|
||||
|
|
@ -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
|
|
@ -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
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
# -*- coding:utf-8 -*-
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class HrContract(models.Model):
|
||||
"""
|
||||
Employee contract extension:
|
||||
- Links to Salary Structure
|
||||
- Defines pay schedule
|
||||
- Supports contract advantages
|
||||
"""
|
||||
_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(
|
||||
'resource.calendar',
|
||||
required=True,
|
||||
ondelete="restrict",
|
||||
help="Employee's working schedule."
|
||||
)
|
||||
|
||||
def get_all_structures(self):
|
||||
"""
|
||||
Return all salary structures linked to these contracts,
|
||||
including parent structures, as a recordset.
|
||||
"""
|
||||
structures = self.mapped('struct_id')
|
||||
if not structures:
|
||||
return self.env['hr.payroll.structure']
|
||||
return structures._get_parent_structure()
|
||||
|
||||
def get_attribute(self, code, attribute):
|
||||
"""
|
||||
Fetch a specific attribute from an advantage template by code.
|
||||
"""
|
||||
template = self.env['hr.contract.advantage.template'].search([('code', '=', code)], limit=1)
|
||||
return getattr(template, attribute, False)
|
||||
|
||||
def set_attribute_value(self, code, active):
|
||||
"""
|
||||
Set or reset an advantage value on the contract.
|
||||
"""
|
||||
template = self.env['hr.contract.advantage.template'].search([('code', '=', code)], limit=1)
|
||||
for contract in self:
|
||||
if active and template:
|
||||
contract[code] = template.default_value
|
||||
else:
|
||||
contract[code] = 0.0
|
||||
|
||||
|
||||
class HrContractAdvantageTemplate(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, index=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')
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
# -*- 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)
|
||||
payslip_count = fields.Integer(compute='_compute_payslip_count', string='Payslip Count',
|
||||
groups="om_hr_payroll.group_hr_payroll_user")
|
||||
|
||||
def _compute_payslip_count(self):
|
||||
for employee in self:
|
||||
employee.payslip_count = len(employee.slip_ids)
|
||||
|
|
@ -0,0 +1,607 @@
|
|||
# -*- coding:utf-8 -*-
|
||||
|
||||
from datetime import date, datetime, time
|
||||
|
||||
import babel
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from pytz import timezone
|
||||
|
||||
from odoo import api, fields, models, tools, _
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
|
||||
|
||||
class HrPayslip(models.Model):
|
||||
_name = 'hr.payslip'
|
||||
_description = 'Pay Slip'
|
||||
_order = 'id desc'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
|
||||
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,
|
||||
states={'draft': [('readonly', False)]})
|
||||
employee_id = fields.Many2one('hr.employee', string='Employee', required=True, readonly=True,
|
||||
states={'draft': [('readonly', False)]})
|
||||
date_from = fields.Date(string='Date From', readonly=True, required=True,
|
||||
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,
|
||||
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,
|
||||
default=lambda self: self.env.company,
|
||||
states={'draft': [('readonly', False)]})
|
||||
worked_days_line_ids = fields.One2many('hr.payslip.worked_days', 'payslip_id',
|
||||
string='Payslip Worked Days', copy=True, readonly=True,
|
||||
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,
|
||||
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')
|
||||
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()
|
||||
form_view_ref = self.env.ref('om_hr_payroll.view_hr_payslip_form', False)
|
||||
tree_view_ref = self.env.ref('om_hr_payroll.view_hr_payslip_tree', False)
|
||||
return {
|
||||
'name': (_("Refund Payslip")),
|
||||
'view_mode': 'tree, form',
|
||||
'view_id': False,
|
||||
'view_type': 'form',
|
||||
'res_model': 'hr.payslip',
|
||||
'type': 'ir.actions.act_window',
|
||||
'target': 'current',
|
||||
'domain': "[('id', 'in', %s)]" % copied_payslip.ids,
|
||||
'views': [(tree_view_ref and tree_view_ref.id or False, 'tree'),
|
||||
(form_view_ref and form_view_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.name 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):
|
||||
print('onchange')
|
||||
self.ensure_one()
|
||||
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
|
||||
|
||||
# computation of the salary input
|
||||
contracts = self.env['hr.contract'].browse(contract_ids)
|
||||
if contracts:
|
||||
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')
|
||||
salary_rule_id = fields.Many2one('hr.salary.rule', string='Rule', required=True)
|
||||
employee_id = fields.Many2one('hr.employee', string='Employee', required=True)
|
||||
contract_id = fields.Many2one('hr.contract', string='Contract', required=True, index=True)
|
||||
rate = fields.Float(string='Rate (%)', default=100.0)
|
||||
amount = fields.Float()
|
||||
quantity = fields.Float(default=1.0)
|
||||
total = fields.Float(compute='_compute_total', string='Total', 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)
|
||||
sequence = fields.Integer(required=True, index=True, default=10)
|
||||
code = fields.Char(required=True, help="The code that can be used in the salary rules")
|
||||
number_of_days = fields.Float(string='Number of Days')
|
||||
number_of_hours = fields.Float(string='Number of Hours')
|
||||
contract_id = fields.Many2one('hr.contract', string='Contract', required=True,
|
||||
help="The contract for which applied this input")
|
||||
|
||||
|
||||
class HrPayslipInput(models.Model):
|
||||
_name = 'hr.payslip.input'
|
||||
_description = 'Payslip Input'
|
||||
_order = 'payslip_id, sequence'
|
||||
|
||||
name = fields.Char(string='Description', required=True)
|
||||
payslip_id = fields.Many2one('hr.payslip', string='Pay Slip', required=True, ondelete='cascade', index=True)
|
||||
sequence = fields.Integer(required=True, index=True, default=10)
|
||||
code = fields.Char(required=True, help="The code that can be used in the salary rules")
|
||||
amount = fields.Float(help="It is used in computation. For e.g. A rule for sales having "
|
||||
"1% commission of basic salary for per product can defined in expression "
|
||||
"like result = inputs.SALEURO.amount * contract.wage*0.01.")
|
||||
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'
|
||||
_order = 'date_start desc'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
|
||||
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'),
|
||||
('done', 'Done'),
|
||||
('close', 'Close'),
|
||||
], string='Status', index=True, readonly=True, copy=False, default='draft')
|
||||
date_start = fields.Date(string='Date From', required=True, readonly=True,
|
||||
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,
|
||||
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'})
|
||||
|
||||
def done_payslip_run(self):
|
||||
for line in self.slip_ids:
|
||||
line.action_payslip_done()
|
||||
return self.write({'state': 'done'})
|
||||
|
||||
def unlink(self):
|
||||
for rec in self:
|
||||
if rec.state == 'done':
|
||||
raise ValidationError(_('You Cannot Delete Done Payslips Batches'))
|
||||
return super(HrPayslipRun, self).unlink()
|
||||
|
|
@ -0,0 +1,248 @@
|
|||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.tools.safe_eval import safe_eval
|
||||
|
||||
|
||||
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('om_hr_payroll.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, default=lambda self: self.env.company)
|
||||
note = fields.Text(string='Description')
|
||||
parent_id = fields.Many2one('hr.payroll.structure', string='Parent', default=_get_parent)
|
||||
children_ids = fields.One2many('hr.payroll.structure', 'parent_id', string='Children', copy=True)
|
||||
rule_ids = fields.Many2many('hr.salary.rule', 'hr_structure_salary_rule_rel', 'struct_id', 'rule_id',
|
||||
string='Salary Rules')
|
||||
|
||||
@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.company)
|
||||
partner_id = fields.Many2one('res.partner', string='Partner')
|
||||
name = fields.Char(required=True)
|
||||
register_line_ids = fields.One2many('hr.payslip.line', 'register_id',
|
||||
string='Register Line', readonly=True)
|
||||
note = fields.Text(string='Description')
|
||||
|
||||
|
||||
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)
|
||||
rule_type = fields.Selection(
|
||||
[('allowance', 'Allowance'),
|
||||
('deduction', 'Deduction')],
|
||||
string='Rule Type'
|
||||
)
|
||||
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.company)
|
||||
|
||||
@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.company)
|
||||
condition_select = fields.Selection([
|
||||
('none', 'Always True'),
|
||||
('range', 'Range'),
|
||||
('python', 'Python Expression')
|
||||
], string="Condition Based on", default='none', required=True)
|
||||
condition_range = fields.Char(string='Range Based on', default='contract.wage',
|
||||
help='This will be used to compute the % fields values; in general it is on basic, '
|
||||
'but you can also use categories code fields in lowercase as a variable names '
|
||||
'(hra, ma, lta, etc.) and the variable basic.')
|
||||
condition_python = fields.Text(string='Python Condition', required=True,
|
||||
default='''
|
||||
# Available variables:
|
||||
#----------------------
|
||||
# payslip: 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')
|
||||
amount_percentage = fields.Float(string='Percentage (%)',
|
||||
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 <= 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)
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# -*- coding:utf-8 -*-
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
module_exp_hr_payroll_account = fields.Boolean(
|
||||
string='Payroll Accounting'
|
||||
)
|
||||
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
# -*- coding:utf-8 -*-
|
||||
|
||||
from . import report_payslip_details
|
||||
from . import report_contribution_register
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
# -*- 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.om_hr_payroll.report_contribution_register'
|
||||
_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
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
# -*- coding:utf-8 -*-
|
||||
|
||||
from odoo import api, models
|
||||
|
||||
|
||||
class PayslipDetailsReport(models.AbstractModel):
|
||||
_name = 'report.exp_hr_payroll.report_payslip_details'
|
||||
_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)),
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
|
||||
<record model="ir.module.category" id="base.module_category_payroll_localization">
|
||||
<field name="name">Payroll</field>
|
||||
<field name="visible" eval="0" />
|
||||
</record>
|
||||
|
||||
<record id="group_hr_payroll_user" model="res.groups">
|
||||
<field name="name">Officer</field>
|
||||
<field name="category_id" ref="base.module_category_payroll_localization"/>
|
||||
<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_manager" model="res.groups">
|
||||
<field name="name">Manager</field>
|
||||
<field name="category_id" ref="base.module_category_payroll_localization"/>
|
||||
<field name="implied_ids" eval="[(4, ref('exp_hr_payroll.group_hr_payroll_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('exp_hr_payroll.group_hr_payroll_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('exp_hr_payroll.group_hr_payroll_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('exp_hr_payroll.group_hr_payroll_manager'))]"/>
|
||||
</record-->
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_hr_payroll_structure,hr.payroll.structure,model_hr_payroll_structure,exp_hr_payroll.group_hr_payroll_user,1,1,1,1
|
||||
access_hr_payroll_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,exp_hr_payroll.group_hr_payroll_user,1,1,1,1
|
||||
access_hr_salary_rule_category,hr.salary.rule.category,model_hr_salary_rule_category,exp_hr_payroll.group_hr_payroll_user,1,1,1,1
|
||||
access_hr_payslip,hr.payslip,model_hr_payslip,exp_hr_payroll.group_hr_payroll_user,1,1,1,1
|
||||
access_hr_payslip_line,hr.payslip.line,model_hr_payslip_line,exp_hr_payroll.group_hr_payroll_user,1,1,1,1
|
||||
access_hr_payslip_input_user,hr.payslip.input.user,model_hr_payslip_input,exp_hr_payroll.group_hr_payroll_user,1,1,1,1
|
||||
access_hr_payslip_worked_days_officer,hr.payslip.worked_days.officer,model_hr_payslip_worked_days,exp_hr_payroll.group_hr_payroll_user,1,1,1,1
|
||||
access_hr_payslip_run,hr.payslip.run,model_hr_payslip_run,exp_hr_payroll.group_hr_payroll_manager,1,1,1,1
|
||||
access_hr_rule_input_officer,hr.rule.input.office,model_hr_rule_input,exp_hr_payroll.group_hr_payroll_user,1,1,1,1
|
||||
access_hr_salary_rule_user,hr.salary.rule.user,model_hr_salary_rule,exp_hr_payroll.group_hr_payroll_user,1,1,1,1
|
||||
access_hr_contract_advantage_template,hr.contract.advantage.template.user,model_hr_contract_advantage_template,exp_hr_payroll.group_hr_payroll_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
|
||||
access_hr_payslip_employees_hr_user,hr.payslip.employees.hr.user,model_hr_payslip_employees,hr.group_hr_user,1,1,1,1
|
||||
access_payslip_lines_contribution_register_hr_user,payslip.lines.contribution.register.hr.user,model_payslip_lines_contribution_register,hr.group_hr_user,1,1,1,1
|
||||
|
Binary file not shown.
|
After Width: | Height: | Size: 424 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 8.8 KiB |
|
|
@ -0,0 +1,24 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="70" height="70" viewBox="0 0 70 70">
|
||||
<defs>
|
||||
<path id="icon-a" d="M4,5.35309892e-14 C36.4160122,9.87060235e-15 58.0836068,-3.97961823e-14 65,5.07020818e-14 C69,6.733808e-14 70,1 70,5 C70,43.0488877 70,62.4235458 70,65 C70,69 69,70 65,70 C61,70 9,70 4,70 C1,70 7.10542736e-15,69 7.10542736e-15,65 C7.25721566e-15,62.4676575 3.83358709e-14,41.8005206 3.60818146e-14,5 C-1.13686838e-13,1 1,5.75716207e-14 4,5.35309892e-14 Z"/>
|
||||
<linearGradient id="icon-c" x1="100%" x2="0%" y1="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="#CD7690"/>
|
||||
<stop offset="100%" stop-color="#CA5377"/>
|
||||
</linearGradient>
|
||||
<path id="icon-d" d="M29.5,16.9166667 C34.5338704,16.9166667 38.6145833,20.9973796 38.6145833,26.03125 C38.6145833,31.0651204 34.5338704,35.1458333 29.5,35.1458333 C24.4661296,35.1458333 20.3854167,31.0651204 20.3854167,26.03125 C20.3854167,20.9973796 24.4661296,16.9166667 29.5,16.9166667 Z M38.3969283,46.0833333 L17.6510417,46.0833333 C16.1408691,46.0833333 14.9166667,44.8591309 14.9166667,43.3489583 L14.9166667,41.2386475 C14.9166667,38.7291748 16.6245687,36.5417887 19.0590739,35.9331624 L23.1215007,34.91757 C26.4370443,37.3024007 31.6104248,37.9874756 35.8784993,34.91757 L38.085401,35.4692877 C38.5061499,38.7235298 38.4706769,43.0476895 38.3969283,46.0833333 Z M58.75,30.5 C59.9921875,30.5 61,31.6757812 61,33.125 L61,52.375 C61,53.8242187 59.9921875,55 58.75,55 L42.25,55 C41.0078125,55 40,53.8242187 40,52.375 L40,33.125 C40,31.6757812 41.0078125,30.5 42.25,30.5 L44.5,30.5 L44.5,27.65625 C44.5,27.2953125 44.753125,27 45.0625,27 L46.9375,27 C47.246875,27 47.5,27.2953125 47.5,27.65625 L47.5,30.5 L53.5,30.5 L53.5,27.65625 C53.5,27.2953125 53.753125,27 54.0625,27 L55.9375,27 C56.246875,27 56.5,27.2953125 56.5,27.65625 L56.5,30.5 L58.75,30.5 Z M58.46875,52.375 C58.6234375,52.375 58.75,52.2105322 58.75,52.009516 L58.75,33.8571429 L42.25,33.8571429 L42.25,52.009516 C42.25,52.2105322 42.3765625,52.375 42.53125,52.375 L58.46875,52.375 Z M49.1056813,41.3851318 C49.1056813,42.9223389 54.4224866,42.5321777 54.4224578,45.454248 C54.4224578,46.8539063 53.2582943,48.0494873 51.3671328,48.298877 L51.3671328,49.4570312 C51.3671328,49.6188232 51.2125191,49.75 51.0218203,49.75 L49.8707786,49.75 C49.6800798,49.75 49.5254661,49.6188232 49.5254661,49.4570312 L49.5254661,48.2798584 C48.3887549,48.1158447 47.3758383,47.6706543 46.6838033,47.1086426 C46.5578505,47.0063477 46.5419661,46.8402588 46.6463369,46.7217529 L47.520812,45.7289063 C47.6381319,45.5957275 47.8617793,45.5725586 48.0145801,45.6766602 C48.7315639,46.1651123 49.6576057,46.5528076 50.5424978,46.5528076 C51.5730542,46.5528076 52.0424202,46.0319336 52.0424202,45.5479736 C52.0424202,44.1172119 46.7256148,44.427832 46.7256148,41.4118896 C46.7256148,40.1247314 47.8384419,39.0840332 49.5254949,38.7595703 L49.5254949,37.5429688 C49.5254949,37.3811768 49.6801086,37.25 49.8708074,37.25 L51.0218491,37.25 C51.2125479,37.25 51.3671616,37.3811768 51.3671616,37.5429688 L51.3671616,38.6962646 C52.2924264,38.7869873 53.2953866,39.0949219 53.9940977,39.6192627 C54.1141225,39.7093262 54.1444237,39.8580811 54.068196,39.9776611 L53.3904626,41.0408691 C53.2913292,41.1964111 53.0539268,41.2421875 52.8847237,41.1389404 C52.241723,40.7466064 51.4535473,40.4472168 50.6844788,40.4472168 C49.7254021,40.4472168 49.1056813,40.8153809 49.1056813,41.3851318 Z"/>
|
||||
<path id="icon-e" d="M29.5,14.9166667 C34.5338704,14.9166667 38.6145833,18.9973796 38.6145833,24.03125 C38.6145833,29.0651204 34.5338704,33.1458333 29.5,33.1458333 C24.4661296,33.1458333 20.3854167,29.0651204 20.3854167,24.03125 C20.3854167,18.9973796 24.4661296,14.9166667 29.5,14.9166667 Z M38.3969283,44.0833333 L17.6510417,44.0833333 C16.1408691,44.0833333 14.9166667,42.8591309 14.9166667,41.3489583 L14.9166667,39.2386475 C14.9166667,36.7291748 16.6245687,34.5417887 19.0590739,33.9331624 L23.1215007,32.91757 C26.4370443,35.3024007 31.6104248,35.9874756 35.8784993,32.91757 L38.085401,33.4692877 C38.5061499,36.7235298 38.4706769,41.0476895 38.3969283,44.0833333 Z M58.75,28.5 C59.9921875,28.5 61,29.6757812 61,31.125 L61,50.375 C61,51.8242187 59.9921875,53 58.75,53 L42.25,53 C41.0078125,53 40,51.8242187 40,50.375 L40,31.125 C40,29.6757812 41.0078125,28.5 42.25,28.5 L44.5,28.5 L44.5,25.65625 C44.5,25.2953125 44.753125,25 45.0625,25 L46.9375,25 C47.246875,25 47.5,25.2953125 47.5,25.65625 L47.5,28.5 L53.5,28.5 L53.5,25.65625 C53.5,25.2953125 53.753125,25 54.0625,25 L55.9375,25 C56.246875,25 56.5,25.2953125 56.5,25.65625 L56.5,28.5 L58.75,28.5 Z M58.46875,50.375 C58.6234375,50.375 58.75,50.2105322 58.75,50.009516 L58.75,31.8571429 L42.25,31.8571429 L42.25,50.009516 C42.25,50.2105322 42.3765625,50.375 42.53125,50.375 L58.46875,50.375 Z M49.1056813,39.3851318 C49.1056813,40.9223389 54.4224866,40.5321777 54.4224578,43.454248 C54.4224578,44.8539063 53.2582943,46.0494873 51.3671328,46.298877 L51.3671328,47.4570312 C51.3671328,47.6188232 51.2125191,47.75 51.0218203,47.75 L49.8707786,47.75 C49.6800798,47.75 49.5254661,47.6188232 49.5254661,47.4570312 L49.5254661,46.2798584 C48.3887549,46.1158447 47.3758383,45.6706543 46.6838033,45.1086426 C46.5578505,45.0063477 46.5419661,44.8402588 46.6463369,44.7217529 L47.520812,43.7289063 C47.6381319,43.5957275 47.8617793,43.5725586 48.0145801,43.6766602 C48.7315639,44.1651123 49.6576057,44.5528076 50.5424978,44.5528076 C51.5730542,44.5528076 52.0424202,44.0319336 52.0424202,43.5479736 C52.0424202,42.1172119 46.7256148,42.427832 46.7256148,39.4118896 C46.7256148,38.1247314 47.8384419,37.0840332 49.5254949,36.7595703 L49.5254949,35.5429688 C49.5254949,35.3811768 49.6801086,35.25 49.8708074,35.25 L51.0218491,35.25 C51.2125479,35.25 51.3671616,35.3811768 51.3671616,35.5429688 L51.3671616,36.6962646 C52.2924264,36.7869873 53.2953866,37.0949219 53.9940977,37.6192627 C54.1141225,37.7093262 54.1444237,37.8580811 54.068196,37.9776611 L53.3904626,39.0408691 C53.2913292,39.1964111 53.0539268,39.2421875 52.8847237,39.1389404 C52.241723,38.7466064 51.4535473,38.4472168 50.6844788,38.4472168 C49.7254021,38.4472168 49.1056813,38.8153809 49.1056813,39.3851318 Z"/>
|
||||
</defs>
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<mask id="icon-b" fill="#fff">
|
||||
<use xlink:href="#icon-a"/>
|
||||
</mask>
|
||||
<g mask="url(#icon-b)">
|
||||
<rect width="70" height="70" fill="url(#icon-c)"/>
|
||||
<path fill="#FFF" fill-opacity=".383" d="M4,1.8 L65,1.8 C67.6666667,1.8 69.3333333,1.13333333 70,-0.2 C70,2.46666667 70,3.46666667 70,2.8 L1.10547097e-14,2.8 C-1.65952376e-14,3.46666667 -2.9161925e-14,2.46666667 -2.66453526e-14,-0.2 C0.666666667,1.13333333 2,1.8 4,1.8 Z" transform="matrix(1 0 0 -1 0 2.8)"/>
|
||||
<path fill="#393939" d="M45.4871795,51 L4,51 C2,51 -7.10542736e-15,50.851312 0,46.8367347 L2.08199737e-16,23.7312137 L22,0 L37,1.04081633 L36,15.6122449 L40.5053241,10.5061222 L41.4366949,9.83683131 L44.9722671,6.08098476 L49,12.4897959 L53.8072717,6.21274166 L56,11.4489796 L59,11.4489796 L60.3332838,33.1194301 L45.4871795,51 Z" opacity=".324" transform="translate(0 19)"/>
|
||||
<path fill="#000" fill-opacity=".383" d="M4,4 L65,4 C67.6666667,4 69.3333333,3 70,1 C70,3.66666667 70,5 70,5 L1.77635684e-15,5 C1.77635684e-15,5 1.77635684e-15,3.66666667 1.77635684e-15,1 C0.666666667,3 2,4 4,4 Z" transform="translate(0 65)"/>
|
||||
<use fill="#000" fill-rule="nonzero" opacity=".3" xlink:href="#icon-d"/>
|
||||
<use fill="#FFF" fill-rule="nonzero" xlink:href="#icon-e"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.4 KiB |
|
|
@ -0,0 +1,61 @@
|
|||
<section class="oe_container oe_dark">
|
||||
<div class="col-md-12">
|
||||
<h2 class="oe_slogan" style="font-size: 35px;color:#2C0091"><b>Odoo 14 Payroll For Community Edition</b></h2>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<section class="oe_container">
|
||||
<div class="oe_row oe_spaced">
|
||||
<div class="oe_centeralign oe_websiteonly">
|
||||
<h4 class="oe_slogan"><a href="https://apps.odoo.com/apps/modules/14.0/hr_payroll_account/" target="_blank" style="color: #FFFFFF !important; border-radius: 0; background-color: #9c676e; border-color: #005ca7; padding: 15px; font-weight: bold;">
|
||||
<i class="fa fa-youtube"></i>
|
||||
For Payroll-Accounting Module Click
|
||||
</a></h4>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<section class="oe_container">
|
||||
<div class="oe_row oe_spaced">
|
||||
<div class="oe_span12">
|
||||
<h3 class="oe_slogan" style="color:#332c3c;font-size: 28px;">Payslip Generation</h3>
|
||||
<div class="oe_demo oe_picture oe_screenshot">
|
||||
<img src="odoo14_payslip.png" style="height:400px;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<hr style="width: 100%;height: 4px;background: #2C0091;margin: 0px 0px;">
|
||||
<hr style="width: 100%;height: 4px;background: #148963;margin: 0px 0px;">
|
||||
<section class="oe_container oe_dark">
|
||||
<div class="oe_row ">
|
||||
<div class="oe_slogan text-center">
|
||||
<img src="odoo_mates.png"/>
|
||||
<div style="color:#269900;">
|
||||
<h3 style="color:#2C0091;font-size: 25px;">If you need any help or support, just contact us:</h3><br>
|
||||
<h3 style="color:#2C0091;font-size: 20px;">Email: <a href="odoomates@gmail.com">odoomates@gmail.com</a> <br></h3>
|
||||
</div>
|
||||
<div class="oe_slogan">
|
||||
<h2>
|
||||
<a target="_blank" href="https://www.facebook.com/odoomate/" target="new">
|
||||
<i class="fa fa-facebook-square" style="font-size:38px;"></i>
|
||||
</a>
|
||||
<a target="_blank" href="https://twitter.com/odoomates/" target="new">
|
||||
<i class="fa fa-twitter" style="font-size:38px;"></i>
|
||||
</a>
|
||||
<a href="#" target="_blank">
|
||||
<i class="fa fa-linkedin" style="font-size:38px;"></i>
|
||||
</a>
|
||||
<a target="_blank" href="https://www.youtube.com/channel/UCVKlUZP7HAhdQgs-9iTJklQ">
|
||||
<i class="fa fa-youtube-play" style="font-size:38px;"></i>
|
||||
</a>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<hr style="width: 100%;height: 4px;background: #148963;margin: 0px 0px;">
|
||||
<hr style="width: 100%;height: 4px;background: #2C0091;margin: 0px 0px;">
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 73 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 7.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 9.7 KiB |
|
|
@ -0,0 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import test_payslip_flow
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from odoo.fields import Date
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
|
||||
class TestPayslipBase(TransactionCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestPayslipBase, self).setUp()
|
||||
|
||||
# Some salary rules references
|
||||
self.hra_rule_id = self.ref('om_hr_payroll.hr_salary_rule_houserentallowance1')
|
||||
self.conv_rule_id = self.ref('om_hr_payroll.hr_salary_rule_convanceallowance1')
|
||||
self.prof_tax_rule_id = self.ref('om_hr_payroll.hr_salary_rule_professionaltax1')
|
||||
self.pf_rule_id = self.ref('om_hr_payroll.hr_salary_rule_providentfund1')
|
||||
self.mv_rule_id = self.ref('om_hr_payroll.hr_salary_rule_meal_voucher')
|
||||
self.comm_rule_id = self.ref('om_hr_payroll.hr_salary_rule_sales_commission')
|
||||
|
||||
# I create a new employee "Richard"
|
||||
self.richard_emp = self.env['hr.employee'].create({
|
||||
'name': 'Richard',
|
||||
'gender': 'male',
|
||||
'birthday': '1984-05-01',
|
||||
'country_id': self.ref('base.be'),
|
||||
'department_id': self.ref('hr.dep_rd')
|
||||
})
|
||||
|
||||
# I create a salary structure for "Software Developer"
|
||||
self.developer_pay_structure = self.env['hr.payroll.structure'].create({
|
||||
'name': 'Salary Structure for Software Developer',
|
||||
'code': 'SD',
|
||||
'company_id': self.ref('base.main_company'),
|
||||
'rule_ids': [(4, self.hra_rule_id), (4, self.conv_rule_id),
|
||||
(4, self.prof_tax_rule_id), (4, self.pf_rule_id),
|
||||
(4, self.mv_rule_id), (4, self.comm_rule_id)],
|
||||
})
|
||||
|
||||
# I create a contract for "Richard"
|
||||
self.env['hr.contract'].create({
|
||||
'date_end': Date.to_string((datetime.now() + timedelta(days=365))),
|
||||
'date_start': Date.today(),
|
||||
'name': 'Contract for Richard',
|
||||
'wage': 5000.0,
|
||||
'type_id': self.ref('hr_contract.hr_contract_type_emp'),
|
||||
'employee_id': self.richard_emp.id,
|
||||
'struct_id': self.developer_pay_structure.id,
|
||||
})
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import os
|
||||
|
||||
from odoo.tools import config, test_reports
|
||||
from odoo.addons.om_hr_payroll.tests.common import TestPayslipBase
|
||||
|
||||
|
||||
class TestPayslipFlow(TestPayslipBase):
|
||||
|
||||
def test_00_payslip_flow(self):
|
||||
""" Testing payslip flow and report printing """
|
||||
# I create an employee Payslip
|
||||
richard_payslip = self.env['hr.payslip'].create({
|
||||
'name': 'Payslip of Richard',
|
||||
'employee_id': self.richard_emp.id
|
||||
})
|
||||
|
||||
payslip_input = self.env['hr.payslip.input'].search([('payslip_id', '=', richard_payslip.id)])
|
||||
# I assign the amount to Input data
|
||||
payslip_input.write({'amount': 5.0})
|
||||
|
||||
# I verify the payslip is in draft state
|
||||
self.assertEqual(richard_payslip.state, 'draft', 'State not changed!')
|
||||
|
||||
context = {
|
||||
"lang": "en_US", "tz": False, "active_model": "ir.ui.menu",
|
||||
"department_id": False, "section_id": False,
|
||||
"active_ids": [self.ref("om_hr_payroll.menu_department_tree")],
|
||||
"active_id": self.ref("om_hr_payroll.menu_department_tree")
|
||||
}
|
||||
# I click on 'Compute Sheet' button on payslip
|
||||
richard_payslip.with_context(context).compute_sheet()
|
||||
|
||||
# Then I click on the 'Confirm' button on payslip
|
||||
richard_payslip.action_payslip_done()
|
||||
|
||||
# I verify that the payslip is in done state
|
||||
self.assertEqual(richard_payslip.state, 'done', 'State not changed!')
|
||||
|
||||
# I want to check refund payslip so I click on refund button.
|
||||
richard_payslip.refund_sheet()
|
||||
|
||||
# I check on new payslip Credit Note is checked or not.
|
||||
payslip_refund = self.env['hr.payslip'].search([('name', 'like', 'Refund: '+ richard_payslip.name), ('credit_note', '=', True)])
|
||||
self.assertTrue(bool(payslip_refund), "Payslip not refunded!")
|
||||
|
||||
# I want to generate a payslip from Payslip run.
|
||||
payslip_run = self.env['hr.payslip.run'].create({
|
||||
'date_end': '2011-09-30',
|
||||
'date_start': '2011-09-01',
|
||||
'name': 'Payslip for Employee'
|
||||
})
|
||||
|
||||
# I create record for generating the payslip for this Payslip run.
|
||||
|
||||
payslip_employee = self.env['hr.payslip.employees'].create({
|
||||
'employee_ids': [(4, self.richard_emp.id)]
|
||||
})
|
||||
|
||||
# I generate the payslip by clicking on Generat button wizard.
|
||||
payslip_employee.with_context(active_id=payslip_run.id).compute_sheet()
|
||||
|
||||
# I open Contribution Register and from there I print the Payslip Lines report.
|
||||
self.env['payslip.lines.contribution.register'].create({
|
||||
'date_from': '2011-09-30',
|
||||
'date_to': '2011-09-01'
|
||||
})
|
||||
|
||||
# I print the payslip report
|
||||
data, data_format = self.env.ref('om_hr_payroll.action_report_payslip').render(richard_payslip.ids)
|
||||
|
||||
# I print the payslip details report
|
||||
data, data_format = self.env.ref('om_hr_payroll.payslip_details_report').render(richard_payslip.ids)
|
||||
|
||||
# I print the contribution register report
|
||||
context = {'model': 'hr.contribution.register', 'active_ids': [self.ref('om_hr_payroll.hr_houserent_register')]}
|
||||
test_reports.try_report_action(self.env.cr, self.env.uid, 'action_payslip_lines_contribution_register', context=context, our_module='exp_hr_payroll')
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Root Menus -->
|
||||
|
||||
<menuitem id="menu_hr_payroll_root"
|
||||
name="Payroll"
|
||||
sequence="76"
|
||||
web_icon="exp_hr_payroll,static/description/icon.png" />
|
||||
|
||||
<menuitem id="menu_hr_payroll_configuration"
|
||||
name="Configuration" parent="menu_hr_payroll_root"
|
||||
sequence="100"
|
||||
groups="exp_hr_payroll.group_hr_payroll_manager" />
|
||||
|
||||
<!-- Contract View -->
|
||||
<record id="hr_contract_form_inherit" model="ir.ui.view">
|
||||
<field name="name">hr.contract.view.form.inherit</field>
|
||||
<field name="model">hr.contract</field>
|
||||
<field name="inherit_id" ref="hr_contract.hr_contract_view_form" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='job_id']" position="after">
|
||||
<field name="struct_id" required="1" />
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='job_id']" position="before">
|
||||
<field name="company_id" groups="base.group_multi_company" />
|
||||
<field name="currency_id" invisible="1" />
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='job_id']" position="after">
|
||||
<field name="schedule_pay" />
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_contract_advantage_template_view_form" model="ir.ui.view">
|
||||
<field name="name">hr.contract.advantage.template.form</field>
|
||||
<field name="model">hr.contract.advantage.template</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<h1>
|
||||
<field name="name" placeholder="Advantage Name" />
|
||||
</h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="code" />
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<group>
|
||||
<field name="default_value" />
|
||||
<field name="lower_bound" />
|
||||
<field name="upper_bound" />
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_contract_advantage_template_view_tree" model="ir.ui.view">
|
||||
<field name="name">hr.contract.advantage.template.tree</field>
|
||||
<field name="model">hr.contract.advantage.template</field>
|
||||
<field name="arch" type="xml">
|
||||
<list>
|
||||
<field name="name" />
|
||||
<field name="code" />
|
||||
<field name="lower_bound" />
|
||||
<field name="upper_bound" />
|
||||
<field name="default_value" />
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_contract_advantage_template_action" model="ir.actions.act_window">
|
||||
<field name="name">Contract Advantage Templates</field>
|
||||
<field name="res_model">hr.contract.advantage.template</field>
|
||||
</record>
|
||||
|
||||
<menuitem
|
||||
id="hr_contract_advantage_template_menu_action"
|
||||
action="hr_contract_advantage_template_action"
|
||||
parent="menu_hr_payroll_configuration"
|
||||
sequence="50" />
|
||||
|
||||
</odoo>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue