Django plans payments
Almost automatic integration between django-plans and django-payments. This will add payment buttons to the order page and automatically confirm the Order after the payment. Optionally, it can return the corresponding order when a payment is refunded.
Documentation
The full documentation is at https://django-plans-payments.readthedocs.io.
Quickstart
Install and configure django-plans and django-payments apps. Capture mode is not yet supported, so PAYMENT_VARINANTS with 'capture': False will not get confirmed.
Install Django plans payments:
pip install django-plans-payments
Add it to your INSTALLED_APPS, before the plans:
INSTALLED_APPS = (
...
'related_admin',
'plans_payments',
'plans',
...
)
Add Django plans_payments to the URL patterns:
urlpatterns = [
...
url(r'^plans-payments', include('plans_payments.urls')),
...
]
Set django-plans settings and set model to:
PAYMENT_MODEL = 'plans_payments.Payment'
Charge-attempt idempotency
CreatePaymentView refuses to fire duplicate live charge attempts: a request while a previous payment is still in flight (or just captured) redirects to that payment instead of creating a twin, and a request right after a decline waits out a short cooldown, because banks read rapid-fire retries as fraud. Both windows look across all the user’s orders and are configurable:
PLANS_PAYMENTS_JOIN_IN_FLIGHT_SECONDS = 180 # 0 disables joining
PLANS_PAYMENTS_DECLINE_COOLDOWN_SECONDS = 60 # 0 disables the cooldown
Customer IP address
Customer IP address is stored in Payment model and used for some payment providers (i.e. PayU). For security reasons django-plans-payments does acquire the IP only from request REMOTE_ADDR parameter. If you are behind proxy, you will need to setup some mechanism to populate this variable from HTTP_X_FORWARDED_FOR parameter. The suggested solution is to use django-httpforwardedfor or django-xff application for that.
Running Tests
Does the code actually work?
source <YOURVIRTUALENV>/bin/activate (myenv) $ pip install tox (myenv) $ tox
Credits
Tools used in rendering this package:
History
2.5.0 (2026-10-09)
System check plans_payments.W001 warns when the installed django-payments has no Payment.autocomplete_with_wallet(). Released django-payments lacks it, so automatic renewals failed with an AttributeError on the day they were due.
An automatic renewal that needs 3-D Secure is logged instead of printed to stdout, without the member’s payment link.
The package’s long description is declared as reStructuredText.
A confirmed payment never removes the subscription. 2.4.0 removed it when the payment’s variant differed from the subscription’s provider, so a subscriber who bought a one-off period or an upgrade with another payment method (a payu card payment against a payu-recurring subscription, a PayPal one-off) lost the renewal. The subscription now changes only when a confirmed payment stores a new renew token. A payment with another variant also no longer marks the stored token verified, since it did not use it.
Payment.replace_renew_token (and the matching create_payment_object argument) lets a payment collect new payment details for an existing subscription. get_renew_token() and get_renew_data() return None for it, so the provider asks for a card (with 3-D Secure) instead of charging the stored token, and the token it receives is kept in Payment.new_renew_token and written to the subscription only when the payment is confirmed. Before, a member whose stored card kept failing could only retry that card, and a declined new card would have replaced the stored one, leaving the subscription without a usable token. Both columns are nullable, so code without them can still insert payments during a deploy.
2.4.0 (2026-10-08)
A failed payment cancels its order based on the stored order row, locked, and writes only status. It used the order in memory, which can be stale, and saved it whole, so an order completed meanwhile became CANCELED and lost its completion date.
A subscription with another payment provider is removed when the new payment is confirmed, not when it is created. create_payment_object deleted it as soon as a payment with another method was started, so a buyer who tried another method and gave up lost a working subscription.
Tested on Django 4.2-6.1 with Python 3.10-3.14, each Django version on the Python versions it supports (added Django 6.0/6.1 and Python 3.14). The tests run against the django-payments fork commit BlenderKit runs in production instead of the work-in-progress model-payu branch.
PLANS_PAYMENTS_COMPLETE_ORDER_AFTER_COMMIT (default False) completes the order after the confirmed payment commits instead of inside the same transaction. A failing complete_order() no longer rolls back the confirmation of a payment the provider has already captured (a PayPal checkout left the buyer charged with the payment still waiting).
2.3.2 (2026-09-15)
CreatePaymentView’s in-flight join and decline cooldown consider only payments of the requested variant. A checkout page that pre-creates a card payment for an embedded widget on page load no longer captures a click on another method (every “Pay with PayPal” click within the join window was redirected to the card payment), and a buyer whose card was just declined can switch to another method without waiting out the cooldown.
2.3.1 (2026-09-07)
Payment.set_renew_token on a plan-change order (an order without a pricing) stores only the token-related values on the existing RecurringUserPlan instead of re-arming it from the order, which left the renewal with pricing=None and the one-off difference as its amount.
2.3.0 (2026-08-19)
make CreatePaymentView idempotent: an attempt while a previous payment is in flight joins it instead of creating a twin, and an attempt right after a decline waits out a short cooldown (banks read rapid-fire retries as fraud). Configurable via PLANS_PAYMENTS_JOIN_IN_FLIGHT_SECONDS (default 180) and PLANS_PAYMENTS_DECLINE_COOLDOWN_SECONDS (default 60); 0 disables.
fix create_payment_object crashing with BillingInfo.DoesNotExist for users without saved billing info – billing fields fall back to empty strings.
fix renew_accounts charging automatic renewals for free plans: an armed RecurringUserPlan surviving a switch to a free plan took the customer’s money and extended nothing. The renewal is skipped with a warning instead.
2.2.0 (2026-07-23)
add Payment.invalidate_renew_token() - payment providers call it when the gateway reports the stored recurring token as permanently dead (e.g. PayU INVALID_TOKEN). The token is marked unverified so renewal tasks stop selecting the account and get_renew_token() returns None; the new plans_payments.signals.renew_token_invalidated signal lets host apps prompt the user to update their payment method.
add a pre-commit config (black/isort/flake8 at line length 119 plus hygiene hooks) and run it in CI, so local and CI lint cannot drift.
2.1.0 (2026-07-21)
Don’t disable token_verified on an unsuccessful renewal attempt, so a transient payment failure no longer permanently disarms automatic renewals
Add get_renew_data() and pass provider metadata (e.g. Stripe customer_id) through set_renew_token(**kwargs)
Add migration for the new django-payments PaymentStatus.CANCELLED (choices-only, database no-op)
Tests: drop the unused pytz dependency, satisfy current black
2.0.2 (2025-05-29)
fix the release
2.0.1 (2025-05-29)
fix the release
2.0.0 (2025-05-29)
Implement the wallet logic
Drop support for Python 3.8, Django 4.1
Add support for Django 5.2
don’t obscure Exceptions by catching them
1.5.0 (2025-02-28)
Drop support for Python 3.7
Add support for Python 3.13, Django 5.1
fix the wrong assumption that Payment.extra_data["response"] contains a "transactions" key
1.4.1 (2024-04-24)
do not check whether a confirmed payment of a completed order is left anymore
1.4.0 (2024-04-15)
migrate to RecurringUserPlan.renewal_triggered_by
add renewal_triggered_by parameter to Payment.set_renew_token
deprecate automatic_renewal parameter of Payment.set_renew_token; use renewal_triggered_by parameter instead
deprecate None value of renewal_triggered_by parameter of Payment.set_renew_token; set an AbstractRecurringUserPlan.RENEWAL_TRIGGERED_BY instead
1.3.1 (2024-04-15)
fix typo in payment description
1.3.0 (2024-04-12)
add optional returning orders when payments are refunded
1.2.2 (2023-12-20)
add change_reason for django-simple-history
1.2.1 (2023-12-19)
specify sender=Payment for change_payment_status receiver
1.2.0 (2023-10-16)
bugfix release (fix prevoius bad release)
1.1.3 (2023-10-15)
add some indexes to Payment model
1.1.2 (2023-03-29)
reword Payment description to ommit word “Subscribtion” which might raise warnings for banks/card providers
1.1.1 (2023-01-27)
correction release, include wheel update, correctly rebase to master
1.1.0 (2023-01-27)
Fix transaction fee double counting
1.0.1 (2022-12-09)
Fix migrations
1.0.0 (2022-12-08)
Recurring payments functionality
0.2.0 (2018-08-05)
Payment process without capturing should work
Automatic buttons generation
0.1.0 (2018-07-23)
First release on PyPI.
Metadata
Release files for django-plans-payments 2.5.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| django_plans_payments-2.5.0.tar.gz | 32.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| django_plans_payments-2.5.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 59.6 kB
Release files / django_plans_payments-2.5.0.tar.gz
| Download URL | django_plans_payments-2.5.0.tar.gz |
|---|---|
| Size | 32.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b15b0a7b6f9de97921dc45f97f56ef3c8fa039e3524e70057fc61f5b8f189e50
|
|
BLAKE2b-256 checksum How to use checksums |
aec00e34886089800e657a701d77b172c6da08c2bd82ff237ae276767e5ebfa2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.6
|
Release files / django_plans_payments-2.5.0-py3-none-any.whl
| Download URL | django_plans_payments-2.5.0-py3-none-any.whl |
|---|---|
| Size | 26.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
42e8b63a63581a9c45e8915a08eb1a96ef045e61804455563afaf3889218d1e9
|
|
BLAKE2b-256 checksum How to use checksums |
26f62c89bfacf75479ecd7ac3b7181f46c3ba6790a304f5b765279d4999db606
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.6
|