Skip to content

Run Sheet Job Status Update Summary

kitler edited this page Jan 23, 2026 · 3 revisions

This document explains how status updates are implemented in the Run Sheet doctype.

Overview

Run Sheet status is automatically calculated and updated based on:

  • Document lifecycle events (save, submit, cancel)
  • Transport Leg status changes
  • Document state (docstatus)

Status Values

The Run Sheet status field can have the following values:

  • Draft - Initial state for new documents (docstatus = 0)
  • Dispatched - Run Sheet is submitted and all legs are "Open" or "Assigned"
  • In-Progress - At least one leg has status "Started"
  • Completed - All legs have status "Completed" or "Billed"
  • Hold - Manually set status (prevents auto-updates)
  • Cancelled - Document has been cancelled (docstatus = 2)

Core Implementation

1. Main Status Update Method: update_status()

Location: run_sheet.py lines 533-630

This is the core method that calculates the correct status based on document state and Transport Leg statuses.

def update_status(self):
    """Update status based on document state and Transport Leg statuses
    
    This method automatically determines the correct status based on:
    - Document state (docstatus 0 = Draft, docstatus 1 = Dispatched/In-Progress/Completed, docstatus 2 = Cancelled)
    - Transport Leg statuses for submitted documents
    - Ensures status always follows docstatus
    """

Logic Flow:

  1. New Documents (is_new()):

    • Sets status to "Draft"
    • Returns early
  2. Cancelled Documents (docstatus == 2):

    • Always sets status to "Cancelled"
    • Returns early
  3. Draft Documents (docstatus == 0):

    • Ensures status is "Draft" (unless manually set to "Hold" or "Cancelled")
    • Returns early
  4. Submitted Documents (docstatus == 1):

    • Skips auto-update if status is "Hold" (manually set)
    • If no legs exist, sets status to "Dispatched"
    • Fetches all Transport Leg statuses from database (fresh query)
    • Calculates status based on leg statuses:
      • All legs "Completed" or "Billed" → "Completed"
      • Any leg "Started" → "In-Progress"
      • Any leg "Assigned" → "Dispatched"
      • All legs "Open" → "Dispatched"
      • Mixed statuses → "In-Progress" (if any leg is in progress/completed)
    • Updates status via db_set() if changed (for submitted documents)

Key Implementation Details:

  • Fresh Database Queries: Always fetches leg statuses directly from database to ensure latest values
  • db_set() for Submitted Documents: Uses db_set() instead of save() to avoid validation loops
  • Hold Status Protection: Respects manually set "Hold" status and prevents auto-updates

Document Lifecycle Hooks

2.1 before_save() Hook

Location: run_sheet.py lines 23-28

def before_save(self):
    """Update status before saving"""
    # Call update_status() to calculate correct status
    # For submitted documents (docstatus = 1), update_status() will use db_set() 
    # to persist status changes if the status changed
    self.update_status()

Behavior:

  • Called before every save operation
  • Calls update_status() to recalculate status
  • For submitted documents, update_status() uses db_set() internally

Note: During submission, a _submitting flag is set to prevent status reset (see before_submit()).

2.2 before_submit() Hook

Location: run_sheet.py lines 30-34

def before_submit(self):
    """Validate required fields before submission and mark as submitting"""
    self.validate_vehicle_required()
    # Set flag to prevent before_save from calling update_status during submission
    self._submitting = True

Behavior:

  • Validates that vehicle is required
  • Sets _submitting flag to prevent status reset during submission

2.3 after_submit() Hook

Location: run_sheet.py lines 36-60

def after_submit(self):
    """Set initial status after submission and check leg states"""
    # IMPORTANT: Ensure docstatus is 1 (sometimes it's not set in the object yet)
    # Reload from database to get the actual docstatus
    current_docstatus = frappe.db.get_value(self.doctype, self.name, "docstatus")
    if current_docstatus != 1:
        # If not submitted, something went wrong - log and return
        frappe.log_error(...)
        return
    
    # Ensure docstatus is set in the object for update_status to work
    self.docstatus = 1
    
    # Clear submitting flag
    if hasattr(self, '_submitting'):
        delattr(self, '_submitting')
    
    # Set status to "Dispatched" via db_set()
    self.db_set("status", "Dispatched", update_modified=False)
    
    # Call update_status() to calculate correct status based on leg states
    self.update_status()

Behavior:

  1. Verifies docstatus is 1 (reloads from database if needed)
  2. Clears _submitting flag
  3. Sets initial status to "Dispatched" via db_set()
  4. Calls update_status() to calculate correct status based on leg states

2.4 on_cancel() Hook

