diff --git a/core/mixins/billable.py b/core/mixins/billable.py new file mode 100644 index 0000000..fb0ba58 --- /dev/null +++ b/core/mixins/billable.py @@ -0,0 +1,39 @@ +from django.db.models import PositiveIntegerField, DateField, IntegerChoices + +class ActionChoices(IntegerChoices): + WAIT = 0, "Do not invoice for now" + NEXT = 1, "Add to client's next invoice" + CRON = 2, "Invoice at next cron run" + DATE = 3, "Invoice at date" + + +class CycleChoices(IntegerChoices): + DAYS = 0, "Days" + WEEKS = 1, "Weeks" + MONTHS = 2, "Months" + YEARS = 3, "Years" + +class RecurMixin: + date = DateField() + recur_action = PositiveIntegerField(choices=ActionChoices.choices) + recur_cycle = PositiveIntegerField(choices=CycleChoices.choices) + recur_count = PositiveIntegerField() + + def next_invoicing(self): + from core.models.invoices import InvoiceItem, Invoice + + if not self.recur_action == ActionChoices.DATE: + return False + + try: + invoiceitems = InvoiceItem.objects.filter(billable=self) + invoice = Invoice.objects.filter(invoiceitem_set__contains=invoiceitems).latest("created") + delta = relativedelta(days=self.recur_count if self.recur_cycle == CycleChoices.DAYS else 0, + weeks=self.recur_count if self.recur_cycle == CycleChoices.WEEKS else 0, + months=self.recur_count if self.recur_cycle == CycleChoices.MONTHS else 0, + years=self.recur_count if self.recur_cycle == CycleChoices.YEARS else 0) + + return invoice.created + delta + + except: + return self.date \ No newline at end of file diff --git a/core/models/__init__.py b/core/models/__init__.py index 21d6287..a6e9a43 100644 --- a/core/models/__init__.py +++ b/core/models/__init__.py @@ -4,7 +4,7 @@ from core.models.auth import LoginSession, PWResetToken, LoginLog, IPLimit from core.models.local import Currency, TaxPolicy, TaxRule from core.models.cron import CronLog from core.models.products import ProductGroup, Product, ProductPlan, ProductPlanItem -from core.models.billable import CycleChoices, Billable -from core.models.services import Service, ServicePlan, ServicePlanItem +from core.models.billable import BaseBillable +from core.models.services import Service, ServicePlan from core.models.invoices import Invoice, InvoiceItem from core.models.api import APIKey \ No newline at end of file diff --git a/core/models/billable.py b/core/models/billable.py index 0b57192..5b3c9a0 100644 --- a/core/models/billable.py +++ b/core/models/billable.py @@ -1,4 +1,4 @@ -from django.db.models import IntegerChoices, Model, ForeignKey, CASCADE, TextField, PositiveIntegerField, BooleanField, DateField +from django.db.models import Model, ForeignKey, CASCADE, TextField, PositiveIntegerField, BooleanField, DateField from django.utils import timezone from core.models.profiles import ClientProfile @@ -6,52 +6,28 @@ from core.models.brands import Brand from core.fields.base import LongCharField from core.fields.numbers import CostField from core.models.local import Currency +from core.mixins.billable import RecurMixin from dateutil.relativedelta import relativedelta from polymorphic.models import PolymorphicModel -class ActionChoices(IntegerChoices): - WAIT = 0, "Do not invoice for now" - NEXT = 1, "Add to client's next invoice" - CRON = 2, "Invoice at next cron run" - DATE = 3, "Invoice at date" - - -class CycleChoices(IntegerChoices): - DAYS = 0, "Days" - WEEKS = 1, "Weeks" - MONTHS = 2, "Months" - YEARS = 3, "Years" - -class Billable(Model): - client = ForeignKey(ClientProfile, on_delete=CASCADE) - brand = ForeignKey(Brand, on_delete=CASCADE) +class BaseBillable(PolymorphicModel): name = LongCharField() - description = TextField() - individual_cost = CostField() - amount = PositiveIntegerField() + description = TextField(blank=True, null=True) + amount = CostField() + count = PositiveIntegerField(default=1) currency = ForeignKey(Currency, on_delete=CASCADE) taxable = BooleanField() - action = PositiveIntegerField(choices=ActionChoices.choices) - date = DateField(null=True) - recur_cycle = PositiveIntegerField(choices=CycleChoices.choices, null=True) - recur_count = PositiveIntegerField(null=True) + client = ForeignKey(ClientProfile, on_delete=CASCADE) + brand = ForeignKey(Brand, on_delete=CASCADE) + @property def next_invoicing(self): - from core.models.invoices import InvoiceItem, Invoice + raise NotImplementedError(f"{type(self)} does not implement property next_invoicing!") - if not self.recur_cycle == ActionChoices.DATE: - return False + @property + def can_invoice(self): + raise NotImplementedError(f"{type(self)} does not implement property can_invoice!") - try: - invoiceitems = InvoiceItem.objects.filter(billable=self) - invoice = Invoice.objects.filter(invoiceitem_set__contains=invoiceitems).latest("created") - delta = relativedelta(days=self.recur_count if self.recur_cycle == CycleChoices.DAYS else 0, - weeks=self.recur_count if self.recur_cycle == CycleChoices.WEEKS else 0, - months=self.recur_count if self.recur_cycle == CycleChoices.MONTHS else 0, - years=self.recur_count if self.recur_cycle == CycleChoices.YEARS else 0) - - return invoice.created + delta - - except: - return self.date +class Billable(RecurMixin, BaseBillable): + pass diff --git a/core/models/invoices.py b/core/models/invoices.py index 0fd46b7..c82bb6d 100644 --- a/core/models/invoices.py +++ b/core/models/invoices.py @@ -5,6 +5,7 @@ from core.fields.numbers import CostField from core.models.profiles import ClientProfile from core.models.local import Currency from core.models.brands import Brand +from core.models.billable import BaseBillable class Invoice(Model): client = ForeignKey(ClientProfile, on_delete=PROTECT) @@ -16,9 +17,6 @@ class Invoice(Model): currency = ForeignKey(Currency, on_delete=PROTECT) class InvoiceItem(Model): - from core.models.services import Service - from core.models.billable import Billable - invoice = ForeignKey(Invoice, on_delete=CASCADE) sort = PositiveIntegerField() name = LongCharField() @@ -26,5 +24,4 @@ class InvoiceItem(Model): price = CostField() discount = CostField() taxable = BooleanField() - service = ForeignKey(Service, on_delete=SET_NULL, null=True) - billable = ForeignKey(Billable, on_delete=SET_NULL, null=True) \ No newline at end of file + billable = ForeignKey(BaseBillable, on_delete=SET_NULL, null=True) \ No newline at end of file diff --git a/core/models/products.py b/core/models/products.py index 634d276..3fca4e7 100644 --- a/core/models/products.py +++ b/core/models/products.py @@ -38,7 +38,7 @@ class ProductPlan(Model): currency = ForeignKey(Currency, on_delete=CASCADE) class ProductPlanItem(Model): - from core.models.billable import CycleChoices + from core.mixins.billable import CycleChoices plan = ForeignKey(ProductPlan, on_delete=CASCADE) cycle = PositiveIntegerField(choices=CycleChoices.choices) diff --git a/core/models/profiles.py b/core/models/profiles.py index 3dcbbb6..1c0e3b0 100644 --- a/core/models/profiles.py +++ b/core/models/profiles.py @@ -5,6 +5,7 @@ from django_countries.fields import CountryField from django.db.models import OneToOneField, CASCADE, ImageField, Model, ForeignKey, SET_DEFAULT, ManyToManyField, DateTimeField, TextField from django.contrib.auth import get_user_model +from django.utils.safestring import mark_safe from core.helpers.files import generate_storage_filename from core.models.local import Currency diff --git a/core/models/services.py b/core/models/services.py index df18286..ca7c8db 100644 --- a/core/models/services.py +++ b/core/models/services.py @@ -1,6 +1,7 @@ from core.fields.numbers import CostField from core.models.local import Currency from core.models.profiles import ClientProfile +from core.models.billable import BaseBillable from django.db.models import Model, TextField, CharField, ManyToManyField, ForeignKey, CASCADE, PositiveIntegerField, OneToOneField, BooleanField, SET_NULL @@ -37,10 +38,5 @@ class ServicePlan(Model): for item in productplan.serviceplanitem_set: ServicePlanItem.objects.create(plan=plan, cycle=item.cycle, count=item.count, cost=item.cost) -class ServicePlanItem(Model): - from core.models.billable import CycleChoices - +class ServicePlanItem(BaseBillable): plan = ForeignKey(ServicePlan, on_delete=CASCADE) - cycle = PositiveIntegerField(choices=CycleChoices.choices) - count = PositiveIntegerField() - cost = CostField() diff --git a/core/widgets/color.py b/core/widgets/color.py index 08ce8e0..d5bf29c 100644 --- a/core/widgets/color.py +++ b/core/widgets/color.py @@ -1,6 +1,4 @@ from django.forms import TextInput -from django.conf import settings -from django.utils.safestring import mark_safe class ColorPickerWidget(TextInput): def __init__(self, attrs={}): diff --git a/templates/backend/base.html b/templates/backend/base.html index 01db811..7d7d683 100644 --- a/templates/backend/base.html +++ b/templates/backend/base.html @@ -2,6 +2,7 @@ {% load navigation %} {% load dbsetting %} {% load permissions %} +{% load bootstrap4 %} {% dbsetting "core.title" as sitetitle %}