Skip to content

Commit

Permalink
Add missing LineItem Model (dj-stripe#1873)
Browse files Browse the repository at this point in the history
* Added LineItem enum for the LineItem model

* Added fixtures for the LineItem Model.

* Added LineItem model

* Exposed LineItem model on the admin

It was exposed both as a standalone model and as inline to
the SubscriptionAdmin.

* Updated changelog
  • Loading branch information
arnav13081994 authored Jan 26, 2023
1 parent 677977d commit cdd5283
Show file tree
Hide file tree
Showing 12 changed files with 645 additions and 6 deletions.
16 changes: 15 additions & 1 deletion djstripe/admin/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from .actions import CustomActionMixin
from .admin_inline import (
InvoiceItemInline,
LineItemInline,
SubscriptionInline,
SubscriptionItemInline,
SubscriptionScheduleInline,
Expand Down Expand Up @@ -412,6 +413,19 @@ def get_queryset(self, request):
)


@admin.register(models.LineItem)
class LineItemAdmin(StripeModelAdmin):
list_display = (
"amount",
"invoice_item",
"subscription",
"subscription_item",
"type",
)
list_filter = ("type", "invoice_item", "subscription", "subscription_item")
list_select_related = ("invoice_item", "subscription", "subscription_item")


@admin.register(models.Mandate)
class MandateAdmin(StripeModelAdmin):
list_display = ("status", "type", "payment_method")
Expand Down Expand Up @@ -588,7 +602,7 @@ class SubscriptionAdmin(StripeModelAdmin):
list_display = ("customer", "status", "get_product_name", "get_default_tax_rates")
list_filter = ("status", "cancel_at_period_end")

inlines = (SubscriptionItemInline, SubscriptionScheduleInline)
inlines = (SubscriptionItemInline, SubscriptionScheduleInline, LineItemInline)

def get_actions(self, request):
# get all actions
Expand Down
10 changes: 10 additions & 0 deletions djstripe/admin/admin_inline.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,13 @@ class InvoiceItemInline(admin.StackedInline):
readonly_fields = ("id", "created", "djstripe_owner_account")
raw_id_fields = get_forward_relation_fields_for_model(model)
show_change_link = True


class LineItemInline(admin.StackedInline):
"""A TabularInline for LineItem."""

model = models.LineItem
extra = 0
readonly_fields = ("id", "created", "djstripe_owner_account")
raw_id_fields = get_forward_relation_fields_for_model(model)
show_change_link = True
5 changes: 5 additions & 0 deletions djstripe/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,11 @@ class IntentStatus(Enum):
)


class LineItem(Enum):
invoiceitem = _("Invoice Item")
subscription = _("Subscription")


class MandateStatus(Enum):
active = _("Active")
inactive = _("Inactive")
Expand Down
145 changes: 145 additions & 0 deletions djstripe/migrations/0014_lineitem.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
# Generated by Django 3.2.16 on 2023-01-26 05:14

import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models

import djstripe.enums
import djstripe.fields


class Migration(migrations.Migration):
dependencies = [
("djstripe", "0013_product_default_price"),
]

