Files
he-database/sheets/models.py
T
2026-04-14 10:03:33 +02:00

313 lines
10 KiB
Python

from django.db import models
from django.utils import timezone
from django.core.validators import MinValueValidator, MaxValueValidator
from decimal import Decimal
from django.db.models import Sum
from django.db.models.functions import Coalesce
from django.db.models import DecimalField, Value
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']
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):
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
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}"