314 lines
10 KiB
Python
314 lines
10 KiB
Python
from django.db import models
|
|
from django.utils import timezone
|
|
from django.core.validators import MinValueValidator, MaxValueValidator
|
|
|
|
class Institute(models.Model):
|
|
name = models.CharField(max_length=100)
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
class Client(models.Model):
|
|
name = models.CharField(max_length=100)
|
|
address = models.TextField()
|
|
institute = models.ForeignKey(Institute, on_delete=models.CASCADE)
|
|
|
|
def __str__(self):
|
|
return f"{self.name} ({self.institute.name})"
|
|
|
|
class MonthlySheet(models.Model):
|
|
"""Represents one monthly page"""
|
|
year = models.IntegerField()
|
|
month = models.IntegerField() # 1-12
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
unique_together = ['year', 'month']
|
|
ordering = ['year', 'month']
|
|
|
|
def __str__(self):
|
|
return f"{self.year}-{self.month:02d}"
|
|
|
|
class Cell(models.Model):
|
|
"""A single cell in the spreadsheet"""
|
|
sheet = models.ForeignKey(MonthlySheet, on_delete=models.CASCADE, related_name='cells')
|
|
client = models.ForeignKey(Client, on_delete=models.CASCADE)
|
|
table_type = models.CharField(max_length=20, choices=[
|
|
('top_left', 'Top Left Table'),
|
|
('top_right', 'Top Right Table'),
|
|
('bottom_1', 'Bottom Table 1'),
|
|
('bottom_2', 'Bottom Table 2'),
|
|
('bottom_3', 'Bottom Table 3'),
|
|
])
|
|
row_index = models.IntegerField() # 0-23 for top tables, 0-9 for bottom
|
|
column_index = models.IntegerField() # Actually client index (0-5)
|
|
is_formula = models.BooleanField(default=False)
|
|
# Cell content
|
|
value = models.DecimalField(max_digits=15, decimal_places=6, null=True, blank=True)
|
|
formula = models.TextField(blank=True)
|
|
|
|
|
|
# Metadata
|
|
data_type = models.CharField(max_length=20, default='number', choices=[
|
|
('number', 'Number'),
|
|
('text', 'Text'),
|
|
('date', 'Date'),
|
|
])
|
|
|
|
class Meta:
|
|
unique_together = ['sheet', 'client', 'table_type', 'row_index', 'column_index']
|
|
indexes = [
|
|
models.Index(fields=['sheet', 'table_type', 'row_index']),
|
|
]
|
|
|
|
def __str__(self):
|
|
return f"{self.sheet} - {self.client.name} - {self.table_type}[{self.row_index}][{self.column_index}]"
|
|
|
|
class CellReference(models.Model):
|
|
"""Track dependencies between cells for calculations"""
|
|
source_cell = models.ForeignKey(Cell, on_delete=models.CASCADE, related_name='dependents')
|
|
target_cell = models.ForeignKey(Cell, on_delete=models.CASCADE, related_name='dependencies')
|
|
|
|
class Meta:
|
|
unique_together = ['source_cell', 'target_cell']
|
|
|
|
from decimal import Decimal
|
|
from django.db import models
|
|
|
|
class Betriebskosten(models.Model):
|
|
KOSTENTYP_CHOICES = [
|
|
('sach', 'Sach'),
|
|
('helium', 'Helium'),
|
|
]
|
|
gegenstand = models.CharField("Gegenstand", max_length=200)
|
|
buchungsdatum = models.DateField('Zahlungsdatum')
|
|
rechnungsnummer = models.CharField('Firma', max_length=50)
|
|
kostentyp = models.CharField('Kostentyp', max_length=10, choices=KOSTENTYP_CHOICES)
|
|
|
|
# IMPORTANT: now this field stores m³ (not liters)
|
|
gas_volume = models.DecimalField('Gasvolumen (m³)', max_digits=10, decimal_places=2, null=True, blank=True)
|
|
|
|
betrag = models.DecimalField('Betrag (€)', max_digits=10, decimal_places=2)
|
|
beschreibung = models.TextField('Beschreibung', blank=True)
|
|
|
|
@property
|
|
def gas_volume_liter(self):
|
|
# Liter = m³ / 0.75
|
|
if self.kostentyp == 'helium' and self.gas_volume:
|
|
return self.gas_volume / Decimal("0.75")
|
|
return None
|
|
|
|
@property
|
|
def price_per_m3(self):
|
|
if self.kostentyp == 'helium' and self.gas_volume:
|
|
return self.betrag / self.gas_volume
|
|
return None
|
|
|
|
@property
|
|
def price_per_liter(self):
|
|
# Preis(Liter) = betrag / liter
|
|
liters = self.gas_volume_liter
|
|
if self.kostentyp == 'helium' and liters:
|
|
return self.betrag / liters
|
|
return None
|
|
|
|
# Or simpler: Just store row calculations in a JSONField
|
|
class TableConfig(models.Model):
|
|
"""Configuration for table calculations"""
|
|
table_type = models.CharField(max_length=20, unique=True, choices=[
|
|
('top_left', 'Top Left Table'),
|
|
('top_right', 'Top Right Table'),
|
|
('bottom_1', 'Bottom Table 1'),
|
|
('bottom_2', 'Bottom Table 2'),
|
|
('bottom_3', 'Bottom Table 3'),
|
|
])
|
|
calculations = models.JSONField(default=dict) # {11: "10 + 9", 15: "14 - 13"}
|
|
|
|
def __str__(self):
|
|
return f"{self.get_table_type_display()} Config"
|
|
class ExcelEntry(models.Model):
|
|
client = models.ForeignKey(Client, on_delete=models.CASCADE)
|
|
date = models.DateField(default=timezone.now)
|
|
pressure = models.DecimalField(
|
|
max_digits=10,
|
|
decimal_places=2,
|
|
validators=[MinValueValidator(0)],
|
|
default=0.00
|
|
)
|
|
purity = models.DecimalField(
|
|
max_digits=5,
|
|
decimal_places=2,
|
|
validators=[MinValueValidator(0), MaxValueValidator(100)],
|
|
default=0.00
|
|
)
|
|
notes = models.TextField(blank=True, null=True)
|
|
date_joined = models.DateField(auto_now_add=True)
|
|
|
|
# Manual input
|
|
lhe_zus = models.DecimalField(
|
|
max_digits=10,
|
|
decimal_places=3,
|
|
validators=[MinValueValidator(0)],
|
|
default=0.0
|
|
)
|
|
|
|
druckkorrektur = models.DecimalField(
|
|
max_digits=10,
|
|
decimal_places=3,
|
|
validators=[MinValueValidator(0)],
|
|
default=1.0
|
|
)
|
|
|
|
# Auto-calculated values (saved)
|
|
constant_300 = models.DecimalField(
|
|
max_digits=10,
|
|
decimal_places=3,
|
|
default=300.0
|
|
)
|
|
|
|
korrig_druck = models.DecimalField(
|
|
max_digits=12,
|
|
decimal_places=6,
|
|
default=0.0
|
|
)
|
|
|
|
nm3 = models.DecimalField(
|
|
max_digits=12,
|
|
decimal_places=6,
|
|
default=0.0
|
|
)
|
|
|
|
lhe = models.DecimalField(
|
|
max_digits=12,
|
|
decimal_places=6,
|
|
default=0.0
|
|
)
|
|
|
|
lhe_ges = models.DecimalField(
|
|
max_digits=12,
|
|
decimal_places=6,
|
|
default=0.0
|
|
)
|
|
|
|
def __str__(self):
|
|
return f"{self.client.name} - {self.date}"
|
|
|
|
class SecondTableEntry(models.Model):
|
|
client = models.ForeignKey(Client, on_delete=models.CASCADE)
|
|
date = models.DateField(default=timezone.now)
|
|
is_warm = models.BooleanField(default=False)
|
|
lhe_delivery = models.CharField(max_length=100, blank=True, null=True)
|
|
vor = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)
|
|
nach = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)
|
|
lhe_output = models.DecimalField(
|
|
max_digits=10,
|
|
decimal_places=2,
|
|
validators=[MinValueValidator(0)],
|
|
blank=True,
|
|
null=True
|
|
)
|
|
notes = models.TextField(blank=True, null=True)
|
|
date_joined = models.DateField(auto_now_add=True)
|
|
class Meta:
|
|
ordering = ["date", "id"]
|
|
def __str__(self):
|
|
return f"{self.client.name} - {self.date}"
|
|
|
|
class MonthlySummary(models.Model):
|
|
"""
|
|
Stores per-month summary values that we need in other months
|
|
or in the half-year overview.
|
|
"""
|
|
sheet = models.OneToOneField(
|
|
MonthlySheet,
|
|
on_delete=models.CASCADE,
|
|
related_name='summary',
|
|
)
|
|
|
|
# K44: Gesamtbestand neu (Lit. LHe) from Bottom Table 2
|
|
gesamtbestand_neu_lhe = models.DecimalField(
|
|
max_digits=18,
|
|
decimal_places=6,
|
|
null=True,
|
|
blank=True,
|
|
)
|
|
|
|
# Gasbestand (Lit. LHe) from Bottom Table 1
|
|
gasbestand_lhe = models.DecimalField(
|
|
max_digits=18,
|
|
decimal_places=6,
|
|
null=True,
|
|
blank=True,
|
|
)
|
|
|
|
# Verbraucherverluste (Lit. L-He) from overall summary
|
|
verbraucherverlust_lhe = models.DecimalField(
|
|
max_digits=18,
|
|
decimal_places=6,
|
|
null=True,
|
|
blank=True,
|
|
)
|
|
|
|
def __str__(self):
|
|
return f"Summary {self.sheet.year}-{self.sheet.month:02d}"
|
|
|
|
|
|
class BetriebskostenSummary(models.Model):
|
|
personalkosten = models.DecimalField(max_digits=12, decimal_places=2, default=0)
|
|
|
|
instandhaltung = models.DecimalField(max_digits=12, decimal_places=2, default=0)
|
|
heliumkosten = models.DecimalField(max_digits=12, decimal_places=2, default=0)
|
|
bezugskosten_gashe = models.DecimalField(max_digits=12, decimal_places=4, default=0)
|
|
umlage_personal = models.DecimalField(max_digits=12, decimal_places=2, default=0)
|
|
|
|
def recalculate(self):
|
|
from django.db.models import Sum
|
|
from django.db.models.functions import Coalesce
|
|
from django.db.models import DecimalField, Value
|
|
from .models import Betriebskosten
|
|
|
|
items = Betriebskosten.objects.all()
|
|
|
|
sach_sum = items.filter(kostentyp='sach').aggregate(
|
|
total=Coalesce(Sum('betrag'), Value(0, output_field=DecimalField()))
|
|
)['total'] or Decimal('0')
|
|
|
|
helium_sum = items.filter(kostentyp='helium').aggregate(
|
|
total=Coalesce(Sum('betrag'), Value(0, output_field=DecimalField()))
|
|
)['total'] or Decimal('0')
|
|
|
|
helium_m3_sum = items.filter(kostentyp='helium').aggregate(
|
|
total=Coalesce(Sum('gas_volume'), Value(0, output_field=DecimalField()))
|
|
)['total'] or Decimal('0')
|
|
|
|
bezug = (helium_sum / helium_m3_sum) if helium_m3_sum not in (None, 0, Decimal('0')) else Decimal('0')
|
|
|
|
self.instandhaltung = sach_sum
|
|
self.heliumkosten = helium_sum
|
|
self.bezugskosten_gashe = bezug
|
|
self.umlage_personal = self.personalkosten / 2
|
|
self.save()
|
|
|
|
# models.py
|
|
from django.db import models
|
|
|
|
class AbrechnungCell(models.Model):
|
|
"""
|
|
Storage for the 'Abrechnung' page where columns are custom (not 1:1 with Client).
|
|
Values are saved by: (interval_year, interval_start_month, row_key, col_key).
|
|
"""
|
|
interval_year = models.IntegerField()
|
|
interval_start_month = models.IntegerField() # first month of the 6-month window
|
|
|
|
row_key = models.CharField(max_length=60)
|
|
col_key = models.CharField(max_length=60)
|
|
|
|
value = models.DecimalField(max_digits=18, decimal_places=6, null=True, blank=True)
|
|
|
|
class Meta:
|
|
unique_together = ("interval_year", "interval_start_month", "row_key", "col_key")
|
|
|
|
def __str__(self):
|
|
return f"Abrechnung {self.interval_year}/{self.interval_start_month} {self.row_key}:{self.col_key}={self.value}"
|