When you try to approve an Analysis Record (AR) and approval is blocked because one of the linked instruments has an overdue calibration, this recipe describes the two ELOT EN ISO/IEC 17025:2017-compliant resolution paths:
CalibrationRecord with an updated next-calibration date, and re-run approval.LAB_DIRECTOR), Quality Manager (QUALITY_MANAGER), or Reviewer (REVIEWER) approves without calibration, logging a justification of ≥20 characters. The override is audited.Neither path is silent — every approval over an overdue instrument leaves a trace in the system.
| # | Prerequisite | Where verified |
|---|---|---|
| 1 | The AR is in status REVIEWED, ready for transition to APPROVED. | status field on the Analysis Detail page. |
| 2 | At least one Measurement on the AR references an Instrument with requiresCalibration = true. | Measurements tab on the AR. |
| 3 | The instrument's nextCalibrationDate is before today. | The "Next Calibration" field on the Instrument's own edit form (not the Calibrations tab, which is the history of calibration records). |
| 4 | For Option B, the user holds a role allowed by the "Quality System Override" role-permission entry — default: LAB_DIRECTOR, QUALITY_MANAGER, or REVIEWER. | Settings → Role Permissions. |
| 5 | For Option A, the user can create a CalibrationRecord on that instrument. | Role permissions. |
Instruments with requiresCalibration = false (e.g. volumetric glassware, manual tools) or with no nextCalibrationDate set are automatically exempt and do not trigger the block.
Before choosing between A and B, open the Measurements tab on the AR and note:
APPROVAL_BLOCKED_CALIBRATION_OVERDUE. The frontend recognises the code and opens the override dialog instead of a generic snackbar, listing the overdue instruments (name, serial number, calibration expiry date).CalibrationRecord. Fill in:calibrationDate — today's calibration date.withinTolerance: true — the instrument passed the check.nextDueDate — the recommended next calibration date computed from the SOP frequency (e.g. + 12 months). Note: this is a historical field on the calibration record itself — it does not update the instrument automatically.CalibrationRecord does not, by itself, update the instrument's own nextCalibrationDate — that is the field the approval guard actually checks. Separately open the Instrument's own edit form (not the Calibrations tab) and update the "Next Calibration" field to a future date, then save.OVERRIDE_CALIBRATION_APPROVE is written with userId, entityType = 'AnalysisRecord', entityId, reason, timestamp, and the list of overdue instruments as changes. That tab is a plain list — no filter or CSV export there. To filter by action (OVERRIDE_CALIBRATION_APPROVE) and export CSV for the internal-audit folder, use the standalone Audit Trail page (/audit-trail, gated by the "Audit Trail" permission). The export carries user, timestamp, justification, and the related data — exactly what §6.4.7 expects you to have available.
To avoid repeated rejections for insufficient documentation, structure the justification as:
"Calibration of <instrument name> (S/N <serial>) expired on <date>. It cannot be performed immediately because <reason — e.g. vendor visit scheduled, backup instrument in use>. Metrological confidence is maintained via <internal QC against reference material Lot XXX within limits / duplicate analysis on backup instrument>. CAPA-<number> has been opened and calibration is scheduled for <date>."
The block logic is implemented in the _enforceCalibrationGuard method of analysis-record.resolver.ts (around line 967). For each Measurement on the AR, the distinct instrumentId values are collected and Instrument rows are queried where requiresCalibration = 1, nextCalibrationDate IS NOT NULL, and nextCalibrationDate < CURDATE(). If nothing matches, the guard passes immediately; otherwise, without an overrideReason, the method throws a GraphQLError with extension code APPROVAL_BLOCKED_CALIBRATION_OVERDUE and an instruments[] payload carrying id, name, serialNumber, nextCalibrationDate.
The nextCalibrationDate field on Instrument (instrument.entity.ts, around line 67) is a nullable date string — comparison uses MariaDB's CURDATE(), so every instrument within a tenant is checked against a single server-side timezone. Instruments with nextCalibrationDate IS NULL do not trigger the block.
On the override path, the shared OverrideHelper.assertAndRecord() helper (common/override.helper.ts) performs these checks in order:
isOverrideRole(role) — otherwise rejected with the message errors.overrideCalibrationDenied. This check is not hard-coded to the three named roles — it reads the tenant's QUALITY_SYSTEM_OVERRIDE role-permission entry (Settings → Role Permissions), whose default happens to be exactly LAB_DIRECTOR + QUALITY_MANAGER + REVIEWER, but can be changed.reason.trim().length >= 20 (the MIN_OVERRIDE_REASON_LENGTH constant) — otherwise rejected with the message errors.overrideReasonTooShort.recordEvent({ action: 'OVERRIDE_CALIBRATION_APPROVE', ... }), which writes the entry to the shared AuditTrail table (not a separate "OverrideLog" — it's the same table every audit event in the tenant is written to).The order is intentionally fail-fast: a user without the role will never see a "reason too short" message and therefore cannot probe the system for how to slip past the check.
A short Greek + English narrative is part of the guard's error message itself, so even tooling that does not surface extensions.instruments (e.g. raw curl) still shows which instrument is overdue.
On the frontend, the AnalysisDetail component (analysis-detail.ts, around lines 840-862) inspects err.errors[0].extensions.code (Apollo v4 shape) and opens the override dialog via showOverrideDialog.set(true), stashing the recordId in pendingApprovalRecordId. Submitting the reason calls the same mutation again, this time with overrideReason set — the guard re-runs, sees the reason, and proceeds to log it.
instrumentId column on Measurement has a foreign key to Instrument with no CASCADE — deleting the instrument fails with a foreign-key error as long as any measurement references it. The "deleted instrument, orphaned measurement" scenario therefore cannot occur through normal application use.withinTolerance: false): regardless of the withinTolerance value, saving a CalibrationRecord never automatically updates Instrument.nextCalibrationDate — that update is always manual, on the instrument's own form (see Option A above). An out-of-tolerance result is simply one more reason not to make that manual update.Important bulk-approve caveat. The bulk-approve path (analysis-record.resolver.ts, around line 1787) calls _enforceCalibrationGuard(r, undefined, userRole, userId) with overrideReason = undefined, so no override is allowed: every AR with an overdue instrument fails and must be fixed individually with a reason, or by an actual calibration. This is intentional — bulk approval must not become a shortcut for exceptions.
If you need to clear a large queue, the workflow is:
For the full clause text, see Reference → ISO/IEC 17025:2017.
ESYD assessors frequently spot-check override examples. A well-structured AuditTrail entry with a clear justification, a CAPA reference, and metrological-confidence evidence (QC charts, replicate measurements on a backup instrument) is strong evidence that the lab is not silently bypassing controls but handling the exception with maturity. Conversely, terse "emergency" justifications are a red flag.
Systematic use of Option B (for example more than 5 overrides per month on the same instrument) should trigger a review of the calibration programme under §6.4.7 and possibly an increase in frequency or a change of provider.