Files
Odoo-Modules/fusion_plating/docs/superpowers/specs/2026-05-18-phase-a-shipping-carrier-foundation-design.md
gsinghpal 091f98e1f9 changes
2026-05-18 22:33:23 -04:00

9.7 KiB
Raw Blame History

Phase A — Shipping Carrier Foundation

Date: 2026-05-18 Status: Approved for implementation Author: Brainstorming session (gsinghpal) Project: Full shipping integration (Phases AF). This spec covers Phase A only — the field-level foundation linking plating records to fusion_shipping's existing shipment infrastructure.

Goal

Replace the free-text carrier_name on fp.receiving with a proper M2O to delivery.carrier, and link both fp.receiving and fp.delivery to the fusion.shipment model that already exists in fusion_shipping. After Phase A, the receiver can pick a carrier from a 15-option dropdown and create a draft outbound shipment record — wiring is in place for Phase B (manual label entry) and Phase E (auto-label generation at receiving time).

Out of scope

  • Weight, dimensions, label PDF, tracking number on fp.receiving / fp.delivery themselves — these live on the linked fusion.shipment record (already implemented by fusion_shipping).
  • Bridge module (Phase C), Purolator integration (Phase D), at-receiving auto-label (Phase E), printer hookup (Phase F).
  • Modifying fusion_shipping's existing models — Phase A is additive on the plating side only.

Decisions reached

# Decision Rationale
D1 Carrier field: M2O to delivery.carrier (not Selection) Matches fusion_shipping's framework; allows API integration on the same record without conversion.
D2 Architecture: mirror fields on both fp.receiving and fp.delivery, auto-sync at delivery creation Self-contained records; loose coupling; shipping crew can override per-stage.
D3 Source of truth for weight / dims / label / tracking: fusion.shipment, NOT mirrored on plating records Shipment model already has every field; avoid duplicating + the sync logic.
D4 15 carriers seeded as delivery.carrier data records (XML), all delivery_type='fixed' initially Phase D will flip Purolator (and any others added) to their integration types. Manual carriers (Customer Pickup etc.) stay fixed permanently.
D5 Existing carrier_name (Char) and carrier_tracking (Char) kept as legacy Migration populates x_fc_carrier_id by name match; unmatched text stays for operator review.
D6 fusion_plating_receiving + fusion_plating_logistics gain hard fusion_shipping dependency The M2O to fusion.shipment requires the model to exist; no conditional compilation.

Architecture

┌─ fp.receiving ─────────────────────────────────────────────────────┐
│  NEW: x_fc_carrier_id          M2O delivery.carrier                │
│  NEW: x_fc_outbound_shipment_id M2O fusion.shipment                │
│  NEW: x_fc_outbound_shipment_count Integer (smart-button counter)  │
│  Existing: carrier_name (Char) — legacy, populated by migration    │
│  Existing: carrier_tracking (Char) — legacy                        │
│                                                                     │
│  ACTION: action_create_outbound_shipment()                         │
│    → creates fusion.shipment with sale_order_id + carrier_id       │
│    → idempotent: returns existing if already linked                │
│  ACTION: action_view_outbound_shipment()                           │
│    → opens linked fusion.shipment in form view                     │
│  ONCHANGE: x_fc_carrier_id propagates to linked shipment           │
│    → only if shipment.status == 'draft'                            │
└────────────────────────────────────────────────────────────────────┘

  copy at delivery creation (fp.job._fp_create_delivery)
            │
            ▼

┌─ fp.delivery ──────────────────────────────────────────────────────┐
│  NEW: x_fc_carrier_id          M2O delivery.carrier                │
│  NEW: x_fc_outbound_shipment_id M2O fusion.shipment                │
│  NEW: x_fc_outbound_shipment_count Integer                         │
│  Same ACTIONs and propagation as fp.receiving                      │
└────────────────────────────────────────────────────────────────────┘

┌─ delivery.carrier (seed data) ─────────────────────────────────────┐
│  Already on entech: Standard delivery, Canada Post, Customer Pickup│
│  Phase A adds (delivery_type='fixed', product_id=delivery.         │
│  product_product_delivery):                                        │
│    UPS, FedEx, USPS, DHL, Purolator, CCT, Canpar Express,          │
│    GLS Canada, Loomis Express, Day & Ross, Dicom Transportation,   │
│    Customer Drop-off, Local Delivery                               │
│  Idempotent: XML uses noupdate=1 + record ids check existing names │
└────────────────────────────────────────────────────────────────────┘

