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?¶
- Modularity: Sale module functionality stays in sale module
- Maintainability: Base stock module remains unchanged
- Upgradability: Base model can be updated independently
- 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:
- Picking Line Analysis
- Examines each line in the picking
- Checks
related_idfield for sale order references -
Uses polymorphic field pattern:
model_name,record_id -
Sale Order Linkage Discovery
- Filters for lines where
related_id._model == "sale.order" - Skips lines related to other models (purchase orders, etc.)
-
Collects unique sale order IDs
-
Delivery Status Tracking
- Records sale orders that are undelivered BEFORE completion
- Checks same orders AFTER completion
-
Only triggers event for orders that transitioned to delivered status
-
Event Triggering
- Fires
"delivered"workflow event on sale orders - Can trigger downstream automations
- 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¶
How Pickings Link to Sale Orders¶
The extension discovers sale order relationships through picking line related_id fields:
When a picking line references a 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¶
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!
Related Models¶
| 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¶
1. Always Link Picking Lines to Sale Orders¶
# 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:
- Customer Notifications
- Send delivery confirmation email
- SMS notification with tracking info
-
Push notification via mobile app
-
Status Updates
- Change order state to "done"
- Update delivery date fields
-
Set completion flags
-
Invoicing Automation
- Create invoice if terms are "on delivery"
- Send invoice to customer
-
Update payment tracking
-
Follow-up Activities
- Schedule satisfaction survey
- Create feedback request task
-
Plan reorder reminder
-
Analytics and Reporting
- Update delivery metrics
- Log delivery performance
- 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.pickingmodel - 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.