Skip to content

Stock Picking Extension Documentation (Sale Module)

Overview

The Stock Picking Extension (stock.picking via _inherit) is a MODEL EXTENSION that adds sales order integration to the core stock.picking model from the stock module. This extension enables automatic detection and triggering of sale order delivery events, ensuring that sale orders are properly notified when their goods are delivered through stock picking completion.

This is NOT a standalone model - it extends the existing stock.picking model using the _inherit mechanism to provide sales fulfillment tracking.


Model Information

Model Name: stock.picking (EXTENSION) Base Model: stock.picking (from stock module) Extension Module: netforce_sale Extension Mechanism: _inherit = "stock.picking" Purpose: Trigger sale order "delivered" events when pickings linked to sale orders are completed

Extension Features

  • Monitors picking completion for sale-order-related stock movements
  • Automatically detects sale orders associated with completed pickings
  • Triggers "delivered" workflow event on sale orders when delivery is complete
  • Zero new fields added (behavior extension only)
  • Transparent integration with base picking model

What This Extension Adds

This extension does NOT add any new fields to the stock.picking model. Instead, it: - Overrides the set_done() method to add sale order delivery tracking - Monitors picking completion that relates to sale orders - Triggers workflow events on sale orders when delivery status changes


Understanding Model Extensions

What is _inherit?

The _inherit mechanism allows one module to extend models defined in other modules without modifying the original code:

class Picking(Model):
    _inherit = "stock.picking"

    # This class extends the existing stock.picking model
    # All fields and methods from the base model are available
    # You can add new fields or override existing methods

Extension Architecture

┌─────────────────────────────────────┐
│   stock.picking (Base Model)        │
│   From: stock module                │
│   - Fields: date, lines, state, etc.│
│   - Methods: set_done, etc.         │
└─────────────────────────────────────┘
            [_inherit]
┌─────────────────────────────────────┐
│   Picking Extension (Sale Module)   │
│   Adds: Sale order event triggering │
│   Overrides: set_done() method      │
└─────────────────────────────────────┘

Why Extend Instead of Modify?

  1. Modularity: Sale module functionality stays in sale module
  2. Maintainability: Base stock module remains unchanged
  3. Upgradability: Base model can be updated independently
  4. Separation of Concerns: Stock management vs. sales fulfillment

Extension Implementation

Base Model Location

  • Module: stock
  • Model: stock.picking
  • File: /netforce_stock/netforce_stock/models/stock_picking.py

Extension Location

  • Module: netforce_sale
  • Model: stock.picking (extension)
  • File: /netforce_sale/netforce_sale/models/stock_picking.py

Extended Methods

The sale module extends only ONE method from the base picking model:

Method Extension Type Purpose
set_done() Override with super() call Detect sale order deliveries and trigger "delivered" events

Method Override Details

1. set_done() - Enhanced Picking Completion

Method: set_done(ids, context)

The extension overrides the set_done() method to add sale order delivery event triggering while preserving all original picking completion functionality.

Extension Behavior:

The method follows this workflow:

1. Collect Related Sale Orders from Picking Lines
2. Check Which Orders Are Currently Undelivered
3. Call Original set_done() Method (super())
4. Check Which Orders Became Delivered
5. Trigger "delivered" Event for Newly Delivered Orders

Detailed Algorithm:

def set_done(self, ids, context={}):
    # Step 1: Collect all sale orders related to these pickings
    sale_ids = []
    for obj in self.browse(ids):
        # Step 2: Check each picking line
        for line in obj.lines:
            rel = line.related_id
            if not rel:
                continue  # Skip lines not related to anything

            # Step 3: Check if line is related to a sale order
            if rel._model != "sale.order":
                continue  # Skip non-sale-order relations

            # Step 4: Collect sale order ID
            sale_ids.append(rel.id)

    # Step 5: Get unique sale order IDs
    sale_ids = list(set(sale_ids))
    sale_ids = [int(i) for i in sale_ids]

    # Step 6: Remember which orders are currently undelivered
    undeliv_sale_ids = []
    for sale in get_model("sale.order").browse(sale_ids):
        if not sale.is_delivered:
            undeliv_sale_ids.append(sale.id)

    # Step 7: Execute original picking completion
    res = super().set_done(ids, context=context)

    # Step 8: Check which orders became delivered
    deliv_sale_ids = []
    for sale in get_model("sale.order").browse(undeliv_sale_ids):
        if sale.is_delivered:
            deliv_sale_ids.append(sale.id)

    # Step 9: Trigger "delivered" event for newly delivered orders
    if deliv_sale_ids:
        get_model("sale.order").trigger(deliv_sale_ids, "delivered")

    return res

