Skip to content

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

inactive ←→ active
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:

# Activate a promotion
get_model("sale.promotion").activate([promo_id])


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:

# Deactivate a promotion
get_model("sale.promotion").deactivate([promo_id])


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:

context = {
    "cart_id": 123,                       # Shopping cart ID
}

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:

context = {
    "cart_id": 123,                       # Shopping cart ID
}

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

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.