Timesheet Validator - Validation Rules

Version 4.3.8 | Last Updated: March 31, 2026 | ASK IT Limited
Total Rules
22
Completed
22
Pending
0
Completion
100%
Priority Rule Name Scope Condition (Logic) Output / Remark Pattern Status
Group A No Work Times Recorded — C / D empty A1→A2→A2.5(FT)→A2.7(FT)→A2.7(PT)→A2.5(PT): If/ElseIf chain (mutually exclusive). PT employees are exempt from Rules 1, 2, and 2.5 (FT); they fall into 2.7(PT) (incomplete time — one of C/D filled) or 2.5(PT) (half-day marker, both C/D empty) where applicable.
GRP A1 Leave without reason (all empty)PT: ⊘ Skipped WeekdaySaturday (5.5-day) (Weekday OR Saturday 5.5-day) AND FT AND
• Start Time (C) = Empty
• End Time (D) = Empty
• Col G = Empty AND Col H = Empty
Business Logic:
On scheduled work days (weekday or 5.5-day Saturday), employees are expected to work. If no time is entered and no remarks explain the absence, this indicates unapproved leave. PT employees are exempt from this rule.

Example:
Date: Jan 15 (Mon) | C/D/H: All empty → Error
Date: Jan 18 (Sat, 5.5-day staff) | C/D/H: All empty → Error
DD-MMM & " - Leave without reason (weekday, no time in C/D, no leave type in G/H)" Done
GRP A2 Leave without reason (has text in G/H, no recognised leave code)PT: ⊘ Skipped WeekdaySaturday (5.5-day) (Weekday OR Saturday 5.5-day) AND FT AND
• C = Empty AND D = Empty
• Col G or Col H has text (distinguishes from Rule 1)
upCombined (Col G + Col H) contains none of:
  AL, SL, NPL  ← Col G dropdown values
  NPSL  ← Col H only (Others path)
  PUBLIC HOLIDAY  ← substring match
Business Logic:
Differs from Rule 1 (fully empty row) — this row has some text in Col G or Col H, but none of it matches a recognised leave code. The employee wrote something but it cannot be interpreted as approved leave.

Detection uses whole-word matching (HasLeaveCode) on upCombined = UCase$(ColG & " " & ColH). Both columns are scanned together. If any exempt code is found, this rule does not fire:
AL / SL / NPL — standard Col G dropdown leave types
NPSL — Col H only (Others path); exempts and counted as NPL
PUBLIC HOLIDAY — substring match (not whole-word)

Both C and D must be empty — if only one is filled, routes to Rule 2.7 instead. PT employees are exempt from this rule entirely.

Examples:
Jan 20 (Mon) | C/D: Empty, Col G: "Doctor appointment" → Error (no recognised code)
Jan 20 (Mon) | C/D: Empty, Col G: "AL (AM)" → No error ✅ (AL found → routes to Rule 2.5 (FT))
Jan 25 (Mon) | C/D: Empty, Col G: "Others", Col H: "NPSL" → No Rule 2 error ✅ (NPSL exempts, counted as NPL); Rule 16 fires
DD-MMM & " - Leave without reason (weekday, no time in C/D, no leave code in G/H)" Done
GRP A2.5 (FT) Half-day leave without work hours (FT)PT: ⊘ Skipped (see 2.5 (PT)) WeekdaySaturday (5.5-day) (Weekday OR Saturday 5.5-day) AND FT AND
• Remarks contain "(AM)" or "(PM)"
• C = Empty AND D = Empty (both)
Business Logic:
If marking half-day leave (AM or PM), the employee should still work the other half and enter those hours in columns C/D. PT employees are exempt from this rule — they face Rule 2.5 (PT) instead. The (AM)/(PM) marker is read from Col G for standard leave types, and from Col H when Col G = "OTHERS".

Example:
Date: Feb 5 (Wed) | C/D: Empty, Col G: "AL (AM)" → Error
Date: Jan 18 (Sat, 5.5-day staff) | C/D: Empty, Col G: "SL (PM)" → Error
DD-MMM & " - Half-day leave but no work hours (expected work time in C/D for other half)" Done
GRP A2.5 (PT) Half-day leave without work hours (PT)PT: ★ Exclusive Weekday isPartTime = True AND isWeekday AND
• hasRemark = True
• C = Empty AND D = Empty (both)
• Remarks contain "(AM)" or "(PM)"
Business Logic:
PT equivalent of Rule 2.5 (FT). PT employees are exempt from Rule 2.5 (FT) (FT half-day check), but if they explicitly mark a half-day marker on a weekday with no time entries, the same data-entry error applies.

