Skip to content

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:

_key = ["company_id", "type", "number"]

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:

CREATE UNIQUE INDEX stock_picking_key_uniq 
    ON stock_picking (company_id, type, number);

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

draft → pending (planned) → approved → qc_checked → done (completed)
                                     rejected
                           voided
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:

get_model("stock.picking").pending([123])


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:

get_model("stock.picking").approve([123])


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:

get_model("stock.picking").void([123])


2.5 Revert to Draft

Method: to_draft(ids, context)

Returns picking to draft state for editing.

Example:

get_model("stock.picking").to_draft([123])


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:

get_model("stock.picking").expand_bundles([123])


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:

get_model("stock.picking").assign_lots([123])


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:

{
    "picking_id": 789,
    "picking_number": "GT-2025-00123"
}

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:

get_model("stock.picking").qc_check([123])


4.2 Reject Picking

Method: reject(ids, context)

Rejects picking based on QC failure.

Example:

get_model("stock.picking").reject([123])


4.3 Validate with Barcode

Method: validate_barcode(ids, barcode, context)

Validates product pickup using barcode scanning.

Parameters: - barcode (str): Product barcode/code

Returns:

{
    "validate_line_id": 456  # Created validation line ID
}

Example:

result = get_model("stock.picking").validate_barcode(
    [123],
    "PROD-12345"
)


5. Copy Operations

5.1 Copy to Landed Cost

Method: copy_to_landed(ids, context)

Creates landed cost allocation from goods receipt.

Example:

get_model("stock.picking").copy_to_landed([123])


5.2 Copy to Return

Method: copy_to_return(ids, context)

Creates return picking (reverse movement).

Example:

# Returns goods receipt (creates goods issue)
get_model("stock.picking").copy_to_return([123])


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:

result = get_model("stock.picking").copy_to_transform([123])


5.5 Copy to Grading

Method: copy_to_grading(ids, context)

Creates product grading session from picking.

Example:

result = get_model("stock.picking").copy_to_grading([123])


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:

get_model("stock.picking").check_stock([123])


6.2 Update Currency Rate

Method: set_currency_rate(ids, context)

Recalculates and sets exchange rate for picking.

Example:

get_model("stock.picking").set_currency_rate([123])


6.3 Repost Journal Entry

Method: repost_journal_entry(ids, context)

Recreates accounting journal entry (for perpetual costing).

Example:

get_model("stock.picking").repost_journal_entry([123])


6.4 Update Invoice Reference

Method: update_invoice(ids, context)

Syncs invoice_id to all picking lines.

Example:

get_model("stock.picking").update_invoice([123])


6.5 Check Duplicate Lots

Method: check_dup_lots(ids, context)

Validates no duplicate lots in picking.

Example:

get_model("stock.picking").check_dup_lots([123])


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

# Find pickings containing specific product
condition = [["product_id", "=", 123]]

Search by Container

# Find pickings with container
condition = [["search_container_id", "=", 456]]

Search by Contact Category

# Find pickings for contact category
condition = [["contact_categ_id", "=", 10]]

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

UNIQUE (company_id, type, number)

Each picking number must be unique per company and type.


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 of set_done() for multiple pickings
  • transfer_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 receipts
  • approve_pick_out - Approve goods issues
  • approve_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.