Files @ 252697b842c0
Branch filter:

Location: symposion_app/vendor/registrasion/registrasion/models/commerce.py

Joel Addison
Update to Django 2.2

Upgrade site and modules to Django 2.2. Remove and replace obsolete
functionality with current equivalents. Update requirements to latest
versions where possible. Remove unused dependencies.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
from . import conditions
from . import inventory

from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models import F, Q, Sum
from django.utils import timezone
from django.utils.encoding import python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _
from model_utils.managers import InheritanceManager

User = get_user_model()


# Commerce Models

@python_2_unicode_compatible
class Cart(models.Model):
    ''' Represents a set of product items that have been purchased, or are
    pending purchase. '''

    class Meta:
        app_label = "registrasion"
        index_together = [
            ("status", "time_last_updated"),
            ("status", "user"),
        ]

    def __str__(self):
        return "%d rev #%d" % (self.id, self.revision)

    STATUS_ACTIVE = 1
    STATUS_PAID = 2
    STATUS_RELEASED = 3

    STATUS_TYPES = [
        (STATUS_ACTIVE, _("Active")),
        (STATUS_PAID, _("Paid")),
        (STATUS_RELEASED, _("Released")),
    ]

    user = models.ForeignKey(User, on_delete=models.CASCADE)
    # ProductItems (foreign key)
    vouchers = models.ManyToManyField(inventory.Voucher, blank=True)
    time_last_updated = models.DateTimeField(
        db_index=True,
    )
    reservation_duration = models.DurationField()
    revision = models.PositiveIntegerField(default=1)
    status = models.IntegerField(
        choices=STATUS_TYPES,
        db_index=True,
        default=STATUS_ACTIVE,
    )

    @classmethod
    def reserved_carts(cls):
        ''' Gets all carts that are 'reserved' '''
        return Cart.objects.filter(
            (Q(status=Cart.STATUS_ACTIVE) &
                Q(time_last_updated__gt=(
                    timezone.now()-F('reservation_duration')
                                        ))) |
            Q(status=Cart.STATUS_PAID)
        )


@python_2_unicode_compatible
class ProductItem(models.Model):
    ''' Represents a product-quantity pair in a Cart. '''

    class Meta:
        app_label = "registrasion"
        ordering = ("product", )

    def __str__(self):
        return "product: %s * %d in Cart: %s" % (
            self.product, self.quantity, self.cart)

    cart = models.ForeignKey(Cart, on_delete=models.CASCADE)
    product = models.ForeignKey(inventory.Product, on_delete=models.CASCADE)
    quantity = models.PositiveIntegerField(db_index=True)


@python_2_unicode_compatible
class DiscountItem(models.Model):
    ''' Represents a discount-product-quantity relation in a Cart. '''

    class Meta:
        app_label = "registrasion"
        ordering = ("product", )

    def __str__(self):
        return "%s: %s * %d in Cart: %s" % (
            self.discount, self.product, self.quantity, self.cart)

    cart = models.ForeignKey(Cart, on_delete=models.CASCADE)
    product = models.ForeignKey(inventory.Product, on_delete=models.CASCADE)
    discount = models.ForeignKey(conditions.DiscountBase,
            on_delete=models.CASCADE)
    quantity = models.PositiveIntegerField()


