Commit Graph

622 Commits

Author SHA1 Message Date
gsinghpal
6bbba93963 feat(configurator): flip dual-descriptions to required on template (Sub 2 Task 25) 2026-04-21 23:21:53 -04:00
gsinghpal
a66794084f feat(configurator): flip part_number + revision to required, name optional (Sub 2 Task 24) 2026-04-21 23:21:39 -04:00
gsinghpal
20d547bb4f feat(reports): traveler PDF surfaces part_number + internal description (Sub 2 Task 23) 2026-04-21 23:19:33 -04:00
gsinghpal
84c0745ab5 feat(reports): WO PDF surfaces part_number + internal description (Sub 2 Task 22) 2026-04-21 23:18:10 -04:00
gsinghpal
79d9e6b3b0 feat(reports): BoL PDF uses customer_line_header macro (Sub 2 Task 21)
Rewired portrait + landscape variants of report_fp_bol. The BoL had no
line collection of its own (fusion.plating.delivery only has a soft
`job_ref` Char), so the previous cargo-description block was a single
hardcoded row. Restructured to look up the job's mrp.production via
`job_ref`, iterate its `move_finished_ids` (excluding cancelled), and
render each finished-goods move through the shared
customer_line_header macro using the `move.sale_line_id or move`
adapter pattern.

When no MO is found or there are no finished moves, the template falls
back to the previous single-row "Plated parts — Job X" behavior so
legacy records without a backing MO still print correctly. Per-row QTY
now reflects the individual move's `product_uom_qty` instead of the
MO's aggregate `product_qty`.

Both variants render successfully on entech against a delivery whose
job_ref matches a real MO with one finished move.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 23:06:07 -04:00
gsinghpal
ac824c2cfb feat(reports): packing slip PDF uses customer_line_header macro (Sub 2 Task 20)
Rewired portrait + landscape variants of report_fp_packing_slip to use the
shared customer_line_header QWeb macro. The packing slip iterates
stock.move records (doc.move_ids_without_package); the adapter
`<t t-set="line" t-value="move.sale_line_id or move"/>` bridges the macro's
`line.x_fc_part_catalog_id` lookup to the sale line when the move is tied
to a sale (preferred path), falling back to rendering the stock.move's
product_id for stray moves with no sale line.

SKU + PRODUCT columns collapsed into a single PART column (width
adjusted to absorb the removed SKU column). Both variants render
successfully on entech with a real picking whose move has a sale_line_id.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 23:05:58 -04:00
gsinghpal
ea2cfc37c3 feat(reports+configurator): invoice PDF uses macro; x_fc_part_catalog_id on account.move.line (Sub 2 Task 19)
Invoice PDF (portrait + landscape) now collapses SKU + Description into
a single Part column rendered via fusion_plating_reports.customer_line_header,
so customer-facing invoices print the customer's part number (with
revision) instead of the internal service SKU.

To feed the macro on invoice lines, add x_fc_part_catalog_id to
account.move.line and override sale.order.line._prepare_invoice_line so
the part reference propagates automatically when an SO is invoiced.
2026-04-21 22:59:40 -04:00
gsinghpal
6cbea9d2f3 feat(reports): SO PDF uses customer_line_header macro (Sub 2 Task 18)
Collapse the SKU and Description columns in both the portrait and
landscape sale-order PDFs into a single Part column rendered through
the shared customer_line_header macro, so customer-facing quotes and
confirmed orders print the customer's part number (with revision)
instead of the internal service SKU.

Updates column widths, section/note colspans, and the conditional
col_count used for the landscape template's optional discount column
to reflect the collapsed header.
2026-04-21 22:57:33 -04:00
gsinghpal
d959775648 feat(reports): customer_line_header QWeb macro + version bump (Sub 2 Task 17) 2026-04-21 22:54:17 -04:00
gsinghpal
dcd5d2a1ec feat(configurator): direct-order wizard dual-description inputs + onchange (Sub 2 Task 16)
Adds an `internal_description` text field to the direct-order wizard
line so the shop-floor copy is captured at order entry alongside the
customer-facing text. Picking a template now fires both sides of the
onchange: `line_description` gets `customer_facing_description` (with
fallback to the legacy `description` field for backward compat) and
`internal_description` gets the template's internal text. The
auto-suggest onchange was refactored around a tiny `_apply` helper so
all three fallback paths populate both fields consistently.

The template picker is surfaced as an optional column on the wizard
list (hidden until a part is chosen, domain-scoped to that part) and
as a dedicated labeled row in the per-line form. The internal text
field is also surfaced in the form under "Line Description" so the
estimator can review / edit it before confirm. On create_order, both
`x_fc_description_template_id` and `x_fc_internal_description` are
written through to the generated sale.order.line so the audit trail
and WO printout stay linked.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 22:51:53 -04:00
gsinghpal
a659eba8f5 feat(configurator): SO-line template picker + dual descriptions + onchange (Sub 2 Task 15)
Surfaces the per-part description template on the SO line list alongside
a hidden-by-default internal description column. Picking a template
fires an onchange that copies `customer_facing_description` into Odoo's
standard `name` (customer-visible) and `internal_description` into
x_fc_internal_description (shop-floor / WO only). Estimator can edit
either field after the template is applied.