Key Extension Points:

  1. Picking Line Analysis
  2. Examines each line in the picking
  3. Checks related_id field for sale order references
  4. Uses polymorphic field pattern: model_name,record_id

  5. Sale Order Linkage Discovery

  6. Filters for lines where related_id._model == "sale.order"
  7. Skips lines related to other models (purchase orders, etc.)
  8. Collects unique sale order IDs

  9. Delivery Status Tracking

  10. Records sale orders that are undelivered BEFORE completion
  11. Checks same orders AFTER completion
  12. Only triggers event for orders that transitioned to delivered status

  13. Event Triggering

  14. Fires "delivered" workflow event on sale orders
  15. Can trigger downstream automations
  16. Enables notification workflows, status updates, etc.

Parameters: - ids (list): Picking record IDs to complete - context (dict): Optional context information

Returns: Result from base set_done() method (typically None or status)

Example Usage:

# Complete a picking (works exactly like base model)
picking_id = get_model("stock.picking").create({
    "date": "2025-01-05",
    "type": "out",
    "lines": [
        ("create", {
            "product_id": product_id,
            "qty": 10,
            "location_from_id": warehouse_loc_id,
            "location_to_id": customer_loc_id,
            "related_id": "sale.order,%d" % order_id
        })
    ]
})

# When completed, the extension automatically:
# 1. Finds sale orders linked to picking lines
# 2. Checks delivery status before and after
# 3. Triggers "delivered" event if order becomes fully delivered
get_model("stock.picking").set_done([picking_id])

Integration with Sale Orders

The extension discovers sale order relationships through picking line related_id fields:

picking_line.related_id = "sale.order,123"

When a picking line references a sale order:

Picking → Picking Lines → related_id → Sale Order

Sale Order Delivery Detection

Sale orders track delivery status through the is_delivered computed field:

# Before picking completion
sale.is_delivered = False

# Complete picking
picking.set_done([picking_id])

# After picking completion (if fully delivered)
sale.is_delivered = True  # Extension detects this change

The extension only triggers the "delivered" event for orders that: 1. Were undelivered before picking completion 2. Became delivered after picking completion 3. Are linked to the completed picking lines


Workflow Event: "delivered"

Event Trigger Behavior

get_model("sale.order").trigger(deliv_sale_ids, "delivered")

This event can be configured in workflow automation to: - Send delivery confirmation emails - Update order status to completed - Trigger invoicing workflows - Notify sales team - Update customer records - Generate delivery reports

Event Configuration Example

In workflow settings, you might configure:

# Workflow: Sale Order Delivered
# Trigger: sale.order.delivered
# Actions:
#   1. Send email to customer: "Your order has been delivered"
#   2. Update order state to "done"
#   3. Create invoice if not already invoiced
#   4. Log delivery completion in order notes
#   5. Update inventory analytics

Stock Picking Line Linkage

Creating Pickings Linked to Sale Orders

When sale orders create pickings, they link the picking lines:

# Sale order creates goods-out picking
picking_id = get_model("stock.picking").create({
    "type": "out",
    "lines": [
        ("create", {
            "product_id": product_id,
            "qty": 10,
            "location_from_id": warehouse_id,
            "location_to_id": customer_loc_id,
            "related_id": "sale.order,%d" % order_id,  # Link to sale order
            "related_line_id": "sale.order.line,%d" % order_line_id  # Optional: link to line
        })
    ]
})

Multiple Pickings for One Order

Large orders may require multiple pickings (partial deliveries):

# Order has 100 units
# Picking 1: 60 units delivered
# Picking 2: 40 units delivered

# After picking 1 completion:
# - Order is_delivered = False (partial delivery)
# - No "delivered" event triggered

# After picking 2 completion:
# - Order is_delivered = True (full delivery)
# - "delivered" event triggered!