Location: run_sheet.py lines 62-64

def on_cancel(self):
    """Set status to Cancelled when document is cancelled"""
    self.db_set("status", "Cancelled", update_modified=False)

Behavior:

  • Sets status to "Cancelled" when document is cancelled
  • Uses db_set() to update directly in database

External Triggers: Transport Leg Status Changes

3. Transport Leg Hook: after_save()

Location: transport_leg.py line 39-44

When a Transport Leg is saved, it triggers Run Sheet status update:

def after_save(self):
    """Sync changes back to Run Sheet and trigger auto-vehicle assignment after Transport Leg is saved"""
    self.sync_to_run_sheet()
    self.sync_route_to_run_sheet()
    self.update_transport_job_status()
    self.update_run_sheet_status()  # <-- Updates Run Sheet status
    self._trigger_auto_vehicle_assignment()

3.1 TransportLeg.update_run_sheet_status() Method

Location: transport_leg.py lines 400-491

This method is called whenever a Transport Leg status changes.

Process:

  1. Checks if leg has a run_sheet assigned
  2. Verifies Run Sheet is submitted (docstatus = 1)
  3. Skips if Run Sheet status is "Hold" (manually set)
  4. Gets all leg statuses for the Run Sheet from database
  5. Uses in-memory status for current leg (most up-to-date)
  6. Calculates new Run Sheet status based on leg statuses
  7. Updates Run Sheet status via db_set() if changed

Status Mapping:

  • All legs "Completed" or "Billed" → Run Sheet "Completed"
  • Any leg "Started" → Run Sheet "In-Progress"
  • Any leg "Assigned" → Run Sheet "Dispatched"
  • All legs "Open" → Run Sheet "Dispatched"
  • Mixed statuses → Run Sheet "In-Progress" (if any leg is in progress/completed)

Implementation:

def update_run_sheet_status(self):
    """Update the parent Run Sheet status when this leg's status changes"""
    if not self.run_sheet:
        return
    
    try:
        # Get Run Sheet docstatus from database
        run_sheet_docstatus = frappe.db.get_value("Run Sheet", self.run_sheet, "docstatus")
        
        # Only update status for submitted Run Sheets
        if run_sheet_docstatus != 1:
            return
        
        # Get current Run Sheet status from database
        db_status = frappe.db.get_value("Run Sheet", self.run_sheet, "status")
        
        # Skip if status is manually set to "Hold"
        if db_status == "Hold":
            return
        
        # Get all leg statuses for this Run Sheet from database
        leg_statuses = frappe.db.get_all(
            "Transport Leg",
            filters={"run_sheet": self.run_sheet},
            pluck="status"
        )
        
        # Replace current leg's status with in-memory status (most up-to-date)
        if self.name in leg_statuses:
            leg_statuses = [s for s in leg_statuses if s != self.status]
        leg_statuses.append(self.status)
        
        # Calculate new status based on leg statuses
        if all(status in ["Completed", "Billed"] for status in leg_statuses):
            new_status = "Completed"
        elif any(status == "Started" for status in leg_statuses):
            new_status = "In-Progress"
        elif any(status == "Assigned" for status in leg_statuses):
            new_status = "Dispatched"
        elif all(status == "Open" for status in leg_statuses):
            new_status = "Dispatched"
        else:
            # Mixed statuses - prioritize Started > Assigned > Completed
            new_status = "In-Progress"  # or "Dispatched" based on logic
        
        # Update if status changed
        if new_status != db_status:
            frappe.db.set_value(
                "Run Sheet",
                self.run_sheet,
                "status",
                new_status,
                update_modified=False
            )
            frappe.db.commit()
    except Exception as e:
        frappe.log_error(...)

Client-Side API

4. update_status_from_client() Function

Location: run_sheet.py lines 948-1014

Whitelisted API that can be called from client-side JavaScript to manually trigger status update.

@frappe.whitelist()
def update_status_from_client(run_sheet_name):
    """
    Update Run Sheet status from client-side JavaScript.
    This method recalculates the status based on current leg statuses
    and returns the updated status.
    """

Process:

  1. Validates Run Sheet name is provided
  2. Loads Run Sheet document
  3. Only updates status for submitted documents (docstatus = 1)
  4. Skips update if status is "Hold"
  5. Gets current status from database
  6. Calls update_status() to recalculate
  7. Persists to database via db_set() if changed
  8. Returns status information

Response Format:

{
    "success": True,
    "status": "In-Progress",
    "old_status": "Dispatched",
    "db_status": "Dispatched",
    "changed": True
}

Status Calculation Logic

