GuideOdoo SalesMarch 13, 2026

Odoo 19 Loyalty Programs:
Points, Gift Cards & Coupons for E-commerce and POS

INTRODUCTION

Loyalty Programs Are No Longer a Retail Luxury—They're a Revenue Engine

Every e-commerce store and point-of-sale terminal competes for repeat purchases. Acquiring a new customer costs 5–7x more than retaining an existing one, yet most Odoo implementations leave the Loyalty module untouched. The default installation ships with the feature disabled, buried under the Sales → Configuration menu where it quietly waits for someone to flip the switch.

The business impact of not using it is measurable. Stores without loyalty programs see lower average order values, higher churn rates, and zero insight into which customers are one purchase away from becoming high-value accounts. Meanwhile, competitors hand out points, gift cards, and coupons that create switching costs and emotional attachment.

Odoo 19's loyalty engine is surprisingly powerful—it supports four distinct program types, flexible reward structures, conditional triggers, and native integration with both the Website and POS apps. This guide covers everything: how each program type works, how to configure rewards and trigger rules, how loyalty surfaces in e-commerce and POS, how to track performance, and the mistakes that silently break programs in production.

01

The Four Loyalty Program Types in Odoo 19

Odoo 19 organizes loyalty under four distinct program types. Each serves a different commercial purpose, and choosing the wrong type for your use case creates configuration headaches later. Here's what each one does and when to use it.

Program TypeHow It WorksBest ForKey Setting
Loyalty CardsCustomers earn points per order or per currency spent. Points accumulate across sessions and can be redeemed for rewards.Repeat purchase incentives, tiered VIP programsprogram_type = 'loyalty'
CouponsSingle-use or multi-use codes generated manually or in bulk. Customer enters the code at checkout to unlock a reward.Targeted campaigns, influencer codes, win-back emailsprogram_type = 'coupons'
PromotionsAutomatic discounts applied when order conditions are met. No code required—the system applies the reward if the cart qualifies.Flash sales, buy-X-get-Y, minimum spend thresholdsprogram_type = 'promotion'
Gift CardsPrepaid value cards sold as products. Buyers purchase a gift card; recipients redeem the balance at checkout.Holiday campaigns, B2B incentives, customer creditsprogram_type = 'gift_card'

Enabling the Loyalty Module

The loyalty engine lives in the loyalty module, which is auto-installed when you enable loyalty features via Sales → Configuration → Settings → Pricing. For e-commerce, also ensure website_sale_loyalty is installed. For POS, you need pos_loyalty.

Python — Check installed loyalty modules
# In the Odoo shell, verify which loyalty modules are installed
env['ir.module.module'].search([
    ('name', 'like', '%loyalty%'),
    ('state', '=', 'installed'),
]).mapped('name')

# Expected output for full coverage:
# ['loyalty', 'website_sale_loyalty', 'pos_loyalty']

Creating a Loyalty Program via Code

While most users configure programs through the UI, understanding the model structure helps when automating program creation or debugging issues:

Python — Creating a points-based loyalty program
program = env['loyalty.program'].create({
    'name': 'VIP Rewards Club',
    'program_type': 'loyalty',
    'applies_on': 'both',          # 'current' | 'future' | 'both'
    'trigger': 'auto',             # 'auto' | 'with_code'
    'portal_visible': True,
    'rule_ids': [(0, 0, {
        'reward_point_mode': 'money',   # earn points per $ spent
        'reward_point_amount': 1.0,     # 1 point per $1
        'minimum_amount': 25.00,        # min order to earn
    })],
    'reward_ids': [(0, 0, {
        'reward_type': 'discount',
        'discount': 10.0,              # 10% off
        'discount_mode': 'percent',
        'discount_applicability': 'order',
        'required_points': 100,        # costs 100 points
    })],
})
Loyalty vs. Promotion

The most common configuration mistake is using a Promotion when you need a Loyalty Card. Promotions are stateless—they fire every time the cart meets the conditions. Loyalty cards are stateful—they track accumulated points per customer across orders. If you want customers to "build up" rewards over time, you need Loyalty Cards, not Promotions.

02

Configuring Rewards, Trigger Rules, and Point Earning Logic