Model Relationship Description
stock.picking Base Model (Extended) Core picking/delivery model from stock module
stock.move Picking Lines Individual line items in the picking
sale.order Event Target Sale orders that receive "delivered" event triggers
sale.order.line Referenced Sale order lines linked to stock moves
product Referenced Products being picked/delivered
stock.location From/To Source and destination locations

Common Use Cases

Use Case 1: Complete Delivery Triggers Order Done Event

# Customer places an order
order_id = get_model("sale.order").create({
    "contact_id": customer_id,
    "lines": [
        ("create", {
            "product_id": product_id,
            "qty": 20,
            "unit_price": 50.00
        })
    ]
})

# Order is confirmed
get_model("sale.order").confirm([order_id])

# System creates delivery picking
order = get_model("sale.order").browse(order_id)
picking_id = order.picking_ids[0].id

# Picking lines are linked to sale order
picking = get_model("stock.picking").browse(picking_id)
for line in picking.lines:
    # line.related_id = "sale.order,{order_id}"
    pass

# Verify order not yet delivered
assert not order.is_delivered

# Warehouse completes the picking
get_model("stock.picking").set_done([picking_id])

# Extension automatically:
# 1. Finds order_id through line.related_id
# 2. Checks order was undelivered before
# 3. Completes picking (updates stock levels)
# 4. Checks if order is now delivered
# 5. Triggers "delivered" event on the order

# Result: Order's "delivered" workflow event is triggered
# This can automatically:
# - Send delivery confirmation to customer
# - Update order state to "done"
# - Create invoice if payment terms are "on delivery"
# - Notify sales rep of completion

Use Case 2: Partial Deliveries (Multiple Pickings)

# Order with 100 units
order_id = 123

# First picking: 60 units
picking1_id = get_model("stock.picking").create({
    "type": "out",
    "lines": [
        ("create", {
            "product_id": product_id,
            "qty": 60,
            "related_id": "sale.order,%d" % order_id
        })
    ]
})
get_model("stock.picking").set_done([picking1_id])
# Order still undelivered (is_delivered = False)
# No "delivered" event triggered

# Second picking: 40 units
picking2_id = get_model("stock.picking").create({
    "type": "out",
    "lines": [
        ("create", {
            "product_id": product_id,
            "qty": 40,
            "related_id": "sale.order,%d" % order_id
        })
    ]
})
get_model("stock.picking").set_done([picking2_id])
# Order now fully delivered (is_delivered = True)
# Extension detects transition from undelivered → delivered
# "delivered" event is triggered!

Use Case 3: Mixed Picking (Multiple Orders)

# Warehouse picks multiple orders together for efficiency
picking_id = get_model("stock.picking").create({
    "type": "out",
    "lines": [
        # Order A: 5 units of Product X
        ("create", {
            "product_id": product_x_id,
            "qty": 5,
            "related_id": "sale.order,%d" % order_a_id
        }),
        # Order A: 3 units of Product Y
        ("create", {
            "product_id": product_y_id,
            "qty": 3,
            "related_id": "sale.order,%d" % order_a_id
        }),
        # Order B: 10 units of Product X
        ("create", {
            "product_id": product_x_id,
            "qty": 10,
            "related_id": "sale.order,%d" % order_b_id
        })
    ]
})

# Complete the picking
get_model("stock.picking").set_done([picking_id])

# Extension processes:
# 1. Collects sale_ids: [order_a_id, order_b_id]
# 2. Checks both orders' delivery status before
# 3. Completes picking
# 4. Checks both orders' delivery status after
# 5. Triggers "delivered" event for any that became fully delivered

# If order A is now fully delivered: "delivered" event for A
# If order B is now fully delivered: "delivered" event for B
# Both can be triggered in one picking completion

Use Case 4: Backorder Completion

# Order created and partially fulfilled
order_id = 456
original_picking_id = 100  # Delivered 50 of 100 units

# Backorder created for remaining 50 units
backorder_id = 101

# Backorder lines still linked to same sale order
backorder = get_model("stock.picking").browse(backorder_id)
for line in backorder.lines:
    # line.related_id = "sale.order,456"
    pass

# Complete backorder
get_model("stock.picking").set_done([backorder_id])

