[MOV] hr_payroll: Move to enterprise
For more information about this decision, please read https://www.odoo.com/fr_FR/blog/notre-blog-5/post/odoo-community-enterprise-532
This commit is contained in:
@@ -1,6 +0,0 @@
|
||||
#-*- coding:utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import models
|
||||
from . import wizard
|
||||
from . import controllers
|
||||
@@ -1,42 +0,0 @@
|
||||
#-*- coding:utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
'name': 'Payroll',
|
||||
'category': 'Human Resources/Payroll',
|
||||
'sequence': 38,
|
||||
'summary': 'Manage your employee payroll records',
|
||||
'description': "",
|
||||
'installable': True,
|
||||
'application': True,
|
||||
'depends': [
|
||||
'hr_contract',
|
||||
'hr_holidays',
|
||||
'decimal_precision',
|
||||
],
|
||||
'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',
|
||||
'views/res_config_settings_views.xml',
|
||||
'views/report_contributionregister_templates.xml',
|
||||
'views/report_payslip_templates.xml',
|
||||
'views/hr_work_entry_views.xml',
|
||||
'views/hr_leave_views.xml',
|
||||
'views/resource_views.xml',
|
||||
'views/hr_work_entry_template.xml',
|
||||
'views/hr_payroll_menu.xml',
|
||||
],
|
||||
'demo': ['data/hr_payroll_demo.xml'],
|
||||
'qweb': [
|
||||
"static/src/xml/work_entries_templates.xml",
|
||||
"static/src/xml/payslip_tree_views.xml",
|
||||
],
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import main
|
||||
@@ -1,50 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import io
|
||||
import re
|
||||
|
||||
from PyPDF2 import PdfFileReader, PdfFileWriter
|
||||
|
||||
from odoo.http import request, route, Controller
|
||||
from odoo.tools.safe_eval import safe_eval
|
||||
|
||||
|
||||
class HrPayroll(Controller):
|
||||
|
||||
@route(["/print/payslips"], type='http', auth='user')
|
||||
def get_payroll_report_print(self, list_ids='', **post):
|
||||
if not request.env.user.has_group('hr_payroll.group_hr_payroll_user') or not list_ids or re.search("[^0-9|,]", list_ids):
|
||||
return request.not_found()
|
||||
|
||||
ids = [int(s) for s in list_ids.split(',')]
|
||||
payslips = request.env['hr.payslip'].browse(ids)
|
||||
|
||||
pdf_writer = PdfFileWriter()
|
||||
|
||||
for payslip in payslips:
|
||||
if not payslip.struct_id or not payslip.struct_id.report_id:
|
||||
continue
|
||||
pdf_content, _ = payslip.struct_id.report_id.render_qweb_pdf(payslip.id)
|
||||
reader = PdfFileReader(io.BytesIO(pdf_content), strict=False, overwriteWarnings=False)
|
||||
|
||||
for page in range(reader.getNumPages()):
|
||||
pdf_writer.addPage(reader.getPage(page))
|
||||
|
||||
_buffer = io.BytesIO()
|
||||
pdf_writer.write(_buffer)
|
||||
merged_pdf = _buffer.getvalue()
|
||||
_buffer.close()
|
||||
|
||||
if len(payslips) == 1 and payslips.struct_id.report_id.print_report_name:
|
||||
report_name = safe_eval(payslips.struct_id.report_id.print_report_name, {'object': payslips})
|
||||
else:
|
||||
report_name = "Payslips"
|
||||
|
||||
pdfhttpheaders = [
|
||||
('Content-Type', 'application/pdf'),
|
||||
('Content-Length', len(merged_pdf)),
|
||||
('Content-Disposition', 'attachment; filename=' + report_name + '.pdf;')
|
||||
]
|
||||
|
||||
return request.make_response(merged_pdf, headers=pdfhttpheaders)
|
||||
@@ -1,77 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="0">
|
||||
|
||||
<record id="structure_type_employee" model="hr.payroll.structure.type">
|
||||
<field name="name">Employee</field>
|
||||
</record>
|
||||
|
||||
<record id="resource.resource_calendar_std" model="resource.calendar">
|
||||
<field name="full_time_required_hours">40</field>
|
||||
</record>
|
||||
|
||||
<record id="resource.resource_calendar_std_35h" model="resource.calendar">
|
||||
<field name="full_time_required_hours">35</field>
|
||||
</record>
|
||||
|
||||
<record id="resource.resource_calendar_std_38h" model="resource.calendar">
|
||||
<field name="full_time_required_hours">38</field>
|
||||
</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>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<!-- Work entries Type -->
|
||||
<record id="work_entry_type_attendance" model="hr.work.entry.type">
|
||||
<field name="name">Attendance</field>
|
||||
<field name="color">0</field>
|
||||
<field name="code">WORK100</field>
|
||||
</record>
|
||||
|
||||
<record id="work_entry_type_leave" model="hr.work.entry.type">
|
||||
<field name="name">Generic Time Off</field>
|
||||
<field name="code">LEAVE100</field>
|
||||
<field name="color">3</field>
|
||||
<field name="is_leave">True</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -1,206 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- Contribution Register -->
|
||||
|
||||
<record id="hr_houserent_register" model="res.partner">
|
||||
<field name="name">House Rent Allowance Register</field>
|
||||
<field name="supplier">1</field>
|
||||
<field eval="0" name="customer"/>
|
||||
</record>
|
||||
|
||||
<record id="hr_provident_fund_register" model="res.partner">
|
||||
<field name="name">Provident Fund Register</field>
|
||||
<field name="supplier">1</field>
|
||||
<field eval="0" name="customer"/>
|
||||
</record>
|
||||
|
||||
<record id="hr_professional_tax_register" model="res.partner">
|
||||
<field name="name">Professional Tax Register</field>
|
||||
<field name="supplier">1</field>
|
||||
<field eval="0" name="customer"/>
|
||||
</record>
|
||||
|
||||
<record id="hr_meal_voucher_register" model="res.partner">
|
||||
<field name="name">Meal Voucher Register</field>
|
||||
<field name="supplier">1</field>
|
||||
<field eval="0" name="customer"/>
|
||||
</record>
|
||||
|
||||
<!-- Salary Structure -->
|
||||
<record id="structure_002" model="hr.payroll.structure">
|
||||
<field name="name">Marketing Executive for Gilles Gravie</field>
|
||||
<field name="type_id" ref="structure_type_employee"/>
|
||||
<field name="regular_pay" eval="True"/>
|
||||
</record>
|
||||
|
||||
<!-- Salary Rules for Marketing Executive for Gilles Gravie-->
|
||||
|
||||
<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.ALW"/>
|
||||
<field name="partner_id" ref="hr_houserent_register"/>
|
||||
<field name="name">House Rent Allowance</field>
|
||||
<field name="sequence" eval="5"/>
|
||||
<field name="struct_id" ref="structure_002"/>
|
||||
</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.ALW"/>
|
||||
<field name="name">Conveyance Allowance</field>
|
||||
<field name="sequence" eval="10"/>
|
||||
<field name="struct_id" ref="structure_002"/>
|
||||
</record>
|
||||
|
||||
<record id="hr_salary_rule_ca_gravie" model="hr.salary.rule">
|
||||
<field name="amount_select">fix</field>
|
||||
<field name="amount_fix" eval="600.0"/>
|
||||
<field name="code">CAGG</field>
|
||||
<field name="category_id" ref="hr_payroll.ALW"/>
|
||||
<field name="name">Conveyance Allowance For Gravie</field>
|
||||
<field name="sequence" eval="15"/>
|
||||
<field name="struct_id" ref="structure_002"/>
|
||||
</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.ALW"/>
|
||||
<field name="partner_id" ref="hr_meal_voucher_register"/>
|
||||
<field name="name">Meal Voucher</field>
|
||||
<field name="sequence" eval="16"/>
|
||||
<field name="struct_id" ref="structure_002"/>
|
||||
</record>
|
||||
|
||||
<record id="hr_salary_rule_providentfund1" model="hr.salary.rule">
|
||||
<field name="amount_select">percentage</field>
|
||||
<field name="sequence" eval="120"/>
|
||||
<field name="amount_percentage" eval="-12.5"/>
|
||||
<field name="amount_percentage_base">contract.wage</field>
|
||||
<field name="code">PF</field>
|
||||
<field name="category_id" ref="hr_payroll.DED"/>
|
||||
<field name="partner_id" ref="hr_provident_fund_register"/>
|
||||
<field name="name">Provident Fund</field>
|
||||
<field name="struct_id" ref="structure_002"/>
|
||||
</record>
|
||||
|
||||
<record id="hr_salary_rule_professionaltax1" model="hr.salary.rule">
|
||||
<field name="amount_select">fix</field>
|
||||
<field name="sequence" eval="150"/>
|
||||
<field name="amount_fix" eval="-200.0"/>
|
||||
<field name="code">PT</field>
|
||||
<field name="category_id" ref="hr_payroll.DED"/>
|
||||
<field name="partner_id" ref="hr_professional_tax_register"/>
|
||||
<field name="name">Professional Tax</field>
|
||||
<field name="struct_id" ref="structure_002"/>
|
||||
</record>
|
||||
|
||||
<!-- Employee -->
|
||||
|
||||
<record id="hr_employee_payroll" model="hr.employee">
|
||||
<field name="company_id" ref="base.main_company"/>
|
||||
<field eval="1" name="active"/>
|
||||
<field name="name">Roger Scott</field>
|
||||
<field name="work_location">Building 1, Second Floor</field>
|
||||
<field name="work_phone">+3282823500</field>
|
||||
<field name="image" type="base64" file="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="date_start" eval="time.strftime('%Y-%m')+'-1'"/>
|
||||
<field name="date_end" eval="time.strftime('%Y')+'-12-31'"/>
|
||||
<field name="structure_type_id" ref="hr_payroll.structure_type_employee"/>
|
||||
<field name="employee_id" ref="hr_employee_payroll"/>
|
||||
<field name="notes">Default contract for marketing executives</field>
|
||||
<field eval="4000.0" name="wage"/>
|
||||
<field name="state">open</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_contract_gilles_gravie" model="hr.contract">
|
||||
<field name="name">Contract For Gilles Gravie</field>
|
||||
<field name="date_start" eval="time.strftime('%Y-%m')+'-1'"/>
|
||||
<field name="date_end" eval="time.strftime('%Y')+'-12-31'"/>
|
||||
<field name="structure_type_id" ref="hr_payroll.structure_type_employee"/>
|
||||
<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>
|
||||
|
||||
<record id="hr_contract_admin" model="hr.contract">
|
||||
<field name="name">Contract For Mitchell Admin</field>
|
||||
<field name="date_start" eval="time.strftime('%Y-%m')+'-1'"/>
|
||||
<field name="date_end" eval="time.strftime('%Y')+'-12-31'"/>
|
||||
<field name="structure_type_id" ref="hr_payroll.structure_type_employee"/>
|
||||
<field name="employee_id" ref="hr.employee_admin"/>
|
||||
<field name="notes">This is Mitchell Admin's contract</field>
|
||||
<field eval="5500.0" name="wage"/>
|
||||
<field name="state">open</field>
|
||||
</record>
|
||||
|
||||
<!-- Work entries -->
|
||||
<record id="work_entry_type_home_working" model="hr.work.entry.type">
|
||||
<field name="name">Home Working</field>
|
||||
<field name="code">WORK200</field>
|
||||
<field name="color">2</field>
|
||||
</record>
|
||||
|
||||
<record id="work_entry_type_extra_hours" model="hr.work.entry.type">
|
||||
<field name="name">Extra hours</field>
|
||||
<field name="code">WORK300</field>
|
||||
<field name="color">8</field>
|
||||
</record>
|
||||
|
||||
<record id="work_entry_type_long_leave" model="hr.work.entry.type">
|
||||
<field name="name">Long Term Time Off</field>
|
||||
<field name="code">LEAVE200</field>
|
||||
<field name="is_leave">True</field>
|
||||
<field name="color">4</field>
|
||||
</record>
|
||||
|
||||
<record id="work_entry_type_unpaid_leave" model="hr.work.entry.type">
|
||||
<field name="name">Unpaid Time Off</field>
|
||||
<field name="code">LEAVE300</field>
|
||||
<field name="is_leave">True</field>
|
||||
<field name="color">5</field>
|
||||
</record>
|
||||
|
||||
<record id="work_entry_type_sick_leave" model="hr.work.entry.type">
|
||||
<field name="name">Sick Time Off</field>
|
||||
<field name="code">LEAVE400</field>
|
||||
<field name="is_leave">True</field>
|
||||
<field name="color">6</field>
|
||||
</record>
|
||||
|
||||
<!-- add work_entry type to leave type -->
|
||||
<record id="hr_holidays.holiday_status_sl" model="hr.leave.type">
|
||||
<field name="work_entry_type_id" ref="work_entry_type_sick_leave"></field>
|
||||
</record>
|
||||
|
||||
<record id="hr_holidays.holiday_status_unpaid" model="hr.leave.type">
|
||||
<field name="work_entry_type_id" ref="work_entry_type_unpaid_leave"></field>
|
||||
</record>
|
||||
|
||||
<record id="hr_holidays.holiday_status_hw" model="hr.leave.type">
|
||||
<field name="work_entry_type_id" ref="work_entry_type_home_working"></field>
|
||||
</record>
|
||||
|
||||
<record id="hr_holidays.holiday_status_cl" model="hr.leave.type">
|
||||
<field name="work_entry_type_id" ref="work_entry_type_leave"></field>
|
||||
</record>
|
||||
|
||||
<record id="hr_holidays.holiday_status_comp" model="hr.leave.type">
|
||||
<field name="work_entry_type_id" ref="work_entry_type_leave"></field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -1,13 +0,0 @@
|
||||
<?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
@@ -1,12 +0,0 @@
|
||||
#-*- coding:utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import browsable_object
|
||||
from . import hr_contract
|
||||
from . import hr_employee
|
||||
from . import res_config_settings
|
||||
from . import hr_salary_rule
|
||||
from . import hr_payslip
|
||||
from . import hr_work_entry
|
||||
from . import hr_leave
|
||||
from . import resource
|
||||
@@ -1,62 +0,0 @@
|
||||
#-*- coding:utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields
|
||||
|
||||
|
||||
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
|
||||
@@ -1,45 +0,0 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
from datetime import date
|
||||
|
||||
|
||||
class HrContract(models.Model):
|
||||
_inherit = 'hr.contract'
|
||||
_description = 'Employee Contract'
|
||||
|
||||
structure_type_id = fields.Many2one('hr.payroll.structure.type', string="Salary Structure Type")
|
||||
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.")
|
||||
hours_per_week = fields.Float(related='resource_calendar_id.hours_per_week')
|
||||
full_time_required_hours = fields.Float(related='resource_calendar_id.full_time_required_hours')
|
||||
is_fulltime = fields.Boolean(related='resource_calendar_id.is_fulltime')
|
||||
|
||||
@api.constrains('date_start', 'date_end', 'state')
|
||||
def _check_contracts(self):
|
||||
self._get_leaves()._check_contracts()
|
||||
|
||||
@api.onchange('structure_type_id')
|
||||
def _onchange_structure_type_id(self):
|
||||
if self.structure_type_id.default_schedule_pay:
|
||||
self.schedule_pay = self.structure_type_id.default_schedule_pay
|
||||
if self.structure_type_id.default_resource_calendar_id:
|
||||
self.resource_calendar_id = self.structure_type_id.default_resource_calendar_id
|
||||
|
||||
@api.multi
|
||||
def _get_leaves(self):
|
||||
return self.env['hr.leave'].search([
|
||||
('employee_id', 'in', self.mapped('employee_id.id')),
|
||||
('date_from', '<=', max([end or date.max for end in self.mapped('date_end')])),
|
||||
('date_to', '>=', min(self.mapped('date_start'))),
|
||||
])
|
||||
@@ -1,98 +0,0 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import datetime
|
||||
import pytz
|
||||
|
||||
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="hr_payroll.group_hr_payroll_user")
|
||||
registration_number = fields.Char('Registration Number of the Employee', copy=False)
|
||||
|
||||
_sql_constraints = [
|
||||
('unique_registration_number', 'UNIQUE(registration_number, company_id)', 'No duplication of registration numbers is allowed')
|
||||
]
|
||||
|
||||
def _compute_payslip_count(self):
|
||||
for employee in self:
|
||||
employee.payslip_count = len(employee.slip_ids)
|
||||
|
||||
def has_non_validated_work_entries(self, date_from, date_to):
|
||||
return bool(self.env['hr.work.entry'].search_count([
|
||||
('employee_id', 'in', self.ids),
|
||||
('date_start', '<=', date_to),
|
||||
('date_stop', '>=', date_from),
|
||||
('state', 'in', ['draft', 'confirmed'])
|
||||
]))
|
||||
|
||||
def write(self, vals):
|
||||
res = super(HrEmployee, self).write(vals)
|
||||
if vals.get('contract_id'):
|
||||
for employee in self:
|
||||
employee.resource_calendar_id.transfer_leaves_to(employee.contract_id.resource_calendar_id, employee.resource_id)
|
||||
employee.resource_calendar_id = employee.contract_id.resource_calendar_id
|
||||
return res
|
||||
|
||||
@api.model
|
||||
def generate_work_entry(self, date_start, date_stop):
|
||||
def _format_datetime(date):
|
||||
fmt = '%Y-%m-%d %H:%M:%S'
|
||||
date = datetime.strptime(date, fmt) if isinstance(date, str) else date
|
||||
return date.replace(tzinfo=pytz.utc) if not date.tzinfo else date
|
||||
|
||||
attendance_type = self.env.ref('hr_payroll.work_entry_type_attendance')
|
||||
vals_list = []
|
||||
|
||||
date_start = _format_datetime(date_start)
|
||||
date_stop = _format_datetime(date_stop)
|
||||
|
||||
current_contracts = self.env['hr.employee']._get_all_contracts(date_start, date_stop, states=['open', 'pending', 'close'])
|
||||
current_employees = current_contracts.mapped('employee_id')
|
||||
mapped_data = dict.fromkeys(current_employees, self.env['hr.contract'])
|
||||
|
||||
for contract in current_contracts:
|
||||
mapped_data[contract.employee_id] |= contract
|
||||
|
||||
for employee, contracts in mapped_data.items():
|
||||
|
||||
# Approved leaves
|
||||
emp_leaves = employee.resource_calendar_id.leave_ids.filtered(
|
||||
lambda r:
|
||||
r.resource_id == employee.resource_id and
|
||||
r.date_from.replace(tzinfo=pytz.utc) <= date_stop and
|
||||
r.date_to.replace(tzinfo=pytz.utc) >= date_start
|
||||
)
|
||||
global_leaves = employee.resource_calendar_id.global_leave_ids
|
||||
|
||||
employee_leaves = (emp_leaves | global_leaves).mapped('holiday_id')
|
||||
vals_list.extend(employee_leaves._get_work_entries_values())
|
||||
|
||||
for contract in contracts:
|
||||
|
||||
date_start_work_entries = max(date_start, datetime.combine(contract.date_start, datetime.min.time()).replace(tzinfo=pytz.utc))
|
||||
date_stop_work_entries = min(date_stop, datetime.combine(contract.date_end or datetime.max.date(), datetime.max.time()).replace(tzinfo=pytz.utc))
|
||||
|
||||
calendar = contract.resource_calendar_id
|
||||
resource = employee.resource_id
|
||||
attendances = calendar._work_intervals(date_start_work_entries, date_stop_work_entries, resource=resource)
|
||||
# Attendances
|
||||
for interval in attendances:
|
||||
work_entry_type_id = interval[2].mapped('work_entry_type_id')[:1] or attendance_type
|
||||
vals_list += [{
|
||||
'name': "%s: %s" % (work_entry_type_id.name, employee.name),
|
||||
'date_start': interval[0].astimezone(pytz.utc),
|
||||
'date_stop': interval[1].astimezone(pytz.utc),
|
||||
'work_entry_type_id': work_entry_type_id.id,
|
||||
'employee_id': employee.id,
|
||||
'contract_id': contract.id,
|
||||
'state': 'confirmed',
|
||||
}]
|
||||
|
||||
new_work_entries = self.env['hr.work.entry']._safe_duplicate_create(vals_list, date_start, date_stop)
|
||||
new_work_entries._compute_conflicts_leaves_to_approve()
|
||||
@@ -1,180 +0,0 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
import math
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from datetime import datetime, date
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.addons.resource.models.resource import Intervals
|
||||
from odoo.addons.resource.models.resource import HOURS_PER_DAY
|
||||
|
||||
class HrLeaveType(models.Model):
|
||||
_inherit = 'hr.leave.type'
|
||||
|
||||
work_entry_type_id = fields.Many2one('hr.work.entry.type', string='Work Entry Type')
|
||||
|
||||
|
||||
class HrLeave(models.Model):
|
||||
_inherit = 'hr.leave'
|
||||
|
||||
@api.multi
|
||||
def _get_work_entries_values(self):
|
||||
vals_list = []
|
||||
for leave in self:
|
||||
contract = leave.employee_id._get_contracts(leave.date_from, leave.date_to, states=['open', 'pending', 'close'])
|
||||
start = max(leave.date_from, datetime.combine(contract.date_start, datetime.min.time()))
|
||||
end = min(leave.date_to, datetime.combine(contract.date_end or date.max, datetime.max.time()))
|
||||
work_entry_type = leave.holiday_status_id.work_entry_type_id
|
||||
vals_list += [{
|
||||
'name': "%s%s" % (work_entry_type.name + ": " if work_entry_type else "", leave.employee_id.name),
|
||||
'date_start': start,
|
||||
'date_stop': end,
|
||||
'work_entry_type_id': work_entry_type.id,
|
||||
'display_warning': not bool(work_entry_type),
|
||||
'employee_id': leave.employee_id.id,
|
||||
'leave_id': leave.id,
|
||||
'state': 'confirmed',
|
||||
'contract_id': contract.id,
|
||||
}]
|
||||
return vals_list
|
||||
|
||||
@api.multi
|
||||
def _create_resource_leave(self):
|
||||
"""
|
||||
Add a resource leave in calendars of contracts running at the same period.
|
||||
This is needed in order to compute the correct number of hours/days of the leave
|
||||
according to the contract's calender.
|
||||
"""
|
||||
resource_leaves = super(HrLeave, self)._create_resource_leave()
|
||||
for resource_leave in resource_leaves:
|
||||
resource_leave.work_entry_type_id = resource_leave.holiday_id.holiday_status_id.work_entry_type_id.id
|
||||
|
||||
resource_leave_values = []
|
||||
|
||||
for leave in self.filtered(lambda l: l.employee_id):
|
||||
|
||||
contract = leave.employee_id.sudo()._get_contracts(leave.date_from, leave.date_to, states=['open', 'pending', 'close'])
|
||||
if contract and contract.resource_calendar_id != leave.employee_id.resource_calendar_id:
|
||||
resource_leave_values += [{
|
||||
'name': leave.name,
|
||||
'holiday_id': leave.id,
|
||||
'resource_id': leave.employee_id.resource_id.id,
|
||||
'work_entry_type_id': leave.holiday_status_id.work_entry_type_id.id,
|
||||
'time_type': leave.holiday_status_id.time_type,
|
||||
'date_from': max(leave.date_from, datetime.combine(contract.date_start, datetime.min.time())),
|
||||
'date_to': min(leave.date_to, datetime.combine(contract.date_end or date.max, datetime.max.time())),
|
||||
'calendar_id': contract.resource_calendar_id.id,
|
||||
}]
|
||||
|
||||
return resource_leaves | self.env['resource.calendar.leaves'].create(resource_leave_values)
|
||||
|
||||
@api.constrains('date_from', 'date_to')
|
||||
def _check_contracts(self):
|
||||
"""
|
||||
A leave cannot be set across multiple contracts.
|
||||
Note: a leave can be across multiple contracts despite this constraint.
|
||||
It happens if a leave is correctly created (not accross multiple contracts) but
|
||||
contracts are later modifed/created in the middle of the leave.
|
||||
"""
|
||||
for holiday in self:
|
||||
domain = [
|
||||
('employee_id', '=', holiday.employee_id.id),
|
||||
('date_start', '<=', holiday.date_to),
|
||||
('state', 'not in', ['draft', 'cancel']),
|
||||
'|',
|
||||
('date_end', '>=', holiday.date_from),
|
||||
('date_end', '=', False),
|
||||
]
|
||||
nbr_contracts = self.env['hr.contract'].sudo().search_count(domain)
|
||||
if nbr_contracts > 1:
|
||||
raise ValidationError(_('A leave cannot be set across multiple contracts.'))
|
||||
|
||||
@api.multi
|
||||
def _cancel_work_entry_conflict(self):
|
||||
"""
|
||||
Unlink any work_entry linked to a leave in self.
|
||||
Re-create new work_entries where the leaves do not cover the full range of the deleted work_entries.
|
||||
Create a leave work_entry for each leave in self.
|
||||
Return True if one or more work_entries are unlinked.
|
||||
e.g.:
|
||||
|---------------- work_entry ----------------|
|
||||
|------ leave ------|
|
||||
||
|
||||
vv
|
||||
|-benef-|---work_entry leave---|----work_entry---|
|
||||
"""
|
||||
work_entries = self.env['hr.work.entry'].search([('leave_id', 'in', self.ids)])
|
||||
if work_entries:
|
||||
vals_list = self._get_work_entries_values()
|
||||
# create new work_entries where the leave does not cover the full work_entry
|
||||
work_entries_intervals = Intervals(intervals=[(b.date_start, b.date_stop, b) for b in work_entries])
|
||||
leave_intervals = Intervals(intervals=[(l.date_from, l.date_to, l) for l in self])
|
||||
remaining_work_entries = work_entries_intervals - leave_intervals
|
||||
|
||||
for interval in remaining_work_entries:
|
||||
work_entry = interval[2]
|
||||
leave = work_entry.leave_id
|
||||
work_entry_type = work_entry.work_entry_type_id
|
||||
employee = work_entry.employee_id
|
||||
|
||||
work_entry_start = interval[0] + relativedelta(seconds=1) if leave.date_to == interval[0] else interval[0]
|
||||
work_entry_stop = interval[1] - relativedelta(seconds=1) if leave.date_from == interval[1] else interval[1]
|
||||
|
||||
vals_list += [{
|
||||
'name': "%s: %s" % (work_entry_type.name, employee.name),
|
||||
'date_start': work_entry_start,
|
||||
'date_stop': work_entry_stop,
|
||||
'work_entry_type_id': work_entry_type.id,
|
||||
'contract_id': work_entry.contract_id.id,
|
||||
'employee_id': employee.id,
|
||||
'state': 'confirmed',
|
||||
}]
|
||||
|
||||
date_start = min(work_entries.mapped('date_start'))
|
||||
date_stop = max(work_entries.mapped('date_stop'))
|
||||
self.env['hr.work.entry']._safe_duplicate_create(vals_list, date_start, date_stop)
|
||||
work_entries.unlink()
|
||||
return True
|
||||
return False
|
||||
|
||||
@api.multi
|
||||
def action_validate(self):
|
||||
super(HrLeave, self).action_validate()
|
||||
self.sudo()._cancel_work_entry_conflict() # delete preexisting conflicting work_entries
|
||||
return True
|
||||
|
||||
@api.multi
|
||||
def action_refuse(self):
|
||||
super(HrLeave, self).action_refuse()
|
||||
work_entries = self.env['hr.work.entry'].sudo().search([('leave_id', 'in', self.ids)])
|
||||
work_entries.write({'display_warning': False, 'active': False})
|
||||
return True
|
||||
|
||||
def _get_number_of_days(self, date_from, date_to, employee_id):
|
||||
""" If an employee is currently working full time but requests a leave next month
|
||||
where he has a new contract working only 3 days/week. This should be taken into
|
||||
account when computing the number of days for the leave (2 weeks leave = 6 days).
|
||||
Override this method to get number of days according to the contract's calendar
|
||||
at the time of the leave.
|
||||
"""
|
||||
days = super(HrLeave, self)._get_number_of_days(date_from, date_to, employee_id)
|
||||
if employee_id:
|
||||
employee = self.env['hr.employee'].browse(employee_id)
|
||||
# Use sudo otherwise base users can't compute number of days
|
||||
contracts = employee.sudo()._get_contracts(date_from, date_to, states=['incoming', 'open', 'pending'])
|
||||
calendar = contracts[:1].resource_calendar_id if contracts else None # Note: if len(contracts)>1, the leave creation will crash because of unicity constaint
|
||||
return employee._get_work_days_data(date_from, date_to, calendar=calendar)['days']
|
||||
|
||||
return days
|
||||
|
||||
@api.multi
|
||||
@api.depends('number_of_days')
|
||||
def _compute_number_of_hours_display(self):
|
||||
""" Override for the same reason as _get_number_of_days()"""
|
||||
super(HrLeave, self)._compute_number_of_hours_display()
|
||||
for holiday in self:
|
||||
if holiday.date_from and holiday.date_to:
|
||||
contracts = holiday.employee_id.sudo()._get_contracts(holiday.date_from, holiday.date_to, states=['incoming', 'open', 'pending'])
|
||||
contract_calendar = contracts[:1].resource_calendar_id if contracts else None
|
||||
calendar = contract_calendar or holiday.employee_id.resource_calendar_id or self.env.user.company_id.resource_calendar_id
|
||||
holiday.number_of_hours_display = holiday.number_of_days * (calendar.hours_per_day or HOURS_PER_DAY)
|
||||
@@ -1,442 +0,0 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import babel
|
||||
from datetime import date, datetime, time
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from pytz import timezone
|
||||
|
||||
from odoo import api, fields, models, tools, _
|
||||
from odoo.addons import decimal_precision as dp
|
||||
from odoo.addons.hr_payroll.models.browsable_object import BrowsableObject, InputLine, WorkedDays, Payslips
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.tools import float_round
|
||||
|
||||
|
||||
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)], 'verify': [('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, required=True,
|
||||
states={'draft': [('readonly', False)], 'verify': [('readonly', False)]})
|
||||
number = fields.Char(string='Reference', readonly=True, copy=False,
|
||||
states={'draft': [('readonly', False)], 'verify': [('readonly', False)]})
|
||||
employee_id = fields.Many2one('hr.employee', string='Employee', required=True, readonly=True,
|
||||
states={'draft': [('readonly', False)], 'verify': [('readonly', False)]})
|
||||
date_from = fields.Date(string='From', readonly=True, required=True,
|
||||
default=lambda self: fields.Date.to_string(date.today().replace(day=1)), states={'draft': [('readonly', False)], 'verify': [('readonly', False)]})
|
||||
date_to = fields.Date(string='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)], 'verify': [('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)], 'verify': [('readonly', False)]})
|
||||
company_id = fields.Many2one('res.company', string='Company', readonly=True, copy=False, required=True,
|
||||
default=lambda self: self.env['res.company']._company_default_get(),
|
||||
states={'draft': [('readonly', False)], 'verify': [('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)], 'verify': [('readonly', False)]})
|
||||
input_line_ids = fields.One2many('hr.payslip.input', 'payslip_id', string='Payslip Inputs',
|
||||
readonly=True, states={'draft': [('readonly', False)], 'verify': [('readonly', False)]})
|
||||
paid = fields.Boolean(string='Made Payment Order ? ', readonly=True, copy=False,
|
||||
states={'draft': [('readonly', False)], 'verify': [('readonly', False)]})
|
||||
note = fields.Text(string='Internal Note', readonly=True, states={'draft': [('readonly', False)], 'verify': [('readonly', False)]})
|
||||
contract_id = fields.Many2one('hr.contract', string='Contract', readonly=True,
|
||||
states={'draft': [('readonly', False)], 'verify': [('readonly', False)]})
|
||||
credit_note = fields.Boolean(string='Credit Note', readonly=True,
|
||||
states={'draft': [('readonly', False)], 'verify': [('readonly', False)]},
|
||||
help="Indicates this payslip has a refund of another")
|
||||
payslip_run_id = fields.Many2one('hr.payslip.run', string='Batche Name', readonly=True,
|
||||
copy=False, states={'draft': [('readonly', False)], 'verify': [('readonly', False)]}, ondelete='cascade')
|
||||
compute_date = fields.Date('Computed On')
|
||||
|
||||
basic_wage = fields.Monetary(compute='_compute_basic_net')
|
||||
net_wage = fields.Monetary(compute='_compute_basic_net')
|
||||
currency_id = fields.Many2one(related='contract_id.currency_id')
|
||||
|
||||
@api.multi
|
||||
def _compute_basic_net(self):
|
||||
for payslip in self:
|
||||
payslip.basic_wage = payslip.get_salary_line_total('BASIC')
|
||||
payslip.net_wage = payslip.get_salary_line_total('NET')
|
||||
|
||||
@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'."))
|
||||
|
||||
@api.multi
|
||||
def action_payslip_draft(self):
|
||||
return self.write({'state': 'draft'})
|
||||
|
||||
@api.multi
|
||||
def action_payslip_done(self):
|
||||
self.compute_sheet()
|
||||
return self.write({'state': 'done'})
|
||||
|
||||
@api.multi
|
||||
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'})
|
||||
|
||||
@api.multi
|
||||
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.view_hr_payslip_form', False)
|
||||
treeview_ref = self.env.ref('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': [(treeview_ref and treeview_ref.id or False, 'tree'), (formview_ref and formview_ref.id or False, 'form')],
|
||||
'context': {}
|
||||
}
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
res = super(HrPayslip, self).create(vals)
|
||||
if not res.payslip_run_id:
|
||||
self.env['hr.payslip.run'].create({
|
||||
'name': res.name,
|
||||
'date_start': res.date_from,
|
||||
'date_end': res.date_to,
|
||||
'slip_ids': [(4, res.id)],
|
||||
'state': 'close',
|
||||
})
|
||||
return res
|
||||
|
||||
@api.multi
|
||||
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()
|
||||
|
||||
@api.multi
|
||||
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()
|
||||
lines = [(0, 0, line) for line in payslip._get_payslip_lines()]
|
||||
payslip.write({'line_ids': lines, 'number': number, 'state': 'verify', 'compute_date': fields.Date.today()})
|
||||
return True
|
||||
|
||||
@api.model
|
||||
def get_worked_day_lines(self, contract, date_from, date_to):
|
||||
"""
|
||||
@param contract: Browse record of contracts
|
||||
@return: returns a list of dict containing the worked days 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
|
||||
if contract.resource_calendar_id:
|
||||
day_from = datetime.combine(max(fields.Date.from_string(date_from), contract.date_start), time.min)
|
||||
day_to = datetime.combine(min(fields.Date.from_string(date_to), contract.date_end or date.max), time.max)
|
||||
|
||||
calendar = contract.resource_calendar_id
|
||||
|
||||
work_entry_types = self.env['hr.work.entry.type'].search([('code', '!=', False)])
|
||||
for work_entry_type in work_entry_types:
|
||||
hours = contract.employee_id._get_work_entry_days_data(work_entry_type, day_from, day_to, calendar=calendar)['hours']
|
||||
if hours:
|
||||
line = {
|
||||
'name': work_entry_type.name,
|
||||
'sequence': work_entry_type.sequence,
|
||||
'code': work_entry_type.code,
|
||||
'number_of_days': hours / calendar.hours_per_day, # n_days returned by work_entry_days_data doesn't make sense for extra work
|
||||
'number_of_hours': hours,
|
||||
}
|
||||
res.append(line)
|
||||
|
||||
return res
|
||||
|
||||
def _get_base_local_dict(self):
|
||||
return {
|
||||
'float_round': float_round
|
||||
}
|
||||
|
||||
@api.model
|
||||
def _get_payslip_lines(self):
|
||||
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] = localdict['categories'].dict.get(category.code, 0) + amount
|
||||
return localdict
|
||||
|
||||
self.ensure_one()
|
||||
result = {}
|
||||
rules_dict = {}
|
||||
worked_days_dict = {line.code: line for line in self.worked_days_line_ids if line.code}
|
||||
inputs_dict = {line.code: line for line in self.input_line_ids if line.code}
|
||||
|
||||
employee = self.employee_id
|
||||
contract = self.contract_id
|
||||
|
||||
localdict = {
|
||||
**self._get_base_local_dict(),
|
||||
**{
|
||||
'categories': BrowsableObject(employee.id, {}, self.env),
|
||||
'rules': BrowsableObject(employee.id, rules_dict, self.env),
|
||||
'payslip': Payslips(employee.id, self, self.env),
|
||||
'worked_days': WorkedDays(employee.id, worked_days_dict, self.env),
|
||||
'inputs': InputLine(employee.id, inputs_dict, self.env),
|
||||
'employee': employee,
|
||||
'contract': contract
|
||||
}
|
||||
}
|
||||
for rule in sorted(self.struct_id.rule_ids, key=lambda x: x.sequence):
|
||||
localdict.update({
|
||||
'result': None,
|
||||
'result_qty': 1.0,
|
||||
'result_rate': 100})
|
||||
if rule._satisfy_condition(localdict):
|
||||
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[rule.code] = {
|
||||
'sequence': rule.sequence,
|
||||
'code': rule.code,
|
||||
'name': rule.name,
|
||||
'note': rule.note,
|
||||
'salary_rule_id': rule.id,
|
||||
'contract_id': contract.id,
|
||||
'employee_id': employee.id,
|
||||
'amount': amount,
|
||||
'quantity': qty,
|
||||
'rate': rate,
|
||||
}
|
||||
return result.values()
|
||||
|
||||
@api.onchange('employee_id', 'contract_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
|
||||
|
||||
self.company_id = employee.company_id
|
||||
|
||||
if not self.contract_id: # Add a default contract if not already defined
|
||||
contracts = employee._get_contracts(date_from, date_to)
|
||||
|
||||
if not contracts or not contracts[0].structure_type_id.default_struct_id:
|
||||
return
|
||||
self.contract_id = contracts[0]
|
||||
self.struct_id = contracts[0].structure_type_id.default_struct_id
|
||||
|
||||
|
||||
#computation of the salary worked days
|
||||
worked_days_line_ids = self.get_worked_day_lines(self.contract_id, 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
|
||||
|
||||
@api.onchange('struct_id')
|
||||
def _onchange_struct_id(self):
|
||||
ttyme = datetime.combine(fields.Date.from_string(self.date_from), time.min)
|
||||
locale = self.env.context.get('lang') or 'en_US'
|
||||
payslip_name = self.struct_id.payslip_name or _('Salary Slip')
|
||||
self.name = '%s - %s - %s' % (payslip_name, self.employee_id.name, tools.ustr(babel.dates.format_date(date=ttyme, format='MMMM-y', locale=locale)))
|
||||
|
||||
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
|
||||
|
||||
@api.multi
|
||||
def action_print_payslip(self):
|
||||
return {
|
||||
'name': 'Payslip',
|
||||
'type': 'ir.actions.act_url',
|
||||
'url': '/print/payslips?list_ids=%(list_ids)s' % {'list_ids': ','.join(str(x) for x in self.ids)},
|
||||
}
|
||||
|
||||
|
||||
class HrPayslipLine(models.Model):
|
||||
_name = 'hr.payslip.line'
|
||||
_description = 'Payslip Line'
|
||||
_order = 'contract_id, sequence, code'
|
||||
|
||||
name = fields.Char(required=True, translate=True)
|
||||
note = fields.Text(string='Description')
|
||||
sequence = fields.Integer(required=True, index=True, default=5,
|
||||
help='Use to arrange calculation sequence')
|
||||
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.")
|
||||
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)
|
||||
contract_id = fields.Many2one('hr.contract', string='Contract', required=True, index=True)
|
||||
employee_id = fields.Many2one('hr.employee', string='Employee', required=True)
|
||||
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', digits=dp.get_precision('Payroll'), store=True)
|
||||
|
||||
amount_select = fields.Selection(related='salary_rule_id.amount_select', readonly=True)
|
||||
amount_fix = fields.Float(related='salary_rule_id.amount_fix', readonly=True)
|
||||
amount_percentage = fields.Float(related='salary_rule_id.amount_percentage', readonly=True)
|
||||
appears_on_payslip = fields.Boolean(related='salary_rule_id.appears_on_payslip', readonly=True)
|
||||
category_id = fields.Many2one(related='salary_rule_id.category_id', readonly=True, store=True)
|
||||
partner_id = fields.Many2one(related='salary_rule_id.partner_id', readonly=True, store=True)
|
||||
|
||||
date_from = fields.Date(string='From', related="slip_id.date_from", store=True)
|
||||
date_to = fields.Date(string='To', related="slip_id.date_to", store=True)
|
||||
company_id = fields.Many2one(related='slip_id.company_id')
|
||||
|
||||
@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(related='payslip_id.contract_id', string='Contract', required=True,
|
||||
help="The contract for which applied this worked days")
|
||||
|
||||
|
||||
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(related='payslip_id.contract_id', 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'),
|
||||
('verify', 'Verify'),
|
||||
('close', 'Done'),
|
||||
], 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.")
|
||||
payslip_count = fields.Integer(compute='_compute_payslip_count')
|
||||
company_id = fields.Many2one('res.company', string='Company', readonly=True, required=True,
|
||||
default=lambda self: self.env['res.company']._company_default_get())
|
||||
|
||||
def _compute_payslip_count(self):
|
||||
for payslip_run in self:
|
||||
payslip_run.payslip_count = len(self.slip_ids)
|
||||
|
||||
@api.multi
|
||||
def draft_payslip_run(self):
|
||||
return self.write({'state': 'draft'})
|
||||
|
||||
@api.multi
|
||||
def close_payslip_run(self):
|
||||
self.mapped('slip_ids').filtered(lambda payslip: payslip.state != 'done').action_payslip_done()
|
||||
return self.write({'state': 'close'})
|
||||
|
||||
def action_open_payslips(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "hr.payslip",
|
||||
"views": [[False, "tree"], [False, "form"]],
|
||||
"domain": [['id', 'in', self.slip_ids.ids]],
|
||||
"name": "Payslips",
|
||||
}
|
||||
|
||||
class ContributionRegisterReport(models.AbstractModel):
|
||||
_name = 'report.hr_payroll.contribution_register'
|
||||
_description = 'Model for Printing hr.payslip.line grouped by register'
|
||||
|
||||
def _get_report_values(self, docids, data):
|
||||
docs = []
|
||||
lines_data = {}
|
||||
lines_total = {}
|
||||
|
||||
for result in self.env['hr.payslip.line'].read_group([('id', 'in', docids)], ['partner_id', 'total', 'ids:array_agg(id)'], ['partner_id']):
|
||||
if result['partner_id']:
|
||||
docid = result['partner_id'][0]
|
||||
docs.append(docid)
|
||||
lines_data[docid] = self.env['hr.payslip.line'].browse(result['ids'])
|
||||
lines_total[docid] = result['total']
|
||||
|
||||
return {
|
||||
'docs': self.env['res.partner'].browse(docs),
|
||||
'data': data,
|
||||
'lines_data': lines_data,
|
||||
'lines_total': lines_total
|
||||
}
|
||||
@@ -1,230 +0,0 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.tools.safe_eval import safe_eval
|
||||
|
||||
from odoo.addons import decimal_precision as dp
|
||||
|
||||
class HrPayrollStructure(models.Model):
|
||||
_name = 'hr.payroll.structure'
|
||||
_description = 'Salary Structure'
|
||||
|
||||
@api.model
|
||||
def _get_default_report_id(self):
|
||||
return self.env.ref('hr_payroll.action_report_payslip', False)
|
||||
|
||||
@api.model
|
||||
def _get_default_rule_ids(self):
|
||||
return [
|
||||
(0, 0, {
|
||||
'name': 'Basic Salary',
|
||||
'sequence': 1,
|
||||
'code': 'BASIC',
|
||||
'category_id': self.env.ref('hr_payroll.BASIC').id,
|
||||
'condition_select': 'none',
|
||||
'amount_select': 'code',
|
||||
'amount_python_compute': 'result = contract.wage',
|
||||
}),
|
||||
(0, 0, {
|
||||
'name': 'Gross',
|
||||
'sequence': 100,
|
||||
'code': 'GROSS',
|
||||
'category_id': self.env.ref('hr_payroll.GROSS').id,
|
||||
'condition_select': 'none',
|
||||
'amount_select': 'code',
|
||||
'amount_python_compute': 'result = categories.BASIC + categories.ALW',
|
||||
}),
|
||||
(0, 0, {
|
||||
'name': 'Net Salary',
|
||||
'sequence': 200,
|
||||
'code': 'NET',
|
||||
'category_id': self.env.ref('hr_payroll.NET').id,
|
||||
'condition_select': 'none',
|
||||
'amount_select': 'code',
|
||||
'amount_python_compute': 'result = categories.BASIC + categories.ALW + categories.DED',
|
||||
})
|
||||
]
|
||||
|
||||
name = fields.Char(required=True)
|
||||
type_id = fields.Many2one(
|
||||
'hr.payroll.structure.type', required=True,
|
||||
default=lambda self: self.env.ref('hr_payroll.structure_type_employee', raise_if_not_found=False))
|
||||
country_id = fields.Many2one('res.country', string='Country', default=lambda self: self.env.user.company_id.country_id)
|
||||
note = fields.Text(string='Description')
|
||||
rule_ids = fields.One2many(
|
||||
'hr.salary.rule', 'struct_id',
|
||||
string='Salary Rules', default=_get_default_rule_ids)
|
||||
report_id = fields.Many2one('ir.actions.report',
|
||||
string="Report", domain="[('model','=','hr.payslip'),('report_type','=','qweb-pdf')]", default=_get_default_report_id)
|
||||
payslip_name = fields.Char(string="Payslip Name", translate=True,
|
||||
help="Name to be set on a payslip. Example: 'End of the year bonus'. If not set, the default value is 'Salary Slip'")
|
||||
regular_pay = fields.Boolean("Regular Pay", help="Check this option if this structure is the common one")
|
||||
|
||||
|
||||
class HrPayrollStructureType(models.Model):
|
||||
_name = 'hr.payroll.structure.type'
|
||||
_description = 'Salary Structure Type'
|
||||
|
||||
name = fields.Char('Structure Type')
|
||||
default_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='Default Scheduled Pay', default='monthly',
|
||||
help="Defines the frequency of the wage payment.")
|
||||
default_resource_calendar_id = fields.Many2one(
|
||||
'resource.calendar', 'Default Working Hours',
|
||||
default=lambda self: self.env.user.company_id.resource_calendar_id)
|
||||
struct_ids = fields.One2many('hr.payroll.structure', 'type_id', string="Structures")
|
||||
default_struct_id = fields.Many2one('hr.payroll.structure', compute='_compute_default_struct_id')
|
||||
|
||||
def _compute_default_struct_id(self):
|
||||
for structure_type in self:
|
||||
sorted_structures = sorted(structure_type.struct_ids, key=lambda struct: struct.regular_pay, reverse=True)
|
||||
structure_type.default_struct_id = sorted_structures[0] if sorted_structures else False
|
||||
|
||||
|
||||
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')
|
||||
|
||||
@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.")
|
||||
struct_id = fields.Many2one('hr.payroll.structure', string="Salary Structure", required=True)
|
||||
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.")
|
||||
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')
|
||||
partner_id = fields.Many2one('res.partner', string='Partner',
|
||||
help="Eventual third party involved in the salary payment of the employees.")
|
||||
note = fields.Text(string='Description')
|
||||
|
||||
@api.multi
|
||||
def _compute_rule(self, localdict):
|
||||
"""
|
||||
:param localdict: dictionary containing the current computation environment
|
||||
:return: returns a tuple (amount, qty, rate)
|
||||
:rtype: (float, float, float)
|
||||
"""
|
||||
self.ensure_one()
|
||||
if self.amount_select == 'fix':
|
||||
try:
|
||||
return self.amount_fix or 0.0, float(safe_eval(self.quantity, localdict)), 100.0
|
||||
except Exception as e:
|
||||
raise UserError(_('Wrong quantity defined for salary rule %s (%s).\nError: %s') % (self.name, self.code, e))
|
||||
if self.amount_select == 'percentage':
|
||||
try:
|
||||
return (float(safe_eval(self.amount_percentage_base, localdict)),
|
||||
float(safe_eval(self.quantity, localdict)),
|
||||
self.amount_percentage or 0.0)
|
||||
except Exception as e:
|
||||
raise UserError(_('Wrong percentage base or quantity defined for salary rule %s (%s).\nError: %s') % (self.name, self.code, e))
|
||||
else: # python code
|
||||
try:
|
||||
safe_eval(self.amount_python_compute or 0.0, localdict, mode='exec', nocopy=True)
|
||||
return float(localdict['result']), localdict.get('result_qty', 1.0), localdict.get('result_rate', 100.0)
|
||||
except Exception as e:
|
||||
raise UserError(_('Wrong python code defined for salary rule %s (%s).\nError: %s') % (self.name, self.code, e))
|
||||
|
||||
@api.multi
|
||||
def _satisfy_condition(self, localdict):
|
||||
self.ensure_one()
|
||||
if self.condition_select == 'none':
|
||||
return True
|
||||
if self.condition_select == 'range':
|
||||
try:
|
||||
result = safe_eval(self.condition_range, localdict)
|
||||
return self.condition_range_min <= result <= self.condition_range_max
|
||||
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 localdict.get('result', False)
|
||||
except:
|
||||
raise UserError(_('Wrong python condition defined for salary rule %s (%s).') % (self.name, self.code))
|
||||
@@ -1,333 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import itertools
|
||||
import pytz
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.addons.resource.models.resource import Intervals
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class HrWorkEnrty(models.Model):
|
||||
_name = 'hr.work.entry'
|
||||
_description = 'hr.work.entry'
|
||||
_order = 'display_warning desc,state,date_start'
|
||||
|
||||
name = fields.Char(required=True)
|
||||
active = fields.Boolean(default=True)
|
||||
employee_id = fields.Many2one('hr.employee', required=True, domain=[('contract_ids.state', 'in', ('open', 'pending'))])
|
||||
date_start = fields.Datetime(required=True, string='From')
|
||||
date_stop = fields.Datetime(string='To')
|
||||
duration = fields.Float(compute='_compute_duration', inverse='_inverse_duration', store=True, string="Period")
|
||||
contract_id = fields.Many2one('hr.contract', string="Contract", required=True)
|
||||
work_entry_type_id = fields.Many2one('hr.work.entry.type')
|
||||
color = fields.Integer(related='work_entry_type_id.color', readonly=True)
|
||||
state = fields.Selection([
|
||||
('draft', 'Draft'),
|
||||
('confirmed', 'Confirmed'),
|
||||
('validated', 'Validated'),
|
||||
('cancelled', 'Cancelled')
|
||||
], default='draft')
|
||||
display_warning = fields.Boolean(string="Error")
|
||||
leave_id = fields.Many2one('hr.leave', string='Leave')
|
||||
company_id = fields.Many2one('res.company', string='Company', readonly=True, required=True,
|
||||
default=lambda self: self.env['res.company']._company_default_get())
|
||||
|
||||
_sql_constraints = [
|
||||
('_unique', 'unique (date_start, date_stop, employee_id, work_entry_type_id)', "Work entry already exists for this attendance"),
|
||||
('_work_entry_has_end', 'check (date_stop IS NOT NULL OR duration <> 0)', 'Work entry must end. Please define an end date or a duration.'),
|
||||
('_work_entry_start_before_end', 'check (date_stop is null OR (date_stop > date_start))', 'Starting time should be before end time.')
|
||||
]
|
||||
|
||||
@api.onchange('duration')
|
||||
def _onchange_duration(self):
|
||||
self._inverse_duration()
|
||||
|
||||
def _get_duration(self, date_start, date_stop):
|
||||
if not date_start or not date_stop:
|
||||
return 0
|
||||
dt = date_stop - date_start
|
||||
return dt.days * 24 + dt.seconds / 3600 # Number of hours
|
||||
|
||||
@api.depends('date_stop', 'date_start')
|
||||
def _compute_duration(self):
|
||||
for work_entry in self:
|
||||
work_entry.duration = work_entry._get_duration(work_entry.date_start, work_entry.date_stop)
|
||||
|
||||
def _inverse_duration(self):
|
||||
for work_entry in self:
|
||||
if work_entry.date_start and work_entry.duration:
|
||||
work_entry.date_stop = work_entry.date_start + relativedelta(hours=work_entry.duration)
|
||||
|
||||
def write(self, vals):
|
||||
if 'state' in vals:
|
||||
if vals['state'] == 'draft':
|
||||
vals['active'] = True
|
||||
if vals['state'] == 'cancelled':
|
||||
vals['active'] = False
|
||||
self.mapped('leave_id').action_refuse()
|
||||
return super(HrWorkEnrty, self).write(vals)
|
||||
|
||||
@api.multi
|
||||
def _check_if_error(self):
|
||||
if not self:
|
||||
return False
|
||||
undefined_type = self.filtered(lambda b: not b.work_entry_type_id)
|
||||
undefined_type.write({'display_warning': True})
|
||||
conflict = self._mark_conflicting_work_entries(min(self.mapped('date_start')), max(self.mapped('date_stop')))
|
||||
conflict_with_leaves = self._compute_conflicts_leaves_to_approve()
|
||||
return undefined_type or conflict or conflict_with_leaves
|
||||
|
||||
@api.model
|
||||
def _mark_conflicting_work_entries(self, start, stop):
|
||||
"""
|
||||
Set `display_warning` to True for overlapping work entries
|
||||
between two dates.
|
||||
Return True if overlapping work entries were detected.
|
||||
"""
|
||||
# Use the postgresql range type `tsrange` which is a range of timestamp
|
||||
# It supports the intersection operator (&&) useful to detect overlap.
|
||||
# use '()' to exlude the lower and upper bounds of the range.
|
||||
# Filter on date_start and date_stop (both indexed) in the EXISTS clause to
|
||||
# limit the resulting set size and fasten the query.
|
||||
query = """
|
||||
SELECT b1.id
|
||||
FROM hr_work_entry b1
|
||||
WHERE
|
||||
b1.date_start <= %s
|
||||
AND b1.date_stop >= %s
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM hr_work_entry b2
|
||||
WHERE
|
||||
b2.date_start <= %s
|
||||
AND b2.date_stop >= %s
|
||||
AND tsrange(b1.date_start, b1.date_stop, '()') && tsrange(b2.date_start, b2.date_stop, '()')
|
||||
AND b1.id <> b2.id
|
||||
AND b1.employee_id = b2.employee_id
|
||||
);
|
||||
"""
|
||||
self.env.cr.execute(query, (stop, start, stop, start))
|
||||
conflicts = [res.get('id') for res in self.env.cr.dictfetchall()]
|
||||
self.browse(conflicts).write({
|
||||
'display_warning': True,
|
||||
})
|
||||
return bool(conflicts)
|
||||
|
||||
@api.multi
|
||||
def _compute_conflicts_leaves_to_approve(self):
|
||||
if not self:
|
||||
return False
|
||||
|
||||
query = """
|
||||
SELECT
|
||||
b.id AS work_entry_id,
|
||||
l.id AS leave_id
|
||||
FROM hr_work_entry b
|
||||
INNER JOIN hr_leave l ON b.employee_id = l.employee_id
|
||||
WHERE
|
||||
b.id IN %s AND
|
||||
l.date_from <= b.date_stop AND
|
||||
l.date_to >= b.date_start AND
|
||||
l.state IN ('confirm', 'validate1');
|
||||
"""
|
||||
self.env.cr.execute(query, [tuple(self.ids)])
|
||||
conflicts = self.env.cr.dictfetchall()
|
||||
for res in conflicts:
|
||||
self.browse(res.get('work_entry_id')).write({
|
||||
'display_warning': True,
|
||||
'leave_id': res.get('leave_id')
|
||||
})
|
||||
return bool(conflicts)
|
||||
|
||||
def _safe_duplicate_create(self, vals_list, date_start, date_stop):
|
||||
"""
|
||||
Create work_entries between date_start and date_stop according to vals_list.
|
||||
Skip the values in vals_list if a work_entry already exists for the given
|
||||
date_start, date_stop, employee_id, work_entry_type_id
|
||||
:return: new record id if it didn't exist.
|
||||
"""
|
||||
# The search_read should be fast as date_start and date_stop are indexed from the
|
||||
# unique sql constraint
|
||||
month_recs = self.search_read([('date_start', '>=', date_start), ('date_stop', '<=', date_stop)],
|
||||
['employee_id', 'date_start', 'date_stop', 'work_entry_type_id'])
|
||||
existing_entries = {(
|
||||
r['date_start'],
|
||||
r['date_stop'],
|
||||
r['employee_id'][0],
|
||||
r['work_entry_type_id'][0] if r['work_entry_type_id'] else False,
|
||||
) for r in month_recs}
|
||||
new_vals = [v for v in vals_list if (v['date_start'].replace(tzinfo=None), v['date_stop'].replace(tzinfo=None), v['employee_id'], v['work_entry_type_id']) not in existing_entries]
|
||||
# Remove duplicates from vals_list, shouldn't be necessary from saas-12.2
|
||||
unique_new_vals = set()
|
||||
for values in new_vals:
|
||||
unique_new_vals.add(tuple(values.items()))
|
||||
new_vals = [dict(values) for values in unique_new_vals]
|
||||
return self.create(new_vals)
|
||||
|
||||
def action_leave(self):
|
||||
leave = self.leave_id
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'view_type': 'form',
|
||||
'view_mode': 'form',
|
||||
'res_id': leave.id,
|
||||
'res_model': 'hr.leave',
|
||||
'views': [[False, 'form']],
|
||||
}
|
||||
|
||||
def _split_by_day(self):
|
||||
"""
|
||||
Split the work_entry by days and unlink the original work_entry.
|
||||
@return recordset
|
||||
"""
|
||||
def _split_range_by_day(start, end):
|
||||
days = []
|
||||
current_start = start
|
||||
current_end = start.replace(hour=23, minute=59, second=59)
|
||||
while current_end < end:
|
||||
days.append((current_start, current_end))
|
||||
current_start = current_end + relativedelta(seconds=1)
|
||||
current_end = current_end + relativedelta(days=1)
|
||||
|
||||
days.append((current_start, end))
|
||||
|
||||
# filter to avoid dummy intervals starting and ending at the same time
|
||||
return [(start, end) for start, end in days if start != end]
|
||||
|
||||
new_work_entries = self.env['hr.work.entry']
|
||||
work_entries_to_unlink = self.env['hr.work.entry']
|
||||
|
||||
for work_entry in self:
|
||||
if work_entry.date_start.date() == work_entry.date_stop.date():
|
||||
new_work_entries |= work_entry
|
||||
else:
|
||||
tz = pytz.timezone(work_entry.employee_id.tz)
|
||||
work_entry_start, work_entry_stop = tz.localize(work_entry.date_start), tz.localize(work_entry.date_stop)
|
||||
values = {
|
||||
'name': work_entry.name,
|
||||
'employee_id': work_entry.employee_id.id,
|
||||
'work_entry_type_id': work_entry.work_entry_type_id.id,
|
||||
'contract_id': work_entry.contract_id.id,
|
||||
}
|
||||
work_entry_state = work_entry.state
|
||||
work_entries_to_unlink |= work_entry
|
||||
for start, stop in _split_range_by_day(work_entry_start, work_entry_stop):
|
||||
values['date_start'] = start.astimezone(pytz.utc)
|
||||
values['date_stop'] = stop.astimezone(pytz.utc)
|
||||
new_work_entry = self.create(values)
|
||||
# Write the state after the creation due to the ir.rule on work_entry state
|
||||
new_work_entry.state = work_entry_state
|
||||
new_work_entries |= new_work_entry
|
||||
|
||||
work_entries_to_unlink.unlink()
|
||||
return new_work_entries
|
||||
|
||||
@api.multi
|
||||
def _duplicate_to_calendar(self):
|
||||
"""
|
||||
Duplicate data to keep the complexity in work_entry and not mess up payroll, etc.
|
||||
"""
|
||||
attendance_type = self.env.ref('hr_payroll.work_entry_type_attendance')
|
||||
attendance_work_entries = self.filtered(lambda b:
|
||||
not b.work_entry_type_id.is_leave and
|
||||
# Normal work_entry are global to all employees -> avoid duplicating it
|
||||
not b.work_entry_type_id == attendance_type)
|
||||
leave_work_entries = self.filtered(lambda b: b.work_entry_type_id.is_leave)
|
||||
|
||||
work_entries_to_duplicate = self.env['hr.work.entry']
|
||||
for work_entry in attendance_work_entries:
|
||||
work_entry = work_entry._split_by_day()
|
||||
work_entries_to_duplicate |= work_entry
|
||||
|
||||
work_entries_to_duplicate._duplicate_to_calendar_attendance()
|
||||
leave_work_entries._duplicate_to_calendar_leave()
|
||||
|
||||
@api.multi
|
||||
def _duplicate_to_calendar_leave(self):
|
||||
vals_list = []
|
||||
for work_entry in self:
|
||||
if not work_entry.leave_id:
|
||||
vals_list += [{
|
||||
'name': work_entry.name,
|
||||
'date_from': work_entry.date_start,
|
||||
'date_to': work_entry.date_stop,
|
||||
'calendar_id': work_entry.employee_id.resource_calendar_id.id,
|
||||
'resource_id': work_entry.employee_id.resource_id.id,
|
||||
'work_entry_type_id': work_entry.work_entry_type_id.id,
|
||||
}]
|
||||
if vals_list:
|
||||
self.env['resource.calendar.leaves'].create(vals_list)
|
||||
|
||||
@api.multi
|
||||
def _duplicate_to_calendar_attendance(self):
|
||||
mapped_data = {
|
||||
work_entry: [
|
||||
pytz.utc.localize(work_entry.date_start).astimezone(pytz.timezone(work_entry.employee_id.tz)), # Start date
|
||||
pytz.utc.localize(work_entry.date_stop).astimezone(pytz.timezone(work_entry.employee_id.tz)) # End date
|
||||
] for work_entry in self
|
||||
}
|
||||
|
||||
if any(data[0].date() != data[1].date() for data in mapped_data.values()):
|
||||
raise ValidationError(_("You can't validate a work_entry that covers several days."))
|
||||
|
||||
vals_list = []
|
||||
for work_entry in self:
|
||||
start, end = mapped_data.get(work_entry)
|
||||
|
||||
vals_list += [{
|
||||
'name': work_entry.name,
|
||||
'dayofweek': str(start.weekday()),
|
||||
'date_from': start.date(),
|
||||
'date_to': end.date(),
|
||||
'hour_from': start.hour + start.minute / 60,
|
||||
'hour_to': end.hour + end.minute / 60,
|
||||
'calendar_id': work_entry.contract_id.resource_calendar_id.id,
|
||||
'day_period': 'morning' if end.hour <= 12 else 'afternoon',
|
||||
'resource_id': work_entry.employee_id.resource_id.id,
|
||||
'work_entry_type_id': work_entry.work_entry_type_id.id,
|
||||
}]
|
||||
self.env['resource.calendar.attendance'].create(vals_list)
|
||||
|
||||
@api.multi
|
||||
def action_validate(self):
|
||||
work_entries = self.filtered(lambda work_entry: work_entry.state != 'validated')
|
||||
work_entries.write({'display_warning': False})
|
||||
if not work_entries._check_if_error():
|
||||
work_entries.write({'state': 'validated'})
|
||||
work_entries._duplicate_to_calendar()
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class HrWorkEntryType(models.Model):
|
||||
_name = 'hr.work.entry.type'
|
||||
_description = 'hr.work.entry.type'
|
||||
|
||||
name = fields.Char(required=True)
|
||||
code = fields.Char()
|
||||
color = fields.Integer(default=1) # Will be used with the new calendar/kanban view
|
||||
sequence = fields.Integer(default=25)
|
||||
active = fields.Boolean(
|
||||
'Active', default=True,
|
||||
help="If the active field is set to false, it will allow you to hide the work entry type without removing it.")
|
||||
is_leave = fields.Boolean(default=False, string="Leave")
|
||||
leave_type_ids = fields.One2many('hr.leave.type', 'work_entry_type_id', string='Leave Type')
|
||||
|
||||
|
||||
class Contacts(models.Model):
|
||||
""" Personnal calendar filter """
|
||||
|
||||
_name = 'hr.user.work.entry.employee'
|
||||
_description = 'Work Entries Employees'
|
||||
|
||||
user_id = fields.Many2one('res.users', 'Me', required=True, default=lambda self: self.env.user)
|
||||
employee_id = fields.Many2one('hr.employee', 'Employee', required=True)
|
||||
active = fields.Boolean('Active', default=True)
|
||||
|
||||
_sql_constraints = [
|
||||
('user_id_employee_id_unique', 'UNIQUE(user_id,employee_id)', 'You cannot have twice the same employee.')
|
||||
]
|
||||
@@ -1,13 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
module_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')
|
||||
module_hr_payroll_account = fields.Boolean(string='Payroll with Accounting')
|
||||
@@ -1,108 +0,0 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from odoo.osv.expression import AND
|
||||
from odoo.tools import float_compare
|
||||
from datetime import datetime
|
||||
|
||||
from odoo import models, fields, api
|
||||
from odoo.addons.resource.models.resource_mixin import timezone_datetime
|
||||
|
||||
class ResourceCalendar(models.Model):
|
||||
_inherit = 'resource.calendar'
|
||||
|
||||
hours_per_week = fields.Float(compute="_compute_hours_per_week", string="Hours per Week")
|
||||
full_time_required_hours = fields.Float(string="Fulltime Hours", help="Number of hours to work to be considered as fulltime.")
|
||||
is_fulltime = fields.Boolean(compute='_compute_is_fulltime', string="Is Full Time")
|
||||
|
||||
# UI fields
|
||||
normal_attendance_ids = fields.One2many(
|
||||
'resource.calendar.attendance', 'calendar_id', 'Normal Working Time',
|
||||
domain=[('resource_id', '=', False)])
|
||||
|
||||
extra_attendance_ids = fields.One2many(
|
||||
'resource.calendar.attendance', 'calendar_id', 'Employees Working Time',
|
||||
domain=[('resource_id', '!=', False)])
|
||||
|
||||
@api.depends('normal_attendance_ids.hour_from', 'normal_attendance_ids.hour_to')
|
||||
def _compute_hours_per_week(self):
|
||||
for calendar in self:
|
||||
calendar.hours_per_week = sum((attendance.hour_to - attendance.hour_from) for attendance in calendar.normal_attendance_ids)
|
||||
|
||||
def _compute_is_fulltime(self):
|
||||
for calendar in self:
|
||||
calendar.is_fulltime = not float_compare(calendar.full_time_required_hours, calendar.hours_per_week, 3)
|
||||
|
||||
def _get_global_attendances(self):
|
||||
res = super(ResourceCalendar, self)._get_global_attendances()
|
||||
res |= self.normal_attendance_ids.filtered(lambda attendance: not attendance.date_from and not attendance.date_to)
|
||||
return res
|
||||
|
||||
# Add a key on the api.onchange decorator
|
||||
@api.onchange('attendance_ids', 'normal_attendance_ids')
|
||||
def _onchange_hours_per_day(self):
|
||||
return super(ResourceCalendar, self)._onchange_hours_per_day()
|
||||
|
||||
@api.multi
|
||||
def transfer_leaves_to(self, other_calendar, resources=None, from_date=None):
|
||||
"""
|
||||
Transfer some resource.calendar.leaves from 'self' to another calendar 'other_calendar'.
|
||||
Transfered leaves linked to `resources` (or all if `resources` is None) and starting
|
||||
after 'from_date' (or today if None).
|
||||
"""
|
||||
from_date = from_date or datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
domain = [
|
||||
('calendar_id', 'in', self.ids),
|
||||
('date_from', '>=', from_date),
|
||||
]
|
||||
domain = AND([domain, [('resource_id', 'in', resources.ids)]]) if resources else domain
|
||||
|
||||
self.env['resource.calendar.leaves'].search(domain).write({
|
||||
'calendar_id': other_calendar.id,
|
||||
})
|
||||
|
||||
|
||||
class ResourceCalendarAttendance(models.Model):
|
||||
_inherit = 'resource.calendar.attendance'
|
||||
|
||||
def _default_work_entry_type_id(self):
|
||||
return self.env.ref('hr_payroll.work_entry_type_attendance', raise_if_not_found=False)
|
||||
|
||||
work_entry_type_id = fields.Many2one('hr.work.entry.type', 'Work Entry Type', default=_default_work_entry_type_id)
|
||||
|
||||
|
||||
class ResourceCalendarLeave(models.Model):
|
||||
_inherit = 'resource.calendar.leaves'
|
||||
|
||||
work_entry_type_id = fields.Many2one('hr.work.entry.type', 'Work Entry Type')
|
||||
|
||||
|
||||
class ResourceMixin(models.AbstractModel):
|
||||
_inherit = "resource.mixin"
|
||||
|
||||
def _get_work_entry_days_data(self, work_entry_type, from_datetime, to_datetime, calendar=None):
|
||||
"""
|
||||
By default the resource calendar is used, but it can be
|
||||
changed using the `calendar` argument.
|
||||
|
||||
Returns a dict {'days': n, 'hours': h} containing the number of leaves
|
||||
expressed as days and as hours.
|
||||
"""
|
||||
resource = self.resource_id
|
||||
calendar = calendar or self.resource_calendar_id
|
||||
work_entry_type_ids = work_entry_type.ids
|
||||
if work_entry_type == self.env.ref('hr_payroll.work_entry_type_attendance'): # special case for global attendances
|
||||
work_entry_type_ids += [False]# no work_entry type = normal/global attendance
|
||||
domain = [('work_entry_type_id', 'in', work_entry_type_ids)]
|
||||
|
||||
# naive datetimes are made explicit in UTC
|
||||
from_datetime = timezone_datetime(from_datetime)
|
||||
to_datetime = timezone_datetime(to_datetime)
|
||||
|
||||
day_total = self._get_day_total(from_datetime, to_datetime, calendar, resource)
|
||||
# actual hours per day
|
||||
if work_entry_type.is_leave:
|
||||
intervals = calendar._attendance_intervals(from_datetime, to_datetime, resource) & calendar._leave_intervals(from_datetime, to_datetime, resource, domain) # use domain to only retrieve leaves of this type
|
||||
else:
|
||||
intervals = calendar._attendance_intervals(from_datetime, to_datetime, resource, domain) - calendar._leave_intervals(from_datetime, to_datetime, resource)
|
||||
|
||||
return self._get_days_data(intervals, day_total)
|
||||
@@ -1,99 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
|
||||
<record model="ir.module.category" id="base.module_category_human_resources_payroll">
|
||||
<field name="description">Helps you manage your payrolls.</field>
|
||||
<field name="sequence">16</field>
|
||||
</record>
|
||||
|
||||
<record id="group_hr_payroll_user" model="res.groups">
|
||||
<field name="name">Officer</field>
|
||||
<field name="category_id" ref="base.module_category_human_resources_payroll"/>
|
||||
<field name="implied_ids" eval="[(4, ref('hr.group_hr_user')), (4, ref('hr_contract.group_hr_contract_manager')), (4, ref('hr_holidays.group_hr_holidays_user'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="group_hr_payroll_manager" model="res.groups">
|
||||
<field name="name">Administrator</field>
|
||||
<field name="category_id" ref="base.module_category_human_resources_payroll"/>
|
||||
<field name="implied_ids" eval="[(4, ref('hr_payroll.group_hr_payroll_user')), (4, ref('hr_holidays.group_hr_holidays_manager'))]"/>
|
||||
<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.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('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('hr_payroll.group_hr_payroll_manager'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="hr_work_entry_validated" model="ir.rule">
|
||||
<field name="name">hr.work.entry: only non validated work_entries updated</field>
|
||||
<field name="model_id" ref="model_hr_work_entry"/>
|
||||
<field name="domain_force">[('state', '!=', 'validated')]</field>
|
||||
<field name="groups" eval="[(4, ref('hr_payroll.group_hr_payroll_manager'))]"/>
|
||||
<field name="perm_create" eval="0"/>
|
||||
<field name="perm_write" eval="1"/>
|
||||
<field name="perm_unlink" eval="0"/>
|
||||
<field name="perm_read" eval="0"/>
|
||||
</record>
|
||||
|
||||
<record id="hr_user_work_entry_employee" model="ir.rule">
|
||||
<field name="name">Work entries/Employee calendar filter: only self</field>
|
||||
<field name="model_id" ref="model_hr_user_work_entry_employee"/>
|
||||
<field name="domain_force">[('user_id', '=', user.id)]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
|
||||
<field name="perm_create" eval="1"/>
|
||||
<field name="perm_write" eval="1"/>
|
||||
<field name="perm_unlink" eval="1"/>
|
||||
<field name="perm_read" eval="0"/>
|
||||
</record>
|
||||
|
||||
<record id="ir_rule_hr_payroll_structure_multi_company" model="ir.rule">
|
||||
<field name="name">HR Payroll Structure: Multi Company</field>
|
||||
<field name="model_id" ref="model_hr_payroll_structure"/>
|
||||
<field name="global" eval="True"/>
|
||||
<field name="domain_force">['|', ('country_id', '=', False), ('country_id', '=', user.company_id.country_id.id)]</field>
|
||||
</record>
|
||||
|
||||
<record id="ir_rule_hr_payslip_multi_company" model="ir.rule">
|
||||
<field name="name">HR Payslip: 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', 'child_of', [user.company_id.id])]</field>
|
||||
</record>
|
||||
|
||||
<record id="ir_rule_hr_payslip_line_multi_company" model="ir.rule">
|
||||
<field name="name">HR Payslip Line: Multi Company</field>
|
||||
<field name="model_id" ref="model_hr_payslip_line"/>
|
||||
<field name="global" eval="True"/>
|
||||
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'child_of', [user.company_id.id])]</field>
|
||||
</record>
|
||||
|
||||
<record id="ir_rule_hr_payslip_run_multi_company" model="ir.rule">
|
||||
<field name="name">HR Payslip Batch: Multi Company</field>
|
||||
<field name="model_id" ref="model_hr_payslip_run"/>
|
||||
<field name="global" eval="True"/>
|
||||
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'child_of', [user.company_id.id])]</field>
|
||||
</record>
|
||||
|
||||
<record id="ir_rule_hr_work_entry_multi_company" model="ir.rule">
|
||||
<field name="name">HR Payslip: Multi Company</field>
|
||||
<field name="model_id" ref="model_hr_work_entry"/>
|
||||
<field name="global" eval="True"/>
|
||||
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'child_of', [user.company_id.id])]</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -1,17 +0,0 @@
|
||||
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,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_payroll_structure_hr_contract_manager,hr.payroll.structure.hr.contract.manager,model_hr_payroll_structure,hr_contract.group_hr_contract_manager,1,0,0,0
|
||||
access_hr_salary_rule_category,hr.salary.rule.category,model_hr_salary_rule_category,hr_payroll.group_hr_payroll_user,1,1,1,1
|
||||
access_hr_payslip,hr.payslip,model_hr_payslip,hr_payroll.group_hr_payroll_user,1,1,1,1
|
||||
access_hr_payslip_line,hr.payslip.line,model_hr_payslip_line,hr_payroll.group_hr_payroll_user,1,1,1,1
|
||||
access_hr_payslip_input_user,hr.payslip.input.user,model_hr_payslip_input,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,hr_payroll.group_hr_payroll_user,1,1,1,1
|
||||
access_hr_payslip_run,hr.payslip.run,model_hr_payslip_run,hr_payroll.group_hr_payroll_manager,1,1,1,1
|
||||
access_hr_salary_rule_user,hr.salary.rule.user,model_hr_salary_rule,hr_payroll.group_hr_payroll_user,1,1,1,1
|
||||
access_hr_work_entry,access_hr_work_entry,model_hr_work_entry,group_hr_payroll_user,1,1,1,1
|
||||
access_hr_work_entry_type_officer,access_hr_work_entry_type_officer,model_hr_work_entry_type,group_hr_payroll_user,1,0,0,0
|
||||
access_hr_work_entry_employee,hr.work.entry.employee,model_hr_user_work_entry_employee,group_hr_payroll_user,1,1,1,1
|
||||
access_hr_work_entry_type_manager,access_hr_work_entry_type_manager,model_hr_work_entry_type,group_hr_payroll_manager,1,1,1,1
|
||||
access_hr_payroll_structure_type,hr.payroll.structure.type,model_hr_payroll_structure_type,hr_payroll.group_hr_payroll_user,1,1,1,1
|
||||
access_hr_payroll_structure_type_hr_contract_manager,hr.payroll.structure.type.contract.manager,model_hr_payroll_structure_type,hr_contract.group_hr_contract_manager,1,0,0,0
|
||||
|
Binary file not shown.
|
Before Width: | Height: | Size: 8.8 KiB |
@@ -1,24 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 7.4 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 9.7 KiB |
@@ -1,43 +0,0 @@
|
||||
odoo.define('hr_payroll.payslip.tree', function (require) {
|
||||
"use strict";
|
||||
var core = require('web.core');
|
||||
var ListController = require('web.ListController');
|
||||
var ListView = require('web.ListView');
|
||||
var viewRegistry = require('web.view_registry');
|
||||
|
||||
var QWeb = core.qweb;
|
||||
|
||||
var PayslipListController = ListController.extend({
|
||||
/**
|
||||
* Extends the renderButtons function of ListView by adding a button
|
||||
* on the payslip list.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
renderButtons: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this.$buttons.append($(QWeb.render("PayslipListView.print_button", this)));
|
||||
var self = this;
|
||||
this.$buttons.on('click', '.o_button_print_payslip', function () {
|
||||
if (self.getSelectedIds().length == 0) {
|
||||
return;
|
||||
}
|
||||
return self._rpc({
|
||||
model: 'hr.payslip',
|
||||
method: 'action_print_payslip',
|
||||
args: [self.getSelectedIds()],
|
||||
}).then(function (results) {
|
||||
self.do_action(results);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
var PayslipListView = ListView.extend({
|
||||
config: _.extend({}, ListView.prototype.config, {
|
||||
Controller: PayslipListController,
|
||||
}),
|
||||
});
|
||||
|
||||
viewRegistry.add('hr_payroll_payslip_tree', PayslipListView);
|
||||
});
|
||||
@@ -1,95 +0,0 @@
|
||||
odoo.define('hr_payroll.work_entries_calendar', function(require) {
|
||||
'use strict';
|
||||
|
||||
var core = require('web.core');
|
||||
var WorkEntryControllerMixin = require('hr_payroll.WorkEntryControllerMixin');
|
||||
var CalendarController = require("web.CalendarController");
|
||||
var CalendarModel = require('web.CalendarModel');
|
||||
var CalendarRenderer = require('web.CalendarRenderer');
|
||||
var CalendarView = require('web.CalendarView');
|
||||
var viewRegistry = require('web.view_registry');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
|
||||
var WorkEntryCalendarController = CalendarController.extend(WorkEntryControllerMixin, {
|
||||
events: _.extend({}, CalendarController.prototype.events, {
|
||||
'click .btn-work-entry-generate': '_onGenerateWorkEntries',
|
||||
'click .btn-work-entry-validate': '_onValidateWorkEntries',
|
||||
'click .btn-payslip-generate': '_onGeneratePayslips',
|
||||
}),
|
||||
|
||||
// Returns the records from the model
|
||||
_fetchRecords: function () {
|
||||
var self = this;
|
||||
var records = _.filter(this.model.data.data, function (data) {
|
||||
// Filter records that are not inside the current month
|
||||
// (because in calendar view some days of prev. and next month are visible)
|
||||
return data.record.date_start.isBefore(self.lastDay) && data.record.date_stop.isAfter(self.firstDay);
|
||||
});
|
||||
return _.pluck(records, 'record');
|
||||
},
|
||||
_fetchFirstDay: function () {
|
||||
return this.model.data.target_date.clone().startOf('month');
|
||||
},
|
||||
_fetchLastDay: function () {
|
||||
return this.model.data.target_date.clone().endOf('month');
|
||||
},
|
||||
|
||||
/*
|
||||
Event handlers
|
||||
*/
|
||||
|
||||
_onGenerateWorkEntries: function (e) {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
this._generateWorkEntries();
|
||||
},
|
||||
_onGeneratePayslips: function (e) {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
this._generatePayslips();
|
||||
},
|
||||
_onValidateWorkEntries: function (e) {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
this._validateWorkEntries();
|
||||
},
|
||||
});
|
||||
|
||||
var WorkEntryCalendarModel = CalendarModel.extend({
|
||||
/**
|
||||
* Display everybody's work entries if no employee filter exists
|
||||
* @private
|
||||
* @override
|
||||
* @param {any} filter
|
||||
* @returns {Deferred}
|
||||
*/
|
||||
_loadFilter: function (filter) {
|
||||
return this._super.apply(this, arguments).then(function () {
|
||||
var filters = filter.filters;
|
||||
var all_filter = filters[filters.length - 1];
|
||||
|
||||
if (all_filter) {
|
||||
all_filter.label = _t("Everybody's work entries");
|
||||
|
||||
if (filter.write_model && filter.filters.length <= 1 && all_filter.active === undefined) {
|
||||
filter.all = true;
|
||||
all_filter.active = true;
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
var WorkEntryCalendarView = CalendarView.extend({
|
||||
config: _.extend({}, CalendarView.prototype.config, {
|
||||
Controller: WorkEntryCalendarController,
|
||||
Model: WorkEntryCalendarModel,
|
||||
Renderer: CalendarRenderer,
|
||||
}),
|
||||
});
|
||||
|
||||
viewRegistry.add('work_entries_calendar', WorkEntryCalendarView);
|
||||
});
|
||||
@@ -1,122 +0,0 @@
|
||||
odoo.define('hr_payroll.WorkEntryControllerMixin', function(require) {
|
||||
'use strict';
|
||||
|
||||
var core = require('web.core');
|
||||
var time = require('web.time');
|
||||
|
||||
var _t = core._t;
|
||||
var QWeb = core.qweb;
|
||||
|
||||
/*
|
||||
This mixin implements the behaviours necessary to generate and validate work entries and Payslips
|
||||
It is intended to be used in a Controller and requires three methods to be defined on your Controller
|
||||
|
||||
1. _fetchRecords
|
||||
Which should return a list of records containing at least the state and id fields
|
||||
|
||||
2. _fetchFirstDay
|
||||
Which should return the first day for which we will generate the work entries, it should be a Moment instance
|
||||
(Typically the first day of the current month)
|
||||
|
||||
3. _fetchLastDay
|
||||
Same as _fetchFirstDay except that this is the last day of the period
|
||||
|
||||
This mixin is responsible for rendering the buttons in the control panel and adds the three following methods
|
||||
|
||||
1. _generateWorkEntries
|
||||
2. _generatePayslips
|
||||
3. _validateWorkEntries
|
||||
*/
|
||||
|
||||
var WorkEntryControllerMixin = {
|
||||
/*
|
||||
Overrides of Controller methods
|
||||
*/
|
||||
renderButtons: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this._renderWorkEntryButtons();
|
||||
},
|
||||
|
||||
_update: function () {
|
||||
var self = this;
|
||||
return this._super.apply(this, arguments).then(function () {
|
||||
self.firstDay = self._fetchFirstDay().toDate();
|
||||
self.lastDay = self._fetchLastDay().toDate();
|
||||
self._renderWorkEntryButtons();
|
||||
});
|
||||
},
|
||||
|
||||
/*
|
||||
Private
|
||||
*/
|
||||
|
||||
_generateWorkEntries: function () {
|
||||
var self = this;
|
||||
this._rpc({
|
||||
model: 'hr.employee',
|
||||
method: 'generate_work_entry',
|
||||
args: [time.date_to_str(this.firstDay), time.date_to_str(this.lastDay)],
|
||||
}).then(function () {
|
||||
self.reload();
|
||||
});
|
||||
},
|
||||
|
||||
_generatePayslips: function () {
|
||||
this.do_action('hr_payroll.action_generate_payslips_from_work_entries', {
|
||||
additional_context: {
|
||||
default_date_start: time.date_to_str(this.firstDay),
|
||||
default_date_end: time.date_to_str(this.lastDay),
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
_renderWorkEntryButtons: function () {
|
||||
if (this.modelName !== "hr.work.entry") {
|
||||
return;
|
||||
}
|
||||
|
||||
var records = this._fetchRecords();
|
||||
var isvalidated = _.every(records, function (record) {
|
||||
return record.state === 'validated';
|
||||
});
|
||||
|
||||
this.$buttons.find('.btn-work-entry').remove();
|
||||
|
||||
var text = '';
|
||||
var cl = '';
|
||||
|
||||
if (records.length === 0) {
|
||||
text = _t("Generate Work Entries");
|
||||
cl = 'btn-work-entry-generate';
|
||||
}
|
||||
else if (isvalidated && records.length !== 0) {
|
||||
text = _t("Generate Payslips");
|
||||
cl = 'btn-payslip-generate';
|
||||
}
|
||||
else if (!isvalidated) {
|
||||
text = _t("Validate Work Entries");
|
||||
cl = 'btn-work-entry-validate';
|
||||
}
|
||||
|
||||
this.$buttons.append(QWeb.render('hr_payroll.work_entry_button', {
|
||||
button_text: text,
|
||||
event_class: cl,
|
||||
}));
|
||||
},
|
||||
|
||||
_validateWorkEntries: function () {
|
||||
var self = this;
|
||||
var records = this._fetchRecords();
|
||||
this._rpc({
|
||||
model: 'hr.work.entry',
|
||||
method: 'action_validate',
|
||||
args: [_.map(records, function (record) { return record.id; })],
|
||||
}).then(function () {
|
||||
return self.reload();
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
return WorkEntryControllerMixin;
|
||||
|
||||
});
|
||||
@@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<templates>
|
||||
<t t-name="PayslipListView.print_button">
|
||||
<button type="button" class="btn btn-secondary o_button_print_payslip">
|
||||
Print
|
||||
</button>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<templates>
|
||||
<t t-name="hr_payroll.work_entry_button">
|
||||
<button t-attf-class="btn btn-primary btn-work-entry {{ event_class }}" type="button" t-esc="button_text" />
|
||||
</t>
|
||||
</templates>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user