Every loyalty program has two halves: how customers earn (trigger rules) and what they get (rewards). Odoo 19 separates these into distinct models—loyalty.rule and loyalty.reward—linked to the parent loyalty.program. Understanding this separation is critical because one program can have multiple rules and multiple rewards.

Trigger Rules: How Points Are Earned

Trigger rules define the conditions under which a customer earns points (or qualifies for a reward in the case of promotions). Key fields on loyalty.rule:

FieldValuesEffect
reward_point_mode'order', 'money', 'unit'Earn flat points per order, per currency spent, or per product unit
reward_point_amountFloatNumber of points earned per trigger (e.g., 1.5 points per $1)
minimum_amountFloatMinimum order total to qualify
minimum_qtyIntegerMinimum product quantity to qualify
product_idsMany2manyRestrict earning to specific products
product_category_idMany2oneRestrict earning to a product category

Reward Types: What Customers Get

Odoo 19 supports three reward types, each with its own configuration surface:

Python — Reward type configurations
# 1. Discount reward: percentage or fixed amount off
discount_reward = {
    'reward_type': 'discount',
    'discount': 15.0,
    'discount_mode': 'percent',         # 'percent' | 'per_point' | 'per_order'
    'discount_applicability': 'order',  # 'order' | 'cheapest' | 'specific'
    'discount_max_amount': 50.00,       # cap the discount at $50
    'required_points': 150,
}

# 2. Free product reward: get a product for free
free_product_reward = {
    'reward_type': 'product',
    'reward_product_id': product.id,    # the free product
    'reward_product_qty': 1,
    'required_points': 200,
}

# 3. Free shipping reward (e-commerce only)
free_shipping_reward = {
    'reward_type': 'shipping',
    'required_points': 75,
}

Advanced: Buy X Get Y with Promotions

The classic "Buy 2 Get 1 Free" is configured using a Promotion program with a quantity-based trigger and a free product reward:

Python — Buy 2 Get 1 Free promotion
promo = env['loyalty.program'].create({
    'name': 'Buy 2 Get 1 Free - Summer T-Shirts',
    'program_type': 'promotion',
    'trigger': 'auto',
    'applies_on': 'current',
    'rule_ids': [(0, 0, {
        'reward_point_mode': 'unit',
        'reward_point_amount': 1,
        'minimum_qty': 3,              # must have 3+ in cart
        'product_ids': [(6, 0, tshirt_products.ids)],
    })],
    'reward_ids': [(0, 0, {
        'reward_type': 'product',
        'reward_product_id': tshirt_products[0].id,
        'reward_product_qty': 1,
        'required_points': 1,
    })],
})
Stacking Rules

By default, multiple promotions can stack on the same order. If you run "10% off orders over $100" alongside "Free shipping on orders over $75," a $100 order gets both. To prevent stacking, set pricelist_ids on the program to restrict it to specific pricelists, or use the applies_on = 'current' setting so the discount applies only once per qualifying order.

03

E-commerce Integration: How Loyalty Programs Appear on Your Odoo Website

Installing website_sale_loyalty connects the loyalty engine to the shopping cart, checkout flow, and customer portal. But "installed" doesn't mean "visible"—several configuration steps determine what your customers actually see.

Cart-Level Coupon and Promo Code Entry

When a Coupon or Promotion program has trigger = 'with_code', a promo code input field appears on the cart page. Customers enter their code, the system validates it against the program's rules, and if valid, the reward is applied as a negative line on the order.

XML — Cart promo code widget (website_sale_loyalty)
<!-- The promo code input is injected by website_sale_loyalty
     into the cart template via this xpath: -->
<template id="reduction_code_form"
          inherit_id="website_sale.cart">
  <xpath expr="//div[@id='cart_total']" position="before">
    <div class="o_reward_code_form mt-3">
      <form method="post"
            action="/shop/cart/coupon">
        <input type="hidden" name="csrf_token"
               t-att-value="request.csrf_token()"/>
        <div class="input-group">
          <input type="text" name="promo"
                 class="form-control"
                 placeholder="Enter promo code"/>
          <button type="submit"
                  class="btn btn-secondary">
            Apply
          </button>
        </div>
      </form>
    </div>
  </xpath>
