Skip to content

Transport Job Status Update Summary

kitler edited this page Jan 23, 2026 · 2 revisions

This document explains how the status field is updated in Transport Job after the simplification to trigger-based updates only.

Overview

The status field in Transport Job is automatically updated through trigger-based hooks - no background jobs or polling. Status updates happen immediately when events occur.

Status Values

The status field can have the following values:

  • Draft - Initial state for new documents
  • Submitted - Document has been submitted
  • In Progress - At least one leg has started or been assigned
  • Completed - All legs are completed or billed
  • Cancelled - Document has been cancelled

Status Update Triggers

1. Document Lifecycle Hooks

1.1 before_submit() Hook

  • Location: transport_job.py line 119-134
  • When: Before document submission
  • Actions:
    1. Sets _submitting flag to prevent before_save() from calling update_status() during submission
    2. Sets status to "Submitted" using direct SQL update (bypasses hooks)
    3. Commits the change immediately
  • Purpose: Ensures status is set to "Submitted" before submission completes, even if after_submit() doesn't run

1.2 before_save() Hook

  • Location: transport_job.py line 57-114
  • When: Before saving the document (every save)
  • Actions:
    1. Checks database directly for submitted documents with Draft status and fixes them via SQL
    2. Skips status update if document is being submitted (flagged with _submitting)
    3. Calls update_status() to calculate correct status
    4. For submitted documents (docstatus = 1), if status changed, uses db_set() to persist the change
    5. Ensures status is never "Draft" for submitted documents (safeguard)
  • Why db_set(): For submitted documents, we can't use regular save() as it would trigger validation. db_set() updates the database directly.

1.3 after_submit() Hook

  • Location: transport_job.py line 136-223
  • When: Immediately after document submission
  • Actions:
    1. Verifies document is actually submitted (docstatus = 1)
    2. Clears _submitting flag
    3. Sets status to "Submitted" using direct SQL update (bypasses hooks)
    4. Verifies status was saved correctly, retries with db_set() if needed
    5. Reloads document to get latest state
    6. Calls update_status() to check leg statuses
    7. Updates status if legs are already in progress/completed
    8. Publishes realtime event transport_job_status_changed if status changed
    9. Final verification to ensure status is never "Draft" for submitted documents
  • Purpose: Ensures status is set correctly on submission and reflects current leg states

1.4 after_save() Hook

  • Location: transport_job.py line 225-299
  • When: After every save (including after submission)
  • Actions:
    1. Checks database directly for submitted documents with Draft status and fixes them via SQL
    2. For submitted documents, ensures status is correct based on leg statuses
    3. Calls update_status() to recalculate status
    4. Updates status via SQL if changed
    5. Publishes realtime event transport_job_status_changed if status changed
  • Purpose: Catches cases where after_submit() didn't run or status was reset, ensures status stays in sync

1.5 on_cancel() Hook

  • Location: transport_job.py line 301-324
  • When: When document is cancelled
  • Actions:
    1. Gets previous status before cancellation
    2. Sets status to "Cancelled" via db_set()
    3. Commits the change
    4. Publishes realtime event transport_job_status_changed
    5. Releases capacity if vehicle was assigned

2. Transport Leg Status Changes

2.1 TransportLeg.after_save() Hook

  • Location: transport_leg.py line 39-44
  • When: After Transport Leg is saved (any change to leg)
  • Action: Calls update_transport_job_status() (line 43)

2.2 TransportLeg.update_transport_job_status() Method

  • Location: transport_leg.py line 280-398
  • When: Called from TransportLeg.after_save() hook
  • Actions:
    1. Checks database directly for current job status and docstatus
    2. Only updates if job is submitted (docstatus = 1)
    3. Fetches all leg statuses from database, using in-memory status for current leg
    4. Computes new status using same logic as TransportJob.update_status()
    5. Updates status via frappe.db.set_value() if changed
    6. Publishes realtime event transport_job_status_changed if status changed
    7. Logs status changes for debugging
  • Triggers: Transport Leg status changes due to:
    • Setting start_date → Leg status becomes "Started"
    • Setting end_date → Leg status becomes "Completed"
    • Assigning run_sheet → Leg status becomes "Assigned"
    • Setting sales_invoice → Leg status becomes "Billed"

