Commit Graph

976 Commits

Author SHA1 Message Date
gsinghpal
66e9749853 feat(fusion_clock): Web NFC integration on kiosk page
Adds NDEFReader scan loop, onNfcReading tap dispatcher, handleTap
state machine, postJson helper, capturePhoto stub (Task 17), and
setup wizard activation with error display.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 01:22:55 -04:00
gsinghpal
c9be68a575 feat(fusion_clock): NFC kiosk JS scaffold + state machine + clock display
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 01:20:31 -04:00
gsinghpal
19d692afe7 feat(fusion_clock): NFC kiosk QWeb template with static chrome + setup wizard
Replace placeholder template with full version: static chrome (company,
clock, date, location, settings button), one-time setup wizard state,
hidden video/canvas for camera, and data-* attrs for JS feature flags.
Update test assertion from h1 text to nfc_kiosk_root id to match new markup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 01:18:04 -04:00
gsinghpal
0351dcd497 feat(fusion_clock): NFC kiosk SCSS (always-dark, high-contrast)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 01:14:14 -04:00
gsinghpal
03fd3d7c1c feat(fusion_clock): NFC kiosk employee search endpoint
Add /fusion_clock/kiosk/nfc/employee_search that delegates to the
existing kiosk_search method, avoiding logic duplication. Adds
TestEmployeeSearch HttpCase (33 tests total, all passing).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 01:11:28 -04:00
gsinghpal
f4c9ed3d24 feat(fusion_clock): NFC tap photo capture + photo-required gate
- Add _strip_data_url_prefix() helper to clean data-URL prefix from base64 photo payloads
- Gate nfc_tap on fusion_clock.nfc_photo_required ICP param (default True): rejects with error='photo_required' when photo absent
- Write x_fclk_check_in_photo / x_fclk_check_out_photo on clock-in/out attendance records
- Add TestTapPhotoHandling (3 tests): photo saved, required-rejects-missing, optional-succeeds-without

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 01:09:27 -04:00
gsinghpal
ef885c66dc feat(fusion_clock): NFC tap endpoint debounce + 6 error-case tests
Adds module-level 5s debounce (_is_debounced) with thread-safe dict +
GC. Inserts debounce guard in nfc_tap immediately after uid validation.
Adds TestTapEndpointErrors (6 tests): unknown_card, clock_disabled,
no_location_configured, kiosk_disabled, invalid_uid, debounce.
Adds setUp() to both tap test classes to clear _recent_taps between
tests, preventing cross-test debounce bleed. 29/29 pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 01:06:30 -04:00
gsinghpal
148aa5cba8 feat(fusion_clock): NFC tap endpoint (happy path)
Add /fusion_clock/kiosk/nfc/tap JSON-RPC endpoint that toggles attendance
via _attendance_action_change, writing x_fclk_clock_source='nfc_kiosk' and
location on clock-in, applying break deduction/penalty checks on clock-out.
Add 2 HttpCase tests (clock-in + clock-out with 6s debounce sleep).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 01:02:49 -04:00
gsinghpal
661c8ae227 feat(fusion_clock): NFC card enrollment endpoint
Adds /fusion_clock/kiosk/nfc/enroll (jsonrpc, auth=user) that validates
the enroll password, normalises the card UID, checks for duplicate
assignments, writes x_fclk_nfc_card_uid, and creates a card_enrollment
activity log entry. 4 new tests; 21 total passing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 00:58:25 -04:00
gsinghpal
a24a1ddf1a feat(fusion_clock): NFC card UID normalization helper
Add _normalize_uid static method to FusionClockNfcKiosk that strips
whitespace, uppercases, removes separators, validates hex-only content,
and reformats to canonical colon-separated pairs; returns None for
empty/invalid input. Covered by 7 new TransactionCase unit tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 00:56:02 -04:00
gsinghpal
f05cacec22 feat(fusion_clock): NFC kiosk page render route
Controller scaffold with GET /fusion_clock/kiosk/nfc, placeholder QWeb
template, and HttpCase tests (10 pass, 0 failures). Fixed Odoo 19
res.users create API: groups_id -> group_ids.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 00:53:03 -04:00
gsinghpal
9239ee2822 feat(fusion_clock): add NFC Clock Kiosk settings block
Extends res.config.settings with 5 NFC kiosk fields (enable toggle,
photo required, enroll password, debug mode, kiosk location via
related company field) and adds the corresponding settings view block
with conditional sub-fields hidden until the kiosk is enabled.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 00:45:41 -04:00
gsinghpal
4733885211 feat(fusion_clock): add NFC kiosk ir.config_parameter defaults
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 00:43:07 -04:00
gsinghpal
8e708bf2c4 fix(fusion_clock): NFC kiosk location domain + test isolation
Add domain filter on x_fclk_nfc_kiosk_location_id so the dropdown
only shows locations belonging to the current company in multi-company
setups. Replace shared-company mutation in test with a fresh company
to prevent cross-test state leakage.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 00:40:28 -04:00
gsinghpal
caf240daec feat(fusion_clock): add NFC kiosk location to res.company
Adds x_fclk_nfc_kiosk_location_id (Many2one → fusion.clock.location) to
res.company so each company can designate which NFC kiosk location it uses.
Two tests cover field assignment and default-false behaviour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 00:36:56 -04:00
gsinghpal
4bed8ab2c5 fix(fusion_clock): reorder NFC kiosk source before System per plan
Move ('nfc_kiosk', 'NFC Kiosk') to sit between kiosk and system in the
source Selection field, matching the spec's semantic grouping of
interactive sources before the automated system source.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 00:34:36 -04:00
gsinghpal
50c209b8d3 feat(fusion_clock): NFC kiosk attendance fields + activity-log selections
- Add 'nfc_kiosk' to x_fclk_clock_source selection on hr.attendance
- Add x_fclk_check_in_photo and x_fclk_check_out_photo Binary fields (attachment=True)
- Add 'card_enrollment' and 'unknown_card_tap' to activity log log_type selection
- Add 'nfc_kiosk' to activity log source selection
- Add TestNfcAttendanceFields test class (3 tests); all 6 fusion_clock tests pass

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 00:24:35 -04:00
gsinghpal
65a1c4b17e fix(fusion_clock): remove unused ValidationError import in NFC tests 2026-05-14 00:19:20 -04:00
gsinghpal
91d3a3f9d1 docs(fusion_clock): use actual docker env names (odoo-modsdev-app/modsdev) in NFC plan 2026-05-14 00:14:51 -04:00
gsinghpal
70f855d91b feat(fusion_clock): add x_fclk_nfc_card_uid to hr.employee
Adds the NFC card UID field (Char, unique, manager-only) that the kiosk
will use to identify employees by card tap. Includes the tests package
with three post-install tests covering write, uniqueness, and nullable
multi-row behaviour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 00:13:26 -04:00
gsinghpal
85eddba546 docs(fusion_clock): NFC clock kiosk implementation plan
20-task TDD plan for the NFC clock kiosk feature spec'd in
2026-05-13-nfc-clock-kiosk-design.md. Bite-sized steps with full code
in each, ordered: data model -> config -> backend endpoints ->
SCSS+template -> JS state machine -> NFC + camera -> Enroll Mode ->
debug shortcut -> version bump.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 00:05:23 -04:00
gsinghpal
48d3e48e61 docs(fusion_clock): NFC clock kiosk design
Design for tap-to-clock NFC kiosk in fusion_clock. Pilot scope: 1
station per company, Samsung Galaxy Tab Active 5 Pro running Web NFC
in Chrome kiosk mode. Reuses Ubiquiti-issued cards. Silent photo
verification via front camera. Backend reuses FusionClockAPI helpers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 23:50:00 -04:00
gsinghpal
f07e1bcce1 fix(chatter): wrap HTML message_post bodies in Markup() — 4 sites
Four message_post calls were passing strings with HTML tags as
plain `body=_(...)` instead of `body=Markup(_(...))`. Odoo escapes
non-Markup strings, so the chatter rendered "<b>QA Review failed</b>"
as literal text instead of bolding it.

