GuideMarch 13, 2026

Setting Up Recurring Invoices
and Subscriptions in Odoo 19

INTRODUCTION

The Subscription Economy Is Not Optional Anymore

SaaS, managed services, maintenance contracts, equipment leasing, consumable replenishment—recurring revenue models now account for over $300 billion globally and are growing at 3.7x the rate of traditional one-time sales. If your business sells anything that renews, you need billing infrastructure that handles it without manual intervention every month.

The problem most Odoo implementations run into: they configure subscriptions as regular sales orders and then manually create invoices each period. Someone on the finance team opens a spreadsheet, checks which customers are due, creates invoices one by one, and sends them. This works for 10 subscriptions. It collapses at 200. By 1,000, you've hired a full-time person whose entire job is clicking "Create Invoice" in Odoo.

Odoo 19's subscription module (sale_subscription) eliminates this entirely. It handles recurring product configuration, automated invoice generation, payment collection via payment providers, lifecycle management (upsell, downgrade, churn, renewal), and MRR/ARR reporting out of the box. This guide walks through every configuration step, with the code-level details that documentation skips.

01

Configuring Subscription Products and Recurring Plans

Everything starts with the product. A subscription product in Odoo 19 is a standard product.template with the Recurring flag enabled and a linked recurrence plan. The plan defines the billing interval—the product defines what the customer is paying for.

Enable the Subscription Module

Navigate to Settings → Sales → Subscriptions and enable the feature. This installs sale_subscription and adds the Subscriptions menu under Sales. If you are working from source:

Bash — Install the module from CLI
# Install sale_subscription via CLI
./odoo-bin -d your_db -i sale_subscription --stop-after-init

# Verify the module is installed
./odoo-bin shell -d your_db --no-http <<'EOF'
env['ir.module.module'].search([
    ('name', '=', 'sale_subscription'),
    ('state', '=', 'installed'),
]).mapped('name')
EOF

Create a Recurring Plan

Recurring plans define how often and how much a subscription bills. Navigate to Sales → Configuration → Recurring Plans. Each plan consists of a billing period and pricing rules:

Python — Creating plans programmatically
from odoo import fields, models


class SaleSubscriptionPlan(models.Model):
    _inherit = "sale.subscription.plan"

# Create plans via XML data or Python script:
monthly_plan = env['sale.subscription.plan'].create({
    'name': 'Monthly',
    'billing_period_value': 1,
    'billing_period_unit': 'month',
})

annual_plan = env['sale.subscription.plan'].create({
    'name': 'Annual',
    'billing_period_value': 1,
    'billing_period_unit': 'year',
})

Configure a Subscription Product

XML — data/subscription_products.xml
<odoo>
  <record id="product_saas_platform" model="product.template">
    <field name="name">SaaS Platform - Professional</field>
    <field name="type">service</field>
    <field name="recurring_invoice">True</field>
    <field name="list_price">299.00</field>
    <field name="uom_id" ref="uom.product_uom_unit"/>
    <field name="subscription_plan_id"
           ref="monthly_plan"/>
    <field name="description_sale">
      Professional tier: 50 users, 100GB storage,
      priority support, API access.
    </field>
  </record>
</odoo>
Product Type Matters

Subscription products must be of type service. If you set the type to consu (consumable) or product (storable), Odoo will still allow recurring invoicing but the inventory module will try to create delivery orders each period—which makes no sense for a SaaS subscription. Set it to service and avoid phantom stock moves clogging your warehouse dashboard.

02

Setting Up Subscription Templates: Monthly, Quarterly, and Annual Billing

