Browse Source

Initial commit

master
7u83 2 years ago
parent
commit
39b8792fd2
  1. 4
      README.rst
  2. 27
      __init__.py
  3. 43
      __openerp__.py
  4. 23
      controllers/__init__.py
  5. 42
      controllers/main.py
  6. 160
      i18n/de.po
  7. BIN
      images/overundertime.png
  8. 28
      models/__init__.py
  9. 53
      models/hr_attendance_analysis.py
  10. 111
      models/hr_employee.py
  11. 324
      models/hr_timesheet_dh.py
  12. 45
      models/hr_timesheet_sheet.py
  13. 56
      models/res_users.py
  14. 123
      models/resource_calendar.py
  15. 100
      models/time_clock_resource_calendar.py
  16. 22
      report/__init__.py
  17. 75
      report/report_attendance_analysis.py
  18. 43
      report/report_attendance_analysis_view.xml
  19. 5
      security/ir.model.access.csv
  20. 29
      security/ir_rule.xml
  21. BIN
      static/description/2dBarcodes.png
  22. BIN
      static/description/attendanceana.png
  23. BIN
      static/description/attendancehistory.png
  24. BIN
      static/description/generateLeaves.png
  25. BIN
      static/description/icon.png
  26. 201
      static/description/index.html
  27. BIN
      static/description/leave.png
  28. BIN
      static/description/lunch.png
  29. BIN
      static/description/message.png
  30. BIN
      static/description/odoo-hr-prev.jpg
  31. BIN
      static/description/odoo-time-clock-prev.jpg
  32. BIN
      static/description/screenshot1.png
  33. BIN
      static/description/screenshot2.png
  34. BIN
      static/description/screenshot3.png
  35. BIN
      static/description/screenshot4.png
  36. BIN
      static/description/screenshot5.png
  37. BIN
      static/description/screenshot6.png
  38. BIN
      static/description/screenshot7.png
  39. BIN
      static/description/signin.png
  40. BIN
      static/description/timesheetgeneration.png
  41. 20
      test_dh_api.py
  42. 44
      views/views.xml
  43. 26
      wizard/__init__.py
  44. 102
      wizard/create_timesheet_with_tag.py
  45. 37
      wizard/create_timesheet_with_tag_view.xml
  46. 97
      wizard/import_leave_requests.py
  47. 46
      wizard/import_leave_requests_view.xml

4
README.rst

@ -0,0 +1,4 @@
Employee Time Clock
======================
Find all informations here: <https://apps.openerp.com/apps/modules/8.0/hr_employee_time_clock>

27
__init__.py

@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Clear Groups for Odoo
# Copyright (C) 2016 Bytebrand GmbH (<http://www.bytebrand.net>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import controllers
import models
import wizard
import report

43
__openerp__.py

@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Clear Groups for Odoo
# Copyright (C) 2016 Bytebrand GmbH (<http://www.bytebrand.net>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
{
'name': "Employee time clock",
'author': "Bytebrand GmbH",
'summary': 'Track over- and under-time, generate timesheets, upload public holidays',
'website': "http://www.bytebrand.net",
'category': 'Human Resources',
'version': '8.0.3.0.0',
'depends': ['hr_timesheet_sheet', 'hr_attendance', 'hr_contract', 'hr_holidays'], #,'hr_attendance_analysis'
'images': ['images/overundertime.png'],
'installable': True,
'data': [
'security/ir_rule.xml',
'security/ir.model.access.csv',
'views/views.xml',
# Report
'report/report_attendance_analysis_view.xml',
# View file for the wizard
'wizard/create_timesheet_with_tag_view.xml',
'wizard/import_leave_requests_view.xml',
]
}

23
controllers/__init__.py

@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Clear Groups for Odoo
# Copyright (C) 2016 Bytebrand GmbH (<http://www.bytebrand.net>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import main

42
controllers/main.py

@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Clear Groups for Odoo
# Copyright (C) 2016 Bytebrand GmbH (<http://www.bytebrand.net>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp import http
# class HrTimesheetOvertime(http.Controller):
# @http.route('/hr_timesheet_overtime/hr_timesheet_overtime/', auth='public')
# def index(self, **kw):
# return "Hello, world"
# @http.route('/hr_timesheet_overtime/hr_timesheet_overtime/objects/', auth='public')
# def list(self, **kw):
# return http.request.render('hr_timesheet_overtime.listing', {
# 'root': '/hr_timesheet_overtime/hr_timesheet_overtime',
# 'objects': http.request.env['hr_timesheet_overtime.hr_timesheet_overtime'].search([]),
# })
# @http.route('/hr_timesheet_overtime/hr_timesheet_overtime/objects/<model("hr_timesheet_overtime.hr_timesheet_overtime"):obj>/', auth='public')
# def object(self, obj, **kw):
# return http.request.render('hr_timesheet_overtime.object', {
# 'object': obj
# })

160
i18n/de.po

