Start implementation of automatic invoice generation
This commit is contained in:
parent
96cae766ed
commit
1bc81e7126
5 changed files with 70 additions and 31 deletions
21
core/helpers/billable.py
Normal file
21
core/helpers/billable.py
Normal 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
|
|
@ -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
|
|
@ -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,
|
||||||
|
|
|
@ -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()
|
|
@ -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()
|
|
||||||
|
|
Loading…
Reference in a new issue