The template picker's domain filters by the line's part, and the field
stays hidden until a part is chosen — avoids showing every global
template when the line is blank.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 22:49:49 -04:00
gsinghpal
d1c855698a feat(configurator): two-column dual-description repeater on part form (Sub 2 Task 14) 2026-04-21 22:47:05 -04:00
gsinghpal
afee8d5ee8 feat(configurator): cert requirement + SKU relabel on part form (Sub 2 Task 13) 2026-04-21 22:46:10 -04:00
gsinghpal
8cea92cca4 test(sub2): cert-resolver edge cases (Task 11) 2026-04-21 20:23:32 -04:00
gsinghpal
332cdc8baa refactor(bridge_mrp): route button_mark_done cert cascade through resolver (Sub 2 Task 10) 2026-04-21 20:22:20 -04:00
gsinghpal
334a10dcb7 feat(bridge_mrp): _fp_resolve_cert_requirement single-source resolver (Sub 2 Task 9) 2026-04-21 20:21:40 -04:00
gsinghpal
cf6faf4cdf fix(configurator): strip legacy 'Rev ' prefix in display_name to avoid 'Rev Rev N' 2026-04-21 20:19:14 -04:00
gsinghpal
b89737f3c9 feat(configurator): display_name compute for fp.part.catalog (Sub 2 Task 8) 2026-04-21 20:18:33 -04:00
gsinghpal
fb2f360e7b test(sub2): migration verification SQL (Task 6) 2026-04-21 20:15:42 -04:00
gsinghpal
b3405a21eb feat(configurator): Sub 2 data migration — backfill part_number/revision, split descriptions (Task 5) 2026-04-21 20:15:12 -04:00
gsinghpal
93b2b2e0cf chore(configurator): bump version to 19.0.9.0.0 for Sub 2 (Task 4) 2026-04-21 20:15:10 -04:00
gsinghpal
77d5b91327 feat(configurator): add dual descriptions to sale.order.line (Sub 2 Task 3) 2026-04-21 20:13:17 -04:00
gsinghpal
c574689664 feat(configurator): add internal + customer-facing description fields (Sub 2 Task 2) 2026-04-21 20:13:05 -04:00
gsinghpal
868b418333 feat(configurator): add certificate_requirement field to fp.part.catalog (Sub 2 Task 1) 2026-04-21 20:12:52 -04:00
gsinghpal
418dabc688 docs(plating): Sub 2 implementation plan (30 tasks, 3 phases)
Full bite-sized plan matching the approved spec. Each task has file
paths, complete code, syntax-check commands, upgrade commands, expected
outputs, and commit messages.

Phase A (Tasks 1-12): additive schema + migration + cert-resolver.
System runnable throughout.

Phase B (Tasks 13-23): UI + QWeb macro + report rewiring. Users see new
fields. Old fields still exist.

Phase C (Tasks 24-30): flip required=True, drop legacy column, regression,
deploy to entech.

Self-review pass: every spec section mapped to a task; no TBD/TODO/placeholder.
Type signatures (_fp_resolve_cert_requirement, display_name, macro
params) consistent across tasks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 20:02:34 -04:00
gsinghpal
a15b75e38a docs(plating): fine-tuning initiative roadmap + Sub 2 design spec
Captures the current state of the system-wide fine-tuning initiative so a
fresh Claude Code session can resume without context loss.

CLAUDE.md additions (fusion_plating/CLAUDE.md):
* Sub-project roadmap (Sub 1 through 8 + two deferred items)
* Sub 2 locked decisions (Q1–Q6 answers)
* Sub 2 defensive measures that prevent rework when later subs land
* Sub 6 / 7 / 8 previews from the client transcript
* Client-confirmed operational thresholds (tank polling, active tanks)
* How to resume in a fresh session

Sub 2 design spec (docs/superpowers/specs/):
* Part Data Model Overhaul — covers gaps 2b, 2c, 2d, 4
* 12 sections: scope, data model, migration, UI, cert resolution,
  reports, testing, defensive measures, files touched, rollout,
  success criteria, open questions
* All clarifying questions answered; zero placeholders
* Ready for writing-plans skill to generate implementation plan

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 19:53:40 -04:00
gsinghpal
bdbfda7ce9 feat(plating): merge Fischerscope PDF into CoC as page 2+
When a QC uploaded the XDAL 600 report, the CoC PDF render pipeline
now appends the Fischerscope PDF directly after the cert pages. This
matches what aerospace / Nadcap auditors expect (and how Steelhead
ships certs today) — a single PDF file carrying both the certificate
declaration and the raw equipment report.

Flow:
* _fp_generate_cert_pdf renders the CoC via QWeb as before
* _fp_merge_thickness_into_cert resolves the QC for the MO (preferring
  the passed one) and extracts its thickness_report_pdf_id bytes
* PyPDF2.PdfMerger concatenates CoC then Fischerscope into a single PDF
* Merged bytes replace pdf_content before the ir.attachment is written
* Falls back to CoC-only (and logs a warning) if PyPDF2 is missing or
  either PDF fails to parse — never blocks MO completion

Smoke test: synthetic Fischerscope + real QWeb CoC → 2-page merged PDF
with page 1 CoC text and page 2 Fischerscope text, verified via
PyPDF2 extract_text.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 08:37:09 -04:00
gsinghpal
e86d897bce feat(plating): QC gate + mobile checklist + Fischerscope thickness capture
Phase 1 — Backend QC gate (bridge_mrp)
* fp.qc.checklist.template / .line — per-customer checklist definitions
* fusion.plating.quality.check / .line — per-MO instances walked by inspectors
* res.partner.x_fc_requires_qc + x_fc_qc_template_id toggles policy per customer
* mrp.production.button_mark_done blocks close until QC passes (plus optional
  thickness-readings + thickness-PDF gates on aerospace templates)
