Shipping Method Documentation¶
Overview¶
The Shipping Method module (ship.method) defines available shipping options for customers in e-commerce and sales operations. It supports intelligent rate calculation based on geographic location, order amount, and weight, with business rules for method exclusions and free shipping.
Model Information¶
Model Name: ship.method
Display Name: Shipping Method
Key Fields: None
Features¶
- ❌ No audit logging
- ❌ No multi-company support
- ✅ Full-text search on name, code, description
- ❌ No unique key constraint
- ✅ Active flag for enabling/disabling methods
- ✅ Sequencing for display order
Key Fields Reference¶
Header Fields¶
| Field | Type | Required | Description |
|---|---|---|---|
name |
Char | ✅ | Method name (translatable, searchable) |
code |
Char | ❌ | Short reference code (searchable) |
description |
Text | ❌ | Detailed description (translatable, searchable) |
type |
Selection | ❌ | Method type (extensible for customization) |
sequence |
Integer | ❌ | Display order (lower numbers first) |
active |
Boolean | ❌ | Active status (default: True) |
Relationship Fields¶
| Field | Type | Description |
|---|---|---|
rates |
One2Many | Shipping rates by location/criteria (ship.rate) |
comments |
One2Many | Comments and notes (message) |
ship_product_id |
Many2One | Product for shipping revenue/costs |
exclude_ship_methods |
Many2Many | Incompatible shipping methods |
Computed Fields¶
| Field | Type | Function | Description |
|---|---|---|---|
ship_amount |
Decimal | get_ship_amount |
Calculated shipping cost |
Default Order: Ordered by sequence
API Methods¶
1. name_get¶
Method: name_get(ids, context)
Returns formatted display names with code prefix when available.
Parameters:
- ids (list): Record IDs to format
Returns: List of tuples [(id, name, image), ...]
Behavior:
- If code exists: Returns "[CODE] Name"
- If no code: Returns "Name"
- Always includes image field (typically None)
Example:
# Get display names for dropdown/selection
method_ids = [1, 2, 3]
names = get_model("ship.method").name_get(method_ids)
# Example output:
# [(1, "[STD] Standard Shipping", None),
# (2, "[EXP] Express Delivery", None),
# (3, "Free Shipping", None)]
for method_id, display_name, image in names:
print(f"ID {method_id}: {display_name}")
2. get_ship_amount¶
Method: get_ship_amount(ids, context)
Calculates shipping cost for each method based on customer address, order amount, and weight. Uses intelligent rate matching to find the best applicable rate.
Context Parameters:
context = {
"contact_id": int, # Customer contact ID (for free shipping check)
"ship_address_id": int, # Shipping address ID (for location matching)
"order_amount": Decimal, # Order total amount (for min amount rules)
"order_weight": Decimal # Total weight in Kg (for min weight rules)
}
Matching Logic:
The method iterates through all configured rates for each shipping method and applies these filters in order:
- Free Shipping Check: If contact has
ship_free = True, returns 0.00 immediately - Country Match: Rate must match shipping address country (if specified)
- Province Match: Rate must match province/state (if specified)
- District Match: Rate must match district (if specified)
- Postal Code Match: Rate must match exact postal code (if specified)
- Address Name Match: Rate must match exact address name (if specified)
- Minimum Amount: Order amount must meet minimum (if specified)
- Minimum Weight: Order weight must meet minimum (if specified)
Returns: Dict {method_id: amount} where:
- amount is Decimal if matching rate found
- amount is None if no matching rate found
Example:
# Calculate shipping for a customer order
shipping_costs = get_model("ship.method").get_ship_amount(
[1, 2, 3, 4], # Check multiple methods
context={
"contact_id": 456,
"ship_address_id": 789,
"order_amount": 1500.00,
"order_weight": 25.5
}
)
# Example output: {1: 50.00, 2: 75.00, 3: None, 4: 0.00}
for method_id, cost in shipping_costs.items():
if cost is None:
print(f"Method {method_id}: Not available")
elif cost == 0:
print(f"Method {method_id}: FREE")
else:
print(f"Method {method_id}: ${cost}")
Lowest Rate Selection:
When multiple rates match for a single method, the function returns the lowest rate:
# If method has rates: $50 (province), $45 (district), $40 (postal code)
# All matching the address, returns: $40 (lowest)
3. create_delivery_order¶
Method: create_delivery_order(ids, context)
Placeholder method for creating delivery orders. Currently not implemented.
Purpose: Reserved for future implementation of automatic delivery order generation.
Common Use Cases¶
Use Case 1: Setup Basic Shipping Methods¶
# Create standard shipping method
standard_id = get_model("ship.method").create({
"name": "Standard Shipping",
"code": "STD",
"description": "Delivery in 5-7 business days",
"sequence": 10,
"active": True
})
# Create express shipping method
express_id = get_model("ship.method").create({
"name": "Express Shipping",
"code": "EXP",
"description": "Delivery in 2-3 business days",
"sequence": 20,
"active": True
})
# Create overnight shipping
overnight_id = get_model("ship.method").create({
"name": "Overnight Delivery",
"code": "OVER",
"description": "Next business day delivery",
"sequence": 30,
"active": True
})
# Create free shipping promotion
free_id = get_model("ship.method").create({
"name": "Free Shipping",
"code": "FREE",
"description": "Free delivery on orders over $100",
"sequence": 5,
"active": True
})
Use Case 2: Configure with Shipping Product¶
# Create or get shipping product for accounting
ship_product = get_model("product").search([
["code", "=", "SHIP-STD"]
])
if not ship_product:
ship_product_id = get_model("product").create({
"name": "Standard Shipping Fee",
"code": "SHIP-STD",
"type": "service",
"sale_price": 10.00
})
else:
ship_product_id = ship_product[0]
# Link to shipping method
get_model("ship.method").write([standard_id], {
"ship_product_id": ship_product_id
})
Use Case 3: Configure Method Exclusions¶
# Express and Free shipping are mutually exclusive
get_model("ship.method").write([express_id], {
"exclude_ship_methods": [("set", [free_id])]
})
# Free shipping excludes both express and overnight
get_model("ship.method").write([free_id], {
"exclude_ship_methods": [("set", [express_id, overnight_id])]
})
Use Case 4: Calculate Shipping for E-commerce Order¶
def calculate_available_shipping(order_data):
"""Calculate available shipping methods and costs for order"""
# Get all active shipping methods
methods = get_model("ship.method").search_browse([
["active", "=", True]
])
if not methods:
return []
# Prepare context for rate calculation
context = {
"contact_id": order_data["contact_id"],
"ship_address_id": order_data["ship_address_id"],
"order_amount": order_data["total_amount"],
"order_weight": order_data["total_weight"]
}
# Calculate costs for all methods
method_ids = [m.id for m in methods]
costs = get_model("ship.method").get_ship_amount(method_ids, context=context)
# Build available options
shipping_options = []
for method in methods:
cost = costs.get(method.id)
if cost is not None: # Method is available
shipping_options.append({
"method_id": method.id,
"method_code": method.code,
"method_name": method.name,
"description": method.description,
"cost": float(cost),
"is_free": cost == 0
})
# Sort by cost (free first, then cheapest to most expensive)
shipping_options.sort(key=lambda x: (not x["is_free"], x["cost"]))
return shipping_options
# Example usage
order = {
"contact_id": 123,
"ship_address_id": 456,
"total_amount": 250.00,
"total_weight": 15.5
}
available_shipping = calculate_available_shipping(order)
print("Available Shipping Options:")
for option in available_shipping:
cost_display = "FREE" if option["is_free"] else f"${option['cost']:.2f}"
print(f" [{option['method_code']}] {option['method_name']}: {cost_display}")
Use Case 5: Handle Free Shipping Customers¶
# Mark VIP customer for free shipping
vip_customer_id = 789
get_model("contact").write([vip_customer_id], {
"ship_free": True
})
# Now when calculating shipping for this customer
context = {
"contact_id": vip_customer_id,
"ship_address_id": 456,
"order_amount": 100.00,
"order_weight": 5.0
}
costs = get_model("ship.method").get_ship_amount([1, 2, 3], context=context)
# Returns: {1: 0.00, 2: 0.00, 3: 0.00} # All methods free!
Search Functions¶
Search Active Methods¶
# Get all active shipping methods
active_methods = get_model("ship.method").search_browse([
["active", "=", True]
])
Search by Code¶
# Find specific method by code
method = get_model("ship.method").search_browse([
["code", "=", "STD"]
])
Search by Name Pattern¶
# Find all express shipping methods
express_methods = get_model("ship.method").search_browse([
["name", "ilike", "%express%"]
])
Full-Text Search¶
# Search across name, code, and description
results = get_model("ship.method").search_browse([
["name", "ilike", "%overnight%"]
])
Related Models¶
| Model | Relationship | Description |
|---|---|---|
ship.rate |
One2Many | Geographic and criteria-based rates |
product |
Many2One | Shipping charge product for accounting |
contact |
Referenced | Customer with ship_free flag |
address |
Referenced | Shipping destination for rate matching |
sale.order |
Referenced by | Sales orders select shipping method |
message |
One2Many | Comments and notes |
Best Practices¶
1. Rate Configuration Strategy¶
# Good: Configure rates from specific to general
# This ensures most specific rate is found first
# 1. Setup method with general rate first
method_id = get_model("ship.method").create({
"name": "Standard Shipping",
"code": "STD"
})
# 2. Add country-wide rate (most general)
get_model("ship.rate").create({
"method_id": method_id,
"sequence": "100",
"country_id": usa_id,
"ship_price": 25.00
})
# 3. Add province-specific rate
get_model("ship.rate").create({
"method_id": method_id,
"sequence": "50",
"country_id": usa_id,
"province_id": california_id,
"ship_price": 15.00
})
# 4. Add postal-code specific rate (most specific)
get_model("ship.rate").create({
"method_id": method_id,
"sequence": "10",
"postal_code": "90210",
"ship_price": 10.00
})
# Algorithm will select lowest matching rate
2. Use Sequencing for Display Order¶
# Good: Use meaningful sequence numbers
methods = [
{"name": "Free Shipping", "sequence": 10}, # Show first
{"name": "Standard", "sequence": 20},
{"name": "Express", "sequence": 30},
{"name": "Overnight", "sequence": 40} # Show last
]
# Bad: All same sequence
methods = [
{"name": "Express", "sequence": 1},
{"name": "Free Shipping", "sequence": 1}, # Unpredictable order
{"name": "Standard", "sequence": 1}
]
3. Implement Business Logic with Exclusions¶
# Good: Use exclude_ship_methods for business rules
# Rule: Cannot combine free shipping with express
free_method_id = 1
express_method_id = 2
get_model("ship.method").write([free_method_id], {
"exclude_ship_methods": [("set", [express_method_id])]
})
# In order processing, check exclusions
def validate_shipping_method(method_id, current_methods):
method = get_model("ship.method").browse(method_id)
excluded_ids = [m.id for m in method.exclude_ship_methods]
for current_id in current_methods:
if current_id in excluded_ids:
return False, "This shipping method cannot be combined with selected method"
return True, None
4. Handle No Available Shipping Gracefully¶
# Good: Always handle None costs
costs = get_model("ship.method").get_ship_amount([1, 2, 3], context=ctx)
available_methods = []
unavailable_methods = []
for method_id, cost in costs.items():
if cost is None:
unavailable_methods.append(method_id)
else:
available_methods.append(method_id)
if not available_methods:
# Show message to customer
print("Sorry, we cannot ship to your location currently.")
# Or fall back to manual quote
else:
# Show available options
pass
Performance Tips¶
1. Batch Calculate Shipping Costs¶
# Good: Calculate all methods at once
method_ids = [1, 2, 3, 4, 5]
all_costs = get_model("ship.method").get_ship_amount(method_ids, context=ctx)
# Bad: Calculate one at a time
for method_id in method_ids:
cost = get_model("ship.method").get_ship_amount([method_id], context=ctx)
# This is much slower!
2. Cache Active Methods¶
# Good: Cache frequently accessed data
class ShippingMethodCache:
_active_methods = None
_last_refresh = None
@classmethod
def get_active_methods(cls):
from datetime import datetime, timedelta
# Refresh every 5 minutes
now = datetime.now()
if cls._active_methods is None or \
(now - cls._last_refresh) > timedelta(minutes=5):
cls._active_methods = get_model("ship.method").search_browse([
["active", "=", True]
])
cls._last_refresh = now
return cls._active_methods
# Usage
active_methods = ShippingMethodCache.get_active_methods()
3. Optimize Rate Queries¶
# Good: Load rates with method in single query
methods = get_model("ship.method").search_browse([
["active", "=", True]
])
# Rates are loaded via One2Many automatically
for method in methods:
print(f"{method.name}: {len(method.rates)} rates configured")
# Bad: Query rates separately
methods = get_model("ship.method").search_browse([["active", "=", True]])
for method in methods:
rates = get_model("ship.rate").search_browse([
["method_id", "=", method.id]
]) # Extra query for each method!
Troubleshooting¶
"No shipping methods available"¶
Cause: Either no active methods or no matching rates
Solution: Check method activation and rate configuration
# Check active methods
active = get_model("ship.method").search([["active", "=", True]])
print(f"Active methods: {len(active)}")
# Check rates for method
method_id = 1
rates = get_model("ship.rate").search([["method_id", "=", method_id]])
print(f"Rates configured: {len(rates)}")
"Shipping cost is None"¶
Cause: No rate matches the order criteria
Solution: Review rate configuration for coverage
# Debug rate matching
def debug_rate_matching(method_id, context):
method = get_model("ship.method").browse(method_id)
address = get_model("address").browse(context["ship_address_id"])
print(f"Method: {method.name}")
print(f"Address: {address.country_id.name}, {address.province_id.name}")
print(f"Postal Code: {address.postal_code}")
print(f"Order Amount: {context['order_amount']}")
print(f"Order Weight: {context['order_weight']}")
print("\nAvailable Rates:")
for rate in method.rates:
print(f"\n Rate ID {rate.id}:")
print(f" Country: {rate.country_id.name if rate.country_id else 'Any'}")
print(f" Province: {rate.province_id.name if rate.province_id else 'Any'}")
print(f" Postal: {rate.postal_code or 'Any'}")
print(f" Min Amount: {rate.min_amount or 'None'}")
print(f" Min Weight: {rate.min_weight or 'None'}")
print(f" Price: ${rate.ship_price}")
"get_ship_amount returns 0.00 unexpectedly"¶
Cause: Customer has ship_free flag enabled
Solution: Check contact record
# Check customer free shipping status
contact_id = 123
contact = get_model("contact").browse(contact_id)
if contact.ship_free:
print(f"Customer {contact.name} has free shipping enabled")
# This bypasses all rate calculations
Testing Examples¶
Unit Test: Rate Calculation¶
def test_shipping_rate_calculation():
# Create test method
method_id = get_model("ship.method").create({
"name": "Test Shipping",
"code": "TEST",
"active": True
})
# Create test rate
rate_id = get_model("ship.rate").create({
"method_id": method_id,
"sequence": "1",
"ship_price": 10.00
})
# Test calculation
costs = get_model("ship.method").get_ship_amount(
[method_id],
context={
"order_amount": 100.00,
"order_weight": 5.0
}
)
assert costs[method_id] == 10.00
# Cleanup
get_model("ship.rate").delete([rate_id])
get_model("ship.method").delete([method_id])
Unit Test: Free Shipping Customer¶
def test_free_shipping_customer():
# Create test customer with free shipping
contact_id = get_model("contact").create({
"name": "VIP Customer",
"ship_free": True
})
# Create test method with rates
method_id = get_model("ship.method").create({
"name": "Test Method",
"active": True
})
get_model("ship.rate").create({
"method_id": method_id,
"sequence": "1",
"ship_price": 50.00
})
# Test - should return 0.00 regardless of rates
costs = get_model("ship.method").get_ship_amount(
[method_id],
context={"contact_id": contact_id}
)
assert costs[method_id] == 0.00
# Cleanup
get_model("contact").delete([contact_id])
get_model("ship.method").delete([method_id])
Security Considerations¶
Permission Model¶
- View: Users with sales/e-commerce access
- Create/Modify: Admin or operations manager
- Delete: Admin only
Data Access¶
- Public information (shipping options)
- Rates may be business-sensitive
- No customer-specific data stored
Version History¶
Last Updated: October 2025
Model File: ship_method.py
Framework: Netforce
Additional Resources¶
- Shipping Rate Documentation:
ship.rate - Address Documentation:
address - Contact Documentation:
contact - Sales Order Documentation:
sale.order
This documentation is generated for developer onboarding and reference purposes.