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")
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)
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', )