* Auto-spawns the QC on MO confirm from the customer's resolved template
* Fischerscope XDAL 600 PDF parser auto-extracts NiP / Ni% / P% readings on upload
* fp.thickness.reading gains quality_check_id + auto_extracted

Phase 2 — Mobile QC checklist (OWL client action)
* fp_qc_checklist registered under registry.category("actions")
* Reuses shopfloor design tokens (_fp_shopfloor_tokens.scss) — 48 px touch
  targets, shadow-based elevation, three-tier contrast, light + dark bundles
* Per-line pass/fail/N/A with numeric value range, mandatory photo, notes
* Fischerscope PDF drop-zone → server-side pdftotext parse
* Sign-off bar with pass / fail / rework actions

Phase 3 — Admin config
* Starter global default + aerospace/Nadcap templates seeded
* Plating → Configuration → QC Checklist Templates (manager-only)
* Plating → Quality → Quality Checks menu
* "Plating Documents" tab on res.partner gains the QC toggle + template picker
* MO form smart button opens the active QC in the mobile checklist

Gap fixes
* Scanner handles FP-QC:<ref> and FP-MO:<name> — launches the checklist
  directly on the tablet
* action_spawn_retry clones a fresh QC from a failed one so rework doesn't
  need a new MO

All 12 models / routes / gates smoke + E2E tested: 24 assertions pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 00:15:58 -04:00
gsinghpal
4d6095cd2a changes 2026-04-20 22:58:25 -04:00
gsinghpal
192aa60d00 docs(plating): flag fusion_plating_culture as do-not-auto-install
Future sessions shouldn't silently re-install retired modules during
install/upgrade sweeps. Added an explicit "Retired / Do-Not-Install
Modules" section with guardrails:

- Don't include in -i / -u sequences
- Don't add as a depends target
- Don't re-sync to entech /mnt/extra-addons/custom/
- Don't recommend installing without user confirmation

Covers the two modules currently in this state:
- fusion_plating_culture (code in repo, uninstalled on entech)
- fusion_plating_sensors (fully removed, absorbed into fusion_iot)

Also struck-through the "| 80 | Culture | ..." menu row and added a
retired tag to the module-list tree so at-a-glance scans don't
suggest it's a live part of the menu hierarchy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 20:33:23 -04:00
gsinghpal
729743e268 Revert "chore(plating): retire fusion_plating_culture — not a priority"
This reverts commit 95310c459d.
2026-04-20 20:31:02 -04:00
gsinghpal
95310c459d chore(plating): retire fusion_plating_culture — not a priority
Culture/values/recognitions framework was shipping zero data and zero
workflow integration for this client. It's a people-ops concern (peer
kudos, "Fundamental of the Week" rotations) with no overlap with the
technical plating pipeline — no interaction with process recipes,
quality holds, sensors, or compliance.

Verified zero data on entech before uninstalling:
  fusion.plating.value              0 records
  fusion.plating.value.set          0
  fusion.plating.value.recognition  0
  fusion.plating.value.rotation     0

Clean uninstall on entech, module dir removed from disk. The Culture
top-level menu disappears. If a future client wants it back, the
module is easy to re-author — nothing we built on top of it depends
on it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 20:30:47 -04:00
gsinghpal
cf205cfd11 refactor(fusion_iot): port sensor taxonomy + dashboards, retire fusion_plating_sensors
fusion_plating_sensors had broader scope (sensor_type taxonomy,
dashboards, location flexibility) but its core logic was ALL
scaffolding — alert rules stored thresholds with zero side effects,
measurement create just filled a name sequence, the HTTP endpoint
required user-auth session cookies. Meanwhile fusion_plating_iot had
the actual working alerting: in-spec checks, quality-hold auto-raise
with excursion dedupe, setpoint + deviation, token-auth ingest for
headless hardware. Plus 563 real readings from the pilot Pi.

Right unification: keep fusion_plating_iot (working) as the base,
port the valuable structural bits from fusion_plating_sensors, demolish
the latter entirely.

**Ported to fusion_plating_iot:**

- `fp.sensor.type` — taxonomy model with 8 seeded types (Temperature,
  pH, Conductivity, Level, Pressure, Flow, Concentration, Switch).
  Richer than the device_kind Selection; hardware-independent (one
  "Temperature" type covers DS18B20 / PT100 / thermocouple).
- `fp.sensor.dashboard` — named grouping of sensors with
  out-of-spec count. Simple but useful ("ENP Line 1 — all tanks")
  without the broken alert-rule complexity.
- Extended `fp.tank.sensor`:
  * `uuid` (stable logical ID, survives hardware swaps)
  * `sensor_type_id` (link to the taxonomy above)
  * `work_center_id`, `facility_id`, `location_name` — alternatives to
    tank_id so probes can live on ovens, ambient air, effluent pipes
    without faking a "tank"
  * `effective_location` computed — picks the first non-empty of the
    four location fields for display

**Post-install hook** backfills UUID + default sensor_type on existing
live sensors. Verified on the 2 pilot sensors: both got UUIDs, both
auto-assigned the Temperature type via device_kind=ds18b20 mapping.