@ -0,0 +1,160 @@
# German translation for hr_employee_time_clock
msgid ""
msgstr ""
#. module: hr_employee_time_clock
#: field:hr_timesheet_sheet.sheet,calculate_diff_hours:0
#: model:ir.model,name:hr_employee_time_clock.model_hr_timesheet_sheet_sheet
#: view:hr_timesheet_sheet.sheet:hr_employee_time_clock.timesheet_overtime
msgid "Total balance"
msgstr "Total Saldo"
#. module: hr_employee_time_clock
#: field:hr_timesheet_sheet.sheet,total_duty_hours:0
#: model:ir.model,name:hr_employee_time_clock.model_hr_timesheet_sheet_sheet
#: view:hr_timesheet_sheet.sheet:hr_employee_time_clock.timesheet_overtime
msgid "Remaining this timesheet"
msgstr "Verbleibend diese Zeiterfassung"
#. module: hr_employee_time_clock
#: field:hr_timesheet_sheet.sheet,prev_timesheet_diff:0
#: model:ir.model,name:hr_employee_time_clock.model_hr_timesheet_sheet_sheet
#: view:hr_timesheet_sheet.sheet:hr_employee_time_clock.timesheet_overtime
msgid "Last timesheets"
msgstr "Letzte Zeiterfassung"
#. module: hr_employee_time_clock
#: field:hr_timesheet_sheet.sheet,total_attendance:0
#: model:ir.model,name:hr_employee_time_clock.model_hr_timesheet_sheet_sheet
#: view:hr_timesheet_sheet.sheet:hr_employee_time_clock.timesheet_overtime
msgid "Attendance this timesheet"
msgstr "Anwesenheit diese Zeiterfassung"
#. module: hr_employee_time_clock
#: field:hr_timesheet_sheet.sheet,analysis:0
msgid "Attendance Analysis"
msgstr "Anwesenheit Analyse"
#. module: hr_employee_time_clock
#: view:hr_timesheet_sheet.sheet:hr_employee_time_clock.timesheet_overtime
msgid "Overtime Analysis"
msgstr "Überzeit Analyse"
#. module: hr_employee_time_clock
#: view:hr.timesheet.current.open:hr_employee_time_clock.view_hr_timesheet_current_open_inherit
msgid "Generate timesheets for employees having the following tag:"
msgstr "Generiere Zeiterfassungen für Mitarbeiter mit folgendem Tag:"
#. module: hr_employee_time_clock
#: field:hr.timesheet.current.open,category_id:0
#: model:ir.model,name:hr.model_hr_employee_category
msgid "Employee Tag"
msgstr "Mitarbeiter Tag"
#. module: hr_employee_time_clock
#: field:hr.timesheet.current.open,date_from:0
msgid "Start Date"
msgstr "Startdatum"
#. module: hr_employee_time_clock
#: field:hr.timesheet.current.open,date_to:0
msgid "End Date"
msgstr "Enddatum"
#. module: hr_employee_time_clock
#: code:addons/hr_employee_time_clock/wizard/create_timesheet_with_tag.py:44
#, python-format
msgid "Timesheet already exists for %s."
msgstr "Zeiterfassung existiert bereits für %s."
#. module: hr_employee_time_clock
#: code:addons/hr_employee_time_clock/wizard/import_leave_requests.py:31
#, python-format
msgid "Data Error!"
msgstr ""
#. module: hr_employee_time_clock
#: code:addons/hr_employee_time_clock/wizard/import_leave_requests.py:31
#, python-format
msgid "Date format in your .csv file does not match with database date format."
msgstr ""
#. module: hr_employee_time_clock
#: view:import.leave.requests:hr_employee_time_clock.view_import_leave_requests
msgid "Generate timesheets for employees having the following tag:"
msgstr "Generiere Zeiterfassungen für Mitarbeiter mit folgendem Tag:"
#. module: hr_employee_time_clock
#: field:import.leave.requests,leave_dates:0
msgid "Select *.csv"
msgstr "Wähle *.csv"
#. module: hr_employee_time_clock
#: field:import.leave.requests,leave_type_id:0
#: model:ir.model,name:hr_holidays.model_hr_holidays_status
#: view:import.leave.requests:hr_employee_time_clock.view_import_leave_requests
msgid "Leave Type"
msgstr "Abwesenheitstyp"
#. module: hr_employee_time_clock
#: field:import.leave.requests,employee_tag_id:0
#: model:ir.model,name:hr.model_hr_employee_category
#: view:import.leave.requests:hr_employee_time_clock.view_import_leave_requests
msgid "Employee Tag"
msgstr "Mitarbeiter Tag"
#. module: hr_employee_time_clock
#: view:import.leave.requests:hr_employee_time_clock.view_import_leave_requests
#: model:ir.actions.act_window,name:hr_employee_time_clock.action_import_leave_requests
#: model:ir.ui.menu,name:hr_employee_time_clock.menu_import_leave_requests
msgid "Import Leave Requests"
msgstr "Importiere Abwesenheiten"
#. module: hr_employee_time_clock
#: view:hr.timesheet.current.open:hr_employee_time_clock.view_hr_timesheet_current_open_inherit
#: model:ir.ui.menu,name:hr_employee_time_clock.menu_act_hr_timesheet_sheet_form_open_current
msgid "Generate Timesheets"
msgstr "Generiere Zeiterfassungen"
#. module: hr_employee_time_clock
#: code:addons/hr_employee_time_clock/models.py:104
#, python-format
msgid "Previous Timesheet:"
msgstr "Letzte Zeiterfassung:"
#. module: hr_employee_time_clock
#: code:addons/hr_employee_time_clock/models.py:124
#, python-format
msgid "Total:"
msgstr "Total:"
#. module: hr_employee_time_clock
#: code:addons/hr_employee_time_clock/models.py:227
#, python-format
msgid "Date"
msgstr "Datum"
#. module: hr_employee_time_clock
#: code:addons/hr_employee_time_clock/models.py:228
#, python-format
msgid "Duty Hours"
msgstr "Pflichtstunden"
#. module: hr_employee_time_clock
#: code:addons/hr_employee_time_clock/models.py:229
#, python-format
msgid "Worked Hours"
msgstr "Gearbeitete Stunden"
#. module: hr_employee_time_clock
#: code:addons/hr_employee_time_clock/models.py:230
#, python-format
msgid "Difference"
msgstr "Differenz"
#. module: hr_employee_time_clock
#: code:addons/hr_employee_time_clock/models.py:231
#, python-format
msgid "Running"
msgstr "Laufend"

BIN
images/overundertime.png

After

Width: 686  |  Height: 531  |  Size: 29 KiB

28
models/__init__.py

@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Clear Groups for Odoo
# Copyright (C) 2016 Bytebrand GmbH (<http://www.bytebrand.net>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from . import hr_employee
from . import hr_attendance_analysis
from . import time_clock_resource_calendar
from . import hr_timesheet_dh
from . import resource_calendar
from . import hr_timesheet_sheet
from . import res_users

53
models/hr_attendance_analysis.py

@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Clear Groups for Odoo
# Copyright (C) 2016 Bytebrand GmbH (<http://www.bytebrand.net>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import math
from openerp import models, api, _
from datetime import datetime
from openerp.exceptions import ValidationError
class HrAttendance(models.Model):
_inherit = "hr.attendance"
# ref: https://bugs.launchpad.net/openobject-client/+bug/887612
# test: 0.9853 - 0.0085
def float_time_convert(self, float_val):
hours = math.floor(abs(float_val))
mins = abs(float_val) - hours
mins = round(mins * 60)
if mins >= 60.0:
hours += 1
mins = 0.0
float_time = '%02d:%02d' % (hours, mins)
return float_time
@api.model
def create(self, values):
if values.get('name'):
times = datetime.strptime(values.get('name'), "%Y-%m-%d %H:%M:%S")
if datetime.now() < times:
raise ValidationError(
_('You can not set time of Sing In (resp. Sing Out) which '
'is later than a current time'))
return super(HrAttendance, self).create(values)

