Skip to content

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")


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.