[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:
Denis Vermylen (dve)
2016-07-06 17:03:05 +02:00
75 changed files with 2554 additions and 1913 deletions
-2
View File
@@ -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
+17 -16
View File
@@ -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>
-126
View File
@@ -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>
-166
View File
@@ -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','&gt;=',datetime.datetime.now().replace(hour=0, minute=0, second=0)),('name','&lt;=',datetime.datetime.now().replace(hour=23, minute=59, second=59))]" />
<filter string="Current Month" domain="[('name','&gt;=', 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>
+2 -1
View File
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
import hr_attendance
import ir_http
import hr_employee
import res_config
+87 -151
View File
@@ -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.'))
+155
View File
@@ -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,))
-9
View File
@@ -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
+12
View File
@@ -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&amp;value=%s&amp;width=%s&amp;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 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
2 access_hr_attendance_user hr.attendance.user model_hr_attendance base.group_hr_user 1 1 1 1
3 access_hr_attendance_system_user hr.attendance.system.user model_hr_attendance base.group_user 1 1 1 1 0
-22
View File
@@ -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')
+4
View File
@@ -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', '&gt;=', datetime.datetime.now().replace(hour=0, minute=0, second=0)),('check_in', '&lt;=', datetime.datetime.now().replace(hour=23, minute=59, second=59))]" />
<filter string="Current Month" domain="[('check_in', '&gt;=', 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>
@@ -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>
-4
View File
@@ -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>
+7 -6
View File
@@ -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'),
+3 -1
View File
@@ -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,
}
@@ -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="[
'&amp;',
('date', '>=', (context_today() + relativedelta(weekday=0)).strftime('%Y-%m-%d')),
('date', '&lt;=', (context_today() + relativedelta(weekday=6)).strftime('%Y-%m-%d')) ,
('date', '>=', (context_today() + relativedelta(weeks=-1,days=1,weekday=0)).strftime('%Y-%m-%d')),
('date', '&lt;=', (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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 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
3 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&lt;0.1" decoration-info="total_difference&gt;=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&lt;0.1" decoration-danger="total_difference&gt;=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>
+2 -4
View File
@@ -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
+11 -12
View File
@@ -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', ],
}
@@ -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>
@@ -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
@@ -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
)""")
@@ -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 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_hr_timesheet_sheet_sheet_user hr_timesheet_sheet.sheet.user model_hr_timesheet_sheet_sheet base.group_user 1 1 1 1
3 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
4 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'
@@ -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>
@@ -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>
@@ -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&lt;0.1" decoration-info="total_difference&gt;=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&lt;0.1" decoration-danger="total_difference&gt;=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