Stock Grade Documentation¶
Overview¶
The Stock Grade module (stock.grade) manages product grading operations where incoming materials or produced goods are sorted into different quality grades (Grade-A, Grade-B, waste, etc.). This is commonly used in industries where raw materials or products have quality variations.
Model Information¶
Model Name: stock.grade
Display Name: Product Grading
Key Fields: None (no unique constraint defined)
Name Field: number (used as display identifier)
Features¶
- ❌ Audit logging enabled (
_audit_log) - ❌ Multi-company support (
company_id) - ❌ Full-text content search (
_content_search) - ✅ Automatic sequence number generation
- ✅ Custom default ordering by date and ID
Sort Order: date desc,id desc (most recent first)
State Workflow¶
| State | Description |
|---|---|
draft |
Initial state, grading not yet completed or validated |
done |
Grading validated and stock movements created |
voided |
Grading cancelled, stock movements reversed |
Field Reference¶
Header Fields¶
| Field | Type | Required | Description |
|---|---|---|---|
date |
DateTime | ✅ | Date and time of grading operation (searchable) |
number |
Char | ✅ | Auto-generated grading reference number (searchable) |
ref |
Char | ❌ | External reference or document number (searchable) |
state |
Selection | ✅ | Current status: draft, done, or voided (searchable) |
notes |
Text | ❌ | Additional notes about the grading operation |
Product and Quantity¶
| Field | Type | Required | Description |
|---|---|---|---|
product_id |
Many2One | ✅ | Product being graded (stock type only, searchable) |
qty |
Decimal | ✅ | Total quantity being graded |
qty_ga |
Decimal | ❌ | Quantity of Grade-A product |
qty_waste |
Decimal | ❌ | Quantity of waste/scrap |
qty_loss |
Decimal | ❌ | Computed: Quantity lost/unaccounted for |
qty_remain |
Decimal | ❌ | Computed: Remaining quantity to be graded |
Location Fields¶
| Field | Type | Required | Description |
|---|---|---|---|
location_id |
Many2One | ✅ | Source location where ungraded product is stored (searchable) |
location_ga_id |
Many2One | ❌ | Destination for Grade-A products (searchable) |
location_gb_id |
Many2One | ❌ | Destination for Grade-B products (searchable) |
location_waste_id |
Many2One | ❌ | Destination for waste/scrap (searchable) |
Related Documents¶
| Field | Type | Description |
|---|---|---|
related_id |
Reference | Generic reference to source: purchase order, picking, or production order |
purchase_id |
Many2One | Related purchase order (if grading received materials) |
production_id |
Many2One | Related production order (if grading manufactured goods) |
Relationship Fields¶
| Field | Type | Description |
|---|---|---|
lines |
One2Many | Individual grading line items with grade breakdowns |
stock_moves |
One2Many | Stock movements created during validation |
API Methods¶
1. Create Grading Record¶
Method: create(vals, context)
Creates a new product grading record with automatic number generation.
Parameters:
vals = {
"date": "2025-10-27 10:30:00", # Required
"product_id": 123, # Required: product to grade
"qty": 1000.0, # Required: total quantity
"location_id": 10, # Required: source location
"location_ga_id": 11, # Grade-A destination
"location_gb_id": 12, # Grade-B destination
"location_waste_id": 13, # Waste destination
"purchase_id": 456, # Optional: if from purchase
"lines": [ # Grading breakdown
("create", {
"product_id": 124,
"qty": 800.0,
"location_id": 11
})
]
}
Returns: int - New grading ID
Example:
# Create grading for received materials
grade_id = get_model("stock.grade").create({
"date": time.strftime("%Y-%m-%d %H:%M:%S"),
"product_id": raw_material_id,
"qty": 1000.0,
"location_id": receiving_location_id,
"location_ga_id": grade_a_location_id,
"location_gb_id": grade_b_location_id,
"location_waste_id": waste_location_id,
"purchase_id": po_id,
"lines": [
("create", {
"product_id": grade_a_product_id,
"qty": 750.0,
"location_id": grade_a_location_id,
"unit_price": 10.00,
"amount": 7500.00
}),
("create", {
"product_id": grade_b_product_id,
"qty": 200.0,
"location_id": grade_b_location_id,
"unit_price": 6.00,
"amount": 1200.00
}),
("create", {
"product_id": waste_product_id,
"qty": 30.0,
"location_id": waste_location_id,
"unit_price": 0.00,
"amount": 0.00
})
]
})
2. Validate Grading¶
Method: validate(ids, context)
Validates the grading operation and creates stock movements to transfer products from source to graded locations.
Parameters:
- ids (list): Grading record IDs to validate
Behavior: 1. Validates required settings (transform location, transform journal) 2. Creates stock move from source location to transform location (intermediate) 3. Creates stock moves from transform location to each graded product location 4. Sets unit costs and amounts based on line data 5. Validates all stock moves (sets them to "done") 6. Updates grading state to "done"
Stock Movement Flow:
Source Location → Transform Location → Grade Locations
(ungraded) (intermediate) (Grade-A, Grade-B, Waste)
Returns: None (updates record in place)
Example:
# Validate grading to create stock movements
get_model("stock.grade").validate([grade_id])
# After validation:
# - Stock moves are created and completed
# - State changes to "done"
# - Products are in graded locations
Requirements: - Transform location must exist (type="transform") - Transform journal must be configured in settings - All grading lines must have valid products and locations
Raises: - Exception if transform location missing - Exception if inventory loss location missing - Exception if transform journal not configured
3. Void Grading¶
Method: void(ids, context)
Cancels a validated grading operation and reverses stock movements.
Parameters:
- ids (list): Grading record IDs to void
Behavior: - Deletes all related stock movements - Changes state to "voided" - Does NOT restore draft state (prevents accidental re-validation)
Returns: None
Example:
# Void a grading operation
get_model("stock.grade").void([grade_id])
# After voiding:
# - All stock movements deleted
# - Products returned to original state
# - State is "voided" (not "draft")
4. Return to Draft¶
Method: to_draft(ids, context)
Returns a voided or done grading back to draft state.
Parameters:
- ids (list): Grading record IDs to return to draft
Behavior: - Deletes all related stock movements - Changes state back to "draft" - Allows re-validation after corrections
Returns: None
Example:
# Return to draft for corrections
get_model("stock.grade").to_draft([grade_id])
# After returning to draft:
# - Stock movements deleted
# - Can modify grading details
# - Can re-validate when ready
5. Delete Grading¶
Method: delete(ids, context)
Deletes grading records with validation.
Behavior: - Only allows deletion if state is "draft" - Prevents deletion of validated gradings to maintain audit trail
Example:
# Delete draft grading
try:
get_model("stock.grade").delete([grade_id])
except Exception as e:
print(f"Cannot delete: {e}")
# Error: "Can not delete product transforms in this status"
UI Events (onchange methods)¶
onchange_product¶
Triggered when product is selected in a grading line. Updates: - UoM to match the product's unit of measure - Quantity to 1 (default)
Usage:
data = {
"product_id": 123
}
result = get_model("stock.grade").onchange_product(
context={"data": data, "path": "lines.0"}
)
# Updates: uom_id, qty=1
update_amount¶
Calculates line amount based on quantity and unit price OR calculates unit price based on quantity and amount.
Logic: - If qty and unit_price provided → calculates amount - If qty and amount provided → calculates unit_price
Usage:
# Called automatically when qty or unit_price changes
data = {
"lines": [{
"qty": 100.0,
"unit_price": 10.00
}]
}
result = get_model("stock.grade").update_amount(
context={"data": data, "path": "lines.0"}
)
# Sets amount = 1000.00
onchange_amount¶
Triggered when amount is manually entered. Calculates unit_price from qty and amount.
Usage:
data = {
"lines": [{
"qty": 100.0,
"amount": 1000.00
}]
}
result = get_model("stock.grade").onchange_amount(
context={"data": data, "path": "lines.0"}
)
# Sets unit_price = 10.00
Computed Fields Functions¶
get_qty_loss(ids, context)¶
Calculates quantity lost or unaccounted for during grading.
Formula:
Example:
# Grading record:
# qty = 1000
# lines: 750 (Grade-A) + 200 (Grade-B) + 30 (Waste) = 980
# qty_loss = 1000 - 980 = 20
grade = get_model("stock.grade").browse(grade_id)
print(f"Loss: {grade.qty_loss}") # 20
get_qty_remain(ids, context)¶
Calculates remaining quantity that still needs to be graded (used when grading from purchase orders).
Logic: 1. Gets all purchase orders linked to grading lines 2. Sums quantities received in purchase order lines 3. Sums quantities already graded across all grading records for each PO 4. Calculates remaining = received - graded
Returns: Dictionary mapping grading ID to remaining quantity
Related Models¶
| Model | Relationship | Description |
|---|---|---|
stock.grade.line |
One2Many | Individual grading line items |
stock.move |
One2Many | Stock movements created during validation |
stock.location |
Many2One | Various locations for grading operations |
product |
Many2One | Products being graded |
purchase.order |
Many2One | Source purchase order (if applicable) |
production.order |
Many2One | Source production order (if applicable) |
stock.picking |
Reference | Source picking (if applicable) |
sequence |
Used | For automatic number generation |
settings |
Referenced | For transform journal configuration |
Common Use Cases¶
Use Case 1: Grade Incoming Raw Materials¶
# Scenario: Receive 1000kg of raw material, grade into A/B/Waste
# 1. Create grading record
grade_id = get_model("stock.grade").create({
"date": time.strftime("%Y-%m-%d %H:%M:%S"),
"product_id": raw_material_id,
"qty": 1000.0,
"location_id": receiving_dock_id,
"location_ga_id": grade_a_storage_id,
"location_gb_id": grade_b_storage_id,
"location_waste_id": scrap_id,
"purchase_id": po_id,
"ref": "PO-2025-001",
"lines": [
("create", {
"product_id": grade_a_sku_id,
"qty": 800.0,
"location_id": grade_a_storage_id,
"unit_price": 12.50,
"amount": 10000.00
}),
("create", {
"product_id": grade_b_sku_id,
"qty": 180.0,
"location_id": grade_b_storage_id,
"unit_price": 7.50,
"amount": 1350.00
}),
("create", {
"product_id": waste_sku_id,
"qty": 15.0,
"location_id": scrap_id,
"unit_price": 0.00,
"amount": 0.00
})
]
})
# 2. Validate grading
get_model("stock.grade").validate([grade_id])
# 3. Check results
grade = get_model("stock.grade").browse(grade_id)
print(f"Loss: {grade.qty_loss}kg") # 5kg (1000 - 800 - 180 - 15)
print(f"State: {grade.state}") # "done"
print(f"Stock moves: {len(grade.stock_moves)}") # 4 moves total
Use Case 2: Grade Production Output¶
# Scenario: Production run produces mixed quality output
grade_id = get_model("stock.grade").create({
"date": time.strftime("%Y-%m-%d %H:%M:%S"),
"product_id": finished_good_id,
"qty": 500.0,
"location_id": production_output_id,
"location_ga_id": finished_goods_a_id,
"location_gb_id": finished_goods_b_id,
"location_waste_id": rework_id,
"production_id": prod_order_id,
"lines": [
("create", {
"product_id": product_grade_a_id,
"qty": 450.0,
"location_id": finished_goods_a_id,
"unit_price": 50.00
}),
("create", {
"product_id": product_grade_b_id,
"qty": 40.0,
"location_id": finished_goods_b_id,
"unit_price": 30.00
}),
("create", {
"product_id": defect_product_id,
"qty": 8.0,
"location_id": rework_id,
"unit_price": 10.00
})
]
})
get_model("stock.grade").validate([grade_id])
Use Case 3: Correct Grading Errors¶
# If grading was done incorrectly
# 1. Return to draft
get_model("stock.grade").to_draft([grade_id])
# 2. Update grading lines
grade = get_model("stock.grade").browse(grade_id)
for line in grade.lines:
if line.product_id.id == grade_a_sku_id:
line.write({"qty": 850.0, "amount": 10625.00})
elif line.product_id.id == grade_b_sku_id:
line.write({"qty": 140.0, "amount": 1050.00})
# 3. Re-validate
get_model("stock.grade").validate([grade_id])
Use Case 4: Cancel Invalid Grading¶
# If grading was validated by mistake
# Void the grading
get_model("stock.grade").void([grade_id])
# Stock movements are reversed
# State becomes "voided"
# Cannot accidentally re-validate
Use Case 5: Track Grading Efficiency¶
# Analyze grading efficiency over time
def analyze_grading_efficiency(date_from, date_to):
grade_ids = get_model("stock.grade").search([
["date", ">=", date_from],
["date", "<=", date_to],
["state", "=", "done"]
])
total_graded = 0
total_grade_a = 0
total_grade_b = 0
total_waste = 0
total_loss = 0
for grade in get_model("stock.grade").browse(grade_ids):
total_graded += grade.qty or 0
total_loss += grade.qty_loss or 0
for line in grade.lines:
# Categorize by product or location
if "grade-a" in line.product_id.name.lower():
total_grade_a += line.qty
elif "grade-b" in line.product_id.name.lower():
total_grade_b += line.qty
elif "waste" in line.product_id.name.lower():
total_waste += line.qty
print(f"Grading Analysis ({date_from} to {date_to}):")
print(f" Total Graded: {total_graded}")
print(f" Grade-A: {total_grade_a} ({total_grade_a/total_graded*100:.1f}%)")
print(f" Grade-B: {total_grade_b} ({total_grade_b/total_graded*100:.1f}%)")
print(f" Waste: {total_waste} ({total_waste/total_graded*100:.1f}%)")
print(f" Loss: {total_loss} ({total_loss/total_graded*100:.1f}%)")
Understanding Stock Movement Flow¶
Validation Creates This Flow:¶
Step 1: Source → Transform
Move ungraded product from source location to transform location
- Product: Original product
- Qty: Total quantity being graded
- From: location_id
- To: Transform location (intermediate)
Step 2: Transform → Graded Locations
For each grading line:
- Product: Graded product SKU
- Qty: Line quantity
- From: Transform location
- To: Line's location_id (Grade-A, Grade-B, Waste, etc.)
- Cost: Line's unit_price and amount
Why Use Transform Location?¶
The transform location acts as an intermediate point where: 1. The original product is consumed 2. New graded products are created 3. Cost allocation can be properly tracked 4. Traceability is maintained
Best Practices¶
1. Configure Settings First¶
# Before using grading, ensure settings are configured
settings = get_model("settings").browse(1)
if not settings.transform_journal_id:
raise Exception("Configure transform journal in settings first")
# Verify transform location exists
transform_locs = get_model("stock.location").search([
["type", "=", "transform"]
])
if not transform_locs:
raise Exception("Create transform location first")
2. Always Account for All Quantity¶
# Bad: Unaccounted quantity
lines = [
{"product_id": grade_a_id, "qty": 700},
{"product_id": grade_b_id, "qty": 200}
]
# Total: 900, but grading qty is 1000
# 100 units unaccounted for (loss)
# Good: Account for everything
lines = [
{"product_id": grade_a_id, "qty": 700},
{"product_id": grade_b_id, "qty": 200},
{"product_id": waste_id, "qty": 80},
{"product_id": loss_id, "qty": 20}
]
# Total: 1000, all accounted for
3. Use Consistent Product Naming¶
# Use consistent naming convention for grade products
products = {
"grade_a": "RAW-MATERIAL-001-GRADE-A",
"grade_b": "RAW-MATERIAL-001-GRADE-B",
"waste": "RAW-MATERIAL-001-WASTE"
}
4. Track Costs Accurately¶
# Allocate costs proportionally to graded products
total_cost = 10000.00
total_qty = 1000.0
grade_a_qty = 800.0
grade_a_cost = (grade_a_qty / total_qty) * total_cost # 8000.00
grade_a_unit_price = grade_a_cost / grade_a_qty # 10.00
grade_b_qty = 180.0
grade_b_cost = (grade_b_qty / total_qty) * total_cost # 1800.00
grade_b_unit_price = grade_b_cost / grade_b_qty # 10.00
# Or use different pricing based on grade quality
grade_a_unit_price = 12.50 # Premium pricing
grade_b_unit_price = 7.50 # Discounted pricing
Database Constraints¶
No explicit constraints defined in the model, but logical constraints should be enforced:
- qty > 0
- Sum of line quantities ≤ total qty
- state in ('draft', 'done', 'voided')
Configuration Settings¶
Required Settings¶
| Setting | Location | Description |
|---|---|---|
transform_journal_id |
settings | Journal for transform stock movements |
Required Locations¶
| Location Type | Purpose |
|---|---|
transform |
Intermediate location for product transformation |
inventory |
Loss location for inventory adjustments |
Troubleshooting¶
"Missing transform location"¶
Cause: No location with type="transform" exists
Solution: Create a transform location:
"Missing transform journal"¶
Cause: Transform journal not configured in settings
Solution: Set transform journal in settings:
"Can not delete product transforms in this status"¶
Cause: Attempting to delete grading in "done" or "voided" state
Solution: Only draft gradings can be deleted. Use void() to cancel instead.
"Stock movements not created"¶
Cause: Grading not validated
Solution: Call validate() method on the grading record
"Quantity loss unexpectedly high"¶
Cause: Grading lines don't sum to total quantity
Solution: Review grading lines and add missing quantities (waste, loss, etc.)
Version History¶
Last Updated: October 2025
Model Version: stock_grade.py
Framework: Netforce
Additional Resources¶
- Stock Grade Line Documentation:
stock.grade.line - Stock Move Documentation:
stock.move - Stock Location Documentation:
stock.location - Purchase Order Documentation:
purchase.order - Production Order Documentation:
production.order
This documentation is generated for developer onboarding and reference purposes.