Skip to content

Product Waste Documentation

Overview

The Product Waste module (product.waste) records and tracks waste or scrap of inventory items. It creates automatic stock movements from internal locations to waste locations, maintaining accurate inventory records for waste management and accounting purposes.


Model Information

Model Name: product.waste
Display Name: Product Waste
Name Field: number
Key Fields: N/A

Features

  • ✅ Automated sequence numbering
  • ✅ State workflow management
  • ✅ Automatic stock movement creation
  • ✅ Waste location tracking

State Workflow

draft → done
voided
State Description
draft Initial state, not yet validated
done Validated and stock moved to waste
voided Cancelled/voided

Key Fields Reference

Header Fields

Field Type Required Description
number Char Auto-generated waste number
date Date Waste date
location_id Many2One Source location (stock.location, internal)
product_id Many2One Product being wasted (product, stock type)
lot_id Many2One Lot/Serial number (stock.lot)
qty Decimal Waste quantity
uom_id Many2One Unit of measure (uom)
user_id Many2One User who recorded waste (base.user)
state Selection Current workflow state
notes Text Notes about waste reason

Relationship Fields

Field Type Description
stock_moves One2Many Stock movements created (stock.move)

API Methods

1. Create Waste Record

Method: create(vals, context)

Creates a new waste record with auto-generated number.

Parameters:

vals = {
    "date": "2025-10-27",                 # Required: Waste date
    "location_id": 123,                    # Required: Source location
    "product_id": 456,                     # Required: Product ID
    "qty": 5.0,                            # Required: Quantity
    "uom_id": 1,                           # Required: UoM
    "lot_id": 789,                         # Optional: Lot/serial
    "notes": "Damaged during handling"     # Optional: Reason
}

Returns: int - New waste record ID

Example:

# Record waste for damaged products
waste_id = get_model("product.waste").create({
    "date": "2025-10-27",
    "location_id": warehouse_loc_id,
    "product_id": product_id,
    "qty": 3,
    "uom_id": uom_id,
    "lot_id": lot_id,
    "notes": "Dropped during transfer, packaging damaged"
})


2. Validate Waste

Method: validate(ids, context)

Validates waste and creates stock movements to waste location.

Parameters: - ids (list): Waste record IDs

Behavior: - Finds waste location (type="waste") - Gets waste journal from settings - Creates stock move from source to waste location - Sets move to done immediately - Updates waste state to "done"

Example:

# Validate waste to move stock
get_model("product.waste").validate([waste_id])

# Verify stock moved
waste = get_model("product.waste").browse(waste_id)
assert waste.state == "done"
assert len(waste.stock_moves) > 0


3. Revert to Draft

Method: to_draft(ids, context)

Reverts validated waste back to draft, deleting stock movements.

Parameters: - ids (list): Waste record IDs

Example:

# Revert to draft if validated by mistake
get_model("product.waste").to_draft([waste_id])

# Verify reverted
waste = get_model("product.waste").browse(waste_id)
assert waste.state == "draft"
assert len(waste.stock_moves) == 0


4. Void Waste

Method: void(ids, context)

Voids the waste record, deleting any stock movements.

Parameters: - ids (list): Waste record IDs

Example:

# Void incorrect waste record
get_model("product.waste").void([waste_id])


5. Delete Waste

Method: delete(ids, context)

Deletes waste record. Only allowed in certain states.

Validation: - Cannot delete if not in appropriate state

Example:

# Delete draft waste
try:
    get_model("product.waste").delete([waste_id])
except Exception as e:
    print(f"Cannot delete: {e}")


UI Events (onchange methods)

onchange_product

Triggered when product is selected. Updates: - UoM from product's default UoM

Usage:

data = {"product_id": 123}
result = get_model("product.waste").onchange_product(
    context={"data": data}
)
# Result updates uom_id


Configuration Settings

Required Settings

Setting Location Description
waste_journal_id settings (ID:1) Journal for waste transactions

Required System Data

  • Waste location must exist (type="waste")
  • Waste sequence must be configured

Setup Example:

# Ensure waste location exists
res = get_model("stock.location").search([["type", "=", "waste"]])
if not res:
    waste_loc_id = get_model("stock.location").create({
        "name": "Waste/Scrap",
        "type": "waste"
    })

