Stock Count Line Documentation¶
Overview¶
The Stock Count Line module (stock.count.line) represents individual product count entries within a stock count. Each line captures the previous (system) quantity, the new (physical) quantity counted, cost information, and lot/serial number details. This model is the detail level of inventory counting where actual variances are recorded and reconciled against system records.
Model Information¶
Model Name: stock.count.line
Display Name: Stock Count Line
Key Fields: None (detail records)
Features¶
- ❌ No audit logging (parent count is audited)
- ❌ No multi-company (inherits from parent)
- ✅ Cascade delete (removed when count deleted)
- ✅ Computed cost amounts
- ✅ Lot/serial tracking support
- ✅ Secondary UOM support (qty2)
Key Fields Reference¶
Header Fields¶
| Field | Type | Required | Description |
|---|---|---|---|
count_id |
Many2One | ✅ | Parent stock count (cascade delete) |
product_id |
Many2One | ❌ | Product being counted (stock type only) |
lot_id |
Many2One | ❌ | Lot or serial number for this count |
system_lot_id |
Many2One | ❌ | System-suggested lot number |
Quantity Fields¶
| Field | Type | Required | Description |
|---|---|---|---|
prev_qty |
Decimal(6) | ✅ | System quantity before count (readonly) |
new_qty |
Decimal(6) | ✅ | Physical quantity counted |
qty2 |
Decimal(6) | ❌ | Secondary quantity (if dual UOM) |
uom_id |
Many2One | ✅ | Unit of measure (readonly) |
uom2 |
Many2One | ❌ | Secondary unit of measure (readonly) |
Cost Fields¶
| Field | Type | Required | Description |
|---|---|---|---|
prev_cost_price |
Decimal(6) | ❌ | Previous average cost (computed) |
prev_cost_amount |
Decimal | ❌ | Previous total cost value |
unit_price |
Decimal(6) | ❌ | New cost price per unit |
new_cost_amount |
Decimal | ❌ | New total cost (computed: qty × price) |
Additional Fields¶
| Field | Type | Description |
|---|---|---|
bin_location |
Char | Warehouse bin/location code (readonly) |
recon |
Char | Reconciliation status (readonly) |
lot_weight |
Decimal | Weight from lot record (computed) |
cyclecount_id |
Many2One | Associated cycle count program |
Understanding Count Line Workflow¶
Line Creation Process¶
Step 1: Count Created - Parent stock count is in draft state - No lines exist yet
Step 2: Lines Added
- Lines created via stock.count.add_lines() or manually
- prev_qty populated from stock balance
- prev_cost_amount populated from stock balance
- new_qty initialized (zero or copied from prev_qty)
- bin_location populated from product settings
Step 3: Quantities Entered
- Users update new_qty based on physical count
- Can use barcode scanning or manual entry
- System calculates new_cost_amount automatically
Step 4: Count Validated - Parent count validated → creates stock movements - Lines become readonly - Variances posted to inventory adjustment location
Computed Fields Functions¶
get_prev_cost_price(ids, context)¶
Calculates the previous average cost per unit from the previous cost amount and quantity.
Formula:
Returns: Dictionary mapping line IDs to cost prices
Example:
get_new_cost_amount(ids, context)¶
Calculates the new total cost amount from new quantity and unit price.
Formula:
Returns: Dictionary mapping line IDs to cost amounts
Example:
Field Relationships¶
Cascade Deletion¶
When a stock count is deleted, all its lines are automatically deleted due to on_delete="cascade" on the count_id field.
Product Filtering¶
The product_id field is restricted to stock-type products only:
API Methods¶
1. Create Count Line¶
Method: create(vals, context)
Creates a new count line within a stock count.
Parameters:
vals = {
"count_id": 123, # Required: Parent stock count ID
"product_id": 456, # Required: Product to count
"lot_id": 789, # Optional: Lot/serial number
"prev_qty": 100.0, # Required: System quantity
"prev_cost_amount": 2500.00, # Optional: System cost value
"new_qty": 98.0, # Required: Physical count
"unit_price": 25.00, # Optional: Cost price
"uom_id": 1, # Required: Unit of measure
"bin_location": "A-01-05" # Optional: Bin location
}
Returns: int - New line ID
Example:
# Create count line manually
line_id = get_model("stock.count.line").create({
"count_id": count_id,
"product_id": product_id,
"lot_id": lot_id,
"prev_qty": 50,
"new_qty": 48,
"unit_price": 12.50,
"uom_id": 1,
"bin_location": "A-02-10"
})
2. Update Count Line¶
Method: write(vals, context)
Updates count line quantities during the counting process.
Parameters:
vals = {
"new_qty": 95.0, # Update counted quantity
"unit_price": 26.00 # Update cost price if needed
}
Example:
# Update line with physical count
line = get_model("stock.count.line").browse(line_id)
line.write({
"new_qty": 47 # Adjust based on recount
})
3. Delete Count Line¶
Method: delete(ids, **kw)
Deletes count lines (only allowed while count in draft state).
Example:
Common Use Cases¶
Use Case 1: Manual Line Creation¶
# Create stock count
count_id = get_model("stock.count").create({
"location_id": warehouse_id,
"date": "2024-10-27 14:00:00",
"memo": "Spot check"
})
# Manually add specific products
products_to_count = [
{"product_id": 101, "bin": "A-01-05"},
{"product_id": 102, "bin": "A-01-06"},
{"product_id": 103, "bin": "A-02-01"}
]
for item in products_to_count:
prod = get_model("product").browse(item["product_id"])
# Get current stock balance
prev_qty = get_model("stock.balance").get_qty_phys(
warehouse_id,
prod.id
)
prev_cost = get_model("stock.balance").get_unit_price(
warehouse_id,
prod.id
)
# Create line with system data
line_id = get_model("stock.count.line").create({
"count_id": count_id,
"product_id": prod.id,
"prev_qty": prev_qty,
"prev_cost_amount": prev_qty * prev_cost,
"new_qty": prev_qty, # Start with system qty
"unit_price": prev_cost,
"uom_id": prod.uom_id.id,
"bin_location": item["bin"]
})
print(f"Created line for {prod.code} at {item['bin']}")
Use Case 2: Barcode Scanning Entry¶
# Count using barcode scanner
count_id = 150 # Existing stock count
def scan_item(barcode):
"""Process barcode scan during counting"""
# Look up lot by barcode
lot_ids = get_model("stock.lot").search([["number", "=", barcode]])
if not lot_ids:
raise Exception(f"Lot not found: {barcode}")
lot = get_model("stock.lot").browse(lot_ids[0])
prod = lot.product_id
# Check if line already exists
existing = get_model("stock.count.line").search([
["count_id", "=", count_id],
["product_id", "=", prod.id],
["lot_id", "=", lot.id]
])
if existing:
# Increment existing line
line = get_model("stock.count.line").browse(existing[0])
line.write({"new_qty": line.new_qty + 1})
print(f"Updated {prod.code} - {lot.number}: {line.new_qty}")
else:
# Create new line
count = get_model("stock.count").browse(count_id)
prev_qty = get_model("stock.balance").get_qty_phys(
count.location_id.id,
prod.id,
lot.id
)
line_id = get_model("stock.count.line").create({
"count_id": count_id,
"product_id": prod.id,
"lot_id": lot.id,
"prev_qty": prev_qty,
"new_qty": 1,
"unit_price": prod.cost_price,
"uom_id": prod.uom_id.id
})
print(f"Created {prod.code} - {lot.number}: 1")
# Scan items
scan_item("LOT-2024-001")
scan_item("LOT-2024-001") # Same item again
scan_item("LOT-2024-002")
Use Case 3: Variance Analysis¶
# Analyze variances before validating count
count = get_model("stock.count").browse(count_id)
print("=== Stock Count Variance Report ===")
print(f"Count: {count.number} - {count.date}")
print(f"Location: {count.location_id.name}\n")
total_variance_qty = 0
total_variance_value = 0
for line in count.lines:
variance_qty = line.new_qty - line.prev_qty
variance_value = line.new_cost_amount - (line.prev_cost_amount or 0)
if variance_qty != 0:
variance_pct = (variance_qty / line.prev_qty * 100) if line.prev_qty else 0
print(f"Product: {line.product_id.code}")
print(f" Location: {line.bin_location or 'N/A'}")
if line.lot_id:
print(f" Lot: {line.lot_id.number}")
print(f" Previous: {line.prev_qty} @ ${line.prev_cost_price:.2f}")
print(f" New: {line.new_qty} @ ${line.unit_price:.2f}")
print(f" Variance: {variance_qty:+.2f} ({variance_pct:+.1f}%)")
print(f" Value Impact: ${variance_value:+.2f}\n")
total_variance_qty += abs(variance_qty)
total_variance_value += variance_value
print(f"Total Items with Variances: {sum(1 for l in count.lines if l.new_qty != l.prev_qty)}")
print(f"Total Quantity Variance: {total_variance_qty:.2f}")
print(f"Total Value Variance: ${total_variance_value:+.2f}")
Use Case 4: Lot-Specific Counting¶
# Count specific lot-tracked products
count_id = get_model("stock.count").create({
"location_id": warehouse_id,
"date": "2024-10-27 14:00:00",
"memo": "Serialized Items Audit"
})
# Get lot-tracked products
lot_tracked_products = get_model("product").search([
["type", "=", "stock"],
["lot_tracking", "=", True]
])
for prod_id in lot_tracked_products:
# Get all lots for this product at location
balances = get_model("stock.balance").search_browse([
["location_id", "=", warehouse_id],
["product_id", "=", prod_id],
["lot_id", "!=", None]
])
for bal in balances:
if bal.qty_phys > 0:
# Create line for each lot
get_model("stock.count.line").create({
"count_id": count_id,
"product_id": bal.product_id.id,
"lot_id": bal.lot_id.id,
"prev_qty": bal.qty_phys,
"prev_cost_amount": bal.amount,
"new_qty": 0, # Will be counted
"unit_price": bal.amount / bal.qty_phys if bal.qty_phys else 0,
"uom_id": bal.product_id.uom_id.id
})
print(f"Created count with {len(count.lines)} lot-tracked lines")
Use Case 5: Bulk Quantity Update¶
# Update multiple lines based on physical count sheet
count = get_model("stock.count").browse(count_id)
# Physical count results (from paper sheets or mobile app)
count_results = {
101: 95, # Product ID: New Quantity
102: 148,
103: 0, # Out of stock
104: 52,
}
for line in count.lines:
prod_id = line.product_id.id
if prod_id in count_results:
new_qty = count_results[prod_id]
line.write({"new_qty": new_qty})
variance = new_qty - line.prev_qty
if variance != 0:
print(f"Updated {line.product_id.code}: {line.prev_qty} → {new_qty} ({variance:+.0f})")
else:
print(f"Warning: No count for {line.product_id.code}")
print(f"\nUpdated {len(count_results)} lines")
Use Case 6: Dual UOM Counting¶
# Count products with dual units of measure (e.g., cases and pieces)
count_id = get_model("stock.count").create({
"location_id": warehouse_id,
"date": "2024-10-27 14:00:00",
"memo": "Dual UOM Count"
})
# Product sold in cases (12 pieces each) and pieces
product = get_model("product").browse(product_id)
# Previous stock: 10 cases + 5 pieces = 125 pieces total
prev_qty_pieces = 125
prev_qty_cases = 10.416667 # 125 / 12
# Physical count: 9 cases + 8 pieces = 116 pieces
new_qty_cases = 9
new_qty_pieces = 8
new_qty_total = (new_qty_cases * 12) + new_qty_pieces # 116
line_id = get_model("stock.count.line").create({
"count_id": count_id,
"product_id": product.id,
"prev_qty": prev_qty_pieces,
"new_qty": new_qty_total,
"qty2": new_qty_cases, # Track cases separately
"uom_id": product.uom_id.id, # Pieces
"uom2": product.sale_uom_id.id, # Cases
"unit_price": product.cost_price
})
print(f"Counted: {new_qty_cases} cases + {new_qty_pieces} pieces = {new_qty_total} pieces")
print(f"Variance: {new_qty_total - prev_qty_pieces} pieces")
Best Practices¶
1. Always Set Previous Quantities¶
# Bad: Creating line without previous quantity context
get_model("stock.count.line").create({
"count_id": count_id,
"product_id": product_id,
"new_qty": 50, # What was it before?
"uom_id": 1
})
# Good: Include system quantity for comparison
prev_qty = get_model("stock.balance").get_qty_phys(location_id, product_id)
prev_cost = get_model("stock.balance").get_unit_price(location_id, product_id)
get_model("stock.count.line").create({
"count_id": count_id,
"product_id": product_id,
"prev_qty": prev_qty,
"prev_cost_amount": prev_qty * prev_cost,
"new_qty": 50,
"unit_price": prev_cost,
"uom_id": 1
})
2. Use Bin Locations for Organization¶
# Good: Include bin locations for efficient counting
count = get_model("stock.count").browse(count_id)
# Group lines by bin location for count sheet printing
lines_by_bin = {}
for line in count.lines:
bin_loc = line.bin_location or "UNASSIGNED"
if bin_loc not in lines_by_bin:
lines_by_bin[bin_loc] = []
lines_by_bin[bin_loc].append(line)
# Print count sheets by bin
for bin_loc in sorted(lines_by_bin.keys()):
print(f"\n=== BIN: {bin_loc} ===")
for line in lines_by_bin[bin_loc]:
print(f" {line.product_id.code}: Expected {line.prev_qty}")
3. Validate Before Creating Lines¶
# Good: Check product is stock type before creating line
product = get_model("product").browse(product_id)
if product.type != "stock":
raise Exception(f"Product {product.code} is not a stock item")
if not product.uom_id:
raise Exception(f"Product {product.code} has no unit of measure")
# Now safe to create line
line_id = get_model("stock.count.line").create({
"count_id": count_id,
"product_id": product.id,
# ...
})
4. Handle Lot Tracking Properly¶
# Good: Respect lot tracking requirements
product = get_model("product").browse(product_id)
if product.lot_tracking:
# Must specify lot for lot-tracked products
if not lot_id:
raise Exception(f"Product {product.code} requires lot/serial number")
get_model("stock.count.line").create({
"count_id": count_id,
"product_id": product.id,
"lot_id": lot_id, # Required
"new_qty": 1,
"uom_id": product.uom_id.id
})
else:
# No lot needed for regular products
get_model("stock.count.line").create({
"count_id": count_id,
"product_id": product.id,
"lot_id": None, # Optional
"new_qty": 50,
"uom_id": product.uom_id.id
})
5. Prevent Duplicate Lines¶
# Good: Check for existing line before creating
existing = get_model("stock.count.line").search([
["count_id", "=", count_id],
["product_id", "=", product_id],
["lot_id", "=", lot_id]
])
if existing:
# Update existing line
line = get_model("stock.count.line").browse(existing[0])
line.write({"new_qty": line.new_qty + quantity})
else:
# Create new line
get_model("stock.count.line").create({
"count_id": count_id,
"product_id": product_id,
"lot_id": lot_id,
"new_qty": quantity,
# ...
})
Performance Tips¶
1. Batch Line Creation¶
# Good: Create multiple lines in single transaction
vals_list = []
for product in products_to_count:
vals_list.append({
"count_id": count_id,
"product_id": product.id,
"prev_qty": product.qty_available,
"new_qty": 0,
"uom_id": product.uom_id.id
})
# Create all at once
for vals in vals_list:
get_model("stock.count.line").create(vals)
2. Minimize Balance Queries¶
# Good: Query all balances once
location_id = count.location_id.id
all_balances = get_model("stock.balance").compute_key_balances(
None,
context={"location_id": location_id}
)
# Use cached balances
for product in products:
key = (product.id, None, location_id, None)
qty, amt, qty2 = all_balances.get(key, (0, 0, 0))
# Create line with cached data
get_model("stock.count.line").create({
"count_id": count_id,
"product_id": product.id,
"prev_qty": qty,
"prev_cost_amount": amt,
# ...
})
Troubleshooting¶
"Product type must be 'stock'"¶
Cause: Attempting to create count line for non-stock product (service, consumable, etc.).
Solution: Verify product type before creating line:
product = get_model("product").browse(product_id)
if product.type != "stock":
print(f"Cannot count {product.code} - type is {product.type}")
Lines Not Appearing in Count¶
Cause: Incorrect count_id or cascade deletion.
Solution: Verify parent count exists and line was saved:
count = get_model("stock.count").browse(count_id)
print(f"Count state: {count.state}")
print(f"Number of lines: {len(count.lines)}")
Cost Amount Calculation Incorrect¶
Cause: Computed field not refreshed or unit_price is zero.
Solution: Refresh object and verify unit price:
line = get_model("stock.count.line").browse(line_id)
line = line.browse()[0] # Refresh
print(f"New qty: {line.new_qty}")
print(f"Unit price: {line.unit_price}")
print(f"Cost amount: {line.new_cost_amount}") # Should be qty × price
Duplicate Product/Lot Combinations¶
Cause: Multiple lines created for same product/lot without checking.
Solution: Use parent count's remove_dup() method:
Cannot Delete Lines After Validation¶
Cause: Lines become effectively readonly after count is validated.
Solution: Return count to draft first:
get_model("stock.count").to_draft([count_id])
# Now can delete lines
get_model("stock.count.line").delete([line_id])
Related Models¶
| Model | Relationship | Description |
|---|---|---|
stock.count |
Many2One | Parent stock count (cascade) |
product |
Many2One | Product being counted |
stock.lot |
Many2One | Lot/serial number |
uom |
Many2One | Unit of measure |
stock.balance |
Referenced | Source of previous quantities |
cycle.stock.count |
Many2One | Associated cycle count program |
Testing Examples¶
Unit Test: Line Creation and Computation¶
def test_count_line_creation():
# Create test count
count_id = get_model("stock.count").create({
"location_id": test_location_id,
"date": "2024-10-27 14:00:00"
})
# Create line
line_id = get_model("stock.count.line").create({
"count_id": count_id,
"product_id": test_product_id,
"prev_qty": 100,
"prev_cost_amount": 2500, # $25 each
"new_qty": 98,
"unit_price": 25,
"uom_id": 1
})
# Verify line created
line = get_model("stock.count.line").browse(line_id)
assert line.count_id.id == count_id
assert line.product_id.id == test_product_id
# Verify computed fields
assert line.prev_cost_price == 25.0 # 2500 / 100
assert line.new_cost_amount == 2450.0 # 98 × 25
# Verify variance
variance_qty = line.new_qty - line.prev_qty
variance_value = line.new_cost_amount - line.prev_cost_amount
assert variance_qty == -2
assert variance_value == -50
Unit Test: Barcode Scanning¶
def test_barcode_increment():
# Create count
count_id = get_model("stock.count").create({
"location_id": test_location_id,
"date": "2024-10-27 14:00:00"
})
# Create initial line
line_id = get_model("stock.count.line").create({
"count_id": count_id,
"product_id": test_product_id,
"lot_id": test_lot_id,
"prev_qty": 10,
"new_qty": 0,
"uom_id": 1
})
# Scan same item 3 times
line = get_model("stock.count.line").browse(line_id)
line.write({"new_qty": line.new_qty + 1})
assert line.browse()[0].new_qty == 1
line.write({"new_qty": line.browse()[0].new_qty + 1})
assert line.browse()[0].new_qty == 2
line.write({"new_qty": line.browse()[0].new_qty + 1})
assert line.browse()[0].new_qty == 3
Security Considerations¶
Data Access¶
- Lines inherit security from parent stock count
- Cannot create lines for counts in completed/voided state
- Cascade deletion ensures data consistency
Validation¶
- Product must be stock type
- Quantities must be non-negative
- UOM must match product's base UOM or compatible
Version History¶
Last Updated: 2024-10-27
Model Version: stock_count_line.py
Framework: Netforce
Additional Resources¶
- Stock Count Documentation:
stock.count - Stock Balance Documentation:
stock.balance - Stock Lot Documentation:
stock.lot - Product Documentation:
product
Support & Feedback¶
For issues or questions about this module: 1. Check parent stock count documentation 2. Verify product is stock type and has valid UOM 3. Review stock balance for previous quantity source 4. Check lot tracking requirements for product 5. Test line creation in development environment first
This documentation is generated for developer onboarding and reference purposes.