Skip to content

Sales Return Line Documentation

Overview

The Sales Return Line module (sale.return.line) represents individual line items within a sales return document. Each line captures details about a specific product being returned, including quantity, pricing, return reason, and condition. This model works in conjunction with sale.return to provide granular tracking of returned items and integrates with inventory and accounting systems for accurate financial and stock reporting.


Model Information

Model Name: sale.return.line Display Name: Sales Return Line Name Field: order_id (references parent return)

Features

  • ❌ Audit logging enabled
  • ❌ Multi-company support (inherited from parent)
  • ❌ Full-text content search
  • ❌ Unique key constraint
  • ✅ Automatic cascade delete with parent return

Key Fields Reference

Core Line Item Fields

Field Type Required Description
order_id Many2One Parent sales return (on_delete="cascade")
product_id Many2One Product being returned
description Text Description of the return item
qty Decimal Quantity being returned
uom_id Many2One Unit of measure
unit_price Decimal Unit price for the return (scale=6)
tax_id Many2One Tax rate applied

Return-Specific Fields

Field Type Description
return_type Selection Type of return: refund/exchange
reason_code_id Many2One Reason code for the return (type="sale_return")
remark Char Additional notes about the return
sequence Char Item sequence/line number

Quantity Tracking Fields

Field Type Description
qty_stock Decimal Quantity in stock UoM (for UoM conversion)
qty_received Decimal Quantity received from customer (computed)
qty_invoiced Decimal Quantity already credited (computed)
qty2 Decimal Secondary quantity (for dual UoM tracking)

Financial Fields (Computed)

Field Type Description
amount Decimal Line total amount (stored, function order 1)
amount_cur Decimal Amount in company currency (stored)
amount_discount Decimal Total discount amount on this line
discount Decimal Discount percentage
discount_amount Decimal Fixed discount amount

Location & Logistics

Field Type Description
location_id Many2One Stock location where items will be received
ship_method_id Many2One Shipping method for this line item
qty_avail Decimal Current stock available at location (computed)
Field Type Description
contact_id Many2One Customer (from order_id.contact_id)
date Date Return date (from order_id.date)
user_id Many2One Owner (from order_id.user_id)
state Selection Return state (from order_id.state)
product_categs Many2Many Product categories (from product_id.categs)

Aggregation Fields

Field Type Description
agg_amount Decimal Sum of line amounts (for reporting)
agg_qty Decimal Sum of quantities (for reporting)

API Methods

1. Create Record

Method: create(vals, context)

Creates a new return line item.

Parameters:

vals = {
    "order_id": 123,               # Required: Parent return ID
    "product_id": 789,             # Product being returned
    "description": "Defective",    # Required: Description
    "qty": 2,                      # Quantity to return
    "unit_price": 100.00,          # Required: Unit price
    "uom_id": 1,                   # Unit of measure
    "tax_id": 5,                   # Tax rate
    "location_id": 10,             # Destination location
    "return_type": "refund",       # Return type
    "reason_code_id": 15,          # Reason code
    "discount": 10.0,              # Discount percentage
    "discount_amount": 5.00        # Fixed discount
}

Behavior: - Auto-triggers function_store to calculate amounts - Cascades to parent return for amount recalculation

Returns: int - New line ID

Example:

# Add a return line for defective product
line_id = get_model("sale.return.line").create({
    "order_id": 123,
    "product_id": 789,
    "description": "Widget - manufacturing defect",
    "qty": 2,
    "unit_price": 100.00,
    "uom_id": 1,
    "tax_id": 5,
    "location_id": 10,
    "return_type": "refund",
    "reason_code_id": 15
})


2. Update Record

Method: write(ids, vals, context)

Updates existing return line(s).

Behavior: - Triggers function_store to recalculate amounts - Updates parent return totals automatically

Example:

# Update quantity and price
get_model("sale.return.line").write([line_id], {
    "qty": 3,
    "unit_price": 95.00
})


Computed Fields Functions

get_amount(ids, context)

Calculates financial amounts for each line: - amount: qty × unit_price - discount - discount_amount - amount_cur: Amount converted to company currency - amount_discount: Total discount (percentage + fixed)

Calculation Logic:

amt = line.qty * line.unit_price
if line.discount:
    disc = amt * line.discount / 100
else:
    disc = 0
if line.discount_amount:
    disc += line.discount_amount
amt -= disc

Returns: Dictionary with calculated amounts for each line


get_qty_received(ids, context)

Calculates quantity received from customer based on completed stock moves.

