255 lines
9.8 KiB
Python
255 lines
9.8 KiB
Python
# -*- 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)
|