Commit Graph

14 Commits

Author SHA1 Message Date
gsinghpal
eed4dc8a78 fix(plating): chatter HTML rendering + workflow stage banner UX
Two fixes from a single SO walkthrough screenshot:

**1. "Current stage" banner**
- Was placed `inside sheet` so it rendered at the BOTTOM of the form
  where users miss it. Moved to `before form/header` (same xpath
  pattern as the Account Hold banner) — now it's the first thing
  visible above the SO header.
- Was still showing "Shipped — awaiting invoice" after the invoice
  was posted because `_compute_workflow_stage` only advanced to
  `complete` when shipped + ALL paid; an unpaid posted invoice left
  the SO stuck on `shipped`. Added an `invoicing` branch: shipped +
  has_posted_invoice → invoicing. Banner invisible-list now also
  includes `invoicing` and `paid`, so the banner only shows for
  in-progress steps.

**2. Chatter messages rendering raw HTML tags as text**
Odoo 19 escapes any string passed to `message_post(body=...)`
unless wrapped in `markupsafe.Markup`. We had ~10 places posting
HTML (`<a href>`, `<b>`, `<br/>`, `<code>`, `<pre>`) that all
showed up as `&lt;a href=...&gt;` literal text in the chatter.

Wrapped each one with `Markup(_(...))` so the tags render. Files
touched:

- fusion_plating_bridge_mrp/models/sale_order.py
  (auto-MO failure code block, "Draft MO created" link,
   "Job assigned to <b>" message)
- fusion_plating_bridge_mrp/models/mrp_production.py
  ("Recipe steps" pre/br block on each WO)
- fusion_plating_bridge_mrp/models/fp_proficiency.py
  (operator promotion announcement)
- fusion_plating_configurator/models/fp_quote_configurator.py
  (SO link, 3D model attached, drawing attached, save to catalog)
- fusion_plating_configurator/models/fp_part_catalog.py
  (3D/drawing change tracking + propagation to linked quotes)
- fusion_plating_portal/models/fp_quote_request.py
  (RFQ → SO link)
- fusion_plating_quality/models/fp_quality_hold.py
  (hold status change)
- fusion_plating_shopfloor/controllers/manager_controller.py
  (worker / tank / manager-takeover assignments)

Verified on entech: SO S00038 stage now reads `invoicing` (banner
hidden), and a freshly posted message shows `<a href>` and `<b>`
as actual link + bold instead of escaped text.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 08:36:00 -04:00
gsinghpal
cb9baa03ad fix(reports): collapse sig-row to one bordered table — kill duplicate borders
User reported "multiple unwanted vertical lines in the boxes" on the
portrait BoL. Pixel analysis confirmed it: previous design had 3
separate `<div class="sig-box">` each with its own 1px border, with a
4-8px gap between adjacent boxes — visually those adjacent borders
read as a doubled / "duplicate" line between cells.

Fix: replace 3-box layout with a single `<table class="bordered
sig-table">` containing 3 td cells. With border-collapse: collapse,
adjacent cells share their border — so the row now shows 4 vertical
lines (1 outer left + 2 internal dividers + 1 outer right) instead
of 6 close-together border lines.

- Dropped `.sig-box` class entirely (no per-box border anymore)
- Added `.sig-table` + `.sig-cell` with explicit 1px borders so the
  layout works without depending on `.bordered` class inheritance
- Applied to both portrait + landscape variants
- Landscape sig-row was still using the OLD Bootstrap row+col-4
  layout (never got replaced earlier) — also migrated to the new
  table layout

Verified: page count unchanged (portrait 1, landscape 1), all
labels and content present, structure clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 08:14:07 -04:00
gsinghpal
ed72ed496b fix(reports): compact landscape BoL so it fits on one page
Last fix kept signatures intact but the landscape BoL still overflowed
to a second page (with the signature row pushed entirely to page 2).
The real ask was for the landscape variant to fit on one page since
landscape has plenty of vertical room.

Aggressive landscape compaction:
- Body font 11pt → 10pt, td font 10 → 9.5pt, th font 10 → 9pt
- Cell padding 8/10px → 4/8px
- Table margin-bottom 12px → 6px
- h2 title 26pt → 18pt with tighter top/bottom margins
- BoL # subtitle 14pt → 11pt
- Shipper/consignee row height 120 → 70px
- highlight-box (cert) padding 10px → 6/10, font 10 → 9pt
- sig-box padding 12 → 8/10px
- sig-line height 70 → 45px