Subscription templates (called Quotation Templates in Odoo 19's unified model) pre-configure the subscription order lines, terms, and recurring plan. Think of them as blueprints your sales team selects when creating a new subscription—they standardize what gets sold and how.

Create Tiered Quotation Templates

XML — data/subscription_templates.xml
<odoo>
  <!-- Monthly Professional Plan -->
  <record id="tmpl_pro_monthly"
          model="sale.order.template">
    <field name="name">Professional - Monthly</field>
    <field name="plan_id" ref="monthly_plan"/>
    <field name="note">
      Monthly subscription. Auto-renews on the same
      day each month. Cancel anytime with 30-day notice.
    </field>
  </record>

  <record id="tmpl_pro_monthly_line1"
          model="sale.order.template.line">
    <field name="sale_order_template_id"
           ref="tmpl_pro_monthly"/>
    <field name="product_id"
           ref="product_saas_platform"/>
    <field name="quantity">1</field>
  </record>

  <!-- Quarterly with 10% discount -->
  <record id="tmpl_pro_quarterly"
          model="sale.order.template">
    <field name="name">Professional - Quarterly</field>
    <field name="plan_id" ref="quarterly_plan"/>
    <field name="note">
      Quarterly billing at 10% discount. Billed every
      3 months. Minimum commitment: 1 quarter.
    </field>
  </record>

  <!-- Annual with 20% discount -->
  <record id="tmpl_pro_annual"
          model="sale.order.template">
    <field name="name">Professional - Annual</field>
    <field name="plan_id" ref="annual_plan"/>
    <field name="note">
      Annual commitment at 20% discount.
      Billed upfront for the full year.
    </field>
  </record>
</odoo>

Pricing Strategy: Discount Tiers via Pricelists

Rather than creating separate products for monthly vs. annual pricing, use pricelists linked to the subscription plan. This keeps your product catalog clean and lets you adjust pricing centrally:

Python — Programmatic pricelist for annual discount
# Annual pricelist: 20% discount on all subscription products
annual_pricelist = env['product.pricelist'].create({
    'name': 'Annual Subscription Pricing',
    'item_ids': [(0, 0, {
        'applied_on': '1_product',
        'product_tmpl_id': product_saas.id,
        'compute_price': 'percentage',
        'percent_price': 20.0,  # 20% discount
    })],
})

# Link pricelist to annual subscription template
annual_template = env.ref('my_module.tmpl_pro_annual')
annual_template.write({
    'pricelist_id': annual_pricelist.id,
})
Template vs. Plan

A common confusion: the plan defines the billing interval (monthly, quarterly, annual). The template defines what products and terms are included. You can have multiple templates using the same plan—e.g., a "Starter Monthly" and "Professional Monthly" template that both use the monthly plan but include different product lines and pricing.

03

Automating Invoice Generation and Payment Collection

This is where the real value lives. Without automation, subscriptions are just sales orders with a calendar reminder. With it, Odoo generates invoices, attempts payment capture, sends email notifications, and handles failures—all without human intervention.

The Recurring Invoice Cron Job

Odoo 19 ships a scheduled action (ir.cron) that runs daily and processes all subscriptions due for invoicing. Here's how the engine works under the hood:

Python — How the cron processes subscriptions
# Simplified view of the recurring invoice logic
# Located in: sale_subscription/models/sale_order.py

def _cron_recurring_create_invoice(self):
    """Called daily by ir.cron. Processes all active
    subscriptions where next_invoice_date <= today."""

    today = fields.Date.today()
    subscriptions = self.search([
        ('is_subscription', '=', True),
        ('subscription_state', '=', '3_progress'),
        ('next_invoice_date', '<=', today),
    ])

    for sub in subscriptions:
        try:
            # 1. Create the invoice
            invoice = sub._create_recurring_invoice()

            # 2. Auto-post the invoice (if configured)
            if sub.company_id.subscription_auto_post:
                invoice.action_post()

            # 3. Attempt automatic payment
            if sub.payment_token_id:
                sub._do_payment(invoice)

            # 4. Advance the next invoice date
            sub._update_next_invoice_date()

            # 5. Send the invoice email
            if sub.company_id.subscription_auto_send:
                invoice._send_email()

        except Exception as e:
            # Log the error, don't break the batch
            _logger.error(
                "Subscription %s invoice failed: %s",
                sub.name, str(e)
            )

Configure Automatic Payment with Payment Providers

For true hands-off billing, you need a payment provider that supports tokenization (saving the customer's payment method for future charges). Odoo 19 supports this with Stripe, Adyen, Authorize.net, and Mollie:

Python — Configure Stripe for recurring payments
# Ensure the payment provider supports tokenization
stripe_provider = env['payment.provider'].search([
    ('code', '=', 'stripe'),
    ('state', '=', 'enabled'),
])

# Enable tokenization for subscriptions
stripe_provider.write({
    'allow_tokenization': True,
    'allow_express_checkout': True,
})

# When a customer pays their first invoice through
# the portal, Odoo saves the payment token.
# Subsequent invoices charge that token automatically.

# To manually assign a token to a subscription:
subscription.write({
    'payment_token_id': customer_token.id,
})

Email Automation for Invoice Delivery

XML — Custom email template for subscription invoices
<odoo>
  <record id="email_subscription_invoice"
          model="mail.template">
    <field name="name">Subscription Invoice</field>
    <field name="model_id"
           ref="account.model_account_move"/>
    <field name="subject">
      {{ object.company_id.name }} - Invoice
      {{ object.name }} for {{ object.partner_id.name }}
    </field>
    <field name="body_html"><![CDATA[
      <p>Dear {{ object.partner_id.name }},</p>
      <p>Your subscription invoice
         <strong>{{ object.name }}</strong>
         for <strong>{{ object.amount_total }}</strong>
         {{ object.currency_id.name }} is ready.</p>
      <p t-if="object.payment_state == 'paid'">
        Payment has been automatically processed
        using your saved payment method.
      </p>
      <p t-else="">
        Please review and pay at your earliest
        convenience via the button below.
      </p>
    ]]></field>
    <field name="report_template_ids"
           eval="[(4, ref('account.account_invoices'))]"/>
  </record>
</odoo>
Cron Timing Matters

The default cron runs at midnight server time. If your server is in UTC but your customers are in US Pacific, invoices dated "March 1" get generated at 4 PM PST on February 28. This confuses customers who see an invoice before their billing period actually starts. Adjust the cron's nextcall to run at 6:00 AM UTC (or your preferred time) to align invoice generation with your customers' business hours.

04

Managing the Subscription Lifecycle: Upsell, Downgrade, Churn, and Renewal

A subscription is not a set-and-forget object. Customers upgrade, downgrade, pause, cancel, and renew. Each lifecycle event has billing implications that Odoo handles through subscription state transitions and order line modifications.

The Subscription State Machine

StateInternal ValueMeaningBilling Active?
Draft1_draftQuotation stage, not yet confirmedNo
In Progress3_progressActive subscription, auto-invoicing enabledYes
Paused4_pausedTemporarily halted, no invoices generatedNo
Closed6_churnPermanently cancelled, cannot be reactivatedNo

Upsell: Adding Lines to an Active Subscription

Python — Upsell workflow
# Upsell: Create a new SO linked to the subscription
# that adds additional products or increases quantities

subscription = env['sale.order'].browse(subscription_id)

# Option 1: Use the built-in upsell wizard
action = subscription.prepare_upsell_order()
upsell_order = env['sale.order'].browse(
    action['res_id']
)

# Add the upsell line
upsell_order.write({
    'order_line': [(0, 0, {
        'product_id': addon_product.id,
        'product_uom_qty': 10,  # 10 extra user seats
        'price_unit': 15.00,    # per seat per month
    })],
})

# Confirm the upsell — merges into the subscription
upsell_order.action_confirm()

Handling Churn: Close vs. Pause

When a customer wants to stop their subscription, you have two options with very different data implications:

Python — Close vs. Pause
# PAUSE: Subscription remains in the system,
# can be reactivated. No invoices generated.
subscription.write({
    'subscription_state': '4_paused',
})

# CLOSE: Permanent cancellation. Sets an end date.
# The subscription cannot be restarted.
close_wizard = env['sale.subscription.close.reason.wizard']\
    .create({
        'close_reason_id': env.ref(
            'sale_subscription.close_reason_too_expensive'
        ).id,
    })
close_wizard.with_context(
    active_ids=subscription.ids
).action_close_subscription()

# RENEW: Creates a new subscription from a closed one
# with the same terms (new start date, fresh lifecycle)
action = subscription.prepare_renewal_order()
renewal = env['sale.order'].browse(action['res_id'])
renewal.action_confirm()
Track Churn Reasons

Always require a close reason when cancelling subscriptions. Odoo's sale.subscription.close.reason model lets you define reasons like "Too expensive," "Switched to competitor," "No longer needed," or "Poor support." This data feeds directly into churn analysis dashboards. Without it, you know who left but never why—and you can't fix what you can't measure.

05

Revenue Recognition and MRR/ARR Reporting in Odoo 19

Recurring revenue businesses live and die by their metrics: MRR (Monthly Recurring Revenue), ARR (Annual Recurring Revenue), churn rate, LTV, and expansion revenue. Odoo 19 tracks these natively through the subscription module's reporting engine.

Understanding MRR Calculation

Odoo computes MRR by normalizing every subscription's recurring amount to a monthly value. An annual subscription at $2,400/year contributes $200/month to MRR. A quarterly subscription at $900/quarter contributes $300/month. The formula:

Python — MRR computation logic
# Simplified MRR calculation from sale_subscription
# Located in: sale_subscription/models/sale_order.py

@api.depends('order_line.price_subtotal',
             'plan_id.billing_period_value',
             'plan_id.billing_period_unit')
def _compute_recurring_monthly(self):
    for order in self:
        period = order.plan_id
        if not period:
            order.recurring_monthly = 0.0
            continue

        # Normalize to monthly
        if period.billing_period_unit == 'month':
            divisor = period.billing_period_value
        elif period.billing_period_unit == 'year':
            divisor = period.billing_period_value * 12
        elif period.billing_period_unit == 'week':
            divisor = period.billing_period_value / 4.33
        else:
            divisor = 1

        order.recurring_monthly = (
            order.amount_untaxed / divisor
        )

Built-in Subscription Dashboard

Navigate to Sales → Reporting → Subscriptions to access the dashboard. Key metrics available out of the box:

MetricDefinitionWhere in Odoo
MRRSum of all active subscriptions normalized to monthlySubscription Analysis pivot/graph
ARRMRR × 12Computed field on dashboard
Net New MRRNew subscriptions + expansion − contraction − churnMRR timeline graph
Churn RateMRR lost from cancellations / starting MRRSubscription Analysis filter by state
ARPUTotal MRR / number of active subscriptionsCustom measure in pivot view

Custom KPI Dashboard with Server Actions

Python — Custom MRR/churn report
# Server action for monthly subscription KPI email
def _compute_subscription_kpis(self):
    today = fields.Date.today()
    first_of_month = today.replace(day=1)

    active_subs = self.env['sale.order'].search([
        ('is_subscription', '=', True),
        ('subscription_state', '=', '3_progress'),
    ])

    churned_subs = self.env['sale.order'].search([
        ('is_subscription', '=', True),
        ('subscription_state', '=', '6_churn'),
        ('end_date', '>=', first_of_month),
        ('end_date', '<=', today),
    ])

    total_mrr = sum(active_subs.mapped(
        'recurring_monthly'
    ))
    churned_mrr = sum(churned_subs.mapped(
        'recurring_monthly'
    ))
    churn_rate = (
        churned_mrr / (total_mrr + churned_mrr)
    ) * 100 if (total_mrr + churned_mrr) else 0

    return {
        'total_mrr': total_mrr,
        'arr': total_mrr * 12,
        'active_count': len(active_subs),
        'churned_count': len(churned_subs),
        'churn_rate_pct': round(churn_rate, 2),
        'arpu': total_mrr / len(active_subs)
                if active_subs else 0,
    }
Revenue Recognition Warning

If you bill annually but recognize revenue monthly (as ASC 606 / IFRS 15 requires), you need the Revenue Recognition feature in Odoo Accounting. This creates deferred revenue journal entries that spread the annual payment across 12 months. Without it, your P&L shows a massive spike in the billing month and zero revenue for the remaining 11—which misrepresents your actual financial performance to investors and auditors.

06

3 Subscription Billing Mistakes That Cost You Revenue and Customers

1

Proration on Mid-Cycle Plan Changes

A customer upgrades from the $99/month Starter plan to the $299/month Professional plan on day 15 of a 30-day billing cycle. What appears on the next invoice? If you haven't configured proration, the answer is: the full $299—even though the customer only used 15 days of the Professional tier. They'll dispute the charge, and they'll be right.

Our Fix

Odoo 19 handles proration through the upsell order mechanism. When you create an upsell order mid-cycle, the system calculates the prorated amount based on remaining days. However, this only works if you use the upsell wizard—not if you manually edit the subscription order lines. Manual edits bypass proration entirely. Always use prepare_upsell_order() for mid-cycle changes, and verify the prorated amount on the upsell quotation before confirming.

2

Failed Payment Retry Logic

The cron generates an invoice, attempts to charge the customer's saved card, and the payment fails (expired card, insufficient funds, bank decline). What happens next? By default, nothing. The invoice sits in "Not Paid" status, no retry is attempted, and no notification is sent to the customer. You lose revenue from what the SaaS world calls "involuntary churn"—customers who wanted to pay but couldn't.

Our Fix

Implement a dunning sequence using Odoo's follow-up module (account_followup) combined with a custom cron that retries failed payment tokens. Create a retry schedule: attempt again after 1 day, 3 days, and 7 days. After the final retry, send a "payment method update required" email with a portal link. If no payment after 14 days, auto-pause the subscription. This recovers 30-40% of failed payments that would otherwise be lost.

3

Subscription Close vs. Pause Behavior

A customer asks to "cancel" their subscription. Your support agent clicks Close instead of Pause. Two weeks later, the customer wants to come back. But a closed subscription in Odoo 19 is permanent—it sets the state to 6_churn, stamps an end date, and the original subscription order cannot be reactivated. You have to create an entirely new subscription, losing the customer's history, MRR timeline, and accumulated data.

Our Fix

Establish a clear internal policy: Pause first, close later. When a customer says "cancel," pause the subscription and set a calendar reminder for 30 days. Only close after the cooling-off period expires with no reactivation request. Add access rights so that only subscription managers (not regular sales reps) can execute the Close action. Use the renewal workflow (prepare_renewal_order()) for customers who return after a close—it creates a new subscription pre-filled with the original terms, preserving continuity in your reporting.

BUSINESS ROI

What Automated Subscription Billing Saves Your Business

Moving from manual recurring invoicing to automated subscriptions isn't a technical upgrade—it's a revenue protection strategy:

40+ hrs/moFinance Team Time Saved

Automated invoice generation, payment collection, and email delivery eliminates the manual billing cycle that consumes 1-2 full weeks of finance team effort each month.

30-40%Failed Payment Recovery

Automated retry logic and dunning sequences recover revenue from expired cards and temporary declines that would otherwise be lost to involuntary churn.

Real-timeMRR/ARR Visibility

Investors and leadership get accurate recurring revenue metrics without waiting for month-end spreadsheet reconciliation. Every subscription change reflects in MRR instantly.

For a company with 500 active subscriptions at $200/month average MRR, automated billing prevents an estimated $24,000-$48,000/year in involuntary churn through payment retry alone. Add the labor savings from eliminating manual invoicing, and the ROI pays for the entire implementation within the first quarter.

SEO NOTES

Optimization Metadata

Meta Desc

Complete guide to setting up recurring invoices and subscriptions in Odoo 19. Configure subscription products, automate billing, manage lifecycle events, and track MRR/ARR.

H2 Keywords

1. "Configuring Subscription Products and Recurring Plans"
2. "Automating Invoice Generation and Payment Collection"
3. "Revenue Recognition and MRR/ARR Reporting in Odoo 19"
4. "3 Subscription Billing Mistakes That Cost You Revenue and Customers"

Your Subscriptions Should Run Themselves

Manual recurring billing is a liability that scales linearly with your customer count. Every subscription you add increases the work for your finance team, the risk of missed invoices, and the probability of revenue leakage from failed payments that nobody notices. Odoo 19's subscription engine eliminates all of this—but only if it's configured correctly from day one.

If you're still creating recurring invoices manually, or if your subscription setup is half-configured and leaking revenue, let's fix it. We implement end-to-end subscription billing in Odoo—from product configuration and payment provider integration to dunning sequences and MRR dashboards. Most implementations take 1-2 weeks, and the ROI shows up in your first automated billing cycle.

Book a Free Subscription Audit