111
models/hr_employee.py

@ -0,0 +1,111 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Clear Groups for Odoo
# Copyright (C) 2016 Bytebrand GmbH (<http://www.bytebrand.net>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from datetime import date
from openerp import api, fields, models, _
from openerp.exceptions import ValidationError
class HrEmployee(models.Model):
_inherit = "hr.employee"
_description = "Employee"
@api.multi
def attendance_action_change(self):
hr_timesheet_sheet_sheet_pool = self.env['hr_timesheet_sheet.sheet']
hr_timesheet_ids = hr_timesheet_sheet_sheet_pool.search(
[('employee_id', '=', self.id),
('date_from', '<=', date.today()),
('date_to', '>=', date.today())])
if not hr_timesheet_ids:
raise ValidationError(
_('Please contact your manager to create timesheet for you.'))
return super(HrEmployee, self).attendance_action_change()
@api.model
def check_in_out_action(self, values):
employee = self.sudo().browse(values.get('employee_id')).exists()
# employee.attendance_action_change()
if not employee:
return [
{'error': _(
'Please contact your manager to create '
'employee for you and change QR-code.')}]
hr_timesheet_sheet_sheet_pool = self.env['hr_timesheet_sheet.sheet']
hr_timesheet_ids = hr_timesheet_sheet_sheet_pool.search(
[('employee_id', '=', employee.id),
('date_from', '<=', date.today()),
('date_to', '>=', date.today())])
if not hr_timesheet_ids:
return [
{'error': _(
'Please contact your manager to create '
'timesheet for you.')}]
""" 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 ValidationError(
_('Cannot perform check in or '
'check out on multiple employees.'))
action_date = fields.Datetime.now()
if employee.state != 'absent':
vals = {'name': action_date,
'action': 'sign_out',
'employee_id': employee.id, }
log = 'checked_out'
else:
vals = {'name': action_date,
'action': 'sign_in',
'employee_id': employee.id, }
log = 'checked_in'
self.env['hr.attendance'].sudo().create(vals)
employee = self.sudo().browse(employee.id)
ctx = self.env.context.copy()
ctx.update(online_analysis=True)
res = hr_timesheet_ids.with_context(
ctx).attendance_analysis(hr_timesheet_ids.id)
running = 0
date_line = values.get('date').split(' ')[0]
dddd = (fields.Datetime.from_string(date_line + ' 00:00:00'))
date_format, time_format = \
hr_timesheet_sheet_sheet_pool._get_user_datetime_format()
date_line = dddd.strftime("{} {}".format(date_format,
time_format)).split(' ')[0]
for d in res.get('hours'):
if d.get('name') == date_line:
running = d.get('running')
re = {'log': log,
'name': employee.name,
'image': employee.image_medium or '',
'running': running,
'user_id': employee.user_id.id}
print('\n re >>>>>> %s' % re)
return [{'log': log,
'name': employee.name,
'image': employee.image_medium or '',
'running': running,
'user_id': employee.user_id.id}]

324
models/hr_timesheet_dh.py

@ -0,0 +1,324 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Clear Groups for Odoo
# Copyright (C) 2016 Bytebrand GmbH (<http://www.bytebrand.net>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import datetime as dtime
from datetime import datetime, timedelta
from openerp import api, fields, models, _
from dateutil import rrule, parser
from openerp.tools.translate import _
class HrTimesheetDh(models.Model):
"""
Addition plugin for HR timesheet for work with duty hours
"""
_inherit = 'hr_timesheet_sheet.sheet'
@api.multi
def _duty_hours(self):
for sheet in self:
sheet['total_duty_hours'] = 0.0
if sheet.state == 'done':
sheet['total_duty_hours'] = sheet.total_duty_hours_done
else:
dates = list(rrule.rrule(rrule.DAILY,
dtstart=parser.parse(sheet.date_from),
until=parser.parse(sheet.date_to)))
period = {'date_from': sheet.date_from,
'date_to': sheet.date_to}
for date_line in dates:
duty_hours = sheet.calculate_duty_hours(date_from=date_line,
period=period,
)
sheet['total_duty_hours'] += duty_hours
sheet['total_duty_hours'] = (sheet.total_duty_hours -
sheet.total_attendance)
@api.multi
def count_leaves(self, date_from, employee_id, period):
holiday_obj = self.env['hr.holidays']
start_leave_period = end_leave_period = False
if period.get('date_from') and period.get('date_to'):
start_leave_period = period.get('date_from')
end_leave_period = period.get('date_to')
holiday_ids = holiday_obj.search(
['|', '&',
('date_from', '>=', start_leave_period),
('date_from', '<=', end_leave_period),
'&', ('date_to', '<=', end_leave_period),
('date_to', '>=', start_leave_period),
('employee_id', '=', employee_id),
('state', '=', 'validate'),
('type', '=', 'remove')])
leaves = []
for leave in holiday_ids:
leave_date_from = datetime.strptime(leave.date_from,
'%Y-%m-%d %H:%M:%S')
leave_date_to = datetime.strptime(leave.date_to,
'%Y-%m-%d %H:%M:%S')
leave_dates = list(rrule.rrule(rrule.DAILY,
dtstart=parser.parse(
leave.date_from),
until=parser.parse(leave.date_to)))
for date in leave_dates:
if date.strftime('%Y-%m-%d') == date_from.strftime('%Y-%m-%d'):
leaves.append(
(leave_date_from, leave_date_to, leave.number_of_days))
break
return leaves
@api.multi
def get_overtime(self, start_date):
for sheet in self:
if sheet.state == 'done':
return sheet.total_duty_hours_done * -1
return self.calculate_diff(start_date)
@api.multi
def _overtime_diff(self):
for sheet in self:
old_timesheet_start_from = parser.parse(
sheet.date_from) - timedelta(days=1)
prev_timesheet_diff = \
self.get_previous_month_diff(
sheet.employee_id.id,
old_timesheet_start_from.strftime('%Y-%m-%d')
)
sheet['calculate_diff_hours'] = (
self.get_overtime(datetime.today().strftime('%Y-%m-%d'), ) +
prev_timesheet_diff)
sheet['prev_timesheet_diff'] = prev_timesheet_diff
@api.multi
def _get_analysis(self):
res = {}
for sheet in self:
function_call = True
data = self.attendance_analysis(sheet.id, function_call)
values = []
output = [
'<style>.attendanceTable td,.attendanceTable th {padding: 3px; border: 1px solid #C0C0C0; border-collapse: collapse; text-align: right;} </style><table class="attendanceTable" >']
for val in data.values():
if isinstance(val, (int, float)):
output.append('<tr>')
prev_ts = _('Previous Timesheet:')
output.append('<th colspan="2">' + prev_ts + ' </th>')
output.append('<td colspan="3">' + str(val) + '</td>')
output.append('</tr>')
for k, v in data.items():
if isinstance(v, list):
output.append('<tr>')
for th in v[0].keys():
output.append('<th>' + th + '</th>')
output.append('</tr>')
for res in v:
values.append(res.values())
for tr in values:
output.append('<tr>')
for td in tr:
output.append('<td>' + td + '</td>')
output.append('</tr>')
if isinstance(v, dict):
output.append('<tr>')
total_ts = _('Total:')
output.append('<th>' + total_ts + ' </th>')
for td in v.values():
output.append('<td>' + '%s' % round(td, 4) + '</td>')
output.append('</tr>')
output.append('</table>')
sheet['analysis'] = '\n'.join(output)
total_duty_hours = fields.Float(compute='_duty_hours',
string='Total Duty Hours',
multi="_duty_hours")
total_duty_hours_done = fields.Float(string='Total Duty Hours',
readonly=True,
default=0.0)
total_diff_hours = fields.Float(string='Total Diff Hours',
readonly=True,
default=0.0)
calculate_diff_hours = fields.Char(compute='_overtime_diff',
string="Diff (worked-duty)",
multi="_diff")
prev_timesheet_diff = fields.Char(compute='_overtime_diff',
method=True,
string="Diff from old",
multi="_diff")
analysis = fields.Text(compute='_get_analysis',
type="text",
string="Attendance Analysis")
@api.multi
def calculate_duty_hours(self, date_from, period):
contract_obj = self.env['hr.contract']
calendar_obj = self.env['resource.calendar']
duty_hours = 0.0
contract_ids = contract_obj.search(
[
('employee_id', '=', self.employee_id.id),
('date_start', '<=', date_from),
'|',
('date_end', '>=', date_from),
('date_end', '=', None)
]
)
for contract in contract_ids:
ctx = dict(self.env.context).copy()
ctx.update(period)
dh = calendar_obj.get_working_hours_of_date(
cr=self._cr,
uid=self.env.user.id,
ids=contract.working_hours.id,
start_dt=date_from,
resource_id=self.employee_id.id,
context=ctx)
leaves = self.count_leaves(date_from, self.employee_id.id, period)
if not leaves:
if not dh:
dh = 0.00
duty_hours += dh
else:
if leaves[-1] and leaves[-1][-1]:
if float(leaves[-1][-1]) == (-0.5):
duty_hours += dh / 2
return duty_hours
@api.multi
def get_previous_month_diff(self, employee_id, prev_timesheet_date_from):
total_diff = 0.0
timesheet_ids = self.search(
[('employee_id', '=', employee_id),
('date_from', '<', prev_timesheet_date_from)
])
for timesheet in timesheet_ids:
total_diff += timesheet.get_overtime(
start_date=prev_timesheet_date_from)
return total_diff
@api.multi
def _get_user_datetime_format(self):
""" Get user's language & fetch date/time formats of
that language """
lang_obj = self.env['res.lang']
language = self.env.user.lang
lang_ids = lang_obj.search([('code', '=', language)])
date_format = _('%Y-%m-%d')
time_format = _('%H:%M:%S')
for lang in lang_ids:
date_format = lang.date_format
time_format = lang.time_format
return date_format, time_format
@api.multi
def attendance_analysis(self, timesheet_id=None, function_call=False):
attendance_obj = self.env['hr.attendance']
date_format, time_format = self._get_user_datetime_format()
for sheet in self:
if sheet.id == timesheet_id:
employee_id = sheet.employee_id.id
start_date = sheet.date_from
end_date = sheet.date_to
previous_month_diff = self.get_previous_month_diff(
employee_id, start_date)
current_month_diff = previous_month_diff
res = {
'previous_month_diff': previous_month_diff,
'hours': []
}
period = {'date_from': start_date,
'date_to': end_date
}
dates = list(rrule.rrule(rrule.DAILY,
dtstart=parser.parse(start_date),
until=parser.parse(
end_date)))
work_current_month_diff = 0.0
total = {'worked_hours': 0.0, 'duty_hours': 0.0,
'diff':
current_month_diff, 'work_current_month_diff': ''}
for date_line in dates:
dh = sheet.calculate_duty_hours(date_from=date_line,
period=period,
)
worked_hours = 0.0
for att in sheet.period_ids:
if att.name == date_line.strftime('%Y-%m-%d'):
worked_hours = att.total_attendance
diff = worked_hours - dh
current_month_diff += diff
work_current_month_diff += diff
if function_call:
res['hours'].append({
_('Date'): date_line.strftime(date_format),
_('Duty Hours'):
attendance_obj.float_time_convert(dh),
_('Worked Hours'):
attendance_obj.float_time_convert(worked_hours),
_('Difference'): self.sign_float_time_convert(diff),
_('Running'): self.sign_float_time_convert(
current_month_diff)})
else:
res['hours'].append({
'name': date_line.strftime(date_format),
'dh': attendance_obj.float_time_convert(dh),
'worked_hours': attendance_obj.float_time_convert(
worked_hours),
'diff': self.sign_float_time_convert(diff),
'running': self.sign_float_time_convert(
current_month_diff)
})
total['duty_hours'] += dh
total['worked_hours'] += worked_hours
total['diff'] += diff
total['work_current_month_diff'] = work_current_month_diff
res['total'] = total
return res
@api.multi
def sign_float_time_convert(self, float_time):
sign = '-' if float_time < 0 else ''
attendance_obj = self.pool.get('hr.attendance')
return sign + attendance_obj.float_time_convert(float_time)
@api.multi
def write(self, vals):
if 'state' in vals and vals['state'] == 'done':
vals['total_diff_hours'] = self.calculate_diff(None)
for sheet in self:
vals['total_duty_hours_done'] = sheet.total_duty_hours
elif 'state' in vals and vals['state'] == 'draft':
vals['total_diff_hours'] = 0.0
res = super(HrTimesheetDh, self).write(vals)
return res
@api.multi
def calculate_diff(self, end_date=None):
for sheet in self:
return sheet.total_duty_hours * (-1)

45
models/hr_timesheet_sheet.py

@ -0,0 +1,45 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Clear Groups for Odoo
# Copyright (C) 2016 Bytebrand GmbH (<http://www.bytebrand.net>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp import api, fields, models, _
from openerp.exceptions import ValidationError
class HrTimesheetSheet(models.Model):
_inherit = "hr_timesheet_sheet.sheet"
@api.onchange('date_from', 'date_to')
@api.multi
def change_date(self):
if self.date_to and self.date_from and self.date_from > self.date_to:
raise ValidationError(
_('You added wrong date period.'))
@api.model
def create(self, values):
if values.get('date_to') and values.get('date_from') \
and values.get('date_from') > values.get('date_to'):
raise ValidationError(
_('You added wrong date period.'))
return super(HrTimesheetSheet, self).create(values)

56
models/res_users.py

@ -0,0 +1,56 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2016 - now Bytebrand Outsourcing AG (<http://www.bytebrand.net>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp import fields, models, SUPERUSER_ID, api
import logging
_logger = logging.getLogger(__name__)
class ResUsers(models.Model):
_inherit = 'res.users'
def authenticate(cls, db, login, password, user_agent_env):
uid = cls._login(db, login, password)
if uid == SUPERUSER_ID:
# Successfully logged in as admin!
# Attempt to guess the web base url...
if user_agent_env and user_agent_env.get('base_location'):
try:
with cls.pool.cursor() as cr:
base = user_agent_env['base_location']
ICP = api.Environment(cr, uid, {})[
'ir.config_parameter']
if not ICP.get_param('web.base.url.freeze'):
ICP.set_param('web.base.url', base)
except Exception:
_logger.exception(
"Failed to update web.base.url configuration parameter")
if user_agent_env:
return uid
else:
with cls.pool.cursor() as cr:
module = api.Environment(
cr, uid, {})['ir.module.module'].sudo().search(
[('name', '=', 'hr_employee_time_clock')])
return {'uid': uid, 'version': module.latest_version}

123
models/resource_calendar.py

@ -0,0 +1,123 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Clear Groups for Odoo
# Copyright (C) 2016 Bytebrand GmbH (<http://www.bytebrand.net>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import datetime as dtime
from datetime import datetime
from openerp import api, fields, models, _
class ResourceCalendar(models.Model):
_inherit = 'resource.calendar'
@api.multi
def get_working_hours_of_date(self, cr, uid, ids, start_dt=None,
end_dt=None, leaves=None,
compute_leaves=False, resource_id=None,
default_interval=None, context=None):
""" Get the working hours of the day based on calendar. This method uses
get_working_intervals_of_day to have the work intervals of the day. It
then calculates the number of hours contained in those intervals. """
res = dtime.timedelta()
intervals = self.get_working_intervals_of_day(
cr, uid, ids,
start_dt, end_dt, leaves,
compute_leaves, resource_id,
default_interval, context)
for interval in intervals:
res += interval[1] - interval[0]
return seconds(res) / 3600.0
@api.multi
def get_working_intervals_of_day(self, cr, uid, ids, start_dt=None,
end_dt=None, leaves=None,
compute_leaves=False, resource_id=None,
default_interval=None, context=None):
if isinstance(ids, (list, tuple)):
ids = ids[0]
work_limits = []
if start_dt is None and end_dt is not None:
start_dt = end_dt.replace(hour=0, minute=0, second=0)
elif start_dt is None:
start_dt = datetime.now().replace(hour=0, minute=0, second=0)
else:
work_limits.append((start_dt.replace(
hour=0, minute=0, second=0), start_dt))
if end_dt is None:
end_dt = start_dt.replace(hour=23, minute=59, second=59)
else:
work_limits.append((end_dt, end_dt.replace(
hour=23, minute=59, second=59)))
assert start_dt.date() == end_dt.date(), \
'get_working_intervals_of_day is restricted to one day'
intervals = []
work_dt = start_dt.replace(hour=0, minute=0, second=0)
# no calendar: try to use the default_interval, then return directly
if ids is None:
working_interval = []
if default_interval:
working_interval = (
start_dt.replace(hour=default_interval[0],
minute=0, second=0),
start_dt.replace(hour=default_interval[1],
minute=0, second=0))
intervals = self.interval_remove_leaves(working_interval,
work_limits)
return intervals
working_intervals = []
for calendar_working_day in self.get_attendances_for_weekdays(
ids, [start_dt.weekday()]):
working_interval = (
work_dt.replace(hour=int(calendar_working_day.hour_from)),
work_dt.replace(hour=int(calendar_working_day.hour_to))
)
working_intervals += self.interval_remove_leaves(working_interval,
work_limits)
# find leave intervals
if leaves is None and compute_leaves:
leaves = self.get_leave_intervals(cr, uid, ids,
resource_id=resource_id,
context=context)
# filter according to leaves
for interval in working_intervals:
work_intervals = self.interval_remove_leaves(interval, leaves)
intervals += work_intervals
return intervals
@api.multi
def get_attendances_for_weekdays(self, ids, weekdays):
""" Given a list of weekdays, return matching
resource.calendar.attendance"""
calendar = self.browse(ids)
return [att for att in calendar.attendance_ids
if int(att.dayofweek) in weekdays]
def seconds(td):
assert isinstance(td, dtime.timedelta)
return (td.microseconds + (
td.seconds + td.days * 24 * 3600) * 10 ** 6) / 10. ** 6

100
models/time_clock_resource_calendar.py

@ -0,0 +1,100 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Clear Groups for Odoo
# Copyright (C) 2016 Bytebrand GmbH (<http://www.bytebrand.net>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from datetime import datetime
import math
from openerp import fields, models, api
class TimeClockResourceCalendar(models.Model):
_inherit = "resource.calendar"
@api.multi
def get_working_intervals_of_day(self, start_dt=None, end_dt=None,
leaves=None, compute_leaves=False,
resource_id=None, default_interval=None):
""" To resolve issue of 0.5h on duty hours,
this method has to be overriden here."""
""" Computes start_dt, end_dt (with default values if not set) +
off-interval work limits """
work_limits = []
if start_dt is None and end_dt is not None:
start_dt = end_dt.replace(hour=0, minute=0, second=0)
elif start_dt is None:
start_dt = datetime.datetime.now().replace(hour=0, minute=0,
second=0)
else:
work_limits.append(
(start_dt.replace(hour=0, minute=0, second=0), start_dt))
if end_dt is None:
end_dt = start_dt.replace(hour=23, minute=59, second=59)
else:
work_limits.append(
(end_dt, end_dt.replace(hour=23, minute=59, second=59)))
assert start_dt.date() == end_dt.date(), \
'get_working_intervals_of_day is restricted to one day'
intervals = []
work_dt = start_dt.replace(hour=0, minute=0, second=0)
# no calendar: try to use the default_interval, then return directly
if self.id is None:
if default_interval:
working_interval = (
start_dt.replace(
hour=default_interval[0], minute=0, second=0),
start_dt.replace(
hour=default_interval[1], minute=0, second=0))
intervals = self.interval_remove_leaves(working_interval,
work_limits)
if intervals:
return intervals
else:
return []
working_intervals = []
for calendar_working_day in self.get_attendances_for_weekdays(
[start_dt.weekday()])[0]:
# FIXED by Addition IT Solutions: Counting
# minutes to get result when 0.5h are added to calendar
minutes_from = math.modf(calendar_working_day.hour_from)[0] * 60
minutes_to = math.modf(calendar_working_day.hour_to)[0] * 60
working_interval = (
work_dt.replace(hour=int(calendar_working_day.hour_from),
minute=int(minutes_from)),
work_dt.replace(hour=int(calendar_working_day.hour_to),
minute=int(minutes_to))
)
working_intervals += self.interval_remove_leaves(working_interval,
work_limits)
# find leave intervals
if leaves is None and compute_leaves:
leaves = self.get_leave_intervals(resource_id=resource_id)
# filter according to leaves
for interval in working_intervals:
work_intervals = self.interval_remove_leaves(interval, leaves)
intervals += work_intervals
return intervals

22
report/__init__.py

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import report_attendance_analysis

75
report/report_attendance_analysis.py

@ -0,0 +1,75 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp import tools
from openerp.osv import fields, osv
class HrAttendanceAnalysisReport(osv.osv):
_name = "hr.attendance.analysis.report"
_description = "Attendance Analysis based on Timesheet"
_auto = False
_columns = {
'name': fields.many2one('hr.employee',
'Employee'),
'department_id': fields.many2one('hr.department',
'Department'),
'timesheet_id': fields.many2one('hr_timesheet_sheet.sheet',
'Timesheet'),
'total_duty_hours_running': fields.float('Running Hours'),
'total_duty_hours_done': fields.float('Duty Hours'),
'user_id': fields.many2one('res.users',
'User of Employee'),
'parent_user_id': fields.many2one('res.users',
'User of Manager'),
}
def init(self, cr):
tools.drop_view_if_exists(cr, 'hr_attendance_analysis_report')
cr.execute("""
CREATE or REPLACE view hr_attendance_analysis_report as (
select
min(sheet.id) as id,
sheet.id as timesheet_id,
sheet.employee_id as name,
emp.department_id as department_id,
res.user_id as user_id,
(select r.user_id
from resource_resource r, hr_employee e
where r.id = e.resource_id and e.id=emp.parent_id) as parent_user_id,
sheet.total_diff_hours as total_duty_hours_running,
sheet.total_duty_hours_done as total_duty_hours_done
from
hr_timesheet_sheet_sheet sheet,
hr_employee emp,
resource_resource res,
hr_department dp
where
sheet.employee_id=emp.id AND
emp.resource_id=res.id AND
emp.department_id=dp.id
group by
sheet.id, emp.department_id, res.user_id, emp.parent_id
)
""")
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

43
report/report_attendance_analysis_view.xml

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record id="view_hr_attendance_analysis_report_tree" model="ir.ui.view">
<field name="name">hr.attendance.analysis.report.tree</field>
<field name="model">hr.attendance.analysis.report</field>
<field name="arch" type="xml">
<tree string="Analysis by Timesheet">
<field name="name"/>
<field name="department_id"/>
<field name="timesheet_id"/>
</tree>
</field>
</record>
<record id="view_hr_attendance_analysis_report_graph" model="ir.ui.view">
<field name="name">hr.attendance.analysis.report.graph</field>
<field name="model">hr.attendance.analysis.report</field>
<field name="arch" type="xml">
<graph string="Analysis by Timesheet" type="pivot" stacked="True">
<field name="department_id" type="row"/>
<field name="name" type="row"/>
<field name="total_duty_hours_running" type="measure"/>
<field name="total_duty_hours_done" type="measure"/>
<field name="timesheet_id" type="col"/>
</graph>
</field>
</record>
<record id="action_hr_attendance_analysis_report" model="ir.actions.act_window">
<field name="name">Analysis by Timesheet</field>
<field name="res_model">hr.attendance.analysis.report</field>
<field name="view_type">form</field>
<field name="view_mode">graph</field>
<field name="context">{'group_by_no_leaf':1,'group_by':[]}</field>
</record>
<menuitem action="action_hr_attendance_analysis_report" id="menu_hr_attendance_analysis_report"
parent="hr.menu_hr_reporting" groups="base.group_hr_user" sequence="3"/>
</data>
</openerp>

5
security/ir.model.access.csv

@ -0,0 +1,5 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_hr_contract_user,hr.contract.employee,hr_contract.model_hr_contract,base.group_user,1,0,0,0
access_resource_calendar_user,resource.calendar.employee,resource.model_resource_calendar,base.group_user,1,0,0,0
access_resource_calendar_attendance_user,resource.calendar.attendance.employee,resource.model_resource_calendar_attendance,base.group_user,1,0,0,0
access_hr_attendance_analysis_report_user,hr.attendance.analysis.report.officer,model_hr_attendance_analysis_report,base.group_hr_user,1,1,1,0

29
security/ir_rule.xml

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data noupdate="1">
<record id="employee_see_own_contract" model="ir.rule">
<field name="model_id" ref="hr_contract.model_hr_contract"/>
<field name="name">Personal Employee Contracts</field>
<field name="domain_force">['|',('employee_id.user_id','=',user.id),('employee_id.parent_id.user_id','child_of',user.id)]</field>
<field name="perm_read" eval="1"/>
<field name="perm_write" eval="0"/>
<field name="perm_create" eval="0"/>
<field name="perm_unlink" eval="0" />
<field name="groups" eval="[(4,ref('base.group_user'))]"/>
</record>
<!--<record model="ir.rule" id="manager_timesheet_rule">-->
<!--<field name="name">Timesheet Rule for Manager</field>-->
<!--<field name="model_id" ref="hr_timesheet_sheet.model_hr_timesheet_sheet_sheet"/>-->
<!--<field name="domain_force">['|',('employee_id.user_id','=',user.id), ('employee_id.parent_id.user_id','child_of',user.id)]</field> -->
<!--</record>-->
<!--<record model="ir.rule" id="manager_timesheet_report_rule">-->
<!--<field name="name">Timesheet Report Rule for Manager</field>-->
<!--<field name="model_id" ref="hr_employee_time_clock.model_hr_attendance_analysis_report"/>-->
<!--<field name="domain_force">['|',('user_id','=',user.id), ('parent_user_id','child_of',user.id)]</field>-->
<!--</record>-->
</data>
</openerp>

BIN
static/description/2dBarcodes.png

After

Width: 738  |  Height: 357  |  Size: 60 KiB

BIN
static/description/attendanceana.png

After

Width: 640  |  Height: 960  |  Size: 71 KiB

BIN
static/description/attendancehistory.png

After

Width: 640  |  Height: 960  |  Size: 59 KiB

BIN
static/description/generateLeaves.png

After

Width: 886  |  Height: 309  |  Size: 18 KiB

BIN
static/description/icon.png

After

Width: 64  |  Height: 64  |  Size: 25 KiB

201
static/description/index.html

@ -0,0 +1,201 @@
<section class="oe_container">
<div class="oe_row oe_spaced">
<h2 class="oe_slogan">Employee Time Clock</h2>
<h3 class="oe_slogan">Tracking employees' over- and undertime with ease</h3>
<h4 class="oe_slogan" style="margin-bottom: 16px; margin-top: 16px;">by Bytebrand Outsourcing AG</h4>
<div class="oe_row oe_centeralign">
<a href="mailto:odoo@bytebrand.net" class="btn btn-primary btn-lg mt8" style="color: white;"><span class="fa fa-envelope fa-1x"></span>&nbsp;Help Mail&nbsp; </a>
<a href="https://github.com/marcok/odoo_modules/issues" class="btn btn-primary btn-lg mt8" style="color: white;"><span class="fa fa-bug fa-1x"></span>&nbsp;Bugs&nbsp; </a>
<a href="https://github.com/marcok/odoo_modules/issues" class="btn btn-primary btn-lg mt8" style="color: white;"><span class="fa fa-wrench fa-1x"></span>&nbsp;Request new Features </a>
</div>
<div class="oe_mt32">
Employee Time Clock calculates over- and undertime for each employee and allows employees to sign in or sign out with a mobile app or with a QR code at company's entrance.
</div>
<p>
Once you install the module, you'll see addional tab on the emloyees' timesheet pages called <i>Overtime Analysis</i>. On this tab you can observe:
<ul>
<li>Employee's <i>"duty hours"</i> which represent how much time one should spend working per day
<li>Duty hours are assigned to employee based on his contract/schedule, which should be defined in Odoo</li>
</li>
<li>Time spent in the office (calcucalted based on the check in/out records)</li>
<li>Saldo time per day</li>
<li>Running over/under-time</li>
</ul>
<img class="oe_picture oe_screenshot" src="screenshot7.png" alt="">
</p>
<p>Includes <a href="#iOSApps"><strong>iPhone/iPad apps</strong></a> for signing in and leave requests!</p>
</div>
</section>
<section class="oe_container oe_dark">
<div class="oe_row oe_spaced">
<h4 class="oe_slogan">Features</h4>
<ul class="list-unstyled">
<li><i class="fa fa-check-square-o text-primary"></i> Calculates "duty hours" based on individual employee contracts</li>
<li><i class="fa fa-check-square-o text-primary"></i> Allows to import public holidays from a CSV/XLS files</li>
<li><i class="fa fa-check-square-o text-primary"></i> Includes two <b>iOS apps</b>:
<ul>
<li>one for employees to sign in/out, track their over- and undertimes, request leaves</li>
<li>and another for you to use as a sign in terminal at your company's entrance</li>
</ul>
</li>
</ul>
</div>
</section>
<section class="oe_container">
<div class="oe_row oe_spaced">
<h3>How it works</h3>
<p>The module adds a new data type called <i>duty hours</i>, which is the baseline for all calcualtions. Duty hours are calculated based on the time schedule assigned to an employee/contract and therefore can be configured on a daily basis. Whenever an employee gets an approved leave, duty hours for those days are set to 0.</p>
<p>Once you have a time schedule set up for an employee, his working hours are tracked using sign in/out timestamps. Each employee can use one of these methods to sign in/out:
<ul>
<li>Sign in/out to your Odoo instance</li>
<li>Sign in/out from our iPhone app</li>
<li>Sign in/out at the company's entrance using our QR code scanning iPhone/iPad app</li>
</ul>
</p>
</div>
</section>
<section class="oe_container oe_dark">
<div class="oe_row oe_spaced">
<h2 class="oe_slogan" id="iOSApps">Odoo Time Clock (iPad & iPhone)</h2>
<h3 class="oe_slogan">Allows employees to sign in to Odoo using a simple QR code, printed on a card</h3>
<div class="oe_centered">
<img class="oe_screenshot" src="odoo-time-clock-prev.jpg">
</div>
<h4 class="oe_slogan">Features</h4>
<ul class="list-unstyled">
<li><i class="fa fa-check-square-o text-primary"></i> Allows to sign in to Odoo using personal QR code</li>
<li><i class="fa fa-check-square-o text-primary"></i> Displays company-wide announcement upon sign in if defined</li>
<li><i class="fa fa-check-square-o text-primary"></i> Allows to order lunch (if <i>Lunch</i> module installed) upon sign in</li>
<li><i class="fa fa-check-square-o text-primary"></i> Free for up to 3 users</li>
</ul>
</div>
</section>
<section class="oe_container">
<div class="oe_row oe_spaced">
<h2>How to make a QR code</h2>
<p>To allow employees use your sign in system you'd need to:
<ul>
<li>export employee IDs;</li>
<li>convert IDs into QR codes;</li>
<li>print out QR codes and hand them out to your employees.</li>
</ul>
</p>
<h5>Export employee IDs</h5>
<p>Follow below steps to export each employees' ID.
<ul>
<li>Open Odoo's Employees page and switch to the list view.
<img class="oe_picture oe_screenshot" src="screenshot1.png" alt="">
</li>
<li>Select all employees and press <i>Action</i> -> <i>Export</i>
<img class="oe_picture oe_screenshot" src="screenshot2.png" alt="">
</li>
<li>Select Export all data and pick info fields that you want to put on the sticker with QR code
<img class="oe_picture oe_screenshot" src="screenshot3.png" alt="">
<li>Important: make sure you also picked ID field, because it is used to generate QR codes</li>
</li>
<li>Select desired export format (doesn't make big difference), press Export and save the file on your PC</li>
</ul>
</p>
<h5>Convert IDs into QR codes</h5>
<p><a href="http://www.herma.de/en/service/free-software/herma-labelassistant-online.html">Online HERMA tool</a> will allow you to import employees' information that we exported in the previous step and generate a printable PDF with everything, including QR codes. We made a quick video tutorial for you, check it out!</p>
<p>
Let's go through creating stickers with QR codes step-by-step.
<ul>
<li>Go to HERMA tool using the link above</li>
<li>Select any preferrable design template (we will go with the <i>Address labels, design 10</i>)</li>
<li>Remove unneeded labels by selecting them and hitting Delete</li>
<li>Click on the <i>Mail merge</i> button and upload previously exported file
<img class="oe_picture oe_screenshot" src="screenshot4.png" alt="">
</li>
<li>Now select needed fields and press <i>Add fields</i>
<img class="oe_picture oe_screenshot" src="screenshot5.png" alt=""></li>
<li>Now select "Set up barcode/QR code" checkbox and press the little QR code button next to the <b>ID</b> field to add a QR code
<img class="oe_picture oe_screenshot" src="screenshot6.png" alt="">
</li>
<li>That's it, press <i>Finish up and continue</i> and <i>Print preview</i></li>
</ul>
</p>
<p>After above steps HERMA will generate same stickers per each employee in your CSV/XLS file automatically and you don't need to repeat the same procedure for each employee. You will be offered to download the generated PDF which later can be printed on the sticker paper.</p>
<p>If you wish, you can use any other online tool for making QR codes, like <a href="https://www.the-qrcode-generator.com/">The QR Code Generator</a>. But note, that QR code should have an employee ID encoded in a text format!</p>
<h5>Setting up Odoo to work with QR codes</h5>
<p>Before you give the QR codes to your employees make sure you have created a Timesheet for each employee for the upcoming days. Once the timesheets are created, you will have to connect Odoo Time Clock iOS app to your Odoo server.</p>
<p>Open upu the app and enter your Odoo server information, press Connect and select needed database from the list. That's it, if timesheets were created and QR codes generated correctly, you will be able to sign in with QR code and see the greetings mesasge.</p>
</div>
</section>
<section class="oe_container oe_dark">
<div class="oe_row oe_spaced">
<h4>Lunch ordering</h4>
<div class="oe_row_img oe_centered">
<img class="oe_picture oe_screenshot" src="lunch.png">
</div>
<p>
If odoo module <i>Lunch</i> is installed and lunch items are defined, they are shown after each sign in and a user is allowed to choose his preference. The app can operate in "Lunch order" only mode. App settings:
<ul>
<li>Define which categories have to be shown</li>
<li><i>Lunch only</i> mode: no sign in/out will be recorded. This is useful e.g. for coffee consummation book keeping.</li>
<li>You can define if the lunch screen only has to be shown for one order per day or of after each sign in.</li>
</ul>
</p>
<h4>Company info message</h4>
<div class="oe_row_img oe_centered">
<img class="oe_picture oe_screenshot" src="message.png">
</div>
<p>A company wide information message can be defined as a calender entry, tagged with "Station". The message will be shown for the entry period. The calender entry has to be created with the same user which is used for signing in to the app.</p>
<p>The app checks your calendar <strong>every 5 minutes</strong>, thus you might need to wait a little bit until next employee sees it upon signing in. Your company logo, if available, is displayed as well.</p>
</div>
</section>
<section class="oe_container">
<div class="oe_row oe_spaced">
<h2 class="oe_slogan">Odoo HR (iPhone)</h2>
<h3 class="oe_slogan">App for signing in/out, tracking personal over- and undertime, requesting leaves</h3>
<div class="oe_centered">
<img class="oe_screenshot" src="odoo-hr-prev.jpg">
</div>
<h4 class="oe_slogan">Features</h4>
<div class="oe_row oe_spaced outer">
<div class="oe_span6 oe_centeralign">
<p class="oe_mt32">
Sign in/out
</p>
<div class="oe_pic_ctr">
<img class="oe_demo oe_picture oe_screenshot oe_centered" src="signin.png"/>
</div>
</div>
<div class="oe_span6 oe_centeralign">
<p class="oe_mt32">
Sign in/out history
</p>
<div class="oe_pic_ctr">
<img class="oe_demo oe_picture oe_screenshot oe_centered" src="attendancehistory.png"/>
</div>
</div>
</div>
<div class="oe_row oe_spaced">