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:
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:
This translates to:
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¶
| 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:
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:
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:
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:
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:
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:
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:
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:
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¶
Ensures that return numbers are unique within each company.
Related Models¶
| 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_deliveredare 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.