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}"