예제 #1
0
class Order(models.Model):
    """
    Order in this app supports only one item per order. This item is defined by
    plan and pricing attributes. If both are defined the order represents buying
    an account extension.

    If only plan is provided (with pricing set to None) this means that user purchased
    a plan upgrade.
    """
    STATUS = Enumeration([
        (1, 'NEW', pgettext_lazy('Order status', 'new')),
        (2, 'COMPLETED', pgettext_lazy('Order status', 'completed')),
        (3, 'NOT_VALID', pgettext_lazy('Order status', 'not valid')),
        (4, 'CANCELED', pgettext_lazy('Order status', 'canceled')),
        (5, 'RETURNED', pgettext_lazy('Order status', 'returned')),
    ])

    user = models.ForeignKey(settings.AUTH_USER_MODEL,
                             verbose_name=_('user'),
                             on_delete=models.CASCADE)
    flat_name = models.CharField(max_length=200, blank=True, null=True)
    plan = models.ForeignKey('Plan',
                             verbose_name=_('plan'),
                             related_name="plan_order",
                             on_delete=models.CASCADE)
    pricing = models.ForeignKey(
        'Pricing',
        blank=True,
        null=True,
        verbose_name=_('pricing'),
        on_delete=models.CASCADE
    )  # if pricing is None the order is upgrade plan, not buy new pricing
    created = models.DateTimeField(_('created'), db_index=True)
    completed = models.DateTimeField(_('completed'),
                                     null=True,
                                     blank=True,
                                     db_index=True)
    amount = models.DecimalField(_('amount'),
                                 max_digits=7,
                                 decimal_places=2,
                                 db_index=True)
    tax = models.DecimalField(
        _('tax'),
        max_digits=4,
        decimal_places=2,
        db_index=True,
        null=True,
        blank=True)  # Tax=None is when tax is not applicable
    currency = models.CharField(_('currency'), max_length=3, default='EUR')
    status = models.IntegerField(_('status'),
                                 choices=STATUS,
                                 default=STATUS.NEW)

    def save(self,
             force_insert=False,
             force_update=False,
             using=None,
             update_fields=None):
        if self.created is None:
            self.created = now()
        return super(Order, self).save(force_insert, force_update, using)

    def __str__(self):
        return _("Order #%(id)d") % {'id': self.id}

    @property
    def name(self):
        """
        Support for two kind of Order names:
        * (preferred) dynamically generated from Plan and Pricing (if flatname is not provided) (translatable)
        * (legacy) just return flat name, which is any text (not translatable)

        Flat names are only introduced for legacy system support, when you need to migrate old orders into
        django-plans and you cannot match Plan&Pricings convention.
        """
        if self.flat_name:
            return self.flat_name
        else:
            return "%s %s %s " % (_('Plan'), self.plan.name,
                                  "(upgrade)" if self.pricing is None else
                                  '- %s' % self.pricing)

    def is_ready_for_payment(self):
        return self.status == self.STATUS.NEW and (
            now() - self.created).days < getattr(settings,
                                                 'PLANS_ORDER_EXPIRATION', 14)

    def complete_order(self):
        if self.completed is None:
            status = self.user.userplan.extend_account(self.plan, self.pricing)
            self.completed = now()
            if status:
                self.status = Order.STATUS.COMPLETED
            else:
                self.status = Order.STATUS.NOT_VALID
            self.save()
            order_completed.send(self)
            return True
        else:
            return False

    def get_invoices_proforma(self):
        return Invoice.proforma.filter(order=self)

    def get_invoices(self):
        return Invoice.invoices.filter(order=self)

    def get_all_invoices(self):
        return self.invoice_set.order_by('issued', 'issued_duplicate', 'pk')

    def tax_total(self):
        if self.tax is None:
            return Decimal('0.00')
        else:
            return self.total() - self.amount

    def total(self):
        if self.tax is not None:
            return (self.amount * (self.tax + 100) / 100).quantize(
                Decimal('1.00'))
        else:
            return self.amount

    def get_absolute_url(self):
        return reverse('order', kwargs={'pk': self.pk})

    class Meta:
        ordering = ('-created', )
        verbose_name = _("Order")
        verbose_name_plural = _("Orders")
