Skip to content

Sales Return Documentation

Overview

The Sales Return module (sale.return) manages the complete lifecycle of customer returns and product returns in the sales process. It handles return authorization, stock receipt tracking, credit note generation, and maintains a comprehensive audit trail of all return transactions. This module integrates seamlessly with inventory management (stock.picking) and accounting (account.invoice) to ensure accurate financial and inventory records.


Model Information

Model Name: sale.return Display Name: Sales Return Key Fields: company_id, number

Features

  • ✅ Audit logging enabled (_audit_log = True)
  • ✅ Multi-company support (company_id)
  • ❌ Full-text content search
  • ✅ Unique key constraint per company and return number

Understanding Key Fields

What are Key Fields?

In Netforce models, Key Fields are a combination of fields that together create a composite unique identifier for a record. Think of them as a business key that ensures data integrity across the system.

For the sale.return model, the key fields are:

_key = ["company_id", "number"]

This means the combination of these fields must be unique: - company_id - The company that owns this sales return - number - The return authorization number (e.g., "RET-2025-0001")

Why Key Fields Matter

Uniqueness Guarantee - Key fields prevent duplicate records by ensuring unique combinations:

# Examples of valid combinations:
Company A, RET-2025-0001   Valid
Company A, RET-2025-0002   Valid
Company B, RET-2025-0001   Valid (different company)

# This would fail - duplicate key:
Company A, RET-2025-0001   ERROR: Sales return number already exists!

Database Implementation

The key fields are enforced at the database level using a unique constraint:

_key = ["company_id", "number"]

This translates to:

CREATE UNIQUE INDEX sale_return_unique_key
    ON sale_return (company_id, number);

Return Types

Sales returns can handle different scenarios:

Type Code Description
Refund refund Customer receives monetary refund for returned items
Exchange exchange Customer exchanges items for different products
Credit Note credit Customer receives store credit for future purchases

State Workflow

draft → confirmed → done
                   voided
State Description
draft Initial state. Return is being prepared and can be edited freely
confirmed Return has been approved. Stock movements and invoicing can proceed
done Return is complete. All stock received and credit notes issued
voided Return has been cancelled. No further processing possible

Key Fields Reference

Header Fields

Field Type Required Description
number Char Unique return authorization number (auto-generated)
ref Char External reference number or customer return number
contact_id Many2One Customer returning the products
date Date Return date (defaults to today)
state Selection Current workflow state (draft/confirmed/done/voided)
orig_sale_id Many2One Reference to original sales order being returned

Financial Fields

Field Type Description
amount_subtotal Decimal Subtotal amount (calculated, stored)
amount_tax Decimal Total tax amount (calculated, stored)
amount_total Decimal Total return amount including tax (calculated, stored)
amount_total_discount Decimal Total discount amount applied (calculated, stored)
amount_total_cur Decimal Total in company currency (calculated, stored)
amount_total_words Char Amount in words for documents
currency_id Many2One Currency used for this return (required)
tax_type Selection Tax calculation type: tax_ex/tax_in/no_tax (required)

Address & Shipping Fields

Field Type Description
bill_address_id Many2One Billing address for credit note
ship_address_id Many2One Shipping address where goods are returned from
ship_method_id Many2One Shipping method used for return
ship_term_id Many2One Shipping terms for the return
location_id Many2One Default warehouse location (deprecated, use line-level)

Dates & Terms

Field Type Description
due_date Date Due date for credit note payment
delivery_date Date Expected delivery date of returned goods (deprecated)
payment_terms Text Payment terms for credit processing

Relationship Fields

Field Type Description
lines One2Many Return line items (sale.return.line)
invoices One2Many Related credit notes (account.invoice)
pickings Many2Many Related goods receipts (stock.picking)
stock_moves One2Many Stock movements for this return
comments One2Many Comments and notes (message)
activities One2Many Related activities
emails One2Many Email correspondence
documents One2Many Attached documents
addresses One2Many Related addresses

Status Fields (Computed)

Field Type Description
is_delivered Boolean True if all items have been received
is_paid Boolean True if all credit notes have been paid/applied
qty_total Decimal Total quantity across all lines

Administrative Fields

Field Type Description
user_id Many2One Owner/creator of the return
approved_by_id Many2One User who approved the return (readonly)
company_id Many2One Company that owns this return
sequence_id Many2One Number sequence used for numbering
price_list_id Many2One Price list for valuation
pay_method_id Many2One Payment method for refund

Aggregation Fields

