Skip to content
This repository has been archived by the owner on Sep 3, 2024. It is now read-only.

Commit

Permalink
Add support for upcoming invoices
Browse files Browse the repository at this point in the history
Also adds migrations (including missing migrations).
  • Loading branch information
lskillen committed Jun 30, 2016
1 parent e60d17b commit 537b507
Show file tree
Hide file tree
Showing 9 changed files with 631 additions and 40 deletions.
6 changes: 5 additions & 1 deletion djstripe/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,12 @@ class StripeIdField(StripeCharField):
"""A field with enough space to hold any stripe ID."""

def __init__(self, *args, **kwargs):
# As per: https://stripe.com/docs/upgrades
# You can safely assume object IDs we generate will never exceed 255
# characters, but you should be able to handle IDs of up to that
# length.
defaults = {
'max_length': 50,
'max_length': 255,
'blank': False,
'null': False,
}
Expand Down
33 changes: 33 additions & 0 deletions djstripe/migrations/0014_auto_20160625_1851.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.1 on 2016-06-25 17:51
from __future__ import unicode_literals

import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import djstripe.fields


class Migration(migrations.Migration):

dependencies = [
('djstripe', '0013_sync_cleanup'),
]

operations = [
migrations.AlterField(
model_name='charge',
name='receipt_sent',
field=models.BooleanField(default=False, help_text='Whether or not a receipt was sent for this charge.'),
),
migrations.AlterField(
model_name='charge',
name='source',
field=models.ForeignKey(help_text='The source used for this charge.', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='charges', to='djstripe.StripeSource'),
),
migrations.AlterField(
model_name='subscription',
name='application_fee_percent',
field=djstripe.fields.StripePercentField(decimal_places=2, help_text=b'A positive decimal that represents the fee percentage of the subscription invoice amount that will be transferred to the application owner\xe2\x80\x99s Stripe account each billing period.', max_digits=5, null=True, validators=[django.core.validators.MinValueValidator(1.0), django.core.validators.MaxValueValidator(100.0)]),
),
]
32 changes: 32 additions & 0 deletions djstripe/migrations/0015_upcoming_invoices.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.1 on 2016-06-25 17:51
from __future__ import unicode_literals

import django.core.validators
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('djstripe', '0014_auto_20160625_1851'),
]

