diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..b2d7f41 --- /dev/null +++ b/README.rst @@ -0,0 +1,4 @@ +Employee Time Clock +====================== + +Find all informations here: diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..f2857d2 --- /dev/null +++ b/__init__.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- + +############################################################################## +# +# Clear Groups for Odoo +# Copyright (C) 2016 Bytebrand GmbH (). +# +# 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 . +# +############################################################################## + + +import controllers +import models +import wizard +import report diff --git a/__openerp__.py b/__openerp__.py new file mode 100644 index 0000000..4db2b18 --- /dev/null +++ b/__openerp__.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- + +############################################################################## +# +# Clear Groups for Odoo +# Copyright (C) 2016 Bytebrand GmbH (). +# +# 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 . +# +############################################################################## + +{ + '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', + ] +} diff --git a/controllers/__init__.py b/controllers/__init__.py new file mode 100644 index 0000000..afab2ca --- /dev/null +++ b/controllers/__init__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- + +############################################################################## +# +# Clear Groups for Odoo +# Copyright (C) 2016 Bytebrand GmbH (). +# +# 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 . +# +############################################################################## + +import main \ No newline at end of file diff --git a/controllers/main.py b/controllers/main.py new file mode 100644 index 0000000..8c45414 --- /dev/null +++ b/controllers/main.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- + +############################################################################## +# +# Clear Groups for Odoo +# Copyright (C) 2016 Bytebrand GmbH (). +# +# 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 . +# +############################################################################## + + +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//', auth='public') +# def object(self, obj, **kw): +# return http.request.render('hr_timesheet_overtime.object', { +# 'object': obj +# }) \ No newline at end of file diff --git a/i18n/de.po b/i18n/de.po new file mode 100644 index 0000000..68c9742 --- /dev/null +++ b/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" diff --git a/images/overundertime.png b/images/overundertime.png new file mode 100644 index 0000000..65ee018 Binary files /dev/null and b/images/overundertime.png differ diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000..b741496 --- /dev/null +++ b/models/__init__.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Clear Groups for Odoo +# Copyright (C) 2016 Bytebrand GmbH (). +# +# 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 . +# +############################################################################## + +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 diff --git a/models/hr_attendance_analysis.py b/models/hr_attendance_analysis.py new file mode 100644 index 0000000..449e04a --- /dev/null +++ b/models/hr_attendance_analysis.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- + +############################################################################## +# +# Clear Groups for Odoo +# Copyright (C) 2016 Bytebrand GmbH (). +# +# 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 . +# +############################################################################## + +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) diff --git a/models/hr_employee.py b/models/hr_employee.py new file mode 100644 index 0000000..9ca1dd8 --- /dev/null +++ b/models/hr_employee.py @@ -0,0 +1,111 @@ +# -*- coding: utf-8 -*- + +############################################################################## +# +# Clear Groups for Odoo +# Copyright (C) 2016 Bytebrand GmbH (). +# +# 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 . +# +############################################################################## + + +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}] \ No newline at end of file diff --git a/models/hr_timesheet_dh.py b/models/hr_timesheet_dh.py new file mode 100644 index 0000000..c6481c8 --- /dev/null +++ b/models/hr_timesheet_dh.py @@ -0,0 +1,324 @@ +# -*- coding: utf-8 -*- + +############################################################################## +# +# Clear Groups for Odoo +# Copyright (C) 2016 Bytebrand GmbH (). +# +# 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 . +# +############################################################################## + +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 = [ + ''] + for val in data.values(): + if isinstance(val, (int, float)): + output.append('') + prev_ts = _('Previous Timesheet:') + output.append('') + output.append('') + output.append('') + for k, v in data.items(): + if isinstance(v, list): + output.append('') + for th in v[0].keys(): + output.append('') + output.append('') + for res in v: + values.append(res.values()) + for tr in values: + output.append('') + for td in tr: + output.append('') + output.append('') + + if isinstance(v, dict): + output.append('') + total_ts = _('Total:') + output.append('') + for td in v.values(): + output.append('') + output.append('') + output.append('
' + prev_ts + ' ' + str(val) + '
' + th + '
' + td + '
' + total_ts + ' ' + '%s' % round(td, 4) + '
') + 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) diff --git a/models/hr_timesheet_sheet.py b/models/hr_timesheet_sheet.py new file mode 100644 index 0000000..aa4aa3e --- /dev/null +++ b/models/hr_timesheet_sheet.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- + +############################################################################## +# +# Clear Groups for Odoo +# Copyright (C) 2016 Bytebrand GmbH (). +# +# 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 . +# +############################################################################## + + +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) + diff --git a/models/res_users.py b/models/res_users.py new file mode 100644 index 0000000..f611174 --- /dev/null +++ b/models/res_users.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- + +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2016 - now Bytebrand Outsourcing AG (). +# +# 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 . +# +############################################################################## + + +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} diff --git a/models/resource_calendar.py b/models/resource_calendar.py new file mode 100644 index 0000000..2c63946 --- /dev/null +++ b/models/resource_calendar.py @@ -0,0 +1,123 @@ +# -*- coding: utf-8 -*- + +############################################################################## +# +# Clear Groups for Odoo +# Copyright (C) 2016 Bytebrand GmbH (). +# +# 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 . +# +############################################################################## + +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 diff --git a/models/time_clock_resource_calendar.py b/models/time_clock_resource_calendar.py new file mode 100644 index 0000000..28945d4 --- /dev/null +++ b/models/time_clock_resource_calendar.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- + +############################################################################## +# +# Clear Groups for Odoo +# Copyright (C) 2016 Bytebrand GmbH (). +# +# 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 . +# +############################################################################## + +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 diff --git a/report/__init__.py b/report/__init__.py new file mode 100644 index 0000000..05794d3 --- /dev/null +++ b/report/__init__.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-2010 Tiny SPRL (). +# +# 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 . +# +############################################################################## + +import report_attendance_analysis \ No newline at end of file diff --git a/report/report_attendance_analysis.py b/report/report_attendance_analysis.py new file mode 100644 index 0000000..b35cb17 --- /dev/null +++ b/report/report_attendance_analysis.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-2010 Tiny SPRL (). +# +# 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 . +# +############################################################################## + +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: diff --git a/report/report_attendance_analysis_view.xml b/report/report_attendance_analysis_view.xml new file mode 100644 index 0000000..f70ce2c --- /dev/null +++ b/report/report_attendance_analysis_view.xml @@ -0,0 +1,43 @@ + + + + + + hr.attendance.analysis.report.tree + hr.attendance.analysis.report + + + + + + + + + + + hr.attendance.analysis.report.graph + hr.attendance.analysis.report + + + + + + + + + + + + + Analysis by Timesheet + hr.attendance.analysis.report + form + graph + {'group_by_no_leaf':1,'group_by':[]} + + + + + + diff --git a/security/ir.model.access.csv b/security/ir.model.access.csv new file mode 100644 index 0000000..d073c4f --- /dev/null +++ b/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 diff --git a/security/ir_rule.xml b/security/ir_rule.xml new file mode 100644 index 0000000..be30d65 --- /dev/null +++ b/security/ir_rule.xml @@ -0,0 +1,29 @@ + + + + + + + Personal Employee Contracts + ['|',('employee_id.user_id','=',user.id),('employee_id.parent_id.user_id','child_of',user.id)] + + + + + + + + + + + + + + + + + + + + + diff --git a/static/description/2dBarcodes.png b/static/description/2dBarcodes.png new file mode 100644 index 0000000..051f7ee Binary files /dev/null and b/static/description/2dBarcodes.png differ diff --git a/static/description/attendanceana.png b/static/description/attendanceana.png new file mode 100644 index 0000000..7e88e77 Binary files /dev/null and b/static/description/attendanceana.png differ diff --git a/static/description/attendancehistory.png b/static/description/attendancehistory.png new file mode 100644 index 0000000..3be0cdf Binary files /dev/null and b/static/description/attendancehistory.png differ diff --git a/static/description/generateLeaves.png b/static/description/generateLeaves.png new file mode 100644 index 0000000..455d6bc Binary files /dev/null and b/static/description/generateLeaves.png differ diff --git a/static/description/icon.png b/static/description/icon.png new file mode 100644 index 0000000..7af0fbd Binary files /dev/null and b/static/description/icon.png differ diff --git a/static/description/index.html b/static/description/index.html new file mode 100644 index 0000000..96a4f7d --- /dev/null +++ b/static/description/index.html @@ -0,0 +1,201 @@ +
+
+

