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:
gsinghpal
2026-05-31 11:20:06 -04:00
parent e230e42d81
commit 3f78f652e7
15 changed files with 376 additions and 48 deletions

View File

@@ -3,3 +3,4 @@
# License OPL-1 (Odoo Proprietary License v1.0)
from . import clock_nfc_enrollment_wizard
from . import clock_period_picker_wizard

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_fusion_clock_period_picker_form" model="ir.ui.view">
<field name="name">fusion.clock.period.picker.form</field>
<field name="model">fusion.clock.period.picker</field>
<field name="arch" type="xml">
<form string="Bi-Weekly Period">
<sheet>
<div class="alert alert-info" role="alert">
Pick the period start — the end auto-fills to one pay period later
(two weeks by default). Adjust either date, then click
<b>View Attendances</b>.
</div>
<group>
<field name="date_start"/>
<field name="date_end"/>
</group>
</sheet>
<footer>
<button name="action_apply" string="View Attendances" type="object" class="btn-primary"/>
<button special="cancel" string="Cancel" class="btn-secondary"/>
</footer>
</form>
</field>
</record>
<record id="action_fusion_clock_period_picker" model="ir.actions.act_window">
<field name="name">Bi-Weekly Period</field>
<field name="res_model">fusion.clock.period.picker</field>
<field name="view_mode">form</field>
<field name="view_id" ref="view_fusion_clock_period_picker_form"/>
<field name="target">new</field>
</record>
</odoo>

View File

@@ -0,0 +1,66 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
from datetime import timedelta
from odoo import models, fields, api, _
from odoo.exceptions import ValidationError
from ..models.pay_period import compute_pay_period, period_length_days, current_prev_next
from ..models.tz_utils import get_local_today, get_local_day_boundaries
class FusionClockPeriodPicker(models.TransientModel):
"""Pick a pay-period window and open the attendance list filtered to it.
Defaults to the current pay period. Changing the start auto-fills the end
to one pay period later (two weeks by default); the end stays editable so a
fully custom range can be entered too.
"""
_name = 'fusion.clock.period.picker'
_description = 'Bi-Weekly Period Picker'
date_start = fields.Date(string='Period Start', required=True,
default=lambda self: self._fclk_default_window()[0])
date_end = fields.Date(string='Period End', required=True,
default=lambda self: self._fclk_default_window()[1])
def _fclk_config(self):
ICP = self.env['ir.config_parameter'].sudo()
return (ICP.get_param('fusion_clock.pay_period_type', 'biweekly'),
ICP.get_param('fusion_clock.pay_period_start', ''))
def _fclk_default_window(self):
frequency, anchor = self._fclk_config()
return current_prev_next(frequency, anchor, get_local_today(self.env))['current']
@api.onchange('date_start')
def _onchange_date_start(self):
if not self.date_start:
return
frequency, anchor = self._fclk_config()
length = period_length_days(frequency)
if length:
self.date_end = self.date_start + timedelta(days=length - 1)
else:
self.date_end = compute_pay_period(frequency, anchor, self.date_start)[1]
@api.constrains('date_start', 'date_end')
def _check_dates(self):
for rec in self:
if rec.date_start and rec.date_end and rec.date_end < rec.date_start:
raise ValidationError(_("Period end cannot be before period start."))
def action_apply(self):
self.ensure_one()
start_utc, _dummy = get_local_day_boundaries(self.env, self.date_start)
_dummy2, end_excl_utc = get_local_day_boundaries(self.env, self.date_end)
return {
'type': 'ir.actions.act_window',
'name': _("Attendances · %s %s") % (self.date_start, self.date_end),
'res_model': 'hr.attendance',
'view_mode': 'list,form',
'domain': ['&',
('check_in', '>=', fields.Datetime.to_string(start_utc)),
('check_in', '<', fields.Datetime.to_string(end_excl_utc))],
'target': 'current',
}