Skip to content

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

draft → confirmed
draft (revert)
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:

context = {
    "job_id": "task_123"  # For progress tracking
}

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

# When marking lots as scanned, batch database writes
# Instead of individual writes for each scan

2. Index Lot Numbers

# Ensure lot.number is indexed for fast lookups
# Critical for barcode scanning performance

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:

get_model("stock.location").create({
    "name": "Inventory Adjustments",
    "type": "inventory"
})

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


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.