# Extension:
# 1. Finds order 456 through line linkage
# 2. Order was undelivered (waiting for backorder)
# 3. Completes backorder picking
# 4. Order now fully delivered
# 5. Triggers "delivered" event

Use Case 5: Batch Picking Completion

# Warehouse completes multiple pickings at once
picking_ids = [201, 202, 203, 204, 205]

# Each picking may be for different orders
# Picking 201, 202: Order A
# Picking 203: Order B
# Picking 204, 205: Order C

# Complete all pickings in batch
get_model("stock.picking").set_done(picking_ids)

# Extension processes efficiently:
# 1. Collects ALL related sale orders from ALL pickings
# 2. Records which orders are currently undelivered
# 3. Completes ALL pickings via super()
# 4. Checks which orders became delivered
# 5. Triggers "delivered" event for all newly-delivered orders

# If pickings 201+202 complete order A: Event for A
# If picking 203 completes order B: Event for B
# If pickings 204+205 complete order C: Event for C
# Result: All three orders get "delivered" event triggered

Best Practices

# Good: Link picking line to sale order
picking_id = get_model("stock.picking").create({
    "type": "out",
    "lines": [
        ("create", {
            "product_id": product_id,
            "qty": 10,
            "related_id": "sale.order,%d" % order_id,  # Essential link!
        })
    ]
})

# Bad: No linkage to sale order
picking_id = get_model("stock.picking").create({
    "type": "out",
    "lines": [
        ("create", {
            "product_id": product_id,
            "qty": 10,
            "related_id": None,  # No link!
        })
    ]
})
# Result: Delivery won't trigger "delivered" event on order

Why? The extension discovers sale orders through related_id fields. Without proper linking, deliveries won't trigger order events.


2. Let Sale Orders Create Pickings

# Good: Use sale order's built-in picking creation
order_id = get_model("sale.order").create({...})
get_model("sale.order").confirm([order_id])
# Confirmation automatically creates picking with proper linkage

# Less Preferred: Manually create picking
# You must ensure all lines are correctly linked to order
picking_id = get_model("stock.picking").create({
    "lines": [
        ("create", {
            "related_id": "sale.order,%d" % order_id,
            # Must manually set all links
        })
    ]
})

Why? Sale order confirmation automatically creates properly linked pickings. Manual creation requires careful attention to linkage.


3. Configure Workflow Actions for "delivered" Event

# Set up workflow automation to handle "delivered" events
# Example workflow configuration:

# Trigger: sale.order.delivered
# Conditions: state == "confirmed"
# Actions:
#   1. Send email template "delivery_confirmation" to order.contact_id
#   2. Update state to "done"
#   3. If invoice_method == "on_delivery": Create invoice
#   4. Create activity: "Follow up on delivery satisfaction"
#   5. Update analytics: delivery_date = today

Why? The extension provides the event trigger, but you need workflow configuration to define what happens when orders are delivered.


4. Handle Partial Deliveries Correctly

# The extension automatically handles partial deliveries correctly:

# First partial delivery
get_model("stock.picking").set_done([picking1_id])
# is_delivered = False, no event

# Second partial delivery completing order
get_model("stock.picking").set_done([picking2_id])
# is_delivered transitions from False → True
# "delivered" event triggered!

# Don't try to manually track partial deliveries for event triggering
# The extension's before/after comparison handles this automatically

5. Use Batch Completion for Efficiency

# Good: Batch complete multiple pickings together
picking_ids = [101, 102, 103, 104, 105]
get_model("stock.picking").set_done(picking_ids)
# Extension efficiently:
# - Collects all related orders once
# - Completes all pickings together
# - Triggers events for all affected orders

# Less Efficient: Complete pickings individually
for picking_id in picking_ids:
    get_model("stock.picking").set_done([picking_id])
# Extension runs detection logic for each picking separately

Why? Batch processing is more efficient when completing multiple pickings that might affect the same orders.


Extension Impact Analysis

What Changes When This Extension is Active?

Aspect Without Extension With Extension
Picking Completion Works normally Works normally + sale order event triggering
Stock Movements Processes movements Processes movements + detects related sale orders
Sale Order Status Manual tracking Automatic "delivered" event when fully delivered
Workflow Triggers None "delivered" event enables automations
Performance Baseline Minimal overhead (only checks line linkage)

