Skip to content

Stock Grade Line Documentation

Overview

The Stock Grade Line module (stock.grade.line) represents individual graded product lines within a grading operation. Each line specifies a graded product, its quantity, grade quality breakdown (Grade-A, Grade-B, waste), and cost allocation.


Model Information

Model Name: stock.grade.line
Display Name: (Not explicitly defined)
Key Fields: None (no unique constraint defined)

Features

  • ❌ Audit logging enabled (_audit_log)
  • ❌ Multi-company support (company_id)
  • ❌ Full-text content search (_content_search)
  • ❌ Unique key constraint

Field Reference

Header Fields

Field Type Required Description
grade_id Many2One Parent grading operation (on_delete="cascade")
product_id Many2One Graded product SKU (stock type only, searchable)
qty Decimal Total quantity of this graded product
uom_id Many2One Unit of measure
location_id Many2One Destination location for this graded product (searchable)

Grade Breakdown Fields

Field Type Description
qty_ga Decimal Quantity classified as Grade-A quality
qty_gb Decimal Quantity classified as Grade-B quality
product_gb_id Many2One Separate product SKU for Grade-B (if different from main product)
qty_waste Decimal Quantity classified as waste/scrap
qty_loss Decimal Computed: Quantity lost or unaccounted for

Lot Tracking

Field Type Description
lot_id Many2One Lot or serial number for traceability (searchable)

Cost Tracking

Field Type Description
unit_price Decimal Cost per unit for this graded product
amount Decimal Total cost for this line (qty × unit_price)
Field Type Description
related_id Reference Generic reference to source: picking or production order
picking_id Many2One Source stock picking (if grading from receiving)
purchase_id Many2One Source purchase order
production_id Many2One Source production order
qty_remain Decimal Computed: Remaining quantity to grade from source

Computed Fields Functions

get_qty_loss(ids, context)

Calculates quantity lost or unaccounted for in this grading line.

Formula:

qty_loss = qty - qty_ga - qty_gb - qty_waste

Example:

# Line: qty=100, qty_ga=80, qty_gb=15, qty_waste=3
# qty_loss = 100 - 80 - 15 - 3 = 2

line = get_model("stock.grade.line").browse(line_id)
print(f"Loss: {line.qty_loss}")  # 2

Returns: Dictionary mapping line ID to loss quantity


get_qty_remain(ids, context)

Calculates remaining quantity that still needs to be graded from the source purchase order.

Logic: 1. Identifies all purchase orders linked to grading lines 2. For each purchase order: - Sums total quantities received across all PO lines - Sums total quantities already graded across all grading records 3. Calculates remaining = received - graded

Complex Multi-Step Process: 1. Collects purchase IDs from all lines being queried 2. Retrieves quantity received per (purchase_id, product_id) combination 3. Finds all grading records linked to those purchases 4. Sums graded quantities per (purchase_id, product_id) 5. Computes remaining for each line

Returns: Dictionary mapping line ID to remaining quantity

Example:

# Purchase Order received: 1000kg of Product A
# Grading 1: 400kg graded
# Grading 2 (current): 300kg graded
# qty_remain = 1000 - 400 - 300 = 300kg

line = get_model("stock.grade.line").browse(line_id)
print(f"Remaining to grade: {line.qty_remain}kg")


get_amount(ids, context)

Calculates line amount based on quantity and unit price.

Formula:

amount = qty × unit_price

Note: This function is defined but not actively used as a computed field in the model definition. Amount is typically calculated via onchange events instead.


API Methods

1. Create Grade Line

Method: create(vals, context)

Creates a new grading line record.

Parameters:

vals = {
    "grade_id": 1,                   # Required: parent grading
    "product_id": 123,               # Required: graded product
    "qty": 800.0,                    # Required: quantity
    "location_id": 10,               # Required: destination
    "lot_id": 456,                   # Optional: lot tracking
    "qty_ga": 750.0,                 # Optional: Grade-A breakdown
    "qty_gb": 40.0,                  # Optional: Grade-B breakdown
    "qty_waste": 10.0,               # Optional: waste
    "unit_price": 12.50,             # Optional: cost
    "amount": 10000.00,              # Optional: total cost
    "purchase_id": 789,              # Optional: source PO
}

Returns: int - New line ID

Example:

# Create grading line for Grade-A product
line_id = get_model("stock.grade.line").create({
    "grade_id": grade_id,
    "product_id": grade_a_product_id,
    "qty": 800.0,
    "location_id": grade_a_location_id,
    "qty_ga": 800.0,  # All is Grade-A
    "qty_gb": 0.0,
    "qty_waste": 0.0,
    "unit_price": 12.50,
    "amount": 10000.00,
    "purchase_id": po_id,
    "lot_id": lot_id
})


Model Relationship Description
stock.grade Many2One Parent grading operation
product Many2One Graded product and optional Grade-B product
stock.location Many2One Destination location for graded product
stock.lot Many2One Lot/serial number for traceability
uom Many2One Unit of measure
stock.picking Many2One/Reference Source picking
purchase.order Many2One Source purchase order
production.order Many2One Source production order

Common Use Cases

Use Case 1: Create Multi-Grade Line

# When a graded product has mixed quality
line_id = get_model("stock.grade.line").create({
    "grade_id": grade_id,
    "product_id": graded_product_id,
    "qty": 1000.0,
    "location_id": main_storage_id,
    "qty_ga": 850.0,      # 85% Grade-A
    "qty_gb": 130.0,      # 13% Grade-B
    "qty_waste": 15.0,    # 1.5% waste
    # qty_loss = 5.0 (computed: 0.5% loss)
    "unit_price": 10.00,
    "amount": 10000.00,
    "lot_id": lot_id
})

Use Case 2: Separate Grade-B Product

# When Grade-B uses different product SKU
# Line 1: Grade-A
line_a_id = get_model("stock.grade.line").create({
    "grade_id": grade_id,
    "product_id": grade_a_sku_id,
    "qty": 850.0,
    "location_id": grade_a_location_id,
    "qty_ga": 850.0,
    "unit_price": 12.00,
    "amount": 10200.00
})

# Line 2: Grade-B with separate SKU
line_b_id = get_model("stock.grade.line").create({
    "grade_id": grade_id,
    "product_id": grade_b_sku_id,  # Different product
    "qty": 130.0,
    "location_id": grade_b_location_id,
    "qty_gb": 130.0,
    "unit_price": 7.00,
    "amount": 910.00,
    "product_gb_id": grade_b_sku_id  # Reference to Grade-B product
})

# Line 3: Waste
line_w_id = get_model("stock.grade.line").create({
    "grade_id": grade_id,
    "product_id": waste_sku_id,
    "qty": 15.0,
    "location_id": waste_location_id,
    "qty_waste": 15.0,
    "unit_price": 0.00,
    "amount": 0.00
})

Use Case 3: Track Grading from Purchase Order

# Create grading lines linked to purchase order
def create_grading_from_po(po_id, grade_id):
    po = get_model("purchase.order").browse(po_id)

    for po_line in po.lines:
        if po_line.qty_received > 0:
            # Create grading line
            get_model("stock.grade.line").create({
                "grade_id": grade_id,
                "product_id": po_line.product_id.id,
                "qty": po_line.qty_received,
                "location_id": grade_a_location_id,
                "purchase_id": po_id,
                "unit_price": po_line.unit_price,
                "amount": po_line.qty_received * po_line.unit_price
            })

# Check remaining quantity to grade
grade = get_model("stock.grade").browse(grade_id)
for line in grade.lines:
    if line.qty_remain > 0:
        print(f"Product {line.product_id.name}: {line.qty_remain} remaining to grade")

Use Case 4: Cost Allocation Based on Grade