operations = [
migrations.CreateModel(
name="LineItem",
fields=[
("djstripe_created", models.DateTimeField(auto_now_add=True)),
("djstripe_updated", models.DateTimeField(auto_now=True)),
(
"djstripe_id",
models.BigAutoField(
primary_key=True, serialize=False, verbose_name="ID"
),
),
("id", djstripe.fields.StripeIdField(max_length=255, unique=True)),
(
"livemode",
models.BooleanField(
blank=True,
default=None,
help_text="Null here indicates that the livemode status is unknown or was previously unrecorded. Otherwise, this field indicates whether this record comes from Stripe test mode or live mode operation.",
null=True,
),
),
("created", djstripe.fields.StripeDateTimeField(blank=True, null=True)),
("metadata", djstripe.fields.JSONField(blank=True, null=True)),
(
"description",
models.TextField(
blank=True, help_text="A description of this object.", null=True
),
),
("amount", djstripe.fields.StripeQuantumCurrencyAmountField()),
(
"amount_excluding_tax",
djstripe.fields.StripeQuantumCurrencyAmountField(),
),
("currency", djstripe.fields.StripeCurrencyCodeField(max_length=3)),
("discount_amounts", djstripe.fields.JSONField(blank=True, null=True)),
(
"discountable",
models.BooleanField(
default=False,
help_text="If True, discounts will apply to this line item. Always False for prorations.",
),
),
("discounts", djstripe.fields.JSONField(blank=True, null=True)),
("period", djstripe.fields.JSONField()),
("period_end", djstripe.fields.StripeDateTimeField()),
("period_start", djstripe.fields.StripeDateTimeField()),
("price", djstripe.fields.JSONField()),
(
"proration",
models.BooleanField(
default=False,
help_text="Whether or not the invoice item was created automatically as a proration adjustment when the customer switched plans.",
),
),
("proration_details", djstripe.fields.JSONField()),
("tax_amounts", djstripe.fields.JSONField(blank=True, null=True)),
("tax_rates", djstripe.fields.JSONField(blank=True, null=True)),
(
"type",
djstripe.fields.StripeEnumField(
enum=djstripe.enums.LineItem, max_length=12
),
),
(
"unit_amount_excluding_tax",
djstripe.fields.StripeDecimalCurrencyAmountField(
blank=True, decimal_places=2, max_digits=11, null=True
),
),
(
"quantity",
models.IntegerField(
blank=True,
help_text="The quantity of the subscription, if the line item is a subscription or a proration.",
null=True,
),
),
(
"djstripe_owner_account",
djstripe.fields.StripeForeignKey(
blank=True,
help_text="The Stripe Account this object belongs to.",
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="djstripe.account",
to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD,
),
),
(
"invoice_item",
djstripe.fields.StripeForeignKey(
blank=True,
help_text="The ID of the invoice item associated with this line item if any.",
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="djstripe.invoiceitem",
to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD,
),
),
(
"subscription",
djstripe.fields.StripeForeignKey(
blank=True,
help_text="The subscription that the invoice item pertains to, if any.",
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="djstripe.subscription",
to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD,
),
),
(
"subscription_item",
djstripe.fields.StripeForeignKey(
blank=True,
help_text="The subscription item that generated this invoice item. Left empty if the line item is not an explicit result of a subscription.",
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="djstripe.subscriptionitem",
to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD,
),
),
],
options={
"get_latest_by": "created",
"abstract": False,
},
),
]
2 changes: 2 additions & 0 deletions djstripe/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
Coupon,
Invoice,
InvoiceItem,
LineItem,
Plan,
ShippingRate,
Subscription,
Expand Down Expand Up @@ -74,6 +75,7 @@
"IdempotencyKey",
"Invoice",
"InvoiceItem",
"LineItem",
"Mandate",
"Order",
"PaymentIntent",
Expand Down
143 changes: 143 additions & 0 deletions djstripe/models/billing.py
Original file line number Diff line number Diff line change
Expand Up @@ -1027,6 +1027,149 @@ def api_retrieve(self, *args, **kwargs):
return super().api_retrieve(*args, **kwargs)


class LineItem(StripeModel):
"""
The individual line items that make up the invoice.
Stripe documentation: https://stripe.com/docs/api/invoices/line_item
"""

stripe_class = stripe.InvoiceLineItem
# todo uncomment when discount model gets implemented
# expand_fields = ["discounts"]

