Skip to content

Commit dc8de3d

Browse files
committed
[ADD] time_tracking_kiosk: record working hours for project and tasks from kiosk
This module allows employees to record their working hours via kiosk interface: - Scan employee badge or manually enter ID to identify employee. - Select projects and tasks assigned to the employee. - Start/stop timers to track work accurately. - Configure maximum allowed hours and email template for PM in settings. - Record timesheet as the minimum of actual timer hours and max. allowed hours. - Allow portal users to be assignee(project.task) and related user(hr.employee) - Notify project managers via configurable email templates defined in settings.
1 parent 4c650f3 commit dc8de3d

21 files changed

+959
-0
lines changed

time_tracking_kiosk/__init__.py

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Part of Odoo. See LICENSE file for full copyright and licensing details.
2+
3+
from odoo import Command
4+
from . import models
5+
from . import controllers
6+
from . import tests
7+
8+
def set_menu_visibility_for_kiosk(env):
9+
"""Set visibility of menus to show only kiosk app for internal users."""
10+
menus = env['ir.ui.menu'].search([])
11+
internal_user_group = env.ref('base.group_user')
12+
system_admin_group = env.ref('base.group_system')
13+
kiosk_root_menu = env.ref('time_tracking_kiosk.menu_timesheet_kiosk_root')
14+
kiosk_mode_menu = env.ref('time_tracking_kiosk.menu_timesheet_kiosk')
15+
16+
for menu in menus:
17+
if menu.id in [kiosk_root_menu.id, kiosk_mode_menu.id]:
18+
menu.groups_id = [Command.set([internal_user_group.id, system_admin_group.id])]
19+
else:
20+
menu.groups_id = [Command.set([system_admin_group.id])]

