218 Commits

Author SHA1 Message Date
gsinghpal
76c898aadf docs(fusion_accounting): Phase 0 foundation implementation plan
Detailed task-by-task plan for executing Phase 0 of the Enterprise
Takeover Roadmap. 22 tasks covering:

- Sub-module skeletons (_core, _ai, _migration) and meta-module conversion
- Move all current AI module code into fusion_accounting_ai with git mv
- ir_model_data ownership reassignment via post-migration script
- Data adapter pattern (base + bank_rec + reports + followup + assets adapters)
- Refactor of every AI tool to route through adapters (pilot in bank_rec, then survey + per-file)
- Strip all hard Enterprise dependencies from manifests
- Enterprise-detection helper and shared-field-ownership models in _core
- Multi-company record rule on fusion.accounting.session (was a Known Issue)
- Migration safety guard that blocks Enterprise uninstall until wizard runs
- Migration wizard skeleton (per-feature migrations added by future phases)
- tools/check_odoo_diff.sh for the annual upgrade ritual
- Per-sub-module CLAUDE.md, UPGRADE_NOTES.md, README.md
- CI pipeline (or deferral note if not yet viable)
- Empirical Enterprise-uninstall verification test on a throwaway instance
- End-to-end smoke test + completion tag

Each task uses TDD where applicable (test fails, implement, test passes,
commit) and concrete validation commands where TDD doesn't fit (file moves,
config changes, manual smoke tests).

Made-with: Cursor
2026-04-18 21:13:07 -04:00
gsinghpal
6c4ff7751f feat(plating): comprehensive timezone fix across dashboards/PDFs/emails
Database stores datetimes naive-UTC, but the dashboards and emails were
showing UTC strings to users in EST/EDT — making 9pm Toronto look like 1am
the next day. Adds a single helper module + auto-detection on install.

Core changes (fusion_plating):
- New fp_tz.py helper: fp_user_tz, fp_format, fp_isoformat_utc, fp_time_ago
  Resolves user.tz → company.x_fc_default_tz → UTC.
- res.company.x_fc_default_tz Selection (full pytz IANA list)
- res.config.settings exposes the company tz under a new "Regional
  Settings" block in Settings > Fusion Plating
- post_init_hook auto-populates the tz on first install: tries admin
  user → server /etc/timezone → America/Toronto fallback
- fp_process_node._to_dict now sends create_date/write_date as ISO with
  explicit +00:00 marker so JS new Date() parses it as UTC and the
  recipe tree editor's "time ago" math works correctly

Shop-floor controllers:
- shopfloor_controller.py: every fields.Datetime.to_string() and naive
  .strftime() swapped for fp_format(env, ...) — due_at, bake times,
  last_log_date, gates, server_time all now in user's tz
- _time_ago() removed; replaced with fp_time_ago helper which compares
  tz-aware datetimes (the local one was naive-vs-naive and could be
  off by hours)
- manager_controller.py date_planned: str(...)[:10] slice replaced
  with fp_format MM/DD in user's tz

Notifications + reports:
- mail_template_data.xml: 5 .strftime() calls in body_html → babel
  format_datetime / format_date with tz=(user.tz or company tz)
- report_fp_job_traveller.xml: rec.received_date (Datetime) gets
  t-options="{'widget':'datetime'}" so Odoo's QWeb renders in user tz

Settings view layout:
- fusion_plating now owns the Settings page "Fusion Plating" app shell
- fusion_plating_certificates xpaths into it instead of redefining
  (prevents app-name collision)

Verified on odoo-entech (LXC 111): post_init_hook detects
America/Toronto from /etc/timezone, MO date_start 2026-04-17 05:28 UTC
correctly displays as 2026-04-17 01:28 EDT.

Module versions bumped: fusion_plating 19.0.3.0.0,
fusion_plating_shopfloor 19.0.9.0.0, plus certificates / notifications /
reports → 19.0.3.0.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 21:03:02 -04:00
gsinghpal
956678dd27 docs(fusion_accounting): roadmap design for Enterprise takeover
Adds the brainstormed roadmap design that turns fusion_accounting from an
AI-only extension into a full replacement for Odoo 19 Enterprise accounting
(account_accountant, account_reports, accountant, account_followup, plus
selected satellites) for Nexa client deployments.

Covers:
- Sub-module topology (9 modules + meta-module): _core, _bank_rec, _reports,
  _dashboard, _followup, _assets, _budget, _ai, _migration
- Data preservation strategy: bank reconciliations verified preserved
  automatically (live in Community account.partial.reconcile);
  shared-field-ownership pattern for Enterprise extension fields on
  account.move; pre-uninstall migration wizard for Enterprise-only tables
- Phased roadmap: Phase 0 foundation through Phase 7+ optional satellites,
  with Bank Rec as Phase 1 priority and Reports as the largest phase
- Architecture rules: hybrid mirror/abstract zones, fusion.* naming,
  runtime coexistence detection, zero hard Enterprise deps
- Cross-version upgrade workflow: pinned Odoo source snapshots per version,
  annual diff ritual, UPGRADE_NOTES.md per sub-module
- AI integration via adapter pattern (current AI tools route through
  adapters that prefer fusion native, fall back to Enterprise, then to
  pure Community)
- Testing strategy, security, performance, multi-company/currency,
  localization, hosting

Implementation of each phase happens in subsequent sessions, each with
its own writing-plans pass starting with Phase 0 Foundation.

Made-with: Cursor
2026-04-18 20:55:22 -04:00
gsinghpal
e52477e2ba fix(plant-overview): priority stripe clips to card's rounded corners
The coloured priority stripe (4px vertical bar at the card's left
edge, set via ::before pseudo) extended past the top and bottom
rounded corners of the card — visible as sharp corners on cards with
Urgent or HOT priority (yellow/red stripe).

Cause:
  .o_fp_po_card::before was positioned at left/top/bottom: -1px and
  given its own border-radius, but the stripe's own radii didn't
  match the card's 14px radius precisely, and the -1px offsets
  pushed the stripe outside the card's curves.

Fix:
  1. .o_fp_po_card gets overflow: hidden. Shadows are painted outside
     the content box in CSS so box-shadow still renders fine, but any
     child element (including ::before) now clips to the parent's
     border-radius automatically.
  2. Stripe ::before simplified to left/top/bottom: 0 — no more
     negative offsets, no more independent border-radius rules.
     The parent's overflow does the corner-matching.

Verified in /web/assets/5e85f15/web.assets_backend.min.css:
  .o_fp_po_card { ...; overflow: hidden; ... }
  .o_fp_po_card::before { content: ""; position: absolute;
      left: 0; top: 0; bottom: 0; width: 4px; ... }

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 20:00:14 -04:00
gsinghpal
83271ee69e fix(shopfloor): pages own the scroll + sharp corner fix
Two problems after the previous round:

1) Mobile scroll still not working, even on a real phone.

Dug into /usr/lib/python3/dist-packages/odoo/addons/web/static/src/
webclient/webclient_layout.scss and found Odoo's mobile layout
switches scroll ownership at @media-breakpoint-down(md) (<768px):

  Desktop: .o_content has overflow:auto — your content scrolls there
  Mobile:  .o_action gets overflow:auto, .o_content is overflow:initial

Our client action roots had `min-height: 100%` and relied on an
ancestor for scroll. That ancestor changes between breakpoints, and
somewhere in the transition scroll gets lost — the page fills but
can't scroll.

Fix: make each page OWN its scroll, like .o_content on desktop
kanban/list views. Three roots now have:

  .o_fp_tablet / .o_fp_manager / .o_fp_plant_overview {
      height: 100%;
      overflow-y: auto;
      -webkit-overflow-scrolling: touch;
  }

Scroll works regardless of which ancestor Odoo decides owns it at
any given breakpoint.

2) Sharp corner on column header at mobile widths.

The previous commit set `overflow: visible` on .o_fp_po_column at
<=900px trying to help scroll. But the column has border-radius: 20px
and contains .o_fp_po_col_header (which has its own background). When
overflow is visible, the header bg extends to the column's corners
without being clipped — you see squared corners on the mobile card.