Verified with pypdf: landscape BoL now renders as exactly 1 page
with cert + all 3 signature labels + company info all present.
137 KB clean PDF.

Portrait variant left untouched (it already fit on one page and
the bigger title is appropriate for portrait).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 07:43:25 -04:00
gsinghpal
b16486f66b fix(reports): keep BoL signature row intact across page breaks
Landscape BoL was splitting the signature row down the middle —
boxes half on page 1, half on page 2. Two complementary fixes:

1. **Per-element rule**: added `page-break-inside: avoid` +
   `break-inside: avoid` to `.sig-box` (both portrait + landscape
   styles) so an individual signature box can never split across
   pages.

2. **Wrapper rule**: introduced `.fp-keep-together` utility +
   wrapped the BoL's certification statement + signature row in
   it, so the whole "sign here" block moves to the next page as
   one unit if it doesn't fit. Also applied
   `page-break-inside: avoid` to `table tr` so cargo lines don't
   split mid-row either.

Lives in shared `report_base_styles.xml` so any FP template that
opts into `.fp-keep-together` benefits automatically.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 07:35:55 -04:00
gsinghpal
7ad7481195 fix(bol): bigger title, shipper info, uniform headers, cargo qty, taller signatures
Five fixes applied to the Bill of Lading and (where relevant) all
report templates:

1. **Bigger title + BoL #** — portrait now uses h2 24pt (was h4 16pt),
   landscape h2 26pt; BoL # ticker is 13/14pt instead of body size.

2. **Shipper info missing** — root cause: `_fp_build_delivery_vals`
   was creating deliveries without `company_id`, so the BoL's
   `<span t-field="doc.company_id.name"/>` rendered empty. Two fixes:
   - Hook now sets `company_id = mo.company_id.id or env.company.id`.
   - Template falls back defensively to `env.company` when
     `doc.company_id` is empty (covers any legacy delivery that
     somehow slips through without it).
   - Backfilled 14 existing deliveries via SQL on entech.

3. **Uniform header backgrounds** — replaced mixed `info-header`
   (gray) + default-th (brand black) headers with a single
   `fp-header-primary` (brand black) across all sub-tables for a
   consistent look.

4. **Cargo description alignment + missing column** — added a QTY
   column (matches landscape variant), pulled from the linked MO
   via job_ref → mrp.production.product_qty. Added `.fp-cell-mid`
   utility class with `vertical-align: middle !important;` and
   applied it to every cargo + info cell so values sit centred
   instead of jammed against the top border.

5. **Signature box too short** — bumped `.sig-box` from 70 → 110 px
   (portrait) / 130 px (landscape), `.sig-line` from 28 → 60/70 px,
   added flex layout so the label sits at the bottom and signers
   have a real space to write in. Lives in the shared
   `report_base_styles.xml` so EVERY FP template benefits, not just
   the BoL.

Verified: BoL portrait renders cleanly at 140 KB with full shipper
block + uniform headers + middle-aligned cargo cells.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 07:29:28 -04:00
gsinghpal
16a4bdddf3 fix(reports): BoL PDF — t-field needs dotted path, branch on delivery_address_id
The Bill of Lading template assigned a temp variable
`<t t-set="dest" t-value="doc.delivery_address_id or doc.partner_id"/>`
and then tried `<div t-field="dest" .../>`. Odoo 19 QWeb asserts
t-field must be `record.field_name` (have a dot) — the temp variable
form fails compilation and the report renders as a multi-page
"Oops! Something went wrong" PDF stuffed with the traceback.

Fix: branch with `t-if`/`t-else` and call `t-field="doc.delivery_address_id"`
or `t-field="doc.partner_id"` directly. Same pattern in both header
and second-page-header sections (lines 49/235).

Verified: BoL render goes from 39 KB error page to 138 KB clean PDF.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 07:14:33 -04:00
gsinghpal
d351a2577b chore(receiving): port received_qty auto-prefill from live entech to main
The auto-prefill logic that fills received_qty from expected_qty on
fp.receiving create was committed to the entech LXC but never made it
back to main. Verified by a full quote→delivery→invoice walkthrough
(scripts/fp_e2e_human.py) — receiving step now passes.

