Skip to content

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:

  1. Free Shipping Check: If contact has ship_free = True, returns 0.00 immediately
  2. Country Match: Rate must match shipping address country (if specified)
  3. Province Match: Rate must match province/state (if specified)
  4. District Match: Rate must match district (if specified)
  5. Postal Code Match: Rate must match exact postal code (if specified)
  6. Address Name Match: Rate must match exact address name (if specified)
  7. Minimum Amount: Order amount must meet minimum (if specified)
  8. 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%"]
])
# Search across name, code, and description
results = get_model("ship.method").search_browse([
    ["name", "ilike", "%overnight%"]
])

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.