Employee Time Clock

+

Tracking employees' over- and undertime with ease

+

by Bytebrand Outsourcing AG

+ +
+ 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. +
+

+ Once you install the module, you'll see addional tab on the emloyees' timesheet pages called Overtime Analysis. On this tab you can observe: +

    +
  • Employee's "duty hours" which represent how much time one should spend working per day +
  • Duty hours are assigned to employee based on his contract/schedule, which should be defined in Odoo
  • + +
  • Time spent in the office (calcucalted based on the check in/out records)
  • +
  • Saldo time per day
  • +
  • Running over/under-time
  • +
+ +

+

Includes iPhone/iPad apps for signing in and leave requests!

+
+
+
+
+

Features

+
    +
  • Calculates "duty hours" based on individual employee contracts
  • +
  • Allows to import public holidays from a CSV/XLS files
  • +
  • Includes two iOS apps: +
      +
    • one for employees to sign in/out, track their over- and undertimes, request leaves
    • +
    • and another for you to use as a sign in terminal at your company's entrance
    • +
    +
  • +
+
+
+
+
+

How it works

+

The module adds a new data type called duty hours, 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.

+

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: +

    +
  • Sign in/out to your Odoo instance
  • +
  • Sign in/out from our iPhone app
  • +
  • Sign in/out at the company's entrance using our QR code scanning iPhone/iPad app
  • +
