Start implementation of automatic invoice generation

This commit is contained in:
Kumi 2020-06-05 09:38:55 +02:00
parent 96cae766ed
commit 1bc81e7126
5 changed files with 70 additions and 31 deletions

21
core/helpers/billable.py Normal file
View file

@ -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

View file

@ -5,6 +5,6 @@ from core.models.local import Currency, TaxPolicy, TaxRule
from core.models.cron import CronLog from core.models.cron import CronLog
from core.models.products import ProductGroup, Product, ProductPlan, ProductPlanItem from core.models.products import ProductGroup, Product, ProductPlan, ProductPlanItem
from core.models.billable import CycleChoices, Billable 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.invoices import Invoice, InvoiceItem
from core.models.api import APIKey from core.models.api import APIKey

View file

@ -5,6 +5,7 @@ from core.models.profiles import ClientProfile
from core.models.brands import Brand from core.models.brands import Brand
from core.fields.base import LongCharField from core.fields.base import LongCharField
from core.fields.numbers import CostField from core.fields.numbers import CostField
from core.models.local import Currency
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
@ -30,6 +31,7 @@ class Billable(Model):
description = TextField() description = TextField()
individual_cost = CostField() individual_cost = CostField()
amount = PositiveIntegerField() amount = PositiveIntegerField()
currency = ForeignKey(Currency, on_delete=CASCADE)
taxable = BooleanField() taxable = BooleanField()
action = PositiveIntegerField(choices=ActionChoices.choices) action = PositiveIntegerField(choices=ActionChoices.choices)
date = DateField(null=True) date = DateField(null=True)
@ -39,12 +41,11 @@ class Billable(Model):
def next_invoicing(self): def next_invoicing(self):
from core.models.invoices import InvoiceItem, Invoice from core.models.invoices import InvoiceItem, Invoice
if not self.recur_cycle == ActionChoices.DATE:
return False
try: try:
invoiceitems = InvoiceItem.objects.filter(billable=self) invoiceitems = InvoiceItem.objects.filter(billable=self)
if not recur_cycle:
return False
invoice = Invoice.objects.filter(invoiceitem_set__contains=invoiceitems).latest("created") invoice = Invoice.objects.filter(invoiceitem_set__contains=invoiceitems).latest("created")
delta = relativedelta(days=self.recur_count if self.recur_cycle == CycleChoices.DAYS else 0, 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, weeks=self.recur_count if self.recur_cycle == CycleChoices.WEEKS else 0,

View file

@ -1,8 +1,10 @@
from core.fields.base import LongCharField from core.fields.base import LongCharField
from core.fields.color import ColorField from core.fields.color import ColorField
from core.fields.numbers import CostField 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 from importlib import import_module
@ -19,19 +21,21 @@ class Product(Model):
description = TextField(null=True, blank=True) description = TextField(null=True, blank=True)
product_type = LongCharField(null=True, blank=True) product_type = LongCharField(null=True, blank=True)
product_groups = ManyToManyField(ProductGroup) product_groups = ManyToManyField(ProductGroup)
brand = ForeignKey(Brand, on_delete=CASCADE)
@property @property
def handler(self): def handler(self):
if self.product_type: if self.product_type:
try: try:
product_type = import_module(self.product_type) product_type = import_module(self.product_type)
return product_type.ProductRouter(self.id) return product_type.ProductRouter(self)
except Exception as e: except Exception as e:
logger.error(f"Could not load product type {self.product_type} for product {self.id}: {e}") logger.error(f"Could not load product type {self.product_type} for product {self.id}: {e}")
return None return None
class ProductPlan(Model): class ProductPlan(Model):
product = ForeignKey(Product, on_delete=CASCADE) product = ForeignKey(Product, on_delete=CASCADE)
currency = ForeignKey(Currency, on_delete=CASCADE)
class ProductPlanItem(Model): class ProductPlanItem(Model):
from core.models.billable import CycleChoices from core.models.billable import CycleChoices
@ -39,4 +43,5 @@ class ProductPlanItem(Model):
plan = ForeignKey(ProductPlan, on_delete=CASCADE) plan = ForeignKey(ProductPlan, on_delete=CASCADE)
cycle = PositiveIntegerField(choices=CycleChoices.choices) cycle = PositiveIntegerField(choices=CycleChoices.choices)
count = PositiveIntegerField() count = PositiveIntegerField()
cost = CostField() cost = CostField()
taxable = BooleanField()

View file

@ -1,44 +1,56 @@
from core.fields.numbers import CostField 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 importlib import import_module
from logging import getLogger from logging import getLogger
logger = getLogger(__name__) 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) name = CharField(max_length=255)
description = TextField(null=True, blank=True) 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) product_groups = ManyToManyField(ProductGroup)
currency = ForeignKey(Currency, on_delete=CASCADE)
cycle = PositiveIntegerField(choices=CycleChoices.choices)
count = PositiveIntegerField()
cost = CostField()
taxable=BooleanField()
@property @property
def handler(self): def handler(self):
if self.handler_module: if self.service_type and self.product:
try: try:
handler_module = import_module(self.handler_module) service_type = import_module(self.service_type)
return handler_module.ProductRouter(self.id) return service_type.ProductRouter(self.product, self)
except Exception as e: 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 return None
class ServicePlan(Model):
service = OneToOneField(Service, on_delete=CASCADE)
@classmethod @classmethod
def from_productplan(cls, productplan, service): def from_productplanitem(cls, productplanitem, client):
plan = cls.objects.create(service=service) 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: @property
ServicePlanItem.objects.create(plan=plan, cycle=item.cycle, count=item.count, cost=item.cost) def invoicable(self):
pass
class ServicePlanItem(Model):
from core.models.billable import CycleChoices
plan = ForeignKey(ServicePlan, on_delete=CASCADE)
cycle = PositiveIntegerField(choices=CycleChoices.choices)
count = PositiveIntegerField()
cost = CostField()