Status Calculation Logic

The update_status() method (line 648-721 in transport_job.py) determines status based on:

For New Documents

  • Sets status to "Draft" if not set

For Cancelled Documents (docstatus = 2)

  • Always sets status to "Cancelled"

For Submitted Documents (docstatus = 1)

  1. Gets all Transport Leg statuses from database
  2. Maps leg statuses to job status:
    • All legs "Completed" or "Billed" → Job status = "Completed"
    • Any leg "Started" or "Assigned" → Job status = "In Progress"
    • All legs "Open" → Job status = "Submitted"
    • Mixed statuses → Job status = "In Progress" (if any leg is in progress/completed)

For Draft Documents (docstatus = 0)

  • Ensures status is "Draft" if missing
  • Doesn't override valid statuses during submission process

Key Implementation Details

Database Updates for Submitted Documents

For submitted documents, status is updated using multiple methods:

  1. Direct SQL Updates (used in before_submit(), after_submit(), after_save()):

    • Bypasses all hooks and validation
    • Ensures status is set even if hooks fail
    • Used as safeguards to prevent "Draft" status on submitted documents
    frappe.db.sql(
        f"UPDATE `tab{self.doctype}` SET `status` = 'Submitted' WHERE `name` = %s",
        (self.name,)
    )
    frappe.db.commit()
  2. db_set() Method (used in before_save(), on_cancel(), TransportLeg.update_transport_job_status()):

    • Updates database directly, bypassing validation
    • Prevents recursive save loops and validation errors
    • Used when status needs to change based on leg statuses
    if self.docstatus == 1 and old_status != new_status and new_status:
        self.db_set("status", new_status, update_modified=False)
  3. frappe.db.set_value() (used in TransportLeg.update_transport_job_status()):

    • Similar to db_set() but called from external context
    • Updates database directly with better safety checks
    frappe.db.set_value(
        "Transport Job",
        self.transport_job,
        "status",
        new_status,
        update_modified=False
    )

Why not use save()?

  • save() would trigger validation which might fail for submitted documents
  • Direct database updates bypass validation and prevent recursive save loops

Status Transition Rules

The validate_status_transition() method (line 621-646) enforces these rules:

Allowed Transitions:

  • Draft → Submitted, Cancelled
  • Submitted → In Progress, Cancelled
  • In Progress → Completed, Cancelled
  • Completed → (no transitions allowed)
  • Cancelled → (no transitions allowed)

Prevention Rules:

  • Cannot cancel if Sales Invoice exists

Safeguards Against Draft Status on Submitted Documents

Multiple safeguards ensure status is never "Draft" for submitted documents:

  1. before_submit() Hook: Sets status to "Submitted" before submission completes
  2. before_save() Hook: Checks database and fixes Draft status on submitted documents via SQL
  3. after_submit() Hook: Sets status to "Submitted" via SQL, verifies it was saved, retries if needed
  4. after_save() Hook: Catches cases where status was reset to Draft and fixes it immediately
  5. Status Calculation: update_status() ensures status is never "Draft" for docstatus = 1

These safeguards handle edge cases where hooks might not run or status might be reset.

Auto-billing Trigger

When status changes to "Completed", _trigger_auto_billing() is called (line 709) to automatically create sales invoices if enabled.

Realtime Events

Status changes are broadcast via realtime events to update client-side UI immediately:

  • Event Name: transport_job_status_changed
  • Published From:
    • after_submit() hook (line 196-204)
    • after_save() hook (line 290-299)
    • on_cancel() hook (line 312-320)
    • TransportLeg.update_transport_job_status() (line 376-387)
  • Event Data: Includes job_name, status, previous_status, docstatus, and optionally triggered_by and leg_name
  • Purpose: Provides real-time UI updates without polling or manual refresh

Status Update Flow

Document Creation
    ↓
before_save() → update_status() → Sets "Draft"
    ↓