Example:
PT employee | Date: Feb 5 (Wed) | C/D: Empty, Col G: "AL (AM)" → Error
DD-MMM & " - Half-day leave but no work hours (expected work time in C/D for other half)" Done
GRP A2.7 (FT) Incomplete time entry — one of start/end missing (FT)PT: ⊘ Skipped (see 2.7 (PT)) WeekdaySaturday (5.5-day) (Weekday OR scheduled Saturday) AND FT AND
• Exactly one of C or D is filled
Business Logic:
Both start time (C) and end time (D) must be provided together. Having one without the other is a data entry error regardless of what leave codes are present in G/H.

Examples:
Date: Jan 20 (Mon) | C: 09:00, D: Empty, Remarks: "AL" → Error
Date: Jan 20 (Mon) | C: Empty, D: 18:00, Remarks: "" → Error
DD-MMM & " - Incomplete time entry (has start time in C but missing end time in D)"
or
DD-MMM & " - Incomplete time entry (has end time in D but missing start time in C)"
Done
GRP A2.7 (PT) Incomplete time entry — one of start/end missing (PT)PT: ★ Exclusive All Days isPartTime = True AND
• Exactly one of C or D is filled
Business Logic:
PT employees are exempt from Rules 1, 2, and 2.5 (FT) (leave without reason). However, having only one of start/end time is still a data-entry error on any day type.

Example:
PT employee | Date: Jan 20 (Mon) | C: 09:00, D: Empty → Error
DD-MMM & " - Incomplete time entry (has start time in C but missing end time in D)"
or
DD-MMM & " - Incomplete time entry (has end time in D but missing start time in C)"
Done
Group B Work Times Present — C & D both filled Each rule is an independent parallel check — not an If/ElseIf chain.
GRP B2.6 Full-day NPL/NPSL with work hours presentPT: ✓ Applied Weekday Saturday (5.5-day) (Weekday OR Saturday 5.5-day) AND
• C filled AND D filled
• Col G starts with "NPL" or "NPSL" (non-Others path)
  OR Col G = "OTHERS" AND Col H contains "NPSL" (exact case)
• No "(AM)" or "(PM)" marker
Business Logic:
Full-day NPL or NPSL means no work should be recorded. If C/D are filled alongside a full-day NPL/NPSL entry (without an AM/PM marker), the entry is contradictory.

Half-day NPL(AM) or NPL(PM) is valid — the employee works the other half and enters times in C/D.

Leave source rules for this check:
• Non-Others path: NPL/NPSL detected from Col G prefix (e.g. "NPL (Full Day)", "NPSL")
• Others path (Col G = "OTHERS"): only NPSL in Col H triggers this check (exact-case match). "NPL" in Col H under Others is a data-entry mistake and is not treated as full-day NPL — employees must use the Col G NPL dropdown instead.

Example (error, non-Others):
Date: Mar 4 (Tue) | C: 09:00, D: 18:00, Col G: "NPL (Full Day)" → Error

Example (error, Others path):
Date: Mar 4 (Tue) | C: 09:00, D: 18:00, Col G: "OTHERS", Col H: "NPSL" → Error

Example (valid):
Date: Mar 4 (Tue) | C: 13:00, D: 18:00, Col G: "NPL (AM)" → Half-day NPL, afternoon worked → OK
DD-MMM & " - Full-day NPL/NPSL but work hours present (C-D=" & Format(roundedActual,"0.0") & "h)" Done
GRP B3 Working hours not numeric (E is N/A)PT: ⊘ Skipped Weekday Weekday AND
• C filled AND D filled
• actualHours > 0 (from C-D)
• E NOT numeric (N/A, text, error)
Business Logic:
If time is entered (C & D), the working hours formula (E) should calculate a numeric value. "N/A" or errors indicate formula/data issues.