Behavior: - Aggregates done stock moves from parent return - Groups by product and location - Handles multiple lines for same product - Allocates received quantities to lines in order - Handles surplus quantities by adding to last lines

Returns: Dictionary mapping line IDs to received quantities


get_qty_invoiced(ids, context)

Calculates quantity already credited on invoices.

Behavior: - Scans all credit notes from parent return - Matches by product_id (or description if no product) - Excludes voided invoices - Handles both out and in invoice types

Returns: Dictionary mapping line IDs to invoiced quantities


get_qty_avail(ids, context)

Returns current stock available at the line's location.

Behavior: - Calls stock.location.compute_balance for the product/location - Returns None if product or location not specified

Returns: Dictionary mapping line IDs to available quantities


_get_related(ids, context)

Generic function to retrieve related field values from parent return or product.

Used For: - contact_id: From order_id.contact_id - date: From order_id.date - user_id: From order_id.user_id - state: From order_id.state - product_categs: From product_id.categs


_search_related(clause, context)

Enables searching on related fields.

Example:

# Search lines by customer
lines = get_model("sale.return.line").search([
    ["contact_id", "=", 123]
])

# Search lines by state
lines = get_model("sale.return.line").search([
    ["state", "=", "confirmed"]
])


Search Functions

Search by Product

# Find all return lines for a specific product
condition = [["product_id", "=", product_id]]
line_ids = get_model("sale.return.line").search(condition)

Search by Customer

# Find all return lines for a customer
condition = [["contact_id", "=", customer_id]]
line_ids = get_model("sale.return.line").search(condition)

Search by State

# Find all confirmed return lines
condition = [["state", "=", "confirmed"]]
line_ids = get_model("sale.return.line").search(condition)

Search by Product Category

# Find returns for products in a category
condition = [["product_categs", "in", [category_id]]]
line_ids = get_model("sale.return.line").search(condition)

Search by Date Range

# Find return lines in a date range
condition = [
    ["date", ">=", "2025-01-01"],
    ["date", "<=", "2025-01-31"]
]
line_ids = get_model("sale.return.line").search(condition)

Best Practices

1. Always Set Location for Stock Products

# Bad: Missing location for stock product
{
    "product_id": 789,  # Stock product
    "qty": 5,
    "unit_price": 100.00
    # Missing location_id!
}

# Good: Location specified
{
    "product_id": 789,
    "qty": 5,
    "unit_price": 100.00,
    "location_id": 10,  # Warehouse location
    "return_type": "refund",
    "reason_code_id": 15
}

2. Use Reason Codes for Better Tracking

# Good: Detailed return tracking
{
    "product_id": 789,
    "qty": 2,
    "unit_price": 100.00,
    "return_type": "refund",
    "reason_code_id": 15,  # e.g., "Defective"
    "remark": "Widget not functioning - motor failure"
}

3. Handle UoM Conversions Properly

# When using different UoM than product default
line_vals = {
    "product_id": 789,
    "qty": 10,  # 10 boxes
    "uom_id": box_uom_id,
    "qty_stock": 100,  # 100 pieces in stock UoM
    "unit_price": 50.00  # Price per box
}

Model Relationship Description
sale.return Many2One Parent return document
product Many2One Product being returned
contact Function Field Customer (from parent)
uom Many2One Unit of measure
account.tax.rate Many2One Tax rate for line
stock.location Many2One Destination warehouse location
ship.method Many2One Shipping method
reason.code Many2One Return reason code

Common Use Cases

Use Case 1: Create Return Line with Full Details

# Complete return line with all tracking fields
line_id = get_model("sale.return.line").create({
    "order_id": return_id,
    "product_id": 789,
    "description": "Wireless Mouse - Model XYZ",
    "qty": 5,
    "uom_id": 1,  # PCS
    "unit_price": 25.00,
    "tax_id": 5,  # VAT 7%
    "location_id": 10,  # Main Warehouse
    "discount": 10.0,  # 10% discount
    "return_type": "refund",
    "reason_code_id": 15,  # Defective
    "remark": "Mouse buttons not working properly",
    "sequence": "001"
})

Use Case 2: Check Line Status

# Check quantities and status
line = get_model("sale.return.line").browse([line_id])[0]

print(f"Product: {line.product_id.name}")
print(f"Ordered Qty: {line.qty}")
print(f"Received Qty: {line.qty_received}")
print(f"Invoiced Qty: {line.qty_invoiced}")
print(f"Pending Receipt: {line.qty - line.qty_received}")
print(f"Pending Credit: {line.qty - line.qty_invoiced}")
print(f"Amount: {line.amount}")
print(f"State: {line.state}")

