Skip to content

Stock Count Documentation

Overview

The Stock Count module (stock.count) provides comprehensive physical inventory counting and reconciliation capabilities. This module enables businesses to perform periodic physical counts of warehouse inventory, compare physical quantities against system records, and automatically generate stock adjustment movements to correct discrepancies. It supports full physical inventories, cycle counting programs, barcode scanning, and detailed variance analysis with complete audit trails.


Model Information

Model Name: stock.count
Display Name: Stock Count
Key Fields: number, company_id

Features

  • ✅ Audit logging enabled (_audit_log)
  • ✅ Multi-company support (company_id)
  • ✅ Unique number per company
  • ✅ Draft/Done/Voided workflow
  • ✅ Automatic sequence numbering
  • ✅ Full audit trail with stock movements

Understanding Key Fields

What are Key Fields?

In Netforce models, Key Fields are a combination of fields that together create a composite unique identifier for a record. Think of them as a business key that ensures data integrity across the system.

For the stock.count model, the key fields are:

_key = ["number", "company_id"]

This means the combination of these fields must be unique: - number - The stock count reference number (e.g., "SC-2024-001") - company_id - The company performing the count

Why Key Fields Matter

Uniqueness Guarantee - Key fields prevent duplicate records by ensuring unique combinations:

# Examples of valid combinations:
Company A + "SC-2024-001"   Valid
Company B + "SC-2024-001"   Valid (different company)
Company A + "SC-2024-002"   Valid (different number)

# This would fail - duplicate key:
Company A + "SC-2024-001"   ERROR: Key already exists!

Database Implementation

The key fields are enforced at the database level through a unique constraint, ensuring that each stock count number is unique within each company.


Stock Count States

draft → done
       voided
State Description
draft Count is being prepared, lines can be edited
done Count completed, stock movements created and posted
voided Count cancelled, stock movements reversed

Key Fields Reference

Header Fields

Field Type Required Description
number Char Unique count reference number (auto-generated)
memo Char Brief note about the count
location_id Many2One Warehouse/location being counted
date DateTime Date and time of the count
description Char Detailed description of the count
state Selection Current status (draft/done/voided)
company_id Many2One Company (defaults to active company)
journal_id Many2One Stock journal for movements

Computed Fields

Field Type Description
total_cost_amount Decimal Total value of new inventory amounts
total_prev_qty Decimal Sum of all previous quantities
total_new_qty Decimal Sum of all new counted quantities
num_lines Integer Number of count lines

Search Fields

Field Type Description
product_id Many2One Search counts by product (stored=False)

Relationship Fields

Field Type Description
lines One2Many Count lines (stock.count.line)
moves One2Many Generated stock movements
comments One2Many Comments and discussions

API Methods

1. Create Stock Count

Method: create(vals, context)

Creates a new stock count record with automatic number generation.

Parameters:

vals = {
    "location_id": 123,              # Required: Warehouse location ID
    "date": "2024-10-27 14:30:00",  # Required: Count date/time
    "memo": "Monthly cycle count",   # Optional: Brief note
    "journal_id": 5,                 # Optional: Stock journal
    "lines": [                       # Optional: Initial count lines
        ("create", {
            "product_id": 456,
            "lot_id": 789,
            "prev_qty": 100,
            "new_qty": 98,
            "unit_price": 25.50,
            "uom_id": 1
        })
    ]
}

context = {
    "date": "2024-10-27"            # For number sequence generation
}

Returns: int - New stock count ID

Example:

# Create a new stock count for main warehouse
count_id = get_model("stock.count").create({
    "location_id": 5,  # Main warehouse
    "date": "2024-10-27 14:30:00",
    "memo": "Q4 Physical Inventory",
    "lines": [
        ("create", {
            "product_id": 100,
            "prev_qty": 50,
            "new_qty": 48,
            "unit_price": 12.50,
            "uom_id": 1
        })
    ]
})


2. Add Lines from Stock Balance

Method: add_lines(ids, context)

Automatically adds count lines based on current stock balances for the selected location.

Parameters: - ids (list): Stock count IDs - context (dict): Options for line generation

Context Options:

context = {
    "product_id": 123,                    # Optional: Specific product only
    "categ_id": 45,                       # Optional: Product category filter
    "sale_invoice_uom_id": 2,             # Optional: UOM filter
    "lot_type": "with_lot",               # Optional: "with_lot", "without_lot"
    "qty_type": "previous",               # Optional: "previous" (copy qty) or default (zero)
    "price_type": "previous",             # Optional: "previous", "product", or default (zero)
    "job_id": "task_001"                  # Optional: For progress tracking
}

Behavior: - Queries stock balances for the count's location at the count date - Creates one line per product/lot combination with inventory - Filters by product, category, UOM, or lot type if specified - Sets previous quantities from stock balance - Sets new quantities based on qty_type parameter - Sets unit prices based on price_type parameter - Skips products already in count lines (no duplicates) - Supports long-running background jobs with progress updates

Returns: dict - Flash message with number of lines added

Example:

# Add all products with existing stock
get_model("stock.count").add_lines([count_id], context={
    "qty_type": "previous",      # Copy current qty to new qty
    "price_type": "product"      # Use product cost price
})

# Add only products in specific category with lots
get_model("stock.count").add_lines([count_id], context={
    "categ_id": 10,              # Electronics category
    "lot_type": "with_lot",      # Only serialized items
    "qty_type": "previous",
    "price_type": "previous"
})

# Add specific product only
get_model("stock.count").add_lines([count_id], context={
    "product_id": 456,
    "qty_type": "previous",
    "price_type": "previous"
})


3. Delete Lines

Method: delete_lines(ids, context)

Deletes all count lines for the specified stock count(s).

Parameters: - ids (list): Stock count IDs

Example:

# Clear all lines to start over
get_model("stock.count").delete_lines([count_id])

Returns: dict - Flash message confirming deletion


4. Update Previous Quantities

Method: update_prev_qtys(ids, context)

Recalculates previous quantities and amounts for all lines based on stock balances at the count date.

Parameters: - ids (list): Stock count IDs

Behavior: - Computes stock balances at count date for all line products/lots - Updates prev_qty and prev_cost_amount fields - Updates bin_location from product location settings - Useful when count date changes or to refresh stale data

Example:

# Refresh previous quantities after changing count date
count = get_model("stock.count").browse(count_id)
count.write({"date": "2024-10-27 08:00:00"})
get_model("stock.count").update_prev_qtys([count_id])


5. Bulk Update Previous Quantities

Method: bulk_update_prev_qtys(ids, context)

Updates previous quantities for multiple stock counts in draft state.

Parameters: - ids (list): Stock count IDs

Behavior: - Only processes counts in draft state - Calls update_prev_qtys for each count - Useful for batch updates of multiple counts

Example:

# Update multiple draft counts
draft_count_ids = get_model("stock.count").search([["state", "=", "draft"]])
get_model("stock.count").bulk_update_prev_qtys(draft_count_ids)


6. Remove Duplicates

Method: remove_dup(ids, context)

Removes duplicate lines (same product and lot) from the count.

Parameters: - ids (list): Stock count IDs

Behavior: - Keeps first occurrence of each product/lot combination - Deletes subsequent duplicates

Example:

# Clean up duplicate lines
get_model("stock.count").remove_dup([count_id])

Returns: dict - Flash message with number of duplicates removed


State Transition Methods

6.1 Validate Count

Method: validate(ids, context)

Completes the stock count by creating stock movements for all variances and posting them.

Parameters: - ids (list): Stock count IDs to validate

Context Options:

context = {
    "job_id": "task_123"          # Optional: For progress tracking in background jobs
}

Behavior: - Validates no duplicate product/lot combinations exist - For each line, calculates quantity and cost variances: - If new_qty < prev_qty: Creates movement OUT to inventory loss location - If new_qty > prev_qty: Creates movement IN from inventory loss location - Creates stock movements with calculated cost differences - Posts all movements (sets to 'done' state) - Updates count state to 'done' - Triggers stock balance recalculation - Supports background job execution with progress tracking - Can be aborted if running as background task

Example:

# Complete the stock count
get_model("stock.count").validate([count_id])

# With background job tracking
get_model("stock.count").validate([count_id], context={
    "job_id": "count_validation_001"
})

Permission Requirements: - User must have permission to create and post stock movements - User must have access to inventory loss location


6.2 Void Count

Method: void(ids, context)

Cancels a completed stock count by deleting all associated movements.

Parameters: - ids (list): Stock count IDs

