From 1bc81e71265c4b8249f6ea0d4b163ec2de832b9f Mon Sep 17 00:00:00 2001 From: Klaus-Uwe Mitterer Date: Fri, 5 Jun 2020 09:38:55 +0200 Subject: [PATCH] Start implementation of automatic invoice generation --- core/helpers/billable.py | 21 +++++++++++++++ core/models/__init__.py | 2 +- core/models/billable.py | 9 ++++--- core/models/products.py | 11 +++++--- core/models/services.py | 58 ++++++++++++++++++++++++---------------- 5 files changed, 70 insertions(+), 31 deletions(-) create mode 100644 core/helpers/billable.py diff --git a/core/helpers/billable.py b/core/helpers/billable.py new file mode 100644 index 0000000..dc159ea --- /dev/null +++ b/core/helpers/billable.py @@ -0,0 +1,21 @@ +from core.models.billable import Billable, ActionChoices +from core.models.services import Service + +from django.utils import timezone + +def generate_invoices(): + todo = {} + + for billable in Billable.objects.filter(recur_cycle=ActionChoices.DATE): + date = billable.next_invoicing() + if date and date <= timezone.now(): + if not billable.brand in todo.keys(): + todo[billable.brand] = {} + if not billable.currency in todo[billable.brand].keys(): + todo[billable.brand][billable.currency] = {} + if not billable.client in todo[billable.brand][billable.currency].keys(): + todo[billable.brand][billable.currency][billable.client] = {"billable": []} + todo[billable.brand][billable.currency][billable.client]["billable"].append(billable) + + for service in Service.objects.all(): + pass \ No newline at end of file diff --git a/core/models/__init__.py b/core/models/__init__.py index 21d6287..6399208 100644 --- a/core/models/__init__.py +++ b/core/models/__init__.py @@ -5,6 +5,6 @@ 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.services import Service 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 8c30e19..60e2178 100644 --- a/core/models/billable.py +++ b/core/models/billable.py @@ -5,6 +5,7 @@ from core.models.profiles import ClientProfile 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 dateutil.relativedelta import relativedelta @@ -30,6 +31,7 @@ class Billable(Model): description = TextField() individual_cost = CostField() amount = PositiveIntegerField() + currency = ForeignKey(Currency, on_delete=CASCADE) taxable = BooleanField() action = PositiveIntegerField(choices=ActionChoices.choices) date = DateField(null=True) @@ -39,12 +41,11 @@ class Billable(Model): def next_invoicing(self): from core.models.invoices import InvoiceItem, Invoice + if not self.recur_cycle == ActionChoices.DATE: + return False + try: invoiceitems = InvoiceItem.objects.filter(billable=self) - - if not recur_cycle: - return False - 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, diff --git a/core/models/products.py b/core/models/products.py index 1515c1e..71ca9ec 100644 --- a/core/models/products.py +++ b/core/models/products.py @@ -1,8 +1,10 @@ from core.fields.base import LongCharField from core.fields.color import ColorField from core.fields.numbers import CostField +from core.models.local import Currency +from core.models.brands import Brand -from django.db.models import Model, PositiveIntegerField, ForeignKey, CASCADE, CharField, TextField, ManyToManyField +from django.db.models import Model, PositiveIntegerField, ForeignKey, CASCADE, CharField, TextField, ManyToManyField, BooleanField from importlib import import_module @@ -19,19 +21,21 @@ class Product(Model): description = TextField(null=True, blank=True) product_type = LongCharField(null=True, blank=True) product_groups = ManyToManyField(ProductGroup) + brand = ForeignKey(Brand, on_delete=CASCADE) @property def handler(self): if self.product_type: try: product_type = import_module(self.product_type) - return product_type.ProductRouter(self.id) + return product_type.ProductRouter(self) except Exception as e: logger.error(f"Could not load product type {self.product_type} for product {self.id}: {e}") return None class ProductPlan(Model): product = ForeignKey(Product, on_delete=CASCADE) + currency = ForeignKey(Currency, on_delete=CASCADE) class ProductPlanItem(Model): from core.models.billable import CycleChoices @@ -39,4 +43,5 @@ class ProductPlanItem(Model): plan = ForeignKey(ProductPlan, on_delete=CASCADE) cycle = PositiveIntegerField(choices=CycleChoices.choices) count = PositiveIntegerField() - cost = CostField() \ No newline at end of file + cost = CostField() + taxable = BooleanField() \ No newline at end of file diff --git a/core/models/services.py b/core/models/services.py index 02b35ed..4b43ca6 100644 --- a/core/models/services.py +++ b/core/models/services.py @@ -1,44 +1,56 @@ from core.fields.numbers import CostField +from core.models.local import Currency +from core.models.profiles import ClientProfile -from django.db.models import Model, TextField, CharField, ManyToManyField, ForeignKey, CASCADE, PositiveIntegerField, OneToOneField +from django.db.models import Model, TextField, CharField, ManyToManyField, ForeignKey, CASCADE, PositiveIntegerField, OneToOneField, BooleanField, SET_NULL from importlib import import_module from logging import getLogger logger = getLogger(__name__) -class Service(Model): - from core.models.products import ProductGroup +class Service(Model): + from core.models.products import ProductGroup, Product + from core.models.billable import CycleChoices + + client = ForeignKey(ClientProfile, on_delete=CASCADE) name = CharField(max_length=255) description = TextField(null=True, blank=True) - handler_module = CharField(max_length=255, null=True, blank=True) + service_type = CharField(max_length=255, null=True, blank=True) + product = ForeignKey(Product, on_delete=SET_NULL, null=True) product_groups = ManyToManyField(ProductGroup) + currency = ForeignKey(Currency, on_delete=CASCADE) + cycle = PositiveIntegerField(choices=CycleChoices.choices) + count = PositiveIntegerField() + cost = CostField() + taxable=BooleanField() @property def handler(self): - if self.handler_module: + if self.service_type and self.product: try: - handler_module = import_module(self.handler_module) - return handler_module.ProductRouter(self.id) + service_type = import_module(self.service_type) + return service_type.ProductRouter(self.product, self) except Exception as e: - logger.error(f"Could not load product handler {self.handler_module} for product {self.id}: {e}") + logger.error( + f"Could not load product handler {self.service_type} for product {self.id}: {e}") return None -class ServicePlan(Model): - service = OneToOneField(Service, on_delete=CASCADE) - @classmethod - def from_productplan(cls, productplan, service): - plan = cls.objects.create(service=service) + def from_productplanitem(cls, productplanitem, client): + plan = cls.objects.create(client=client, + name=productplanitem.plan.product.name, + description=productplanitem.plan.product.description, + service_type=productplanitem.plan.product.product_type, + product=productplanitem.plan.product, + product_groups=productplanitem.plan.product.product_groups, + currency=productplanitem.plan.currency, + cycle=productplanitem.cycle, + count=productplanitem.count, + cost=productplanitem.cost, + taxable=productplanitem.taxable) - 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 - - plan = ForeignKey(ServicePlan, on_delete=CASCADE) - cycle = PositiveIntegerField(choices=CycleChoices.choices) - count = PositiveIntegerField() - cost = CostField() \ No newline at end of file + @property + def invoicable(self): + pass