Custom Stock Count Session Documentation¶
Overview¶
The Custom Stock Count Session module (custom.stock.count.session) provides a specialized counting workflow based on lot/serial number scanning. Unlike traditional stock counting that focuses on product quantities, this module implements a "present/absent" counting methodology where scanned lots are marked as present (quantity=1) and unscanned lots are marked as absent (quantity=0). This is particularly useful for high-value serialized inventory where individual unit tracking is critical.
Model Information¶
Model Name: custom.stock.count.session
Display Name: Stock Count Session
Key Fields: None
Features¶
- ✅ Lot-centric counting - Focus on serial/lot presence
- ✅ User tracking - Records who scanned each lot
- ✅ Time stamping - Tracks when each lot was scanned
- ✅ Remaining lots tracking - Identifies unscanned items
- ✅ Binary quantities - Scanned=1, Unscanned=0
- ✅ Automatic sequence numbering
- ✅ Draft/Confirmed workflow
Understanding Custom Count Sessions¶
Traditional vs Custom Counting¶
Traditional Stock Count: - Focus: Product quantities - Input: Count each product's total quantity - Result: Adjustments based on variance
Custom Count Session: - Focus: Individual lot/serial presence - Input: Scan each lot present - Result: Present lots=1, Absent lots=0
Use Case Scenarios¶
Perfect for: - ✅ Serialized electronics (laptops, phones, tools) - ✅ High-value equipment with serial numbers - ✅ Asset tracking and verification - ✅ Warranty item audits - ✅ Theft prevention audits
Not ideal for: - ❌ Bulk products without serial numbers - ❌ Consumables counted by weight/volume - ❌ High-volume low-value items
Key Fields Reference¶
Header Fields¶
| Field | Type | Required | Description |
|---|---|---|---|
number |
Char | ✅ | Session number (auto-generated) |
date |
Date | ✅ | Date of count session |
location_id |
Many2One | ✅ | Warehouse location |
state |
Selection | ✅ | Status (draft/confirmed) |
Filter Fields¶
| Field | Type | Description |
|---|---|---|
master_product_id |
Many2One | Filter by master product |
prod_categ_id |
Many2One | Filter by product category |
product_code |
Char | Filter by product code pattern |
Relationship Fields¶
| Field | Type | Description |
|---|---|---|
lots |
One2Many | Scanned lots (user_id not null) |
lots_remain |
One2Many | Unscanned lots (user_id is null) |
all_lots |
One2Many | All lots in session |
stock_moves |
One2Many | Generated stock movements |
State Workflow¶
| State | Description |
|---|---|
draft |
Session in progress, lots can be scanned |
confirmed |
Session completed, movements created |
State transitions:
- to_draft() - Revert to draft, delete movements
- confirm() - Complete session, create adjustments
API Methods¶
1. Create Count Session¶
Method: create(vals, context)
Creates a new custom count session with automatic numbering.
Parameters:
vals = {
"date": "2024-10-27", # Required: Count date
"location_id": 123, # Required: Location ID
"master_product_id": 456, # Optional: Filter by master product
"prod_categ_id": 789, # Optional: Filter by category
"product_code": "LAP", # Optional: Filter by code pattern
}
Returns: int - New session ID
Example:
# Create session for laptop serial numbers
session_id = get_model("custom.stock.count.session").create({
"date": "2024-10-27",
"location_id": warehouse_id,
"master_product_id": laptop_master_id, # Only laptops
"state": "draft"
})
2. Update Remaining Lots¶
Method: update_lots_remain(ids, context)
Populates the unscanned lots list based on stock balances and filters.
Parameters:
- ids (list): Session IDs
Behavior:
1. Gets all stock balances at location
2. Applies session filters (master product, category, code pattern)
3. Compares against already scanned lots
4. Creates lot records for unscanned items with user_id=None
5. Deletes old remaining lot records first
Example:
# After creating session, load expected lots
session = get_model("custom.stock.count.session").browse(session_id)
session.update_lots_remain()
# Now session.lots_remain contains unscanned lots
print(f"Lots to scan: {len(session.lots_remain)}")
3. Update Previous Quantities¶
Method: update_prev_qtys(ids, context)
Updates previous quantities for all lots in the session based on stock balances.
Parameters:
- ids (list): Session IDs
Behavior:
- For each lot in session, computes stock balance at session date
- Updates prev_qty field
- Used before confirmation to ensure accurate comparison
Example:
# Before confirming, refresh quantities
session = get_model("custom.stock.count.session").browse(session_id)
session.update_prev_qtys()
# Review lots with mismatches
for lot in session.lots:
if lot.prev_qty != 1:
print(f"Warning: {lot.lot_id.number} has prev_qty={lot.prev_qty}")
4. Confirm Session¶
Method: confirm(ids, context)
Completes the count session by creating stock movements for variances.
Parameters:
- ids (list): Session IDs
- context (dict): Optional job_id for background processing
Context Options:
Behavior: 1. Updates previous quantities for all lots 2. For each scanned lot (user_id not null): - If prev_qty != 1: Creates movement to adjust to qty=1 3. For each unscanned lot (user_id is null): - If prev_qty != 0: Creates movement to adjust to qty=0 4. Sets state to 'confirmed'
Movement Logic:
# Scanned lot (should be present)
if scanned:
new_qty = 1
else:
new_qty = 0
diff_qty = new_qty - prev_qty
if diff_qty > 0:
# Move FROM inventory loss TO warehouse
create_movement(from=inv_loc, to=warehouse, qty=diff_qty)
elif diff_qty < 0:
# Move FROM warehouse TO inventory loss
create_movement(from=warehouse, to=inv_loc, qty=-diff_qty)
Example:
# Complete the session
get_model("custom.stock.count.session").confirm([session_id])
# Check created movements
session = get_model("custom.stock.count.session").browse(session_id)
print(f"State: {session.state}")
print(f"Movements created: {len(session.stock_moves)}")
5. Return to Draft¶
Method: to_draft(ids, context)
Reverts confirmed session to draft, deleting all stock movements.
Parameters:
- ids (list): Session IDs
Behavior: - Deletes all related stock movements - Changes state back to 'draft' - Allows re-scanning and re-confirmation
Example:
# Revert to make corrections
get_model("custom.stock.count.session").to_draft([session_id])
# Now can scan more lots or adjust
Workflow Integration¶
Complete Custom Count Workflow¶
1. Create Session
↓
2. Load Expected Lots (update_lots_remain)
↓
3. Scan Present Items
↓
4. Review Remaining
↓
5. Confirm Session
↓
6. Movements Created
Common Use Cases¶
Use Case 1: Laptop Serial Number Count¶
# Complete workflow for counting laptops
# Step 1: Create session
session_id = get_model("custom.stock.count.session").create({
"date": "2024-10-27",
"location_id": warehouse_id,
"master_product_id": laptop_master_id,
"prod_categ_id": electronics_category_id
})
# Step 2: Load expected laptop serial numbers
session = get_model("custom.stock.count.session").browse(session_id)
session.update_lots_remain()
print(f"Expected laptops: {len(session.lots_remain)}")
# Step 3: Scan each laptop (simulated)
scanned_serials = [
"LAP-SN-001",
"LAP-SN-002",
"LAP-SN-003",
# ... continue scanning
]
for serial in scanned_serials:
# Find lot by serial number
lot_ids = get_model("stock.lot").search([
["number", "=", serial]
])
if lot_ids:
# Check if lot is in session
lot_rec_ids = get_model("custom.stock.count.lot").search([
["session_id", "=", session_id],
["lot_id", "=", lot_ids[0]]
])
if lot_rec_ids:
# Mark as scanned
lot_rec = get_model("custom.stock.count.lot").browse(lot_rec_ids[0])
lot_rec.write({
"user_id": access.get_active_user(),
"time": time.strftime("%Y-%m-%d %H:%M:%S")
})
print(f"✓ Scanned: {serial}")
# Step 4: Review what's missing
session = session.browse()[0] # Refresh
print(f"\nScanned: {len(session.lots)}")
print(f"Missing: {len(session.lots_remain)}")
for lot in session.lots_remain:
print(f" Not found: {lot.lot_id.number}")
# Step 5: Confirm session
session.confirm()
print(f"Session confirmed. Movements: {len(session.stock_moves)}")
Use Case 2: Tool Audit by Category¶
# Audit all power tools in warehouse
# Create session for power tools
session_id = get_model("custom.stock.count.session").create({
"date": "2024-10-27",
"location_id": warehouse_id,
"prod_categ_id": power_tools_category_id
})
# Load expected tools
session = get_model("custom.stock.count.session").browse(session_id)
session.update_lots_remain()
# Print checklist for manual scanning
print("=== Power Tool Audit Checklist ===")
for lot in session.lots_remain:
print(f"[ ] {lot.lot_id.number} - {lot.lot_id.product_id.name}")
# After scanning (manually mark as found)
# ... scanning process ...
# Confirm
session.confirm()
Use Case 3: Asset Verification with Filters¶
# Verify assets matching specific code pattern
# Count all items with "ASSET-" prefix
session_id = get_model("custom.stock.count.session").create({
"date": "2024-10-27",
"location_id": warehouse_id,
"product_code": "ASSET-" # Pattern match
})
session = get_model("custom.stock.count.session").browse(session_id)
session.update_lots_remain()
print(f"Assets to verify: {len(session.lots_remain)}")
# Organize by product for systematic scanning
by_product = {}
for lot in session.lots_remain:
prod_code = lot.lot_id.product_id.code
if prod_code not in by_product:
by_product[prod_code] = []
by_product[prod_code].append(lot.lot_id.number)
# Print organized checklist
for prod_code, serials in sorted(by_product.items()):
print(f"\n{prod_code}: {len(serials)} units")
for serial in serials:
print(f" [ ] {serial}")
Use Case 4: Theft Prevention Audit¶
# Regular audit to detect missing high-value items
def monthly_theft_audit(location_id, high_value_category_id):
"""
Monthly audit of high-value items
"""
# Create session
session_id = get_model("custom.stock.count.session").create({
"date": time.strftime("%Y-%m-%d"),
"location_id": location_id,
"prod_categ_id": high_value_category_id
})
session = get_model("custom.stock.count.session").browse(session_id)
session.update_lots_remain()
# Expected items
expected_count = len(session.lots_remain)
# Scan items (in real implementation)
# ... scanning process ...
# Analyze results
session = session.browse()[0]
found_count = len(session.lots)
missing_count = len(session.lots_remain)
# Alert if items missing
if missing_count > 0:
alert_message = f"⚠️ AUDIT ALERT: {missing_count} items missing!"
print(alert_message)
# List missing items
for lot in session.lots_remain:
prod = lot.lot_id.product_id
print(f" Missing: {lot.lot_id.number} - {prod.name} (${prod.list_price})")
# Send alert (email/notification)
# send_security_alert(session_id, missing_items)
# Confirm session
session.confirm()
return {
"session_id": session_id,
"expected": expected_count,
"found": found_count,
"missing": missing_count
}
# Run monthly audit
result = monthly_theft_audit(main_warehouse_id, high_value_categ_id)
Use Case 5: Warranty Item Verification¶
# Verify items under warranty
def warranty_verification(location_id, warranty_end_date):
"""
Verify items still under warranty
"""
# Get products with warranties ending soon
products = get_model("product").search_browse([
["warranty_end", "<=", warranty_end_date],
["type", "=", "stock"]
])
product_ids = [p.id for p in products]
# Create session
session_id = get_model("custom.stock.count.session").create({
"date": time.strftime("%Y-%m-%d"),
"location_id": location_id
})
# Manually add lots for these products
session = get_model("custom.stock.count.session").browse(session_id)
for prod_id in product_ids:
# Get lots for this product at location
balances = get_model("stock.balance").search_browse([
["location_id", "=", location_id],
["product_id", "=", prod_id],
["lot_id", "!=", None],
["qty_phys", ">", 0]
])
for bal in balances:
get_model("custom.stock.count.lot").create({
"session_id": session_id,
"lot_id": bal.lot_id.id,
"prev_qty": bal.qty_phys
})
# Update remaining
session.update_lots_remain()
print(f"Warranty items to verify: {len(session.lots_remain)}")
# After scanning, confirm
# session.confirm()
return session_id
Best Practices¶
1. Filter Appropriately¶
# Good: Use filters to narrow scope
# Count specific category
session_id = get_model("custom.stock.count.session").create({
"location_id": warehouse_id,
"date": "2024-10-27",
"prod_categ_id": electronics_id # Narrow to electronics
})
# Bad: No filters for large inventory
# (Results in thousands of lots to scan)
session_id = get_model("custom.stock.count.session").create({
"location_id": warehouse_id,
"date": "2024-10-27"
# No filters - ALL lots!
})
2. Always Update Remaining Before Scanning¶
# Good: Load expected lots first
session = get_model("custom.stock.count.session").browse(session_id)
session.update_lots_remain() # Populate expected list
# NOW start scanning
# Bad: Start scanning without loading expected
# (Can't track what's missing)
3. Review Before Confirming¶
# Good: Review results before confirmation
session = get_model("custom.stock.count.session").browse(session_id)
# Calculate accuracy
total = len(session.all_lots)
scanned = len(session.lots)
missing = len(session.lots_remain)
accuracy = (scanned / total * 100) if total else 100
print(f"Accuracy: {accuracy:.1f}%")
print(f"Scanned: {scanned}/{total}")
print(f"Missing: {missing}")
# Only confirm if acceptable
if accuracy >= 95: # 95% threshold
session.confirm()
else:
print("Warning: Low accuracy - recount recommended")
4. Handle Background Processing¶
# Good: Use job tracking for large sessions
import uuid
# Create job
job_id = str(uuid.uuid4())
# Confirm with progress tracking
get_model("custom.stock.count.session").confirm([session_id], context={
"job_id": job_id
})
# Monitor progress
while True:
status = tasks.get_status(job_id)
if status['state'] in ['done', 'error']:
break
print(f"Progress: {status.get('progress', 0)}%")
time.sleep(1)
Performance Tips¶
1. Batch Lot Updates¶
2. Index Lot Numbers¶
3. Filter Aggressively¶
# Use all available filters to reduce lot count
session_id = get_model("custom.stock.count.session").create({
"location_id": warehouse_id,
"master_product_id": master_id, # Filter 1
"prod_categ_id": category_id, # Filter 2
"product_code": "PREFIX-" # Filter 3
})
# Better than loading all lots
Troubleshooting¶
"Inventory loss location not found"¶
Cause: Missing inventory adjustment location.
Solution: Create inventory location:
No Lots in Remaining List¶
Cause: Filters too restrictive or no stock balances match.
Solution: Check filters and stock:
# Verify stock exists
balances = get_model("stock.balance").search_browse([
["location_id", "=", location_id],
["lot_id", "!=", None]
])
print(f"Lots in stock: {len(balances)}")
Duplicate Lots in Session¶
Cause: Multiple calls to update_lots_remain().
Solution: Method deletes old records first, but verify:
session = get_model("custom.stock.count.session").browse(session_id)
session.update_lots_remain() # Call once
Related Models¶
| Model | Relationship | Description |
|---|---|---|
custom.stock.count.lot |
One2Many | Lot scan records |
stock.location |
Many2One | Warehouse location |
stock.move |
One2Many | Adjustment movements |
product |
Referenced | Via master_product_id filter |
product.categ |
Many2One | Category filter |
settings |
Referenced | For journal configuration |
Version History¶
Last Updated: 2024-10-27
Model Version: custom_stock_count_session.py
Framework: Netforce
Additional Resources¶
- Custom Stock Count Lot Documentation:
custom.stock.count.lot - Stock Lot Documentation:
stock.lot - Stock Balance Documentation:
stock.balance - Stock Move Documentation:
stock.move
This documentation is generated for developer onboarding and reference purposes.