[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:
Yannick Tivisse
2019-03-19 10:53:06 +01:00
parent 6806b8f8d8
commit 77944721ce
297 changed files with 0 additions and 140546 deletions
-6
View File
@@ -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
-42
View File
@@ -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
-50
View File
@@ -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>
-206
View File
@@ -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
-12
View File
@@ -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
-45
View File
@@ -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'))),
])
-98
View File
@@ -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()
-180
View File
@@ -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)
-442
View File
@@ -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
}
-230
View File
@@ -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))
-333
View File
@@ -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')
-108
View File
@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_hr_payroll_structure hr.payroll.structure model_hr_payroll_structure hr_payroll.group_hr_payroll_user 1 1 1 1
3 access_hr_payroll_structure_hr_user hr.payroll.structure.hr.user model_hr_payroll_structure hr.group_hr_user 1 0 0 0
4 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
5 access_hr_salary_rule_category hr.salary.rule.category model_hr_salary_rule_category hr_payroll.group_hr_payroll_user 1 1 1 1
6 access_hr_payslip hr.payslip model_hr_payslip hr_payroll.group_hr_payroll_user 1 1 1 1
7 access_hr_payslip_line hr.payslip.line model_hr_payslip_line hr_payroll.group_hr_payroll_user 1 1 1 1
8 access_hr_payslip_input_user hr.payslip.input.user model_hr_payslip_input hr_payroll.group_hr_payroll_user 1 1 1 1
9 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
10 access_hr_payslip_run hr.payslip.run model_hr_payslip_run hr_payroll.group_hr_payroll_manager 1 1 1 1
11 access_hr_salary_rule_user hr.salary.rule.user model_hr_salary_rule hr_payroll.group_hr_payroll_user 1 1 1 1
12 access_hr_work_entry access_hr_work_entry model_hr_work_entry group_hr_payroll_user 1 1 1 1
13 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
14 access_hr_work_entry_employee hr.work.entry.employee model_hr_user_work_entry_employee group_hr_payroll_user 1 1 1 1
15 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
16 access_hr_payroll_structure_type hr.payroll.structure.type model_hr_payroll_structure_type hr_payroll.group_hr_payroll_user 1 1 1 1
17 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