Original bug surfaced via the Contract Review (QA-005) flow:
  body: "&lt;b&gt;QA Review failed&lt;/b&gt; by Garry Singh. Awaiting
  client information.&lt;br/&gt;&lt;b&gt;Reason:&lt;/b&gt;&lt;br/&gt;
  &lt;div data-oe-version=\"2.0\"&gt;Need to get updated
  drawing...&lt;/div&gt;"

Audit scan turned up three more identical patterns:

  fusion_plating/models/fp_parent_numbered_mixin.py:118
     "Issued <strong>%s</strong> to ..."
  fusion_plating_jobs/models/sale_order.py:282
     "Confirmed quote <strong>%s</strong> as <strong>%s</strong>."
  fusion_plating_quality/models/fp_contract_review.py:430
     "<b>QA Review failed</b> by ... <b>Reason:</b><br/>%(reason)s"
  fusion_plating_quality/models/fp_contract_review.py:524
     "<b>QA Review completed</b> by ... <b>Special Instructions
      captured:</b><br/>%(notes)s"

Fixes:
- Wrapped each body=_(...) with Markup(_(...)) using the
  Markup(template) % values pattern (auto-escapes the substituted
  values; user-supplied free text stays safe).
- For Html-field substitutions (qa_failure_reason,
  special_instructions), explicitly wrapped the value in Markup()
  so already-formatted HTML editor content (with data-oe-version="2.0"
  wrapper divs) flows through without being re-escaped.