Field Type Description
agg_amount_total Decimal Sum of total amounts (for reporting)
agg_amount_subtotal Decimal Sum of subtotals (for reporting)
year Char Year extracted from date (for grouping)
quarter Char Quarter extracted from date (for grouping)
month Char Month extracted from date (for grouping)
week Char Week extracted from date (for grouping)

API Methods

1. Create Record

Method: create(vals, context)

Creates a new sales return record.

Parameters:

vals = {
    "contact_id": 123,              # Required: Customer ID
    "date": "2025-01-05",          # Required: Return date
    "currency_id": 1,              # Required: Currency
    "tax_type": "tax_ex",          # Required: Tax type
    "ref": "CUST-RET-001",         # Optional: Customer reference
    "orig_sale_id": 456,           # Optional: Original sale order
    "lines": [                      # Return line items
        ("create", {
            "product_id": 789,
            "qty": 2,
            "unit_price": 100.00,
            "description": "Defective product",
            "location_id": 5,
            "return_type": "refund",
            "reason_code_id": 10
        })
    ]
}

context = {}

Returns: int - New record ID

Example:

# Create a sales return for defective products
return_id = get_model("sale.return").create({
    "contact_id": 123,
    "date": "2025-01-05",
    "currency_id": 1,
    "tax_type": "tax_ex",
    "ref": "Customer complaint #12345",
    "orig_sale_id": 456,
    "lines": [
        ("create", {
            "product_id": 789,
            "qty": 2,
            "unit_price": 100.00,
            "description": "Defective widget - manufacturing defect",
            "location_id": 5,
            "tax_id": 1,
            "return_type": "refund",
            "reason_code_id": 10
        })
    ]
})


2. Confirm Return

Method: confirm(ids, context)

Confirms a sales return, moving it from draft to confirmed state.

Parameters: - ids (list): Return IDs to confirm

Behavior: - Validates that return is in draft state - Validates that all stock products have locations assigned - Updates state to "confirmed" - Fires workflow trigger "confirm" - Returns navigation to the confirmed return form

Returns: dict - Navigation and flash message

Example:

result = get_model("sale.return").confirm([return_id])
# Returns: {
#     "next": {"name": "sale_return", "mode": "form", "active_id": return_id},
#     "flash": "Sales return RET-2025-0001 confirmed"
# }


3. State Transition Methods

3.1 Mark as Done

Method: done(ids, context)

Marks a return as completed.

Parameters: - ids (list): Return IDs to complete

Behavior: - Validates that return is in confirmed state - Updates state to "done" - No further modifications allowed

Example:

get_model("sale.return").done([return_id])

3.2 Reopen Return

Method: reopen(ids, context)

Reopens a completed return back to confirmed state.

Parameters: - ids (list): Return IDs to reopen

Behavior: - Validates that return is in done state - Updates state back to "confirmed" - Allows further modifications

Example:

get_model("sale.return").reopen([return_id])

3.3 Return to Draft

Method: to_draft(ids, context)

Moves a return back to draft state from any state.

Parameters: - ids (list): Return IDs to reset

Example:

get_model("sale.return").to_draft([return_id])

3.4 Void Return

Method: void(ids, context)

Cancels a sales return.

Parameters: - ids (list): Return IDs to void

Behavior: - Validates no pending goods receipts exist - Validates no invoices are waiting payment - Updates state to "voided"

Example:

get_model("sale.return").void([return_id])


4. Document Generation Methods

4.1 Create Goods Receipt (Picking)

Method: copy_to_picking(ids, context)

Generates a goods receipt (incoming picking) for the returned products.

Parameters: - ids (list): Return ID to process

Context Options:

context = {
    "pick_type": "in"              # Incoming picking type
}

Behavior: - Locates customer location and warehouse location - Creates incoming picking for each shipping method - Generates stock moves for unreceived quantities - Only includes stock/consumable/bundle products - Sets picking state to "draft"

Returns: dict - Navigation to created picking

Example:

result = get_model("sale.return").copy_to_picking([return_id])
# Returns: {
#     "next": {"name": "pick_in", "mode": "form", "active_id": pick_id},
#     "flash": "Picking GR-2025-0001 created from sales order RET-2025-0001",
#     "picking_id": pick_id
# }

4.2 Create Credit Note

Method: copy_to_credit_note(ids, context)

Generates credit note(s) for the returned products.

Parameters: - ids (list): Return ID to process

Behavior: - Creates separate credit notes per shipping method - Uses sale return account from product/category settings - Only includes uninvoiced quantities - Applies discounts and tax rates from return lines - Sets invoice type to "credit" - Uses contact's sale journal if configured