# Configure waste journal
settings = get_model("settings").browse(1)
settings.write({"waste_journal_id": journal_id})


Model Relationship Description
stock.location Many2One Source and waste locations
product Many2One Product being wasted
stock.lot Many2One Lot/serial tracking
uom Many2One Unit of measure
stock.move One2Many Stock movements created
base.user Many2One User who recorded waste

Common Use Cases

Use Case 1: Record Damaged Inventory

# Product damaged during operations
waste_id = get_model("product.waste").create({
    "date": "2025-10-27",
    "location_id": production_loc_id,
    "product_id": damaged_product_id,
    "qty": 10,
    "uom_id": uom_id,
    "notes": "Damaged during production - quality control rejection"
})

# Validate to move to waste
get_model("product.waste").validate([waste_id])

Use Case 2: Record Expired Product by Lot

# Record expired products
expired_lot_id = 456

waste_id = get_model("product.waste").create({
    "date": "2025-10-27",
    "location_id": warehouse_loc_id,
    "product_id": product_id,
    "lot_id": expired_lot_id,
    "qty": 50,
    "uom_id": uom_id,
    "notes": "Expired - past use-by date"
})

get_model("product.waste").validate([waste_id])

Use Case 3: Batch Waste Recording

# Record multiple damaged items
damaged_items = [
    {"product_id": prod1_id, "qty": 5, "notes": "Broken packaging"},
    {"product_id": prod2_id, "qty": 3, "notes": "Water damage"},
    {"product_id": prod3_id, "qty": 2, "notes": "Quality defect"}
]

waste_ids = []
for item in damaged_items:
    waste_id = get_model("product.waste").create({
        "date": "2025-10-27",
        "location_id": warehouse_loc_id,
        "product_id": item["product_id"],
        "qty": item["qty"],
        "uom_id": uom_id,
        "notes": item["notes"]
    })
    waste_ids.append(waste_id)

# Validate all at once
for waste_id in waste_ids:
    get_model("product.waste").validate([waste_id])

Best Practices

1. Always Provide Waste Reason

# Good: Document why waste occurred
waste_vals = {
    "notes": "Quality control failure - wrong color specification"  # ✅ Clear reason
}

# Bad: No explanation
waste_vals = {
    "notes": ""  # ❌ No audit trail
}

2. Use Lot Tracking for Traceability

# Good: Track which lot was wasted
waste_vals = {
    "product_id": product_id,
    "lot_id": lot_id,  # ✅ Can trace source
    "qty": 5
}

# Bad: No lot information for tracked product
waste_vals = {
    "product_id": product_id,  # Product requires lot tracking
    "qty": 5
    # ❌ Missing lot_id - cannot trace
}

3. Validate Immediately After Recording

# Good: Validate right away for accurate inventory
waste_id = get_model("product.waste").create({...})
get_model("product.waste").validate([waste_id])  # ✅ Immediate stock update

# Bad: Leave in draft for extended period
waste_id = get_model("product.waste").create({...})
# ❌ Inventory inaccurate until validated

Search Functions

Search by Date Range

# Find waste recorded this month
condition = [
    ["date", ">=", "2025-10-01"],
    ["date", "<=", "2025-10-31"]
]
waste_records = get_model("product.waste").search(condition)

Search by Product

# Find all waste for specific product
condition = [["product_id", "=", product_id]]
waste_records = get_model("product.waste").search(condition)

Search by Location

# Find waste from specific location
condition = [["location_id", "=", location_id]]
waste_records = get_model("product.waste").search(condition)

Search by State

# Find pending waste records
condition = [["state", "=", "draft"]]
pending_waste = get_model("product.waste").search(condition)

Performance Tips

1. Batch Validation

# Efficient: Validate multiple waste records
waste_ids = get_model("product.waste").search([["state", "=", "draft"]])

for waste_id in waste_ids:
    get_model("product.waste").validate([waste_id])

2. Index on Search Fields

  • Fields like date, location_id, product_id, and notes are indexed with search=True
  • Use these in search conditions for better performance

Troubleshooting

"Waste location not found"

Cause: No location with type="waste" exists
Solution: Create waste location in system

waste_loc_id = get_model("stock.location").create({
    "name": "Waste/Scrap",
    "type": "waste"
})


"Waste journal not found"