Document Submit
    ↓
before_submit() → Sets "Submitted" via SQL (safeguard)
    ↓
after_submit() → Sets "Submitted" via SQL → update_status() → May change to "In Progress" or "Completed"
    ↓
after_save() → Verifies status → update_status() if needed → Publishes realtime event
    ↓
Transport Leg Changes (start_date, end_date, run_sheet, sales_invoice)
    ↓
TransportLeg.after_save() → update_transport_job_status() → Computes status → Updates via db.set_value() → Publishes realtime event
    ↓
Document Save (if status changed)
    ↓
before_save() → Checks for Draft status on submitted docs → update_status() → db_set() if submitted and status changed
    ↓
after_save() → Verifies status → update_status() if needed → Publishes realtime event

Client-Side Updates

JavaScript (transport_job.js)

Realtime Event Listener

  • Location: transport_job.js line 145-214
  • When: For submitted documents (docstatus = 1)
  • Action: Listens for transport_job_status_changed realtime events
  • Behavior:
    • Updates document status immediately when event is received
    • Triggers status change handlers (on_status_in_progress, on_status_completed)
    • Refreshes status field in UI
    • Cleans up listener when document is no longer submitted
  • Purpose: Provides real-time UI updates without polling

Form Refresh

  • Location: transport_job.js line 115-339
  • For submitted documents, sets up realtime event listener
  • For draft documents, fetches latest status from database if needed
  • Updates UI to reflect current status
  • No periodic polling - status is updated via realtime events or on form load/refresh

Form Submission

  • Location: transport_job.js line 641-716
  • After submission, fetches latest status with retry mechanism (up to 10 retries)
  • Ensures UI reflects server-side status changes
  • Handles cases where Python after_submit() hasn't completed yet

What Was Removed

Background Jobs (Removed)

  • update_transport_job_statuses() - Previously ran every 15 minutes
  • fix_stuck_transport_job_statuses() - Previously ran hourly
  • Reason: Status updates now happen immediately via triggers, making background jobs unnecessary

Periodic Polling (Removed)

  • ❌ 30-second interval status refresh in JavaScript
  • Reason: Status updates happen in real-time via triggers, so polling is not needed

Benefits of Trigger-Based Approach

  1. Real-time Updates: Status updates happen immediately when events occur
  2. Simpler Architecture: No background jobs or polling to maintain
  3. More Reliable: Status is always in sync with actual document/leg states
  4. Better Performance: No unnecessary periodic checks
  5. Easier to Debug: Status changes are directly tied to specific events

Troubleshooting

Status Not Updating

If status is not updating correctly:

  1. Check if document is submitted: Status updates for submitted documents use db_set(), which requires docstatus = 1
  2. Check Transport Leg statuses: Job status is calculated from leg statuses
  3. Check Transport Leg linkage: Ensure legs are properly linked to the Transport Job
  4. Check for errors: Look for errors in logs when hooks are triggered

Manual Status Fix

If needed, you can manually fix status using the whitelisted method:

frappe.call({
    method: "logistics.transport.doctype.transport_job.transport_job.fix_submitted_job_status",
    args: { job_name: "TRJ00000138" }
})

Summary

The status field is updated through:

  • ✅ Document lifecycle hooks (before_submit, before_save, after_submit, after_save, on_cancel)
  • ✅ Transport Leg status changes (via after_save hook)
  • ✅ Realtime events for immediate client-side updates
  • ✅ Client-side JavaScript (realtime event listener, form refresh, no polling)
  • Scheduled background tasks (removed)
  • Periodic polling (removed)

The status is automatically calculated based on Transport Leg statuses for submitted documents, ensuring the job status always reflects the current state of its legs. Multiple safeguards ensure status is never "Draft" for submitted documents, and realtime events provide immediate UI updates without polling.

Getting Started

Setup and Settings

Sea Freight

Air Freight

Transport

Customs

Warehousing

Pricing Center

Job Management

Sustainability

Intercompany

Special Projects

Pages

Features

Reports

Glossary

Clone this wiki locally