# -*- coding: utf-8 -*- from datetime import date, timedelta from odoo import fields from odoo.tests import tagged, TransactionCase from odoo.addons.fusion_clock.models.pay_period import ( compute_pay_period, period_length_days, current_prev_next, ) from odoo.addons.fusion_clock.models.tz_utils import get_local_today @tagged('-at_install', 'post_install', 'fusion_clock') class TestPayPeriodMath(TransactionCase): def test_biweekly_window_is_14_days(self): # anchor Mon 2026-05-04; a date inside the 2nd period s, e = compute_pay_period('biweekly', '2026-05-04', date(2026, 5, 20)) self.assertEqual(s, date(2026, 5, 18)) self.assertEqual(e, date(2026, 5, 31)) self.assertEqual((e - s).days, 13) def test_weekly_window_is_7_days(self): s, e = compute_pay_period('weekly', '2026-05-04', date(2026, 5, 20)) self.assertEqual(s, date(2026, 5, 18)) self.assertEqual(e, date(2026, 5, 24)) def test_datetime_string_anchor_still_parses(self): # Anchor is a free-text Char; a stored "YYYY-MM-DD hh:mm:ss" must resolve # the same as the bare date (parity with Odoo's fields.Date.from_string). s, e = compute_pay_period('biweekly', '2026-05-04 00:00:00', date(2026, 5, 20)) self.assertEqual((s, e), (date(2026, 5, 18), date(2026, 5, 31))) def test_reference_before_anchor(self): # 2026-04-25 is one biweekly period BEFORE the anchor s, e = compute_pay_period('biweekly', '2026-05-04', date(2026, 4, 25)) self.assertEqual(s, date(2026, 4, 20)) self.assertEqual(e, date(2026, 5, 3)) def test_monthly_window(self): s, e = compute_pay_period('monthly', '', date(2026, 2, 10)) self.assertEqual(s, date(2026, 2, 1)) self.assertEqual(e, date(2026, 2, 28)) def test_semi_monthly_window(self): s1, e1 = compute_pay_period('semi_monthly', '', date(2026, 3, 10)) self.assertEqual((s1, e1), (date(2026, 3, 1), date(2026, 3, 15))) s2, e2 = compute_pay_period('semi_monthly', '', date(2026, 3, 20)) self.assertEqual((s2, e2), (date(2026, 3, 16), date(2026, 3, 31))) def test_period_length_days(self): self.assertEqual(period_length_days('weekly'), 7) self.assertEqual(period_length_days('biweekly'), 14) self.assertIsNone(period_length_days('monthly')) def test_current_prev_next_are_contiguous(self): w = current_prev_next('biweekly', '2026-05-04', date(2026, 5, 20)) self.assertEqual(w['current'], (date(2026, 5, 18), date(2026, 5, 31))) self.assertEqual(w['previous'][1], w['current'][0] - timedelta(days=1)) self.assertEqual(w['next'][0], w['current'][1] + timedelta(days=1)) @tagged('-at_install', 'post_install', 'fusion_clock') class TestPayPeriodFilters(TransactionCase): def setUp(self): super().setUp() self.ICP = self.env['ir.config_parameter'].sudo() self.today = get_local_today(self.env) # Make TODAY the first day of the current bi-weekly window. self.ICP.set_param('fusion_clock.pay_period_type', 'biweekly') self.ICP.set_param('fusion_clock.pay_period_start', str(self.today)) self.emp = self.env['hr.employee'].create({'name': 'Filter Fred'}) Att = self.env['hr.attendance'] # current period: today .. today+13 -> attendance "now" self.att_current = Att.create({ 'employee_id': self.emp.id, 'check_in': fields.Datetime.now(), 'check_out': fields.Datetime.now(), }) # previous period: today-14 .. today-1 -> attendance 8 days ago eight_ago = fields.Datetime.now() - timedelta(days=8) self.att_prev = Att.create({ 'employee_id': self.emp.id, 'check_in': eight_ago, 'check_out': eight_ago, }) def test_resolve_tz_company_fallback_when_user_has_no_tz(self): # Regression: res.company has no `tz` field in Odoo 19; the resolver must # fall back to env.company.partner_id.tz, not env.company.tz, or a # tz-less user crashes the period filters with AttributeError. self.env.user.tz = False self.env.company.partner_id.tz = 'America/Toronto' res = self.env['hr.attendance'].search([ ('employee_id', '=', self.emp.id), ('x_fclk_in_current_period', '=', True), ]) self.assertIn(self.att_current, res) def test_current_filter_returns_only_current(self): res = self.env['hr.attendance'].search([ ('employee_id', '=', self.emp.id), ('x_fclk_in_current_period', '=', True), ]) self.assertIn(self.att_current, res) self.assertNotIn(self.att_prev, res) def test_previous_filter_returns_only_previous(self): res = self.env['hr.attendance'].search([ ('employee_id', '=', self.emp.id), ('x_fclk_in_previous_period', '=', True), ]) self.assertIn(self.att_prev, res) self.assertNotIn(self.att_current, res) @tagged('-at_install', 'post_install', 'fusion_clock') class TestPeriodPickerWizard(TransactionCase): def setUp(self): super().setUp() self.ICP = self.env['ir.config_parameter'].sudo() self.ICP.set_param('fusion_clock.pay_period_type', 'biweekly') self.ICP.set_param('fusion_clock.pay_period_start', '2026-05-04') self.today = get_local_today(self.env) def test_default_start_is_current_period_start(self): wiz = self.env['fusion.clock.period.picker'].create({}) expected_start = current_prev_next('biweekly', '2026-05-04', self.today)['current'][0] self.assertEqual(wiz.date_start, expected_start) def test_onchange_autofills_two_weeks(self): wiz = self.env['fusion.clock.period.picker'].new({'date_start': date(2026, 6, 1)}) wiz._onchange_date_start() self.assertEqual(wiz.date_end, date(2026, 6, 1) + timedelta(days=13)) def test_action_apply_returns_attendance_domain(self): wiz = self.env['fusion.clock.period.picker'].create({ 'date_start': date(2026, 6, 1), 'date_end': date(2026, 6, 14), }) act = wiz.action_apply() self.assertEqual(act['res_model'], 'hr.attendance') self.assertEqual(act['view_mode'], 'list,form') leaves = [leaf for leaf in act['domain'] if isinstance(leaf, tuple)] self.assertTrue(any(leaf[0] == 'check_in' and leaf[1] == '>=' for leaf in leaves)) self.assertTrue(any(leaf[0] == 'check_in' and leaf[1] == '<' for leaf in leaves))