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:
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¶
| 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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
This ensures: - Each company has unique count numbers - Different companies can use same count numbers - Prevents duplicate count creation
Related Models¶
| 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:
"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:
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.