Behavior: - Deletes all related stock movements - Changes count state to 'voided' - Cannot be reversed (count must be recreated)

Example:

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


6.3 Return to Draft

Method: to_draft(ids, context)

Returns completed counts to draft state by removing all stock movements.

Parameters: - ids (list): Stock count IDs

Behavior: - Deletes all related stock movements - Returns count to 'draft' state - Allows re-editing and re-validation

Example:

# Revert to draft to make corrections
get_model("stock.count").to_draft([count_id])


UI Events (onchange methods)

onchange_product

Triggered when a product is selected in a count line. Automatically populates: - bin_location - From product's location settings - prev_qty - Current physical quantity at location - prev_cost_price - Current average cost at location - new_qty - Initialized to previous quantity - unit_price - Set to current cost or product cost price - uom_id - Product's unit of measure

Usage:

data = {
    "location_id": 5,
    "lines": [
        {
            "product_id": 123,
            "lot_id": 456
        }
    ]
}
result = get_model("stock.count").onchange_product(
    context={"data": data, "path": "lines.0"}
)


onchange_date

Triggered when count date changes. Generates new count number based on date's sequence.

Usage:

data = {
    "date": "2024-10-27 14:30:00"
}
result = get_model("stock.count").onchange_date(
    context={"data": data}
)
# result["number"] contains new sequence number


Barcode Scanning

on_barcode

Handles barcode scans during counting to quickly add or increment product quantities.

Method: on_barcode(context)

Context:

context = {
    "data": {
        "id": count_id           # Stock count ID
    },
    "barcode": "LOT123456"       # Scanned barcode
}

Behavior: - Looks up lot by barcode number - Finds product associated with lot - If product/lot exists in count: Increments new_qty by 1 - If not exists: Creates new line with new_qty = 1 - Auto-populates previous quantity and cost

Example:

# Scan barcode during counting
get_model("stock.count").on_barcode(context={
    "data": {"id": count_id},
    "barcode": "LOT-2024-001"
})


Search Functions

Search by Product

Method: search_product(clause, context)

Custom search function that finds stock counts containing a specific product, including variants and components.

Usage:

# Find all counts containing product ID 123
condition = [["product_id", "=", 123]]
count_ids = get_model("stock.count").search(condition)

Behavior: - Searches main product, variants, and components - Returns counts with any matching line


Computed Fields Functions

get_total_cost_amount(ids, context)

Calculates total value of all new inventory amounts (new_qty × unit_price) across all lines.

get_total_qty(ids, context)

Returns both total_prev_qty and total_new_qty - sums of previous and new quantities.

get_num_lines(ids, context)

Returns count of lines in the stock count.


Helper Methods

copy(ids, context)

Creates a duplicate of an existing stock count.

Parameters: - ids (list): Stock count ID to copy

Behavior: - Copies header and all lines - Generates new count number - Sets state to 'draft'

Example:

result = get_model("stock.count").copy([count_id])
# Returns navigation to new count form

Returns: dict - Navigation to new count with flash message


view_journal_entry(ids, context)

Navigates to the accounting journal entry created from stock movements.

Parameters: - ids (list): Stock count ID

Behavior: - Finds journal entry associated with count's movements - Opens journal entry form

Example:

result = get_model("stock.count").view_journal_entry([count_id])
# Opens journal entry form


delete(ids, **kw)

Deletes stock count(s) and all associated stock movements.

Parameters: - ids (list): Stock count IDs to delete

Behavior: - Deletes all related stock movements first - Then deletes the count record

Example:

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


Best Practices

1. Count Preparation

# Good: Prepare count systematically

# Step 1: Create count with clear date/time
count_id = get_model("stock.count").create({
    "location_id": warehouse_id,
    "date": "2024-10-27 08:00:00",  # Start of day
    "memo": "Q4 Physical Inventory - Warehouse A"
})

# Step 2: Add lines from balance
get_model("stock.count").add_lines([count_id], context={
    "qty_type": "previous",     # Start with system qty
    "price_type": "product"     # Use current product cost
})

# Step 3: Update quantities based on physical count
# (via UI or mobile app)

# Step 4: Validate when ready
get_model("stock.count").validate([count_id])

2. Cycle Counting Strategy

# Good: Implement ABC cycle counting