# Allocate costs differently based on grade quality
def allocate_grading_costs(total_cost, qty_ga, qty_gb, qty_waste):
    """
    Allocate total cost across grades
    Grade-A: Full cost
    Grade-B: 60% of full cost
    Waste: Zero cost
    """
    total_weighted = qty_ga + (qty_gb * 0.6)

    if total_weighted == 0:
        return 0, 0, 0

    unit_cost = total_cost / total_weighted

    cost_ga = qty_ga * unit_cost
    cost_gb = qty_gb * unit_cost * 0.6
    cost_waste = 0

    return cost_ga / qty_ga if qty_ga else 0, \
           cost_gb / qty_gb if qty_gb else 0, \
           0

# Apply to grading lines
grade_a_unit_price, grade_b_unit_price, waste_unit_price = \
    allocate_grading_costs(10000.00, 850.0, 130.0, 15.0)

# Create lines with calculated prices
lines = [
    {
        "product_id": grade_a_id,
        "qty": 850.0,
        "unit_price": grade_a_unit_price,
        "amount": 850.0 * grade_a_unit_price
    },
    {
        "product_id": grade_b_id,
        "qty": 130.0,
        "unit_price": grade_b_unit_price,
        "amount": 130.0 * grade_b_unit_price
    },
    {
        "product_id": waste_id,
        "qty": 15.0,
        "unit_price": 0.00,
        "amount": 0.00
    }
]

Use Case 5: Analyze Grade Distribution

# Analyze grading patterns
def analyze_grade_distribution(product_id, date_from, date_to):
    # Find all grading lines for product in date range
    line_ids = get_model("stock.grade.line").search([
        ["product_id", "=", product_id],
        ["grade_id.date", ">=", date_from],
        ["grade_id.date", "<=", date_to],
        ["grade_id.state", "=", "done"]
    ])

    total_qty = 0
    total_ga = 0
    total_gb = 0
    total_waste = 0
    total_loss = 0

    for line in get_model("stock.grade.line").browse(line_ids):
        total_qty += line.qty or 0
        total_ga += line.qty_ga or 0
        total_gb += line.qty_gb or 0
        total_waste += line.qty_waste or 0
        total_loss += line.qty_loss or 0

    print(f"Grade Distribution Analysis:")
    print(f"  Total Graded: {total_qty}")
    print(f"  Grade-A: {total_ga} ({total_ga/total_qty*100:.1f}%)")
    print(f"  Grade-B: {total_gb} ({total_gb/total_qty*100:.1f}%)")
    print(f"  Waste: {total_waste} ({total_waste/total_qty*100:.1f}%)")
    print(f"  Loss: {total_loss} ({total_loss/total_qty*100:.1f}%)")

Understanding Grade Breakdown

Each grading line can track quality distribution:

Total Quantity: 1000 units
├─ Grade-A (qty_ga): 850 units (85%)
├─ Grade-B (qty_gb): 130 units (13%)
├─ Waste (qty_waste): 15 units (1.5%)
└─ Loss (qty_loss): 5 units (0.5%) [computed]

This allows: - Cost allocation based on quality - Inventory tracking by grade - Quality analysis over time - Loss identification and monitoring


Best Practices

1. Account for All Quantities

# Good: All quantity accounted for
line = {
    "qty": 1000.0,
    "qty_ga": 850.0,
    "qty_gb": 130.0,
    "qty_waste": 20.0
    # qty_loss will be 0
}

# Bad: Large unaccounted loss
line = {
    "qty": 1000.0,
    "qty_ga": 850.0,
    "qty_gb": 130.0,
    "qty_waste": 0.0
    # qty_loss will be 20 - investigate!
}

2. Use Lot Tracking

# Enable traceability through lot tracking
line = {
    "grade_id": grade_id,
    "product_id": product_id,
    "qty": 500.0,
    "lot_id": source_lot_id,  # Track back to source
    "location_id": dest_location_id
}

