changes
This commit is contained in:
@@ -2,3 +2,4 @@
|
||||
|
||||
from . import test_nfc_models
|
||||
from . import test_clock_nfc_kiosk
|
||||
from . import test_shift_planner
|
||||
|
||||
254
fusion_clock/tests/test_shift_planner.py
Normal file
254
fusion_clock/tests/test_shift_planner.py
Normal file
@@ -0,0 +1,254 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import json
|
||||
from datetime import date, timedelta
|
||||
|
||||
from psycopg2 import IntegrityError
|
||||
|
||||
from odoo import fields
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tests.common import HttpCase, TransactionCase, tagged
|
||||
from odoo.tools.misc import mute_logger
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install', 'fusion_clock')
|
||||
class TestShiftPlannerModels(TransactionCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.Schedule = cls.env['fusion.clock.schedule'].sudo()
|
||||
cls.Shift = cls.env['fusion.clock.shift'].sudo()
|
||||
cls.employee = cls.env['hr.employee'].sudo().create({
|
||||
'name': 'Planner Model Employee',
|
||||
'company_id': cls.env.company.id,
|
||||
'x_fclk_enable_clock': True,
|
||||
})
|
||||
cls.default_shift = cls.Shift.create({
|
||||
'name': 'Default Planner Shift',
|
||||
'start_time': 8.0,
|
||||
'end_time': 16.5,
|
||||
'break_minutes': 30,
|
||||
'company_id': cls.env.company.id,
|
||||
})
|
||||
cls.employee.x_fclk_shift_id = cls.default_shift.id
|
||||
cls.schedule_date = date(2026, 1, 5)
|
||||
|
||||
def test_unique_employee_date_schedule(self):
|
||||
self.Schedule.create({
|
||||
'employee_id': self.employee.id,
|
||||
'schedule_date': self.schedule_date,
|
||||
'is_off': True,
|
||||
})
|
||||
with self.assertRaises(IntegrityError), mute_logger('odoo.sql_db'):
|
||||
with self.env.cr.savepoint():
|
||||
self.Schedule.create({
|
||||
'employee_id': self.employee.id,
|
||||
'schedule_date': self.schedule_date,
|
||||
'is_off': True,
|
||||
})
|
||||
|
||||
def test_off_schedule_has_zero_hours(self):
|
||||
schedule = self.Schedule.create({
|
||||
'employee_id': self.employee.id,
|
||||
'schedule_date': date(2026, 1, 6),
|
||||
'is_off': True,
|
||||
})
|
||||
self.assertEqual(schedule.planned_hours, 0)
|
||||
self.assertEqual(schedule.fclk_display_value(), 'OFF')
|
||||
|
||||
def test_working_schedule_computes_hours_minus_break(self):
|
||||
schedule = self.Schedule.create({
|
||||
'employee_id': self.employee.id,
|
||||
'schedule_date': date(2026, 1, 7),
|
||||
'start_time': 9.0,
|
||||
'end_time': 17.5,
|
||||
'break_minutes': 30,
|
||||
})
|
||||
self.assertEqual(schedule.planned_hours, 8.0)
|
||||
self.assertEqual(self.Schedule.fclk_hours_display(schedule.planned_hours), '8:00')
|
||||
|
||||
def test_invalid_same_day_range_is_rejected(self):
|
||||
with self.assertRaises(ValidationError):
|
||||
self.Schedule.create({
|
||||
'employee_id': self.employee.id,
|
||||
'schedule_date': date(2026, 1, 8),
|
||||
'start_time': 17.0,
|
||||
'end_time': 9.0,
|
||||
'break_minutes': 30,
|
||||
})
|
||||
|
||||
def test_apply_planner_cell_creates_audit(self):
|
||||
schedule_date = date(2026, 1, 9)
|
||||
self.Schedule.fclk_apply_planner_cell(
|
||||
self.employee,
|
||||
schedule_date,
|
||||
{'input': '9:00 am - 5:30 pm'},
|
||||
self.env.user,
|
||||
)
|
||||
audit = self.env['fusion.clock.schedule.audit'].sudo().search([
|
||||
('employee_id', '=', self.employee.id),
|
||||
('schedule_date', '=', schedule_date),
|
||||
], limit=1)
|
||||
self.assertTrue(audit)
|
||||
self.assertFalse(audit.old_value)
|
||||
self.assertEqual(audit.new_value, '9:00 am - 5:30 pm')
|
||||
|
||||
def test_dated_schedule_overrides_employee_shift_and_fallback_remains(self):
|
||||
planned_date = date(2026, 1, 12)
|
||||
self.Schedule.create({
|
||||
'employee_id': self.employee.id,
|
||||
'schedule_date': planned_date,
|
||||
'start_time': 10.0,
|
||||
'end_time': 18.0,
|
||||
'break_minutes': 60,
|
||||
})
|
||||
|
||||
planned = self.employee._get_fclk_day_plan(planned_date)
|
||||
fallback = self.employee._get_fclk_day_plan(planned_date + timedelta(days=1))
|
||||
|
||||
self.assertEqual(planned['source'], 'schedule')
|
||||
self.assertEqual(planned['start_time'], 10.0)
|
||||
self.assertEqual(planned['hours'], 7.0)
|
||||
self.assertEqual(fallback['source'], 'fallback')
|
||||
self.assertEqual(fallback['start_time'], 8.0)
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install', 'fusion_clock')
|
||||
class TestShiftPlannerApi(HttpCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
manager_group = cls.env.ref('fusion_clock.group_fusion_clock_manager')
|
||||
user_group = cls.env.ref('fusion_clock.group_fusion_clock_user')
|
||||
cls.manager_user = cls.env['res.users'].sudo().create({
|
||||
'name': 'Planner Manager',
|
||||
'login': 'planner-manager',
|
||||
'password': 'plannerpass',
|
||||
'company_id': cls.env.company.id,
|
||||
'company_ids': [(6, 0, [cls.env.company.id])],
|
||||
'group_ids': [(6, 0, [manager_group.id])],
|
||||
})
|
||||
cls.employee_user = cls.env['res.users'].sudo().create({
|
||||
'name': 'Planner Employee User',
|
||||
'login': 'planner-employee-user',
|
||||
'password': 'plannerpass',
|
||||
'company_id': cls.env.company.id,
|
||||
'company_ids': [(6, 0, [cls.env.company.id])],
|
||||
'group_ids': [(6, 0, [user_group.id])],
|
||||
'tz': 'UTC',
|
||||
})
|
||||
cls.employee = cls.env['hr.employee'].sudo().create({
|
||||
'name': 'Planner API Employee',
|
||||
'user_id': cls.employee_user.id,
|
||||
'company_id': cls.env.company.id,
|
||||
'x_fclk_enable_clock': True,
|
||||
})
|
||||
cls.shift = cls.env['fusion.clock.shift'].sudo().create({
|
||||
'name': 'API Morning',
|
||||
'start_time': 7.0,
|
||||
'end_time': 15.5,
|
||||
'break_minutes': 30,
|
||||
'company_id': cls.env.company.id,
|
||||
})
|
||||
cls.week_start = '2026-01-19'
|
||||
|
||||
def _json_call(self, route, payload, login='planner-manager'):
|
||||
self.authenticate(login, 'plannerpass')
|
||||
response = self.url_open(
|
||||
route,
|
||||
data=json.dumps({'jsonrpc': '2.0', 'method': 'call', 'params': payload}),
|
||||
headers={'Content-Type': 'application/json'},
|
||||
)
|
||||
return response.json().get('result', {})
|
||||
|
||||
def test_manager_can_load_save_and_export_planner(self):
|
||||
load_result = self._json_call('/fusion_clock/shift_planner/load', {
|
||||
'week_start': self.week_start,
|
||||
})
|
||||
self.assertIn(self.employee.id, [row['id'] for row in load_result['employees']])
|
||||
|
||||
save_result = self._json_call('/fusion_clock/shift_planner/save', {
|
||||
'week_start': self.week_start,
|
||||
'changes': [{
|
||||
'employee_id': self.employee.id,
|
||||
'date': self.week_start,
|
||||
'input': '9-5',
|
||||
'shift_id': False,
|
||||
}],
|
||||
})
|
||||
self.assertTrue(save_result.get('success'))
|
||||
|
||||
schedule = self.env['fusion.clock.schedule'].sudo().search([
|
||||
('employee_id', '=', self.employee.id),
|
||||
('schedule_date', '=', fields.Date.to_date(self.week_start)),
|
||||
], limit=1)
|
||||
self.assertTrue(schedule)
|
||||
self.assertEqual(schedule.start_time, 9.0)
|
||||
self.assertEqual(schedule.end_time, 17.0)
|
||||
|
||||
export_result = self._json_call('/fusion_clock/shift_planner/export_xlsx', {
|
||||
'week_start': self.week_start,
|
||||
})
|
||||
self.assertTrue(export_result.get('success'))
|
||||
self.assertTrue(export_result.get('url', '').startswith('/web/content/'))
|
||||
self.assertTrue(self.env['ir.attachment'].sudo().browse(export_result['attachment_id']).exists())
|
||||
|
||||
def test_copy_previous_week(self):
|
||||
previous_monday = fields.Date.to_date(self.week_start) - timedelta(days=7)
|
||||
self.env['fusion.clock.schedule'].sudo().create({
|
||||
'employee_id': self.employee.id,
|
||||
'schedule_date': previous_monday,
|
||||
'shift_id': self.shift.id,
|
||||
'start_time': self.shift.start_time,
|
||||
'end_time': self.shift.end_time,
|
||||
'break_minutes': self.shift.break_minutes,
|
||||
})
|
||||
result = self._json_call('/fusion_clock/shift_planner/copy_previous_week', {
|
||||
'week_start': self.week_start,
|
||||
})
|
||||
self.assertTrue(result.get('success'))
|
||||
copied = self.env['fusion.clock.schedule'].sudo().search([
|
||||
('employee_id', '=', self.employee.id),
|
||||
('schedule_date', '=', fields.Date.to_date(self.week_start)),
|
||||
], limit=1)
|
||||
self.assertEqual(copied.shift_id, self.shift)
|
||||
|
||||
def test_non_manager_cannot_mutate_planner(self):
|
||||
result = self._json_call('/fusion_clock/shift_planner/save', {
|
||||
'week_start': self.week_start,
|
||||
'changes': [],
|
||||
}, login='planner-employee-user')
|
||||
self.assertEqual(result.get('error'), 'Access denied.')
|
||||
|
||||
def test_off_day_clock_in_succeeds_and_logs_unscheduled_shift(self):
|
||||
today = fields.Date.today()
|
||||
location = self.env['fusion.clock.location'].sudo().create({
|
||||
'name': 'Planner Test Location',
|
||||
'latitude': 43.65,
|
||||
'longitude': -79.38,
|
||||
'radius': 100,
|
||||
'company_id': self.env.company.id,
|
||||
'all_employees': True,
|
||||
})
|
||||
self.env['fusion.clock.schedule'].sudo().create({
|
||||
'employee_id': self.employee.id,
|
||||
'schedule_date': today,
|
||||
'is_off': True,
|
||||
})
|
||||
|
||||
result = self._json_call('/fusion_clock/clock_action', {
|
||||
'latitude': location.latitude,
|
||||
'longitude': location.longitude,
|
||||
'source': 'portal',
|
||||
}, login='planner-employee-user')
|
||||
|
||||
self.assertTrue(result.get('success'))
|
||||
self.assertEqual(result.get('action'), 'clock_in')
|
||||
self.assertIn('unscheduled', result.get('message', ''))
|
||||
log = self.env['fusion.clock.activity.log'].sudo().search([
|
||||
('employee_id', '=', self.employee.id),
|
||||
('log_type', '=', 'unscheduled_shift'),
|
||||
], limit=1)
|
||||
self.assertTrue(log)
|
||||
Reference in New Issue
Block a user