Custom Stock Count Lot Documentation¶
Overview¶
The Custom Stock Count Lot module (custom.stock.count.lot) represents individual lot/serial number scan records within a custom count session. Each record tracks whether a specific lot was scanned (present) or not scanned (absent), along with who scanned it and when. This model is the core data structure for the lot-centric counting methodology used in custom count sessions.
Model Information¶
Model Name: custom.stock.count.lot
Display Name: Count Lot
Key Fields: None (detail records)
Features¶
- ✅ User tracking - Records who scanned each lot
- ✅ Time stamping - Records scan timestamp
- ✅ Cascade delete - Removed with parent session
- ✅ Presence tracking - Scanned (user_id set) vs unscanned (user_id null)
- ✅ Product reference - Via computed field
Understanding Scanned vs Unscanned¶
Field Significance¶
The user_id field determines lot status:
| user_id | Meaning | Source |
|---|---|---|
| Not Null | Lot was scanned (present) | User scanned barcode |
| Null | Lot not scanned (absent) | Created by system |
Lot Record States¶
Unscanned (Expected) Scanned (Found)
┌──────────────────┐ ┌──────────────────┐
│ user_id: NULL │ → │ user_id: 123 │
│ time: NULL │ │ time: 14:30:00 │
│ prev_qty: 1 │ │ prev_qty: 1 │
└──────────────────┘ └──────────────────┘
(Missing) (Present)
Key Fields Reference¶
Core Fields¶
| Field | Type | Required | Description |
|---|---|---|---|
session_id |
Many2One | ✅ | Parent count session (cascade) |
lot_id |
Many2One | ✅ | Lot/serial number being tracked |
user_id |
Many2One | ❌ | User who scanned (null = unscanned) |
time |
DateTime | ❌ | When lot was scanned |
prev_qty |
Decimal | ❌ | Previous system quantity |
Computed Fields¶
| Field | Type | Description |
|---|---|---|
product_code |
Char | Product code via lot |
Computed Field Function¶
_get_related(ids, context)¶
Retrieves the product name from the related lot's product.
Returns: Dictionary mapping lot IDs to product names
Example:
lot_rec = get_model("custom.stock.count.lot").browse(lot_rec_id)
print(f"Product: {lot_rec.product_code}")
# Output: "Laptop Computer ThinkPad X1"
API Methods¶
1. Create Lot Record¶
Method: create(vals, context)
Creates a lot record in a count session.
Parameters:
vals = {
"session_id": 123, # Required: Parent session
"lot_id": 456, # Required: Lot/serial number
"user_id": 789, # Optional: If scanned
"time": "2024-10-27 14:30:00", # Optional: Scan time
"prev_qty": 1.0 # Optional: System quantity
}
Example:
# System creates unscanned lot record
lot_rec_id = get_model("custom.stock.count.lot").create({
"session_id": session_id,
"lot_id": lot_id,
"user_id": None, # Not yet scanned
"time": None,
"prev_qty": 1
})
2. Mark as Scanned¶
Method: write(vals, context)
Updates lot record to mark as scanned.
Example:
# When user scans barcode
lot_rec = get_model("custom.stock.count.lot").browse(lot_rec_id)
lot_rec.write({
"user_id": access.get_active_user(),
"time": time.strftime("%Y-%m-%d %H:%M:%S")
})
3. Query Scanned Lots¶
# Get all scanned lots in session
scanned_lots = get_model("custom.stock.count.lot").search_browse([
["session_id", "=", session_id],
["user_id", "!=", None] # Scanned
])
print(f"Scanned: {len(scanned_lots)}")
for lot in scanned_lots:
print(f" {lot.lot_id.number} by {lot.user_id.name} at {lot.time}")
4. Query Unscanned Lots¶
# Get all unscanned (missing) lots in session
unscanned_lots = get_model("custom.stock.count.lot").search_browse([
["session_id", "=", session_id],
["user_id", "=", None] # Not scanned
])
print(f"Missing: {len(unscanned_lots)}")
for lot in unscanned_lots:
print(f" {lot.lot_id.number} - {lot.product_code}")
Relationship to Parent Session¶
The parent custom.stock.count.session provides filtered views:
session = get_model("custom.stock.count.session").browse(session_id)
# Scanned lots (user_id != null)
session.lots # One2Many with condition
# Unscanned lots (user_id == null)
session.lots_remain # One2Many with condition
# All lots
session.all_lots # One2Many without condition
Common Use Cases¶
Use Case 1: Barcode Scan Handler¶
# Handle barcode scan during custom count
def handle_custom_count_scan(session_id, barcode):
"""
Process barcode scan in custom count session
"""
# Look up lot by barcode
lot_ids = get_model("stock.lot").search([
["number", "=", barcode]
])
if not lot_ids:
return {"error": f"Lot not found: {barcode}"}
lot_id = lot_ids[0]
# Find lot record in session
lot_rec_ids = get_model("custom.stock.count.lot").search([
["session_id", "=", session_id],
["lot_id", "=", lot_id]
])
if not lot_rec_ids:
return {"error": "Lot not in this count session"}
# Mark as scanned
lot_rec = get_model("custom.stock.count.lot").browse(lot_rec_ids[0])
# Check if already scanned
if lot_rec.user_id:
return {
"warning": f"Already scanned by {lot_rec.user_id.name} at {lot_rec.time}"
}
# Mark as scanned
lot_rec.write({
"user_id": access.get_active_user(),
"time": time.strftime("%Y-%m-%d %H:%M:%S")
})
return {
"success": True,
"message": f"✓ Scanned: {barcode}"
}
# Usage
result = handle_custom_count_scan(session_id, "SN-LAP-001")
Use Case 2: Progress Tracking¶
# Track custom count progress
def get_count_progress(session_id):
"""
Calculate count progress statistics
"""
all_lots = get_model("custom.stock.count.lot").search_browse([
["session_id", "=", session_id]
])
total = len(all_lots)
scanned = sum(1 for lot in all_lots if lot.user_id)
remaining = total - scanned
return {
"total": total,
"scanned": scanned,
"remaining": remaining,
"percent": f"{scanned/total*100:.1f}%" if total else "0%"
}
# Usage
progress = get_count_progress(session_id)
print(f"Progress: {progress['scanned']}/{progress['total']} ({progress['percent']})")
Use Case 3: Missing Item Report¶
# Generate report of missing items
def generate_missing_report(session_id):
"""
Create report of unscanned lots
"""
missing_lots = get_model("custom.stock.count.lot").search_browse([
["session_id", "=", session_id],
["user_id", "=", None] # Not scanned
])
if not missing_lots:
print("All items accounted for!")
return
print(f"\n=== Missing Items Report ===")
print(f"Session: {missing_lots[0].session_id.number}")
print(f"Date: {missing_lots[0].session_id.date}")
print(f"Missing: {len(missing_lots)}\n")
# Group by product
by_product = {}
for lot in missing_lots:
product = lot.lot_id.product_id
if product:
prod_key = (product.id, product.code, product.name)
if prod_key not in by_product:
by_product[prod_key] = []
by_product[prod_key].append(lot.lot_id.number)
# Print grouped report
for (prod_id, code, name), serials in sorted(by_product.items()):
print(f"{code} - {name}")
print(f" Missing: {len(serials)} units")
for serial in serials:
print(f" • {serial}")
print()
# Usage
generate_missing_report(session_id)
Use Case 4: Scan History Audit¶
# Audit who scanned what and when
def scan_history_audit(session_id):
"""
Generate scan history for audit purposes
"""
scanned_lots = get_model("custom.stock.count.lot").search_browse([
["session_id", "=", session_id],
["user_id", "!=", None]
], order="time")
print(f"\n=== Scan History Audit ===")
print(f"Session: {scanned_lots[0].session_id.number if scanned_lots else 'N/A'}\n")
# Group by user
by_user = {}
for lot in scanned_lots:
user_name = lot.user_id.name
if user_name not in by_user:
by_user[user_name] = []
by_user[user_name].append({
"serial": lot.lot_id.number,
"product": lot.product_code,
"time": lot.time
})
# Print by user
for user_name, scans in by_user.items():
print(f"{user_name}: {len(scans)} scans")
for scan in scans:
print(f" {scan['time']} - {scan['serial']}")
print()
# Usage
scan_history_audit(session_id)
Use Case 5: Re-scan Detection¶
# Prevent/detect duplicate scans
def check_duplicate_scan(session_id, barcode):
"""
Check if lot already scanned
"""
# Find lot
lot_ids = get_model("stock.lot").search([
["number", "=", barcode]
])
if not lot_ids:
return {"error": "Lot not found"}
# Check if in session
lot_recs = get_model("custom.stock.count.lot").search_browse([
["session_id", "=", session_id],
["lot_id", "=", lot_ids[0]]
])
if not lot_recs:
return {"error": "Lot not in this session"}
lot_rec = lot_recs[0]
if lot_rec.user_id:
# Already scanned
return {
"duplicate": True,
"message": f"Already scanned by {lot_rec.user_id.name}",
"scan_time": lot_rec.time,
"action": "skip" # Don't re-scan
}
else:
# Not yet scanned
return {
"duplicate": False,
"message": "Ready to scan",
"action": "proceed"
}
# Usage before scanning
check = check_duplicate_scan(session_id, "SN-001")
if check.get("duplicate"):
print(check["message"])
Best Practices¶
1. Always Set Both user_id and time Together¶
# Good: Set both when marking as scanned
lot_rec.write({
"user_id": access.get_active_user(),
"time": time.strftime("%Y-%m-%d %H:%M:%S")
})
# Bad: Setting only user_id
lot_rec.write({"user_id": user_id}) # Missing time!
2. Use Conditions for Filtering¶
# Good: Use One2Many conditions on parent
# Parent session model has:
"lots": fields.One2Many(..., condition=[["user_id","!=",None]])
"lots_remain": fields.One2Many(..., condition=[["user_id","=",None]])
# Then simply access:
session.lots # Scanned
session.lots_remain # Unscanned
# Bad: Filtering manually every time
all_lots = session.all_lots
scanned = [l for l in all_lots if l.user_id] # Inefficient
3. Handle Missing Products Gracefully¶
# Good: Check product exists
lot_rec = get_model("custom.stock.count.lot").browse(lot_rec_id)
if lot_rec.lot_id.product_id:
product_name = lot_rec.product_code
else:
product_name = "Unknown Product"
# Bad: Assuming product always exists
product_name = lot_rec.lot_id.product_id.name # May fail!
Performance Considerations¶
Bulk Scan Processing¶
# When scanning many items, batch database updates
scans_to_process = [
("SN-001", user_id, timestamp1),
("SN-002", user_id, timestamp2),
# ... many more
]
# Process in batch
for serial, user_id, timestamp in scans_to_process:
lot_ids = get_model("stock.lot").search([["number", "=", serial]])
if lot_ids:
lot_rec_ids = get_model("custom.stock.count.lot").search([
["session_id", "=", session_id],
["lot_id", "=", lot_ids[0]]
])
if lot_rec_ids:
get_model("custom.stock.count.lot").write(lot_rec_ids, {
"user_id": user_id,
"time": timestamp
})
Troubleshooting¶
Lot Record Not Found in Session¶
Cause: Lot not loaded into session or wrong session ID.
Solution: Verify lot is in session:
lot_recs = get_model("custom.stock.count.lot").search([
["session_id", "=", session_id],
["lot_id.number", "=", barcode]
])
if not lot_recs:
print("Lot not in this session - call update_lots_remain()")
Cannot Update Confirmed Session¶
Cause: Session already confirmed, lots locked.
Solution: Revert to draft first:
session = get_model("custom.stock.count.session").browse(session_id)
if session.state == "confirmed":
session.to_draft() # Then can update lots
Product Code Not Showing¶
Cause: Computed field not refreshed or lot has no product.
Solution: Refresh and verify:
lot_rec = get_model("custom.stock.count.lot").browse(lot_rec_id)
lot_rec = lot_rec.browse()[0] # Refresh
if lot_rec.lot_id.product_id:
print(lot_rec.product_code)
else:
print("Lot has no associated product")
Related Models¶
| Model | Relationship | Description |
|---|---|---|
custom.stock.count.session |
Many2One | Parent session (cascade) |
stock.lot |
Many2One | Lot/serial being tracked |
base.user |
Many2One | User who scanned |
product |
Referenced | Via lot.product_id |
Version History¶
Last Updated: 2024-10-27
Model Version: custom_stock_count_lot.py
Framework: Netforce
Additional Resources¶
- Custom Stock Count Session Documentation:
custom.stock.count.session - Stock Lot Documentation:
stock.lot - Stock Balance Documentation:
stock.balance
This documentation is generated for developer onboarding and reference purposes.