# Later: trace graded products back to source
def trace_grade_source(lot_id):
    lines = get_model("stock.grade.line").search([
        ["lot_id", "=", lot_id]
    ])

    for line in get_model("stock.grade.line").browse(lines):
        print(f"Lot {lot_id} graded in {line.grade_id.number}")
        print(f"  Qty: {line.qty}")
        print(f"  Grade-A: {line.qty_ga}")
        print(f"  Source PO: {line.purchase_id.number if line.purchase_id else 'N/A'}")

3. Consistent Cost Allocation

# Establish standard cost allocation rules
GRADE_COST_FACTORS = {
    "grade_a": 1.0,     # 100% of base cost
    "grade_b": 0.6,     # 60% of base cost
    "waste": 0.0        # No cost
}

def calculate_line_costs(base_unit_cost, qty_ga, qty_gb, qty_waste):
    return {
        "grade_a_unit_price": base_unit_cost * GRADE_COST_FACTORS["grade_a"],
        "grade_b_unit_price": base_unit_cost * GRADE_COST_FACTORS["grade_b"],
        "waste_unit_price": base_unit_cost * GRADE_COST_FACTORS["waste"]
    }

4. Monitor Loss Rates

# Set acceptable loss thresholds
MAX_ACCEPTABLE_LOSS_PCT = 2.0  # 2%

def validate_loss_rate(line):
    if line.qty == 0:
        return True

    loss_pct = (line.qty_loss / line.qty) * 100

    if loss_pct > MAX_ACCEPTABLE_LOSS_PCT:
        print(f"⚠ Warning: Line {line.id} has {loss_pct:.1f}% loss (threshold: {MAX_ACCEPTABLE_LOSS_PCT}%)")
        return False

    return True

Search Functions

Search by Product

# Find all grading lines for a product
line_ids = get_model("stock.grade.line").search([
    ["product_id", "=", product_id]
])

Search by Location

# Find all grading lines going to a location
line_ids = get_model("stock.grade.line").search([
    ["location_id", "=", location_id]
])

Search by Purchase Order

# Find all grading lines from a purchase order
line_ids = get_model("stock.grade.line").search([
    ["purchase_id", "=", po_id]
])

Search by Lot

# Find all grading lines for a lot
line_ids = get_model("stock.grade.line").search([
    ["lot_id", "=", lot_id]
])

Search by Date Range (via parent)

# Find grading lines in date range
line_ids = get_model("stock.grade.line").search([
    ["grade_id.date", ">=", "2025-01-01"],
    ["grade_id.date", "<=", "2025-12-31"],
    ["grade_id.state", "=", "done"]
])

Performance Tips

1. Batch Line Processing

# Good: Create multiple lines in one transaction
lines_data = [
    {"product_id": prod_a_id, "qty": 800.0, ...},
    {"product_id": prod_b_id, "qty": 150.0, ...},
    {"product_id": waste_id, "qty": 20.0, ...}
]

for line_data in lines_data:
    get_model("stock.grade.line").create(line_data)

2. Optimize qty_remain Calculation

The get_qty_remain function performs complex multi-model queries. Cache results when possible:

# Cache remaining quantities for a grading session
def get_remaining_cache(purchase_ids):
    cache = {}

    for po_id in purchase_ids:
        po = get_model("purchase.order").browse(po_id)
        for line in po.lines:
            key = (po_id, line.product_id.id)
            cache[key] = line.qty_received

    # Subtract already graded
    grade_lines = get_model("stock.grade.line").search([
        ["purchase_id", "in", purchase_ids]
    ])

    for line in get_model("stock.grade.line").browse(grade_lines):
        key = (line.purchase_id.id, line.product_id.id)
        if key in cache:
            cache[key] -= line.qty

    return cache

Troubleshooting

"qty_loss is unexpectedly high"

