Sale Promotion Documentation¶
Overview¶
The Sale Promotion module (sale.promotion) provides a comprehensive promotional campaign system that enables automatic discount application to sales orders based on configurable rules and criteria. It supports flexible promotion types including percentage discounts, fixed amount discounts, product-specific promotions, and customer segment targeting.
Model Information¶
Model Name: sale.promotion
Display Name: Promotion
Key Fields: None (standard ID-based identification)
Features¶
- Multi-company support (
company_id) - Flexible discount rules (percentage/fixed amount per item/order)
- Product and product group targeting
- Customer segmentation (categories and groups)
- Date-based validity periods
- Usage limit controls (per customer and total)
- Auto-application and manual coupon-based promotions
- E-commerce cart integration
Promotion Types¶
The system supports multiple discount application methods:
| Type | Field | Description |
|---|---|---|
| Percent Per Item | discount_percent_item |
Percentage discount applied to each qualifying item |
| Amount Per Item | discount_amount_item |
Fixed amount discount per qualifying item |
| Percent Per Order | discount_percent_order |
Percentage discount on entire order total |
| Amount Per Order | discount_amount_order |
Fixed amount discount on entire order |
State Workflow¶
| State | Description |
|---|---|
active |
Promotion is enabled and can be applied to orders |
inactive |
Promotion is disabled and will not be applied |
Key Fields Reference¶
Basic Information Fields¶
| Field | Type | Required | Description |
|---|---|---|---|
name |
Char | Yes | Promotional campaign title/name |
code |
Char | No | Legacy promotion code (deprecated) |
description |
Text | No | Detailed description of the promotion |
state |
Selection | Yes | Promotion status (active/inactive) |
company_id |
Many2One | No | Company (multi-company support) |
Date and Validity Fields¶
| Field | Type | Description |
|---|---|---|
date_from |
Date | Start date for promotion validity |
date_to |
Date | End date for promotion validity |
Purchase Criteria Fields¶
| Field | Type | Description |
|---|---|---|
buy_prod_groups |
Many2Many | Product groups that qualify for the promotion |
buy_products |
Many2Many | Specific products that qualify for the promotion |
buy_min_amount |
Integer | Minimum order amount required |
buy_min_qty |
Integer | Minimum order quantity required |
Discount Configuration Fields¶
| Field | Type | Description |
|---|---|---|
discount_percent_item |
Decimal | Percentage discount per item (e.g., 10 = 10%) |
discount_amount_item |
Decimal | Fixed amount discount per item |
discount_percent_order |
Decimal | Percentage discount on entire order |
discount_amount_order |
Decimal | Fixed amount discount on entire order |
discount_max_qty |
Integer | Maximum quantity of discounted items |
Discount Target Fields¶
| Field | Type | Description |
|---|---|---|
discount_products |
Many2Many | Products that receive the discount |
discount_prod_groups |
Many2Many | Product groups that receive the discount |
related_products |
Many2Many | Related products for cross-promotion |
related_prod_groups |
Many2Many | Related product groups for cross-promotion |
Customer Targeting Fields¶
| Field | Type | Description |
|---|---|---|
contact_categs |
Many2Many | Customer categories eligible for promotion |
contact_groups |
Many2Many | Customer groups eligible for promotion |
Usage Control Fields¶
| Field | Type | Description |
|---|---|---|
max_uses_per_customer |
Integer | Maximum times a customer can use this promotion |
max_total_uses |
Integer | Maximum total uses across all customers |
apply_multi |
Boolean | Allow multiple applications per order |
auto_apply |
Boolean | Automatically apply promotion when criteria met |
Coupon Integration Fields¶
| Field | Type | Description |
|---|---|---|
coupon_master_id |
Many2One | Require specific coupon to activate promotion |
E-commerce Fields¶
| Field | Type | Description |
|---|---|---|
cart_offer_message |
Text | Message shown when offering discounted product |
cart_confirm_message |
Text | Message shown when offer is accepted |
product_id |
Many2One | Configuration product for the promotion |
Computed Fields¶
| Field | Type | Description |
|---|---|---|
can_apply |
Boolean | Function field: whether promotion can be applied to current cart |
is_applied |
Boolean | Function field: whether promotion is already applied to cart |
API Methods¶
1. Create Promotion¶
Method: create(vals, context)
Creates a new promotion campaign.
Parameters:
vals = {
"name": "Summer Sale 2026", # Required: promotion title
"state": "active", # Required: active or inactive
"date_from": "2026-06-01", # Optional: start date
"date_to": "2026-08-31", # Optional: end date
"discount_percent_item": 20, # Optional: 20% off per item
"buy_min_amount": 1000, # Optional: minimum order $1000
"auto_apply": True, # Optional: auto-apply when criteria met
"buy_products": [ # Optional: qualifying products
("set", [101, 102, 103])
],
"contact_categs": [ # Optional: customer categories
("set", [1, 2])
]
}
Returns: int - New promotion ID
Example:
# Create a 20% off promotion for electronics with minimum purchase
promo_id = get_model("sale.promotion").create({
"name": "Electronics Summer Sale",
"state": "active",
"date_from": "2026-06-01",
"date_to": "2026-08-31",
"discount_percent_item": 20,
"buy_min_amount": 1000,
"buy_prod_groups": [("set", [electronics_group_id])],
"auto_apply": True,
"description": "Get 20% off all electronics with minimum purchase of $1000"
})
2. Activate Promotion¶
Method: activate(ids, context)
Activates one or more promotions, making them available for application.
Parameters:
- ids (list): Promotion IDs to activate
Behavior: - Sets state to "active" - Allows promotion to be applied to qualifying orders - If auto_apply is enabled, will automatically apply to new orders
Example:
3. Deactivate Promotion¶
Method: deactivate(ids, context)
Deactivates one or more promotions, preventing them from being applied.
Parameters:
- ids (list): Promotion IDs to deactivate
Behavior: - Sets state to "inactive" - Prevents promotion from being applied to new orders - Does not remove promotion from existing orders
Example:
4. Check if Promotion Can Be Applied¶
Method: _can_apply(ids, context)
Computed function that checks if a promotion can be applied to the current cart.
Context Options:
Behavior:
- Calls ecom.cart.can_apply_promotion() to validate
- Checks product criteria, customer eligibility, date validity
- Returns boolean for each promotion ID
Returns: dict - {promotion_id: True/False}
Example:
# Check if promotion can be applied (called automatically in UI)
result = get_model("sale.promotion")._can_apply(
[promo_id],
context={"cart_id": cart_id}
)
# Returns: {promo_id: True}
5. Check if Promotion Is Applied¶
Method: _is_applied(ids, context)
Computed function that checks if a promotion is already applied to the current cart.
Context Options:
Behavior:
- Calls ecom.cart.is_promotion_applied() to check
- Returns whether promotion is currently active on cart
- Used for UI display logic
Returns: dict - {promotion_id: True/False}
Example:
# Check if promotion is already applied
result = get_model("sale.promotion")._is_applied(
[promo_id],
context={"cart_id": cart_id}
)
# Returns: {promo_id: False}
Search Functions¶
Search by Active Status¶
# Find all active promotions
active_promos = get_model("sale.promotion").search([["state", "=", "active"]])
Search by Date Range¶
# Find promotions valid on a specific date
from datetime import date
today = date.today().strftime("%Y-%m-%d")
current_promos = get_model("sale.promotion").search([
["state", "=", "active"],
["date_from", "<=", today],
["date_to", ">=", today]
])
Search by Customer Category¶
# Find promotions for VIP customers
vip_promos = get_model("sale.promotion").search([
["state", "=", "active"],
["contact_categs.id", "in", [vip_category_id]]
])
Computed Fields Functions¶
_can_apply(ids, context)¶
Determines whether a promotion can be applied to the current shopping cart. Integrates with ecom.cart model to validate all promotion criteria including products, customer eligibility, and date ranges.
_is_applied(ids, context)¶
Checks whether a promotion is currently applied to the shopping cart. Used for UI state management to show applied vs. available promotions.
Integration with E-commerce Cart¶
The promotion system integrates with the e-commerce cart (ecom.cart) model:
# When processing cart, check applicable promotions
cart = get_model("ecom.cart").browse(cart_id)
# Auto-apply promotions
active_promos = get_model("sale.promotion").search([
["state", "=", "active"],
["auto_apply", "=", True]
])
for promo in get_model("sale.promotion").browse(active_promos):
if get_model("ecom.cart").can_apply_promotion([cart_id], promo.id):
# Apply promotion to cart
pass
Related Models¶
| Model | Relationship | Description |
|---|---|---|
sale.order |
Indirect (via sale.order.promotion) | Promotions applied to sales orders |
sale.order.promotion |
One2Many (reverse) | Tracks promotion applications on orders |
sale.coupon.master |
Many2One (reverse) | Coupon masters that require this promotion |
ecom.cart |
Indirect | Shopping carts where promotions are validated |
product |
Many2Many | Products involved in promotion criteria |
product.group |
Many2Many | Product groups for targeting |
contact.categ |
Many2Many | Customer categories for eligibility |
contact.group |
Many2Many | Customer groups for targeting |
company |
Many2One | Company in multi-company setup |
Common Use Cases¶
Use Case 1: Percentage Discount on Product Category¶
# Create 15% off all laptops promotion
# 1. Get the laptop product group
laptop_group_id = get_model("product.group").search([["name", "=", "Laptops"]])[0]
# 2. Create the promotion
promo_id = get_model("sale.promotion").create({
"name": "15% Off All Laptops",
"state": "active",
"date_from": "2026-01-15",
"date_to": "2026-01-31",
"discount_percent_item": 15,
"buy_prod_groups": [("set", [laptop_group_id])],
"discount_prod_groups": [("set", [laptop_group_id])],
"auto_apply": True,
"description": "Get 15% off all laptop purchases this January"
})
# 3. Promotion will automatically apply to qualifying orders
Use Case 2: Buy One Get One (BOGO) with Amount Discount¶
# Buy one product, get second at 50% off
# 1. Create the promotion
product_id = 150 # Specific product
promo_id = get_model("sale.promotion").create({
"name": "BOGO 50% Off",
"state": "active",
"buy_products": [("set", [product_id])],
"buy_min_qty": 2,
"discount_products": [("set", [product_id])],
"discount_percent_item": 50,
"discount_max_qty": 1, # Only one discounted item
"apply_multi": False,
"auto_apply": True
})
Use Case 3: VIP Customer Exclusive Promotion¶
# 25% off for VIP customers with minimum purchase
# 1. Get VIP customer category
vip_categ_id = get_model("contact.categ").search([["name", "=", "VIP"]])[0]
# 2. Create exclusive promotion
promo_id = get_model("sale.promotion").create({
"name": "VIP Exclusive 25% Off",
"state": "active",
"date_from": "2026-02-01",
"date_to": "2026-02-28",
"contact_categs": [("set", [vip_categ_id])],
"buy_min_amount": 5000,
"discount_percent_order": 25,
"max_uses_per_customer": 1, # One time per VIP
"auto_apply": True,
"description": "Exclusive 25% off for VIP members on orders over $5000"
})
Use Case 4: Coupon-Required Promotion¶
# Promotion that requires a specific coupon code
# 1. Create coupon master
coupon_master_id = get_model("sale.coupon.master").create({
"name": "SAVE20 Coupon Campaign",
"active": True
})
# 2. Create promotion requiring the coupon
promo_id = get_model("sale.promotion").create({
"name": "20% Off with Coupon",
"state": "active",
"coupon_master_id": coupon_master_id,
"discount_percent_order": 20,
"auto_apply": False, # Requires manual coupon entry
"description": "Use code SAVE20 for 20% off your order"
})
# 3. Create individual coupons for customers
get_model("sale.coupon.master").create_coupons([coupon_master_id])
Use Case 5: Flash Sale with Usage Limits¶
# Limited time flash sale with usage cap
promo_id = get_model("sale.promotion").create({
"name": "Flash Sale - First 100 Customers",
"state": "active",
"date_from": "2026-03-15 09:00:00",
"date_to": "2026-03-15 18:00:00", # 9 hours only
"discount_percent_order": 30,
"max_total_uses": 100, # Only first 100 orders
"max_uses_per_customer": 1, # One per customer
"auto_apply": True,
"description": "30% off - first 100 customers only!"
})
Use Case 6: Cross-Promotion (Buy X Get Discount on Y)¶
# Buy accessories, get discount on related products
# 1. Get product groups
accessory_group_id = get_model("product.group").search([["name", "=", "Accessories"]])[0]
main_product_group_id = get_model("product.group").search([["name", "=", "Cameras"]])[0]
# 2. Create cross-promotion
promo_id = get_model("sale.promotion").create({
"name": "Buy Accessories, Save on Cameras",
"state": "active",
"buy_prod_groups": [("set", [accessory_group_id])],
"buy_min_amount": 500,
"discount_prod_groups": [("set", [main_product_group_id])],
"related_prod_groups": [("set", [main_product_group_id])],
"discount_percent_item": 10,
"auto_apply": True,
"description": "Buy $500+ in accessories and get 10% off cameras"
})
Best Practices¶
1. Promotion Planning¶
# Good: Clear naming and documentation
promo_id = get_model("sale.promotion").create({
"name": "Q1 2026 Electronics Clearance",
"description": "End of quarter clearance sale on all electronics. "
"Customer must purchase minimum $1000. "
"Discount applies to qualifying items only.",
"state": "active",
"discount_percent_item": 25,
"buy_min_amount": 1000
})
# Bad: Vague naming
promo_id = get_model("sale.promotion").create({
"name": "Sale", # Too generic
"state": "active",
"discount_percent_item": 25
})
2. Date Range Management¶
Always set both start and end dates for promotions to avoid runaway discounts:
# Good: Defined date range
get_model("sale.promotion").create({
"name": "Holiday Sale",
"date_from": "2026-12-20",
"date_to": "2026-12-31",
"discount_percent_order": 15
})
# Risky: No end date
get_model("sale.promotion").create({
"name": "Holiday Sale",
"date_from": "2026-12-20",
# No date_to - promotion never expires!
"discount_percent_order": 15
})
3. Usage Limits¶
Set usage limits to control promotion costs:
# Good: Controlled usage
get_model("sale.promotion").create({
"name": "Welcome Discount",
"discount_percent_order": 10,
"max_uses_per_customer": 1, # Prevent abuse
"max_total_uses": 500, # Budget control
"contact_categs": [("set", [new_customer_categ_id])]
})
# Risky: Unlimited usage
get_model("sale.promotion").create({
"name": "Welcome Discount",
"discount_percent_order": 10,
# No limits - could be very expensive!
})
4. Auto-Apply vs Manual¶
Choose appropriate application method:
# Auto-apply: For general sales and customer segments
get_model("sale.promotion").create({
"name": "Summer Sale",
"auto_apply": True, # Automatically applies
"discount_percent_item": 15
})
# Manual: For coupon codes and targeted campaigns
get_model("sale.promotion").create({
"name": "Referral Program Discount",
"auto_apply": False, # Requires coupon
"coupon_master_id": coupon_id,
"discount_percent_order": 20
})
5. Testing Promotions¶
Always test promotions before activation:
# 1. Create promotion in inactive state
promo_id = get_model("sale.promotion").create({
"name": "Test: Black Friday Sale",
"state": "inactive", # Start inactive
"discount_percent_order": 40,
"date_from": "2026-11-27",
"date_to": "2026-11-29"
})
# 2. Test with sample orders
# ... testing logic ...
# 3. Activate when ready
get_model("sale.promotion").activate([promo_id])
Performance Tips¶
1. Use Product Groups Instead of Individual Products¶
- Product groups are more efficient for large catalogs
- Easier to maintain as product catalog grows
# Good: Use product groups
promo_id = get_model("sale.promotion").create({
"buy_prod_groups": [("set", [electronics_group_id])]
})
# Less efficient: Individual products
promo_id = get_model("sale.promotion").create({
"buy_products": [("set", [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...])] # 100s of IDs
})
2. Limit Active Promotions¶
- Too many active promotions slow down order processing
- Deactivate expired or unused promotions regularly
# Cleanup expired promotions
from datetime import date
today = date.today().strftime("%Y-%m-%d")
expired_promos = get_model("sale.promotion").search([
["state", "=", "active"],
["date_to", "<", today]
])
if expired_promos:
get_model("sale.promotion").deactivate(expired_promos)
3. Index Searchable Fields¶
The model marks date_from as searchable, which improves query performance on date-based searches.
Troubleshooting¶
"Promotion not applying to order"¶
Cause: Criteria not met (date range, minimum amount, customer category, etc.)
Solution:
- Check promotion state is "active"
- Verify current date is within date_from and date_to
- Confirm customer meets contact_categs or contact_groups criteria
- Ensure order amount meets buy_min_amount requirement
- Check that order contains products in buy_products or buy_prod_groups
"Discount amount seems incorrect"¶
Cause: Confusion between item-level and order-level discounts
Solution:
- discount_percent_item / discount_amount_item: Applied to each qualifying item
- discount_percent_order / discount_amount_order: Applied to entire order total
- Check which fields are set on the promotion
"Multiple promotions applying when they shouldn't"¶
Cause: apply_multi is enabled
Solution: Set apply_multi to False if only one promotion should apply per order
Security Considerations¶
Permission Model¶
- Create/edit promotions: Requires sales management permissions
- View promotions: Available to sales staff and e-commerce frontend
- Apply promotions: Automatic based on criteria validation
Data Access¶
- Multi-company support ensures promotions are company-specific
- Customer category filtering prevents unauthorized promotion access
- Usage limits prevent promotion abuse
Configuration Settings¶
Optional Settings¶
| Setting | Default | Description |
|---|---|---|
state |
active |
New promotions start active |
company_id |
Current company | Automatically set from user context |
auto_apply |
False |
Manual application by default |
apply_multi |
False |
Single promotion per order by default |
Integration Points¶
Internal Modules¶
- sale.order: Promotions apply discounts to sales orders
- sale.order.promotion: Tracks which promotions were applied
- ecom.cart: E-commerce shopping cart promotion validation
- sale.coupon.master: Coupon-based promotion activation
- product: Product targeting for promotions
- contact: Customer segmentation for eligibility
Version History¶
Last Updated: 2026-01-05 Model Version: sale_promotion.py (83 lines) Framework: Netforce
Additional Resources¶
- Sale Order Documentation:
sale.order - Sale Order Promotion Documentation:
sale.order.promotion - Coupon Master Documentation:
sale.coupon.master - E-commerce Cart Documentation:
ecom.cart
Support & Feedback¶
For issues or questions about this module: 1. Check related model documentation (sale.order, ecom.cart) 2. Review promotion criteria and date ranges 3. Verify customer eligibility and product targeting 4. Test promotions in inactive state before activating
This documentation is generated for developer onboarding and reference purposes.