@python_2_unicode_compatible
class Invoice(models.Model):
    ''' An invoice. Invoices can be automatically generated when checking out
    a Cart, in which case, it is attached to a given revision of a Cart.

    Attributes:

        user (User): The owner of this invoice.

        cart (commerce.Cart): The cart that was used to generate this invoice.

        cart_revision (int): The value of ``cart.revision`` at the time of this
            invoice's creation. If a change is made to the underlying cart,
            this invoice is automatically void -- this change is detected
            when ``cart.revision != cart_revision``.

        status (int): One of ``STATUS_UNPAID``, ``STATUS_PAID``,
            ``STATUS_REFUNDED``, OR ``STATUS_VOID``. Call
            ``get_status_display`` for a human-readable representation.

        recipient (str): A rendered representation of the invoice's recipient.

        issue_time (datetime): When the invoice was issued.

        due_time (datetime): When the invoice is due.

        value (Decimal): The total value of the line items attached to the
            invoice.

        lineitem_set (Queryset[LineItem]): The set of line items that comprise
            this invoice.

        paymentbase_set(Queryset[PaymentBase]): The set of PaymentBase objects
            that have been applied to this invoice.

    '''

    class Meta:
        app_label = "registrasion"

    STATUS_UNPAID = 1
    STATUS_PAID = 2
    STATUS_REFUNDED = 3
    STATUS_VOID = 4

    STATUS_TYPES = [
        (STATUS_UNPAID, _("Unpaid")),
        (STATUS_PAID, _("Paid")),
        (STATUS_REFUNDED, _("Refunded")),
        (STATUS_VOID, _("VOID")),
    ]

    def __str__(self):
        return "Invoice #%d (to: %s, due: %s, value: %s)" % (
            self.id, self.user.email, self.due_time, self.value
        )

    def clean(self):
        if self.cart is not None and self.cart_revision is None:
            raise ValidationError(
                "If this is a cart invoice, it must have a revision")

    @property
    def is_unpaid(self):
        return self.status == self.STATUS_UNPAID

    @property
    def is_void(self):
        return self.status == self.STATUS_VOID

    @property
    def is_paid(self):
        return self.status == self.STATUS_PAID

    @property
    def is_refunded(self):
        return self.status == self.STATUS_REFUNDED

    def total_payments(self):
        ''' Returns the total amount paid towards this invoice. '''

        payments = PaymentBase.objects.filter(invoice=self)
        total_paid = payments.aggregate(Sum("amount"))["amount__sum"] or 0
        return total_paid

    def balance_due(self):
        ''' Returns the total balance remaining towards this invoice. '''
        return self.value - self.total_payments()

    # Invoice Number
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    cart = models.ForeignKey(Cart, null=True, on_delete=models.CASCADE)
    cart_revision = models.IntegerField(
        null=True,
        db_index=True,
    )
    # Line Items (foreign key)
    status = models.IntegerField(
        choices=STATUS_TYPES,
        db_index=True,
    )
    recipient = models.CharField(max_length=1024)
    issue_time = models.DateTimeField()
    due_time = models.DateTimeField()
    value = models.DecimalField(max_digits=8, decimal_places=2)


@python_2_unicode_compatible
class LineItem(models.Model):
    ''' Line items for an invoice. These are denormalised from the ProductItems
    and DiscountItems that belong to a cart (for consistency), but also allow
    for arbitrary line items when required.

    Attributes:

        invoice (commerce.Invoice): The invoice to which this LineItem is
            attached.

        description (str): A human-readable description of the line item.

        quantity (int): The quantity of items represented by this line.

        price (Decimal): The per-unit price for this line item.

        product (Optional[inventory.Product]): The product that this LineItem
            applies to. This allows you to do reports on sales and applied
            discounts to individual products.

    '''

    class Meta:
        app_label = "registrasion"
        ordering = ("id", )

    def __str__(self):
        return "Line: %s * %d @ %s" % (
            self.description, self.quantity, self.price)

    @property
    def total_price(self):
        ''' price * quantity '''
        return self.price * self.quantity

    invoice = models.ForeignKey(Invoice, on_delete=models.CASCADE)
    description = models.CharField(max_length=255)
    quantity = models.PositiveIntegerField()
    price = models.DecimalField(max_digits=8, decimal_places=2)
    product = models.ForeignKey(inventory.Product, null=True, blank=True,
            on_delete=models.CASCADE)


