From 6a9c7c74eabc161b4b22b42eaaca906fb722e1ea Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Sat, 30 May 2026 23:01:19 -0400 Subject: [PATCH] feat(fusion_clock): multi-day leave requests (date range) Request Leave now takes a From/To date range instead of a single day (the To field is optional -> single-day). Added date_to to fusion.clock.leave.request (start kept as leave_date), with overlap detection on submit and a date_to >= leave_date constraint. The absence check and reports now treat a leave as covering its whole span. The form shows two date inputs; the controller accepts date_from/date_to (the old single leave_date payload is still honoured). A migration backfills date_to = leave_date for existing rows. Live and verified on entech 19.0.3.13.0. Co-Authored-By: Claude Opus 4.8 --- fusion_clock/__manifest__.py | 2 +- fusion_clock/controllers/clock_api.py | 30 ++++++++++----- .../migrations/19.0.3.13.0/post-migrate.py | 17 +++++++++ fusion_clock/models/clock_leave_request.py | 38 +++++++++++++++---- fusion_clock/models/clock_report.py | 3 +- fusion_clock/models/hr_attendance.py | 3 +- fusion_clock/static/src/css/portal_clock.css | 27 +++++++++++++ .../static/src/js/fusion_clock_portal.js | 20 +++++++--- .../views/clock_leave_request_views.xml | 2 + fusion_clock/views/portal_clock_templates.xml | 13 ++++++- 10 files changed, 128 insertions(+), 27 deletions(-) create mode 100644 fusion_clock/migrations/19.0.3.13.0/post-migrate.py diff --git a/fusion_clock/__manifest__.py b/fusion_clock/__manifest__.py index 249318a5..4a8c2bb6 100644 --- a/fusion_clock/__manifest__.py +++ b/fusion_clock/__manifest__.py @@ -5,7 +5,7 @@ { 'name': 'Fusion Clock', - 'version': '19.0.3.12.4', + 'version': '19.0.3.13.0', 'category': 'Human Resources/Attendances', 'summary': 'Complete Employee T&A with Geofencing, Shifts, Penalties, Overtime, Kiosk, Dashboard & Payroll Export', 'description': """ diff --git a/fusion_clock/controllers/clock_api.py b/fusion_clock/controllers/clock_api.py index 244af06a..1641bd47 100644 --- a/fusion_clock/controllers/clock_api.py +++ b/fusion_clock/controllers/clock_api.py @@ -482,35 +482,47 @@ class FusionClockAPI(http.Controller): return {'success': True, 'message': 'Reason submitted. You may now clock in.'} @http.route('/fusion_clock/request_leave', type='jsonrpc', auth='user', methods=['POST']) - def request_leave(self, leave_date='', reason='', **kw): - """Submit a leave request from the portal.""" + def request_leave(self, date_from='', date_to='', reason='', leave_date='', **kw): + """Submit a (possibly multi-day) leave request from the portal.""" employee = self._get_employee() if not employee: return {'error': 'No employee record found for current user.'} - if not leave_date or not reason: - return {'error': 'Please provide both a date and a reason.'} + date_from = date_from or leave_date # back-compat with the old single-date payload + date_to = date_to or date_from + if not date_from or not reason: + return {'error': 'Please provide a start date and a reason.'} try: - date_obj = fields.Date.from_string(leave_date) + from_obj = fields.Date.from_string(date_from) + to_obj = fields.Date.from_string(date_to) except Exception: return {'error': 'Invalid date format. Use YYYY-MM-DD.'} + if to_obj < from_obj: + return {'error': 'The end date cannot be before the start date.'} + # Reject if an existing request overlaps the requested range. existing = request.env['fusion.clock.leave.request'].sudo().search([ ('employee_id', '=', employee.id), - ('leave_date', '=', date_obj), + ('leave_date', '<=', to_obj), + ('date_to', '>=', from_obj), ], limit=1) if existing: - return {'error': 'A leave request already exists for this date.'} + return {'error': 'A leave request already overlaps these dates.'} request.env['fusion.clock.leave.request'].sudo().create({ 'employee_id': employee.id, - 'leave_date': date_obj, + 'leave_date': from_obj, + 'date_to': to_obj, 'reason': reason, 'created_from': 'portal', }) - return {'success': True, 'message': f'Leave request for {leave_date} submitted.'} + if from_obj == to_obj: + msg = f'Leave request for {date_from} submitted.' + else: + msg = f'Leave request for {date_from} to {date_to} submitted.' + return {'success': True, 'message': msg} @http.route('/fusion_clock/request_correction', type='jsonrpc', auth='user', methods=['POST']) def request_correction(self, attendance_id=0, check_in='', check_out='', reason='', **kw): diff --git a/fusion_clock/migrations/19.0.3.13.0/post-migrate.py b/fusion_clock/migrations/19.0.3.13.0/post-migrate.py new file mode 100644 index 00000000..a774e2fb --- /dev/null +++ b/fusion_clock/migrations/19.0.3.13.0/post-migrate.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 Nexa Systems Inc. +# License OPL-1 (Odoo Proprietary License v1.0) +"""Backfill leave-request end dates on upgrade to 19.0.3.13.0. + +Leave requests gained a `date_to` (end of a multi-day range). Existing +single-day requests have no end date; set it to the start date so they keep +being treated as one-day leaves by the absence check and reports. +""" + + +def migrate(cr, version): + if not version: + return + cr.execute( + "UPDATE fusion_clock_leave_request SET date_to = leave_date WHERE date_to IS NULL" + ) diff --git a/fusion_clock/models/clock_leave_request.py b/fusion_clock/models/clock_leave_request.py index 2fe2592a..ac4342fc 100644 --- a/fusion_clock/models/clock_leave_request.py +++ b/fusion_clock/models/clock_leave_request.py @@ -3,7 +3,8 @@ # License OPL-1 (Odoo Proprietary License v1.0) import logging -from odoo import models, fields, api +from odoo import models, fields, api, _ +from odoo.exceptions import ValidationError _logger = logging.getLogger(__name__) @@ -23,10 +24,16 @@ class FusionClockLeaveRequest(models.Model): ondelete='cascade', ) leave_date = fields.Date( - string='Leave Date', + string='From Date', required=True, index=True, ) + date_to = fields.Date( + string='To Date', + index=True, + help="Last day of the leave (inclusive); equals the start date for a " + "single-day request.", + ) reason = fields.Text( string='Reason', required=True, @@ -59,15 +66,32 @@ class FusionClockLeaveRequest(models.Model): store=True, ) - @api.depends('employee_id', 'leave_date') + @api.depends('employee_id', 'leave_date', 'date_to') def _compute_display_name(self): for rec in self: emp = rec.employee_id.name or '' - date_str = str(rec.leave_date) if rec.leave_date else '' - rec.display_name = f"{emp} - Leave ({date_str})" + rec.display_name = f"{emp} - Leave ({rec._fclk_date_label()})" + + def _fclk_date_label(self): + """Human label for the leave period: a single date, or 'from to to'.""" + self.ensure_one() + if not self.leave_date: + return '' + if self.date_to and self.date_to != self.leave_date: + return f"{self.leave_date} to {self.date_to}" + return str(self.leave_date) + + @api.constrains('leave_date', 'date_to') + def _check_leave_dates(self): + for rec in self: + if rec.date_to and rec.leave_date and rec.date_to < rec.leave_date: + raise ValidationError(_("The end date cannot be before the start date.")) @api.model_create_multi def create(self, vals_list): + for vals in vals_list: + if not vals.get('date_to') and vals.get('leave_date'): + vals['date_to'] = vals['leave_date'] records = super().create(vals_list) for rec in records: rec._notify_office_user() @@ -86,7 +110,7 @@ class FusionClockLeaveRequest(models.Model): try: self.env['mail.activity'].sudo().create({ 'activity_type_id': self.env.ref('mail.mail_activity_data_todo').id, - 'summary': f"Leave Request: {self.employee_id.name} on {self.leave_date}", + 'summary': f"Leave Request: {self.employee_id.name} ({self._fclk_date_label()})", 'note': f"Reason: {self.reason}", 'user_id': office_user.id, 'res_model_id': self.env['ir.model']._get_id('fusion.clock.leave.request'), @@ -102,7 +126,7 @@ class FusionClockLeaveRequest(models.Model): self.env['fusion.clock.activity.log'].sudo().create({ 'employee_id': self.employee_id.id, 'log_type': 'leave_request', - 'description': f"Leave requested for {self.leave_date}: {self.reason}", + 'description': f"Leave requested for {self._fclk_date_label()}: {self.reason}", 'source': 'portal' if self.created_from == 'portal' else 'system', }) except Exception as e: diff --git a/fusion_clock/models/clock_report.py b/fusion_clock/models/clock_report.py index 13a6df9d..3dc51995 100644 --- a/fusion_clock/models/clock_report.py +++ b/fusion_clock/models/clock_report.py @@ -166,8 +166,9 @@ class FusionClockReport(models.Model): self.attendance_ids = [(6, 0, attendances.ids)] leave_domain = [ - ('leave_date', '>=', self.date_start), + # Any leave whose range overlaps the report period. ('leave_date', '<=', self.date_end), + ('date_to', '>=', self.date_start), ] if self.employee_id: leave_domain.append(('employee_id', '=', self.employee_id.id)) diff --git a/fusion_clock/models/hr_attendance.py b/fusion_clock/models/hr_attendance.py index 9a551b67..c72ffebf 100644 --- a/fusion_clock/models/hr_attendance.py +++ b/fusion_clock/models/hr_attendance.py @@ -423,7 +423,8 @@ class HrAttendance(models.Model): leave = LeaveRequest.search([ ('employee_id', '=', emp.id), - ('leave_date', '=', yesterday), + ('leave_date', '<=', yesterday), + ('date_to', '>=', yesterday), ], limit=1) if leave: continue diff --git a/fusion_clock/static/src/css/portal_clock.css b/fusion_clock/static/src/css/portal_clock.css index f8c9958f..d3258ff5 100644 --- a/fusion_clock/static/src/css/portal_clock.css +++ b/fusion_clock/static/src/css/portal_clock.css @@ -1319,6 +1319,33 @@ html.o_dark .fclk-wizard-overlay { text-decoration: underline; } +/* Leave request: From / To date-range row */ +.fclk-leave-daterange { + display: flex; + gap: 10px; +} +.fclk-leave-daterange-col { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 4px; +} +.fclk-leave-daterange-col .fclk-wizard-input { + width: 100%; +} +.fclk-leave-daterange-cap { + font-size: 11px; + color: var(--fclk-text-muted); + text-transform: uppercase; + letter-spacing: 0.4px; +} +.fclk-leave-daterange-cap small { + text-transform: none; + letter-spacing: 0; + color: var(--fclk-text-dim); +} + /* ---- Reports Page ---- */ .fclk-reports-container { max-width: 600px; diff --git a/fusion_clock/static/src/js/fusion_clock_portal.js b/fusion_clock/static/src/js/fusion_clock_portal.js index c570eba6..e3f4aeb1 100644 --- a/fusion_clock/static/src/js/fusion_clock_portal.js +++ b/fusion_clock/static/src/js/fusion_clock_portal.js @@ -655,26 +655,34 @@ export class FusionClockPortal extends Interaction { } async _submitLeave() { - const dateEl = document.getElementById("fclk-leave-date"); + const fromEl = document.getElementById("fclk-leave-date"); + const toEl = document.getElementById("fclk-leave-date-to"); const reasonEl = document.getElementById("fclk-leave-reason"); - const leaveDate = dateEl ? dateEl.value : ""; + const dateFrom = fromEl ? fromEl.value : ""; + let dateTo = toEl && toEl.value ? toEl.value : dateFrom; // single day if "To" left blank const reason = reasonEl ? reasonEl.value.trim() : ""; - if (!leaveDate || !reason) { - this._showToast("Please provide both a date and reason.", "error"); + if (!dateFrom || !reason) { + this._showToast("Please provide a start date and a reason.", "error"); + return; + } + if (dateTo < dateFrom) { + this._showToast("End date can't be before the start date.", "error"); return; } try { const result = await rpc("/fusion_clock/request_leave", { - leave_date: leaveDate, + date_from: dateFrom, + date_to: dateTo, reason: reason, }); if (result.success) { const modal = document.getElementById("fclk-leave-modal"); if (modal) modal.style.display = "none"; this._showToast(result.message, "success"); - if (dateEl) dateEl.value = ""; + if (fromEl) fromEl.value = ""; + if (toEl) toEl.value = ""; if (reasonEl) reasonEl.value = ""; } else { this._showToast(result.error || "Failed to submit.", "error"); diff --git a/fusion_clock/views/clock_leave_request_views.xml b/fusion_clock/views/clock_leave_request_views.xml index c756de1c..b45bb832 100644 --- a/fusion_clock/views/clock_leave_request_views.xml +++ b/fusion_clock/views/clock_leave_request_views.xml @@ -8,6 +8,7 @@ + + diff --git a/fusion_clock/views/portal_clock_templates.xml b/fusion_clock/views/portal_clock_templates.xml index 521bb7ca..8daaac64 100644 --- a/fusion_clock/views/portal_clock_templates.xml +++ b/fusion_clock/views/portal_clock_templates.xml @@ -423,9 +423,18 @@
- +
+
+ From + +
+
+ To (optional) + +
+