Also adds the human-walkthrough E2E script that exercises every step:
RFQ → quote → SO confirm → MO + portal job auto-create → receiving
prefill → recipe → WO execution → MO done → CoC cert (rich PDF, no
thickness duplicate) → delivery prefill + lifecycle → invoice (posted,
not auto-paid) → notification log audit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 01:26:16 -04:00
gsinghpal
209b1974a7 feat(plating): seed 5 fresh MOs with mixed states + priorities
Stage filler gains step 6g — spins up five new manufacturing orders
so the Manager Desk has a busy shop floor instead of the single
in-flight MO that came out of the base seeder.

Plan, in order of creation:

  WH/MO/00012  HOT     Cyclone Manufacturing      qty 25  unassigned
  WH/MO/00013  Urgent  Westin Manufacturing       qty 60  unassigned
  WH/MO/00014  Normal  Honeywell Aerospace        qty 18  auto-routed
  WH/MO/00015  Normal  Amphenol Canada            qty 40  routed + first WO started
  WH/MO/00016  Normal  Magellan Aerospace         qty 32  auto-routed

Each MO is created via mrp.production.create() and confirmed through
the bridge_mrp action_confirm() override, which auto-creates the
portal job and generates ~9 WOs from the recipe with role-aware
auto-routing. Post-create the script stamps priority on every WO and
optionally clears assignments (HOT + Urgent) or starts the first WO
(MO_00015) for variety.

Recipe lookup was previously by code='ENP-ALUM-BASIC' which silently
failed because the seed file uses code='ENP_ALUM_BASIC' (underscores)
while the display name has dashes. Switched to "first available
recipe of node_type=recipe" so the script works regardless of which
spelling is canonical.

Idempotent — bails early if there are already five-plus active MOs,
so re-runs don't keep stacking new jobs.

