Stock Picking Documentation¶
Overview¶
The Stock Picking module (stock.picking) manages all stock movements within the system including goods receipts (incoming), goods issues (outgoing), and internal transfers. This is a core inventory management component.
Model Information¶
Model Name: stock.picking
Display Name: Stock Picking
Key Fields: company_id, type, number
Features¶
- ✅ Audit logging enabled
- ✅ Multi-company support
- ✅ Full-text content search
- ✅ Unique key constraint per company/type/number
Understanding Key Fields¶
What are Key Fields?¶
In Netforce models, Key Fields are a combination of fields that together create a composite unique identifier for a record. Think of them as a business key that ensures data integrity across the system.
For the stock.picking model, the key fields are:
This means the combination of these three fields must be unique:
- company_id - The company ID
- type - The picking type (in/out/internal)
- number - The picking reference number
Why Key Fields Matter¶
Uniqueness Guarantee - Key fields prevent duplicate records by ensuring unique combinations:
# These can all coexist - different key combinations:
Company A + type "in" + number "GR-001" ✅ Valid
Company A + type "out" + number "GR-001" ✅ Valid (different type)
Company B + type "in" + number "GR-001" ✅ Valid (different company)
# This would fail - duplicate key:
Company A + type "in" + number "GR-001" ❌ ERROR: Key already exists!
Database Implementation¶
The key fields are enforced at the database level using a unique constraint:
_sql_constraints = [
("key_uniq",
"unique (company_id, type, number)",
"The number of each company and type must be unique!")
]
This translates to:
Picking Types¶
| Type | Code | Description |
|---|---|---|
| Goods Receipt | in |
Receiving stock from suppliers |
| Goods Issue | out |
Shipping stock to customers |
| Goods Transfer | internal |
Moving stock between locations |
Picking States¶
| State | Description |
|---|---|
draft |
Initial state, editable |
pending |
Planned/scheduled |
approved |
Authorized for execution |
qc_checked |
Quality control verified |
done |
Completed and validated |
voided |
Cancelled |
rejected |
Failed QC inspection |
Key Fields Reference¶
Header Fields¶
| Field | Type | Required | Description |
|---|---|---|---|
type |
Selection | ✅ | Picking type: in/out/internal |
journal_id |
Many2One | ✅ | Stock journal |
number |
Char | ✅ | Unique reference number |
ref |
Char | ❌ | External reference |
contact_id |
Many2One | ❌ | Supplier/Customer |
date |
DateTime | ✅ | Transaction date |
date_done |
DateTime | ❌ | Completion timestamp |
state |
Selection | ✅ | Current status |
related_id |
Reference | ❌ | Link to SO/PO/Project |
currency_id |
Many2One | ❌ | Transaction currency |
ship_address_id |
Many2One | ❌ | Delivery address |
company_id |
Many2One | ❌ | Operating company |
Cost & Tracking Fields¶
| Field | Type | Description |
|---|---|---|
qty_total |
Decimal | Total quantity (computed) |
qty_validated |
Decimal | Validated quantity (computed) |
cost_total |
Decimal | Total cost amount (computed) |
currency_rate |
Decimal | Exchange rate |
ship_cost |
Decimal | Shipping charges |
gross_weight |
Decimal | Total weight |
Relationship Fields¶
| Field | Type | Description |
|---|---|---|
lines |
One2Many | Stock movement lines |
lines_balance |
One2Many | Balance product lines |
expand_lines |
One2Many | Expanded bundle components |
comments |
One2Many | Discussion thread |
documents |
One2Many | Attached files |
containers |
One2Many | Container associations |
validate_lines |
One2Many | Validation records |
qc_results |
One2Many | Quality control results |
API Methods¶
1. Create Picking¶
Method: create(vals, context)
Creates a new stock picking record.
Parameters:
vals = {
"type": "in", # Required: in/out/internal
"journal_id": 1, # Required: journal ID
"contact_id": 5, # Optional: supplier/customer
"date": "2025-10-15 10:00:00",
"lines": [ # Stock movement lines
("create", {
"product_id": 10,
"qty": 100,
"uom_id": 1,
"location_from_id": 2,
"location_to_id": 3
})
]
}
context = {
"pick_type": "in", # Used for auto-number generation
"journal_id": 1
}
Returns: int - New picking ID
Example:
# Create a goods receipt
picking_id = get_model("stock.picking").create({
"type": "in",
"journal_id": 1,
"contact_id": 42,
"lines": [
("create", {
"product_id": 100,
"qty": 50,
"uom_id": 1,
"location_from_id": 5,
"location_to_id": 10,
"cost_price_cur": 25.50
})
]
}, context={"pick_type": "in"})
2. State Transitions¶
2.1 Set to Pending¶
Method: pending(ids, context)
Moves picking from draft to pending state and validates stock availability.
Parameters:
- ids (list): Picking IDs to process
Behavior: - Validates product types (stock/consumable/bundle) - Sets all lines to pending state - Checks stock availability - Expands bundle products
Example:
2.2 Approve Picking¶
Method: approve(ids, context)
Approves pending pickings for execution.
Permission Requirements:
- Goods Receipt: approve_pick_in
- Goods Issue: approve_pick_out
- Goods Transfer: approve_pick_internal
Example:
2.3 Validate (Complete) Picking¶
Method: set_done(ids, context)
Method: set_done_fast(ids, deactivate_lots, context) - Optimized version
Completes and validates the picking transaction.
Parameters:
context = {
"job_id": "task_123", # Optional: for progress tracking
"no_check_stock": False # Skip stock validation
}
deactivate_lots = True # Deactivate lots after transfer (fast method)
Behavior: - Expands bundles automatically - Validates QC requirements - Posts accounting journal entries - Updates stock balances - Checks negative stock
Example:
# Standard validation
get_model("stock.picking").set_done([123])
# Fast validation (bulk operations)
get_model("stock.picking").set_done_fast([123, 124, 125], deactivate_lots=True)
# Async validation (background task)
get_model("stock.picking").set_done_fast_async([123])
2.4 Void Picking¶
Method: void(ids, context)
Cancels a picking and reverses all movements.
Example:
2.5 Revert to Draft¶
Method: to_draft(ids, context)
Returns picking to draft state for editing.
Example:
3. Specialized Operations¶
3.1 Copy Picking¶
Method: copy(ids, from_location, to_location, state, type, context)
Creates a copy of an existing picking with optional modifications.
Parameters:
- from_location (str): Location code override
- to_location (str): Location code override
- state (str): Initial state (planned/approved)
- type (str): Override picking type
Example:
get_model("stock.picking").copy(
[123],
from_location="WH-IN",
to_location="WH-PROD",
state="approved",
type="internal"
)
3.2 Copy to Invoice¶
Method: copy_to_invoice(ids, context)
Generates customer/supplier invoice from picking.
Behavior: - Goods Issue (out): Creates customer invoice - Goods Receipt (in): Creates supplier invoice - Links invoice to picking - Transfers product pricing and taxes
Returns: Navigation to new invoice
Example:
result = get_model("stock.picking").copy_to_invoice([123])
# result = {
# "next": {
# "name": "view_invoice",
# "active_id": 456
# },
# "flash": "Customer invoice copied from goods issue"
# }
3.3 Expand Bundles¶
Method: expand_bundles(ids, context)
Expands bundle products into component lines.
Example:
3.4 Assign Lots¶
Method: assign_lots(ids, context)
Automatically assigns lots to picking lines based on product settings.
Lot Selection Methods:
- fifo: First In First Out
- fefo: First Expired First Out
- qty: Highest quantity first
Respects: - Minimum life remaining percentage - Maximum lots per sale order - Available quantity per lot
Example:
3.5 Expand Containers¶
Method: expand_containers(ids, context)
Method: expand_containers_async(ids, context) - Background version
Expands container contents into picking lines.
Example:
# Synchronous
get_model("stock.picking").expand_containers([123])
# Asynchronous (for large containers)
get_model("stock.picking").expand_containers_async([123])
3.6 Transfer Containers¶
Method: transfer_containers(journal_id, container_ids, vals, context)
Method: transfer_containers_fast(journal_id, container_ids, vals, deactivate_lots, context)
Creates and validates picking for container transfer in one operation.
Parameters:
journal_id = 5 # Transfer journal
container_ids = [10, 11, 12] # Containers to move
vals = { # Additional picking values
"ref": "TRANSFER-001",
"project_id": 3
}
deactivate_lots = True # Deactivate transferred lots
Returns:
Example:
# Fast synchronous transfer
result = get_model("stock.picking").transfer_containers_fast(
journal_id=5,
container_ids=[101, 102],
vals={"ref": "BATCH-A"},
deactivate_lots=True
)
# Async transfer (background task)
get_model("stock.picking").transfer_containers_fast_async(
journal_id=5,
container_ids=[101, 102, 103, 104]
)
4. Validation & Quality Control¶
4.1 QC Check¶
Method: qc_check(ids, context)
Validates quality control results before completion.
Requirements (from settings):
- require_qc_in: Goods receipts need QC
- require_qc_out: Goods issues need QC
Example:
4.2 Reject Picking¶
Method: reject(ids, context)
Rejects picking based on QC failure.
Example:
4.3 Validate with Barcode¶
Method: validate_barcode(ids, barcode, context)
Validates product pickup using barcode scanning.
Parameters:
- barcode (str): Product barcode/code
Returns:
Example:
5. Copy Operations¶
5.1 Copy to Landed Cost¶
Method: copy_to_landed(ids, context)
Creates landed cost allocation from goods receipt.
Example:
5.2 Copy to Return¶
Method: copy_to_return(ids, context)
Creates return picking (reverse movement).
Example:
5.3 Copy to Delivery Order¶
Method: copy_to_delivery(ids, context)
Creates 3PL delivery orders from goods issues.
Requirements: - Type must be "out" - Must have contact and shipping address
Example:
get_model("stock.picking").copy_to_delivery([123, 124])
# Returns: {"flash": "2 delivery orders created"}
5.4 Copy to Product Transform¶
Method: copy_to_transform(ids, context)
Creates product transformation from picking lines.
Example:
5.5 Copy to Grading¶
Method: copy_to_grading(ids, context)
Creates product grading session from picking.
Example:
6. Helper Methods¶
6.1 Check Stock Availability¶
Method: check_stock(ids, context)
Validates sufficient stock for movements.
Checks: - Negative stock prevention - Lot-specific stock levels - Location availability
Example:
6.2 Update Currency Rate¶
Method: set_currency_rate(ids, context)
Recalculates and sets exchange rate for picking.
Example:
6.3 Repost Journal Entry¶
Method: repost_journal_entry(ids, context)
Recreates accounting journal entry (for perpetual costing).
Example:
6.4 Update Invoice Reference¶
Method: update_invoice(ids, context)
Syncs invoice_id to all picking lines.
Example:
6.5 Check Duplicate Lots¶
Method: check_dup_lots(ids, context)
Validates no duplicate lots in picking.
Example:
UI Events (onchange methods)¶
onchange_contact¶
Triggered when contact is selected. Updates: - Shipping address - Default journal based on contact settings
Usage:
data = {
"contact_id": 42,
"type": "in"
}
result = get_model("stock.picking").onchange_contact(
context={"data": data}
)
onchange_journal¶
Triggered when journal changes. Updates: - Auto-generates picking number - Sets default from/to locations on lines
onchange_product¶
Triggered when product selected in line. Updates: - Default quantity to 1 - Product UOM - Cost/sale price - Default locations from journal - Landed cost tracking
onchange_container¶
Triggered when container selected. Populates lines with container contents.
onchange_uom2¶
Triggered when secondary UOM changes. Recalculates qty2.
Search Functions¶
Search by Product¶
Search by Container¶
Search by Contact Category¶
Computed Fields Functions¶
get_qty_total(ids, context)¶
Returns sum of all line quantities
get_qty_validated(ids, context)¶
Returns sum of validated quantities
get_cost_total(ids, context)¶
Returns total cost amount
get_total_weight(ids, context)¶
Returns net and gross weights
get_from_coords(ids, context)¶
Returns source location GPS coordinates
get_to_coords(ids, context)¶
Returns destination GPS coordinates
Workflow Integration¶
Trigger Events¶
The picking module fires workflow triggers:
self.trigger(ids, "approve_in") # After goods receipt approval
self.trigger(ids, "approve_out") # After goods issue approval
self.trigger(ids, "done") # After completion
self.trigger(ids, "reject_out") # After rejection
These can be configured in workflow automation.
Best Practices¶
1. Creating Pickings Programmatically¶
# Always provide pick_type in context
context = {"pick_type": "in", "journal_id": 1}
vals = {
"type": "in",
"journal_id": 1,
"contact_id": supplier_id,
"lines": [
("create", {
"product_id": prod_id,
"qty": 100,
"uom_id": uom_id,
"location_from_id": from_loc,
"location_to_id": to_loc,
"cost_price_cur": 50.00
})
]
}
pick_id = get_model("stock.picking").create(vals, context)
2. Batch Processing¶
For bulk operations, use fast methods:
# Instead of looping set_done()
picking_ids = [1, 2, 3, 4, 5]
get_model("stock.picking").set_done_fast(picking_ids)
# For very large batches, use async
get_model("stock.picking").set_done_fast_async(picking_ids)
3. Permission Checks¶
Always verify permissions before state changes:
from netforce import access
if access.check_permission_other("approve_pick_in"):
get_model("stock.picking").approve([pick_id])
else:
raise Exception("User lacks approval permission")
4. Error Handling¶
try:
get_model("stock.picking").set_done([pick_id])
except Exception as e:
if "out of stock" in str(e):
# Handle stock shortage
pass
elif "Missing QC" in str(e):
# Handle QC requirement
pass
else:
raise
Database Constraints¶
Unique Constraint¶
Each picking number must be unique per company and type.
Related Models¶
| Model | Relationship | Description |
|---|---|---|
stock.move |
One2Many | Movement lines |
stock.journal |
Many2One | Stock journal |
contact |
Many2One | Supplier/Customer |
address |
Many2One | Shipping address |
stock.container |
Many2One | Container |
stock.location |
Many2One | From/To locations |
account.move |
Many2One | Journal entry |
account.invoice |
Many2One | Related invoice |
sale.order |
Reference | Source sales order |
purchase.order |
Reference | Source purchase order |
project |
Reference | Related project |
Common Use Cases¶
Use Case 1: Receive Purchase Order¶
# 1. Create goods receipt from PO
po = get_model("purchase.order").browse(po_id)
vals = {
"type": "in",
"journal_id": 1,
"contact_id": po.supplier_id.id,
"related_id": "purchase.order,%s" % po.id,
"lines": []
}
for line in po.lines:
vals["lines"].append(("create", {
"product_id": line.product_id.id,
"qty": line.qty,
"uom_id": line.uom_id.id,
"location_to_id": warehouse_loc_id,
"cost_price_cur": line.unit_price
}))
pick_id = get_model("stock.picking").create(
vals,
context={"pick_type": "in"}
)
# 2. Validate receipt
get_model("stock.picking").pending([pick_id])
get_model("stock.picking").approve([pick_id])
get_model("stock.picking").set_done([pick_id])
Use Case 2: Ship Sales Order¶
# 1. Create goods issue from SO
so = get_model("sale.order").browse(so_id)
vals = {
"type": "out",
"journal_id": 2,
"contact_id": so.customer_id.id,
"ship_address_id": so.ship_address_id.id,
"related_id": "sale.order,%s" % so.id,
"lines": []
}
for line in so.lines:
vals["lines"].append(("create", {
"product_id": line.product_id.id,
"qty": line.qty,
"uom_id": line.uom_id.id,
"location_from_id": warehouse_loc_id,
}))
pick_id = get_model("stock.picking").create(
vals,
context={"pick_type": "out"}
)
# 2. Assign lots automatically
get_model("stock.picking").assign_lots([pick_id])
# 3. Validate shipment
get_model("stock.picking").pending([pick_id])
get_model("stock.picking").approve([pick_id])
get_model("stock.picking").set_done([pick_id])
# 4. Create customer invoice
get_model("stock.picking").copy_to_invoice([pick_id])
Use Case 3: Internal Transfer¶
# Transfer between locations
vals = {
"type": "internal",
"journal_id": 3,
"lines": [
("create", {
"product_id": prod_id,
"qty": 50,
"uom_id": uom_id,
"location_from_id": wh_main_id,
"location_to_id": wh_retail_id
})
]
}
pick_id = get_model("stock.picking").create(
vals,
context={"pick_type": "internal"}
)
get_model("stock.picking").set_done([pick_id])
Use Case 4: Container Transfer¶
# Move entire container between locations
result = get_model("stock.picking").transfer_containers_fast(
journal_id=5,
container_ids=[container_id],
vals={
"ref": "RELOCATION-2025-001",
"project_id": project_id
},
deactivate_lots=False
)
print("Created picking:", result["picking_number"])
Performance Tips¶
1. Use Fast Methods for Bulk¶
set_done_fast()instead ofset_done()for multiple pickingstransfer_containers_fast()for container moves- Async variants for background processing
2. Optimize Database Queries¶
# Bad: Loop with browse
for pick_id in picking_ids:
obj = get_model("stock.picking").browse(pick_id)
# process...
# Good: Use search_browse
for obj in get_model("stock.picking").search_browse([["id", "in", picking_ids]]):
# process...
3. Batch Container Transfers¶
# Process multiple containers in one operation
get_model("stock.picking").transfer_containers_fast(
journal_id=5,
container_ids=[c1, c2, c3, c4, c5]
)
Troubleshooting¶
"Product is out of stock"¶
Cause: Negative stock check enabled
Solution: Check stock.balance for product availability or disable check
"Missing QC results"¶
Cause: QC required by settings but not performed
Solution: Create qc.result records before validation
"User does not have permission"¶
Cause: Missing approve_pick_in/out/internal permission
Solution: Grant permission in user access settings
"Missing picking number"¶
Cause: Sequence not configured
Solution: Check stock.journal has valid sequence_id
"Journal entry not found"¶
Cause: Perpetual costing not configured
Solution: Verify stock journal has accounting settings
Testing Examples¶
Unit Test: Create and Validate Picking¶
def test_picking_lifecycle():
# Create draft picking
vals = {
"type": "in",
"journal_id": 1,
"lines": [
("create", {
"product_id": 100,
"qty": 10,
"uom_id": 1,
"location_to_id": 5
})
]
}
pick_id = get_model("stock.picking").create(
vals,
context={"pick_type": "in"}
)
# Verify draft state
obj = get_model("stock.picking").browse(pick_id)
assert obj.state == "draft"
# Approve
get_model("stock.picking").pending([pick_id])
get_model("stock.picking").approve([pick_id])
# Validate
get_model("stock.picking").set_done([pick_id])
# Verify completed
obj = obj.browse()[0] # Refresh
assert obj.state == "done"
assert obj.date_done is not None
Security Considerations¶
Permission Model¶
approve_pick_in- Approve goods receiptsapprove_pick_out- Approve goods issuesapprove_pick_internal- Approve transfers
Data Access¶
- Multi-company isolation via
company_id - Audit trail via
_audit_log=True - User tracking:
pending_by_id,done_by_id,done_approved_by_id
Version History¶
Last Updated: October 2025
Model Version: stock_picking.py
Framework: Netforce
Additional Resources¶
- Stock Move Documentation:
stock.move - Stock Balance Documentation:
stock.balance - Stock Journal Configuration:
stock.journal - Workflow Automation: Netforce Workflow Guide
Support & Feedback¶
For issues or questions about this module: 1. Check related model documentation 2. Review system logs for detailed error messages 3. Verify permissions and settings configuration 4. Test in development environment first
This documentation is generated for developer onboarding and reference purposes.