Example:
Date: Jan 12 (Mon) | C: 09:00, D: 18:00, E: "N/A", actual: 9.0h → Error
DD-MMM & " - Working hours missing C-D=" & Format(actualHours,"0.0") & "h E is not numeric (N/A)" Done
GRP B4 C/D vs E mismatchPT: ✓ Applied Weekday Weekday AND
• C filled AND D filled
• E is numeric
• E ≠ (D-C) × 24
Business Logic:
The working hours in column E must match the calculated difference between end time and start time [(D-C) × 24].

Example:
Date: Jan 18 (Mon) | C: 09:00, D: 18:00, E: 8.5h, actual: 9.0h → Error
DD-MMM & " - Working hours mismatch C-D=" & Format(actualHours,"0.0") & "h E=" & Format(workHours,"0.0") & "h" Done
GRP B5 Actual < expected hoursPT: ⊘ Skipped Weekday Weekday AND
• actualHours + tolerance(0.5) < expectedHours
Business Logic:
Employees should work their expected hours unless on approved leave. Small discrepancies within tolerance (0.5h) are allowed for rounding.

Example:
Date: Feb 8 (Mon) | actual: 7.5h, expected: 9.0h → 7.5+0.5=8.0<9.0 → Error
DD-MMM & " - Hours mismatch actual " & Format(actualHours,"0.0") & "h expected " & Format(expectedHours,"0.0") & "h" Done
GRP B6 OT not allowed (no OT eligibility)PT: ⊘ Skipped Weekday Weekday AND
• actualHours ≤ expectedHours + tolerance
• OT (F) > 0
Business Logic:
Overtime is only permitted when actual hours exceed expected hours + tolerance. Working exactly expected hours = no OT allowed.

Example:
Date: Jan 22 (Mon) | actual: 9.0h, expected: 9.0h, OT: 1.0h → Error
DD-MMM & " - OT not allowed actual " & Format(actualHours,"0.0") & "h expected " & Format(expectedHours,"0.0") & "h OT(col F)=" & Format(otFromCol,"0.0") & "h" Done
GRP B7 OT miscalculation (1-hour buffer)PT: ⊘ Skipped Weekday Weekday AND
• actualHours > expectedHours
• If hrOTFreeHr = Y: calcOT = MAX(0, actual - expected - 1)
  If hrOTFreeHr = N: calcOT = actual - expected
• calcOT ≠ OT in col F
Business Logic - OT buffer rule (hrOTFreeHr):
When HR column "OT free hour" = Y, employees must work >1 hour beyond expected hours before OT is counted.
• hrOTFreeHr = Y: actual - expected ≤ 1h → No OT; > 1h → OT = excess − 1
• hrOTFreeHr = N: OT = actual - expected (no buffer)

Example:
Date: Feb 3 (Wed) | actual: 11.0h, expected: 9.0h, OT(F): 2.0h, calcOT: 11-9-1=1.0h → Error
DD-MMM & " - OT miscalculation actual " & Format(actualHours,"0.0") & "h expected " & Format(expectedHours,"0.0") & "h OT(col F)=" & Format(otFromCol,"0.0") & "h OT(calc)=" & Format(calcOT,"0.0") & "h"
Output field breakdown:
OT(col F) = what the employee declared in the timesheet
OT(calc) = what the system calculated (respects hrOTFreeHr setting)

Example (hrOTFreeHr = Y):
actual=11h, expected=9h → excess=2h → OT = 2−1 = 1.0h
But col F shows 2.0h → mismatch → Error

Example (hrOTFreeHr = N):
actual=11h, expected=9h → excess=2h → OT = 2.0h
But col F shows 1.0h → mismatch → Error
Done
Group C Saturday — 5.5-day Staff Only Applies only when HR flag "5.5 days (Y/N)" = Y. 5-day staff Saturday falls into Group D instead.
GRP C11 Saturday leave without reason (5.5-day staff)PT: ⊘ Skipped Saturday Saturday AND 5.5-day employee AND
• 0 < actualHours < 4
• actualHours < expectedHours
• No recognised leave code in Col G or Col H
  (AL/SL/NPL from Col G; NPSL from Col H; PH/Public Holiday)
Business Logic:
5.5-day employees are expected to work Saturdays (typically 4 hours). Partial hours without a valid leave explanation require justification. Leave detection scans upCombined (Col G + Col H) — same mechanism as Rule 2.

