Sale Order Promotion Documentation¶
Overview¶
The Sale Order Promotion module (sale.order.promotion) is a junction/relationship model that tracks which promotions have been applied to which sales orders. It maintains the historical record of promotional discounts applied during order processing, enabling accurate reporting, auditing, and commission calculations.
Model Information¶
Model Name: sale.order.promotion
Display Name: Sale Order Promotion
Key Fields: None (standard ID-based identification)
Features¶
- Promotion application tracking
- Historical discount record keeping
- Relationship model between orders and promotions
- Support for both percentage and fixed amount discounts
- Product-specific promotion tracking
- On-delete cascade with sales orders
Model Purpose¶
This model serves as a many-to-many relationship table with additional data:
Why This Model Exists: - Track which specific promotions were applied to each order - Record the actual discount values at time of order creation - Enable reporting on promotion effectiveness - Maintain audit trail of promotional discounts - Support multiple promotions on a single order
Key Fields Reference¶
Relationship Fields¶
| Field | Type | Required | Description |
|---|---|---|---|
sale_id |
Many2One | Yes | Link to sales order (cascade on delete) |
promotion_id |
Many2One | Yes | Link to promotion that was applied |
product_id |
Many2One | No | Specific product the promotion was applied to |
Discount Value Fields¶
| Field | Type | Description |
|---|---|---|
percent |
Decimal | Percentage discount that was applied (e.g., 15.00 = 15%) |
amount |
Decimal | Fixed amount discount that was applied |
Understanding the Relationship¶
How Promotions Are Recorded¶
When a promotion is applied to a sales order:
- Promotion Validation: The
sale.promotionmodel checks if criteria are met - Discount Calculation: Discount amount/percentage is calculated
- Record Creation: A
sale.order.promotionrecord is created linking the order to the promotion - Values Stored: The actual discount values are stored (not just references)
Why Store Discount Values?¶
# Scenario: Promotion changes after order is placed
# Original promotion: 20% off
promo = get_model("sale.promotion").browse(promo_id)
promo.discount_percent_item = 20 # Initially
# Order created with this promotion
order_promo = get_model("sale.order.promotion").create({
"sale_id": order_id,
"promotion_id": promo_id,
"percent": 20, # Snapshot of value at order time
"amount": None
})
# Later, promotion is changed to 25%
promo.write({"discount_percent_item": 25})
# The order still shows 20% because we stored the value
# This preserves order integrity and audit trail
API Methods¶
1. Create Promotion Application Record¶
Method: create(vals, context)
Creates a record linking a sales order to a promotion.
Parameters:
vals = {
"sale_id": 123, # Required: sales order ID
"promotion_id": 456, # Required: promotion ID
"product_id": 789, # Optional: specific product
"percent": 15.00, # Optional: percentage discount
"amount": 100.00 # Optional: fixed amount discount
}
Returns: int - New sale order promotion ID
Example:
# Record that "Summer Sale" promotion was applied to order
order_promo_id = get_model("sale.order.promotion").create({
"sale_id": order_id,
"promotion_id": summer_sale_promo_id,
"percent": 20, # 20% discount was applied
})
2. Query Promotions on Order¶
Method: search(conditions) and browse(ids)
Find all promotions applied to a specific order.
Example:
# Get all promotions on an order
order_promos = get_model("sale.order.promotion").search([
["sale_id", "=", order_id]
])
# Browse to see details
for op in get_model("sale.order.promotion").browse(order_promos):
print(f"Promotion: {op.promotion_id.name}")
print(f"Discount: {op.percent}% or ${op.amount}")
if op.product_id:
print(f"Applied to: {op.product_id.name}")
3. Cascade Deletion¶
Behavior: When a sales order is deleted, all associated sale.order.promotion records are automatically deleted due to on_delete="cascade".
# Deleting an order automatically removes promotion records
get_model("sale.order").delete([order_id])
# All sale.order.promotion records with sale_id = order_id are deleted
Search Functions¶
Find All Promotions Applied to an Order¶
# Get promotions for a specific order
promotions_on_order = get_model("sale.order.promotion").search([
["sale_id", "=", order_id]
])
Find All Orders Using a Specific Promotion¶
# Get all orders that used "Black Friday" promotion
black_friday_orders = get_model("sale.order.promotion").search([
["promotion_id", "=", black_friday_promo_id]
])
Find Product-Specific Promotion Applications¶
# Find promotions applied to a specific product
product_promotions = get_model("sale.order.promotion").search([
["product_id", "=", product_id]
])
Related Models¶
| Model | Relationship | Description |
|---|---|---|
sale.order |
Many2One | The sales order the promotion was applied to |
sale.promotion |
Many2One | The promotion definition that was applied |
product |
Many2One | Optional product the promotion targeted |
Common Use Cases¶
Use Case 1: Recording a Percentage Discount Promotion¶
# When processing an order with 15% off promotion
# 1. Validate promotion can be applied (done in sale.promotion)
# 2. Record the promotion application
order_promo_id = get_model("sale.order.promotion").create({
"sale_id": order_id,
"promotion_id": promo_id,
"percent": 15, # 15% discount
"amount": None # Not a fixed amount discount
})
# 3. Order line items will reflect the discount
Use Case 2: Recording a Fixed Amount Discount¶
# When applying a $50 off promotion
order_promo_id = get_model("sale.order.promotion").create({
"sale_id": order_id,
"promotion_id": promo_id,
"percent": None, # Not a percentage discount
"amount": 50.00 # $50 off
})
Use Case 3: Product-Specific Promotion Tracking¶
# Recording a promotion that applies to a specific product
# Buy one laptop, get 20% off
order_promo_id = get_model("sale.order.promotion").create({
"sale_id": order_id,
"promotion_id": laptop_promo_id,
"product_id": laptop_product_id, # Applied to this product
"percent": 20,
"amount": None
})
Use Case 4: Multiple Promotions on One Order¶
# An order can have multiple promotions if allowed
# Promotion 1: 10% off entire order
promo1_id = get_model("sale.order.promotion").create({
"sale_id": order_id,
"promotion_id": sitewide_promo_id,
"percent": 10
})
# Promotion 2: Additional $25 off for VIP customers
promo2_id = get_model("sale.order.promotion").create({
"sale_id": order_id,
"promotion_id": vip_promo_id,
"amount": 25
})
# Order receives both discounts (if apply_multi is enabled)
Use Case 5: Promotion Effectiveness Reporting¶
# Report: How many orders used each promotion in December
from datetime import date
# Get all order promotions in December
dec_start = "2026-12-01"
dec_end = "2026-12-31"
# Join with sale.order to filter by date
order_promos = get_model("sale.order.promotion").search_browse([
["sale_id.order_date", ">=", dec_start],
["sale_id.order_date", "<=", dec_end]
])
# Group by promotion
promotion_stats = {}
for op in order_promos:
promo_name = op.promotion_id.name
if promo_name not in promotion_stats:
promotion_stats[promo_name] = {
"count": 0,
"total_discount_percent": 0,
"total_discount_amount": 0
}
promotion_stats[promo_name]["count"] += 1
if op.percent:
promotion_stats[promo_name]["total_discount_percent"] += op.percent
if op.amount:
promotion_stats[promo_name]["total_discount_amount"] += op.amount
# Display results
for promo_name, stats in promotion_stats.items():
print(f"{promo_name}: {stats['count']} orders")
Use Case 6: Audit Trail for Commission Calculations¶
# When calculating sales commissions, exclude promotional discounts
order = get_model("sale.order").browse(order_id)
# Get all promotions applied to this order
order_promos = get_model("sale.order.promotion").search_browse([
["sale_id", "=", order_id]
])
# Calculate base amount before promotions
base_amount = order.amount_total
for op in order_promos:
if op.amount:
base_amount += op.amount
# Handle percentage discounts based on business logic
# Commission calculated on base_amount
commission = base_amount * commission_rate
Best Practices¶
1. Always Store Actual Values¶
# Good: Store the actual discount values at time of order
order_promo_id = get_model("sale.order.promotion").create({
"sale_id": order_id,
"promotion_id": promo_id,
"percent": promo.discount_percent_item, # Snapshot
"amount": promo.discount_amount_item # Snapshot
})
# Bad: Don't store just the reference
order_promo_id = get_model("sale.order.promotion").create({
"sale_id": order_id,
"promotion_id": promo_id,
# Missing percent and amount - loses historical accuracy
})
2. Use Cascade Deletion Properly¶
The model uses on_delete="cascade" for sale_id, which means:
# When you delete an order, promotion records are automatically deleted
# This is correct behavior - promotion applications are meaningless without the order
# No need for manual cleanup:
get_model("sale.order").delete([order_id])
# sale.order.promotion records are automatically removed
3. Reporting and Analytics¶
# Good: Use this model for promotion analytics
# Get promotion performance
def get_promotion_performance(promo_id, date_from, date_to):
order_promos = get_model("sale.order.promotion").search_browse([
["promotion_id", "=", promo_id],
["sale_id.order_date", ">=", date_from],
["sale_id.order_date", "<=", date_to],
["sale_id.state", "!=", "voided"] # Exclude voided orders
])
return {
"total_uses": len(order_promos),
"orders": [op.sale_id.number for op in order_promos]
}
Performance Tips¶
1. Query Optimization¶
# Efficient: Use direct search on sale_id
order_promos = get_model("sale.order.promotion").search([
["sale_id", "=", order_id]
])
# Less efficient: Load order then access reverse relationship
order = get_model("sale.order").browse(order_id)
# Assuming there's a One2Many field on sale.order
# order_promos = order.promotion_ids
2. Bulk Operations¶
# When creating multiple promotion applications, use batch create
promotion_records = []
for promo_id in applied_promotion_ids:
promotion_records.append({
"sale_id": order_id,
"promotion_id": promo_id,
"percent": promotions[promo_id]["percent"],
"amount": promotions[promo_id]["amount"]
})
# Bulk create if supported by framework
for record in promotion_records:
get_model("sale.order.promotion").create(record)
Troubleshooting¶
"Promotion record not created when applying discount"¶
Cause: Order processing may not be creating the tracking record
Solution:
- Ensure order processing code creates sale.order.promotion record
- Check that promotion application logic includes record creation
- Verify no errors during order.promotion.create()
"Promotion values don't match promotion definition"¶
Cause: This is expected - values are snapshots at order time
Solution:
- sale.order.promotion stores historical values
- Promotion definitions may change after order creation
- Use promotion records for accurate historical reporting
"Deleting promotion deletes order promotion records"¶
Cause: Default foreign key behavior may cascade Solution: - Check on_delete settings on promotion_id field - Current implementation uses default behavior - Consider if historical records should be preserved when promotions are deleted
Database Constraints¶
Foreign Key with Cascade¶
-- The sale_id field has on_delete="cascade"
ALTER TABLE sale_order_promotion
ADD CONSTRAINT fk_sale_order_promotion_sale
FOREIGN KEY (sale_id)
REFERENCES sale_order(id)
ON DELETE CASCADE;
This ensures that when a sales order is deleted, all its promotion application records are also deleted automatically.
Integration Points¶
Internal Modules¶
- sale.order: Parent model - orders that receive promotions
- sale.promotion: Promotion definitions that are applied
- product: Optional product targeting
- Reporting Systems: Uses this model for promotion analytics
Version History¶
Last Updated: 2026-01-05 Model Version: sale_order_promotion.py (36 lines) Framework: Netforce
Additional Resources¶
- Sale Order Documentation:
sale.order - Sale Promotion Documentation:
sale.promotion - Product Documentation:
product
Support & Feedback¶
For issues or questions about this module: 1. Check sale.order and sale.promotion documentation 2. Verify that promotions are being applied correctly 3. Review order processing workflow 4. Check database for orphaned records (shouldn't exist with cascade)
This documentation is generated for developer onboarding and reference purposes.