Returns: dict - Navigation to created credit note

Example:

result = get_model("sale.return").copy_to_credit_note([return_id])
# Returns: {
#     "next": {"name": "view_invoice", "active_id": invoice_id},
#     "flash": "Credit note created from sales order RET-2025-0001",
#     "invoice_id": invoice_id
# }


5. Helper Methods

5.1 Get Quantity to Deliver

Method: get_qty_to_deliver(ids)

Calculates remaining quantities to receive for each product.

Returns: dict - Mapping of (product_id, uom_id) to remaining quantity

Example:

to_deliver = get_model("sale.return").get_qty_to_deliver([return_id])
# Returns: {(789, 1): 2.0}  # Product 789 in UoM 1, qty 2 remaining

5.2 Copy Return

Method: copy(ids, context)

Creates a duplicate of an existing sales return.

Example:

result = get_model("sale.return").copy([return_id])

5.3 Approve Return

Method: approve(ids, context)

Records approval of a sales return.

Permission Requirements: - sale_approve_done: Required to approve returns

Behavior: - Checks permission - Records approving user ID - Updates approved_by_id field

Example:

get_model("sale.return").approve([return_id])


UI Events (onchange methods)

onchange_contact

Triggered when customer is selected. Updates: - Payment terms from customer record - Price list from customer's sale price list - Billing address (pref_type="billing") - Shipping address (pref_type="shipping")

Usage:

data = {
    "contact_id": 123
}
result = get_model("sale.return").onchange_contact(
    context={"data": data}
)
# Returns updated data with payment_terms, price_list_id, addresses

onchange_product

Triggered when a product is selected in a line. Updates: - Description from product - Quantity (defaults to 1) - UoM from product - Unit price from price list or product - Tax rate from product - Location from product

Usage:

data = {
    "contact_id": 123,
    "currency_id": 1,
    "price_list_id": 5,
    "lines": [
        {"product_id": 789}
    ]
}
result = get_model("sale.return").onchange_product(
    context={"data": data, "path": "lines.0"}
)

onchange_qty

Triggered when quantity changes. Updates: - Unit price based on quantity breaks in price list - Recalculates line amounts

onchange_uom

Triggered when UoM changes. Updates: - Unit price adjusted for UoM ratio

onchange_sequence

Triggered when sequence is selected. Updates: - Number field with next available number from sequence


Search Functions

Search by Customer

# Find all returns for a specific customer
condition = [["contact_id", "=", customer_id]]
return_ids = get_model("sale.return").search(condition)

Search by State

# Find all confirmed returns
condition = [["state", "=", "confirmed"]]
return_ids = get_model("sale.return").search(condition)

Search by Date Range

# Find returns in a specific period
condition = [
    ["date", ">=", "2025-01-01"],
    ["date", "<=", "2025-01-31"]
]
return_ids = get_model("sale.return").search(condition)

Search by Number

# Find specific return by number
condition = [["number", "=", "RET-2025-0001"]]
return_ids = get_model("sale.return").search(condition)

Computed Fields Functions

get_amount(ids, context)

Calculates financial amounts for the return: - amount_subtotal: Sum of line amounts (excluding tax for tax_in type) - amount_tax: Sum of tax amounts across all lines - amount_total: Subtotal + tax - amount_total_cur: Total converted to company currency - amount_total_discount: Total discount amounts

get_qty_total(ids, context)

Returns total quantity across all return lines.

get_delivered(ids, context)

Returns True if all stock items have been received (qty_received >= qty).

get_paid(ids, context)

Returns True if sum of paid credit notes >= amount_total.

get_pickings(ids, context)

Returns list of picking IDs associated with this return's stock moves.

get_amount_total_words(ids, context)

Converts total amount to words for printing on documents.


Workflow Integration

Trigger Events

The sales return module fires workflow triggers:

self.trigger(ids, "confirm")    # When return is confirmed

These can be configured in workflow automation to: - Send email notifications - Create approval tasks - Update external systems - Generate reports


Best Practices

1. Location Assignment

# Bad example: No location specified for stock products
{
    "product_id": 789,  # Stock product
    "qty": 5
    # Missing location_id - will cause error on confirm!
}

# Good example: Always specify location for stock products
{
    "product_id": 789,
    "qty": 5,
    "location_id": 5,  # Warehouse location
    "return_type": "refund",
    "reason_code_id": 10
}

2. Return Processing Workflow

Always follow the proper sequence:

# 1. Create and confirm return
return_id = get_model("sale.return").create({...})
get_model("sale.return").confirm([return_id])

# 2. Create goods receipt
result = get_model("sale.return").copy_to_picking([return_id])
pick_id = result["picking_id"]

# 3. Receive the goods
get_model("stock.picking").pending([pick_id])
get_model("stock.picking").approve([pick_id])

# 4. Create credit note
result = get_model("sale.return").copy_to_credit_note([return_id])
invoice_id = result["invoice_id"]

# 5. Post credit note
get_model("account.invoice").post([invoice_id])

# 6. Mark return as done
get_model("sale.return").done([return_id])

3. Credit Note Account Configuration

Ensure proper account configuration for returns:

# Configure return accounts at product level
product.write({
    "sale_return_account_id": revenue_returns_account_id
})

# Or at category level
category.write({
    "sale_return_account_id": revenue_returns_account_id
})

# System will check: product.sale_return_account_id
#                 -> product.parent_id.sale_return_account_id
#                 -> product.categ_id.sale_return_account_id

Database Constraints

Unique Key Constraint

CREATE UNIQUE INDEX sale_return_unique_key
    ON sale_return (company_id, number);

Ensures that return numbers are unique within each company.


Model Relationship Description
sale.return.line One2Many Line items for the return
contact Many2One Customer returning products
account.invoice One2Many Credit notes generated
stock.picking Many2Many Goods receipt pickings
stock.move One2Many Stock movements for returned goods
sale.order Many2One Original sales order (if applicable)
currency Many2One Transaction currency
price.list Many2One Price list for valuation
address Many2One Billing and shipping addresses
sequence Many2One Numbering sequence

Common Use Cases

Use Case 1: Process Customer Return with Refund

# Step-by-step process for handling a customer return

# 1. Create the sales return
return_id = get_model("sale.return").create({
    "contact_id": 123,
    "date": "2025-01-05",
    "currency_id": 1,
    "tax_type": "tax_ex",
    "ref": "Customer complaint #12345",
    "orig_sale_id": 456,
    "lines": [
        ("create", {
            "product_id": 789,
            "qty": 2,
            "unit_price": 100.00,
            "description": "Defective product - not working",
            "location_id": 5,
            "tax_id": 1,
            "return_type": "refund",
            "reason_code_id": 10
        })
    ]
})

# 2. Confirm the return
get_model("sale.return").confirm([return_id])

# 3. Create goods receipt to receive returned items
result = get_model("sale.return").copy_to_picking([return_id])
pick_id = result["picking_id"]

# 4. Process the goods receipt
get_model("stock.picking").pending([pick_id])
get_model("stock.picking").approve([pick_id])

# 5. Create credit note for refund
result = get_model("sale.return").copy_to_credit_note([return_id])
invoice_id = result["invoice_id"]

# 6. Post the credit note
get_model("account.invoice").post([invoice_id])

# 7. Mark return as complete
get_model("sale.return").done([return_id])

Use Case 2: Check Return Status

# Check if return is fully processed
return_obj = get_model("sale.return").browse(return_id)

if return_obj.is_delivered:
    print("All items received")
else:
    print("Waiting for goods receipt")

if return_obj.is_paid:
    print("Credit note processed")
else:
    print("Credit note pending")

# Get detailed quantities
for line in return_obj.lines:
    print(f"Product: {line.product_id.name}")
    print(f"  Ordered: {line.qty}")
    print(f"  Received: {line.qty_received}")
    print(f"  Invoiced: {line.qty_invoiced}")
    print(f"  Pending: {line.qty - line.qty_received}")

Use Case 3: Bulk Return Processing

# Process multiple returns in batch

# Find all confirmed returns that need picking
return_ids = get_model("sale.return").search([
    ["state", "=", "confirmed"],
    ["is_delivered", "=", False]
])

for return_id in return_ids:
    try:
        # Create goods receipt
        result = get_model("sale.return").copy_to_picking([return_id])
        pick_id = result["picking_id"]

        # Auto-approve if configured
        get_model("stock.picking").pending([pick_id])
        get_model("stock.picking").approve([pick_id])

        print(f"Processed return {return_id}: picking {pick_id}")
    except Exception as e:
        print(f"Error processing return {return_id}: {str(e)}")

Performance Tips

1. Use Function Store Wisely

  • Amount calculations are stored (store=True) to avoid recalculation
  • Computed fields like is_delivered are calculated on-demand
  • Update stored fields only when line items change

2. Batch Operations