# High-value items (A items) - count monthly
a_count_id = get_model("stock.count").create({
    "location_id": warehouse_id,
    "date": time.strftime("%Y-%m-%d %H:%M:%S"),
    "memo": "Monthly A-item cycle count"
})
get_model("stock.count").add_lines([a_count_id], context={
    "categ_id": high_value_category_id,
    "qty_type": "previous",
    "price_type": "product"
})

# Medium-value items (B items) - count quarterly
# Low-value items (C items) - count annually

3. Handling Variances

# Good: Investigate large variances before validating

count = get_model("stock.count").browse(count_id)

# Check for significant variances
large_variances = []
for line in count.lines:
    variance_pct = 0
    if line.prev_qty > 0:
        variance_pct = abs((line.new_qty - line.prev_qty) / line.prev_qty * 100)

    if variance_pct > 10:  # More than 10% variance
        large_variances.append({
            "product": line.product_id.code,
            "prev": line.prev_qty,
            "new": line.new_qty,
            "variance_pct": variance_pct
        })

# Review and recount items with large variances
if large_variances:
    print("Items requiring recount:", large_variances)
    # Perform recount before validating
else:
    get_model("stock.count").validate([count_id])

4. Barcode Scanning Workflow

# Good: Use barcodes for fast, accurate counting

# Count prep - create with zero quantities
count_id = get_model("stock.count").create({
    "location_id": warehouse_id,
    "date": time.strftime("%Y-%m-%d %H:%M:%S"),
    "memo": "Barcode scan count"
})

# Add lines with zero new_qty
get_model("stock.count").add_lines([count_id], context={
    "qty_type": None,           # Start at zero
    "price_type": "product"
})

# During physical count, scan each item
# Each scan automatically increments the quantity
for barcode in scanned_barcodes:
    get_model("stock.count").on_barcode(context={
        "data": {"id": count_id},
        "barcode": barcode
    })

# Items with zero new_qty were not found
# Review before validating

5. Background Job Processing

# Good: Use background jobs for large counts

import uuid

# Create job for tracking
job_id = str(uuid.uuid4())

# Start validation as background task
get_model("stock.count").validate([count_id], context={
    "job_id": job_id
})

# Monitor progress
while True:
    job_status = tasks.get_status(job_id)
    print(f"Progress: {job_status['progress']}% - {job_status['message']}")
    if job_status['state'] in ['done', 'error', 'aborted']:
        break
    time.sleep(2)

Database Constraints

Unique Count Number per Company

The combination of count number and company must be unique:

_key = ["number", "company_id"]

This ensures: - Each company has unique count numbers - Different companies can use same count numbers - Prevents duplicate count creation


Model Relationship Description
stock.count.line One2Many Individual product counts in this count
stock.location Many2One Warehouse location being counted
stock.move One2Many Stock adjustments created from count
stock.balance Referenced Source of previous quantities
stock.journal Many2One Journal for posting movements
stock.lot Referenced For barcode scanning lookup
product Referenced Products being counted
company Many2One Company ownership
message One2Many Comments and discussions

Common Use Cases

Use Case 1: Year-End Physical Inventory

# Complete physical inventory at year-end

# 1. Create count at midnight to freeze quantities
count_id = get_model("stock.count").create({
    "location_id": main_warehouse_id,
    "date": "2024-12-31 23:59:59",
    "memo": "Year-End Physical Inventory 2024",
    "journal_id": inventory_journal_id
})

# 2. Generate count sheets for all products
get_model("stock.count").add_lines([count_id], context={
    "qty_type": None,           # Start with zero (blind count)
    "price_type": "product"     # Use product cost
})

# 3. Print count sheets grouped by location/aisle
count = get_model("stock.count").browse(count_id)
for line in sorted(count.lines, key=lambda l: l.bin_location or ''):
    print(f"Bin: {line.bin_location}, Product: {line.product_id.code}, UOM: {line.uom_id.name}")

# 4. After physical count entry, validate
get_model("stock.count").validate([count_id])

# 5. Generate variance report
for line in count.lines:
    if line.prev_qty != line.new_qty:
        variance = line.new_qty - line.prev_qty
        value_diff = (line.new_qty - line.prev_qty) * line.unit_price
        print(f"Variance: {line.product_id.code}, Qty: {variance:+.2f}, Value: ${value_diff:+.2f}")

Use Case 2: ABC Cycle Counting Program