Cause: Sum of qty_ga, qty_gb, and qty_waste doesn't match total qty
Solution: - Review grading data for accuracy - Check if waste quantity was properly recorded - Investigate potential measurement errors

"qty_remain shows negative value"

Cause: More quantity graded than received in purchase order
Solution: - Verify purchase order received quantities - Check for duplicate grading entries - Review grading quantities for errors

"Cost allocation doesn't sum to total"

Cause: Inconsistent unit_price calculations across lines
Solution: - Use standard cost allocation formula - Ensure all lines account for total cost - Consider rounding errors in calculations

"Cannot find grading lines by date"

Cause: Searching on line directly instead of through parent
Solution:

# Correct: Search through parent relationship
line_ids = get_model("stock.grade.line").search([
    ["grade_id.date", ">=", date_from],
    ["grade_id.date", "<=", date_to]
])


Integration Points

Purchase Order Integration

  • Links grading lines to source purchase orders
  • Tracks remaining quantities to grade
  • Enables cost traceability from procurement

Production Order Integration

  • Links grading lines to production outputs
  • Tracks quality of manufactured goods
  • Supports yield analysis

Lot/Serial Tracking

  • Maintains traceability through lot numbers
  • Enables recall management
  • Supports quality tracking by batch

Reporting Examples

Grade Quality Report

def generate_quality_report(date_from, date_to):
    line_ids = get_model("stock.grade.line").search([
        ["grade_id.date", ">=", date_from],
        ["grade_id.date", "<=", date_to],
        ["grade_id.state", "=", "done"]
    ])

    by_product = {}

    for line in get_model("stock.grade.line").browse(line_ids):
        prod_name = line.product_id.name

        if prod_name not in by_product:
            by_product[prod_name] = {
                "total": 0, "ga": 0, "gb": 0, 
                "waste": 0, "loss": 0
            }

        by_product[prod_name]["total"] += line.qty or 0
        by_product[prod_name]["ga"] += line.qty_ga or 0
        by_product[prod_name]["gb"] += line.qty_gb or 0
        by_product[prod_name]["waste"] += line.qty_waste or 0
        by_product[prod_name]["loss"] += line.qty_loss or 0

    # Print report
    print("Quality Report")
    print("=" * 80)
    for product, data in by_product.items():
        print(f"\n{product}:")
        print(f"  Total: {data['total']}")
        print(f"  Grade-A: {data['ga']} ({data['ga']/data['total']*100:.1f}%)")
        print(f"  Grade-B: {data['gb']} ({data['gb']/data['total']*100:.1f}%)")
        print(f"  Waste: {data['waste']} ({data['waste']/data['total']*100:.1f}%)")
        print(f"  Loss: {data['loss']} ({data['loss']/data['total']*100:.1f}%)")

Cost Analysis Report

def generate_cost_report(date_from, date_to):
    line_ids = get_model("stock.grade.line").search([
        ["grade_id.date", ">=", date_from],
        ["grade_id.date", "<=", date_to],
        ["grade_id.state", "=", "done"]
    ])

    total_amount = 0
    total_qty = 0

    for line in get_model("stock.grade.line").browse(line_ids):
        total_amount += line.amount or 0
        total_qty += line.qty or 0

    avg_unit_cost = total_amount / total_qty if total_qty else 0

    print(f"Cost Analysis Report")
    print(f"  Total Value: ${total_amount:,.2f}")
    print(f"  Total Quantity: {total_qty:,.2f}")
    print(f"  Average Unit Cost: ${avg_unit_cost:.2f}")

Version History

Last Updated: October 2025
Model Version: stock_grade_line.py
Framework: Netforce


Additional Resources

  • Stock Grade Documentation: stock.grade
  • Product Documentation: product
  • Stock Location Documentation: stock.location
  • Purchase Order Documentation: purchase.order
  • Production Order Documentation: production.order
  • Stock Lot Documentation: stock.lot

This documentation is generated for developer onboarding and reference purposes.