+

+
+
+
+
+

Odoo Time Clock (iPad & iPhone)

+

Allows employees to sign in to Odoo using a simple QR code, printed on a card

+
+ +
+

Features

+
    +
  • Allows to sign in to Odoo using personal QR code
  • +
  • Displays company-wide announcement upon sign in if defined
  • +
  • Allows to order lunch (if Lunch module installed) upon sign in
  • +
  • Free for up to 3 users
  • +
+
+
+
+
+

How to make a QR code

+

To allow employees use your sign in system you'd need to: +

    +
  • export employee IDs;
  • +
  • convert IDs into QR codes;
  • +
  • print out QR codes and hand them out to your employees.
  • +
+

+
Export employee IDs
+

Follow below steps to export each employees' ID. +

    +
  • Open Odoo's Employees page and switch to the list view. + +
  • +
  • Select all employees and press Action -> Export + +
  • +
  • Select Export all data and pick info fields that you want to put on the sticker with QR code + +
  • Important: make sure you also picked ID field, because it is used to generate QR codes
  • + +
  • Select desired export format (doesn't make big difference), press Export and save the file on your PC
  • +
+

+
Convert IDs into QR codes
+

Online HERMA tool 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!

+

+ Let's go through creating stickers with QR codes step-by-step. +

    +
  • Go to HERMA tool using the link above
  • +
  • Select any preferrable design template (we will go with the Address labels, design 10)
  • +
  • Remove unneeded labels by selecting them and hitting Delete
  • +
  • Click on the Mail merge button and upload previously exported file + +
  • +
  • Now select needed fields and press Add fields +
  • +
  • Now select "Set up barcode/QR code" checkbox and press the little QR code button next to the ID field to add a QR code + +
  • +
  • That's it, press Finish up and continue and Print preview
  • +
+

+

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.

+

If you wish, you can use any other online tool for making QR codes, like The QR Code Generator. But note, that QR code should have an employee ID encoded in a text format!

+
Setting up Odoo to work with QR codes
+

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.

+

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.

+
+
+
+
+

Lunch ordering

+
+ +
+

+ If odoo module Lunch 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: +

    +
  • Define which categories have to be shown
  • +
  • Lunch only mode: no sign in/out will be recorded. This is useful e.g. for coffee consummation book keeping.
  • +
  • You can define if the lunch screen only has to be shown for one order per day or of after each sign in.
  • +
+

+

Company info message

+
+ +
+

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.

+

The app checks your calendar every 5 minutes, 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.

+
+
+
+
+

Odoo HR (iPhone)

+

App for signing in/out, tracking personal over- and undertime, requesting leaves

+
+ +
+

Features

+
+
+

+ Sign in/out +

+
+ +
+
+
+

+ Sign in/out history +

+
+ +
+
+
+
+
+

+ Attendance analysis +

+
+ +
+
+
+

+ Leave requests +

+
+ +
+
+
+
+
+
+
+
+

Bytebrand Outsourcing AG

+ Bytebrand.net + Facebook + LinkedIn +
+
\ No newline at end of file diff --git a/static/description/leave.png b/static/description/leave.png new file mode 100644 index 0000000..af0199d Binary files /dev/null and b/static/description/leave.png differ diff --git a/static/description/lunch.png b/static/description/lunch.png new file mode 100644 index 0000000..74e85b9 Binary files /dev/null and b/static/description/lunch.png differ diff --git a/static/description/message.png b/static/description/message.png new file mode 100644 index 0000000..8c67fda Binary files /dev/null and b/static/description/message.png differ diff --git a/static/description/odoo-hr-prev.jpg b/static/description/odoo-hr-prev.jpg new file mode 100644 index 0000000..f0462b2 Binary files /dev/null and b/static/description/odoo-hr-prev.jpg differ diff --git a/static/description/odoo-time-clock-prev.jpg b/static/description/odoo-time-clock-prev.jpg new file mode 100644 index 0000000..281ee7c Binary files /dev/null and b/static/description/odoo-time-clock-prev.jpg differ diff --git a/static/description/screenshot1.png b/static/description/screenshot1.png new file mode 100644 index 0000000..0974b09 Binary files /dev/null and b/static/description/screenshot1.png differ diff --git a/static/description/screenshot2.png b/static/description/screenshot2.png new file mode 100644 index 0000000..9eed96b Binary files /dev/null and b/static/description/screenshot2.png differ diff --git a/static/description/screenshot3.png b/static/description/screenshot3.png new file mode 100644 index 0000000..6413829 Binary files /dev/null and b/static/description/screenshot3.png differ diff --git a/static/description/screenshot4.png b/static/description/screenshot4.png new file mode 100644 index 0000000..7033e76 Binary files /dev/null and b/static/description/screenshot4.png differ diff --git a/static/description/screenshot5.png b/static/description/screenshot5.png new file mode 100644 index 0000000..6932f5e Binary files /dev/null and b/static/description/screenshot5.png differ diff --git a/static/description/screenshot6.png b/static/description/screenshot6.png new file mode 100644 index 0000000..f1c3a33 Binary files /dev/null and b/static/description/screenshot6.png differ diff --git a/static/description/screenshot7.png b/static/description/screenshot7.png new file mode 100644 index 0000000..c6fa9c9 Binary files /dev/null and b/static/description/screenshot7.png differ diff --git a/static/description/signin.png b/static/description/signin.png new file mode 100644 index 0000000..f8e14ce Binary files /dev/null and b/static/description/signin.png differ diff --git a/static/description/timesheetgeneration.png b/static/description/timesheetgeneration.png new file mode 100644 index 0000000..e8f79a2 Binary files /dev/null and b/static/description/timesheetgeneration.png differ diff --git a/test_dh_api.py b/test_dh_api.py new file mode 100644 index 0000000..1299295 --- /dev/null +++ b/test_dh_api.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python + +import xmlrpclib +import datetime + + +url = "http://localhost" +db = "db" +username = "admin" +password = 'admin' + +common = xmlrpclib.ServerProxy('{}/xmlrpc/2/common'.format(url)) +print common.version() + +uid = common.authenticate(db, username, password, {}) +print uid + +models = xmlrpclib.ServerProxy('{}/xmlrpc/2/object'.format(url), verbose=True) +print models.execute_kw(db, uid, password, + 'hr_timesheet_sheet.sheet', 'attendance_analysis', [], dict(timesheet_id=1)) \ No newline at end of file diff --git a/views/views.xml b/views/views.xml new file mode 100644 index 0000000..6bf2e7c --- /dev/null +++ b/views/views.xml @@ -0,0 +1,44 @@ + + + + Academy teachers + hr_timesheet_sheet.sheet + form + tree,form + + + + view.name + hr_timesheet_sheet.sheet + + + + + + + + + + hr_timesheet_sheet.overtime.form + hr_timesheet_sheet.sheet + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/wizard/__init__.py b/wizard/__init__.py new file mode 100644 index 0000000..d06634e --- /dev/null +++ b/wizard/__init__.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- + +############################################################################## +# +# Clear Groups for Odoo +# Copyright (C) 2016 Bytebrand GmbH (). +# +# 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 . +# +############################################################################## + +from . import create_timesheet_with_tag +from . import import_leave_requests + +# END \ No newline at end of file diff --git a/wizard/create_timesheet_with_tag.py b/wizard/create_timesheet_with_tag.py new file mode 100644 index 0000000..852cbac --- /dev/null +++ b/wizard/create_timesheet_with_tag.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- + +############################################################################## +# +# Clear Groups for Odoo +# Copyright (C) 2016 Bytebrand GmbH (). +# +# 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 . +# +############################################################################## + + +import time +from openerp import fields, models, api, _ +from openerp.exceptions import ValidationError + + +class CreateTimesheetWithTag(models.TransientModel): + _inherit = 'hr.timesheet.current.open' + _description = 'Create Timesheet With Employee Tag' + + # Added below fields on the wizard + category_id = fields.Many2one('hr.employee.category', + string="Employee Tag", + required=True, + help='Category of Employee') + date_from = fields.Date(string='Start Date') + date_to = fields.Date(string='End Date') + + @api.onchange('date_from', 'date_to') + @api.multi + def change_date(self, date_from, date_to): + if date_to and date_from and date_from > 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(CreateTimesheetWithTag, self).create(values) + + @api.multi + def open_timesheet(self): + employee_obj = self.env['hr.employee'] + ts = self.env['hr_timesheet_sheet.sheet'] + value = super(CreateTimesheetWithTag, self).open_timesheet() + # First: Search all employees of selected Tag + if not self.category_id: + return value + category_id = self.category_id.id + employee_objects = employee_obj.search([ + ('category_ids', 'in', [category_id])]) + user_ids = [] + ts_ids = [] + date_from = self.date_from or time.strftime('%Y-%m-%d') + date_to = self.date_to or time.strftime('%Y-%m-%d') + # Second: Create/Open Timesheets for all fetched employees. + for emp in employee_objects: + + if emp.user_id: + user_ids.append(emp.user_id.id) + ts_id = ts.search([ + ('user_id', '=', emp.user_id.id), + ('state', 'in', ('draft', 'new')), + ('date_from', '<=', date_from), + ('date_to', '>=', date_to) + ]) + if ts_id: + raise ValidationError( + _('Timesheet already exists for {name}.'.format( + name=emp.name))) + if not ts_id: + values = {'employee_id': emp.id} + if self.date_from and self.date_to: + values.update({ + 'date_from': date_from, + 'date_to': date_to}) + ts_id = ts.create(values) + + ts_ids.append(ts_id.id) + + # Third: Add it to dictionary to be returned + domain = "[('id','in',%s),('user_id', 'in', %s)]" % (ts_ids, user_ids) + value.update(domain=domain) + value.update(view_mode='tree,form') + return value + +# END diff --git a/wizard/create_timesheet_with_tag_view.xml b/wizard/create_timesheet_with_tag_view.xml new file mode 100644 index 0000000..56be7d6 --- /dev/null +++ b/wizard/create_timesheet_with_tag_view.xml @@ -0,0 +1,37 @@ + + + + + + + + hr_timesheet_current_open.form.inherit + hr.timesheet.current.open + + + + Generate timesheets for employees having the following tag: + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/wizard/import_leave_requests.py b/wizard/import_leave_requests.py new file mode 100644 index 0000000..885bb2d --- /dev/null +++ b/wizard/import_leave_requests.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- + +############################################################################## +# +# Clear Groups for Odoo +# Copyright (C) 2016 Bytebrand GmbH (). +# +# 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 . +# +############################################################################## +from datetime import datetime, timedelta +import pytz + +from openerp.osv import fields, osv +from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT +from openerp.tools.translate import _ + + +class import_leave_requests(osv.osv_memory): + _name = 'import.leave.requests' + _description = 'Import Leave Requests With Employee Tag' + _columns = { + 'leave_dates': fields.binary('Select *.csv', + required=True, + help="Select csv file having " + "holiday dates."), + 'leave_type_id': fields.many2one('hr.holidays.status', + 'Leave Type', + required=True), + 'employee_tag_id': fields.many2one('hr.employee.category', + "Employee Tag", required=True), + } + + def convert_to_user_timezone(self, user_tz, dt): + input_tz = pytz.timezone(user_tz) + converted_date = input_tz.localize(dt, is_dst=False) + converted_date = converted_date.astimezone(pytz.UTC).strftime( + DEFAULT_SERVER_DATETIME_FORMAT) + return converted_date + + def import_leave_data(self, cr, uid, ids, context=None): + holiday_obj = self.pool.get('hr.holidays') + employee_obj = self.pool.get('hr.employee') + timesheet_obj = self.pool.get('hr_timesheet_sheet.sheet') + converter = self.pool.get('ir.fields.converter') + for data in self.browse(cr, uid, ids, context): + leaves = (data.leave_dates.decode('base64')).split('\n') + category_id = data.employee_tag_id.id + employee_ids = employee_obj.search(cr, uid, [ + ('category_ids', 'in', [category_id])], context=context) + for employee_id in employee_ids: + employee = employee_obj.browse(cr, uid, employee_id, context) + for leave in leaves[:-1]: + dt_fmt, tm_fmt = \ + (timesheet_obj._get_user_datetime_format( + cr, + uid, + context=context)) + try: + datetime.strptime(leave, dt_fmt) + except ValueError: + raise osv.except_osv(_('Data Error!'), _( + "Date format in your .csv file does not " + "match with database date format.")) + dt1 = datetime.strptime(leave, dt_fmt) + dt2 = (datetime.strptime(leave, dt_fmt) + + timedelta(hours=23, + minutes=59, + seconds=59)) + user_tz = employee.user_id and employee.user_id.tz or 'utc' + leave_date = self.convert_to_user_timezone(user_tz, dt1) + leave_date_to = self.convert_to_user_timezone(user_tz, dt2) + holiday_id = holiday_obj.create(cr, uid, { + 'name': data.leave_type_id.name, + 'date_from': leave_date, + 'date_to': leave_date_to, + 'holiday_status_id': data.leave_type_id.id, + 'employee_id': employee_id, + 'number_of_days_temp': 1.0, + 'type': 'remove' + }) + holiday_obj.holidays_validate(cr, uid, [holiday_id], + context=context) + return True + + # END diff --git a/wizard/import_leave_requests_view.xml b/wizard/import_leave_requests_view.xml new file mode 100644 index 0000000..24dc380 --- /dev/null +++ b/wizard/import_leave_requests_view.xml @@ -0,0 +1,46 @@ + + + + + + + import.leave.requests.form + import.leave.requests + +
+ + + + + + + + + + +
+
+
+
+
+ + + Import Leave Requests + import.leave.requests + form + form + + new + + + + +
+
+ + \ No newline at end of file