Add LFRM (Le Mans Arnage) customs form - #15
Conversation
_fix_autosize_fields only rebuilt the appearance stream for fields whose
/DA asked for auto-sized text (0 Tf). Fields with a fixed size were left
alone even when that size overflowed a short box, where pypdf places the
baseline below the box and the value renders clipped. LFQA has shipped
this way: "Nature du vol: private" is cut off on every generated form.
Rebuild the appearance for a fixed size too when it does not fit, clamped
to the box height. Along the way, handle two things LibreOffice-authored
templates do that the old code skipped:
- a widget whose /T lives on a parent field node rather than the widget
- a field with no appearance stream at all, which previously left the
rendering up to whatever the viewer decided to draw
Font sizes are read from the template, because filling rewrites an
inherited auto-size /DA to a concrete "12 Tf" on the field node.
Values are unchanged — no existing snapshot moves.
Three additions to the filler's vocabulary, all unused by existing
mappings, for a form landing in the next commit:
- convert times to the form's time zone when a mapping declares
time_reference "local"
- arrival.remote / departure.remote (plus _name and _country), the
other end of the leg, filled only on the side matching the flight
direction
- allow several enum values to share one checkbox via "|", e.g.
flight.nature.private|business; mapping them as separate entries
made the later one overwrite the earlier one's "on" with "off"
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015cLZEJUmiCnbxfytMKN18e
Airport-specific préavis douane for LFRM, alongside the generic LF-prefix
form which stays available as a second option. Same customs routing as
the french_customs entry for LFRM (preavis-vols-douane-le-mans, cc
codt-idf).
The template asks for local time, so the mapping declares time_reference
"local" with Europe/Paris.
Two quirks of the template the mapping works around:
- the four tick boxes are text fields, not checkboxes, so they take an
"X" (checkbox_on) rather than a checkbox state
- passenger rows are named ...Row{n}_2 for rows 1-4 and plain ...Row{n}
for rows 5-7, which no single {n} pattern spans, so they use literal
indices
Provenance / Aéroport / Pays map to arrival.remote / departure.remote, so
only the side matching the flight direction is filled.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015cLZEJUmiCnbxfytMKN18e
|
Code Review Reviewed against designs/form-system.md (no repo-root CLAUDE.md exists in this repo). The mapping structure, registry resolution, and text-appearance fix all match the documented design. One correctness issue worth fixing before merge: Local time can cross midnight without the paired date field following it — src/flightforms/fillers/pdf_filler.py:110-120 def _time(time_str, date_str): arr_time/dep_time are converted to local wall-clock time, but arrival.date/departure.date (and flight.date) still use the raw UTC-based date from the request, unconverted. For a flight where the UTC time is late evening, converting to Europe/Paris can roll the local time into the next calendar day (e.g. 2024-06-01 23:50 UTC -> 2024-06-02 01:50 local CEST), but the date field printed on the form stays 2024-06-01. This is exactly the field pair (ARRIVAL_DATE/ARRIVAL_LOCAL_TIME, DEPARTURE_DATE/DEPARTURE_LOCAL_TIME) that lfrm.json fills, and this is a customs pre-notification document, so a mismatched date+time is a real problem for flights near midnight UTC, not just cosmetic. Since this is the first mapping to actually exercise time_reference: "local" in pdf_filler.py, this edge case has not been hit by any existing form. Suggest deriving the printed date from the same converted local datetime (e.g. have _utc_to_local also expose the local date) rather than reusing the UTC-based date string. Everything else looks solid: the _fix_autosize_fields -> _fix_text_appearances change is well-scoped (font size read from template, box-height clamping, LibreOffice parent-node /T lookup, missing-/AP handling) and is documented as value-neutral with passing snapshots for existing forms; the | -alternation fix for enum checkboxes correctly addresses a real last-value-wins overwrite bug when two enum values map to the same box; lfrm.json follows the documented mapping schema (scope, email_overrides, literal-index person fields for the irregular Row{n}_2/Row{n} naming) and preview.py additions match the checklist in designs/form-system.md. |
A form declaring time_reference "local" had its times converted to the airport's wall clock while its dates stayed on the UTC calendar day. A late-evening UTC slot falls on the next day locally — 23:50Z on 1 June is 01:50 on 2 June in Paris — so the form printed "01/06 at 01:50", misdating the flight by a day on a customs pre-notification. Convert each date/time pair together. Affects the ARRIVAL_DATE / ARRIVAL_LOCAL_TIME and DEPARTURE_DATE / DEPARTURE_LOCAL_TIME pairs that lfrm.json fills, and the same pairs in french_customs, which has shipped this way for the whole LF prefix. The conversion moves to a shared fillers/_datetime.py, replacing the two copies that had drifted apart in intent. Regression tests cover the midnight roll, a same-day conversion that must not shift, and a UTC form that must not convert at all. Reported by the PR review bot on #15. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015cLZEJUmiCnbxfytMKN18e
|
Good catch on the midnight roll — confirmed and fixed in 55bf449. Reproduced on Fixed by converting each date/time pair together rather than exposing a local date from the time helper — same result, but it makes it structurally hard to convert one without the other, since the caller gets both back or neither. One thing beyond what the review flagged: Regression tests added for three cases: the midnight roll, a same-day conversion that must not shift the date, and a UTC-reference form ( 157 tests pass; no existing snapshot moved. |
Code ReviewReviewed the LFRM form addition and the shared filler changes it surfaced. Overall solid: the appearance-stream fix is well targeted at the described LFQA clipping bug, Two minor gaps worth a follow-up (not blocking):
🤖 Generated with Claude Code |
Both follow-ups from the review on #15, neither of which changes behaviour. Snapshot tests compare field values only, so nothing in the suite noticed whether a value was drawn at a size that fits its box — the clipping the appearance logic exists to prevent. TestTextAppearances reads the font size and baseline back out of the generated appearance streams and asserts every value lands inside its rectangle, plus the specific cases: an oversized fixed size is shrunk, a fitting one is left as authored, and an auto-size field still caps at 12pt. Verified these fail against the pre-fix behaviour. Design doc: add lfrm to the form inventory and the architecture tree, document the arrival.remote/departure.remote triple and the "|" enum alternation, and record three things this form cost time to learn — LibreOffice's parent-node /T and missing appearance streams, font size having to fit the box, and date/time converting together. Also corrects the lfqa inventory row, which said 3 airports where the icao_list has 11. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015cLZEJUmiCnbxfytMKN18e
Adds the airport-specific préavis douane for LFRM, and fixes a rendering bug in the shared PDF filler that it surfaced.
The form
lfrm.json+lfrm_customs.pdf, scoped toicao: LFRM, 4 crew / 7 passenger rows, routed topreavis-vols-douane-le-mans@douane.finances.gouv.frcccodt-idf@…— the same routing the genericfrench_customsentry already used for LFRM. That generic form stays available as a second option for LFRM; no other airport's form list changes.The template asks for Heure Locale, so the mapping declares
time_reference: "local"withEurope/Paris.Two quirks of the template are handled in the mapping rather than by re-authoring the PDF:
"X"viacheckbox_on…Row{n}_2for rows 1–4 and plain…Row{n}for rows 5–7, which no single{n}pattern spans, so they use the filler's literal-index formThe filler fix
_fix_autosize_fieldsonly rebuilt appearance streams for auto-size (0 Tf) fields. Fields with a fixed size were left alone even when that size overflowed a short box — pypdf then places the text baseline below the box and the value renders clipped.This is already affecting LFQA in production: "Nature du vol: private" is cut off on every form generated today. Confirmed by rendering the current
mainoutput.The function now rebuilds the appearance whenever the declared size doesn't fit, clamped to the box height, and additionally handles two things LibreOffice-authored templates do that the old code skipped: widgets whose
/Tlives on a parent field node, and fields with no appearance stream at all (previously left to whatever the viewer chose to draw).Font sizes are read from the template, because filling rewrites an inherited auto-size
/DAto a concrete12 Tfon the field node — the same check against the filled document finds nothing.Value-neutral: no existing snapshot changed; only the two new LFRM snapshots were created. The change is purely in how text is drawn.
Also in the filler
Three additions, none used by any existing mapping:
time_reference: "local"arrival.remote/departure.remote(+_name,_country) for the Provenance / Aéroport / Pays triple, filled only on the side matching the flight direction|alternation on enum checkboxes (flight.nature.private|business). Without it, mapping two nature values to the same field would have the second silently overwrite the first with "off"Testing
153 passed. The 2
flattenfailures are pre-existing — verified they fail on a clean tree too.Previews visually verified in both directions with self-describing values;
ORIG/OriginCity/OriginCountryon arrival andDEST/DestCity/DestCountryon departure land in the right boxes, and only the active direction's column fills.Note for review
The lower rows (Provenance / Aéroport / Pays / Nature du vol) render at ~7.6pt vs 12pt above, because those boxes are progressively shorter and sit slightly below their printed labels. Nothing is clipped and it's legible. Making those boxes ~14pt tall in LibreOffice would even it out — deliberately not done here as a re-export would discard any geometry edited outside the source document.
🤖 Generated with Claude Code
https://claude.ai/code/session_015cLZEJUmiCnbxfytMKN18e