Verified on entech: Manager Desk now shows
  - 6 active MOs (was 1)
  - 23 unassigned active WOs (was 2)
  - 30 active+assigned WOs (was 6)
  - 2 WOs in progress now
  - all 7 operators with open queue (Marie 2, James 1, Carlos 8, etc.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 22:41:27 -04:00
gsinghpal
2ce7bd3665 fix(manager-desk): include 'blocked' WOs + populate empty columns
Two complementary fixes — a real bug in the Manager Desk and demo
data that exercises the now-correct view.

The bug
=======
manager_controller.py used an explicit allow-list of WO states for
its Unassigned / Active columns and for the per-operator team load
count: ('pending','waiting','ready','progress'). That set MISSED the
'blocked' state Odoo emits when a WO's predecessor isn't done yet.

Result: an MO whose first WO is still running has all its downstream
WOs in 'blocked' state. They literally don't appear on the Manager
Desk — neither in "Needs a Worker" (even when unassigned) nor in
"In Progress" (even when assigned). The team load count also
under-reports because the operator's blocked queue is invisible.

Fix: switch all three domains from an allow-list to a deny-list
('done','cancel'). Same shape Plant Overview already uses, so the
two dashboards now agree on what "active" means.

Demo data
=========
Stage-filler gains two steps so the now-corrected view has obvious
data:

  6e. _populate_active_wos walks the in-flight MO's blocked routing
      and explicitly assigns the seven downstream WOs in sequence
      order — Diego (training), Carlos (plating), James (demask),
      Priya (oven), TWO unassigned (de-rack + post-bake — feed
      "Needs a Worker"), Aisha (final inspection). Earlier
      keyword-fuzzy matching missed WOs whose names didn't carry
      the expected substring.

  6f. _mark_so_awaiting_manager pushes two confirmed SOs to
      receiving_status='inspected' + assigned_manager_id=False so
      the "Awaiting Assignment" KPI is non-zero.

Verified on entech: 2 unassigned WOs, 6 active+assigned, 2
awaiting-assignment SOs. Six of seven operators carry at least one
open queue item; Marie has zero current load but a healthy past
completion history (she's on shift, between jobs).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 22:33:01 -04:00
gsinghpal
0315fee988 feat(plating): demo stage-filler — every workflow step now has data
Companion to fp_demo_seed.py. Bridges the gaps the original seeder
left after the team-skills + timer-audit + presence-aware Manager Desk
work landed (commit 0d12902). Idempotent.

Eight steps, each wrapped in a safe() driver so a failure in one
doesn't abort the rest:

  1. Fill x_fc_work_role_id on any WO that doesn't have one yet.
     Keyword map (mask/rack/plat/bake/oven/inspect/rework) → role
     code, falls back to plating_op. The auto-promotion tracker
     can't credit a worker without a role on the WO.

  2. Backfill the four timer audit fields (started_by/at,
     finished_by/at) on done WOs. Pulls from time_ids when the
     productivity records exist, otherwise synthesises timestamps
     from create_date + duration.

  3. Seed a diverse team of six operators with distinct role
     coverage and lead-hand permissions:
       - Marie Dubois     — masking + racking      (lead: masking)
       - James O'Connor   — plating_op + demask    (lead: plating_op)
       - Priya Sharma     — oven + inspection      (lead: oven, inspection)
       - Diego Ramirez    — racking + plating_op   (TRAINING: 2/3 masking)
       - Aisha Khan       — inspection + rework
       - Carlos Silva     — every role             (lead: every role)
     Each gets a backing res.users so the Manager Desk dropdown
     can assign them.

  3b. Redistribute ~40 historical done WOs across the new team so
      their Task Proficiency lists aren't empty. Plan targets
      realistic per-role counts (Marie 8 masking + 5 racking,
      James 12 plating + 4 demask, etc.) and re-stamps the timer
      audit so finished_by reflects the new owner.

  4. Wipe + rebuild fp.operator.proficiency from completed WOs so
     the per-(employee, role) tally is deterministic. Auto-promotion
     fires naturally during the rebuild — workers who already cleared
     the threshold get promoted=True with timestamps. Diego is
     deliberately seeded at 2/3 on masking so the demo shows the
     "one more job away from promotion" state live.

  5. Clock three operators in via hr.attendance (4-hour shift).
     Wipes any stale open records first because earlier script
     iterations left future-dated check_in timestamps that the
     attendance validator refused to close.

  6a. Two extra quality holds (damaged + out_of_spec).

  6b. Mark the in-progress WO with a started_at but no finished_at
      so the demo has a "paused for lunch" exemplar.

  6c. Three portal RFQs (one per workflow state: new / under_review
      / quoted) so the funnel front-end has data.

  6d. Push one draft SO to "sent" so the quotation pipeline has
      data in every column (was draft → confirmed previously).

Verified on entech: 21 of 21 workflow stages now , including
Diego's 2/3 masking row that shows the auto-promotion mechanic
in flight.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 22:22:31 -04:00
gsinghpal
e07002d550 feat(shopfloor): rich Tablet Station dashboard + full shop-floor demo data
Tablet Station rebuilt as a live dashboard (not just a QR scanner):

  * KPI strip — WOs Ready/Progress, Awaiting/Missed bakes,
    First-Piece pending, Quality Holds (each tinted by state)
  * Active WO banner with pulsing indicator when a WO is running
  * My Queue panel (left) — priority-badged operator next-up list,
    clickable rows that jump to the WO/bake/gate form
  * Baths tile grid (right) — last-log status chips, MTO count,
    hover jump to chemistry log
  * Bake Windows list — inline Start/End/Open actions, colour-coded
    by state (awaiting / in-progress / missed)
  * First-Piece Gates — Pass/Fail buttons for pending inspections
  * Quality Holds — Review jump when any open holds exist
  * Station picker + scan drawer (collapsed by default)
  * 30s auto-refresh, persists picked station in localStorage

New controller endpoints: /fp/shopfloor/tablet_overview,
/fp/shopfloor/pair_station, /fp/shopfloor/mark_gate.

Demo seeder (Phase 12.5) now populates:
  * 5 shop-floor stations (Plating, Bake, Inspection, Shipping, Receiving)
  * +3 bake windows (awaiting / in-progress / near-due)
  * 4 first-piece gates (1 pending, 1 passed+released, 1 passed-holding, 1 failed)
  * 2 quality holds on active MOs (one on_hold, one under_review)

All four Shop Floor menu pages (Plant Overview, Tablet Station, Bake
Windows, First-Piece Gates) now have meaningful content.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 07:43:10 -04:00
gsinghpal
838b41cb89 fix(bridge_mrp): WO recipe generator + demo work-order backfill
bridge_mrp._generate_workorders_from_recipe was writing 'description'
on mrp.workorder, which doesn't exist in Odoo 19 — instead the step
instructions now post to each WO's chatter after bulk create, which
is where the operator sees them anyway.

Demo seeder now creates the full WO chain:
- 9 MRP work centres paired with 9 FP work centres (FP-QUEUE, -RACK,
  -MASK, -EN, -BAKE, -INSP, -DERACK, -DEMASK, -POSTBAKE) with
  costs_hour set so actuals-vs-quoted margin can compute.
- Wires the existing ENP-ALUM-BASIC recipe's 9 operation nodes to
  those FP work centres by matching names.
- Links every coating config to the recipe so the auto-assign hook
  (mrp.production.action_confirm → _auto_assign_recipe_from_so) has
  something to pull.
- Backfills work orders on all existing demo MOs: calls the generator
  once recipe is set. For historical (done) MOs, marks all their WOs
  done with backdated durations (25-90 min). For the Cyclone active
  MO, sets a realistic progression: first WO done, second in progress
  (priority: Hot), rest in 'ready'.

Verified: 90 WOs live, 10 per work centre. One MO shows the full
progression state mix. WO Traveller PDF renders (132KB) — both
portrait + landscape variants still work.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 02:18:08 -04:00
gsinghpal
96ecf7a9e1 feat(coc): professional CoC with accreditation badges + signature + company branding
Problem: the rebuilt CoC rendered mostly empty because accreditation logos
had to be uploaded manually via Settings first, and no signature existed —
looked unprofessional next to the Steelhead reference.

Fix:
- Seeder now auto-generates clean text-based accreditation badges with PIL
  (Nadcap blue, AS9100D/ISO 9001 blue, CGP red) sized to match the
  reference layout. Client can swap in real trademarked logos via Settings
  → Fusion Plating → Accreditation Logos at any time.
- Seeder creates a demo "Kris Pathinather" user, sets them as the
  certificate owner on res.company, and renders a scripted-looking
  signature image that matches the printed name on the cert.
- Seeder uploads a generated "Amphenol Canada Corp." badge to Amphenol's
  res.partner.image_1920 so that customer's CoCs include their logo
  on the top-right corner (mirrors how the reference shows it).
- coc_body template: guard hr.employee.signature access with a field-
  exists check (the field is provided by an optional module not
  installed on every Odoo).
- CoC uses web.html_container directly instead of wrapping in
  web.basic_layout — the outer wrapper was injecting top padding that
  pushed the title ~25% down the page. Now starts cleanly at the top.
- Tightened CoC CSS: removed unused label classes, added @page margin
  directive, fixed vertical-align on header cells so logos and company
  contact stay middle-aligned regardless of row height.
- Invoice PDF PAID stamp now also triggers on payment_state =
  'in_payment', so historical demo invoices look paid without needing
  full bank reconciliation.

Verified: renders a 152KB PDF with 5 embedded images, signer name
matches signature, all accreditation badges visible.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 01:42:35 -04:00
gsinghpal
fbaf318832 chore(fusion_plating): add story-driven demo seeder + polish invoice PAID stamp
Demo seeder (scripts/fp_demo_seed.py):
- Idempotent Python script run via odoo shell; populates ~60 records
  across 6 customer stories covering every workflow state for live demo
- Customers: Amphenol (net-terms, deep history), Magellan (progress
  billing, active), Cyclone (deposit, in-production), Honeywell
  (net-terms, just confirmed), Westin (COD, direct-order path),
  Delinquent Industries (account hold — Confirm raises UserError)
- Coating configs with realistic AMS specs (2404, 2700 Rev G, 2406)
  and bake-relief flags set on applicable processes
- Part catalog with revision chains (Rev 1 / Rev 2 / Rev 3 for hot parts)
- Customer price lists with volume tiers
- Per-customer invoice strategy defaults
- Bath chemistry logs (15 readings, last 2 OOS → pending replenishment
  suggestion visible in menu)
- Racks: 4 active + 1 needing strip (MTO 3.2 / 3.0) for kanban demo
- Bake windows: 1 awaiting (ticking down), 1 baked, 1 missed (alert)
- Quote configurator sessions: 3 draft, 3 confirmed/won, 3 lost (with
  reasons), 1 expired — populates the win/loss analysis
- Historical closed orders: 8 jobs backdated across 4 months with
  SO → MO → Delivery → Invoice → Payment run through each hook so
  portal-job progression, certificates with thickness readings, and
  invoice AR aging all look real
- Active orders at every workflow stage for the live demo cycle

Polish:
- report_fp_invoice PAID stamp now also triggers on payment_state ==
  'in_payment' (in addition to 'paid'). Odoo leaves payments in
  'in_payment' until the bank reconciliation job matches them against
  a statement line, so historical demo invoices would otherwise never
  show as stamped even though the payment is posted and the customer
  owes nothing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 01:30:53 -04:00