**Deleted** (all of fusion_plating_sensors, 1205 LOC):
- fp.sensor (replaced by fp.tank.sensor with added fields)
- fp.sensor.measurement (replaced by fp.tank.reading)
- fp.sensor.alert.rule (replaced by inline alert_min/max + working hold)
- /fp/sensor/measure controller (replaced by /fp/iot/ingest)
- fp.sensor.measure.wizard (not needed — Odoo's normal create form works)
- The "Sensors" submenu hierarchy (Dashboards/All Sensors/Measurements/
  Sensor Types) that created the dup menus the user reported

**Menu now**: Plating → Operations → Sensors
  - Dashboards    (fp.sensor.dashboard)
  - Sensors       (fp.tank.sensor — renamed from "Tank Sensors" since
                   it supports non-tank locations now)
  - Readings      (fp.tank.reading)
  - Sensor Types  (fp.sensor.type)

No data loss: all 591 Pi readings preserved (up from 563 earlier as
the live poller kept running throughout the refactor). Brief 503 on
the Pi during the Odoo module-update restart; poller auto-retried on
the next 30s tick.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 19:58:57 -04:00
gsinghpal
118f96dad4 feat(fusion_iot): add setpoint/optimum + deviation to sensor schema
Sensors previously only tracked alarm thresholds (alert_min/alert_max).
Missing the third piece of standard process control: the SETPOINT —
what the heater/chiller controls toward and what dashboards compare
against. Without it an operator can't tell whether 89°C is "on target"
or "barely still in spec".

Schema changes:

**fusion.plating.bath.parameter** (shop-wide default)
- New `target_value` field — the default setpoint for this parameter
  across the shop (e.g. 87°C for ENP bath). Parallel to existing
  target_min / target_max.

**fp.tank.sensor** (per-sensor override)
- New `target_value_override` — per-sensor override, zero = inherit
  from parameter. Matches the existing override pattern for alert
  thresholds so users can fine-tune per-tank without touching the
  shop-wide spec.
- New `effective_target` / `effective_target_unit` computed — resolves
  override → parameter default, converts to company-preferred unit.
- New `_get_setpoint()` helper for internal use.

**fp.tank.reading**
- New `deviation_from_target` — signed Δ from the sensor's effective
  setpoint, in the company's preferred unit. Positive = above, negative
  = below, zero if no setpoint defined.
- New `deviation_band` (selection: on/near/far/out/none) — coarse band
  for fast visual scanning. `on` = within ±1° of target, `near` = ±3°,
  `far` = beyond, `out` = actually out of the alarm band.

**Views**
- Sensor form: split the alerting panel into two groups — "Target
  (setpoint)" on the left, "Alarm band" on the right. Makes the
  distinction between "where we want to be" and "where we'd panic"
  visually obvious.
- Reading list: new Δ + band columns, with decoration classes
  (success/info/warning/danger) so the list reads at a glance.
- Tank form Sensors tab: inline setpoint + unit column.

Seeded: parameter "Bath Temperature (Hot Process)" now carries
target_value=87°C as a realistic ENP shop default. Sensors inherit
unless they set their own override.

Design decisions kept simple:
- Did NOT add a warning band (warn_min/warn_max). Two-tier model
  (setpoint + alarm band) is enough for the pilot. Can add soft
  warnings later as a separate commit if ops wants them.
- Did NOT auto-control heaters. Setpoint is stored as metadata only;
  actual heater actuation via IoT is a future phase C project.

Verified: setpoint 87°C stored → displays 188.60°F on the live pilot
sensor (company pref = F). Each incoming reading correctly computes
signed deviation; bands colour the reading list appropriately.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 16:36:58 -04:00
gsinghpal
089cda71fe fix(fusion_iot): respect company temperature-unit preference in sensor UI
The sensor readings list always showed raw °C regardless of the
Plating Settings Temperature Unit preference (res.company.x_fc_default_temp_uom).
On a Fahrenheit-preferred shop, a 40°C reading should render as 105°F.

Fix: add display-aware computed fields alongside the canonical ones.

**fp.tank.reading**
- `value` / `unit` renamed with "(raw)" labels — these are the stored
  canonical values (always °C for temperature, because every
  temperature chip reports in Celsius natively)
- `display_value` + `display_unit` computed from company pref — only
  flips C→F when parameter_type='temperature' AND company pref='F';
  pH/conductivity/etc pass through untouched
- `display_name` now uses display_value so it reads naturally
  ("Sensor — 105.58 °F @ ...") regardless of region

**fp.tank.sensor**
- Mirrored the same pattern on the cached last-reading fields
- `last_reading_display` + `last_reading_display_unit` for lists
- `last_reading_value` hidden behind group_no_one (debug-only)

**Views**
- fp.tank.reading list: show display_value/display_unit, raw value
  hidden by default (toggle from column picker if needed)
- fp.tank.sensor list + form + tank inline: same pattern
- Raw value kept visible as an optional column so data engineers
  can still audit canonical storage

Why store canonical: spec thresholds (alert_min/max) live on the
sensor in °C. If the same Odoo serves a multi-region company
(Canada in C, US affiliate in F), switching a single preference
flips every UI without touching data. Alert logic keeps comparing
canonical values, so out-of-spec holds fire correctly regardless
of display unit.

Verified: 40.88°C raw → 105.58°F display on the live pilot probe
with company pref='F'. All 5 recent readings tested, display
fields updated correctly on every poll.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 16:13:17 -04:00
gsinghpal
dd575135ae fix(fusion_iot): point poller at public URL so Pi is site-portable
Pi is at our office today but moves to the client's shop in the next
few days. The client accesses Odoo at https://erp.enplating.ca (not a
LAN/Tailscale path — it's the same HTTPS URL any browser uses). By
pointing the poller at the public URL instead of the internal
10.200.1.26 LAN IP, the Pi works IDENTICALLY wherever it's plugged
in — no reconfiguration when it physically relocates.

- Updated poller's docstring + example config to use
  https://erp.enplating.ca
- Updated fusion_iot/CLAUDE.md with the portable-deployment notes and
  the failed-Tailscale-on-entech side-story (LXC can't create tun,
  apt state broken from a pre-existing python3-lxml-html-clean
  conflict — skipped because public URL is simpler anyway).

Verified live: poller restarted against https://erp.enplating.ca,
HTTP 200, TLS valid, 121ms RTT, two consecutive readings accepted
(46.25°C, 45.94°C — probe still cooling from the out-of-spec test).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 16:03:11 -04:00
gsinghpal
efb11df983 docs(fusion_iot): Tailscale + SSH cheat sheet for the pilot Pi
fp-iot-01 is now on Tailscale at 100.108.41.97. SSH config on the
Mac aliases `ssh fp-iot-01` to the Tailscale IP with key-based auth
(no more sshpass + password flying around in shell history).

Also noted the Pi-side folder structure (pi/ + scripts/) and the
live deployment facts (probe serial, systemd unit, config path)
so future sessions can pick up from zero without re-investigating.

Verified end-to-end with real hardware:
- Physical probe heated to 79.94°C → auto-raised HOLD-0015
- 30 subsequent out-of-spec readings → no duplicate holds (as designed)
- hold_id correctly linked back to the triggering reading

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 15:47:18 -04:00
gsinghpal
19a49acba0 feat(fusion_iot): live DS18B20 poller for Pi-side — first real tank reading in Odoo
Phase B kickoff — Pi hardware is wired up and posting readings to
Odoo via /fp/iot/ingest every 30 seconds. No more simulations;
this is real tank-temperature data.

New files:

- `pi/fp_iot_poller.py` — tiny systemd daemon. Reads every DS18B20
  under /sys/bus/w1/devices/28-* (kernel CRC-validated) and POSTs
  a batch to /fp/iot/ingest with the shared-secret token. Handles
  transient network failures by logging + retrying on the next
  30-second tick. Config in /etc/fp-iot/poller.conf (ODOO_URL,
  INGEST_TOKEN, INTERVAL_SECONDS).

- `pi/fp-iot-poller.service` — systemd unit with hardened sandbox
  (NoNewPrivileges, ProtectSystem=strict, ProtectHome, PrivateTmp,
  ReadOnlyPaths=/sys/bus/w1 /etc/fp-iot). Auto-starts on boot,
  restarts on failure.

- `scripts/fp_iot_setup_live_sensor.py` — one-shot entech
  initialiser: rotates the ingest token to a real random secret,
  picks a test tank + temperature parameter, creates the
  fp.tank.sensor record for serial 28-000000b276e4 with 15-35°C
  alert thresholds sized for bench testing.

Verified end-to-end: Pi reads probe → posts to Odoo → reading lands
in fp.tank.reading within 1s. 5 consecutive readings at 30s
cadence show smooth temperature trend (probe cooling from 27.25°C
to 26.06°C after being handled). in_spec flag correct, sensor
cache (last_reading_value / _at / _in_spec) updates on every
reading.

Not yet done — Phase B continued:
- Repackaged iot_drivers path (full Odoo IoT integration vs this
  simple HTTP path) — this poller is the minimal viable pilot.
- Multi-probe (scalable to N probes per Pi; code already supports,
  just need more hardware).
- Graduate to the proper iot.device + iot.box Odoo registration
  flow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 14:59:20 -04:00
gsinghpal
8a841e7534 changes 2026-04-20 13:07:15 -04:00
gsinghpal
2334c0a1fe fix(notifications): Send dialog cancel no longer pre-sends + no duplicate PDF
Two user-reported bugs on S00056 (and visible on any SO/invoice):

Bug 1: Cancelling the Send dialog still sends the email.
  action_quotation_send is a button handler that returns a
  compose-dialog action synchronously. Our override was calling
  _dispatch('quote_sent', ...) AFTER super(), which immediately
  sends the email via template.send_mail() before the user ever
  sees the dialog. Clicking Cancel at that point only dismisses
  the already-sent email's compose window.

  Fix: removed the _dispatch call entirely from action_quotation_send.
  The Send button IS the manual-send path; Odoo's compose dialog
  (pre-populated with our FP: Quotation Sent template thanks to
  _find_mail_template) handles the send-or-cancel choice correctly.

  If an auto-quote-sent notification is ever wanted, it should be
  wired from a cron that scans SOs that just transitioned
  draft -> sent, not from the button handler. Noted in a code
  comment.

Bug 2: Two copies of the same PDF attached to every email.
  The mail.template records now carry report_template_ids (set by
  the post_init hook from the previous refactor) which Odoo uses to
  auto-attach PDFs on send_mail(). But the fp.notification.template
  records ALSO had attach_quotation / attach_sale_order / attach_invoice
  flags set, which cause _collect_attachments() to render the same
  PDF a second time and pass it in email_values.

  Fix: turned those three flags off in the XML data file. Other
  flags (attach_coc, attach_bol, attach_receipt, attach_thickness_report,
  attach_packing_list, attach_pod) stay on — those are genuinely
  different documents, not dupes.

  Because the records are noupdate="1", updated the post_init_hook
  to also backfill the flag changes onto existing DB rows so
  production instances get cleaned up on -u, not just fresh installs.

Smoke:
  attach_quotation=False attach_sale_order=False attach_invoice=False on all three records
  1 report per template (no dupes)
  action_quotation_send no longer contains _dispatch("quote_sent", ...)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 01:45:32 -04:00
gsinghpal
36964d6b79 feat(fusion_accounting_reports): adopt Enterprise account_reports look
User feedback: 'i like the odoo enterprise style reports, I hate our style.'

Replaces our custom 'o_fusion_reports' visual with a faithful adaptation
of Enterprise account_reports. Same .account_report root class, same
table semantics (line_name + line_cell + line_level_N), same border
treatment (1px gray-300 borders, 0.25rem radius, sticky thead), same
button hover behavior (gray-300 -> enterprise-action-color), same dense
0.8rem font-size + padded cells.

SCSS layout:
- reports.scss in web.assets_backend bundle (eager light)
- reports.dark.scss in web.assets_web_dark bundle (lazy dark mode)
- _variables.scss reduced to spacing/typography only -- colors use
  Odoo's \$o-* SCSS vars so dark mode flips automatically via the
  separate dark bundle
- old dark_mode.scss removed (was using non-Odoo [data-color-scheme]
  selector that never matched anything)

QWeb templates rewritten to mirror Enterprise's structure:
- report_viewer.xml roots at .account_report with scroll container
- report_table.xml uses Enterprise's td.line_name + td.line_cell with
  .wrapper > .content nesting; partner-grouped reports now actually
  render their aging buckets (previously showed nothing)
- period_filter.xml is now a clean Bootstrap-styled inline filter bar

Kept Fusion-only components but restyled to fit:
- anomaly_strip uses Bootstrap alert-{danger,warning,info} colors
- ai_commentary_panel is a plain bordered panel, no gradients/emojis
- drill_down_dialog unchanged (already a Bootstrap modal)

Made-with: Cursor
2026-04-20 01:41:41 -04:00
gsinghpal
f09bef9083 refactor(reports): consolidate SO Acknowledgement back into the Sales Order PDF
Earlier I built report_fp_so_acknowledgement.xml as a separate
customer-facing document. On review there was no good reason — our
existing report_fp_sale.xml already flips its title between
"Quotation" and "Sales Order" based on state, and carried ~90% of
the same content. Two documents would have meant the shop had to
remember which to send when, and the customer would get two
near-identical PDFs in their inbox.

Consolidation:

1. Merged the four unique blocks from the acknowledgement into
   report_fp_sale.xml (both portrait AND landscape variants):
   - CUSTOMER JOB # / PLANNED START / CUSTOMER DEADLINE / SHIP VIA
     info row (shown only when any of those fields is populated)
   - Blanket / block-partial highlight-box callout (shown only
     when the flags are set)
   - External notes (x_fc_external_note) block above Terms and
     Conditions

2. Deleted fusion_plating_reports/report/report_fp_so_acknowledgement.xml
   and removed it from the module manifest. Also purged the orphan
   ir.actions.report and ir.ui.view DB rows + the stale
   ir.model.data entries.

3. Re-pointed the fp_mail_template_so_confirmed mail template's
   report_template_ids from the now-gone acknowledgement report to
   action_report_fp_sale_portrait. Updated hooks.py accordingly; the
   hook now uses "set" semantics (replace all) instead of "add" so
   re-running it cleans up stale attachments from prior refactors.

4. UAT on S00071: the Send button pre-selects the FP: Order
   Confirmation template with SalesOrder_S00071.pdf attached. The
   PDF renders with the new plating rows populated — Customer Job #
   AMPH-2026-0420-01, Customer Deadline 05/14/2026 08:00:00 PM,
   "Partial shipments blocked" callout, all lines + totals.

One PDF, one Send button behaviour, matching what Odoo and most
ERP systems do.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 01:30:06 -04:00
gsinghpal
54e56ed0e6 changes 2026-04-20 01:16:12 -04:00
gsinghpal
8217bb0ff6 fix(fusion_accounting_reports): expose dynamic OWL reports as menu items
User reported that after Enterprise uninstall, clicking 'Reports' opened
PDF statements instead of the dynamic Fusion report viewer. Root cause:
the OWL ReportViewer (registered as view_type='fusion_reports') was only
reachable via the period-picker WIZARD; no menu items used the OWL view
directly. Plus the JS service ignored report_code, so even within the
viewer, all PnL-typed reports rendered the canonical P&L line_specs.

Changes:

JS layer
- reports_service.js: runReport now accepts and forwards reportCode;
  state tracks currentReportCode so re-runs after period/comparison
  changes preserve the variant.
- report_viewer.js: reads default_report_code (and default_comparison)
  from the action context.
- period_filter.js: passes the cached reportCode on date changes;
  clears it when the user picks a different report_type.

Backend
- New fusion_accounting_reports/views/report_actions.xml with 11
  dedicated ir.actions.act_window records, one per built-in report
  (P&L, Balance Sheet, Trial Balance, GL, Cash Flow, Executive Summary,
  Annual Statements, Aged Receivable, Aged Payable, Partner Ledger,
  Tax Summary). Each opens view_mode='fusion_reports' with the
  appropriate default_report_type + default_report_code context.
- views/menu_views.xml: each report now gets its own menu item
  directly under Accounting > Reporting (sequence 10-40), matching
  Enterprise's flat structure. Custom Period wizard, XLSX export and
  Anomaly browser collected under a 'Tools' sub-group at the bottom.
- fusion_accounting_l10n_ca: adds menu items for 'Profit and Loss
  (Canada)' and 'Balance Sheet (Canada)' as siblings, plus a 'Tax
  Returns (CA)' configuration menu.

Verified live on westin-v19:
- pnl rendering 3 rows, cash_flow 9, executive_summary 7,
  annual_statements 5, ca_profit_loss 9 \u2014 each report now renders
  its own line_specs correctly.
- Reporting menu shows 14 Fusion report entries + Tools group.
- 136/136 reports + l10n_ca tests pass.

Version bumps: reports 19.0.1.1.1, l10n_ca 19.0.1.1.0.

Made-with: Cursor
2026-04-20 01:11:48 -04:00
gsinghpal
867b5f71a1 fix(fusion_accounting): unified Accounting menu under one root, hide migration when Enterprise gone
Some checks failed
fusion_accounting CI / test (fusion_accounting_ai) (push) Has been cancelled
fusion_accounting CI / test (fusion_accounting_core) (push) Has been cancelled
fusion_accounting CI / test (fusion_accounting_migration) (push) Has been cancelled
User reported two UX problems after the Enterprise uninstall:
1. Each Fusion sub-module showed up as its own standalone app in the
   launcher (Bank Reconciliation, Financial Reports, Asset Management,
   Customer Follow-ups, Fusion AI). Should look like ONE Accounting app.
2. Clicking the 'Fusion Accounting' app still opened the migration
   wizard even though Enterprise had been uninstalled and there was
   nothing to migrate.

Fix:
- Move all Fusion sub-module roots under the Community account.menu_finance
  hierarchy:
    * Bank Reconciliation \u2192 Accounting > Bank Reconciliation
    * Asset Management    \u2192 Accounting > Asset Management
    * Financial Reports   \u2192 Reporting > Financial Reports
    * Follow-ups          \u2192 Customers > Follow-ups
    * Fusion AI           \u2192 Configuration > Fusion AI
    * Migrate from Ent.   \u2192 Configuration > Migrate from Enterprise
- Rename Community's 'Invoicing' top-level menu to 'Accounting' (what
  Enterprise's accountant module did). Set the Fusion icon on it. This
  rename lives in the meta-module so it only fires when the full suite
  is installed.
- Add second computed group 'group_fusion_show_when_enterprise_present'
  (inverse of the existing 'absent' group). Migration menus are gated
  by this group, so they auto-hide once Enterprise is uninstalled.
- _fusion_recompute_coexistence_group now maintains both groups in lockstep.
- Meta-module now also depends on l10n_ca, hr_payroll, ocr, documents
  (the Phase 6/7 sub-modules) for one-click full-suite install.
- Fusion AI menu's old parent ('accountant.menu_accounting') was deleted
  with the Enterprise uninstall \u2014 reparented under Configuration.

Result: single 'Accounting' top-level menu containing the standard
V19 Community structure (Dashboard / Customers / Vendors / Accounting /
Reporting / Configuration), with all Fusion features slotted into the
appropriate sub-section. Verified live on westin-v19: 6 separate
Fusion top-level menus collapsed to 1; coexistence groups recomputed
(absent=10 users, present=0 users); 604/604 tests pass.

Version bump: all touched modules \u2192 19.0.1.1.0.

Made-with: Cursor
2026-04-20 01:04:49 -04:00
gsinghpal
bee5ba4d3f fix(plating): UAT-caught UX annoyances + lurking bugs
Five fixes from the end-to-end UAT debrief:

1. Menu discoverability (HIGH)
   Added a prominent "+ New Direct Order" button in the Sale Orders
   list header toolbar (class=btn-primary, display=always). The
   existing menuitem at Plating > Sales > New Direct Order was
   buried in a submenu that didn't always expand; the toolbar
   button is a guaranteed entry point from the most common screen.

2. Escape/X destroys wizard state (HIGH)
   Added a prominent info banner at the top of the wizard form:
   "Changes are not saved until you click Create & Confirm Order.
   Closing this window (Esc or X) discards your entries." The
   Cancel button now has confirm="Discard this order? All header
   data and line items will be lost." so the intentional-cancel
   path also prompts.

3. Shell/cron crash in _fp_auto_create_mo (MEDIUM)
   bridge_mrp/models/sale_order.py:232-264 used _() inside list
   comprehensions to format the internal chatter summary of newly
   created / adopted MOs. _() resolves language from env.context,
   which is empty in odoo-shell and cron contexts — triggering a
   translate.get_text_alias crash AFTER the MOs had been created.
   These strings are internal audit log text, not user-facing UI;
   dropped the _() wrappers so the message builds safely from any
   context. Same for the per-group error-message on savepoint
   rollback.

4. Misleading "100%" margin (MEDIUM)
   x_fc_margin_percent displayed 100% on every SO because the cost
   rollup from fp.coating.config.unit_cost isn't populated yet.
   Added x_fc_margin_available Boolean (True only when at least
   one line's coating has a non-zero unit_cost). The SO Plating
   tab now hides the margin numbers when margin_available=False
   and shows an inline muted note: "Margin n/a — coating cost
   rollup not yet populated on any line's treatment."

5. Account Hold banner too loud (LOW)
   fusion_plating_invoicing was injecting a full-height danger
   alert above every SO header. Slimmed it to a one-line compact
   alert with icon: "Account Hold — SO confirmation, invoicing
   and shipping are blocked for non-managers." Half the vertical
   footprint, less visual competition with the Plating chip bar.

Verified via UAT on S00071.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 01:03:26 -04:00
gsinghpal
068a654c2b fix(fusion_accounting_bank_rec): test factory adapts to V19 Community semantics
Some checks failed
fusion_accounting CI / test (fusion_accounting_ai) (push) Has been cancelled
fusion_accounting CI / test (fusion_accounting_core) (push) Has been cancelled
fusion_accounting CI / test (fusion_accounting_migration) (push) Has been cancelled
After Enterprise's account_accountant is uninstalled,
account.bank.statement.journal_id reverts to its V19 Community definition
\u2014 a read-only computed field derived from line_ids.journal_id. Direct
writes are silently dropped (which is what was happening: 55 tests
errored with 'null value in column journal_id' because the test's
statement had no journal, and the line factory was reading
statement.journal_id (False) and passing that to the line create).

Fix:
- make_bank_statement now bootstraps the statement with one zero-amount
  line carrying journal_id, so the computed journal_id resolves correctly.
- make_bank_line no longer routes journal through the statement \u2014
  journal_id is set directly on the line (which is V19 Community's
  intended path; lines can exist standalone without a statement).

This is a test-only change; runtime behaviour is unchanged. Real users
creating bank lines via the UI already use the correct path.

Made-with: Cursor
fusion_accounting/enterprise-free
2026-04-20 00:52:02 -04:00
gsinghpal
71f39c8d33 feat(fusion_accounting_documents): Documents app <-> invoice bridge
Replaces Enterprise's documents_account with a Fusion-native bridge.
When a PDF/image lands in the Documents app, users can convert it
into a draft vendor bill via a wizard that copies the document's
binary onto the new account.move and posts a chatter note linking
back to the source document.

Adds:
- documents.document.move_id (Many2one to the linked invoice)
- documents.document.is_invoice_candidate (computed; True for
  unlinked PDF/image binaries)
- documents.document.action_create_invoice() opens the wizard
- account.move.source_document_ids reverse linkage + statinfo button
- fusion.create.invoice.from.document.wizard (TransientModel + form)
- ir.actions.server bound to documents.document so the workflow
  appears in the kanban/list Actions menu (the Documents app has
  no regular form view to inherit from in v19)

The wizard:
- defaults to the company's first purchase journal
- supports vendor bill or vendor credit note
- copies the source attachment onto the new move
- posts a chatter note linking back
- marks the document linked so it stops appearing as a candidate

Auto-installs when documents + fusion_accounting_core are both
present. 8 unit tests cover the candidate flag, wizard happy path,
attachment copy, reverse linkage, already-linked guard, non-PDF
guard, and credit-note creation.

Made-with: Cursor
2026-04-20 00:34:50 -04:00
gsinghpal
125f48377a feat(fusion_accounting_ocr): pluggable OCR for vendor bills
Replaces Enterprise's account_invoice_extract with a Fusion-native pipeline:

Stage 1 (text extraction): Tesseract OCRs the bill attachment via
pytesseract + pdf2image. Pluggable OCRProvider adapter pattern allows
future Mindee / Google Document AI / Ollama-vision backends.

Stage 2 (field parsing): The fusion_accounting_ai LLMProvider reads the
raw OCR text and returns structured invoice fields (vendor, invoice
number, dates, amounts, line items) as JSON.

Draft invoice fields are auto-populated for empty-only fields (never
overwriting user-entered data). Vendor matching by name against
res.partner with supplier_rank > 0.

Adds:
- account.move.ocr_state (selection: not_requested/pending/processing/
  done/failed/manual)
- account.move.ocr_raw_text, ocr_extracted_data (Json), ocr_backend,
  ocr_confidence
- fusion.ocr.log (audit trail per OCR run)
- res.company.fusion_ocr_enabled / fusion_ocr_default_backend / auto_run
- /fusion/ocr/request_for_invoice JSON-RPC endpoint

Backend availability detected at runtime via OCRProvider.is_available()
classmethods. Tesseract 5.3.4 + pytesseract 0.3.13 + pdf2image 1.17.0
are installed in the container.

Tests: 13 (TesseractAdapter availability + image OCR; flow tests for
draft autofill, no-attachment guard, customer-invoice guard, ref-not-
overwritten; field parser empty/clean-json/markdown-fence/bad-JSON/
provider-exception). All pass on westin-v19 OrbStack VM.

Made-with: Cursor
2026-04-20 00:32:50 -04:00
gsinghpal
a730942d24 feat(fusion_accounting_hr_payroll): payroll -> GL bridge
Replaces Enterprise's hr_payroll_account with a Fusion-native bridge:
- Adds account_debit / account_credit / fusion_analytic_account_id /
  not_computed_in_net to hr.salary.rule (company-dependent GL mapping)
- Adds move_id + move_state + journal_id + _fusion_create_account_move
  to hr.payslip (validated payslip -> balanced account.move)
- Adds move_id + move_state + action_open_move to hr.payslip.run
- Adds journal_id (company-dependent) to hr.payroll.structure
- Adds is_payroll_journal flag to account.journal
- Adds payslip_ids / payslip_count + action_open_payslip on account.move
- Adds payslip_id reverse link on account.move.line
- Adds move_line_id reverse link on hr.payslip.line
- Adds fusion_payroll_journal_id + fusion_payroll_auto_post to res.company
  (with res.config.settings exposure)

Coexistence: detects Enterprise hr_payroll_account at runtime via
ir.module.module and yields move creation to it while both modules are
installed, so payslips do not get duplicate entries. Once the Enterprise
module is uninstalled, this module owns the bridge.

Auto-installs whenever both hr_payroll and fusion_accounting_core are
present on the database.

10 smoke tests verifying field surface + bridge entrypoints all pass on
westin-v19. Full payslip-to-move integration test deferred (needs
seeded payroll structure).

Removes Westin's last payroll-accounting dependency on Enterprise's
accountant umbrella module (Phase 6b of the Fusion Accounting suite).

Made-with: Cursor
2026-04-20 00:18:08 -04:00