Payment infrastructure in ERP systems is one of those areas where small API decisions compound into real architectural consequences. When a checkout flow needs to figure out which payment options to show a customer, it has to answer three related questions: which providers are available, which payment methods work with those providers, and which saved tokens (stored card details) the customer can reuse. Until now, Odoo answered each of those questions from a different model. That just changed.
The Problem With Distributed Discovery
In the previous architecture, finding available payment options meant calling three separate methods on three separate models.PaymentProvider._get_compatible_providers() returned eligible providers. PaymentMethod._get_compatible_payment_methods()returned eligible methods. PaymentToken._get_available_tokens()returned the customer’s saved tokens. Each model handled its own filtering logic independently.
This worked fine when each model’s availability logic was self-contained. But payment availability is inherently relational. A payment method is only “available” if its provider is available. A saved token is only usable if both its provider and its method are still active. Distributing the discovery logic across three models meant that coordinating these dependencies required the calling code to stitch results together — and that stitching was where bugs crept in.
One Model to Query Them All
The refactored API moves all three discovery methods onto thePaymentProvider model. The new entry points are_find_available_providers(),_find_available_payment_methods(), and_find_available_tokens(). Calling code now asks the provider for everything: “Given this context, what’s available?”
This isn’t just a rename. Centralizing on the provider means the relational dependencies are resolved in one place. The provider knows which methods it supports, and it knows which tokens are linked to those methods. Instead of three independent queries that the caller has to reconcile, there’s a single model that owns the full picture and can enforce consistency internally.
From ‘Compatible’ to ‘Available’
The terminology shift from “compatible” to “available” is subtle but meaningful. “Compatible” described a static relationship — this method works with this provider in principle. “Available” describes a runtime state — this method works with this provider right now, given the current transaction context, currency, and customer.
That distinction matters for developers integrating custom payment providers. The old _get_compatible_providers name suggested a simple capability check. The new_find_available_providers name makes it clear that the method considers live state: is the provider enabled, does it support this currency, is it configured for this company? The name sets the right expectation about what the method actually evaluates.
PaymentMethod Gets a New Role
With discovery logic moved to the provider, thePaymentMethod model has been refocused on post-processing. Two new methods replace the old lookup:_deduplicate_by_code() and_sort_by_display_order().
Deduplication solves a real problem in multi-provider setups. If a merchant has both Stripe and Adyen enabled, and both support Visa, the checkout page shouldn’t show Visa twice._deduplicate_by_code() collapses methods that share the same code into a single checkout option. Meanwhile,_sort_by_display_order() ensures the surviving options appear in the sequence the merchant configured, not in whatever arbitrary order the database returned them.
This separation of concerns is clean. The provider decides what’s available. The method model decides how to present it. Neither needs to know about the other’s internals.
A Helper That Moved Models
One method had a particularly interesting migration. The oldPaymentMethod._get_from_code()— a helper that looked up a payment method by its string code — moved toPaymentProvider._get_pm_from_code(). The functionality is identical, but the new home makes more sense: the provider is the entity that needs to resolve method codes during transaction processing, not the method model itself.
Similarly, PaymentToken._get_available_tokens() was removed entirely from the token model. Its replacement lives on the provider as _find_available_tokens(). Tokens no longer self-select; the provider selects them based on its own availability criteria.
What This Means for Custom Payment Integrations
For developers maintaining custom payment provider modules, this refactor touches every integration point. Any code that called _get_compatible_providers,_get_compatible_payment_methods, or_get_available_tokens needs to be updated to use the new _find_available_* methods on the provider model.
The migration is mechanical but not trivial. The method signatures may have changed along with the names, and the fact that all three now live on one model means the calling patterns are different. Code that previously queried PaymentToken for saved cards now queries PaymentProvider instead.
The payoff, though, is a payment API that’s easier to reason about. When something goes wrong in a checkout flow — a method that should appear doesn’t, or a saved token isn’t offered — the debugging path now starts and ends at the provider model. There’s one place to look, not three. For teams supporting high-volume e-commerce deployments, that clarity is worth the migration effort.