Use Case 3: Bulk Update Return Lines

# Update multiple lines with same discount
return_obj = get_model("sale.return").browse([return_id])[0]
line_ids = [line.id for line in return_obj.lines]

get_model("sale.return.line").write(line_ids, {
    "discount": 15.0  # Apply 15% discount to all lines
})

Use Case 4: Find Unreceived Return Lines

# Find lines where goods not yet received
all_lines = get_model("sale.return.line").search([
    ["state", "=", "confirmed"]
])

unreceived_lines = []
for line in get_model("sale.return.line").browse(all_lines):
    if line.product_id and line.product_id.type == "stock":
        if line.qty_received < line.qty:
            unreceived_lines.append(line.id)

print(f"Found {len(unreceived_lines)} lines waiting for receipt")

Performance Tips

1. Stored Function Fields

  • amount and amount_cur are stored for fast retrieval
  • Recalculated automatically on line changes
  • Use function_order=1 to ensure proper calculation sequence

2. Batch Browsing

# Bad: One query per line
for line_id in line_ids:
    line = get_model("sale.return.line").browse([line_id])[0]
    process_line(line)

# Good: Single query for all lines
lines = get_model("sale.return.line").browse(line_ids)
for line in lines:
    process_line(line)

3. Aggregation Fields

Use aggregation fields for reporting instead of manual sum:

# Use built-in aggregation
result = get_model("sale.return.line").search_read(
    [["contact_id", "=", 123]],
    ["agg_amount", "agg_qty"],
    group=["contact_id"]
)


Troubleshooting

"Description is required"

Cause: Creating line without description field Solution: Always provide description, even if using product name

Incorrect Amount Calculation

Cause: Amount not recalculating after changes Solution: Amounts auto-recalculate via function_store; check parent return state

Qty Received Not Updating

Cause: Stock moves not in "done" state Solution: Complete the goods receipt picking to update qty_received

Qty Invoiced Not Matching

Cause: Credit note voided or not posted Solution: Check credit note state; only non-voided invoices count


Testing Examples

Unit Test: Line Amount Calculation

def test_line_amount_calculation():
    # Create return
    return_id = get_model("sale.return").create({
        "contact_id": 123,
        "date": "2025-01-05",
        "currency_id": 1,
        "tax_type": "tax_ex"
    })

    # Create line with discount
    line_id = get_model("sale.return.line").create({
        "order_id": return_id,
        "description": "Test Product",
        "qty": 10,
        "unit_price": 100.00,
        "discount": 10.0,  # 10% discount
        "discount_amount": 50.00  # Additional $50 off
    })

    # Verify calculation
    line = get_model("sale.return.line").browse([line_id])[0]
    # Expected: (10 * 100) - (1000 * 0.1) - 50 = 1000 - 100 - 50 = 850
    assert line.amount == 850.00
    assert line.amount_discount == 150.00

Security Considerations

Data Access

  • Lines inherit security from parent return
  • Cascade delete ensures data integrity
  • No direct multi-company field (uses parent)

Validation

  • Parent return must exist (on_delete="cascade")
  • Unit price required for financial accuracy
  • Description required for clarity

Configuration Settings

Required Settings

Setting Location Description
UoM Product Default unit of measure
Tax Rates Account Settings Tax configurations
Locations Stock Warehouse locations
Reason Codes Settings Return reason codes (type="sale_return")

Integration Points

Stock Management

  • stock.move: Tracked via qty_received computation
  • stock.location: Destination for returned goods
  • product: Product information and stock tracking

Accounting

  • account.invoice.line: Credit note lines (qty_invoiced)
  • account.tax.rate: Tax calculations
  • currency: Multi-currency amounts

Parent Document

  • sale.return: Parent document for all line data
  • Changes propagate to parent totals

Version History

Last Updated: 2025-01-05 Model Version: sale_return_line.py (168 lines) Framework: Netforce


Additional Resources

  • Sales Return Documentation: sale.return
  • Product Documentation: product
  • Stock Location Documentation: stock.location
  • Reason Code Documentation: reason.code

Support & Feedback

For issues or questions about this module: 1. Check parent return documentation 2. Verify product and location configuration 3. Check reason code setup 4. Review stock move status for qty_received issues 5. Check invoice status for qty_invoiced issues


This documentation is generated for developer onboarding and reference purposes.