# Implement ongoing cycle counting based on ABC classification

def perform_cycle_count(abc_class, location_id):
    """
    Perform cycle count for products in specified ABC class
    """
    # Get products in ABC class
    product_ids = get_model("product").search([
        ["abc_class", "=", abc_class],
        ["type", "=", "stock"]
    ])

    if not product_ids:
        return None

    # Create cycle count
    count_id = get_model("stock.count").create({
        "location_id": location_id,
        "date": time.strftime("%Y-%m-%d %H:%M:%S"),
        "memo": f"Cycle Count - Class {abc_class}"
    })

    # Add only products in this ABC class
    for prod_id in product_ids:
        get_model("stock.count").add_lines([count_id], context={
            "product_id": prod_id,
            "qty_type": "previous",
            "price_type": "product"
        })

    return count_id

# Monthly schedule:
# A items (high value) - count every month
a_count = perform_cycle_count("A", warehouse_id)

# Quarterly schedule:
# B items (medium value) - count every quarter
if current_month in [1, 4, 7, 10]:
    b_count = perform_cycle_count("B", warehouse_id)

# Annual schedule:
# C items (low value) - count once per year
if current_month == 1:
    c_count = perform_cycle_count("C", warehouse_id)

Use Case 3: Spot Check with Mobile Scanning

# Quick spot check of specific products using mobile device

# 1. Create spot count
count_id = get_model("stock.count").create({
    "location_id": warehouse_id,
    "date": time.strftime("%Y-%m-%d %H:%M:%S"),
    "memo": "Spot Check - High Movers"
})

# 2. Add specific high-movement products
high_mover_ids = [101, 102, 103, 104, 105]  # Product IDs
for prod_id in high_mover_ids:
    get_model("stock.count").add_lines([count_id], context={
        "product_id": prod_id,
        "qty_type": None,           # Blind count
        "price_type": "product"
    })

# 3. Use mobile device to scan barcodes
# Each scan adds or increments quantity
scanned_lots = ["LOT-001", "LOT-001", "LOT-002", "LOT-003"]
for barcode in scanned_lots:
    try:
        get_model("stock.count").on_barcode(context={
            "data": {"id": count_id},
            "barcode": barcode
        })
    except Exception as e:
        print(f"Error scanning {barcode}: {e}")

# 4. Validate immediately
get_model("stock.count").validate([count_id])

# 5. Check for discrepancies
count = get_model("stock.count").browse(count_id)
discrepancies = [
    line for line in count.lines 
    if abs(line.new_qty - line.prev_qty) > 0
]
if discrepancies:
    print(f"Found {len(discrepancies)} discrepancies requiring investigation")

Use Case 4: Perpetual Inventory with Daily Counts

# Implement perpetual inventory system with daily mini-counts

def daily_count_rotation(location_id, day_of_month):
    """
    Count 1/30th of inventory each day for continuous verification
    """
    # Get all products
    all_products = get_model("product").search([
        ["type", "=", "stock"]
    ])

    # Divide into 30 groups
    group_size = len(all_products) // 30
    start_idx = (day_of_month - 1) * group_size
    end_idx = start_idx + group_size
    today_products = all_products[start_idx:end_idx]

    # Create daily count
    count_id = get_model("stock.count").create({
        "location_id": location_id,
        "date": time.strftime("%Y-%m-%d %H:%M:%S"),
        "memo": f"Daily Perpetual Count - Day {day_of_month}"
    })

    # Add products for today
    for prod_id in today_products:
        get_model("stock.count").add_lines([count_id], context={
            "product_id": prod_id,
            "qty_type": "previous",
            "price_type": "product"
        })

    return count_id

# Run daily
import datetime
today = datetime.date.today()
daily_count_id = daily_count_rotation(warehouse_id, today.day)

Use Case 5: Lot-Tracked Product Verification

# Verify lot-tracked products for compliance/audit

# 1. Create count for lot-tracked products only
count_id = get_model("stock.count").create({
    "location_id": warehouse_id,
    "date": time.strftime("%Y-%m-%d %H:%M:%S"),
    "memo": "Lot Verification - Serialized Inventory"
})

# 2. Add only products with lot tracking
get_model("stock.count").add_lines([count_id], context={
    "lot_type": "with_lot",         # Only lot-tracked items
    "qty_type": "previous",
    "price_type": "product"
})

