feat(fusion_clock): bi-weekly attendance filter — pay-period filters + picker
Reuse the existing Pay Period setting (Frequency + Anchor) as the single source of truth via a shared pure helper (models/pay_period.py); fusion.clock.report delegates to it. Add Current/Previous/Next Pay Period filters to the attendance search view (search-method computed booleans on hr.attendance), a Bi-Weekly Period picker wizard (pick start -> auto +2 weeks, editable; Apply opens the filtered list) reachable from an Attendance menu item and a dashboard tile. Window follows the configured frequency; TZ-correct via local-day boundaries. Bump 3.14.4 -> 3.15.0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -6,3 +6,4 @@ from . import test_shift_planner
|
||||
from . import test_photo_retention
|
||||
from . import test_schedule_driven
|
||||
from . import test_dashboard
|
||||
from . import test_pay_period
|
||||
|
||||
133
fusion_clock/tests/test_pay_period.py
Normal file
133
fusion_clock/tests/test_pay_period.py
Normal file
@@ -0,0 +1,133 @@
|
||||
# -*- 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_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))
|
||||
Reference in New Issue
Block a user