예제 #2
0
class Invoice(models.Model):
    """
    Single invoice document.
    """

    INVOICE_TYPES = Enumeration([
        (1, 'INVOICE', _('Invoice')),
        (2, 'DUPLICATE', _('Invoice Duplicate')),
        (3, 'PROFORMA', pgettext_lazy('proforma', 'Order confirmation')),
    ])

    objects = models.Manager()
    invoices = InvoiceManager()
    proforma = InvoiceProformaManager()
    duplicates = InvoiceDuplicateManager()

    class NUMBERING:
        """Used as a choices for settings.PLANS_INVOICE_COUNTER_RESET"""

        DAILY = 1
        MONTHLY = 2
        ANNUALLY = 3

    user = models.ForeignKey(settings.AUTH_USER_MODEL,
                             verbose_name=_('user'),
                             on_delete=models.CASCADE)
    order = models.ForeignKey('Order',
                              verbose_name=_('order'),
                              on_delete=models.CASCADE)
    number = models.IntegerField(db_index=True)
    full_number = models.CharField(max_length=200)
    type = models.IntegerField(choices=INVOICE_TYPES,
                               default=INVOICE_TYPES.INVOICE,
                               db_index=True)
    issued = models.DateField(db_index=True)
    issued_duplicate = models.DateField(db_index=True, null=True, blank=True)
    selling_date = models.DateField(db_index=True, null=True, blank=True)
    payment_date = models.DateField(db_index=True)
    unit_price_net = models.DecimalField(max_digits=7, decimal_places=2)
    quantity = models.IntegerField(default=1)
    total_net = models.DecimalField(max_digits=7, decimal_places=2)
    total = models.DecimalField(max_digits=7, decimal_places=2)
    tax_total = models.DecimalField(max_digits=7, decimal_places=2)
    tax = models.DecimalField(
        max_digits=4, decimal_places=2, db_index=True, null=True,
        blank=True)  # Tax=None is whet tax is not applicable
    rebate = models.DecimalField(max_digits=4,
                                 decimal_places=2,
                                 default=Decimal(0))
    currency = models.CharField(max_length=3, default='EUR')
    item_description = models.CharField(max_length=200)
    buyer_name = models.CharField(max_length=200, verbose_name=_("Name"))
    buyer_street = models.CharField(max_length=200, verbose_name=_("Street"))
    buyer_zipcode = models.CharField(max_length=200,
                                     verbose_name=_("Zip code"))
    buyer_city = models.CharField(max_length=200, verbose_name=_("City"))
    buyer_country = CountryField(verbose_name=_("Country"), default='PL')
    buyer_tax_number = models.CharField(max_length=200,
                                        blank=True,
                                        verbose_name=_("TAX/VAT number"))
    shipping_name = models.CharField(max_length=200, verbose_name=_("Name"))
    shipping_street = models.CharField(max_length=200,
                                       verbose_name=_("Street"))
    shipping_zipcode = models.CharField(max_length=200,
                                        verbose_name=_("Zip code"))
    shipping_city = models.CharField(max_length=200, verbose_name=_("City"))
    shipping_country = CountryField(verbose_name=_("Country"), default='PL')
    require_shipment = models.BooleanField(default=False, db_index=True)
    issuer_name = models.CharField(max_length=200, verbose_name=_("Name"))
    issuer_street = models.CharField(max_length=200, verbose_name=_("Street"))
    issuer_zipcode = models.CharField(max_length=200,
                                      verbose_name=_("Zip code"))
    issuer_city = models.CharField(max_length=200, verbose_name=_("City"))
    issuer_country = CountryField(verbose_name=_("Country"), default='PL')
    issuer_tax_number = models.CharField(max_length=200,
                                         blank=True,
                                         verbose_name=_("TAX/VAT number"))

    class Meta:
        verbose_name = _("Invoice")
        verbose_name_plural = _("Invoices")

    def __str__(self):
        return self.full_number

    def get_absolute_url(self):
        return reverse('invoice_preview_html', kwargs={'pk': self.pk})

    def clean(self):
        if self.number is None:
            invoice_counter_reset = getattr(settings,
                                            'PLANS_INVOICE_COUNTER_RESET',
                                            Invoice.NUMBERING.MONTHLY)

            if invoice_counter_reset == Invoice.NUMBERING.DAILY:
                last_number = Invoice.objects.filter(
                    issued=self.issued, type=self.type).aggregate(
                        Max('number'))['number__max'] or 0
            elif invoice_counter_reset == Invoice.NUMBERING.MONTHLY:
                last_number = Invoice.objects.filter(
                    issued__year=self.issued.year,
                    issued__month=self.issued.month,
                    type=self.type).aggregate(
                        Max('number'))['number__max'] or 0
            elif invoice_counter_reset == Invoice.NUMBERING.ANNUALLY:
                last_number = \
                    Invoice.objects.filter(issued__year=self.issued.year, type=self.type).aggregate(Max('number'))[
                        'number__max'] or 0
            else:
                raise ImproperlyConfigured(
                    "PLANS_INVOICE_COUNTER_RESET can be set only to these values: daily, monthly, yearly."
                )
            self.number = last_number + 1

        if self.full_number == "":
            self.full_number = self.get_full_number()
        super(Invoice, self).clean()

    #    def validate_unique(self, exclude=None):
    #        super(Invoice, self).validate_unique(exclude)
    #        if self.type == Invoice.INVOICE_TYPES.INVOICE:
    #            if Invoice.objects.filter(order=self.order).count():
    #                raise ValidationError("Duplicate invoice for order")
    #        if self.type in (Invoice.INVOICE_TYPES.INVOICE, Invoice.INVOICE_TYPES.PROFORMA):
    #            pass

    def get_full_number(self):
        """
        Generates on the fly invoice full number from template provided by ``settings.PLANS_INVOICE_NUMBER_FORMAT``.
        ``Invoice`` object is provided as ``invoice`` variable to the template, therefore all object fields
        can be used to generate full number format.

        .. warning::

            This is only used to prepopulate ``full_number`` field on saving new invoice.
            To get invoice full number always use ``full_number`` field.

        :return: string (generated full number)
        """
        format = getattr(
            settings, "PLANS_INVOICE_NUMBER_FORMAT",
            "{{ invoice.number }}/{% ifequal invoice.type invoice.INVOICE_TYPES.PROFORMA %}PF{% else %}FV{% endifequal %}/{{ invoice.issued|date:'m/Y' }}"
        )
        return Template(format).render(Context({'invoice': self}))

    def set_issuer_invoice_data(self):
        """
        Fills models object with issuer data copied from ``settings.PLANS_INVOICE_ISSUER``

        :raise: ImproperlyConfigured
        """
        try:
            issuer = getattr(settings, 'PLANS_INVOICE_ISSUER')
        except:
            raise ImproperlyConfigured(
                "Please set PLANS_INVOICE_ISSUER in order to make an invoice.")
        self.issuer_name = issuer['issuer_name']
        self.issuer_street = issuer['issuer_street']
        self.issuer_zipcode = issuer['issuer_zipcode']
        self.issuer_city = issuer['issuer_city']
        self.issuer_country = issuer['issuer_country']
        self.issuer_tax_number = issuer['issuer_tax_number']

    def set_buyer_invoice_data(self, billing_info):
        """
        Fill buyer invoice billing and shipping data by copy them from provided user's ``BillingInfo`` object.

        :param billing_info: BillingInfo object
        :type billing_info: BillingInfo
        """
        self.buyer_name = billing_info.name
        self.buyer_street = billing_info.street
        self.buyer_zipcode = billing_info.zipcode
        self.buyer_city = billing_info.city
        self.buyer_country = billing_info.country
        self.buyer_tax_number = billing_info.tax_number

        self.shipping_name = billing_info.shipping_name or billing_info.name
        self.shipping_street = billing_info.shipping_street or billing_info.street
        self.shipping_zipcode = billing_info.shipping_zipcode or billing_info.zipcode
        self.shipping_city = billing_info.shipping_city or billing_info.city
        # TODO: Should allow shipping to other country? Not think so
        self.shipping_country = billing_info.country

    def copy_from_order(self, order):
        """
        Filling orders details likes totals, taxes, etc and linking provided ``Order`` object with an invoice

        :param order: Order object
        :type order: Order
        """
        self.order = order
        self.user = order.user
        self.unit_price_net = order.amount
        self.total_net = order.amount
        self.total = order.total()
        self.tax_total = order.total() - order.amount
        self.tax = order.tax
        self.currency = order.currency
        if Site is not None:
            self.item_description = "%s - %s" % (
                Site.objects.get_current().name, order.name)
        else:
            self.item_description = order.name

    @classmethod
    def create(cls, order, invoice_type):
        language_code = get_user_language(order.user)

        if language_code is not None:
            translation.activate(language_code)

        try:
            billing_info = BillingInfo.objects.get(user=order.user)
        except BillingInfo.DoesNotExist:
            return

        day = date.today()
        pday = order.completed
        if invoice_type == Invoice.INVOICE_TYPES['PROFORMA']:
            pday = day + timedelta(days=14)

        invoice = cls(
            issued=day, selling_date=order.completed, payment_date=pday
        )  # FIXME: 14 - this should set accordingly to ORDER_TIMEOUT in days
        invoice.type = invoice_type
        invoice.copy_from_order(order)
        invoice.set_issuer_invoice_data()
        invoice.set_buyer_invoice_data(billing_info)
        invoice.clean()
        invoice.save()
        if language_code is not None:
            translation.deactivate()

    def send_invoice_by_email(self):
        language_code = get_user_language(self.user)

        if language_code is not None:
            translation.activate(language_code)
        mail_context = {
            'user': self.user,
            'invoice_type': self.get_type_display(),
            'invoice_number': self.get_full_number(),
            'order': self.order.id,
            'url': self.get_absolute_url(),
        }
        if language_code is not None:
            translation.deactivate()
        send_template_email([self.user.email],
                            'mail/invoice_created_title.txt',
                            'mail/invoice_created_body.txt', mail_context,
                            language_code)

    def is_UE_customer(self):
        return EUTaxationPolicy.is_in_EU(self.buyer_country.code)