Example:
Date: Jan 11 (Sat) | Type: 5.5-day, expected: 4.0h, actual: 2.5h, Col G: "Personal matters" → Error
Date: Jan 11 (Sat) | Type: 5.5-day, expected: 4.0h, actual: 2.5h, Col G: "AL (AM)" → OK ✅ (AL found)
DD-MMM & " - Sat leave without reason (5.5-day staff) actual " & Format(actualHours,"0.0") & "h expected " & Format(expectedHours,"0.0") & "h" Done
GRP C11.5 Saturday leave without AM/PM marker (5.5-day staff)PT: ⊘ Skipped Saturday Saturday AND 5.5-day employee AND
• Active leave type detected (effHasAL / effHasSL / effHasNPL)
• No "(AM)" or "(PM)" marker in remarks
Business Logic:
5.5-day staff Saturdays are working half-days. Any leave taken on a Saturday should specify AM or PM to clarify which half was taken. A warning is generated when a leave type is present but no half-day marker is found.

Example (warning):
Date: Jan 11 (Sat) | 5.5-day, Col G: "SL" → Warning: "Jan 11 - Saturday SL should specify (AM) or (PM)"

Example (valid):
Date: Jan 11 (Sat) | 5.5-day, Col G: "SL (AM)" → OK ✅

Active leave types: AL, SL, NPL only. NPSL is routed through NPL (message shows "NPL" not "NPSL").
DD-MMM & " - Saturday AL should specify (AM) or (PM)"
DD-MMM & " - Saturday SL should specify (AM) or (PM)"
DD-MMM & " - Saturday NPL should specify (AM) or (PM)"
(3 warning variants — AL, SL, NPL only)
Done
GRP C12 Saturday OT after 5.5 days (4h+ only)PT: ⊘ Skipped Saturday Sat AND 5.5-day AND
• actualHours > 4
• calcOT = actualHours - 4
• calcOT ≠ OT in col F
Business Logic - Saturday OT Rule:
• First 4 hours = regular working hours
• Hours beyond 4 = overtime
• Formula: Saturday OT = actualHours - 4
• No 1-hour buffer (unlike weekday Rule 7)

Example:
Date: Jan 18 (Sat) | Type: 5.5-day, actual: 6.0h, OT(F): 2.5h, calcOT: 6.0-4.0=2.0h → Error
DD-MMM & " - Sat OT miscalculation (5.5-day staff) actual " & Format(actualHours,"0.0") & "h OT(col F)=" & Format(otFromCol,"0.0") & "h OT(calc)=" & Format(satOT,"0.0") & "h (OT = hours - 4)"
Saturday OT formula:
Saturday OT formula is actual − 4 (first 4h = regular, beyond 4h = OT). No 1-hour buffer applies.

Example:
actual=6.0h → OT should be 6−4=2.0h
But col F shows 2.5h → mismatch → Error
Done
Group D Rest Day — Sunday / Public Holiday / Saturday (5-day staff) 5-day staff Saturday treated as rest day (same as Sun/PH). 5.5-day staff Saturday handled by Group C.
GRP D13 Rest day hours mismatch (Sun/PH/Sat-5day)PT: ⊘ Skipped Sunday/PH Saturday (5-day) (Sunday OR Public Holiday OR
Saturday with 5-day staff) AND
• At least one of C-D / E / F filled
• NOT (C-D = E = F)
Business Logic - Rest day work rule:
• All hours worked on a rest day = OT (no "regular hours")
• C-D, E, and F must all match
• Applies to: Sunday, Public Holiday, AND Saturday for 5-day staff
• 5-day staff Saturday is treated as a rest day (same as Sun/PH)
• 5.5-day staff Saturday is handled separately by Rules 11 & 12

Example (Sun/PH):
Date: Jan 1 (Sun-PH) | C: 10:00, D: 15:00, E: 5.0h, F: 4.5h → Error

Example (5-day Saturday):
Date: Jan 11 (Sat) | 5-day staff | C: 10:00, D: 14:00, E: 4.0h, F: 3.5h → Error
Weekend (Sun/Sat-5day):
DD-MMM & " - Weekend hours mismatch C-D=" & Format(actualHours,"0.0") & "h E=" & Format(vE,"0.0") & "h F=" & Format(vF,"0.0") & "h"

