Skip to content

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

draft → done
voided
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)
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:

qty_loss = qty - sum(line.qty for all lines)

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


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:

get_model("stock.location").create({
    "name": "Transform Location",
    "type": "transform"
})

"Missing transform journal"

Cause: Transform journal not configured in settings
Solution: Set transform journal in settings:

settings = get_model("settings").browse(1)
settings.write({"transform_journal_id": journal_id})

"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.