Cause: waste_journal_id not configured in settings
Solution: Configure waste journal in inventory settings

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


"Can not delete waste products in this status"

Cause: Trying to delete non-draft waste
Solution: Only delete draft records; void or revert to draft first


Stock not moving to waste location

Cause: Waste not validated
Solution: Call validate() method after creating waste record


Testing Examples

Unit Test: Create and Validate Waste

def test_waste_creation_and_validation():
    # Create waste
    waste_id = get_model("product.waste").create({
        "date": "2025-10-27",
        "location_id": loc_id,
        "product_id": prod_id,
        "qty": 5,
        "uom_id": uom_id,
        "notes": "Test waste"
    })

    # Verify draft state
    waste = get_model("product.waste").browse(waste_id)
    assert waste.state == "draft"
    assert len(waste.stock_moves) == 0

    # Validate
    get_model("product.waste").validate([waste_id])

    # Verify done state and stock move created
    waste = get_model("product.waste").browse(waste_id)
    assert waste.state == "done"
    assert len(waste.stock_moves) == 1
    assert waste.stock_moves[0].state == "done"

Unit Test: Revert to Draft

def test_revert_to_draft():
    # Create and validate waste
    waste_id = get_model("product.waste").create({
        "date": "2025-10-27",
        "location_id": loc_id,
        "product_id": prod_id,
        "qty": 5,
        "uom_id": uom_id
    })
    get_model("product.waste").validate([waste_id])

    # Revert to draft
    get_model("product.waste").to_draft([waste_id])

    # Verify reverted
    waste = get_model("product.waste").browse(waste_id)
    assert waste.state == "draft"
    assert len(waste.stock_moves) == 0

Unit Test: Void Waste

def test_void_waste():
    # Create and validate waste
    waste_id = get_model("product.waste").create({
        "date": "2025-10-27",
        "location_id": loc_id,
        "product_id": prod_id,
        "qty": 5,
        "uom_id": uom_id
    })
    get_model("product.waste").validate([waste_id])

    # Void
    get_model("product.waste").void([waste_id])

    # Verify voided
    waste = get_model("product.waste").browse(waste_id)
    assert waste.state == "voided"
    assert len(waste.stock_moves) == 0

Reporting Examples

Report: Waste by Product

def get_waste_by_product_report(start_date, end_date):
    """Generate waste report grouped by product"""

    waste_records = get_model("product.waste").search_browse([
        ["date", ">=", start_date],
        ["date", "<=", end_date],
        ["state", "=", "done"]
    ])

    waste_by_product = {}
    for waste in waste_records:
        prod_id = waste.product_id.id
        prod_name = waste.product_id.name

        if prod_id not in waste_by_product:
            waste_by_product[prod_id] = {
                "product_name": prod_name,
                "total_qty": 0,
                "count": 0
            }

        waste_by_product[prod_id]["total_qty"] += waste.qty
        waste_by_product[prod_id]["count"] += 1

    return waste_by_product

# Usage
report = get_waste_by_product_report("2025-10-01", "2025-10-31")
for prod_id, data in report.items():
    print(f"{data['product_name']}: {data['total_qty']} units wasted ({data['count']} records)")

Report: Waste by Reason

def get_waste_by_reason_report(start_date, end_date):
    """Analyze waste reasons"""

    waste_records = get_model("product.waste").search_browse([
        ["date", ">=", start_date],
        ["date", "<=", end_date],
        ["state", "=", "done"]
    ])

    reasons = {}
    for waste in waste_records:
        reason = waste.notes or "No reason specified"
        if reason not in reasons:
            reasons[reason] = {"qty": 0, "count": 0}

        reasons[reason]["qty"] += waste.qty
        reasons[reason]["count"] += 1

    return reasons

# Usage
report = get_waste_by_reason_report("2025-10-01", "2025-10-31")
for reason, data in sorted(reasons.items(), key=lambda x: x[1]["qty"], reverse=True):
    print(f"{reason}: {data['qty']} units ({data['count']} occurrences)")

Version History

Last Updated: 2025-10-27
Model Version: product_waste.py
Framework: Netforce


Additional Resources

  • Stock Move Documentation: stock.move
  • Stock Location Documentation: stock.location
  • Product Documentation: product
  • Settings Configuration: Inventory Settings

This documentation is generated for developer onboarding and reference purposes.