Performance Considerations

The extension adds minimal overhead: - Only examines picking lines that have related_id set - Skips lines not related to sale orders - Uses efficient set operations for unique order collection - Single batch trigger call for all affected orders


Delivery Status Synchronization

How is_delivered is Computed

Sale orders compute is_delivered status by:

# Pseudo-code for sale order's is_delivered field
def get_is_delivered(self, ids, context={}):
    vals = {}
    for obj in self.browse(ids):
        # Sum quantities from order lines
        qty_ordered = sum(line.qty for line in obj.lines)

        # Sum quantities delivered via pickings
        qty_delivered = sum(
            move.qty
            for move in stock_moves
            if move.related_id == obj and move.state == "done"
        )

        # Order is delivered if all quantities fulfilled
        vals[obj.id] = (qty_delivered >= qty_ordered)

    return vals

The extension leverages this computed field to detect status changes.


Workflow Integration

Trigger Events

The extension fires workflow triggers:

get_model("sale.order").trigger(deliv_sale_ids, "delivered")
# Fired when: Sale orders transition from undelivered to delivered status

Configurable Workflow Actions

You can configure workflows to respond to the "delivered" event:

  1. Customer Notifications
  2. Send delivery confirmation email
  3. SMS notification with tracking info
  4. Push notification via mobile app

  5. Status Updates

  6. Change order state to "done"
  7. Update delivery date fields
  8. Set completion flags

  9. Invoicing Automation

  10. Create invoice if terms are "on delivery"
  11. Send invoice to customer
  12. Update payment tracking

  13. Follow-up Activities

  14. Schedule satisfaction survey
  15. Create feedback request task
  16. Plan reorder reminder

  17. Analytics and Reporting

  18. Update delivery metrics
  19. Log delivery performance
  20. Trigger reporting dashboards

Troubleshooting

"delivered" Event Not Triggering

Cause 1: Picking lines not linked to sale order Solution: Ensure picking lines have related_id = "sale.order,{order_id}"

Cause 2: Order already delivered Solution: Extension only triggers event for orders that BECOME delivered. If order was already delivered before this picking, no event is triggered.

Cause 3: Partial delivery doesn't complete order Solution: This is expected behavior. Event only triggers when order becomes fully delivered.

Cause 4: Picking line linked to wrong model Solution: Verify related_id._model == "sale.order" not "purchase.order" or other model


Multiple "delivered" Events for Same Order

Cause: Multiple pickings completed simultaneously that all complete the order Solution: The extension uses list(set(sale_ids)) to deduplicate, so this shouldn't happen. If it does, check workflow configuration for multiple rules responding to "delivered" event.


Picking Completion Fails

Cause: Error in base picking completion, not extension Solution: Extension calls super().set_done() which can fail for various stock-module reasons. Check stock levels, location permissions, picking state, etc.


Testing Examples

Unit Test: Picking Completion Triggers Delivered Event

def test_picking_completion_triggers_delivered_event():
    # Create a sale order
    order_id = get_model("sale.order").create({
        "contact_id": customer_id,
        "lines": [
            ("create", {
                "product_id": product_id,
                "qty": 10,
                "unit_price": 100.00
            })
        ]
    })

    # Confirm order to generate picking
    get_model("sale.order").confirm([order_id])

    # Get the picking
    order = get_model("sale.order").browse(order_id)
    picking_id = order.picking_ids[0].id

    # Verify order is not yet delivered
    assert not order.is_delivered

    # Set up event listener to verify trigger fires
    events_fired = []
    def event_listener(ids, event):
        events_fired.append((ids, event))

    # Mock the trigger method to capture events
    original_trigger = get_model("sale.order").trigger
    get_model("sale.order").trigger = event_listener

    # Complete the picking
    get_model("stock.picking").set_done([picking_id])

    # Restore original trigger
    get_model("sale.order").trigger = original_trigger

    # Verify "delivered" event was triggered for our order
    assert ([order_id], "delivered") in events_fired

    # Verify order is now delivered
    order = get_model("sale.order").browse(order_id)
    assert order.is_delivered

Unit Test: Partial Delivery Doesn't Trigger Event

