fix(fusion_clock): stop stale missed-clock-in nag; add Owner role + attendance exemption
The "explain your missed clock-out" dialog (driven by hr.employee. x_fclk_pending_reason) was set by the absence + auto-clock-out crons but only cleared by the systray reason dialog -- never by the kiosk/NFC clock paths that staff actually use. During the kiosk rollout the absence cron flagged the whole company (hundreds of "absent" logs); those stale flags then nagged everyone forever, even while currently clocked in. Fixes: - Clear x_fclk_pending_reason on every successful clock-in (portal, systray, PIN kiosk, NFC kiosk). Back on the clock => no nag. - get_status / dashboard never report pending while checked-in or exempt; the systray also guards the dialog client-side. - Absence detection no longer sets x_fclk_pending_reason (an absence has no "departure time" to explain). It still logs 'absent' + notifies the office. - One-time migration (19.0.4.2.0) clears existing stale flags. Owner / attendance exemption: - New "Owner" role (top of the Fusion Clock access dropdown, implies Manager) plus a per-employee "Exempt from Attendance" checkbox. - hr.employee._fclk_is_attendance_exempt(); the absence, auto-clock-out, reminder and weekly-summary crons all skip exempt employees, and the dialog is suppressed for them. Tests: tests/test_pending_reason_exempt.py (13 cases). Full fusion_clock suite green except pre-existing env-sensitive failures. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
|
||||
{
|
||||
'name': 'Fusion Clock',
|
||||
'version': '19.0.4.1.0',
|
||||
'version': '19.0.4.2.0',
|
||||
'category': 'Human Resources/Attendances',
|
||||
'summary': 'Complete Employee T&A with Geofencing, Shifts, Penalties, Overtime, Kiosk, Dashboard & Payroll Export',
|
||||
'description': """
|
||||
|
||||
@@ -287,6 +287,11 @@ class FusionClockAPI(http.Controller):
|
||||
|
||||
attendance.sudo().write(write_vals)
|
||||
|
||||
# A successful clock-in resolves any pending missed-clock-out flag,
|
||||
# so the employee is never nagged once they are back on the clock.
|
||||
if employee.x_fclk_pending_reason:
|
||||
employee.sudo().write({'x_fclk_pending_reason': False})
|
||||
|
||||
# Log clock-in
|
||||
self._log_activity(
|
||||
employee, 'clock_in',
|
||||
@@ -542,7 +547,10 @@ class FusionClockAPI(http.Controller):
|
||||
'is_checked_in': is_checked_in,
|
||||
'employee_name': employee.name,
|
||||
'enable_clock': employee.x_fclk_enable_clock,
|
||||
'pending_reason': employee.x_fclk_pending_reason,
|
||||
# Only nag when there is genuinely something to explain: a flag set,
|
||||
# the employee NOT currently on the clock, and not attendance-exempt.
|
||||
'pending_reason': (employee.x_fclk_pending_reason and not is_checked_in
|
||||
and not employee._fclk_is_attendance_exempt()),
|
||||
'ontime_streak': employee.x_fclk_ontime_streak,
|
||||
}
|
||||
local_today = get_local_today(request.env, employee)
|
||||
@@ -728,7 +736,8 @@ class FusionClockAPI(http.Controller):
|
||||
'is_checked_in': is_checked_in,
|
||||
'check_in': check_in,
|
||||
'location_name': location_name,
|
||||
'pending_reason': employee.x_fclk_pending_reason,
|
||||
'pending_reason': (employee.x_fclk_pending_reason and not is_checked_in
|
||||
and not employee._fclk_is_attendance_exempt()),
|
||||
'today_hours': today_hours,
|
||||
'week_hours': week_hours,
|
||||
'overtime_week': round(employee.x_fclk_overtime_this_week or 0, 2),
|
||||
|
||||
@@ -137,6 +137,9 @@ class FusionClockKiosk(http.Controller):
|
||||
'x_fclk_clock_source': 'kiosk',
|
||||
'x_fclk_check_in_photo': photo_bytes if photo_bytes else False,
|
||||
})
|
||||
# Back on the clock -> clear any stale missed-clock-out flag.
|
||||
if employee.x_fclk_pending_reason:
|
||||
employee.sudo().write({'x_fclk_pending_reason': False})
|
||||
api._log_activity(employee, 'clock_in', f"Kiosk clock-in at {location.name}",
|
||||
attendance=attendance, location=location,
|
||||
latitude=0, longitude=0, distance=0, source='kiosk')
|
||||
|
||||
@@ -345,6 +345,9 @@ class FusionClockNfcKiosk(http.Controller):
|
||||
'x_fclk_clock_source': 'nfc_kiosk',
|
||||
'x_fclk_check_in_photo': photo_bytes if photo_bytes else False,
|
||||
})
|
||||
# Back on the clock -> clear any stale missed-clock-out flag.
|
||||
if employee.x_fclk_pending_reason:
|
||||
employee.sudo().write({'x_fclk_pending_reason': False})
|
||||
api._log_activity(
|
||||
employee, 'clock_in',
|
||||
f"NFC kiosk clock-in at {location.name}",
|
||||
|
||||
25
fusion_clock/migrations/19.0.4.2.0/post-migrate.py
Normal file
25
fusion_clock/migrations/19.0.4.2.0/post-migrate.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
"""One-time reset of stale missed-clock-out flags on upgrade to 19.0.4.1.0.
|
||||
|
||||
Background: x_fclk_pending_reason was set by the absence + auto-clock-out crons
|
||||
but only cleared by the systray reason dialog -- never by the kiosk / NFC clock
|
||||
paths that staff actually use. During the kiosk rollout the absence cron flagged
|
||||
essentially the whole company (hundreds of "absent" logs), and those flags then
|
||||
nagged everyone forever, even while currently clocked in.
|
||||
|
||||
This release clears the flag on every clock-in (all paths), stops absences from
|
||||
setting it at all, and exempts owners. The flags already on record are stale
|
||||
artifacts of the rollout, so wipe them once here; correct ones re-appear only
|
||||
for a genuine forgotten clock-out from now on.
|
||||
"""
|
||||
|
||||
|
||||
def migrate(cr, version):
|
||||
if not version:
|
||||
return
|
||||
cr.execute(
|
||||
"UPDATE hr_employee SET x_fclk_pending_reason = false "
|
||||
"WHERE x_fclk_pending_reason = true"
|
||||
)
|
||||
@@ -345,6 +345,9 @@ class HrAttendance(models.Model):
|
||||
continue
|
||||
|
||||
employee = att.employee_id
|
||||
# Owners / attendance-exempt employees are never auto-clocked-out or nagged.
|
||||
if employee._fclk_is_attendance_exempt():
|
||||
continue
|
||||
clock_out_time = effective_deadline
|
||||
try:
|
||||
with self.env.cr.savepoint():
|
||||
@@ -456,6 +459,9 @@ class HrAttendance(models.Model):
|
||||
for emp in employees:
|
||||
try:
|
||||
with self.env.cr.savepoint():
|
||||
# Owners / attendance-exempt employees are never flagged absent.
|
||||
if emp._fclk_is_attendance_exempt():
|
||||
continue
|
||||
yesterday = get_local_today(self.env, emp) - timedelta(days=1)
|
||||
|
||||
# Only days the employee was actually scheduled to work
|
||||
@@ -498,7 +504,11 @@ class HrAttendance(models.Model):
|
||||
'source': 'system',
|
||||
})
|
||||
|
||||
emp.sudo().write({'x_fclk_pending_reason': True})
|
||||
# NOTE: an absence does NOT set x_fclk_pending_reason. That flag
|
||||
# drives the "explain your missed clock-OUT (departure time)"
|
||||
# dialog, which is meaningless for a day with no attendance and
|
||||
# caused a persistent false nag. The absence is logged + the
|
||||
# office is notified on excess; that is the absence remedy.
|
||||
|
||||
month_start = yesterday.replace(day=1)
|
||||
month_boundary_start, _ = get_local_day_boundaries(self.env, month_start, emp)
|
||||
@@ -546,6 +556,9 @@ class HrAttendance(models.Model):
|
||||
for emp in employees:
|
||||
try:
|
||||
with self.env.cr.savepoint():
|
||||
# Owners / attendance-exempt employees are never reminded.
|
||||
if emp._fclk_is_attendance_exempt():
|
||||
continue
|
||||
today = get_local_today(self.env, emp)
|
||||
if not emp._get_fclk_day_plan(today).get('scheduled'):
|
||||
continue
|
||||
@@ -610,6 +623,9 @@ class HrAttendance(models.Model):
|
||||
company_name = company.name or ''
|
||||
|
||||
for emp in employees:
|
||||
# Owners / attendance-exempt employees get no weekly summary.
|
||||
if emp._fclk_is_attendance_exempt():
|
||||
continue
|
||||
if not emp.work_email:
|
||||
continue
|
||||
|
||||
|
||||
@@ -40,6 +40,18 @@ class HrEmployee(models.Model):
|
||||
help="If set, employee must explain a missed clock-out before clocking in again.",
|
||||
)
|
||||
|
||||
# Attendance exemption (owners / anyone who works but is not "on the clock").
|
||||
# Exempt employees are skipped by absence detection, auto-clock-out and
|
||||
# reminders, and never see the missed-clock-out reason dialog.
|
||||
x_fclk_exempt_from_attendance = fields.Boolean(
|
||||
string='Exempt from Attendance Tracking',
|
||||
default=False,
|
||||
help="If set, this employee is never flagged absent, auto-clocked-out, "
|
||||
"reminded, or asked to explain a missed clock-out. Use for owners "
|
||||
"and others who work but are not on the clock. The Fusion Clock "
|
||||
"'Owner' role grants this automatically.",
|
||||
)
|
||||
|
||||
# Kiosk PIN
|
||||
x_fclk_kiosk_pin = fields.Char(
|
||||
string='Kiosk PIN',
|
||||
@@ -122,6 +134,19 @@ class HrEmployee(models.Model):
|
||||
help="Tracks the last date a reminder was sent to avoid duplicates.",
|
||||
)
|
||||
|
||||
def _fclk_is_attendance_exempt(self):
|
||||
"""True when this employee is exempt from attendance automation.
|
||||
|
||||
Exempt = the per-employee checkbox is set, OR the linked user holds the
|
||||
Fusion Clock 'Owner' role. Exempt employees are never flagged absent,
|
||||
auto-clocked-out, reminded, or shown the missed-clock-out reason dialog.
|
||||
"""
|
||||
self.ensure_one()
|
||||
if self.x_fclk_exempt_from_attendance:
|
||||
return True
|
||||
user = self.user_id
|
||||
return bool(user) and user.has_group('fusion_clock.group_fusion_clock_owner')
|
||||
|
||||
def _get_fclk_schedule_for_date(self, date):
|
||||
"""Return this employee's dated Fusion Clock schedule for a local date."""
|
||||
self.ensure_one()
|
||||
|
||||
@@ -49,6 +49,18 @@
|
||||
<field name="comment">Can manage locations, view all attendance, generate reports</field>
|
||||
</record>
|
||||
|
||||
<!-- Owner: top of the role ladder. Carries ALL Manager permissions but is
|
||||
exempt from attendance automation (no absence flags, no auto-clock-out
|
||||
nag, no reminders, no missed-clock-out dialog). For owners/principals
|
||||
who work but are not "on the clock". Implies Manager, so it renders as
|
||||
the highest role in the single Fusion Clock access dropdown. -->
|
||||
<record id="group_fusion_clock_owner" model="res.groups">
|
||||
<field name="name">Owner</field>
|
||||
<field name="privilege_id" ref="res_groups_privilege_fusion_clock"/>
|
||||
<field name="implied_ids" eval="[(4, ref('group_fusion_clock_manager'))]"/>
|
||||
<field name="comment">Full Clock management; exempt from attendance tracking, reminders and missed-clock alerts.</field>
|
||||
</record>
|
||||
|
||||
<!-- Dedicated kiosk-operator permission: can run the shared clock kiosk
|
||||
(NFC tap / PIN) WITHOUT full Clock Manager access. Gates the
|
||||
"Fusion Clock Kiosk" app menu and is accepted by the kiosk controllers.
|
||||
|
||||
@@ -71,7 +71,10 @@ export class FusionClockFAB extends Component {
|
||||
this.state.todayHours = (result.today_hours || 0).toFixed(1);
|
||||
this.state.weekHours = (result.week_hours || 0).toFixed(1);
|
||||
|
||||
if (result.pending_reason) {
|
||||
// Never raise the missed-clock-out dialog while the employee is
|
||||
// currently on the clock (the server already guards this, but keep
|
||||
// the UI honest too).
|
||||
if (result.pending_reason && !result.is_checked_in) {
|
||||
this.state.showReasonDialog = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,3 +10,4 @@ from . import test_pay_period
|
||||
from . import test_settings
|
||||
from . import test_clock_kiosk
|
||||
from . import test_break_rules
|
||||
from . import test_pending_reason_exempt
|
||||
|
||||
241
fusion_clock/tests/test_pending_reason_exempt.py
Normal file
241
fusion_clock/tests/test_pending_reason_exempt.py
Normal file
@@ -0,0 +1,241 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
"""Regression tests for the missed-clock-out ("pending reason") nag and the
|
||||
new owner/attendance-exemption.
|
||||
|
||||
Root cause these tests pin down:
|
||||
* The `x_fclk_pending_reason` flag was set by the absence + auto-clock-out
|
||||
crons but ONLY cleared by the systray reason dialog. The kiosk / NFC clock
|
||||
paths (how Entech actually clocks in) never cleared it, so a stale flag
|
||||
nagged employees forever -- even while currently clocked in.
|
||||
* Owners work but are not "on the clock"; they must be exempt from absence
|
||||
flagging, auto-clock-out nags and the reason dialog.
|
||||
"""
|
||||
|
||||
import json
|
||||
from datetime import date, timedelta
|
||||
|
||||
from odoo import fields
|
||||
from odoo.tests import tagged
|
||||
from odoo.tests.common import HttpCase, TransactionCase
|
||||
|
||||
try:
|
||||
from freezegun import freeze_time
|
||||
except ImportError: # freezegun may be absent on the runtime image
|
||||
freeze_time = None
|
||||
|
||||
MON = date(2026, 6, 1) # Monday
|
||||
TUE = date(2026, 6, 2) # Tuesday
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install', 'fusion_clock')
|
||||
class TestAttendanceExemptHelper(TransactionCase):
|
||||
"""`hr.employee._fclk_is_attendance_exempt()` truth table."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.Employee = cls.env['hr.employee']
|
||||
cls.owner_group = cls.env.ref('fusion_clock.group_fusion_clock_owner')
|
||||
|
||||
def test_plain_employee_not_exempt(self):
|
||||
emp = self.Employee.create({'name': 'Plain', 'x_fclk_enable_clock': True})
|
||||
self.assertFalse(emp._fclk_is_attendance_exempt())
|
||||
|
||||
def test_checkbox_makes_exempt(self):
|
||||
emp = self.Employee.create({
|
||||
'name': 'Flagged', 'x_fclk_enable_clock': True,
|
||||
'x_fclk_exempt_from_attendance': True,
|
||||
})
|
||||
self.assertTrue(emp._fclk_is_attendance_exempt())
|
||||
|
||||
def test_owner_group_makes_exempt(self):
|
||||
user = self.env['res.users'].create({
|
||||
'name': 'Olivia Owner', 'login': 'olivia-owner-test',
|
||||
'group_ids': [(4, self.owner_group.id)],
|
||||
})
|
||||
emp = self.Employee.create({
|
||||
'name': 'Olivia Owner', 'x_fclk_enable_clock': True, 'user_id': user.id,
|
||||
})
|
||||
self.assertTrue(emp._fclk_is_attendance_exempt())
|
||||
|
||||
def test_owner_group_implies_manager(self):
|
||||
"""The Owner role must carry full Manager permissions."""
|
||||
user = self.env['res.users'].create({
|
||||
'name': 'Manager-by-owner', 'login': 'owner-implies-mgr',
|
||||
'group_ids': [(4, self.owner_group.id)],
|
||||
})
|
||||
self.assertTrue(user.has_group('fusion_clock.group_fusion_clock_manager'))
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install', 'fusion_clock')
|
||||
class TestCronsRespectExemptAndPending(TransactionCase):
|
||||
"""Absence + auto-clock-out crons: no more pending nag, owners skipped."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.Employee = cls.env['hr.employee']
|
||||
cls.Schedule = cls.env['fusion.clock.schedule']
|
||||
cls.Attendance = cls.env['hr.attendance']
|
||||
cls.Log = cls.env['fusion.clock.activity.log']
|
||||
cls.ICP = cls.env['ir.config_parameter'].sudo()
|
||||
cls.ICP.set_param('fusion_clock.enable_auto_clockout', 'True')
|
||||
cls.ICP.set_param('fusion_clock.max_shift_hours', '16')
|
||||
|
||||
def _post(self, emp, day):
|
||||
return self.Schedule.create({
|
||||
'employee_id': emp.id, 'schedule_date': day, 'state': 'posted',
|
||||
'start_time': 9.0, 'end_time': 17.0, 'break_minutes': 30.0,
|
||||
})
|
||||
|
||||
def test_absence_does_not_set_pending_reason(self):
|
||||
if freeze_time is None:
|
||||
self.skipTest("freezegun not available")
|
||||
emp = self.Employee.create({'name': 'NoShow', 'x_fclk_enable_clock': True, 'tz': 'UTC'})
|
||||
self._post(emp, MON)
|
||||
with freeze_time("2026-06-02 09:00:00"): # yesterday = scheduled Monday
|
||||
self.Attendance._cron_fusion_check_absences()
|
||||
# Absence is still logged ...
|
||||
self.assertEqual(self.Log.search_count([
|
||||
('employee_id', '=', emp.id), ('log_type', '=', 'absent')]), 1)
|
||||
# ... but it must NOT raise the missed-clock-out reason nag.
|
||||
self.assertFalse(emp.x_fclk_pending_reason)
|
||||
|
||||
def test_absence_skips_exempt_employee(self):
|
||||
if freeze_time is None:
|
||||
self.skipTest("freezegun not available")
|
||||
emp = self.Employee.create({
|
||||
'name': 'OwnerNoShow', 'x_fclk_enable_clock': True, 'tz': 'UTC',
|
||||
'x_fclk_exempt_from_attendance': True,
|
||||
})
|
||||
self._post(emp, MON)
|
||||
with freeze_time("2026-06-02 09:00:00"):
|
||||
self.Attendance._cron_fusion_check_absences()
|
||||
self.assertEqual(self.Log.search_count([
|
||||
('employee_id', '=', emp.id), ('log_type', '=', 'absent')]), 0)
|
||||
self.assertFalse(emp.x_fclk_pending_reason)
|
||||
|
||||
def test_auto_clockout_skips_exempt_employee(self):
|
||||
emp = self.Employee.create({
|
||||
'name': 'OwnerStale', 'x_fclk_enable_clock': True, 'tz': 'UTC',
|
||||
'x_fclk_exempt_from_attendance': True,
|
||||
})
|
||||
now = fields.Datetime.now()
|
||||
stale = self.Attendance.create({
|
||||
'employee_id': emp.id, 'check_in': now - timedelta(hours=20),
|
||||
})
|
||||
self.Attendance._cron_fusion_auto_clock_out()
|
||||
self.assertFalse(stale.check_out, "Exempt employee must not be auto-clocked-out.")
|
||||
self.assertFalse(emp.x_fclk_pending_reason)
|
||||
|
||||
def test_auto_clockout_still_flags_normal_employee(self):
|
||||
emp = self.Employee.create({'name': 'Forgetful', 'x_fclk_enable_clock': True, 'tz': 'UTC'})
|
||||
now = fields.Datetime.now()
|
||||
stale = self.Attendance.create({
|
||||
'employee_id': emp.id, 'check_in': now - timedelta(hours=20),
|
||||
})
|
||||
self.Attendance._cron_fusion_auto_clock_out()
|
||||
self.assertTrue(stale.check_out, "Over-cap shift must be auto-closed.")
|
||||
self.assertTrue(emp.x_fclk_pending_reason, "Forgotten clock-out still asks for a reason.")
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install', 'fusion_clock')
|
||||
class TestKioskClearsPendingReason(HttpCase):
|
||||
"""Clocking in via either kiosk clears a stale pending-reason flag."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.ICP = cls.env['ir.config_parameter'].sudo()
|
||||
cls.ICP.set_param('fusion_clock.enable_kiosk', 'True')
|
||||
cls.ICP.set_param('fusion_clock.enable_nfc_kiosk', 'True')
|
||||
cls.ICP.set_param('fusion_clock.nfc_photo_required', 'False')
|
||||
cls.location = cls.env['fusion.clock.location'].create({
|
||||
'name': 'Clear Plant', 'latitude': 43.65, 'longitude': -79.38, 'radius': 100,
|
||||
})
|
||||
cls.env.company.x_fclk_nfc_kiosk_location_id = cls.location.id
|
||||
cls.env['res.users'].create({
|
||||
'name': 'Clear Op', 'login': 'clear-op', 'password': 'kioskpass123',
|
||||
'group_ids': [(4, cls.env.ref('fusion_clock.group_fusion_clock_manager').id)],
|
||||
})
|
||||
cls.pin_emp = cls.env['hr.employee'].create({
|
||||
'name': 'Pat Pending', 'x_fclk_enable_clock': True, 'x_fclk_kiosk_pin': '1234',
|
||||
'x_fclk_pending_reason': True,
|
||||
})
|
||||
cls.nfc_emp = cls.env['hr.employee'].create({
|
||||
'name': 'Nina Pending', 'x_fclk_enable_clock': True,
|
||||
'x_fclk_nfc_card_uid': '04:A2:B5:62:CC:01', 'x_fclk_pending_reason': True,
|
||||
})
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
from odoo.addons.fusion_clock.controllers import clock_nfc_kiosk as nfc_mod
|
||||
nfc_mod._recent_taps.clear()
|
||||
|
||||
def _post(self, route, params):
|
||||
self.authenticate('clear-op', 'kioskpass123')
|
||||
resp = self.url_open(route, data=json.dumps({
|
||||
'jsonrpc': '2.0', 'method': 'call', 'params': params,
|
||||
}), headers={'Content-Type': 'application/json'})
|
||||
return resp.json().get('result', {})
|
||||
|
||||
def test_pin_kiosk_clock_in_clears_pending(self):
|
||||
res = self._post('/fusion_clock/kiosk/clock', {'employee_id': self.pin_emp.id})
|
||||
self.assertEqual(res.get('action'), 'clock_in')
|
||||
self.pin_emp.invalidate_recordset()
|
||||
self.assertFalse(self.pin_emp.x_fclk_pending_reason)
|
||||
|
||||
def test_nfc_tap_clock_in_clears_pending(self):
|
||||
res = self._post('/fusion_clock/kiosk/nfc/tap', {'card_uid': '04:A2:B5:62:CC:01'})
|
||||
self.assertEqual(res.get('action'), 'clock_in')
|
||||
self.nfc_emp.invalidate_recordset()
|
||||
self.assertFalse(self.nfc_emp.x_fclk_pending_reason)
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install', 'fusion_clock')
|
||||
class TestGetStatusPendingReason(HttpCase):
|
||||
"""get_status must never raise the dialog for a clocked-in or exempt user."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.user = cls.env['res.users'].create({
|
||||
'name': 'Status User', 'login': 'status-user', 'password': 'statuspass123',
|
||||
'group_ids': [(4, cls.env.ref('fusion_clock.group_fusion_clock_user').id)],
|
||||
})
|
||||
cls.emp = cls.env['hr.employee'].create({
|
||||
'name': 'Status User', 'x_fclk_enable_clock': True, 'tz': 'UTC',
|
||||
'user_id': cls.user.id, 'x_fclk_pending_reason': True,
|
||||
})
|
||||
|
||||
def _status(self):
|
||||
self.authenticate('status-user', 'statuspass123')
|
||||
resp = self.url_open('/fusion_clock/get_status', data=json.dumps({
|
||||
'jsonrpc': '2.0', 'method': 'call', 'params': {},
|
||||
}), headers={'Content-Type': 'application/json'})
|
||||
return resp.json().get('result', {})
|
||||
|
||||
def test_pending_hidden_while_checked_in(self):
|
||||
self.env['hr.attendance'].create({
|
||||
'employee_id': self.emp.id, 'check_in': fields.Datetime.now() - timedelta(hours=1),
|
||||
})
|
||||
self.emp.invalidate_recordset()
|
||||
res = self._status()
|
||||
self.assertTrue(res.get('is_checked_in'))
|
||||
self.assertFalse(res.get('pending_reason'),
|
||||
"A currently clocked-in employee must never be nagged.")
|
||||
|
||||
def test_pending_hidden_for_exempt(self):
|
||||
self.emp.write({'x_fclk_exempt_from_attendance': True})
|
||||
res = self._status()
|
||||
self.assertFalse(res.get('is_checked_in'))
|
||||
self.assertFalse(res.get('pending_reason'),
|
||||
"An exempt (owner) employee must never be nagged.")
|
||||
|
||||
def test_pending_shown_for_normal_not_checked_in(self):
|
||||
"""Sanity: the dialog still works for a genuine forgotten clock-out."""
|
||||
res = self._status()
|
||||
self.assertFalse(res.get('is_checked_in'))
|
||||
self.assertTrue(res.get('pending_reason'))
|
||||
@@ -15,6 +15,7 @@
|
||||
<group>
|
||||
<group string="Configuration">
|
||||
<field name="x_fclk_enable_clock"/>
|
||||
<field name="x_fclk_exempt_from_attendance"/>
|
||||
<field name="x_fclk_shift_id"/>
|
||||
<field name="x_fclk_default_location_id"/>
|
||||
<field name="x_fclk_break_minutes"/>
|
||||
|
||||
Reference in New Issue
Block a user