예제 #3
0
class Order(models.Model):


    STATUS=Enumeration([
        (1, 'NEW', pgettext_lazy(u'Order status', u'new')),
        (2, 'COMPLETED', pgettext_lazy(u'Order status', u'completed')),
        (3, 'NOT_VALID', pgettext_lazy(u'Order status', u'not valid')),
        (4, 'CANCELED', pgettext_lazy(u'Order status', u'canceled')),
        (5, 'RETURNED', pgettext_lazy(u'Order status', u'returned')),

    ])

    user = models.ForeignKey('auth.User', verbose_name=_('user'))
    plan = models.ForeignKey('plan', verbose_name=_('plan'), related_name="plan_order")
    pricing = models.ForeignKey('pricing', blank=True, null=True, verbose_name=_('pricing')) #if pricing is None the order is upgrade plan, not buy new pricing
    created = models.DateTimeField(_('created'), auto_now_add=True, db_index=True)
    completed = models.DateTimeField(_('completed'), null=True, blank=True, db_index=True)
    amount = models.DecimalField(_('amount'), max_digits=7, decimal_places=2, db_index=True)
    tax = models.DecimalField(_('tax'), max_digits=4, decimal_places=2, db_index=True, null=True,
        blank=True) # Tax=None is when tax is not applicable
    currency = models.CharField(_('currency'), max_length=3, default='EUR')
    status = models.IntegerField(_('status'), choices=STATUS, default=STATUS.NEW)

    def is_ready_for_payment(self):
        return self.status == self.STATUS.NEW and (datetime.utcnow().replace(tzinfo=utc) - self.created).days < getattr(settings, 'ORDER_EXPIRATION', 14)

    def complete_order(self):
        if self.completed is  None:
            status = self.user.userplan.extend_account(self.plan, self.pricing)
            self.completed = datetime.utcnow().replace(tzinfo=utc)
            if status:
                self.status = Order.STATUS.COMPLETED
            else:
                self.status = Order.STATUS.NOT_VALID
            self.save()
            order_completed.send(self)
            return True
        else:
            return False


    def get_invoices_proforma(self):
        return Invoice.proforma.filter(order=self)

    def get_invoices(self):
        return Invoice.invoices.filter(order=self)

    def get_all_invoices(self):
        return self.invoice_set.order_by('issued', 'issued_duplicate', 'pk')

    def total(self):
        if self.tax is not None:
            return (self.amount * (self.tax + 100) / 100).quantize(Decimal('1.00'))
        else:
            return self.amount


    def get_absolute_url(self):
        return reverse('order', kwargs={'pk':self.pk})

    class Meta:
        ordering = ('-created', )