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 %} +
+ +
+ ← Previous +

{{ year }} - {{ month_name }} (Sheet {{ month }}/6)

+ Next → + Go to Summary +
+ + +
+ +
+

Table 1: Top Left

+ + + + + {% for header in top_left_headers %} + + {% endfor %} + + + + + {% for row in top_left_rows %} + {% with rownum=forloop.counter %} + + + + {% 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) #} + + {% else %} + {# Readonly for non-start sheets #} + + {% endif %} + {% endfor %} + + + + + {% endwith %} + {% endfor %} + +
Row Label{{ header }}Sum
+ {% 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 %} + + {{ cell.value|default:"" }} + + {{ cell.value|default:"" }} + + {{ row.sum|default_if_none:"" }} +
+
+ + + +
+

Table 2: Top Right

+ + + + + {% for header in top_right_headers %} + + {% endfor %} + + + + + {% for row in top_right_rows %} + {% with rownum=forloop.counter %} + + + + {% for cell in row.cells %} + {% with client_name=cell.client.name|default:"" %} + {# Determine if this cell should be editable #} + {% if is_start_sheet %} + + {% elif rownum == 4 or rownum == 6 or rownum == 1 and client_name in "M3 Thiele,M3 Buntkowsky,M3 Gutfleisch" %} + + {% else %} + + {% endif %} + {% endwith %} + {% endfor %} + + + + {% endwith %} + {% endfor %} + + +
Row Label{{ header }}Sum
+ {% 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 %} + + {{ cell.value|default:"" }} + + {{ cell.value|default:"" }} + + {% if rownum == 2 %} + Aufteilung Nach Verbrauch + {% else %} + {{ cell.value|default:"" }} + {% endif %} + + {{ row.sum|default_if_none:"" }} +
+
+ + +
+
+

Bottom Table 1

+ + + + + {% for client in clients %} + + {% endfor %} + + + + {% for row in cells_by_table.bottom_1 %} + + + {% for cell in row %} + + {% endfor %} + + {% endfor %} + +
Row Label{{ client.name }}
Row {{ forloop.counter }} + {{ cell.value|default:"" }} +
+
+ +
+

Bottom Table 2

+ + + + + {% for client in clients %} + + {% endfor %} + + + + {% for row in cells_by_table.bottom_2 %} + + + {% for cell in row %} + + {% endfor %} + + {% endfor %} + +
Row Label{{ client.name }}
Row {{ forloop.counter }} + {{ cell.value|default:"" }} +
+
+ +
+

Bottom Table 3

+ + + + + {% for client in clients %} + + {% endfor %} + + + + {% for row in cells_by_table.bottom_3 %} + + + {% for cell in row %} + + {% endfor %} + + {% endfor %} + +
Row Label{{ client.name }}
Row {{ forloop.counter }} + {{ cell.value|default:"" }} +
+
+
+ + +
+ +
+
+ + 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 }} +
+ + + + + + {% for g in overview.groups %} + + {% endfor %} + + + + + {% for row in overview.rows %} + + + {% for value in row.values %} + + {% endfor %} + + + {% endfor %} + + + + {% for total in overview.group_totals %} + + {% endfor %} + + + +
Month{{ g.label }}Month total
+ {{ row.month_number }} - {{ row.month_label|slice:":3" }} + {{ value|floatformat:2 }}{{ row.total|floatformat:2 }}
Summe{{ total|floatformat:2 }}{{ overview.grand_total|floatformat:2 }}
+ {% else %} +
+ No data yet – choose a year and start month and click “Show overview”. +
+ {% endif %} +
+ +

Helium Input

@@ -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 %} + @@ -645,6 +825,7 @@ ${response.lhe_zus} ${response.lhe_ges} ${response.date || ''} + ${response.month || ''} @@ -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