amount = StripeQuantumCurrencyAmountField(help_text="The amount, in cents.")
amount_excluding_tax = StripeQuantumCurrencyAmountField(
help_text="The integer amount in cents representing the amount for this line item, excluding all tax and discounts."
)
currency = StripeCurrencyCodeField()
discount_amounts = JSONField(
null=True,
blank=True,
help_text="The amount of discount calculated per discount for this line item.",
)
discountable = models.BooleanField(
default=False,
help_text="If True, discounts will apply to this line item. "
"Always False for prorations.",
)
discounts = JSONField(
null=True,
blank=True,
help_text="The discounts applied to the invoice line item. Line item discounts are applied before invoice discounts.",
)
invoice_item = StripeForeignKey(
"InvoiceItem",
null=True,
blank=True,
on_delete=models.CASCADE,
help_text="The ID of the invoice item associated with this line item if any.",
)
period = JSONField(
help_text="The period this line_item covers. For subscription line items, this is the subscription period. For prorations, this starts when the proration was calculated, and ends at the period end of the subscription. For invoice items, this is the time at which the invoice item was created or the period of the item."
)
period_end = StripeDateTimeField(
help_text="The end of the period, which must be greater than or equal to the start."
)
period_start = StripeDateTimeField(help_text="The start of the period.")
price = JSONField(
help_text="The price of the line item.",
)
proration = models.BooleanField(
default=False,
help_text="Whether or not the invoice item was created automatically as a "
"proration adjustment when the customer switched plans.",
)
proration_details = JSONField(
help_text="Additional details for proration line items"
)
subscription = StripeForeignKey(
"Subscription",
null=True,
blank=True,
on_delete=models.CASCADE,
help_text="The subscription that the invoice item pertains to, if any.",
)
subscription_item = StripeForeignKey(
"SubscriptionItem",
null=True,
blank=True,
on_delete=models.CASCADE,
help_text="The subscription item that generated this invoice item. Left empty if the line item is not an explicit result of a subscription.",
)
tax_amounts = JSONField(
null=True,
blank=True,
help_text="The amount of tax calculated per tax rate for this line item",
)
tax_rates = JSONField(
null=True, blank=True, help_text="The tax rates which apply to the line item."
)
type = StripeEnumField(enum=enums.LineItem)
unit_amount_excluding_tax = StripeDecimalCurrencyAmountField(
null=True,
blank=True,
help_text=(
"The amount in cents representing the unit amount for this line item, excluding all tax and discounts."
),
)
quantity = models.IntegerField(
null=True,
blank=True,
help_text="The quantity of the subscription, if the line item is a subscription or a proration.",
)

@classmethod
def _manipulate_stripe_object_hook(cls, data):
data["period_start"] = data["period"]["start"]
data["period_end"] = data["period"]["end"]

return data

# todo uncomment when discount model gets implemented
# def _attach_objects_post_save_hook(
# self,
# cls,
# data,
# api_key=djstripe_settings.STRIPE_SECRET_KEY,
# pending_relations=None,
# ):
# super()._attach_objects_post_save_hook(
# cls, data, api_key=api_key, pending_relations=pending_relations
# )
#
#
# # sync every discount
# for discount in self.discounts:
# Discount.sync_from_stripe_data(discount, api_key=api_key)

@classmethod
def api_list(cls, api_key=djstripe_settings.STRIPE_SECRET_KEY, **kwargs):
"""
Call the stripe API's list operation for this model.
Note that we only iterate and sync the LineItem associated with the
passed in Invoice.
Upcoming invoices are virtual and are not saved and hence their
line items are also not retrieved and synced
:param api_key: The api key to use for this request. \
Defaults to djstripe_settings.STRIPE_SECRET_KEY.
:type api_key: string
See Stripe documentation for accepted kwargs for each object.
:returns: an iterator over all items in the query
"""
# get current invoice if any
invoice_id = kwargs.pop("id")

invoice = Invoice.stripe_class.retrieve(invoice_id, api_key=api_key, **kwargs)

# iterate over all the line items on the current invoice
return invoice.lines.list(api_key=api_key, **kwargs).auto_paging_iter()


class Plan(StripeModel):
"""
A subscription plan contains the pricing information for different
Expand Down
2 changes: 1 addition & 1 deletion docs/history/2_8_0.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
- Python 3.11 is now supported.
- Django 4.1 is now supported.
- Python 3.7 is no longer supported. Python 3.8 or higher is required.
- Added `LineItem` model.
- New webhook signals are available:
- `djstripe.signals.webhook_pre_validate(instance, api_key)`: Fired before webhook validation
- `djstripe.signals.webhook_post_validate(instance, api_key, valid)`: Fired after validation (even unsuccessful validations)
Expand Down Expand Up @@ -42,6 +43,5 @@
## Other changes

- Updated `check_stripe_api_key` django system check to not be a blocker for new dj-stripe users by raising Info warnings on the console. If the Stripe keys were not defined in the settings file, the `Critical` warning was preventing users to add them directly from the admin as mentioned in the docs. This was creating a chicken-egg situation where one could only add keys in the admin before they were defined in settings.

- `check_stripe_api_key` will raise appropriate warnings on the console directing users to add keys directly from the django admin.
- Swapped Critical Error to Info for `_check_webhook_endpoint_validation` check to allow the users to use the django admin.
Loading

0 comments on commit cdd5283

Please sign in to comment.