Field details

On fp.receiving:

x_fc_carrier_id = fields.Many2one(
    'delivery.carrier', string='Outbound Carrier', tracking=True,
    ondelete='set null',
    help='Who picks up the parts when work is done. Used to generate '
         'the return shipping label on the linked Outbound Shipment.',
)
x_fc_outbound_shipment_id = fields.Many2one(
    'fusion.shipment', string='Outbound Shipment', tracking=True,
    ondelete='set null',
    help='The shipment record carrying weight, dimensions, label PDF, '
         'and tracking. Created via the "Create Outbound Shipment" '
         'button.',
)
x_fc_outbound_shipment_count = fields.Integer(
    compute='_compute_x_fc_outbound_shipment_count',
)

Identical pair on fp.delivery.

Module changes

Module Bump Files
fusion_plating_receiving 19.0.3.9.0 → 19.0.3.10.0 manifest (+depends), models/fp_receiving.py, views/fp_receiving_views.xml, data/delivery_carrier_seed_data.xml (NEW), migrations/19.0.3.10.0/post-migrate.py (NEW), tests/test_carrier_fields.py (NEW)
fusion_plating_logistics bump manifest (+depends), models/fp_delivery.py, views/fp_delivery_views.xml, tests/test_delivery_shipping_fields.py (NEW)
fusion_plating_jobs bump models/fp_job.py (mirror at _fp_create_delivery), extend existing milestone-cascade test class

Migration logic (post-migrate)

def migrate(cr, version):
    # Name-match existing carrier_name text → delivery.carrier.name
    cr.execute("""
        UPDATE fp_receiving r
           SET x_fc_carrier_id = dc.id
          FROM delivery_carrier dc
         WHERE r.carrier_name IS NOT NULL
           AND r.carrier_name <> ''
           AND r.x_fc_carrier_id IS NULL
           AND LOWER(TRIM(r.carrier_name)) =
               LOWER(TRIM((dc.name->>'en_US')))
    """)

delivery.carrier.name is jsonb in Odoo 19 (translatable). The migration strips to en_US for the match.

Edge cases

Case Behavior
Receiving has no SO link Shipment creation works without sale_order_id (set_null on shipment side).
Carrier picked but no shipment yet Smart button reads "Create Outbound Shipment" → one click creates + opens.
User changes carrier on receiving after shipment exists Onchange propagates only when shipment.status == 'draft'. Confirmed/shipped shipments are left alone.
Two receivings on same SO (split deliveries) Each has its own x_fc_outbound_shipment_id. Mirror picks first one; user can change.
Migration finds ambiguous name Case-insensitive exact match only. Unmatched stays in carrier_name text.
Shipment is deleted ondelete='set null' — receiving keeps carrier but smart button reverts to "Create".
fusion_shipping not installed Manifest dependency fails fast on module load — correct failure mode.

Test plan

Unit tests in fusion_plating_receiving/tests/test_carrier_fields.py:

  • test_carrier_id_field_exists_on_receiving
  • test_outbound_shipment_id_field_exists_on_receiving
  • test_action_create_outbound_shipment_creates_draft
  • test_action_create_outbound_shipment_idempotent
  • test_carrier_id_change_propagates_to_draft_shipment
  • test_carrier_id_change_does_not_propagate_to_confirmed_shipment

Unit tests in fusion_plating_logistics/tests/test_delivery_shipping_fields.py:

  • test_carrier_id_field_exists_on_delivery
  • test_outbound_shipment_id_field_exists_on_delivery

Unit tests extending fusion_plating_jobs/tests/test_fp_job_milestone_cascade.py:

  • test_create_delivery_mirrors_carrier_from_receiving
  • test_create_delivery_mirrors_outbound_shipment
  • test_create_delivery_no_receiving_no_mirror

Manual verification post-deploy:

  1. Open RCV-30041 → carrier dropdown shows 15 options.
  2. Pick FedEx → click "Create Outbound Shipment" → fusion.shipment opens in draft.
  3. Confirm x_fc_outbound_shipment_id is populated on RCV-30041.
  4. Confirm same fields/buttons on a fresh fp.delivery record auto-created via mark-done.

Deployment

  • 3 module upgrades: fusion_plating_receiving, fusion_plating_logistics, fusion_plating_jobs.
  • fusion_shipping is already installed — no action needed.
  • Migration runs automatically; spot-check by querying fp_receiving.x_fc_carrier_id post-deploy.