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>
66 lines
2.8 KiB
Python
66 lines
2.8 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Copyright 2026 Nexa Systems Inc.
|
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
|
"""Pay-period date math shared by reports, attendance filters and the period
|
|
picker. Pure functions (no ORM) so they unit-test trivially and never drift
|
|
between callers."""
|
|
|
|
from datetime import date, timedelta
|
|
|
|
|
|
def period_length_days(frequency):
|
|
"""Fixed window length for grid frequencies; None for calendar-based ones."""
|
|
return {'weekly': 7, 'biweekly': 14}.get(frequency)
|
|
|
|
|
|
def compute_pay_period(frequency, anchor_str, reference_date):
|
|
"""Return (start_date, end_date) for the period containing reference_date.
|
|
|
|
``anchor_str`` is a 'YYYY-MM-DD' string or falsy (falls back to
|
|
first-of-month). Mirrors the original
|
|
fusion.clock.report._calculate_current_period logic, including floor
|
|
division so dates before the anchor resolve to the correct earlier period.
|
|
"""
|
|
if anchor_str:
|
|
try:
|
|
# Truncate to 'YYYY-MM-DD' first, matching Odoo's fields.Date.from_string
|
|
# (Date.to_date), so a stored datetime-ish anchor like
|
|
# "2026-05-04 00:00:00" still parses instead of silently falling back.
|
|
anchor = date.fromisoformat(anchor_str[:10])
|
|
except (ValueError, TypeError):
|
|
anchor = reference_date.replace(day=1)
|
|
else:
|
|
anchor = reference_date.replace(day=1)
|
|
|
|
if frequency == 'weekly':
|
|
period_num = (reference_date - anchor).days // 7
|
|
start = anchor + timedelta(days=period_num * 7)
|
|
end = start + timedelta(days=6)
|
|
elif frequency == 'semi_monthly':
|
|
if reference_date.day <= 15:
|
|
start = reference_date.replace(day=1)
|
|
end = reference_date.replace(day=15)
|
|
else:
|
|
start = reference_date.replace(day=16)
|
|
next_month = reference_date.replace(day=28) + timedelta(days=4)
|
|
end = next_month - timedelta(days=next_month.day)
|
|
elif frequency == 'monthly':
|
|
start = reference_date.replace(day=1)
|
|
next_month = reference_date.replace(day=28) + timedelta(days=4)
|
|
end = next_month - timedelta(days=next_month.day)
|
|
else: # 'biweekly' and default
|
|
period_num = (reference_date - anchor).days // 14
|
|
start = anchor + timedelta(days=period_num * 14)
|
|
end = start + timedelta(days=13)
|
|
return start, end
|
|
|
|
|
|
def current_prev_next(frequency, anchor_str, today):
|
|
"""Return {'current','previous','next'} (start,end) windows. Previous/next
|
|
are derived by stepping the reference date one day outside the current
|
|
window, which works for grid AND calendar frequencies."""
|
|
cur = compute_pay_period(frequency, anchor_str, today)
|
|
prev = compute_pay_period(frequency, anchor_str, cur[0] - timedelta(days=1))
|
|
nxt = compute_pay_period(frequency, anchor_str, cur[1] + timedelta(days=1))
|
|
return {'current': cur, 'previous': prev, 'next': nxt}
|