# 3. Verify each lot physically
count = get_model("stock.count").browse(count_id)
for line in count.lines:
    print(f"Verify Product: {line.product_id.code}")
    print(f"  Lot: {line.lot_id.number if line.lot_id else 'N/A'}")
    print(f"  Expected Qty: {line.prev_qty}")
    print(f"  Bin Location: {line.bin_location}")

# 4. Update quantities and validate
get_model("stock.count").validate([count_id])

# 5. Generate lot traceability report
print("\n=== Lot Adjustments ===")
for move in count.moves:
    if move.lot_id:
        print(f"Lot: {move.lot_id.number}, Product: {move.product_id.code}, Adj: {move.qty:+.2f}")

Performance Tips

1. Use Bulk Operations for Large Counts

  • Use add_lines() instead of creating lines individually
  • Process in background jobs for counts with 500+ lines
  • Use bulk_update_prev_qtys() for multiple counts
# Bad: Create lines one by one
for product in products:
    get_model("stock.count.line").create({
        "count_id": count_id,
        "product_id": product.id,
        # ...
    })

# Good: Use add_lines for batch creation
get_model("stock.count").add_lines([count_id], context={
    "qty_type": "previous",
    "price_type": "product"
})

2. Optimize Date-Based Balance Queries

When adding lines or updating quantities, the system computes balances at the count date. For better performance: - Set count date before adding lines - Avoid frequent date changes after lines are added - Use update_prev_qtys() only when necessary

3. Index Product/Lot Lookups

For barcode scanning performance: - Ensure lot numbers are indexed in database - Cache product data when scanning multiple items - Process scans in batches if possible


Troubleshooting

"Key already exists: number + company_id"

Cause: Attempting to create a stock count with a number that already exists for this company.
Solution: The system auto-generates unique numbers. If you're manually setting the number, ensure it's unique within the company. Check existing counts:

existing = get_model("stock.count").search([["number", "=", "SC-2024-001"], ["company_id", "=", company_id]])

"Duplicate item in stock count: product=XXX / lot=YYY"

Cause: Attempting to validate a count that has multiple lines for the same product/lot combination.
Solution: Remove duplicate lines before validating:

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

"Inventory loss location not found"

Cause: System cannot find the inventory adjustment location when validating the count.
Solution: Create or verify inventory loss location exists:

# Create inventory loss location if missing
loss_loc = get_model("stock.location").create({
    "name": "Inventory Adjustments",
    "type": "inventory",
    "code": "INV-LOSS"
})

"Lot not found: 'BARCODE123'"

Cause: Scanned barcode doesn't match any lot number in the system.
Solution: Verify lot exists or create it:

# Check if lot exists
lot_ids = get_model("stock.lot").search([["number", "=", "BARCODE123"]])
if not lot_ids:
    print("Lot must be created first or check barcode format")

Validation Takes Too Long

Cause: Large count with many lines being validated synchronously.
Solution: Use background job processing:

# Create background job
job_id = tasks.create_job("Validate Count")

# Validate with job tracking
get_model("stock.count").validate([count_id], context={"job_id": job_id})

# Monitor progress
status = tasks.get_status(job_id)

Previous Quantities Don't Match Expected Values

Cause: Stock movements occurred after count date or balance calculation issue.
Solution: Refresh previous quantities:

# Update to latest balances at count date
get_model("stock.count").update_prev_qtys([count_id])

Stock Movements Not Created After Validation

Cause: All lines have matching previous and new quantities (no variances).
Solution: This is normal - movements are only created for actual variances. Verify lines:

count = get_model("stock.count").browse(count_id)
variances = [l for l in count.lines if l.prev_qty != l.new_qty]
print(f"Lines with variances: {len(variances)}")


Testing Examples

Unit Test: Create and Validate Count