# Bad: Processing returns one at a time in loops
for return_id in return_ids:
    obj = get_model("sale.return").browse([return_id])
    process_return(obj)

# Good: Use browse with multiple IDs
returns = get_model("sale.return").browse(return_ids)
for return_obj in returns:
    process_return(return_obj)

3. Search Optimization

# Use indexed fields in search conditions
condition = [
    ["contact_id", "=", 123],  # Indexed
    ["state", "=", "confirmed"],  # Indexed
    ["date", ">=", "2025-01-01"]  # Indexed
]

Troubleshooting

"Missing location for product XXX"

Cause: Stock product in return line doesn't have location_id set Solution: Always specify location_id for products of type stock, consumable, or bundle

"Sequence not found for Module: Sales Return"

Cause: No sequence configured for sale_return type Solution: Create a sequence with type="sale_return" in Settings > Sequences

"Can not delete sales order in this status"

Cause: Trying to delete a confirmed or done return Solution: Void the return first, or use to_draft() to reset state

"There are still pending goods issues for this sales order"

Cause: Trying to void return with pending pickings Solution: Cancel or complete all related pickings first

"There are still invoices waiting payment for this sales order"

Cause: Trying to void return with unpaid credit notes Solution: Complete payment processing or void credit notes first

"Nothing left to deliver"

Cause: All quantities already received when creating picking Solution: Check qty_received on return lines

"Nothing to invoice"

Cause: All quantities already invoiced when creating credit note Solution: Check qty_invoiced on return lines


Testing Examples

Unit Test: Create and Confirm Return

def test_create_and_confirm_return():
    # Create customer
    contact_id = get_model("contact").create({
        "name": "Test Customer",
        "customer": True
    })

    # Create product
    product_id = get_model("product").create({
        "name": "Test Product",
        "type": "stock",
        "sale_price": 100.00,
        "uom_id": 1
    })

    # Create location
    location_id = get_model("stock.location").search([
        ["type", "=", "internal"]
    ])[0]

    # Create return
    return_id = get_model("sale.return").create({
        "contact_id": contact_id,
        "date": "2025-01-05",
        "currency_id": 1,
        "tax_type": "tax_ex",
        "lines": [
            ("create", {
                "product_id": product_id,
                "qty": 2,
                "unit_price": 100.00,
                "location_id": location_id
            })
        ]
    })

    # Verify created
    assert return_id > 0

    # Verify state
    return_obj = get_model("sale.return").browse([return_id])[0]
    assert return_obj.state == "draft"

    # Confirm
    get_model("sale.return").confirm([return_id])

    # Verify confirmed
    return_obj = get_model("sale.return").browse([return_id])[0]
    assert return_obj.state == "confirmed"
    assert return_obj.amount_total == 200.00

Security Considerations

Permission Model

  • sale_approve_done - Required to approve sales returns
  • Multi-company: Returns are isolated by company_id
  • Audit log enabled: All changes are tracked

Data Access

  • Returns are company-specific (multi-company support)
  • Deleted returns must be in draft state only
  • State transitions enforce business rules
  • Void operation validates no pending documents

Configuration Settings

Required Settings

Setting Location Description
Sequence Settings > Sequences Sequence with type="sale_return" for number generation
Currency Company Settings Default currency for returns
Customer Location Stock Locations Location with type="customer"
Warehouse Location Stock Locations Location with type="internal"

Optional Settings

Setting Default Description
Sale Return Account Product/Category Account for credit note line items
Sale Journal Contact Customer-specific journal for credit notes
Price List Contact Customer-specific pricing
Tax Type tax_ex Default tax calculation method

Integration Points

Stock Management

  • stock.picking: Generates incoming pickings for returned goods
  • stock.move: Creates stock movements to track inventory changes
  • stock.location: Uses customer and warehouse locations

Accounting

  • account.invoice: Generates credit notes for returned items
  • account.tax.rate: Calculates taxes on return amounts
  • currency: Handles multi-currency returns

Sales

  • sale.order: Links back to original sales orders
  • contact: Customer information and defaults
  • product: Product information, pricing, accounts

Version History

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


Additional Resources

  • Sale Return Line Documentation: sale.return.line
  • Sales Order Documentation: sale.order
  • Stock Picking Documentation: stock.picking
  • Account Invoice Documentation: account.invoice

Support & Feedback

For issues or questions about this module: 1. Check related model documentation 2. Review system logs for detailed error messages 3. Verify sequence configuration 4. Verify location and account configuration 5. Test in development environment first


This documentation is generated for developer onboarding and reference purposes.