</template>

Loyalty Points Display in Customer Portal

When portal_visible = True on a loyalty program, customers can see their points balance in the portal under My Account → Loyalty Cards. This is critical for engagement—customers can't redeem points they don't know they have.

Python — Querying a customer's loyalty card balance
# Find all loyalty cards for a specific customer
cards = env['loyalty.card'].search([
    ('partner_id', '=', customer.id),
    ('program_id.program_type', '=', 'loyalty'),
])

for card in cards:
    print(f"Program: {{card.program_id.name}}")
    print(f"  Points: {{card.points}}")
    print(f"  Code:   {{card.code}}")

Gift Card Purchase Flow

Gift cards are sold as regular products on the website. The key is linking the product to the gift card program:

Python — Creating a gift card product
# The gift card product must be a service with specific settings
gift_product = env['product.product'].create({
    'name': '$50 Gift Card',
    'type': 'service',
    'list_price': 50.00,
    'sale_ok': True,
    'purchase_ok': False,
    'taxes_id': [(5, 0, 0)],  # Gift cards are typically tax-exempt
})

# Link it to the gift card program
gift_program = env['loyalty.program'].create({
    'name': 'Gift Cards',
    'program_type': 'gift_card',
    'trigger': 'with_code',
    'applies_on': 'future',
    'rule_ids': [(0, 0, {
        'reward_point_mode': 'money',
        'reward_point_amount': 1,
        'product_ids': [(6, 0, [gift_product.id])],
    })],
    'reward_ids': [(0, 0, {
        'reward_type': 'discount',
        'discount_mode': 'per_point',
        'discount': 1.0,           # $1 discount per point
        'discount_applicability': 'order',
        'required_points': 1,
    })],
})
Email Delivery of Gift Cards

When a gift card order is confirmed, Odoo automatically generates a loyalty.card record with a unique code and sends it to the buyer via email. Make sure your email template for gift cards is customized—the default template is minimal. Navigate to Settings → Technical → Email Templates and search for "Gift Card" to customize the design.

04

POS Integration: Loyalty Programs at the Point of Sale

The pos_loyalty module extends the POS interface with loyalty features. Cashiers can scan loyalty cards, apply coupon codes, redeem points, and sell gift cards—all within the POS session. The integration is real-time: points earned in-store are immediately available online, and vice versa.

POS Configuration

Each POS configuration can enable or disable specific loyalty programs. This allows you to run different promotions in different stores:

Navigation — POS Loyalty Settings
# Point of Sale → Configuration → Point of Sale
# Select your POS config, then under the "Pricing" tab:

# 1. Enable "Loyalty, Coupons & Gift Cards"
# 2. Select which programs are available in this POS
# 3. Programs with trigger='auto' apply automatically
# 4. Programs with trigger='with_code' require manual entry

# The POS will show:
# - A "Loyalty" button on the order screen
# - A "Promo Code" button for code-based programs
# - Earned points on the receipt

Cashier Workflow: Earning and Redeeming

The POS loyalty flow works in three steps:

  • Customer identification — The cashier sets the customer on the order. This links the order to the customer's loyalty card. Without a customer, points-based programs won't track anything.
  • Automatic earning — As products are scanned, the POS calculates earned points in real time based on the program's rules. The cashier sees a notification: "Customer will earn X points."
  • Redemption — The cashier clicks "Rewards" to see available rewards. If the customer has enough points, they can apply the reward as a discount line on the order. The points are deducted from the loyalty card.
JavaScript — POS loyalty card lookup (pos_loyalty module)
// Inside the POS JS, the loyalty card is fetched when
// a customer is set on the order:
async _onSetCustomer(partner) {
    const loyaltyCards = await this.orm.searchRead(
        'loyalty.card',
        [
            ['partner_id', '=', partner.id],
            ['program_id.pos_ok', '=', true],
            ['points', '>', 0],
        ],
        ['program_id', 'points', 'code'],
    );
    this.currentOrder.setLoyaltyCards(loyaltyCards);
}

Receipt Customization

By default, the POS receipt shows earned points and remaining balance. You can customize the receipt template to display the loyalty information more prominently:

XML — POS receipt loyalty section
<!-- Loyalty info on POS receipt (pos_loyalty module) -->
<t t-if="loyaltyPoints">
  <br/>
  <div style="text-align:center; font-weight:bold;">
    --- LOYALTY REWARDS ---
  </div>
  <t t-foreach="loyaltyPoints" t-as="program">
    <div>
      <t t-out="program.program_name"/>
    </div>
    <div>
      Points earned: +<t t-out="program.points_won"/>
    </div>
    <div>
      Total balance: <t t-out="program.points_total"/>
    </div>
  </t>
</t>
Offline Mode

The POS loyalty module works offline, but with a caveat: loyalty card balances are cached locally. If a customer redeems points at one terminal while another terminal has a stale cache, double-redemption is possible. Points are reconciled when the POS session is closed and synced. For high-volume stores, consider shortening the sync interval or requiring online mode for redemptions above a threshold.

05

Loyalty Program Reporting and Analytics

Running a loyalty program without tracking its performance is like running ads without conversion metrics. Odoo 19 provides several reporting angles, but you need to know where to look—and what to build yourself.

Built-in Reports

ReportLocationWhat It Shows
Loyalty Cards ListSales → Loyalty CardsAll issued cards with points balance, customer, and program
Coupon UsageSales → Coupon Programs → CouponsGenerated codes, usage count, remaining uses
Gift Card BalancesSales → Gift CardsOutstanding gift card value (a liability on your books)

Custom Analytics: Measuring Program ROI

The built-in reports show operational data but not business intelligence. Here's a SQL query pattern for measuring the actual ROI of your loyalty program:

SQL — Loyalty program ROI analysis
-- Compare average order value: loyalty members vs. non-members
WITH loyalty_customers AS (
    SELECT DISTINCT partner_id
    FROM loyalty_card
    WHERE program_id IN (
        SELECT id FROM loyalty_program
        WHERE program_type = 'loyalty'
    )
    AND points > 0
)
SELECT
    CASE
        WHEN lc.partner_id IS NOT NULL THEN 'Loyalty Member'
        ELSE 'Non-Member'
    END AS segment,
    COUNT(DISTINCT so.id) AS total_orders,
    ROUND(AVG(so.amount_total), 2) AS avg_order_value,
    ROUND(SUM(so.amount_total), 2) AS total_revenue,
    COUNT(DISTINCT so.partner_id) AS unique_customers
FROM sale_order so
LEFT JOIN loyalty_customers lc
    ON so.partner_id = lc.partner_id
WHERE so.state IN ('sale', 'done')
    AND so.date_order >= NOW() - INTERVAL '90 days'
GROUP BY
    CASE
        WHEN lc.partner_id IS NOT NULL THEN 'Loyalty Member'
        ELSE 'Non-Member'
    END;

Tracking Outstanding Liability

Gift cards and unredeemed loyalty points represent a financial liability. For accounting compliance, you should track the total outstanding value:

Python — Calculate outstanding gift card liability
# Total unredeemed gift card value
gift_cards = env['loyalty.card'].search([
    ('program_id.program_type', '=', 'gift_card'),
    ('points', '>', 0),
])

total_liability = sum(card.points for card in gift_cards)
print(f"Outstanding gift card liability: ${{total_liability:,.2f}}")
Breakage Revenue

"Breakage" is the industry term for gift cards and points that are never redeemed. Typical breakage rates are 10–15% for gift cards and 20–30% for loyalty points. Accounting standards (ASC 606 / IFRS 15) require you to recognize breakage revenue proportionally as redemptions occur. Make sure your finance team understands this before launching a large gift card program.

06

3 Loyalty Program Mistakes That Silently Cost You Revenue

1

Points Earned But Never Visible to Customers

You configure a beautiful loyalty program, customers earn points on every order, but nobody redeems. The reason: portal_visible is set to False (the default). Customers have no idea they have points. They never see a balance, never get prompted to redeem, and the program generates zero incremental revenue. You've created a cost center (you're giving discounts to the few who discover it by accident) with none of the retention benefits.

Our Fix

