Skip to content

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

1. Count created → 2. Lines added → 3. Quantities entered → 4. Count validated

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:

prev_cost_price = prev_cost_amount / prev_qty if prev_qty > 0 else 0

Returns: Dictionary mapping line IDs to cost prices

Example:

# Line with previous amount $1,250 and qty 50
# Previous cost price = $1,250 / 50 = $25.00


get_new_cost_amount(ids, context)

Calculates the new total cost amount from new quantity and unit price.

Formula:

new_cost_amount = round(new_qty × unit_price, 2)

Returns: Dictionary mapping line IDs to cost amounts

Example:

# Line with new qty 48 and unit price $25.00
# New cost amount = 48 × $25.00 = $1,200.00


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:

condition=[["type", "=", "stock"]]


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:

# Remove incorrect line
get_model("stock.count.line").delete([line_id])


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:

get_model("stock.count").remove_dup([count_id])

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


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.