Fix: keep `overflow: hidden` on .o_fp_po_column at every breakpoint
(that's what clips the rounded corners). Only lift `max-height` on
mobile so columns size to content naturally. Since the PAGE now owns
the scroll (see fix #1), the column doesn't need internal scroll —
no `overflow: auto` on the body is needed either.

Verified in compiled CSS at /web/assets/7ff5b28/web.assets_backend.min.css:
  .o_fp_tablet          { height: 100%; overflow-y: auto; ... }
  .o_fp_manager         { height: 100%; overflow-y: auto; ... }
  .o_fp_plant_overview  { height: 100%; overflow-y: auto; ... }
  .o_fp_po_column       { border-radius: 20px; overflow: hidden }
  @media (max-width: 900px) .o_fp_po_column {
      flex: 1 1 auto; min-width: 100%; max-width: 100%;
      max-height: none;   // no overflow override — hidden stays
  }

Version bumped 19.0.6.0.0 -> 19.0.7.0.0 to force bundle hash change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 19:49:35 -04:00
gsinghpal
082c585e24 fix(shopfloor): mobile scroll works — remove nested scroll containers
User: "scrolling is not working" in Chrome DevTools mobile simulation.
Three actual problems:

1. Plant Overview columns had max-height: calc(100vh - 180px) +
   overflow: hidden, with a nested overflow-y: auto on the column
   body. Classic Trello kanban pattern — works on desktop, breaks
   on mobile. You get two scroll containers fighting each other and
   the PAGE itself can't scroll past the viewport height.

2. .o_fp_po_columns had overflow-x: auto on all widths. On the
   phone-stack breakpoint (<600px) this was also still on, creating
   another nested scroll container.

3. Draggable cards can swallow touch events on mobile because
   touch-action defaults to "auto" and Chrome's mobile simulator
   treats touch on draggable elements as potential drag-start.

Fixes — all at the <=900px breakpoint (tablets + phones):

  .o_fp_po_column          max-height: none; overflow: visible
  .o_fp_po_col_body        overflow-y: visible
  .o_fp_po_columns         flex-direction: column; overflow: visible

Plus .o_fp_po_card carries `touch-action: pan-y` unconditionally —
touch-scroll gestures never get hijacked by the draggable="true"
attribute. Desktop mousedown drag still works (HTML5 drag-drop
isn't touch-based by default).

Also added -webkit-overflow-scrolling: touch to all three page
roots (.o_fp_tablet, .o_fp_manager, .o_fp_plant_overview) and to
the internal scroll containers that remain on desktop — gives iOS
Safari proper momentum scroll (11 occurrences in the compiled
bundle).

Drag-drop JS preventDefault calls audited — they only fire on
dragover/drop (HTML5 drag events), which don't exist on touch by
default, so no touch interference there.

Verified via compiled CSS:
  .o_fp_po_card { touch-action: pan-y; ... }
  @media (max-width: 900px) .o_fp_po_column { overflow-x: visible;
         overflow-y: visible; min-height: auto }
  @media (max-width: 900px) .o_fp_po_col_body { overflow-y: visible }

Version bumped 19.0.5.0.0 -> 19.0.6.0.0 to force the bundle hash
to change. New URL: /web/assets/4a1b69e/web.assets_backend.min.css

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 19:40:52 -04:00
gsinghpal
afc01ec1d9 fix(shopfloor): proper dark-mode via \$o-webclient-color-scheme branch
Dug deeper after the user reported shop-floor pages staying white in
dark mode. Traced through Odoo 19 source:

  _dependencies/web_enterprise/static/src/
    webclient/color_scheme/color_scheme_service.js  <- reads cookie
    scss/primary_variables.scss       \$o-webclient-color-scheme: bright
    scss/primary_variables.dark.scss  \$o-webclient-color-scheme: dark

Odoo compiles TWO separate CSS bundles:
  web.assets_backend       -> compiled with \$...scheme: bright
  web.assets_web_dark      -> compiled with \$...scheme: dark
    (the .dark.scss files are layered in front of the light ones)

Our shop-floor SCSS is in web.assets_backend, which means it gets
compiled into BOTH bundles. But the previous CSS-variable fallback
chain (var(--fp-page-bg, var(--bs-tertiary-bg, #hex))) baked the
SAME hex fallback into both bundles, so cards stayed white in dark.

Odoo's own code doesn't redefine --bs-* CSS custom properties at
runtime either — it just bakes the dark palette straight into the
dark bundle via SCSS \$-variables during compile.

Fix: _fp_shopfloor_tokens.scss now branches at compile time:

    \$o-webclient-color-scheme: bright !default;

    \$_fp-page-hex: #f3f4f6;  // light defaults
    \$_fp-card-hex: #ffffff;
    ...
    @if \$o-webclient-color-scheme == dark {
        \$_fp-page-hex: #1a1d21 !global;
        \$_fp-card-hex: #22262d !global;
        ...
    }

    \$fp-page: var(--fp-page-bg, \$_fp-page-hex);
    \$fp-card: var(--fp-card-bg, \$_fp-card-hex);

The CSS-custom-property fallback stays so deployments can still skin
via --fp-* without touching SCSS; the underlying hex changes between
bundles.

Verified via odoo-shell:
  LIGHT bundle: .o_fp_plant_overview { background-color: var(...#f3f4f6) }
                .o_fp_po_card         { background-color: var(...#ffffff);
                                         border: ... #d8dadd }
  DARK bundle:  .o_fp_plant_overview { background-color: var(...#1a1d21) }
                .o_fp_po_card         { background-color: var(...#22262d);
                                         border: ... #343942 }

Two separate bundle URLs generated:
  /web/assets/a593157/web.assets_backend.min.css
  /web/assets/a9dba7d/web.assets_web_dark.min.css

=== CLAUDE.md ===
Replaced the previous (incorrect) .o_dark_mode override advice with
a proper "Branch on \$o-webclient-color-scheme at SCSS compile time"
section, including the bundle names and the verify-via-odoo-shell
snippet. Future redesigns now have a single, correct pattern to
follow.

Version bumped 19.0.4.0.0 -> 19.0.5.0.0 to force asset hash change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 19:30:14 -04:00
gsinghpal
11f7791c5e fix(shopfloor): dark mode auto-inverts + Quick View button visible
Two fixes + a memory entry in CLAUDE.md.

=== Dark mode ===

User: "when I change the theme the whole background does not turn
dark like the other pages does". Digging through Odoo 19 source:

  /_dependencies/web_enterprise/static/src/scss/
    bootstrap_overridden.dark.scss
    primary_variables.dark.scss
    secondary_variables.dark.scss

Odoo doesn't flip dark mode via a runtime .o_dark_mode class on the
DOM — it compiles a SEPARATE asset bundle where $o-webclient-color-
scheme: dark is set, which redefines every --bs-* token with dark
values. When the user toggles dark mode, Odoo swaps the whole CSS
bundle.

So my previous :root[data-bs-theme="dark"] { --fp-page-bg: #13161a; }
block was DEAD CODE — nothing ever sets data-bs-theme on the root.

Fixed: tokens now fall through to Bootstrap's --bs-* semantic tokens
before hitting a hex default, so they auto-invert when Odoo swaps
bundles. Three-level fallback chain:

  $fp-page : var(--fp-page-bg,
                 var(--bs-tertiary-bg, #f3f4f6));
  $fp-card : var(--fp-card-bg,
                 var(--bs-card-bg,
                     var(--o-view-background-color, #ffffff)));
  $fp-border : var(--fp-border-color,
                   var(--bs-border-color, #d8dadd));
  $fp-ink : var(--fp-ink, var(--bs-body-color, #1f2937));

Dead .o_dark_mode block removed. No runtime selector needed.

=== Quick View button ===

User: "Quick View button color is white with white button in light
mode." Cause: Bootstrap's .btn-primary loads AFTER our custom CSS
in the bundle and resets color: #fff, background: var(--bs-btn-bg)
— which clobbered our $fp-accent / $fp-ink assignment because a
later rule at the same specificity wins.

Fix: split the primary button into its own rule with higher
specificity (.o_fp_manager .o_fp_manager_head_actions .btn.btn-primary)
and !important on the three key properties — so Bootstrap can't
shout us down. Hover uses brightness(1.08) for a subtle darken
without needing another color assignment.

=== CLAUDE.md additions ===

Added two new rules documenting the lessons so this isn't relearned:

  Rule 8 — Odoo 19 forbids @import in custom SCSS (silent warning,
  falls back to cached bundle). Register partials in the assets list
  in load order; SCSS variables cascade through the bundle.

  "Card Styling — Copy Odoo's Kanban Pattern" section explaining:
  - Don't rely on --bs-border-color directly for card surfaces
  - Chain through $fp-* → --fp-* → --bs-* → hex
  - 3-layer contrast rule (page → container → card)
  - Reference _fp_shopfloor_tokens.scss as canonical

  "Asset Bundle Cache Busting" section with 4-step escalation path
  for when CSS changes don't show up in browser.

Verified: bundle regenerated to /web/assets/b48ab17/web.assets_backend.min.css
(id 1945). Card rule compiled with full fallback chain visible.
Primary button carries !important modifier for bg/border/color.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 19:22:17 -04:00
gsinghpal
81277edb25 fix(shopfloor): explicit hex colors like Odoo's own kanban
Why the borders weren't showing: the previous approach used
color-mix(var(--bs-body-color) 4%, var(--o-view-background-color)) for
card/column backgrounds. Under Odoo 19 the resolved values for those
variables were nearly identical to var(--bs-body-bg), so the card
surfaces visually merged into the page. Same problem for borders:
var(--bs-border-color) can render extremely faint depending on theme.

Checked what Odoo's native kanban does — dug through the compiled
CSS and found:
    .o_kanban_record { background-color: white;
                        border: 1px solid #d8dadd; }
    .o_kanban_group  { background: var(--KanbanGroup-background); }

Odoo uses EXPLICIT hex values and card-specific tokens, not the
generic body/border variables. Adopted the same approach.

New tokens in _fp_shopfloor_tokens.scss — all explicit, plus a
dark-mode override block keyed off [data-bs-theme="dark"] and
.o_dark_mode (Odoo 19 uses both):

    light                             dark
    ------------------------          ------------------
    --fp-page-bg: #f3f4f6             #13161a
    --fp-column-bg: #e9ebef           #1a1e24
    --fp-card-bg: #ffffff             #22262d
    --fp-card-soft-bg: #f8fafc        #1c2027
    --fp-border-color: #d8dadd        #343942
    --fp-ink: #1f2937                 #e5e7eb
    --fp-ink-mute: #6b7280             #8a909a

    shadow scale switched from color-mix to explicit rgba(0,0,0,...)
    so it renders identically across browsers.

All three SCSS files updated via sed to swap
var(--bs-border-color)  ->  #{$fp-border}
...then $fp-border resolves to var(--fp-border-color, #d8dadd) — a
proper card-level border that is VISIBLE (28 refs to --fp-card-bg
and 35 refs to --fp-border-color confirmed in the compiled bundle).

Plant Overview specifically now has:
  * Column: #f8fafc bg + #d8dadd border + shadow
    (column is brighter than the page it sits on)
  * Column HEADER: #ffffff inside the column, with bottom border
    (clear separator between stages)
  * Card: solid #ffffff bg + #d8dadd border + shadow
    (brightest surface, pops off the column)
  * Gap between columns: 16px so the column borders don't touch

Module version bumped to 19.0.3.0.0. Bundle regenerated at
/web/assets/0cd8bc1/web.assets_backend.min.css (1.45 MB, id 1939).
Verified by parsing compiled CSS:
  .o_fp_po_card: background-color: var(--fp-card-bg, #ffffff);
                 border: 1px solid var(--fp-border-color, #d8dadd);
  .o_fp_po_column: background-color: var(--fp-card-soft-bg, #f8fafc);
                   border: 1px solid var(--fp-border-color, #d8dadd);

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 19:13:38 -04:00
gsinghpal
2588a2b651 fix(plant-overview): real drop insertion indicator + small logo back
Three direct fixes responding to user feedback:

1. Drag-drop "simulation" — now works like Trello/Linear. As the
   cursor moves over a column, a live DOM placeholder node is
   INJECTED into the card list at the exact position the dragged
   card will drop. The placeholder is a 4px pulsing accent-coloured
   bar with a soft glow ring. Slides smoothly between cards as the
   cursor moves. Column body also gets a tinted background + inset
   accent outline for the "whole column is receptive" cue.

   Previous version only tinted the column — no indicator of WHERE
   the card would land. The new approach actually mimics the physical
   gesture: cards visually make room for the incoming card.

2. Customer logo restored at 32×32px.
   Removing it was the wrong call. It's back now as a small
   thumbnail avatar (rounded 10px corners, soft border, object-fit
   contain so wide logos don't squish). Sits to the left of the
   customer name in the card top row. Fallback icon for customers
   without a logo. Takes the same space as the step badge on the
   right — compact and organised.

3. Module version bumped 19.0.1.0.0 → 19.0.2.0.0 so the asset
   bundle content hash changes. The new compiled CSS is served at
   /web/assets/022171c/web.assets_backend.min.css (previously
   /web/assets/278b43c/...). Fresh URL forces browser to refetch —
   this is what was causing the "still no border" complaint.

Verified in compiled CSS: o_fp_po_card_avatar, o_fp_po_drop_placeholder,
o_fp_placeholder_pulse keyframes, o_fp_drop_target — all present.
Zero SCSS warnings. Module upgrade clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 19:06:40 -04:00
gsinghpal
83a999afad style(shopfloor): borders back, real drop indicator, logo demoted
Three concrete fixes based on user feedback:

1. Card borders restored — every card / panel / KPI tile / queue row /
   bake row / team card now has a thin 1px border (var(--bs-border-color))
   ON TOP of the soft shadow. That's the classic SaaS card treatment
   and solves the "jobs have no borders, they blend together" problem.
   Hover lifts the border to the accent colour (~45% mix) so cards
   feel responsive.

2. Plant Overview drop-zone indicator restored.
   - Column body gets inset outline + tinted background on dragover
     (.o_fp_drop_target class already added by onColDragOver in JS)
   - A 56px dashed placeholder bar appears at the bottom of the column
     via ::after on the drop target. That's the "here's where the card
     will land" visual the user remembered.
   - Dragged card gets scale(0.97) + slight rotation + opacity 0.4 for
     a clearer "I'm picking this up" feedback.

3. Customer logo removed from Plant Overview cards.
   The big company logo at the top of each kanban card was wasting
   space. Customer NAME still shows (in bold, full-width, with text-
   ellipsis), step badge pill stays on the right. No more wasted
   real estate on visuals nobody looks at twice.

Extra polish while in there:
   - Section headers (Tablet + Manager) now have a coloured icon badge
     — a rounded square 36×36 with tinted background + accent-coloured
     icon next to the H3 title. Adds visual weight without noise.
   - Panel head gets a 1px bottom divider.
   - Manager panels tint the icon badge per panel tone (amber for
     Unassigned, green for In Progress, blue for Team).
   - Header action buttons (Tablet scan/picker, Manager refresh/mode)
     get proper borders + hover state.
   - State dividers on bake/gate/hold rows preserved as inset shadows.

Verified: bundle rebuilt at /web/assets/278b43c/web.assets_backend.min.css
(1.45MB, id 1930). All key classes present: o_fp_drop_target,
o_fp_dragging, o_fp_po_parts_bar, o_fp_po_parts_fill, section-header
icon badges. Zero SCSS warnings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 19:01:04 -04:00
gsinghpal
067d1f01c8 redesign(shopfloor): clean slate — depth by shadow, no card borders
User feedback: the previous gradient-heavy look felt cluttered, job
cards had confusing heavy borders, the hierarchy was noisy. Wiped all
three SCSS files and both OWL templates and rebuilt from scratch with
a clean minimalist design language.

Design philosophy — the single source of truth:

  * NO borders on cards — depth comes from elevation (shadow) + a
    tiny surface-tint difference between page and card
  * ONE accent colour (var(--o-action)); semantic red/amber/green only
    for status pills and state bars
  * Shadow-only cards: $fp-elev-1, $fp-elev-2, $fp-elev-3 built on
    color-mix of foreground so they adapt to dark mode automatically
  * Generous whitespace, 8pt spacing scale ($fp-space-1 through
    $fp-space-10)
  * Type-first hierarchy: 32px page titles, 44px KPI numbers, tabular
    numerics so refreshing counts don't jitter
  * Priority/state cues via narrow 4-6px coloured bars and small dots
    — never via loud backgrounds or gradient washes
  * All interactive elements at 48px touch minimum (shop-floor gloves)

New token file (_fp_shopfloor_tokens.scss) exports:
  - $fp-space-1..10, $fp-radius-sm..xl, $fp-radius-pill
  - $fp-page / $fp-card / $fp-card-soft surface tints
  - $fp-ink / $fp-ink-soft / $fp-ink-mute / $fp-ink-faint text tiers
  - $fp-elev-1..3 layered shadows
  - $fp-text-xs..3xl type scale
  - @mixin fp-pill, fp-focus-ring, fp-card, fp-hover-only
  - fp-wash() function for state-coloured soft backgrounds

Tablet Station (fusion_plating_shopfloor.scss + shopfloor_tablet.xml):
  - Clean hero: just the title, station chip, picker + scan button
  - KPI cards: no gradient overlay, just a 10px coloured dot and big
    44px number. Hover lifts with shadow
  - Active WO: soft green wash background, no border, pulsing dot
  - Panels contain queue/baths/bakes/gates/holds — all on the same
    card surface with big rounded corners, no internal borders
  - Queue rows: flat on a soft page-tinted background, hover slides
    right 2px (no lift, cleaner)
  - Bake/Gate/Hold rows: state-coloured inset shadow as a 4px stripe,
    no border
  - Empty states: centred with a 44px muted icon and friendly copy

Manager Desk (manager_dashboard.scss + manager_dashboard.xml):
  - Matching hero with live dot that calmly pulses green during a fetch
  - 4 KPI cards in the same language as the tablet
  - Three panels (Unassigned / In Progress / Team) with coloured dots
    next to their titles instead of top accent bars
  - MO cards NO borders, subtle page-tint background, 4px left stripe
    only for priority (red HOT, amber Urgent)
  - Team cards: avatar + name + live load pill, hover slides right
  - WO expanded rows use card-soft buttons/dropdowns for low contrast

Plant Overview (plant_overview.scss):
  - Columns are now shadow-lifted cards on the tinted page background
  - Kanban cards: no border, small shadow, lift on hover
  - Priority stripe is an inset box-shadow (not a border) so hover
    transform doesn't wobble

Backend contract preserved — OWL class names, prop signatures, RPC
endpoints, and stateBadge mapping all unchanged. Only visuals.

Verified:
  * Bundle compiled to /web/assets/.../web.assets_backend.min.css
    (1.45MB, id 1926)
  * All 6 new classes present in compiled CSS
  * Zero SCSS "forbidden import" warnings
  * Zero Odoo module upgrade errors

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 18:45:16 -04:00
gsinghpal
6d1efc6c43 fix(shopfloor): register tokens SCSS in bundle, drop forbidden @import
Odoo 19 forbids local SCSS @import statements for security reasons and
silently falls back to the OLD cached CSS bundle when it sees them. My
redesign commit used:

    @import "./fp_shopfloor_tokens";

in three SCSS files. Odoo logged

    WARNING Local import './fp_shopfloor_tokens' is forbidden for
    security reasons. Please remove all @import {your_file} imports
    in your custom files.

...and the compiled bundle kept rendering the old look. That's what
the user saw.

Fix:
  1. Add _fp_shopfloor_tokens.scss as the FIRST entry in
     web.assets_backend in the manifest. Odoo concatenates the bundle
     in order, so variables/mixins in the first file are visible to
     every later file — native @import is not needed.
  2. Strip the @import "./fp_shopfloor_tokens"; line from all three
     consumer files (tablet, manager, plant overview).

Verified: asset bundle regenerated to /web/assets/.../web.assets_backend.min.css
(1.45 MB). Grepped the compiled CSS and all five new classes are present:
o_fp_tablet_header, o_fp_kpi_strip, o_fp_mgr_card, o_fp_live_dot,
o_fp_panel_unassigned. 8 radial-gradients baked in. Zero warnings in
the Odoo server log post-rebuild.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 18:35:00 -04:00
gsinghpal
298f5942eb refactor(shopfloor): modern redesign w/ gradients, theme-safe tokens
Shop-floor operators and managers live on these screens all day — the
old look worked but felt like a spec sheet. Pass through all 5 pages
with one design language: gradient KPI cards, hero banners, soft
shadows, rounded corners, large touch targets, friendly empty states.
Dark mode and light mode both look deliberate, not inverted.

New shared file: _fp_shopfloor_tokens.scss

  A single source of truth for radii, elevation (shadows that respect
  dark mode via color-mix on foreground), typography scale (tabular
  numerics for KPIs, 18px base for shop-floor readability), animation
  easings, semantic gradients (@mixin fp-grad), tone helpers
  (@mixin fp-tone), focus ring, and the 44px touch-min token.
  Every other SCSS file imports this — no duplicated colour math.

  Gradients are built on color-mix(in srgb, var(--bs-foo) X%, transparent)
  so they layer naturally on either the light or dark page background.
  No @media-prefers-color-scheme forks needed.

Tablet Station (fusion_plating_shopfloor.scss):

  * Hero banner with dual radial-gradient wash (brand + success), live
    station chip, gradient focus ring on the picker.
  * KPI cards (6-up on desktop, 2x3 on phone) get a subtle top accent
    line, coloured gradient overlay, 40px headline number, faded icon
    at corner. Tone variants (info/success/warning/danger/muted) drive
    colour without extra CSS.
  * Active WO banner is a green gradient pill with a breathing-dot
    pulse — unmissable when something is running.
  * Panels get top accents, queue rows get priority pills (HI/M/·),
    bake/gate/hold rows get colour-coded left accent bars.
  * Tiles have a 4px left stripe keyed to state + hover lift.
  * Status chips are uppercase, pill-shaped, tone-tinted with
    color-mix so they respect theme.
  * Empty states now have a large 36px icon + friendly copy instead
    of a one-liner.
  * Focus rings use the shared @mixin fp-focus-ring.

Manager Desk (manager_dashboard.scss):

  * Same hero treatment with radial gradient + live-dot pulse.
  * 3 panels carry a coloured top accent bar — amber (Unassigned),
    green (In Progress), blue (Team). Instant visual routing.
  * KPI strip matches tablet.
  * MO cards get a left priority stripe (red for HOT, amber for
    Urgent), lift on hover, expand cleanly.
  * Team avatars get a border + subtle tint background for depth.
  * Worker/tank pickers have custom focus rings.

Plant Overview (plant_overview.scss):

  * Header is now a gradient wash tied to the brand colour.
  * Work-centre columns get a thin gradient top-stripe and pill-style
    count badges.
  * Cards have real depth (layered shadow), lift harder on hover,
    change border colour on hover.

All three files share the same design tokens, so colours/shadows/
radii are identical across pages. Edit one place, everything updates.

Verified: backend asset bundle compiles clean (no SCSS errors), zero
warnings on module upgrade, asset cache cleared for fresh delivery.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 18:30:47 -04:00
gsinghpal
ae03e32b5d style(shopfloor): phone + iPad responsive across all 5 pages
Shop-floor workers and managers use phones and iPads on the line.
The existing layouts only stacked at 1100px / 1280px, which left
everything cramped on a 375px iPhone or 390px Android. Pass through
all 5 shop-floor screens with disciplined breakpoints and touch-first
sizing.

Breakpoint ladder (consistent across files):
  1400px : manager WO row: worker/tank pickers drop to their own rows
  1280px : manager grid 3 → 2 columns, Team spans both
   1100px : tablet dashboard 2 → 1 column
    900px : manager grid → 1 column; tablet + manager padding shrinks
    768px : plant overview columns stack; first-piece & bake kanbans
            already handled natively by Odoo
    600px : PHONE — all columns stack, everything full-width, every
            button min-height 44px (Apple HIG touch target), font
            shrinks for denser phone screens

Manager Desk (manager_dashboard.scss):
  - Header stacks into two full-width rows on phone, action buttons
    flex-grow to share the row
  - 3 column grid stacks earlier (900px instead of 800px) so iPad
    portrait gets a clean single-column view
  - WO rows: assign/tank pickers go full-width on their own rows at
    1400px, then the whole row stacks to 1 column at 600px
  - Cards min 56px tap zone
  - Team avatars keep their layout but cap gap on phone

Tablet Station (fusion_plating_shopfloor.scss):
  - Header: picker/scan button stack full-width on phone
  - KPI strip auto-fit by default, forced 2×3 grid on phone so 6
    tiles stay visible without scrolling past a wall of tall cards
  - Queue rows: Start/Finish buttons drop to their own row on phone,
    each flexing to 50% width → easy one-thumb tap
  - Bake/Gate/Hold rows: full stack on phone, action buttons flex-grow
  - Bath tile grid: 2-up on phone (not auto-fit)
  - Active WO banner stacks, Open-WO button full-width
  - Station picker and scan input go full-width

Plant Overview (plant_overview.scss):
  - Columns stack at 768px (already there) + 600px padding shrink,
    search input full-width, header wraps sensibly
  - Cards get min-height 64px for touch

Touch-device hover suppression:
  @media (hover: none) — hover highlights were sticking after tap on
  phones/iPads. Block them for .o_fp_queue_row, .o_fp_tile,
  .o_fp_tablet_card, .o_fp_mgr_card, .o_fp_team_card, .o_fp_po_card.

Asset cache cleared so phones pick up the new SCSS on next load.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 18:16:35 -04:00
gsinghpal
d29857078a fix(manager-desk): unstick the spinner + live updates that don't flash
Root cause of the stuck "Loading manager data..." spinner: the overview
endpoint included a search_count on sale.order.x_fc_workflow_stage,
which is a non-stored computed field. Odoo 19 raised:

  ValueError: Cannot convert sale.order.x_fc_workflow_stage to SQL
              because it is not stored

The controller silently logged the error; the JS caught and swallowed
the RPC failure, leaving state.overview=null forever. So the UI just
kept spinning while production changed around the manager.

Fixes:

1. Controller (manager_controller.py)
   - "Awaiting assignment SOs" is now computed from STORED fields only:
       state='sale' AND x_fc_receiving_status='inspected'
             AND x_fc_assigned_manager_id=False
     Same stage, legal SQL.
   - Whole endpoint wrapped in try/except; failures return
     {'ok': False, 'error': '...'} so the UI can surface them instead
     of dying silently.
   - Response carries a payload_hash (md5 of the JSON body minus
     user_name). If the client sends back known_hash and nothing has
     moved, the server returns {'unchanged': True, 'payload_hash': ...}
     and the client skips the repaint entirely. Keeps the UI quiet
     between polls.

2. OWL component (manager_dashboard.js)
   - Poll cadence tightened from 30s → 8s (production-pace).
   - Unchanged payloads don't mutate state.overview → no re-render,
     no flash. Live dot just updates its tooltip.
   - Changed payloads do an in-place MERGE of the overview (copying
     scalars/arrays onto the existing reactive object) instead of
     replacing it wholesale. OWL's diff only re-renders rows that
     actually moved.
   - isFetching guard so overlapping polls can't stack up.
   - state.loadError surfaces backend errors in a red banner with a
     Retry button — no more silent spinner.

3. UX
   - Live dot next to the title: soft green at rest, bright green
     pulsing during a fetch.
   - "Updated Xs ago" subtitle uses a getter so the label freshens
     between polls.
   - Manual Refresh button next to Quick/Detailed toggle.
   - Spinner only appears on the genuine first load; gone forever
     once the first payload lands.

Verified: the old crashing query now runs clean on demo data; odoo
logs show zero errors for the last 5 minutes of polling.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 18:06:04 -04:00
gsinghpal
a660f1f05d fix(configurator): part-level saved descriptions (not generic)
The earlier description templates were global — same 8 generic texts
applied to any part. That's useless when a customer has 3,500 parts
and each part has 3–5 canned variants (standard, masked threads,
masked bore, rework, rush packaging). That's ~17,500 rows total, and
the variants ONLY make sense in the context of a specific part number.

Restructured so descriptions live on each part:

Model changes:
  fp.sale.description.template.part_catalog_id (new M2O, indexed,
    ondelete cascade) — the primary scoping field
  fp.sale.description.template.partner_id — now a related store=True
    field pulled from the part, so customer-level search still works
  fp.part.catalog.description_template_ids (new O2M inverse) — the
    5–10 canned descriptions attached to this specific part
  fp.part.catalog.description_template_count (computed)

UI changes:
  Part Catalog form: new "Descriptions" notebook page with inline
    editable list (sequence + name + tag + description + usage_count).
    5 variants take 30 seconds to enter.
  Part Catalog form: new smart button "Descriptions" showing the count,
    jumps to the full list filtered by this part.
  Template list view: part_catalog_id column added, list ordered by
    part first. Search view adds Part filter + Part-Specific /
    Generic (No Part) filters + Group By Part.

Wizard changes:
  description_template_id domain now prioritises part-specific, falls
    through to partner, coating, or generic on a single dynamic domain.
  _onchange_suggest_template priority: part → customer → coating →
    none. No longer auto-picks a random global template when a part
    has its own.

Smoke-tested on VS-HSA201-B (Amphenol):
  5 canned variants seeded on the part form
  Wizard with this part auto-suggested the lowest-sequence one
  The part's Descriptions smart button shows "5"

Bulk data entry path for the client's 3,500 parts: either use the
inline list on each part form, or import via CSV with the new
part_catalog_id column (external_id or DB id).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:46:53 -04:00
gsinghpal
f340c87b6a feat(bridge_mrp): shop-role auto-routing + tablet worker mode (CHUNK 4/4)
Completes the worker-access story. Handoffs now route themselves.

New model fp.work.role with 8 seeded defaults (noupdate so shops can
rename/prune):
  masking · racking · plating_op · demask · oven · derack ·
  inspection · rework

Each one has a code, icon, description, sequence, active flag.
Config menu: Configuration → Shop Roles (manager-only).

Field additions:
  hr.employee.x_fc_work_role_ids (Many2many) — tag workers with the
    roles they perform. One-person shop: one employee, every role.
    Specialised shop: one role per employee. Cross-trained: multiple.
  fusion.plating.process.node.x_fc_work_role_id (Many2one) — tag
    each recipe operation with the role that performs it.
  mrp.workorder.x_fc_work_role_id (Many2one) — copied from the recipe
    operation on WO generation.

Auto-assignment on WO generation:
  _generate_workorders_from_recipe() now copies the operation's role
  onto the WO, then calls _fp_pick_worker_for_role() which picks the
  least-loaded employee (active WO count) with that role. WO lands in
  their Tablet "My Queue" the moment the MO is confirmed. No manual
  routing needed for the common case.

Tablet Station — worker mode:
  /fp/shopfloor/tablet_overview now filters to WOs where
  x_fc_assigned_user_id == env.user when the field is populated.
  KPIs (WOs Ready / In Progress) reflect the logged-in worker's load,
  not shop-wide totals. "My Queue" rows carry wo_state + can_start +
  can_finish so inline Start/Finish buttons appear.
  New JS handlers onStartWo / onFinishWo call /fp/shopfloor/start_wo
  and /fp/shopfloor/stop_wo (finish=true). One-tap progression.

Views:
  hr.employee form gets a "Shop Roles" notebook page with many2many_tags.
  Process node form gets x_fc_work_role_id inline after work_center_id.
  Work Order form shows role + assigned worker.

Smoke-tested end-to-end on WH/MO/00010:
  Masking      → Administrator (masking role)
  Racking      → Administrator (racking role)
  E-Nickel     → Andrew (plating_op, least-loaded tiebreaker)
  Demask       → Administrator (masking)
  Oven bake    → Andrew (oven)
  Derack       → Administrator (racking fallback)
  Post-plate QA → Administrator (inspection)

80 existing WOs backfilled with role + worker via name-match.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 20:08:23 -04:00
gsinghpal
1c6a460ca1 feat(shopfloor): Manager Desk — assign workers, swap tanks, take over (CHUNK 2/4)
New client action "Manager Desk" under Shop Floor menu (manager-only).
Three-column dashboard designed for the shop manager's daily job:

  Column 1 — Needs a Worker
    MOs with active WOs missing user assignment. Each card expands to
    show per-WO rows with:
      - Assign Worker dropdown (pulls from group_fusion_plating_operator)
      - Tank swap dropdown (all tanks with current bath)
      - Take Over (claims for the manager in one click)
      - Open (jump to WO form)

  Column 2 — In Progress
    MOs with workers actively running WOs. Shows who's on each step,
    lets manager reassign or take over if someone steps away.

  Column 3 — Team
    Avatar grid of operators with live queue + in-progress counts.
    Click to drill into that operator's full WO list.

KPI strip on top: Unassigned WOs, In Progress, Ready to Ship, Awaiting
Assignment SOs.

Quick / Detailed view toggle — Detailed auto-expands every card body.

New field mrp.workorder.x_fc_assigned_user_id (indexed, tracked) —
the worker currently owning this step. Will be the pivot the Tablet
Station filters on in Chunk 4.

Three new endpoints:
  /fp/manager/overview       — dashboard snapshot (30s auto-refresh)
  /fp/manager/assign_worker  — set user on a WO
  /fp/manager/assign_tank    — swap tank on a WO
  /fp/manager/take_over      — manager claims the WO (no-show coverage)

Controller is graceful when mrp module isn't installed (empty overview,
no crash) and when the bridge_mrp assignment field isn't present (falls
back to showing all active WOs as "unassigned").

Verified: 4 WOs assigned across 2 users, overview queries return the
expected counts, res.groups.user_ids (Odoo 19 API, not deprecated .users).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 20:03:01 -04:00
gsinghpal
095d9f487c feat(bridge_mrp): SO workflow stage + contextual buttons (CHUNK 3/4)
Sale Order form now guides the user through the next step without
making them navigate between screens.

New computed field sale.order.x_fc_workflow_stage with 12 stages:
  draft → awaiting_parts → inspecting → accept_parts → assign_work
       → in_production → ready_to_ship → shipped → invoicing
       → paid → complete (+ cancelled)

Driven by SO.state + x_fc_receiving_status + MO state +
delivery.state + invoice payment state.

Five contextual header buttons (only 1-2 visible at any time,
fusion_claims pattern — invisible="x_fc_workflow_stage != 'foo'"):

  Mark Inspecting       → flips receiving to 'inspecting'
  Accept Parts          → flips receiving to 'accepted' + SO status
                          to 'inspected', unlocks manager assignment
  Assign To Me & Release → manager claims the job, confirms all draft
                          MOs (which auto-generates WOs already)
  Open Shop Floor       → jumps to Plant Overview during production
  Mark Shipped          → closes open delivery records → triggers
                          auto-invoice per strategy

Info banner shows current stage + assigned manager on the sheet so
users always know where they are.

New fields:
  sale.order.x_fc_assigned_manager_id (Many2one res.users, tracked)
  mrp.production.x_fc_assigned_manager_id (Many2one, propagated on
                                           MO confirm)

MO.action_confirm() now pulls the assigned manager from the SO (or
falls back to SO.user_id) and sets it on the MO — sets up the
Manager Dashboard (chunk 2) and role-based assignment (chunk 4) to
filter "my jobs" cleanly.

Smoke-tested across 10 demo SOs — stages compute correctly:
  S00028 → ready_to_ship, S00027-25 → awaiting_parts,
  S00023-20 → complete/shipped, S00029 → draft.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 19:57:41 -04:00
gsinghpal
28dd7fdd76 feat(certificates): per-customer document preferences (CHUNK 1/4)
Customers can now pick which shipping-time documents they actually want
instead of the shop remembering it per account. Four booleans on
res.partner (only shown on companies, not contacts):

  x_fc_send_coc              (default True)  Certificate of Conformance
  x_fc_send_thickness_report (default True)  Thickness readings
  x_fc_send_packing_slip     (default True)  Packing slip PDF
  x_fc_send_bol              (default False) Bill of Lading

Surfaced in a "Plating Documents" page on the customer form.

Two downstream gates:

1. fp.notification.template._collect_attachments() now reads the flags
   when attaching CoC / thickness / packing / BoL PDFs to the shipping
   confirmation email. Flags missing on the partner (e.g. legacy
   customers) fall back to the original defaults so nothing regresses.

2. mrp.production.button_mark_done() only auto-creates the quality
   documents the customer wants. A customer that unchecks both CoC and
   thickness gets zero certs auto-generated — shop can still create
   them manually if needed.

Note: today a standalone thickness-only report template doesn't exist,
so when a customer asks for thickness only (CoC off, thickness on) the
dispatcher still attaches the CoC PDF (which carries thickness data)
but with CoC creation gated off. A dedicated thickness-only template
is a follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 19:54:54 -04:00
gsinghpal
f94be9dfa9 fix(part-catalog): upload slot + swapped Number/Name + smart buttons
Three fixes on fp.part.catalog form:

1. 3D Model upload actually works now. The old field exposed only a
   Many2one search dropdown — no way to add a new file. Added a
   Binary upload slot (model_upload + model_upload_filename) that
   fires an onchange which wraps the bytes in an ir.attachment and
   links it to model_attachment_id. The upload slot is hidden once a
   model is already attached, so the current file stays visible.
   Accepts STEP/STP/STL/IGES/IGS/BREP. Auto-runs the surface-area
   calculation after attach, same as before.

2. Part Number is now the big <h1> title, Part Name is the smaller
   field underneath. Matches how plating shops actually identify
   parts (by customer part number, not a free-text name). Swapped
   column order in the list view too — Part Number first, then Name.

3. Four smart buttons now on the part form:
     - Customer  → opens res.partner record
     - Sale Orders  (already existed)
     - Work Orders  → filtered mrp.workorder list across SOs for this part
     - Quotes  (already existed)
     - Revisions  → shown only when 2+ revs exist, opens the revision
       tree filtered by root part
   New compute fields workorder_count + revision_count feed the
   statinfo widgets, with matching action_view_customer,
   action_view_workorders, action_view_revisions handlers.

Verified on demo data:
  VS-ESMC6H00801P01 → SO=2, WO=18, REV=2
  VS-PQR8440        → SO=1, WO=9,  REV=3
  All counts light up, buttons drill in cleanly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 19:00:52 -04:00
gsinghpal
70fe10c214 fix(configurator): money fields now show $ everywhere
Root cause: pricing.rule records had currency_id=NULL because the
default=lambda only applies on new records. Monetary fields without a
currency silently render as plain numbers — no $ symbol.

Fixes:

1. currency_id now required=True on fp.pricing.rule, fp.treatment,
   fp.customer.price.list, fp.quote.configurator, fusion.plating.quote.request
   — so it can never be missing going forward.

2. post_init_hook + matching backfill helper in
   fusion_plating_configurator/__init__.py pins the company currency
   on any existing records that were created before the required flag.
   Ran on upgrade → all 4 pricing.rule rows now have CAD/$.

3. Flipped two remaining Float money fields to Monetary:
   - fp.job.consumption.unit_cost and total_cost (were Float digits=4/2)
   - (mrp.workorder.x_fc_workcenter_cost_hour stays Float — it is a
     related field from core mrp.workcenter.costs_hour which is Float)

4. Every Monetary field reference in views now has explicit:
     widget="monetary" options="{'currency_field': 'currency_id'}"
   Previously Odoo's default rendering dropped the $ in some contexts.
   Touched: fp_pricing_rule_views (list + form), fp_treatment_views,
   fp_customer_price_list_views (already done), fp_quote_configurator_views
   (list + form shipping/delivery/calculated/override), fp_quote_request_views
   (list + form), fp_job_consumption_views, mrp_production_views job-costing
   group, direct-order wizard (already done earlier).

5. Unit / % suffix polish as we went: rush_surcharge_percent shows "%",
   default_duration_minutes shows "min" on treatment form, treatment list
   labels duration column.

Verified: all 4 pricing rules now render "$0.45", "$0.85" etc; 62 records
across 6 models all have currency_id populated; zero remaining Float $
fields in the codebase.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 18:54:14 -04:00
gsinghpal
b85642816f feat(configurator): menu reorder, currency/unit display polish, line description templates
Three UX improvements:

1. Sales menu reordered — New Quote (seq 1) is now the first entry,
   followed by New Direct Order (5), Quotations (10), Sale Orders (20).
   "New Quote" moved out of Configurator submenu into Sales so both
   quote-creation paths live side-by-side.

2. Currency + unit display audit:
   - fp.customer.price.list.unit_price flipped from Float to Monetary
     with currency_field='currency_id' — list view now shows $ symbol
     and a Total sum row
   - fp.direct.order.wizard.unit_price flipped to Monetary, added
     currency_id field and computed line_subtotal ($)
   - % suffix appended to deposit_percent and progress_initial_percent
     in the wizard
   - Unit suffixes added where missing: bake_window.quantity (pcs),
     window_hours (h), bake_temp (°F), bake_duration_hours (h);
     bath.volume (L), bath.mto_count (turnovers); tank.volume shows
     volume_uom inline

3. Saved line descriptions (new feature):
   - New model fp.sale.description.template with name, description,
     tag (standard/masking/rework/aerospace/nuclear/packaging/other),
     optional coating_config_id and partner_id, usage_count bumped
     on each use
   - List + form + search views; new "Line Descriptions" menu under
     Configurator
   - 8 starter templates seeded (noupdate=1): ENP Standard/Aerospace/
     Nuclear, masking variants, rework, packaging, delicate handling
   - Direct Order Wizard gets a template picker (searchable Many2one)
     + editable paragraph; picking a template copies text to the
     editable field, user tweaks freely, tweaked text lands on the
     SO line as "<header>\n\n<description>"
   - Auto-suggests template on coating+partner match if nothing
     picked yet

Smoke-tested end-to-end: picked aerospace template, tweaked text,
confirmed wizard → SO S00030 has full description on line, usage
counter bumped from 0 to 1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 18:43:58 -04:00
gsinghpal
b09538b4e2 changes 2026-04-17 17:31:12 -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
3b5b5cbf7c feat(reports): centralised Job Traveller / Shop Router
One PDF that follows a job through the shop — prints from either the
Sale Order or the Manufacturing Order. Matches existing design language
(fp_landscape_styles, .fp-header-primary banners, bordered tables,
.sig-line for sign-off, .highlight-box for callouts).

Sections per traveller:
  1. Title bar with REWORK / RUSH ORDER badges
  2. Job header — customer, PO #, part #, coating, recipe, facility,
     qty, dates, current parts location
  3. Receiving summary — received qty, state, damage flag
  4. Process Routing table — one row per WO with step #, operation,
     work centre, bath, tank, target thickness, dwell, expected
     duration, + sign-off columns (operator, date/time, initials,
     qty pass/reject)
  5. Bath chemistry targets snapshot per bath used
  6. Quality holds — red callout only when present
  7. Certificates issued + Delivery info (side-by-side)
  8. Rework reason block (only on rework MOs)
  9. Ruled notes / exceptions area
  10. Final supervisor + QA sign-off

Four ir.actions.report entries registered:
  - Job Traveller (Landscape) on mrp.production  [default print]
  - Job Traveller (Portrait)  on mrp.production
  - Job Traveller (Landscape) on sale.order      [iterates MOs]
  - Job Traveller (Portrait)  on sale.order

Regression-tested all 15 existing reports (SO, WO, MO margin, invoice,
BoL, CoC EN, receipt) — every one still renders.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 02:48:03 -04:00
gsinghpal
adc27c637a feat(bridge_mrp): SO smart buttons for full production lifecycle
Sale Order form now hubs the full flow — Manufacturing, Work Orders,
Portal Jobs, Quality Holds, Certificates, Deliveries — hidden when
count == 0. Clicking each jumps to the filtered list/form so users
can drill in without leaving the SO.

Counts are computed on the fly from: mrp.production.origin == SO.name,
production.workorder_ids, production.x_fc_portal_job_id, quality.hold
production_id, fp.certificate.sale_order_id, fp.delivery.job_ref.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 02:33:21 -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
cb79186325 fix(coc): customer logo in 3rd column of customer block (not separate row)
Moved doc.partner_id.image_1920 from a standalone right-aligned div
below the accreditation table to a third column (20% width, centre-
aligned) of the customer-info table — sits inline with Customer
Address (40%) and Contact Name/Email/Phone (40%). The customer
block is now a single bordered 3-column row.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 02:07:03 -04:00
gsinghpal
edd52f16a7 fix(coc): bump top padding to 50mm
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 02:04:52 -04:00
gsinghpal
22b06f47d9 fix(coc): bump top padding to 36mm to fully clear external_layout header
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 02:03:40 -04:00
gsinghpal
71bd0da5e1 fix(coc): add 18mm top padding so title clears external_layout header
Body was overlapping the company letterhead band — added padding-top
to .fp-coc so the title starts below it cleanly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 02:02:09 -04:00
gsinghpal
44a980c468 refactor(coc): use web.external_layout for header/footer + 3-column bordered accreditations
Per feedback, dropped the custom company-contact header and paperformat
in favour of Odoo's standard web.external_layout. This gives the CoC:
  - Company-branded header (logo, name, address, phone, email, tax id)
    matching whichever layout variant the company picked in
    Settings → General → Document Layout (Standard / Boxed / Clean /
    Striped). Automatically themed with company.primary_color.
  - Consistent page X/Y footer + "Printed on" timestamp.
  - Correct header_spacing so the letterhead band lines up with the
    default paperformat.

Our body now owns:
  - Centred "Certificate of Conformance" / "Certificat de Conformité"
  - 3-column bordered accreditation table — one logo per cell (Nadcap,
    AS9100D/ISO 9001, CGP) with equal 33.33% widths and #000 borders,
    2.8cm cell height so logos centre vertically
  - Optional customer logo (res.partner.image_1920) right-aligned
    below the accreditation row
  - Customer info block (name, address, contact, email, phone)
  - Certification info table (date, generated-by, WO#)
  - Quantities table (part, process, PO, shipped, NC qty, job no)
  - Signature image + bordered cert statement
  - "Fusion Plating by Nexa Systems" brand note

Template plumbing:
- Explicit `<t t-set="company" t-value="doc.sale_order_id.company_id
  or doc.production_id.company_id or env.company"/>` in the EN/FR
  wrappers because QWeb's t-call scoping doesn't expose variables set
  inside external_layout to the body we pass through. Without this,
  coc_body's `company.x_fc_owner_user_id` raises KeyError.
- Removed paperformat_fp_coc from the report actions (now uses the
  default paperformat, which is designed for external_layout's
  reserved header_spacing).

Verified: 332KB PDF, 1 page, all 5 images embedded, Amphenol logo on
right side of accreditation row, signature renders, company header
band at top, page footer at bottom.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 01:59:54 -04:00
gsinghpal
66f7f6c644 fix(coc): single-page layout — custom paperformat + strip Odoo wrappers
The CoC was rendering on 2 pages with ~35mm of dead whitespace at the
top. Three compounding causes:

1. Default Odoo paperformat reserves header_spacing=35mm (where the
   standard letterhead would sit when using web.external_layout). Our
   CoC has its own full-bleed header so that reservation was pure
   empty space.
   → New paperformat_fp_coc with header_spacing=0, 8mm all-around
     margins, attached to both report_coc_en and report_coc_fr actions.

2. The `<div class="article o_report_layout_boxed">` and nested
   `<div class="page">` wrappers inherited Odoo's CSS which applies
   `page-break-after: always` on `.page` and additional padding on
   `.article`.
   → Dropped both wrappers — template now renders body directly
     inside html_container.

3. Inline style block didn't override Odoo's body/main padding.
   → Aggressive !important reset at the top of the style block on
     html, body, main, .article, .page, and the hidden header/footer
     classes. Also shrunk all paddings by ~30% and bumped base font
     to 9pt to guarantee single-page fit.

Verified: PDF is now 1 page, content starts at the top (title flush
with top margin), accreditation logos + customer logo + signature all
render correctly within the single page.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 01:50:35 -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
gsinghpal
a623c6684d fix(fusion_plating): bug review fixes + progress/net-terms invoicing + formal CoC rebuild
Bug review fixes (found by code review + live QWeb error):
- report_fp_sale.xml: product_uom → product_uom_id (Odoo 19 renamed;
  was raising KeyError during PDF render, blocking all sale-order prints)
- mrp_production.button_mark_done: add idempotency guard on delivery
  auto-create (was duplicating on every re-close)
- fp.certificate._compute_batch_ids: use empty recordset instead of
  False for Many2many computed fields
- fp_notification_template._collect_attachments: collapse attach_quotation
  + attach_sale_order into a single render so email doesn't double-attach
  the same PDF
- fp.operator.certification: SQL unique on computed state was unreliable;
  added explicit `revoked` boolean, made state pure-compute, replaced
  SQL constraint with @api.constrains that checks active-only uniqueness;
  has_active_cert now reads revoked + expires_date directly (no stale
  stored state between nightly recomputes)

Two missing invoice strategies implemented + 1 pre-existing deposit bug fix:
- Progress Billing: new x_fc_progress_initial_percent field on sale.order;
  _create_progress_initial_invoice bills the configured % on SO confirm
  via down-payment wizard, _create_final_balance_invoice bills the
  remainder on delivery
- Net Terms: no invoice on confirm; full invoice auto-created when
  fusion.plating.delivery.action_mark_delivered fires
- Fix for deposit (pre-existing, silent): sale.advance.payment.inv
  reads active_ids at wizard-create time, not on create_invoices();
  context was being set on the wrong call, so every deposit attempt
  raised "Expected singleton" and message-posted to chatter instead
  of actually invoicing
- New fusion_plating_invoicing/models/fp_delivery.py hooks
  action_mark_delivered to dispatch final invoice for progress/net_terms
- fp.direct.order.wizard + SO form surface the progress_initial_percent
  field (conditional on strategy)

Report styling cleanup:
- Hide DISCOUNT column from sale + invoice landscape reports unless at
  least one line has a non-zero discount; colspan auto-adjusts
- Replace hardcoded #0066a1 in all reports with company.primary_color
  driven by doc.company_id → company → user.company_id fallback chain,
  with #1d1f1e as ultimate fallback; new .fp-header-primary class
  exposes the colour for inline section headers (CARGO DESCRIPTION,
  PAYMENT DETAILS, OPERATOR SIGN-OFF, etc.) so they retint with the
  company theme without template edits

Certificate of Conformance — formal ENTECH-style rebuild:
- New res.company fields: x_fc_owner_user_id (default signer, sig from
  hr.employee.signature), x_fc_coc_signature_override (manual upload),
  x_fc_{nadcap,as9100,cgp}_logo + _active toggles for accreditation
  badges
- New res.config.settings section "Fusion Plating" exposing the above
  as configurable blocks; manager-only menu under Configuration →
  Fusion Plating Settings
- New fp.certificate fields: nc_quantity, customer_job_no,
  contact_partner_id (child contact for Name / Email / Phone block)
- New report_coc_en + report_coc_fr templates (primary): custom header
  (company contact | accreditations | company logo), bilingual labels
  per variant, customer info block with customer logo, 3-column cert
  info table, 6-column line-item table (Part # | Process | Customer
  PO | Shipped | NC Qty | Customer Job No.), signature image + bordered
  certification statement, footer "Fusion Plating by Nexa Systems"
- Legacy report_coc + report_coc_portrait kept for existing portal-job
  bindings (no behaviour change)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 01:18:22 -04:00
gsinghpal
6658544f85 feat(fusion_plating): Tier 2 (quality + audit) and Tier 3 (business) features
Tier 2 — Quality & audit readiness:

- T2.1 SPC on thickness readings (fp.certificate)
  - spec_min_mils / spec_max_mils auto-pulled from coating config on create
  - Computed: std_dev_mils, min/max, cpk, cpk_status (incapable/marginal/
    capable/excellent/insufficient)
  - Western Electric trend rules (rule 1: any point beyond 3σ; rule 4:
    8 consecutive on one side of mean) → trend_alert + explanation
  - New SPC group on certificate form with badge-coloured indicators

- T2.2 Operator certification enforcement (fp.operator.certification)
  - Per (employee, process_type) records with issued/expires dates,
    training record attachment, revocation workflow
  - State auto-computed: active → expired when date passes
  - MrpWorkorder.button_start() blocks with UserError if current user's
    linked hr.employee lacks an active cert for the bath's process_type
  - Managers bypass the check; expiring-soon filter in search view
  - HR Employee form: "Plating Certifications" tab

- T2.3 Material traceability chain
  - fusion.plating.batch.workorder_id (new Many2one) + production_id
    (related through WO) for full chain
  - fp.certificate gets computed batch_ids / bath_ids / batch_count
  - "Batches" stat button → list of batches used for this cert's MO,
    with their chemistry logs intact

- T2.4 Pre-treatment as first-class baths
  - process_family selection on fusion.plating.process.type
    (pre_treatment / plating / post_treatment / bake / strip / passivation /
    masking / inspection)
  - Bath search view: Pre-Treatments / Plating / Post-Treatments / Strip
    quick filters
  - Existing bath infra (logs, replenishment, SPC) now applies to pre-
    treatment baths equally

Tier 3 — Business / revenue:

- T3.1 Customer-specific price lists (fp.customer.price.list)
  - Per (customer, coating_config) with unit_price + basis (per_part /
    sqin / sqft / lb)
  - effective_from / effective_to for annual contract pricing
  - min_quantity for volume breaks (cheapest price at requested qty wins)
  - _find_price() helper resolves active entry by date + qty
  - Direct Order wizard auto-fills unit_price on (partner, coating, qty)
    change unless operator has typed an override
  - Configurator menu → Customer Price Lists

- T3.2 Quote win/loss tracking (fp.quote.configurator)
  - State values: draft → confirmed (won) / lost / expired / cancelled
  - lost_reason selection (price / lead_time / tech / spec_mismatch /
    no_bid / no_response / competitor / other) + lost_competitor_name
    + lost_details text
  - Action buttons: Mark as Lost (requires reason), Mark as Expired
  - won_date auto-set on SO creation; lost_date auto-set on mark_lost
  - New "Win / Loss" tab on configurator form

- T3.3 Actuals vs. quoted margin (mrp.production)
  - Computed monetary fields: x_fc_consumables_cost, x_fc_labour_cost,
    x_fc_actual_cost, x_fc_quoted_revenue, x_fc_margin_actual,
    x_fc_margin_pct
  - Labour = sum(WO duration × workcentre cost_hour)
  - Revenue = SO amount_untaxed via mo.origin lookup
  - New "Job Costing" group on MO form with badge-coloured margin

- T3.4 Job consumables tracking (fp.job.consumption)
  - One row per consumable event (bath replenisher, masking tape, PPE,
    chemistry): product, qty, uom, unit_cost (snapshot), total_cost,
    source, optional workorder link
  - One2many x_fc_consumption_ids on mrp.production
  - "Consumables" stat button on MO → filtered list

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-16 23:55:22 -04:00
gsinghpal
d3dd6376a6 feat(fusion_plating): quote-to-cash infra, notifications, wizards, Tier 1 plating features
Quote-to-cash PDF reports (portrait + landscape variants, 16 new actions):
- Quotation / Sales Order, Work Order Traveller, Packing Slip, Bill of Lading,
  Certificate of Conformance (portrait added), Invoice, Payment Receipt
- Shared fp_portrait_styles + fp_landscape_styles base templates

Workflow gap fixes (fusion_plating_bridge_mrp):
- Auto-assign recipe from SO coating config in MrpProduction.action_confirm
- Auto-create draft CoC (fp.certificate) on MrpProduction.button_mark_done

Notifications overhaul (fusion_plating_notifications v2.0):
- Expanded TRIGGER_EVENTS to 7 (added quote_sent, mo_complete, shipped, payment_received)
- Shared _dispatch method replaces three duplicated send helpers
- Auto-attach PDF reports per template config (quote, SO, CoC, invoice, receipt, BoL)
- Rebuilt 7 email templates with fusion_claims accent-bar design
  (info/success color-coded, theme-safe, 600px max-width)
- New hooks: MrpProduction done, FpDelivery mark_delivered, AccountPayment post,
  SaleOrder action_quotation_send

Wizards (fusion_plating_configurator):
- fp.direct.order.wizard — skip quotation for repeat customers with PO in hand;
  optional new-revision drawing upload bumps fp.part.catalog revision and links
  new rev to the SO; creates + confirms the SO in one step
- fp.part.catalog.import.wizard — 3-step CSV import with dry-run preview,
  tolerant parsing (customer by name/email/xmlid, human-readable selections),
  duplicate detection, create-missing-customers option, single transaction commit
- Partner form stat buttons: Direct Order, Import Parts
- CSV template download button

Tier 1 practical plating features:
- T1.1 Hydrogen bake window enforcement (fp.coating.config.requires_bake_relief,
  auto-create fusion.plating.bake.window on plating WO finish, FpDelivery lockout
  when window is open)
- T1.2 Bath replenishment rules + pending suggestion queue
  (fusion.plating.bath.replenishment.rule + .suggestion, hook on bath log line
  create, operator Apply / Dismiss actions)
- T1.3 Rack/fixture library (fusion.plating.rack with MTO counter, strip
  schedule, lifecycle: active → needs_strip → stripping → retired)
- T1.4 Rework / strip-and-replate MOs (x_fc_is_rework, x_fc_original_production_id,
  Create Rework stat button on completed MOs)
- T1.5 Parts location (x_fc_current_location computed on mrp.production —
  "In progress: Alkaline Clean" / "Queued: Bake Oven" / "Ready to Ship")

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-16 23:41:12 -04:00
gsinghpal
7c7ef06057 folder rename 2026-04-16 20:53:53 -04:00
gsinghpal
3f3ddcbab4 changes 2026-04-14 10:17:55 -04:00
gsinghpal
e0e2c6cfda Merge branch 'main' of https://github.com/gsinghpal/Odoo-Modules 2026-04-14 08:05:58 -04:00
gsinghpal
b62d4b1f36 changes 2026-04-14 08:05:56 -04:00
gsinghpal
4f97a8b089 changes 2026-04-14 05:28:05 -04:00
gsinghpal
d3c8782505 changes 2026-04-13 09:45:28 -04:00
gsinghpal
0ff8c0b93f changes 2026-04-13 02:35:35 -04:00
gsinghpal
1176ba68ae fix(fusion_tasks): disable map view assets — JS imports break factory enterprise bundle 2026-04-12 22:07:00 -04:00
gsinghpal
d58f11384e fix(configurator): disable 3D viewer assets — causes JS fatal error in asset bundle 2026-04-12 22:03:16 -04:00
gsinghpal
510fd02e9d CLAUDE.md: comprehensive update — all 8 phases built, 29 models documented
- Added 7 new modules to structure (configurator, receiving, invoicing,
  certificates, notifications, fusion_tasks)
- Added 5 new critical rules (res.groups privilege_id, XML comments,
  XML action ordering, module install flag, implied group cascade)
- Updated naming conventions (fp.* prefix for new models, currency_id)
- All 3 workflow gaps marked DONE with implementation details
- Architectural decisions documented (recipe→WO, account hold, email,
  configurator, model naming, security groups)
- Key models table expanded from 13 to 29 models

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 21:10:41 -04:00
gsinghpal
3d0e3e276b fix(portal): remove double-hyphen in XML comment (invalid XML syntax) 2026-04-12 21:05:19 -04:00
gsinghpal
2af9d37f45 feat(portal): customer configurator wizard — upload, coating, estimate, submit
Three-step self-service quoting flow on the customer portal:
- Step 1: Upload part (STL/PDF) or enter manual measurements + material
- Step 2: Select coating config from card grid with specs and thickness
- Step 3: View estimated price range and submit quote request

Adds dependency on fusion_plating_configurator for fp.coating.config
and fp.pricing.rule models. Price estimation uses the same rule-matching
logic as the backend configurator with a +/-15-25% range.

Dashboard updated with prominent "Get a Quote" button and portal sidebar
entry. Breadcrumbs added for configurator pages.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 21:00:59 -04:00
gsinghpal
3db30339b5 feat(configurator): Three.js 3D viewer for STL files
Add OWL field widget (fp_3d_preview) that renders uploaded STL files
in an interactive 3D viewport:
- Three.js r170 ESM loaded lazily via dynamic import with importmap
- STLLoader + OrbitControls for full model interaction
- Fallback binary STL parser when addon import fails
- Toolbar with wireframe toggle and camera reset
- Vertex/face count display
- Theme-aware SCSS using CSS custom properties and $border-color
- Registered on model_attachment_id in the Part Catalog form

Vendored libs: three.module.min.js (691KB), STLLoader.js, OrbitControls.js

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 20:54:30 -04:00
gsinghpal
795c66c126 feat(configurator): server-side surface area calculation from STL
Add trimesh-based surface area calculation for uploaded STL files:
- New /fp/configurator/calculate_surface_area jsonrpc endpoint
- action_calculate_surface_area() method on fp.part.catalog
- "Calculate from 3D Model" button visible when a 3D model is attached
- Returns area in sq in (converted from mm2), vertex/face counts,
  and bounding box dimensions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 20:53:54 -04:00
gsinghpal
14d7781f4a fix(fusion_tasks): clean sync refs from views and JS map component
Remove x_fc_sync_source, x_fc_is_shadow, x_fc_sync_client_name,
x_fc_source_label references from form/list/kanban/calendar XML
views and the map view JS component.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 20:32:26 -04:00
gsinghpal
f06e48e1a2 feat(fusion_tasks): delivery-specific fields + reduced task types
Add delivery integration fields (delivery_id, sale_order_id,
portal_job_id, packages_count, weight_total, requires_signature,
requires_photo, coc_attachment_id). Reduce task_type selection to
delivery/pickup/return/rush with matching duration defaults.
Add delivery cascade in action_complete_task() that calls
delivery_id.action_mark_delivered() on completion.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 20:30:04 -04:00
gsinghpal
10607c48f0 refactor(fusion_tasks): update deps, config, crons for delivery context
- Change depends to [base, mail, hr, fusion_plating_logistics]
- Rename module to 'Fusion Plating -- Delivery Tasks'
- Replace all fusion_claims.* config params with fusion_tasks.*
- Remove sync crons (pull_remote_tasks, cleanup_old_shadows)
- Remove sync config ACL lines from ir.model.access.csv
- Replace sales_team group refs with hr/base equivalents
- Update license from OPL-1 to LGPL-3

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 20:28:38 -04:00
gsinghpal
e10bf9d8fd refactor(fusion_tasks): strip all sync references from task model
Remove cross-instance sync fields (x_fc_sync_source, x_fc_sync_remote_id,
x_fc_sync_uuid, x_fc_is_shadow, x_fc_sync_client_name, x_fc_sync_client_phone,
x_fc_source_label), sync push calls in create/write/action methods, shadow
task logic in constraints and email guards, and x_fc_tech_sync_id from
res.users. Also remove task_sync import from models/__init__.py.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 20:26:23 -04:00
gsinghpal
bc72486808 feat(fusion_tasks): copy from Entech Plating, remove sync system
Forked fusion_tasks module into fusion-plating repo for EN Tech
delivery dispatch. Removed:
- models/task_sync.py (748 lines — cross-instance sync)
- views/task_sync_views.xml
- __pycache__ directories

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 20:22:09 -04:00
gsinghpal
234a5b2b9f feat(notifications): workflow hooks + views + menu
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 20:11:54 -04:00
gsinghpal
ad6906254f feat(notifications): module scaffold + models + mail template seed data
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 20:09:07 -04:00
gsinghpal
ab99aaa5da feat(bridge_mrp): recipe-to-workorder generation on MO confirm (v19.0.2.1.0)
Add _generate_workorders_from_recipe() which walks the recipe tree,
creates one mrp.workorder per operation node, and formats child step
nodes as plain-text WO instructions. Respects opt-in/out overrides
from the per-job configuration wizard. Called automatically at the end
of action_confirm() after portal job creation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 20:01:31 -04:00
gsinghpal
24656cc02b fix(certificates): search view — remove group string attr, fix date filter syntax 2026-04-12 19:46:20 -04:00
gsinghpal
54540d5b1e feat(certificates): fp.certificate model + views + menu
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 19:44:00 -04:00
gsinghpal
ec8b26f8c8 feat(certificates): module scaffold + fp.thickness.reading model
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 19:42:33 -04:00
gsinghpal
7dea212c13 feat(invoicing): strategy auto-fill + hold check + deposit/COD automation + menu
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 19:36:27 -04:00
gsinghpal
10e3ada9e9 feat(invoicing): module scaffold + strategy defaults + account hold
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 19:33:44 -04:00
gsinghpal
d13517071c feat(receiving): SO auto-create + MRP soft gate + menu structure
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 19:24:54 -04:00
gsinghpal
6a368993bf feat(receiving): fp.receiving model with state machine and views
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 19:22:22 -04:00
gsinghpal
d06b9fd522 feat(receiving): module scaffold + fp.receiving.damage + fp.receiving.line models
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 19:18:57 -04:00
gsinghpal
269469aa4f fix(configurator): add user_ids to shop manager group so admin sees menus 2026-04-12 18:58:36 -04:00
gsinghpal
081612c903 fix(configurator): move action_fp_customers before menu reference 2026-04-12 18:53:13 -04:00
gsinghpal
86985bc023 fix(configurator): use privilege_id not category_id on res.groups (Odoo 19)
category_id is not valid on res.groups in Odoo 19.
Use privilege_id + sequence matching core module pattern.
Also remove user_ids (CLAUDE.md rule).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 18:52:03 -04:00
gsinghpal
aec7659a2e fix(configurator): address code review findings — pricing engine + views
- Fix thickness factor: now scales linearly (thickness * factor), not
  multiplicatively. Default factor=1.0 means price scales 1:1 with mils.
- Fix batch_size: setup fee now multiplied by ceil(qty/batch_size) batches
- Fix hardcoded $ in price breakdown HTML: uses currency_id.symbol
- Add coating_config_id.certification_level to @api.depends
- Remove readonly on x_fc_receiving_status (placeholder until receiving module)
- Add currency_id to treatment list view for Monetary widget

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 18:43:02 -04:00
gsinghpal
a337a510c1 feat(configurator): seed data — common pre/post treatments
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 18:37:03 -04:00
gsinghpal
a5761b9863 feat(configurator): menu restructure — Sales as default landing in Fusion Plating
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 18:36:47 -04:00
gsinghpal
d3e2614620 feat(configurator): fp.quote.configurator — pricing engine + SO creation
Add the core configurator model that collects part geometry, coating
config, and pricing inputs, calculates a price from matching pricing
rules (scored by specificity), and creates sale orders on confirmation.

- fp.quote.configurator model with mail.thread, sequence numbering
- Stored computed price with full breakdown HTML table
- Estimator override price support
- Auto-population from part catalog and coating config onchanges
- Surface area normalization (sq in/ft/cm/m)
- Specificity-scored rule matching (coating > substrate > cert level)
- action_create_quotation creates SO with FP-SERVICE product
- Form/list/search views with statusbar and chatter
- ACL: operator (read), estimator (read/write/create), manager (full)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 18:35:08 -04:00
gsinghpal
5143245f57 feat(configurator): sale.order plating extensions + custom list/form views
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 18:29:24 -04:00
gsinghpal
2fa7f2aa2e feat(configurator): fp.pricing.rule — formula-based pricing engine with complexity surcharges
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 18:28:43 -04:00
gsinghpal
2e80fd3ca1 feat(configurator): fp.coating.config — coating configuration templates
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 18:25:48 -04:00
gsinghpal
87325e2caf feat(configurator): fp.part.catalog — customer part library with views
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 18:24:54 -04:00
gsinghpal
73b7325b46 feat(configurator): module scaffold + fp.treatment model with views and ACL
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 18:21:55 -04:00
gsinghpal
dde970a2f5 docs: Phase 1 implementation plan — configurator + sales integration
10 tasks covering: module scaffold, 7 models (treatment, part catalog,
coating config, pricing rule, complexity surcharge, configurator, SO
extensions), security groups, menu restructure, seed data, integration test.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 18:14:30 -04:00
gsinghpal
d424dfdb19 docs: address spec review findings — 5 critical, 8 major issues fixed
- Add model naming convention table (fp.* for new, fusion.plating.* for existing)
- Add fusion_plating_certificates as dedicated module with fp.thickness.reading model
- Fix complexity_surcharge: companion model instead of JSON text field
- Add recipe_id domain constraint [('node_type', '=', 'recipe')]
- Align security groups with existing 4-level privilege hierarchy
- Add currency_id to all monetary models
- Clarify fp.quote.configurator as persistent model with state lifecycle
- Fix canonical model names (fusion.plating.portal.job, fusion.plating.delivery)
- Add auto-population rules for invoice strategy and configurator defaults
- Lighten bridge_mrp deps: gates as mixins in receiving/invoicing modules
- Add deployment strategy for fusion_tasks (same server, not standalone)
- Add data migration section for existing quote request coexistence
- Add work centre mapping note (fusion.plating.work.center ↔ mrp.workcenter)
- Change x_fc_account_hold_date to Datetime for audit precision
- Add bilingual CoC implementation note (QWeb, not ir.translation)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 18:05:42 -04:00
gsinghpal
f69b3ac855 docs: EN Tech end-to-end workflow design spec
Complete design covering 5 new modules + updates to existing:
- fusion_plating_configurator (3D viewer, pricing engine, part catalog)
- fusion_plating_receiving (inspection, damage logging, PO matching)
- fusion_plating_invoicing (deposit/progress/net/COD, account holds)
- fusion_plating_notifications (auto-email, certificate assembly)
- fusion_tasks fork (local delivery dispatch, GPS tracking)
Plus: sales integration, certificate registry, 9-stage workflow.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 17:57:56 -04:00
gsinghpal
2b84c31a12 CLAUDE.md: comprehensive workflow status — 12/15 stages built, 3 gaps remain
Full ASCII diagram of the end-to-end lifecycle with [DONE] / [NOT BUILT]
tags. Key models quick reference table. 3 remaining gaps: Recipe→WO
generation, account hold check, auto-email package. Architectural
decisions documented for next session.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 16:05:08 -04:00
gsinghpal
8fa53017c4 CLAUDE.md: document per-job recipe overrides feature
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 15:39:21 -04:00
gsinghpal
4185b149bd fusion_plating_bridge_mrp: per-job recipe overrides with config wizard (v19.0.2.0.0)
Links recipes to manufacturing orders via x_fc_recipe_id on mrp.production.
New model fusion.plating.job.node.override stores per-job opt-in/out
decisions for optional recipe steps.

Config wizard (fp.recipe.config.wizard) shows all optional nodes as a
checklist — opt-in steps default unchecked, opt-out steps default checked.
Planner toggles and confirms. "Overrides" stat button on MO form opens wizard.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 15:36:03 -04:00
gsinghpal
cb57585b5a CLAUDE.md: add end-to-end custom workflow spec (9-stage plating lifecycle)
Documents the full Quote→PO→SO→Recipe+WO→Invoice→Ship→Email workflow
for next sessions. Includes status table, architectural decisions needed,
and component mapping to Odoo models.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 15:30:22 -04:00
gsinghpal
6305faccb7 add CLAUDE.md for fusion-plating module context
Comprehensive instructions for future sessions: module structure,
critical Odoo 19 rules, recipe system architecture, deployment commands
for both servers, Steelhead feature status, and naming conventions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 15:13:08 -04:00
gsinghpal
330112f29e fusion_plating: change icon from Char to Selection dropdown (v19.0.2.0.5)
Icon field is now a selection with 24 curated plating icons. Users pick
from a dropdown with descriptive labels (e.g. "Fire / Bake", "Diamond /
Plating") instead of typing FA class codes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 15:10:21 -04:00
gsinghpal
e4b41828a3 fusion_plating: add opt_in_out field + time tracking display (v19.0.2.0.4)
New opt_in_out selection field (disabled/opt-in/opt-out) matching
Steelhead's Configure OPT IN/OUT feature. Shown in both the form
view and the tree editor side panel.

Time tracking: form view now shows Created, Created By, Last Updated,
Updated By fields. Tree editor side panel shows relative timestamps
down to the second (e.g. "46w 3d 4h 17m 21s ago by Brett Kinzett").

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 15:05:59 -04:00
gsinghpal
3316b5d519 fusion_plating: icon picker grid + auto-icon from name (v19.0.2.0.3)
Replaced text input with a clickable 24-icon grid picker for the side
panel. Icons are curated for plating (flask, blast, mask, rinse, bake,
plate, inspect, etc.). When adding a new step, the icon is automatically
guessed from the name via keyword matching (e.g. "Masking" → paint-brush,
"Oven baking" → fire, "Acid Dip" → flask).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 15:01:46 -04:00
gsinghpal
edc7b11cb6 fusion_plating: add ENP-ALUM-BASIC recipe from client's Steelhead export (v19.0.2.0.2)
9 operations, 15 steps matching the client's Electroless Nickel Plating
Aluminium Basic recipe: Masking, Racking, Ready for processing,
ENP-Alum Line (E-Nickel Plating), De-Masking, Oven baking, De-racking,
Oven bake (Post de-rack), Post-plate Inspection.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 14:55:09 -04:00
gsinghpal
b38310709a fusion_plating: fix search view for Odoo 19 + remove unaccent param (v19.0.2.0.1)
Odoo 19 search views: removed <group string="..."> wrapper (invalid),
removed string attr from <search>, removed filter_domain on name field.
Removed unaccent=False from parent_path (unknown param in this version).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 14:41:33 -04:00
gsinghpal
a7d224899a fusion_plating: add process recipe system with OWL tree editor (v19.0.2.0.0)
New model fusion.plating.process.node with _parent_store hierarchy for
defining reusable plating recipes. Node types: recipe, sub_process,
operation, step. Includes companion model for operator input definitions.

Full OWL tree editor (client action) with:
- Hierarchical tree with connector lines and type-coloured borders
- Click-to-edit side panel with save
- Add/delete child nodes inline
- Drag & drop reorder and reparent
- Theme-aware SCSS (light + dark mode)
- Demo data: Electroless Nickel Plating — Steel Line (30+ nodes)

Backend: 7 JSON-RPC endpoints for tree CRUD, reorder, move, duplicate.
Security: 3-tier ACL (operator read / supervisor write / manager full).
Menu: Process Recipes under Plating > Operations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 14:29:58 -04:00
gsinghpal
e146daf4c8 fusion_plating_shopfloor: use SCSS $border-color for card borders like Odoo core (v19.0.1.1.3)
color-mix() in border shorthand was being dropped by the SCSS compiler.
Switched to the same pattern Odoo core kanban uses: split border into
border-width/border-style/border-color with $border-color SCSS variable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 14:02:07 -04:00
gsinghpal
1159864eb6 fusion_plating_shopfloor: opaque card border via color-mix against body-bg (v19.0.1.1.2)
Mixing body-color into transparent produced a nearly-invisible border at
1px. Now mixing 22% body-color into body-bg creates an opaque border
that is guaranteed visible in both themes (~#d0d0d0 light, ~#3a3d41 dark).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 13:59:20 -04:00
gsinghpal
59dfb3335a fusion_plating_shopfloor: stronger card border using color-mix for visibility (v19.0.1.1.1)
var(--bs-border-color) was nearly invisible against var(--bs-body-bg) in
both light and dark mode. Switched to color-mix(var(--bs-body-color) 20%)
which guarantees visible contrast regardless of theme. Hover darkens to 30%.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 13:53:54 -04:00
gsinghpal
ccfae66975 fusion_plating_shopfloor: card styling fix + drag & drop between work centres (v19.0.1.1.0)
Cards now have visible borders and elevation shadow in both light/dark
mode. Column count badge restored to high-contrast white-on-gray.

Added HTML5 drag & drop: users can drag work order cards between work
centre columns. Backend endpoint writes workcenter_id on mrp.workorder.
Drop target columns highlight with the action colour.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 13:49:47 -04:00
gsinghpal
a8eacc94bc fusion_plating_shopfloor: replace hardcoded colors with CSS vars for dark mode (v19.0.1.0.1)
Plant overview and process tree SCSS files had 90+ hardcoded hex colors
that broke dark mode. Replaced all with Bootstrap/Odoo CSS custom
properties (--bs-body-bg, --bs-body-color, --bs-border-color, etc.)
matching the pattern already used in fusion_plating_shopfloor.scss.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 13:27:53 -04:00
gsinghpal
b79e3d5c2e changes 2026-04-12 09:11:35 -04:00
gsinghpal
be611876ad changes 2026-04-12 09:09:50 -04:00
gsinghpal
d07159b9b5 fusion_authorizer_portal: wire accessibility assessments into MOD/ODSP/WSIB workflows (v19.0.2.8.0)
Audit found that fusion.accessibility.assessment._create_draft_sale_order
hardcoded x_fc_sale_type='direct_private' for ALL accessibility cases —
meaning MOD, ODSP, WSIB, and insurance projects never entered their
respective downstream workflows. The MOD workflow rework I shipped in
fusion_claims 19.0.8.0.3 was effectively unreachable from the portal.

Also: x_fc_authorizer_id never propagated from the assessment to the SO,
the new x_fc_mod_accessibility_specialist_id was orphaned, and there
was no back-reference from sale.order to the accessibility assessment.

Fixes:
- New required field x_fc_funding_source on fusion.accessibility.assessment
  (march_of_dimes / odsp / wsib / insurance / direct_private / other)
- _create_draft_sale_order now maps funding_source -> x_fc_sale_type,
  copies authorizer_id -> x_fc_authorizer_id, sets accessibility_assessment_id
  back-ref, and for MOD cases pre-populates
  x_fc_mod_accessibility_specialist_id from sales_rep_id.partner_id
- New accessibility_assessment_id field on sale.order so the back-link
  is queryable both directions (previously only assessment->SO existed)
- action_complete now guards against double-completion and missing
  funding_source: raises UserError instead of silently creating duplicates
- Expanded fusion.accessibility.assessment.state from 3 to 6 values
  (draft/scheduled/in_progress/pending_review/completed/cancelled),
  added copy=False, added _expand_states group_expand for kanban
- Booking form at /book-assessment now collects funding_source
  (required dropdown) so the path is known before the visit happens
- portal_assessment.py book_assessment_submit accepts funding_source
  with whitelist validation (defaults to direct_private if missing)

Deployed to odoo-westin (westin-v19) and odoo-mobility (mobility),
both verified at v19.0.2.8.0 with the new columns present.
2026-04-09 08:24:30 -04:00
gsinghpal
5d89e04f82 fusion_claims: shorten handoff_to_client label to 'Handed Off' (v19.0.8.0.5)
The long label 'Handed Off (Client/Authorizer Submitting)' was squeezing
the MOD statusbar on the sale order form — the parenthetical pushed half
the states off-screen behind a '...' overflow indicator. Context about
WHO is handling the submission is already captured in x_fc_mod_submitted_by
and visible in the MOD Documents tab, so the statusbar label does not need
to repeat it.

Deployed to odoo-westin and odoo-mobility, re-ran -u fusion_claims on
both so the ir_model_fields_selection.name row was resynced.
2026-04-09 08:09:36 -04:00
gsinghpal
b6d101c9a2 fusion_claims: fix handoff_to_client Selection ordering (v19.0.8.0.4)
In v19.0.8.0.3 the new handoff_to_client MOD state was inserted between
project_complete and pod_submitted in the Selection list, so it rendered
near the bottom of status dropdowns even though it happens very early in
the workflow. Also forgot to add it to _expand_mod_statuses, so it would
not appear as a kanban column without holding records.

Fixes:
- Move handoff_to_client in the Selection tuple to position 6 (right after
  quote_submitted, right before awaiting_funding) — parallel to
  quote_submitted as the alternative entry into awaiting_funding
- Add handoff_to_client to the main list in _expand_mod_statuses so it
  always appears as a kanban column
- Add handoff_to_client AND awaiting_funding to statusbar_visible on the
  sale_order form view (awaiting_funding was also missing before)

Deployed to odoo-westin (westin-v19) and odoo-mobility (mobility).
Re-ran -u fusion_claims on both so Odoo resynced the Selection
sequence column. Verified: both databases now show sequence 0..15 with
handoff_to_client at sequence 5, identical between servers.
2026-04-09 08:02:39 -04:00
gsinghpal
0fe8a71c05 fusion_claims: MOD workflow rework — two-assessment split, 3 submission paths, recovery actions (v19.0.8.0.3)
Reworks the March of Dimes workflow to match reality: the OT does their own
disability assessment and provides the VOD letter; our accessibility specialist
then visits to produce the proposal/drawings/quote; and the application can be
submitted by us (internal), the client, or the authorizer themselves. The old
workflow flattened all this into one assessment state with a dead-end
funding_denied and no document tracking.

Data model (13 new sale.order fields):
- 5 new document binaries + filenames: VOD letter, Application Form (filled),
  Notice of Assessment, Property Tax, Proposal Document
- x_fc_mod_submitted_by Selection (internal/client/authorizer)
- x_fc_mod_handoff_date, x_fc_mod_vod_requested_date
- x_fc_mod_accessibility_specialist_id (m2o res.partner — internal or external)
- x_fc_mod_previous_status_before_hold (for proper resume)
- x_fc_mod_funding_denial_reason (captured via wizard)

Settings (4 res.company fields + res_config_settings mirrors):
- x_fc_mod_application_form (blank) + filename
- x_fc_mod_vod_form (blank) + filename
- x_fc_mod_followup_assignee_mode (office_contact / sales_rep)
- x_fc_mod_followup_office_contact_id

res.partner: added 'accessibility_specialist' to x_fc_contact_type.

State machine:
- New state handoff_to_client between quote_submitted and awaiting_funding,
  used for paths B/C (client or authorizer submits themselves)
- Fixed action_mod_on_hold to save x_fc_mod_previous_status_before_hold
- Fixed action_mod_resume to restore previous status (was hardcoded to
  in_production, losing context for cases held earlier)

4 new wizards:
- mod_submission_path_wizard — chooses submitted_by, auto-fires VOD request
  email on first switch to 'internal'
- mod_funding_denied_wizard — captures denial category + reason
- mod_resubmit_wizard — revises + resubmits denied cases (with optional
  doc clearing)
- mod_submission_confirmed_wizard — records client/authorizer confirmed
  submission, advances to awaiting_funding

8 new action methods:
- action_mod_set_submission_path, action_mod_request_vod,
  action_mod_handoff_to_client (validates docs, fires handoff email),
  action_mod_confirmed_submission, action_mod_resubmit_from_denied,
  action_mod_cancel_from_denied, action_mod_reopen_cancelled
- action_mod_funding_denied now opens the denial wizard

3 new email methods + 2 existing fixes:
- _send_mod_vod_request_email — auto-attaches blank VOD form from company
  settings, sent to authorizer when we are handling submission
- _send_mod_handoff_email — two templates (client vs authorizer), attaches
  proposal + drawing + blank MOD Application Form
- _mod_company_attachment helper for building attachments from company Binary
- Fixed _send_mod_assessment_completed_email to include authorizer
- Fixed _send_mod_pod_submitted_email to include client

New cron:
- _cron_mod_handoff_followup (daily 09:00) — creates mail.activity for office
  to confirm MOD submission. Assignee via company setting (office contact or
  sales rep). Uses existing rolling-window cap (2/month per order).

Views:
- sale_order form: new status-bar buttons (set path, request VOD, handoff,
  confirm, resubmit, cancel, reopen), new document section in MOD Documents
  tab with submission-path tracking, denial details, hold history
- res_config_settings: new MOD blank forms upload + assignee config

Deployed to odoo-westin (westin-v19) and odoo-mobility (mobility). Pre-deploy
FK cleanup from earlier session means mobility updated cleanly without
workaround. HTTP 200 on both, cron verified active, all new fields present.
2026-04-09 07:34:17 -04:00
gsinghpal
8b2cbd9085 fusion_claims: ADP workflow recovery actions + email gap fixes
Workflow (from the FigJam ADP board):
- 9 new ADP action methods to wire up the orphan states that the board
  showed had no entry or no exit path: put_on_hold, withdraw, mark_denied,
  mark_rejected, mark_needs_correction, cancel, reopen_cancelled,
  reopen_expired, resubmit_from_denied.
- 12-month auto-expire cron (_cron_adp_expire_approved) configurable via
  fusion_claims.adp_approval_expiry_months, runs daily at 03:00.
- 3 new recovery buttons in the ADP form view (Reopen cancelled, Reopen
  expired, Resubmit from denied) in both the primary status bar and the
  secondary details panel.

Email (from the 2026-04 email audit):
- 6 new ADP stage email methods via a shared _adp_send_stage_email helper:
  assessment_scheduled, assessment_completed, application_received, accepted,
  cancelled, expired. Each has a matching dispatch entry in write().
- _send_rejection_email now includes the client (was authorizer-only).
- _send_accepted_email excludes the authorizer per the new rule: "Accepted"
  is a passive intermediate state with no authorizer action required.
- _send_ready_for_delivery_email excludes the authorizer: operational
  scheduling, not delivery confirmation. Authorizers are notified at
  case_closed when the product is actually delivered.
- action_adp_put_on_hold and action_adp_withdraw now fire their matching
  email methods so direct action-method calls get the same notifications
  as the status_change_reason_wizard path.

Authorizer notification rule (locked in for this update):
  Send to authorizer ONLY for initial involvement (assessment/submit/
  resubmit), delivery confirmation (case_closed), and problem states
  (rejected, denied, needs_correction, withdrawn, on_hold, cancelled,
  expired). Skip for billing, payment, ready_delivery scheduling, and
  passive intermediates (accepted).

Scope: ADP + ADP/ODSP only. MOD workflow emails reverted and deferred
to a separate update.

Deployed to odoo-westin (westin-v19) and odoo-mobility (mobility).
Pre-existing stock_route_warehouse FK orphans on mobility worked around
by verifying fusion_claims transaction committed before container restart.
2026-04-09 06:06:33 -04:00
gsinghpal
d60a75a391 fusion_claims: cap MOD follow-up email flood with rolling 30-day window
Two daily MOD crons were fighting each other. _cron_mod_schedule_followups
created a mail.activity on every MOD order in quote_submitted/awaiting_funding;
_cron_mod_escalate_followups unconditionally deleted the activity after
sending its one-time reminder email. The activity was recreated every day
in a tight loop with no per-period cap — a legitimate 2-4 month wait for
a MOD funding decision would generate dozens of activity churn events and
a bulk email burst the first time the escalate cron ran against a backlog.

Fix:
- New fields x_fc_mod_followup_month_count / _month_start / _cap_notified
  (copy=False) track a rolling window per order.
- New config params mod_followup_max_per_month (default 2),
  mod_followup_window_days (30), mod_followup_max_per_cron_run (10).
- _send_mod_followup_email resets the window after 30 days, refuses to
  send past the cap, and posts a one-shot chatter note explaining why.
- _cron_mod_schedule_followups no longer recreates the activity when the
  cap has been hit and stops daily-bumping x_fc_mod_next_followup_date.
- _cron_mod_escalate_followups processes oldest-deadline-first with a
  per-run throttle, only unlinks the activity on a successful send so
  humans can still action capped cases manually.
- write() resets the rolling counters on any real MOD status change.

Deployed to fusion_claims v19.0.8.0.1 on odoo-westin (westin-v19,
36 affected orders) and odoo-mobility (mobility, 2 affected orders).
2026-04-08 00:01:19 -04:00
gsinghpal
c30a61c93f changes 2026-04-07 22:03:20 -04:00
gsinghpal
f4c6dca171 Update woo_instance.py 2026-04-07 21:47:15 -04:00
gsinghpal
87a649b63d changes 2026-04-07 21:42:21 -04:00
gsinghpal
7d8f30627f changes 2026-04-07 21:42:12 -04:00
gsinghpal
4fde4c7bd1 changes 2026-04-07 20:49:21 -04:00
gsinghpal
3cc93b8783 changes 2026-04-04 15:37:16 -04:00
gsinghpal
c66bdf5089 changes 2026-04-03 15:45:18 -04:00
gsinghpal
4cd7357aa0 changes 2026-04-02 23:40:34 -04:00
gsinghpal
1c560c6df2 changes 2026-04-02 17:55:32 -04:00
gsinghpal
f715570e0c Update .DS_Store 2026-04-02 11:27:28 -04:00
gsinghpal
3022b8ed59 CHANGES 2026-04-02 10:45:07 -04:00
gsinghpal
2a363c6b40 changes 2026-04-02 03:30:02 -04:00
gsinghpal
5b76037988 fix: don't re-upload images for already-synced variants
Pre-fill image field only for NEW variants. Already-synced variants
get an empty image field — existing WC image is kept unless the user
actively uploads a new one in the wizard.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 22:06:32 -04:00
gsinghpal
39e007b42f feat: default variant selector — user picks which variant is the default
Added 'Default' toggle column in variant push wizard. First variant is
pre-selected as default. User can change it. The default variant's
attribute values are set as WC product's default_attributes using
WC attribute IDs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 22:02:29 -04:00
gsinghpal
8c01deb2e3 fix: properly decode base64 image data and detect MIME type
Image endpoint was returning base64 text instead of decoded binary.
Now properly decodes base64 from Odoo Binary field and detects actual
content type from magic bytes (JPEG, PNG, GIF, WebP).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 21:57:14 -04:00
gsinghpal
1a679a45c3 fix: serve variant images via custom endpoint instead of product URL
Product image URL was serving placeholder because writing to image_1920
doesn't work for variants (computed field). New approach:
1. Store image in wizard line table (attachment=False, bytea column)
2. Serve directly via /woo/image/{line_id}/{filename} endpoint
3. WC downloads real image data from this URL
4. Also saves to image_variant_1920 for Odoo reference

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 21:53:41 -04:00
gsinghpal
88305a4ce0 chore: bump version to 19.0.3.0.0
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 21:49:08 -04:00
gsinghpal
e3d784f566 fix: store variant image in DB table not ir.attachment
Binary field with attachment=True (default) stores in ir.attachment
which doesn't work reliably for transient model inline list records.
Set attachment=False to store in the woo_variant_push_line table directly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 21:44:30 -04:00
gsinghpal
27955c8c41 fix: force commit image save and add debug logging for variant images
Image wasn't persisting because transient model write was in the same
transaction. Added cr.commit() after saving image to ensure it's
available when WC downloads it. Added size/type logging to trace
image data flow.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 21:37:39 -04:00
gsinghpal
21c1e37211 fix: save wizard images to Odoo product before passing URL to WC
The wizard's binary image field wasn't being saved to the product.product
record, so the public URL returned the default placeholder. Now saves
line.image → variant.image_1920 first, then generates the URL with a
cache-busting timestamp. Only includes image in WC data if the wizard
line has an actual image uploaded.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 21:35:03 -04:00
gsinghpal
ed426912ce fix: add .png extension to image URLs — WC rejects extensionless URLs
WooCommerce requires a file extension in image src URLs to determine
file type. Added filename.png to all Odoo image URLs. Also fixed
variable name ordering bugs where img_name was used before defined.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 21:26:25 -04:00
gsinghpal
768731da0f fix: use Odoo public image URLs instead of broken WP media upload
WC consumer key cannot auth against /wp/v2/media (401). Instead, pass
Odoo's public image URL in the product/variation data and let WC
download it directly from {odoo_base}/web/image/product.product/{id}/image_1920.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 19:10:47 -04:00
gsinghpal
75ceee1e69 fix: use WC attribute ID (not name) when setting variation attributes
WooCommerce silently ignores attribute 'name' on variation updates —
requires 'id' (the WC attribute ID). Built a lookup map from the
parent product's wc_attributes and use 'id' in all variation payloads.
Fixed in all 3 files: variant push wizard, product creation wizard,
and product map direct push.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 18:59:54 -04:00
gsinghpal
71dea1f91b fix: remove readonly from variant line model — use force_save in view
Odoo web client strips readonly field values during record creation,
so already_synced/wc_variation_id/map_id were always NULL. Removed
readonly from Python model, added force_save="1" in XML view to ensure
these tracking fields are persisted through the wizard lifecycle.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 18:39:13 -04:00
gsinghpal
842832cc41 fix: filter inactive/attributeless variants from push wizard
Archived variants (active=False) and variants with no attribute values
were being included, causing WC 400 errors. Now only active variants
with attribute assignments are shown and pushed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 18:33:50 -04:00
gsinghpal
08d7ee17ff fix: populate variant lines in default_get instead of onchange
Onchange wasn't firing when wizard opened via context defaults. Moved all
variant line population to default_get so lines are pre-filled immediately.
Added debug logging to trace new vs update classification.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 18:32:13 -04:00
gsinghpal
39d4fe5020 fix: variant update now sets attribute values and default variant on WC
- Update path sends attributes per variation (was missing, causing
  "Any COLOR..." on WC)
- Sets default_attributes on parent product (first variant's values)
- Better API error logging with response body

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 18:20:52 -04:00
gsinghpal
8983c8bd50 feat: variant wizard now creates AND updates existing WC variations
Already synced variants are editable — change price, SKU, image and click
Save & Sync to update them on WooCommerce. New variants are created,
existing ones updated in a single action. Button shows on all products
with variants (purple for new, grey for already synced).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 18:10:07 -04:00
gsinghpal
1bf4092b39 feat: variant push wizard — review pricing, SKU, images before pushing
Replaced direct push with a wizard that shows all variants in an editable
table. User can review/edit standard price, sale price, SKU, and image
per variant before pushing. Include/exclude toggle per variant. Already
synced variants shown for reference. Geo-tags images if configured.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 18:05:39 -04:00
gsinghpal
2afe54d15e fix: move variant push button inline with Odoo Product name
Removed separate Variants column — button now appears right next to the
Odoo product name so it's visible without scrolling.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 17:59:25 -04:00
gsinghpal
a0ad52fe46 feat: push variants button on mapped products — convert simple→variable in WC
When a mapped product is simple on WC but has variants in Odoo, a purple
"N variants" button appears in the Variants column. Clicking it:
1. Converts the WC product from simple to variable
2. Creates WC attributes and terms from Odoo attribute lines
3. Creates a WC variation for each Odoo variant with price/SKU/stock/tax/image
4. Creates woo.product.map records for each variation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 17:52:37 -04:00
gsinghpal
a3fa1ced16 fix: group Odoo products by template — show variant count instead of duplicates
Search endpoint now queries product.template instead of product.product,
so a product with 20 variants shows as 1 row with a "20 variants" badge
instead of 20 duplicate rows. Category name shown in sub-text.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 17:42:01 -04:00
gsinghpal
9d483fb474 feat: full variable product support — create WC products with variants from Odoo
Detects Odoo product variants (product.template.attribute_line) and creates
WC variable products with attributes and variations. Each variation gets
its own price, SKU, image, stock, and tax class. Variant lines shown in
wizard with include/exclude toggle. WC attributes and terms auto-created.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 17:22:40 -04:00
gsinghpal
05c84d077d feat: move tax and pricelist mapping inline to Sync Settings tab
Tax mapping and pricelist mapping now live directly on the instance
form under Sync Settings. Added Fetch WC Tax Classes button that pulls
tax classes from WC API and auto-matches. Removed standalone menu items.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 16:58:01 -04:00
gsinghpal
bc2bba14aa fix: redesign product creation wizard — use Odoo statusbar, clean layout
- Replaced hardcoded badge step indicator with Odoo's native statusbar widget
- Removed all hardcoded Bootstrap colour classes (bg-primary, text-muted, etc.)
- Using oe_highlight and oe_link for buttons (theme-aware)
- Cleaner 2-column layout for basic info and review steps
- Full-width separators and fields for content step
- Images in 2-column grid
- SEO meta fields use proper group layout

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 16:23:07 -04:00
gsinghpal
b83c9fd318 fix: auto-refresh product list when category filter wizard closes
Used doAction onClose callback instead of await — the wizard dialog
close now triggers reload of excluded count and product list. Same
fix applied to product creation wizard.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 16:11:36 -04:00
gsinghpal
5e806745da feat: persistent hidden categories with wizard and toggle
Categories to hide are stored on woo.instance and persist across sessions.
Click 'Hidden (N)' button to open wizard where you can add/remove categories
using a tag picker. Eye/eye-slash toggle to quickly apply or unapply the
filter without losing the saved list.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 16:06:15 -04:00
gsinghpal
52be90c10d feat: category filter dropdown on unmatched Odoo products panel
Filter by Odoo product category or clear filter. Backend supports
both include and exclude category filtering. Loads all categories
on init for the dropdown.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 15:56:13 -04:00
gsinghpal
9f0badfb7e fix: AI prompt fields use full width instead of narrow column
Removed group wrappers around prompt fields. Using label + field pattern
directly inside the page so prompts span the full available width.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 15:45:18 -04:00
gsinghpal
c46e4c0b28 feat: AI model selection dropdown with Claude and OpenAI models
Replaced free-text model field with Selection showing standard models
from both providers: Claude Opus/Sonnet/Haiku and OpenAI GPT-4o/4.1/o3.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 15:36:23 -04:00
gsinghpal
c169704687 chore: bump version to 19.0.2.0.0
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 14:50:06 -04:00
gsinghpal
4efd5066d0 feat: AI-powered product creation wizard with SEO and image geo-tagging
4-step wizard: Basic Info → Images → Content & SEO → Review & Create.
AI generates product titles, descriptions, meta data using Claude or OpenAI.
Image geo-tagging with company EXIF data. SEO meta pushed to Rank Math,
Yoast, AIOSEO, and SEOPress simultaneously. Products created with CAPS
name in Odoo, Title Case in WooCommerce.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 14:44:48 -04:00
gsinghpal
f759bf558f feat: add AI service (Claude + OpenAI) and image processor with EXIF geo-tagging
AIService wraps both Anthropic Claude and OpenAI APIs for product content
generation. ImageProcessor handles EXIF geo-tagging with company info and
GPS coordinates using piexif.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 14:39:44 -04:00
gsinghpal
3493c43916 feat: add category mapping model and AI settings on woo.instance
Category mapping between Odoo product categories and WC categories with
auto-match by name and manual mapping UI. AI settings for Claude/OpenAI
with customizable prompts for product content generation. GPS coordinates
for image geo-tagging pulled from company settings.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 14:30:02 -04:00
gsinghpal
3179cc1f7b feat: add editable WC SKU and Odoo SKU columns with bidirectional sync
Both SKU fields are inline-editable. Arrow buttons sync individual SKUs.
Bulk buttons sync all SKUs Odoo→WC or WC→Odoo.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 13:32:26 -04:00
gsinghpal
1a1a37b3e3 feat: add Fusion WooCommerce app icon
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 12:34:52 -04:00
gsinghpal
3de513fb7b fix: round edit values — margin shows whole number, prices show 2 decimals
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 11:44:10 -04:00
gsinghpal
4941df8131 fix: remove decimal points from margin percentage — whole numbers only
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 11:41:50 -04:00
gsinghpal
b3fb2ef40c feat: add Cost and Margin % columns with inline editing
Cost column shows Odoo standard_price (editable). Margin % is calculated
from cost and sale price. Editing margin auto-calculates the sale price
using: price = cost / (1 - margin/100). All cells are inline-editable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 11:38:37 -04:00
gsinghpal
c5b519f8f4 feat: inline-editable prices in product mapping UI
Click any price cell (WC Standard, WC Sale, Odoo Price) to edit inline.
Enter or click away saves and syncs to the source. Escape cancels.
Validation: sale price cannot exceed standard price.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 11:23:23 -04:00
gsinghpal
8354e82dc4 changes 2026-04-01 01:32:30 -04:00
gsinghpal
8b723086b9 docs: add CLAUDE.md for both fusion_woocommerce and fusion-woodoo
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 01:02:21 -04:00
gsinghpal
80f9ddd0d6 feat: WC standard price + sale price with smart sync logic
- Split woo_price into woo_regular_price and woo_sale_price
- Sync to WC: if standard=0 sets regular_price, otherwise sets sale_price
- Validation: standard price cannot be less than sale price
- Both prices shown in mapping UI with sale price highlighted in green
- Refresh Prices pulls both values from WC API

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 23:19:01 -04:00
gsinghpal
b1e4ed5ec8 feat: clickable WooCommerce product links in mapping UI
Product names in the mapped table are now links that open the WC product
page in a new tab. Added woo_permalink field, stored during fetch,
returned by search endpoint.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 23:06:27 -04:00
gsinghpal
0e84b97c89 fix: sync never changes product map state from mapped
Price conflicts and sync errors were setting woo.product.map state to
'conflict'/'error', making products disappear from the mapped list.
Conflicts are now tracked only in woo.conflict model. Map state stays
'mapped' as long as the product link exists.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 23:01:26 -04:00
gsinghpal
1cc3ea7a18 fix: health check no longer changes instance state
The health check cron runs inside Docker which can't always reach
external URLs. It was setting state to 'error' breaking fetch/sync.
Now it only logs warnings — state changes only via explicit Test
Connection button.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 22:53:53 -04:00
gsinghpal
f9fcb6612b feat: add pagination, individual price sync arrows, and bulk price sync
- Pagination with page nav for mapped, unmatched Odoo, and unmatched WC tabs
- Per-product arrow buttons to push price in either direction
- Bulk price sync buttons: All Prices Odoo→WC and All Prices WC→Odoo
- Server-side offset/limit with total count in search endpoints

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 22:51:07 -04:00
gsinghpal
80b7d3d620 fix: OWL template error — Number() not available in template context
Use a formatPrice() method on the component instead of calling Number()
directly in the OWL template. Fixes TypeError: ctx.Number is not a function.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 22:39:50 -04:00
gsinghpal
cec499e9e9 fix: handle null/zero prices in mapping UI template
toFixed() was failing on null values. Now uses explicit null/undefined
checks so $0.00 prices display correctly instead of showing dashes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 22:38:27 -04:00
gsinghpal
3ea283247a feat: add Refresh Prices button to pull WC prices for existing mappings
Existing mapped products had no WC price since they were fetched before
the woo_price field existed. action_refresh_prices fetches current prices
from WC API for all mapped products.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 22:35:13 -04:00
gsinghpal
919dbcb4cf feat: add WC Price and Odoo Price columns to product mapping UI
Added woo_price field to woo.product.map model, populated during fetch.
Search endpoint now returns both odoo_price and woo_price for side-by-side
comparison in the mapped products table.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 22:32:42 -04:00
gsinghpal
ab16040eb4 fix: add missing action_fetch_products and action_sync methods
Product mapping UI called these methods but they didn't exist on
woo.instance. action_fetch_products fetches WC products via API,
auto-matches by SKU, handles variations. action_sync runs all
enabled sync types manually.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 22:28:35 -04:00
gsinghpal
396f895ae2 fix: proper Odoo 19 dark mode detection via cookie-based theme_detect.js
Odoo 19 does NOT use .o_dark class or data-bs-theme attribute. It sets
color-scheme via SCSS and stores preference in color_scheme cookie.
Added theme_detect.js that reads the cookie and sets data-woo-theme="dark"
on <html> for reliable CSS targeting. Fixed CSS dark mode selectors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 22:21:17 -04:00
gsinghpal
c5d2ac1d6b fix: thorough dark mode fixes across all OWL templates and CSS
- Replace Bootstrap badge classes with theme-aware woo-badge classes in
  product mapping tabs
- Replace <code> with theme-aware .woo-code class
- Replace all text-muted with woo-text-muted using CSS custom properties
- Add client action wrapper background
- Add theme-aware select/checkbox/strong/input overrides
- Add .woo-code, .woo-text-muted, .woo-text-faint utility classes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 22:12:10 -04:00
gsinghpal
645119c9b7 fix: theme-aware CSS with dark mode support
Replace all hardcoded colours with CSS custom properties. Dark mode
overrides via html.o_dark / body.o_dark / [color-scheme: dark] selectors
matching Odoo 19's theme system.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 22:09:07 -04:00
gsinghpal
84f01ebd7b fix: remove deprecated attrs/states attributes for Odoo 19 compatibility
Converted all attrs= expressions to inline invisible/readonly attributes
in wizard XML views (woo_product_fetch_views.xml, woo_setup_wizard_views.xml).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 21:43:03 -04:00
gsinghpal
5d361d8c8a fix: Odoo 19 compatibility — tree→list views, remove search group wrappers, remove numbercall
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 21:40:53 -04:00
gsinghpal
ad50c579c3 feat: add communication history push, invoice auto-trigger, health checks, rate limiting
- Override account.move action_post to auto-push invoice PDF to WC on posting
- Add _cron_health_check to ping all instances and flag unreachable ones
- Add health check cron record (every 10 minutes) to data/cron.xml
- Add IP-based rate limiting (100 req/min) to webhook controller
- Fill in API endpoints: order/documents, order/status, order/messages
  with real data from woo.order, stock.picking, and mail.message
- Implement return/create API endpoint with product mapping and line creation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 21:02:15 -04:00
gsinghpal
cc35c28760 feat: implement returns/refunds, customer sync, tax/pricelist mapping
- Add woo.return workflow: action_approve (creates reverse picking),
  action_reject, action_receive, action_refund (creates credit note + WC refund)
- Add woo.customer._find_or_create class method for customer lookup/creation
- Replace _sync_customers placeholder to push address updates to WC
- Add _sync_customer_from_wc webhook handler for inbound customer updates
- Add woo.tax.map helpers: get_odoo_tax, get_wc_tax_class
- Add woo.pricelist.map helper: get_pricelist_for_role

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 21:02:07 -04:00
gsinghpal
7e302b0a27 feat: implement price, inventory, image sync and conflict resolution
- Replace _sync_products placeholder with price comparison logic that
  detects Odoo vs WC changes and creates woo.conflict on both-changed
- Replace _sync_inventory placeholder to push Odoo stock levels to WC
- Add _sync_product_from_wc webhook handler for inbound price updates
- Add action_sync_images on woo.product.map with hash-based comparison
- Add conflict resolution methods: action_use_odoo, action_use_woo,
  action_bulk_resolve_odoo, action_bulk_resolve_woo

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 21:01:57 -04:00
gsinghpal
9c2c043de9 feat: implement order sync lifecycle (WC→Odoo SO/invoice, shipping push, completion push)
- Replace _sync_orders placeholder with full WC order import pipeline
- Add _sync_order_from_wc, _find_or_create_customer, _prepare_sale_order_vals,
  _prepare_order_line_vals, _prepare_shipping_line_vals helpers
- Add push methods on woo.order: action_push_shipping, action_push_completed,
  action_push_invoice_pdf, action_push_delivery_pdf, _push_messages_to_wc
- Override stock.picking button_validate to auto-create shipment records and
  push tracking/delivery PDFs to WooCommerce

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 21:01:48 -04:00
gsinghpal
2dd1a2be6c feat: add WooCommerce sync dashboard widget
- WooDashboard OWL client action registered as fusion_woocommerce.woo_dashboard
- Cards: pending orders (clickable), last sync (relative time), errors 24h (clickable),
  products mapped % with progress bar
- Quick actions: Sync Now, View Conflicts, Open Mapping, View Orders
- Instances table showing name, connection state, last sync datetime
- Auto-refreshes every 60 s while mounted
- All data fetched via standalone rpc() per Odoo 19 rules

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 20:53:50 -04:00
gsinghpal
de14a28112 feat: add OWL product mapping UI with live AJAX search
- AjaxSearch component: debounced 300ms, calls /woo/search/* endpoints via rpc()
- ProductMapping client action: 3 tabs (Mapped, Unmatched, Conflicts)
  - Mapped tab: live search, bulk unmap/sync, per-row price/inventory sync toggles
  - Unmatched tab: split Odoo|WC panels, click-to-select, Map/Create/Ignore actions
  - Conflicts tab: Use Odoo / Use WC per-row and bulk resolve
- Top bar: instance selector, Fetch Products, Sync Now, live stats
- woo_dashboard.xml updated with ir.actions.client records
- woo_menus.xml pointed at new client action
- CSS: full layout styles, badges, split view, progress bar, buttons

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 20:53:43 -04:00
gsinghpal
1f86a7c497 feat: complete fusion-woodoo WordPress plugin with portal, REST endpoints, webhooks, and admin settings
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 20:48:16 -04:00
gsinghpal
102fbd65f2 feat: add setup wizard and product fetch wizard with SKU auto-match
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 20:43:36 -04:00
gsinghpal
116c0b03bf feat: add cron jobs, sync engine scaffolding, log cleanup, and email templates
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 20:41:04 -04:00
gsinghpal
0ce599c4ac feat: add webhook, API, and product search controllers
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 20:39:07 -04:00
gsinghpal
50dbd63dd2 feat: add all views, menus, and settings for WooCommerce module
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 20:36:37 -04:00
gsinghpal
7b085a87a1 feat: extend sale.order, stock.picking, account.move, res.partner with WooCommerce fields
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 20:10:08 -04:00
gsinghpal
fdd67c9e51 feat: add all remaining models (product map, order, shipment, customer, sync log, conflict, tax/pricelist map, returns)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 20:08:49 -04:00
gsinghpal
cd710065c6 feat: add woo.instance and woo.shipping.carrier models with default carriers
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 20:08:41 -04:00
gsinghpal
2cc146b253 feat: add WooCommerce REST API v3 client wrapper
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 20:01:13 -04:00
gsinghpal
45775fa295 feat: scaffold fusion_woocommerce Odoo module and fusion-woodoo WP plugin directories
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 20:00:15 -04:00
gsinghpal
126264217e Add Fusion WooOdoo implementation plan (30 tasks, 9 phases)
Comprehensive plan covering Odoo module scaffold, core models, API client,
views/menus, sync engine (webhooks + cron), wizards, OWL frontend,
WordPress plugin, order lifecycle, advanced features (tax mapping,
pricelist sync, multi-currency, refund handling), and deployment.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 19:54:10 -04:00
gsinghpal
149a45a752 Fix remaining spec review issues (round 2)
Add company_id to woo.shipment, woo.sync.log, woo.return.line. Add
order_id to woo.conflict for order-type conflicts. Add _odoo_messages
to WP meta table. Fix section numbering. Document lib/ import path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 19:37:30 -04:00
gsinghpal
a1c479f916 Add Fusion WooOdoo design specification
Complete design spec for bidirectional Odoo 19 <-> WooCommerce sync system
covering product mapping, order lifecycle, inventory sync, customer portal,
and all supporting data models. Addresses all review findings including
variant support, tax/pricelist mapping models, return/RMA workflow,
multi-shipment tracking, and Odoo 19 technical requirements.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 19:35:02 -04:00
gsinghpal
2563208f53 changes 2026-03-26 15:16:51 -04:00
gsinghpal
bd7275881f changes 2026-03-24 18:08:48 -04:00
gsinghpal
92369be6e0 changes 2026-03-20 11:46:41 -04:00
gsinghpal
595dccc17d changes 2026-03-17 13:32:08 -04:00
gsinghpal
e56974d46f update 2026-03-16 08:14:56 -04:00
gsinghpal
fdca9518ab fix: remove unsupported <group> wrapper from search view for Odoo 19
Made-with: Cursor
2026-03-15 12:33:37 -04:00
gsinghpal
a839285bd4 feat: ADP Export Files menu with filestore storage, remove Sync All button
- Add fusion_claims.adp.export.record model with filestore-backed Binary field
  for tracking exported ADP claims files organized by Year > Month > Posting Period
- Add tree/form/search views with default group-by hierarchy, latest first
- Add "Export Files" menuitem under ADP menu section
- Add bulk ZIP download server action for multi-select export
- Replace Documents app storage with new model in export wizard
- Remove Documents-related methods (_save_to_documents, folder creation)
- Add migration button in Settings to move existing Documents files
- Fix Export ADP button visibility: only show on ADP portion invoices
- Remove redundant Sync All button from invoice form
- Add ACL entries for billing users (read/create) and managers (full CRUD)
- Bump version to 19.0.7.3.0

Made-with: Cursor
2026-03-15 12:27:06 -04:00
gsinghpal
0e04f4ecc6 changes 2026-03-14 12:11:07 -04:00
gsinghpal
e9cf75ee48 changes 2026-03-14 12:04:20 -04:00
gsinghpal
fc3c966484 changes 2026-03-13 12:38:28 -04:00
gsinghpal
db4b9aa278 changes 2026-03-11 12:15:53 -04:00
gsinghpal
f81e0cd918 changes 2026-03-09 23:45:00 -04:00
gsinghpal
acd3fc455e changes 2026-03-09 15:21:22 -04:00
gsinghpal
a3e85a23ef changes 2026-03-01 14:42:49 -05:00
gsinghpal
b925766966 changes 2026-02-27 14:32:32 -05:00
gsinghpal
b649246e81 fix: auto-set rental sale type via onchange and context
is_rental_order is a computed field, not passed in vals during create.
Use in_rental_app context flag in create() and add onchange handler
so sale type sets to 'Rentals' immediately when toggled in the form.

Made-with: Cursor
2026-02-26 08:04:06 -05:00
gsinghpal
14fe9ab716 feat: hide authorizer for rental orders, auto-set sale type
Rental orders no longer show the "Authorizer Required?" question or
the Authorizer field. The sale type is automatically set to 'Rentals'
when creating or confirming a rental order. Validation logic also
skips authorizer checks for rental sale type.

Made-with: Cursor
2026-02-25 23:33:23 -05:00
gsinghpal
3c8f83b8e6 fix: remove invalid category_id from res.groups (not supported in Odoo 19)
Odoo 19 replaced category_id with privilege_id on res.groups.
Keep only privilege_id=False to clear it from the dropdown.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-25 12:55:17 -05:00
gsinghpal
4384987b82 fix: move Document Lock Override out of privilege dropdown
Rename to "Fusion: Document Lock Override" for clarity, clear
privilege_id so it appears under extra permissions instead of the
hierarchy dropdown, and add a descriptive tooltip explaining its
temporary nature and dependency on the settings toggle.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-25 12:54:40 -05:00
gsinghpal
de8e3a83bb fix: explicitly clear privilege_id on portal groups to remove from dropdown
Setting privilege_id eval="False" forces Odoo to null out the existing
database value on upgrade. Simply omitting the field did not clear it.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-25 12:45:53 -05:00
gsinghpal
3e59f9d5f6 fix: simplify fusion_claims permission dropdown and restrict settings access
Remove privilege_id from portal groups so they no longer appear in the
User settings dropdown (they are auto-assigned from Contact form).
Restrict Fusion Claims settings view to managers only.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-25 12:41:20 -05:00
gsinghpal
34e5b46025 fix: comprehensive permission overhaul for fusion_faxes and fusion_ringcentral
Users without fax/RC groups could not open Sale Orders, Invoices, or
Contacts because the One2many computed fields triggered AccessError
on fusion.fax. Now base.group_user gets read-only access so computed
fields work silently, while all UI elements (smart buttons, header
buttons, menus, partner fields, settings) are restricted to the
proper security groups. Both modules now use Odoo 19 privilege
pattern for the user settings dropdown.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-25 11:52:06 -05:00
gsinghpal
e71bc503f9 changes 2026-02-25 09:40:41 -05:00
5692 changed files with 384273 additions and 11567 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,135 @@
---
description: Use the Fusion API module for all API calls (OpenAI, Anthropic, Google Maps, Twilio, OAuth) in any Fusion module
globs: fusion_*/models/**/*.py, fusion_*/services/**/*.py
alwaysApply: false
---
# Fusion API Integration Guide
When any Fusion module needs to call an external API (OpenAI, Anthropic, Google Maps, Twilio, Google/Microsoft OAuth), it MUST use the centralized `fusion.api.service` instead of managing its own keys.
The service lives at `fusion_api/models/api_service.py` and is an AbstractModel accessible via `self.env['fusion.api.service']`.
## Available Methods
### 1. `call_openai()` -- AI text generation via OpenAI
```python
result = self.env['fusion.api.service'].call_openai(
consumer='fusion_clock_ai', # your module's technical name
feature='timesheet_summary', # descriptive feature name for tracking
messages=[
{'role': 'system', 'content': 'You are a helpful assistant.'},
{'role': 'user', 'content': 'Summarize this timesheet...'},
],
model='gpt-4o-mini', # optional, defaults to gpt-4o-mini
max_tokens=1024, # optional, defaults to 1024
user=self.env.user, # optional, defaults to current user
)
# Returns: str (the text response)
```
### 2. `call_anthropic()` -- AI text generation via Claude
```python
result = self.env['fusion.api.service'].call_anthropic(
consumer='fusion_notes',
feature='voice_transcription',
messages=[
{'role': 'user', 'content': 'Transcribe this text...'},
],
system='You are a medical transcription assistant.', # optional system prompt
model='claude-sonnet-4-20250514', # optional, defaults to claude-sonnet-4-20250514
max_tokens=1024, # optional
)
# Returns: str (the text response)
```
### 3. `get_api_key()` -- raw API key for non-AI services
```python
api_key = self.env['fusion.api.service'].get_api_key(
provider_type='google_maps', # one of: google_maps, twilio, custom
consumer='fusion_shipping', # your module's technical name
feature='geocoding', # optional feature name
)
# Returns: str (the raw API key to use in your own HTTP calls)
```
### 4. `get_oauth_credentials()` -- OAuth tokens for Google/Microsoft
```python
creds = self.env['fusion.api.service'].get_oauth_credentials(
provider_type='google_oauth', # google_oauth or microsoft_oauth
consumer='fusion_schedule',
feature='calendar_sync',
)
# Returns: dict with keys:
# client_id, client_secret, access_token, refresh_token, token_expiry, redirect_uri
```
## Provider Types
| Type | Use For |
|------|---------|
| `openai` | GPT-4o, GPT-4o-mini, o1, etc. |
| `anthropic` | Claude Sonnet, Haiku, Opus |
| `google_maps` | Geocoding, Places, Distance Matrix |
| `google_oauth` | Google Calendar, Drive OAuth |
| `microsoft_oauth` | Microsoft Calendar, Graph OAuth |
| `twilio` | SMS, Voice |
| `custom` | Any other API provider |
## What Happens Automatically
- The calling module is auto-registered as a consumer in Fusion API
- Every call is logged with: tokens, cost, response time, user, feature, model
- Budget caps (monthly/daily) are enforced before each call
- Rate limits (RPM/RPD) are enforced before each call
- Per-user limits are checked if configured
- On limit exceeded, a `UserError` is raised with a clear message
## Phased Migration Pattern
Do NOT add `fusion_api` as a hard dependency. Use a try/fallback pattern so the module works with or without Fusion API installed:
```python
def _call_ai(self, messages, feature='general'):
"""Call OpenAI through Fusion API, falling back to own key."""
try:
return self.env['fusion.api.service'].call_openai(
consumer='fusion_clock_ai',
feature=feature,
messages=messages,
)
except Exception:
api_key = self.env['ir.config_parameter'].sudo().get_param(
'fusion_clock_ai.openai_api_key'
)
if not api_key:
raise
from openai import OpenAI
client = OpenAI(api_key=api_key)
response = client.chat.completions.create(
model='gpt-4o-mini', messages=messages,
)
return response.choices[0].message.content
```
## Rules
- NEVER hardcode API keys in Python code
- NEVER store API keys in `ir.config_parameter` for new modules -- use Fusion API
- ALWAYS pass the module's technical name (e.g. `fusion_clock_ai`) as the `consumer` parameter
- ALWAYS pass a descriptive `feature` string for usage tracking granularity
- NEVER catch and silently swallow errors from `fusion.api.service` -- let UserError propagate
- DO NOT add `fusion_api` to `depends` in `__manifest__.py` -- use the fallback pattern above
- The `consumer` string should match the module's technical name exactly (folder name)
## Odoo 19 Conventions
- This project targets Odoo 19 (Community/Enterprise)
- No hardcoded colors in views or SCSS -- let Odoo handle dark/light theming
- Use `res.groups.privilege` for security groups (not `category_id`)
- Do not use deprecated `ir.cron` fields (`numbercall`, `doall`)
- Settings views: use `name=` attribute on `<app>` tags, not `data-key=`

94
CLAUDE.md Normal file
View File

@@ -0,0 +1,94 @@
# Odoo Modules — Claude Code Instructions
## Project
27 custom Odoo 19 modules for Fusion Central (Westin Healthcare + NEXA Systems).
## Critical Rules — Odoo 19
1. **NEVER code from memory** — Always read a reference file from Docker first:
```bash
docker exec odoo-dev-app cat /usr/lib/python3/dist-packages/odoo/addons/<module>/static/src/<path>
```
2. **Frontend JS**: Use `Interaction` class from `@web/public/interaction`, registered via `registry.category("public.interactions")`. NOT IIFE/DOMContentLoaded.
3. **Backend OWL**: Use standalone `rpc()` from `@web/core/network/rpc`. NOT `useService("rpc")`. `static props = []` not `{}`.
4. **HTTP routes**: `type="jsonrpc"` — NOT `type="json"` (deprecated).
5. **res.config.settings**: Only boolean/integer/float/char/selection/many2one/datetime. NO Date fields.
6. **res.groups**: NO `users` field, NO `category_id` field.
7. **Search views**: NO `group expand="0"` syntax.
8. **SCSS imports**: `@import "./partial"` is FORBIDDEN in Odoo 19 custom SCSS. It prints a warning and silently falls back to the old cached bundle. Register every SCSS file (including `_partial.scss` tokens) as a separate entry in `web.assets_backend`. Put tokens first; Odoo concatenates bundle files so SCSS variables/mixins from the first file are visible to every later file.
## Card Styling — Copy Odoo's Kanban Pattern
Don't rely on `var(--bs-border-color)` or `var(--bs-body-bg)` for card surfaces — they drift between themes/addons and often render **invisible**. Odoo's own kanban (`.o_kanban_record`) uses **explicit hex** values:
```css
background-color: white;
border: 1px solid #d8dadd;
```
For custom OWL dashboards / client actions use the same approach:
- Define a `_tokens.scss` partial with explicit hex values wrapped in a CSS custom property:
```scss
$fp-card: var(--fp-card-bg, #ffffff);
$fp-border: var(--fp-border-color, #d8dadd);
```
- Reference those tokens everywhere (never `var(--bs-border-color)` directly)
- Three-layer contrast: **page** (grayest) → **container/column** (mid) → **card** (brightest). That's what makes cards pop.
- Reference implementation: `fusion_plating_shopfloor/static/src/scss/_fp_shopfloor_tokens.scss`.
## Dark Mode — Branch on `$o-webclient-color-scheme` at SCSS Compile Time
Odoo 19 does NOT flip dark mode via a runtime DOM class. It compiles TWO asset bundles:
- `web.assets_backend` — compiled with `$o-webclient-color-scheme: bright`
- `web.assets_web_dark` — compiled with `$o-webclient-color-scheme: dark` (dark variant primary variables loaded first)
Your SCSS file is compiled into BOTH bundles. To make the dark bundle have different colors, **branch at compile time** using the SCSS variable Odoo sets:
```scss
$o-webclient-color-scheme: bright !default;
$_my-page-hex: #f3f4f6;
$_my-card-hex: #ffffff;
@if $o-webclient-color-scheme == dark {
$_my-page-hex: #1a1d21 !global;
$_my-card-hex: #22262d !global;
}
$my-page: var(--my-page-bg, $_my-page-hex);
$my-card: var(--my-card-bg, $_my-card-hex);
```
**Do NOT use** `.o_dark_mode` class selectors, `[data-bs-theme="dark"]`, or `@media (prefers-color-scheme: dark)` — none of those fire reliably in Odoo 19. The user toggles dark mode via the user profile, which sets a `color_scheme` cookie and reloads the page; Odoo then serves the dark bundle. Your SCSS `@if` handles the rest at compile time.
Verify by inspecting the attachments — you should see two files with different URLs for the two bundles:
```python
env['ir.qweb']._get_asset_bundle('web.assets_backend').css() # light
env['ir.qweb']._get_asset_bundle('web.assets_web_dark').css() # dark
```
## Asset Bundle Cache Busting
Odoo content-hashes the compiled bundle URL (`/web/assets/<hash>/...`). When CSS changes but the hash doesn't update, the browser serves the old bundle. Fixes in order of escalation:
1. Bump the module `version` in `__manifest__.py`
2. `DELETE FROM ir_attachment WHERE url LIKE '/web/assets/%';` then restart odoo
3. Call `env['ir.qweb']._get_asset_bundle('web.assets_backend').css()` in odoo-shell to force regeneration
4. Hard-refresh browser with cache clear (DevTools → right-click refresh → *Empty Cache and Hard Reload*); on mobile clear website data
## Naming
- New fields: `x_fc_*` prefix
- Legacy fields: `x_studio_*`
- Canadian English for all user-facing text
- Currency: `$` sign with Monetary fields + currency_id
## Cursor-Managed Modules
- **fusion_clock** is currently being modified in Cursor — always read files fresh before editing, don't assume you know the current state
## Workflow
- Local dev: `docker exec odoo-dev-app odoo -d fusion-dev -u <module> --stop-after-init`
- Local URL: http://localhost:8069
- Test before deploying. Edit existing files — don't create unnecessary new ones.
## Supabase Knowledge Base
Before starting unfamiliar work, check Supabase for context:
```bash
PGPASSWORD='a09e12e0995dc29446631fa458f3d4b3' psql -h 100.74.28.73 -p 5433 -U postgres -d postgres
```
- `fusionapps.decisions` — past architecture decisions
- `fusionapps.issues` — known issues and fixes
- `fusionapps.code_snippets` — reference code
- `fusionapps.quick_commands` — deployment and admin commands

View File

@@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
# Copyright 2024-2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
from . import models
def _fusion_tasks_post_init(env):
"""Post-install hook for fusion_tasks.
1. Sets default ICP values (upsert - safe if keys already exist).
2. Adds all active internal users to group_field_technician so
the Field Service menus are visible immediately after install.
"""
ICP = env['ir.config_parameter'].sudo()
defaults = {
'fusion_claims.google_maps_api_key': '',
'fusion_claims.store_open_hour': '9.0',
'fusion_claims.store_close_hour': '18.0',
'fusion_claims.push_enabled': 'False',
'fusion_claims.push_advance_minutes': '30',
'fusion_claims.sync_instance_id': '',
'fusion_claims.technician_start_address': '',
}
for key, default_value in defaults.items():
if not ICP.get_param(key):
ICP.set_param(key, default_value)
# Add all active internal users to Field Technician group
ft_group = env.ref('fusion_tasks.group_field_technician', raise_if_not_found=False)
if ft_group:
internal_users = env['res.users'].search([
('active', '=', True),
('share', '=', False),
])
ft_group.write({'user_ids': [(4, u.id) for u in internal_users]})

View File

@@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
# Copyright 2024-2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
{
'name': 'Fusion Tasks',
'version': '19.0.1.0.0',
'category': 'Services/Field Service',
'summary': 'Technician scheduling, route planning, GPS tracking, and cross-instance sync.',
'author': 'Nexa Systems Inc.',
'website': 'https://www.nexasystems.ca',
'license': 'OPL-1',
'depends': [
'base',
'mail',
'calendar',
'sales_team',
],
'data': [
'security/security.xml',
'security/ir.model.access.csv',
'data/ir_cron_data.xml',
'views/technician_task_views.xml',
'views/task_sync_views.xml',
'views/technician_location_views.xml',
'views/res_config_settings_views.xml',
],
'post_init_hook': '_fusion_tasks_post_init',
'assets': {
'web.assets_backend': [
'fusion_tasks/static/src/css/fusion_task_map_view.scss',
'fusion_tasks/static/src/js/fusion_task_map_view.js',
'fusion_tasks/static/src/xml/fusion_task_map_view.xml',
],
},
'installable': True,
'application': True,
}

View File

@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!--
Default configuration parameters for Fusion Tasks.
noupdate="1" ensures these are ONLY set on first install.
forcecreate="false" prevents errors if keys already exist.
Keys use fusion_claims.* prefix to preserve existing data.
-->
<data noupdate="1">
<!-- Google Maps API Key -->
<record id="config_google_maps_api_key" model="ir.config_parameter" forcecreate="false">
<field name="key">fusion_claims.google_maps_api_key</field>
<field name="value"></field>
</record>
<!-- Store Hours -->
<record id="config_store_open_hour" model="ir.config_parameter" forcecreate="false">
<field name="key">fusion_claims.store_open_hour</field>
<field name="value">9.0</field>
</record>
<record id="config_store_close_hour" model="ir.config_parameter" forcecreate="false">
<field name="key">fusion_claims.store_close_hour</field>
<field name="value">18.0</field>
</record>
<!-- Push Notifications -->
<record id="config_push_enabled" model="ir.config_parameter" forcecreate="false">
<field name="key">fusion_claims.push_enabled</field>
<field name="value">False</field>
</record>
<record id="config_push_advance_minutes" model="ir.config_parameter" forcecreate="false">
<field name="key">fusion_claims.push_advance_minutes</field>
<field name="value">30</field>
</record>
<!-- Cross-instance task sync -->
<record id="config_sync_instance_id" model="ir.config_parameter" forcecreate="false">
<field name="key">fusion_claims.sync_instance_id</field>
<field name="value"></field>
</record>
<!-- Technician start address (HQ default) -->
<record id="config_technician_start_address" model="ir.config_parameter" forcecreate="false">
<field name="key">fusion_claims.technician_start_address</field>
<field name="value"></field>
</record>
</data>
</odoo>

View File

@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2024-2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
-->
<odoo>
<data>
<!-- Cron Job: Calculate Travel Times for Technician Tasks (every 15 min) -->
<record id="ir_cron_technician_travel_times" model="ir.cron">
<field name="name">Fusion Tasks: Calculate Technician Travel Times</field>
<field name="model_id" ref="model_fusion_technician_task"/>
<field name="state">code</field>
<field name="code">model._cron_calculate_travel_times()</field>
<field name="interval_number">15</field>
<field name="interval_type">minutes</field>
<field name="active">True</field>
</record>
<!-- Cron Job: Send Push Notifications for Upcoming Tasks -->
<record id="ir_cron_technician_push_notifications" model="ir.cron">
<field name="name">Fusion Tasks: Technician Push Notifications</field>
<field name="model_id" ref="model_fusion_technician_task"/>
<field name="state">code</field>
<field name="code">model._cron_send_push_notifications()</field>
<field name="interval_number">15</field>
<field name="interval_type">minutes</field>
<field name="active">True</field>
</record>
<!-- Cron Job: Pull Remote Technician Tasks (cross-instance sync) -->
<record id="ir_cron_task_sync_pull" model="ir.cron">
<field name="name">Fusion Tasks: Sync Remote Tasks (Pull)</field>
<field name="model_id" ref="model_fusion_task_sync_config"/>
<field name="state">code</field>
<field name="code">model._cron_pull_remote_tasks()</field>
<field name="interval_number">2</field>
<field name="interval_type">minutes</field>
<field name="active">True</field>
</record>
<!-- Cron Job: Cleanup Old Shadow Tasks (30+ days) -->
<record id="ir_cron_task_sync_cleanup" model="ir.cron">
<field name="name">Fusion Tasks: Cleanup Old Shadow Tasks</field>
<field name="model_id" ref="model_fusion_task_sync_config"/>
<field name="state">code</field>
<field name="code">model._cron_cleanup_old_shadows()</field>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="active">True</field>
<field name="nextcall" eval="DateTime.now().replace(hour=3, minute=0, second=0)"/>
</record>
<!-- Cron Job: Check for Late Technician Arrivals -->
<record id="ir_cron_check_late_arrivals" model="ir.cron">
<field name="name">Fusion Tasks: Check Late Technician Arrivals</field>
<field name="model_id" ref="model_fusion_technician_task"/>
<field name="state">code</field>
<field name="code">model._cron_check_late_arrivals()</field>
<field name="interval_number">10</field>
<field name="interval_type">minutes</field>
<field name="active">True</field>
</record>
<!-- Cron Job: Cleanup Old Technician Locations -->
<record id="ir_cron_cleanup_locations" model="ir.cron">
<field name="name">Fusion Tasks: Cleanup Old Locations</field>
<field name="model_id" ref="model_fusion_technician_location"/>
<field name="state">code</field>
<field name="code">model._cron_cleanup_old_locations()</field>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="active">True</field>
<field name="nextcall" eval="DateTime.now().replace(hour=4, minute=0, second=0)"/>
</record>
</data>
</odoo>

View File

@@ -0,0 +1,13 @@
# -*- coding: utf-8 -*-
# Copyright 2024-2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
from . import email_builder_mixin
from . import res_partner
from . import res_company
from . import res_users
from . import res_config_settings
from . import technician_task
from . import task_sync
from . import technician_location
from . import push_subscription

View File

@@ -57,29 +57,29 @@ class FusionEmailBuilderMixin(models.AbstractModel):
company = self._get_company_info()
parts = []
# -- Wrapper open + accent bar
# -- Wrapper open + accent bar (no forced bg/color so it adapts to dark/light)
parts.append(
f'<div style="font-family:-apple-system,BlinkMacSystemFont,\'Segoe UI\',Roboto,Arial,sans-serif;'
f'max-width:600px;margin:0 auto;color:#2d3748;">'
f'max-width:600px;margin:0 auto;">'
f'<div style="height:4px;background-color:{accent};"></div>'
f'<div style="background:#ffffff;padding:32px 28px;border:1px solid #e2e8f0;border-top:none;">'
f'<div style="padding:32px 28px;">'
)
# -- Company name
# -- Company name (accent color works in both themes)
parts.append(
f'<p style="color:{accent};font-size:13px;font-weight:600;letter-spacing:0.5px;'
f'text-transform:uppercase;margin:0 0 24px 0;">{company["name"]}</p>'
)
# -- Title
# -- Title (inherits text color from container)
parts.append(
f'<h2 style="color:#1a202c;font-size:22px;font-weight:700;'
f'<h2 style="font-size:22px;font-weight:700;'
f'margin:0 0 6px 0;line-height:1.3;">{title}</h2>'
)
# -- Summary
# -- Summary (muted via opacity)
parts.append(
f'<p style="color:#718096;font-size:15px;line-height:1.5;'
f'<p style="opacity:0.65;font-size:15px;line-height:1.5;'
f'margin:0 0 24px 0;">{summary}</p>'
)
@@ -108,10 +108,10 @@ class FusionEmailBuilderMixin(models.AbstractModel):
# -- Sign-off
signer = sender_name or (self.env.user.name if self.env.user else '')
parts.append(
f'<p style="color:#2d3748;font-size:14px;line-height:1.6;margin:24px 0 0 0;">'
f'<p style="font-size:14px;line-height:1.6;margin:24px 0 0 0;">'
f'Best regards,<br/>'
f'<strong>{signer}</strong><br/>'
f'<span style="color:#718096;">{company["name"]}</span></p>'
f'<span style="opacity:0.6;">{company["name"]}</span></p>'
)
# -- Close content card
@@ -127,7 +127,7 @@ class FusionEmailBuilderMixin(models.AbstractModel):
parts.append(
f'<div style="padding:16px 28px;text-align:center;">'
f'<p style="color:#a0aec0;font-size:11px;line-height:1.5;margin:0;">'
f'<p style="opacity:0.5;font-size:11px;line-height:1.5;margin:0;">'
f'{footer_text}<br/>'
f'This is an automated notification from the ADP Claims Management System.</p>'
f'</div>'
@@ -155,8 +155,8 @@ class FusionEmailBuilderMixin(models.AbstractModel):
html = (
'<table style="width:100%;border-collapse:collapse;margin:0 0 24px 0;">'
f'<tr><td colspan="2" style="padding:10px 14px;font-size:12px;font-weight:600;'
f'color:#718096;text-transform:uppercase;letter-spacing:0.5px;'
f'border-bottom:2px solid #e2e8f0;">{heading}</td></tr>'
f'opacity:0.55;text-transform:uppercase;letter-spacing:0.5px;'
f'border-bottom:2px solid rgba(128,128,128,0.25);">{heading}</td></tr>'
)
for label, value in rows:
@@ -164,10 +164,10 @@ class FusionEmailBuilderMixin(models.AbstractModel):
continue
html += (
f'<tr>'
f'<td style="padding:10px 14px;color:#718096;font-size:14px;'
f'border-bottom:1px solid #f0f0f0;width:35%;">{label}</td>'
f'<td style="padding:10px 14px;color:#2d3748;font-size:14px;'
f'border-bottom:1px solid #f0f0f0;">{value}</td>'
f'<td style="padding:10px 14px;opacity:0.6;font-size:14px;'
f'border-bottom:1px solid rgba(128,128,128,0.15);width:35%;">{label}</td>'
f'<td style="padding:10px 14px;font-size:14px;'
f'border-bottom:1px solid rgba(128,128,128,0.15);">{value}</td>'
f'</tr>'
)
@@ -178,8 +178,8 @@ class FusionEmailBuilderMixin(models.AbstractModel):
"""Build a left-border accent note block."""
return (
f'<div style="border-left:3px solid {color};padding:12px 16px;'
f'margin:0 0 24px 0;background:#f7fafc;">'
f'<p style="margin:0;font-size:14px;line-height:1.5;color:#2d3748;">{text}</p>'
f'margin:0 0 24px 0;">'
f'<p style="margin:0;font-size:14px;line-height:1.5;">{text}</p>'
f'</div>'
)
@@ -199,23 +199,22 @@ class FusionEmailBuilderMixin(models.AbstractModel):
description: e.g. "ADP Application (PDF), XML Data File"
"""
return (
f'<div style="padding:10px 14px;border:1px dashed #e2e8f0;border-radius:6px;'
f'<div style="padding:10px 14px;border:1px dashed rgba(128,128,128,0.35);border-radius:6px;'
f'margin:0 0 24px 0;">'
f'<p style="margin:0;font-size:13px;color:#718096;">'
f'<strong style="color:#2d3748;">Attached:</strong> {description}</p>'
f'<p style="margin:0;font-size:13px;opacity:0.65;">'
f'<strong style="opacity:1;">Attached:</strong> {description}</p>'
f'</div>'
)
def _email_status_badge(self, label, color='#2B6CB0'):
"""Return an inline status badge/pill HTML snippet."""
# Pick a light background tint for the badge
bg_map = {
'#38a169': '#f0fff4',
'#2B6CB0': '#ebf4ff',
'#d69e2e': '#fefcbf',
'#c53030': '#fff5f5',
'#38a169': 'rgba(56,161,105,0.12)',
'#2B6CB0': 'rgba(43,108,176,0.12)',
'#d69e2e': 'rgba(214,158,46,0.12)',
'#c53030': 'rgba(197,48,48,0.12)',
}
bg = bg_map.get(color, '#ebf4ff')
bg = bg_map.get(color, 'rgba(43,108,176,0.12)')
return (
f'<span style="display:inline-block;background:{bg};color:{color};'
f'padding:2px 10px;border-radius:12px;font-size:12px;font-weight:600;">'

View File

@@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
# Copyright 2024-2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
from odoo import models, fields
class ResCompany(models.Model):
_inherit = 'res.company'
x_fc_google_review_url = fields.Char(
string='Google Review URL',
help='Google Business Profile review link sent to clients after service completion',
)

View File

@@ -0,0 +1,73 @@
# -*- coding: utf-8 -*-
# Copyright 2024-2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
from odoo import models, fields
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
# Google Maps API Settings
fc_google_maps_api_key = fields.Char(
string='Google Maps API Key',
config_parameter='fusion_claims.google_maps_api_key',
help='API key for Google Maps Places autocomplete in address fields',
)
fc_google_review_url = fields.Char(
related='company_id.x_fc_google_review_url',
readonly=False,
string='Google Review URL',
)
# Technician Management
fc_store_open_hour = fields.Float(
string='Store Open Time',
config_parameter='fusion_claims.store_open_hour',
help='Store opening time for technician scheduling (e.g. 9.0 = 9:00 AM)',
)
fc_store_close_hour = fields.Float(
string='Store Close Time',
config_parameter='fusion_claims.store_close_hour',
help='Store closing time for technician scheduling (e.g. 18.0 = 6:00 PM)',
)
fc_google_distance_matrix_enabled = fields.Boolean(
string='Enable Distance Matrix',
config_parameter='fusion_claims.google_distance_matrix_enabled',
help='Enable Google Distance Matrix API for travel time calculations between technician tasks',
)
fc_technician_start_address = fields.Char(
string='Technician Start Address',
config_parameter='fusion_claims.technician_start_address',
help='Default start location for technician travel calculations (e.g. warehouse/office address)',
)
fc_location_retention_days = fields.Char(
string='Location History Retention (Days)',
config_parameter='fusion_claims.location_retention_days',
help='How many days to keep technician location history. '
'Leave empty = 30 days (1 month). '
'0 = delete at end of each day. '
'1+ = keep for that many days.',
)
# Web Push Notifications
fc_push_enabled = fields.Boolean(
string='Enable Push Notifications',
config_parameter='fusion_claims.push_enabled',
help='Enable web push notifications for technician tasks',
)
fc_vapid_public_key = fields.Char(
string='VAPID Public Key',
config_parameter='fusion_claims.vapid_public_key',
help='Public key for Web Push VAPID authentication (auto-generated)',
)
fc_vapid_private_key = fields.Char(
string='VAPID Private Key',
config_parameter='fusion_claims.vapid_private_key',
help='Private key for Web Push VAPID authentication (auto-generated)',
)
fc_push_advance_minutes = fields.Integer(
string='Notification Advance (min)',
config_parameter='fusion_claims.push_advance_minutes',
help='Send push notifications this many minutes before a scheduled task',
)

View File

@@ -0,0 +1,79 @@
# -*- coding: utf-8 -*-
# Copyright 2024-2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
import logging
import requests
from odoo import models, fields, api
_logger = logging.getLogger(__name__)
class ResPartner(models.Model):
_inherit = 'res.partner'
x_fc_start_address = fields.Char(
string='Start Location',
help='Technician daily start location (home, warehouse, etc.). '
'Used as origin for first travel time calculation. '
'If empty, the company default HQ address is used.',
)
x_fc_start_address_lat = fields.Float(
string='Start Latitude', digits=(10, 7),
)
x_fc_start_address_lng = fields.Float(
string='Start Longitude', digits=(10, 7),
)
def _geocode_start_address(self, address):
if not address or not address.strip():
return 0.0, 0.0
api_key = self.env['ir.config_parameter'].sudo().get_param(
'fusion_claims.google_maps_api_key', '')
if not api_key:
return 0.0, 0.0
try:
resp = requests.get(
'https://maps.googleapis.com/maps/api/geocode/json',
params={'address': address.strip(), 'key': api_key, 'region': 'ca'},
timeout=10,
)
data = resp.json()
if data.get('status') == 'OK' and data.get('results'):
loc = data['results'][0]['geometry']['location']
return loc['lat'], loc['lng']
except Exception as e:
_logger.warning("Start address geocoding failed for '%s': %s", address, e)
return 0.0, 0.0
@api.model_create_multi
def create(self, vals_list):
records = super().create(vals_list)
for rec, vals in zip(records, vals_list):
addr = vals.get('x_fc_start_address')
if addr:
lat, lng = rec._geocode_start_address(addr)
if lat and lng:
rec.write({
'x_fc_start_address_lat': lat,
'x_fc_start_address_lng': lng,
})
return records
def write(self, vals):
res = super().write(vals)
if 'x_fc_start_address' in vals:
addr = vals['x_fc_start_address']
if addr and addr.strip():
lat, lng = self._geocode_start_address(addr)
if lat and lng:
super().write({
'x_fc_start_address_lat': lat,
'x_fc_start_address_lng': lng,
})
else:
super().write({
'x_fc_start_address_lat': 0.0,
'x_fc_start_address_lng': 0.0,
})
return res

View File

@@ -2,7 +2,7 @@
# Copyright 2024-2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
from odoo import models, fields, api
from odoo import models, fields
class ResUsers(models.Model):
@@ -23,4 +23,4 @@ class ResUsers(models.Model):
help='Shared identifier for this technician across Odoo instances. '
'Must be the same value on all instances for the same person.',
copy=False,
)
)

View File

@@ -26,12 +26,21 @@ from datetime import timedelta
_logger = logging.getLogger(__name__)
SYNC_TASK_FIELDS = [
'x_fc_sync_uuid', 'name', 'technician_id', 'task_type', 'status',
'x_fc_sync_uuid', 'name', 'technician_id', 'additional_technician_ids',
'task_type', 'status',
'scheduled_date', 'time_start', 'time_end', 'duration_hours',
'address_street', 'address_street2', 'address_city', 'address_zip',
'address_lat', 'address_lng', 'priority', 'partner_id',
'address_state_id', 'address_buzz_code',
'address_lat', 'address_lng', 'priority', 'partner_id', 'partner_phone',
'pod_required', 'description',
'travel_time_minutes', 'travel_distance_km', 'travel_origin',
'completed_latitude', 'completed_longitude',
'action_latitude', 'action_longitude',
'completion_datetime',
]
TERMINAL_STATUSES = ('completed', 'cancelled')
class FusionTaskSyncConfig(models.Model):
_name = 'fusion.task.sync.config'
@@ -51,7 +60,7 @@ class FusionTaskSyncConfig(models.Model):
last_sync_error = fields.Text('Last Error', readonly=True)
# ------------------------------------------------------------------
# JSON-RPC helpers
# JSON-RPC helpers (uses /jsonrpc dispatch, muted on receiving side)
# ------------------------------------------------------------------
def _jsonrpc(self, service, method, args):
@@ -93,9 +102,7 @@ class FusionTaskSyncConfig(models.Model):
return uid
def _rpc(self, model, method, args, kwargs=None):
"""Execute a method on the remote instance via execute_kw.
execute_kw(db, uid, password, model, method, [args], {kwargs})
"""
"""Execute a method on the remote instance via execute_kw."""
self.ensure_one()
uid = self._authenticate()
if not uid:
@@ -196,7 +203,11 @@ class FusionTaskSyncConfig(models.Model):
_logger.exception("Task sync push to %s failed", config.name)
def _push_tasks_to_remote(self, tasks, operation, local_instance_id):
"""Push task data to a single remote instance."""
"""Push task data to a single remote instance.
Maps additional_technician_ids via sync IDs so the remote instance
also blocks those technicians' schedules.
"""
self.ensure_one()
local_map = self._get_local_tech_map()
remote_map = self._get_remote_tech_map()
@@ -213,12 +224,22 @@ class FusionTaskSyncConfig(models.Model):
if not remote_tech_uid:
continue
# Map additional technicians to remote user IDs
remote_additional_ids = []
for tech in task.additional_technician_ids:
add_sync_id = local_map.get(tech.id)
if add_sync_id:
remote_add_uid = remote_map.get(add_sync_id)
if remote_add_uid:
remote_additional_ids.append(remote_add_uid)
task_data = {
'x_fc_sync_uuid': task.x_fc_sync_uuid,
'x_fc_sync_source': local_instance_id,
'x_fc_sync_remote_id': task.id,
'name': f"[{local_instance_id.upper()}] {task.name}",
'technician_id': remote_tech_uid,
'additional_technician_ids': [(6, 0, remote_additional_ids)],
'task_type': task.task_type,
'status': task.status,
'scheduled_date': str(task.scheduled_date) if task.scheduled_date else False,
@@ -233,7 +254,16 @@ class FusionTaskSyncConfig(models.Model):
'address_lng': float(task.address_lng or 0),
'priority': task.priority or 'normal',
'x_fc_sync_client_name': task.partner_id.name if task.partner_id else '',
'travel_time_minutes': task.travel_time_minutes or 0,
'travel_distance_km': float(task.travel_distance_km or 0),
'travel_origin': task.travel_origin or '',
'completed_latitude': float(task.completed_latitude or 0),
'completed_longitude': float(task.completed_longitude or 0),
'action_latitude': float(task.action_latitude or 0),
'action_longitude': float(task.action_longitude or 0),
}
if task.completion_datetime:
task_data['completion_datetime'] = str(task.completion_datetime)
existing = self._rpc(
'fusion.technician.task', 'search',
@@ -253,17 +283,126 @@ class FusionTaskSyncConfig(models.Model):
self._rpc('fusion.technician.task', 'write',
[existing, {'status': 'cancelled', 'active': False}], ctx)
@api.model
def _push_shadow_status(self, shadow_tasks):
"""Push local status changes on shadow tasks back to their source instance.
When a tech changes a shadow task status locally, update the original
task on the remote instance and trigger the appropriate client emails
there. Only the parent (originating) instance sends client-facing
emails -- the child instance skips them via x_fc_sync_source guards.
"""
configs = self.sudo().search([('active', '=', True)])
config_by_instance = {c.instance_id: c for c in configs}
ctx = {'context': {'skip_task_sync': True, 'skip_travel_recalc': True}}
for task in shadow_tasks:
config = config_by_instance.get(task.x_fc_sync_source)
if not config or not task.x_fc_sync_remote_id:
continue
try:
update_vals = {'status': task.status}
if task.status == 'completed' and task.completion_datetime:
update_vals['completion_datetime'] = str(task.completion_datetime)
if task.completed_latitude and task.completed_longitude:
update_vals['completed_latitude'] = task.completed_latitude
update_vals['completed_longitude'] = task.completed_longitude
if task.action_latitude and task.action_longitude:
update_vals['action_latitude'] = task.action_latitude
update_vals['action_longitude'] = task.action_longitude
config._rpc(
'fusion.technician.task', 'write',
[[task.x_fc_sync_remote_id], update_vals], ctx)
_logger.info(
"Pushed status '%s' for shadow task %s back to %s (remote id %d)",
task.status, task.name, config.name, task.x_fc_sync_remote_id)
self._trigger_parent_notifications(config, task)
except Exception:
_logger.exception(
"Failed to push status for shadow task %s to %s",
task.name, config.name)
@api.model
def _push_technician_location(self, user_id, latitude, longitude, accuracy=0):
"""Push a technician's location update to all remote instances.
Called when a technician performs a task action (en_route, complete)
so the other instance immediately knows where the tech is, without
waiting for the next pull cron cycle.
"""
configs = self.sudo().search([('active', '=', True)])
if not configs:
return
local_map = configs[0]._get_local_tech_map()
sync_id = local_map.get(user_id)
if not sync_id:
return
for config in configs:
try:
remote_map = config._get_remote_tech_map()
remote_uid = remote_map.get(sync_id)
if not remote_uid:
continue
# Create location record on remote instance
config._rpc(
'fusion.technician.location', 'create',
[[{
'user_id': remote_uid,
'latitude': latitude,
'longitude': longitude,
'accuracy': accuracy,
'source': 'sync',
'sync_instance': configs[0]._get_local_instance_id(),
}]])
except Exception:
_logger.warning(
"Failed to push location for tech %s to %s",
user_id, config.name)
def _trigger_parent_notifications(self, config, task):
"""After pushing a shadow status, trigger appropriate emails and
notifications on the parent instance so the client gets notified
exactly once (from the originating instance only)."""
remote_id = task.x_fc_sync_remote_id
if task.status == 'completed':
for method in ('_notify_scheduler_on_completion',
'_send_task_completion_email'):
try:
config._rpc('fusion.technician.task', method, [[remote_id]])
except Exception:
_logger.warning(
"Could not call %s on remote for %s", method, task.name)
elif task.status == 'en_route':
try:
config._rpc(
'fusion.technician.task',
'_send_task_en_route_email', [[remote_id]])
except Exception:
_logger.warning(
"Could not trigger en-route email on remote for %s",
task.name)
elif task.status == 'cancelled':
try:
config._rpc(
'fusion.technician.task',
'_send_task_cancelled_email', [[remote_id]])
except Exception:
_logger.warning(
"Could not trigger cancel email on remote for %s",
task.name)
# ------------------------------------------------------------------
# PULL: cron-based full reconciliation
# ------------------------------------------------------------------
@api.model
def _cron_pull_remote_tasks(self):
"""Cron job: pull tasks from all active remote instances."""
"""Cron job: pull tasks and technician locations from all active remote instances."""
configs = self.sudo().search([('active', '=', True)])
for config in configs:
try:
config._pull_tasks_from_remote()
config._pull_technician_locations()
config.sudo().write({
'last_sync': fields.Datetime.now(),
'last_sync_error': False,
@@ -273,7 +412,11 @@ class FusionTaskSyncConfig(models.Model):
config.sudo().write({'last_sync_error': str(e)})
def _pull_tasks_from_remote(self):
"""Pull all active tasks for matched technicians from the remote instance."""
"""Pull all active tasks for matched technicians from the remote instance.
After syncing, recalculates travel chains for all affected tech+date
combos so route planning accounts for both local and shadow tasks.
"""
self.ensure_one()
local_syncid_to_uid = self._get_local_syncid_to_uid()
if not local_syncid_to_uid:
@@ -295,7 +438,9 @@ class FusionTaskSyncConfig(models.Model):
remote_tasks = self._rpc(
'fusion.technician.task', 'search_read',
[[
'|',
('technician_id', 'in', remote_tech_ids),
('additional_technician_ids', 'in', remote_tech_ids),
('scheduled_date', '>=', str(cutoff)),
('x_fc_sync_source', '=', False),
]],
@@ -308,6 +453,8 @@ class FusionTaskSyncConfig(models.Model):
skip_task_sync=True, skip_travel_recalc=True)
remote_uuids = set()
affected_combos = set()
for rt in remote_tasks:
sync_uuid = rt.get('x_fc_sync_uuid')
if not sync_uuid:
@@ -323,6 +470,25 @@ class FusionTaskSyncConfig(models.Model):
partner_raw = rt.get('partner_id')
client_name = partner_raw[1] if isinstance(partner_raw, (list, tuple)) and len(partner_raw) > 1 else ''
client_phone = rt.get('partner_phone', '') or ''
state_raw = rt.get('address_state_id')
state_name = ''
if isinstance(state_raw, (list, tuple)) and len(state_raw) > 1:
state_name = state_raw[1]
# Map additional technicians from remote to local
local_additional_ids = []
remote_add_raw = rt.get('additional_technician_ids', [])
if remote_add_raw and isinstance(remote_add_raw, list):
for add_uid in remote_add_raw:
add_sync_id = remote_syncid_by_uid.get(add_uid)
if add_sync_id:
local_add_uid = local_syncid_to_uid.get(add_sync_id)
if local_add_uid:
local_additional_ids.append(local_add_uid)
sched_date = rt.get('scheduled_date')
vals = {
'x_fc_sync_uuid': sync_uuid,
@@ -330,9 +496,10 @@ class FusionTaskSyncConfig(models.Model):
'x_fc_sync_remote_id': rt['id'],
'name': f"[{self.instance_id.upper()}] {rt.get('name', '')}",
'technician_id': local_uid,
'additional_technician_ids': [(6, 0, local_additional_ids)],
'task_type': rt.get('task_type', 'delivery'),
'status': rt.get('status', 'scheduled'),
'scheduled_date': rt.get('scheduled_date'),
'scheduled_date': sched_date,
'time_start': rt.get('time_start', 9.0),
'time_end': rt.get('time_end', 10.0),
'duration_hours': rt.get('duration_hours', 1.0),
@@ -340,19 +507,45 @@ class FusionTaskSyncConfig(models.Model):
'address_street2': rt.get('address_street2', ''),
'address_city': rt.get('address_city', ''),
'address_zip': rt.get('address_zip', ''),
'address_buzz_code': rt.get('address_buzz_code', ''),
'address_lat': rt.get('address_lat', 0),
'address_lng': rt.get('address_lng', 0),
'priority': rt.get('priority', 'normal'),
'pod_required': rt.get('pod_required', False),
'description': rt.get('description', ''),
'x_fc_sync_client_name': client_name,
'x_fc_sync_client_phone': client_phone,
'travel_time_minutes': rt.get('travel_time_minutes', 0),
'travel_distance_km': rt.get('travel_distance_km', 0),
'travel_origin': rt.get('travel_origin', ''),
'completed_latitude': rt.get('completed_latitude', 0),
'completed_longitude': rt.get('completed_longitude', 0),
'action_latitude': rt.get('action_latitude', 0),
'action_longitude': rt.get('action_longitude', 0),
}
if rt.get('completion_datetime'):
vals['completion_datetime'] = rt['completion_datetime']
if state_name:
state_rec = self.env['res.country.state'].sudo().search(
[('name', '=', state_name)], limit=1)
if state_rec:
vals['address_state_id'] = state_rec.id
existing = Task.search([('x_fc_sync_uuid', '=', sync_uuid)], limit=1)
if existing:
if existing.status in TERMINAL_STATUSES:
vals.pop('status', None)
existing.write(vals)
else:
vals['sale_order_id'] = False
Task.create([vals])
if sched_date:
affected_combos.add((local_uid, sched_date))
for add_uid in local_additional_ids:
affected_combos.add((add_uid, sched_date))
stale_shadows = Task.search([
('x_fc_sync_source', '=', self.instance_id),
('x_fc_sync_uuid', 'not in', list(remote_uuids)),
@@ -360,10 +553,149 @@ class FusionTaskSyncConfig(models.Model):
('active', '=', True),
])
if stale_shadows:
for st in stale_shadows:
if st.scheduled_date and st.technician_id:
affected_combos.add((st.technician_id.id, st.scheduled_date))
for tech in st.additional_technician_ids:
if st.scheduled_date:
affected_combos.add((tech.id, st.scheduled_date))
stale_shadows.write({'active': False, 'status': 'cancelled'})
_logger.info("Deactivated %d stale shadow tasks from %s",
len(stale_shadows), self.instance_id)
if affected_combos:
today = fields.Date.today()
today_str = str(today)
future_combos = set()
for tid, d in affected_combos:
if not d:
continue
d_str = str(d) if not isinstance(d, str) else d
if d_str >= today_str:
future_combos.add((tid, d_str))
if future_combos:
TaskModel = self.env['fusion.technician.task'].sudo()
try:
ungeocode = TaskModel.search([
('x_fc_sync_source', '=', self.instance_id),
('active', '=', True),
('scheduled_date', '>=', today_str),
('status', 'not in', ['cancelled']),
'|',
('address_lat', '=', 0), ('address_lat', '=', False),
])
geocoded = 0
for shadow in ungeocode:
if shadow.address_display:
if shadow.with_context(skip_travel_recalc=True)._geocode_address():
geocoded += 1
if geocoded:
_logger.info("Geocoded %d shadow tasks from %s",
geocoded, self.name)
except Exception:
_logger.exception(
"Shadow task geocoding after sync from %s failed", self.name)
try:
TaskModel._recalculate_combos_travel(future_combos)
_logger.info(
"Recalculated travel for %d tech+date combos after sync from %s",
len(future_combos), self.name)
except Exception:
_logger.exception(
"Travel recalculation after sync from %s failed", self.name)
# ------------------------------------------------------------------
# PULL: technician locations from remote instance
# ------------------------------------------------------------------
def _pull_technician_locations(self):
"""Pull latest GPS locations for matched technicians from the remote instance.
Creates local location records with source='sync' so the map view
shows technician positions from both instances. Only keeps the single
most recent synced location per technician (replaces older synced
records to avoid clutter).
"""
self.ensure_one()
local_syncid_to_uid = self._get_local_syncid_to_uid()
if not local_syncid_to_uid:
return
remote_map = self._get_remote_tech_map()
if not remote_map:
return
matched_sync_ids = set(local_syncid_to_uid.keys()) & set(remote_map.keys())
if not matched_sync_ids:
return
remote_tech_ids = [remote_map[sid] for sid in matched_sync_ids]
remote_syncid_by_uid = {v: k for k, v in remote_map.items()}
remote_locations = self._rpc(
'fusion.technician.location', 'search_read',
[[
('user_id', 'in', remote_tech_ids),
('logged_at', '>', str(fields.Datetime.subtract(
fields.Datetime.now(), hours=24))),
('source', '!=', 'sync'),
]],
{
'fields': ['user_id', 'latitude', 'longitude',
'accuracy', 'logged_at'],
'order': 'logged_at desc',
})
if not remote_locations:
return
Location = self.env['fusion.technician.location'].sudo()
seen_techs = set()
synced_count = 0
for rloc in remote_locations:
remote_uid_raw = rloc['user_id']
remote_uid = (remote_uid_raw[0]
if isinstance(remote_uid_raw, (list, tuple))
else remote_uid_raw)
if remote_uid in seen_techs:
continue
seen_techs.add(remote_uid)
sync_id = remote_syncid_by_uid.get(remote_uid)
local_uid = local_syncid_to_uid.get(sync_id) if sync_id else None
if not local_uid:
continue
lat = rloc.get('latitude', 0)
lng = rloc.get('longitude', 0)
if not lat or not lng:
continue
old_synced = Location.search([
('user_id', '=', local_uid),
('source', '=', 'sync'),
('sync_instance', '=', self.instance_id),
])
if old_synced:
old_synced.unlink()
Location.create({
'user_id': local_uid,
'latitude': lat,
'longitude': lng,
'accuracy': rloc.get('accuracy', 0),
'logged_at': rloc.get('logged_at', fields.Datetime.now()),
'source': 'sync',
'sync_instance': self.instance_id,
})
synced_count += 1
if synced_count:
_logger.info("Synced %d technician location(s) from %s",
synced_count, self.name)
# ------------------------------------------------------------------
# CLEANUP
# ------------------------------------------------------------------
@@ -390,6 +722,7 @@ class FusionTaskSyncConfig(models.Model):
"""Manually trigger a full sync for this config."""
self.ensure_one()
self._pull_tasks_from_remote()
self._pull_technician_locations()
self.sudo().write({
'last_sync': fields.Datetime.now(),
'last_sync_error': False,
@@ -397,12 +730,18 @@ class FusionTaskSyncConfig(models.Model):
shadow_count = self.env['fusion.technician.task'].sudo().search_count([
('x_fc_sync_source', '=', self.instance_id),
])
loc_count = self.env['fusion.technician.location'].sudo().search_count([
('source', '=', 'sync'),
('sync_instance', '=', self.instance_id),
])
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': 'Sync Complete',
'message': f'Synced from {self.name}. {shadow_count} shadow task(s) now visible.',
'message': (f'Synced from {self.name}. '
f'{shadow_count} shadow task(s), '
f'{loc_count} technician location(s) visible.'),
'type': 'success',
'sticky': False,
},

View File

@@ -48,7 +48,12 @@ class FusionTechnicianLocation(models.Model):
source = fields.Selection([
('portal', 'Portal'),
('app', 'Mobile App'),
('sync', 'Synced'),
], string='Source', default='portal')
sync_instance = fields.Char(
'Sync Instance', index=True,
help='Source instance ID if synced (e.g. westin, mobility)',
)
@api.model
def log_location(self, latitude, longitude, accuracy=None):
@@ -63,18 +68,27 @@ class FusionTechnicianLocation(models.Model):
@api.model
def get_latest_locations(self):
"""Get the most recent location for each technician (for map view)."""
"""Get the most recent location for each technician (for map view).
Includes both local GPS pings and synced locations from remote
instances, so the map shows all shared technicians regardless of
which Odoo instance they are clocked into.
"""
self.env.cr.execute("""
SELECT DISTINCT ON (user_id)
user_id, latitude, longitude, accuracy, logged_at
user_id, latitude, longitude, accuracy, logged_at,
COALESCE(sync_instance, '') AS sync_instance
FROM fusion_technician_location
WHERE logged_at > NOW() - INTERVAL '24 hours'
ORDER BY user_id, logged_at DESC
""")
rows = self.env.cr.dictfetchall()
local_id = self.env['ir.config_parameter'].sudo().get_param(
'fusion_claims.sync_instance_id', '')
result = []
for row in rows:
user = self.env['res.users'].sudo().browse(row['user_id'])
src = row.get('sync_instance') or local_id
result.append({
'user_id': row['user_id'],
'name': user.name,
@@ -82,6 +96,7 @@ class FusionTechnicianLocation(models.Model):
'longitude': row['longitude'],
'accuracy': row['accuracy'],
'logged_at': str(row['logged_at']),
'sync_instance': src,
})
return result

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,12 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_fusion_technician_task_user,fusion.technician.task.user,model_fusion_technician_task,sales_team.group_sale_salesman,1,1,1,0
access_fusion_technician_task_manager,fusion.technician.task.manager,model_fusion_technician_task,sales_team.group_sale_manager,1,1,1,1
access_fusion_technician_task_technician,fusion.technician.task.technician,model_fusion_technician_task,fusion_tasks.group_field_technician,1,1,0,0
access_fusion_technician_task_portal,fusion.technician.task.portal,model_fusion_technician_task,base.group_portal,1,0,0,0
access_fusion_push_subscription_user,fusion.push.subscription.user,model_fusion_push_subscription,base.group_user,1,1,1,0
access_fusion_push_subscription_portal,fusion.push.subscription.portal,model_fusion_push_subscription,base.group_portal,1,1,1,0
access_fusion_technician_location_manager,fusion.technician.location.manager,model_fusion_technician_location,sales_team.group_sale_manager,1,1,1,1
access_fusion_technician_location_user,fusion.technician.location.user,model_fusion_technician_location,sales_team.group_sale_salesman,1,0,0,0
access_fusion_technician_location_portal,fusion.technician.location.portal,model_fusion_technician_location,base.group_portal,0,0,1,0
access_fusion_task_sync_config_manager,fusion.task.sync.config.manager,model_fusion_task_sync_config,sales_team.group_sale_manager,1,1,1,1
access_fusion_task_sync_config_user,fusion.task.sync.config.user,model_fusion_task_sync_config,sales_team.group_sale_salesman,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_fusion_technician_task_user fusion.technician.task.user model_fusion_technician_task sales_team.group_sale_salesman 1 1 1 0
3 access_fusion_technician_task_manager fusion.technician.task.manager model_fusion_technician_task sales_team.group_sale_manager 1 1 1 1
4 access_fusion_technician_task_technician fusion.technician.task.technician model_fusion_technician_task fusion_tasks.group_field_technician 1 1 0 0
5 access_fusion_technician_task_portal fusion.technician.task.portal model_fusion_technician_task base.group_portal 1 0 0 0
6 access_fusion_push_subscription_user fusion.push.subscription.user model_fusion_push_subscription base.group_user 1 1 1 0
7 access_fusion_push_subscription_portal fusion.push.subscription.portal model_fusion_push_subscription base.group_portal 1 1 1 0
8 access_fusion_technician_location_manager fusion.technician.location.manager model_fusion_technician_location sales_team.group_sale_manager 1 1 1 1
9 access_fusion_technician_location_user fusion.technician.location.user model_fusion_technician_location sales_team.group_sale_salesman 1 0 0 0
10 access_fusion_technician_location_portal fusion.technician.location.portal model_fusion_technician_location base.group_portal 0 0 1 0
11 access_fusion_task_sync_config_manager fusion.task.sync.config.manager model_fusion_task_sync_config sales_team.group_sale_manager 1 1 1 1
12 access_fusion_task_sync_config_user fusion.task.sync.config.user model_fusion_task_sync_config sales_team.group_sale_salesman 1 0 0 0

View File

@@ -0,0 +1,103 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ================================================================== -->
<!-- MODULE CATEGORY -->
<!-- ================================================================== -->
<record id="module_category_fusion_tasks" model="ir.module.category">
<field name="name">Fusion Tasks</field>
<field name="sequence">46</field>
</record>
<!-- ================================================================== -->
<!-- FUSION TASKS PRIVILEGE (Odoo 19 pattern) -->
<!-- ================================================================== -->
<record id="res_groups_privilege_fusion_tasks" model="res.groups.privilege">
<field name="name">Fusion Tasks</field>
<field name="sequence">46</field>
<field name="category_id" ref="module_category_fusion_tasks"/>
</record>
<!-- ================================================================== -->
<!-- FIELD TECHNICIAN GROUP -->
<!-- Standalone group safe for both portal and internal users. -->
<!-- Do NOT imply base.group_user — that chain conflicts with portal -->
<!-- users (share=True). -->
<!-- ================================================================== -->
<record id="group_field_technician" model="res.groups">
<field name="name">Field Technician</field>
<field name="privilege_id" ref="res_groups_privilege_fusion_tasks"/>
</record>
<!-- ================================================================== -->
<!-- TECHNICIAN TASK RECORD RULES -->
<!-- ================================================================== -->
<!-- Managers: full access to all tasks -->
<record id="rule_technician_task_manager" model="ir.rule">
<field name="name">Technician Task: Manager Full Access</field>
<field name="model_id" ref="model_fusion_technician_task"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('sales_team.group_sale_manager'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_unlink" eval="True"/>
</record>
<!-- Sales users: read/write all tasks, create tasks -->
<record id="rule_technician_task_sales_user" model="ir.rule">
<field name="name">Technician Task: Sales User Access</field>
<field name="model_id" ref="model_fusion_technician_task"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('sales_team.group_sale_salesman'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_unlink" eval="False"/>
</record>
<!-- Field Technicians (internal): own tasks only -->
<record id="rule_technician_task_technician" model="ir.rule">
<field name="name">Technician Task: Technician Own Tasks</field>
<field name="model_id" ref="model_fusion_technician_task"/>
<field name="domain_force">[('technician_id', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('group_field_technician'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="False"/>
<field name="perm_unlink" eval="False"/>
</record>
<!-- Portal technicians: own tasks only, read + limited write -->
<record id="rule_technician_task_portal" model="ir.rule">
<field name="name">Technician Task: Portal Technician Access</field>
<field name="model_id" ref="model_fusion_technician_task"/>
<field name="domain_force">[('technician_id', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_unlink" eval="False"/>
</record>
<!-- ================================================================== -->
<!-- PUSH SUBSCRIPTION RECORD RULES -->
<!-- ================================================================== -->
<!-- Users: own subscriptions only -->
<record id="rule_push_subscription_user" model="ir.rule">
<field name="name">Push Subscription: Own Only</field>
<field name="model_id" ref="model_fusion_push_subscription"/>
<field name="domain_force">[('user_id', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
</record>
<!-- Portal: own subscriptions only -->
<record id="rule_push_subscription_portal" model="ir.rule">
<field name="name">Push Subscription: Portal Own Only</field>
<field name="model_id" ref="model_fusion_push_subscription"/>
<field name="domain_force">[('user_id', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
</record>
</odoo>

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View File

@@ -0,0 +1,488 @@
// =====================================================================
// Fusion Task Map View - Sidebar + Google Maps
// Theme-aware: uses Odoo/Bootstrap variables for dark mode support
// =====================================================================
$sidebar-width: 340px;
$transition-speed: .25s;
.o_fusion_task_map_view {
height: 100%;
.o_content {
height: 100%;
display: flex;
flex-direction: column;
}
}
// ── Main wrapper: sidebar + map side by side ────────────────────────
.fc_map_wrapper {
display: flex;
flex-direction: row;
height: 100%;
min-height: 0;
overflow: hidden;
position: relative;
}
// ── Sidebar ─────────────────────────────────────────────────────────
.fc_sidebar {
width: $sidebar-width;
min-width: $sidebar-width;
max-width: $sidebar-width;
background: var(--o-view-background-color, $o-view-background-color);
border-right: 1px solid $border-color;
display: flex;
flex-direction: column;
transition: width $transition-speed ease, min-width $transition-speed ease,
max-width $transition-speed ease, opacity $transition-speed ease;
overflow: hidden;
&--collapsed {
width: 0;
min-width: 0;
max-width: 0;
opacity: 0;
border-right: none;
}
}
.fc_sidebar_header {
padding: 14px 16px 12px;
border-bottom: 1px solid $border-color;
flex-shrink: 0;
h6 {
font-size: 14px;
color: $headings-color;
}
}
.fc_sidebar_body {
flex: 1 1 auto;
overflow-y: auto;
overflow-x: hidden;
padding: 6px 0;
&::-webkit-scrollbar { width: 5px; }
&::-webkit-scrollbar-track { background: transparent; }
&::-webkit-scrollbar-thumb { background: $border-color; border-radius: 4px; }
}
.fc_sidebar_footer {
padding: 10px 16px;
border-top: 1px solid $border-color;
flex-shrink: 0;
}
.fc_sidebar_empty {
text-align: center;
padding: 40px 20px;
color: $text-muted;
}
// ── Day filter chips ────────────────────────────────────────────────
.fc_day_filters {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.fc_day_chip {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 10px;
font-size: 11px;
font-weight: 600;
border: 1px solid $border-color;
border-radius: 12px;
background: transparent;
color: $text-muted;
cursor: pointer;
transition: all .15s;
line-height: 18px;
&:hover {
border-color: rgba($primary, .3);
color: $body-color;
}
&--active {
color: #fff !important;
border-color: transparent !important;
}
&--all {
color: $body-color;
font-weight: 500;
&:hover { background: rgba($primary, .1); }
}
}
.fc_day_chip_count {
font-size: 10px;
opacity: .8;
}
.fc_group_hidden_tag {
font-size: 9px;
text-transform: uppercase;
letter-spacing: .5px;
color: $text-muted;
background: rgba($secondary, .1);
padding: 0 5px;
border-radius: 3px;
margin-left: 4px;
font-weight: 500;
}
// ── Technician filter chips ─────────────────────────────────────────
.fc_tech_filters {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.fc_tech_chip {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 3px 10px 3px 4px;
font-size: 11px;
font-weight: 600;
border: 1px solid $border-color;
border-radius: 14px;
background: transparent;
color: $text-muted;
cursor: pointer;
transition: all .15s;
line-height: 18px;
max-width: 100%;
overflow: hidden;
&:hover {
border-color: rgba($primary, .35);
color: $body-color;
background: rgba($primary, .06);
}
&--active {
background: $primary !important;
color: #fff !important;
border-color: $primary !important;
.fc_tech_chip_avatar {
background: rgba(#fff, .25);
color: #fff;
}
}
&--all {
padding: 3px 10px;
color: $body-color;
font-weight: 500;
&:hover { background: rgba($primary, .1); }
}
}
.fc_tech_chip_avatar {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
border-radius: 50%;
background: rgba($secondary, .15);
color: $body-color;
font-size: 9px;
font-weight: 700;
flex-shrink: 0;
}
.fc_tech_chip_name {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
// Collapsed toggle button (floating)
.fc_sidebar_toggle_btn {
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
z-index: 15;
background: var(--o-view-background-color, $o-view-background-color);
border: 1px solid $border-color;
border-left: none;
border-radius: 0 8px 8px 0;
padding: 12px 6px;
cursor: pointer;
box-shadow: 2px 0 6px rgba(0,0,0,.08);
color: $text-muted;
transition: background .15s;
&:hover {
background: $o-gray-100;
color: $body-color;
}
}
// ── Group headers ───────────────────────────────────────────────────
.fc_group_header {
display: flex;
align-items: center;
padding: 8px 16px;
cursor: pointer;
user-select: none;
font-weight: 600;
font-size: 12px;
color: $text-muted;
text-transform: uppercase;
letter-spacing: .5px;
background: rgba($secondary, .08);
border-bottom: 1px solid $border-color;
transition: background .15s;
&:hover {
background: rgba($secondary, .15);
}
.fa-caret-right,
.fa-caret-down {
width: 14px;
text-align: center;
font-size: 13px;
}
}
.fc_group_label {
flex: 1;
}
.fc_group_badge {
background: rgba($secondary, .2);
color: $body-color;
font-size: 10px;
font-weight: 700;
padding: 1px 7px;
border-radius: 10px;
min-width: 20px;
text-align: center;
}
// ── Task cards ──────────────────────────────────────────────────────
.fc_group_tasks {
padding: 4px 0;
}
.fc_task_card {
margin: 3px 10px;
padding: 10px 12px;
background: var(--o-view-background-color, $o-view-background-color);
border: 1px solid $border-color;
border-radius: 8px;
cursor: pointer;
transition: all .15s;
position: relative;
&:hover {
background: rgba($primary, .05);
border-color: rgba($primary, .2);
box-shadow: 0 1px 4px rgba(0,0,0,.06);
}
&--active {
background: rgba($primary, .1) !important;
border-color: rgba($primary, .35) !important;
box-shadow: 0 0 0 2px rgba($primary, .15);
}
}
.fc_task_card_top {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 6px;
}
.fc_task_num {
display: inline-block;
color: #fff;
font-size: 11px;
font-weight: 700;
padding: 1px 8px;
border-radius: 4px;
line-height: 18px;
}
.fc_task_status {
font-size: 11px;
font-weight: 600;
}
.fc_task_client {
font-size: 13px;
font-weight: 600;
color: $headings-color;
margin-bottom: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.fc_task_meta {
display: flex;
gap: 12px;
font-size: 11px;
color: $body-color;
margin-bottom: 3px;
.fa { opacity: .5; }
}
.fc_task_date {
font-size: 11px;
color: #6366f1;
font-weight: 600;
margin-bottom: 3px;
.fa { opacity: .5; }
}
.fc_task_detail {
font-size: 11px;
color: $body-color;
margin-bottom: 2px;
.fa { opacity: .5; }
}
.fc_task_address {
font-size: 10px;
color: $text-muted;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-top: 2px;
}
.fc_task_bottom_row {
display: flex;
align-items: center;
gap: 6px;
margin-top: 4px;
flex-wrap: wrap;
}
.fc_task_travel {
display: inline-flex;
align-items: center;
font-size: 10px;
color: $body-color;
background: rgba($secondary, .1);
padding: 1px 8px;
border-radius: 4px;
.fa { opacity: .5; }
}
.fc_task_source {
display: inline-flex;
align-items: center;
font-size: 10px;
color: #fff;
font-weight: 600;
padding: 1px 8px;
border-radius: 4px;
.fa { opacity: .8; }
}
.fc_task_edit_btn {
display: inline-flex;
align-items: center;
font-size: 10px;
font-weight: 600;
color: var(--btn-primary-color, #fff);
background: var(--btn-primary-bg, #{$primary});
padding: 2px 10px;
border-radius: 4px;
cursor: pointer;
margin-left: auto;
transition: all .15s;
&:hover {
opacity: .85;
filter: brightness(1.15);
}
}
// ── Map area ────────────────────────────────────────────────────────
.fc_map_area {
flex: 1 1 auto;
display: flex;
flex-direction: column;
min-width: 0;
position: relative;
}
.fc_map_legend_bar {
flex: 0 0 auto;
font-size: 12px;
min-height: 40px;
}
.fc_map_container {
flex: 1 1 auto;
position: relative;
min-height: 400px;
}
// ── Google Maps InfoWindow override ──────────────────────────────────
.gm-style-iw-d {
overflow: auto !important;
}
.gm-style .gm-style-iw-c {
padding: 0 !important;
border-radius: 10px !important;
overflow: hidden !important;
box-shadow: 0 4px 20px rgba(0,0,0,.15) !important;
}
.gm-style .gm-style-iw-tc {
display: none !important;
}
.gm-style .gm-ui-hover-effect {
display: none !important;
}
// ── Responsive ──────────────────────────────────────────────────────
@media (max-width: 768px) {
.fc_map_wrapper {
flex-direction: column;
}
.fc_sidebar {
width: 100% !important;
min-width: 100% !important;
max-width: 100% !important;
max-height: 40vh;
border-right: none;
border-bottom: 1px solid $border-color;
&--collapsed {
max-height: 0;
opacity: 0;
}
}
.fc_sidebar_toggle_btn {
top: auto;
bottom: 10px;
left: 50%;
transform: translateX(-50%);
border-radius: 8px;
border: 1px solid $border-color;
padding: 8px 16px;
}
.fc_map_area {
flex: 1;
min-height: 300px;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,255 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="fusion_tasks.FusionTaskMapView">
<div class="o_fusion_task_map_view">
<Layout display="display">
<t t-set-slot="control-panel-additional-actions">
<CogMenu/>
</t>
<t t-set-slot="layout-buttons">
<t t-call="{{ props.buttonTemplate }}"/>
</t>
<t t-set-slot="layout-actions">
<SearchBar toggler="searchBarToggler"/>
</t>
<t t-set-slot="control-panel-navigation-additional">
<t t-component="searchBarToggler.component" t-props="searchBarToggler.props"/>
</t>
<div class="fc_map_wrapper">
<!-- ========== SIDEBAR ========== -->
<div t-att-class="'fc_sidebar' + (state.sidebarOpen ? '' : ' fc_sidebar--collapsed')">
<!-- Sidebar header -->
<div class="fc_sidebar_header">
<div class="d-flex align-items-center justify-content-between">
<h6 class="mb-0 fw-bold">
<i class="fa fa-list-ul me-2"/>Deliveries
<span class="badge text-bg-primary ms-1" t-esc="state.taskCount"/>
</h6>
<button class="btn btn-sm btn-link text-muted p-0" t-on-click="toggleSidebar"
title="Toggle sidebar">
<i t-att-class="'fa ' + (state.sidebarOpen ? 'fa-chevron-left' : 'fa-chevron-right')"/>
</button>
</div>
<!-- New task button -->
<button class="btn btn-primary btn-sm w-100 mt-2" t-on-click="createNewTask">
<i class="fa fa-plus me-1"/>New Delivery Task
</button>
<!-- Day filter chips -->
<div class="fc_day_filters mt-2">
<t t-foreach="state.groups" t-as="group" t-key="group.key + '_filter'">
<button t-att-class="'fc_day_chip' + (isGroupVisible(group.key) ? ' fc_day_chip--active' : '')"
t-att-style="isGroupVisible(group.key) ? 'background:' + group.dayColor + ';color:#fff;border-color:' + group.dayColor : ''"
t-on-click="() => this.toggleDayFilter(group.key)">
<t t-esc="group.label"/>
<span class="fc_day_chip_count" t-esc="group.count"/>
</button>
</t>
<button class="fc_day_chip fc_day_chip--all" t-on-click="showAllDays"
title="Show all">All</button>
</div>
<!-- Technician filter -->
<t t-if="state.allTechnicians.length > 1">
<div class="fc_tech_filters mt-2">
<t t-foreach="state.allTechnicians" t-as="tech" t-key="tech.id">
<button t-att-class="'fc_tech_chip' + (isTechVisible(tech.id) ? ' fc_tech_chip--active' : '')"
t-on-click="() => this.toggleTechFilter(tech.id)"
t-att-title="tech.name">
<span class="fc_tech_chip_avatar" t-esc="tech.initials"/>
<span class="fc_tech_chip_name" t-esc="tech.name"/>
</button>
</t>
<button class="fc_tech_chip fc_tech_chip--all" t-on-click="showAllTechs"
title="Show all technicians">All</button>
</div>
</t>
</div>
<!-- Sidebar body: grouped task list -->
<div class="fc_sidebar_body">
<t t-foreach="state.groups" t-as="group" t-key="group.key">
<!-- Group header (collapsible) with day color -->
<div class="fc_group_header" t-on-click="() => this.toggleGroup(group.key)">
<i t-att-class="'fa me-1 ' + (isGroupCollapsed(group.key) ? 'fa-caret-right' : 'fa-caret-down')"/>
<i class="fa fa-circle me-1" style="font-size:8px;"
t-att-style="'color:' + group.dayColor"/>
<span class="fc_group_label" t-esc="group.label"/>
<span t-if="!isGroupVisible(group.key)" class="fc_group_hidden_tag">hidden</span>
<span class="fc_group_badge" t-esc="group.count"/>
</div>
<!-- Group tasks -->
<div t-if="!isGroupCollapsed(group.key)" class="fc_group_tasks">
<t t-foreach="group.tasks" t-as="task" t-key="task.id">
<div t-att-class="'fc_task_card' + (state.activeTaskId === task.id ? ' fc_task_card--active' : '')"
t-on-click="() => this.focusTask(task.id)">
<!-- Card top row: number + status -->
<div class="fc_task_card_top">
<span class="fc_task_num" t-att-style="'background:' + task._dayColor">
<t t-esc="'#' + task._scheduleNum"/>
</span>
<span class="fc_task_status" t-att-style="'color:' + task._statusColor">
<i t-att-class="'fa ' + task._statusIcon" style="margin-right:3px;"/>
<t t-esc="task._statusLabel"/>
</span>
</div>
<!-- Client name -->
<div class="fc_task_client" t-esc="task._clientName"/>
<!-- Type + time -->
<div class="fc_task_meta">
<span><i class="fa fa-tag me-1"/><t t-esc="task._typeLbl"/></span>
<span><i class="fa fa-clock-o me-1"/><t t-esc="task._timeRange"/></span>
</div>
<!-- Date -->
<div class="fc_task_date">
<i class="fa fa-calendar me-1"/><t t-esc="task._dateLabel"/>
</div>
<!-- Technician + address -->
<div class="fc_task_detail">
<span><i class="fa fa-user me-1"/><t t-esc="task._techName"/></span>
</div>
<div t-if="task.address_display" class="fc_task_address">
<i class="fa fa-map-marker me-1"/>
<t t-esc="task.address_display"/>
</div>
<!-- Travel + source -->
<div class="fc_task_bottom_row">
<span t-if="task.travel_time_minutes" class="fc_task_travel">
<i class="fa fa-car me-1"/>
<t t-esc="task.travel_time_minutes"/> min travel
</span>
<span t-if="task._sourceLabel" class="fc_task_source"
t-att-style="'background:' + task._sourceColor">
<i class="fa fa-building-o me-1"/>
<t t-esc="task._sourceLabel"/>
</span>
<span class="fc_task_edit_btn"
t-on-click.stop="() => this.openTask(task.id)"
title="Edit task">
<i class="fa fa-pencil me-1"/>Edit
</span>
</div>
</div>
</t>
</div>
</t>
<!-- Empty state -->
<div t-if="state.groups.length === 0 and !state.loading" class="fc_sidebar_empty">
<i class="fa fa-inbox fa-2x text-muted d-block mb-2"/>
<span class="text-muted">No tasks found</span>
</div>
</div>
<!-- Sidebar footer: technician count -->
<div class="fc_sidebar_footer">
<div class="d-flex align-items-center gap-2">
<svg width="14" height="14" viewBox="0 0 48 48">
<rect x="2" y="2" width="44" height="44" rx="12" ry="12" fill="#1d4ed8" stroke="#fff" stroke-width="3"/>
<text x="24" y="30" text-anchor="middle" fill="#fff" font-size="17" font-family="Arial,sans-serif" font-weight="bold">T</text>
</svg>
<small class="text-muted">
<t t-esc="state.techCount"/> technician(s) online
</small>
</div>
</div>
</div>
<!-- Collapsed sidebar toggle -->
<button t-if="!state.sidebarOpen"
class="fc_sidebar_toggle_btn" t-on-click="toggleSidebar"
title="Open sidebar">
<i class="fa fa-chevron-right"/>
</button>
<!-- ========== MAP AREA ========== -->
<div class="fc_map_area">
<!-- Legend bar -->
<div class="fc_map_legend_bar d-flex align-items-center gap-3 px-3 py-2 border-bottom bg-view flex-wrap">
<button class="btn btn-sm d-flex align-items-center gap-1"
t-att-class="state.showTasks ? 'btn-primary' : 'btn-outline-secondary'"
t-on-click="toggleTasks">
<i class="fa fa-map-marker"/>Tasks <t t-esc="state.taskCount"/>
</button>
<button class="btn btn-sm d-flex align-items-center gap-1"
t-att-class="state.showTechnicians ? 'btn-primary' : 'btn-outline-secondary'"
t-on-click="toggleTechnicians">
<i class="fa fa-user"/>Techs <t t-esc="state.techCount"/>
</button>
<span class="border-start mx-1" style="height:20px;"/>
<span class="text-muted fw-bold" style="font-size:11px;">Pins:</span>
<span style="font-size:11px;"><i class="fa fa-map-marker me-1" style="color:#f59e0b;"/>Pending</span>
<span style="font-size:11px;"><i class="fa fa-map-marker me-1" style="color:#ef4444;"/>Today</span>
<span style="font-size:11px;"><i class="fa fa-map-marker me-1" style="color:#3b82f6;"/>Tomorrow</span>
<span style="font-size:11px;"><i class="fa fa-map-marker me-1" style="color:#10b981;"/>This Week</span>
<span style="font-size:11px;"><i class="fa fa-map-marker me-1" style="color:#a855f7;"/>Upcoming</span>
<span style="font-size:11px;"><i class="fa fa-map-marker me-1" style="color:#9ca3af;"/>Yesterday</span>
<span class="flex-grow-1"/>
<button class="btn btn-sm d-flex align-items-center gap-1"
t-att-class="state.showRoute ? 'btn-info' : 'btn-outline-secondary'"
t-on-click="toggleRoute" title="Toggle route animation">
<i class="fa fa-road"/>Route
</button>
<button class="btn btn-sm d-flex align-items-center gap-1"
t-att-class="state.showTraffic ? 'btn-warning' : 'btn-outline-secondary'"
t-on-click="toggleTraffic" title="Toggle traffic layer">
<i class="fa fa-car"/>Traffic
</button>
<button class="btn btn-outline-secondary btn-sm" t-on-click="onRefresh" title="Refresh">
<i class="fa fa-refresh" t-att-class="{'fa-spin': state.loading}"/>
</button>
</div>
<!-- Map container -->
<div class="fc_map_container">
<div t-ref="mapContainer" style="position:absolute;top:0;left:0;right:0;bottom:0;"/>
<!-- Loading -->
<div t-if="state.loading"
class="position-absolute top-0 start-0 w-100 h-100 d-flex justify-content-center align-items-center"
style="z-index:10;background:rgba(255,255,255,.92);">
<div class="text-center">
<i class="fa fa-spinner fa-spin fa-3x text-primary mb-3 d-block"/>
<span class="text-muted">Loading Google Maps...</span>
</div>
</div>
<!-- Error -->
<div t-if="state.error"
class="position-absolute top-0 start-0 w-100 h-100 d-flex justify-content-center align-items-center"
style="z-index:10;background:rgba(255,255,255,.92);">
<div class="alert alert-danger m-4" role="alert">
<i class="fa fa-exclamation-triangle me-2"/><t t-esc="state.error"/>
</div>
</div>
<!-- Empty -->
<div t-if="!state.loading and !state.error and state.taskCount === 0 and state.techCount === 0"
class="position-absolute top-50 start-50 translate-middle text-center" style="z-index:5;">
<div class="bg-white rounded-3 shadow p-4">
<i class="fa fa-map-marker fa-3x text-muted mb-3 d-block"/>
<h5>No locations to show</h5>
<p class="text-muted mb-0">Try adjusting the filters or date range.</p>
</div>
</div>
</div>
</div>
</div>
</Layout>
</div>
</t>
<t t-name="fusion_tasks.FusionTaskMapView.Buttons"/>
</templates>

View File

@@ -0,0 +1,156 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Add Fusion Tasks Settings as a new app block -->
<record id="res_config_settings_view_form_fusion_tasks" model="ir.ui.view">
<field name="name">res.config.settings.view.form.fusion.tasks</field>
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="base.res_config_settings_view_form"/>
<field name="arch" type="xml">
<xpath expr="//form" position="inside">
<app data-string="Fusion Tasks" string="Fusion Tasks" name="fusion_tasks"
groups="fusion_tasks.group_field_technician">
<h2>Technician Management</h2>
<div class="row mt-4 o_settings_container">
<!-- Google Maps API Key -->
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_right_pane">
<span class="o_form_label">Google Maps API</span>
<div class="text-muted">
API key for Google Maps Places autocomplete in address fields and Distance Matrix travel calculations.
</div>
<div class="mt-2">
<field name="fc_google_maps_api_key" placeholder="Enter your Google Maps API Key" password="True"/>
</div>
<div class="alert alert-info mt-2" role="alert">
<i class="fa fa-info-circle"/> Enable the "Places API" and "Distance Matrix API" in your Google Cloud Console.
</div>
</div>
</div>
<!-- Google Business Review URL -->
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_right_pane">
<span class="o_form_label">Google Business Review URL</span>
<div class="text-muted">
Link to your Google Business Profile review page.
Sent to clients after service completion (when "Request Google Review" is enabled on the task).
</div>
<div class="mt-2">
<field name="fc_google_review_url" placeholder="https://g.page/r/your-business/review"/>
</div>
</div>
</div>
<!-- Store Hours -->
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_right_pane">
<span class="o_form_label">Store / Scheduling Hours</span>
<div class="text-muted">
Operating hours for technician task scheduling. Tasks can only be booked
within these hours. Calendar view is also restricted to this range.
</div>
<div class="mt-2 d-flex align-items-center gap-2">
<field name="fc_store_open_hour" widget="float_time" style="max-width: 100px;"/>
<span>to</span>
<field name="fc_store_close_hour" widget="float_time" style="max-width: 100px;"/>
</div>
</div>
</div>
<!-- Distance Matrix Toggle -->
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_left_pane">
<field name="fc_google_distance_matrix_enabled"/>
</div>
<div class="o_setting_right_pane">
<label for="fc_google_distance_matrix_enabled"/>
<div class="text-muted">
Calculate travel time between technician tasks using Google Distance Matrix API.
Requires Google Maps API key above with Distance Matrix API enabled.
</div>
</div>
</div>
<!-- Start Address (Company Default / Fallback) -->
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_right_pane">
<span class="o_form_label">Default HQ / Fallback Address</span>
<div class="text-muted">
Company default start location used when a technician has no personal
start address set. Each technician can set their own start location
in their user profile or from the portal.
</div>
<div class="mt-2">
<field name="fc_technician_start_address" placeholder="e.g. 123 Main St, Brampton, ON"/>
</div>
</div>
</div>
<!-- Location History Retention -->
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_right_pane">
<span class="o_form_label">Location History Retention</span>
<div class="text-muted">
How many days to keep technician GPS location history before automatic cleanup.
</div>
<div class="mt-2 d-flex align-items-center gap-2">
<field name="fc_location_retention_days" placeholder="30" style="max-width: 80px;"/>
<span class="text-muted">days</span>
</div>
<div class="text-muted small mt-1">
Leave empty = 30 days. Enter 0 = delete at end of each day. 1+ = keep that many days.
</div>
</div>
</div>
</div>
<h2>Push Notifications</h2>
<div class="row mt-4 o_settings_container">
<!-- Push Enable -->
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_left_pane">
<field name="fc_push_enabled"/>
</div>
<div class="o_setting_right_pane">
<label for="fc_push_enabled"/>
<div class="text-muted">
Send web push notifications to technicians about upcoming tasks.
Requires VAPID keys (auto-generated on first save if empty).
</div>
</div>
</div>
<!-- Advance Minutes -->
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_right_pane">
<span class="o_form_label">Notification Advance Time</span>
<div class="text-muted">
Send push notification this many minutes before a scheduled task.
</div>
<div class="mt-2">
<field name="fc_push_advance_minutes"/> minutes
</div>
</div>
</div>
<!-- VAPID Public Key -->
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_right_pane">
<span class="o_form_label">VAPID Public Key</span>
<div class="mt-2">
<field name="fc_vapid_public_key" placeholder="Auto-generated"/>
</div>
</div>
</div>
<!-- VAPID Private Key -->
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_right_pane">
<span class="o_form_label">VAPID Private Key</span>
<div class="mt-2">
<field name="fc_vapid_private_key" password="True" placeholder="Auto-generated"/>
</div>
</div>
</div>
</div>
</app>
</xpath>
</field>
</record>
</odoo>

View File

@@ -73,8 +73,8 @@
<menuitem id="menu_task_sync_config"
name="Task Sync"
parent="fusion_claims.menu_technician_schedule"
parent="menu_technician_config"
action="action_task_sync_config"
sequence="99"/>
sequence="10"/>
</odoo>

View File

@@ -91,38 +91,12 @@
</record>
<!-- ================================================================== -->
<!-- MAP VIEW (QWeb HTML with Google Maps) -->
<!-- ================================================================== -->
<record id="action_technician_location_map" model="ir.actions.act_url">
<field name="name">Technician Map</field>
<field name="url">/my/technician/admin/map</field>
<field name="target">self</field>
</record>
<!-- ================================================================== -->
<!-- MENU ITEMS (under Technician Management) -->
<!-- MENU ITEMS (under Configuration) -->
<!-- ================================================================== -->
<menuitem id="menu_technician_locations"
name="Location History"
parent="menu_technician_management"
parent="menu_technician_config"
action="action_technician_locations"
sequence="40"/>
<menuitem id="menu_technician_map"
name="Live Map"
parent="menu_technician_management"
action="action_technician_location_map"
sequence="45"/>
<!-- CRON: Cleanup old location records (runs daily) -->
<record id="ir_cron_cleanup_technician_locations" model="ir.cron">
<field name="name">Cleanup Old Technician Locations</field>
<field name="model_id" ref="model_fusion_technician_location"/>
<field name="state">code</field>
<field name="code">model._cron_cleanup_old_locations()</field>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="active">True</field>
</record>
sequence="20"/>
</odoo>

View File

@@ -0,0 +1,507 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ================================================================== -->
<!-- SEQUENCE -->
<!-- ================================================================== -->
<record id="seq_technician_task" model="ir.sequence">
<field name="name">Technician Task</field>
<field name="code">fusion.technician.task</field>
<field name="prefix">TASK-</field>
<field name="padding">5</field>
<field name="number_increment">1</field>
</record>
<!-- ================================================================== -->
<!-- RES.USERS FORM EXTENSION - Field Staff toggle -->
<!-- ================================================================== -->
<record id="view_users_form_field_staff" model="ir.ui.view">
<field name="name">res.users.form.field.staff</field>
<field name="model">res.users</field>
<field name="inherit_id" ref="base.view_users_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='login']" position="after">
<field name="x_fc_is_field_staff"/>
<field name="x_fc_start_address"
invisible="not x_fc_is_field_staff"
placeholder="e.g. 123 Main St, Brampton, ON"/>
<field name="x_fc_tech_sync_id"
invisible="not x_fc_is_field_staff"
placeholder="e.g. gordy, manpreet"/>
</xpath>
</field>
</record>
<!-- ================================================================== -->
<!-- SEARCH VIEW -->
<!-- ================================================================== -->
<record id="view_technician_task_search" model="ir.ui.view">
<field name="name">fusion.technician.task.search</field>
<field name="model">fusion.technician.task</field>
<field name="arch" type="xml">
<search string="Search Tasks">
<field name="technician_id" string="Technician"/>
<field name="partner_id" string="Client"/>
<field name="name" string="Task"/>
<separator/>
<!-- Quick Filters -->
<filter string="Today" name="filter_today"
domain="[('scheduled_date', '=', context_today().strftime('%Y-%m-%d'))]"/>
<filter string="Tomorrow" name="filter_tomorrow"
domain="[('scheduled_date', '=', (context_today() + datetime.timedelta(days=1)).strftime('%Y-%m-%d'))]"/>
<filter string="This Week" name="filter_this_week"
domain="[('scheduled_date', '>=', (context_today() - datetime.timedelta(days=context_today().weekday())).strftime('%Y-%m-%d')),
('scheduled_date', '&lt;=', (context_today() + datetime.timedelta(days=6-context_today().weekday())).strftime('%Y-%m-%d'))]"/>
<separator/>
<filter string="Pending" name="filter_pending" domain="[('status', '=', 'pending')]"/>
<filter string="Scheduled" name="filter_scheduled" domain="[('status', '=', 'scheduled')]"/>
<filter string="En Route" name="filter_en_route" domain="[('status', '=', 'en_route')]"/>
<filter string="In Progress" name="filter_in_progress" domain="[('status', '=', 'in_progress')]"/>
<filter string="Completed" name="filter_completed" domain="[('status', '=', 'completed')]"/>
<filter string="Active" name="filter_active" domain="[('status', 'not in', ['cancelled', 'completed'])]"/>
<separator/>
<filter string="My Tasks" name="filter_my_tasks"
domain="['|', ('technician_id', '=', uid), ('additional_technician_ids', 'in', [uid])]"/>
<filter string="Deliveries" name="filter_deliveries" domain="[('task_type', '=', 'delivery')]"/>
<filter string="Repairs" name="filter_repairs" domain="[('task_type', '=', 'repair')]"/>
<filter string="POD Required" name="filter_pod" domain="[('pod_required', '=', True)]"/>
<separator/>
<filter string="Local Tasks" name="filter_local"
domain="[('x_fc_sync_source', '=', False)]"/>
<filter string="Synced Tasks" name="filter_synced"
domain="[('x_fc_sync_source', '!=', False)]"/>
<separator/>
<!-- Group By -->
<filter string="Technician" name="group_technician" context="{'group_by': 'technician_id'}"/>
<filter string="Date" name="group_date" context="{'group_by': 'scheduled_date'}"/>
<filter string="Status" name="group_status" context="{'group_by': 'status'}"/>
<filter string="Task Type" name="group_type" context="{'group_by': 'task_type'}"/>
<filter string="Client" name="group_client" context="{'group_by': 'partner_id'}"/>
</search>
</field>
</record>
<!-- ================================================================== -->
<!-- FORM VIEW -->
<!-- ================================================================== -->
<record id="view_technician_task_form" model="ir.ui.view">
<field name="name">fusion.technician.task.form</field>
<field name="model">fusion.technician.task</field>
<field name="arch" type="xml">
<form string="Technician Task">
<field name="x_fc_is_shadow" invisible="1"/>
<field name="x_fc_sync_source" invisible="1"/>
<header>
<button name="action_start_en_route" type="object" string="En Route"
class="btn-primary" invisible="status != 'scheduled' or x_fc_is_shadow"/>
<button name="action_start_task" type="object" string="Start Task"
class="btn-primary" invisible="status not in ('scheduled', 'en_route') or x_fc_is_shadow"/>
<button name="action_complete_task" type="object" string="Complete"
class="btn-success" invisible="status not in ('in_progress', 'en_route') or x_fc_is_shadow"/>
<button name="action_reschedule" type="object" string="Reschedule"
class="btn-warning" invisible="status not in ('scheduled', 'en_route') or x_fc_is_shadow"/>
<button name="action_cancel_task" type="object" string="Cancel"
class="btn-danger" invisible="status in ('completed', 'cancelled') or x_fc_is_shadow"
confirm="Are you sure you want to cancel this task?"/>
<button name="action_reset_to_scheduled" type="object" string="Reset to Scheduled"
invisible="status not in ('cancelled', 'rescheduled') or x_fc_is_shadow"/>
<button string="Calculate Travel"
class="btn-secondary o_fc_calculate_travel" icon="fa-car"
invisible="x_fc_is_shadow"/>
<field name="status" widget="statusbar"
statusbar_visible="pending,scheduled,en_route,in_progress,completed"/>
</header>
<sheet>
<!-- Shadow task banner -->
<div class="alert alert-info text-center" role="alert"
invisible="not x_fc_is_shadow">
<strong><i class="fa fa-link"/> This task is synced from
<field name="x_fc_sync_source" readonly="1" nolabel="1" class="d-inline"/>
— view only.</strong>
</div>
<div class="oe_button_box" name="button_box">
</div>
<widget name="web_ribbon" title="Completed" bg_color="text-bg-success"
invisible="status != 'completed'"/>
<widget name="web_ribbon" title="Cancelled" bg_color="text-bg-danger"
invisible="status != 'cancelled'"/>
<widget name="web_ribbon" title="Synced" bg_color="text-bg-info"
invisible="not x_fc_is_shadow or status in ('completed', 'cancelled')"/>
<div class="oe_title">
<h1>
<field name="name" readonly="1"/>
</h1>
</div>
<!-- Schedule Info Banner -->
<field name="schedule_info_html" nolabel="1" colspan="2"
invisible="not technician_id or not scheduled_date"/>
<!-- Previous Task / Travel Warning Banner -->
<field name="prev_task_summary_html" nolabel="1" colspan="2"
invisible="not technician_id or not scheduled_date"/>
<!-- Hidden fields for calendar sync and legacy -->
<field name="datetime_start" invisible="1"/>
<field name="datetime_end" invisible="1"/>
<field name="time_start_12h" invisible="1"/>
<field name="time_end_12h" invisible="1"/>
<group>
<group string="Assignment">
<field name="technician_id"
domain="[('x_fc_is_field_staff', '=', True)]"/>
<field name="additional_technician_ids"
widget="many2many_tags_avatar"
domain="[('x_fc_is_field_staff', '=', True), ('id', '!=', technician_id)]"
options="{'color_field': 'color'}"/>
<field name="task_type"/>
<field name="priority" widget="priority"/>
</group>
<group string="Schedule">
<field name="scheduled_date"/>
<field name="time_start" widget="float_time"
string="Start Time"/>
<field name="duration_hours" widget="float_time"
string="Duration"/>
<field name="time_end" widget="float_time"
string="End Time" readonly="1"
force_save="1"/>
</group>
</group>
<group>
<group string="Client">
<field name="partner_id"/>
<field name="partner_phone" widget="phone"/>
</group>
<group string="Location">
<field name="is_in_store"/>
<field name="address_partner_id" invisible="is_in_store"/>
<field name="address_street" readonly="is_in_store"/>
<field name="address_street2" string="Unit/Suite #" invisible="is_in_store"/>
<field name="address_buzz_code" invisible="is_in_store"/>
<field name="address_city" invisible="1"/>
<field name="address_state_id" invisible="1"/>
<field name="address_zip" invisible="1"/>
<field name="address_lat" invisible="1"/>
<field name="address_lng" invisible="1"/>
</group>
</group>
<group>
<group string="Travel (Auto-Calculated)">
<field name="travel_time_minutes" readonly="1"/>
<field name="travel_distance_km" readonly="1"/>
<field name="travel_origin" readonly="1"/>
<field name="previous_task_id" readonly="1"/>
</group>
<group string="Options">
<field name="pod_required"/>
<field name="x_fc_send_client_updates"/>
<field name="x_fc_ask_google_review"/>
<field name="active" invisible="1"/>
</group>
</group>
<notebook>
<page string="Description" name="description">
<group>
<field name="description" placeholder="What needs to be done..."/>
</group>
<group>
<field name="equipment_needed" placeholder="Tools, parts, materials..."/>
</group>
</page>
<page string="Completion" name="completion">
<group>
<field name="completion_datetime"/>
<field name="completion_notes"/>
</group>
<group>
<field name="voice_note_transcription"/>
</group>
</page>
</notebook>
</sheet>
<chatter/>
</form>
</field>
</record>
<!-- ================================================================== -->
<!-- LIST VIEW -->
<!-- ================================================================== -->
<record id="view_technician_task_list" model="ir.ui.view">
<field name="name">fusion.technician.task.list</field>
<field name="model">fusion.technician.task</field>
<field name="arch" type="xml">
<list string="Technician Tasks" decoration-success="status == 'completed'"
decoration-warning="status == 'in_progress'"
decoration-info="status == 'en_route'"
decoration-danger="status == 'cancelled'"
decoration-muted="status == 'rescheduled'"
default_order="scheduled_date, sequence, time_start">
<field name="name"/>
<field name="technician_id" widget="many2one_avatar_user"/>
<field name="additional_technician_ids" widget="many2many_tags_avatar"
optional="show" string="+ Techs"/>
<field name="task_type" decoration-bf="1"/>
<field name="scheduled_date"/>
<field name="time_start_display" string="Start"/>
<field name="time_end_display" string="End"/>
<field name="partner_id"/>
<field name="address_city"/>
<field name="travel_time_minutes" string="Travel (min)" optional="show"/>
<field name="status" widget="badge"
decoration-success="status == 'completed'"
decoration-warning="status == 'in_progress'"
decoration-info="status in ('scheduled', 'en_route')"
decoration-danger="status == 'cancelled'"/>
<field name="priority" widget="priority" optional="hide"/>
<field name="pod_required" optional="hide"/>
<field name="x_fc_source_label" string="Source" optional="show"
widget="badge" decoration-info="x_fc_is_shadow"
decoration-success="not x_fc_is_shadow"/>
</list>
</field>
</record>
<!-- ================================================================== -->
<!-- KANBAN VIEW -->
<!-- ================================================================== -->
<record id="view_technician_task_kanban" model="ir.ui.view">
<field name="name">fusion.technician.task.kanban</field>
<field name="model">fusion.technician.task</field>
<field name="arch" type="xml">
<kanban default_group_by="status" class="o_kanban_small_column"
records_draggable="1" group_create="0">
<field name="color"/>
<field name="priority"/>
<field name="technician_id"/>
<field name="additional_technician_ids"/>
<field name="additional_tech_count"/>
<field name="partner_id"/>
<field name="task_type"/>
<field name="scheduled_date"/>
<field name="time_start_display"/>
<field name="address_city"/>
<field name="travel_time_minutes"/>
<field name="status"/>
<field name="x_fc_is_shadow"/>
<field name="x_fc_sync_client_name"/>
<templates>
<t t-name="card">
<div t-attf-class="oe_kanban_color_#{record.color.raw_value} oe_kanban_card oe_kanban_global_click">
<div class="oe_kanban_content">
<div class="o_kanban_record_top mb-1">
<div class="o_kanban_record_headings">
<strong class="o_kanban_record_title">
<field name="name"/>
</strong>
</div>
<field name="priority" widget="priority"/>
</div>
<div class="mb-1">
<span class="badge bg-primary me-1"><field name="task_type"/></span>
<span class="text-muted"><field name="scheduled_date"/> - <field name="time_start_display"/></span>
</div>
<div class="mb-1">
<i class="fa fa-user me-1"/>
<t t-if="record.x_fc_is_shadow.raw_value">
<span t-out="record.x_fc_sync_client_name.value"/>
</t>
<t t-else="">
<field name="partner_id"/>
</t>
</div>
<div class="text-muted small" t-if="record.address_city.raw_value">
<i class="fa fa-map-marker me-1"/><field name="address_city"/>
<t t-if="record.travel_time_minutes.raw_value">
<span class="ms-2"><i class="fa fa-car me-1"/><field name="travel_time_minutes"/> min</span>
</t>
</div>
<div t-if="record.additional_tech_count.raw_value > 0" class="text-muted small mb-1">
<i class="fa fa-users me-1"/>
<span>+<field name="additional_tech_count"/> technician(s)</span>
</div>
<div class="o_kanban_record_bottom mt-2">
<div class="oe_kanban_bottom_left">
<field name="activity_ids" widget="kanban_activity"/>
</div>
<div class="oe_kanban_bottom_right">
<field name="technician_id" widget="many2one_avatar_user"/>
</div>
</div>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<!-- ================================================================== -->
<!-- CALENDAR VIEW -->
<!-- ================================================================== -->
<record id="view_technician_task_calendar" model="ir.ui.view">
<field name="name">fusion.technician.task.calendar</field>
<field name="model">fusion.technician.task</field>
<field name="arch" type="xml">
<calendar string="Technician Schedule"
date_start="datetime_start" date_stop="datetime_end"
color="technician_id" mode="week" event_open_popup="1"
quick_create="0">
<!-- Displayed on the calendar card -->
<field name="partner_id"/>
<field name="x_fc_sync_client_name"/>
<field name="task_type"/>
<field name="time_start_display" string="Start"/>
<field name="time_end_display" string="End"/>
<!-- Popover (hover/click) details -->
<field name="name"/>
<field name="technician_id" avatar_field="image_128"/>
<field name="address_display" string="Address"/>
<field name="travel_time_minutes" string="Travel (min)"/>
<field name="status"/>
<field name="duration_hours" widget="float_time" string="Duration"/>
</calendar>
</field>
</record>
<!-- ================================================================== -->
<!-- MAP VIEW (Enterprise web_map) -->
<!-- ================================================================== -->
<record id="view_technician_task_map" model="ir.ui.view">
<field name="name">fusion.technician.task.map</field>
<field name="model">fusion.technician.task</field>
<field name="arch" type="xml">
<map res_partner="address_partner_id" default_order="time_start"
routing="1" js_class="fusion_task_map">
<field name="partner_id" string="Client"/>
<field name="task_type" string="Type"/>
<field name="technician_id" string="Technician"/>
<field name="time_start_display" string="Start"/>
<field name="time_end_display" string="End"/>
<field name="status" string="Status"/>
<field name="travel_time_minutes" string="Travel (min)"/>
</map>
</field>
</record>
<!-- ================================================================== -->
<!-- ACTIONS -->
<!-- ================================================================== -->
<!-- Main Tasks Action (List/Kanban) -->
<record id="action_technician_tasks" model="ir.actions.act_window">
<field name="name">Technician Tasks</field>
<field name="res_model">fusion.technician.task</field>
<field name="view_mode">list,kanban,form,calendar,map</field>
<field name="search_view_id" ref="view_technician_task_search"/>
<field name="context">{'search_default_filter_active': 1}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create your first technician task
</p>
<p>Schedule deliveries, repairs, and other field tasks for your technicians.</p>
</field>
</record>
<!-- Schedule Action (Map default) -->
<record id="action_technician_schedule" model="ir.actions.act_window">
<field name="name">Schedule</field>
<field name="res_model">fusion.technician.task</field>
<field name="view_mode">map,calendar,list,kanban,form</field>
<field name="search_view_id" ref="view_technician_task_search"/>
<field name="context">{'search_default_filter_active': 1}</field>
</record>
<!-- Map View Action (for app landing page) -->
<record id="action_technician_map_view" model="ir.actions.act_window">
<field name="name">Task Map</field>
<field name="res_model">fusion.technician.task</field>
<field name="view_mode">map,list,kanban,form,calendar</field>
<field name="search_view_id" ref="view_technician_task_search"/>
<field name="context">{'search_default_filter_active': 1}</field>
</record>
<!-- Today's Tasks Action -->
<record id="action_technician_tasks_today" model="ir.actions.act_window">
<field name="name">Today's Tasks</field>
<field name="res_model">fusion.technician.task</field>
<field name="view_mode">kanban,list,form,map</field>
<field name="search_view_id" ref="view_technician_task_search"/>
<field name="context">{'search_default_filter_today': 1, 'search_default_filter_active': 1}</field>
</record>
<!-- My Tasks Action -->
<record id="action_technician_my_tasks" model="ir.actions.act_window">
<field name="name">My Tasks</field>
<field name="res_model">fusion.technician.task</field>
<field name="view_mode">list,kanban,form,calendar,map</field>
<field name="search_view_id" ref="view_technician_task_search"/>
<field name="context">{'search_default_filter_my_tasks': 1, 'search_default_filter_active': 1}</field>
</record>
<!-- Pending Tasks Action -->
<record id="action_technician_tasks_pending" model="ir.actions.act_window">
<field name="name">Pending Tasks</field>
<field name="res_model">fusion.technician.task</field>
<field name="view_mode">list,kanban,form</field>
<field name="search_view_id" ref="view_technician_task_search"/>
<field name="context">{'search_default_filter_pending': 1}</field>
</record>
<!-- Calendar Action -->
<record id="action_technician_calendar" model="ir.actions.act_window">
<field name="name">Task Calendar</field>
<field name="res_model">fusion.technician.task</field>
<field name="view_mode">calendar,list,kanban,form,map</field>
<field name="search_view_id" ref="view_technician_task_search"/>
<field name="context">{'search_default_filter_active': 1}</field>
</record>
<!-- ================================================================== -->
<!-- MENU ITEMS - Standalone Field Service App -->
<!-- ================================================================== -->
<!-- Root app menu -->
<menuitem id="menu_field_service_root"
name="Field Service"
web_icon="fusion_tasks,static/description/icon.png"
groups="fusion_tasks.group_field_technician"
sequence="45"/>
<!-- Map View - first item = default landing view -->
<menuitem id="menu_technician_map"
name="Map View"
parent="menu_field_service_root"
action="action_technician_map_view"
sequence="5"
groups="fusion_tasks.group_field_technician"/>
<!-- Tasks -->
<menuitem id="menu_technician_tasks"
name="Tasks"
parent="menu_field_service_root"
action="action_technician_tasks"
sequence="10"
groups="fusion_tasks.group_field_technician"/>
<!-- Calendar -->
<menuitem id="menu_technician_calendar"
name="Calendar"
parent="menu_field_service_root"
action="action_technician_calendar"
sequence="30"
groups="fusion_tasks.group_field_technician"/>
<!-- Task Sync (submenu) -->
<menuitem id="menu_technician_config"
name="Configuration"
parent="menu_field_service_root"
sequence="90"
groups="fusion_tasks.group_field_technician"/>
</odoo>

BIN
Obsolete Files/.DS_Store vendored Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More