Always set portal_visible = True on loyalty programs. Then go further: customize the order confirmation email template to include the points earned and current balance. Add a banner on the cart page showing available rewards. The goal is to make points feel tangible. A points balance that customers can see is 3x more likely to drive a repeat purchase than one they can't.

2

Promotion Stacking That Erodes Your Margins

You launch three promotions: "10% off first order," "Free shipping over $50," and "Buy 2 Get 1 Free on accessories." A savvy customer adds 3 accessories ($30 each = $90), gets one free ($30 off), gets 10% off the remaining $60 ($6 off), and gets free shipping ($8 saved). Your effective discount is 49% on a $90 cart. The margin on those accessories just vanished. This happens because Odoo applies all qualifying promotions by default—there's no built-in stacking limit.

Our Fix

Use the rule_ids to set mutually exclusive conditions. Assign promotions to different pricelists so they can't co-exist on the same order. For code-based promotions, set maximum_use_number on the coupon. Most importantly: model the worst-case stacking scenario before launching. Calculate what happens when a customer qualifies for every active promotion simultaneously. If the combined discount exceeds your target margin, restructure the programs.

3

Gift Cards Not Generating loyalty.card Records After Purchase

A customer buys a $100 gift card. The order is confirmed, payment is received, but the recipient never gets a gift card code. The loyalty.card record was never created. This happens when the gift card product isn't correctly linked to the gift card program—either the product isn't in the program's rule product_ids, or the program's applies_on is set to 'current' instead of 'future'. You've taken the customer's money and delivered nothing.

Our Fix

After creating a gift card program, always test the full flow: purchase the gift card product, confirm the order, check that a loyalty.card record was created with the correct points balance, and verify the email was sent with the code. Automate this test in your CI pipeline. The three critical settings: the product must be in the rule's product_ids, the program must have applies_on = 'future', and the program trigger must be 'with_code'.

BUSINESS ROI

What a Well-Configured Loyalty Program Adds to Your Bottom Line

A loyalty program isn't a cost—it's an investment in customer lifetime value:

25–40%Higher Repeat Purchase Rate

Customers with an active points balance are significantly more likely to return. The unredeemed balance creates a psychological switching cost that keeps them in your ecosystem.

12–18%Increase in Average Order Value

Customers spend more to hit earning thresholds. A "Earn double points on orders over $75" rule nudges a $60 cart to $75—a 25% increase on that transaction at almost zero cost.

3–5xROI on Gift Card Programs

Gift card buyers spend an average of 20% above the card's face value. Combined with 10–15% breakage, gift cards generate net-positive revenue before accounting for the new customer acquisition they drive.

For a mid-size e-commerce store doing $500,000/month in revenue, a loyalty program that increases repeat purchase rate by 30% and AOV by 15% adds approximately $1.35M in annual incremental revenue. The cost of configuring and maintaining the program in Odoo is a fraction of a single month's uplift.

SEO NOTES

Optimization Metadata

Meta Desc

Complete guide to Odoo 19 loyalty programs. Configure points, coupons, promotions, and gift cards for e-commerce and POS with trigger rules, rewards, and reporting.

H2 Keywords

1. "The Four Loyalty Program Types in Odoo 19"
2. "Configuring Rewards, Trigger Rules, and Point Earning Logic"
3. "E-commerce Integration: How Loyalty Programs Appear on Your Odoo Website"
4. "POS Integration: Loyalty Programs at the Point of Sale"
5. "3 Loyalty Program Mistakes That Silently Cost You Revenue"

Your Loyalty Program Should Pay for Itself in Month One

The loyalty engine in Odoo 19 is one of the highest-ROI features you can activate. Points programs create repeat buyers. Gift cards bring in new customers at zero acquisition cost. Coupons give your marketing team surgical precision. Promotions automate what used to require manual discount approvals. And all of it works across both your e-commerce store and your physical POS terminals.

If your Odoo loyalty module is still sitting at factory defaults, you're leaving money on the table. We configure loyalty programs that match your commercial strategy, integrate cleanly with your e-commerce and POS workflows, and include the reporting you need to measure ROI from day one. Most implementations take 3–5 days and start paying for themselves within the first month.

Book a Free Loyalty Program Audit