diff --git a/He-Anlage 2024_1.Halbjahr.ods b/He-Anlage 2024_1.Halbjahr.ods
index 9263bc9..b8c7e43 100644
Binary files a/He-Anlage 2024_1.Halbjahr.ods and b/He-Anlage 2024_1.Halbjahr.ods differ
diff --git a/db.sqlite3 b/db.sqlite3
index d829c37..10e7ffb 100644
Binary files a/db.sqlite3 and b/db.sqlite3 differ
diff --git a/excel_mimic/__pycache__/__init__.cpython-313.pyc b/excel_mimic/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 0000000..b2de94a
Binary files /dev/null and b/excel_mimic/__pycache__/__init__.cpython-313.pyc differ
diff --git a/excel_mimic/__pycache__/settings.cpython-313.pyc b/excel_mimic/__pycache__/settings.cpython-313.pyc
new file mode 100644
index 0000000..15f5ca9
Binary files /dev/null and b/excel_mimic/__pycache__/settings.cpython-313.pyc differ
diff --git a/excel_mimic/__pycache__/urls.cpython-313.pyc b/excel_mimic/__pycache__/urls.cpython-313.pyc
new file mode 100644
index 0000000..124f410
Binary files /dev/null and b/excel_mimic/__pycache__/urls.cpython-313.pyc differ
diff --git a/excel_mimic/__pycache__/wsgi.cpython-313.pyc b/excel_mimic/__pycache__/wsgi.cpython-313.pyc
new file mode 100644
index 0000000..d62485e
Binary files /dev/null and b/excel_mimic/__pycache__/wsgi.cpython-313.pyc differ
diff --git a/fix_top_right_cells.py b/fix_top_right_cells.py
new file mode 100644
index 0000000..3b33707
--- /dev/null
+++ b/fix_top_right_cells.py
@@ -0,0 +1,36 @@
+from sheets.models import MonthlySheet, Client, Cell
+
+TOP_RIGHT_CLIENTS = [
+ "Dr. Fohrer", # L
+ "AG Buntk.", # M
+ "AG Alff", # N
+ "AG Gutfl.", # O
+ "M3 Thiele", # P
+ "M3 Buntkowsky", # Q
+ "M3 Gutfleisch", # R
+]
+
+ROW_COUNT = 16 # top_right rows 0–15
+
+sheets = MonthlySheet.objects.all().order_by('year', 'month')
+
+for sheet in sheets:
+ print(f"Fixing sheet {sheet.year}-{sheet.month}...")
+ for col_idx, name in enumerate(TOP_RIGHT_CLIENTS):
+ try:
+ client = Client.objects.get(name=name)
+ except Client.DoesNotExist:
+ print(f" WARNING: client {name!r} not found, skipping this column")
+ continue
+
+ for row_idx in range(ROW_COUNT):
+ cell, created = Cell.objects.get_or_create(
+ sheet=sheet,
+ table_type='top_right',
+ client=client,
+ row_index=row_idx,
+ column_index=col_idx,
+ defaults={'value': None},
+ )
+ if created:
+ print(f" created cell: {name} row {row_idx}")
diff --git a/sheets/__init__.py b/sheets/__init__.py
index e69de29..7f29f4f 100644
--- a/sheets/__init__.py
+++ b/sheets/__init__.py
@@ -0,0 +1,2 @@
+# sheets/__init__.py
+default_app_config = 'sheets.apps.SheetsConfig'
\ No newline at end of file
diff --git a/sheets/__pycache__/__init__.cpython-313.pyc b/sheets/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 0000000..839958d
Binary files /dev/null and b/sheets/__pycache__/__init__.cpython-313.pyc differ
diff --git a/sheets/__pycache__/admin.cpython-313.pyc b/sheets/__pycache__/admin.cpython-313.pyc
new file mode 100644
index 0000000..b7545a9
Binary files /dev/null and b/sheets/__pycache__/admin.cpython-313.pyc differ
diff --git a/sheets/__pycache__/apps.cpython-313.pyc b/sheets/__pycache__/apps.cpython-313.pyc
new file mode 100644
index 0000000..076120a
Binary files /dev/null and b/sheets/__pycache__/apps.cpython-313.pyc differ
diff --git a/sheets/__pycache__/forms.cpython-313.pyc b/sheets/__pycache__/forms.cpython-313.pyc
new file mode 100644
index 0000000..03002b1
Binary files /dev/null and b/sheets/__pycache__/forms.cpython-313.pyc differ
diff --git a/sheets/__pycache__/models.cpython-313.pyc b/sheets/__pycache__/models.cpython-313.pyc
new file mode 100644
index 0000000..ea468c7
Binary files /dev/null and b/sheets/__pycache__/models.cpython-313.pyc differ
diff --git a/sheets/__pycache__/urls.cpython-313.pyc b/sheets/__pycache__/urls.cpython-313.pyc
new file mode 100644
index 0000000..dc17ed7
Binary files /dev/null and b/sheets/__pycache__/urls.cpython-313.pyc differ
diff --git a/sheets/__pycache__/views.cpython-313.pyc b/sheets/__pycache__/views.cpython-313.pyc
new file mode 100644
index 0000000..58cd178
Binary files /dev/null and b/sheets/__pycache__/views.cpython-313.pyc differ
diff --git a/sheets/apps.py b/sheets/apps.py
index 757cee8..519e7f8 100644
--- a/sheets/apps.py
+++ b/sheets/apps.py
@@ -1,6 +1,8 @@
from django.apps import AppConfig
-
class SheetsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'sheets'
+
+ def ready(self):
+ import sheets.signals
diff --git a/sheets/migrations/0011_alter_betriebskosten_kostentyp_monthlysheet_cell_and_more.py b/sheets/migrations/0011_alter_betriebskosten_kostentyp_monthlysheet_cell_and_more.py
new file mode 100644
index 0000000..4acbd69
--- /dev/null
+++ b/sheets/migrations/0011_alter_betriebskosten_kostentyp_monthlysheet_cell_and_more.py
@@ -0,0 +1,67 @@
+# Generated by Django 5.1.5 on 2026-01-07 11:29
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('sheets', '0010_excelentry_constant_300_excelentry_druckkorrektur_and_more'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='betriebskosten',
+ name='kostentyp',
+ field=models.CharField(choices=[('sach', 'Sachkosten'), ('ln2', 'LN2'), ('helium', 'Helium'), ('inv', 'Inventar')], max_length=10, verbose_name='Kostentyp'),
+ ),
+ migrations.CreateModel(
+ name='MonthlySheet',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('year', models.IntegerField()),
+ ('month', models.IntegerField()),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ],
+ options={
+ 'ordering': ['year', 'month'],
+ 'unique_together': {('year', 'month')},
+ },
+ ),
+ migrations.CreateModel(
+ name='Cell',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('table_type', models.CharField(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')], max_length=20)),
+ ('row_index', models.IntegerField()),
+ ('column_index', models.IntegerField()),
+ ('value', models.DecimalField(blank=True, decimal_places=6, max_digits=15, null=True)),
+ ('formula', models.TextField(blank=True)),
+ ('is_formula', models.BooleanField(default=False)),
+ ('data_type', models.CharField(choices=[('number', 'Number'), ('text', 'Text'), ('date', 'Date')], default='number', max_length=20)),
+ ('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sheets.client')),
+ ('sheet', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cells', to='sheets.monthlysheet')),
+ ],
+ ),
+ migrations.CreateModel(
+ name='CellReference',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('source_cell', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='dependents', to='sheets.cell')),
+ ('target_cell', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='dependencies', to='sheets.cell')),
+ ],
+ options={
+ 'unique_together': {('source_cell', 'target_cell')},
+ },
+ ),
+ migrations.AddIndex(
+ model_name='cell',
+ index=models.Index(fields=['sheet', 'table_type', 'row_index'], name='sheets_cell_sheet_i_511238_idx'),
+ ),
+ migrations.AlterUniqueTogether(
+ name='cell',
+ unique_together={('sheet', 'client', 'table_type', 'row_index', 'column_index')},
+ ),
+ ]
diff --git a/sheets/migrations/0012_tableconfig_rowcalculation.py b/sheets/migrations/0012_tableconfig_rowcalculation.py
new file mode 100644
index 0000000..3660091
--- /dev/null
+++ b/sheets/migrations/0012_tableconfig_rowcalculation.py
@@ -0,0 +1,34 @@
+# Generated by Django 6.0.1 on 2026-02-01 11:03
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('sheets', '0011_alter_betriebskosten_kostentyp_monthlysheet_cell_and_more'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='TableConfig',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('table_type', models.CharField(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')], max_length=20, unique=True)),
+ ('calculations', models.JSONField(default=dict)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='RowCalculation',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('table_type', models.CharField(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')], max_length=20)),
+ ('row_index', models.IntegerField()),
+ ('formula', models.TextField()),
+ ('description', models.CharField(blank=True, max_length=200)),
+ ],
+ options={
+ 'unique_together': {('table_type', 'row_index')},
+ },
+ ),
+ ]
diff --git a/sheets/migrations/__pycache__/0001_initial.cpython-313.pyc b/sheets/migrations/__pycache__/0001_initial.cpython-313.pyc
new file mode 100644
index 0000000..82ac4dc
Binary files /dev/null and b/sheets/migrations/__pycache__/0001_initial.cpython-313.pyc differ
diff --git a/sheets/migrations/__pycache__/0002_remove_secondtableentry_age_and_more.cpython-313.pyc b/sheets/migrations/__pycache__/0002_remove_secondtableentry_age_and_more.cpython-313.pyc
new file mode 100644
index 0000000..fa8a409
Binary files /dev/null and b/sheets/migrations/__pycache__/0002_remove_secondtableentry_age_and_more.cpython-313.pyc differ
diff --git a/sheets/migrations/__pycache__/0003_alter_secondtableentry_lhe_output.cpython-313.pyc b/sheets/migrations/__pycache__/0003_alter_secondtableentry_lhe_output.cpython-313.pyc
new file mode 100644
index 0000000..5f440ff
Binary files /dev/null and b/sheets/migrations/__pycache__/0003_alter_secondtableentry_lhe_output.cpython-313.pyc differ
diff --git a/sheets/migrations/__pycache__/0004_alter_secondtableentry_lhe_output.cpython-313.pyc b/sheets/migrations/__pycache__/0004_alter_secondtableentry_lhe_output.cpython-313.pyc
new file mode 100644
index 0000000..494393b
Binary files /dev/null and b/sheets/migrations/__pycache__/0004_alter_secondtableentry_lhe_output.cpython-313.pyc differ
diff --git a/sheets/migrations/__pycache__/0005_alter_secondtableentry_lhe_output.cpython-313.pyc b/sheets/migrations/__pycache__/0005_alter_secondtableentry_lhe_output.cpython-313.pyc
new file mode 100644
index 0000000..a35dee6
Binary files /dev/null and b/sheets/migrations/__pycache__/0005_alter_secondtableentry_lhe_output.cpython-313.pyc differ
diff --git a/sheets/migrations/__pycache__/0006_remove_excelentry_age_remove_excelentry_email_and_more.cpython-313.pyc b/sheets/migrations/__pycache__/0006_remove_excelentry_age_remove_excelentry_email_and_more.cpython-313.pyc
new file mode 100644
index 0000000..de75832
Binary files /dev/null and b/sheets/migrations/__pycache__/0006_remove_excelentry_age_remove_excelentry_email_and_more.cpython-313.pyc differ
diff --git a/sheets/migrations/__pycache__/0007_betriebskosten.cpython-313.pyc b/sheets/migrations/__pycache__/0007_betriebskosten.cpython-313.pyc
new file mode 100644
index 0000000..7a4fd09
Binary files /dev/null and b/sheets/migrations/__pycache__/0007_betriebskosten.cpython-313.pyc differ
diff --git a/sheets/migrations/__pycache__/0008_institute_alter_betriebskosten_kostentyp_and_more.cpython-313.pyc b/sheets/migrations/__pycache__/0008_institute_alter_betriebskosten_kostentyp_and_more.cpython-313.pyc
new file mode 100644
index 0000000..5d2f2b8
Binary files /dev/null and b/sheets/migrations/__pycache__/0008_institute_alter_betriebskosten_kostentyp_and_more.cpython-313.pyc differ
diff --git a/sheets/migrations/__pycache__/0009_alter_client_institute.cpython-313.pyc b/sheets/migrations/__pycache__/0009_alter_client_institute.cpython-313.pyc
new file mode 100644
index 0000000..889e2a3
Binary files /dev/null and b/sheets/migrations/__pycache__/0009_alter_client_institute.cpython-313.pyc differ
diff --git a/sheets/migrations/__pycache__/0010_excelentry_constant_300_excelentry_druckkorrektur_and_more.cpython-313.pyc b/sheets/migrations/__pycache__/0010_excelentry_constant_300_excelentry_druckkorrektur_and_more.cpython-313.pyc
new file mode 100644
index 0000000..4a9be8a
Binary files /dev/null and b/sheets/migrations/__pycache__/0010_excelentry_constant_300_excelentry_druckkorrektur_and_more.cpython-313.pyc differ
diff --git a/sheets/migrations/__pycache__/0011_alter_betriebskosten_kostentyp_monthlysheet_cell_and_more.cpython-313.pyc b/sheets/migrations/__pycache__/0011_alter_betriebskosten_kostentyp_monthlysheet_cell_and_more.cpython-313.pyc
new file mode 100644
index 0000000..a2def52
Binary files /dev/null and b/sheets/migrations/__pycache__/0011_alter_betriebskosten_kostentyp_monthlysheet_cell_and_more.cpython-313.pyc differ
diff --git a/sheets/migrations/__pycache__/0012_tableconfig_rowcalculation.cpython-313.pyc b/sheets/migrations/__pycache__/0012_tableconfig_rowcalculation.cpython-313.pyc
new file mode 100644
index 0000000..9dda3c0
Binary files /dev/null and b/sheets/migrations/__pycache__/0012_tableconfig_rowcalculation.cpython-313.pyc differ
diff --git a/sheets/migrations/__pycache__/__init__.cpython-313.pyc b/sheets/migrations/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 0000000..4cd7cee
Binary files /dev/null and b/sheets/migrations/__pycache__/__init__.cpython-313.pyc differ
diff --git a/sheets/models.py b/sheets/models.py
index 3076f5d..d1990bc 100644
--- a/sheets/models.py
+++ b/sheets/models.py
@@ -8,9 +8,74 @@ class Institute(models.Model):
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', 'Sachkostöen'),
+ ('sach', 'Sachkosten'),
('ln2', 'LN2'),
('helium', 'Helium'),
('inv', 'Inventar'),
@@ -30,16 +95,40 @@ class Betriebskosten(models.Model):
return None
def __str__(self):
- return f"{self.buchungsdatum} - {self.get_kostentyp_display()} - {self.betrag}€" # Fixed the missing quote
-
-class Client(models.Model):
- name = models.CharField(max_length=100)
- address = models.TextField()
- institute = models.ForeignKey(Institute, on_delete=models.CASCADE) # Remove null=True, blank=True
+ return f"{self.buchungsdatum} - {self.get_kostentyp_display()} - {self.betrag}€"
+class RowCalculation(models.Model):
+ """Define calculations for specific rows"""
+ 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() # Which row has the formula
+ formula = models.TextField() # e.g., "row_10 + row_9"
+ description = models.CharField(max_length=200, blank=True)
+
+ class Meta:
+ unique_together = ['table_type', 'row_index']
def __str__(self):
- return f"{self.name} ({self.institute.name})"
+ return f"{self.table_type}[{self.row_index}]: {self.formula}"
+# 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)
@@ -103,6 +192,10 @@ class ExcelEntry(models.Model):
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)
@@ -119,4 +212,4 @@ class SecondTableEntry(models.Model):
date_joined = models.DateField(auto_now_add=True)
def __str__(self):
- return f"{self.client.name} - {self.date}"
\ No newline at end of file
+ return f"{self.client.name} - {self.date}"
\ No newline at end of file
diff --git a/sheets/signals.py b/sheets/signals.py
new file mode 100644
index 0000000..0b464bc
--- /dev/null
+++ b/sheets/signals.py
@@ -0,0 +1,82 @@
+# sheets/signals.py
+from django.db.models.signals import post_save, post_delete
+from django.dispatch import receiver
+from django.db.models import Sum
+from django.db.models.functions import Coalesce
+from decimal import Decimal
+
+# IMPORT THE MODELS AT THE TOP
+from .models import ExcelEntry, SecondTableEntry, MonthlySheet, Cell
+
+@receiver([post_save, post_delete], sender=ExcelEntry)
+def update_top_right_helium_input(sender, instance, **kwargs):
+ """Update top-right table when ExcelEntry changes"""
+ if instance.date:
+ # Get the monthly sheet for this date
+ sheet = MonthlySheet.objects.filter(
+ year=instance.date.year,
+ month=instance.date.month
+ ).first()
+
+ if sheet:
+ # Re-populate helium input data
+ from .views import MonthlySheetView
+ view = MonthlySheetView()
+ view.populate_helium_input_to_top_right(sheet)
+
+@receiver([post_save, post_delete], sender=SecondTableEntry)
+def update_monthly_sheet_bezug(sender, instance, **kwargs):
+ """Update B11 (Bezug) in monthly sheet when SecondTableEntry changes - ONLY for non-start sheets"""
+ if instance.date and instance.client:
+ # Check if this is the start sheet (2025-01)
+ if instance.date.year == 2025 and instance.date.month == 1:
+ return # Don't auto-update for start sheet
+
+ # Get or create the monthly sheet for this date
+ sheet, created = MonthlySheet.objects.get_or_create(
+ year=instance.date.year,
+ month=instance.date.month
+ )
+
+ # Calculate total LHe output for this client in this month
+ lhe_output_sum = SecondTableEntry.objects.filter(
+ client=instance.client,
+ date__year=instance.date.year,
+ date__month=instance.date.month
+ ).aggregate(
+ total=Coalesce(Sum('lhe_output'), Decimal('0'))
+ )['total']
+
+ # Update B11 cell (row_index 8 = Excel B11) in TOP-LEFT table
+ b11_cell_left = Cell.objects.filter(
+ sheet=sheet,
+ table_type='top_left',
+ client=instance.client,
+ row_index=8 # Excel B11
+ ).first()
+
+ if b11_cell_left and (b11_cell_left.value != lhe_output_sum or b11_cell_left.value is None):
+ b11_cell_left.value = lhe_output_sum
+ b11_cell_left.save()
+
+ # Import here to avoid circular imports
+ from .views import SaveCellsView
+ save_view = SaveCellsView()
+ save_view.calculate_top_left_dependents(sheet, b11_cell_left)
+
+ # ALSO update Bezug (row_index 8) in TOP-RIGHT table
+ b11_cell_right = Cell.objects.filter(
+ sheet=sheet,
+ table_type='top_right',
+ client=instance.client,
+ row_index=8 # Excel row 12 = Bezug
+ ).first()
+
+ if b11_cell_right and (b11_cell_right.value != lhe_output_sum or b11_cell_right.value is None):
+ b11_cell_right.value = lhe_output_sum
+ b11_cell_right.save()
+
+ # Also trigger top-right calculations
+ from .views import SaveCellsView
+ save_view = SaveCellsView()
+ save_view.calculate_top_right_dependents(sheet, b11_cell_right)
\ No newline at end of file
diff --git a/sheets/templates/clients_table.html b/sheets/templates/clients_table.html
index 985b14a..cd65cba 100644
--- a/sheets/templates/clients_table.html
+++ b/sheets/templates/clients_table.html
@@ -47,6 +47,7 @@
Go to Helium Output
Go to Admin Panel
Betriebskosten
+ Monthly Sheets
diff --git a/sheets/templates/monthly_sheet.html b/sheets/templates/monthly_sheet.html
new file mode 100644
index 0000000..896b8db
--- /dev/null
+++ b/sheets/templates/monthly_sheet.html
@@ -0,0 +1,674 @@
+{% extends "base.html" %}
+
+{% block content %}
+
+
+
+
+
+
+
+
+
Table 1: Top Left
+
+
+
+ Row Label
+ {% for header in top_left_headers %}
+ {{ header }}
+ {% endfor %}
+ Sum
+
+
+
+ {% for row in top_left_rows %}
+ {% with rownum=forloop.counter %}
+
+
+ {% if rownum == 1 %}
+ Stand der Gaszähler (Nm³)
+ {% elif rownum == 2 %}
+ Stand der Gaszähler (Vormonat) (Nm³)
+ {% elif rownum == 3 %}
+ Gasrückführung (Nm³)
+ {% elif rownum == 4 %}
+ Rückführung flüssig (Lit. L-He)
+ {% elif rownum == 5 %}
+ Sonderrückführungen (Lit. L-He)
+ {% elif rownum == 6 %}
+ Bestand in Kannen-1 (Lit. L-He)
+ {% elif rownum == 7 %}
+ Summe Bestand (Lit. L-He)
+ {% elif rownum == 8 %}
+ Best. in Kannen Vormonat (Lit. L-He)
+ {% elif rownum == 9 %}
+ Bezug (Liter L-He)
+ {% elif rownum == 10 %}
+ Rückführ. Soll (Lit. L-He)
+ {% elif rownum == 11 %}
+ Verluste (Soll-Rückf.) (Lit. L-He)
+ {% elif rownum == 12 %}
+ Füllungen warm (Lit. L-He)
+ {% elif rownum == 13 %}
+ Kaltgas Rückgabe (Lit. L-He) – Faktor
+ {% elif rownum == 14 %}
+ Faktor 0.06
+ {% elif rownum == 15 %}
+ Verbraucherverluste (Liter L-He)
+ {% elif rownum == 16 %}
+ %
+ {% elif rownum == 17 %}
+ Gesamtrückführung (Nm³)
+ {% elif rownum == 18 %}
+ Aufgeteilte Verluste (Liter L-He)
+ {% endif %}
+
+
+ {% for cell in row.cells %}
+ {% if is_start_sheet or rownum == 1 or rownum == 5 or rownum == 6 %}
+ {# Editable in start sheet OR always editable rows (B3, B7, B8) #}
+
+ {{ cell.value|default:"" }}
+
+ {% else %}
+ {# Readonly for non-start sheets #}
+
+ {{ cell.value|default:"" }}
+
+ {% endif %}
+ {% endfor %}
+
+
+
+ {{ row.sum|default_if_none:"" }}
+
+
+ {% endwith %}
+ {% endfor %}
+
+
+
+
+
+
+
+
Table 2: Top Right
+
+
+
+ Row Label
+ {% for header in top_right_headers %}
+ {{ header }}
+ {% endfor %}
+ Sum
+
+
+
+ {% for row in top_right_rows %}
+ {% with rownum=forloop.counter %}
+
+
+ {% if rownum == 1 %}
+ Stand der Gaszähler (Vormonat) (Nm³)
+ {% elif rownum == 2 %}
+ Gasrückführung (Nm³)
+ {% elif rownum == 3 %}
+ Rückführung flüssig (Lit. L-He)
+ {% elif rownum == 4 %}
+ Sonderrückführungen (Lit. L-He)
+ {% elif rownum == 5 %}
+ Sammelrückführungen (Lit. L-He)
+ {% elif rownum == 6 %}
+ Bestand in Kannen-1 (Lit. L-He)
+ {% elif rownum == 7 %}
+ Summe Bestand (Lit. L-He)
+ {% elif rownum == 8 %}
+ Best. in Kannen Vormonat (Lit. L-He)
+ {% elif rownum == 9 %}
+ Bezug (Liter L-He)
+ {% elif rownum == 10 %}
+ Rückführ. Soll (Lit. L-He)
+ {% elif rownum == 11 %}
+ Verluste (Soll-Rückf.) (Lit. L-He)
+ {% elif rownum == 12 %}
+ Füllungen warm (Lit. L-He)
+ {% elif rownum == 13 %}
+ Kaltgas Rückgabe (Lit. L-He) – Faktor
+ {% elif rownum == 14 %}
+ Faktor 0.06
+ {% elif rownum == 15 %}
+ Verbraucherverluste (Liter L-He)
+ {% elif rownum == 16 %}
+ %
+ {% endif %}
+
+
+ {% for cell in row.cells %}
+ {% with client_name=cell.client.name|default:"" %}
+ {# Determine if this cell should be editable #}
+ {% if is_start_sheet %}
+
+ {{ cell.value|default:"" }}
+
+ {% elif rownum == 4 or rownum == 6 or rownum == 1 and client_name in "M3 Thiele,M3 Buntkowsky,M3 Gutfleisch" %}
+
+ {{ cell.value|default:"" }}
+
+ {% else %}
+
+ {% if rownum == 2 %}
+ Aufteilung Nach Verbrauch
+ {% else %}
+ {{ cell.value|default:"" }}
+ {% endif %}
+
+ {% endif %}
+ {% endwith %}
+ {% endfor %}
+
+
+ {{ row.sum|default_if_none:"" }}
+
+
+ {% endwith %}
+ {% endfor %}
+
+
+
+
+
+
+
+
+
Bottom Table 1
+
+
+
+ Row Label
+ {% for client in clients %}
+ {{ client.name }}
+ {% endfor %}
+
+
+
+ {% for row in cells_by_table.bottom_1 %}
+
+ Row {{ forloop.counter }}
+ {% for cell in row %}
+
+ {{ cell.value|default:"" }}
+
+ {% endfor %}
+
+ {% endfor %}
+
+
+
+
+
+
Bottom Table 2
+
+
+
+ Row Label
+ {% for client in clients %}
+ {{ client.name }}
+ {% endfor %}
+
+
+
+ {% for row in cells_by_table.bottom_2 %}
+
+ Row {{ forloop.counter }}
+ {% for cell in row %}
+
+ {{ cell.value|default:"" }}
+
+ {% endfor %}
+
+ {% endfor %}
+
+
+
+
+
+
Bottom Table 3
+
+
+
+ Row Label
+ {% for client in clients %}
+ {{ client.name }}
+ {% endfor %}
+
+
+
+ {% for row in cells_by_table.bottom_3 %}
+
+ Row {{ forloop.counter }}
+ {% for cell in row %}
+
+ {{ cell.value|default:"" }}
+
+ {% endfor %}
+
+ {% endfor %}
+
+
+
+
+
+
+
+
Save All Cells
+
+
+
+ Saved cells
+ Calculated cells
+
+
+
+
+
+
+
+
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/sheets/templates/table_one.html b/sheets/templates/table_one.html
index 8218007..aac8fba 100644
--- a/sheets/templates/table_one.html
+++ b/sheets/templates/table_one.html
@@ -191,15 +191,184 @@
.readonly-field {
background-color: #e9ecef;
color: #6c757d;
+ }
+ /* ---- 6-month overview card ---- */
+ .overview-card {
+ background-color: #ffffff;
+ padding: 16px 20px;
+ margin: 20px 0;
+ border-radius: 8px;
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.08);
+ }
+
+ .overview-header {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 12px;
+ align-items: flex-end;
+ margin-bottom: 12px;
+ }
+
+ .overview-header label {
+ font-size: 0.9rem;
+ color: #555;
+ }
+
+ .overview-header select {
+ padding: 6px 8px;
+ border-radius: 4px;
+ border: 1px solid #ccc;
+ }
+
+ .overview-header button {
+ padding: 7px 14px;
+ border-radius: 4px;
+ border: none;
+ background-color: #007bff;
+ color: #fff;
+ cursor: pointer;
+ }
+
+ .overview-header button:hover {
+ background-color: #0056b3;
+ }
+
+ .overview-title {
+ font-size: 1.1rem;
+ font-weight: bold;
+ margin-bottom: 8px;
+ color: #333;
+ }
+
+ .overview-subtitle {
+ font-size: 0.9rem;
+ color: #555;
+ margin-bottom: 8px;
+ }
+
+ .overview-table {
+ width: 100%;
+ border-collapse: collapse;
+ }
+
+ .overview-table th,
+ .overview-table td {
+ padding: 6px 8px;
+ border-bottom: 1px solid #eee;
+ font-size: 0.9rem;
+ }
+
+ .overview-table thead th {
+ background-color: #007bff;
+ color: #fff;
+ text-align: right;
+ }
+
+ .overview-table thead th:first-child {
+ text-align: left;
+ }
+
+ .overview-table tbody tr:hover {
+ background-color: #f8f9ff;
+ }
+
+ .overview-table .number-cell {
+ text-align: right;
+ }
+
+ .overview-table .summary-row {
+ background-color: #f0f4ff;
+ font-weight: bold;
}
+
⇦ Go to Clients
+
+
+
Helium Input – 6 Month Overview
+
+
+
+ {% if overview %}
+
+ Period:
+ {{ overview.start_month }}–{{ overview.end_month }} / {{ overview.year }}
+
+
+
+
+
+ Month
+ {% for g in overview.groups %}
+ {{ g.label }}
+ {% endfor %}
+ Month total
+
+
+
+ {% for row in overview.rows %}
+
+
+ {{ row.month_number }} - {{ row.month_label|slice:":3" }}
+
+ {% for value in row.values %}
+ {{ value|floatformat:2 }}
+ {% endfor %}
+ {{ row.total|floatformat:2 }}
+
+ {% endfor %}
+
+
+ Summe
+ {% for total in overview.group_totals %}
+ {{ total|floatformat:2 }}
+ {% endfor %}
+ {{ overview.grand_total|floatformat:2 }}
+
+
+
+ {% else %}
+
+ No data yet – choose a year and start month and click “Show overview”.
+
+ {% endif %}
+
+
+
Helium Input
Add Row
@@ -218,7 +387,8 @@
-
+
+
@@ -236,7 +406,8 @@
L-He
L-He zus.
L-He ges.
- Date Joined
+ Date
+ Month
Actions
@@ -256,7 +427,16 @@
{{ entry.lhe|floatformat:6 }}
{{ entry.lhe_zus|floatformat:3 }}
{{ entry.lhe_ges|floatformat:6 }}
- {{ entry.date_joined|date:"Y-m-d" }}
+
+ {% if entry.date %}
+ {{ entry.date|date:"d.m.Y" }}
+ {% endif %}
+
+
+ {% if entry.date %}
+ {{ entry.date|date:"m" }} {# e.g. 01–12 #}
+ {% endif %}
+
Edit
Delete
@@ -645,6 +825,7 @@
${response.lhe_zus}
${response.lhe_ges}
${response.date || ''}
+ ${response.month || ''}
Edit
Delete
@@ -805,6 +986,7 @@
row.find('td:eq(11)').text(response.lhe_zus);
row.find('td:eq(12)').text(response.lhe_ges);
row.find('td:eq(13)').text(response.date || '');
+ row.find('td:eq(14)').text(response.month || '');
$('#edit-popup-one').fadeOut();
} else {
alert('Error: ' + (response.message || 'Failed to update entry'));
diff --git a/sheets/urls.py b/sheets/urls.py
index 1663a7b..4aa8172 100644
--- a/sheets/urls.py
+++ b/sheets/urls.py
@@ -1,15 +1,26 @@
from django.urls import path
+from .views import DebugTopRightView
+from .views import SaveCellsView
from . import views
-# Create your URLs here.
+
urlpatterns = [
- path('', views.clients_list, name='clients_list'), # Main page
- path('table-one/', views.table_one_view, name='table_one'), # Table One
- path('table-two/', views.table_two_view, name='table_two'), # Table Two
+ path('', views.clients_list, name='clients_list'),
+ path('table-one/', views.table_one_view, name='table_one'),
+ path('table-two/', views.table_two_view, name='table_two'),
path('add-entry//', views.add_entry, name='add_entry'),
path('update-entry//', views.update_entry, name='update_entry'),
path('delete-entry//', views.delete_entry, name='delete_entry'),
path('betriebskosten/', views.betriebskosten_list, name='betriebskosten_list'),
path('betriebskosten/create/', views.betriebskosten_create, name='betriebskosten_create'),
path('betriebskosten/delete/', views.betriebskosten_delete, name='betriebskosten_delete'),
-
-]
+ path('check-sheets/', views.CheckSheetView.as_view(), name='check_sheets'),
+ path('quick-debug/', views.QuickDebugView.as_view(), name='quick_debug'),
+ path('test-formula/', views.TestFormulaView.as_view(), name='test_formula'),
+ path('simple-debug/', views.SimpleDebugView.as_view(), name='simple_debug'),
+ path('sheet///', views.MonthlySheetView.as_view(), name='monthly_sheet'),
+ path('summary///', views.SummarySheetView.as_view(), name='summary_sheet'),
+ path("save-cells/", SaveCellsView.as_view(), name="save_cells"),
+ path('calculate/', views.CalculateView.as_view(), name='calculate'),
+ path('debug-calculation/', views.DebugCalculationView.as_view(), name='debug_calculation'),
+ path('debug-top-right/', DebugTopRightView.as_view(), name='debug_top_right'),
+]
\ No newline at end of file
diff --git a/sheets/views.py b/sheets/views.py
index 6bc86cf..4a7898f 100644
--- a/sheets/views.py
+++ b/sheets/views.py
@@ -1,22 +1,1955 @@
-from django.shortcuts import render ,redirect
+from django.shortcuts import render, redirect
from django.db.models import Sum, Value, DecimalField
from django.http import JsonResponse
from django.db.models import Q
from decimal import Decimal, InvalidOperation
from django.apps import apps
from datetime import date, datetime
+import calendar
from django.utils import timezone
-from .models import Client, SecondTableEntry, Institute
+from django.views.generic import TemplateView, View
+from .models import (
+ Client, SecondTableEntry, Institute, ExcelEntry,
+ Betriebskosten, MonthlySheet, Cell, CellReference
+)
from django.db.models import Sum
from django.urls import reverse
from django.db.models.functions import Coalesce
from .forms import BetriebskostenForm
-from .models import Betriebskosten
-from django.utils.dateparse import parse_date
+from django.utils.dateparse import parse_date
+from django.contrib.auth.mixins import LoginRequiredMixin
+FIRST_SHEET_YEAR = 2025
+FIRST_SHEET_MONTH = 1
+CLIENT_GROUPS = {
+ 'ikp': {
+ 'label': 'IKP',
+ # exactly as in the Clients admin
+ 'names': ['IKP'],
+ },
+ 'phys_chem_bunt_fohrer': {
+ 'label': 'Buntkowsky + Dr. Fohrer',
+ # include all variants you might have used for Buntkowsky
+ 'names': [
+ 'AG Buntk.', # the one in your new entry
+ 'AG Buntkowsky.', # from your original list
+ 'AG Buntkowsky',
+ 'Dr. Fohrer',
+ ],
+ },
+ 'mawi_alff_gutfleisch': {
+ 'label': 'Alff + AG Gutfleisch',
+ # include both short and full forms
+ 'names': [
+ 'AG Alff',
+ 'AG Gutfl.',
+ 'AG Gutfleisch',
+ ],
+ },
+ 'm3_group': {
+ 'label': 'M3 Buntkowsky + M3 Thiele + M3 Gutfleisch',
+ 'names': [
+ 'M3 Buntkowsky',
+ 'M3 Thiele',
+ 'M3 Gutfleisch',
+ ],
+ },
+}
+
+# Add this CALCULATION_CONFIG at the top of views.py
+
+CALCULATION_CONFIG = {
+ 'top_left': {
+ # Row mappings: Django row_index (0-based) to Excel row
+ # Excel B4 -> Django row_index 1 (UI row 2)
+ # Excel B5 -> Django row_index 2 (UI row 3)
+ # Excel B6 -> Django row_index 3 (UI row 4)
+
+ # B6 (row_index 3) = B5 (row_index 2) / 0.75
+ 3: "2 / 0.75",
+
+ # B11 (row_index 10) = B9 (row_index 8)
+ 10: "8",
+
+ # B14 (row_index 13) = B13 (row_index 12) - B11 (row_index 10) + B12 (row_index 11)
+ 13: "12 - 10 + 11",
+
+ # Note: B5, B17, B19, B20 require IF logic, so they'll be handled separately
+ },
+
+ # other tables (top_right, bottom_1, ...) stay as they are
+
+
+ ' top_right': {
+ # UI Row 1 (Excel Row 4): Stand der Gaszähler (Vormonat) (Nm³)
+ 0: {
+ 'L': "9 / (9 + 9) if (9 + 9) > 0 else 0", # L4 = L13/(L13+M13)
+ 'M': "9 / (9 + 9) if (9 + 9) > 0 else 0", # M4 = M13/(L13+M13)
+ 'N': "9 / (9 + 9) if (9 + 9) > 0 else 0", # N4 = N13/(N13+O13)
+ 'O': "9 / (9 + 9) if (9 + 9) > 0 else 0", # O4 = O13/(N13+O13)
+ 'P': None, # Editable
+ 'Q': None, # Editable
+ 'R': None, # Editable
+ },
+
+ # UI Row 2 (Excel Row 5): Gasrückführung (Nm³)
+ 1: {
+ 'L': "4", # L5 = L8
+ 'M': "4", # M5 = L8 (merged)
+ 'N': "4", # N5 = N8
+ 'O': "4", # O5 = N8 (merged)
+ 'P': "4 * 0", # P5 = P8 * P4
+ 'Q': "4 * 0", # Q5 = P8 * Q4
+ 'R': "4 * 0", # R5 = P8 * R4
+ },
+
+ # UI Row 3 (Excel Row 6): Rückführung flüssig (Lit. L-He)
+ 2: {
+ 'L': "4", # L6 = L8 (Sammelrückführungen)
+ 'M': "4",
+ 'N': "4",
+ 'O': "4",
+ 'P': "4",
+ 'Q': "4",
+ 'R': "4",
+},
+
+ # UI Row 4 (Excel Row 7): Sonderrückführungen (Lit. L-He) - EDITABLE
+ 3: {
+ 'L': None,
+ 'M': None,
+ 'N': None,
+ 'O': None,
+ 'P': None,
+ 'Q': None,
+ 'R': None,
+ },
+
+ # UI Row 5 (Excel Row 8): Sammelrückführungen (Lit. L-He)
+ 4: {
+ 'L': None, # Will be populated from ExcelEntry
+ 'M': None,
+ 'N': None,
+ 'O': None,
+ 'P': None,
+ 'Q': None,
+ 'R': None,
+ },
+
+ # UI Row 6 (Excel Row 9): Bestand in Kannen-1 (Lit. L-He) - EDITABLE
+ 5: {
+ 'L': None,
+ 'M': None,
+ 'N': None,
+ 'O': None,
+ 'P': None,
+ 'Q': None,
+ 'R': None,
+ },
+
+ # UI Row 7 (Excel Row 10): Summe Bestand (Lit. L-He)
+ 6: {
+ 'L': None,
+ 'M': None,
+ 'N': None,
+ 'O': None,
+ 'P': None,
+ 'Q': None,
+ 'R': None,
+ },
+
+ # UI Row 11 (Excel Row 14): Rückführ. Soll (Lit. L-He)
+ # handled in calculate_top_right_dependents (merged pairs + M3)
+ 10: {
+ 'L': None,
+ 'M': None,
+ 'N': None,
+ 'O': None,
+ 'P': None,
+ 'Q': None,
+ 'R': None,
+ },
+
+ # UI Row 12 (Excel Row 15): Verluste (Soll-Rückf.) (Lit. L-He)
+ # handled in calculate_top_right_dependents
+ 11: {
+ 'L': None,
+ 'M': None,
+ 'N': None,
+ 'O': None,
+ 'P': None,
+ 'Q': None,
+ 'R': None,
+ },
+
+ # UI Row 14 (Excel Row 17): Kaltgas Rückgabe (Lit. L-He) – Faktor
+ # handled in calculate_top_right_dependents (different formulas for pair 1 vs pair 2 + M3)
+ 13: {
+ 'L': None,
+ 'M': None,
+ 'N': None,
+ 'O': None,
+ 'P': None,
+ 'Q': None,
+ 'R': None,
+ },
+
+ # UI Row 16 (Excel Row 19): Verbraucherverluste (Liter L-He)
+ # handled in calculate_top_right_dependents
+ 15: {
+ 'L': None,
+ 'M': None,
+ 'N': None,
+ 'O': None,
+ 'P': None,
+ 'Q': None,
+ 'R': None,
+ },
+
+ # UI Row 17 (Excel Row 20): %
+ # handled in calculate_top_right_dependents
+ 16: {
+ 'L': None,
+ 'M': None,
+ 'N': None,
+ 'O': None,
+ 'P': None,
+ 'Q': None,
+ 'R': None,
+ }
+},
+ 'bottom_1': {
+ 5: "4 + 3 + 2",
+ 8: "7 - 6",
+ },
+ 'bottom_2': {
+ 3: "1 + 2",
+ 6: "5 - 4",
+ },
+ 'bottom_3': {
+ 2: "0 + 1",
+ 5: "3 + 4",
+ },
+ # Special configuration for summation column (last column)
+ 'summation_column': {
+ # For each row that should be summed across columns
+ 'rows_to_sum': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23], # All rows
+ # OR specify specific rows:
+ # 'rows_to_sum': [0, 5, 10, 15, 20], # Only specific rows
+ # The last column index (0-based)
+ 'sum_column_index': 5, # 6th column (0-5) since you have 6 clients
+ }
+}
+
+def get_group_clients(group_key):
+ """Return queryset of clients that belong to a logical group."""
+ from .models import Client # local import to avoid circulars
+
+ group = CLIENT_GROUPS.get(group_key)
+ if not group:
+ return Client.objects.none()
+ return Client.objects.filter(name__in=group['names'])
+
+def calculate_summation(sheet, table_type, row_index, sum_column_index):
+ """Calculate summation for a row, with special handling for % row"""
+ from decimal import Decimal
+ from .models import Cell
+
+ try:
+ # Special case: top_left, % row (Excel B20 -> row_index 19)
+ if table_type == 'top_left' and row_index == 19:
+ # K13 = sum of row 13 (Excel B13 -> row_index 12) across all clients
+ cells_row13 = Cell.objects.filter(
+ sheet=sheet,
+ table_type='top_left',
+ row_index=12, # Excel B13 = row_index 12
+ column_index__lt=sum_column_index # Exclude sum column itself
+ )
+ total_13 = Decimal('0')
+ for cell in cells_row13:
+ if cell.value is not None:
+ total_13 += Decimal(str(cell.value))
+
+ # K19 = sum of row 19 (Excel B19 -> row_index 18) across all clients
+ cells_row19 = Cell.objects.filter(
+ sheet=sheet,
+ table_type='top_left',
+ row_index=18, # Excel B19 = row_index 18
+ column_index__lt=sum_column_index
+ )
+ total_19 = Decimal('0')
+ for cell in cells_row19:
+ if cell.value is not None:
+ total_19 += Decimal(str(cell.value))
+
+ # Calculate: IF(K13=0; 0; K19/K13)
+ if total_13 == 0:
+ return Decimal('0')
+ return total_19 / total_13
+
+ # Normal summation for other rows
+ cells_in_row = Cell.objects.filter(
+ sheet=sheet,
+ table_type=table_type,
+ row_index=row_index,
+ column_index__lt=sum_column_index
+ )
+
+ total = Decimal('0')
+ for cell in cells_in_row:
+ if cell.value is not None:
+ total += Decimal(str(cell.value))
+
+ return total
+
+ except Exception as e:
+ print(f"Error calculating summation for {table_type}[{row_index}]: {e}")
+ return None
+
+# Helper function for calculations
+def evaluate_formula(formula, values_dict):
+ """
+ Safely evaluate a formula like "10 + 9" where numbers are row indices
+ values_dict: {row_index: decimal_value}
+ """
+ from decimal import Decimal
+ import re
+
+ try:
+ # Create a copy of the formula to work with
+ expr = formula
+
+ # Find all row numbers in the formula
+ row_refs = re.findall(r'\b\d+\b', expr)
+
+ for row_ref in row_refs:
+ row_num = int(row_ref)
+ if row_num in values_dict and values_dict[row_num] is not None:
+ # Replace row reference with actual value
+ expr = expr.replace(row_ref, str(values_dict[row_num]))
+ else:
+ # Missing value - can't calculate
+ return None
+
+ # Evaluate the expression
+ # Note: In production, use a safer evaluator like `asteval`
+ result = eval(expr, {"__builtins__": {}}, {})
+
+ # Convert to Decimal with proper rounding
+ return Decimal(str(round(result, 6)))
+
+ except Exception:
+ return None
+
+# Monthly Sheet View
+class MonthlySheetView(TemplateView):
+ template_name = 'monthly_sheet.html'
+ def populate_helium_input_to_top_right(self, sheet):
+ """Populate bezug data from SecondTableEntry to top-right table (row 8 = Excel row 12)"""
+ from .models import SecondTableEntry, Cell, Client
+ from django.db.models.functions import Coalesce
+ from decimal import Decimal
+
+ year = sheet.year
+ month = sheet.month
+
+ TOP_RIGHT_CLIENTS = [
+ "Dr. Fohrer", # Column index 0 (L)
+ "AG Buntk.", # Column index 1 (M)
+ "AG Alff", # Column index 2 (N)
+ "AG Gutfl.", # Column index 3 (O)
+ "M3 Thiele", # Column index 4 (P)
+ "M3 Buntkowsky", # Column index 5 (Q)
+ "M3 Gutfleisch", # Column index 6 (R)
+ ]
+
+ # For each client in top-right table
+ for client_name in TOP_RIGHT_CLIENTS:
+ try:
+ client = Client.objects.get(name=client_name)
+ column_index = TOP_RIGHT_CLIENTS.index(client_name)
+
+ # Calculate total LHe_output for this client in this month from SecondTableEntry
+ total_lhe_output = SecondTableEntry.objects.filter(
+ client=client,
+ date__year=year,
+ date__month=month
+ ).aggregate(
+ total=Coalesce(Sum('lhe_output'), Decimal('0'))
+ )['total']
+
+ # Get or create the cell for row_index 8 (Excel row 12) - Bezug
+ cell, created = Cell.objects.get_or_create(
+ sheet=sheet,
+ table_type='top_right',
+ client=client,
+ row_index=8, # Bezug row (Excel row 12)
+ column_index=column_index,
+ defaults={'value': total_lhe_output}
+ )
+
+ if not created and cell.value != total_lhe_output:
+ cell.value = total_lhe_output
+ cell.save()
+
+ except Client.DoesNotExist:
+ continue
+
+ # After populating bezug, trigger calculation for all dependent cells
+ # Get any cell to start the calculation
+ first_cell = Cell.objects.filter(
+ sheet=sheet,
+ table_type='top_right'
+ ).first()
+
+ if first_cell:
+ save_view = SaveCellsView()
+ save_view.calculate_top_right_dependents(sheet, first_cell)
+
+ return True
+ def calculate_bezug_from_entries(self, sheet, year, month):
+ """Calculate B11 (Bezug) from SecondTableEntry for all clients - ONLY for non-start sheets"""
+ from .models import SecondTableEntry, Cell, Client
+ from django.db.models import Sum
+ from django.db.models.functions import Coalesce
+ from decimal import Decimal
+
+ # Check if this is the start sheet
+ if year == 2025 and month == 1:
+ return # Don't auto-calculate for start sheet
+
+ for client in Client.objects.all():
+ # Calculate total LHe output for this client in this month
+ lhe_output_sum = SecondTableEntry.objects.filter(
+ client=client,
+ date__year=year,
+ date__month=month
+ ).aggregate(
+ total=Coalesce(Sum('lhe_output'), Decimal('0'))
+ )['total']
+
+ # Update B11 cell (row_index 8 = UI Row 9)
+ b11_cell = Cell.objects.filter(
+ sheet=sheet,
+ table_type='top_left',
+ client=client,
+ row_index=8 # Excel B11
+ ).first()
+
+ if b11_cell and (b11_cell.value != lhe_output_sum or b11_cell.value is None):
+ b11_cell.value = lhe_output_sum
+ b11_cell.save()
+
+ # Also trigger dependent calculations
+ from .views import SaveCellsView
+ save_view = SaveCellsView()
+ save_view.calculate_top_left_dependents(sheet, b11_cell)
+ # In MonthlySheetView.get_context_data() method, update the TOP_RIGHT_CLIENTS and row count:
+
+
+
+ return True
+ def get_context_data(self, **kwargs):
+ from decimal import Decimal
+ context = super().get_context_data(**kwargs)
+ year = self.kwargs.get('year', datetime.now().year)
+ month = self.kwargs.get('month', datetime.now().month)
+ is_start_sheet = (year == FIRST_SHEET_YEAR and month == FIRST_SHEET_MONTH)
+
+ # Get or create the monthly sheet
+ sheet, created = MonthlySheet.objects.get_or_create(
+ year=year, month=month
+ )
+
+ # All clients (used for bottom tables etc.)
+ clients = Client.objects.all().order_by('name')
+
+ # Pre-fill cells if creating new sheet
+ if created:
+ self.initialize_sheet_cells(sheet, clients)
+
+ # Apply previous month links (for B4 and B12)
+ self.apply_previous_month_links(sheet, year, month)
+ self.calculate_bezug_from_entries(sheet, year, month)
+ self.populate_helium_input_to_top_right(sheet)
+ self.apply_previous_month_links_top_right(sheet, year, month)
+
+ # Define client groups
+ TOP_LEFT_CLIENTS = [
+ "AG Vogel",
+ "AG Halfm",
+ "IKP",
+ ]
+
+ TOP_RIGHT_CLIENTS = [
+ "Dr. Fohrer",
+ "AG Buntk.",
+ "AG Alff",
+ "AG Gutfl.",
+ "M3 Thiele",
+ "M3 Buntkowsky",
+ "M3 Gutfleisch",
+ ]
+
+ # Update row counts in build_group_rows function
+ def build_group_rows(sheet, table_type, client_names):
+ """Build rows for display in monthly sheet."""
+ from decimal import Decimal
+ from .models import Cell
+
+ rows = []
+
+ # Determine row count - UPDATED for top_right
+ row_counts = {
+ 'top_left': 16,
+ 'top_right': 16, # Changed from 16 to 17 to include all rows (0-16)
+ 'bottom_1': 10,
+ 'bottom_2': 10,
+ 'bottom_3': 10,
+ }
+ row_count = row_counts.get(table_type, 0)
+
+ # Get all cells for this sheet and table
+ all_cells = Cell.objects.filter(
+ sheet=sheet,
+ table_type=table_type
+ ).select_related('client')
+
+ # Group cells by row index
+ cells_by_row = {}
+ for cell in all_cells:
+ if cell.row_index not in cells_by_row:
+ cells_by_row[cell.row_index] = {}
+ cells_by_row[cell.row_index][cell.client.name] = cell
+
+ # For each row (including row 7)
+ for row_idx in range(row_count):
+ display_cells = []
+ row_cells_dict = cells_by_row.get(row_idx, {})
+
+ # Build cells in the requested client order
+ for name in client_names:
+ cell = row_cells_dict.get(name)
+ display_cells.append(cell)
+
+ # Calculate sum for this row
+ sum_column_index = len(client_names)
+
+ # Calculate sum for the row
+ sum_value = None
+ total = Decimal('0')
+ has_value = False
+
+ for cell in display_cells:
+ if cell and cell.value is not None:
+ try:
+ total += Decimal(str(cell.value))
+ has_value = True
+ except:
+ pass
+
+ if has_value:
+ sum_value = total
+
+ rows.append({
+ 'cells': display_cells,
+ 'sum': sum_value,
+ 'row_index': row_idx, # Add this for debugging
+ })
+
+ return rows
+
+ # Now call the local function
+ top_left_rows = build_group_rows(sheet, 'top_left', TOP_LEFT_CLIENTS)
+ top_right_rows = build_group_rows(sheet, 'top_right', TOP_RIGHT_CLIENTS)
+
+ # Get cells for bottom tables
+ cells_by_table = self.get_cells_by_table(sheet)
+
+ context.update({
+ 'sheet': sheet,
+ 'clients': clients,
+ 'year': year,
+ 'month': month,
+ 'month_name': calendar.month_name[month],
+ 'prev_month': self.get_prev_month(year, month),
+ 'next_month': self.get_next_month(year, month),
+ 'cells_by_table': cells_by_table,
+ 'top_left_headers': TOP_LEFT_CLIENTS + ['Sum'],
+ 'top_right_headers': TOP_RIGHT_CLIENTS + ['Sum'],
+ 'top_left_rows': top_left_rows,
+ 'top_right_rows': top_right_rows,
+ 'is_start_sheet': is_start_sheet,
+ })
+ return context
+
+ def get_cells_by_table(self, sheet):
+ """Organize cells by table type for easy template rendering"""
+ cells = sheet.cells.select_related('client').all()
+ organized = {
+ 'top_left': [[] for _ in range(16)],
+ 'top_right': [[] for _ in range(16)], # now 16 rows
+ 'bottom_1': [[] for _ in range(10)],
+ 'bottom_2': [[] for _ in range(10)],
+ 'bottom_3': [[] for _ in range(10)],
+ }
+
+ for cell in cells:
+ if cell.table_type not in organized:
+ continue
+
+ max_rows = len(organized[cell.table_type])
+ if cell.row_index < 0 or cell.row_index >= max_rows:
+ # This is an "extra" cell from an older layout (e.g. top_left rows 18–23)
+ continue
+
+ row_list = organized[cell.table_type][cell.row_index]
+ while len(row_list) <= cell.column_index:
+ row_list.append(None)
+
+ row_list[cell.column_index] = cell
+
+ return organized
-from .models import Client, SecondTableEntry
+ def initialize_sheet_cells(self, sheet, clients):
+ """Create all empty cells for a new monthly sheet"""
+ cells_to_create = []
+ summation_config = CALCULATION_CONFIG.get('summation_column', {})
+ sum_column_index = summation_config.get('sum_column_index', len(clients) - 1) # Last column
+
+ # For each table type and row
+ table_configs = [
+ ('top_left', 16),
+ ('top_right', 16),
+ ('bottom_1', 10),
+ ('bottom_2', 10),
+ ('bottom_3', 10),
+ ]
+
+ for table_type, row_count in table_configs:
+ for row_idx in range(row_count):
+ for col_idx, client in enumerate(clients):
+ is_summation = (col_idx == sum_column_index)
+ cells_to_create.append(Cell(
+ sheet=sheet,
+ client=client,
+ table_type=table_type,
+ row_index=row_idx,
+ column_index=col_idx,
+ value=None,
+ is_formula=is_summation, # Mark summation cells as formulas
+ ))
+
+ # Bulk create all cells at once
+ Cell.objects.bulk_create(cells_to_create)
+ def get_prev_month(self, year, month):
+ """Get previous month year and month"""
+ if month == 1:
+ return {'year': year - 1, 'month': 12}
+ return {'year': year, 'month': month - 1}
+
+ def get_next_month(self, year, month):
+ """Get next month year and month"""
+ if month == 12:
+ return {'year': year + 1, 'month': 1}
+ return {'year': year, 'month': month + 1}
+ def apply_previous_month_links(self, sheet, year, month):
+ """
+ For non-start sheets:
+ B4 (row 2) = previous sheet B3 (row 1)
+ B10 (row 8) = previous sheet B9 (row 7)
+ """
+ # Do nothing on the first sheet
+ if year == FIRST_SHEET_YEAR and month == FIRST_SHEET_MONTH:
+ return
+ # Figure out previous month
+ if month == 1:
+ prev_year = year - 1
+ prev_month = 12
+ else:
+ prev_year = year
+ prev_month = month - 1
+
+ from .models import MonthlySheet, Cell, Client
+
+ prev_sheet = MonthlySheet.objects.filter(
+ year=prev_year,
+ month=prev_month
+ ).first()
+
+ if not prev_sheet:
+ # No previous sheet created yet → nothing to copy
+ return
+
+ # For each client, copy values
+ for client in Client.objects.all():
+ # B3(prev) → B4(curr): UI row 1 → row 2 → row_index 0 → 1
+ prev_b3 = Cell.objects.filter(
+ sheet=prev_sheet,
+ table_type='top_left',
+ client=client,
+ row_index=0, # UI row 1
+ ).first()
+
+ curr_b4 = Cell.objects.filter(
+ sheet=sheet,
+ table_type='top_left',
+ client=client,
+ row_index=1, # UI row 2
+ ).first()
+
+ if prev_b3 and curr_b4:
+ curr_b4.value = prev_b3.value
+ curr_b4.save()
+
+ # B9(prev) → B10(curr): UI row 7 → row 8 → row_index 6 → 7
+ prev_b9 = Cell.objects.filter(
+ sheet=prev_sheet,
+ table_type='top_left',
+ client=client,
+ row_index=6, # UI row 7 (Summe Bestand)
+ ).first()
+
+ curr_b10 = Cell.objects.filter(
+ sheet=sheet,
+ table_type='top_left',
+ client=client,
+ row_index=7, # UI row 8 (Best. in Kannen Vormonat)
+ ).first()
+
+ if prev_b9 and curr_b10:
+ curr_b10.value = prev_b9.value
+ curr_b10.save()
+
+ def apply_previous_month_links_top_right(self, sheet, year, month):
+ """
+ top_right row 7: Best. in Kannen Vormonat (Lit. L-He)
+ = previous sheet's Summe Bestand (row 6).
+
+ For merged pairs:
+ - Dr. Fohrer + AG Buntk. share the SAME value (from previous month's AG Buntk. or Dr. Fohrer)
+ - AG Alff + AG Gutfl. share the SAME value (from previous month's AG Alff or AG Gutfl.)
+ M3 clients just copy their own value.
+ """
+ from .models import MonthlySheet, Cell, Client
+ from decimal import Decimal
+
+ # Do nothing on first sheet
+ if year == FIRST_SHEET_YEAR and month == FIRST_SHEET_MONTH:
+ return
+
+ # find previous month
+ if month == 1:
+ prev_year = year - 1
+ prev_month = 12
+ else:
+ prev_year = year
+ prev_month = month - 1
+
+ prev_sheet = MonthlySheet.objects.filter(
+ year=prev_year,
+ month=prev_month
+ ).first()
+
+ if not prev_sheet:
+ return # nothing to copy from
+
+ TOP_RIGHT_CLIENTS = [
+ "Dr. Fohrer",
+ "AG Buntk.",
+ "AG Alff",
+ "AG Gutfl.",
+ "M3 Thiele",
+ "M3 Buntkowsky",
+ "M3 Gutfleisch",
+ ]
+
+ # Helper function to get a cell value
+ def get_cell_value(sheet_obj, client_name, row_index):
+ client_obj = Client.objects.filter(name=client_name).first()
+ if not client_obj:
+ return None
+
+ try:
+ col_idx = TOP_RIGHT_CLIENTS.index(client_name)
+ except ValueError:
+ return None
+
+ cell = Cell.objects.filter(
+ sheet=sheet_obj,
+ table_type='top_right',
+ client=client_obj,
+ row_index=row_index,
+ column_index=col_idx,
+ ).first()
+
+ return cell.value if cell else None
+
+ # Helper function to set a cell value
+ def set_cell_value(sheet_obj, client_name, row_index, value):
+ client_obj = Client.objects.filter(name=client_name).first()
+ if not client_obj:
+ return False
+
+ try:
+ col_idx = TOP_RIGHT_CLIENTS.index(client_name)
+ except ValueError:
+ return False
+
+ cell, created = Cell.objects.get_or_create(
+ sheet=sheet_obj,
+ table_type='top_right',
+ client=client_obj,
+ row_index=row_index,
+ column_index=col_idx,
+ defaults={'value': value}
+ )
+
+ if not created and cell.value != value:
+ cell.value = value
+ cell.save()
+ elif created:
+ cell.save()
+
+ return True
+
+ # ----- Pair 1: Dr. Fohrer + AG Buntk. -----
+ # Get previous month's Summe Bestand (row 6) for either client in the pair
+ pair1_prev_val = None
+
+ # Try AG Buntk. first
+ prev_buntk_val = get_cell_value(prev_sheet, "AG Buntk.", 6)
+ if prev_buntk_val is not None:
+ pair1_prev_val = prev_buntk_val
+ else:
+ # Try Dr. Fohrer if AG Buntk. is empty
+ prev_fohrer_val = get_cell_value(prev_sheet, "Dr. Fohrer", 6)
+ if prev_fohrer_val is not None:
+ pair1_prev_val = prev_fohrer_val
+
+ # Apply the value to both clients in the pair
+ if pair1_prev_val is not None:
+ set_cell_value(sheet, "Dr. Fohrer", 7, pair1_prev_val)
+ set_cell_value(sheet, "AG Buntk.", 7, pair1_prev_val)
+
+ # ----- Pair 2: AG Alff + AG Gutfl. -----
+ pair2_prev_val = None
+
+ # Try AG Alff first
+ prev_alff_val = get_cell_value(prev_sheet, "AG Alff", 6)
+ if prev_alff_val is not None:
+ pair2_prev_val = prev_alff_val
+ else:
+ # Try AG Gutfl. if AG Alff is empty
+ prev_gutfl_val = get_cell_value(prev_sheet, "AG Gutfl.", 6)
+ if prev_gutfl_val is not None:
+ pair2_prev_val = prev_gutfl_val
+
+ # Apply the value to both clients in the pair
+ if pair2_prev_val is not None:
+ set_cell_value(sheet, "AG Alff", 7, pair2_prev_val)
+ set_cell_value(sheet, "AG Gutfl.", 7, pair2_prev_val)
+
+ # ----- M3 clients: copy their own Summe Bestand -----
+ for name in ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"]:
+ prev_val = get_cell_value(prev_sheet, name, 6)
+ if prev_val is not None:
+ set_cell_value(sheet, name, 7, prev_val)
+# Add this helper function to views.py
+def get_factor_value(table_type, row_index):
+ """Get factor value (like 0.06 for top_left row 17)"""
+ factors = {
+ ('top_left', 17): Decimal('0.06'), # A18 in Excel (UI row 17, 0-based index 16)
+ }
+ return factors.get((table_type, row_index), Decimal('0'))
+# Save Cells View
+# views.py - Updated SaveCellsView
+# views.py - Update SaveCellsView class
+def debug_cell_values(self, sheet, client_id):
+ """Debug method to check cell values"""
+ from .models import Cell
+ cells = Cell.objects.filter(
+ sheet=sheet,
+ table_type='top_left',
+ client_id=client_id
+ ).order_by('row_index')
+
+ debug_info = {}
+ for cell in cells:
+ debug_info[f"row_{cell.row_index}"] = {
+ 'value': str(cell.value) if cell.value else 'None',
+ 'ui_row': cell.row_index + 1,
+ 'excel_ref': f"B{cell.row_index + 3}"
+ }
+
+ return debug_info
+class DebugCalculationView(View):
+ """Debug view to test calculations directly"""
+ def get(self, request):
+ sheet_id = request.GET.get('sheet_id', 1)
+ client_name = request.GET.get('client', 'AG Vogel')
+
+ try:
+ sheet = MonthlySheet.objects.get(id=sheet_id)
+ client = Client.objects.get(name=client_name)
+
+ # Get SaveCellsView instance
+ save_view = SaveCellsView()
+
+ # Create a dummy cell to trigger calculations
+ dummy_cell = Cell.objects.filter(
+ sheet=sheet,
+ table_type='top_left',
+ client=client,
+ row_index=0 # B3
+ ).first()
+
+ if not dummy_cell:
+ return JsonResponse({'error': 'No cells found for this client'})
+
+ # Trigger calculation
+ updated = save_view.calculate_top_left_dependents(sheet, dummy_cell)
+
+ # Get updated cell values
+ cells = Cell.objects.filter(
+ sheet=sheet,
+ table_type='top_left',
+ client=client
+ ).order_by('row_index')
+
+ cell_data = []
+ for cell in cells:
+ cell_data.append({
+ 'row_index': cell.row_index,
+ 'ui_row': cell.row_index + 1,
+ 'excel_ref': f"B{cell.row_index + 3}",
+ 'value': str(cell.value) if cell.value else 'None',
+ 'description': self.get_row_description(cell.row_index)
+ })
+
+ return JsonResponse({
+ 'sheet': f"{sheet.year}-{sheet.month:02d}",
+ 'client': client.name,
+ 'cells': cell_data,
+ 'updated_count': len(updated),
+ 'calculation': 'B5 = IF(B3>0; B3-B4; 0)'
+ })
+
+ except Exception as e:
+ return JsonResponse({'error': str(e)}, status=400)
+
+ def get_row_description(self, row_index):
+ """Get description for row index"""
+ descriptions = {
+ 0: "B3: Stand der Gaszähler (Nm³)",
+ 1: "B4: Stand der Gaszähler (Vormonat) (Nm³)",
+ 2: "B5: Gasrückführung (Nm³)",
+ 3: "B6: Rückführung flüssig (Lit. L-He)",
+ 4: "B7: Sonderrückführungen (Lit. L-He)",
+ 5: "B8: Bestand in Kannen-1 (Lit. L-He)",
+ 6: "B9: Summe Bestand (Lit. L-He)",
+ 7: "B10: Best. in Kannen Vormonat (Lit. L-He)",
+ 8: "B11: Bezug (Liter L-He)",
+ 9: "B12: Rückführ. Soll (Lit. L-He)",
+ 10: "B13: Verluste (Soll-Rückf.) (Lit. L-He)",
+ 11: "B14: Füllungen warm (Lit. L-He)",
+ 12: "B15: Kaltgas Rückgabe (Lit. L-He) – Faktor",
+ 13: "B16: Faktor",
+ 14: "B17: Verbraucherverluste (Liter L-He)",
+ 15: "B18: %"
+ }
+ return descriptions.get(row_index, f"Row {row_index}")
+def recalculate_stand_der_gaszahler(self, sheet):
+ """Recalculate Stand der Gaszähler for all client pairs"""
+ from decimal import Decimal
+
+ # For Dr. Fohrer and AG Buntk. (L & M columns)
+ try:
+ # Get Dr. Fohrer's bezug
+ dr_fohrer_client = Client.objects.get(name="Dr. Fohrer")
+ dr_fohrer_cell = Cell.objects.filter(
+ sheet=sheet,
+ table_type='top_right',
+ client=dr_fohrer_client,
+ row_index=9 # Row 9 = Bezug
+ ).first()
+ L13 = Decimal(str(dr_fohrer_cell.value)) if dr_fohrer_cell and dr_fohrer_cell.value else Decimal('0')
+
+ # Get AG Buntk.'s bezug
+ ag_buntk_client = Client.objects.get(name="AG Buntk.")
+ ag_buntk_cell = Cell.objects.filter(
+ sheet=sheet,
+ table_type='top_right',
+ client=ag_buntk_client,
+ row_index=9
+ ).first()
+ M13 = Decimal(str(ag_buntk_cell.value)) if ag_buntk_cell and ag_buntk_cell.value else Decimal('0')
+
+ total = L13 + M13
+ if total > 0:
+ # Update Dr. Fohrer's row 0
+ dr_fohrer_row0 = Cell.objects.filter(
+ sheet=sheet,
+ table_type='top_right',
+ client=dr_fohrer_client,
+ row_index=0
+ ).first()
+ if dr_fohrer_row0:
+ dr_fohrer_row0.value = L13 / total
+ dr_fohrer_row0.save()
+
+ # Update AG Buntk.'s row 0
+ ag_buntk_row0 = Cell.objects.filter(
+ sheet=sheet,
+ table_type='top_right',
+ client=ag_buntk_client,
+ row_index=0
+ ).first()
+ if ag_buntk_row0:
+ ag_buntk_row0.value = M13 / total
+ ag_buntk_row0.save()
+ except Exception as e:
+ print(f"Error recalculating Stand der Gaszähler for Dr. Fohrer/AG Buntk.: {e}")
+
+ # For AG Alff and AG Gutfl. (N & O columns)
+ try:
+ # Get AG Alff's bezug
+ ag_alff_client = Client.objects.get(name="AG Alff")
+ ag_alff_cell = Cell.objects.filter(
+ sheet=sheet,
+ table_type='top_right',
+ client=ag_alff_client,
+ row_index=9
+ ).first()
+ N13 = Decimal(str(ag_alff_cell.value)) if ag_alff_cell and ag_alff_cell.value else Decimal('0')
+
+ # Get AG Gutfl.'s bezug
+ ag_gutfl_client = Client.objects.get(name="AG Gutfl.")
+ ag_gutfl_cell = Cell.objects.filter(
+ sheet=sheet,
+ table_type='top_right',
+ client=ag_gutfl_client,
+ row_index=9
+ ).first()
+ O13 = Decimal(str(ag_gutfl_cell.value)) if ag_gutfl_cell and ag_gutfl_cell.value else Decimal('0')
+
+ total = N13 + O13
+ if total > 0:
+ # Update AG Alff's row 0
+ ag_alff_row0 = Cell.objects.filter(
+ sheet=sheet,
+ table_type='top_right',
+ client=ag_alff_client,
+ row_index=0
+ ).first()
+ if ag_alff_row0:
+ ag_alff_row0.value = N13 / total
+ ag_alff_row0.save()
+
+ # Update AG Gutfl.'s row 0
+ ag_gutfl_row0 = Cell.objects.filter(
+ sheet=sheet,
+ table_type='top_right',
+ client=ag_gutfl_client,
+ row_index=0
+ ).first()
+ if ag_gutfl_row0:
+ ag_gutfl_row0.value = O13 / total
+ ag_gutfl_row0.save()
+ except Exception as e:
+ print(f"Error recalculating Stand der Gaszähler for AG Alff/AG Gutfl.: {e}")
+# In your SaveCellsView class in views.py
+class DebugTopRightView(View):
+ """Debug view to check top_right calculations"""
+ def get(self, request):
+ sheet_id = request.GET.get('sheet_id', 1)
+ client_name = request.GET.get('client', 'Dr. Fohrer')
+
+ try:
+ sheet = MonthlySheet.objects.get(id=sheet_id)
+ client = Client.objects.get(name=client_name)
+
+ # Get all cells for this client in top_right
+ cells = Cell.objects.filter(
+ sheet=sheet,
+ table_type='top_right',
+ client=client
+ ).order_by('row_index')
+
+ cell_data = []
+ descriptions = {
+ 0: "Stand der Gaszähler (Vormonat)",
+ 1: "Gasrückführung (Nm³)",
+ 2: "Rückführung flüssig",
+ 3: "Sonderrückführungen",
+ 4: "Sammelrückführungen",
+ 5: "Bestand in Kannen-1",
+ 6: "Summe Bestand",
+ 7: "Best. in Kannen Vormonat",
+ 8: "Same as row 9 from prev sheet",
+ 9: "Bezug",
+ 10: "Rückführ. Soll",
+ 11: "Verluste",
+ 12: "Füllungen warm",
+ 13: "Kaltgas Rückgabe",
+ 14: "Faktor 0.06",
+ 15: "Verbraucherverluste",
+ 16: "%"
+ }
+
+ for cell in cells:
+ cell_data.append({
+ 'row_index': cell.row_index,
+ 'ui_row': cell.row_index + 1,
+ 'description': descriptions.get(cell.row_index, f"Row {cell.row_index}"),
+ 'value': str(cell.value) if cell.value else 'Empty',
+ 'cell_id': cell.id
+ })
+
+ # Test calculation
+ row3_cell = cells.filter(row_index=3).first()
+ row5_cell = cells.filter(row_index=5).first()
+ row6_cell = cells.filter(row_index=6).first()
+
+ calculation_info = {
+ 'row3_value': str(row3_cell.value) if row3_cell and row3_cell.value else '0',
+ 'row5_value': str(row5_cell.value) if row5_cell and row5_cell.value else '0',
+ 'row6_value': str(row6_cell.value) if row6_cell and row6_cell.value else '0',
+ 'expected_sum': '0'
+ }
+
+ if row3_cell and row5_cell and row6_cell:
+ row3_val = Decimal(str(row3_cell.value)) if row3_cell.value else Decimal('0')
+ row5_val = Decimal(str(row5_cell.value)) if row5_cell.value else Decimal('0')
+ expected = row3_val + row5_val
+ calculation_info['expected_sum'] = str(expected)
+ calculation_info['is_correct'] = row6_cell.value == expected
+
+ return JsonResponse({
+ 'sheet': f"{sheet.year}-{sheet.month}",
+ 'client': client.name,
+ 'cells': cell_data,
+ 'calculation': calculation_info
+ })
+
+ except Exception as e:
+ return JsonResponse({'error': str(e)}, status=400)
+
+
+class SaveCellsView(View):
+ def post(self, request, *args, **kwargs):
+ """
+ Handle AJAX saves from monthly_sheet.html
+ - Single-cell save: when cell_id is present (blur on one cell)
+ - Bulk save: when the 'Save All Cells' button is used (no cell_id)
+ """
+ try:
+ sheet_id = request.POST.get('sheet_id')
+ if not sheet_id:
+ return JsonResponse({
+ 'status': 'error',
+ 'message': 'Missing sheet_id'
+ })
+
+ sheet = MonthlySheet.objects.get(id=sheet_id)
+
+ # -------- Single-cell update (blur) --------
+ cell_id = request.POST.get('cell_id')
+ if cell_id:
+ value_raw = (request.POST.get('value') or '').strip()
+
+ try:
+ cell = Cell.objects.get(id=cell_id, sheet=sheet)
+ except Cell.DoesNotExist:
+ return JsonResponse({
+ 'status': 'error',
+ 'message': 'Cell not found'
+ })
+
+ # Convert value to Decimal or None
+ if value_raw == '':
+ new_value = None
+ else:
+ try:
+ # Allow comma or dot
+ value_clean = value_raw.replace(',', '.')
+ new_value = Decimal(value_clean)
+ except (InvalidOperation, ValueError):
+ # If conversion fails, treat as empty
+ new_value = None
+
+ old_value = cell.value
+ cell.value = new_value
+ cell.save()
+
+ updated_cells = [{
+ 'id': cell.id,
+ 'value': '' if cell.value is None else str(cell.value),
+ 'is_calculated': cell.is_formula, # model field
+ }]
+
+ # Recalculate dependents depending on table_type
+ if cell.table_type == 'top_left':
+ updated_cells.extend(
+ self.calculate_top_left_dependents(sheet, cell)
+ )
+ elif cell.table_type == 'top_right':
+ updated_cells.extend(
+ self.calculate_top_right_dependents(sheet, cell)
+ )
+ # bottom_1 / bottom_2 / bottom_3 currently have no formulas:
+ # they just save the new value.
+
+ return JsonResponse({
+ 'status': 'success',
+ 'updated_cells': updated_cells
+ })
+
+ # -------- Bulk save (Save All button) --------
+ return self.save_bulk_cells(request, sheet)
+
+ except MonthlySheet.DoesNotExist:
+ return JsonResponse({
+ 'status': 'error',
+ 'message': 'Sheet not found'
+ })
+ except Exception as e:
+ # Generic safety net so the frontend sees an error message
+ return JsonResponse({
+ 'status': 'error',
+ 'message': str(e)
+ })
+
+ # ... your other methods above ...
+
+ # Update the calculate_top_right_dependents method in SaveCellsView class
+
+ def calculate_top_right_dependents(self, sheet, changed_cell):
+ """
+ Recalculate all dependent cells in the top-right table according to Excel formulas.
+
+ Excel rows (4-20) -> 0-based indices (0-15)
+ Rows:
+ 0: Stand der Gaszähler (Vormonat) (Nm³) - shares for M3 clients
+ 1: Gasrückführung (Nm³)
+ 2: Rückführung flüssig (Lit. L-He)
+ 3: Sonderrückführungen (Lit. L-He) - editable
+ 4: Sammelrückführungen (Lit. L-He) - from helium_input groups
+ 5: Bestand in Kannen-1 (Lit. L-He) - editable, merged in pairs
+ 6: Summe Bestand (Lit. L-He) = row 5
+ 7: Best. in Kannen Vormonat (Lit. L-He) - from previous month
+ 8: Bezug (Liter L-He) - from SecondTableEntry
+ 9: Rückführ. Soll (Lit. L-He) - calculated
+ 10: Verluste (Soll-Rückf.) (Lit. L-He) - calculated
+ 11: Füllungen warm (Lit. L-He) - from SecondTableEntry warm outputs
+ 12: Kaltgas Rückgabe (Lit. L-He) – Faktor - calculated
+ 13: Faktor 0.06 - fixed
+ 14: Verbraucherverluste (Liter L-He) - calculated
+ 15: % - calculated
+ """
+ from decimal import Decimal
+ from django.db.models import Sum, Count
+ from django.db.models.functions import Coalesce
+ from .models import Client, Cell, ExcelEntry, SecondTableEntry
+
+ TOP_RIGHT_CLIENTS = [
+ "Dr. Fohrer", # L
+ "AG Buntk.", # M (merged with L)
+ "AG Alff", # N
+ "AG Gutfl.", # O (merged with N)
+ "M3 Thiele", # P
+ "M3 Buntkowsky", # Q
+ "M3 Gutfleisch", # R
+ ]
+
+ # Define merged pairs
+ MERGED_PAIRS = [
+ ("Dr. Fohrer", "AG Buntk."), # L and M are merged
+ ("AG Alff", "AG Gutfl."), # N and O are merged
+ ]
+
+ # M3 clients (not merged, calculated individually)
+ M3_CLIENTS = ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"]
+
+ # Groups for Sammelrückführungen (helium_input)
+ HELIUM_INPUT_GROUPS = {
+ "fohrer_buntk": ["Dr. Fohrer", "AG Buntk."],
+ "alff_gutfl": ["AG Alff", "AG Gutfl."],
+ "m3": ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"],
+ }
+
+ year = sheet.year
+ month = sheet.month
+ factor = Decimal('0.06') # Fixed factor from Excel
+ updated_cells = []
+
+ # Helper functions
+ def get_val(client_name, row_idx):
+ """Get cell value for a client and row"""
+ try:
+ client = Client.objects.get(name=client_name)
+ col_idx = TOP_RIGHT_CLIENTS.index(client_name)
+ cell = Cell.objects.filter(
+ sheet=sheet,
+ table_type='top_right',
+ client=client,
+ row_index=row_idx,
+ column_index=col_idx
+ ).first()
+ if cell and cell.value is not None:
+ return Decimal(str(cell.value))
+ except (Client.DoesNotExist, ValueError, KeyError):
+ pass
+ return Decimal('0')
+
+ def set_val(client_name, row_idx, value, is_calculated=True):
+ """Set cell value for a client and row"""
+ try:
+ client = Client.objects.get(name=client_name)
+ col_idx = TOP_RIGHT_CLIENTS.index(client_name)
+
+ cell, created = Cell.objects.get_or_create(
+ sheet=sheet,
+ table_type='top_right',
+ client=client,
+ row_index=row_idx,
+ column_index=col_idx,
+ defaults={'value': value}
+ )
+
+ # Only update if value changed
+ if not created and cell.value != value:
+ cell.value = value
+ cell.save()
+ elif created:
+ cell.save()
+
+ updated_cells.append({
+ 'id': cell.id,
+ 'value': str(cell.value) if cell.value else '',
+ 'is_calculated': is_calculated
+ })
+
+ return True
+ except (Client.DoesNotExist, ValueError):
+ return False
+
+ # 1. Update Summe Bestand (row 6) from Bestand in Kannen-1 (row 5)
+ # For merged pairs: copy value from changed cell to its pair
+ if changed_cell and changed_cell.table_type == 'top_right':
+ if changed_cell.row_index == 5: # Bestand in Kannen-1
+ client_name = changed_cell.client.name
+ new_value = changed_cell.value
+
+ # Check if this client is in a merged pair
+ for pair in MERGED_PAIRS:
+ if client_name in pair:
+ # Update both clients in the pair
+ for client_in_pair in pair:
+ if client_in_pair != client_name:
+ set_val(client_in_pair, 5, new_value, is_calculated=False)
+ break
+
+ # 2. For all clients: Set Summe Bestand (row 6) = Bestand in Kannen-1 (row 5)
+ for client_name in TOP_RIGHT_CLIENTS:
+ bestand_value = get_val(client_name, 5)
+ set_val(client_name, 6, bestand_value, is_calculated=True)
+
+ # 3. Update Sammelrückführungen (row 4) from helium_input groups
+ for group_name, client_names in HELIUM_INPUT_GROUPS.items():
+ # Get total helium_input for this group
+ clients_in_group = Client.objects.filter(name__in=client_names)
+ total_helium = ExcelEntry.objects.filter(
+ client__in=clients_in_group,
+ date__year=year,
+ date__month=month
+ ).aggregate(total=Coalesce(Sum('lhe_ges'), Decimal('0')))['total']
+
+ # Set same value for all clients in group
+ for client_name in client_names:
+ set_val(client_name, 4, total_helium, is_calculated=True)
+
+ # 4. Calculate Rückführung flüssig (row 2)
+ # For merged pairs: =L8 for Dr. Fohrer/AG Buntk., =N8 for AG Alff/AG Gutfl.
+ # For M3 clients: =$P$8 * P4, $P$8 * Q4, $P$8 * R4
+
+ # Get Sammelrückführungen values for groups
+ sammel_fohrer_buntk = get_val("Dr. Fohrer", 4) # L8
+ sammel_alff_gutfl = get_val("AG Alff", 4) # N8
+ sammel_m3_group = get_val("M3 Thiele", 4) # P8 (same for all M3)
+
+ # For merged pairs
+ set_val("Dr. Fohrer", 2, sammel_fohrer_buntk, is_calculated=True)
+ set_val("AG Buntk.", 2, sammel_fohrer_buntk, is_calculated=True)
+ set_val("AG Alff", 2, sammel_alff_gutfl, is_calculated=True)
+ set_val("AG Gutfl.", 2, sammel_alff_gutfl, is_calculated=True)
+
+ # For M3 clients: =$P$8 * column4 (Stand der Gaszähler)
+ for m3_client in M3_CLIENTS:
+ stand_value = get_val(m3_client, 0) # Stand der Gaszähler (row 0)
+ rueck_value = sammel_m3_group * stand_value
+ set_val(m3_client, 2, rueck_value, is_calculated=True)
+
+ # 5. Calculate Füllungen warm (row 11) from SecondTableEntry warm outputs
+ # 5. Calculate Füllungen warm (row 11) as NUMBER of warm fillings
+ # (sum of 1s where each warm SecondTableEntry is one filling)
+ for client_name in TOP_RIGHT_CLIENTS:
+ client = Client.objects.get(name=client_name)
+ warm_count = SecondTableEntry.objects.filter(
+ client=client,
+ date__year=year,
+ date__month=month,
+ is_warm=True
+ ).aggregate(
+ total=Coalesce(Count('id'), 0)
+ )['total']
+
+ # store as Decimal so later formulas (warm * 15) still work nicely
+ warm_value = Decimal(warm_count)
+ set_val(client_name, 11, warm_value, is_calculated=True)
+ # 6. Set Faktor row (13) to 0.06
+ for client_name in TOP_RIGHT_CLIENTS:
+ set_val(client_name, 13, factor, is_calculated=True)
+
+ # 6a. Recalculate Stand der Gaszähler (row 0) for the merged pairs
+ # according to Excel:
+ # L4 = L13 / (L13 + M13), M4 = M13 / (L13 + M13)
+ # N4 = N13 / (N13 + O13), O4 = O13 / (N13 + O13)
+
+ # Pair 1: Dr. Fohrer / AG Buntk.
+ bezug_dr = get_val("Dr. Fohrer", 8) # L13
+ bezug_buntk = get_val("AG Buntk.", 8) # M13
+ total_pair1 = bezug_dr + bezug_buntk
+
+ if total_pair1 != 0:
+ set_val("Dr. Fohrer", 0, bezug_dr / total_pair1, is_calculated=True)
+ set_val("AG Buntk.", 0, bezug_buntk / total_pair1, is_calculated=True)
+ else:
+ # if no Bezug, both shares are 0
+ set_val("Dr. Fohrer", 0, Decimal('0'), is_calculated=True)
+ set_val("AG Buntk.", 0, Decimal('0'), is_calculated=True)
+
+ # Pair 2: AG Alff / AG Gutfl.
+ bezug_alff = get_val("AG Alff", 8) # N13
+ bezug_gutfl = get_val("AG Gutfl.", 8) # O13
+ total_pair2 = bezug_alff + bezug_gutfl
+
+ if total_pair2 != 0:
+ set_val("AG Alff", 0, bezug_alff / total_pair2, is_calculated=True)
+ set_val("AG Gutfl.", 0, bezug_gutfl / total_pair2, is_calculated=True)
+ else:
+ set_val("AG Alff", 0, Decimal('0'), is_calculated=True)
+ set_val("AG Gutfl.", 0, Decimal('0'), is_calculated=True)
+
+
+ # 7. Calculate all other dependent rows for merged pairs
+ for pair in MERGED_PAIRS:
+ client1, client2 = pair
+
+ # Get values for the pair
+ bezug1 = get_val(client1, 8) # Bezug client1
+ bezug2 = get_val(client2, 8) # Bezug client2
+ total_bezug = bezug1 + bezug2 # L13+M13 or N13+O13
+
+ summe_bestand = get_val(client1, 6) # L11 or N11 (merged, same value)
+ best_vormonat = get_val(client1, 7) # L12 or N12 (merged, same value)
+ rueck_fl = get_val(client1, 2) # L6 or N6 (merged, same value)
+ warm1 = get_val(client1, 11) # L16 or N16
+ warm2 = get_val(client2, 11) # M16 or O16
+ total_warm = warm1 + warm2 # L16+M16 or N16+O16
+
+ # Calculate Rückführ. Soll (row 9)
+ # = L13+M13 - L11 + L12 for first pair
+ # = N13+O13 - N11 + N12 for second pair
+ rueck_soll = total_bezug - summe_bestand + best_vormonat
+ set_val(client1, 9, rueck_soll, is_calculated=True)
+ set_val(client2, 9, rueck_soll, is_calculated=True)
+
+ # Calculate Verluste (row 10) = Rückführ. Soll - Rückführung flüssig
+ verluste = rueck_soll - rueck_fl
+ set_val(client1, 10, verluste, is_calculated=True)
+ set_val(client2, 10, verluste, is_calculated=True)
+
+ # Calculate Kaltgas Rückgabe (row 12)
+ # = (L13+M13)*$A18 + (L16+M16)*15
+ kaltgas = (total_bezug * factor) + (total_warm * Decimal('15'))
+ set_val(client1, 12, kaltgas, is_calculated=True)
+ set_val(client2, 12, kaltgas, is_calculated=True)
+
+ # Calculate Verbraucherverluste (row 14) = Verluste - Kaltgas
+ verbrauch = verluste - kaltgas
+ set_val(client1, 14, verbrauch, is_calculated=True)
+ set_val(client2, 14, verbrauch, is_calculated=True)
+
+ # Calculate % (row 15) = Verbraucherverluste / (L13+M13)
+ if total_bezug != 0:
+ prozent = verbrauch / total_bezug
+ else:
+ prozent = Decimal('0')
+ set_val(client1, 15, prozent, is_calculated=True)
+ set_val(client2, 15, prozent, is_calculated=True)
+
+ # 8. Calculate all dependent rows for M3 clients (individual calculations)
+ for m3_client in M3_CLIENTS:
+ # Get individual values
+ bezug = get_val(m3_client, 8) # Bezug for this M3 client
+ summe_bestand = get_val(m3_client, 6) # Summe Bestand
+ best_vormonat = get_val(m3_client, 7) # Best. in Kannen Vormonat
+ rueck_fl = get_val(m3_client, 2) # Rückführung flüssig
+ warm = get_val(m3_client, 11) # Füllungen warm
+
+ # Calculate Rückführ. Soll (row 9) = Bezug - Summe Bestand + Best. Vormonat
+ rueck_soll = bezug - summe_bestand + best_vormonat
+ set_val(m3_client, 9, rueck_soll, is_calculated=True)
+
+ # Calculate Verluste (row 10) = Rückführ. Soll - Rückführung flüssig
+ verluste = rueck_soll - rueck_fl
+ set_val(m3_client, 10, verluste, is_calculated=True)
+
+ # Calculate Kaltgas Rückgabe (row 12) = Bezug * factor + warm * 15
+ kaltgas = (bezug * factor) + (warm * Decimal('15'))
+ set_val(m3_client, 12, kaltgas, is_calculated=True)
+
+ # Calculate Verbraucherverluste (row 14) = Verluste - Kaltgas
+ verbrauch = verluste - kaltgas
+ set_val(m3_client, 14, verbrauch, is_calculated=True)
+
+ # Calculate % (row 15) = Verbraucherverluste / Bezug
+ if bezug != 0:
+ prozent = verbrauch / bezug
+ else:
+ prozent = Decimal('0')
+ set_val(m3_client, 15, prozent, is_calculated=True)
+
+ return updated_cells
+
+
+
+
+
+
+
+ def calculate_top_left_dependents(self, sheet, changed_cell):
+ """Calculate dependent cells in top_left table"""
+ from decimal import Decimal
+ from django.db.models import Sum
+ from django.db.models.functions import Coalesce
+
+ client_id = changed_cell.client_id
+ updated_cells = []
+
+ # Get all cells for this client in top_left table
+ client_cells = Cell.objects.filter(
+ sheet=sheet,
+ table_type='top_left',
+ client_id=client_id
+ )
+
+ # Create a dict for easy access
+ cell_dict = {}
+ for cell in client_cells:
+ cell_dict[cell.row_index] = cell
+
+ # Get values with safe defaults
+ def get_cell_value(row_idx):
+ cell = cell_dict.get(row_idx)
+ if cell and cell.value is not None:
+ try:
+ return Decimal(str(cell.value))
+ except:
+ return Decimal('0')
+ return Decimal('0')
+
+ # 1. B5 = IF(B3>0; B3-B4; 0) - Gasrückführung
+ b3 = get_cell_value(0) # row_index 0 = Excel B3 (UI row 1)
+ b4 = get_cell_value(1) # row_index 1 = Excel B4 (UI row 2)
+
+ # Calculate B5
+ if b3 > 0:
+ b5_value = b3 - b4
+ if b5_value < 0:
+ b5_value = Decimal('0')
+ else:
+ b5_value = Decimal('0')
+
+ # Update B5 - FORCE update even if value appears the same
+ b5_cell = cell_dict.get(2) # row_index 2 = Excel B5 (UI row 3)
+ if b5_cell:
+ # Always update B5 when B3 or B4 changes
+ b5_cell.value = b5_value
+ b5_cell.save()
+ updated_cells.append({
+ 'id': b5_cell.id,
+ 'value': str(b5_cell.value),
+ 'is_calculated': True
+ })
+
+ # 2. B6 = B5 / 0.75 - Rückführung flüssig
+ b6_value = b5_value / Decimal('0.75')
+ b6_cell = cell_dict.get(3) # row_index 3 = Excel B6 (UI row 4)
+ if b6_cell:
+ b6_cell.value = b6_value
+ b6_cell.save()
+ updated_cells.append({
+ 'id': b6_cell.id,
+ 'value': str(b6_cell.value),
+ 'is_calculated': True
+ })
+
+ # 3. B9 = B7 + B8 - Summe Bestand
+ b7 = get_cell_value(4) # row_index 4 = Excel B7 (UI row 5)
+ b8 = get_cell_value(5) # row_index 5 = Excel B8 (UI row 6)
+ b9_value = b7 + b8
+ b9_cell = cell_dict.get(6) # row_index 6 = Excel B9 (UI row 7)
+ if b9_cell:
+ b9_cell.value = b9_value
+ b9_cell.save()
+ updated_cells.append({
+ 'id': b9_cell.id,
+ 'value': str(b9_cell.value),
+ 'is_calculated': True
+ })
+
+ # 4. B11 = Sum of LHe Output from SecondTableEntry for this client/month - Bezug
+ from .models import SecondTableEntry
+ client = changed_cell.client
+
+ # Calculate total LHe output for this client in this month
+ # 4. B11 = Bezug (Liter L-He)
+ # For start sheet: manual entry, for other sheets: auto-calculated from SecondTableEntry
+ from .models import SecondTableEntry
+ client = changed_cell.client
+
+ b11_cell = cell_dict.get(8) # row_index 8 = Excel B11 (UI row 9)
+
+ # Check if this is the start sheet (2025-01)
+ is_start_sheet = (sheet.year == 2025 and sheet.month == 1)
+
+ # Get the B11 value for calculations
+ b11_value = Decimal('0')
+
+ if is_start_sheet:
+ # For start sheet, keep whatever value is already there (manual entry)
+ if b11_cell and b11_cell.value is not None:
+ b11_value = Decimal(str(b11_cell.value))
+ else:
+ # For non-start sheets: auto-calculate from SecondTableEntry
+ lhe_output_sum = SecondTableEntry.objects.filter(
+ client=client,
+ date__year=sheet.year,
+ date__month=sheet.month
+ ).aggregate(
+ total=Coalesce(Sum('lhe_output'), Decimal('0'))
+ )['total']
+
+ b11_value = lhe_output_sum
+
+ if b11_cell and (b11_cell.value != b11_value or b11_cell.value is None):
+ b11_cell.value = b11_value
+ b11_cell.save()
+ updated_cells.append({
+ 'id': b11_cell.id,
+ 'value': str(b11_cell.value),
+ 'is_calculated': True # Calculated from SecondTableEntry
+ })
+
+ # 5. B12 = B11 + B10 - B9 - Rückführ. Soll
+ b10 = get_cell_value(7) # row_index 7 = Excel B10 (UI row 8)
+ b12_value = b11_value + b10 - b9_value # Use b11_value instead of lhe_output_sum
+ b12_cell = cell_dict.get(9) # row_index 9 = Excel B12 (UI row 10)
+ if b12_cell:
+ b12_cell.value = b12_value
+ b12_cell.save()
+ updated_cells.append({
+ 'id': b12_cell.id,
+ 'value': str(b12_cell.value),
+ 'is_calculated': True
+ })
+
+ # 6. B13 = B12 - B6 - Verluste (Soll-Rückf.)
+ b13_value = b12_value - b6_value
+ b13_cell = cell_dict.get(10) # row_index 10 = Excel B13 (UI row 11)
+ if b13_cell:
+ b13_cell.value = b13_value
+ b13_cell.save()
+ updated_cells.append({
+ 'id': b13_cell.id,
+ 'value': str(b13_cell.value),
+ 'is_calculated': True
+ })
+
+ # 7. B14 = Count of warm outputs
+ warm_count = SecondTableEntry.objects.filter(
+ client=client,
+ date__year=sheet.year,
+ date__month=sheet.month,
+ is_warm=True
+ ).count()
+
+ b14_cell = cell_dict.get(11) # row_index 11 = Excel B14 (UI row 12)
+ if b14_cell:
+ b14_cell.value = Decimal(str(warm_count))
+ b14_cell.save()
+ updated_cells.append({
+ 'id': b14_cell.id,
+ 'value': str(b14_cell.value),
+ 'is_calculated': True
+ })
+
+ # 8. B15 = IF(B11>0; B11 * factor + B14 * 15; 0) - Kaltgas Rückgabe
+ factor = get_cell_value(13) # row_index 13 = Excel B16 (Faktor) (UI row 14)
+ if factor == 0:
+ factor = Decimal('0.06') # default factor
+
+ if b11_value > 0: # Use b11_value
+ b15_value = b11_value * factor + Decimal(str(warm_count)) * Decimal('15')
+ else:
+ b15_value = Decimal('0')
+
+ b15_cell = cell_dict.get(12) # row_index 12 = Excel B15 (UI row 13)
+ if b15_cell:
+ b15_cell.value = b15_value
+ b15_cell.save()
+ updated_cells.append({
+ 'id': b15_cell.id,
+ 'value': str(b15_cell.value),
+ 'is_calculated': True
+ })
+
+ # 9. B17 = B13 - B15 - Verbraucherverluste
+ b17_value = b13_value - b15_value
+ b17_cell = cell_dict.get(14) # row_index 14 = Excel B17 (UI row 15)
+ if b17_cell:
+ b17_cell.value = b17_value
+ b17_cell.save()
+ updated_cells.append({
+ 'id': b17_cell.id,
+ 'value': str(b17_cell.value),
+ 'is_calculated': True
+ })
+
+ # 10. B18 = IF(B11=0; 0; B17/B11) - %
+ if b11_value == 0: # Use b11_value
+ b18_value = Decimal('0')
+ else:
+ b18_value = b17_value / b11_value # Use b11_value
+
+ b18_cell = cell_dict.get(15) # row_index 15 = Excel B18 (UI row 16)
+ if b18_cell:
+ b18_cell.value = b18_value
+ b18_cell.save()
+ updated_cells.append({
+ 'id': b18_cell.id,
+ 'value': str(b18_cell.value),
+ 'is_calculated': True
+ })
+
+ return updated_cells
+ def save_bulk_cells(self, request, sheet):
+ """Original bulk save logic (for backward compatibility)"""
+ # Get all cell updates
+ cell_updates = {}
+ for key, value in request.POST.items():
+ if key.startswith('cell_'):
+ cell_id = key.replace('cell_', '')
+ cell_updates[cell_id] = value
+
+ # Update cells and track which ones changed
+ updated_cells = []
+ changed_clients = set()
+
+ for cell_id, new_value in cell_updates.items():
+ try:
+ cell = Cell.objects.get(id=cell_id, sheet=sheet)
+ old_value = cell.value
+
+ # Convert new value
+ try:
+ if new_value.strip():
+ cell.value = Decimal(new_value)
+ else:
+ cell.value = None
+ except:
+ cell.value = None
+
+ # Only save if value changed
+ if cell.value != old_value:
+ cell.save()
+ updated_cells.append({
+ 'id': cell.id,
+ 'value': str(cell.value) if cell.value else ''
+ })
+ changed_clients.add(cell.client_id)
+
+ except Cell.DoesNotExist:
+ continue
+
+ # Recalculate for each changed client
+ for client_id in changed_clients:
+ self.recalculate_top_left_table(sheet, client_id)
+
+ # Get all updated cells for response
+ all_updated_cells = []
+ for client_id in changed_clients:
+ client_cells = Cell.objects.filter(
+ sheet=sheet,
+ client_id=client_id
+ )
+ for cell in client_cells:
+ all_updated_cells.append({
+ 'id': cell.id,
+ 'value': str(cell.value) if cell.value else '',
+ 'is_calculated': cell.is_formula
+ })
+
+ return JsonResponse({
+ 'status': 'success',
+ 'message': f'Saved {len(updated_cells)} cells',
+ 'updated_cells': all_updated_cells
+ })
+
+ def recalculate_top_left_table(self, sheet, client_id):
+ """Recalculate the top-left table for a specific client"""
+ from decimal import Decimal
+
+ # Get all cells for this client in top_left table
+ cells = Cell.objects.filter(
+ sheet=sheet,
+ table_type='top_left',
+ client_id=client_id
+ ).order_by('row_index')
+
+ # Create a dictionary of cell values
+ cell_dict = {}
+ for cell in cells:
+ cell_dict[cell.row_index] = cell
+
+ # Excel logic implementation for top-left table
+ # B3 (row_index 0) = Stand der Gaszähler (Nm³) - manual
+ # B4 (row_index 1) = Stand der Gaszähler (Vormonat) (Nm³) - from previous sheet
+
+ # Get B3 and B4
+ b3_cell = cell_dict.get(0) # UI Row 3
+ b4_cell = cell_dict.get(1) # UI Row 4
+
+ if b3_cell and b3_cell.value and b4_cell and b4_cell.value:
+ # B5 = IF(B3>0; B3-B4; 0)
+ b3 = Decimal(str(b3_cell.value))
+ b4 = Decimal(str(b4_cell.value))
+
+ if b3 > 0:
+ b5 = b3 - b4
+ if b5 < 0:
+ b5 = Decimal('0')
+ else:
+ b5 = Decimal('0')
+
+ # Update B5 (row_index 2)
+ b5_cell = cell_dict.get(2)
+ if b5_cell and (b5_cell.value != b5 or b5_cell.value is None):
+ b5_cell.value = b5
+ b5_cell.save()
+
+ # B6 = B5 / 0.75 (row_index 3)
+ b6 = b5 / Decimal('0.75')
+ b6_cell = cell_dict.get(3)
+ if b6_cell and (b6_cell.value != b6 or b6_cell.value is None):
+ b6_cell.value = b6
+ b6_cell.save()
+
+ # Get previous month's sheet for B10
+ if sheet.month == 1:
+ prev_year = sheet.year - 1
+ prev_month = 12
+ else:
+ prev_year = sheet.year
+ prev_month = sheet.month - 1
+
+ prev_sheet = MonthlySheet.objects.filter(
+ year=prev_year,
+ month=prev_month
+ ).first()
+
+ if prev_sheet:
+ # Get B9 from previous sheet (row_index 7 in previous)
+ prev_b9 = Cell.objects.filter(
+ sheet=prev_sheet,
+ table_type='top_left',
+ client_id=client_id,
+ row_index=7 # UI Row 9
+ ).first()
+
+ if prev_b9 and prev_b9.value:
+ # Update B10 in current sheet (row_index 8)
+ b10_cell = cell_dict.get(8)
+ if b10_cell and (b10_cell.value != prev_b9.value or b10_cell.value is None):
+ b10_cell.value = prev_b9.value
+ b10_cell.save()
+
+# Calculate View (placeholder for calculations)
+class CalculateView(View):
+ def post(self, request):
+ # This will be implemented when you provide calculation rules
+ return JsonResponse({
+ 'status': 'success',
+ 'message': 'Calculation endpoint ready'
+ })
+
+# Summary Sheet View
+class SummarySheetView(TemplateView):
+ template_name = 'summary_sheet.html'
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ start_month = int(self.kwargs.get('start_month', 1))
+ year = int(self.kwargs.get('year', datetime.now().year))
+
+ # Get 6 monthly sheets
+ months = [(year, m) for m in range(start_month, start_month + 6)]
+ sheets = MonthlySheet.objects.filter(
+ year=year,
+ month__in=list(range(start_month, start_month + 6))
+ ).order_by('month')
+
+ # Aggregate data across months
+ summary_data = self.calculate_summary(sheets)
+
+ context.update({
+ 'year': year,
+ 'start_month': start_month,
+ 'end_month': start_month + 5,
+ 'sheets': sheets,
+ 'clients': Client.objects.all(),
+ 'summary_data': summary_data,
+ })
+ return context
+
+ def calculate_summary(self, sheets):
+ """Calculate totals across 6 months"""
+ summary = {}
+
+ for client in Client.objects.all():
+ client_total = 0
+ for sheet in sheets:
+ # Get specific cells and sum
+ cells = sheet.cells.filter(
+ client=client,
+ table_type='top_left',
+ row_index=0 # Example: first row
+ )
+ for cell in cells:
+ if cell.value:
+ try:
+ client_total += float(cell.value)
+ except (ValueError, TypeError):
+ continue
+
+ summary[client.id] = client_total
+
+ return summary
+
+# Existing views below (keep all your existing functions)
def clients_list(request):
# Get all clients
clients = Client.objects.all()
@@ -60,26 +1993,124 @@ def clients_list(request):
'months': ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
})
+
# Table One View (ExcelEntry)
def table_one_view(request):
- ExcelEntry = apps.get_model('sheets', 'ExcelEntry')
- entries_table1 = ExcelEntry.objects.all().select_related('client')
- clients = Client.objects.all().select_related('institute') # Add select_related
- institutes = Institute.objects.all() # Add institutes
-
+ from .models import ExcelEntry, Client, Institute
+
+ # Main table (unchanged)
+ entries_table1 = ExcelEntry.objects.all().select_related('client', 'client__institute')
+ clients = Client.objects.all().select_related('institute')
+ institutes = Institute.objects.all()
+
+ # ---- Overview filters ----
+ # years present in ExcelEntry.date
+ year_qs = ExcelEntry.objects.dates('date', 'year', order='DESC')
+ available_years = [d.year for d in year_qs]
+
+ # default year/start month
+ if available_years:
+ default_year = available_years[0] # newest year
+ else:
+ default_year = timezone.now().year
+
+ year_param = request.GET.get('overview_year')
+ start_month_param = request.GET.get('overview_start_month')
+
+ year = int(year_param) if year_param else int(default_year)
+ start_month = int(start_month_param) if start_month_param else 1
+
+ # six-month window
+ end_month = start_month + 5
+ if end_month > 12:
+ end_month = 12
+
+ months = list(range(start_month, end_month + 1))
+
+ overview = None
+
+ if months:
+ # Build per-group data
+ groups_entries = [] # for internal calculations
+
+ for key, group in CLIENT_GROUPS.items():
+ clients_qs = get_group_clients(key)
+
+ values = []
+ group_total = Decimal('0')
+
+ for m in months:
+ total = ExcelEntry.objects.filter(
+ client__in=clients_qs,
+ date__year=year,
+ date__month=m
+ ).aggregate(
+ total=Coalesce(Sum('lhe_ges'), Decimal('0'))
+ )['total']
+
+ values.append(total)
+ group_total += total
+
+ groups_entries.append({
+ 'key': key,
+ 'label': group['label'],
+ 'values': values,
+ 'total': group_total,
+ })
+
+ # month totals across all groups
+ month_totals = []
+ for idx in range(len(months)):
+ s = sum((g['values'][idx] for g in groups_entries), Decimal('0'))
+ month_totals.append(s)
+
+ grand_total = sum(month_totals, Decimal('0'))
+
+ # Build rows for the template
+ rows = []
+ for idx, m in enumerate(months):
+ row_values = [g['values'][idx] for g in groups_entries]
+ rows.append({
+ 'month_number': m,
+ 'month_label': calendar.month_name[m],
+ 'values': row_values,
+ 'total': month_totals[idx],
+ })
+
+ groups_meta = [{'key': g['key'], 'label': g['label']} for g in groups_entries]
+ group_totals = [g['total'] for g in groups_entries]
+
+ overview = {
+ 'year': year,
+ 'start_month': start_month,
+ 'end_month': end_month,
+ 'rows': rows,
+ 'groups': groups_meta,
+ 'group_totals': group_totals,
+ 'grand_total': grand_total,
+ }
+
+ # Month dropdown labels
+ MONTH_CHOICES = [
+ (1, 'Jan'), (2, 'Feb'), (3, 'Mar'), (4, 'Apr'),
+ (5, 'May'), (6, 'Jun'), (7, 'Jul'), (8, 'Aug'),
+ (9, 'Sep'), (10, 'Oct'), (11, 'Nov'), (12, 'Dec'),
+ ]
+
return render(request, 'table_one.html', {
'entries_table1': entries_table1,
'clients': clients,
- 'institutes': institutes, # Add institutes to context
+ 'institutes': institutes,
+ 'available_years': available_years,
+ 'month_choices': MONTH_CHOICES,
+ 'overview': overview,
})
-
# Table Two View (SecondTableEntry)
def table_two_view(request):
try:
- SecondTableEntry = apps.get_model('sheets', 'SecondTableEntry')
entries = SecondTableEntry.objects.all().order_by('-date')
- clients = Client.objects.all().select_related('institute') # Add select_related
+ clients = Client.objects.all().select_related('institute')
institutes = Institute.objects.all()
return render(request, 'table_two.html', {
@@ -96,15 +2127,14 @@ def table_two_view(request):
'institutes': Institute.objects.all()
})
-
# Add Entry (Generic)
def add_entry(request, model_name):
if request.method == 'POST':
try:
- model = apps.get_model('sheets', model_name)
-
if model_name == 'SecondTableEntry':
- # Handle date conversion
+ model = SecondTableEntry
+
+ # Parse date
date_str = request.POST.get('date')
try:
date_obj = datetime.strptime(date_str, '%Y-%m-%d').date() if date_str else None
@@ -114,12 +2144,17 @@ def add_entry(request, model_name):
'message': 'Invalid date format. Use YYYY-MM-DD'
}, status=400)
+ # NEW: robust parse of warm flag (handles 0/1, true/false, on/off)
+ raw_warm = request.POST.get('is_warm')
+ is_warm_bool = str(raw_warm).lower() in ('1', 'true', 'on', 'yes')
+
# Handle Helium Output (Table Two)
lhe_output = request.POST.get('lhe_output')
+
entry = model.objects.create(
client=Client.objects.get(id=request.POST.get('client_id')),
date=date_obj,
- is_warm=request.POST.get('is_warm') == 'true',
+ is_warm=is_warm_bool, # <-- use the boolean here
lhe_delivery=request.POST.get('lhe_delivery', ''),
lhe_output=Decimal(lhe_output) if lhe_output else None,
notes=request.POST.get('notes', '')
@@ -129,7 +2164,7 @@ def add_entry(request, model_name):
'status': 'success',
'id': entry.id,
'client_name': entry.client.name,
- 'institute_name': entry.client.institute.name, # Add this line
+ 'institute_name': entry.client.institute.name,
'date': entry.date.strftime('%Y-%m-%d') if entry.date else '',
'is_warm': entry.is_warm,
'lhe_delivery': entry.lhe_delivery,
@@ -138,19 +2173,44 @@ def add_entry(request, model_name):
})
elif model_name == 'ExcelEntry':
+ model = ExcelEntry
+
# Parse the date string into a date object
date_str = request.POST.get('date')
try:
date_obj = datetime.strptime(date_str, '%Y-%m-%d').date() if date_str else None
except (ValueError, TypeError):
date_obj = None
-
- # Create the entry
+
+ try:
+ pressure = Decimal(request.POST.get('pressure', 0))
+ purity = Decimal(request.POST.get('purity', 0))
+ druckkorrektur = Decimal(request.POST.get('druckkorrektur', 1))
+ lhe_zus = Decimal(request.POST.get('lhe_zus', 0))
+ constant_300 = Decimal(request.POST.get('constant_300', 300))
+ korrig_druck = Decimal(request.POST.get('korrig_druck', 0))
+ nm3 = Decimal(request.POST.get('nm3', 0))
+ lhe = Decimal(request.POST.get('lhe', 0))
+ lhe_ges = Decimal(request.POST.get('lhe_ges', 0))
+ except InvalidOperation:
+ return JsonResponse({
+ 'status': 'error',
+ 'message': 'Invalid numeric value in Helium Input'
+ }, status=400)
+
+ # Create the entry with ALL fields
entry = model.objects.create(
client=Client.objects.get(id=request.POST.get('client_id')),
date=date_obj,
- pressure=Decimal(request.POST.get('pressure', 0)),
- purity=Decimal(request.POST.get('purity', 0)),
+ pressure=pressure,
+ purity=purity,
+ druckkorrektur=druckkorrektur,
+ lhe_zus=lhe_zus,
+ constant_300=constant_300,
+ korrig_druck=korrig_druck,
+ nm3=nm3,
+ lhe=lhe,
+ lhe_ges=lhe_ges,
notes=request.POST.get('notes', '')
)
@@ -159,31 +2219,46 @@ def add_entry(request, model_name):
'status': 'success',
'id': entry.id,
'client_name': entry.client.name,
- 'institute_name': entry.client.institute.name, # Add this line
+ 'institute_name': entry.client.institute.name,
'pressure': str(entry.pressure),
'purity': str(entry.purity),
- 'notes': entry.notes
+ 'druckkorrektur': str(entry.druckkorrektur),
+ 'constant_300': str(entry.constant_300),
+ 'korrig_druck': str(entry.korrig_druck),
+ 'nm3': str(entry.nm3),
+ 'lhe': str(entry.lhe),
+ 'lhe_zus': str(entry.lhe_zus),
+ 'lhe_ges': str(entry.lhe_ges),
+ 'notes': entry.notes,
}
-
- # Only add date if it exists
+
if entry.date:
+ # JS uses this for the Date column and for the Month column
response_data['date'] = entry.date.strftime('%Y-%m-%d')
+ response_data['month'] = f"{entry.date.month:02d}"
else:
- response_data['date'] = None
-
+ response_data['date'] = ''
+ response_data['month'] = ''
+
return JsonResponse(response_data)
- # Keep your existing SecondTableEntry code here...
except Exception as e:
return JsonResponse({'status': 'error', 'message': str(e)}, status=400)
return JsonResponse({'status': 'error', 'message': 'Invalid request'}, status=400)
+
# Update Entry (Generic)
def update_entry(request, model_name):
if request.method == 'POST':
try:
- model = apps.get_model('sheets', model_name)
+ if model_name == 'SecondTableEntry':
+ model = SecondTableEntry
+ elif model_name == 'ExcelEntry':
+ model = ExcelEntry
+ else:
+ return JsonResponse({'status': 'error', 'message': 'Invalid model'}, status=400)
+
entry_id = int(request.POST.get('id'))
entry = model.objects.get(id=entry_id)
@@ -204,9 +2279,12 @@ def update_entry(request, model_name):
if model_name == 'SecondTableEntry':
# Handle Helium Output specific fields
- entry.is_warm = request.POST.get('is_warm') == 'true'
+
+ raw_warm = request.POST.get('is_warm')
+ entry.is_warm = str(raw_warm).lower() in ('1', 'true', 'on', 'yes')
+
entry.lhe_delivery = request.POST.get('lhe_delivery', '')
-
+
lhe_output = request.POST.get('lhe_output')
try:
entry.lhe_output = Decimal(lhe_output) if lhe_output else None
@@ -222,7 +2300,7 @@ def update_entry(request, model_name):
'status': 'success',
'id': entry.id,
'client_name': entry.client.name,
- 'institute_name': entry.client.institute.name, # Add this line
+ 'institute_name': entry.client.institute.name,
'date': entry.date.strftime('%Y-%m-%d') if entry.date else '',
'is_warm': entry.is_warm,
'lhe_delivery': entry.lhe_delivery,
@@ -230,15 +2308,23 @@ def update_entry(request, model_name):
'notes': entry.notes
})
+
elif model_name == 'ExcelEntry':
# Handle Helium Input specific fields
try:
entry.pressure = Decimal(request.POST.get('pressure', 0))
entry.purity = Decimal(request.POST.get('purity', 0))
+ entry.druckkorrektur = Decimal(request.POST.get('druckkorrektur', 1))
+ entry.lhe_zus = Decimal(request.POST.get('lhe_zus', 0))
+ entry.constant_300 = Decimal(request.POST.get('constant_300', 300))
+ entry.korrig_druck = Decimal(request.POST.get('korrig_druck', 0))
+ entry.nm3 = Decimal(request.POST.get('nm3', 0))
+ entry.lhe = Decimal(request.POST.get('lhe', 0))
+ entry.lhe_ges = Decimal(request.POST.get('lhe_ges', 0))
except InvalidOperation:
return JsonResponse({
'status': 'error',
- 'message': 'Invalid pressure or purity value'
+ 'message': 'Invalid numeric value in Helium Input'
}, status=400)
entry.save()
@@ -247,24 +2333,40 @@ def update_entry(request, model_name):
'status': 'success',
'id': entry.id,
'client_name': entry.client.name,
- 'institute_name': entry.client.institute.name, # Add this line
+ 'institute_name': entry.client.institute.name,
'date': entry.date.strftime('%Y-%m-%d') if entry.date else '',
+ 'month': f"{entry.date.month:02d}" if entry.date else '',
'pressure': str(entry.pressure),
'purity': str(entry.purity),
+ 'druckkorrektur': str(entry.druckkorrektur),
+ 'constant_300': str(entry.constant_300),
+ 'korrig_druck': str(entry.korrig_druck),
+ 'nm3': str(entry.nm3),
+ 'lhe': str(entry.lhe),
+ 'lhe_zus': str(entry.lhe_zus),
+ 'lhe_ges': str(entry.lhe_ges),
'notes': entry.notes
})
+
except model.DoesNotExist:
return JsonResponse({'status': 'error', 'message': 'Entry not found'}, status=404)
except Exception as e:
return JsonResponse({'status': 'error', 'message': str(e)}, status=400)
return JsonResponse({'status': 'error', 'message': 'Invalid request method'}, status=400)
+
# Delete Entry (Generic)
def delete_entry(request, model_name):
if request.method == 'POST':
try:
- model = apps.get_model('sheets', model_name)
+ if model_name == 'SecondTableEntry':
+ model = SecondTableEntry
+ elif model_name == 'ExcelEntry':
+ model = ExcelEntry
+ else:
+ return JsonResponse({'status': 'error', 'message': 'Invalid model'}, status=400)
+
entry_id = request.POST.get('id')
entry = model.objects.get(id=entry_id)
entry.delete()
@@ -355,4 +2457,144 @@ def betriebskosten_delete(request):
except Exception as e:
return JsonResponse({'status': 'error', 'message': str(e)})
- return JsonResponse({'status': 'error', 'message': 'Invalid request method'})
\ No newline at end of file
+ return JsonResponse({'status': 'error', 'message': 'Invalid request method'})
+
+class CheckSheetView(View):
+ def get(self, request):
+ # Get current month/year
+ current_year = datetime.now().year
+ current_month = datetime.now().month
+
+ # Get all sheets
+ sheets = MonthlySheet.objects.all()
+
+ sheet_data = []
+ for sheet in sheets:
+ cells_count = sheet.cells.count()
+ # Count non-empty cells
+ non_empty = sheet.cells.exclude(value__isnull=True).count()
+
+ sheet_data.append({
+ 'id': sheet.id,
+ 'year': sheet.year,
+ 'month': sheet.month,
+ 'month_name': calendar.month_name[sheet.month],
+ 'total_cells': cells_count,
+ 'non_empty_cells': non_empty,
+ 'has_data': non_empty > 0
+ })
+
+ # Also check what the default view would show
+ default_sheet = MonthlySheet.objects.filter(
+ year=current_year,
+ month=current_month
+ ).first()
+
+ return JsonResponse({
+ 'current_year': current_year,
+ 'current_month': current_month,
+ 'current_month_name': calendar.month_name[current_month],
+ 'default_sheet_exists': default_sheet is not None,
+ 'default_sheet_id': default_sheet.id if default_sheet else None,
+ 'sheets': sheet_data,
+ 'total_sheets': len(sheet_data)
+ })
+
+
+class QuickDebugView(View):
+ def get(self, request):
+ # Get ALL sheets
+ sheets = MonthlySheet.objects.all().order_by('year', 'month')
+
+ result = {
+ 'sheets': []
+ }
+
+ for sheet in sheets:
+ sheet_info = {
+ 'id': sheet.id,
+ 'display': f"{sheet.year}-{sheet.month:02d}",
+ 'url': f"/sheet/{sheet.year}/{sheet.month}/", # CHANGED THIS LINE
+ 'sheet_url_pattern': 'sheet/{year}/{month}/', # Add this for clarity
+ }
+
+ # Count cells with data for first client in top_left table
+ first_client = Client.objects.first()
+ if first_client:
+ test_cells = sheet.cells.filter(
+ client=first_client,
+ table_type='top_left',
+ row_index__in=[8, 9, 10] # Rows 9, 10, 11
+ ).order_by('row_index')
+
+ cell_values = {}
+ for cell in test_cells:
+ cell_values[f"row_{cell.row_index}"] = str(cell.value) if cell.value else "Empty"
+
+ sheet_info['test_cells'] = cell_values
+ else:
+ sheet_info['test_cells'] = "No clients"
+
+ result['sheets'].append(sheet_info)
+
+ return JsonResponse(result)
+class TestFormulaView(View):
+ def get(self, request):
+ # Test the formula evaluation directly
+ test_values = {
+ 8: 2, # Row 9 value (0-based index 8)
+ 9: 2, # Row 10 value (0-based index 9)
+ }
+
+ # Test formula "9 + 8" (using 0-based indices)
+ formula = "9 + 8"
+ result = evaluate_formula(formula, test_values)
+
+ return JsonResponse({
+ 'test_values': test_values,
+ 'formula': formula,
+ 'result': str(result),
+ 'note': 'Formula uses 0-based indices. 9=Row10, 8=Row9'
+ })
+
+
+class SimpleDebugView(View):
+ """Simplest debug view to check if things are working"""
+ def get(self, request):
+ sheet_id = request.GET.get('sheet_id', 1)
+
+ try:
+ sheet = MonthlySheet.objects.get(id=sheet_id)
+
+ # Get first client
+ client = Client.objects.first()
+ if not client:
+ return JsonResponse({'error': 'No clients found'})
+
+ # Check a few cells
+ cells = Cell.objects.filter(
+ sheet=sheet,
+ client=client,
+ table_type='top_left',
+ row_index__in=[8, 9, 10]
+ ).order_by('row_index')
+
+ cell_data = []
+ for cell in cells:
+ cell_data.append({
+ 'row_index': cell.row_index,
+ 'ui_row': cell.row_index + 1,
+ 'value': str(cell.value) if cell.value is not None else 'Empty',
+ 'cell_id': cell.id
+ })
+
+ return JsonResponse({
+ 'sheet': f"{sheet.year}-{sheet.month}",
+ 'sheet_id': sheet.id,
+ 'client': client.name,
+ 'cells': cell_data,
+ 'note': 'Row 8 = UI Row 9, Row 9 = UI Row 10, Row 10 = UI Row 11'
+ })
+
+ except MonthlySheet.DoesNotExist:
+ return JsonResponse({'error': f'Sheet with id {sheet_id} not found'})
\ No newline at end of file
diff --git a/sheets/views.txt b/sheets/views.txt
new file mode 100644
index 0000000..adeefa0
--- /dev/null
+++ b/sheets/views.txt
@@ -0,0 +1,2873 @@
+from django.shortcuts import render, redirect
+from django.db.models import Sum, Value, DecimalField
+from django.http import JsonResponse
+from django.db.models import Q
+from decimal import Decimal, InvalidOperation
+from django.apps import apps
+from datetime import date, datetime
+import calendar
+from django.utils import timezone
+from django.views.generic import TemplateView, View
+from .models import (
+ Client, SecondTableEntry, Institute, ExcelEntry,
+ Betriebskosten, MonthlySheet, Cell, CellReference
+)
+from django.db.models import Sum
+from django.urls import reverse
+from django.db.models.functions import Coalesce
+from .forms import BetriebskostenForm
+from django.utils.dateparse import parse_date
+from django.contrib.auth.mixins import LoginRequiredMixin
+FIRST_SHEET_YEAR = 2025
+FIRST_SHEET_MONTH = 1
+CLIENT_GROUPS = {
+ 'ikp': {
+ 'label': 'IKP',
+ # exactly as in the Clients admin
+ 'names': ['IKP'],
+ },
+ 'phys_chem_bunt_fohrer': {
+ 'label': 'Buntkowsky + Dr. Fohrer',
+ # include all variants you might have used for Buntkowsky
+ 'names': [
+ 'AG Buntk.', # the one in your new entry
+ 'AG Buntkowsky.', # from your original list
+ 'AG Buntkowsky',
+ 'Dr. Fohrer',
+ ],
+ },
+ 'mawi_alff_gutfleisch': {
+ 'label': 'Alff + AG Gutfleisch',
+ # include both short and full forms
+ 'names': [
+ 'AG Alff',
+ 'AG Gutfl.',
+ 'AG Gutfleisch',
+ ],
+ },
+ 'm3_group': {
+ 'label': 'M3 Buntkowsky + M3 Thiele + M3 Gutfleisch',
+ 'names': [
+ 'M3 Buntkowsky',
+ 'M3 Thiele',
+ 'M3 Gutfleisch',
+ ],
+ },
+}
+
+# Add this CALCULATION_CONFIG at the top of views.py
+
+CALCULATION_CONFIG = {
+ 'top_left': {
+ # Row mappings: Django row_index (0-based) to Excel row
+ # Excel B4 -> Django row_index 1 (UI row 2)
+ # Excel B5 -> Django row_index 2 (UI row 3)
+ # Excel B6 -> Django row_index 3 (UI row 4)
+
+ # B6 (row_index 3) = B5 (row_index 2) / 0.75
+ 3: "2 / 0.75",
+
+ # B11 (row_index 10) = B9 (row_index 8)
+ 10: "8",
+
+ # B14 (row_index 13) = B13 (row_index 12) - B11 (row_index 10) + B12 (row_index 11)
+ 13: "12 - 10 + 11",
+
+ # Note: B5, B17, B19, B20 require IF logic, so they'll be handled separately
+ },
+
+ # other tables (top_right, bottom_1, ...) stay as they are
+
+
+ ' top_right': {
+ # UI Row 1 (Excel Row 4): Stand der Gaszähler (Vormonat) (Nm³)
+ 0: {
+ 'L': "9 / (9 + 9) if (9 + 9) > 0 else 0", # L4 = L13/(L13+M13)
+ 'M': "9 / (9 + 9) if (9 + 9) > 0 else 0", # M4 = M13/(L13+M13)
+ 'N': "9 / (9 + 9) if (9 + 9) > 0 else 0", # N4 = N13/(N13+O13)
+ 'O': "9 / (9 + 9) if (9 + 9) > 0 else 0", # O4 = O13/(N13+O13)
+ 'P': None, # Editable
+ 'Q': None, # Editable
+ 'R': None, # Editable
+ },
+
+ # UI Row 2 (Excel Row 5): Gasrückführung (Nm³)
+ 1: {
+ 'L': "4", # L5 = L8
+ 'M': "4", # M5 = L8 (merged)
+ 'N': "4", # N5 = N8
+ 'O': "4", # O5 = N8 (merged)
+ 'P': "4 * 0", # P5 = P8 * P4
+ 'Q': "4 * 0", # Q5 = P8 * Q4
+ 'R': "4 * 0", # R5 = P8 * R4
+ },
+
+ # UI Row 3 (Excel Row 6): Rückführung flüssig (Lit. L-He)
+ 2: {
+ 'L': "4", # L6 = L8 (Sammelrückführungen)
+ 'M': "4",
+ 'N': "4",
+ 'O': "4",
+ 'P': "4",
+ 'Q': "4",
+ 'R': "4",
+},
+
+ # UI Row 4 (Excel Row 7): Sonderrückführungen (Lit. L-He) - EDITABLE
+ 3: {
+ 'L': None,
+ 'M': None,
+ 'N': None,
+ 'O': None,
+ 'P': None,
+ 'Q': None,
+ 'R': None,
+ },
+
+ # UI Row 5 (Excel Row 8): Sammelrückführungen (Lit. L-He)
+ 4: {
+ 'L': None, # Will be populated from ExcelEntry
+ 'M': None,
+ 'N': None,
+ 'O': None,
+ 'P': None,
+ 'Q': None,
+ 'R': None,
+ },
+
+ # UI Row 6 (Excel Row 9): Bestand in Kannen-1 (Lit. L-He) - EDITABLE
+ 5: {
+ 'L': None,
+ 'M': None,
+ 'N': None,
+ 'O': None,
+ 'P': None,
+ 'Q': None,
+ 'R': None,
+ },
+
+ # UI Row 7 (Excel Row 10): Summe Bestand (Lit. L-He)
+ 6: {
+ 'L': "3 + 5", # L10 = L7 + L9
+ 'M': "3 + 5",
+ 'N': "3 + 5",
+ 'O': "3 + 5",
+ 'P': "3 + 5",
+ 'Q': "3 + 5",
+ 'R': "3 + 5",
+ },
+
+ # UI Row 8 (Excel Row 11): Best. in Kannen Vormonat (Lit. L-He)
+ 7: {
+ 'L': None, # From previous sheet
+ 'M': None,
+ 'N': None,
+ 'O': None,
+ 'P': None,
+ 'Q': None,
+ 'R': None,
+ },
+
+ # UI Row 9 (Excel Row 12): same as row 9 from previous sheet
+ 8: {
+ 'L': None,
+ 'M': None,
+ 'N': None,
+ 'O': None,
+ 'P': None,
+ 'Q': None,
+ 'R': None,
+ },
+
+ # UI Row 10 (Excel Row 13): Bezug (Liter L-He)
+ 9: {
+ 'L': None, # From SecondTableEntry
+ 'M': None,
+ 'N': None,
+ 'O': None,
+ 'P': None,
+ 'Q': None,
+ 'R': None,
+ },
+
+ # UI Row 11 (Excel Row 14): Rückführ. Soll (Lit. L-He)
+ 10: {
+ 'L': "9 + 9 + 7 - 8", # L14 = L13 + M13 - L11 + L12
+ 'M': "9 + 9 + 7 - 8",
+ 'N': "9 + 9 + 7 - 8", # N14 = N13 + O13 - N11 + N12
+ 'O': "9 + 9 + 7 - 8",
+ 'P': "9 + 7 - 8", # P14 = P13 + P12 - P11
+ 'Q': "9 + 7 - 8", # Q14 = Q13 + Q12 - Q11
+ 'R': "9 + 7 - 8", # R14 = R13 + R12 - R11
+ },
+
+ # UI Row 12 (Excel Row 15): Verluste (Soll-Rückf.) (Lit. L-He)
+ 11: {
+ 'L': "10 - 2", # L15 = L14 - L6
+ 'M': "10 - 2",
+ 'N': "10 - 2", # N15 = N14 - N6
+ 'O': "10 - 2",
+ 'P': "10 - 2", # P15 = P14 - P6
+ 'Q': "10 - 2", # Q15 = Q14 - Q6
+ 'R': "10 - 2", # R15 = R14 - R6
+ },
+
+ # UI Row 13 (Excel Row 16): Füllungen warm (Lit. L-He)
+ 12: {
+ 'L': None, # From SecondTableEntry warm count
+ 'M': None,
+ 'N': None,
+ 'O': None,
+ 'P': None,
+ 'Q': None,
+ 'R': None,
+ },
+
+ # UI Row 14 (Excel Row 17): Kaltgas Rückgabe (Lit. L-He) – Faktor
+ 13: {
+ 'L': "(9 + 9) * 14 + 12 * 15", # L17 = (L13+M13)*0.06 + (L16+M16)*15
+ 'M': "(9 + 9) * 14 + 12 * 15",
+ 'N': "(9 + 9) * 14 + 12 * 15", # N17 = (N13+O13)*0.06 + (N16+O16)*15
+ 'O': "(9 + 9) * 14 + 12 * 15",
+ 'P': "9 * 14 + 12 * 15", # P17 = P13*0.06 + P16*15
+ 'Q': "9 * 14 + 12 * 15", # Q17 = Q13*0.06 + Q16*15
+ 'R': "9 * 14 + 12 * 15", # R17 = R13*0.06 + R16*15
+ },
+
+ # UI Row 15 (Excel Row 18): Faktor 0.06
+ 14: {
+ 'L': "0.06",
+ 'M': "0.06",
+ 'N': "0.06",
+ 'O': "0.06",
+ 'P': "0.06",
+ 'Q': "0.06",
+ 'R': "0.06",
+ },
+
+ # UI Row 16 (Excel Row 19): Verbraucherverluste (Liter L-He)
+ 15: {
+ 'L': "11 - 13", # L19 = L15 - L17
+ 'M': "11 - 13",
+ 'N': "11 - 13", # N19 = N15 - N17
+ 'O': "11 - 13",
+ 'P': "11 - 13", # P19 = P15 - P17
+ 'Q': "11 - 13", # Q19 = Q15 - Q17
+ 'R': "11 - 13", # R19 = R15 - R17
+ },
+
+ # UI Row 17 (Excel Row 20): %
+ 16: {
+ 'L': "15 / (9 + 9) if (9 + 9) > 0 else 0", # L20 = L19/(L13+M13)
+ 'M': "15 / (9 + 9) if (9 + 9) > 0 else 0",
+ 'N': "15 / (9 + 9) if (9 + 9) > 0 else 0", # N20 = N19/(N13+O13)
+ 'O': "15 / (9 + 9) if (9 + 9) > 0 else 0",
+ 'P': "15 / 9 if 9 > 0 else 0", # P20 = P19/P13
+ 'Q': "15 / 9 if 9 > 0 else 0", # Q20 = Q19/Q13
+ 'R': "15 / 9 if 9 > 0 else 0", # R20 = R19/R13
+ },
+},
+ 'bottom_1': {
+ 5: "4 + 3 + 2",
+ 8: "7 - 6",
+ },
+ 'bottom_2': {
+ 3: "1 + 2",
+ 6: "5 - 4",
+ },
+ 'bottom_3': {
+ 2: "0 + 1",
+ 5: "3 + 4",
+ },
+ # Special configuration for summation column (last column)
+ 'summation_column': {
+ # For each row that should be summed across columns
+ 'rows_to_sum': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23], # All rows
+ # OR specify specific rows:
+ # 'rows_to_sum': [0, 5, 10, 15, 20], # Only specific rows
+ # The last column index (0-based)
+ 'sum_column_index': 5, # 6th column (0-5) since you have 6 clients
+ }
+}
+
+def get_group_clients(group_key):
+ """Return queryset of clients that belong to a logical group."""
+ from .models import Client # local import to avoid circulars
+
+ group = CLIENT_GROUPS.get(group_key)
+ if not group:
+ return Client.objects.none()
+ return Client.objects.filter(name__in=group['names'])
+
+def calculate_summation(sheet, table_type, row_index, sum_column_index):
+ """Calculate summation for a row, with special handling for % row"""
+ from decimal import Decimal
+ from .models import Cell
+
+ try:
+ # Special case: top_left, % row (Excel B20 -> row_index 19)
+ if table_type == 'top_left' and row_index == 19:
+ # K13 = sum of row 13 (Excel B13 -> row_index 12) across all clients
+ cells_row13 = Cell.objects.filter(
+ sheet=sheet,
+ table_type='top_left',
+ row_index=12, # Excel B13 = row_index 12
+ column_index__lt=sum_column_index # Exclude sum column itself
+ )
+ total_13 = Decimal('0')
+ for cell in cells_row13:
+ if cell.value is not None:
+ total_13 += Decimal(str(cell.value))
+
+ # K19 = sum of row 19 (Excel B19 -> row_index 18) across all clients
+ cells_row19 = Cell.objects.filter(
+ sheet=sheet,
+ table_type='top_left',
+ row_index=18, # Excel B19 = row_index 18
+ column_index__lt=sum_column_index
+ )
+ total_19 = Decimal('0')
+ for cell in cells_row19:
+ if cell.value is not None:
+ total_19 += Decimal(str(cell.value))
+
+ # Calculate: IF(K13=0; 0; K19/K13)
+ if total_13 == 0:
+ return Decimal('0')
+ return total_19 / total_13
+
+ # Normal summation for other rows
+ cells_in_row = Cell.objects.filter(
+ sheet=sheet,
+ table_type=table_type,
+ row_index=row_index,
+ column_index__lt=sum_column_index
+ )
+
+ total = Decimal('0')
+ for cell in cells_in_row:
+ if cell.value is not None:
+ total += Decimal(str(cell.value))
+
+ return total
+
+ except Exception as e:
+ print(f"Error calculating summation for {table_type}[{row_index}]: {e}")
+ return None
+
+# Helper function for calculations
+def evaluate_formula(formula, values_dict):
+ """
+ Safely evaluate a formula like "10 + 9" where numbers are row indices
+ values_dict: {row_index: decimal_value}
+ """
+ from decimal import Decimal
+ import re
+
+ try:
+ # Create a copy of the formula to work with
+ expr = formula
+
+ # Find all row numbers in the formula
+ row_refs = re.findall(r'\b\d+\b', expr)
+
+ for row_ref in row_refs:
+ row_num = int(row_ref)
+ if row_num in values_dict and values_dict[row_num] is not None:
+ # Replace row reference with actual value
+ expr = expr.replace(row_ref, str(values_dict[row_num]))
+ else:
+ # Missing value - can't calculate
+ return None
+
+ # Evaluate the expression
+ # Note: In production, use a safer evaluator like `asteval`
+ result = eval(expr, {"__builtins__": {}}, {})
+
+ # Convert to Decimal with proper rounding
+ return Decimal(str(round(result, 6)))
+
+ except Exception:
+ return None
+
+# Monthly Sheet View
+class MonthlySheetView(TemplateView):
+ template_name = 'monthly_sheet.html'
+ def populate_helium_input_to_top_right(self, sheet):
+ """Populate bezug data from SecondTableEntry to top-right table (row 9 = Excel row 13)"""
+ from .models import SecondTableEntry, Cell, Client
+ from django.db.models import Sum
+ from django.db.models.functions import Coalesce
+ from decimal import Decimal
+
+ year = sheet.year
+ month = sheet.month
+
+ # Define the client groups for top-right table IN ORDER
+ TOP_RIGHT_CLIENTS = [
+ "Dr. Fohrer", # Column index 0 (L)
+ "AG Buntk.", # Column index 1 (M)
+ "AG Alff", # Column index 2 (N)
+ "AG Gutfl.", # Column index 3 (O)
+ "M3 Thiele", # Column index 4 (P)
+ "M3 Buntkowsky", # Column index 5 (Q)
+ "M3 Gutfleisch", # Column index 6 (R)
+ ]
+
+ # Clear and update row 9 (Excel row 13 = Bezug)
+ Cell.objects.filter(
+ sheet=sheet,
+ table_type='top_right',
+ row_index=9
+ ).update(value=None)
+
+ # For each client in top-right table
+ for client_name in TOP_RIGHT_CLIENTS:
+ try:
+ client = Client.objects.get(name=client_name)
+ column_index = TOP_RIGHT_CLIENTS.index(client_name)
+
+ # Calculate total LHe_output for this client in this month from SecondTableEntry
+ total_lhe_output = SecondTableEntry.objects.filter(
+ client=client,
+ date__year=year,
+ date__month=month
+ ).aggregate(
+ total=Coalesce(Sum('lhe_output'), Decimal('0'))
+ )['total']
+
+ # Get or create the cell for row_index 9 (Excel row 13) - Bezug
+ cell, created = Cell.objects.get_or_create(
+ sheet=sheet,
+ table_type='top_right',
+ client=client,
+ row_index=9, # Bezug row
+ column_index=column_index,
+ defaults={'value': total_lhe_output}
+ )
+
+ if not created:
+ # Only update if the value is actually different
+ if cell.value != total_lhe_output:
+ cell.value = total_lhe_output
+ cell.save()
+
+ except Client.DoesNotExist:
+ continue
+
+ # After populating bezug, trigger calculation for all dependent cells
+ first_cell = Cell.objects.filter(
+ sheet=sheet,
+ table_type='top_right'
+ ).first()
+
+ if first_cell:
+ save_view = SaveCellsView()
+ save_view.calculate_top_right_dependents(sheet, first_cell)
+
+ return True
+ def calculate_bezug_from_entries(self, sheet, year, month):
+ """Calculate B11 (Bezug) from SecondTableEntry for all clients - ONLY for non-start sheets"""
+ from .models import SecondTableEntry, Cell, Client
+ from django.db.models import Sum
+ from django.db.models.functions import Coalesce
+ from decimal import Decimal
+
+ # Check if this is the start sheet
+ if year == 2025 and month == 1:
+ return # Don't auto-calculate for start sheet
+
+ for client in Client.objects.all():
+ # Calculate total LHe output for this client in this month
+ lhe_output_sum = SecondTableEntry.objects.filter(
+ client=client,
+ date__year=year,
+ date__month=month
+ ).aggregate(
+ total=Coalesce(Sum('lhe_output'), Decimal('0'))
+ )['total']
+
+ # Update B11 cell (row_index 8 = UI Row 9)
+ b11_cell = Cell.objects.filter(
+ sheet=sheet,
+ table_type='top_left',
+ client=client,
+ row_index=8 # Excel B11
+ ).first()
+
+ if b11_cell and (b11_cell.value != lhe_output_sum or b11_cell.value is None):
+ b11_cell.value = lhe_output_sum
+ b11_cell.save()
+
+ # Also trigger dependent calculations
+ from .views import SaveCellsView
+ save_view = SaveCellsView()
+ save_view.calculate_top_left_dependents(sheet, b11_cell)
+ # In MonthlySheetView.get_context_data() method, update the TOP_RIGHT_CLIENTS and row count:
+
+
+
+ return True
+ def get_context_data(self, **kwargs):
+ from decimal import Decimal
+ context = super().get_context_data(**kwargs)
+ year = self.kwargs.get('year', datetime.now().year)
+ month = self.kwargs.get('month', datetime.now().month)
+ is_start_sheet = (year == FIRST_SHEET_YEAR and month == FIRST_SHEET_MONTH)
+
+ # Get or create the monthly sheet
+ sheet, created = MonthlySheet.objects.get_or_create(
+ year=year, month=month
+ )
+
+ # All clients (used for bottom tables etc.)
+ clients = Client.objects.all().order_by('name')
+
+ # Pre-fill cells if creating new sheet
+ if created:
+ self.initialize_sheet_cells(sheet, clients)
+
+ # Apply previous month links (for B4 and B12)
+ self.apply_previous_month_links(sheet, year, month)
+ self.calculate_bezug_from_entries(sheet, year, month)
+ self.populate_helium_input_to_top_right(sheet)
+ self.apply_previous_month_links_top_right(sheet, year, month)
+
+ # Define client groups
+ TOP_LEFT_CLIENTS = [
+ "AG Vogel",
+ "AG Halfm",
+ "IKP",
+ ]
+
+ TOP_RIGHT_CLIENTS = [
+ "Dr. Fohrer",
+ "AG Buntk.",
+ "AG Alff",
+ "AG Gutfl.",
+ "M3 Thiele",
+ "M3 Buntkowsky",
+ "M3 Gutfleisch",
+ ]
+
+ # Update row counts in build_group_rows function
+ def build_group_rows(sheet, table_type, client_names):
+ """Build rows for display in monthly sheet."""
+ from decimal import Decimal
+ from .models import Cell
+
+ rows = []
+
+ # Determine row count - UPDATED for top_right
+ row_counts = {
+ 'top_left': 16,
+ 'top_right': 17, # Changed from 16 to 17 to include all rows (0-16)
+ 'bottom_1': 10,
+ 'bottom_2': 10,
+ 'bottom_3': 10,
+ }
+ row_count = row_counts.get(table_type, 0)
+
+ # Get all cells for this sheet and table
+ all_cells = Cell.objects.filter(
+ sheet=sheet,
+ table_type=table_type
+ ).select_related('client')
+
+ # Group cells by row index
+ cells_by_row = {}
+ for cell in all_cells:
+ if cell.row_index not in cells_by_row:
+ cells_by_row[cell.row_index] = {}
+ cells_by_row[cell.row_index][cell.client.name] = cell
+
+ # For each row (including row 7)
+ for row_idx in range(row_count):
+ display_cells = []
+ row_cells_dict = cells_by_row.get(row_idx, {})
+
+ # Build cells in the requested client order
+ for name in client_names:
+ cell = row_cells_dict.get(name)
+ display_cells.append(cell)
+
+ # Calculate sum for this row
+ sum_column_index = len(client_names)
+
+ # Calculate sum for the row
+ sum_value = None
+ total = Decimal('0')
+ has_value = False
+
+ for cell in display_cells:
+ if cell and cell.value is not None:
+ try:
+ total += Decimal(str(cell.value))
+ has_value = True
+ except:
+ pass
+
+ if has_value:
+ sum_value = total
+
+ rows.append({
+ 'cells': display_cells,
+ 'sum': sum_value,
+ 'row_index': row_idx, # Add this for debugging
+ })
+
+ return rows
+
+ # Now call the local function
+ top_left_rows = build_group_rows(sheet, 'top_left', TOP_LEFT_CLIENTS)
+ top_right_rows = build_group_rows(sheet, 'top_right', TOP_RIGHT_CLIENTS)
+
+ # Get cells for bottom tables
+ cells_by_table = self.get_cells_by_table(sheet)
+
+ context.update({
+ 'sheet': sheet,
+ 'clients': clients,
+ 'year': year,
+ 'month': month,
+ 'month_name': calendar.month_name[month],
+ 'prev_month': self.get_prev_month(year, month),
+ 'next_month': self.get_next_month(year, month),
+ 'cells_by_table': cells_by_table,
+ 'top_left_headers': TOP_LEFT_CLIENTS + ['Sum'],
+ 'top_right_headers': TOP_RIGHT_CLIENTS + ['Sum'],
+ 'top_left_rows': top_left_rows,
+ 'top_right_rows': top_right_rows,
+ 'is_start_sheet': is_start_sheet,
+ })
+ return context
+
+ def get_cells_by_table(self, sheet):
+ """Organize cells by table type for easy template rendering"""
+ cells = sheet.cells.select_related('client').all()
+ organized = {
+ 'top_left': [[] for _ in range(16)], # 16 rows now
+ 'top_right': [[] for _ in range(24)],
+ 'bottom_1': [[] for _ in range(10)],
+ 'bottom_2': [[] for _ in range(10)],
+ 'bottom_3': [[] for _ in range(10)],
+ }
+
+ for cell in cells:
+ if cell.table_type not in organized:
+ continue
+
+ max_rows = len(organized[cell.table_type])
+ if cell.row_index < 0 or cell.row_index >= max_rows:
+ # This is an "extra" cell from an older layout (e.g. top_left rows 18–23)
+ continue
+
+ row_list = organized[cell.table_type][cell.row_index]
+ while len(row_list) <= cell.column_index:
+ row_list.append(None)
+
+ row_list[cell.column_index] = cell
+
+ return organized
+
+
+ def initialize_sheet_cells(self, sheet, clients):
+ """Create all empty cells for a new monthly sheet"""
+ cells_to_create = []
+ summation_config = CALCULATION_CONFIG.get('summation_column', {})
+ sum_column_index = summation_config.get('sum_column_index', len(clients) - 1) # Last column
+
+ # For each table type and row
+ table_configs = [
+ ('top_left', 16),
+ ('top_right', 24),
+ ('bottom_1', 10),
+ ('bottom_2', 10),
+ ('bottom_3', 10),
+ ]
+
+ for table_type, row_count in table_configs:
+ for row_idx in range(row_count):
+ for col_idx, client in enumerate(clients):
+ is_summation = (col_idx == sum_column_index)
+ cells_to_create.append(Cell(
+ sheet=sheet,
+ client=client,
+ table_type=table_type,
+ row_index=row_idx,
+ column_index=col_idx,
+ value=None,
+ is_formula=is_summation, # Mark summation cells as formulas
+ ))
+
+ # Bulk create all cells at once
+ Cell.objects.bulk_create(cells_to_create)
+ def get_prev_month(self, year, month):
+ """Get previous month year and month"""
+ if month == 1:
+ return {'year': year - 1, 'month': 12}
+ return {'year': year, 'month': month - 1}
+
+ def get_next_month(self, year, month):
+ """Get next month year and month"""
+ if month == 12:
+ return {'year': year + 1, 'month': 1}
+ return {'year': year, 'month': month + 1}
+ def apply_previous_month_links(self, sheet, year, month):
+ """
+ For non-start sheets:
+ B4 (row 2) = previous sheet B3 (row 1)
+ B10 (row 8) = previous sheet B9 (row 7)
+ """
+ # Do nothing on the first sheet
+ if year == FIRST_SHEET_YEAR and month == FIRST_SHEET_MONTH:
+ return
+
+ # Figure out previous month
+ if month == 1:
+ prev_year = year - 1
+ prev_month = 12
+ else:
+ prev_year = year
+ prev_month = month - 1
+
+ from .models import MonthlySheet, Cell, Client
+
+ prev_sheet = MonthlySheet.objects.filter(
+ year=prev_year,
+ month=prev_month
+ ).first()
+
+ if not prev_sheet:
+ # No previous sheet created yet → nothing to copy
+ return
+
+ # For each client, copy values
+ for client in Client.objects.all():
+ # B3(prev) → B4(curr): UI row 1 → row 2 → row_index 0 → 1
+ prev_b3 = Cell.objects.filter(
+ sheet=prev_sheet,
+ table_type='top_left',
+ client=client,
+ row_index=0, # UI row 1
+ ).first()
+
+ curr_b4 = Cell.objects.filter(
+ sheet=sheet,
+ table_type='top_left',
+ client=client,
+ row_index=1, # UI row 2
+ ).first()
+
+ if prev_b3 and curr_b4:
+ curr_b4.value = prev_b3.value
+ curr_b4.save()
+
+ # B9(prev) → B10(curr): UI row 7 → row 8 → row_index 6 → 7
+ prev_b9 = Cell.objects.filter(
+ sheet=prev_sheet,
+ table_type='top_left',
+ client=client,
+ row_index=6, # UI row 7 (Summe Bestand)
+ ).first()
+
+ curr_b10 = Cell.objects.filter(
+ sheet=sheet,
+ table_type='top_left',
+ client=client,
+ row_index=7, # UI row 8 (Best. in Kannen Vormonat)
+ ).first()
+
+ if prev_b9 and curr_b10:
+ curr_b10.value = prev_b9.value
+ curr_b10.save()
+
+ def apply_previous_month_links_top_right(self, sheet, year, month):
+ """
+ top_right row 7: Best. in Kannen Vormonat (Lit. L-He)
+ = previous sheet's Summe Bestand (row 6).
+
+ For merged pairs:
+ - Dr. Fohrer + AG Buntk. share the SAME value (from previous month's AG Buntk. or Dr. Fohrer)
+ - AG Alff + AG Gutfl. share the SAME value (from previous month's AG Alff or AG Gutfl.)
+ M3 clients just copy their own value.
+ """
+ from .models import MonthlySheet, Cell, Client
+ from decimal import Decimal
+
+ # Do nothing on first sheet
+ if year == FIRST_SHEET_YEAR and month == FIRST_SHEET_MONTH:
+ return
+
+ # find previous month
+ if month == 1:
+ prev_year = year - 1
+ prev_month = 12
+ else:
+ prev_year = year
+ prev_month = month - 1
+
+ prev_sheet = MonthlySheet.objects.filter(
+ year=prev_year,
+ month=prev_month
+ ).first()
+
+ if not prev_sheet:
+ return # nothing to copy from
+
+ TOP_RIGHT_CLIENTS = [
+ "Dr. Fohrer",
+ "AG Buntk.",
+ "AG Alff",
+ "AG Gutfl.",
+ "M3 Thiele",
+ "M3 Buntkowsky",
+ "M3 Gutfleisch",
+ ]
+
+ # Helper function to get a cell value
+ def get_cell_value(sheet_obj, client_name, row_index):
+ client_obj = Client.objects.filter(name=client_name).first()
+ if not client_obj:
+ return None
+
+ try:
+ col_idx = TOP_RIGHT_CLIENTS.index(client_name)
+ except ValueError:
+ return None
+
+ cell = Cell.objects.filter(
+ sheet=sheet_obj,
+ table_type='top_right',
+ client=client_obj,
+ row_index=row_index,
+ column_index=col_idx,
+ ).first()
+
+ return cell.value if cell else None
+
+ # Helper function to set a cell value
+ def set_cell_value(sheet_obj, client_name, row_index, value):
+ client_obj = Client.objects.filter(name=client_name).first()
+ if not client_obj:
+ return False
+
+ try:
+ col_idx = TOP_RIGHT_CLIENTS.index(client_name)
+ except ValueError:
+ return False
+
+ cell, created = Cell.objects.get_or_create(
+ sheet=sheet_obj,
+ table_type='top_right',
+ client=client_obj,
+ row_index=row_index,
+ column_index=col_idx,
+ defaults={'value': value}
+ )
+
+ if not created and cell.value != value:
+ cell.value = value
+ cell.save()
+ elif created:
+ cell.save()
+
+ return True
+
+ # ----- Pair 1: Dr. Fohrer + AG Buntk. -----
+ # Get previous month's Summe Bestand (row 6) for either client in the pair
+ pair1_prev_val = None
+
+ # Try AG Buntk. first
+ prev_buntk_val = get_cell_value(prev_sheet, "AG Buntk.", 6)
+ if prev_buntk_val is not None:
+ pair1_prev_val = prev_buntk_val
+ else:
+ # Try Dr. Fohrer if AG Buntk. is empty
+ prev_fohrer_val = get_cell_value(prev_sheet, "Dr. Fohrer", 6)
+ if prev_fohrer_val is not None:
+ pair1_prev_val = prev_fohrer_val
+
+ # Apply the value to both clients in the pair
+ if pair1_prev_val is not None:
+ set_cell_value(sheet, "Dr. Fohrer", 7, pair1_prev_val)
+ set_cell_value(sheet, "AG Buntk.", 7, pair1_prev_val)
+
+ # ----- Pair 2: AG Alff + AG Gutfl. -----
+ pair2_prev_val = None
+
+ # Try AG Alff first
+ prev_alff_val = get_cell_value(prev_sheet, "AG Alff", 6)
+ if prev_alff_val is not None:
+ pair2_prev_val = prev_alff_val
+ else:
+ # Try AG Gutfl. if AG Alff is empty
+ prev_gutfl_val = get_cell_value(prev_sheet, "AG Gutfl.", 6)
+ if prev_gutfl_val is not None:
+ pair2_prev_val = prev_gutfl_val
+
+ # Apply the value to both clients in the pair
+ if pair2_prev_val is not None:
+ set_cell_value(sheet, "AG Alff", 7, pair2_prev_val)
+ set_cell_value(sheet, "AG Gutfl.", 7, pair2_prev_val)
+
+ # ----- M3 clients: copy their own Summe Bestand -----
+ for name in ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"]:
+ prev_val = get_cell_value(prev_sheet, name, 6)
+ if prev_val is not None:
+ set_cell_value(sheet, name, 7, prev_val)
+# Add this helper function to views.py
+def get_factor_value(table_type, row_index):
+ """Get factor value (like 0.06 for top_left row 17)"""
+ factors = {
+ ('top_left', 17): Decimal('0.06'), # A18 in Excel (UI row 17, 0-based index 16)
+ }
+ return factors.get((table_type, row_index), Decimal('0'))
+# Save Cells View
+# views.py - Updated SaveCellsView
+# views.py - Update SaveCellsView class
+def debug_cell_values(self, sheet, client_id):
+ """Debug method to check cell values"""
+ from .models import Cell
+ cells = Cell.objects.filter(
+ sheet=sheet,
+ table_type='top_left',
+ client_id=client_id
+ ).order_by('row_index')
+
+ debug_info = {}
+ for cell in cells:
+ debug_info[f"row_{cell.row_index}"] = {
+ 'value': str(cell.value) if cell.value else 'None',
+ 'ui_row': cell.row_index + 1,
+ 'excel_ref': f"B{cell.row_index + 3}"
+ }
+
+ return debug_info
+class DebugCalculationView(View):
+ """Debug view to test calculations directly"""
+ def get(self, request):
+ sheet_id = request.GET.get('sheet_id', 1)
+ client_name = request.GET.get('client', 'AG Vogel')
+
+ try:
+ sheet = MonthlySheet.objects.get(id=sheet_id)
+ client = Client.objects.get(name=client_name)
+
+ # Get SaveCellsView instance
+ save_view = SaveCellsView()
+
+ # Create a dummy cell to trigger calculations
+ dummy_cell = Cell.objects.filter(
+ sheet=sheet,
+ table_type='top_left',
+ client=client,
+ row_index=0 # B3
+ ).first()
+
+ if not dummy_cell:
+ return JsonResponse({'error': 'No cells found for this client'})
+
+ # Trigger calculation
+ updated = save_view.calculate_top_left_dependents(sheet, dummy_cell)
+
+ # Get updated cell values
+ cells = Cell.objects.filter(
+ sheet=sheet,
+ table_type='top_left',
+ client=client
+ ).order_by('row_index')
+
+ cell_data = []
+ for cell in cells:
+ cell_data.append({
+ 'row_index': cell.row_index,
+ 'ui_row': cell.row_index + 1,
+ 'excel_ref': f"B{cell.row_index + 3}",
+ 'value': str(cell.value) if cell.value else 'None',
+ 'description': self.get_row_description(cell.row_index)
+ })
+
+ return JsonResponse({
+ 'sheet': f"{sheet.year}-{sheet.month:02d}",
+ 'client': client.name,
+ 'cells': cell_data,
+ 'updated_count': len(updated),
+ 'calculation': 'B5 = IF(B3>0; B3-B4; 0)'
+ })
+
+ except Exception as e:
+ return JsonResponse({'error': str(e)}, status=400)
+
+ def get_row_description(self, row_index):
+ """Get description for row index"""
+ descriptions = {
+ 0: "B3: Stand der Gaszähler (Nm³)",
+ 1: "B4: Stand der Gaszähler (Vormonat) (Nm³)",
+ 2: "B5: Gasrückführung (Nm³)",
+ 3: "B6: Rückführung flüssig (Lit. L-He)",
+ 4: "B7: Sonderrückführungen (Lit. L-He)",
+ 5: "B8: Bestand in Kannen-1 (Lit. L-He)",
+ 6: "B9: Summe Bestand (Lit. L-He)",
+ 7: "B10: Best. in Kannen Vormonat (Lit. L-He)",
+ 8: "B11: Bezug (Liter L-He)",
+ 9: "B12: Rückführ. Soll (Lit. L-He)",
+ 10: "B13: Verluste (Soll-Rückf.) (Lit. L-He)",
+ 11: "B14: Füllungen warm (Lit. L-He)",
+ 12: "B15: Kaltgas Rückgabe (Lit. L-He) – Faktor",
+ 13: "B16: Faktor",
+ 14: "B17: Verbraucherverluste (Liter L-He)",
+ 15: "B18: %"
+ }
+ return descriptions.get(row_index, f"Row {row_index}")
+def recalculate_stand_der_gaszahler(self, sheet):
+ """Recalculate Stand der Gaszähler for all client pairs"""
+ from decimal import Decimal
+
+ # For Dr. Fohrer and AG Buntk. (L & M columns)
+ try:
+ # Get Dr. Fohrer's bezug
+ dr_fohrer_client = Client.objects.get(name="Dr. Fohrer")
+ dr_fohrer_cell = Cell.objects.filter(
+ sheet=sheet,
+ table_type='top_right',
+ client=dr_fohrer_client,
+ row_index=9 # Row 9 = Bezug
+ ).first()
+ L13 = Decimal(str(dr_fohrer_cell.value)) if dr_fohrer_cell and dr_fohrer_cell.value else Decimal('0')
+
+ # Get AG Buntk.'s bezug
+ ag_buntk_client = Client.objects.get(name="AG Buntk.")
+ ag_buntk_cell = Cell.objects.filter(
+ sheet=sheet,
+ table_type='top_right',
+ client=ag_buntk_client,
+ row_index=9
+ ).first()
+ M13 = Decimal(str(ag_buntk_cell.value)) if ag_buntk_cell and ag_buntk_cell.value else Decimal('0')
+
+ total = L13 + M13
+ if total > 0:
+ # Update Dr. Fohrer's row 0
+ dr_fohrer_row0 = Cell.objects.filter(
+ sheet=sheet,
+ table_type='top_right',
+ client=dr_fohrer_client,
+ row_index=0
+ ).first()
+ if dr_fohrer_row0:
+ dr_fohrer_row0.value = L13 / total
+ dr_fohrer_row0.save()
+
+ # Update AG Buntk.'s row 0
+ ag_buntk_row0 = Cell.objects.filter(
+ sheet=sheet,
+ table_type='top_right',
+ client=ag_buntk_client,
+ row_index=0
+ ).first()
+ if ag_buntk_row0:
+ ag_buntk_row0.value = M13 / total
+ ag_buntk_row0.save()
+ except Exception as e:
+ print(f"Error recalculating Stand der Gaszähler for Dr. Fohrer/AG Buntk.: {e}")
+
+ # For AG Alff and AG Gutfl. (N & O columns)
+ try:
+ # Get AG Alff's bezug
+ ag_alff_client = Client.objects.get(name="AG Alff")
+ ag_alff_cell = Cell.objects.filter(
+ sheet=sheet,
+ table_type='top_right',
+ client=ag_alff_client,
+ row_index=9
+ ).first()
+ N13 = Decimal(str(ag_alff_cell.value)) if ag_alff_cell and ag_alff_cell.value else Decimal('0')
+
+ # Get AG Gutfl.'s bezug
+ ag_gutfl_client = Client.objects.get(name="AG Gutfl.")
+ ag_gutfl_cell = Cell.objects.filter(
+ sheet=sheet,
+ table_type='top_right',
+ client=ag_gutfl_client,
+ row_index=9
+ ).first()
+ O13 = Decimal(str(ag_gutfl_cell.value)) if ag_gutfl_cell and ag_gutfl_cell.value else Decimal('0')
+
+ total = N13 + O13
+ if total > 0:
+ # Update AG Alff's row 0
+ ag_alff_row0 = Cell.objects.filter(
+ sheet=sheet,
+ table_type='top_right',
+ client=ag_alff_client,
+ row_index=0
+ ).first()
+ if ag_alff_row0:
+ ag_alff_row0.value = N13 / total
+ ag_alff_row0.save()
+
+ # Update AG Gutfl.'s row 0
+ ag_gutfl_row0 = Cell.objects.filter(
+ sheet=sheet,
+ table_type='top_right',
+ client=ag_gutfl_client,
+ row_index=0
+ ).first()
+ if ag_gutfl_row0:
+ ag_gutfl_row0.value = O13 / total
+ ag_gutfl_row0.save()
+ except Exception as e:
+ print(f"Error recalculating Stand der Gaszähler for AG Alff/AG Gutfl.: {e}")
+# In your SaveCellsView class in views.py
+class DebugTopRightView(View):
+ """Debug view to check top_right calculations"""
+ def get(self, request):
+ sheet_id = request.GET.get('sheet_id', 1)
+ client_name = request.GET.get('client', 'Dr. Fohrer')
+
+ try:
+ sheet = MonthlySheet.objects.get(id=sheet_id)
+ client = Client.objects.get(name=client_name)
+
+ # Get all cells for this client in top_right
+ cells = Cell.objects.filter(
+ sheet=sheet,
+ table_type='top_right',
+ client=client
+ ).order_by('row_index')
+
+ cell_data = []
+ descriptions = {
+ 0: "Stand der Gaszähler (Vormonat)",
+ 1: "Gasrückführung (Nm³)",
+ 2: "Rückführung flüssig",
+ 3: "Sonderrückführungen",
+ 4: "Sammelrückführungen",
+ 5: "Bestand in Kannen-1",
+ 6: "Summe Bestand",
+ 7: "Best. in Kannen Vormonat",
+ 8: "Same as row 9 from prev sheet",
+ 9: "Bezug",
+ 10: "Rückführ. Soll",
+ 11: "Verluste",
+ 12: "Füllungen warm",
+ 13: "Kaltgas Rückgabe",
+ 14: "Faktor 0.06",
+ 15: "Verbraucherverluste",
+ 16: "%"
+ }
+
+ for cell in cells:
+ cell_data.append({
+ 'row_index': cell.row_index,
+ 'ui_row': cell.row_index + 1,
+ 'description': descriptions.get(cell.row_index, f"Row {cell.row_index}"),
+ 'value': str(cell.value) if cell.value else 'Empty',
+ 'cell_id': cell.id
+ })
+
+ # Test calculation
+ row3_cell = cells.filter(row_index=3).first()
+ row5_cell = cells.filter(row_index=5).first()
+ row6_cell = cells.filter(row_index=6).first()
+
+ calculation_info = {
+ 'row3_value': str(row3_cell.value) if row3_cell and row3_cell.value else '0',
+ 'row5_value': str(row5_cell.value) if row5_cell and row5_cell.value else '0',
+ 'row6_value': str(row6_cell.value) if row6_cell and row6_cell.value else '0',
+ 'expected_sum': '0'
+ }
+
+ if row3_cell and row5_cell and row6_cell:
+ row3_val = Decimal(str(row3_cell.value)) if row3_cell.value else Decimal('0')
+ row5_val = Decimal(str(row5_cell.value)) if row5_cell.value else Decimal('0')
+ expected = row3_val + row5_val
+ calculation_info['expected_sum'] = str(expected)
+ calculation_info['is_correct'] = row6_cell.value == expected
+
+ return JsonResponse({
+ 'sheet': f"{sheet.year}-{sheet.month}",
+ 'client': client.name,
+ 'cells': cell_data,
+ 'calculation': calculation_info
+ })
+
+ except Exception as e:
+ return JsonResponse({'error': str(e)}, status=400)
+class SaveCellsView(View):
+ def recalculate_summe_bestand_for_all(self, sheet):
+ """Recalculate Summe Bestand for all clients in top_right table"""
+ from decimal import Decimal
+ from .models import Cell, Client
+
+ # Get all clients in top_right table
+ TOP_RIGHT_CLIENTS = [
+ "Dr. Fohrer", # Column index 0 (L)
+ "AG Buntk.", # Column index 1 (M)
+ "AG Alff", # Column index 2 (N)
+ "AG Gutfl.", # Column index 3 (O)
+ "M3 Thiele", # Column index 4 (P)
+ "M3 Buntkowsky", # Column index 5 (Q)
+ "M3 Gutfleisch", # Column index 6 (R)
+ ]
+
+ updated_cells = []
+
+ for client_name in TOP_RIGHT_CLIENTS:
+ try:
+ client = Client.objects.get(name=client_name)
+
+ # Get cells for this client
+ cells = Cell.objects.filter(
+ sheet=sheet,
+ table_type='top_right',
+ client=client,
+ row_index__in=[3, 5, 6] # Row 3, 5, 6
+ )
+
+ # Create dict
+ cell_dict = {}
+ for cell in cells:
+ cell_dict[cell.row_index] = cell
+
+ # Get values
+ row3_cell = cell_dict.get(3) # Sonderrückführungen
+ row5_cell = cell_dict.get(5) # Bestand in Kannen-1
+ row6_cell = cell_dict.get(6) # Summe Bestand
+
+ if row3_cell and row5_cell and row6_cell:
+ row5_value = Decimal(str(row5_cell.value)) if row5_cell.value else Decimal('0')
+
+ # Calculate Summe Bestand
+ summe_bestand = row3_value + row5_value
+
+ # Update if different
+ if row6_cell.value != summe_bestand:
+ row6_cell.value = summe_bestand
+ row6_cell.save()
+ updated_cells.append({
+ 'id': row6_cell.id,
+ 'value': str(row6_cell.value),
+ 'is_calculated': True
+ })
+
+ except Client.DoesNotExist:
+ continue
+
+ return updated_cells
+class SaveCellsView(View):
+ # In your SaveCellsView class, update the calculate_top_right_dependents method:
+
+ def calculate_top_right_dependents(self, sheet, changed_cell):
+ """
+ Recalculate ALL dependent cells in the top_right table whenever one cell changes.
+ This implements the Excel logic for merged client pairs with correct formulas.
+ """
+ from decimal import Decimal
+ from django.db.models import Sum
+ from django.db.models.functions import Coalesce
+ from .models import SecondTableEntry, ExcelEntry, Client, Cell
+
+ updated_cells = []
+
+ TOP_RIGHT_CLIENTS = [
+ "Dr. Fohrer",
+ "AG Buntk.",
+ "AG Alff",
+ "AG Gutfl.",
+ "M3 Thiele",
+ "M3 Buntkowsky",
+ "M3 Gutfleisch",
+ ]
+
+ MERGED_PAIRS = [
+ ("Dr. Fohrer", "AG Buntk."), # pair 1: L/M
+ ("AG Alff", "AG Gutfl."), # pair 2: N/O
+ ]
+
+ M3_CLIENTS = ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"]
+
+ factor = Decimal("0.06")
+
+ # --------- helper functions ---------
+ def get_cell(client_name: str, row_idx: int):
+ client_obj = Client.objects.filter(name=client_name).first()
+ if not client_obj:
+ return None
+ try:
+ col_idx = TOP_RIGHT_CLIENTS.index(client_name)
+ except ValueError:
+ return None
+ return Cell.objects.filter(
+ sheet=sheet,
+ table_type="top_right",
+ client=client_obj,
+ row_index=row_idx,
+ column_index=col_idx,
+ ).first()
+
+ def get_val(client_name: str, row_idx: int) -> Decimal:
+ cell = get_cell(client_name, row_idx)
+ if cell and cell.value is not None:
+ try:
+ return Decimal(str(cell.value))
+ except Exception:
+ return Decimal("0")
+ return Decimal("0")
+
+ def set_val(client_name: str, row_idx: int, value: Decimal, is_calculated=True):
+ client_obj = Client.objects.get(name=client_name)
+ col_idx = TOP_RIGHT_CLIENTS.index(client_name)
+
+ cell, created = Cell.objects.get_or_create(
+ sheet=sheet,
+ table_type="top_right",
+ client=client_obj,
+ row_index=row_idx,
+ column_index=col_idx,
+ defaults={"value": value},
+ )
+ if not created and cell.value == value:
+ return
+
+ cell.value = value
+ cell.save()
+ updated_cells.append(
+ {"id": cell.id, "value": str(cell.value), "is_calculated": is_calculated}
+ )
+
+ # --------- Get all current values ---------
+ all_cells = Cell.objects.filter(
+ sheet=sheet,
+ table_type="top_right"
+ ).select_related('client')
+
+ client_values = {}
+ for cell in all_cells:
+ if cell.client.name not in client_values:
+ client_values[cell.client.name] = {}
+ client_values[cell.client.name][cell.row_index] = cell.value
+
+ def get_cached_val(client_name: str, row_idx: int) -> Decimal:
+ if client_name in client_values and row_idx in client_values[client_name]:
+ val = client_values[client_name][row_idx]
+ if val is not None:
+ try:
+ return Decimal(str(val))
+ except Exception:
+ return Decimal("0")
+ return Decimal("0")
+
+ # --------- 1. Füllungen warm (row 12) for ALL clients ---------
+ for name in TOP_RIGHT_CLIENTS:
+ client_obj = Client.objects.filter(name=name).first()
+ if not client_obj:
+ continue
+ warm_count = SecondTableEntry.objects.filter(
+ client=client_obj,
+ date__year=sheet.year,
+ date__month=sheet.month,
+ is_warm=True,
+ ).count()
+ set_val(name, 12, Decimal(str(warm_count)))
+ client_values[name][12] = Decimal(str(warm_count))
+
+ # --------- 2. Calculate Summe Bestand (row 6) ---------
+ # Summe Bestand = Sonderrückführungen (row 3) + Bestand in Kannen-1 (row 5)
+ for name in TOP_RIGHT_CLIENTS:
+ row3_val = get_cached_val(name, 3) # Sonderrückführungen
+ row5_val = get_cached_val(name, 5) # Bestand in Kannen-1
+
+ summe_bestand = row3_val + row5_val
+ set_val(name, 6, summe_bestand, is_calculated=True)
+ client_values[name][6] = summe_bestand
+
+ # --------- 3. Process merged pairs ---------
+ for left_name, right_name in MERGED_PAIRS:
+ # Get values for the pair
+ L9 = get_cached_val(left_name, 9) # Bezug (left)
+ R9 = get_cached_val(right_name, 9) # Bezug (right)
+ L8 = get_cached_val(left_name, 8) # same as row 9 from prev sheet
+ L7 = get_cached_val(left_name, 7) # Best. in Kannen Vormonat
+ L2 = get_cached_val(left_name, 2) # Rückführung flüssig
+ L12 = get_cached_val(left_name, 12) # Füllungen warm (left)
+ R12 = get_cached_val(right_name, 12) # Füllungen warm (right)
+
+ # ----- 1. Rückführ. Soll (row 10) -----
+ # For Dr. Fohrer/AG Buntk: L14 = L13 - L11 + L12
+ # For AG Alff/AG Gutfl: N14 = N13 + N12 - N11 (which is the same formula: bezug + Best.Vormonat - PrevSheet)
+ rueckf_soll = L9 - L8 + L7
+
+ # Set same value for both clients in the pair
+ set_val(left_name, 10, rueckf_soll)
+ set_val(right_name, 10, rueckf_soll)
+ client_values[left_name][10] = rueckf_soll
+ client_values[right_name][10] = rueckf_soll
+
+ # ----- 2. Verluste (row 11) -----
+ # Formula: = L14 - L6 (where L14 is Rückführ. Soll, L6 is Rückführung flüssig)
+ verluste = rueckf_soll - L2
+ set_val(left_name, 11, verluste)
+ set_val(right_name, 11, verluste)
+ client_values[left_name][11] = verluste
+ client_values[right_name][11] = verluste
+
+ # ----- 3. Kaltgas Rückgabe (row 13) -----
+ # Formula: = L13 * factor + (L16 + M16) * 15
+ # Note: For AG Alff/AG Gutfl, it should be (N13+O13)*factor + (N16+O16)*15
+ if left_name == "Dr. Fohrer":
+ # For Dr. Fohrer/AG Buntk: use only Dr. Fohrer's bezug
+ kaltgas = L9 * factor + (L12 + R12) * Decimal("15")
+ else:
+ # For AG Alff/AG Gutfl: use sum of both bezugs
+ kaltgas = (L9 + R9) * factor + (L12 + R12) * Decimal("15")
+
+ set_val(left_name, 13, kaltgas)
+ set_val(right_name, 13, kaltgas)
+ client_values[left_name][13] = kaltgas
+ client_values[right_name][13] = kaltgas
+
+ # ----- 4. Faktor 0.06 (row 14) -----
+ set_val(left_name, 14, factor)
+ set_val(right_name, 14, factor)
+ client_values[left_name][14] = factor
+ client_values[right_name][14] = factor
+
+ # ----- 5. Verbraucherverluste (row 15) -----
+ # Formula: = L15 - L17
+ verbrauch = verluste - kaltgas
+ set_val(left_name, 15, verbrauch)
+ set_val(right_name, 15, verbrauch)
+ client_values[left_name][15] = verbrauch
+ client_values[right_name][15] = verbrauch
+
+ # ----- 6. % (row 16) -----
+ if left_name == "Dr. Fohrer":
+ # For Dr. Fohrer/AG Buntk: L20 = L19 / L13
+ if L9 > 0:
+ prozent = verbrauch / L9
+ else:
+ prozent = Decimal("0")
+ else:
+ # For AG Alff/AG Gutfl: N20 = N19 / (N13 + O13)
+ total_bezug = L9 + R9
+ if total_bezug > 0:
+ prozent = verbrauch / total_bezug
+ else:
+ prozent = Decimal("0")
+
+ set_val(left_name, 16, prozent)
+ set_val(right_name, 16, prozent)
+ client_values[left_name][16] = prozent
+ client_values[right_name][16] = prozent
+
+ # --------- 4. M3 clients (no merging, each individually) ---------
+ for name in M3_CLIENTS:
+ P9 = get_cached_val(name, 9) # Bezug (row 9)
+ P8 = get_cached_val(name, 8) # prev sheet (row 8)
+ P7 = get_cached_val(name, 7) # Best. in Kannen Vormonat (row 7)
+ P2 = get_cached_val(name, 2) # Rückführung flüssig (row 2)
+ P12 = get_cached_val(name, 12) # Füllungen warm (row 12)
+
+ # Rückführ. Soll = P9 - P8 + P7
+ rueckf_soll_m3 = P9 - P8 + P7
+ set_val(name, 10, rueckf_soll_m3)
+ client_values[name][10] = rueckf_soll_m3
+
+ # Verluste = P10 - P2
+ verluste_m3 = rueckf_soll_m3 - P2
+ set_val(name, 11, verluste_m3)
+ client_values[name][11] = verluste_m3
+
+ # Kaltgas = P9 * factor + P12 * 15
+ kaltgas_m3 = P9 * factor + P12 * Decimal("15")
+ set_val(name, 13, kaltgas_m3)
+ client_values[name][13] = kaltgas_m3
+
+ # Faktor 0.06
+ set_val(name, 14, factor)
+ client_values[name][14] = factor
+
+ # Verbraucherverluste = P11 - P13
+ verbrauch_m3 = verluste_m3 - kaltgas_m3
+ set_val(name, 15, verbrauch_m3)
+ client_values[name][15] = verbrauch_m3
+
+ # % = P15 / P9
+ if P9 > 0:
+ prozent_m3 = verbrauch_m3 / P9
+ else:
+ prozent_m3 = Decimal("0")
+ set_val(name, 16, prozent_m3)
+ client_values[name][16] = prozent_m3
+
+ return updated_cells
+
+
+ def post(self, request):
+ try:
+ sheet_id = request.POST.get('sheet_id')
+ sheet = MonthlySheet.objects.get(id=sheet_id)
+
+ # Check if it's a single cell save or bulk save
+ single_cell_id = request.POST.get('cell_id')
+
+ if single_cell_id:
+ # Single cell save
+ return self.save_single_cell(request, sheet, single_cell_id)
+ else:
+ # Bulk save (for backward compatibility)
+ return self.save_bulk_cells(request, sheet)
+
+ except MonthlySheet.DoesNotExist:
+ return JsonResponse({
+ 'status': 'error',
+ 'message': 'Sheet not found'
+ }, status=404)
+ except Exception as e:
+ return JsonResponse({
+ 'status': 'error',
+ 'message': str(e)
+ }, status=400)
+ def apply_top_right_group_links(self, sheet):
+ """
+ Top-right merging logic for client pairs:
+
+ 1. Bestand in Kannen-1 (row 5) is shared within each pair
+ 2. Summe Bestand (row 6) = Bestand in Kannen-1 (row 5) only (not row3+row5)
+ 3. Several other calculations are merged for each pair
+
+ Merged pairs:
+ - (Dr. Fohrer, AG Buntk.)
+ - (AG Alff, AG Gutfl.)
+ """
+ from decimal import Decimal
+ from .models import Client, Cell
+
+ TOP_RIGHT_CLIENTS = [
+ "Dr. Fohrer",
+ "AG Buntk.",
+ "AG Alff",
+ "AG Gutfl.",
+ "M3 Thiele",
+ "M3 Buntkowsky",
+ "M3 Gutfleisch",
+ ]
+
+ MERGED_PAIRS = [
+ ("Dr. Fohrer", "AG Buntk."),
+ ("AG Alff", "AG Gutfl."),
+ ]
+
+ updated_cells = []
+
+ def get_cell(client_name, row_index):
+ client = Client.objects.filter(name=client_name).first()
+ if not client:
+ return None
+ try:
+ col_index = TOP_RIGHT_CLIENTS.index(client_name)
+ except ValueError:
+ return None
+
+ return Cell.objects.filter(
+ sheet=sheet,
+ table_type='top_right',
+ client=client,
+ row_index=row_index,
+ column_index=col_index,
+ ).first()
+
+ def calculate_rueckf_soll_for_pair(left_name, right_name):
+ """Calculate Rückführ. Soll for a merged pair: L13+M13-L11+L12"""
+ left_13 = get_cell(left_name, 9) # Bezug (row 9)
+ right_13 = get_cell(right_name, 9) # Bezug (row 9)
+ left_11 = get_cell(left_name, 8) # Same as row 9 from prev sheet (row 8)
+ left_12 = get_cell(left_name, 7) # Best. in Kannen Vormonat (row 7)
+
+ L13 = Decimal(str(left_13.value)) if left_13 and left_13.value else Decimal('0')
+ M13 = Decimal(str(right_13.value)) if right_13 and right_13.value else Decimal('0')
+ L11 = Decimal(str(left_11.value)) if left_11 and left_11.value else Decimal('0')
+ L12 = Decimal(str(left_12.value)) if left_12 and left_12.value else Decimal('0')
+
+ return L13 + M13 - L11 + L12
+
+ def calculate_verluste_for_pair(left_name, right_name, rueckf_soll_value):
+ """Calculate Verluste for a merged pair: (L14+M14)-L6"""
+ left_14 = get_cell(left_name, 10) # Rückführ. Soll (row 10)
+ right_14 = get_cell(right_name, 10) # Rückführ. Soll (row 10)
+ left_6 = get_cell(left_name, 2) # Rückführung flüssig (row 2)
+
+ # Use provided rueckf_soll_value for calculation
+ L6 = Decimal(str(left_6.value)) if left_6 and left_6.value else Decimal('0')
+
+ return rueckf_soll_value - L6
+
+ def calculate_kaltgas_for_pair(left_name, right_name):
+ """Calculate Kaltgas Rückgabe for a merged pair: (L13+M13)*0.06 + (L16+M16)*15"""
+ left_13 = get_cell(left_name, 9) # Bezug (row 9)
+ right_13 = get_cell(right_name, 9) # Bezug (row 9)
+ left_16 = get_cell(left_name, 12) # Füllungen warm (row 12)
+ right_16 = get_cell(right_name, 12) # Füllungen warm (row 12)
+
+ L13 = Decimal(str(left_13.value)) if left_13 and left_13.value else Decimal('0')
+ M13 = Decimal(str(right_13.value)) if right_13 and right_13.value else Decimal('0')
+ L16 = Decimal(str(left_16.value)) if left_16 and left_16.value else Decimal('0')
+ M16 = Decimal(str(right_16.value)) if right_16 and right_16.value else Decimal('0')
+
+ return (L13 + M13) * Decimal('0.06') + (L16 + M16) * Decimal('15')
+
+ def calculate_verbraucherverluste_for_pair(verluste_value, kaltgas_value):
+ """Calculate Verbraucherverluste for a merged pair: L15-L17"""
+ return verluste_value - kaltgas_value
+
+ def calculate_prozent_for_pair(verbraucher_value, left_name, right_name):
+ """Calculate % for a merged pair: L19/(L13+M13)"""
+ left_13 = get_cell(left_name, 9) # Bezug (row 9)
+ right_13 = get_cell(right_name, 9) # Bezug (row 9)
+
+ L13 = Decimal(str(left_13.value)) if left_13 and left_13.value else Decimal('0')
+ M13 = Decimal(str(right_13.value)) if right_13 and right_13.value else Decimal('0')
+
+ total_bezug = L13 + M13
+ if total_bezug > 0:
+ return verbraucher_value / total_bezug
+ else:
+ return Decimal('0')
+
+ # Process each merged pair
+ for left_name, right_name in MERGED_PAIRS:
+ # ----- 1) Sync Bestand in Kannen-1 (row 5) between the two clients -----
+ left_best = get_cell(left_name, 5)
+ right_best = get_cell(right_name, 5)
+
+ # Choose a value to propagate (prefer left if present, else right)
+ bestand_value = None
+ if left_best and left_best.value is not None:
+ bestand_value = left_best.value
+ elif right_best and right_best.value is not None:
+ bestand_value = right_best.value
+
+ # Update both cells with the same value
+ if bestand_value is not None:
+ for cell in (left_best, right_best):
+ if cell and cell.value != bestand_value:
+ cell.value = bestand_value
+ cell.save()
+ updated_cells.append({
+ 'id': cell.id,
+ 'value': str(cell.value),
+ 'is_calculated': False,
+ })
+
+ # ----- 2) Update Summe Bestand (row 6) = Bestand in Kannen-1 (row 5) -----
+ # Summe Bestand should only equal row 5 (not row3+row5)
+ for name in (left_name, right_name):
+ bestand_cell = get_cell(name, 5)
+ summe_cell = get_cell(name, 6)
+
+ if bestand_cell and summe_cell:
+ new_sum = bestand_cell.value if bestand_cell.value is not None else Decimal('0')
+
+ if summe_cell.value != new_sum:
+ summe_cell.value = new_sum
+ summe_cell.save()
+ updated_cells.append({
+ 'id': summe_cell.id,
+ 'value': str(summe_cell.value),
+ 'is_calculated': True,
+ })
+
+ # ----- 3) Calculate merged values for the pair -----
+ # Calculate Rückführ. Soll (row 10)
+ rueckf_soll_value = calculate_rueckf_soll_for_pair(left_name, right_name)
+
+ # Update both clients with the same Rückführ. Soll value
+ for name in (left_name, right_name):
+ rueckf_cell = get_cell(name, 10)
+ if rueckf_cell and rueckf_cell.value != rueckf_soll_value:
+ rueckf_cell.value = rueckf_soll_value
+ rueckf_cell.save()
+ updated_cells.append({
+ 'id': rueckf_cell.id,
+ 'value': str(rueckf_cell.value),
+ 'is_calculated': True,
+ })
+
+ # Calculate Verluste (row 11)
+ verluste_value = calculate_verluste_for_pair(left_name, right_name, rueckf_soll_value)
+
+ # Update both clients with the same Verluste value
+ for name in (left_name, right_name):
+ verluste_cell = get_cell(name, 11)
+ if verluste_cell and verluste_cell.value != verluste_value:
+ verluste_cell.value = verluste_value
+ verluste_cell.save()
+ updated_cells.append({
+ 'id': verluste_cell.id,
+ 'value': str(verluste_cell.value),
+ 'is_calculated': True,
+ })
+
+ # Calculate Kaltgas Rückgabe (row 13)
+ kaltgas_value = calculate_kaltgas_for_pair(left_name, right_name)
+
+ # Update both clients with the same Kaltgas value
+ for name in (left_name, right_name):
+ kaltgas_cell = get_cell(name, 13)
+ if kaltgas_cell and kaltgas_cell.value != kaltgas_value:
+ kaltgas_cell.value = kaltgas_value
+ kaltgas_cell.save()
+ updated_cells.append({
+ 'id': kaltgas_cell.id,
+ 'value': str(kaltgas_cell.value),
+ 'is_calculated': True,
+ })
+
+ # Calculate Verbraucherverluste (row 15)
+ verbraucher_value = calculate_verbraucherverluste_for_pair(verluste_value, kaltgas_value)
+
+ # Update both clients with the same Verbraucherverluste value
+ for name in (left_name, right_name):
+ verbraucher_cell = get_cell(name, 15)
+ if verbraucher_cell and verbraucher_cell.value != verbraucher_value:
+ verbraucher_cell.value = verbraucher_value
+ verbraucher_cell.save()
+ updated_cells.append({
+ 'id': verbraucher_cell.id,
+ 'value': str(verbraucher_cell.value),
+ 'is_calculated': True,
+ })
+
+ # Calculate % (row 16)
+ prozent_value = calculate_prozent_for_pair(verbraucher_value, left_name, right_name)
+
+ # Update both clients with the same % value
+ for name in (left_name, right_name):
+ prozent_cell = get_cell(name, 16)
+ if prozent_cell and prozent_cell.value != prozent_value:
+ prozent_cell.value = prozent_value
+ prozent_cell.save()
+ updated_cells.append({
+ 'id': prozent_cell.id,
+ 'value': str(prozent_cell.value),
+ 'is_calculated': True,
+ })
+
+ return updated_cells
+ def save_single_cell(self, request, sheet, cell_id):
+ """Save a single cell and calculate dependents"""
+ try:
+ # Get the cell
+ cell = Cell.objects.get(id=cell_id, sheet=sheet)
+ old_value = cell.value
+ new_value_str = request.POST.get('value', '').strip()
+
+ # Convert new value
+ try:
+ if new_value_str:
+ cell.value = Decimal(new_value_str)
+ else:
+ cell.value = None
+ except Exception:
+ cell.value = None
+
+ # Save the cell
+ cell.save()
+
+ # Start with this cell in the updated list
+ updated_cells = [{
+ 'id': cell.id,
+ 'value': str(cell.value) if cell.value else '',
+ 'is_calculated': False
+ }]
+
+ # Calculate dependent cells based on table type
+ if cell.table_type == 'top_left':
+ dependent_updates = self.calculate_top_left_dependents(sheet, cell)
+ updated_cells.extend(dependent_updates)
+ elif cell.table_type == 'top_right':
+ # Always use the unified calculation logic for the top-right table
+ dependent_updates = self.calculate_top_right_dependents(sheet, cell)
+ updated_cells.extend(dependent_updates)
+
+ return JsonResponse({
+ 'status': 'success',
+ 'updated_cells': updated_cells
+ })
+
+ except Cell.DoesNotExist:
+ return JsonResponse({
+ 'status': 'error',
+ 'message': 'Cell not found'
+ }, status=404)
+ except Exception as e:
+ return JsonResponse({
+ 'status': 'error',
+ 'message': str(e)
+ }, status=400)
+
+ def calculate_top_left_dependents(self, sheet, changed_cell):
+ """Calculate dependent cells in top_left table"""
+ from decimal import Decimal
+ from django.db.models import Sum
+ from django.db.models.functions import Coalesce
+
+ client_id = changed_cell.client_id
+ updated_cells = []
+
+ # Get all cells for this client in top_left table
+ client_cells = Cell.objects.filter(
+ sheet=sheet,
+ table_type='top_left',
+ client_id=client_id
+ )
+
+ # Create a dict for easy access
+ cell_dict = {}
+ for cell in client_cells:
+ cell_dict[cell.row_index] = cell
+
+ # Get values with safe defaults
+ def get_cell_value(row_idx):
+ cell = cell_dict.get(row_idx)
+ if cell and cell.value is not None:
+ try:
+ return Decimal(str(cell.value))
+ except:
+ return Decimal('0')
+ return Decimal('0')
+
+ # 1. B5 = IF(B3>0; B3-B4; 0) - Gasrückführung
+ b3 = get_cell_value(0) # row_index 0 = Excel B3 (UI row 1)
+ b4 = get_cell_value(1) # row_index 1 = Excel B4 (UI row 2)
+
+ # Calculate B5
+ if b3 > 0:
+ b5_value = b3 - b4
+ if b5_value < 0:
+ b5_value = Decimal('0')
+ else:
+ b5_value = Decimal('0')
+
+ # Update B5 - FORCE update even if value appears the same
+ b5_cell = cell_dict.get(2) # row_index 2 = Excel B5 (UI row 3)
+ if b5_cell:
+ # Always update B5 when B3 or B4 changes
+ b5_cell.value = b5_value
+ b5_cell.save()
+ updated_cells.append({
+ 'id': b5_cell.id,
+ 'value': str(b5_cell.value),
+ 'is_calculated': True
+ })
+
+ # 2. B6 = B5 / 0.75 - Rückführung flüssig
+ b6_value = b5_value / Decimal('0.75')
+ b6_cell = cell_dict.get(3) # row_index 3 = Excel B6 (UI row 4)
+ if b6_cell:
+ b6_cell.value = b6_value
+ b6_cell.save()
+ updated_cells.append({
+ 'id': b6_cell.id,
+ 'value': str(b6_cell.value),
+ 'is_calculated': True
+ })
+
+ # 3. B9 = B7 + B8 - Summe Bestand
+ b7 = get_cell_value(4) # row_index 4 = Excel B7 (UI row 5)
+ b8 = get_cell_value(5) # row_index 5 = Excel B8 (UI row 6)
+ b9_value = b7 + b8
+ b9_cell = cell_dict.get(6) # row_index 6 = Excel B9 (UI row 7)
+ if b9_cell:
+ b9_cell.value = b9_value
+ b9_cell.save()
+ updated_cells.append({
+ 'id': b9_cell.id,
+ 'value': str(b9_cell.value),
+ 'is_calculated': True
+ })
+
+ # 4. B11 = Bezug (Liter L-He)
+ client = changed_cell.client
+ b11_cell = cell_dict.get(8) # row_index 8 = Excel B11 (UI row 9)
+
+ # Check if this is the start sheet (2025-01)
+ is_start_sheet = (sheet.year == 2025 and sheet.month == 1)
+
+ # Get the B11 value for calculations
+ b11_value = Decimal('0')
+
+ if is_start_sheet:
+ # For start sheet, keep whatever value is already there (manual entry)
+ if b11_cell and b11_cell.value is not None:
+ b11_value = Decimal(str(b11_cell.value))
+ else:
+ # For non-start sheets: auto-calculate from SecondTableEntry
+ from .models import SecondTableEntry
+ lhe_output_sum = SecondTableEntry.objects.filter(
+ client=client,
+ date__year=sheet.year,
+ date__month=sheet.month
+ ).aggregate(
+ total=Coalesce(Sum('lhe_output'), Decimal('0'))
+ )['total']
+
+ b11_value = lhe_output_sum
+
+ if b11_cell and (b11_cell.value != b11_value or b11_cell.value is None):
+ b11_cell.value = b11_value
+ b11_cell.save()
+ updated_cells.append({
+ 'id': b11_cell.id,
+ 'value': str(b11_cell.value),
+ 'is_calculated': True # Calculated from SecondTableEntry
+ })
+
+ # 5. B12 = B11 + B10 - B9 - Rückführ. Soll
+ b10 = get_cell_value(7) # row_index 7 = Excel B10 (UI row 8)
+ b12_value = b11_value + b10 - b9_value
+ b12_cell = cell_dict.get(9) # row_index 9 = Excel B12 (UI row 10)
+ if b12_cell:
+ b12_cell.value = b12_value
+ b12_cell.save()
+ updated_cells.append({
+ 'id': b12_cell.id,
+ 'value': str(b12_cell.value),
+ 'is_calculated': True
+ })
+
+ # 6. B13 = B12 - B6 - Verluste (Soll-Rückf.)
+ b13_value = b12_value - b6_value
+ b13_cell = cell_dict.get(10) # row_index 10 = Excel B13 (UI row 11)
+ if b13_cell:
+ b13_cell.value = b13_value
+ b13_cell.save()
+ updated_cells.append({
+ 'id': b13_cell.id,
+ 'value': str(b13_cell.value),
+ 'is_calculated': True
+ })
+
+ # 7. B14 = Count of warm outputs
+ from .models import SecondTableEntry
+ warm_count = SecondTableEntry.objects.filter(
+ client=client,
+ date__year=sheet.year,
+ date__month=sheet.month,
+ is_warm=True
+ ).count()
+
+ b14_cell = cell_dict.get(11) # row_index 11 = Excel B14 (UI row 12)
+ if b14_cell:
+ b14_cell.value = Decimal(str(warm_count))
+ b14_cell.save()
+ updated_cells.append({
+ 'id': b14_cell.id,
+ 'value': str(b14_cell.value),
+ 'is_calculated': True
+ })
+
+ # 8. B15 = IF(B11>0; B11 * factor + B14 * 15; 0) - Kaltgas Rückgabe
+ factor = get_cell_value(13) # row_index 13 = Excel B16 (Faktor) (UI row 14)
+ if factor == 0:
+ factor = Decimal('0.06') # default factor
+
+ if b11_value > 0: # Use b11_value
+ b15_value = b11_value * factor + Decimal(str(warm_count)) * Decimal('15')
+ else:
+ b15_value = Decimal('0')
+
+ b15_cell = cell_dict.get(12) # row_index 12 = Excel B15 (UI row 13)
+ if b15_cell:
+ b15_cell.value = b15_value
+ b15_cell.save()
+ updated_cells.append({
+ 'id': b15_cell.id,
+ 'value': str(b15_cell.value),
+ 'is_calculated': True
+ })
+
+ # 9. B17 = B13 - B15 - Verbraucherverluste
+ b17_value = b13_value - b15_value
+ b17_cell = cell_dict.get(14) # row_index 14 = Excel B17 (UI row 15)
+ if b17_cell:
+ b17_cell.value = b17_value
+ b17_cell.save()
+ updated_cells.append({
+ 'id': b17_cell.id,
+ 'value': str(b17_cell.value),
+ 'is_calculated': True
+ })
+
+ # 10. B18 = IF(B11=0; 0; B17/B11) - %
+ if b11_value == 0: # Use b11_value
+ b18_value = Decimal('0')
+ else:
+ b18_value = b17_value / b11_value # Use b11_value
+
+ b18_cell = cell_dict.get(15) # row_index 15 = Excel B18 (UI row 16)
+ if b18_cell:
+ b18_cell.value = b18_value
+ b18_cell.save()
+ updated_cells.append({
+ 'id': b18_cell.id,
+ 'value': str(b18_cell.value),
+ 'is_calculated': True
+ })
+
+ return updated_cells
+
+ def save_bulk_cells(self, request, sheet):
+ """Original bulk save logic (for backward compatibility)"""
+ # Get all cell updates
+ cell_updates = {}
+ for key, value in request.POST.items():
+ if key.startswith('cell_'):
+ cell_id = key.replace('cell_', '')
+ cell_updates[cell_id] = value
+
+ # Update cells and track which ones changed
+ updated_cells = []
+ changed_clients = set()
+
+ for cell_id, new_value in cell_updates.items():
+ try:
+ cell = Cell.objects.get(id=cell_id, sheet=sheet)
+ old_value = cell.value
+
+ # Convert new value
+ try:
+ if new_value.strip():
+ cell.value = Decimal(new_value)
+ else:
+ cell.value = None
+ except:
+ cell.value = None
+
+ # Only save if value changed
+ if cell.value != old_value:
+ cell.save()
+ updated_cells.append({
+ 'id': cell.id,
+ 'value': str(cell.value) if cell.value else ''
+ })
+ changed_clients.add(cell.client_id)
+
+ except Cell.DoesNotExist:
+ continue
+
+ # Recalculate for each changed client
+ for client_id in changed_clients:
+ self.recalculate_top_left_table(sheet, client_id)
+
+ # Get all updated cells for response
+ all_updated_cells = []
+ for client_id in changed_clients:
+ client_cells = Cell.objects.filter(
+ sheet=sheet,
+ client_id=client_id
+ )
+ for cell in client_cells:
+ all_updated_cells.append({
+ 'id': cell.id,
+ 'value': str(cell.value) if cell.value else '',
+ 'is_calculated': cell.is_formula
+ })
+
+ return JsonResponse({
+ 'status': 'success',
+ 'message': f'Saved {len(updated_cells)} cells',
+ 'updated_cells': all_updated_cells
+ })
+
+ def recalculate_top_left_table(self, sheet, client_id):
+ """Recalculate the top-left table for a specific client"""
+ from decimal import Decimal
+
+ # Get all cells for this client in top_left table
+ cells = Cell.objects.filter(
+ sheet=sheet,
+ table_type='top_left',
+ client_id=client_id
+ ).order_by('row_index')
+
+ # Create a dictionary of cell values
+ cell_dict = {}
+ for cell in cells:
+ cell_dict[cell.row_index] = cell
+
+ # Excel logic implementation for top-left table
+ # B3 (row_index 0) = Stand der Gaszähler (Nm³) - manual
+ # B4 (row_index 1) = Stand der Gaszähler (Vormonat) (Nm³) - from previous sheet
+
+ # Get B3 and B4
+ b3_cell = cell_dict.get(0) # UI Row 3
+ b4_cell = cell_dict.get(1) # UI Row 4
+
+ if b3_cell and b3_cell.value and b4_cell and b4_cell.value:
+ # B5 = IF(B3>0; B3-B4; 0)
+ b3 = Decimal(str(b3_cell.value))
+ b4 = Decimal(str(b4_cell.value))
+
+ if b3 > 0:
+ b5 = b3 - b4
+ if b5 < 0:
+ b5 = Decimal('0')
+ else:
+ b5 = Decimal('0')
+
+ # Update B5 (row_index 2)
+ b5_cell = cell_dict.get(2)
+ if b5_cell and (b5_cell.value != b5 or b5_cell.value is None):
+ b5_cell.value = b5
+ b5_cell.save()
+
+ # B6 = B5 / 0.75 (row_index 3)
+ b6 = b5 / Decimal('0.75')
+ b6_cell = cell_dict.get(3)
+ if b6_cell and (b6_cell.value != b6 or b6_cell.value is None):
+ b6_cell.value = b6
+ b6_cell.save()
+
+ # Get previous month's sheet for B10
+ if sheet.month == 1:
+ prev_year = sheet.year - 1
+ prev_month = 12
+ else:
+ prev_year = sheet.year
+ prev_month = sheet.month - 1
+
+ prev_sheet = MonthlySheet.objects.filter(
+ year=prev_year,
+ month=prev_month
+ ).first()
+
+ if prev_sheet:
+ # Get B9 from previous sheet (row_index 7 in previous)
+ prev_b9 = Cell.objects.filter(
+ sheet=prev_sheet,
+ table_type='top_left',
+ client_id=client_id,
+ row_index=7 # UI Row 9
+ ).first()
+
+ if prev_b9 and prev_b9.value:
+ # Update B10 in current sheet (row_index 8)
+ b10_cell = cell_dict.get(8)
+ if b10_cell and (b10_cell.value != prev_b9.value or b10_cell.value is None):
+ b10_cell.value = prev_b9.value
+ b10_cell.save()
+
+# Calculate View (placeholder for calculations)
+class CalculateView(View):
+ def post(self, request):
+ # This will be implemented when you provide calculation rules
+ return JsonResponse({
+ 'status': 'success',
+ 'message': 'Calculation endpoint ready'
+ })
+
+# Summary Sheet View
+class SummarySheetView(TemplateView):
+ template_name = 'summary_sheet.html'
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ start_month = int(self.kwargs.get('start_month', 1))
+ year = int(self.kwargs.get('year', datetime.now().year))
+
+ # Get 6 monthly sheets
+ months = [(year, m) for m in range(start_month, start_month + 6)]
+ sheets = MonthlySheet.objects.filter(
+ year=year,
+ month__in=list(range(start_month, start_month + 6))
+ ).order_by('month')
+
+ # Aggregate data across months
+ summary_data = self.calculate_summary(sheets)
+
+ context.update({
+ 'year': year,
+ 'start_month': start_month,
+ 'end_month': start_month + 5,
+ 'sheets': sheets,
+ 'clients': Client.objects.all(),
+ 'summary_data': summary_data,
+ })
+ return context
+
+ def calculate_summary(self, sheets):
+ """Calculate totals across 6 months"""
+ summary = {}
+
+ for client in Client.objects.all():
+ client_total = 0
+ for sheet in sheets:
+ # Get specific cells and sum
+ cells = sheet.cells.filter(
+ client=client,
+ table_type='top_left',
+ row_index=0 # Example: first row
+ )
+ for cell in cells:
+ if cell.value:
+ try:
+ client_total += float(cell.value)
+ except (ValueError, TypeError):
+ continue
+
+ summary[client.id] = client_total
+
+ return summary
+
+# Existing views below (keep all your existing functions)
+def clients_list(request):
+ # Get all clients
+ clients = Client.objects.all()
+
+ # Get all years available in SecondTableEntry
+ available_years_qs = SecondTableEntry.objects.dates('date', 'year', order='DESC')
+ available_years = [y.year for y in available_years_qs]
+
+ # Determine selected year
+ year_param = request.GET.get('year')
+ if year_param:
+ selected_year = int(year_param)
+ else:
+ # If no year in GET, default to latest available year or current year if DB empty
+ selected_year = available_years[0] if available_years else datetime.now().year
+
+ # Prepare monthly totals per client
+ monthly_data = []
+ for client in clients:
+ monthly_totals = []
+ for month in range(1, 13):
+ total = SecondTableEntry.objects.filter(
+ client=client,
+ date__year=selected_year,
+ date__month=month
+ ).aggregate(
+ total=Coalesce(Sum('lhe_output'), Value(0, output_field=DecimalField()))
+ )['total']
+ monthly_totals.append(total)
+
+ monthly_data.append({
+ 'client': client,
+ 'monthly_totals': monthly_totals,
+ 'year_total': sum(monthly_totals)
+ })
+
+ return render(request, 'clients_table.html', {
+ 'monthly_data': monthly_data,
+ 'current_year': selected_year,
+ 'available_years': available_years,
+ 'months': ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
+ 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
+ })
+
+# Table One View (ExcelEntry)
+def table_one_view(request):
+ from .models import ExcelEntry, Client, Institute
+
+ # Main table (unchanged)
+ entries_table1 = ExcelEntry.objects.all().select_related('client', 'client__institute')
+ clients = Client.objects.all().select_related('institute')
+ institutes = Institute.objects.all()
+
+ # ---- Overview filters ----
+ # years present in ExcelEntry.date
+ year_qs = ExcelEntry.objects.dates('date', 'year', order='DESC')
+ available_years = [d.year for d in year_qs]
+
+ # default year/start month
+ if available_years:
+ default_year = available_years[0] # newest year
+ else:
+ default_year = timezone.now().year
+
+ year_param = request.GET.get('overview_year')
+ start_month_param = request.GET.get('overview_start_month')
+
+ year = int(year_param) if year_param else int(default_year)
+ start_month = int(start_month_param) if start_month_param else 1
+
+ # six-month window
+ end_month = start_month + 5
+ if end_month > 12:
+ end_month = 12
+
+ months = list(range(start_month, end_month + 1))
+
+ overview = None
+
+ if months:
+ # Build per-group data
+ groups_entries = [] # for internal calculations
+
+ for key, group in CLIENT_GROUPS.items():
+ clients_qs = get_group_clients(key)
+
+ values = []
+ group_total = Decimal('0')
+
+ for m in months:
+ total = ExcelEntry.objects.filter(
+ client__in=clients_qs,
+ date__year=year,
+ date__month=m
+ ).aggregate(
+ total=Coalesce(Sum('lhe_ges'), Decimal('0'))
+ )['total']
+
+ values.append(total)
+ group_total += total
+
+ groups_entries.append({
+ 'key': key,
+ 'label': group['label'],
+ 'values': values,
+ 'total': group_total,
+ })
+
+ # month totals across all groups
+ month_totals = []
+ for idx in range(len(months)):
+ s = sum((g['values'][idx] for g in groups_entries), Decimal('0'))
+ month_totals.append(s)
+
+ grand_total = sum(month_totals, Decimal('0'))
+
+ # Build rows for the template
+ rows = []
+ for idx, m in enumerate(months):
+ row_values = [g['values'][idx] for g in groups_entries]
+ rows.append({
+ 'month_number': m,
+ 'month_label': calendar.month_name[m],
+ 'values': row_values,
+ 'total': month_totals[idx],
+ })
+
+ groups_meta = [{'key': g['key'], 'label': g['label']} for g in groups_entries]
+ group_totals = [g['total'] for g in groups_entries]
+
+ overview = {
+ 'year': year,
+ 'start_month': start_month,
+ 'end_month': end_month,
+ 'rows': rows,
+ 'groups': groups_meta,
+ 'group_totals': group_totals,
+ 'grand_total': grand_total,
+ }
+
+ # Month dropdown labels
+ MONTH_CHOICES = [
+ (1, 'Jan'), (2, 'Feb'), (3, 'Mar'), (4, 'Apr'),
+ (5, 'May'), (6, 'Jun'), (7, 'Jul'), (8, 'Aug'),
+ (9, 'Sep'), (10, 'Oct'), (11, 'Nov'), (12, 'Dec'),
+ ]
+
+ return render(request, 'table_one.html', {
+ 'entries_table1': entries_table1,
+ 'clients': clients,
+ 'institutes': institutes,
+ 'available_years': available_years,
+ 'month_choices': MONTH_CHOICES,
+ 'overview': overview,
+ })
+
+# Table Two View (SecondTableEntry)
+def table_two_view(request):
+ try:
+ entries = SecondTableEntry.objects.all().order_by('-date')
+ clients = Client.objects.all().select_related('institute')
+ institutes = Institute.objects.all()
+
+ return render(request, 'table_two.html', {
+ 'entries_table2': entries,
+ 'clients': clients,
+ 'institutes': institutes,
+ })
+
+ except Exception as e:
+ return render(request, 'table_two.html', {
+ 'error_message': f"Failed to load data: {str(e)}",
+ 'entries_table2': [],
+ 'clients': Client.objects.all().select_related('institute'),
+ 'institutes': Institute.objects.all()
+ })
+
+# Add Entry (Generic)
+def add_entry(request, model_name):
+ if request.method == 'POST':
+ try:
+ if model_name == 'SecondTableEntry':
+ model = SecondTableEntry
+
+ # Parse date
+ date_str = request.POST.get('date')
+ try:
+ date_obj = datetime.strptime(date_str, '%Y-%m-%d').date() if date_str else None
+ except (ValueError, TypeError):
+ return JsonResponse({
+ 'status': 'error',
+ 'message': 'Invalid date format. Use YYYY-MM-DD'
+ }, status=400)
+
+ # NEW: robust parse of warm flag (handles 0/1, true/false, on/off)
+ raw_warm = request.POST.get('is_warm')
+ is_warm_bool = str(raw_warm).lower() in ('1', 'true', 'on', 'yes')
+
+ # Handle Helium Output (Table Two)
+ lhe_output = request.POST.get('lhe_output')
+
+ entry = model.objects.create(
+ client=Client.objects.get(id=request.POST.get('client_id')),
+ date=date_obj,
+ is_warm=is_warm_bool, # <-- use the boolean here
+ lhe_delivery=request.POST.get('lhe_delivery', ''),
+ lhe_output=Decimal(lhe_output) if lhe_output else None,
+ notes=request.POST.get('notes', '')
+ )
+
+ return JsonResponse({
+ 'status': 'success',
+ 'id': entry.id,
+ 'client_name': entry.client.name,
+ 'institute_name': entry.client.institute.name,
+ 'date': entry.date.strftime('%Y-%m-%d') if entry.date else '',
+ 'is_warm': entry.is_warm,
+ 'lhe_delivery': entry.lhe_delivery,
+ 'lhe_output': str(entry.lhe_output) if entry.lhe_output else '',
+ 'notes': entry.notes
+ })
+
+ elif model_name == 'ExcelEntry':
+ model = ExcelEntry
+
+ # Parse the date string into a date object
+ date_str = request.POST.get('date')
+ try:
+ date_obj = datetime.strptime(date_str, '%Y-%m-%d').date() if date_str else None
+ except (ValueError, TypeError):
+ date_obj = None
+
+ try:
+ pressure = Decimal(request.POST.get('pressure', 0))
+ purity = Decimal(request.POST.get('purity', 0))
+ druckkorrektur = Decimal(request.POST.get('druckkorrektur', 1))
+ lhe_zus = Decimal(request.POST.get('lhe_zus', 0))
+ constant_300 = Decimal(request.POST.get('constant_300', 300))
+ korrig_druck = Decimal(request.POST.get('korrig_druck', 0))
+ nm3 = Decimal(request.POST.get('nm3', 0))
+ lhe = Decimal(request.POST.get('lhe', 0))
+ lhe_ges = Decimal(request.POST.get('lhe_ges', 0))
+ except InvalidOperation:
+ return JsonResponse({
+ 'status': 'error',
+ 'message': 'Invalid numeric value in Helium Input'
+ }, status=400)
+
+ # Create the entry with ALL fields
+ entry = model.objects.create(
+ client=Client.objects.get(id=request.POST.get('client_id')),
+ date=date_obj,
+ pressure=pressure,
+ purity=purity,
+ druckkorrektur=druckkorrektur,
+ lhe_zus=lhe_zus,
+ constant_300=constant_300,
+ korrig_druck=korrig_druck,
+ nm3=nm3,
+ lhe=lhe,
+ lhe_ges=lhe_ges,
+ notes=request.POST.get('notes', '')
+ )
+
+ # Prepare the response
+ response_data = {
+ 'status': 'success',
+ 'id': entry.id,
+ 'client_name': entry.client.name,
+ 'institute_name': entry.client.institute.name,
+ 'pressure': str(entry.pressure),
+ 'purity': str(entry.purity),
+ 'druckkorrektur': str(entry.druckkorrektur),
+ 'constant_300': str(entry.constant_300),
+ 'korrig_druck': str(entry.korrig_druck),
+ 'nm3': str(entry.nm3),
+ 'lhe': str(entry.lhe),
+ 'lhe_zus': str(entry.lhe_zus),
+ 'lhe_ges': str(entry.lhe_ges),
+ 'notes': entry.notes,
+ }
+
+ if entry.date:
+ # JS uses this for the Date column and for the Month column
+ response_data['date'] = entry.date.strftime('%Y-%m-%d')
+ response_data['month'] = f"{entry.date.month:02d}"
+ else:
+ response_data['date'] = ''
+ response_data['month'] = ''
+
+ return JsonResponse(response_data)
+
+
+ except Exception as e:
+ return JsonResponse({'status': 'error', 'message': str(e)}, status=400)
+
+ return JsonResponse({'status': 'error', 'message': 'Invalid request'}, status=400)
+
+# Update Entry (Generic)
+def update_entry(request, model_name):
+ if request.method == 'POST':
+ try:
+ if model_name == 'SecondTableEntry':
+ model = SecondTableEntry
+ elif model_name == 'ExcelEntry':
+ model = ExcelEntry
+ else:
+ return JsonResponse({'status': 'error', 'message': 'Invalid model'}, status=400)
+
+ entry_id = int(request.POST.get('id'))
+ entry = model.objects.get(id=entry_id)
+
+ # Common updates for both models
+ entry.client = Client.objects.get(id=request.POST.get('client_id'))
+ entry.notes = request.POST.get('notes', '')
+
+ # Handle date properly for both models
+ date_str = request.POST.get('date')
+ if date_str:
+ try:
+ entry.date = datetime.strptime(date_str, '%Y-%m-%d').date()
+ except ValueError:
+ return JsonResponse({
+ 'status': 'error',
+ 'message': 'Invalid date format. Use YYYY-MM-DD'
+ }, status=400)
+
+ if model_name == 'SecondTableEntry':
+ # Handle Helium Output specific fields
+
+ raw_warm = request.POST.get('is_warm')
+ entry.is_warm = str(raw_warm).lower() in ('1', 'true', 'on', 'yes')
+
+ entry.lhe_delivery = request.POST.get('lhe_delivery', '')
+
+ lhe_output = request.POST.get('lhe_output')
+ try:
+ entry.lhe_output = Decimal(lhe_output) if lhe_output else None
+ except InvalidOperation:
+ return JsonResponse({
+ 'status': 'error',
+ 'message': 'Invalid LHe Output value'
+ }, status=400)
+
+ entry.save()
+
+ return JsonResponse({
+ 'status': 'success',
+ 'id': entry.id,
+ 'client_name': entry.client.name,
+ 'institute_name': entry.client.institute.name,
+ 'date': entry.date.strftime('%Y-%m-%d') if entry.date else '',
+ 'is_warm': entry.is_warm,
+ 'lhe_delivery': entry.lhe_delivery,
+ 'lhe_output': str(entry.lhe_output) if entry.lhe_output else '',
+ 'notes': entry.notes
+ })
+
+
+ elif model_name == 'ExcelEntry':
+ # Handle Helium Input specific fields
+ try:
+ entry.pressure = Decimal(request.POST.get('pressure', 0))
+ entry.purity = Decimal(request.POST.get('purity', 0))
+ entry.druckkorrektur = Decimal(request.POST.get('druckkorrektur', 1))
+ entry.lhe_zus = Decimal(request.POST.get('lhe_zus', 0))
+ entry.constant_300 = Decimal(request.POST.get('constant_300', 300))
+ entry.korrig_druck = Decimal(request.POST.get('korrig_druck', 0))
+ entry.nm3 = Decimal(request.POST.get('nm3', 0))
+ entry.lhe = Decimal(request.POST.get('lhe', 0))
+ entry.lhe_ges = Decimal(request.POST.get('lhe_ges', 0))
+ except InvalidOperation:
+ return JsonResponse({
+ 'status': 'error',
+ 'message': 'Invalid numeric value in Helium Input'
+ }, status=400)
+
+ entry.save()
+
+ return JsonResponse({
+ 'status': 'success',
+ 'id': entry.id,
+ 'client_name': entry.client.name,
+ 'institute_name': entry.client.institute.name,
+ 'date': entry.date.strftime('%Y-%m-%d') if entry.date else '',
+ 'month': f"{entry.date.month:02d}" if entry.date else '',
+ 'pressure': str(entry.pressure),
+ 'purity': str(entry.purity),
+ 'druckkorrektur': str(entry.druckkorrektur),
+ 'constant_300': str(entry.constant_300),
+ 'korrig_druck': str(entry.korrig_druck),
+ 'nm3': str(entry.nm3),
+ 'lhe': str(entry.lhe),
+ 'lhe_zus': str(entry.lhe_zus),
+ 'lhe_ges': str(entry.lhe_ges),
+ 'notes': entry.notes
+ })
+
+
+ except model.DoesNotExist:
+ return JsonResponse({'status': 'error', 'message': 'Entry not found'}, status=404)
+ except Exception as e:
+ return JsonResponse({'status': 'error', 'message': str(e)}, status=400)
+
+ return JsonResponse({'status': 'error', 'message': 'Invalid request method'}, status=400)
+
+# Delete Entry (Generic)
+def delete_entry(request, model_name):
+ if request.method == 'POST':
+ try:
+ if model_name == 'SecondTableEntry':
+ model = SecondTableEntry
+ elif model_name == 'ExcelEntry':
+ model = ExcelEntry
+ else:
+ return JsonResponse({'status': 'error', 'message': 'Invalid model'}, status=400)
+
+ entry_id = request.POST.get('id')
+ entry = model.objects.get(id=entry_id)
+ entry.delete()
+ return JsonResponse({'status': 'success', 'message': 'Entry deleted'})
+
+ except model.DoesNotExist:
+ return JsonResponse({'status': 'error', 'message': 'Entry not found'}, status=404)
+ except Exception as e:
+ return JsonResponse({'status': 'error', 'message': str(e)}, status=400)
+
+ return JsonResponse({'status': 'error', 'message': 'Invalid request'}, status=400)
+
+def betriebskosten_list(request):
+ items = Betriebskosten.objects.all().order_by('-buchungsdatum')
+ return render(request, 'betriebskosten_list.html', {'items': items})
+
+def betriebskosten_create(request):
+ if request.method == 'POST':
+ try:
+ entry_id = request.POST.get('id')
+ if entry_id:
+ # Update existing entry
+ entry = Betriebskosten.objects.get(id=entry_id)
+ else:
+ # Create new entry
+ entry = Betriebskosten()
+
+ # Get form data
+ buchungsdatum_str = request.POST.get('buchungsdatum')
+ rechnungsnummer = request.POST.get('rechnungsnummer')
+ kostentyp = request.POST.get('kostentyp')
+ betrag = request.POST.get('betrag')
+ beschreibung = request.POST.get('beschreibung')
+ gas_volume = request.POST.get('gas_volume')
+
+ # Validate required fields
+ if not all([buchungsdatum_str, rechnungsnummer, kostentyp, betrag]):
+ return JsonResponse({'status': 'error', 'message': 'All required fields must be filled'})
+
+ # Convert date string to date object
+ try:
+ buchungsdatum = parse_date(buchungsdatum_str)
+ if not buchungsdatum:
+ return JsonResponse({'status': 'error', 'message': 'Invalid date format'})
+ except (ValueError, TypeError):
+ return JsonResponse({'status': 'error', 'message': 'Invalid date format'})
+
+ # Set entry values
+ entry.buchungsdatum = buchungsdatum
+ entry.rechnungsnummer = rechnungsnummer
+ entry.kostentyp = kostentyp
+ entry.betrag = betrag
+ entry.beschreibung = beschreibung
+
+ # Only set gas_volume if kostentyp is helium and gas_volume is provided
+ if kostentyp == 'helium' and gas_volume:
+ entry.gas_volume = gas_volume
+ else:
+ entry.gas_volume = None
+
+ entry.save()
+
+ return JsonResponse({
+ 'status': 'success',
+ 'id': entry.id,
+ 'buchungsdatum': entry.buchungsdatum.strftime('%Y-%m-%d'),
+ 'rechnungsnummer': entry.rechnungsnummer,
+ 'kostentyp_display': entry.get_kostentyp_display(),
+ 'gas_volume': str(entry.gas_volume) if entry.gas_volume else '-',
+ 'betrag': str(entry.betrag),
+ 'beschreibung': entry.beschreibung or ''
+ })
+
+ except Exception as e:
+ return JsonResponse({'status': 'error', 'message': str(e)})
+
+ return JsonResponse({'status': 'error', 'message': 'Invalid request method'})
+
+def betriebskosten_delete(request):
+ if request.method == 'POST':
+ try:
+ entry_id = request.POST.get('id')
+ entry = Betriebskosten.objects.get(id=entry_id)
+ entry.delete()
+ return JsonResponse({'status': 'success'})
+ except Betriebskosten.DoesNotExist:
+ return JsonResponse({'status': 'error', 'message': 'Entry not found'})
+ except Exception as e:
+ return JsonResponse({'status': 'error', 'message': str(e)})
+
+ return JsonResponse({'status': 'error', 'message': 'Invalid request method'})
+
+class CheckSheetView(View):
+ def get(self, request):
+ # Get current month/year
+ current_year = datetime.now().year
+ current_month = datetime.now().month
+
+ # Get all sheets
+ sheets = MonthlySheet.objects.all()
+
+ sheet_data = []
+ for sheet in sheets:
+ cells_count = sheet.cells.count()
+ # Count non-empty cells
+ non_empty = sheet.cells.exclude(value__isnull=True).count()
+
+ sheet_data.append({
+ 'id': sheet.id,
+ 'year': sheet.year,
+ 'month': sheet.month,
+ 'month_name': calendar.month_name[sheet.month],
+ 'total_cells': cells_count,
+ 'non_empty_cells': non_empty,
+ 'has_data': non_empty > 0
+ })
+
+ # Also check what the default view would show
+ default_sheet = MonthlySheet.objects.filter(
+ year=current_year,
+ month=current_month
+ ).first()
+
+ return JsonResponse({
+ 'current_year': current_year,
+ 'current_month': current_month,
+ 'current_month_name': calendar.month_name[current_month],
+ 'default_sheet_exists': default_sheet is not None,
+ 'default_sheet_id': default_sheet.id if default_sheet else None,
+ 'sheets': sheet_data,
+ 'total_sheets': len(sheet_data)
+ })
+
+
+class QuickDebugView(View):
+ def get(self, request):
+ # Get ALL sheets
+ sheets = MonthlySheet.objects.all().order_by('year', 'month')
+
+ result = {
+ 'sheets': []
+ }
+
+ for sheet in sheets:
+ sheet_info = {
+ 'id': sheet.id,
+ 'display': f"{sheet.year}-{sheet.month:02d}",
+ 'url': f"/sheet/{sheet.year}/{sheet.month}/", # CHANGED THIS LINE
+ 'sheet_url_pattern': 'sheet/{year}/{month}/', # Add this for clarity
+ }
+
+ # Count cells with data for first client in top_left table
+ first_client = Client.objects.first()
+ if first_client:
+ test_cells = sheet.cells.filter(
+ client=first_client,
+ table_type='top_left',
+ row_index__in=[8, 9, 10] # Rows 9, 10, 11
+ ).order_by('row_index')
+
+ cell_values = {}
+ for cell in test_cells:
+ cell_values[f"row_{cell.row_index}"] = str(cell.value) if cell.value else "Empty"
+
+ sheet_info['test_cells'] = cell_values
+ else:
+ sheet_info['test_cells'] = "No clients"
+
+ result['sheets'].append(sheet_info)
+
+ return JsonResponse(result)
+class TestFormulaView(View):
+ def get(self, request):
+ # Test the formula evaluation directly
+ test_values = {
+ 8: 2, # Row 9 value (0-based index 8)
+ 9: 2, # Row 10 value (0-based index 9)
+ }
+
+ # Test formula "9 + 8" (using 0-based indices)
+ formula = "9 + 8"
+ result = evaluate_formula(formula, test_values)
+
+ return JsonResponse({
+ 'test_values': test_values,
+ 'formula': formula,
+ 'result': str(result),
+ 'note': 'Formula uses 0-based indices. 9=Row10, 8=Row9'
+ })
+
+
+class SimpleDebugView(View):
+ """Simplest debug view to check if things are working"""
+ def get(self, request):
+ sheet_id = request.GET.get('sheet_id', 1)
+
+ try:
+ sheet = MonthlySheet.objects.get(id=sheet_id)
+
+ # Get first client
+ client = Client.objects.first()
+ if not client:
+ return JsonResponse({'error': 'No clients found'})
+
+ # Check a few cells
+ cells = Cell.objects.filter(
+ sheet=sheet,
+ client=client,
+ table_type='top_left',
+ row_index__in=[8, 9, 10]
+ ).order_by('row_index')
+
+ cell_data = []
+ for cell in cells:
+ cell_data.append({
+ 'row_index': cell.row_index,
+ 'ui_row': cell.row_index + 1,
+ 'value': str(cell.value) if cell.value is not None else 'Empty',
+ 'cell_id': cell.id
+ })
+
+ return JsonResponse({
+ 'sheet': f"{sheet.year}-{sheet.month}",
+ 'sheet_id': sheet.id,
+ 'client': client.name,
+ 'cells': cell_data,
+ 'note': 'Row 8 = UI Row 9, Row 9 = UI Row 10, Row 10 = UI Row 11'
+ })
+
+ except MonthlySheet.DoesNotExist:
+ return JsonResponse({'error': f'Sheet with id {sheet_id} not found'})
\ No newline at end of file
diff --git a/~$He-Anlage 2024_1.Halbjahr.ods b/~$He-Anlage 2024_1.Halbjahr.ods
new file mode 100644
index 0000000..b1bf45b
Binary files /dev/null and b/~$He-Anlage 2024_1.Halbjahr.ods differ