Status Priority (for submitted documents):

  1. All legs "Completed" or "Billed" → "Completed"
  2. Any leg "Started" → "In-Progress"
  3. Any leg "Assigned" → "Dispatched"
  4. All legs "Open" → "Dispatched"
  5. Mixed statuses → "In-Progress" (if any leg is in progress/completed)

Transport Leg Status Values:

  • Open - No run_sheet assigned, no dates set
  • Assigned - run_sheet assigned but not started
  • Started - start_date is set
  • Completed - end_date is set
  • Billed - sales_invoice is set (highest priority)

Key Design Decisions

1. Use db_set() for Submitted Documents

Why:

  • save() would trigger validation which might fail for submitted documents
  • db_set() updates the database directly, bypassing validation
  • Prevents recursive save loops and validation errors

Example:

if self.docstatus == 1:
    self.db_set("status", new_status, update_modified=False)

2. Fresh Database Queries

Why:

  • Leg statuses may have changed since Run Sheet was loaded
  • Ensures status calculations are based on the most current data
  • Prevents stale status issues

Example:

# Always fetch fresh status from database
leg_status = frappe.db.get_value("Transport Leg", transport_leg_name, "status")

3. In-Memory Status for Current Leg

Why:

  • When Transport Leg is being saved, in-memory status is most up-to-date
  • Database may not yet reflect the current change
  • Ensures accurate status calculation

Example:

# Replace current leg's status with in-memory status (most up-to-date)
leg_statuses.append(self.status)  # Uses in-memory status

4. Hold Status Protection

Why:

  • Allows manual override of automatic status updates
  • Useful for pausing Run Sheet without affecting leg statuses
  • Prevents automatic updates from overwriting manual status

Implementation:

# Don't auto-update if status is manually set to "Hold"
if self.status == "Hold":
    return

Status Update Flow Diagram

Run Sheet Save/Submit
    ↓
before_save() / after_submit()
    ↓
update_status()
    ↓
    ├─→ Check docstatus
    │   ├─→ 0 (Draft) → Status = "Draft"
    │   ├─→ 2 (Cancelled) → Status = "Cancelled"
    │   └─→ 1 (Submitted) → Calculate from leg statuses
    │           ↓
    │       Fetch leg statuses from database
    │           ↓
    │       Calculate status based on leg statuses
    │           ↓
    │       Update via db_set() if changed
    │
Transport Leg Status Change
    ↓
TransportLeg.after_save()
    ↓
update_run_sheet_status()
    ↓
    ├─→ Check if leg has run_sheet
    ├─→ Verify Run Sheet is submitted
    ├─→ Skip if status is "Hold"
    ├─→ Get all leg statuses from database
    ├─→ Use in-memory status for current leg
    ├─→ Calculate new Run Sheet status
    └─→ Update via db_set() if changed

Status Update Triggers Summary

Trigger Location When Action
before_save() run_sheet.py:23 Every save Calls update_status()
after_submit() run_sheet.py:36 After submission Sets "Dispatched", then calls update_status()
on_cancel() run_sheet.py:62 On cancel Sets "Cancelled"
TransportLeg.after_save() transport_leg.py:39 When leg is saved Calls update_run_sheet_status()
update_status_from_client() run_sheet.py:948 Manual API call Recalculates and updates status

Best Practices

  1. Always use db_set() for submitted documents - Prevents validation loops
  2. Fetch fresh data from database - Ensures accuracy
  3. Respect "Hold" status - Don't auto-update when manually set
  4. Use in-memory status for current leg - Most up-to-date value
  5. Log significant status changes - Helps with debugging
  6. Handle exceptions gracefully - Prevents status update failures from breaking other operations

Troubleshooting

Status Not Updating

  1. Check docstatus: Status updates only work for submitted Run Sheets (docstatus = 1)
  2. Check Hold status: Hold status prevents automatic updates
  3. Check Transport Leg linkage: Ensure legs are properly linked (run_sheet field)
  4. Check Transport Leg statuses: Run Sheet status is calculated from leg statuses
  5. Check for errors: Look for errors in logs when hooks are triggered

Status Stuck

  1. Verify leg statuses: Check if leg statuses are correct
  2. Check for Hold status: Remove Hold status to allow auto-updates
  3. Manually trigger update: Use update_status_from_client() API
  4. Check database directly: Verify status in database matches expectations

Related Documentation

  • RUN_SHEET_JOB_STATUS_UPDATE_SUMMARY.md - Explains relationship between Run Sheet and Transport Job status updates
  • transport_leg.py - Transport Leg status update implementation
  • transport_job.py - Transport Job status update implementation

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