operations = [
migrations.CreateModel(
name='UpcomingInvoice',
fields=[
('invoice_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='djstripe.Invoice')),
],
options={
'abstract': False,
},
bases=('djstripe.invoice',),
),
migrations.AlterField(
model_name='invoiceitem',
name='invoice',
field=models.ForeignKey(help_text='The invoice to which this invoiceitem is attached.', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='invoiceitems', to='djstripe.Invoice'),
),
]
86 changes: 86 additions & 0 deletions djstripe/migrations/0016_stripe_id_255_length.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.1 on 2016-06-25 22:40
from __future__ import unicode_literals

from django.db import migrations
import djstripe.fields


class Migration(migrations.Migration):

dependencies = [
('djstripe', '0015_upcoming_invoices'),
]

operations = [
migrations.AlterField(
model_name='account',
name='stripe_id',
field=djstripe.fields.StripeIdField(max_length=255, unique=True),
),
migrations.AlterField(
model_name='charge',
name='source_stripe_id',
field=djstripe.fields.StripeIdField(help_text=b'The payment source id.', max_length=255, null=True),
),
migrations.AlterField(
model_name='charge',
name='stripe_id',
field=djstripe.fields.StripeIdField(max_length=255, unique=True),
),
migrations.AlterField(
model_name='customer',
name='stripe_id',
field=djstripe.fields.StripeIdField(max_length=255, unique=True),
),
migrations.AlterField(
model_name='event',
name='stripe_id',
field=djstripe.fields.StripeIdField(max_length=255, unique=True),
),
migrations.AlterField(
model_name='invoice',
name='stripe_id',
field=djstripe.fields.StripeIdField(max_length=255, unique=True),
),
migrations.AlterField(
model_name='invoiceitem',
name='stripe_id',
field=djstripe.fields.StripeIdField(max_length=255, unique=True),
),
migrations.AlterField(
model_name='plan',
name='stripe_id',
field=djstripe.fields.StripeIdField(max_length=255, unique=True),
),
migrations.AlterField(
model_name='stripesource',
name='stripe_id',
field=djstripe.fields.StripeIdField(max_length=255, unique=True),
),
migrations.AlterField(
model_name='subscription',
name='stripe_id',
field=djstripe.fields.StripeIdField(max_length=255, unique=True),
),
migrations.AlterField(
model_name='transfer',
name='destination',
field=djstripe.fields.StripeIdField(help_text=b'ID of the bank account, card, or Stripe account the transfer was sent to.', max_length=255),
),
migrations.AlterField(
model_name='transfer',
name='destination_payment',
field=djstripe.fields.StripeIdField(help_text=b'If the destination is a Stripe account, this will be the ID of the payment that the destination account received for the transfer.', max_length=255, null=True),
),
migrations.AlterField(
model_name='transfer',
name='source_transaction',
field=djstripe.fields.StripeIdField(help_text=b'ID of the charge (or other transaction) that was used to fund the transfer. If null, the transfer was funded from the available balance.', max_length=255, null=True),
),
migrations.AlterField(
model_name='transfer',
name='stripe_id',
field=djstripe.fields.StripeIdField(max_length=255, unique=True),
),
]
84 changes: 81 additions & 3 deletions djstripe/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from django.utils import timezone
from django.utils.encoding import python_2_unicode_compatible, smart_text
from doc_inherit import class_doc_inherit
from mock_django.query import QuerySetMock
from model_utils.models import TimeStampedModel
from stripe.error import StripeError, InvalidRequestError

Expand Down Expand Up @@ -541,20 +542,94 @@ def _attach_objects_hook(self, cls, data):
if subscription:
self.subscription = subscription

self._attach_invoice_items(cls, data)

def _attach_invoice_items(self, cls, data):
if not self.pk:
# InvoiceItems need a saved invoice because they're associated via
# a RelatedManager. It might be better to make this pattern more
# generic with some sort-of _attach_objects_post_save_hook().
self.save()

cls._stripe_object_to_invoice_items(InvoiceItem, data, self)

@property
def plan(self):
""" Gets the associated plan for this invoice.
In order to provide a consistent view of invoices, it is necessary to
examine the invoice items for the associated subscription plan rather
than the top-level link. The reason for this is that the subscription
plan when requested by the customer, but the invoice item plan will
remain the same. This makes it difficult to work with an invoice
history if the plan is expected to remain consistent (e.g. for creating
downloadable invoices).
:returns: The associated plan for the invoice.
:rtype: ``djstripe.models.Plan``
"""
for item in self.invoiceitems.all():
if item.plan:
return item.plan

if self.subscription:
return self.subscription.plan


@class_doc_inherit
class UpcomingInvoice(Invoice):
__doc__ = getattr(Invoice, "__doc__")

def __init__(self, *args, **kwargs):
super(UpcomingInvoice, self).__init__(*args, **kwargs)
self._items = []

def _attach_invoice_items(self, cls, data):
self._items = cls._stripe_object_to_invoice_items(InvoiceItem, data, self)

@property
def invoiceitems(self):
""" Gets the invoice items associated with this upcoming invoice.
This differs from normal (non-upcoming) invoices, in that upcoming
invoices are in-memory and do not persist to the database. Therefore,
all of the data comes from the Stripe API itself.
Instead of returning a normal queryset for the invoiceitems, this will
return a mock of a queryset, but with the data fetched from Stripe - It
will act like a normal queryset, but mutation will silently fail.
"""
return QuerySetMock(InvoiceItem, *self._items)

@property
def stripe_id(self):
return None

@stripe_id.setter
def stripe_id(self, value):
return # noop

def save(self, *args, **kwargs):
return # noop


@class_doc_inherit
class InvoiceItem(StripeInvoiceItem):
__doc__ = getattr(StripeInvoiceItem, "__doc__")

# account = models.ForeignKey(Account, related_name="invoiceitems")
customer = models.ForeignKey(Customer, related_name="invoiceitems", help_text="The customer associated with this invoiceitem.")
invoice = models.ForeignKey(Invoice, related_name="invoiceitems", help_text="The invoice to which this invoiceitem is attached.")
invoice = models.ForeignKey(Invoice, null=True, related_name="invoiceitems", help_text="The invoice to which this invoiceitem is attached.")
plan = models.ForeignKey("Plan", null=True, related_name="invoiceitems", help_text="If the invoice item is a proration, the plan of the subscription for which the proration was computed.")
subscription = models.ForeignKey("Subscription", null=True, related_name="invoiceitems", help_text="The subscription that this invoice item has been created for, if any.")

def _attach_objects_hook(self, cls, data):
self.customer = cls._stripe_object_to_customer(target_cls=Customer, data=data)
self.invoice = cls._stripe_object_to_invoice(target_cls=Invoice, data=data)
customer = cls._stripe_object_to_customer(target_cls=Customer, data=data)

invoice = cls._stripe_object_to_invoice(target_cls=Invoice, data=data)
if invoice:
self.invoice = invoice
customer = customer or invoice.customer

plan = cls._stripe_object_to_plan(target_cls=Plan, data=data)
if plan:
Expand All @@ -563,6 +638,9 @@ def _attach_objects_hook(self, cls, data):
subscription = cls._stripe_object_to_subscription(target_cls=Subscription, data=data)
if subscription:
self.subscription = subscription
customer = customer or subscription.customer

self.customer = customer


@class_doc_inherit
Expand Down
Loading

0 comments on commit 537b507

Please sign in to comment.