@python_2_unicode_compatible
class PaymentBase(models.Model):
    ''' The base payment type for invoices. Payment apps should subclass this
    class to handle implementation-specific issues.

    Attributes:
        invoice (commerce.Invoice): The invoice that this payment applies to.

        time (datetime): The time that this payment was generated. Note that
            this will default to the current time when the model is created.

        reference (str): A human-readable reference for the payment, this will
            be displayed alongside the invoice.

        amount (Decimal): The amount the payment is for.

    '''

    class Meta:
        ordering = ("time", )

    objects = InheritanceManager()

    def __str__(self):
        return "Payment: ref=%s amount=%s" % (self.reference, self.amount)

    invoice = models.ForeignKey(Invoice, on_delete=models.CASCADE)
    time = models.DateTimeField(default=timezone.now)
    reference = models.CharField(max_length=255)
    amount = models.DecimalField(max_digits=8, decimal_places=2)


class ManualPayment(PaymentBase):
    ''' Payments that are manually entered by staff. '''

    class Meta:
        app_label = "registrasion"

    entered_by = models.ForeignKey(User, on_delete=models.CASCADE)


class CreditNote(PaymentBase):
    ''' Credit notes represent money accounted for in the system that do not
    belong to specific invoices. They may be paid into other invoices, or
    cashed out as refunds.

    Each CreditNote may either be used to pay towards another Invoice in the
    system (by attaching a CreditNoteApplication), or may be marked as
    refunded (by attaching a CreditNoteRefund).'''

    class Meta:
        app_label = "registrasion"

    @classmethod
    def unclaimed(cls):
        return cls.objects.filter(
            creditnoteapplication=None,
            creditnoterefund=None,
        )

    @classmethod
    def refunded(cls):
        return cls.objects.exclude(creditnoterefund=None)

    @property
    def status(self):
        if self.is_unclaimed:
            return "Unclaimed"

        if hasattr(self, 'creditnoteapplication'):
            destination = self.creditnoteapplication.invoice.id
            return "Applied to invoice %d" % destination

        elif hasattr(self, 'creditnoterefund'):
            reference = self.creditnoterefund.reference
            return "Refunded with reference: %s" % reference

        raise ValueError("This should never happen.")

    @property
    def is_unclaimed(self):
        return not (
            hasattr(self, 'creditnoterefund') or
            hasattr(self, 'creditnoteapplication')
        )

    @property
    def value(self):
        ''' Returns the value of the credit note. Because CreditNotes are
        implemented as PaymentBase objects internally, the amount is a
        negative payment against an invoice. '''
        return -self.amount


class CleanOnSave(object):

    def save(self, *a, **k):
        self.full_clean()
        super(CleanOnSave, self).save(*a, **k)


class CreditNoteApplication(CleanOnSave, PaymentBase):
    ''' Represents an application of a credit note to an Invoice. '''

    class Meta:
        app_label = "registrasion"

    def clean(self):
        if not hasattr(self, "parent"):
            return
        if hasattr(self.parent, 'creditnoterefund'):
            raise ValidationError(
                "Cannot apply a refunded credit note to an invoice"
            )

    parent = models.OneToOneField(CreditNote, on_delete=models.CASCADE)


class CreditNoteRefund(CleanOnSave, models.Model):
    ''' Represents a refund of a credit note to an external payment.
    Credit notes may only be refunded in full. How those refunds are handled
    is left as an exercise to the payment app.

    Attributes:
        parent (commerce.CreditNote): The CreditNote that this refund
            corresponds to.

        time (datetime): The time that this refund was generated.

        reference (str): A human-readable reference for the refund, this should
            allow the user to identify the refund in their records.

    '''

    def clean(self):
        if not hasattr(self, "parent"):
            return
        if hasattr(self.parent, 'creditnoteapplication'):
            raise ValidationError(
                "Cannot refund a credit note that has been paid to an invoice"
            )

    parent = models.OneToOneField(CreditNote, on_delete=models.CASCADE)
    time = models.DateTimeField(default=timezone.now)
    reference = models.CharField(max_length=255)


class ManualCreditNoteRefund(CreditNoteRefund):
    ''' Credit notes that are entered by a staff member. '''

    class Meta:
        app_label = "registrasion"

    entered_by = models.ForeignKey(User, on_delete=models.CASCADE)