def test_stock_count_basic_flow():
    # Create test warehouse
    location_id = get_model("stock.location").create({
        "name": "Test Warehouse",
        "type": "internal",
        "code": "WH-TEST"
    })

    # Create test product with stock
    product_id = get_model("product").create({
        "name": "Test Product",
        "code": "TEST-001",
        "type": "stock"
    })

    # Add initial stock
    get_model("stock.move").create({
        "product_id": product_id,
        "location_from_id": get_supplier_location(),
        "location_to_id": location_id,
        "qty": 100,
        "uom_id": 1,
        "state": "done"
    })

    # Create stock count
    count_id = get_model("stock.count").create({
        "location_id": location_id,
        "date": time.strftime("%Y-%m-%d %H:%M:%S"),
        "memo": "Test count"
    })

    # Verify draft state
    count = get_model("stock.count").browse(count_id)
    assert count.state == "draft"

    # Add lines
    get_model("stock.count").add_lines([count_id], context={
        "product_id": product_id,
        "qty_type": "previous",
        "price_type": "product"
    })

    # Verify line created
    count = count.browse()[0]  # Refresh
    assert len(count.lines) == 1
    assert count.lines[0].prev_qty == 100

    # Adjust quantity
    count.lines[0].write({"new_qty": 95})

    # Validate count
    get_model("stock.count").validate([count_id])

    # Verify done state
    count = count.browse()[0]
    assert count.state == "done"

    # Verify movement created
    assert len(count.moves) == 1
    assert count.moves[0].qty == 5  # Variance

    # Verify new balance
    new_qty = get_model("stock.balance").get_qty_phys(location_id, product_id)
    assert new_qty == 95

Unit Test: Barcode Scanning

def test_barcode_scanning():
    # Setup
    location_id = create_test_location()
    product_id = create_test_product()
    lot_id = get_model("stock.lot").create({
        "number": "LOT-TEST-001",
        "product_id": product_id
    })

    # Create count
    count_id = get_model("stock.count").create({
        "location_id": location_id,
        "date": time.strftime("%Y-%m-%d %H:%M:%S")
    })

    # Scan barcode (first time - creates line)
    get_model("stock.count").on_barcode(context={
        "data": {"id": count_id},
        "barcode": "LOT-TEST-001"
    })

    # Verify line created with qty 1
    count = get_model("stock.count").browse(count_id)
    assert len(count.lines) == 1
    assert count.lines[0].new_qty == 1
    assert count.lines[0].lot_id.id == lot_id

    # Scan again (increments)
    get_model("stock.count").on_barcode(context={
        "data": {"id": count_id},
        "barcode": "LOT-TEST-001"
    })

    # Verify incremented
    count = count.browse()[0]
    assert count.lines[0].new_qty == 2

Security Considerations

Permission Model

  • Create Stock Count: Requires inventory user role
  • Validate Count: Requires inventory manager role
  • Void Count: Requires inventory manager role
  • Delete Count: Requires inventory manager role

Data Access

  • Stock counts are company-specific (multi-company support)
  • Users can only access counts for companies they have access to
  • Audit log tracks all count modifications
  • Stock movements created from counts inherit company context

Best Practices

  • Limit validate/void permissions to supervisors
  • Use audit log to track who performed counts
  • Restrict access to inventory loss location
  • Review large variances before validation

Configuration Settings

Required Settings

Setting Location Description
Stock Count Sequence Sequences Defines number format (SC-YYYY-NNNN)
Inventory Loss Location Stock Locations Where to post variances (type=inventory)
Stock Count Journal Stock Journals Default journal for movements

Optional Settings

Setting Default Description
Auto Number Format SC-{year}-{seq} Count number template
Default Count Time Current Default date/time for new counts

Integration Points

Internal Modules

  • Stock Balance: Source of previous quantities and amounts
  • Stock Move: Target for adjustment movements
  • Stock Location: Warehouse and loss locations
  • Stock Lot: For serialized item tracking
  • Product: Product master data
  • Accounting: Journal entries for inventory adjustments

Workflow Integration

The stock count module fires workflow triggers at key points: - On validation: "count_done" - Can trigger notifications or approvals - On void: "count_voided" - Can trigger audit logs


Version History

Last Updated: 2024-10-27
Model Version: stock_count.py
Framework: Netforce


Additional Resources

  • Stock Count Line Documentation: stock.count.line
  • Cycle Count Documentation: cycle.stock.count
  • Stock Balance Documentation: stock.balance
  • Stock Move Documentation: stock.move

Support & Feedback

For issues or questions about this module: 1. Check stock balance and stock move documentation 2. Review system logs for detailed error messages 3. Verify inventory loss location exists and is properly configured 4. Verify stock journal settings 5. Test count workflow in development environment first


This documentation is generated for developer onboarding and reference purposes.