Public Holiday:
DD-MMM & " - Public Holiday hours mismatch C-D=" & Format(actualHours,"0.0") & "h E=" & Format(vE,"0.0") & "h F=" & Format(vF,"0.0") & "h"
Why two versions?
Two slightly different message formats exist in the code due to different execution paths within the same rule.

Version 1 — Fired when all three of C−D, E, and F are available. Includes the F column value in the message for full visibility.

Version 2 — Fired when F is missing or zero. Only compares C−D vs E.

Both indicate the same underlying issue: the hours across C−D, E, and F don't all match. A future cleanup could unify these into one message format.
Done
GRP D14 Rest day no work, no issue (Sun/PH/Sat-5day)PT: ✓ Applied Sunday/PH Saturday (5-day) (Sunday OR Public Holiday OR
Saturday with 5-day staff) AND
• C = Empty
• D = Empty
Business Logic:
Sundays, public holidays, and Saturdays for 5-day staff are all rest days. No work = no error.
5-day staff Saturday is a rest day (same treatment as Sun/PH).

Example (Sun/PH):
Date: Jan 1 (Sun-New Year) | C/D: Empty → OK ✅ (no error)

Example (5-day Saturday):
Date: Jan 11 (Sat) | 5-day staff | C/D: Empty → OK ✅ (no error)
(No remark) Done
GRP D15 Leave marked on rest day (warning)PT: ✓ Applied Sunday/PH Saturday (5-day) (Sunday OR Public Holiday OR
Saturday with 5-day staff) AND
• Active leave type detected (effHasAL / effHasSL / effHasNPL)
Business Logic:
Leave types shouldn't be marked on rest days (Sunday, Public Holiday, or Saturday for 5-day staff) since these are already non-working days.

AL / NPL on rest day: NOT counted. Warning remark + MarkError G:H.
SL on rest day: IS counted (cntSL += 0.5 or 1). Informational remark only, no MarkError. SL is counted on rest days because consecutive SL tracking spans across weekends/PH.

Example (AL):
Date: Jan 1 (Sun-New Year) | Col G: "AL" → Warning: "Jan 1 - AL marked on rest day (Sun/PH)" + MarkError

Example (SL):
Date: Jan 1 (Sun-New Year) | Col G: "SL" → Info: "Jan 1 - SL on rest day (included in consecutive SL streak)" (no MarkError, SL counted)

Why does 5-day Saturday say "(Sun/PH)" in the message?
The message template was written when this rule only covered Sunday and Public Holidays. 5-day staff Saturday was added later but the label was not updated.
DD-MMM & " - AL marked on rest day (Sun/PH)"
DD-MMM & " - SL on rest day (included in consecutive SL streak)"
DD-MMM & " - NPL marked on rest day (Sun/PH)"
(3 active leave types only — AL warning, SL informational, NPL warning)
Done
GRP D16 Others (Col G) informational remarkPT: ✓ Applied All Days Col G = "Others" (any employee, any day type)
Business Logic:
When Col G = "OTHERS", leave resolution switches to Col H. Only NPSL from Col H is counted (as NPL). An informational remark is always generated so HR can review what was entered in Col H.

Leave accumulation under Others:
AL, SL, NPL (from Col G dropdown) — counted normally
NPSL (Col H) — counted as NPL (effHasNPL = colHHasNPSL)
Example:
FT employee | Col G: "Others", Col H: "NPSL" → NPSL counted as NPL + informational remark
DD-MMM & " - Others (Col G): '" & Col H value & "' - please review" Done
Group E Part-Time (PT) Employees — Specific Outputs These outputs apply exclusively to employees with work schedule "PT" in the HR sheet. FT employees receive "N/A" for PT-specific columns.
GRP EPT-1 PT working hours & daysPT: ★ Exclusive All Days isPartTime = True
After day loop ends:
• Read footer "TOTAL WORKING HOURS" col E → totalPTHours
• Read footer "TOTAL WORKING DAYS" col E → totalPTDays
Intermediate col B: totalPTHours
Intermediate col C: totalPTDays
(FT employees: both written as "N/A")
Done
GRP EPT-2 468-hour statutory cap statusPT: ★ Exclusive All Days isPartTime = True AND
HR column "468" = "Y"
Intermediate col P (pt_468_status): "✓ entitled"
(Only written if HR 468 = Y AND isPartTime)
Done