- Added `from markupsafe import Markup` to the two files that
  didn't already import it (mixin + contract_review).

Drift cleanup: pulled the 180-line newer fp_contract_review.py
from entech to the local repo (added action_qa_review_failed,
action_open_client_email_wizard, action_view_client_emails,
action_complete_after_info, awaiting_info state, qa_failure_reason
+ special_instructions Html fields, etc. that had been edited on
entech without being committed).

Tested by re-posting via odoo shell on review 10: body now stores
"<b>QA Review failed</b>..." with literal HTML tags instead of
the double-escaped "&lt;b&gt;..." entities. Old chatter records
with the bad escape stay as-is in the audit trail.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 08:41:39 -04:00
gsinghpal
e7c6960de9 feat(sticker): restore customer-name secrecy cover (ABC-MANU)
Body Customer row now prints a 3-and-4 short code instead of the
full company name. Operators see "ABC-MANU" on the floor; visiting
customers / unauthorised passers-by can't immediately tell whose
parts are on which rack.

Rule (per user's reference design):
  - First 3 chars of first word + "-" + first 4 chars of second word
  - Single-word names → just first 3 chars
  - All uppercase
  - Strips non-alphanumeric per word so "St. John's Mfg." doesn't
    leak punctuation into the slice

Logic lives in the shared inner template, so all 4 variants pick
it up automatically:
  sale.order     External + Internal Sticker
  fp.job         External + Internal Job Sticker

Verified on fp.job 2635: Customer row now reads "ABC-MANU" (was
"ABC Manufactoring").

Doesn't use the orphaned x_fc_short_code field on res.partner
(that field has no column or compute — broken Studio remnant).
A future spec can replace this inline computation with a proper
stored+inverse field if customers want per-partner overrides.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 08:24:07 -04:00
gsinghpal
ad64b0b4c9 changes 2026-05-13 08:17:35 -04:00
gsinghpal
cd763fa1d7 chore(sticker): rename External action labels for the variant split
Print menu now shows External + Internal as paired entries:

  sale.order:  External Sticker      / Internal Sticker
  fp.job:      External Job Sticker  / Internal Job Sticker

XML IDs unchanged (action_report_fp_so_sticker /
action_report_fp_job_sticker) so existing bookmarks and
binding_model_id records keep working. print_report_name strings
also updated so the downloaded filename matches the new label.

DB verification:
  fp.job      | External Job Sticker
  fp.job      | Internal Job Sticker
  sale.order  | External Sticker
  sale.order  | Internal Sticker

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 08:07:50 -04:00
gsinghpal
f40f44aafd feat(sticker): add Internal Job Sticker variant on fp.job Print menu
Mirror of the SO Internal variant for fp.job. Same body fields,
same per-box loop; Notes column reads x_fc_internal_description
from the first linked SO line (job.sale_order_line_ids[:1]).
Operator on the shop floor sees ops-internal notes without those
ever appearing on the customer-facing External sticker.

Verified on fp.job 2635 with seeded internal_description: Notes
column reads "INTERNAL JOB: handle with care, no rework on this
batch" — confirms the Job Internal variant's override path mirrors
the SO Internal variant's.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 08:06:12 -04:00
gsinghpal
63bf271725 feat(sticker): add Internal Sticker variant on sale.order Print menu
Same 3-cell + body layout as External; Notes column reads
x_fc_internal_description (Sub 2 internal-description field on the
SO line) instead of line.name. Shop floor gets ops-facing notes
without leaking them to the customer-facing variant.

New action record action_report_fp_so_sticker_internal — binds to
sale.order, appears in the Print menu next to the existing External
sticker. New template report_fp_so_sticker_internal that pre-sets
_notes_content before t-calling the shared inner.

Verified on SO-30019 with a seeded internal_description: Notes
column reads "INTERNAL: rework if any dings on flange. Buff per
WI-104." — confirms the override path is wired through the
defaults-block initialiser, the inner's fallback chain, and the
new outer template.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 08:04:29 -04:00
gsinghpal
974b8a5152 feat(sticker): wire _qty_total in SO + Job External outers
Activates the per-box loop landed in the prior commit. SO External
reads line.product_uom_qty; Job External reads job.qty. Inner
template now renders one sticker per physical box, marking each
with "X / N" in the Qty row.

Verified on fp.job 2635 (qty temporarily set to 3): 3-page PDF
with Qty rows "1 / 3", "2 / 3", "3 / 3" — each page identical
otherwise (same WO#, same QR, same body fields).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 08:02:32 -04:00
gsinghpal
0a32ed2da7 feat(sticker): per-box render loop + Notes override hook
Inner sticker template gains two parameters that outer templates
pre-set:

  _qty_total — total qty for the line/job. Inner wraps the body
    in t-foreach="range(int(_qty_total or 1))" so a qty=5 line
    produces 5 consecutive single-box stickers. Qty row in the
    body switches from "5" to "1 / 5", "2 / 5", ... "5 / 5".
    When _qty_total is missing/0/1, the Qty row keeps showing
    the plain integer (regression-free).

  _notes_content — Notes column source. Existing inner code
    hard-read _line.name; new code accepts an outer override
    and falls back to _line.name. External outers don't set it
    (unchanged behaviour); the new Internal outers (Task 4+5)
    pre-set it to x_fc_internal_description.

Defaults template initialises both new vars to False so the
inner's "outer-supplied OR fallback" pattern doesn't NameError
when called from existing outers that haven't been updated yet.

Verified regression-free: fp.job 2635 (qty=1) renders identically
to its pre-Task baseline — Qty row shows plain "1", Notes from
line.name as before.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 08:00:22 -04:00
gsinghpal
e4681a58c6 fix(jobs): split fp.jobs by thickness + serial on SO confirm
The _fp_auto_create_job grouping key was (recipe, part, coating).
Lines that shared all three but differed in thickness (or serial)
silently collapsed into one fp.job — the second line's thickness/SN
was lost, and any downstream cert printed the first line's values
across both batches. Silent mis-attestation = compliance hole.

Extended the key tuple to (recipe, part, coating, thickness, serial).
Single-line SOs and same-(thickness, SN) multi-line SOs collapse
identically to before. Only lines that previously merged when they
shouldn't have now split into their own fp.jobs.

TDD via test_so_confirm_splits_by_thickness:
  - seeds the part with default_process_id so both lines hit the
    `if recipe:` branch (where the bug lived — the no_recipe branch
    already split correctly per line)
  - confirms 2 jobs after action_confirm with each carrying its
    own thickness via the linked SO line

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 07:57:56 -04:00
gsinghpal
135cbd3a5c docs: implementation plan — sticker multi-part / per-box / Internal+External
7 tasks, bite-sized steps with exact code + commands. TDD on the
backend grouping change (new test_so_confirm_splits_by_thickness);
deploy-and-render-PDF on the QWeb template changes. Each task
self-contained, pushes to entech LXC 111 via the standard pct
exec + cat-pipe path, bumps the module version, and commits.

Task 7 is verification-only — creates a multi-line test SO with
two different thicknesses, renders External + Internal stickers
on both the SO and each spawned fp.job, confirms the box loop
and the Notes variant pattern both work.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 07:47:40 -04:00
gsinghpal
3182ca3c39 docs: design spec — sticker multi-part / per-box / Internal+External
Three problems on the box-sticker stack rolled into one spec:

1. Backend: _create_fp_jobs grouping key collapses lines with
   different thicknesses or SNs into one job. Silent compliance
   hole. Fix: add thickness_id + serial_id to the key tuple.
2. No per-box stickers: a line with qty=5 prints 1 page showing
   "Qty: 5". Want 5 pages with "1 / 5", "2 / 5", ... "5 / 5".
3. No Internal variant: sticker always reads line.name (customer
   facing). Want a parallel variant that reads
   x_fc_internal_description (Sub 2 internal description field).

Renaming: existing actions keep their XML IDs (bookmarks /
binding_model_id records survive). Labels become:
  sale.order:  External Sticker      + Internal Sticker      (new)
  fp.job:      External Job Sticker  + Internal Job Sticker  (new)

All three changes share the same inner template, same files —
ship together. No data migration required; existing fp.jobs are
protected by the idempotency guard.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 07:41:53 -04:00
gsinghpal
677e460438 fix(sticker): wire SN # + Thickness to the correct Sub 5 fields
The values were structurally blank because the variable
resolution was reading the wrong field names:

  Was:  _line.x_fc_serial_number    (doesn't exist)
        _line.x_fc_thickness        (doesn't exist)
  Now:  _line.x_fc_serial_id.name           (M2O fp.serial)
        _line.x_fc_thickness_id.display_name (M2O fp.coating.thickness)

Sub 5 shipped these as Many2one registries (fp.serial,
fp.coating.thickness) — the sticker was guessing at flat
Char-field equivalents that were never created.

Verified on SO-30019: SN # now prints "65767", Thickness now
prints "0.3-0.5 mils" (the en-dash in display_name mojibakes
to "â€"" through wkhtmltopdf's font path on entech, so we
replace en-dash + em-dash with ASCII hyphen-minus before
render — ASCII-only is what label printers want anyway).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 07:25:42 -04:00
gsinghpal
c7b794f604 fix(sticker): drop SO-line sequence suffix + bump Notes type
SO sticker (report_fp_so_sticker):
  Was: "SO-30019 / 10"  (the "/ 10" was line.sequence — Odoo's
       default increment-by-10 — meaningless to the operator)
  Now: "SO-30019"
Multi-line SOs are disambiguated by the body fields (Part #,
Customer, etc.) which already differ per sticker, so the
suffix wasn't earning its keep.

Notes column size bumps:
- Label 44pt -> 48pt
- Content 30pt -> 36pt (+20%) — easier to read from across
  the line. Line-height tightened 1.15 -> 1.1 to keep the
  multi-paragraph wrap inside the body band.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 07:21:09 -04:00
gsinghpal
64c61dcca8 feat(sticker): much bigger text + QR +30%
wkhtmltopdf renders CSS font-size at a smaller physical scale
than the em-square math predicts (a "30pt" cell text was only
~4mm tall visually). Pushing all type up significantly so it
actually reads at scan/print distance:

Text bumps:
- Body field text 30pt -> 50pt (+67%, label + value)
- WO# 56pt -> 72pt (+29%)
- Notes label 30pt -> 44pt
- Notes content 22pt -> 30pt (+36%)
- Muted rev tag 22pt -> 30pt
- Body cell padding 0 10px -> 0 8px (a touch more horizontal
  room for long values now that the font is bigger)

QR + 30% as asked:
- Wrapper 280 -> 365px (+30.4%). Image 368 -> 480px, offset
  -44 -> -58px (recomputed for the new quiet-zone crop).

Header re-balanced for the bigger content:
- Height 25% -> 32% (fits the +30% QR + bigger WO# + bigger
  logo at 135px)
- Body band: 75% -> 68% (rows now ~9.6mm tall; line-height
  1.0 keeps the 50pt body text snug inside)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 23:48:40 -04:00
gsinghpal
649b75d4a1 feat(sticker): bigger field labels + values + notes text
Trimmed the header from 30% to 25% of page height to free up
vertical room for the body band's 7 rows. Each row is now
~10.45mm tall (was 9.88mm), so the field font fits comfortably
at the bigger size.

Size bumps:
- Body field text 26pt -> 30pt (label + value, +15%)
- Muted rev tag 18pt -> 22pt
- Notes label 26pt -> 30pt
- Notes content 19pt -> 22pt (+16%, wraps cleanly to 2 lines
  when the customer description runs long)

Header re-fit (smaller cells, same content):
- Header height 30% -> 25%
- WO# font 62pt -> 56pt
- Logo max-height 135 -> 105px
- QR wrapper 340 -> 280px (image 447 -> 368px, offset -53 ->
  -44px to keep the quiet-zone crop math right)
- High-def 600x600 QR source unchanged — still prints crisp
  at the smaller wrapper size

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 23:41:17 -04:00
gsinghpal
8aa817b1a0 feat(sticker): bigger text, bigger high-def QR, drop "WO #" prefix
WO# cell now just renders the number (e.g. "WO-30019") since the
"WO" is already baked into the doc index format — the redundant
prefix was eating cell width without adding information.

Size bumps:
- WO# 44pt -> 62pt (text is shorter so the cell can carry the
  extra weight)
- Body field text 22pt -> 26pt, line-height 1.1 -> 1.0 so the
  bigger font still fits 7 rows in the body band
- Notes label 22pt -> 26pt, content 16pt -> 19pt
- Logo max-height 120 -> 135px
- Muted rev tag 16pt -> 18pt

QR upgrades (both "bigger" and "high def" as asked):
- Source resolution 300x300 -> 600x600. At 300dpi print across
  a 28.8mm wrapper, effective output is ~515ppi vs the prior
  ~256ppi. Scanners on the floor will read it cleanly even at
  steeper angles / scuffed labels.
- Wrapper 290 -> 340px (+17%). Image 390 -> 447px, offset -50
  -> -53px (recomputed quiet-zone crop: 600 * 0.12 = 72px
  margin -> 456px effective QR data -> 340 * 600/456 = 447
  scaled image -> (447-340)/2 = 53px offset).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 23:38:05 -04:00
gsinghpal
80d1cc5639 feat(sticker): 3-cell header + right-side Notes column + new field list
Restores the original ENTECH sticker layout from the operator's
screenshot reference:

Header (3 horizontal cells, divided by vertical rules):
  [Logo]  |  WO #WO-30019  |  [QR]

Body (left side = field table, right side = Notes column):
  PO #:        587854         | Notes:
  SN #:        -              | <customer-facing description>
  Customer:    ABC Manufact.  |
  Part #:      9876... Rev A  |
  Due Date:    May 17, 2026   |
  Thickness:   -              |
  Qty:         1              |

Changes from previous (stacked-left) layout:
- Header: 1-row 3-cell (Logo 28% | WO# 44% | QR 28%) replaces
  the 2-cell w/ logo+WO# stacked on left.
- Body: 2-region (66% / 34%) replaces single 7-row table.
  Notes column now spans full body height on the right.
- Fields: SN # and Thickness added; Process row removed.
- Labels: "PO (RO)" -> "PO #", "Part Number" -> "Part #".
- Notes content: switched from SO.x_fc_internal_note to the SO
  line's `name` (= customer-facing description per Sub 2 Q6).
- SN # reads _line.x_fc_serial_number (Sub 5 field).
- Thickness reads _line.x_fc_thickness with coating.thickness
  fallback (Sub 5 field, defensive 'in _fields' check).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 23:33:18 -04:00
gsinghpal
2db789d7dd feat(sticker): bigger QR + double-height Notes row
Both changes the operator asked for, applied to the original
ENTECH stacked-left layout (no other structural changes):

- QR wrapper 380px → 460px (image 510px → 620px, offset -65 → -80
  to keep the white quiet-zone cropped). Roughly +21% surface area.
- Notes row height 14.28% → 24% (~2x). Other 6 rows shrink
  proportionally from 14.28% to 12.67% each so the band still
  totals 100%. Notes value also gets white-space: normal +
  vertical-align: top so the operator's handwriting room sits at
  the top of the cell and a long internal note can wrap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 23:25:37 -04:00
gsinghpal
7a02382623 fix(reports): WO Margin model name must match report_name + '_template' suffix
The previous fix swapped t-field -> t-esc so the QWeb error stopped,
but the report still printed blank. Root cause: Odoo looks up the
report data model via env['report.<report_name>'], but our model was
named 'report.fusion_plating_jobs.report_fp_job_margin' while the
action's report_name is 'fusion_plating_jobs.report_fp_job_margin_template'.
The model lookup missed, _get_report_values never fired, and the
template rendered with no 'rows' in scope — empty foreach -> empty
page.

Renamed the model to report.fusion_plating_jobs.report_fp_job_margin_template.

Verified: PDF size jumped from 1229 bytes (blank) to 125880 bytes
(fully populated). HTML now contains 'Job Margin', 'Step Breakdown',
and the actual WO name.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 19:08:38 -04:00
gsinghpal
169e97af02 feat(nexa_coa_setup): analytic plans + seed accounts
- 'Customer Project' plan (renamed from 'Project' to avoid duplicate with
  project module's auto-created plan) — mandatory
- 'Department' plan (mandatory) — seeded with DEPT-DEV, DEPT-SALES,
  DEPT-ADMIN, DEPT-HOSTING
- 'SR&ED Tag' plan (optional) — seeded with 7 tag values:
  SRED-T4-DEV-SALARY, SRED-SPECIFIED-EMPLOYEE,
  SRED-CONTRACTOR-CA-ARM-LENGTH, SRED-CONTRACTOR-CA-NON-ARM-LENGTH,
  SRED-MATERIALS-CONSUMED, SRED-OVERHEAD-PROXY-BASIS, NOT-ELIGIBLE

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 18:53:21 -04:00
gsinghpal
3c959771ae feat(nexa_coa_setup): pre_init_hook to clear l10n_ca code collisions
Bakes the staging-side one-off collision clearing into the module install
itself so production install will execute the same sweep automatically.

For each of the 29 l10n_ca codes that conflict with Nexa's planned chart:
- If the account has zero postings: suffix code with '.OLD', mark inactive,
  rename to '(l10n_ca LEGACY) <original>'
- If the account has postings (currently 115100 AR control with 240 lines
  and 511100 Inside Purchases with 1 line): leave alone (Nexa renumbered
  to 119100 / 511105 in the XML)

Idempotent — pre_init_hook re-running has no effect (already-suffixed
codes are skipped).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 18:51:25 -04:00
gsinghpal
449f29fc7f fix(reports): WO Margin PDF — t-field requires dot-notation on Odoo 19
The template used 't-field="step['rate']"' for monetary values pulled
from dict rows. Odoo 19's QWeb asserts t-field has at least one dot
(it's strictly for record.field_name lookups). Replaced six bare-dict
t-field usages with t-esc; the existing t-options widget=monetary +
display_currency still applies for currency formatting.

Verified by rendering report for WO-30019 — 1229-byte valid PDF, no
QWeb error.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 18:51:17 -04:00
gsinghpal
3c2fb22346 feat(nexa_coa_setup): chart of accounts — 128 accounts across 1-6xxxxx
Renumbered to avoid collisions with pre-loaded l10n_ca codes:
- Due From Shareholder/Associated: 115xxx → 119xxx range (115100/115110 already
  held l10n_ca AR control accounts with 240 postings)
- Cloud Infrastructure: 511100 → 511105 (511100 was l10n_ca 'Inside Purchases'
  with 1 historical posting)

All other 28 colliding l10n_ca codes (118xxx, 213xxx, 214xxx, 221xxx, 311xxx,
411xxx, 413xxx, 511110-511210, 512100-512200, 611100-300, 612xxx) had zero
postings and were cleared in-place by suffixing existing codes with '.OLD'
via a one-off odoo-shell script on staging.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 18:50:00 -04:00
gsinghpal
3a41370189 fix(nexa_coa_setup): tolerant fiscal-year lock hook
The post_init_hook attempt to set fiscalyear_lock_date=2025-12-31 fails
with RedirectWarning when unreconciled bank statement lines exist in
the period. Catch RedirectWarning/UserError/ValidationError, log a
clear instruction to set the lock manually after reconciliation, and
let install continue.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 18:41:15 -04:00
gsinghpal
d6513ff7ab feat(nexa_coa_setup): module skeleton with hooks stub
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 18:39:24 -04:00
gsinghpal
457d9b7dbf fix(numbering): post-review fixes — credit notes, SO unlink, multi-part grouping, SQL whitelist
- B1: Add Credit Note wizard path was blocked because invoice_origin
  has copy=False and the wizard doesn't set fp_from_so_invoice. Now
  the validator allows reversals when reversed_entry_id points at a
  customer-facing move that itself went through the validator at
  original creation time. account.move._fp_parent_sale_order also
  walks self.reversed_entry_id._fp_parent_sale_order so the credit
  note inherits the parent number (CN-<parent>).

- Bug 1: sale.order.unlink() now blocks deletion when x_fc_parent_number
  is set (matches spec §6.2). Draft quotes remain freely deletable
  per Odoo standard. Applies to all users including admins.

- Bug 2: out_receipt added to CUSTOMER_TYPES so POS-style receipts
  hit the same SO-flow gate as out_invoice / out_refund.

- C1: WO grouping key changed from recipe.id to (recipe.id, part.id,
  coating.id). Bundling lines with different parts under one WO put
  first_line's part_number on the CoC header — silent compliance
  mis-attestation. Now distinct parts always get distinct WOs even
  when they share a recipe.

- C3: SQL whitelist (_FP_COUNTER_FIELD_RE) on _fp_assign_parent_name's
  interpolated counter field name. No user input today; defence in
  depth for future subclasses that might read the name from context.

Verified on entech: parent=30017, credit note = CN-30017,
multi-part SO produces 2 WOs (one per part), confirmed-SO unlink
blocked, out_receipt blocked, whitelist regex enforced.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 18:19:08 -04:00
gsinghpal
c85a9bbf82 docs: nexa_coa_setup implementation plan
Bite-sized task plan to implement the CoA design against odoo-nexa
nexamain database. 12 phases:
0. Safety backup + staging clone
1. Module skeleton (nexa_coa_setup)
2. Chart of accounts (~110 new accounts across 1-6xxxxx)
3. Analytic plans (Project, Department, SR&ED Tag)
4. Hooks for archive-unused / rename-legacy
5. Tax cleanup
6. 8 fiscal positions with auto-detect
7. Service product categories
8. Westin/Divine partner records (RP-Associated tag)
9. 8 bank reconciliation rules
10. End-to-end test invoices (ON, US, intercompany)
11. Apply to production (with explicit GO/NO-GO gate)
12. Operating runbook

Each task has a verify-before / change / verify-after / commit cycle.
Staging clone (nexamain_staging) used for every phase before prod.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 18:05:33 -04:00
gsinghpal
5b399fbdda fix(configurator): copy operator-input prompts when cloning recipe to part
_clone_subtree() in fp_part_composer_controller built node vals
manually and never copied source.input_ids — so 'Load Template'
copied the recipe tree structure but dropped every custom prompt,
leaving operators with empty data-capture screens. The fix iterates
input_ids and calls .copy({'node_id': new_node.id}) so kind,
target_min/max/unit, compliance_tag, hint, selection_options,
sequence — every field on the input model — flows through.

Verified on entech: ENP-ALUM-BASIC clone now shows all 105 prompts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 17:58:11 -04:00