def test_partial_delivery_no_event():
    # Create order with 100 units
    order_id = get_model("sale.order").create({
        "contact_id": customer_id,
        "lines": [
            ("create", {
                "product_id": product_id,
                "qty": 100,
                "unit_price": 10.00
            })
        ]
    })

    # Confirm order
    get_model("sale.order").confirm([order_id])

    # Create first partial picking: 60 units
    picking1_id = get_model("stock.picking").create({
        "type": "out",
        "lines": [
            ("create", {
                "product_id": product_id,
                "qty": 60,
                "location_from_id": warehouse_loc_id,
                "location_to_id": customer_loc_id,
                "related_id": "sale.order,%d" % order_id
            })
        ]
    })

    # Capture events
    events_fired = []
    original_trigger = get_model("sale.order").trigger
    get_model("sale.order").trigger = lambda ids, event: events_fired.append((ids, event))

    # Complete partial picking
    get_model("stock.picking").set_done([picking1_id])

    # Restore trigger
    get_model("sale.order").trigger = original_trigger

    # Verify NO "delivered" event was triggered
    assert ([order_id], "delivered") not in events_fired

    # Verify order is still undelivered
    order = get_model("sale.order").browse(order_id)
    assert not order.is_delivered

Unit Test: Mixed Picking Triggers Multiple Orders

def test_mixed_picking_triggers_multiple_orders():
    # Create two orders
    order1_id = get_model("sale.order").create({...})
    order2_id = get_model("sale.order").create({...})

    # Both confirmed and ready for delivery

    # Create mixed picking for both orders
    picking_id = get_model("stock.picking").create({
        "type": "out",
        "lines": [
            ("create", {
                "product_id": product_id,
                "qty": 10,
                "related_id": "sale.order,%d" % order1_id
            }),
            ("create", {
                "product_id": product_id,
                "qty": 20,
                "related_id": "sale.order,%d" % order2_id
            })
        ]
    })

    # Capture events
    events_fired = []
    original_trigger = get_model("sale.order").trigger
    get_model("sale.order").trigger = lambda ids, event: events_fired.append((ids, event))

    # Complete picking
    get_model("stock.picking").set_done([picking_id])

    # Restore trigger
    get_model("sale.order").trigger = original_trigger

    # Verify both orders got "delivered" event
    # (assuming both are now fully delivered)
    triggered_order_ids = [ids[0] for ids, event in events_fired if event == "delivered"]
    assert order1_id in triggered_order_ids
    assert order2_id in triggered_order_ids

Security Considerations

Permission Model

  • Extension uses same permissions as base stock.picking model
  • Completing pickings requires stock module permissions
  • Triggering sale order events doesn't require separate permissions

Data Access

  • Extension only accesses picking lines already in the picking
  • No additional data access beyond what picking completion already requires
  • Event triggering operates on orders already related to the picking's lines

Integration with Other Systems

Shipping Systems

# Typical integration flow:
# 1. Sale order confirmed → Picking created
# 2. Picking sent to WMS/shipping system
# 3. Goods picked and packed
# 4. Shipping label printed
# 5. Carrier picks up → set_done() called
# 6. Extension triggers "delivered" event
# 7. Customer notified automatically

Invoicing Workflows

# Workflow: Invoice on Delivery
# Trigger: sale.order.delivered
# Condition: invoice_method == "on_delivery" AND not invoiced
# Actions:
#   1. Call sale.order.make_invoices()
#   2. Post invoice
#   3. Send invoice to customer
#   4. Create payment reminder task

Version History

Last Updated: 2025-01-05 Model Version: stock_picking.py (Extension) Framework: Netforce Base Model: stock.picking from stock module Extension Module: netforce_sale


Additional Resources

  • Base Model Documentation: stock.picking
  • Sale Order Documentation: sale.order
  • Stock Move Documentation: stock.move
  • Workflow Configuration Guide
  • Inventory Management Guide

Support & Feedback

For issues or questions about this extension: 1. Verify picking lines are properly linked to sale order via related_id 2. Check picking line related_id._model is "sale.order" 3. Confirm order delivery status before and after picking completion 4. Review workflow configurations for "delivered" event handlers 5. Test in development environment with logging enabled


This documentation covers the sale module's extension of the stock.picking model for automatic sale order delivery event triggering.