📋 Intermediate Sheet — intermediate - after scanning

One row is written per employee per run. Col P (pt_468_status) is written only for PT employees where HR 468 = Y. Col Q (total allowance) is the parsed numeric total from HR "other allowance" column. All other columns always written.

Column Schema (A–Q)

Col Header Description
Aemployee IDEmployee code (e.g. A0012). Row is matched by this key on subsequent runs — updated in-place if already exists.
Btotal working hours (PT)PT employees only — read from timesheet footer row "TOTAL WORKING HOURS" col E (pre-calculated by Generator). Written as "N/A" for FT employees.
Ctotal working days (PT)PT employees only — read from timesheet footer row "TOTAL WORKING DAYS" col E (pre-calculated by Generator). Written as "N/A" for FT employees.
D468Reserved column (468-hour statutory cap tracking). Currently written as "N/A".
EOT - weekdays/sat (hours)Total OT hours accumulated on weekdays and scheduled Saturdays for the month.
FOT - sun/PH (hours)Total OT hours accumulated on Sundays, Public Holidays, and 5-day-staff Saturdays.
Gno. of AL taken (days)Count of AL day-equivalents taken (full-day = 1, half-day = 0.5). Validated against HR quota; overtaking AL highlighted red in remarks.
Hno. of SL taken (days)Count of SL day-equivalents taken (full-day = 1, half-day = 0.5). Validated against HR quota; overtaking SL highlighted red in remarks.
I4 consecutive SL (Y/N)"Y" if 4 or more consecutive SL days detected, "N" otherwise. "Y" displayed in red font (RGB 255,0,0); "N" resets to automatic colour.
Jno. of NPL/NPSL taken (days)Count of NPL/NPSL day-equivalents taken (full-day = 1, half-day = 0.5).
Lstatus"completed" — no issues found. "needs review" — one or more remarks generated.
MremarksAll validation messages for the employee, newline-separated. "No issue" when clean. Certain keywords are colour-highlighted inline (see table below).
Nissues"Y" if any remarks exist, blank otherwise. Used by downstream processing to filter employees needing attention.
Oscan timestampDate/time of the most recent validation run for this employee (format: yyyy-mm-dd hh:mm). Always written; overwrites previous value.
Ppt_468_statusWritten as "✓ entitled" only when isPartTime = True AND HR column "468" = "Y". Blank for all FT employees and PT employees without 468 flag.
Qtotal allowanceParsed numeric total from HR "other allowance" column (e.g. "Housing 500; Standby 200" → 700). Always written; overwrites previous value.

Remarks Column (M) — Inline Colour Highlights

After all remarks are written, specific keywords are coloured within the cell text (character-level formatting, not cell background):

Keyword Colour Trigger condition
SL > 3 daysRedMonthly SL count exceeds 3 days
overtaking ALRedAL taken exceeds HR quota (no. of AL column in input (HR))
overtaking SLRedSL taken exceeds HR quota (no. of SL column in input (HR))
overtaking CL hoursRedCL hours taken exceeds HR quota
Blank date/dayRed (entire line)Inserted row with blank date/day detected — entire remark line coloured red
Others (Col G):Orange (entire line)Col G = "Others" — informational, HR to review Col H content (RGB 255,140,0)
Statutory Holiday:Blue (entire line)468 PT employee SL on statutory holiday — HR to action full-pay (RGB 0,112,192)
SL gap across rest daysBlackSL streak spans across rest days — informational only, not an error (vbBlack)

📄 Standalone Validation Log File

Each validation run appends one row per issue to timesheet - validation log.xlsx in the same source folder. The file and "issue log" sheet are auto-created if missing. SharePoint URL paths are supported. The log is append-only — it preserves full run history and never overwrites previous results.

Column Description
A — Run TimestampDate/time the validation ran (format: yyyy-mm-dd hh:mm:ss)
B — Employee IDEmployee code parsed from filename (e.g. A0012)
C — Employee NameFull name read from timesheet cell C6
D — YearTimesheet year
E — Month3-letter month code (e.g. JAN)
F — DateDate extracted from the remark line (text before first " - "); blank for "No issue" rows
G — MessageRemark text after the date prefix; "No issue" when no errors detected
H — Severity "Warning" — message contains "rest day" or "should specify"
"Error" — all other issues
Blank — "No issue" rows