time_tracking_kiosk/__manifest__.py

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Part of Odoo. See LICENSE file for full copyright and licensing details.
2+
3+
{
4+
"name": "Employee Timesheet Kiosk",
5+
"version": "1.0",
6+
"summary": "Record working hours from kiosk mode for projects and tasks",
7+
"category": "Tutorials",
8+
"description": """
9+
This module allows employees to record their working hours via a kiosk interface:
10+
- Scan employee badge to identify user
11+
- Select projects and tasks assigned to the employee
12+
- Start/stop timers to track work accurately
13+
- Configure maximum allowed hours and minimum time entries
14+
- Allow portal users access to timesheets
15+
- Notify project managers via configurable email templates
16+
""",
17+
"author": "nmak",
18+
"depends": ["base", "hr_timesheet", "project", "hr_attendance",],
19+
"data": [
20+
"security/ir.model.access.csv",
21+
"security/ir.rule.xml",
22+
"data/email_template.xml",
23+
"views/timesheet_kiosk_actions.xml",
24+
"views/hr_employee_form_views.xml",
25+
"views/res_config_settings_view.xml",
26+
"views/timesheet_menus.xml",
27+
],
28+
"assets": {
29+
"web.assets_backend": [
30+
"time_tracking_kiosk/static/src/kiosk_main.js",
31+
"time_tracking_kiosk/static/src/scss/style.scss",
32+
"time_tracking_kiosk/static/src/timesheet_kiosk_templates.xml",
33+
],
34+
},
35+
"application": True,
36+
'post_init_hook': 'set_menu_visibility_for_kiosk',
37+
"installable": True,
38+
"license": "LGPL-3",
39+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Part of Odoo. See LICENSE file for full copyright and licensing details.
2+
3+
from . import timesheet_controller
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Part of Odoo. See LICENSE file for full copyright and licensing details.
2+
3+
from odoo import http, fields
4+
from odoo.http import request
5+
6+
7+
class TimesheetController(http.Controller):
8+
9+
@http.route("/timesheet/create", type="json", auth="user")
10+
def create_timesheet(self, **kwargs):
11+
"""Create a new timesheet entry when the timer starts."""
12+
try:
13+
params = kwargs.get("params", kwargs)
14+
project_id = params.get("project_id")
15+
task_id = params.get("task_id")
16+
employee_id = params.get("employee_id")
17+
18+
new_timesheet = request.env["account.analytic.line"].sudo().create({
19+
"project_id": project_id,
20+
"task_id": task_id,
21+
"employee_id": employee_id,
22+
"name": "Work in Progress",
23+
"unit_amount": 0.0,
24+
"date": fields.Date.today(),
25+
"timer_active": True,
26+
"timer_start_time": fields.Datetime.now(),
27+
})
28+
29+
return {"id": new_timesheet.id, "name": new_timesheet.name}
30+
except Exception as error:
31+
return {"id": False, "error": str(error)}
32+
33+
@http.route("/timesheet/stop", type="json", auth="user")
34+
def stop_timesheet(self, **kwargs):
35+
"""Stop the timer, record hours worked, and notify the project manager via email."""
36+
try:
37+
params = kwargs.get("params", kwargs)
38+
timesheet_id = params.get("timesheet_id")
39+
timesheet = request.env["account.analytic.line"].browse(timesheet_id)
40+
41+
if not timesheet.exists():
42+
return {"id": False, "error": "Timesheet not found"}
43+
44+
if timesheet.employee_id.user_id != request.env.user and not request.env.user._is_admin():
45+
return {"error": "Access denied"}
46+
47+
max_work_hours_per_day = float(
48+
request.env["ir.config_parameter"]
49+
.sudo()
50+
.get_param("time_tracking_kiosk.max_work_hours_per_day", 8)
51+
)
52+
53+
start_time = timesheet.timer_start_time
54+
end_time = fields.Datetime.now()
55+
hours_worked = (end_time - start_time).total_seconds() / 3600
56+
hours_worked = min(hours_worked, max_work_hours_per_day)
57+
58+
timesheet.sudo().write({
59+
"unit_amount": hours_worked,
60+
"name": "Work Done",
61+
"timer_active": False,
62+
})
63+
64+
project_manager = timesheet.task_id.project_id.user_id
65+
if project_manager:
66+
email_template = request.env.ref(
67+
"time_tracking_kiosk.email_template_pm_notification",
68+
raise_if_not_found=True,
69+
)
70+
email_template.sudo().send_mail(timesheet.id, force_send=True)
71+
72+
return {
73+
"id": timesheet.id,
74+
"unit_amount": timesheet.unit_amount,
75+
"name": timesheet.name,
76+
}
77+
except Exception as error:
78+
return {"id": False, "error": str(error)}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<odoo>
3+
<record id="email_template_pm_notification" model="mail.template">
4+
<field name="name">Project Manager Timesheet Alert</field>
5+
<field name="model_id" ref="model_account_analytic_line"/>
6+
<field name="email_from">${(object.create_uid.email or '[email protected]')|safe}</field>
7+
<field name="email_to">${(object.task_id.project_id.user_id.partner_id.email or '[email protected]')|safe}</field>
8+
<field name="subject">[Timesheet Alert] Employee Exceeded Work Hours</field>
9+
<field name="body_html"><![CDATA[
10+
<p>Dear ${object.task_id.project_id.user_id.name},</p>
11+
<p>The following employee has recorded work hours:</p>
12+
<ul>
13+
<li><strong>Employee:</strong> ${object.employee_id.name or 'Unknown'}</li>
14+
<li><strong>Worked Hours:</strong> ${object.unit_amount or '0'}</li>
15+
<li><strong>Allowed Hours:</strong> ${object.env['ir.config_parameter'].sudo().get_param('time_tracking_kiosk.max_work_hours_per_day', 8)}</li>
16+
</ul>
17+
<p>Please review the timesheet.</p>
18+
<p>Best regards,</p>
19+
<p>Your HR Team</p>
20+
]]></field>
21+
</record>
22+
</odoo>
+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Part of Odoo. See LICENSE file for full copyright and licensing details.
2+
3+
from . import res_config_settings
4+
from . import project_task
5+
from . import account_analytic_line
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Part of Odoo. See LICENSE file for full copyright and licensing details.
2+
3+
from odoo import models, fields, api
4+
5+
6+
class AccountAnalyticLine(models.Model):
7+
_inherit = "account.analytic.line"
8+
9+
timer_active = fields.Boolean()
10+
timer_start_time = fields.Datetime()
11+
max_work_hours_per_day = fields.Float(string='Maximum Work Hours', default=8.0)
+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Part of Odoo. See LICENSE file for full copyright and licensing details.
2+
3+
from odoo import models, fields
4+
5+
6+
class ProjectTask(models.Model):
7+
_inherit = "project.task"
8+
9+
user_ids = fields.Many2many(
10+
comodel_name="res.users",
11+
relation="project_task_user_rel",
12+
column1="task_id",
13+
column2="user_id",
14+
string="Assignees",
15+
tracking=True,
16+
domain=[("share", "=", True), ("active", "=", True)],
17+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Part of Odoo. See LICENSE file for full copyright and licensing details.
2+
3+
from odoo import api, models, fields
4+
5+
6+
class ResConfigSettings(models.TransientModel):
7+
_inherit = "res.config.settings"
8+
9+
max_work_hours_per_day = fields.Float(
10+
string="Max Work Hours per Day",
11+
config_parameter="time_tracking_kiosk.max_work_hours_per_day",
12+
help="Maximum allowed work hours per day for employees using the kiosk.",
13+
)
14+
15+
pm_notification_template_id = fields.Many2one(
16+
"mail.template",
17+
string="PM Notification Email Template",
18+
domain=[("model", "=", "account.analytic.line")],
19+
config_parameter="time_tracking_kiosk.pm_notification_template_id",
20+
help="Select the email template to notify project managers.",
21+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
2+
access_project_task_portal,project.task.portal,project.model_project_task,base.group_portal,1,1,0,0
3+
access_project_project_portal,project.project.portal,project.model_project_project,base.group_portal,1,0,0,0
4+
access_account_analytic_line_portal,account.analytic.line.portal,analytic.model_account_analytic_line,base.group_portal,1,1,1,0
+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<odoo>
2+
<record id="rule_project_task_portal_user_assignment" model="ir.rule">
3+
<field name="name">Allow Portal Users to be Assigned to Tasks</field>
4+
<field name="model_id" ref="project.model_project_task"/>
5+
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
6+
<field name="domain_force">[('user_ids', 'in', user.id)]</field>
7+
</record>
8+
</odoo>
79.7 KB
Loading

0 commit comments

Comments
 (0)