[MERGE] hr_attendance/timesheet_sheet
- Migration of hr_attendance, hr_timesheet_attendance and hr_timesheet_sheet to new api. - Revamping of hr_attendance; adaptation of hr_timesheet_attendance and hr_timesheet_sheet to the revamp. hr_timesheet_sheet now independent of hr_attendance, all dependencies moved to hr_timesheet_attendance which auto-installs when both modules are installed. Closes #12698
This commit is contained in:
@@ -1,6 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import wizard
|
||||
import report
|
||||
import models
|
||||
|
||||
@@ -4,34 +4,35 @@
|
||||
|
||||
{
|
||||
'name': 'Attendances',
|
||||
'version': '1.1',
|
||||
'version': '2.0',
|
||||
'category': 'Human Resources',
|
||||
'sequence': 81,
|
||||
'summary': 'Manage employee attendances',
|
||||
'description': """
|
||||
This module aims to manage employee's attendances.
|
||||
==================================================
|
||||
|
||||
Keeps account of the attendances of the employees on the basis of the
|
||||
actions(Sign in/Sign out) performed by them.
|
||||
actions(Check in/Check out) performed by them.
|
||||
""",
|
||||
'website': 'https://www.odoo.com/page/employees',
|
||||
'depends': ['hr', 'report'],
|
||||
'depends': ['hr', 'report', 'barcodes'],
|
||||
'data': [
|
||||
'security/ir_rule.xml',
|
||||
'security/hr_attendance_security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'hr_attendance_view.xml',
|
||||
'hr_attendance_report.xml',
|
||||
'wizard/hr_attendance_error_view.xml',
|
||||
'views/report_attendanceerrors.xml',
|
||||
'views/hr_attendance.xml',
|
||||
'hr_dashboard.xml',
|
||||
'views/web_asset_backend_template.xml',
|
||||
'views/hr_attendance_view.xml',
|
||||
'report/hr_employee_badge.xml',
|
||||
'views/hr_department_view.xml',
|
||||
'views/hr_employee_view.xml',
|
||||
'views/res_config_view.xml',
|
||||
],
|
||||
'demo': ['hr_attendance_demo.xml'],
|
||||
'test': [
|
||||
'test/attendance_process.yml',
|
||||
'test/hr_attendance_report.yml',
|
||||
'demo': [
|
||||
'data/hr_attendance_demo.xml'
|
||||
],
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
#web
|
||||
'qweb': ["static/src/xml/attendance.xml"],
|
||||
'qweb': [
|
||||
"static/src/xml/attendance.xml",
|
||||
],
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="True">
|
||||
<record id="hr.employee_al" model="hr.employee">
|
||||
<field name="pin">0000</field>
|
||||
<field name="barcode">123</field>
|
||||
</record>
|
||||
|
||||
<record id="hr.employee_root" model="hr.employee">
|
||||
<field name="pin">0000</field>
|
||||
<field name="barcode">456</field>
|
||||
</record>
|
||||
|
||||
<record id="attendance_root1" model="hr.attendance">
|
||||
<field eval="(datetime.now()+relativedelta(days=-1)).strftime('%Y-%m-%d 08:00')" name="check_in"/>
|
||||
<field eval="(datetime.now()+relativedelta(days=-1)).strftime('%Y-%m-%d 12:00')" name="check_out"/>
|
||||
<field name="employee_id" ref="hr.employee_root"/>
|
||||
</record>
|
||||
|
||||
<record id="attendance_root2" model="hr.attendance">
|
||||
<field eval="(datetime.now()+relativedelta(days=-1)).strftime('%Y-%m-%d 13:00')" name="check_in"/>
|
||||
<field eval="(datetime.now()+relativedelta(days=-1)).strftime('%Y-%m-%d 18:00')" name="check_out"/>
|
||||
<field name="employee_id" ref="hr.employee_root"/>
|
||||
</record>
|
||||
|
||||
<record id="attendance1" model="hr.attendance">
|
||||
<field eval="time.strftime('%Y-%m-01 08:21')" name="check_in"/>
|
||||
<field eval="time.strftime('%Y-%m-01 11:51')" name="check_out"/>
|
||||
<field name="employee_id" ref="hr.employee_qdp"/>
|
||||
</record>
|
||||
|
||||
<record id="attendance2" model="hr.attendance">
|
||||
<field eval="time.strftime('%Y-%m-02 12:47')" name="check_in"/>
|
||||
<field eval="time.strftime('%Y-%m-02 19:53')" name="check_out"/>
|
||||
<field name="employee_id" ref="hr.employee_qdp"/>
|
||||
</record>
|
||||
|
||||
<record id="attendance3" model="hr.attendance">
|
||||
<field eval="time.strftime('%Y-%m-03 07:32')" name="check_in"/>
|
||||
<field eval="time.strftime('%Y-%m-03 12:32')" name="check_out"/>
|
||||
<field name="employee_id" ref="hr.employee_qdp"/>
|
||||
</record>
|
||||
|
||||
<record id="attendance4" model="hr.attendance">
|
||||
<field eval="time.strftime('%Y-%m-04 14:01')" name="check_in"/>
|
||||
<field eval="time.strftime('%Y-%m-04 17:21')" name="check_out"/>
|
||||
<field name="employee_id" ref="hr.employee_qdp"/>
|
||||
</record>
|
||||
|
||||
<record id="attendance5" model="hr.attendance">
|
||||
<field eval="time.strftime('%Y-%m-05 09:10')" name="check_in"/>
|
||||
<field eval="time.strftime('%Y-%m-05 12:42')" name="check_out"/>
|
||||
<field name="employee_id" ref="hr.employee_qdp"/>
|
||||
</record>
|
||||
|
||||
<record id="attendance6" model="hr.attendance">
|
||||
<field eval="time.strftime('%Y-%m-06 13:10')" name="check_in"/>
|
||||
<field eval="time.strftime('%Y-%m-06 18:34')" name="check_out"/>
|
||||
<field name="employee_id" ref="hr.employee_qdp"/>
|
||||
</record>
|
||||
|
||||
<record id="attendance7" model="hr.attendance">
|
||||
<field eval="time.strftime('%Y-%m-07 08:21')" name="check_in"/>
|
||||
<field eval="time.strftime('%Y-%m-07 18:21')" name="check_out"/>
|
||||
<field name="employee_id" ref="hr.employee_qdp"/>
|
||||
</record>
|
||||
|
||||
<record id="attendance8" model="hr.attendance">
|
||||
<field eval="time.strftime('%Y-%m-08 08:21')" name="check_in"/>
|
||||
<field eval="time.strftime('%Y-%m-08 12:54')" name="check_out"/>
|
||||
<field name="employee_id" ref="hr.employee_qdp"/>
|
||||
</record>
|
||||
|
||||
<record id="attendance9" model="hr.attendance">
|
||||
<field eval="time.strftime('%Y-%m-09 13:32')" name="check_in"/>
|
||||
<field eval="time.strftime('%Y-%m-09 19:31')" name="check_out"/>
|
||||
<field name="employee_id" ref="hr.employee_qdp"/>
|
||||
</record>
|
||||
|
||||
<record id="attendance10" model="hr.attendance">
|
||||
<field eval="time.strftime('%Y-%m-10 08:00')" name="check_in"/>
|
||||
<field eval="time.strftime('%Y-%m-10 09:00')" name="check_out"/>
|
||||
<field name="employee_id" ref="hr.employee_qdp"/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -1,126 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data noupdate="True">
|
||||
|
||||
<record id="attendance1" model="hr.attendance">
|
||||
<field eval="time.strftime('%Y-%m-01 08:21')" name="name"/>
|
||||
<field name="action">sign_in</field>
|
||||
<field name="employee_id" ref="hr.employee_root"/>
|
||||
</record>
|
||||
|
||||
<record id="attendance2" model="hr.attendance">
|
||||
<field eval="time.strftime('%Y-%m-01 11:51')" name="name"/>
|
||||
<field name="action">sign_out</field>
|
||||
<field name="employee_id" ref="hr.employee_root"/>
|
||||
</record>
|
||||
|
||||
<record id="attendance3" model="hr.attendance">
|
||||
<field eval="time.strftime('%Y-%m-02 12:47')" name="name"/>
|
||||
<field name="action">sign_in</field>
|
||||
<field name="employee_id" ref="hr.employee_root"/>
|
||||
</record>
|
||||
|
||||
<record id="attendance4" model="hr.attendance">
|
||||
<field eval="time.strftime('%Y-%m-02 19:53')" name="name"/>
|
||||
<field name="action">sign_out</field>
|
||||
<field name="employee_id" ref="hr.employee_root"/>
|
||||
</record>
|
||||
|
||||
<record id="attendance5" model="hr.attendance">
|
||||
<field eval="time.strftime('%Y-%m-03 07:32')" name="name"/>
|
||||
<field name="action">sign_in</field>
|
||||
<field name="employee_id" ref="hr.employee_root"/>
|
||||
</record>
|
||||
|
||||
<record id="attendance6" model="hr.attendance">
|
||||
<field eval="time.strftime('%Y-%m-03 12:32')" name="name"/>
|
||||
<field name="action">sign_out</field>
|
||||
<field name="employee_id" ref="hr.employee_root"/>
|
||||
</record>
|
||||
|
||||
<record id="attendance7" model="hr.attendance">
|
||||
<field eval="time.strftime('%Y-%m-04 14:01')" name="name"/>
|
||||
<field name="action">sign_in</field>
|
||||
<field name="employee_id" ref="hr.employee_root"/>
|
||||
</record>
|
||||
|
||||
<record id="attendance8" model="hr.attendance">
|
||||
<field eval="time.strftime('%Y-%m-04 17:21')" name="name"/>
|
||||
<field name="action">sign_out</field>
|
||||
<field name="employee_id" ref="hr.employee_root"/>
|
||||
</record>
|
||||
|
||||
<record id="attendance9" model="hr.attendance">
|
||||
<field eval="time.strftime('%Y-%m-05 09:10')" name="name"/>
|
||||
<field name="action">sign_in</field>
|
||||
<field name="employee_id" ref="hr.employee_root"/>
|
||||
</record>
|
||||
|
||||
<record id="attendance10" model="hr.attendance">
|
||||
<field eval="time.strftime('%Y-%m-05 12:42')" name="name"/>
|
||||
<field name="action">sign_out</field>
|
||||
<field name="employee_id" ref="hr.employee_root"/>
|
||||
</record>
|
||||
|
||||
<record id="attendance11" model="hr.attendance">
|
||||
<field eval="time.strftime('%Y-%m-06 13:10')" name="name"/>
|
||||
<field name="action">sign_in</field>
|
||||
<field name="employee_id" ref="hr.employee_root"/>
|
||||
</record>
|
||||
|
||||
<record id="attendance12" model="hr.attendance">
|
||||
<field eval="time.strftime('%Y-%m-06 18:34')" name="name"/>
|
||||
<field name="action">sign_out</field>
|
||||
<field name="employee_id" ref="hr.employee_root"/>
|
||||
</record>
|
||||
|
||||
<record id="attendance13" model="hr.attendance">
|
||||
<field eval="time.strftime('%Y-%m-07 08:21')" name="name"/>
|
||||
<field name="action">sign_in</field>
|
||||
<field name="employee_id" ref="hr.employee_root"/>
|
||||
</record>
|
||||
|
||||
<record id="attendance14" model="hr.attendance">
|
||||
<field eval="time.strftime('%Y-%m-07 18:21')" name="name"/>
|
||||
<field name="action">sign_out</field>
|
||||
<field name="employee_id" ref="hr.employee_root"/>
|
||||
</record>
|
||||
|
||||
<record id="attendance15" model="hr.attendance">
|
||||
<field eval="time.strftime('%Y-%m-08 08:21')" name="name"/>
|
||||
<field name="action">sign_in</field>
|
||||
<field name="employee_id" ref="hr.employee_root"/>
|
||||
</record>
|
||||
|
||||
<record id="attendance16" model="hr.attendance">
|
||||
<field eval="time.strftime('%Y-%m-08 12:54')" name="name"/>
|
||||
<field name="action">sign_out</field>
|
||||
<field name="employee_id" ref="hr.employee_root"/>
|
||||
</record>
|
||||
|
||||
<record id="attendance17" model="hr.attendance">
|
||||
<field eval="time.strftime('%Y-%m-09 13:32')" name="name"/>
|
||||
<field name="action">sign_in</field>
|
||||
<field name="employee_id" ref="hr.employee_root"/>
|
||||
</record>
|
||||
|
||||
<record id="attendance18" model="hr.attendance">
|
||||
<field eval="time.strftime('%Y-%m-09 19:31')" name="name"/>
|
||||
<field name="action">sign_out</field>
|
||||
<field name="employee_id" ref="hr.employee_root"/>
|
||||
</record>
|
||||
|
||||
<record id="attendance19" model="hr.attendance">
|
||||
<field eval="time.strftime('%Y-%m-10 07:10')" name="name"/>
|
||||
<field name="action">sign_in</field>
|
||||
<field name="employee_id" ref="hr.employee_root"/>
|
||||
</record>
|
||||
|
||||
<record id="attendance20" model="hr.attendance">
|
||||
<field eval="time.strftime('%Y-%m-10 12:34')" name="name"/>
|
||||
<field name="action">sign_out</field>
|
||||
<field name="employee_id" ref="hr.employee_root"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
@@ -1,13 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<report
|
||||
id="action_report_hrattendanceerror"
|
||||
model="hr.employee"
|
||||
string="Attendance Error Report"
|
||||
report_type="qweb-pdf"
|
||||
name="hr_attendance.report_attendanceerrors"
|
||||
file="hr_attendance.report_attendanceerrors"
|
||||
/>
|
||||
</data>
|
||||
</openerp>
|
||||
@@ -1,166 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
|
||||
<record id="view_attendance_form" model="ir.ui.view">
|
||||
<field name="name">hr.attendance.form</field>
|
||||
<field name="model">hr.attendance</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Employee attendances">
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="employee_id"/>
|
||||
<field name="name"/>
|
||||
<field name="action"/>
|
||||
<field name="action_desc"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_attendance_tree" model="ir.ui.view">
|
||||
<field name="name">hr.attendance.tree</field>
|
||||
<field name="model">hr.attendance</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Employee attendances">
|
||||
<field name="name"/>
|
||||
<field name="action"/>
|
||||
<field name="action_desc"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_attendance_who" model="ir.ui.view">
|
||||
<field name="name">hr.attendance.tree</field>
|
||||
<field name="model">hr.attendance</field>
|
||||
<field eval="3" name="priority"/>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Employee attendances">
|
||||
<field name="employee_id"/>
|
||||
<field name="name"/>
|
||||
<field name="action"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="view_hr_attendance_filter">
|
||||
<field name="name">view_hr_attendance_filter</field>
|
||||
<field name="model">hr.attendance</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Hr Attendance Search">
|
||||
<field name="name" string="Attendance"/>
|
||||
<field name="action"/>
|
||||
<field name="department_id"/>
|
||||
<filter string="Today" name="today" domain="[('name','>=',datetime.datetime.now().replace(hour=0, minute=0, second=0)),('name','<=',datetime.datetime.now().replace(hour=23, minute=59, second=59))]" />
|
||||
<filter string="Current Month" domain="[('name','>=', datetime.datetime.now().strftime('%Y-%m-01'))]" />
|
||||
<separator/>
|
||||
<filter string="My Attendance" domain="[('employee_id.user_id.id', '=', uid)]" />
|
||||
<field name="employee_id"/>
|
||||
<group expand="0" string="Group By">
|
||||
<filter name="employee" string="Employee" context="{'group_by':'employee_id'}"/>
|
||||
<separator/>
|
||||
<filter name="groupby_name" string="Month" context="{'group_by':'name'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_hr_attendance_pivot" model="ir.ui.view">
|
||||
<field name="name">hr.attendance.pivot</field>
|
||||
<field name="model">hr.attendance</field>
|
||||
<field name="arch" type="xml">
|
||||
<pivot string="Attendance">
|
||||
<field name="employee_id" type="row"/>
|
||||
<field name="name" type="col"/>
|
||||
<field name="worked_hours" type="measure"/>
|
||||
</pivot>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_hr_attendance_graph" model="ir.ui.view">
|
||||
<field name="name">hr.attendance.graph</field>
|
||||
<field name="model">hr.attendance</field>
|
||||
<field name="arch" type="xml">
|
||||
<graph string="Attendance">
|
||||
<field name="employee_id"/>
|
||||
<field name="name"/>
|
||||
<field name="worked_hours" type="measure"/>
|
||||
</graph>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="open_view_attendance" model="ir.actions.act_window">
|
||||
<field name="name">Attendances</field>
|
||||
<field name="res_model">hr.attendance</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="context">{"search_default_today":1}</field>
|
||||
<field name="search_view_id" ref="view_hr_attendance_filter" />
|
||||
<field name="help">The Time Tracking functionality aims to manage employee attendances from Sign in/Sign out actions. You can also link this feature to an attendance device using Odoo's web service features.</field>
|
||||
</record>
|
||||
|
||||
<record id="action_hr_attendance_graph" model="ir.actions.act_window">
|
||||
<field name="name">Attendance Analysis</field>
|
||||
<field name="res_model">hr.attendance</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">pivot,graph</field>
|
||||
<field name="view_id"></field> <!-- force empty -->
|
||||
</record>
|
||||
|
||||
<record id="action_hr_attendance_graph_filtered" model="ir.actions.act_window">
|
||||
<field name="name">Attendance Analysis</field>
|
||||
<field name="res_model">hr.attendance</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">pivot,graph</field>
|
||||
<field name="context">{
|
||||
'search_default_department_id': [active_id],
|
||||
'default_department_id': active_id}
|
||||
</field>
|
||||
<field name="view_id"></field> <!-- force empty -->
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_hr_attendance_root" name="Attendances" sequence="90" groups="base.group_hr_attendance" web_icon="hr_attendance,static/description/icon.png"/>
|
||||
|
||||
<menuitem action="open_view_attendance" id="menu_open_view_attendance" parent="menu_hr_attendance_root" sequence="20" groups="base.group_hr_attendance"/>
|
||||
|
||||
<menuitem name="Reports" parent="menu_hr_attendance_root" sequence="99"
|
||||
id="menu_hr_attendance_report" action="action_hr_attendance_graph"/>
|
||||
|
||||
<record id="edit_attendance_reason" model="ir.ui.view">
|
||||
<field name="name">hr.action.reason.form</field>
|
||||
<field name="model">hr.action.reason</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Define attendance reason">
|
||||
<group col="4">
|
||||
<field name="name"/>
|
||||
<field name="action_type"/>
|
||||
</group>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_attendance_reason" model="ir.ui.view">
|
||||
<field name="name">hr.action.reason.tree</field>
|
||||
<field name="model">hr.action.reason</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Attendance reasons">
|
||||
<field name="name" />
|
||||
<field name="action_type"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="open_view_attendance_reason" model="ir.actions.act_window">
|
||||
<field name="name">Attendance Reasons</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">hr.action.reason</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="view_id" ref="view_attendance_reason"/>
|
||||
</record>
|
||||
|
||||
<menuitem name="Configuration" parent="menu_hr_attendance_root" id="menu_hr_attendance_config" sequence="100" groups="base.group_hr_attendance" action="open_view_attendance_reason"/>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
@@ -1,4 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import hr_attendance
|
||||
import ir_http
|
||||
import hr_employee
|
||||
import res_config
|
||||
|
||||
@@ -1,169 +1,105 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
from openerp.osv import fields, osv
|
||||
from openerp.tools.translate import _
|
||||
from openerp.exceptions import UserError
|
||||
from odoo import models, fields, api, exceptions, _
|
||||
from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT
|
||||
|
||||
|
||||
class hr_action_reason(osv.osv):
|
||||
_name = "hr.action.reason"
|
||||
_description = "Action Reason"
|
||||
_columns = {
|
||||
'name': fields.char('Reason', required=True, help='Specifies the reason for Signing In/Signing Out.'),
|
||||
'action_type': fields.selection([('sign_in', 'Sign in'), ('sign_out', 'Sign out')], "Action Type"),
|
||||
}
|
||||
_defaults = {
|
||||
'action_type': 'sign_in',
|
||||
}
|
||||
|
||||
|
||||
def _employee_get(obj, cr, uid, context=None):
|
||||
ids = obj.pool.get('hr.employee').search(cr, uid, [('user_id', '=', uid)], context=context)
|
||||
return ids and ids[0] or False
|
||||
|
||||
|
||||
class hr_attendance(osv.osv):
|
||||
class HrAttendance(models.Model):
|
||||
_name = "hr.attendance"
|
||||
_description = "Attendance"
|
||||
_order = "check_in desc"
|
||||
|
||||
def _worked_hours_compute(self, cr, uid, ids, fieldnames, args, context=None):
|
||||
"""For each hr.attendance record of action sign-in: assign 0.
|
||||
For each hr.attendance record of action sign-out: assign number of hours since last sign-in.
|
||||
"""
|
||||
res = {}
|
||||
for obj in self.browse(cr, uid, ids, context=context):
|
||||
if obj.action == 'sign_in':
|
||||
res[obj.id] = 0
|
||||
elif obj.action == 'sign_out':
|
||||
# Get the associated sign-in
|
||||
last_signin_id = self.search(cr, uid, [
|
||||
('employee_id', '=', obj.employee_id.id),
|
||||
('name', '<', obj.name), ('action', '=', 'sign_in')
|
||||
], limit=1, order='name DESC')
|
||||
if last_signin_id:
|
||||
last_signin = self.browse(cr, uid, last_signin_id, context=context)[0]
|
||||
def _default_employee(self):
|
||||
return self.env['hr.employee'].search([('user_id', '=', self.env.uid)], limit=1)
|
||||
|
||||
# Compute time elapsed between sign-in and sign-out
|
||||
last_signin_datetime = datetime.strptime(last_signin.name, '%Y-%m-%d %H:%M:%S')
|
||||
signout_datetime = datetime.strptime(obj.name, '%Y-%m-%d %H:%M:%S')
|
||||
workedhours_datetime = (signout_datetime - last_signin_datetime)
|
||||
res[obj.id] = ((workedhours_datetime.seconds) / 60) / 60.0
|
||||
else:
|
||||
res[obj.id] = False
|
||||
return res
|
||||
employee_id = fields.Many2one('hr.employee', string="Employee", default=_default_employee, required=True, ondelete='cascade', index=True)
|
||||
department_id = fields.Many2one('hr.department', string="Department", related="employee_id.department_id")
|
||||
check_in = fields.Datetime(string="Check In", default=fields.Datetime.now, required=True)
|
||||
check_out = fields.Datetime(string="Check Out")
|
||||
worked_hours = fields.Float(string='Worked Hours', compute='_compute_worked_hours', store=True, readonly=True)
|
||||
|
||||
_columns = {
|
||||
'name': fields.datetime('Date', required=True, select=1),
|
||||
'action': fields.selection([('sign_in', 'Sign In'), ('sign_out', 'Sign Out'), ('action','Action')], 'Action', required=True),
|
||||
'action_desc': fields.many2one("hr.action.reason", "Action Reason", domain="[('action_type', '=', action)]", help='Specifies the reason for Signing In/Signing Out in case of extra hours.'),
|
||||
'employee_id': fields.many2one('hr.employee', "Employee", required=True, select=True),
|
||||
'department_id': fields.many2one('hr.department', "Department", related="employee_id.department_id"),
|
||||
'worked_hours': fields.function(_worked_hours_compute, type='float', string='Worked Hours', store=True),
|
||||
}
|
||||
_defaults = {
|
||||
'name': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'), #please don't remove the lambda, if you remove it then the current time will not change
|
||||
'employee_id': _employee_get,
|
||||
}
|
||||
|
||||
def _altern_si_so(self, cr, uid, ids, context=None):
|
||||
""" Alternance sign_in/sign_out check.
|
||||
Previous (if exists) must be of opposite action.
|
||||
Next (if exists) must be of opposite action.
|
||||
"""
|
||||
for att in self.browse(cr, uid, ids, context=context):
|
||||
# search and browse for first previous and first next records
|
||||
prev_att_ids = self.search(cr, uid, [('employee_id', '=', att.employee_id.id), ('name', '<', att.name), ('action', 'in', ('sign_in', 'sign_out'))], limit=1, order='name DESC')
|
||||
next_add_ids = self.search(cr, uid, [('employee_id', '=', att.employee_id.id), ('name', '>', att.name), ('action', 'in', ('sign_in', 'sign_out'))], limit=1, order='name ASC')
|
||||
prev_atts = self.browse(cr, uid, prev_att_ids, context=context)
|
||||
next_atts = self.browse(cr, uid, next_add_ids, context=context)
|
||||
# check for alternance, return False if at least one condition is not satisfied
|
||||
if prev_atts and prev_atts[0].action == att.action: # previous exists and is same action
|
||||
return False
|
||||
if next_atts and next_atts[0].action == att.action: # next exists and is same action
|
||||
return False
|
||||
if (not prev_atts) and (not next_atts) and att.action != 'sign_in': # first attendance must be sign_in
|
||||
return False
|
||||
return True
|
||||
|
||||
_constraints = [(_altern_si_so, 'Error ! Sign in (resp. Sign out) must follow Sign out (resp. Sign in)', ['action'])]
|
||||
_order = 'name desc'
|
||||
|
||||
|
||||
class hr_employee(osv.osv):
|
||||
_inherit = "hr.employee"
|
||||
_description = "Employee"
|
||||
|
||||
def _state(self, cr, uid, ids, name, args, context=None):
|
||||
result = {}
|
||||
if not ids:
|
||||
return result
|
||||
for id in ids:
|
||||
result[id] = 'absent'
|
||||
cr.execute('SELECT hr_attendance.action, hr_attendance.employee_id \
|
||||
FROM ( \
|
||||
SELECT MAX(name) AS name, employee_id \
|
||||
FROM hr_attendance \
|
||||
WHERE action in (\'sign_in\', \'sign_out\') \
|
||||
GROUP BY employee_id \
|
||||
) AS foo \
|
||||
LEFT JOIN hr_attendance \
|
||||
ON (hr_attendance.employee_id = foo.employee_id \
|
||||
AND hr_attendance.name = foo.name) \
|
||||
WHERE hr_attendance.employee_id IN %s',(tuple(ids),))
|
||||
for res in cr.fetchall():
|
||||
result[res[1]] = res[0] == 'sign_in' and 'present' or 'absent'
|
||||
@api.multi
|
||||
def name_get(self):
|
||||
result = []
|
||||
for attendance in self:
|
||||
if not attendance.check_out:
|
||||
result.append((self.id, _("%(empl_name)s from %(check_in)s") % {
|
||||
'empl_name': self.employee_id.name_related,
|
||||
'check_in': fields.Datetime.to_string(fields.Datetime.context_timestamp(self, fields.Datetime.from_string(self.check_in))),
|
||||
}))
|
||||
else:
|
||||
result.append((self.id, _("%(empl_name)s from %(check_in)s to %(check_out)s") % {
|
||||
'empl_name': self.employee_id.name_related,
|
||||
'check_in': fields.Datetime.to_string(fields.Datetime.context_timestamp(self, fields.Datetime.from_string(self.check_in))),
|
||||
'check_out': fields.Datetime.to_string(fields.Datetime.context_timestamp(self, fields.Datetime.from_string(self.check_out))),
|
||||
}))
|
||||
return result
|
||||
|
||||
def _last_sign(self, cr, uid, ids, name, args, context=None):
|
||||
result = {}
|
||||
if not ids:
|
||||
return result
|
||||
for id in ids:
|
||||
result[id] = False
|
||||
cr.execute("""select max(name) as name
|
||||
from hr_attendance
|
||||
where action in ('sign_in', 'sign_out') and employee_id = %s""",(id,))
|
||||
for res in cr.fetchall():
|
||||
result[id] = res[0]
|
||||
return result
|
||||
@api.depends('check_in', 'check_out')
|
||||
def _compute_worked_hours(self):
|
||||
for attendance in self:
|
||||
if attendance.check_out:
|
||||
delta = datetime.strptime(attendance.check_out, DEFAULT_SERVER_DATETIME_FORMAT) - datetime.strptime(
|
||||
attendance.check_in, DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
attendance.worked_hours = delta.total_seconds() / 3600.0
|
||||
|
||||
def _attendance_access(self, cr, uid, ids, name, args, context=None):
|
||||
# this function field use to hide attendance button to singin/singout from menu
|
||||
visible = self.pool.get("res.users").has_group(cr, uid, "base.group_hr_attendance")
|
||||
return dict([(x, visible) for x in ids])
|
||||
@api.constrains('check_in', 'check_out')
|
||||
def _check_validity_check_in_check_out(self):
|
||||
""" verifies if check_in is earlier than check_out. """
|
||||
for attendance in self:
|
||||
if attendance.check_in and attendance.check_out:
|
||||
if attendance.check_out < attendance.check_in:
|
||||
raise exceptions.ValidationError(_('"Check Out" time cannot be earlier than "Check In" time.'))
|
||||
|
||||
_columns = {
|
||||
'state': fields.function(_state, type='selection', selection=[('absent', 'Absent'), ('present', 'Present')], string='Attendance'),
|
||||
'last_sign': fields.function(_last_sign, type='datetime', string='Last Sign'),
|
||||
'attendance_access': fields.function(_attendance_access, string='Attendance Access', type='boolean'),
|
||||
}
|
||||
@api.constrains('check_in', 'check_out', 'employee_id')
|
||||
def _check_validity(self):
|
||||
""" Verifies the validity of the attendance record compared to the others from the same employee.
|
||||
For the same employee we must have :
|
||||
* maximum 1 "open" attendance record (without check_out)
|
||||
* no overlapping time slices with previous employee records
|
||||
"""
|
||||
for attendance in self:
|
||||
# we take the latest attendance before our check_in time and check it doesn't overlap with ours
|
||||
last_attendance_before_check_in = self.env['hr.attendance'].search([
|
||||
('employee_id', '=', attendance.employee_id.id),
|
||||
('check_in', '<=', attendance.check_in),
|
||||
('id', '!=', attendance.id),
|
||||
], order='check_in desc', limit=1)
|
||||
if last_attendance_before_check_in and last_attendance_before_check_in.check_out and last_attendance_before_check_in.check_out >= attendance.check_in:
|
||||
raise exceptions.ValidationError(_("Cannot create new attendance record for %(empl_name)s, the employee was already checked in on %(datetime)s") % {
|
||||
'empl_name': attendance.employee_id.name_related,
|
||||
'datetime': fields.Datetime.to_string(fields.Datetime.context_timestamp(self, fields.Datetime.from_string(attendance.check_in))),
|
||||
})
|
||||
|
||||
def _action_check(self, cr, uid, emp_id, dt=False, context=None):
|
||||
cr.execute('SELECT MAX(name) FROM hr_attendance WHERE employee_id=%s', (emp_id,))
|
||||
res = cr.fetchone()
|
||||
return not (res and (res[0]>=(dt or time.strftime('%Y-%m-%d %H:%M:%S'))))
|
||||
if not attendance.check_out:
|
||||
# if our attendance is "open" (no check_out), we verify there is no other "open" attendance
|
||||
no_check_out_attendances = self.env['hr.attendance'].search([
|
||||
('employee_id', '=', attendance.employee_id.id),
|
||||
('check_out', '=', False),
|
||||
('id', '!=', attendance.id),
|
||||
])
|
||||
if no_check_out_attendances:
|
||||
raise exceptions.ValidationError(_("Cannot create new attendance record for %(empl_name)s, the employee hasn't checked out since %(datetime)s") % {
|
||||
'empl_name': attendance.employee_id.name_related,
|
||||
'datetime': fields.Datetime.to_string(fields.Datetime.context_timestamp(self, fields.Datetime.from_string(no_check_out_attendances.check_in))),
|
||||
})
|
||||
else:
|
||||
# we verify that the latest attendance with check_in time before our check_out time
|
||||
# is the same as the one before our check_in time computed before, otherwise it overlaps
|
||||
last_attendance_before_check_out = self.env['hr.attendance'].search([
|
||||
('employee_id', '=', attendance.employee_id.id),
|
||||
('check_in', '<=', attendance.check_out),
|
||||
('id', '!=', attendance.id),
|
||||
], order='check_in desc', limit=1)
|
||||
if last_attendance_before_check_out and last_attendance_before_check_in != last_attendance_before_check_out:
|
||||
raise exceptions.ValidationError(_("Cannot create new attendance record for %(empl_name)s, the employee was already checked in on %(datetime)s") % {
|
||||
'empl_name': attendance.employee_id.name_related,
|
||||
'datetime': fields.Datetime.to_string(fields.Datetime.context_timestamp(self, fields.Datetime.from_string(last_attendance_before_check_out.check_in))),
|
||||
})
|
||||
|
||||
def attendance_action_change(self, cr, uid, ids, context=None):
|
||||
if context is None:
|
||||
context = {}
|
||||
action_date = context.get('action_date', False)
|
||||
action = context.get('action', False)
|
||||
hr_attendance = self.pool.get('hr.attendance')
|
||||
warning_sign = {'sign_in': _('Sign In'), 'sign_out': _('Sign Out')}
|
||||
for employee in self.browse(cr, uid, ids, context=context):
|
||||
if not action:
|
||||
if employee.state == 'present': action = 'sign_out'
|
||||
if employee.state == 'absent': action = 'sign_in'
|
||||
|
||||
if not self._action_check(cr, uid, employee.id, action_date, context):
|
||||
raise UserError(_('You tried to %s with a date anterior to another event !\nTry to contact the HR Manager to correct attendances.') % (warning_sign[action],))
|
||||
|
||||
vals = {'action': action, 'employee_id': employee.id}
|
||||
if action_date:
|
||||
vals['name'] = action_date
|
||||
hr_attendance.create(cr, uid, vals, context=context)
|
||||
return True
|
||||
@api.multi
|
||||
def copy(self):
|
||||
raise exceptions.UserError(_('You cannot duplicate an attendance.'))
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from random import choice
|
||||
from string import digits
|
||||
|
||||
from odoo import models, fields, api, exceptions, _, SUPERUSER_ID
|
||||
|
||||
|
||||
class HrEmployee(models.Model):
|
||||
_inherit = "hr.employee"
|
||||
_description = "Employee"
|
||||
|
||||
def _default_random_pin(self):
|
||||
return ("".join(choice(digits) for i in range(4)))
|
||||
|
||||
def _default_random_barcode(self):
|
||||
barcode = None
|
||||
while not barcode or self.env['hr.employee'].search([('barcode', '=', barcode)]):
|
||||
barcode = "".join(choice(digits) for i in range(8))
|
||||
return barcode
|
||||
|
||||
barcode = fields.Char(string="Badge ID", help="ID used for employee identification.", default=_default_random_barcode, copy=False, groups='base.group_hr_user')
|
||||
pin = fields.Char(string="PIN", default=_default_random_pin, help="PIN used for Check In/Out in Attendance.", copy=False, groups='base.group_hr_user')
|
||||
|
||||
attendance_ids = fields.One2many('hr.attendance', 'employee_id', help='list of attendances for the employee')
|
||||
last_attendance_id = fields.Many2one('hr.attendance', compute='_compute_last_attendance_id')
|
||||
attendance_state = fields.Selection(string="Attendance", compute='_compute_attendance_state', selection=[('checked_out', "Checked out"), ('checked_in', "Checked in")])
|
||||
manual_attendance = fields.Boolean(string='Manual Attendance', compute='_compute_manual_attendance', inverse='_inverse_manual_attendance',
|
||||
help='The employee will have access to the "My Attendances" menu to perform his check in and out from his session', default=False)
|
||||
|
||||
_sql_constraints = [('barcode_uniq', 'unique (barcode)', "The Badge ID must be unique, this one is already assigned to another employee.")]
|
||||
|
||||
@api.multi
|
||||
def _compute_manual_attendance(self):
|
||||
for employee in self:
|
||||
employee.manual_attendance = employee.user_id.has_group('base.group_hr_attendance') if employee.user_id else False
|
||||
|
||||
@api.multi
|
||||
def _inverse_manual_attendance(self):
|
||||
manual_attendance_group = self.env['res.groups'].search([('name', '=', 'Manual Attendances')])
|
||||
for employee in self:
|
||||
if employee.user_id:
|
||||
if employee.manual_attendance:
|
||||
manual_attendance_group.users = [(4, employee.user_id.id, 0)]
|
||||
else:
|
||||
manual_attendance_group.users = [(3, employee.user_id.id, 0)]
|
||||
|
||||
@api.depends('attendance_ids')
|
||||
def _compute_last_attendance_id(self):
|
||||
for employee in self:
|
||||
employee.last_attendance_id = employee.attendance_ids and employee.attendance_ids[0] or False
|
||||
|
||||
@api.depends('last_attendance_id.check_in', 'last_attendance_id.check_out', 'last_attendance_id')
|
||||
def _compute_attendance_state(self):
|
||||
"""compute employee state, last check in and last check out
|
||||
"""
|
||||
for employee in self:
|
||||
employee.attendance_state = employee.last_attendance_id and not employee.last_attendance_id.check_out and 'checked_in' or 'checked_out'
|
||||
|
||||
@api.constrains('pin')
|
||||
def _verify_pin(self):
|
||||
for employee in self:
|
||||
if employee.pin and not employee.pin.isdigit():
|
||||
raise exceptions.ValidationError(_("The PIN must be a sequence of digits."))
|
||||
|
||||
@api.model
|
||||
def attendance_scan(self, barcode):
|
||||
""" Receive a barcode scanned from the main menu and change the attendances of corresponding employee.
|
||||
Returns either an action or a warning.
|
||||
"""
|
||||
employee = self.search([('barcode', '=', barcode)], limit=1)
|
||||
return employee and employee.attendance_action('hr_attendance.hr_attendance_action_main_menu') or \
|
||||
{'warning': _('No employee corresponding to barcode %(barcode)s') % {'barcode': barcode}}
|
||||
|
||||
@api.multi
|
||||
def attendance_manual(self, next_action, entered_pin=None):
|
||||
self.ensure_one()
|
||||
if self.env['res.users'].browse(SUPERUSER_ID).has_group('base.group_hr_attendance_use_pin') and (self.user_id and self.user_id.id != self._uid or not self.user_id):
|
||||
if entered_pin != self.pin:
|
||||
return {'warning': _('Wrong PIN')}
|
||||
return self.attendance_action(next_action)
|
||||
|
||||
@api.multi
|
||||
def attendance_action(self, next_action):
|
||||
""" Changes the attendance of the employee.
|
||||
Returns an action to the check in/out message,
|
||||
next_action defines which menu the check in/out message should return to.
|
||||
"""
|
||||
self.ensure_one()
|
||||
action_message = self.env.ref('hr_attendance.hr_attendance_action_message').read()[0]
|
||||
action_message['previous_attendance_change_date'] = self.last_attendance_id and (self.last_attendance_id.check_out or self.last_attendance_id.check_in) or False
|
||||
if action_message['previous_attendance_change_date']:
|
||||
action_message['previous_attendance_change_date'] = \
|
||||
fields.Datetime.to_string(fields.Datetime.context_timestamp(self, fields.Datetime.from_string(action_message['previous_attendance_change_date'])))
|
||||
action_message['employee_name'] = self.name
|
||||
action_message['next_action'] = next_action
|
||||
|
||||
if self.user_id:
|
||||
modified_attendance = self.sudo(self.user_id.id).attendance_action_change()
|
||||
else:
|
||||
modified_attendance = self.sudo().attendance_action_change()
|
||||
action_message['attendance'] = modified_attendance.read()[0]
|
||||
return {'action': action_message}
|
||||
|
||||
@api.multi
|
||||
def attendance_action_change(self):
|
||||
""" Check In/Check Out action
|
||||
Check In: create a new attendance record
|
||||
Check Out: modify check_out field of appropriate attendance record
|
||||
"""
|
||||
if len(self) > 1:
|
||||
raise exceptions.UserError(_('Cannot perform check in or check out on multiple employees.'))
|
||||
action_date = fields.Datetime.now()
|
||||
|
||||
action = 'check_in'
|
||||
if self.attendance_state == 'checked_in':
|
||||
action = 'check_out'
|
||||
|
||||
if action == 'check_in':
|
||||
vals = {
|
||||
'employee_id': self.id,
|
||||
'check_in': action_date,
|
||||
}
|
||||
return self.env['hr.attendance'].create(vals)
|
||||
else:
|
||||
attendance = self.env['hr.attendance'].search([('employee_id', '=', self.id), ('check_out', '=', False)], limit=1)
|
||||
if attendance:
|
||||
attendance.check_out = action_date
|
||||
else:
|
||||
raise exceptions.UserError(_('Cannot perform check out on %(empl_name)s, could not find corresponding check in. \
|
||||
Your attendances have probably been modified manually by human resources.') % {'empl_name': self.name, })
|
||||
return attendance
|
||||
|
||||
def _init_column(self, cr, column_name, context=None):
|
||||
""" Initialize the value of the given column for existing rows.
|
||||
Overridden here because we need to have different default values
|
||||
for barcode and pin for every employee.
|
||||
"""
|
||||
if column_name not in ["barcode", "pin"]:
|
||||
super(HrEmployee, self)._init_column(cr, column_name, context=context)
|
||||
else:
|
||||
default_compute = self._defaults.get(column_name)
|
||||
|
||||
query = 'SELECT id FROM "%s" WHERE "%s" is NULL' % (
|
||||
self._table, column_name)
|
||||
cr.execute(query)
|
||||
employee_ids = cr.fetchall()
|
||||
|
||||
for employee_id in employee_ids:
|
||||
default_value = default_compute(self, cr, SUPERUSER_ID, context)
|
||||
|
||||
query = 'UPDATE "%s" SET "%s"=%%s WHERE id = %s' % (
|
||||
self._table, column_name, employee_id[0])
|
||||
cr.execute(query, (default_value,))
|
||||
@@ -1,9 +0,0 @@
|
||||
from openerp import models, service
|
||||
|
||||
class Http(models.AbstractModel):
|
||||
_inherit = 'ir.http'
|
||||
|
||||
def session_info(self):
|
||||
result = super(Http, self).session_info()
|
||||
result['is_employee'] = self.env.user.has_group('base.group_user')
|
||||
return result
|
||||
@@ -0,0 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class BaseConfigSettings(models.TransientModel):
|
||||
_inherit = 'base.config.settings'
|
||||
|
||||
group_attendance_use_pin = fields.Selection([(0, 'Employees do not need to enter their PIN to check in manually in the "Company Screen".'),
|
||||
(1, 'Employees must enter their PIN to check in manually in the "Company Screen".')],
|
||||
string='Employee PIN', help='Enable or disable employee PIN identification at check in', implied_group="base.group_hr_attendance_use_pin")
|
||||
@@ -1,65 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import datetime
|
||||
import time
|
||||
from openerp.osv import osv
|
||||
from openerp.report import report_sxw
|
||||
|
||||
|
||||
class attendance_print(report_sxw.rml_parse):
|
||||
|
||||
def __init__(self, cr, uid, name, context):
|
||||
super(attendance_print, self).__init__(cr, uid, name, context=context)
|
||||
self.localcontext.update({
|
||||
'time': time,
|
||||
'lst': self._lst,
|
||||
'total': self._lst_total,
|
||||
'get_employees':self._get_employees,
|
||||
})
|
||||
|
||||
def _get_employees(self, emp_ids):
|
||||
emp_obj_list = self.pool.get('hr.employee').browse(self.cr, self.uid, emp_ids)
|
||||
return emp_obj_list
|
||||
|
||||
def _lst(self, employee_id, dt_from, dt_to, max, *args):
|
||||
self.cr.execute("select name as date, create_date, action, create_date-name as delay from hr_attendance where employee_id=%s and to_char(name,'YYYY-mm-dd')<=%s and to_char(name,'YYYY-mm-dd')>=%s and action IN (%s,%s) order by name", (employee_id, dt_to, dt_from, 'sign_in', 'sign_out'))
|
||||
res = self.cr.dictfetchall()
|
||||
for r in res:
|
||||
if r['action'] == 'sign_out':
|
||||
r['delay'] = -r['delay']
|
||||
temp = r['delay'].seconds
|
||||
|
||||
r['delay'] = str(r['delay']).split('.')[0]
|
||||
if abs(temp) < max*60:
|
||||
r['delay2'] = r['delay']
|
||||
else:
|
||||
r['delay2'] = '/'
|
||||
return res
|
||||
|
||||
def _lst_total(self, employee_id, dt_from, dt_to, max, *args):
|
||||
self.cr.execute("select name as date, create_date, action, create_date-name as delay from hr_attendance where employee_id=%s and to_char(name,'YYYY-mm-dd')<=%s and to_char(name,'YYYY-mm-dd')>=%s and action IN (%s,%s) order by name", (employee_id, dt_to, dt_from, 'sign_in', 'sign_out'))
|
||||
res = self.cr.dictfetchall()
|
||||
if not res:
|
||||
return ('/','/')
|
||||
total2 = datetime.timedelta(seconds = 0, minutes = 0, hours = 0)
|
||||
total = datetime.timedelta(seconds = 0, minutes = 0, hours = 0)
|
||||
for r in res:
|
||||
if r['action'] == 'sign_out':
|
||||
r['delay'] = -r['delay']
|
||||
total += r['delay']
|
||||
if abs(r['delay'].seconds) < max*60:
|
||||
total2 += r['delay']
|
||||
|
||||
result_dict = {
|
||||
'total': total and str(total).split('.')[0],
|
||||
'total2': total2 and str(total2).split('.')[0]
|
||||
}
|
||||
return [result_dict]
|
||||
|
||||
|
||||
class report_hr_attendanceerrors(osv.AbstractModel):
|
||||
_name = 'report.hr_attendance.report_attendanceerrors'
|
||||
_inherit = 'report.abstract_report'
|
||||
_template = 'hr_attendance.report_attendanceerrors'
|
||||
_wrapped_report_class = attendance_print
|
||||
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<report
|
||||
id="hr_employee_print_badge"
|
||||
string="Print Badge"
|
||||
model="hr.employee"
|
||||
report_type="qweb-html"
|
||||
name="hr_attendance.print_employee_badge"
|
||||
file="hr_attendance.print_employee_badge"/>
|
||||
|
||||
<template id="print_employee_badge">
|
||||
<t t-call="report.html_container">
|
||||
<div class="page">
|
||||
<t t-foreach="docs" t-as="employee">
|
||||
<div class="col-sm-6">
|
||||
<table style="width:243pt; height:153pt; border: 1pt solid black; border-collapse:separate; border-radius:8pt; margin:5pt">
|
||||
<td style="width:33%;" valign="center">
|
||||
<table style="width:77pt; height:150pt">
|
||||
<tr style="height:30%">
|
||||
<td align="center" valign="center">
|
||||
<img t-if="employee.company_id and employee.company_id.logo" t-att-src="'data:image/png;base64,%s' % (employee.company_id.logo,)" style="max-height:45pt;max-width:90%" alt="Company Logo"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="height:70%;">
|
||||
<td align="center" valign="center">
|
||||
<img t-if="employee.image" t-att-src="'data:image/png;base64,%s' % (employee.image,)" style="max-height:85pt;max-width:90%" alt="Employee Image"/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
<td style="width:67%" valign="center">
|
||||
<table style="width:155pt; height:85pt">
|
||||
<tr><th><div style="font-size:15pt; margin-bottom:0pt;margin-top:0pt;" align="center"><t t-esc="employee.name"/></div></th></tr>
|
||||
<tr><td><div align="center" style="font-size:10pt;margin-bottom:5pt;"><t t-esc="employee.job_id.name"/></div></td></tr>
|
||||
<tr><td><img t-if="employee.barcode" t-att-src="'/report/barcode/?type=%s&value=%s&width=%s&height=%s' % ('Code128', employee.barcode, 600, 120)" style="max-height:50pt;max-width:100%;" align="center"/></td></tr>
|
||||
</table>
|
||||
</td>
|
||||
</table>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
</odoo>
|
||||
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="base.group_hr_manager" model="res.groups">
|
||||
<field name="implied_ids" eval="[(4, ref('base.group_hr_attendance'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="base.group_hr_user" model="res.groups">
|
||||
<field name="implied_ids" eval="[(4, ref('base.group_hr_attendance'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="base.group_hr_attendance" model="res.groups">
|
||||
<field name="name">Manual Attendances</field>
|
||||
<field name="comment">The user will gain access to the human resources attendance menu, enabling him to manage his own attendance.</field>
|
||||
</record>
|
||||
|
||||
<record id="base.group_hr_attendance_use_pin" model="res.groups">
|
||||
<field name="name">Enable PIN use</field>
|
||||
<field name="category_id" ref="base.module_category_hidden"/>
|
||||
<field name="comment">The user will have to enter his PIN to check in and out manually at the company screen.</field>
|
||||
</record>
|
||||
|
||||
<data noupdate="1">
|
||||
|
||||
<record id="hr_attendance_rule_attendance_manager" model="ir.rule">
|
||||
<field name="name">attendance officer: full access</field>
|
||||
<field name="model_id" ref="model_hr_attendance"/>
|
||||
<field name="domain_force">[(1,'=',1)]</field>
|
||||
<field name="groups" eval="[(4,ref('base.group_hr_user'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="hr_attendance_rule_attendance_employee" model="ir.rule">
|
||||
<field name="name">user: modify own attendance only</field>
|
||||
<field name="model_id" ref="model_hr_attendance"/>
|
||||
<field name="domain_force">[('employee_id.user_id','=',user.id)]</field>
|
||||
<field name="perm_read" eval="0"/>
|
||||
<field name="perm_write" eval="1"/>
|
||||
<field name="perm_create" eval="1"/>
|
||||
<field name="perm_unlink" eval="0"/>
|
||||
<field name="groups" eval="[(4,ref('base.group_user'))]"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -1,5 +1,3 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_hr_action_reason_user,hr.action.reason.user,model_hr_action_reason,base.group_hr_user,1,1,1,1
|
||||
access_hr_action_reason_employee,hr.action.reason.employee,model_hr_action_reason,base.group_user,1,1,1,1
|
||||
access_hr_attendance_user,hr.attendance.user,model_hr_attendance,base.group_hr_user,1,1,1,1
|
||||
access_hr_attendance_system_user,hr.attendance.system.user,model_hr_attendance,base.group_user,1,1,1,1
|
||||
access_hr_attendance_system_user,hr.attendance.system.user,model_hr_attendance,base.group_user,1,1,1,0
|
||||
|
@@ -1,22 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
|
||||
<record id="base.group_hr_manager" model="res.groups">
|
||||
<field name="implied_ids" eval="[(4, ref('base.group_hr_attendance'))]"/>
|
||||
</record>
|
||||
<record id="property_rule_attendace_manager" model="ir.rule">
|
||||
<field name="name">Manager Attendance</field>
|
||||
<field name="model_id" ref="model_hr_attendance"/>
|
||||
<field name="domain_force">[(1,'=',1)]</field>
|
||||
<field name="groups" eval="[(4,ref('base.group_hr_manager')), (4,ref('base.group_hr_user'))]"/>
|
||||
</record>
|
||||
<record id="property_rule_attendace_employee" model="ir.rule">
|
||||
<field name="name">Employee Attendance</field>
|
||||
<field name="model_id" ref="model_hr_attendance"/>
|
||||
<field name="domain_force">[('employee_id.user_id','=',user.id)]</field>
|
||||
<field name="groups" eval="[(4,ref('base.group_user'))]"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -1,3 +0,0 @@
|
||||
.oe_systray #oe_attendance_sign_in_out_icon {
|
||||
color: white;
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
odoo.define('hr_attendance.hr_attendance', function(require) {
|
||||
"use strict";
|
||||
|
||||
var core = require('web.core');
|
||||
var data = require('web.data');
|
||||
var formats = require('web.formats');
|
||||
var session = require('web.session');
|
||||
var SystrayMenu = require('web.SystrayMenu');
|
||||
var time = require('web.time');
|
||||
var Widget = require('web.Widget');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
var AttendanceSlider = Widget.extend({
|
||||
template: 'AttendanceSlider',
|
||||
init: function (parent) {
|
||||
this._super(parent);
|
||||
this.set({"signed_in": false});
|
||||
},
|
||||
start: function() {
|
||||
var self = this;
|
||||
var tmp = function() {
|
||||
var $sign_in_out_icon = this.$('#oe_attendance_sign_in_out_icon');
|
||||
$sign_in_out_icon.toggleClass("fa-sign-in", ! this.get("signed_in"));
|
||||
$sign_in_out_icon.toggleClass("fa-sign-out", this.get("signed_in"));
|
||||
};
|
||||
this.on("change:signed_in", this, tmp);
|
||||
_.bind(tmp, this)();
|
||||
this.$(".oe_attendance_sign_in_out").click(function(ev) {
|
||||
ev.preventDefault();
|
||||
self.do_update_attendance();
|
||||
});
|
||||
this.$el.tooltip({
|
||||
title: function() {
|
||||
var last_text = formats.format_value(self.last_sign, {type: "datetime"});
|
||||
var current_text = formats.format_value(new Date(), {type: "datetime"});
|
||||
var duration = self.last_sign ? moment(self.last_sign).fromNow() : "none";
|
||||
if (self.get("signed_in")) {
|
||||
return _.str.sprintf(_t("Last sign in: %s,<br />%s.<br />Click to sign out."), last_text, duration);
|
||||
} else {
|
||||
return _.str.sprintf(_t("Click to Sign In at %s."), current_text);
|
||||
}
|
||||
},
|
||||
});
|
||||
return this.check_attendance();
|
||||
},
|
||||
do_update_attendance: function () {
|
||||
var self = this;
|
||||
var hr_employee = new data.DataSet(self, 'hr.employee');
|
||||
hr_employee.call('attendance_action_change', [
|
||||
[self.employee.id]
|
||||
]).done(function (result) {
|
||||
self.last_sign = new Date();
|
||||
self.set({"signed_in": ! self.get("signed_in")});
|
||||
});
|
||||
},
|
||||
check_attendance: function () {
|
||||
var self = this;
|
||||
self.employee = false;
|
||||
this.do_hide();
|
||||
var employee = new data.DataSetSearch(self, 'hr.employee', self.session.user_context, [
|
||||
['user_id', '=', self.session.uid]
|
||||
]);
|
||||
return employee.read_slice(['id', 'name', 'state', 'last_sign', 'attendance_access']).then(function (res) {
|
||||
if (_.isEmpty(res) )
|
||||
return;
|
||||
if (res[0].attendance_access === false){
|
||||
return;
|
||||
}
|
||||
self.do_show();
|
||||
self.employee = res[0];
|
||||
self.last_sign = time.str_to_datetime(self.employee.last_sign);
|
||||
self.set({"signed_in": self.employee.state !== "absent"});
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
session.is_bound.then(function () {
|
||||
// Put the AttendanceSlider widget in the systray menu if the user is an employee
|
||||
if (session.is_employee) {
|
||||
SystrayMenu.Items.push(AttendanceSlider);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
|
||||
odoo.define('hr_attendance.employee_kanban_view_handler', function(require) {
|
||||
"use strict";
|
||||
|
||||
var KanbanRecord = require('web_kanban.Record');
|
||||
|
||||
KanbanRecord.include({
|
||||
on_card_clicked: function() {
|
||||
if (this.model === 'hr.employee' && this.$el.parents('.o_hr_employee_attendance_kanban').length) {
|
||||
// needed to diffentiate : check in/out kanban view of employees <-> standard employee kanban view
|
||||
var action = {
|
||||
type: 'ir.actions.client',
|
||||
name: 'Confirm',
|
||||
tag: 'hr_attendance_main_menu_confirm',
|
||||
employee_id: this.record.id.raw_value,
|
||||
employee_name: this.record.name.raw_value,
|
||||
employee_state: this.record.attendance_state.raw_value,
|
||||
};
|
||||
this.do_action(action);
|
||||
} else {
|
||||
this._super.apply(this, arguments);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
odoo.define('hr_attendance.MainMenu', function (require) {
|
||||
"use strict";
|
||||
|
||||
var core = require('web.core');
|
||||
var Model = require('web.Model');
|
||||
var Widget = require('web.Widget');
|
||||
var Session = require('web.session');
|
||||
var BarcodeHandlerMixin = require('barcodes.BarcodeHandlerMixin');
|
||||
|
||||
var QWeb = core.qweb;
|
||||
var _t = core._t;
|
||||
|
||||
|
||||
var MainMenu = Widget.extend(BarcodeHandlerMixin, {
|
||||
events: {
|
||||
"click .o_hr_attendance_button_employees": function(){ this.do_action('hr_attendance.hr_employee_attendance_action_kanban'); },
|
||||
},
|
||||
|
||||
init: function (parent, action) {
|
||||
// Note: BarcodeHandlerMixin.init calls this._super.init, so there's no need to do it here.
|
||||
// Yet, "_super" must be present in a function for the class mechanism to replace it with the actual parent method.
|
||||
this._super;
|
||||
BarcodeHandlerMixin.init.apply(this, arguments);
|
||||
},
|
||||
|
||||
start: function () {
|
||||
var self = this;
|
||||
self.session = Session;
|
||||
var res_company = new Model('res.company');
|
||||
res_company.query(['name'])
|
||||
.filter([['id', '=', self.session.company_id]])
|
||||
.all()
|
||||
.then(function (companies){
|
||||
self.company_name = companies[0].name;
|
||||
self.company_image_url = self.session.url('/web/image', {model: 'res.company', id: self.session.company_id, field: 'logo',})
|
||||
self.$el.html(QWeb.render("HrAttendanceMainMenu", {widget: self}));
|
||||
self.start_clock();
|
||||
});
|
||||
return self._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
on_barcode_scanned: function(barcode) {
|
||||
var self = this;
|
||||
var hr_employee = new Model('hr.employee');
|
||||
hr_employee.call('attendance_scan', [barcode, ])
|
||||
.then(function (result) {
|
||||
if (result.action) {
|
||||
self.do_action(result.action);
|
||||
} else if (result.warning) {
|
||||
self.do_warn(result.warning);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
start_clock: function() {
|
||||
this.clock_start = setInterval(function() {this.$(".o_hr_attendance_clock").text(new Date().toLocaleTimeString(navigator.language, {hour: '2-digit', minute:'2-digit'}));}, 500);
|
||||
// First clock refresh before interval to avoid delay
|
||||
this.$(".o_hr_attendance_clock").text(new Date().toLocaleTimeString(navigator.language, {hour: '2-digit', minute:'2-digit'}));
|
||||
},
|
||||
|
||||
destroy: function () {
|
||||
clearInterval(this.clock_start);
|
||||
this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
core.action_registry.add('hr_attendance_main_menu', MainMenu);
|
||||
|
||||
return MainMenu;
|
||||
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
odoo.define('hr_attendance.MainMenuConfirm', function (require) {
|
||||
"use strict";
|
||||
|
||||
var core = require('web.core');
|
||||
var Model = require('web.Model');
|
||||
var Widget = require('web.Widget');
|
||||
|
||||
var QWeb = core.qweb;
|
||||
var _t = core._t;
|
||||
|
||||
|
||||
var MainMenuConfirm = Widget.extend({
|
||||
events: {
|
||||
"click .o_hr_attendance_back_button": function () { this.do_action(this.next_action, {clear_breadcrumbs: true}); },
|
||||
"click .o_hr_attendance_sign_in_out_icon": function () {
|
||||
var self = this;
|
||||
this.$('.o_hr_attendance_sign_in_out_icon').attr("disabled", "disabled");
|
||||
var hr_employee = new Model('hr.employee');
|
||||
hr_employee.call('attendance_manual', [[this.employee_id], this.next_action])
|
||||
.then(function(result) {
|
||||
if (result.action) {
|
||||
self.do_action(result.action);
|
||||
} else if (result.warning) {
|
||||
self.do_warn(result.warning);
|
||||
self.$('.o_hr_attendance_sign_in_out_icon').removeAttr("disabled");
|
||||
}
|
||||
});
|
||||
},
|
||||
'click .o_hr_attendance_pin_pad_button_0': function() { this.$('.o_hr_attendance_PINbox').val(this.$('.o_hr_attendance_PINbox').val() + 0); },
|
||||
'click .o_hr_attendance_pin_pad_button_1': function() { this.$('.o_hr_attendance_PINbox').val(this.$('.o_hr_attendance_PINbox').val() + 1); },
|
||||
'click .o_hr_attendance_pin_pad_button_2': function() { this.$('.o_hr_attendance_PINbox').val(this.$('.o_hr_attendance_PINbox').val() + 2); },
|
||||
'click .o_hr_attendance_pin_pad_button_3': function() { this.$('.o_hr_attendance_PINbox').val(this.$('.o_hr_attendance_PINbox').val() + 3); },
|
||||
'click .o_hr_attendance_pin_pad_button_4': function() { this.$('.o_hr_attendance_PINbox').val(this.$('.o_hr_attendance_PINbox').val() + 4); },
|
||||
'click .o_hr_attendance_pin_pad_button_5': function() { this.$('.o_hr_attendance_PINbox').val(this.$('.o_hr_attendance_PINbox').val() + 5); },
|
||||
'click .o_hr_attendance_pin_pad_button_6': function() { this.$('.o_hr_attendance_PINbox').val(this.$('.o_hr_attendance_PINbox').val() + 6); },
|
||||
'click .o_hr_attendance_pin_pad_button_7': function() { this.$('.o_hr_attendance_PINbox').val(this.$('.o_hr_attendance_PINbox').val() + 7); },
|
||||
'click .o_hr_attendance_pin_pad_button_8': function() { this.$('.o_hr_attendance_PINbox').val(this.$('.o_hr_attendance_PINbox').val() + 8); },
|
||||
'click .o_hr_attendance_pin_pad_button_9': function() { this.$('.o_hr_attendance_PINbox').val(this.$('.o_hr_attendance_PINbox').val() + 9); },
|
||||
'click .o_hr_attendance_pin_pad_button_C': function() { this.$('.o_hr_attendance_PINbox').val(''); },
|
||||
'click .o_hr_attendance_pin_pad_button_ok': function() {
|
||||
var self = this;
|
||||
this.$('.o_hr_attendance_pin_pad_button_ok').attr("disabled", "disabled");
|
||||
var hr_employee = new Model('hr.employee');
|
||||
hr_employee.call('attendance_manual', [[this.employee_id], this.next_action, this.$('.o_hr_attendance_PINbox').val()])
|
||||
.then(function(result) {
|
||||
if (result.action) {
|
||||
self.do_action(result.action);
|
||||
} else if (result.warning) {
|
||||
self.do_warn(result.warning);
|
||||
setTimeout( function() { self.$('.o_hr_attendance_pin_pad_button_ok').removeAttr("disabled"); }, 500);
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
init: function (parent, action) {
|
||||
this._super.apply(this, arguments);
|
||||
this.next_action = { type: "ir.actions.client", name: "Attendances", tag: 'hr_attendance_main_menu', target: "fullscreen", params: {} };
|
||||
this.employee_id = action.employee_id;
|
||||
this.employee_name = action.employee_name;
|
||||
this.employee_state = action.employee_state;
|
||||
var self = this;
|
||||
},
|
||||
|
||||
start: function () {
|
||||
var self = this;
|
||||
self.session.user_has_group('base.group_hr_attendance_use_pin').then(function(has_group){
|
||||
self.use_pin = has_group;
|
||||
self.$el.html(QWeb.render("HrAttendanceMainMenuConfirm", {widget: self}));
|
||||
self.start_clock();
|
||||
});
|
||||
return self._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
start_clock: function () {
|
||||
this.clock_start = setInterval(function() {this.$(".o_hr_attendance_clock").text(new Date().toLocaleTimeString(navigator.language, {hour: '2-digit', minute:'2-digit'}));}, 500);
|
||||
// First clock refresh before interval to avoid delay
|
||||
this.$(".o_hr_attendance_clock").text(new Date().toLocaleTimeString(navigator.language, {hour: '2-digit', minute:'2-digit'}));
|
||||
},
|
||||
|
||||
destroy: function () {
|
||||
clearInterval(this.clock_start);
|
||||
this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
core.action_registry.add('hr_attendance_main_menu_confirm', MainMenuConfirm);
|
||||
|
||||
return MainMenuConfirm;
|
||||
|
||||
});
|
||||
@@ -0,0 +1,131 @@
|
||||
odoo.define('hr_attendance.Message', function (require) {
|
||||
"use strict";
|
||||
|
||||
var core = require('web.core');
|
||||
var Model = require('web.Model');
|
||||
var Widget = require('web.Widget');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
|
||||
var Message = Widget.extend({
|
||||
template: 'HrAttendanceMessage',
|
||||
|
||||
events: {
|
||||
"click .o_hr_attendance_button_dismiss": function () { this.do_action(this.next_action, {clear_breadcrumbs: true}); },
|
||||
},
|
||||
|
||||
init: function(parent, action) {
|
||||
this._super.apply(this, arguments);
|
||||
var self = this;
|
||||
var default_action = { type: "ir.actions.client", name: 'Attendances', tag: 'hr_attendance_my_main_menu', params: {} };
|
||||
if(!action.attendance){
|
||||
self.session.user_has_group('base.group_hr_user').then(function(has_group){
|
||||
if(has_group){
|
||||
self.next_action = { type: "ir.actions.client", name: 'Attendances', tag: 'hr_attendance_main_menu', target: 'fullscreen', params: {} };
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.next_action = action.next_action || default_action;
|
||||
this.attendance = action.attendance;
|
||||
this.attendance.check_in_time = (new Date((new Date(this.attendance.check_in)).valueOf() - (new Date()).getTimezoneOffset()*60*1000)).toTimeString().slice(0,8);
|
||||
this.attendance.check_out_time = this.attendance.check_out && (new Date((new Date(this.attendance.check_out)).valueOf() - (new Date()).getTimezoneOffset()*60*1000)).toTimeString().slice(0,8);
|
||||
this.previous_attendance_change_date = action.previous_attendance_change_date;
|
||||
this.employee_name = action.employee_name;
|
||||
},
|
||||
|
||||
start: function() {
|
||||
if (!this.attendance){
|
||||
return;
|
||||
}
|
||||
if (this.attendance.check_out) {
|
||||
this.farewell_message();
|
||||
} else {
|
||||
this.welcome_message();
|
||||
}
|
||||
},
|
||||
|
||||
welcome_message: function() {
|
||||
var self = this;
|
||||
var now = new Date((new Date(this.attendance.check_in)).valueOf() - (new Date()).getTimezoneOffset()*60*1000);
|
||||
self.return_to_main_menu = setTimeout( function() { self.do_action(self.next_action, {clear_breadcrumbs: true}); }, 5000);
|
||||
|
||||
if(now.getHours()<5){
|
||||
self.$('.o_hr_attendance_message_message').append(_t("Good night"));
|
||||
} else if(now.getHours() < 12){
|
||||
if(now.getHours() < 8 && Math.random() < .3){
|
||||
if(Math.random()<.75){
|
||||
self.$('.o_hr_attendance_message_message').append(_t("The early bird catches the worm"));
|
||||
} else {
|
||||
self.$('.o_hr_attendance_message_message').append(_t("First come, first served"));
|
||||
}
|
||||
|
||||
} else {
|
||||
self.$('.o_hr_attendance_message_message').append(_t("Good morning"));
|
||||
}
|
||||
} else if(now.getHours()<17){
|
||||
self.$('.o_hr_attendance_message_message').append(_t("Good afternoon"));
|
||||
} else if(now.getHours()<23){
|
||||
self.$('.o_hr_attendance_message_message').append(_t("Good evening"));
|
||||
} else {
|
||||
self.$('.o_hr_attendance_message_message').append(_t("Good night"));
|
||||
}
|
||||
if(this.previous_attendance_change_date){
|
||||
var last_check_out_date = new Date((new Date(this.previous_attendance_change_date)).valueOf() - (new Date()).getTimezoneOffset()*60*1000);
|
||||
if(now.valueOf() - last_check_out_date.valueOf() > 1000*60*60*24*7){
|
||||
self.$('.o_hr_attendance_random_message').html(_t("Glad to have you back, it's been a while!"));
|
||||
} else {
|
||||
if(Math.random() < .02){
|
||||
self.$('.o_hr_attendance_random_message').html(_t("If a job is worth doing, it is worth doing well!"));
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
farewell_message: function() {
|
||||
var self = this;
|
||||
var now = new Date((new Date(this.attendance.check_out)).valueOf() - (new Date()).getTimezoneOffset()*60*1000);
|
||||
self.return_to_main_menu = setTimeout( function() { self.do_action(self.next_action, {clear_breadcrumbs: true}); }, 5000);
|
||||
|
||||
if(this.previous_attendance_change_date){
|
||||
var last_check_in_date = new Date((new Date(this.previous_attendance_change_date)).valueOf() - (new Date()).getTimezoneOffset()*60*1000);
|
||||
if(now.valueOf() - last_check_in_date.valueOf() > 1000*60*60*12){
|
||||
self.$('.o_hr_attendance_warning_message').append(_t("Warning! Last check in was over 12 hours ago.<br/>If this isn't right, please contact Human Resources."));
|
||||
clearTimeout(self.return_to_main_menu);
|
||||
} else if(now.valueOf() - last_check_in_date.valueOf() > 1000*60*60*8){
|
||||
self.$('.o_hr_attendance_random_message').html(_t("Another good day's work! See you soon!"));
|
||||
}
|
||||
}
|
||||
|
||||
if(now.getHours()<12){
|
||||
self.$('.o_hr_attendance_message_message').append(_t("Have a good day!"));
|
||||
} else if(now.getHours()<14){
|
||||
self.$('.o_hr_attendance_message_message').append(_t("Have a nice lunch!"));
|
||||
if(Math.random() < 0.05){
|
||||
self.$('.o_hr_attendance_random_message').html(_t("Eat breakfast as a king, lunch as a merchant and supper as a beggar"));
|
||||
} else if(Math.random() < 0.06){
|
||||
self.$('.o_hr_attendance_random_message').html(_t("An apple a day keeps the doctor away"));
|
||||
}
|
||||
} else if(now.getHours()<17){
|
||||
self.$('.o_hr_attendance_message_message').append(_t("Have a good afternoon"));
|
||||
} else {
|
||||
if(now.getHours()<18 && Math.random()<0.2){
|
||||
self.$('.o_hr_attendance_message_message').append(_t("Early to bed and early to rise, makes a man healthy, wealthy and wise"));
|
||||
} else {
|
||||
self.$('.o_hr_attendance_message_message').append(_t("Have a good evening"));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
destroy: function () {
|
||||
clearTimeout(this.return_to_main_menu);
|
||||
this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
core.action_registry.add('hr_attendance_message', Message);
|
||||
|
||||
return Message;
|
||||
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
odoo.define('hr_attendance.MyMainMenu', function (require) {
|
||||
"use strict";
|
||||
|
||||
var core = require('web.core');
|
||||
var Model = require('web.Model');
|
||||
var Widget = require('web.Widget');
|
||||
|
||||
var QWeb = core.qweb;
|
||||
var _t = core._t;
|
||||
|
||||
|
||||
var MyMainMenu = Widget.extend({
|
||||
events: {
|
||||
"click .o_hr_attendance_sign_in_out_icon": function() {
|
||||
this.$('.o_hr_attendance_sign_in_out_icon').attr("disabled", "disabled");
|
||||
this.update_attendance();
|
||||
},
|
||||
},
|
||||
|
||||
start: function () {
|
||||
var self = this;
|
||||
|
||||
var hr_employee = new Model('hr.employee');
|
||||
hr_employee.query(['attendance_state', 'name'])
|
||||
.filter([['user_id', '=', self.session.uid]])
|
||||
.all()
|
||||
.then(function (res) {
|
||||
if (_.isEmpty(res) ) {
|
||||
self.$('.o_hr_attendance_employee').append(_t("Error : Could not find employee linked to user"));
|
||||
return;
|
||||
}
|
||||
self.employee = res[0];
|
||||
self.$el.html(QWeb.render("HrAttendanceMyMainMenu", {widget: self}));
|
||||
});
|
||||
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
update_attendance: function () {
|
||||
var self = this;
|
||||
var hr_employee = new Model('hr.employee');
|
||||
hr_employee.call('attendance_manual', [[self.employee.id], 'hr_attendance.hr_attendance_action_my_main_menu'])
|
||||
.then(function(result) {
|
||||
if (result.action) {
|
||||
self.do_action(result.action);
|
||||
} else if (result.warning) {
|
||||
self.do_warn(result.warning);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
core.action_registry.add('hr_attendance_my_main_menu', MyMainMenu);
|
||||
|
||||
return MyMainMenu;
|
||||
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
odoo.define('hr_attendance.presence_widgets', function (require) {
|
||||
"use strict";
|
||||
|
||||
var core = require('web.core');
|
||||
var form_common = require('web.form_common');
|
||||
var kanban_widgets = require('web_kanban.widgets');
|
||||
|
||||
var QWeb = core.qweb;
|
||||
var _t = core._t;
|
||||
|
||||
var FormPresenceIndicator = form_common.AbstractField.extend({
|
||||
init: function() {
|
||||
this._super.apply(this, arguments);
|
||||
},
|
||||
start: function() {
|
||||
this.display_field();
|
||||
// this.render_value(); -> gets called automatically in form_common.AbstractField
|
||||
this.$el.tooltip({title: _t("employee presence<br/>green: checked in<br/>red: checked out"), trigger: 'hover'});
|
||||
return this._super();
|
||||
},
|
||||
render_value: function() {
|
||||
this.$('.oe_hr_attendance_form_status').toggleClass("oe_hr_attendance_form_status_green", this.get_value() == 'checked_in');
|
||||
this.$('.oe_hr_attendance_form_status').toggleClass("oe_hr_attendance_form_status_red", this.get_value() == 'checked_out');
|
||||
},
|
||||
display_field: function() {
|
||||
this.$el.html(QWeb.render("FormPresenceIndicator"));
|
||||
},
|
||||
});
|
||||
|
||||
var KanbanPresenceIndicator = kanban_widgets.AbstractField.extend({
|
||||
init: function() {
|
||||
this._super.apply(this, arguments);
|
||||
},
|
||||
start: function() {
|
||||
this.display_field();
|
||||
this.render_value(); // doesn't get called automatically in kanban_widgets.AbstractField
|
||||
this.$el.tooltip({title: _t("employee presence<br/>green: checked in<br/>red: checked out"), trigger: 'hover'});
|
||||
return this._super();
|
||||
},
|
||||
render_value: function() {
|
||||
this.$('.oe_kanban_status').toggleClass("oe_kanban_status_green", this.field.raw_value == 'checked_in');
|
||||
this.$('.oe_kanban_status').toggleClass("oe_kanban_status_red", this.field.raw_value == 'checked_out');
|
||||
},
|
||||
display_field: function() {
|
||||
this.$el.html(QWeb.render("KanbanPresenceIndicator"));
|
||||
},
|
||||
});
|
||||
|
||||
core.form_widget_registry.add('hr_attendance_form_presence_indicator', FormPresenceIndicator);
|
||||
kanban_widgets.registry.add('hr_attendance_kanban_presence_indicator', KanbanPresenceIndicator);
|
||||
|
||||
});
|
||||
@@ -0,0 +1,211 @@
|
||||
.o_hr_attendance_sign_in_out_icon {
|
||||
font-size: 15em;
|
||||
cursor: pointer;
|
||||
margin: 20px 0px 20px 0px;
|
||||
padding: 0px 15px 0px 15px;
|
||||
border-radius: 10%;
|
||||
box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.oe_hr_attendance_form_status {
|
||||
height: .6em;
|
||||
width: .6em;
|
||||
border-radius: 50%;
|
||||
margin-right: .3em;
|
||||
display: inline-block;
|
||||
|
||||
background-color: @odoo-brand-secondary;
|
||||
|
||||
&.oe_hr_attendance_form_status_green {
|
||||
background-color: @brand-success;
|
||||
}
|
||||
&.oe_hr_attendance_form_status_red {
|
||||
background-color: @brand-danger;
|
||||
}
|
||||
}
|
||||
|
||||
.o_hr_attendance_back_button {
|
||||
box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.6);
|
||||
font-size: 3em;
|
||||
border-radius: 50%;
|
||||
height: 1.7em;
|
||||
width: 1.7em;
|
||||
position: absolute;
|
||||
left: auto;
|
||||
right: 0px;
|
||||
top: 0px;
|
||||
bottom: auto;
|
||||
|
||||
@media (min-width: @screen-xs-max) {
|
||||
transform: translate(50%, -50%);
|
||||
}
|
||||
}
|
||||
|
||||
.o_hr_attendance_validation {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.o_hr_attendance_btn-round-corners {
|
||||
border-radius: 10%;
|
||||
}
|
||||
|
||||
.o_hr_attendance_pin_pad_border {
|
||||
margin: 10px 0px 10px 0px;
|
||||
}
|
||||
|
||||
.o_hr_attendance_PINbox {
|
||||
background: white;
|
||||
border: 1px solid #d5d5d5;
|
||||
text-align: center;
|
||||
margin: 5px 0px 5px 0px;
|
||||
}
|
||||
|
||||
.o_hr_attendance_main_menu_container {
|
||||
.o-flex-display();
|
||||
.o-flex-flow(column, nowrap);
|
||||
.o-justify-content(center);
|
||||
.o-align-items(center);
|
||||
|
||||
@media (min-width: @screen-xs-max) {
|
||||
background: url("../../../../web_enterprise/static/src/img/application-switcher-bg.jpg") no-repeat center center fixed;
|
||||
background-size: cover;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.o_hr_attendance_warning_message {
|
||||
color: @brand-warning;
|
||||
}
|
||||
.o_hr_attendance_random_message {
|
||||
font-style: italic;
|
||||
}
|
||||
.o_hr_attendance_main_menu {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
background-color: #fff;
|
||||
padding: 2em;
|
||||
|
||||
h1 {
|
||||
margin: 0 0 2rem 0;
|
||||
}
|
||||
|
||||
.message_demo_barcodes {
|
||||
font-size: 0.9em;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
img {
|
||||
overflow:hidden; // prevent margins colapsing with h1
|
||||
margin-top: 3rem;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
p {
|
||||
text-align: left;
|
||||
margin: 3rem 0;
|
||||
}
|
||||
|
||||
> button {
|
||||
font-size: 1.2em;
|
||||
margin-bottom: 2rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
> button:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@media (min-width: @screen-xs-max) {
|
||||
flex: 0 0 auto;
|
||||
width: 550px;
|
||||
border-radius: 10px;
|
||||
background-color: rgba(255,255,255,0.8);
|
||||
box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.6);
|
||||
font-size: 1.2em;
|
||||
padding: 3em;
|
||||
}
|
||||
}
|
||||
|
||||
.o_hr_attendance_clock {
|
||||
font-size: 1.5em;
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
|
||||
@media (min-width: @screen-xs-max) {
|
||||
flex: 0 0 auto;
|
||||
border-radius: 3px;
|
||||
background-color: rgba(255,255,255,0.5);
|
||||
padding: 3px 10px 3px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.o_hr_attendance_message_container {
|
||||
.o-flex-display();
|
||||
.o-flex-flow(column, nowrap);
|
||||
.o-justify-content(center);
|
||||
.o-align-items(center);
|
||||
|
||||
@media (min-width: @screen-xs-max) {
|
||||
background: url("../../../../web_enterprise/static/src/img/application-switcher-bg.jpg") no-repeat center center fixed;
|
||||
background-size: cover;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.o_hr_attendance_message {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
background-color: #fff;
|
||||
padding: 2em;
|
||||
|
||||
h1 {
|
||||
margin: 0 0 2rem 0;
|
||||
}
|
||||
|
||||
.message_demo_barcodes {
|
||||
font-size: 0.9em;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
img {
|
||||
overflow:hidden; // prevent margins colapsing with h1
|
||||
margin-top: 3rem;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
p {
|
||||
text-align: left;
|
||||
margin: 3rem 0;
|
||||
}
|
||||
|
||||
> button {
|
||||
font-size: 1.2em;
|
||||
margin-bottom: 2rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
> button:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@media (min-width: @screen-xs-max) {
|
||||
flex: 0 0 auto;
|
||||
width: 550px;
|
||||
border-radius: 10px;
|
||||
background-color: rgba(255,255,255,0.8);
|
||||
box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.6);
|
||||
font-size: 1.2em;
|
||||
padding: 3em;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,132 @@
|
||||
<template>
|
||||
<t t-name="AttendanceSlider">
|
||||
<li class="oe_attendance_status" data-toggle="tooltip">
|
||||
<a href="#" class="oe_attendance_sign_in_out">
|
||||
<i id="oe_attendance_sign_in_out_icon" class="fa fa-sign-in"/>
|
||||
</a>
|
||||
</li>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<template xml:space="preserve">
|
||||
<t t-name="KanbanPresenceIndicator">
|
||||
<div class="oe_kanban_status">
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="FormPresenceIndicator">
|
||||
<div class="oe_hr_attendance_form_status">
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="HrAttendanceMainMenu">
|
||||
<div class="o_hr_attendance_main_menu_container">
|
||||
<div class="o_hr_attendance_main_menu">
|
||||
<h1>Welcome to <t t-esc="widget.company_name"/></h1>
|
||||
<img t-attf-src="{{widget.company_image_url}}" alt="Company Logo"/>
|
||||
<h2>Scan your badge</h2>
|
||||
<h4>or</h4>
|
||||
<button class="o_hr_attendance_button_employees btn btn-primary btn-lg">Select Employee</button>
|
||||
</div>
|
||||
<div class="o_hr_attendance_clock"></div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="HrAttendanceMyMainMenu">
|
||||
<div class="o_hr_attendance_main_menu_container">
|
||||
<div class="o_hr_attendance_main_menu">
|
||||
<t t-if="widget.employee">
|
||||
<h1>Welcome <t t-esc="widget.employee.name"/></h1>
|
||||
<t t-if="widget.employee.attendance_state=='checked_out'">
|
||||
<h2>Click to check in</h2>
|
||||
<i class="fa fa-sign-in btn-primary o_hr_attendance_sign_in_out_icon"/>
|
||||
</t>
|
||||
<t t-if="widget.employee.attendance_state=='checked_in'">
|
||||
<h2>Click to check out</h2>
|
||||
<i class="fa fa-sign-out btn-primary o_hr_attendance_sign_in_out_icon"/>
|
||||
</t>
|
||||
</t>
|
||||
<t t-if="!widget.employee">
|
||||
Error : Could not find employee linked to user.
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="HrAttendanceMainMenuConfirm">
|
||||
<div class="o_hr_attendance_main_menu_container">
|
||||
<div class="o_hr_attendance_main_menu">
|
||||
<t t-if="widget.employee_id">
|
||||
<h1>Welcome <t t-esc="widget.employee_name"/></h1>
|
||||
<t t-if="!widget.use_pin">
|
||||
<t t-if="widget.employee_state=='checked_in'">
|
||||
<h2>Check out</h2>
|
||||
<a class="fa btn-primary o_hr_attendance_sign_in_out_icon fa-sign-out fa-sign-in"/>
|
||||
</t>
|
||||
<t t-if="widget.employee_state=='checked_out'">
|
||||
<h2>Check in</h2>
|
||||
<a class="fa btn-primary o_hr_attendance_sign_in_out_icon fa-sign-out fa-sign-out"/>
|
||||
</t>
|
||||
</t>
|
||||
<t t-if="widget.use_pin">
|
||||
<t t-if="widget.employee_state=='checked_in'">
|
||||
<h2>Please enter your PIN to check out</h2>
|
||||
</t>
|
||||
<t t-if="widget.employee_state=='checked_out'">
|
||||
<h2>Please enter your PIN to check in</h2>
|
||||
</t>
|
||||
<div class="row">
|
||||
<div class="col-sm-8 col-sm-offset-2">
|
||||
<div class="row" >
|
||||
<div class="col-xs-4 col-xs-offset-4"><input class="o_hr_attendance_PINbox" type="password" disabled="true"/></div>
|
||||
</div>
|
||||
<div class="row o_hr_attendance_pin_pad">
|
||||
<t t-foreach="['1', '2', '3', '4', '5', '6', '7', '8', '9', 'C', '0', 'ok']" t-as="btn_name">
|
||||
<div class="col-xs-4 o_hr_attendance_pin_pad_border">
|
||||
<a t-attf-class="btn btn-primary btn-block btn-lg o_hr_attendance_btn-round-corners {{ 'o_hr_attendance_pin_pad_button_' + btn_name }}"><t t-esc="btn_name"/></a>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
</t>
|
||||
</t>
|
||||
<t t-if="!widget.employee_id">
|
||||
Error: could not find corresponding employee.<br/>Please return to the main menu.
|
||||
</t>
|
||||
<div class="oe_attendance_sign_in_out">
|
||||
</div>
|
||||
<div class="o_hr_attendance_back_button btn btn-danger fa fa-times"/>
|
||||
</div>
|
||||
<div class="o_hr_attendance_clock"></div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="HrAttendanceMessage">
|
||||
<div class="o_hr_attendance_message_container">
|
||||
<div class="o_hr_attendance_message">
|
||||
<t t-if="widget.attendance">
|
||||
<t t-if="widget.attendance.check_out">
|
||||
<h1 class="o_hr_attendance_validation">
|
||||
Checked out at <t t-esc="widget.attendance.check_out_time"/>
|
||||
</h1>
|
||||
<h2 class="o_hr_attendance_message_message">
|
||||
Goodbye <t t-esc="widget.employee_name"/>
|
||||
<br/>
|
||||
</h2>
|
||||
<h3 class="o_hr_attendance_random_message"></h3>
|
||||
<h2 class="o_hr_attendance_warning_message"></h2>
|
||||
|
||||
</t>
|
||||
<t t-if="!widget.attendance.check_out">
|
||||
<h1 class="o_hr_attendance_validation">
|
||||
Checked in at <t t-esc="widget.attendance.check_in_time"/>
|
||||
</h1>
|
||||
<h2 class="o_hr_attendance_message_message">
|
||||
Welcome <t t-esc="widget.employee_name"/>
|
||||
<br/>
|
||||
</h2>
|
||||
<h3 class="o_hr_attendance_random_message"></h3>
|
||||
<h2 class="o_hr_attendance_warning_message"></h2>
|
||||
</t>
|
||||
</t>
|
||||
<t t-if="!widget.attendance">
|
||||
Invalid request, please return to the main menu.<br/>
|
||||
</t>
|
||||
<button class="o_hr_attendance_button_dismiss btn btn-primary btn-sm">ok</button>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
-
|
||||
Create a user as 'HR Attendance Officer'
|
||||
-
|
||||
!record {model: res.users, id: res_users_attendance_officer, view: False}:
|
||||
company_id: base.main_company
|
||||
name: HR Officer
|
||||
login: ao
|
||||
-
|
||||
I added groups for HR Attendance Officer.
|
||||
-
|
||||
!record {model: res.users, id: res_users_attendance_officer}:
|
||||
groups_id:
|
||||
- base.group_hr_user
|
||||
-
|
||||
Give the access rights of Hr Officer to test attendance process.
|
||||
-
|
||||
!context
|
||||
uid: 'res_users_attendance_officer'
|
||||
-
|
||||
In order to test attendance process in OpenERP, I entry of SignIn of employee.
|
||||
-
|
||||
!python {model: hr.employee}: |
|
||||
self.attendance_action_change(cr, uid, [ref("hr.employee_al")], context=context)
|
||||
-
|
||||
I check that Employee is "Present".
|
||||
-
|
||||
!assert {model: hr.employee, id: hr.employee_al, severity: error, string: Employee should be in present state}:
|
||||
- state == 'present'
|
||||
-
|
||||
After few seconds, employee sign's out.
|
||||
-
|
||||
!python {model: hr.employee}: |
|
||||
import time
|
||||
time.sleep(2)
|
||||
self.attendance_action_change(cr, uid, [ref("hr.employee_al")], context=context)
|
||||
-
|
||||
I check that Employee is "Absent".
|
||||
-
|
||||
!assert {model: hr.employee, id: hr.employee_al, severity: error, string: Employee should be in absent state}:
|
||||
- state == 'absent'
|
||||
-
|
||||
In order to check that first attendance must be Sign In.
|
||||
-
|
||||
!python {model: hr.attendance}: |
|
||||
import time
|
||||
try:
|
||||
self.create(cr, uid, {'employee_id': ref('hr.employee_niv'), 'name': time.strftime('%Y-%m-%d 09:59:25'), 'action': 'sign_out'}, None)
|
||||
except openerp.exceptions.ValidationError:
|
||||
pass
|
||||
-
|
||||
First of all, Employee Sign's In.
|
||||
-
|
||||
!record {model: hr.attendance, id: hr_attendance_0}:
|
||||
employee_id: hr.employee_niv
|
||||
name: !eval time.strftime('%Y-%m-%d 09:59:25')
|
||||
action: 'sign_in'
|
||||
-
|
||||
Now Employee is going to Sign In prior to First Sign In.
|
||||
-
|
||||
!python {model: hr.attendance}: |
|
||||
import time
|
||||
try:
|
||||
self.create(cr, uid, {'employee_id': ref('hr.employee_niv'), 'name': time.strftime('%Y-%m-%d 08:59:25'), 'action': 'sign_in'}, None)
|
||||
except openerp.exceptions.ValidationError:
|
||||
pass
|
||||
-
|
||||
After that Employee is going to Sign In after First Sign In.
|
||||
-
|
||||
!python {model: hr.attendance}: |
|
||||
import time
|
||||
try:
|
||||
self.create(cr, uid, {'employee_id': ref('hr.employee_niv'), 'name': time.strftime('%Y-%m-%d 10:59:25'), 'action': 'sign_in'}, None)
|
||||
except openerp.exceptions.ValidationError:
|
||||
pass
|
||||
-
|
||||
After two hours, Employee Sign's Out.
|
||||
-
|
||||
!record {model: hr.attendance, id: hr_attendance_1}:
|
||||
employee_id: hr.employee_niv
|
||||
name: !eval time.strftime('%Y-%m-%d 11:59:25')
|
||||
action: 'sign_out'
|
||||
-
|
||||
Now Employee is going to Sign Out prior to First Sign Out.
|
||||
-
|
||||
!python {model: hr.attendance}: |
|
||||
import time
|
||||
try:
|
||||
self.create(cr, uid, {'employee_id': ref('hr.employee_niv'), 'name': time.strftime('%Y-%m-%d 10:59:25'), 'action': 'sign_out'}, None)
|
||||
except openerp.exceptions.ValidationError:
|
||||
pass
|
||||
-
|
||||
After that Employee is going to Sign Out After First Sign Out.
|
||||
-
|
||||
!python {model: hr.attendance}: |
|
||||
import time
|
||||
try:
|
||||
self.create(cr, uid, {'employee_id': ref('hr.employee_niv'), 'name': time.strftime('%Y-%m-%d 12:59:25'), 'action': 'sign_out'}, None)
|
||||
except openerp.exceptions.ValidationError:
|
||||
pass
|
||||
@@ -1,9 +0,0 @@
|
||||
-
|
||||
Print HR Attendance Error Report through the wizard
|
||||
-
|
||||
!python {model: hr.employee}: |
|
||||
import time
|
||||
from openerp.tools import test_reports
|
||||
ctx = {'model': 'hr.employee','active_ids': [ref('hr.employee_root')]}
|
||||
data_dict = {'init_date': time.strftime('%Y-01-01')}
|
||||
test_reports.try_report_action(cr, uid, 'action_hr_attendance_error',wiz_data=data_dict, context=ctx, our_module='hr_attendance')
|
||||
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import test_hr_attendance_constraints
|
||||
import test_hr_attendance_process
|
||||
@@ -0,0 +1,96 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo.tests.common import TransactionCase
|
||||
import time
|
||||
|
||||
|
||||
class TestHrAttendance(TransactionCase):
|
||||
"""Tests for attendance date ranges validity"""
|
||||
|
||||
def setUp(self):
|
||||
super(TestHrAttendance, self).setUp()
|
||||
self.attendance = self.env['hr.attendance']
|
||||
self.test_employee = self.browse_ref('hr.employee_qdp')
|
||||
# demo data contains set up for self.test_employee
|
||||
|
||||
def test_attendance_in_before_out(self):
|
||||
# Make sure check_out is before check_in
|
||||
with self.assertRaises(Exception):
|
||||
self.my_attend = self.attendance.create({
|
||||
'employee_id': self.test_employee.id,
|
||||
'check_in': time.strftime('%Y-%m-10 12:00'),
|
||||
'check_out': time.strftime('%Y-%m-10 11:00'),
|
||||
})
|
||||
|
||||
def test_attendance_no_check_out(self):
|
||||
# Make sure no second attandance without check_out can be created
|
||||
self.attendance.create({
|
||||
'employee_id': self.test_employee.id,
|
||||
'check_in': time.strftime('%Y-%m-10 10:00'),
|
||||
})
|
||||
with self.assertRaises(Exception):
|
||||
self.attendance.create({
|
||||
'employee_id': self.test_employee.id,
|
||||
'check_in': time.strftime('%Y-%m-10 11:00'),
|
||||
})
|
||||
|
||||
# 5 next tests : Make sure that when attendances overlap an error is raised
|
||||
def test_attendance_1(self):
|
||||
with self.assertRaises(Exception):
|
||||
self.attendance.create({
|
||||
'employee_id': self.test_employee.id,
|
||||
'check_in': time.strftime('%Y-%m-10 08:30'),
|
||||
'check_out': time.strftime('%Y-%m-10 09:30'),
|
||||
})
|
||||
|
||||
def test_attendance_2(self):
|
||||
with self.assertRaises(Exception):
|
||||
self.attendance.create({
|
||||
'employee_id': self.test_employee.id,
|
||||
'check_in': time.strftime('%Y-%m-10 07:30'),
|
||||
'check_out': time.strftime('%Y-%m-10 08:30'),
|
||||
})
|
||||
|
||||
def test_attendance_3(self):
|
||||
with self.assertRaises(Exception):
|
||||
self.attendance.create({
|
||||
'employee_id': self.test_employee.id,
|
||||
'check_in': time.strftime('%Y-%m-10 07:30'),
|
||||
'check_out': time.strftime('%Y-%m-10 09:30'),
|
||||
})
|
||||
|
||||
def test_attendance_4(self):
|
||||
with self.assertRaises(Exception):
|
||||
self.attendance.create({
|
||||
'employee_id': self.test_employee.id,
|
||||
'check_in': time.strftime('%Y-%m-10 08:15'),
|
||||
'check_out': time.strftime('%Y-%m-10 08:45'),
|
||||
})
|
||||
|
||||
def test_attendance_5(self):
|
||||
self.attendance.create({
|
||||
'employee_id': self.test_employee.id,
|
||||
'check_in': time.strftime('%Y-%m-10 10:00'),
|
||||
})
|
||||
with self.assertRaises(Exception):
|
||||
self.attendance.create({
|
||||
'employee_id': self.test_employee.id,
|
||||
'check_in': time.strftime('%Y-%m-10 09:30'),
|
||||
'check_out': time.strftime('%Y-%m-10 10:30'),
|
||||
})
|
||||
|
||||
def test_new_attendances(self):
|
||||
# Make sure attendance modification raises an error when it causes an overlap
|
||||
self.attendance.create({
|
||||
'employee_id': self.test_employee.id,
|
||||
'check_in': time.strftime('%Y-%m-10 11:00'),
|
||||
'check_out': time.strftime('%Y-%m-10 12:00'),
|
||||
})
|
||||
open_attendance = self.attendance.create({
|
||||
'employee_id': self.test_employee.id,
|
||||
'check_in': time.strftime('%Y-%m-10 10:00'),
|
||||
})
|
||||
with self.assertRaises(Exception):
|
||||
open_attendance.write({
|
||||
'check_out': time.strftime('%Y-%m-10 11:30'),
|
||||
})
|
||||
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
|
||||
class TestHrAttendance(TransactionCase):
|
||||
"""Test for presence validity"""
|
||||
|
||||
def setUp(self):
|
||||
super(TestHrAttendance, self).setUp()
|
||||
self.test_employee = self.browse_ref('hr.employee_al')
|
||||
|
||||
def test_employee_state(self):
|
||||
# Make sure the attendance of the employee will display correctly
|
||||
assert self.test_employee.attendance_state == 'checked_out'
|
||||
self.test_employee.attendance_action_change()
|
||||
assert self.test_employee.attendance_state == 'checked_in'
|
||||
self.test_employee.attendance_action_change()
|
||||
assert self.test_employee.attendance_state == 'checked_out'
|
||||
@@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<template id="assets_backend" name="hr_attendance assets" inherit_id="web.assets_backend">
|
||||
<xpath expr="." position="inside">
|
||||
<script type="text/javascript" src="/hr_attendance/static/src/js/attendance.js"></script>
|
||||
<link rel="stylesheet" href="/hr_attendance/static/src/css/attendance.css"/>
|
||||
</xpath>
|
||||
</template>
|
||||
</data>
|
||||
</openerp>
|
||||
@@ -0,0 +1,144 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- views -->
|
||||
|
||||
<record id="view_attendance_tree" model="ir.ui.view">
|
||||
<field name="name">hr.attendance.tree</field>
|
||||
<field name="model">hr.attendance</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Employee attendances">
|
||||
<field name="employee_id"/>
|
||||
<field name="check_in"/>
|
||||
<field name="check_out"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_attendance_view_form" model="ir.ui.view">
|
||||
<field name="name">hr.attendance.form</field>
|
||||
<field name="model">hr.attendance</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Employee attendances">
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="employee_id"/>
|
||||
<field name="check_in"/>
|
||||
<field name="check_out"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_attendance_view_filter" model="ir.ui.view">
|
||||
<field name="name">hr_attendance_view_filter</field>
|
||||
<field name="model">hr.attendance</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Hr Attendance Search">
|
||||
<field name="employee_id"/>
|
||||
<field name="department_id"/>
|
||||
<filter name="today" string="Today" domain="[('check_in', '>=', datetime.datetime.now().replace(hour=0, minute=0, second=0)),('check_in', '<=', datetime.datetime.now().replace(hour=23, minute=59, second=59))]" />
|
||||
<filter string="Current Month" domain="[('check_in', '>=', datetime.datetime.now().strftime('%Y-%m-01'))]" />
|
||||
<separator/>
|
||||
<filter string="No Check Out" domain="[('check_out', '=', False)]" />
|
||||
<separator/>
|
||||
<filter string="My Attendances" domain="[('employee_id.user_id.id', '=', uid)]" />
|
||||
<group expand="0" string="Group By">
|
||||
<filter name="employee" string="Employee" context="{'group_by':'employee_id'}"/>
|
||||
<separator/>
|
||||
<filter name="groupby_name" string="Month" context="{'group_by':'check_in'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_attendance_view_pivot" model="ir.ui.view">
|
||||
<field name="name">hr.attendance.pivot</field>
|
||||
<field name="model">hr.attendance</field>
|
||||
<field name="arch" type="xml">
|
||||
<pivot string="Attendance">
|
||||
<field name="employee_id" type="row"/>
|
||||
<field name="check_in" type="col"/>
|
||||
<field name="worked_hours" type="measure"/>
|
||||
</pivot>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_attendance_view_graph" model="ir.ui.view">
|
||||
<field name="name">hr.attendance.graph</field>
|
||||
<field name="model">hr.attendance</field>
|
||||
<field name="arch" type="xml">
|
||||
<graph string="Attendance">
|
||||
<field name="employee_id"/>
|
||||
<field name="check_in"/>
|
||||
<field name="worked_hours" type="measure"/>
|
||||
</graph>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- actions -->
|
||||
|
||||
<record id="hr_attendance_action" model="ir.actions.act_window">
|
||||
<field name="name">Attendances</field>
|
||||
<field name="res_model">hr.attendance</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="context">{"search_default_today":1}</field>
|
||||
<field name="search_view_id" ref="hr_attendance_view_filter" />
|
||||
<field name="help">The Time Tracking functionality aims to manage employee attendances from Check in/Check out actions. You can also link this feature to an attendance device using Odoo's web service features.</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_attendance_action_graph" model="ir.actions.act_window">
|
||||
<field name="name">Attendance Analysis</field>
|
||||
<field name="res_model">hr.attendance</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">pivot,graph</field>
|
||||
<field name="view_id"></field> <!-- force empty -->
|
||||
</record>
|
||||
|
||||
<record id="hr_attendance_action_graph_filtered" model="ir.actions.act_window">
|
||||
<field name="name">Attendance Analysis</field>
|
||||
<field name="res_model">hr.attendance</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">pivot,graph</field>
|
||||
<field name="context">{
|
||||
'search_default_department_id': [active_id],
|
||||
'default_department_id': active_id}
|
||||
</field>
|
||||
<field name="view_id"></field> <!-- force empty -->
|
||||
</record>
|
||||
|
||||
<record id="hr_attendance_action_main_menu" model="ir.actions.client">
|
||||
<field name="name">Attendances</field>
|
||||
<field name="tag">hr_attendance_main_menu</field>
|
||||
<field name="target">fullscreen</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_attendance_action_my_main_menu" model="ir.actions.client">
|
||||
<field name="name">Attendance</field>
|
||||
<field name="tag">hr_attendance_my_main_menu</field>
|
||||
<field name="target">main</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_attendance_action_message" model="ir.actions.client">
|
||||
<field name="name">Message</field>
|
||||
<field name="tag">hr_attendance_message</field>
|
||||
</record>
|
||||
|
||||
<!-- Menus -->
|
||||
|
||||
<menuitem id="menu_hr_attendance_root" name="Attendances" sequence="90" groups="base.group_hr_attendance" web_icon="hr_attendance,static/description/icon.png"/>
|
||||
|
||||
<menuitem id="menu_hr_attendance_my_attendances" name="My Attendances" parent="menu_hr_attendance_root" sequence="10" groups="base.group_hr_attendance" action="hr_attendance_action_my_main_menu"/>
|
||||
|
||||
<menuitem id="menu_hr_attendance_manage_attendances" name="Manage Attendances" parent="menu_hr_attendance_root" sequence="20" groups="base.group_hr_user"/>
|
||||
|
||||
<menuitem id="menu_hr_attendance_view_attendances" name="Attendances" parent="menu_hr_attendance_manage_attendances" sequence="10" groups="base.group_hr_user" action="hr_attendance_action"/>
|
||||
|
||||
<menuitem id="menu_hr_attendance_view_employees_kanban" name="Employees" parent="menu_hr_attendance_manage_attendances" sequence="15" groups="base.group_hr_user" action="hr.open_view_employee_list_my"/>
|
||||
|
||||
<menuitem id="menu_hr_attendance_Kiosk_mode" name="Kiosk Mode" parent="menu_hr_attendance_manage_attendances" sequence="20" groups="base.group_hr_user" action="hr_attendance_action_main_menu"/>
|
||||
|
||||
<menuitem id="menu_hr_attendance_report" name="Reports" parent="menu_hr_attendance_root" sequence="30" groups="base.group_hr_user" action="hr_attendance_action_graph"/>
|
||||
</odoo>
|
||||
+2
-1
@@ -1,3 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!--Hr Department Inherit Kanban view-->
|
||||
<record id="hr_department_view_kanban" model="ir.ui.view">
|
||||
@@ -10,7 +11,7 @@
|
||||
<xpath expr="//div[contains(@class, 'o_kanban_manage_reports')]" position="inside">
|
||||
<div class="row">
|
||||
<div class="col-xs-12 text-left">
|
||||
<a name="%(action_hr_attendance_graph_filtered)d" type="action">
|
||||
<a name="%(hr_attendance_action_graph_filtered)d" type="action">
|
||||
Attendances
|
||||
</a>
|
||||
</div>
|
||||
@@ -0,0 +1,86 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<record id="view_employee_form_inherit_hr_attendance" model="ir.ui.view">
|
||||
<field name="name">hr.employee</field>
|
||||
<field name="model">hr.employee</field>
|
||||
<field name="inherit_id" ref="hr.view_employee_form"/>
|
||||
<field name="priority">10</field>
|
||||
<field name="arch" type="xml">
|
||||
<field name="name" position="replace">
|
||||
<div id="textbox">
|
||||
<field name="attendance_state" widget="hr_attendance_form_presence_indicator"/>
|
||||
<field name="name" placeholder="Employee's Name"/>
|
||||
</div>
|
||||
</field>
|
||||
<field name="user_id" position="after">
|
||||
<field name="barcode" groups="base.group_hr_user"/>
|
||||
<field name="pin" groups="base.group_hr_user,base.group_hr_attendance_use_pin"/>
|
||||
<field name="manual_attendance" groups="base.group_hr_user"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- employee kanban view specifically for hr_attendance (to check in/out) -->
|
||||
<record id="hr_employees_view_kanban" model="ir.ui.view">
|
||||
<field name="name">hr.employee.kanban</field>
|
||||
<field name="model">hr.employee</field>
|
||||
<field name="priority">99</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban class="o_hr_employee_attendance_kanban" create="false">
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div class="oe_kanban_global_click">
|
||||
<div class="o_kanban_image">
|
||||
<img t-att-src="kanban_image('hr.employee', 'image_medium', record.id.value)"/>
|
||||
</div>
|
||||
<div class="oe_kanban_details">
|
||||
<div id="textbox">
|
||||
<div class="col-xs-11" style="padding:0px">
|
||||
<strong>
|
||||
<field name="name"/>
|
||||
</strong>
|
||||
</div><div class="col-xs-1" style="padding:0px">
|
||||
<field name="attendance_state" widget="hr_attendance_kanban_presence_indicator"/>
|
||||
</div>
|
||||
</div>
|
||||
<ul>
|
||||
<li t-if="record.job_id.raw_value"><field name="job_id"/></li>
|
||||
<li t-if="record.work_location.raw_value"><field name="work_location"/></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_employee_kanban_inherit_hr_attendance" model="ir.ui.view">
|
||||
<field name="name">hr.employee</field>
|
||||
<field name="model">hr.employee</field>
|
||||
<field name="inherit_id" ref="hr.hr_kanban_view_employees"/>
|
||||
<field name="priority">1</field>
|
||||
<field name="arch" type="xml">
|
||||
<field name="name" position="replace">
|
||||
<div id="row">
|
||||
<div class="col-xs-11" style="padding:0px">
|
||||
<strong>
|
||||
<field name="name" placeholder="Employee's Name"/>
|
||||
</strong>
|
||||
</div>
|
||||
<div class="col-xs-1" style="padding:0px">
|
||||
<field name="attendance_state" widget="hr_attendance_kanban_presence_indicator"/>
|
||||
</div>
|
||||
</div>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_employee_attendance_action_kanban" model="ir.actions.act_window">
|
||||
<field name="name">Employees</field>
|
||||
<field name="res_model">hr.employee</field>
|
||||
<field name="view_mode">kanban</field>
|
||||
<field name="view_id" ref="hr_employees_view_kanban"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -1,45 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<template id="report_attendanceerrors">
|
||||
<t t-call="report.html_container">
|
||||
<t t-foreach="get_employees(data['form']['emp_ids'])" t-as="employee">
|
||||
<t t-call="report.internal_layout">
|
||||
<div class="page">
|
||||
<div class="oe_structure"/>
|
||||
<h2>Attendance Errors: <span t-esc="employee.name"/></h2>
|
||||
|
||||
<table class="table table-condensed mt32">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Operation</th>
|
||||
<th>Date Signed</th>
|
||||
<th>Date Recorded</th>
|
||||
<th>Delay</th>
|
||||
<th>Min Delay</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr t-foreach="lst(employee.id,data['form']['init_date'], data['form']['end_date'], data['form']['max_delay'])" t-as="att">
|
||||
<td><span t-esc="att['action']"/></td>
|
||||
<td><span t-esc="formatLang(att['date'],date_time=True)"/></td>
|
||||
<td><span t-esc="formatLang(att['create_date'],date_time=True)"/></td>
|
||||
<td><span t-esc="att['delay']"/></td>
|
||||
<td><span t-esc="att['delay2']"/></td>
|
||||
</tr>
|
||||
<tr class="border-black" t-foreach="total(employee.id,data['form']['init_date'], data['form']['end_date'], data['form']['max_delay'])" t-as="total">
|
||||
<td colspan="3"><strong>Total period</strong></td>
|
||||
<td><strong t-esc="total['total']"/></td>
|
||||
<td><strong t-esc="total['total2']"/></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>(*) A positive delay means that the employee worked less than recorded.<br/>
|
||||
(*) A negative delay means that the employee worked more than encoded.</p>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
</data>
|
||||
</openerp>
|
||||
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="hr_attendance_view_config" model="ir.ui.view">
|
||||
<field name="name">base.config.setting.hr.form.attendance</field>
|
||||
<field name="model">base.config.settings</field>
|
||||
<field name="priority">99</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Configuration" class= "oe_form_configuration">
|
||||
<header>
|
||||
<button string="Apply" type="object" name="execute" class="oe_highlight"/>
|
||||
<button string="Cancel" type="object" name="cancel" class="oe_link"/>
|
||||
</header>
|
||||
<group string="Settings">
|
||||
<field name="group_attendance_use_pin" widget="radio"/>
|
||||
</group>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_hr_attendance_settings" model="ir.actions.act_window">
|
||||
<field name="name">Attendance Settings</field>
|
||||
<field name="res_model">base.config.settings</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">inline</field>
|
||||
<field name="view_id" ref="hr_attendance_view_config"/>
|
||||
</record>
|
||||
|
||||
<menuitem id="hr_attendance.menu_hr_attendance_settings" name="Configuration" parent="menu_hr_attendance_root"
|
||||
sequence="99" action="action_hr_attendance_settings" groups="base.group_hr_manager"/>
|
||||
</odoo>
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<template id="assets_backend" name="hr_attendance assets" inherit_id="web.assets_backend">
|
||||
<xpath expr="." position="inside">
|
||||
<script type="text/javascript" src="/hr_attendance/static/src/js/employee_kanban_view_handler.js"></script>
|
||||
<script type="text/javascript" src="/hr_attendance/static/src/js/presence_widgets.js"></script>
|
||||
<script type="text/javascript" src="/hr_attendance/static/src/js/message.js"></script>
|
||||
<script type="text/javascript" src="/hr_attendance/static/src/js/main_menu.js"></script>
|
||||
<script type="text/javascript" src="/hr_attendance/static/src/js/main_menu_confirm.js"></script>
|
||||
<script type="text/javascript" src="/hr_attendance/static/src/js/my_main_menu.js"></script>
|
||||
<link rel="stylesheet" href="/hr_attendance/static/src/less/hr_attendance.less"/>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
||||
@@ -1,4 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import hr_attendance_error
|
||||
@@ -1,48 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
import time
|
||||
|
||||
from openerp.osv import fields, osv
|
||||
from openerp.tools.translate import _
|
||||
from openerp.exceptions import UserError
|
||||
|
||||
|
||||
class hr_attendance_error(osv.osv_memory):
|
||||
|
||||
_name = 'hr.attendance.error'
|
||||
_description = 'Print Error Attendance Report'
|
||||
_columns = {
|
||||
'init_date': fields.date('Starting Date', required=True),
|
||||
'end_date': fields.date('Ending Date', required=True),
|
||||
'max_delay': fields.integer('Maximum Tolerance (in minutes)', required=True,
|
||||
help="Allowed difference in minutes between the signin/signout and the timesheet computation for one sheet. Set this to 0 for no tolerance.")
|
||||
}
|
||||
_defaults = {
|
||||
'init_date': lambda *a: time.strftime('%Y-%m-%d'),
|
||||
'end_date': lambda *a: time.strftime('%Y-%m-%d'),
|
||||
'max_delay': 120,
|
||||
}
|
||||
|
||||
def print_report(self, cr, uid, ids, context=None):
|
||||
emp_ids = []
|
||||
data_error = self.read(cr, uid, ids, context=context)[0]
|
||||
date_from = data_error['init_date']
|
||||
date_to = data_error['end_date']
|
||||
cr.execute("SELECT id FROM hr_attendance WHERE employee_id IN %s AND to_char(name,'YYYY-mm-dd')<=%s AND to_char(name,'YYYY-mm-dd')>=%s AND action IN %s ORDER BY name" ,(tuple(context['active_ids']), date_to, date_from, tuple(['sign_in','sign_out'])))
|
||||
attendance_ids = [x[0] for x in cr.fetchall()]
|
||||
if not attendance_ids:
|
||||
raise UserError(_('No records are found for your selection!'))
|
||||
attendance_records = self.pool.get('hr.attendance').browse(cr, uid, attendance_ids, context=context)
|
||||
|
||||
for rec in attendance_records:
|
||||
if rec.employee_id.id not in emp_ids:
|
||||
emp_ids.append(rec.employee_id.id)
|
||||
data_error['emp_ids'] = emp_ids
|
||||
datas = {
|
||||
'ids': [],
|
||||
'model': 'hr.employee',
|
||||
'form': data_error
|
||||
}
|
||||
return self.pool['report'].get_action(
|
||||
cr, uid, [], 'hr_attendance.report_attendanceerrors', data=datas, context=context
|
||||
)
|
||||
@@ -1,43 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
|
||||
<record id="view_hr_attendance_error" model="ir.ui.view">
|
||||
<field name="name">Attendance Report Error</field>
|
||||
<field name="model">hr.attendance.error</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Print Attendance Report Error">
|
||||
<group string="Analysis Information" col="4">
|
||||
<field name="init_date"/>
|
||||
<field name="end_date"/>
|
||||
<field name="max_delay"/>
|
||||
</group>
|
||||
<label string="Below the maximum tolerance, the attendance error will not be taken into account. Above the maximum tolerance, the error is considered to be voluntary and will be taken into account." colspan="2"/>
|
||||
<footer>
|
||||
<button name="print_report" string="Print" type="object" class="btn-primary"/>
|
||||
<button string="Cancel" class="btn-default" special="cancel" />
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_hr_attendance_error" model="ir.actions.act_window">
|
||||
<field name="name">Attendance Error Report</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">hr.attendance.error</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.values" id="hr_attendance_error_values">
|
||||
<field name="model_id" ref="hr.model_hr_employee" />
|
||||
<field name="name">Attendance Error Report</field>
|
||||
<field name="key2">client_print_multi</field>
|
||||
<field name="value" eval="'ir.actions.act_window,' + str(ref('action_hr_attendance_error'))" />
|
||||
<field name="key">action</field>
|
||||
<field name="model">hr.employee</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
@@ -2,11 +2,12 @@
|
||||
Creating demo data that require looking for accounts
|
||||
-
|
||||
!python {model: ir.model.data} : |
|
||||
import time
|
||||
from datetime import datetime
|
||||
from dateutil.relativedelta import relativedelta
|
||||
vals = {
|
||||
'name': 'Requirements analysis and specification',
|
||||
'user_id': ref('base.user_root'),
|
||||
'date': time.strftime('%Y-%m-%d'),
|
||||
'date': (datetime.now()+relativedelta(days=-1)).strftime('%Y-%m-%d'),
|
||||
'unit_amount': 2.00,
|
||||
'product_id': ref('product.product_product_1'),
|
||||
'product_uom_id': ref('product.product_uom_hour'),
|
||||
@@ -17,7 +18,7 @@
|
||||
vals = {
|
||||
'name': 'Design and specification',
|
||||
'user_id': ref('base.user_root'),
|
||||
'date': time.strftime('%Y-%m-%d'),
|
||||
'date': (datetime.now()+relativedelta(days=-1)).strftime('%Y-%m-%d'),
|
||||
'unit_amount': 1.00,
|
||||
'product_id': ref('product.product_product_1'),
|
||||
'product_uom_id': ref('product.product_uom_hour'),
|
||||
@@ -28,7 +29,7 @@
|
||||
vals = {
|
||||
'name': 'Coding and module testing',
|
||||
'user_id': ref('base.user_root'),
|
||||
'date': time.strftime('%Y-%m-%d'),
|
||||
'date': (datetime.now()+relativedelta(days=-1)).strftime('%Y-%m-%d'),
|
||||
'unit_amount': 3.00,
|
||||
'product_id': ref('product.product_product_1'),
|
||||
'product_uom_id': ref('product.product_uom_hour'),
|
||||
@@ -39,7 +40,7 @@
|
||||
vals = {
|
||||
'name': 'Integration and system testing',
|
||||
'user_id': ref('base.user_root'),
|
||||
'date': time.strftime('%Y-%m-%d'),
|
||||
'date': (datetime.now()+relativedelta(days=-1)).strftime('%Y-%m-%d'),
|
||||
'unit_amount': 1.00,
|
||||
'product_id': ref('product.product_product_1'),
|
||||
'product_uom_id': ref('product.product_uom_hour'),
|
||||
@@ -50,7 +51,7 @@
|
||||
vals = {
|
||||
'name': 'Delivery and maintenance',
|
||||
'user_id': ref('base.user_root'),
|
||||
'date': time.strftime('%Y-%m-%d'),
|
||||
'date': (datetime.now()+relativedelta(days=-1)).strftime('%Y-%m-%d'),
|
||||
'unit_amount': 1.00,
|
||||
'product_id': ref('product.product_product_1'),
|
||||
'product_uom_id': ref('product.product_uom_hour'),
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import report
|
||||
import models
|
||||
import report
|
||||
@@ -2,12 +2,15 @@
|
||||
{
|
||||
'name': "Timesheets/attendances reporting",
|
||||
'description': """
|
||||
Module linking the attendance module to the timesheet app.
|
||||
""",
|
||||
'category': 'Hidden',
|
||||
'version': '0.1',
|
||||
'version': '1.0',
|
||||
|
||||
# any module necessary for this one to work correctly
|
||||
'depends': ['hr_timesheet', 'hr_attendance'],
|
||||
'data': ['report/hr_timesheet_attendance_report_view.xml'],
|
||||
'depends': ['hr_timesheet_sheet', 'hr_attendance'],
|
||||
'data': [
|
||||
'report/hr_timesheet_attendance_report_view.xml',
|
||||
'views/hr_timesheet_sheet_views.xml',
|
||||
],
|
||||
'auto_install': True,
|
||||
}
|
||||
|
||||
+3
-1
@@ -1,4 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import attendance_errors
|
||||
import hr_attendance
|
||||
import hr_timesheet_sheet
|
||||
import res_company
|
||||
@@ -0,0 +1,120 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import time
|
||||
from datetime import datetime
|
||||
from pytz import timezone
|
||||
import pytz
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class HrAttendance(models.Model):
|
||||
_inherit = "hr.attendance"
|
||||
|
||||
sheet_id_computed = fields.Many2one('hr_timesheet_sheet.sheet', string='Sheet', compute='_compute_sheet', index=True, ondelete='cascade',
|
||||
search='_search_sheet')
|
||||
sheet_id = fields.Many2one('hr_timesheet_sheet.sheet', compute='_compute_sheet', string='Sheet', store=True)
|
||||
|
||||
@api.depends('employee_id', 'check_in', 'check_out', 'sheet_id_computed.date_to', 'sheet_id_computed.date_from', 'sheet_id_computed.employee_id')
|
||||
def _compute_sheet(self):
|
||||
"""Links the attendance to the corresponding sheet
|
||||
"""
|
||||
for attendance in self:
|
||||
corresponding_sheet = self.env['hr_timesheet_sheet.sheet'].search(
|
||||
[('date_to', '>=', attendance.check_in), ('date_from', '<=', attendance.check_in),
|
||||
('employee_id', '=', attendance.employee_id.id),
|
||||
('state', 'in', ['draft', 'new'])], limit=1)
|
||||
if corresponding_sheet:
|
||||
attendance.sheet_id_computed = corresponding_sheet[0]
|
||||
attendance.sheet_id = corresponding_sheet[0]
|
||||
|
||||
def _search_sheet(self, operator, value):
|
||||
assert operator == 'in'
|
||||
ids = []
|
||||
for ts in self.env['hr_timesheet_sheet.sheet'].browse(value):
|
||||
self._cr.execute("""
|
||||
SELECT a.id
|
||||
FROM hr_attendance a
|
||||
WHERE %(date_to)s >= a.check_in
|
||||
AND %(date_from)s <= a.check_in
|
||||
AND %(employee_id)s = a.employee_id
|
||||
GROUP BY a.id""", {'date_from': ts.date_from,
|
||||
'date_to': ts.date_to,
|
||||
'employee_id': ts.employee_id.id, })
|
||||
ids.extend([row[0] for row in self._cr.fetchall()])
|
||||
return [('id', 'in', ids)]
|
||||
|
||||
def _get_attendance_employee_tz(self, employee_id, date):
|
||||
""" Simulate timesheet in employee timezone
|
||||
|
||||
Return the attendance date in string format in the employee
|
||||
tz converted from utc timezone as we consider date of employee
|
||||
timesheet is in employee timezone
|
||||
"""
|
||||
tz = False
|
||||
if employee_id:
|
||||
employee = self.env['hr.employee'].browse(employee_id)
|
||||
tz = employee.user_id.partner_id.tz
|
||||
|
||||
if not date:
|
||||
date = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
|
||||
att_tz = timezone(tz or 'utc')
|
||||
|
||||
attendance_dt = datetime.strptime(date, DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
att_tz_dt = pytz.utc.localize(attendance_dt)
|
||||
att_tz_dt = att_tz_dt.astimezone(att_tz)
|
||||
# We take only the date omiting the hours as we compare with timesheet
|
||||
# date_from which is a date format thus using hours would lead to
|
||||
# be out of scope of timesheet
|
||||
att_tz_date_str = datetime.strftime(att_tz_dt, DEFAULT_SERVER_DATE_FORMAT)
|
||||
return att_tz_date_str
|
||||
|
||||
def _get_current_sheet(self, employee_id, date=False):
|
||||
if not date:
|
||||
date = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
|
||||
att_tz_date_str = self._get_attendance_employee_tz(employee_id, date=date)
|
||||
sheet = self.env['hr_timesheet_sheet.sheet'].search(
|
||||
[('date_from', '<=', att_tz_date_str),
|
||||
('date_to', '>=', att_tz_date_str),
|
||||
('employee_id', '=', employee_id)], limit=1)
|
||||
return sheet or False
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
if self.env.context.get('sheet_id'):
|
||||
sheet = self.env['hr_timesheet_sheet.sheet'].browse(self.env.context.get('sheet_id'))
|
||||
else:
|
||||
sheet = self._get_current_sheet(vals.get('employee_id'), vals.get('check_in'))
|
||||
if sheet:
|
||||
att_tz_date_str = self._get_attendance_employee_tz(vals.get('employee_id'), date=vals.get('check_in'))
|
||||
if sheet.state not in ('draft', 'new'):
|
||||
raise UserError(_('You can not enter an attendance in a submitted timesheet. Ask your manager to reset it before adding attendance.'))
|
||||
elif sheet.date_from > att_tz_date_str or sheet.date_to < att_tz_date_str:
|
||||
raise UserError(_('You can not enter an attendance date outside the current timesheet dates.'))
|
||||
return super(HrAttendance, self).create(vals)
|
||||
|
||||
@api.multi
|
||||
def unlink(self):
|
||||
self._check()
|
||||
return super(HrAttendance, self).unlink()
|
||||
|
||||
@api.multi
|
||||
def write(self, vals):
|
||||
self._check()
|
||||
res = super(HrAttendance, self).write(vals)
|
||||
if 'sheet_id' in self.env.context:
|
||||
for attendance in self:
|
||||
if self.env.context['sheet_id'] != attendance.sheet_id.id:
|
||||
raise UserError(_('You cannot enter an attendance date outside the current timesheet dates.'))
|
||||
return res
|
||||
|
||||
def _check(self):
|
||||
for att in self:
|
||||
if att.sheet_id and att.sheet_id.state not in ('draft', 'new'):
|
||||
raise UserError(_('You cannot modify an entry in a confirmed timesheet'))
|
||||
return True
|
||||
@@ -0,0 +1,177 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.tools.translate import _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class HrTimesheetSheet(models.Model):
|
||||
_inherit = "hr_timesheet_sheet.sheet"
|
||||
|
||||
attendances_ids = fields.One2many('hr.attendance', 'sheet_id', 'Attendances')
|
||||
total_attendance = fields.Integer(string='Total Attendance', compute='_compute_total')
|
||||
total_timesheet = fields.Float(string='Total Timesheet', compute="_compute_total")
|
||||
total_difference = fields.Float(string='Difference', compute="_compute_total")
|
||||
period_ids = fields.One2many('hr_timesheet_sheet.sheet.day', 'sheet_id', string='Period', readonly=True)
|
||||
attendance_count = fields.Integer(compute='_compute_attendances', string="Attendances")
|
||||
|
||||
@api.depends('period_ids.total_attendance', 'period_ids.total_timesheet', 'period_ids.total_difference')
|
||||
def _compute_total(self):
|
||||
""" Compute the attendances, analytic lines timesheets and differences
|
||||
between them for all the days of a timesheet and the current day
|
||||
"""
|
||||
if len(self.ids) == 0:
|
||||
return
|
||||
|
||||
self.env.cr.execute("""
|
||||
SELECT sheet_id as id,
|
||||
sum(total_attendance) as total_attendance,
|
||||
sum(total_timesheet) as total_timesheet,
|
||||
sum(total_difference) as total_difference
|
||||
FROM hr_timesheet_sheet_sheet_day
|
||||
WHERE sheet_id IN %s
|
||||
GROUP BY sheet_id
|
||||
""", (tuple(self.ids), ))
|
||||
|
||||
for x in self.env.cr.dictfetchall():
|
||||
sheet = self.browse(x.pop('id'))
|
||||
sheet.total_attendance = x.pop('total_attendance')
|
||||
sheet.total_timesheet = x.pop('total_timesheet')
|
||||
sheet.total_difference = x.pop('total_difference')
|
||||
|
||||
@api.depends('attendances_ids')
|
||||
def _compute_attendances(self):
|
||||
for sheet in self:
|
||||
sheet.attendance_count = len(sheet.attendances_ids)
|
||||
|
||||
@api.multi
|
||||
def unlink(self):
|
||||
sheets = self.read(['total_attendance'])
|
||||
for sheet in sheets:
|
||||
if sheet['total_attendance'] > 0.00:
|
||||
raise UserError(_('You cannot delete a timesheet that has attendance entries.'))
|
||||
return super(HrTimesheetSheet, self).unlink()
|
||||
|
||||
@api.multi
|
||||
def action_sheet_report(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': 'HR Timesheet/Attendance Report',
|
||||
'res_model': 'hr.timesheet.attendance.report',
|
||||
'domain': [('date', '>=', self.date_from), ('date', '<=', self.date_to)],
|
||||
'view_mode': 'pivot',
|
||||
'context': {'search_default_user_id': self.user_id.id, }
|
||||
}
|
||||
|
||||
@api.multi
|
||||
def button_confirm(self):
|
||||
for sheet in self:
|
||||
sheet.check_employee_attendance_state()
|
||||
di = sheet.user_id.company_id.timesheet_max_difference
|
||||
if (abs(sheet.total_difference) <= di) or not di:
|
||||
return super(HrTimesheetSheet, self).button_confirm()
|
||||
else:
|
||||
raise UserError(_('Please verify that the total difference of the sheet is lower than %.2f.') % (di,))
|
||||
|
||||
@api.multi
|
||||
def check_employee_attendance_state(self):
|
||||
""" Checks the attendance records of the timesheet, make sure they are all closed
|
||||
(by making sure they have a check_out time)
|
||||
"""
|
||||
self.ensure_one()
|
||||
if any(self.attendances_ids.filtered(lambda r: not r.check_out)):
|
||||
raise UserError(_("The timesheet cannot be validated as it contains an attendance record with no Check Out)."))
|
||||
return True
|
||||
|
||||
|
||||
class hr_timesheet_sheet_sheet_day(models.Model):
|
||||
_name = "hr_timesheet_sheet.sheet.day"
|
||||
_description = "Timesheets by Period"
|
||||
_auto = False
|
||||
_order = 'name'
|
||||
|
||||
name = fields.Date('Date', readonly=True)
|
||||
sheet_id = fields.Many2one('hr_timesheet_sheet.sheet', 'Sheet', readonly=True, index=True)
|
||||
total_timesheet = fields.Float('Total Timesheet', readonly=True)
|
||||
total_attendance = fields.Float('Attendance', readonly=True)
|
||||
total_difference = fields.Float('Difference', readonly=True)
|
||||
|
||||
_depends = {
|
||||
'account.analytic.line': ['date', 'unit_amount'],
|
||||
'hr.attendance': ['check_in', 'check_out', 'sheet_id'],
|
||||
'hr_timesheet_sheet.sheet': ['attendances_ids', 'timesheet_ids'],
|
||||
}
|
||||
|
||||
def init(self, cr):
|
||||
cr.execute("""create or replace view %s as
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
sheet_id,
|
||||
total_timesheet,
|
||||
total_attendance,
|
||||
cast(round(cast(total_attendance - total_timesheet as Numeric),2) as Double Precision) AS total_difference
|
||||
FROM
|
||||
((
|
||||
SELECT
|
||||
MAX(id) as id,
|
||||
name,
|
||||
sheet_id,
|
||||
timezone,
|
||||
SUM(total_timesheet) as total_timesheet,
|
||||
CASE WHEN SUM(orphan_attendances) != 0
|
||||
THEN (SUM(total_attendance) +
|
||||
CASE WHEN current_date <> name
|
||||
THEN 1440
|
||||
ELSE (EXTRACT(hour FROM current_time AT TIME ZONE 'UTC' AT TIME ZONE coalesce(timezone, 'UTC')) * 60) + EXTRACT(minute FROM current_time AT TIME ZONE 'UTC' AT TIME ZONE coalesce(timezone, 'UTC'))
|
||||
END
|
||||
)
|
||||
ELSE SUM(total_attendance)
|
||||
END /60 as total_attendance
|
||||
FROM
|
||||
((
|
||||
select
|
||||
min(l.id) as id,
|
||||
p.tz as timezone,
|
||||
l.date::date as name,
|
||||
s.id as sheet_id,
|
||||
sum(l.unit_amount) as total_timesheet,
|
||||
0 as orphan_attendances,
|
||||
0.0 as total_attendance
|
||||
from
|
||||
account_analytic_line l
|
||||
LEFT JOIN hr_timesheet_sheet_sheet s ON s.id = l.sheet_id
|
||||
JOIN hr_employee e ON s.employee_id = e.id
|
||||
JOIN resource_resource r ON e.resource_id = r.id
|
||||
LEFT JOIN res_users u ON r.user_id = u.id
|
||||
LEFT JOIN res_partner p ON u.partner_id = p.id
|
||||
group by l.date::date, s.id, timezone
|
||||
) union (
|
||||
select
|
||||
-min(a.id) as id,
|
||||
p.tz as timezone,
|
||||
(a.check_in AT TIME ZONE 'UTC' AT TIME ZONE coalesce(p.tz, 'UTC'))::date as name,
|
||||
s.id as sheet_id,
|
||||
0.0 as total_timesheet,
|
||||
0 as orphan_attendances,
|
||||
SUM((EXTRACT(hour FROM (a.check_out AT TIME ZONE 'UTC' AT TIME ZONE coalesce(p.tz, 'UTC'))) * 60 + EXTRACT(minute FROM (a.check_out AT TIME ZONE 'UTC' AT TIME ZONE coalesce(p.tz, 'UTC'))))
|
||||
- (EXTRACT(hour FROM (a.check_in AT TIME ZONE 'UTC' AT TIME ZONE coalesce(p.tz, 'UTC'))) * 60 + EXTRACT(minute FROM (a.check_in AT TIME ZONE 'UTC' AT TIME ZONE coalesce(p.tz, 'UTC'))))) as total_attendance
|
||||
from
|
||||
hr_attendance a
|
||||
LEFT JOIN hr_timesheet_sheet_sheet s
|
||||
ON s.id = a.sheet_id
|
||||
JOIN hr_employee e
|
||||
ON a.employee_id = e.id
|
||||
JOIN resource_resource r
|
||||
ON e.resource_id = r.id
|
||||
LEFT JOIN res_users u
|
||||
ON r.user_id = u.id
|
||||
LEFT JOIN res_partner p
|
||||
ON u.partner_id = p.id
|
||||
WHERE check_out IS NOT NULL
|
||||
group by (a.check_in AT TIME ZONE 'UTC' AT TIME ZONE coalesce(p.tz, 'UTC'))::date, s.id, timezone
|
||||
)) AS foo
|
||||
GROUP BY name, sheet_id, timezone
|
||||
)) AS bar""" % self._table)
|
||||
@@ -0,0 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResCompany(models.Model):
|
||||
_inherit = 'res.company'
|
||||
|
||||
timesheet_max_difference = fields.Float(string='Timesheet allowed difference(Hours)',
|
||||
help="Allowed difference in hours between the sign in/out and the timesheet " \
|
||||
"computation for one sheet. Set this to 0 if you do not want any control.",
|
||||
default=0.0)
|
||||
@@ -1,2 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import hr_timesheet_attendance_report
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import hr_timesheet_attendance_report
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from openerp import api, fields, models, tools
|
||||
from openerp import fields, models
|
||||
|
||||
|
||||
class TimesheetAttendance(models.Model):
|
||||
_name = 'hr.timesheet.attendance.report'
|
||||
@@ -27,11 +28,10 @@ class TimesheetAttendance(models.Model):
|
||||
resource_resource.user_id AS user_id,
|
||||
hr_attendance.worked_hours AS attendance,
|
||||
NULL AS timesheet,
|
||||
date_trunc('day', hr_attendance.name) AS date
|
||||
date_trunc('day', hr_attendance.check_in) AS date
|
||||
FROM hr_attendance
|
||||
LEFT JOIN hr_employee ON hr_employee.id = hr_attendance.employee_id
|
||||
LEFT JOIN resource_resource on resource_resource.id = hr_employee.resource_id
|
||||
WHERE hr_attendance.action = 'sign_out'
|
||||
UNION ALL
|
||||
SELECT
|
||||
ts.id AS id,
|
||||
|
||||
@@ -5,11 +5,12 @@
|
||||
<field name="model">hr.timesheet.attendance.report</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="timesheet attendance">
|
||||
<field name="user_id"/>
|
||||
<filter name="week" string="This Week"
|
||||
domain="[
|
||||
'&',
|
||||
('date', '>=', (context_today() + relativedelta(weekday=0)).strftime('%Y-%m-%d')),
|
||||
('date', '<=', (context_today() + relativedelta(weekday=6)).strftime('%Y-%m-%d')) ,
|
||||
('date', '>=', (context_today() + relativedelta(weeks=-1,days=1,weekday=0)).strftime('%Y-%m-%d')),
|
||||
('date', '<=', (context_today() + relativedelta(weekday=6)).strftime('%Y-%m-%d')),
|
||||
]"/>
|
||||
<filter name="month" string="This Month"
|
||||
domain="[
|
||||
@@ -39,7 +40,6 @@
|
||||
<field name="context">{'search_default_week': True}</field>
|
||||
</record>
|
||||
|
||||
|
||||
<menuitem id="menu_hr_timesheet_attendance_report"
|
||||
parent="hr_timesheet.menu_timesheets_reports"
|
||||
action="action_hr_timesheet_attendance_report"
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_hr_timesheet_sheet_sheet_day,hr_timesheet_sheet.sheet.day,model_hr_timesheet_sheet_sheet_day,base.group_hr_user,1,1,1,1
|
||||
access_hr_timesheet_sheet_sheet_day,hr.timesheet.sheet.sheet.day.user,model_hr_timesheet_sheet_sheet_day,base.group_user,1,1,1,0
|
||||
|
@@ -0,0 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import test_hr_timesheet_sheet
|
||||
@@ -0,0 +1,79 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo.tests.common import TransactionCase
|
||||
import time
|
||||
|
||||
|
||||
class TestHrTimesheetSheet(TransactionCase):
|
||||
"""Test for hr_timesheet_sheet.sheet"""
|
||||
|
||||
def setUp(self):
|
||||
super(TestHrTimesheetSheet, self).setUp()
|
||||
self.attendance = self.env['hr.attendance']
|
||||
self.timesheet_sheet = self.env['hr_timesheet_sheet.sheet']
|
||||
self.test_employee = self.browse_ref('hr.employee_qdp')
|
||||
self.company = self.browse_ref('base.main_company')
|
||||
self.company.timesheet_max_difference = 1.00
|
||||
|
||||
def test_hr_timesheet_sheet(self):
|
||||
|
||||
# I create a timesheet for employee "Gilles Gravie".
|
||||
self.test_timesheet_sheet = self.timesheet_sheet.create({
|
||||
'date_from': time.strftime('%Y-%m-11'),
|
||||
'date_to': time.strftime('%Y-%m-17'),
|
||||
'name': 'Gilles Gravie',
|
||||
'state': 'new',
|
||||
'user_id': self.browse_ref('base.user_demo').id,
|
||||
'employee_id': self.test_employee.id,
|
||||
})
|
||||
|
||||
# I check Gilles in at around 9:00 and out at 17:30
|
||||
self.attendance.create({
|
||||
'employee_id': self.test_employee.id,
|
||||
'check_in': time.strftime('%Y-%m-11 09:12:37'),
|
||||
'check_out': time.strftime('%Y-%m-11 17:30:00'),
|
||||
})
|
||||
|
||||
# I add 6 hours of work to Gilles' timesheet
|
||||
self.test_timesheet_sheet.write({'timesheet_ids': [(0, 0, {
|
||||
'project_id': self.browse_ref('project.project_project_2').id,
|
||||
'date': time.strftime('%Y-%m-11'),
|
||||
'name': 'Develop yaml for hr module(1)',
|
||||
'user_id': self.browse_ref('base.user_demo').id,
|
||||
'unit_amount': 6.00,
|
||||
'amount': -90.00,
|
||||
'product_id': self.browse_ref('product.product_product_1').id,
|
||||
})]})
|
||||
|
||||
# I confirm Gilles' timesheet with over 1 hour difference
|
||||
# in attendance and actual worked hours
|
||||
try:
|
||||
self.test_timesheet_sheet.button_confirm()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# I add another 2 hours of work to Gilles' timesheet
|
||||
self.test_timesheet_sheet.write({'timesheet_ids': [(0, 0, {
|
||||
'project_id': self.browse_ref('project.project_project_2').id,
|
||||
'date': time.strftime('%Y-%m-11'),
|
||||
'name': 'Develop yaml for hr module(2)',
|
||||
'user_id': self.browse_ref('base.user_demo').id,
|
||||
'unit_amount': 2.00,
|
||||
'amount': -90.00,
|
||||
'product_id': self.browse_ref('product.product_product_1').id,
|
||||
})]})
|
||||
# I invalidate the cache, otherwise total_timesheet and total_difference doesn't get updated... /this is a disgrace/
|
||||
self.test_timesheet_sheet.invalidate_cache(['total_attendance', 'total_timesheet', 'total_difference'])
|
||||
|
||||
# I confirm Gilles' timesheet with less than 1 hour difference
|
||||
# in attendance and actual worked hours
|
||||
self.test_timesheet_sheet.button_confirm()
|
||||
|
||||
# I check the state is confirmed
|
||||
assert self.test_timesheet_sheet.state == 'confirm'
|
||||
|
||||
# the manager accepts the timesheet
|
||||
self.test_timesheet_sheet.write({'state': 'done'})
|
||||
|
||||
# I check the state is indeed done
|
||||
assert self.test_timesheet_sheet.state == 'done'
|
||||
@@ -0,0 +1,161 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="act_hr_timesheet_sheet_sheet_2_hr_attendance" model="ir.actions.act_window">
|
||||
<field name="context">{'search_default_sheet_id': [active_id]}</field>
|
||||
<field name="name">Attendances</field>
|
||||
<field name="res_model">hr.attendance</field>
|
||||
<field name="src_model">hr_timesheet_sheet.sheet</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_timesheet_sheet_tree_simplified" model="ir.ui.view">
|
||||
<field name="name">hr.timesheet.sheet.tree</field>
|
||||
<field name="model">hr_timesheet_sheet.sheet</field>
|
||||
<field name="inherit_id" ref="hr_timesheet_sheet.hr_timesheet_sheet_tree_simplified" />
|
||||
<field name="arch" type="xml">
|
||||
<field name="department_id" position="after">
|
||||
<field name="total_attendance" widget="float_time" groups="base.group_hr_attendance"/>
|
||||
<field name="total_timesheet" widget="float_time"/>
|
||||
<field name="total_difference" widget="float_time" groups="base.group_hr_attendance"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="act_hr_timesheet_sheet_sheet_2_hr_analytic_timesheet" model="ir.actions.act_window">
|
||||
<field name="context">{'search_default_sheet_id': [active_id]}</field>
|
||||
<field name="name">Timesheet Activities</field>
|
||||
<field name="res_model">account.analytic.line</field>
|
||||
<field name="src_model">hr_timesheet_sheet.sheet</field>
|
||||
<field name="view_ids" eval="[(5, 0, 0),
|
||||
(0, 0, {'view_mode': 'tree', 'view_id': ref('hr_timesheet.hr_timesheet_line_tree')}),
|
||||
(0, 0, {'view_mode': 'form', 'view_id': ref('hr_timesheet.hr_timesheet_line_form')})]"/>
|
||||
</record>
|
||||
|
||||
<record id="hr_timesheet_sheet_form" model="ir.ui.view">
|
||||
<field name="name">hr.timesheet.sheet.form</field>
|
||||
<field name="model">hr_timesheet_sheet.sheet</field>
|
||||
<field name="inherit_id" ref="hr_timesheet_sheet.hr_timesheet_sheet_form" />
|
||||
<field name="arch" type="xml">
|
||||
<div class="oe_title" position="before">
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button type="action"
|
||||
name="%(hr_timesheet_attendance.act_hr_timesheet_sheet_sheet_2_hr_analytic_timesheet)d"
|
||||
class="oe_stat_button"
|
||||
icon="fa-clock-o">
|
||||
<div class="o_form_field o_stat_info">
|
||||
<span class="o_stat_value"><field name="total_timesheet" widget="float_time"/></span>
|
||||
<span class="o_stat_text">Timesheet</span>
|
||||
</div>
|
||||
</button>
|
||||
<button type="object" groups="base.group_hr_attendance"
|
||||
class="oe_stat_button" icon="fa-users"
|
||||
name="action_sheet_report">
|
||||
<field name="attendance_count" widget="statinfo" string="Attendances"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<xpath expr="//page[@class='o_hr_timesheet_sheet_details']" position="after">
|
||||
<page string="Attendances" groups="base.group_hr_attendance">
|
||||
<group>
|
||||
<group groups="base.group_hr_attendance">
|
||||
<field name="total_attendance" widget="float_time"/>
|
||||
<field name="total_difference" widget="float_time"/>
|
||||
</group>
|
||||
</group>
|
||||
<field name="period_ids">
|
||||
<tree decoration-danger="total_difference<0.1" decoration-info="total_difference>=0.1" string="Period">
|
||||
<field name="name"/>
|
||||
<field name="total_attendance" widget="float_time" sum="Attendances"/>
|
||||
<field name="total_timesheet" widget="float_time" sum="Timesheets"/>
|
||||
<field name="total_difference" widget="float_time" sum="Differences"/>
|
||||
</tree>
|
||||
</field>
|
||||
</page>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!--
|
||||
hr.attendance inheritancy
|
||||
-->
|
||||
|
||||
<record model="ir.ui.view" id="hr_attendance_view_filter">
|
||||
<field name="name">hr_attendance_view_filter</field>
|
||||
<field name="model">hr.attendance</field>
|
||||
<field name="inherit_id" ref="hr_attendance.hr_attendance_view_filter" />
|
||||
<field name="arch" type="xml">
|
||||
<field name="employee_id" position="after">
|
||||
<field name="sheet_id"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_attendance_view_form" model="ir.ui.view">
|
||||
<field name="name">hr.attendance.form</field>
|
||||
<field name="model">hr.attendance</field>
|
||||
<field name="inherit_id" ref="hr_attendance.hr_attendance_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="check_out" position="after">
|
||||
<field name="sheet_id"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
<record id="view_attendance_tree" model="ir.ui.view">
|
||||
<field name="name">hr.attendance.tree</field>
|
||||
<field name="model">hr.attendance</field>
|
||||
<field name="inherit_id" ref="hr_attendance.view_attendance_tree"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="check_out" position="after">
|
||||
<field name="sheet_id"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<act_window
|
||||
context="{'search_default_sheet_id': [active_id]}"
|
||||
id="act_hr_timesheet_sheet_sheet_by_account"
|
||||
name="Timesheet by Account"
|
||||
groups="base.group_hr_attendance"
|
||||
res_model="hr_timesheet_sheet.sheet.account"
|
||||
src_model="hr_timesheet_sheet.sheet"/>
|
||||
|
||||
<act_window
|
||||
context="{'search_default_sheet_id': [active_id]}"
|
||||
id="act_hr_timesheet_sheet_sheet_by_day"
|
||||
name="Timesheet by Day"
|
||||
groups="base.group_hr_attendance"
|
||||
res_model="hr_timesheet_sheet.sheet.day"
|
||||
src_model="hr_timesheet_sheet.sheet"/>
|
||||
|
||||
<record id="hr_timesheet_day_tree" model="ir.ui.view">
|
||||
<field name="name">hr.timesheet.day.tree</field>
|
||||
<field name="model">hr_timesheet_sheet.sheet.day</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree decoration-info="total_difference<0.1" decoration-danger="total_difference>=0.1" string="Period">
|
||||
<field name="name"/>
|
||||
<field name="total_attendance" widget="float_time" sum="Total Attendances" groups="base.group_hr_attendance"/>
|
||||
<field name="total_timesheet" widget="float_time" sum="Total Timesheet"/>
|
||||
<field name="total_difference" widget="float_time" sum="Total Difference" groups="base.group_hr_attendance"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!--
|
||||
Company inheritancy
|
||||
-->
|
||||
|
||||
<record id="hr_timesheet_sheet_company" model="ir.ui.view">
|
||||
<field name="name">res.company.sheet</field>
|
||||
<field name="model">res.company</field>
|
||||
<field name="inherit_id" ref="hr_timesheet_sheet.hr_timesheet_sheet_company"/>
|
||||
<field name="arch" type="xml">
|
||||
<div class="o_hr_timesheet_sheet_timesheet_range" position="after">
|
||||
<div>
|
||||
<label for="timesheet_max_difference"/>
|
||||
<field name="timesheet_max_difference" required="1" class="oe_inline"/>
|
||||
</div>
|
||||
</div>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import hr_timesheet_sheet
|
||||
import hr_department
|
||||
import hr_timesheet_sheet_config_settings
|
||||
import wizard
|
||||
import models
|
||||
import wizard
|
||||
@@ -4,15 +4,15 @@
|
||||
|
||||
{
|
||||
'name': 'Timesheets',
|
||||
'version': '1.0',
|
||||
'version': '1.1',
|
||||
'category': 'Human Resources',
|
||||
'sequence': 80,
|
||||
'summary': 'Timesheets, Attendances, Activities',
|
||||
'summary': 'Timesheets, Activities',
|
||||
'description': """
|
||||
Record and validate timesheets and attendances easily
|
||||
=====================================================
|
||||
|
||||
This application supplies a new screen enabling you to manage both attendances (Sign in/Sign out) and your work encoding (timesheet) by period. Timesheet entries are made by employees each day. At the end of the defined period, employees validate their sheet and the manager must then approve his team's entries. Periods are defined in the company forms and you can set them to run monthly or weekly.
|
||||
This application supplies a new screen enabling you to manage your work encoding (timesheet) by period. Timesheet entries are made by employees each day. At the end of the defined period, employees validate their sheet and the manager must then approve his team's entries. Periods are defined in the company forms and you can set them to run monthly or weekly.
|
||||
|
||||
The complete timesheet validation process is:
|
||||
---------------------------------------------
|
||||
@@ -26,20 +26,19 @@ The validation can be configured in the company:
|
||||
* Maximal difference between timesheet and attendances
|
||||
""",
|
||||
'website': 'https://www.odoo.com/page/employees',
|
||||
'depends': ['hr_timesheet', 'hr_attendance'],
|
||||
'depends': ['hr_timesheet'],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'security/hr_timesheet_sheet_security.xml',
|
||||
'hr_timesheet_workflow.xml',
|
||||
'hr_timesheet_sheet_data.xml',
|
||||
'views/hr_timesheet_sheet.xml',
|
||||
'hr_timesheet_sheet_view.xml',
|
||||
'hr_timesheet_sheet_config_settings_views.xml',
|
||||
'hr_dashboard.xml',
|
||||
'data/hr_timesheet_workflow.xml',
|
||||
'data/hr_timesheet_sheet_data.xml',
|
||||
'views/hr_timesheet_sheet_templates.xml',
|
||||
'views/hr_timesheet_sheet_views.xml',
|
||||
'views/hr_department_views.xml',
|
||||
'views/hr_timesheet_sheet_config_settings_views.xml',
|
||||
],
|
||||
'test':['../account/test/account_minimal_test.xml', 'test/test_hr_timesheet_sheet.yml'],
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
'application': True,
|
||||
'qweb': ['static/src/xml/timesheet.xml',],
|
||||
'qweb': ['static/src/xml/timesheet.xml', ],
|
||||
}
|
||||
|
||||
+4
-4
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" ?>
|
||||
<openerp>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<!-- notify all employees of module installation -->
|
||||
<record model="mail.message" id="hr_timesheet_module_install_notification">
|
||||
@@ -11,7 +11,7 @@
|
||||
<field name="body"><![CDATA[<p>From the top menu "Human Resources", enter and validate timesheets and attendances.</p>]]></field>
|
||||
</record>
|
||||
|
||||
<record id="ir_actions_server_timsheet_sheet" model="ir.actions.server">
|
||||
<record id="ir_actions_server_timesheet_sheet" model="ir.actions.server">
|
||||
<field name="sequence" eval="5"/>
|
||||
<field name="state">code</field>
|
||||
<field name="type">ir.actions.server</field>
|
||||
@@ -53,4 +53,4 @@
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
</odoo>
|
||||
+2
-4
@@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<odoo>
|
||||
|
||||
<!-- Workflow definition -->
|
||||
|
||||
@@ -60,5 +59,4 @@
|
||||
<field name="signal">cancel</field>
|
||||
<field name="group_id" ref="base.group_hr_user"/>
|
||||
</record>
|
||||
</data>
|
||||
</openerp>
|
||||
</odoo>
|
||||
@@ -1,654 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import time
|
||||
from datetime import datetime
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from pytz import timezone
|
||||
import pytz
|
||||
|
||||
from openerp.osv import fields, osv
|
||||
from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT
|
||||
from openerp.tools.translate import _
|
||||
from openerp.exceptions import UserError
|
||||
|
||||
class hr_timesheet_sheet(osv.osv):
|
||||
_name = "hr_timesheet_sheet.sheet"
|
||||
_inherit = ['mail.thread', 'ir.needaction_mixin']
|
||||
_table = 'hr_timesheet_sheet_sheet'
|
||||
_order = "id desc"
|
||||
_description = "Timesheet"
|
||||
|
||||
def _total(self, cr, uid, ids, name, args, context=None):
|
||||
""" Compute the attendances, analytic lines timesheets and differences between them
|
||||
for all the days of a timesheet and the current day
|
||||
"""
|
||||
res = dict.fromkeys(ids, {
|
||||
'total_attendance': 0.0,
|
||||
'total_timesheet': 0.0,
|
||||
'total_difference': 0.0,
|
||||
})
|
||||
|
||||
cr.execute("""
|
||||
SELECT sheet_id as id,
|
||||
sum(total_attendance) as total_attendance,
|
||||
sum(total_timesheet) as total_timesheet,
|
||||
sum(total_difference) as total_difference
|
||||
FROM hr_timesheet_sheet_sheet_day
|
||||
WHERE sheet_id IN %s
|
||||
GROUP BY sheet_id
|
||||
""", (tuple(ids),))
|
||||
|
||||
res.update(dict((x.pop('id'), x) for x in cr.dictfetchall()))
|
||||
|
||||
return res
|
||||
|
||||
def check_employee_attendance_state(self, cr, uid, sheet_id, context=None):
|
||||
ids_signin = self.pool.get('hr.attendance').search(cr,uid,[('sheet_id', '=', sheet_id),('action','=','sign_in')])
|
||||
ids_signout = self.pool.get('hr.attendance').search(cr,uid,[('sheet_id', '=', sheet_id),('action','=','sign_out')])
|
||||
|
||||
if len(ids_signin) != len(ids_signout):
|
||||
raise UserError(_('The timesheet cannot be validated as it does not contain an equal number of sign ins and sign outs.'))
|
||||
return True
|
||||
|
||||
def copy(self, cr, uid, ids, *args, **argv):
|
||||
raise UserError(_('You cannot duplicate a timesheet.'))
|
||||
|
||||
def create(self, cr, uid, vals, context=None):
|
||||
if 'employee_id' in vals:
|
||||
if not self.pool.get('hr.employee').browse(cr, uid, vals['employee_id'], context=context).user_id:
|
||||
raise UserError(_('In order to create a timesheet for this employee, you must link him/her to a user.'))
|
||||
if vals.get('attendances_ids'):
|
||||
# If attendances, we sort them by date asc before writing them, to satisfy the alternance constraint
|
||||
vals['attendances_ids'] = self.sort_attendances(cr, uid, vals['attendances_ids'], context=context)
|
||||
return super(hr_timesheet_sheet, self).create(cr, uid, vals, context=context)
|
||||
|
||||
def write(self, cr, uid, ids, vals, context=None):
|
||||
if 'employee_id' in vals:
|
||||
new_user_id = self.pool.get('hr.employee').browse(cr, uid, vals['employee_id'], context=context).user_id.id or False
|
||||
if not new_user_id:
|
||||
raise UserError(_('In order to create a timesheet for this employee, you must link him/her to a user.'))
|
||||
if not self._sheet_date(cr, uid, ids, forced_user_id=new_user_id, context=context):
|
||||
raise UserError(_('You cannot have 2 timesheets that overlap!\nYou should use the menu \'My Timesheet\' to avoid this problem.'))
|
||||
if not self.pool.get('hr.employee').browse(cr, uid, vals['employee_id'], context=context).product_id:
|
||||
raise UserError(_('In order to create a timesheet for this employee, you must link the employee to a product.'))
|
||||
if vals.get('attendances_ids'):
|
||||
# If attendances, we sort them by date asc before writing them, to satisfy the alternance constraint
|
||||
# In addition to the date order, deleting attendances are done before inserting attendances
|
||||
vals['attendances_ids'] = self.sort_attendances(cr, uid, vals['attendances_ids'], context=context)
|
||||
res = super(hr_timesheet_sheet, self).write(cr, uid, ids, vals, context=context)
|
||||
if vals.get('attendances_ids'):
|
||||
for timesheet in self.browse(cr, uid, ids):
|
||||
if not self.pool['hr.attendance']._altern_si_so(cr, uid, [att.id for att in timesheet.attendances_ids]):
|
||||
raise UserError(_('Error ! Sign in (resp. Sign out) must follow Sign out (resp. Sign in)'))
|
||||
return res
|
||||
|
||||
def sort_attendances(self, cr, uid, attendance_tuples, context=None):
|
||||
date_attendances = []
|
||||
for att_tuple in attendance_tuples:
|
||||
if att_tuple[0] in [0,1,4]:
|
||||
if att_tuple[0] in [0,1]:
|
||||
if att_tuple[2] and att_tuple[2].has_key('name'):
|
||||
name = att_tuple[2]['name']
|
||||
else:
|
||||
name = self.pool['hr.attendance'].browse(cr, uid, att_tuple[1]).name
|
||||
else:
|
||||
name = self.pool['hr.attendance'].browse(cr, uid, att_tuple[1]).name
|
||||
date_attendances.append((1, name, att_tuple))
|
||||
elif att_tuple[0] in [2,3]:
|
||||
date_attendances.append((0, self.pool['hr.attendance'].browse(cr, uid, att_tuple[1]).name, att_tuple))
|
||||
else:
|
||||
date_attendances.append((0, False, att_tuple))
|
||||
date_attendances.sort()
|
||||
return [att[2] for att in date_attendances]
|
||||
|
||||
def button_confirm(self, cr, uid, ids, context=None):
|
||||
for sheet in self.browse(cr, uid, ids, context=context):
|
||||
if sheet.employee_id and sheet.employee_id.parent_id and sheet.employee_id.parent_id.user_id:
|
||||
self.message_subscribe_users(cr, uid, [sheet.id], user_ids=[sheet.employee_id.parent_id.user_id.id], context=context)
|
||||
self.check_employee_attendance_state(cr, uid, sheet.id, context=context)
|
||||
di = sheet.user_id.company_id.timesheet_max_difference
|
||||
if (abs(sheet.total_difference) < di) or not di:
|
||||
sheet.signal_workflow('confirm')
|
||||
else:
|
||||
raise UserError(_('Please verify that the total difference of the sheet is lower than %.2f.') %(di,))
|
||||
return True
|
||||
|
||||
def attendance_action_change(self, cr, uid, ids, context=None):
|
||||
hr_employee = self.pool.get('hr.employee')
|
||||
employee_ids = []
|
||||
for sheet in self.browse(cr, uid, ids, context=context):
|
||||
if sheet.employee_id.id not in employee_ids: employee_ids.append(sheet.employee_id.id)
|
||||
return hr_employee.attendance_action_change(cr, uid, employee_ids, context=context)
|
||||
|
||||
def _count_attendances(self, cr, uid, ids, field_name, arg, context=None):
|
||||
res = dict.fromkeys(ids, 0)
|
||||
attendances_groups = self.pool['hr.attendance'].read_group(cr, uid, [('sheet_id' , 'in' , ids)], ['sheet_id'], 'sheet_id', context=context)
|
||||
for attendances in attendances_groups:
|
||||
res[attendances['sheet_id'][0]] = attendances['sheet_id_count']
|
||||
return res
|
||||
|
||||
_columns = {
|
||||
'name': fields.char('Note', select=1,
|
||||
states={'confirm':[('readonly', True)], 'done':[('readonly', True)]}),
|
||||
'employee_id': fields.many2one('hr.employee', 'Employee', required=True),
|
||||
'user_id': fields.related('employee_id', 'user_id', type="many2one", relation="res.users", store=True, string="User", required=False, readonly=True),#fields.many2one('res.users', 'User', required=True, select=1, states={'confirm':[('readonly', True)], 'done':[('readonly', True)]}),
|
||||
'date_from': fields.date('Date from', required=True, select=1, readonly=True, states={'new':[('readonly', False)]}),
|
||||
'date_to': fields.date('Date to', required=True, select=1, readonly=True, states={'new':[('readonly', False)]}),
|
||||
'timesheet_ids' : fields.one2many('account.analytic.line', 'sheet_id',
|
||||
'Timesheet lines',
|
||||
readonly=True, states={
|
||||
'draft': [('readonly', False)],
|
||||
'new': [('readonly', False)]}
|
||||
),
|
||||
'attendances_ids' : fields.one2many('hr.attendance', 'sheet_id', 'Attendances'),
|
||||
'state' : fields.selection([
|
||||
('new', 'New'),
|
||||
('draft','Open'),
|
||||
('confirm','Waiting Approval'),
|
||||
('done','Approved')], 'Status', select=True, required=True, readonly=True,
|
||||
track_visibility='onchange',
|
||||
help=' * The \'Draft\' status is used when a user is encoding a new and unconfirmed timesheet. \
|
||||
\n* The \'Confirmed\' status is used for to confirm the timesheet by user. \
|
||||
\n* The \'Done\' status is used when users timesheet is accepted by his/her senior.'),
|
||||
'state_attendance' : fields.related('employee_id', 'state', type='selection', selection=[('absent', 'Absent'), ('present', 'Present')], string='Current Status', readonly=True),
|
||||
'total_attendance': fields.function(_total, method=True, string='Total Attendance', multi="_total"),
|
||||
'total_timesheet': fields.function(_total, method=True, string='Total Timesheet', multi="_total"),
|
||||
'total_difference': fields.function(_total, method=True, string='Difference', multi="_total"),
|
||||
'period_ids': fields.one2many('hr_timesheet_sheet.sheet.day', 'sheet_id', 'Period', readonly=True),
|
||||
'account_ids': fields.one2many('hr_timesheet_sheet.sheet.account', 'sheet_id', 'Analytic accounts', readonly=True),
|
||||
'company_id': fields.many2one('res.company', 'Company'),
|
||||
'department_id':fields.many2one('hr.department','Department'),
|
||||
'attendance_count': fields.function(_count_attendances, type='integer', string="Attendances"),
|
||||
}
|
||||
|
||||
def _default_date_from(self, cr, uid, context=None):
|
||||
user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
|
||||
r = user.company_id and user.company_id.timesheet_range or 'month'
|
||||
if r=='month':
|
||||
return time.strftime('%Y-%m-01')
|
||||
elif r=='week':
|
||||
return (datetime.today() + relativedelta(weekday=0, days=-6)).strftime('%Y-%m-%d')
|
||||
elif r=='year':
|
||||
return time.strftime('%Y-01-01')
|
||||
return fields.date.context_today(self, cr, uid, context)
|
||||
|
||||
def _default_date_to(self, cr, uid, context=None):
|
||||
user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
|
||||
r = user.company_id and user.company_id.timesheet_range or 'month'
|
||||
if r=='month':
|
||||
return (datetime.today() + relativedelta(months=+1,day=1,days=-1)).strftime('%Y-%m-%d')
|
||||
elif r=='week':
|
||||
return (datetime.today() + relativedelta(weekday=6)).strftime('%Y-%m-%d')
|
||||
elif r=='year':
|
||||
return time.strftime('%Y-12-31')
|
||||
return fields.date.context_today(self, cr, uid, context)
|
||||
|
||||
def _default_employee(self, cr, uid, context=None):
|
||||
emp_ids = self.pool.get('hr.employee').search(cr, uid, [('user_id','=',uid)], context=context)
|
||||
return emp_ids and emp_ids[0] or False
|
||||
|
||||
_defaults = {
|
||||
'date_from' : _default_date_from,
|
||||
'date_to' : _default_date_to,
|
||||
'state': 'new',
|
||||
'employee_id': _default_employee,
|
||||
'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'hr_timesheet_sheet.sheet', context=c)
|
||||
}
|
||||
|
||||
def _sheet_date(self, cr, uid, ids, forced_user_id=False, context=None):
|
||||
for sheet in self.browse(cr, uid, ids, context=context):
|
||||
new_user_id = forced_user_id or sheet.employee_id.user_id and sheet.employee_id.user_id.id
|
||||
if new_user_id:
|
||||
cr.execute('SELECT id \
|
||||
FROM hr_timesheet_sheet_sheet \
|
||||
WHERE (date_from <= %s and %s <= date_to) \
|
||||
AND user_id=%s \
|
||||
AND id <> %s',(sheet.date_to, sheet.date_from, new_user_id, sheet.id))
|
||||
if cr.fetchall():
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
_constraints = [
|
||||
(_sheet_date, 'You cannot have 2 timesheets that overlap!\nPlease use the menu \'My Current Timesheet\' to avoid this problem.', ['date_from','date_to']),
|
||||
]
|
||||
|
||||
def action_set_to_draft(self, cr, uid, ids, *args):
|
||||
self.write(cr, uid, ids, {'state': 'draft'})
|
||||
self.create_workflow(cr, uid, ids)
|
||||
return True
|
||||
|
||||
def name_get(self, cr, uid, ids, context=None):
|
||||
if not ids:
|
||||
return []
|
||||
if isinstance(ids, (long, int)):
|
||||
ids = [ids]
|
||||
# week number according to ISO 8601 Calendar
|
||||
return [(r['id'], _('Week ')+str(datetime.strptime(r['date_from'], '%Y-%m-%d').isocalendar()[1])) \
|
||||
for r in self.read(cr, uid, ids, ['date_from'],
|
||||
context=context, load='_classic_write')]
|
||||
|
||||
def unlink(self, cr, uid, ids, context=None):
|
||||
sheets = self.read(cr, uid, ids, ['state','total_attendance'], context=context)
|
||||
for sheet in sheets:
|
||||
if sheet['state'] in ('confirm', 'done'):
|
||||
raise UserError(_('You cannot delete a timesheet which is already confirmed.'))
|
||||
elif sheet['total_attendance'] <> 0.00:
|
||||
raise UserError(_('You cannot delete a timesheet which have attendance entries.'))
|
||||
|
||||
toremove = []
|
||||
analytic_timesheet = self.pool.get('account.analytic.line')
|
||||
for sheet in self.browse(cr, uid, ids, context=context):
|
||||
for timesheet in sheet.timesheet_ids:
|
||||
toremove.append(timesheet.id)
|
||||
analytic_timesheet.unlink(cr, uid, toremove, context=context)
|
||||
|
||||
return super(hr_timesheet_sheet, self).unlink(cr, uid, ids, context=context)
|
||||
|
||||
def onchange_employee_id(self, cr, uid, ids, employee_id, context=None):
|
||||
department_id = False
|
||||
user_id = False
|
||||
if employee_id:
|
||||
empl_id = self.pool.get('hr.employee').browse(cr, uid, employee_id, context=context)
|
||||
department_id = empl_id.department_id.id
|
||||
user_id = empl_id.user_id.id
|
||||
return {'value': {'department_id': department_id, 'user_id': user_id,}}
|
||||
|
||||
# ------------------------------------------------
|
||||
# OpenChatter methods and notifications
|
||||
# ------------------------------------------------
|
||||
|
||||
def _track_subtype(self, cr, uid, ids, init_values, context=None):
|
||||
record = self.browse(cr, uid, ids[0], context=context)
|
||||
if 'state' in init_values and record.state == 'confirm':
|
||||
return 'hr_timesheet_sheet.mt_timesheet_confirmed'
|
||||
elif 'state' in init_values and record.state == 'done':
|
||||
return 'hr_timesheet_sheet.mt_timesheet_approved'
|
||||
return super(hr_timesheet_sheet, self)._track_subtype(cr, uid, ids, init_values, context=context)
|
||||
|
||||
def _needaction_domain_get(self, cr, uid, context=None):
|
||||
emp_obj = self.pool.get('hr.employee')
|
||||
empids = emp_obj.search(cr, uid, [('parent_id.user_id', '=', uid)], context=context)
|
||||
if not empids:
|
||||
return False
|
||||
dom = ['&', ('state', '=', 'confirm'), ('employee_id', 'in', empids)]
|
||||
return dom
|
||||
|
||||
|
||||
class account_analytic_line(osv.osv):
|
||||
_inherit = "account.analytic.line"
|
||||
|
||||
def _get_default_date(self, cr, uid, context=None):
|
||||
if context is None:
|
||||
context = {}
|
||||
# get the default date (should be: today)
|
||||
res = super(account_analytic_line, self)._get_default_date(cr, uid, context=context)
|
||||
# if we got the dates from and to from the timesheet and if the default date is in between, we use the default
|
||||
# but if the default isn't included in those dates, we use the date start of the timesheet as default
|
||||
if context.get('timesheet_date_from') and context.get('timesheet_date_to'):
|
||||
if context['timesheet_date_from'] <= res <= context['timesheet_date_to']:
|
||||
return res
|
||||
return context.get('timesheet_date_from')
|
||||
# if we don't get the dates from the timesheet, we return the default value from super()
|
||||
return res
|
||||
|
||||
def _sheet(self, cursor, user, ids, name, args, context=None):
|
||||
sheet_obj = self.pool.get('hr_timesheet_sheet.sheet')
|
||||
res = {}.fromkeys(ids, False)
|
||||
for ts_line in self.browse(cursor, user, ids, context=context):
|
||||
if not ts_line.project_id:
|
||||
continue
|
||||
sheet_ids = sheet_obj.search(cursor, user,
|
||||
[('date_to', '>=', ts_line.date), ('date_from', '<=', ts_line.date),
|
||||
('employee_id.user_id', '=', ts_line.user_id.id),
|
||||
('state', 'in', ['draft', 'new'])],
|
||||
context=context)
|
||||
if sheet_ids:
|
||||
# [0] because only one sheet possible for an employee between 2 dates
|
||||
res[ts_line.id] = sheet_obj.name_get(cursor, user, sheet_ids, context=context)[0]
|
||||
return res
|
||||
|
||||
def _get_hr_timesheet_sheet(self, cr, uid, ids, context=None):
|
||||
ts_line_ids = []
|
||||
for ts in self.browse(cr, uid, ids, context=context):
|
||||
cr.execute("""
|
||||
SELECT l.id
|
||||
FROM account_analytic_line l
|
||||
WHERE %(date_to)s >= l.date
|
||||
AND %(date_from)s <= l.date
|
||||
AND %(user_id)s = l.user_id
|
||||
AND l.project_id IS NOT NULL
|
||||
GROUP BY l.id""", {'date_from': ts.date_from,
|
||||
'date_to': ts.date_to,
|
||||
'user_id': ts.employee_id.user_id.id,})
|
||||
ts_line_ids.extend([row[0] for row in cr.fetchall()])
|
||||
return ts_line_ids
|
||||
|
||||
_columns = {
|
||||
'sheet_id': fields.function(_sheet, string='Sheet', select="1",
|
||||
type='many2one', relation='hr_timesheet_sheet.sheet', ondelete="cascade",
|
||||
store={
|
||||
'hr_timesheet_sheet.sheet': (_get_hr_timesheet_sheet, ['employee_id', 'date_from', 'date_to'], 10),
|
||||
'account.analytic.line': (lambda self,cr,uid,ids,context=None: ids, ['user_id', 'date'], 10),
|
||||
},
|
||||
),
|
||||
}
|
||||
|
||||
def write(self, cr, uid, ids, values, context=None):
|
||||
if isinstance(ids, (int, long)):
|
||||
ids = [ids]
|
||||
self._check(cr, uid, ids)
|
||||
return super(account_analytic_line, self).write(cr, uid, ids, values,context=context)
|
||||
|
||||
def unlink(self, cr, uid, ids, *args, **kwargs):
|
||||
if isinstance(ids, (int, long)):
|
||||
ids = [ids]
|
||||
self._check(cr, uid, ids)
|
||||
return super(account_analytic_line,self).unlink(cr, uid, ids,*args, **kwargs)
|
||||
|
||||
def _check(self, cr, uid, ids):
|
||||
for att in self.browse(cr, uid, ids):
|
||||
if att.sheet_id and att.sheet_id.state not in ('draft', 'new'):
|
||||
raise UserError(_('You cannot modify an entry in a confirmed timesheet.'))
|
||||
return True
|
||||
|
||||
|
||||
class hr_attendance(osv.osv):
|
||||
_inherit = "hr.attendance"
|
||||
|
||||
def _get_default_date(self, cr, uid, context=None):
|
||||
if context is None:
|
||||
context = {}
|
||||
if 'name' in context:
|
||||
return context['name'] + time.strftime(' %H:%M:%S')
|
||||
return time.strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
def _get_hr_timesheet_sheet(self, cr, uid, ids, context=None):
|
||||
attendance_ids = []
|
||||
for ts in self.browse(cr, uid, ids, context=context):
|
||||
cr.execute("""
|
||||
SELECT a.id
|
||||
FROM hr_attendance a
|
||||
INNER JOIN hr_employee e
|
||||
INNER JOIN resource_resource r
|
||||
ON (e.resource_id = r.id)
|
||||
ON (a.employee_id = e.id)
|
||||
LEFT JOIN res_users u
|
||||
ON r.user_id = u.id
|
||||
LEFT JOIN res_partner p
|
||||
ON u.partner_id = p.id
|
||||
WHERE %(date_to)s >= date_trunc('day', a.name AT TIME ZONE 'UTC' AT TIME ZONE coalesce(p.tz, 'UTC'))
|
||||
AND %(date_from)s <= date_trunc('day', a.name AT TIME ZONE 'UTC' AT TIME ZONE coalesce(p.tz, 'UTC'))
|
||||
AND %(user_id)s = r.user_id
|
||||
GROUP BY a.id""", {'date_from': ts.date_from,
|
||||
'date_to': ts.date_to,
|
||||
'user_id': ts.employee_id.user_id.id,})
|
||||
attendance_ids.extend([row[0] for row in cr.fetchall()])
|
||||
return attendance_ids
|
||||
|
||||
def _get_attendance_employee_tz(self, cr, uid, employee_id, date, context=None):
|
||||
""" Simulate timesheet in employee timezone
|
||||
|
||||
Return the attendance date in string format in the employee
|
||||
tz converted from utc timezone as we consider date of employee
|
||||
timesheet is in employee timezone
|
||||
"""
|
||||
employee_obj = self.pool['hr.employee']
|
||||
|
||||
tz = False
|
||||
if employee_id:
|
||||
employee = employee_obj.browse(cr, uid, employee_id, context=context)
|
||||
tz = employee.user_id.partner_id.tz
|
||||
|
||||
if not date:
|
||||
date = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
|
||||
att_tz = timezone(tz or 'utc')
|
||||
|
||||
attendance_dt = datetime.strptime(date, DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
att_tz_dt = pytz.utc.localize(attendance_dt)
|
||||
att_tz_dt = att_tz_dt.astimezone(att_tz)
|
||||
# We take only the date omiting the hours as we compare with timesheet
|
||||
# date_from which is a date format thus using hours would lead to
|
||||
# be out of scope of timesheet
|
||||
att_tz_date_str = datetime.strftime(att_tz_dt, DEFAULT_SERVER_DATE_FORMAT)
|
||||
return att_tz_date_str
|
||||
|
||||
def _get_current_sheet(self, cr, uid, employee_id, date=False, context=None):
|
||||
|
||||
sheet_obj = self.pool['hr_timesheet_sheet.sheet']
|
||||
if not date:
|
||||
date = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
|
||||
att_tz_date_str = self._get_attendance_employee_tz(
|
||||
cr, uid, employee_id,
|
||||
date=date, context=context)
|
||||
sheet_ids = sheet_obj.search(cr, uid,
|
||||
[('date_from', '<=', att_tz_date_str),
|
||||
('date_to', '>=', att_tz_date_str),
|
||||
('employee_id', '=', employee_id)],
|
||||
limit=1, context=context)
|
||||
return sheet_ids and sheet_ids[0] or False
|
||||
|
||||
def _sheet(self, cursor, user, ids, name, args, context=None):
|
||||
res = {}.fromkeys(ids, False)
|
||||
for attendance in self.browse(cursor, user, ids, context=context):
|
||||
res[attendance.id] = self._get_current_sheet(
|
||||
cursor, user, attendance.employee_id.id, attendance.name,
|
||||
context=context)
|
||||
return res
|
||||
|
||||
_columns = {
|
||||
'sheet_id': fields.function(_sheet, string='Sheet',
|
||||
type='many2one', relation='hr_timesheet_sheet.sheet',
|
||||
store={
|
||||
'hr_timesheet_sheet.sheet': (_get_hr_timesheet_sheet, ['employee_id', 'date_from', 'date_to'], 10),
|
||||
'hr.attendance': (lambda self,cr,uid,ids,context=None: ids, ['employee_id', 'name', 'day'], 10),
|
||||
},
|
||||
)
|
||||
}
|
||||
_defaults = {
|
||||
'name': _get_default_date,
|
||||
}
|
||||
|
||||
def create(self, cr, uid, vals, context=None):
|
||||
if context is None:
|
||||
context = {}
|
||||
|
||||
sheet_id = context.get('sheet_id') or self._get_current_sheet(cr, uid, vals.get('employee_id'), vals.get('name'), context=context)
|
||||
if sheet_id:
|
||||
att_tz_date_str = self._get_attendance_employee_tz(
|
||||
cr, uid, vals.get('employee_id'),
|
||||
date=vals.get('name'), context=context)
|
||||
ts = self.pool.get('hr_timesheet_sheet.sheet').browse(cr, uid, sheet_id, context=context)
|
||||
if ts.state not in ('draft', 'new'):
|
||||
raise UserError(_('You can not enter an attendance in a submitted timesheet. Ask your manager to reset it before adding attendance.'))
|
||||
elif ts.date_from > att_tz_date_str or ts.date_to < att_tz_date_str:
|
||||
raise UserError(_('You can not enter an attendance date outside the current timesheet dates.'))
|
||||
return super(hr_attendance,self).create(cr, uid, vals, context=context)
|
||||
|
||||
def unlink(self, cr, uid, ids, *args, **kwargs):
|
||||
if isinstance(ids, (int, long)):
|
||||
ids = [ids]
|
||||
self._check(cr, uid, ids)
|
||||
return super(hr_attendance,self).unlink(cr, uid, ids,*args, **kwargs)
|
||||
|
||||
def write(self, cr, uid, ids, vals, context=None):
|
||||
if context is None:
|
||||
context = {}
|
||||
if isinstance(ids, (int, long)):
|
||||
ids = [ids]
|
||||
self._check(cr, uid, ids)
|
||||
res = super(hr_attendance,self).write(cr, uid, ids, vals, context=context)
|
||||
if 'sheet_id' in context:
|
||||
for attendance in self.browse(cr, uid, ids, context=context):
|
||||
if context['sheet_id'] != attendance.sheet_id.id:
|
||||
raise UserError(_('You cannot enter an attendance ' \
|
||||
'date outside the current timesheet dates.'))
|
||||
return res
|
||||
|
||||
def _check(self, cr, uid, ids):
|
||||
for att in self.browse(cr, uid, ids):
|
||||
if att.sheet_id and att.sheet_id.state not in ('draft', 'new'):
|
||||
raise UserError(_('You cannot modify an entry in a confirmed timesheet'))
|
||||
return True
|
||||
|
||||
|
||||
class hr_timesheet_sheet_sheet_day(osv.osv):
|
||||
_name = "hr_timesheet_sheet.sheet.day"
|
||||
_description = "Timesheets by Period"
|
||||
_auto = False
|
||||
_order='name'
|
||||
_columns = {
|
||||
'name': fields.date('Date', readonly=True),
|
||||
'sheet_id': fields.many2one('hr_timesheet_sheet.sheet', 'Sheet', readonly=True, select="1"),
|
||||
'total_timesheet': fields.float('Total Timesheet', readonly=True),
|
||||
'total_attendance': fields.float('Attendance', readonly=True),
|
||||
'total_difference': fields.float('Difference', readonly=True),
|
||||
}
|
||||
_depends = {
|
||||
'account.analytic.line': ['date', 'unit_amount'],
|
||||
'hr.attendance': ['action', 'name', 'sheet_id'],
|
||||
}
|
||||
|
||||
def init(self, cr):
|
||||
cr.execute("""create or replace view hr_timesheet_sheet_sheet_day as
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
sheet_id,
|
||||
total_timesheet,
|
||||
total_attendance,
|
||||
cast(round(cast(total_attendance - total_timesheet as Numeric),2) as Double Precision) AS total_difference
|
||||
FROM
|
||||
((
|
||||
SELECT
|
||||
MAX(id) as id,
|
||||
name,
|
||||
sheet_id,
|
||||
timezone,
|
||||
SUM(total_timesheet) as total_timesheet,
|
||||
CASE WHEN SUM(orphan_attendances) != 0
|
||||
THEN (SUM(total_attendance) +
|
||||
CASE WHEN current_date <> name
|
||||
THEN 1440
|
||||
ELSE (EXTRACT(hour FROM current_time AT TIME ZONE 'UTC' AT TIME ZONE coalesce(timezone, 'UTC')) * 60) + EXTRACT(minute FROM current_time AT TIME ZONE 'UTC' AT TIME ZONE coalesce(timezone, 'UTC'))
|
||||
END
|
||||
)
|
||||
ELSE SUM(total_attendance)
|
||||
END /60 as total_attendance
|
||||
FROM
|
||||
((
|
||||
select
|
||||
min(l.id) as id,
|
||||
p.tz as timezone,
|
||||
l.date::date as name,
|
||||
s.id as sheet_id,
|
||||
sum(l.unit_amount) as total_timesheet,
|
||||
0 as orphan_attendances,
|
||||
0.0 as total_attendance
|
||||
from
|
||||
account_analytic_line l
|
||||
LEFT JOIN hr_timesheet_sheet_sheet s ON s.id = l.sheet_id
|
||||
JOIN hr_employee e ON s.employee_id = e.id
|
||||
JOIN resource_resource r ON e.resource_id = r.id
|
||||
LEFT JOIN res_users u ON r.user_id = u.id
|
||||
LEFT JOIN res_partner p ON u.partner_id = p.id
|
||||
group by l.date::date, s.id, timezone
|
||||
) union (
|
||||
select
|
||||
-min(a.id) as id,
|
||||
p.tz as timezone,
|
||||
(a.name AT TIME ZONE 'UTC' AT TIME ZONE coalesce(p.tz, 'UTC'))::date as name,
|
||||
s.id as sheet_id,
|
||||
0.0 as total_timesheet,
|
||||
SUM(CASE WHEN a.action = 'sign_in' THEN -1 ELSE 1 END) as orphan_attendances,
|
||||
SUM(((EXTRACT(hour FROM (a.name AT TIME ZONE 'UTC' AT TIME ZONE coalesce(p.tz, 'UTC'))) * 60) + EXTRACT(minute FROM (a.name AT TIME ZONE 'UTC' AT TIME ZONE coalesce(p.tz, 'UTC')))) * (CASE WHEN a.action = 'sign_in' THEN -1 ELSE 1 END)) as total_attendance
|
||||
from
|
||||
hr_attendance a
|
||||
LEFT JOIN hr_timesheet_sheet_sheet s
|
||||
ON s.id = a.sheet_id
|
||||
JOIN hr_employee e
|
||||
ON a.employee_id = e.id
|
||||
JOIN resource_resource r
|
||||
ON e.resource_id = r.id
|
||||
LEFT JOIN res_users u
|
||||
ON r.user_id = u.id
|
||||
LEFT JOIN res_partner p
|
||||
ON u.partner_id = p.id
|
||||
WHERE action in ('sign_in', 'sign_out')
|
||||
group by (a.name AT TIME ZONE 'UTC' AT TIME ZONE coalesce(p.tz, 'UTC'))::date, s.id, timezone
|
||||
)) AS foo
|
||||
GROUP BY name, sheet_id, timezone
|
||||
)) AS bar""")
|
||||
|
||||
|
||||
|
||||
class hr_timesheet_sheet_sheet_account(osv.osv):
|
||||
_name = "hr_timesheet_sheet.sheet.account"
|
||||
_description = "Timesheets by Period"
|
||||
_auto = False
|
||||
_order='name'
|
||||
_columns = {
|
||||
'name': fields.many2one('account.analytic.account', 'Project / Analytic Account', readonly=True),
|
||||
'sheet_id': fields.many2one('hr_timesheet_sheet.sheet', 'Sheet', readonly=True),
|
||||
'total': fields.float('Total Time', digits=(16,2), readonly=True),
|
||||
}
|
||||
|
||||
_depends = {
|
||||
'account.analytic.line': ['account_id', 'date', 'unit_amount', 'user_id'],
|
||||
'hr_timesheet_sheet.sheet': ['date_from', 'date_to', 'user_id'],
|
||||
}
|
||||
|
||||
def init(self, cr):
|
||||
cr.execute("""create or replace view hr_timesheet_sheet_sheet_account as (
|
||||
select
|
||||
min(l.id) as id,
|
||||
l.account_id as name,
|
||||
s.id as sheet_id,
|
||||
sum(l.unit_amount) as total
|
||||
from
|
||||
account_analytic_line l
|
||||
LEFT JOIN hr_timesheet_sheet_sheet s
|
||||
ON (s.date_to >= l.date
|
||||
AND s.date_from <= l.date
|
||||
AND s.user_id = l.user_id)
|
||||
group by l.account_id, s.id
|
||||
)""")
|
||||
|
||||
|
||||
|
||||
|
||||
class res_company(osv.osv):
|
||||
_inherit = 'res.company'
|
||||
_columns = {
|
||||
'timesheet_range': fields.selection([('week','Week'),('month','Month')], 'Timesheet range',
|
||||
help="Periodicity on which you validate your timesheets."),
|
||||
'timesheet_max_difference': fields.float('Timesheet allowed difference(Hours)',
|
||||
help="Allowed difference in hours between the sign in/out and the timesheet " \
|
||||
"computation for one sheet. Set this to 0 if you do not want any control."),
|
||||
}
|
||||
_defaults = {
|
||||
'timesheet_range': lambda *args: 'week',
|
||||
'timesheet_max_difference': lambda *args: 0.0
|
||||
}
|
||||
|
||||
class hr_employee(osv.osv):
|
||||
'''
|
||||
Employee
|
||||
'''
|
||||
|
||||
_inherit = 'hr.employee'
|
||||
_description = 'Employee'
|
||||
|
||||
def _timesheet_count(self, cr, uid, ids, field_name, arg, context=None):
|
||||
Sheet = self.pool['hr_timesheet_sheet.sheet']
|
||||
return {
|
||||
employee_id: Sheet.search_count(cr,uid, [('employee_id', '=', employee_id)], context=context)
|
||||
for employee_id in ids
|
||||
}
|
||||
|
||||
_columns = {
|
||||
'timesheet_count': fields.function(_timesheet_count, type='integer', string='Timesheets'),
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import account_analytic_line
|
||||
import hr_department
|
||||
import hr_employee
|
||||
import hr_timesheet_sheet
|
||||
import hr_timesheet_sheet_config_settings
|
||||
import res_company
|
||||
@@ -0,0 +1,61 @@
|
||||
# -*- 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
|
||||
|
||||
|
||||
class AccountAnalyticLine(models.Model):
|
||||
_inherit = "account.analytic.line"
|
||||
|
||||
sheet_id_computed = fields.Many2one('hr_timesheet_sheet.sheet', string='Sheet', compute='_compute_sheet', index=True, ondelete='cascade',
|
||||
search='_search_sheet')
|
||||
sheet_id = fields.Many2one('hr_timesheet_sheet.sheet', compute='_compute_sheet', string='Sheet', store=True)
|
||||
|
||||
@api.depends('date', 'user_id', 'project_id', 'sheet_id_computed.date_to', 'sheet_id_computed.date_from', 'sheet_id_computed.employee_id')
|
||||
def _compute_sheet(self):
|
||||
"""Links the timesheet line to the corresponding sheet
|
||||
"""
|
||||
for ts_line in self:
|
||||
if not ts_line.project_id:
|
||||
continue
|
||||
sheets = self.env['hr_timesheet_sheet.sheet'].search(
|
||||
[('date_to', '>=', ts_line.date), ('date_from', '<=', ts_line.date),
|
||||
('employee_id.user_id.id', '=', ts_line.user_id.id),
|
||||
('state', 'in', ['draft', 'new'])])
|
||||
if sheets:
|
||||
# [0] because only one sheet possible for an employee between 2 dates
|
||||
ts_line.sheet_id_computed = sheets[0]
|
||||
ts_line.sheet_id = sheets[0]
|
||||
|
||||
def _search_sheet(self, operator, value):
|
||||
assert operator == 'in'
|
||||
ids = []
|
||||
for ts in self.env['hr_timesheet_sheet.sheet'].browse(value):
|
||||
self._cr.execute("""
|
||||
SELECT l.id
|
||||
FROM account_analytic_line l
|
||||
WHERE %(date_to)s >= l.date
|
||||
AND %(date_from)s <= l.date
|
||||
AND %(user_id)s = l.user_id
|
||||
GROUP BY l.id""", {'date_from': ts.date_from,
|
||||
'date_to': ts.date_to,
|
||||
'user_id': ts.employee_id.user_id.id, })
|
||||
ids.extend([row[0] for row in self._cr.fetchall()])
|
||||
return [('id', 'in', ids)]
|
||||
|
||||
@api.multi
|
||||
def write(self, values):
|
||||
self._check_state()
|
||||
return super(AccountAnalyticLine, self).write(values)
|
||||
|
||||
@api.multi
|
||||
def unlink(self):
|
||||
self._check_state()
|
||||
return super(AccountAnalyticLine, self).unlink()
|
||||
|
||||
def _check_state(self):
|
||||
for line in self:
|
||||
if line.sheet_id and line.sheet_id.state not in ('draft', 'new'):
|
||||
raise UserError(_('You cannot modify an entry in a confirmed timesheet.'))
|
||||
return True
|
||||
+7
-5
@@ -1,10 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from openerp import api, fields, models
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class hr_department(models.Model):
|
||||
class HrDepartment(models.Model):
|
||||
_inherit = 'hr.department'
|
||||
|
||||
timesheet_to_approve_count = fields.Integer(
|
||||
compute='_compute_timesheet_to_approve', string='Timesheet to Approve')
|
||||
|
||||
@api.multi
|
||||
def _compute_timesheet_to_approve(self):
|
||||
timesheet_data = self.env['hr_timesheet_sheet.sheet'].read_group(
|
||||
@@ -12,6 +17,3 @@ class hr_department(models.Model):
|
||||
result = dict((data['department_id'][0], data['department_id_count']) for data in timesheet_data)
|
||||
for department in self:
|
||||
department.timesheet_to_approve_count = result.get(department.id, 0)
|
||||
|
||||
timesheet_to_approve_count = fields.Integer(
|
||||
compute='_compute_timesheet_to_approve', string='Timesheet to Approve')
|
||||
@@ -0,0 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class HrEmployee(models.Model):
|
||||
_inherit = 'hr.employee'
|
||||
_description = 'Employee'
|
||||
|
||||
timesheet_count = fields.Integer(compute='_compute_timesheet_count', string='Timesheets')
|
||||
|
||||
@api.multi
|
||||
def _compute_timesheet_count(self):
|
||||
for employee in self:
|
||||
employee.timesheet_count = employee.env['hr_timesheet_sheet.sheet'].search_count([('employee_id', '=', employee.id)])
|
||||
@@ -0,0 +1,200 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import time
|
||||
from datetime import datetime
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.tools.translate import _
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
|
||||
|
||||
class HrTimesheetSheet(models.Model):
|
||||
_name = "hr_timesheet_sheet.sheet"
|
||||
_inherit = ['mail.thread', 'ir.needaction_mixin']
|
||||
_table = 'hr_timesheet_sheet_sheet'
|
||||
_order = "id desc"
|
||||
_description = "Timesheet"
|
||||
|
||||
def _default_date_from(self):
|
||||
user = self.env['res.users'].browse(self.env.uid)
|
||||
r = user.company_id and user.company_id.timesheet_range or 'month'
|
||||
if r == 'month':
|
||||
return time.strftime('%Y-%m-01')
|
||||
elif r == 'week':
|
||||
return (datetime.today() + relativedelta(weekday=0, days=-6)).strftime('%Y-%m-%d')
|
||||
elif r == 'year':
|
||||
return time.strftime('%Y-01-01')
|
||||
return fields.date.context_today(self)
|
||||
|
||||
def _default_date_to(self):
|
||||
user = self.env['res.users'].browse(self.env.uid)
|
||||
r = user.company_id and user.company_id.timesheet_range or 'month'
|
||||
if r == 'month':
|
||||
return (datetime.today() + relativedelta(months=+1, day=1, days=-1)).strftime('%Y-%m-%d')
|
||||
elif r == 'week':
|
||||
return (datetime.today() + relativedelta(weekday=6)).strftime('%Y-%m-%d')
|
||||
elif r == 'year':
|
||||
return time.strftime('%Y-12-31')
|
||||
return fields.date.context_today(self)
|
||||
|
||||
def _default_employee(self):
|
||||
emp_ids = self.env['hr.employee'].search([('user_id', '=', self.env.uid)])
|
||||
return emp_ids and emp_ids[0] or False
|
||||
|
||||
name = fields.Char(string="Note", states={'confirm': [('readonly', True)], 'done': [('readonly', True)]})
|
||||
employee_id = fields.Many2one('hr.employee', string='Employee', default=_default_employee, required=True)
|
||||
user_id = fields.Many2one('res.users', related='employee_id.user_id', string='User', store=True, readonly=True)
|
||||
date_from = fields.Date(string='Date From', default=_default_date_from, required=True,
|
||||
index=True, readonly=True, states={'new': [('readonly', False)]})
|
||||
date_to = fields.Date(string='Date To', default=_default_date_to, required=True,
|
||||
index=True, readonly=True, states={'new': [('readonly', False)]})
|
||||
timesheet_ids = fields.One2many('account.analytic.line', 'sheet_id',
|
||||
string='Timesheet lines',
|
||||
readonly=True, states={
|
||||
'draft': [('readonly', False)],
|
||||
'new': [('readonly', False)]})
|
||||
state = fields.Selection([
|
||||
('new', 'New'),
|
||||
('draft', 'Open'),
|
||||
('confirm', 'Waiting Approval'),
|
||||
('done', 'Approved')], default='new', track_visibility='onchange',
|
||||
string='Status', required=True, readonly=True, index=True,
|
||||
help=' * The \'Open\' status is used when a user is encoding a new and unconfirmed timesheet. \
|
||||
\n* The \'Waiting Approval\' status is used to confirm the timesheet by user. \
|
||||
\n* The \'Approved\' status is used when the users timesheet is accepted by his/her senior.')
|
||||
account_ids = fields.One2many('hr_timesheet_sheet.sheet.account', 'sheet_id', string='Analytic accounts', readonly=True)
|
||||
company_id = fields.Many2one('res.company', string='Company')
|
||||
department_id = fields.Many2one('hr.department', string='Department',
|
||||
default=lambda self: self.env['res.company']._company_default_get())
|
||||
|
||||
@api.constrains('date_to', 'date_from', 'employee_id')
|
||||
def _check_sheet_date(self, forced_user_id=False):
|
||||
for sheet in self:
|
||||
new_user_id = forced_user_id or sheet.user_id and sheet.user_id.id
|
||||
if new_user_id:
|
||||
self.env.cr.execute('SELECT id \
|
||||
FROM hr_timesheet_sheet_sheet \
|
||||
WHERE (date_from <= %s and %s <= date_to) \
|
||||
AND user_id=%s \
|
||||
AND id <> %s', (sheet.date_to, sheet.date_from, new_user_id, sheet.id))
|
||||
if any(self.env.cr.fetchall()):
|
||||
raise ValidationError('You cannot have 2 timesheets that overlap!\nPlease use the menu \'My Current Timesheet\' to avoid this problem.')
|
||||
|
||||
@api.multi
|
||||
@api.onchange('employee_id')
|
||||
def onchange_employee_id(self, employee_id):
|
||||
employee = self.env['hr.employee'].browse(employee_id)
|
||||
for sheet in self:
|
||||
if employee_id:
|
||||
sheet.department_id = employee.department_id
|
||||
sheet.user_id = employee.user_id
|
||||
|
||||
def copy(self, *args, **argv):
|
||||
raise UserError(_('You cannot duplicate a timesheet.'))
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
if 'employee_id' in vals:
|
||||
if not self.env['hr.employee'].browse(vals['employee_id']).user_id:
|
||||
raise UserError(_('In order to create a timesheet for this employee, you must link him/her to a user.'))
|
||||
return super(HrTimesheetSheet, self).create(vals)
|
||||
|
||||
@api.multi
|
||||
def write(self, vals):
|
||||
if 'employee_id' in vals:
|
||||
new_user_id = self.env['hr.employee'].browse(vals['employee_id']).user_id.id
|
||||
if not new_user_id:
|
||||
raise UserError(_('In order to create a timesheet for this employee, you must link him/her to a user.'))
|
||||
self._check_sheet_date(forced_user_id=new_user_id)
|
||||
return super(HrTimesheetSheet, self).write(vals)
|
||||
|
||||
@api.multi
|
||||
def button_confirm(self):
|
||||
for sheet in self:
|
||||
if sheet.employee_id and sheet.employee_id.parent_id and sheet.employee_id.parent_id.user_id:
|
||||
self.message_subscribe_users(user_ids=[sheet.employee_id.parent_id.user_id.id])
|
||||
sheet.signal_workflow('confirm')
|
||||
return True
|
||||
|
||||
@api.multi
|
||||
def action_set_to_draft(self):
|
||||
self.write({'state': 'draft'})
|
||||
self.create_workflow()
|
||||
return True
|
||||
|
||||
@api.multi
|
||||
def name_get(self):
|
||||
# week number according to ISO 8601 Calendar
|
||||
return [(r['id'], _('Week ') + str(datetime.strptime(r['date_from'], '%Y-%m-%d').isocalendar()[1]))
|
||||
for r in self.read(['date_from'], load='_classic_write')]
|
||||
|
||||
@api.multi
|
||||
def unlink(self):
|
||||
sheets = self.read(['state'])
|
||||
for sheet in sheets:
|
||||
if sheet['state'] in ('confirm', 'done'):
|
||||
raise UserError(_('You cannot delete a timesheet which is already confirmed.'))
|
||||
|
||||
analytic_timesheet_toremove = self.env['account.analytic.line']
|
||||
for sheet in self:
|
||||
analytic_timesheet_toremove += sheet.timesheet_ids
|
||||
analytic_timesheet_toremove.unlink()
|
||||
|
||||
return super(HrTimesheetSheet, self).unlink()
|
||||
|
||||
# ------------------------------------------------
|
||||
# OpenChatter methods and notifications
|
||||
# ------------------------------------------------
|
||||
|
||||
@api.multi
|
||||
def _track_subtype(self, init_values):
|
||||
if self:
|
||||
record = self[0]
|
||||
if 'state' in init_values and record.state == 'confirm':
|
||||
return 'hr_timesheet_sheet.mt_timesheet_confirmed'
|
||||
elif 'state' in init_values and record.state == 'done':
|
||||
return 'hr_timesheet_sheet.mt_timesheet_approved'
|
||||
return super(HrTimesheetSheet, self)._track_subtype(init_values)
|
||||
|
||||
@api.model
|
||||
def _needaction_domain_get(self):
|
||||
empids = self.env['hr.employee'].search([('parent_id.user_id', '=', self.env.uid)])
|
||||
if not empids:
|
||||
return False
|
||||
return ['&', ('state', '=', 'confirm'), ('employee_id', 'in', empids.ids)]
|
||||
|
||||
|
||||
class HrTimesheetSheetSheetAccount(models.Model):
|
||||
_name = "hr_timesheet_sheet.sheet.account"
|
||||
_description = "Timesheets by Period"
|
||||
_auto = False
|
||||
_order = 'name'
|
||||
|
||||
name = fields.Many2one('account.analytic.account', string='Project / Analytic Account', readonly=True)
|
||||
sheet_id = fields.Many2one('hr_timesheet_sheet.sheet', string='Sheet', readonly=True)
|
||||
total = fields.Float('Total Time', digits=(16, 2), readonly=True)
|
||||
|
||||
# still seing _depends in BaseModel, ok to leave this as is?
|
||||
_depends = {
|
||||
'account.analytic.line': ['account_id', 'date', 'unit_amount', 'user_id'],
|
||||
'hr_timesheet_sheet.sheet': ['date_from', 'date_to', 'user_id'],
|
||||
}
|
||||
|
||||
@api.model_cr
|
||||
def init(self):
|
||||
self._cr.execute("""create or replace view hr_timesheet_sheet_sheet_account as (
|
||||
select
|
||||
min(l.id) as id,
|
||||
l.account_id as name,
|
||||
s.id as sheet_id,
|
||||
sum(l.unit_amount) as total
|
||||
from
|
||||
account_analytic_line l
|
||||
LEFT JOIN hr_timesheet_sheet_sheet s
|
||||
ON (s.date_to >= l.date
|
||||
AND s.date_from <= l.date
|
||||
AND s.user_id = l.user_id)
|
||||
group by l.account_id, s.id
|
||||
)""")
|
||||
+1
@@ -3,6 +3,7 @@
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class HrTimesheetConfiguration(models.TransientModel):
|
||||
_inherit = 'project.config.settings'
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResCompany(models.Model):
|
||||
_inherit = 'res.company'
|
||||
|
||||
timesheet_range = fields.Selection([('week', 'Week'), ('month', 'Month')],
|
||||
default='week', string='Timesheet range', help="Periodicity on which you validate your timesheets.")
|
||||
timesheet_max_difference = fields.Float(string='Timesheet allowed difference(Hours)',
|
||||
help="Allowed difference in hours between the sign in/out and the timesheet " \
|
||||
"computation for one sheet. Set this to 0 if you do not want any control.",
|
||||
default=0.0)
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0"?>
|
||||
<openerp>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
|
||||
<record model="ir.rule" id="timesheet_comp_rule">
|
||||
@@ -10,4 +10,4 @@
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
</odoo>
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_hr_timesheet_sheet_sheet_user,hr_timesheet_sheet.sheet.user,model_hr_timesheet_sheet_sheet,base.group_user,1,1,1,1
|
||||
access_hr_timesheet_sheet_sheet_system_employee,hr_timesheet_sheet.sheet.system.employee,model_hr_timesheet_sheet_sheet,base.group_user,1,1,1,0
|
||||
access_hr_timesheet_sheet_sheet_day,hr_timesheet_sheet.sheet.day,model_hr_timesheet_sheet_sheet_day,base.group_hr_user,1,1,1,1
|
||||
access_hr_timesheet_sheet_sheet_account,hr_timesheet_sheet.sheet.account,model_hr_timesheet_sheet_sheet_account,base.group_hr_user,1,1,1,1
|
||||
access_hr_timesheet_sheet_sheet_day,hr.timesheet.sheet.sheet.day.user,model_hr_timesheet_sheet_sheet_day,base.group_user,1,1,1,0
|
||||
|
||||
|
@@ -1,106 +0,0 @@
|
||||
-
|
||||
In order to test hr_timesheet_sheet module in OpenERP,
|
||||
I create Timesheet and check validation process done by manager.
|
||||
-
|
||||
I set TimeSheet allowed difference for company
|
||||
-
|
||||
!record {model: res.company, id: base.main_company}:
|
||||
timesheet_max_difference: 1.00
|
||||
-
|
||||
I create a timesheet for employee "Gilles Gravie".
|
||||
-
|
||||
!record {model: hr_timesheet_sheet.sheet, id: hr_timesheet_sheet_sheet_deddk0}:
|
||||
date_from: !eval time.strftime('%Y-%m-01')
|
||||
name: Gilles Gravie
|
||||
state: new
|
||||
user_id: base.user_demo
|
||||
employee_id: 'hr.employee_qdp'
|
||||
-
|
||||
Gilles "Sign In" at around 9am.
|
||||
-
|
||||
!record {model: hr.attendance, id: hr_attendance_1}:
|
||||
action: sign_in
|
||||
employee_id: 'hr.employee_qdp'
|
||||
name: !eval datetime.now().strftime('%Y-%m-%d 09:12:37')
|
||||
-
|
||||
I test that Gilles in signed in
|
||||
-
|
||||
!assert {model: hr.employee, id: hr.employee_qdp}:
|
||||
- state == 'present'
|
||||
-
|
||||
At the time of logout, I create attendance and perform "Sign Out".
|
||||
-
|
||||
!record {model: hr.attendance, id: hr_attendance_2}:
|
||||
action: sign_out
|
||||
employee_id: 'hr.employee_qdp'
|
||||
name: !eval datetime.now().strftime('%Y-%m-%d 17:30:00')
|
||||
-
|
||||
I create Timesheet Entry for time spend on today work.
|
||||
-
|
||||
!record {model: hr_timesheet_sheet.sheet, id: hr_timesheet_sheet_sheet_deddk0}:
|
||||
timesheet_ids:
|
||||
- project_id: project.project_project_2
|
||||
date: !eval time.strftime('%Y-%m-%d')
|
||||
name: 'Develop yaml for hr module'
|
||||
user_id: base.user_demo
|
||||
unit_amount: 6.00
|
||||
amount: -90.00
|
||||
product_id: product.product_product_1
|
||||
general_account_id: a_expense
|
||||
-
|
||||
I confirm my Timesheet at end of period by click on "Confirm" button,
|
||||
if the difference between Timesheet hour and attendance hour is more than 1 hour it will give message.
|
||||
-
|
||||
!python {model: hr_timesheet_sheet.sheet}: |
|
||||
uid = ref('base.user_root')
|
||||
from openerp import netsvc
|
||||
from openerp.osv import osv
|
||||
try:
|
||||
self.button_confirm(cr, uid, [ref('hr_timesheet_sheet_sheet_deddk0')], {"active_ids":
|
||||
[ref("hr_timesheet_sheet.menu_act_hr_timesheet_sheet_form")],"active_id": ref("hr_timesheet_sheet.menu_act_hr_timesheet_sheet_form"),
|
||||
})
|
||||
assert False, "The validation of the timesheet was unexpectedly accepted despite the 2:30 hours of difference"
|
||||
except osv.except_osv:
|
||||
pass
|
||||
-
|
||||
I Modified the timesheet record and make the difference less than 1 hour.
|
||||
-
|
||||
!record {model: hr_timesheet_sheet.sheet, id: hr_timesheet_sheet_sheet_deddk0}:
|
||||
timesheet_ids:
|
||||
- project_id: project.project_project_2
|
||||
date: !eval time.strftime('%Y-%m-%d')
|
||||
name: 'Develop yaml for hr module'
|
||||
unit_amount: 2.00
|
||||
user_id: base.user_demo
|
||||
amount: -90.00
|
||||
product_id: product.product_product_1
|
||||
general_account_id: a_expense
|
||||
-
|
||||
I set an allowed difference of 1 hour
|
||||
-
|
||||
!record {model: res.company, id: base.main_company}:
|
||||
timesheet_max_difference: 1.00
|
||||
-
|
||||
I try again to confirm the timesheet after modification.
|
||||
-
|
||||
!python {model: hr_timesheet_sheet.sheet}: |
|
||||
uid = ref('base.user_root')
|
||||
self.button_confirm(cr, uid, [ref('hr_timesheet_sheet_sheet_deddk0')], {"active_ids":
|
||||
[ref("hr_timesheet_sheet.menu_act_hr_timesheet_sheet_form")],"active_id": ref("hr_timesheet_sheet.menu_act_hr_timesheet_sheet_form"),
|
||||
})
|
||||
-
|
||||
This time timesheet confirmed successfully.
|
||||
I check that state is "Confirmed".
|
||||
-
|
||||
!assert {model: hr_timesheet_sheet.sheet, id: hr_timesheet_sheet_sheet_deddk0}:
|
||||
- state == 'confirm'
|
||||
-
|
||||
The manager will accept This request by click on "Accept" button.
|
||||
-
|
||||
!python {model: hr_timesheet_sheet.sheet}: |
|
||||
self.write(cr, uid, [ref('hr_timesheet_sheet_sheet_deddk0')], {'state': 'done'})
|
||||
-
|
||||
I check that state is "Done".
|
||||
-
|
||||
!assert {model: hr_timesheet_sheet.sheet, id: hr_timesheet_sheet_sheet_deddk0}:
|
||||
- state == 'done'
|
||||
+4
-3
@@ -1,3 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!--Hr Department Inherit Kanban view-->
|
||||
<record id="hr_department_view_kanban" model="ir.ui.view">
|
||||
@@ -13,7 +14,7 @@
|
||||
<xpath expr="//div[contains(@class, 'o_kanban_primary_right')]" position="inside">
|
||||
<div t-if="record.timesheet_to_approve_count.raw_value > 0" class="row">
|
||||
<div class="col-xs-9">
|
||||
<a name="%(hr_timesheet_action_from_department)d" type="action">
|
||||
<a name="%(hr_timesheet_sheet.hr_timesheet_action_from_department)d" type="action">
|
||||
Timesheets
|
||||
</a>
|
||||
</div>
|
||||
@@ -26,7 +27,7 @@
|
||||
<xpath expr="//div[contains(@class, 'o_kanban_manage_to_approve')]" position="inside">
|
||||
<div class="row">
|
||||
<div class="col-xs-12 text-left">
|
||||
<a name="%(hr_timesheet_action_from_department)d" type="action">
|
||||
<a name="%(hr_timesheet_sheet.hr_timesheet_action_from_department)d" type="action">
|
||||
<t t-esc="record.timesheet_to_approve_count.raw_value or 0"/> Timesheets
|
||||
</a>
|
||||
</div>
|
||||
@@ -36,4 +37,4 @@
|
||||
</data>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
</odoo>
|
||||
+2
-4
@@ -1,11 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<odoo>
|
||||
<template id="assets_backend" name="hr_timesheet_sheet assets" inherit_id="web.assets_backend">
|
||||
<xpath expr="." position="inside">
|
||||
<link rel="stylesheet" href="/hr_timesheet_sheet/static/src/less/timesheet.less"/>
|
||||
<script type="text/javascript" src="/hr_timesheet_sheet/static/src/js/timesheet.js"></script>
|
||||
</xpath>
|
||||
</template>
|
||||
</data>
|
||||
</openerp>
|
||||
</odoo>
|
||||
+7
-147
@@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<odoo>
|
||||
|
||||
<record id="hr_timesheet_account_form" model="ir.ui.view">
|
||||
<field name="name">hr.timesheet.account.form</field>
|
||||
@@ -37,25 +36,6 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
<record model="ir.actions.act_window" id="act_hr_timesheet_sheet_sheet_2_hr_analytic_timesheet">
|
||||
<field name="context">{'search_default_sheet_id': [active_id]}</field>
|
||||
<field name="name">Timesheet Activities</field>
|
||||
<field name="res_model">account.analytic.line</field>
|
||||
<field name="src_model">hr_timesheet_sheet.sheet</field>
|
||||
<field name="view_ids" eval="[(5, 0, 0),
|
||||
(0, 0, {'view_mode': 'tree', 'view_id': ref('hr_timesheet.hr_timesheet_line_tree')}),
|
||||
(0, 0, {'view_mode': 'form', 'view_id': ref('hr_timesheet.hr_timesheet_line_form')})]"/>
|
||||
</record>
|
||||
|
||||
|
||||
<record model="ir.actions.act_window" id="act_hr_timesheet_sheet_sheet_2_hr_attendance">
|
||||
<field name="context">{'search_default_sheet_id': [active_id]}</field>
|
||||
<field name="name">Attendances</field>
|
||||
<field name="res_model">hr.attendance</field>
|
||||
<field name="src_model">hr_timesheet_sheet.sheet</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_timesheet_sheet_tree_simplified" model="ir.ui.view">
|
||||
<field name="name">hr.timesheet.sheet.tree</field>
|
||||
<field name="model">hr_timesheet_sheet.sheet</field>
|
||||
@@ -65,9 +45,6 @@
|
||||
<field name="date_from"/>
|
||||
<field name="date_to"/>
|
||||
<field name="department_id" invisible="1"/>
|
||||
<field name="total_attendance" widget="float_time" groups="base.group_hr_attendance"/>
|
||||
<field name="total_timesheet" widget="float_time"/>
|
||||
<field name="total_difference" widget="float_time" groups="base.group_hr_attendance"/>
|
||||
<field name="state"/>
|
||||
<field name="message_needaction" invisible="1"/>
|
||||
</tree>
|
||||
@@ -87,22 +64,6 @@
|
||||
<field name="state" widget="statusbar" statusbar_visible="new,confirm,done"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button type="action"
|
||||
name="%(act_hr_timesheet_sheet_sheet_2_hr_analytic_timesheet)d"
|
||||
class="oe_stat_button"
|
||||
icon="fa-clock-o">
|
||||
<div class="o_form_field o_stat_info">
|
||||
<span class="o_stat_value"><field name="total_timesheet" widget="float_time"/></span>
|
||||
<span class="o_stat_text">Timesheet</span>
|
||||
</div>
|
||||
</button>
|
||||
<button type="action" groups="base.group_hr_attendance"
|
||||
class="oe_stat_button" icon="fa-users"
|
||||
name="%(act_hr_timesheet_sheet_sheet_2_hr_attendance)d">
|
||||
<field name="attendance_count" widget="statinfo" string="Attendances"/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="oe_title">
|
||||
<label for="employee_id" class="oe_edit_only"/>
|
||||
<h1><field name="employee_id" on_change="onchange_employee_id(employee_id)" class="oe_inline" placeholder="Employee's Name"/></h1>
|
||||
@@ -122,7 +83,7 @@
|
||||
<widget type="weekly_timesheet" attrs="{'readonly': [['state', 'not in', ['new', 'draft']]]}">
|
||||
</widget>
|
||||
</page>
|
||||
<page string="Details">
|
||||
<page string="Details" class="o_hr_timesheet_sheet_details">
|
||||
<field context="{'employee_id': employee_id, 'user_id':user_id, 'timesheet_date_from': date_from, 'timesheet_date_to': date_to}" name="timesheet_ids" nolabel="1">
|
||||
<tree editable="bottom" string="Timesheet Activities">
|
||||
<field name="date"/>
|
||||
@@ -142,30 +103,6 @@
|
||||
</form>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Attendances" groups="base.group_hr_attendance">
|
||||
<group>
|
||||
<group>
|
||||
<label for="state_attendance"/>
|
||||
<div>
|
||||
<field name="state_attendance"/>
|
||||
<button name="attendance_action_change" attrs="{'invisible': [('state_attendance', '=', 'present')]}" type="object" string="Sign In" class="oe_link"/>
|
||||
<button name="attendance_action_change" attrs="{'invisible': ['|', ('state_attendance','=',False), ('state_attendance', '=', 'absent')]}" type="object" string="Sign Out" class="oe_link"/>
|
||||
</div>
|
||||
</group>
|
||||
<group groups="base.group_hr_attendance">
|
||||
<field name="total_attendance" widget="float_time"/>
|
||||
<field name="total_difference" widget="float_time"/>
|
||||
</group>
|
||||
</group>
|
||||
<field name="period_ids">
|
||||
<tree decoration-danger="total_difference<0.1" decoration-info="total_difference>=0.1" string="Period">
|
||||
<field name="name"/>
|
||||
<field name="total_attendance" widget="float_time" sum="Attendances"/>
|
||||
<field name="total_timesheet" widget="float_time" sum="Timesheets"/>
|
||||
<field name="total_difference" widget="float_time" sum="Differences"/>
|
||||
</tree>
|
||||
</field>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<div class="oe_chatter">
|
||||
@@ -216,7 +153,7 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<menuitem name="Weekly" id="menu_act_hr_timesheet_sheet_form_my_current" parent="hr_timesheet.menu_hr_time_tracking" action="ir_actions_server_timsheet_sheet" sequence="2"/>
|
||||
<menuitem name="My Current Timesheet" id="menu_act_hr_timesheet_sheet_form_my_current" parent="hr_timesheet.menu_hr_time_tracking" action="ir_actions_server_timesheet_sheet" sequence="2"/>
|
||||
|
||||
<menuitem action="act_hr_timesheet_sheet_my_timesheets" id="menu_act_hr_timesheet_sheet_my_timesheets" parent="hr_timesheet.menu_hr_time_tracking"
|
||||
sequence="3"/>
|
||||
@@ -267,14 +204,14 @@
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//group[@name='account_grp']" position="after">
|
||||
<group name="timesheets_grp" string="Timesheets">
|
||||
<div>
|
||||
<div class="o_hr_timesheet_sheet_timesheet_range">
|
||||
<label for="timesheet_range"/>
|
||||
<field name="timesheet_range" required="1" class="oe_inline"/>
|
||||
</div>
|
||||
<div>
|
||||
<!-- <div>
|
||||
<label for="timesheet_max_difference"/>
|
||||
<field name="timesheet_max_difference" required="1" class="oe_inline"/>
|
||||
</div>
|
||||
</div> -->
|
||||
</group>
|
||||
</xpath>
|
||||
</field>
|
||||
@@ -306,82 +243,6 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!--
|
||||
hr.attendance inheritancy
|
||||
-->
|
||||
|
||||
<record model="ir.ui.view" id="view_hr_attendance_filter">
|
||||
<field name="name">view_hr_attendance_filter</field>
|
||||
<field name="model">hr.attendance</field>
|
||||
<field name="inherit_id" ref="hr_attendance.view_hr_attendance_filter" />
|
||||
<field name="arch" type="xml">
|
||||
<field name="employee_id" position="after">
|
||||
<field name="sheet_id"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_attendance_form" model="ir.ui.view">
|
||||
<field name="name">hr.attendance.form</field>
|
||||
<field name="model">hr.attendance</field>
|
||||
<field name="inherit_id" ref="hr_attendance.view_attendance_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="action_desc" position="after">
|
||||
<field name="sheet_id"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
<record id="view_attendance_tree" model="ir.ui.view">
|
||||
<field name="name">hr.attendance.tree</field>
|
||||
<field name="model">hr.attendance</field>
|
||||
<field name="inherit_id" ref="hr_attendance.view_attendance_tree"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="action" position="after">
|
||||
<field name="sheet_id"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
<record id="view_attendance_tree_who" model="ir.ui.view">
|
||||
<field name="name">hr.attendance.tree</field>
|
||||
<field name="model">hr.attendance</field>
|
||||
<field name="inherit_id" ref="hr_attendance.view_attendance_who"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="action" position="after">
|
||||
<field name="sheet_id"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<act_window
|
||||
context="{'search_default_sheet_id': [active_id]}"
|
||||
id="act_hr_timesheet_sheet_sheet_by_account"
|
||||
name="Timesheet by Account"
|
||||
groups="base.group_hr_attendance"
|
||||
res_model="hr_timesheet_sheet.sheet.account"
|
||||
src_model="hr_timesheet_sheet.sheet"/>
|
||||
|
||||
<act_window
|
||||
context="{'search_default_sheet_id': [active_id]}"
|
||||
id="act_hr_timesheet_sheet_sheet_by_day"
|
||||
name="Timesheet by Day"
|
||||
groups="base.group_hr_attendance"
|
||||
res_model="hr_timesheet_sheet.sheet.day"
|
||||
src_model="hr_timesheet_sheet.sheet"/>
|
||||
|
||||
|
||||
<record id="hr_timesheet_day_tree" model="ir.ui.view">
|
||||
<field name="name">hr.timesheet.day.tree</field>
|
||||
<field name="model">hr_timesheet_sheet.sheet.day</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree decoration-info="total_difference<0.1" decoration-danger="total_difference>=0.1" string="Period">
|
||||
<field name="name"/>
|
||||
<field name="total_attendance" widget="float_time" sum="Total Attendances" groups="base.group_hr_attendance"/>
|
||||
<field name="total_timesheet" widget="float_time" sum="Total Timesheet"/>
|
||||
<field name="total_difference" widget="float_time" sum="Total Difference" groups="base.group_hr_attendance"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Timesheet Button on Employee Form -->
|
||||
|
||||
<record id="act_hr_employee_2_hr_timesheet" model="ir.actions.act_window">
|
||||
@@ -421,5 +282,4 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
</odoo>
|
||||
@@ -1,28 +1,24 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
import time
|
||||
|
||||
from openerp.osv import fields, osv
|
||||
from openerp.tools.translate import _
|
||||
from openerp.exceptions import UserError
|
||||
from odoo import api, fields, models, _
|
||||
|
||||
class hr_timesheet_current_open(osv.osv_memory):
|
||||
|
||||
class HrTimesheetCurrentOpen(models.TransientModel):
|
||||
_name = 'hr.timesheet.current.open'
|
||||
_description = 'hr.timesheet.current.open'
|
||||
|
||||
def open_timesheet(self, cr, uid, ids, context=None):
|
||||
ts = self.pool.get('hr_timesheet_sheet.sheet')
|
||||
if context is None:
|
||||
context = {}
|
||||
@api.model
|
||||
def open_timesheet(self):
|
||||
view_type = 'form,tree'
|
||||
|
||||
ids = ts.search(cr, uid, [('user_id','=',uid),('state','in',('draft','new')),('date_from','<=',time.strftime('%Y-%m-%d')), ('date_to','>=',time.strftime('%Y-%m-%d'))], context=context)
|
||||
|
||||
if len(ids) > 1:
|
||||
sheets = self.env['hr_timesheet_sheet.sheet'].search([('user_id', '=', self._uid),
|
||||
('state', 'in', ('draft', 'new')),
|
||||
('date_from', '<=', fields.Date.today()),
|
||||
('date_to', '>=', fields.Date.today())])
|
||||
if len(sheets) > 1:
|
||||
view_type = 'tree,form'
|
||||
domain = "[('id','in',["+','.join(map(str, ids))+"]),('user_id', '=', uid)]"
|
||||
elif len(ids)==1:
|
||||
domain = "[('user_id', '=', uid)]"
|
||||
domain = "[('id', 'in', " + str(sheets.ids) + "),('user_id', '=', uid)]"
|
||||
else:
|
||||
domain = "[('user_id', '=', uid)]"
|
||||
value = {
|
||||
@@ -34,6 +30,6 @@ class hr_timesheet_current_open(osv.osv_memory):
|
||||
'view_id': False,
|
||||
'type': 'ir.actions.act_window'
|
||||
}
|
||||
if len(ids) == 1:
|
||||
value['res_id'] = ids[0]
|
||||
if len(sheets) == 1:
|
||||
value['res_id'] = sheets.ids[0]
|
||||
return value
|
||||
|
||||
Reference in New Issue
Block a user