From e1da4fa0415c400386eb56ab11d5380b8a57af6d Mon Sep 17 00:00:00 2001 From: Mohamad Hesham Jenbaz Date: Tue, 17 Feb 2026 14:59:55 +0100 Subject: [PATCH] update --- db.sqlite3 | Bin 3760128 -> 4030464 bytes round_entries.py | 48 + ...tion_betriebskosten_gegenstand_and_more.py | 42 + .../migrations/0015_betriebskostensummary.py | 24 + sheets/migrations/0016_abrechnungcell.py | 27 + .../0017_alter_secondtableentry_options.py | 17 + ...ondtableentry_nach_secondtableentry_vor.py | 23 + sheets/models.py | 124 +- sheets/new/views.py | 5041 +++++++++++++++++ sheets/old/halfyear_balance.html | 353 ++ sheets/old/views.py | 4401 ++++++++++++++ sheets/old/views_v2.py | 4520 +++++++++++++++ sheets/services/__init__.py | 0 sheets/services/halfyear_calc.py | 1156 ++++ sheets/templates/Rechnung.html | 123 + sheets/templates/abrechnung.html | 370 ++ sheets/templates/betriebskosten_list.html | 333 +- sheets/templates/clients_table.html | 30 +- sheets/templates/halfyear_balance.html | 164 +- sheets/templates/table_one.html | 46 +- sheets/templates/table_two.html | 183 +- sheets/templatetags/__init__.py | 0 sheets/templatetags/dict_extras.py | 9 + sheets/urls.py | 8 +- sheets/views.py | 1918 ++++--- sheets/views.txt | 2873 ---------- ~$He-Anlage 2024_1.Halbjahr.ods | Bin 165 -> 0 bytes 27 files changed, 17914 insertions(+), 3919 deletions(-) create mode 100644 round_entries.py create mode 100644 sheets/migrations/0014_delete_rowcalculation_betriebskosten_gegenstand_and_more.py create mode 100644 sheets/migrations/0015_betriebskostensummary.py create mode 100644 sheets/migrations/0016_abrechnungcell.py create mode 100644 sheets/migrations/0017_alter_secondtableentry_options.py create mode 100644 sheets/migrations/0018_secondtableentry_nach_secondtableentry_vor.py create mode 100644 sheets/new/views.py create mode 100644 sheets/old/halfyear_balance.html create mode 100644 sheets/old/views.py create mode 100644 sheets/old/views_v2.py create mode 100644 sheets/services/__init__.py create mode 100644 sheets/services/halfyear_calc.py create mode 100644 sheets/templates/Rechnung.html create mode 100644 sheets/templates/abrechnung.html create mode 100644 sheets/templatetags/__init__.py create mode 100644 sheets/templatetags/dict_extras.py delete mode 100644 sheets/views.txt delete mode 100644 ~$He-Anlage 2024_1.Halbjahr.ods diff --git a/db.sqlite3 b/db.sqlite3 index 95ff630554da129d8fbffda23f6f1af8d70d1f5d..35a321aee73008d256796efbe9788bb74bc8cc4c 100644 GIT binary patch delta 408397 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�TV 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%0AKeF1Wh!xQdJqRE1(9im`u# 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�+L>{{F4iZPvlo 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

R0Lq>dPuo=OH^% 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 500: + ExcelEntry.objects.bulk_update(to_update, fields) + to_update = [] + print(f"processed={total}, changed={changed_count}") + +if to_update: + ExcelEntry.objects.bulk_update(to_update, fields) + +print(f"DONE. processed={total}, changed={changed_count}") diff --git a/sheets/migrations/0014_delete_rowcalculation_betriebskosten_gegenstand_and_more.py b/sheets/migrations/0014_delete_rowcalculation_betriebskosten_gegenstand_and_more.py new file mode 100644 index 0000000..74d549b --- /dev/null +++ b/sheets/migrations/0014_delete_rowcalculation_betriebskosten_gegenstand_and_more.py @@ -0,0 +1,42 @@ +# Generated by Django 5.1.4 on 2026-02-15 06:02 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('sheets', '0013_monthlysummary'), + ] + + operations = [ + migrations.DeleteModel( + name='RowCalculation', + ), + migrations.AddField( + model_name='betriebskosten', + name='gegenstand', + field=models.CharField(default=1, max_length=200, verbose_name='Gegenstand'), + preserve_default=False, + ), + migrations.AlterField( + model_name='betriebskosten', + name='buchungsdatum', + field=models.DateField(verbose_name='Zahlungsdatum'), + ), + migrations.AlterField( + model_name='betriebskosten', + name='gas_volume', + field=models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='Gasvolumen (m³)'), + ), + migrations.AlterField( + model_name='betriebskosten', + name='kostentyp', + field=models.CharField(choices=[('sach', 'Sach'), ('helium', 'Helium')], max_length=10, verbose_name='Kostentyp'), + ), + migrations.AlterField( + model_name='betriebskosten', + name='rechnungsnummer', + field=models.CharField(max_length=50, verbose_name='Firma'), + ), + ] diff --git a/sheets/migrations/0015_betriebskostensummary.py b/sheets/migrations/0015_betriebskostensummary.py new file mode 100644 index 0000000..cf8bf79 --- /dev/null +++ b/sheets/migrations/0015_betriebskostensummary.py @@ -0,0 +1,24 @@ +# Generated by Django 5.1.4 on 2026-02-15 07:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('sheets', '0014_delete_rowcalculation_betriebskosten_gegenstand_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='BetriebskostenSummary', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('personalkosten', models.DecimalField(decimal_places=2, default=0, max_digits=12)), + ('instandhaltung', models.DecimalField(decimal_places=2, default=0, max_digits=12)), + ('heliumkosten', models.DecimalField(decimal_places=2, default=0, max_digits=12)), + ('bezugskosten_gashe', models.DecimalField(decimal_places=4, default=0, max_digits=12)), + ('umlage_personal', models.DecimalField(decimal_places=2, default=0, max_digits=12)), + ], + ), + ] diff --git a/sheets/migrations/0016_abrechnungcell.py b/sheets/migrations/0016_abrechnungcell.py new file mode 100644 index 0000000..7c9bf1a --- /dev/null +++ b/sheets/migrations/0016_abrechnungcell.py @@ -0,0 +1,27 @@ +# Generated by Django 5.1.4 on 2026-02-15 11:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('sheets', '0015_betriebskostensummary'), + ] + + operations = [ + migrations.CreateModel( + name='AbrechnungCell', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('interval_year', models.IntegerField()), + ('interval_start_month', models.IntegerField()), + ('row_key', models.CharField(max_length=60)), + ('col_key', models.CharField(max_length=60)), + ('value', models.DecimalField(blank=True, decimal_places=6, max_digits=18, null=True)), + ], + options={ + 'unique_together': {('interval_year', 'interval_start_month', 'row_key', 'col_key')}, + }, + ), + ] diff --git a/sheets/migrations/0017_alter_secondtableentry_options.py b/sheets/migrations/0017_alter_secondtableentry_options.py new file mode 100644 index 0000000..0d793b9 --- /dev/null +++ b/sheets/migrations/0017_alter_secondtableentry_options.py @@ -0,0 +1,17 @@ +# Generated by Django 5.1.5 on 2026-02-16 10:47 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('sheets', '0016_abrechnungcell'), + ] + + operations = [ + migrations.AlterModelOptions( + name='secondtableentry', + options={'ordering': ['date', 'id']}, + ), + ] diff --git a/sheets/migrations/0018_secondtableentry_nach_secondtableentry_vor.py b/sheets/migrations/0018_secondtableentry_nach_secondtableentry_vor.py new file mode 100644 index 0000000..71b1901 --- /dev/null +++ b/sheets/migrations/0018_secondtableentry_nach_secondtableentry_vor.py @@ -0,0 +1,23 @@ +# Generated by Django 5.1.5 on 2026-02-17 08:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('sheets', '0017_alter_secondtableentry_options'), + ] + + operations = [ + migrations.AddField( + model_name='secondtableentry', + name='nach', + field=models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True), + ), + migrations.AddField( + model_name='secondtableentry', + name='vor', + field=models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True), + ), + ] diff --git a/sheets/models.py b/sheets/models.py index 2cb7f8f..09dddcc 100644 --- a/sheets/models.py +++ b/sheets/models.py @@ -73,47 +73,45 @@ class CellReference(models.Model): class Meta: unique_together = ['source_cell', 'target_cell'] +from decimal import Decimal +from django.db import models + class Betriebskosten(models.Model): KOSTENTYP_CHOICES = [ - ('sach', 'Sachkosten'), - ('ln2', 'LN2'), + ('sach', 'Sach'), ('helium', 'Helium'), - ('inv', 'Inventar'), ] - - buchungsdatum = models.DateField('Buchungsdatum') - rechnungsnummer = models.CharField('Rechnungsnummer', max_length=50) + gegenstand = models.CharField("Gegenstand", max_length=200) + buchungsdatum = models.DateField('Zahlungsdatum') + rechnungsnummer = models.CharField('Firma', max_length=50) kostentyp = models.CharField('Kostentyp', max_length=10, choices=KOSTENTYP_CHOICES) - gas_volume = models.DecimalField('Gasvolumen (Liter)', max_digits=10, decimal_places=2, null=True, blank=True) + + # IMPORTANT: now this field stores m³ (not liters) + gas_volume = models.DecimalField('Gasvolumen (m³)', max_digits=10, decimal_places=2, null=True, blank=True) + betrag = models.DecimalField('Betrag (€)', max_digits=10, decimal_places=2) beschreibung = models.TextField('Beschreibung', blank=True) - + @property - def price_per_liter(self): + def gas_volume_liter(self): + # Liter = m³ / 0.75 + if self.kostentyp == 'helium' and self.gas_volume: + return self.gas_volume / Decimal("0.75") + return None + + @property + def price_per_m3(self): if self.kostentyp == 'helium' and self.gas_volume: return self.betrag / self.gas_volume return None - - def __str__(self): - 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.table_type}[{self.row_index}]: {self.formula}" + + @property + def price_per_liter(self): + # Preis(Liter) = betrag / liter + liters = self.gas_volume_liter + if self.kostentyp == 'helium' and liters: + return self.betrag / liters + return None # Or simpler: Just store row calculations in a JSONField class TableConfig(models.Model): @@ -201,6 +199,8 @@ class SecondTableEntry(models.Model): date = models.DateField(default=timezone.now) is_warm = models.BooleanField(default=False) lhe_delivery = models.CharField(max_length=100, blank=True, null=True) + vor = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True) + nach = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True) lhe_output = models.DecimalField( max_digits=10, decimal_places=2, @@ -210,7 +210,8 @@ class SecondTableEntry(models.Model): ) notes = models.TextField(blank=True, null=True) date_joined = models.DateField(auto_now_add=True) - + class Meta: + ordering = ["date", "id"] def __str__(self): return f"{self.client.name} - {self.date}" @@ -250,4 +251,63 @@ class MonthlySummary(models.Model): ) def __str__(self): - return f"Summary {self.sheet.year}-{self.sheet.month:02d}" \ No newline at end of file + return f"Summary {self.sheet.year}-{self.sheet.month:02d}" + + +class BetriebskostenSummary(models.Model): + personalkosten = models.DecimalField(max_digits=12, decimal_places=2, default=0) + + instandhaltung = models.DecimalField(max_digits=12, decimal_places=2, default=0) + heliumkosten = models.DecimalField(max_digits=12, decimal_places=2, default=0) + bezugskosten_gashe = models.DecimalField(max_digits=12, decimal_places=4, default=0) + umlage_personal = models.DecimalField(max_digits=12, decimal_places=2, default=0) + + def recalculate(self): + from django.db.models import Sum + from django.db.models.functions import Coalesce + from django.db.models import DecimalField, Value + from .models import Betriebskosten + + items = Betriebskosten.objects.all() + + sach_sum = items.filter(kostentyp='sach').aggregate( + total=Coalesce(Sum('betrag'), Value(0, output_field=DecimalField())) + )['total'] or Decimal('0') + + helium_sum = items.filter(kostentyp='helium').aggregate( + total=Coalesce(Sum('betrag'), Value(0, output_field=DecimalField())) + )['total'] or Decimal('0') + + helium_m3_sum = items.filter(kostentyp='helium').aggregate( + total=Coalesce(Sum('gas_volume'), Value(0, output_field=DecimalField())) + )['total'] or Decimal('0') + + bezug = (helium_sum / helium_m3_sum) if helium_m3_sum not in (None, 0, Decimal('0')) else Decimal('0') + + self.instandhaltung = sach_sum + self.heliumkosten = helium_sum + self.bezugskosten_gashe = bezug + self.umlage_personal = self.personalkosten / 2 + self.save() + +# models.py +from django.db import models + +class AbrechnungCell(models.Model): + """ + Storage for the 'Abrechnung' page where columns are custom (not 1:1 with Client). + Values are saved by: (interval_year, interval_start_month, row_key, col_key). + """ + interval_year = models.IntegerField() + interval_start_month = models.IntegerField() # first month of the 6-month window + + row_key = models.CharField(max_length=60) + col_key = models.CharField(max_length=60) + + value = models.DecimalField(max_digits=18, decimal_places=6, null=True, blank=True) + + class Meta: + unique_together = ("interval_year", "interval_start_month", "row_key", "col_key") + + def __str__(self): + return f"Abrechnung {self.interval_year}/{self.interval_start_month} {self.row_key}:{self.col_key}={self.value}" diff --git a/sheets/new/views.py b/sheets/new/views.py new file mode 100644 index 0000000..d089af7 --- /dev/null +++ b/sheets/new/views.py @@ -0,0 +1,5041 @@ +from django.shortcuts import render, redirect +from django.utils.decorators import method_decorator +from django.views.decorators.csrf import csrf_exempt +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, MonthlySummary ,BetriebskostenSummary,AbrechnungCell +) +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 +import json +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 build_halfyear_window(interval_year: int, start_month: int): + """ + Build a list of (year, month) for the 6-month interval, possibly crossing into the next year. + Example: (2025, 10) -> [(2025,10), (2025,11), (2025,12), (2026,1), (2026,2), (2026,3)] + """ + window = [] + for offset in range(6): + total_index = (start_month - 1) + offset # 0-based + y = interval_year + (total_index // 12) + m = (total_index % 12) + 1 + window.append((y, m)) + return window +# --------------------------------------------------------------------------- +# Halbjahres-Bilanz helpers +# --------------------------------------------------------------------------- + +# You can adjust these indices if needed. +# Assuming: +# - bottom_1.table has row "Gasbestand" at some fixed row index, +# and columns: ... Nm³, Lit. LHe +GASBESTAND_ROW_INDEX = 9 # <-- adjust if your bottom_1 has a different row index +GASBESTAND_COL_NM3 = 3 # <-- adjust to the column index for Nm³ in bottom_1 + +# In top_left / top_right, "Bestand in Kannen-1 (Lit. L-He)" is row_index 5 +BESTAND_KANNEN_ROW_INDEX = 5 + +def pick_sheet_by_gasbestand(window, sheets_by_ym, prev_sheet): + """ + Returns the last sheet in the window whose Gasbestand (J36, Nm³ column) != 0. + If none found, returns prev_sheet (Übertrag_Dez__Vorjahr equivalent). + """ + for (y, m) in reversed(window): + sheet = sheets_by_ym.get((y, m)) + if not sheet: + continue + gasbestand_nm3 = get_bottom1_value(sheet, GASBESTAND_ROW_INDEX, GASBESTAND_COL_NM3) + if gasbestand_nm3 != 0: + return sheet + return prev_sheet +def get_bottom1_value(sheet, row_index: int, col_index: int) -> Decimal: + """Get a numeric value from bottom_1, or 0 if missing.""" + if sheet is None: + return Decimal('0') + + cell = Cell.objects.filter( + sheet=sheet, + table_type='bottom_1', + row_index=row_index, + column_index=col_index, + ).first() + + if cell is None or cell.value in (None, ''): + return Decimal('0') + + try: + return Decimal(str(cell.value)) + except Exception: + return Decimal('0') + +# MUST match the column order in your monthly_sheets top-right table + + + +def get_top_right_value(sheet, client_name: str, row_index: int) -> Decimal: + """ + Read a numeric value from the top_right table of a MonthlySheet for + a given client (by column) and row_index. + + top_right cells are keyed by (sheet, table_type='top_right', + row_index, column_index), where column_index is the position of the + client in HALFYEAR_RIGHT_CLIENTS. + """ + if sheet is None: + return Decimal('0') + + col_index = RIGHT_CLIENT_INDEX.get(client_name) + if col_index is None: + return Decimal('0') + + cell = Cell.objects.filter( + sheet=sheet, + table_type='top_right', + row_index=row_index, + column_index=col_index, + ).first() + + if cell is None or cell.value in (None, ''): + return Decimal('0') + + try: + return Decimal(str(cell.value)) + except Exception: + return Decimal('0') + +TR_RUECKF_FLUESSIG_ROW = 2 # confirmed by your March value 172.840560 +TR_BESTAND_KANNEN_ROW = 5 # confirmed by your earlier query +def get_bestand_kannen_for_month(sheet, client_name: str) -> Decimal: + """ + 'B9' in your description: Bestand in Kannen-1 (Lit. L-He) + For this implementation we take it from top_left row_index = 5 for that client. + """ + return get_top_left_value(sheet, client_name, row_index=BESTAND_KANNEN_ROW_INDEX) + +from decimal import Decimal +from django.db.models import Sum +from django.db.models.functions import Coalesce +from django.db.models import DecimalField, Value + +from .models import MonthlySheet, SecondTableEntry, Client, Cell +from django.shortcuts import redirect, render + +# You already have HALFYEAR_CLIENTS for the left table (AG Vogel, AG Halfm, IKP) +HALFYEAR_CLIENTS = ["AG Vogel", "AG Halfm", "IKP"] + +# NEW: clients for the top-right half-year table +HALFYEAR_RIGHT_CLIENTS = [ + "Dr. Fohrer", + "AG Buntk.", + "AG Alff", + "AG Gutfl.", + "M3 Thiele", + "M3 Buntkowsky", + "M3 Gutfleisch", +] +BOTTOM1_COL_VOLUME = 0 +BOTTOM1_COL_BAR = 1 +BOTTOM1_COL_KORR = 2 +BOTTOM1_COL_NM3 = 3 +BOTTOM1_COL_LHE = 4 +BOTTOM2_ROW_ANLAGE = 0 +BOTTOM2_COL_G39 = 0 # "Gefäss 2,5" (cell id shows column_index=0) +BOTTOM2_COL_I39 = 1 # "Gefäss 1,0" (cell id shows column_index=1) +BOTTOM2_ROW_INPUTS = { + "g39": (0, 0), # row_index=0, column_index=0 (your G39) + "i39": (0, 1), # row_index=0, column_index=1 (your I39) +} +FACTOR_NM3_TO_LHE = Decimal("0.75") +RIGHT_CLIENT_INDEX = {name: idx for idx, name in enumerate(HALFYEAR_RIGHT_CLIENTS)} +def halfyear_balance_view(request): + """ + Read-only Halbjahres-Bilanz view. + + LEFT table: AG Vogel / AG Halfm / IKP (exactly as in your last working version) + RIGHT table: Dr. Fohrer / AG Buntk. / AG Alff / AG Gutfl. / + M3 Thiele / M3 Buntkowsky / M3 Gutfleisch + using the Excel formulas you described. + + Uses the global 6-month interval from the main page (clients_list). + """ + # 1) Read half-year interval from the session + interval_year = request.session.get('halfyear_year') + interval_start = request.session.get('halfyear_start_month') + + if not interval_year or not interval_start: + # No interval chosen yet -> redirect to main page + return redirect('clients_list') + + interval_year = int(interval_year) + interval_start = int(interval_start) + + # You already have this helper in your code + window = build_halfyear_window(interval_year, interval_start) + # window = [(y1, m1), (y2, m2), ..., (y6, m6)] + + # (Year, month) of the first month + start_year, start_month = window[0] + + # Previous month (for "Stand ... (Vorjahr)" and "Best. in Kannen Vormonat") + prev_total_index = (start_month - 1) - 1 # one month back, 0-based + if prev_total_index >= 0: + prev_year = start_year + (prev_total_index // 12) + prev_month = (prev_total_index % 12) + 1 + else: + prev_year = start_year - 1 + prev_month = 12 + + # Load MonthlySheet objects for the window and for the previous month + sheets_by_ym = {} + for (y, m) in window: + sheet = MonthlySheet.objects.filter(year=y, month=m).first() + sheets_by_ym[(y, m)] = sheet + + prev_sheet = MonthlySheet.objects.filter(year=prev_year, month=prev_month).first() + def pick_bottom2_from_window(window, sheets_by_ym, prev_sheet): + # choose sheet (same logic you already use) + chosen = None + for (y, m) in reversed(window): + s = sheets_by_ym.get((y, m)) + # use your existing condition for choosing month + if s: + chosen = s + break + if chosen is None: + chosen = prev_sheet + + # Now read the two inputs safely + bottom2_inputs = {} + for key, (row_idx, col_idx) in BOTTOM2_ROW_INPUTS.items(): + bottom2_inputs[key] = get_bottom2_value(chosen, row_idx, col_idx) + + return chosen, bottom2_inputs + + + chosen_sheet_bottom2, bottom2_inputs = pick_bottom2_from_window(window, sheets_by_ym, prev_sheet) + bottom2_g39 = bottom2_inputs["g39"] + bottom2_i39 = bottom2_inputs["i39"] + # ---------------------------- + # HALF-YEAR BOTTOM TABLE 1 (Bilanz) - Read only + # ---------------------------- + chosen_sheet_bottom1 = pick_sheet_by_gasbestand(window, sheets_by_ym, prev_sheet) + + # IMPORTANT: define which bottom_1 row_index corresponds to Excel rows 27..35 + # If your bottom_1 starts at Excel row 27 => row_index 0 == Excel 27 + # then row_index = excel_row - 27 + BOTTOM1_EXCEL_START_ROW = 27 + + bottom1_excel_rows = list(range(27, 37)) # 27..36 + BOTTOM1_LABELS = [ + "Batterie 1", + "2", + "3", + "4", + "5", + "Batterie Links", + "2 Bündel", + "2 Ballone", + "Reingasspeicher", + "Gasbestand", + ] + + BOTTOM1_VOLUMES = [ + Decimal("2.4"), + Decimal("5.1"), + Decimal("4.0"), + Decimal("1.0"), + Decimal("4.0"), + Decimal("0.6"), + Decimal("1.2"), + Decimal("20.0"), + Decimal("5.0"), + None, # Gasbestand row has no volume + ] + nm3_sum_27_35 = Decimal("0") + lhe_sum_27_35 = Decimal("0") + bottom1_rows = [] + + for excel_row in bottom1_excel_rows: + row_index = excel_row - BOTTOM1_EXCEL_START_ROW + + chosen_sheet_bottom1 = None + for (y, m) in reversed(window): + s = sheets_by_ym.get((y, m)) + gasbestand = get_bottom1_value(s, GASBESTAND_ROW_INDEX, GASBESTAND_COL_NM3) # J36 (Nm3) + if gasbestand != 0: + chosen_sheet_bottom1 = s + break + + if chosen_sheet_bottom1 is None: + chosen_sheet_bottom1 = prev_sheet + + # Normal rows (27..35): read from chosen sheet and accumulate sums + if excel_row != 36: + nm3_val = get_bottom1_value(chosen_sheet_bottom1, row_index, BOTTOM1_COL_NM3) + lhe_val = get_bottom1_value(chosen_sheet_bottom1, row_index, BOTTOM1_COL_LHE) + + nm3_sum_27_35 += nm3_val + lhe_sum_27_35 += lhe_val + + bottom1_rows.append({ + "label": BOTTOM1_LABELS[row_index], + "volume": BOTTOM1_VOLUMES[row_index], + "bar": get_bottom1_value(chosen_sheet_bottom1, row_index, BOTTOM1_COL_BAR), + "korr": get_bottom1_value(chosen_sheet_bottom1, row_index, BOTTOM1_COL_KORR), + "nm3": nm3_val, + "lhe": lhe_val, + }) + + # Gasbestand row (36): show sums (J36 = SUM(J27:J35), K36 = SUM(K27:K35)) + else: + bottom1_rows.append({ + "label": "Gasbestand", + "volume": "", + "bar": "", + "korr": "", + "nm3": nm3_sum_27_35, + "lhe": lhe_sum_27_35, + }) + start_sheet = sheets_by_ym.get((start_year, start_month)) + # ------------------------------------------------------------ + # Bottom Table 2 (Halbjahres Bilanz) – server-side recalcBottom2() + # ------------------------------------------------------------ + + FACTOR_BT2 = Decimal("0.75") + + # 1) Helper: pick last-nonzero value of bottom_2 row0 col0/col1 from the window (fallback: prev_sheet) + def pick_last_nonzero_bottom2(row_index: int, col_index: int) -> Decimal: + # Scan from last month in window backwards + for (y, m) in reversed(window): + s = sheets_by_ym.get((y, m)) + if not s: + continue + v = get_bottom2_value(s, row_index, col_index) + if v is not None and v != 0: + return v + # fallback to month before window + v_prev = get_bottom2_value(prev_sheet, row_index, col_index) + return v_prev if v_prev is not None else Decimal("0") + + # 2) K38 comes from Overall Summary: "Summe Bestand (Lit. L-He)" + # Find it from your already built overall summary rows list. + + k38 = Decimal("0") + j38 = Decimal("0") + # 3) Inputs G39 / I39 (picked from last non-zero month in window) + g39 = pick_last_nonzero_bottom2(row_index=0, col_index=0) # G39 + i39 = pick_last_nonzero_bottom2(row_index=0, col_index=1) # I39 + + k39 = (g39 or Decimal("0")) + (i39 or Decimal("0")) + j39 = k39 * FACTOR_BT2 + + # 4) +Kaltgas (row 40) + # JS: + # g40 = (2500 - g39)/100*10 + # i40 = (1000 - i39)/100*10 + g40 = None + i40 = None + if g39 is not None: + g40 = (Decimal("2500") - g39) / Decimal("100") * Decimal("10") + if i39 is not None: + i40 = (Decimal("1000") - i39) / Decimal("100") * Decimal("10") + + k40 = (g40 or Decimal("0")) + (i40 or Decimal("0")) + j40 = k40 * FACTOR_BT2 + + # 5) Bestand flüssig He (row 43) + k43 = ( + (k38 or Decimal("0")) + + (k39 or Decimal("0")) + + (k40 or Decimal("0")) + ) + j43 = k43 * FACTOR_BT2 + + # 6) Gesamtbestand neu (row 44) = Gasbestand(Lit) from Bottom Table 1 + k43 + gasbestand_lit = Decimal("0") + for r in bottom1_rows: + if (r.get("label") or "").strip().startswith("Gasbestand"): + gasbestand_lit = r.get("lhe") or Decimal("0") + break + + k44 = (gasbestand_lit or Decimal("0")) + (k43 or Decimal("0")) + j44 = k44 * FACTOR_BT2 + + bottom2 = { + "j38": j38, "k38": k38, + "g39": g39, "i39": i39, "j39": j39, "k39": k39, + "g40": g40, "i40": i40, "j40": j40, "k40": k40, + "j43": j43, "k43": k43, + "j44": j44, "k44": k44, + } + + # ------------------------------------------------------------------ + # 2) LEFT TABLE (your existing, working logic) + # ------------------------------------------------------------------ + HALFYEAR_CLIENTS_LEFT = ["AG Vogel", "AG Halfm", "IKP"] + + # We'll collect client-wise values first for clarity. + client_data_left = {name: {} for name in HALFYEAR_CLIENTS_LEFT} + + # --- Row B3: Stand der Gaszähler (Nm³) + # = MAX(B3 from previous month, and B3 from each of the 6 months in the window) + # row_index 0 in top_left = "Stand der Gaszähler (Nm³)" + months_for_max = [(prev_year, prev_month)] + window + + for cname in HALFYEAR_CLIENTS_LEFT: + max_val = Decimal('0') + for (y, m) in months_for_max: + sheet = sheets_by_ym.get((y, m)) + if sheet is None and (y, m) == (prev_year, prev_month): + sheet = prev_sheet + val_b3 = get_top_left_value(sheet, cname, row_index=0) + if val_b3 > max_val: + max_val = val_b3 + client_data_left[cname]['stand_gas'] = max_val + + # --- Row B4: Stand der Gaszähler (Vorjahr) (Nm³) -> previous month same row --- + for cname in HALFYEAR_CLIENTS_LEFT: + val_b4 = get_top_left_value(prev_sheet, cname, row_index=0) + client_data_left[cname]['stand_gas_prev'] = val_b4 + + # --- Row B5: Gasrückführung (Nm³) = B3 - B4 --- + for cname in HALFYEAR_CLIENTS_LEFT: + b3 = client_data_left[cname]['stand_gas'] + b4 = client_data_left[cname]['stand_gas_prev'] + client_data_left[cname]['gasrueckf'] = b3 - b4 + + # --- Row B6: Rückführung flüssig (Lit. L-He) = B5 / 0.75 --- + for cname in HALFYEAR_CLIENTS_LEFT: + b5 = client_data_left[cname]['gasrueckf'] + client_data_left[cname]['rueckf_fluessig'] = (b5 / Decimal('0.75')) if b5 != 0 else Decimal('0') + + # --- Row B7: Sonderrückführungen (Lit. L-He) = sum over 6 months of that row --- + # That row index is 4 in your top_left table. + for cname in HALFYEAR_CLIENTS_LEFT: + sonder_total = Decimal('0') + for (y, m) in window: + sheet = sheets_by_ym.get((y, m)) + if sheet: + sonder_total += get_top_left_value(sheet, cname, row_index=4) + client_data_left[cname]['sonder'] = sonder_total + + # --- Row B8: Bestand in Kannen-1 (Lit. L-He) --- + # Excel-style logic with Gasbestand (J36) and fallback to previous month. + for cname in HALFYEAR_CLIENTS_LEFT: + chosen_value = None + + # Go from last month (window[5]) backwards to first (window[0]) + for (y, m) in reversed(window): + sheet = sheets_by_ym.get((y, m)) + gasbestand = get_bottom1_value(sheet, GASBESTAND_ROW_INDEX, GASBESTAND_COL_NM3) + if gasbestand != 0: + chosen_value = get_bestand_kannen_for_month(sheet, cname) + break + + # If still None -> use previous month (Übertrag_Dez__Vorjahr equivalent) + if chosen_value is None: + sheet_prev = prev_sheet + chosen_value = get_bestand_kannen_for_month(sheet_prev, cname) + + client_data_left[cname]['bestand_kannen'] = chosen_value if chosen_value is not None else Decimal('0') + + # --- Row B9: Summe Bestand (Lit. L-He) = equal to previous row --- + for cname in HALFYEAR_CLIENTS_LEFT: + client_data_left[cname]['summe_bestand'] = client_data_left[cname]['bestand_kannen'] + + # --- Row B10: Best. in Kannen Vormonat (Lit. L-He) + # = Bestand in Kannen-1 from the month BEFORE the window (prev_year, prev_month) + for cname in HALFYEAR_CLIENTS_LEFT: + client_data_left[cname]['best_kannen_vormonat'] = get_bestand_kannen_for_month(prev_sheet, cname) + + # --- Row B13: Bezug (Liter L-He) --- + for cname in HALFYEAR_CLIENTS_LEFT: + total_bezug = Decimal('0') + for (y, m) in window: + qs = SecondTableEntry.objects.filter( + client__name=cname, + date__year=y, + date__month=m, + ).aggregate( + total=Coalesce(Sum('lhe_output'), Value(0, output_field=DecimalField())) + ) + total_bezug += Decimal(str(qs['total'])) + client_data_left[cname]['bezug'] = total_bezug + + # --- Row B14: Rückführ. Soll (Lit. L-He) = Bezug - Summe Bestand + Best. in Kannen Vormonat --- + for cname in HALFYEAR_CLIENTS_LEFT: + b13 = client_data_left[cname]['bezug'] + b11 = client_data_left[cname]['summe_bestand'] + b12 = client_data_left[cname]['best_kannen_vormonat'] + client_data_left[cname]['rueckf_soll'] = b13 - b11 + b12 + + # --- Row B15: Verluste (Soll-Rückf.) (Lit. L-He) = B14 - B6 --- + for cname in HALFYEAR_CLIENTS_LEFT: + b14 = client_data_left[cname]['rueckf_soll'] + b6 = client_data_left[cname]['rueckf_fluessig'] + client_data_left[cname]['verluste'] = b14 - b6 + + # --- Row B16: Füllungen warm (Lit. L-He) = sum over 6 months (row_index=11) --- + for cname in HALFYEAR_CLIENTS_LEFT: + total_warm = Decimal('0') + for (y, m) in window: + sheet = sheets_by_ym.get((y, m)) + total_warm += get_top_left_value(sheet, cname, row_index=11) + client_data_left[cname]['fuellungen_warm'] = total_warm + + # --- Row B17: Kaltgas Rückgabe (Lit. L-He) = Bezug * 0.06 --- + factor = Decimal('0.06') + for cname in HALFYEAR_CLIENTS_LEFT: + b13 = client_data_left[cname]['bezug'] + client_data_left[cname]['kaltgas_rueckgabe'] = b13 * factor + + # --- Row B18: Verbraucherverluste (Liter L-He) = Verluste - Kaltgas Rückgabe --- + for cname in HALFYEAR_CLIENTS_LEFT: + b15 = client_data_left[cname]['verluste'] + b17 = client_data_left[cname]['kaltgas_rueckgabe'] + client_data_left[cname]['verbraucherverluste'] = b15 - b17 + + # --- Row B19: % = Verbraucherverluste / Bezug --- + for cname in HALFYEAR_CLIENTS_LEFT: + bezug = client_data_left[cname]['bezug'] + verb = client_data_left[cname]['verbraucherverluste'] + if bezug != 0: + client_data_left[cname]['percent'] = verb / bezug + else: + client_data_left[cname]['percent'] = None + + # Build LEFT rows structure + left_row_defs = [ + ('Stand der Gaszähler (Nm³)', 'stand_gas'), + ('Stand der Gaszähler (Vorjahr) (Nm³)', 'stand_gas_prev'), + ('Gasrückführung (Nm³)', 'gasrueckf'), + ('Rückführung flüssig (Lit. L-He)', 'rueckf_fluessig'), + ('Sonderrückführungen (Lit. L-He)', 'sonder'), + ('Bestand in Kannen-1 (Lit. L-He)', 'bestand_kannen'), + ('Summe Bestand (Lit. L-He)', 'summe_bestand'), + ('Best. in Kannen Vormonat (Lit. L-He)', 'best_kannen_vormonat'), + ('Bezug (Liter L-He)', 'bezug'), + ('Rückführ. Soll (Lit. L-He)', 'rueckf_soll'), + ('Verluste (Soll-Rückf.) (Lit. L-He)', 'verluste'), + ('Füllungen warm (Lit. L-He)', 'fuellungen_warm'), + ('Kaltgas Rückgabe (Lit. L-He) – Faktor', 'kaltgas_rueckgabe'), + ('Verbraucherverluste (Liter L-He)', 'verbraucherverluste'), + ('%', 'percent'), + ] + + rows_left = [] + for label, key in left_row_defs: + values = [client_data_left[cname][key] for cname in HALFYEAR_CLIENTS_LEFT] + if key == 'percent': + total_bezug = sum((client_data_left[c]['bezug'] for c in HALFYEAR_CLIENTS_LEFT), Decimal('0')) + total_verb = sum((client_data_left[c]['verbraucherverluste'] for c in HALFYEAR_CLIENTS_LEFT), Decimal('0')) + total = (total_verb / total_bezug) if total_bezug != 0 else None + else: + total = sum((v for v in values if v is not None), Decimal('0')) + rows_left.append({ + 'label': label, + 'values': values, + 'total': total, + 'is_percent': key == 'percent', + }) + + # ------------------------------------------------------------------ + # 3) RIGHT TABLE (top-right half-year aggregation) + # ------------------------------------------------------------------ + RIGHT_CLIENTS = HALFYEAR_RIGHT_CLIENTS # for brevity + + right_data = {name: {} for name in RIGHT_CLIENTS} + + # --- Bezug (Liter L-He) for each right client (same as for left) --- + for cname in RIGHT_CLIENTS: + total_bezug = Decimal('0') + for (y, m) in window: + qs = SecondTableEntry.objects.filter( + client__name=cname, + date__year=y, + date__month=m, + ).aggregate( + total=Coalesce(Sum('lhe_output'), Value(0, output_field=DecimalField())) + ) + total_bezug += Decimal(str(qs['total'])) + right_data[cname]['bezug'] = total_bezug + def find_bestand_from_window(reference_client: str) -> Decimal: + """ + Implements: + WENN(last_month!J36=0; WENN(prev_month!J36=0; ...; prev_sheet!9); last_month!9) + reference_client decides which column (L/N/P/Q/R) we read from monthly top_right row_index=5. + """ + # scan backward through window + for (y, m) in reversed(window): + sh = sheets_by_ym.get((y, m)) + if not sh: + continue + gasbestand = get_bottom1_value(sh, GASBESTAND_ROW_INDEX, GASBESTAND_COL_NM3) + if gasbestand != 0: + return get_top_right_value(sh, reference_client, TR_BESTAND_KANNEN_ROW) + + # fallback to previous month (Übertrag_Dez__Vorjahr equivalent) + return get_top_right_value(prev_sheet, reference_client, TR_BESTAND_KANNEN_ROW) + + # Fohrer+Buntk merged: BOTH use Fohrer column (L9) + val_L = find_bestand_from_window("Dr. Fohrer") + right_data["Dr. Fohrer"]["bestand_kannen"] = val_L + right_data["AG Buntk."]["bestand_kannen"] = val_L + + # Alff+Gutfl merged: BOTH use Alff column (N9) + val_N = find_bestand_from_window("AG Alff") + right_data["AG Alff"]["bestand_kannen"] = val_N + right_data["AG Gutfl."]["bestand_kannen"] = val_N + + # M3 each uses its own column (P9/Q9/R9) + right_data["M3 Thiele"]["bestand_kannen"] = find_bestand_from_window("M3 Thiele") + right_data["M3 Buntkowsky"]["bestand_kannen"] = find_bestand_from_window("M3 Buntkowsky") + right_data["M3 Gutfleisch"]["bestand_kannen"] = find_bestand_from_window("M3 Gutfleisch") + # Helper for pair shares (L13/($L13+$M13), etc.) + def pair_share(c1, c2): + total = right_data[c1]['bezug'] + right_data[c2]['bezug'] + if total == 0: + return (Decimal('0'), Decimal('0')) + return ( + right_data[c1]['bezug'] / total, + right_data[c2]['bezug'] / total, + ) + + # --- "Stand der Gaszähler (Vorjahr) (Nm³)" row: share based on Bezug --- + # Dr. Fohrer / AG Buntk. + s_fohrer, s_buntk = pair_share("Dr. Fohrer", "AG Buntk.") + right_data["Dr. Fohrer"]['stand_prev_share'] = s_fohrer + right_data["AG Buntk."]['stand_prev_share'] = s_buntk + + # AG Alff / AG Gutfl. + s_alff, s_gutfl = pair_share("AG Alff", "AG Gutfl.") + right_data["AG Alff"]['stand_prev_share'] = s_alff + right_data["AG Gutfl."]['stand_prev_share'] = s_gutfl + + # M3 Thiele / M3 Buntkowsky / M3 Gutfleisch → empty in Excel → None + for cname in ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"]: + right_data[cname]['stand_prev_share'] = None + + # --- Rückführung flüssig per month (raw sums) --- + # top_right row_index=2 is "Rückführung flüssig (Lit. L-He)" + + # --- Sonderrückführungen (row_index=3 in top_right) --- + for cname in RIGHT_CLIENTS: + sonder_total = Decimal('0') + for (y, m) in window: + sheet = sheets_by_ym.get((y, m)) + if sheet: + sonder_total += get_top_right_value(sheet, cname, row_index=3) + right_data[cname]['sonder'] = sonder_total + + # --- Sammelrückführung (row_index=4 in top_right), grouped & merged --- + # Group 1: Dr. Fohrer + AG Buntk. + group1_total = Decimal('0') + for (y, m) in window: + sheet = sheets_by_ym.get((y, m)) + if sheet: + group1_total += get_top_right_value(sheet, "Dr. Fohrer", row_index=4) + right_data["Dr. Fohrer"]['sammel'] = group1_total + right_data["AG Buntk."]['sammel'] = group1_total + + # Group 2: AG Alff + AG Gutfl. + group2_total = Decimal('0') + for (y, m) in window: + sheet = sheets_by_ym.get((y, m)) + if sheet: + group2_total += get_top_right_value(sheet, "AG Alff", row_index=4) + right_data["AG Alff"]['sammel'] = group2_total + right_data["AG Gutfl."]['sammel'] = group2_total + + # Group 3: M3 Thiele + M3 Buntkowsky + M3 Gutfleisch + group3_total = Decimal('0') + for (y, m) in window: + sheet = sheets_by_ym.get((y, m)) + if sheet: + group3_total += get_top_right_value(sheet, "M3 Thiele", row_index=4) + right_data["M3 Thiele"]['sammel'] = group3_total + right_data["M3 Buntkowsky"]['sammel'] = group3_total + right_data["M3 Gutfleisch"]['sammel'] = group3_total + def safe_div(a: Decimal, b: Decimal) -> Decimal: + return (a / b) if b != 0 else Decimal("0") + + # --- Rückführung flüssig (Lit. L-He) for Halbjahres-Bilanz top-right --- + # Uses your exact formulas. + + # 1) Fohrer / Buntk split by BEZUG share times group SAMMEL (L8) + L13 = right_data["Dr. Fohrer"]["bezug"] + M13 = right_data["AG Buntk."]["bezug"] + L8 = right_data["Dr. Fohrer"]["sammel"] # merged group total + + den = (L13 + M13) + right_data["Dr. Fohrer"]["rueckf_fluessig"] = (safe_div(L13, den) * L8) if den != 0 else Decimal("0") + right_data["AG Buntk."]["rueckf_fluessig"] = (safe_div(M13, den) * L8) if den != 0 else Decimal("0") + + # 2) Alff / Gutfl split by BEZUG share times group SAMMEL (N8) + N13 = right_data["AG Alff"]["bezug"] + O13 = right_data["AG Gutfl."]["bezug"] + N8 = right_data["AG Alff"]["sammel"] # merged group total + + den = (N13 + O13) + right_data["AG Alff"]["rueckf_fluessig"] = (safe_div(N13, den) * N8) if den != 0 else Decimal("0") + right_data["AG Gutfl."]["rueckf_fluessig"] = (safe_div(O13, den) * N8) if den != 0 else Decimal("0") + + # 3) M3 Thiele = sum of monthly Rückführung flüssig (monthly top_right row_index=2) over window + P6_sum = Decimal("0") + for (y, m) in window: + sh = sheets_by_ym.get((y, m)) + P6_sum += get_top_right_value(sh, "M3 Thiele", TR_RUECKF_FLUESSIG_ROW) + right_data["M3 Thiele"]["rueckf_fluessig"] = P6_sum + + # 4) M3 Buntkowsky / M3 Gutfleisch split by BEZUG share times M3-group SAMMEL (P8) + P13 = right_data["M3 Thiele"]["bezug"] + Q13 = right_data["M3 Buntkowsky"]["bezug"] + R13 = right_data["M3 Gutfleisch"]["bezug"] + P8 = right_data["M3 Thiele"]["sammel"] # merged group total + + den = (P13 + Q13 + R13) + right_data["M3 Buntkowsky"]["rueckf_fluessig"] = (safe_div(Q13, den) * P8) if den != 0 else Decimal("0") + right_data["M3 Gutfleisch"]["rueckf_fluessig"] = (safe_div(R13, den) * P8) if den != 0 else Decimal("0") + # --- Bestand in Kannen-1 (Lit. L-He) for right table (grouped) --- + # Use Gasbestand (J36) and fallback logic, but now reading top_right B9 for each group. + TOP_RIGHT_ROW_BESTAND_KANNEN = 6 # <-- most likely correct in your setup + + def pick_bestand_top_right(base_client: str) -> Decimal: + # Go from last month in window backwards: if Gasbestand != 0, use that month's Bestand in Kannen + for (y, m) in reversed(window): + sh = sheets_by_ym.get((y, m)) + if not sh: + continue + + gasbestand = get_bottom1_value(sh, GASBESTAND_ROW_INDEX, GASBESTAND_COL_NM3) + if gasbestand != 0: + return get_top_right_value(sh, base_client, TOP_RIGHT_ROW_BESTAND_KANNEN) + + # Fallback to previous month (Übertrag_Dez__Vorjahr equivalent) + return get_top_right_value(prev_sheet, base_client, TOP_RIGHT_ROW_BESTAND_KANNEN) + + # Group 1 merged (Fohrer + Buntk.) + g1_best = pick_bestand_top_right("Dr. Fohrer") + right_data["Dr. Fohrer"]["bestand_kannen"] = g1_best + right_data["AG Buntk."]["bestand_kannen"] = g1_best + + # Group 2 merged (Alff + Gutfl.) + g2_best = pick_bestand_top_right("AG Alff") + right_data["AG Alff"]["bestand_kannen"] = g2_best + right_data["AG Gutfl."]["bestand_kannen"] = g2_best + + # Group 3 merged (M3 Thiele + M3 Buntkowsky + M3 Gutfleisch) + g3_best = pick_bestand_top_right("M3 Thiele") + right_data["M3 Thiele"]["bestand_kannen"] = g3_best + right_data["M3 Buntkowsky"]["bestand_kannen"] = g3_best + right_data["M3 Gutfleisch"]["bestand_kannen"] = g3_best + + # Summe Bestand = same as previous row + for cname in RIGHT_CLIENTS: + right_data[cname]['summe_bestand'] = right_data[cname]['bestand_kannen'] + + # Best. in Kannen Vormonat (Lit. L-He) from previous month top_right row_index=7 + g1_prev = get_top_right_value(prev_sheet, "Dr. Fohrer", TOP_RIGHT_ROW_BESTAND_KANNEN) + right_data["Dr. Fohrer"]['best_kannen_vormonat'] = g1_prev + right_data["AG Buntk."]['best_kannen_vormonat'] = g1_prev + + g2_prev = get_top_right_value(prev_sheet, "AG Alff", TOP_RIGHT_ROW_BESTAND_KANNEN) + right_data["AG Alff"]['best_kannen_vormonat'] = g2_prev + right_data["AG Gutfl."]['best_kannen_vormonat'] = g2_prev + + + # Group 1 merged (Fohrer + Buntk.) + g1_prev = get_top_right_value(prev_sheet, "Dr. Fohrer", TOP_RIGHT_ROW_BESTAND_KANNEN) + right_data["Dr. Fohrer"]["best_kannen_vormonat"] = g1_prev + right_data["AG Buntk."]["best_kannen_vormonat"] = g1_prev + + # Group 2 merged (Alff + Gutfl.) + g2_prev = get_top_right_value(prev_sheet, "AG Alff", TOP_RIGHT_ROW_BESTAND_KANNEN) + right_data["AG Alff"]["best_kannen_vormonat"] = g2_prev + right_data["AG Gutfl."]["best_kannen_vormonat"] = g2_prev + + # Group 3 UNMERGED (each one reads its own cell) + right_data["M3 Thiele"]["best_kannen_vormonat"] = get_top_right_value(prev_sheet, "M3 Thiele", TOP_RIGHT_ROW_BESTAND_KANNEN) + right_data["M3 Buntkowsky"]["best_kannen_vormonat"] = get_top_right_value(prev_sheet, "M3 Buntkowsky", TOP_RIGHT_ROW_BESTAND_KANNEN) + right_data["M3 Gutfleisch"]["best_kannen_vormonat"] = get_top_right_value(prev_sheet, "M3 Gutfleisch", TOP_RIGHT_ROW_BESTAND_KANNEN) + + # --- Rückführ. Soll (Lit. L-He) according to your formulas --- + + # Group 1: Dr. Fohrer / AG Buntk. + total_bestand_1 = right_data["Dr. Fohrer"]['summe_bestand'] + best_vormonat_1 = right_data["Dr. Fohrer"]['best_kannen_vormonat'] + diff1 = total_bestand_1 - best_vormonat_1 + share_fohrer = right_data["Dr. Fohrer"]['stand_prev_share'] or Decimal('0') + + right_data["Dr. Fohrer"]['rueckf_soll'] = ( + right_data["Dr. Fohrer"]['bezug'] - diff1 * share_fohrer + ) + right_data["AG Buntk."]['rueckf_soll'] = ( + right_data["AG Buntk."]['bezug'] - total_bestand_1 + best_vormonat_1 + ) + + # Group 2: AG Alff / AG Gutfl. + total_bestand_2 = right_data["AG Alff"]['summe_bestand'] + best_vormonat_2 = right_data["AG Alff"]['best_kannen_vormonat'] + diff2 = total_bestand_2 - best_vormonat_2 + share_alff = right_data["AG Alff"]['stand_prev_share'] or Decimal('0') + share_gutfl = right_data["AG Gutfl."]['stand_prev_share'] or Decimal('0') + + right_data["AG Alff"]['rueckf_soll'] = ( + right_data["AG Alff"]['bezug'] - diff2 * share_alff + ) + right_data["AG Gutfl."]['rueckf_soll'] = ( + right_data["AG Gutfl."]['bezug'] - diff2 * share_gutfl + ) + + # Group 3: M3 Thiele / M3 Buntkowsky / M3 Gutfleisch + for cname in ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"]: + b13 = right_data[cname]['bezug'] + b12 = right_data[cname]['best_kannen_vormonat'] + b11 = right_data[cname]['summe_bestand'] + # Excel: P13+P12-P11 etc. + right_data[cname]['rueckf_soll'] = b13 + b12 - b11 + + # --- Verluste (Soll-Rückf.) (Lit. L-He) = B14 - B6 - B7 --- + for cname in RIGHT_CLIENTS: + b14 = right_data[cname]['rueckf_soll'] + b6 = right_data[cname]['rueckf_fluessig'] + b7 = right_data[cname]['sonder'] + right_data[cname]['verluste'] = b14 - b6 - b7 + + # --- Füllungen warm (Lit. L-He) = sum of monthly 'Füllungen warm' (row_index=11 top_right) --- + for cname in RIGHT_CLIENTS: + total_warm = Decimal('0') + for (y, m) in window: + sheet = sheets_by_ym.get((y, m)) + if sheet: + total_warm += get_top_right_value(sheet, cname, row_index=11) + right_data[cname]['fuellungen_warm'] = total_warm + + # --- Kaltgas Rückgabe (Lit. L-He) – Faktor = Bezug * 0.06 --- + for cname in RIGHT_CLIENTS: + b13 = right_data[cname]['bezug'] + right_data[cname]['kaltgas_rueckgabe'] = b13 * factor + + # --- Verbraucherverluste (Liter L-He) = Verluste - Kaltgas Rückgabe --- + for cname in RIGHT_CLIENTS: + b15 = right_data[cname]['verluste'] + b17 = right_data[cname]['kaltgas_rueckgabe'] + right_data[cname]['verbraucherverluste'] = b15 - b17 + + # --- % = Verbraucherverluste / Bezug --- + for cname in RIGHT_CLIENTS: + bezug = right_data[cname]['bezug'] + verb = right_data[cname]['verbraucherverluste'] + if bezug != 0: + right_data[cname]['percent'] = verb / bezug + else: + right_data[cname]['percent'] = None + + # Build RIGHT rows structure + right_row_defs = [ + ('Stand der Gaszähler (Vorjahr) (Nm³)', 'stand_prev_share'), + # We skip the pure-text "Gasrückführung (Nm³)" line here, + # because it’s only text (Aufteilung nach Verbrauch / Gaszähler) + # and easier to render directly in the template if needed. + ('Rückführung flüssig (Lit. L-He)', 'rueckf_fluessig'), + ('Sonderrückführungen (Lit. L-He)', 'sonder'), + ('Sammelrückführung (Lit. L-He)', 'sammel'), + ('Bestand in Kannen-1 (Lit. L-He)', 'bestand_kannen'), + ('Summe Bestand (Lit. L-He)', 'summe_bestand'), + ('Best. in Kannen Vormonat (Lit. L-He)', 'best_kannen_vormonat'), + ('Bezug (Liter L-He)', 'bezug'), + ('Rückführ. Soll (Lit. L-He)', 'rueckf_soll'), + ('Verluste (Soll-Rückf.) (Lit. L-He)', 'verluste'), + ('Füllungen warm (Lit. L-He)', 'fuellungen_warm'), + ('Kaltgas Rückgabe (Lit. L-He) – Faktor', 'kaltgas_rueckgabe'), + ('Verbraucherverluste (Liter L-He)', 'verbraucherverluste'), + ('%', 'percent'), + ] + + rows_right = [] + for label, key in right_row_defs: + values = [right_data[cname].get(key) for cname in RIGHT_CLIENTS] + + if key == 'percent': + total_bezug = sum((right_data[c]['bezug'] for c in RIGHT_CLIENTS), Decimal('0')) + total_verb = sum((right_data[c]['verbraucherverluste'] for c in RIGHT_CLIENTS), Decimal('0')) + total = (total_verb / total_bezug) if total_bezug != 0 else None + else: + total = sum((v for v in values if isinstance(v, Decimal)), Decimal('0')) + + rows_right.append({ + 'label': label, + 'values': values, + 'total': total, + 'is_percent': key == 'percent', + }) + SUM_TABLE_ROWS = [ + ("Rückführung flüssig (Lit. L-He)", "rueckf_fluessig"), + ("Sonderrückführungen (Lit. L-He)", "sonder"), + ("Sammelrückführungen (Lit. L-He)", "sammel"), + ("Bestand in Kannen-1 (Lit. L-He)", "bestand_kannen"), + ("Summe Bestand (Lit. L-He)", "summe_bestand"), + ("Best. in Kannen Vormonat (Lit. L-He)", "best_kannen_vormonat"), + ("Bezug (Liter L-He)", "bezug"), + ("Rückführ. Soll (Lit. L-He)", "rueckf_soll"), + ("Verluste (Soll-Rückf.) (Lit. L-He)", "verluste"), + ("Füllungen warm (Lit. L-He)", "fuellungen_warm"), + ("Kaltgas Rückgabe (Lit. L-He) – Faktor", "kaltgas_rueckgabe"), + ("Faktor 0.06", "factor_row"), + ("Verbraucherverluste (Liter L-He)", "verbraucherverluste"), + ("%", "percent"), + ] + + RIGHT_GROUPS = { + "chemie": ["Dr. Fohrer", "AG Buntk."], + "mawi": ["AG Alff", "AG Gutfl."], + "m3": ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"], + } + + RIGHT_ALL = ["Dr. Fohrer", "AG Buntk.", "AG Alff", "AG Gutfl.", "M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"] + LEFT_ALL = HALFYEAR_CLIENTS_LEFT + + def safe_pct(verb, bez): + return (verb / bez) if bez != 0 else None + rows_sum = [] + def d(x): + return x if isinstance(x, Decimal) else Decimal("0") + for label, key in SUM_TABLE_ROWS: + + if key == "factor_row": + lichtwiese = chemie = mawi = m3 = total = Decimal("0.06") + + elif key == "percent": + # Right totals + rw_bez = sum(d(right_data[c].get("bezug")) for c in RIGHT_ALL) + rw_verb = sum(d(right_data[c].get("verbraucherverluste")) for c in RIGHT_ALL) + lichtwiese = safe_pct(rw_verb, rw_bez) + + # Chemie + ch_bez = sum(d(right_data[c].get("bezug")) for c in RIGHT_GROUPS["chemie"]) + ch_verb = sum(d(right_data[c].get("verbraucherverluste")) for c in RIGHT_GROUPS["chemie"]) + chemie = safe_pct(ch_verb, ch_bez) + + # MaWi + mw_bez = sum(d(right_data[c].get("bezug")) for c in RIGHT_GROUPS["mawi"]) + mw_verb = sum(d(right_data[c].get("verbraucherverluste")) for c in RIGHT_GROUPS["mawi"]) + mawi = safe_pct(mw_verb, mw_bez) + + # M3 + m3_bez = sum(d(right_data[c].get("bezug")) for c in RIGHT_GROUPS["m3"]) + m3_verb = sum(d(right_data[c].get("verbraucherverluste")) for c in RIGHT_GROUPS["m3"]) + m3 = safe_pct(m3_verb, m3_bez) + + # Σ column = (left verb + right verb) / (left bez + right bez) + left_bez = sum(d(client_data_left[c].get("bezug")) for c in LEFT_ALL) + left_verb = sum(d(client_data_left[c].get("verbraucherverluste")) for c in LEFT_ALL) + total = safe_pct(left_verb + rw_verb, left_bez + rw_bez) + + else: + # normal rows = sums + lichtwiese = sum(d(right_data[c].get(key)) for c in RIGHT_ALL) + chemie = sum(d(right_data[c].get(key)) for c in RIGHT_GROUPS["chemie"]) + mawi = sum(d(right_data[c].get(key)) for c in RIGHT_GROUPS["mawi"]) + m3 = sum(d(right_data[c].get(key)) for c in RIGHT_GROUPS["m3"]) + + left_total = sum(d(client_data_left[c].get(key)) for c in LEFT_ALL) + total = left_total + lichtwiese + + rows_sum.append({ + "row_index": row_index, + "label": label, + "total": total, + "lichtwiese": lichtwiese, + "chemie": chemie, + "mawi": mawi, + "m3": m3, + "is_percent": (key == "percent"), + }) + def find_sum_row(rows, label_startswith: str): + for r in rows: + if str(r.get("label", "")).strip().startswith(label_startswith): + return r + return None + + summe_bestand_row = find_sum_row(rows_sum, "Summe Bestand") + k38 = (summe_bestand_row.get("total") if summe_bestand_row else Decimal("0")) or Decimal("0") + j38 = k38 * Decimal("0.75") + # --- FIX: now that k38 is known, update bottom2 + recompute dependent rows --- + bottom2["k38"] = k38 + bottom2["j38"] = j38 + + k39 = bottom2.get("k39") or Decimal("0") + k40 = bottom2.get("k40") or Decimal("0") + + # Row 43: Bestand flüssig He = SUMME(K38:K40) + k43 = (k38 or Decimal("0")) + k39 + k40 + j43 = k43 * Decimal("0.75") + + bottom2["k43"] = k43 + bottom2["j43"] = j43 + + # Row 44: Gesamtbestand neu = Gasbestand(Lit) from bottom table 1 + k43 + gasbestand_lit = Decimal("0") + for r in bottom1_rows: + if (r.get("label") or "").strip().startswith("Gasbestand"): + gasbestand_lit = r.get("lhe") or Decimal("0") + break + + k44 = gasbestand_lit + k43 + j44 = k44 * Decimal("0.75") + + bottom2["k44"] = k44 + bottom2["j44"] = j44 + def d(x): + return x if isinstance(x, Decimal) else Decimal("0") + + # ---- Bottom2: J38/K38 depend on rows_sum (overall summary), so do it HERE ---- + k38 = Decimal("0") + for r in rows_sum: + if r.get("label") == "Summe Bestand (Lit. L-He)": + k38 = r.get("total") or Decimal("0") + break + + j38 = k38 * FACTOR_NM3_TO_LHE # 0.75 + bottom2["k38"] = k38 + bottom2["j38"] = j38 + factor = Decimal("0.75") + + # window = the 6-month list you already build in this view: [(y,m), (y,m), ...] + # bottom2 = dict with "k44" already computed in your view + # rows_sum = overall sum group rows list (your existing halfyear logic) + + # 1) K46 = K44 from the month BEFORE the global month (interval start) + start_year = interval_year + start_month = interval_start # whatever you named your start month variable + + if start_month == 1: + prev_year, prev_month = start_year - 1, 12 + else: + prev_year, prev_month = start_year, start_month - 1 + + prev_sheet = MonthlySheet.objects.filter(year=prev_year, month=prev_month).first() + + # This assumes you have MonthlySummary.gesamtbestand_neu_lhe as K44 equivalent. + # If your field name differs, tell me your MonthlySummary model fields. + prev_k44 = Decimal("0") + if prev_sheet: + prev_sum = MonthlySummary.objects.filter(sheet=prev_sheet).first() + if prev_sum and prev_sum.gesamtbestand_neu_lhe is not None: + prev_k44 = Decimal(str(prev_sum.gesamtbestand_neu_lhe)) + + # helpers: read bottom_3 values for a given sheet/month + def get_bottom3_value(sheet, row_index, col_index): + if not sheet: + return Decimal("0") + c = Cell.objects.filter( + sheet=sheet, table_type="bottom_3", + row_index=row_index, column_index=col_index + ).first() + if not c or c.value in (None, "", "None"): + return Decimal("0") + try: + return Decimal(str(c.value)) + except Exception: + return Decimal("0") + + # 2) Sum rows across the 6-month window + g47_sum = Decimal("0") + i47_sum = Decimal("0") + j47_sum = Decimal("0") + + g50_sum = Decimal("0") + i50_sum = Decimal("0") + + for (yy, mm) in window: + s = MonthlySheet.objects.filter(year=yy, month=mm).first() + ## Excel row 47 maps to bottom_3 row_index=1 in DB (see monthly_sheet.html) + g = get_bottom3_value(s, 1, 1) # G47 editable + i = get_bottom3_value(s, 1, 2) # I47 editable + + g47_sum += g + i47_sum += i + + # In monthly_sheet, J47 is CALCULATED as (G47 + I47), not stored as an editable cell + j47_sum += (g + i) + + # row 50: G(2), I(4) + g50_sum += get_bottom3_value(s, 50, 2) + i50_sum += get_bottom3_value(s, 50, 4) + + # 3) K52 = Verbraucherverlust from overall sum group first column (global Σ) + k52 = Decimal("0") + for r in rows_sum: + label = (r.get("label") or "") + if label.startswith("Verbraucherverluste"): + k52 = r.get("total") or Decimal("0") + break + + # --- apply the SAME monthly formulas, with your overrides --- + # Row 46 + k46 = prev_k44 + j46 = k46 * factor + + # Row 47 + g47 = g47_sum + i47 = i47_sum + j47 = j47_sum + k47 = (j47 / factor) + g47 if (j47 != 0 or g47 != 0) else Decimal("0") + + # Row 48 + k48 = k46 + k47 + j48 = k48 * factor + + # Row 49 (akt. Monat) -> in halfyear we use current bottom2 K44 + k49 = bottom2.get("k44") or Decimal("0") + j49 = k49 * factor + + # Row 50 + g50 = g50_sum + i50 = i50_sum + j50 = i50 + k50 = (j50 / factor) if j50 != 0 else Decimal("0") + + # Row 51 + k51 = k48 - k49 - k50 + j51 = k51 * factor + + # Row 52 + j52 = k52 * factor + + # Row 53 + k53 = k51 - k52 + j53 = j51 - j52 + + bottom3 = { + "j46": j46, "k46": k46, + "g47": g47, "i47": i47, "j47": j47, "k47": k47, + "j48": j48, "k48": k48, + "j49": j49, "k49": k49, + "g50": g50, "i50": i50, "j50": j50, "k50": k50, + "j51": j51, "k51": k51, + "j52": j52, "k52": k52, + "j53": j53, "k53": k53, + } + # ------------------------------------------------------------------ + # 4) Context – keep old keys AND new ones + # ------------------------------------------------------------------ + context = { + 'interval_year': interval_year, + 'interval_start_month': interval_start, + 'window': window, + + # Left table – old names (for your first template) + 'clients': HALFYEAR_CLIENTS_LEFT, + 'rows': rows_left, + + # Left table – explicit + 'clients_left': HALFYEAR_CLIENTS_LEFT, + 'rows_left': rows_left, + + # Right table + 'clients_right': RIGHT_CLIENTS, + 'rows_right': rows_right, + 'rows_sum': rows_sum, + 'bottom1_rows': bottom1_rows, + + } + context["bottom2"] = bottom2 + context["bottom3"] = bottom3 + context["context_bottom2_g39"] = bottom2_inputs["g39"] + context["context_bottom2_i39"] = bottom2_inputs["i39"] + return render(request, 'halfyear_balance.html', context) + +def get_bottom2_value(sheet, row_index: int, col_index: int) -> Decimal: + """Get numeric value from bottom_2 or 0 if missing.""" + if sheet is None: + return Decimal("0") + cell = Cell.objects.filter( + sheet=sheet, + table_type="bottom_2", + row_index=row_index, + column_index=col_index, + ).first() + if cell is None or cell.value in (None, ""): + return Decimal("0") + try: + return Decimal(str(cell.value)) + except Exception: + return Decimal("0") + +def get_top_left_value(sheet, client_name: str, row_index: int) -> Decimal: + """ + Read a numeric value from the top_left table for a given month, client and row. + Does NOT use column_index, because top_left is keyed only by client + row_index. + """ + if sheet is None: + return Decimal('0') + + client_obj = Client.objects.filter(name=client_name).first() + if not client_obj: + return Decimal('0') + + cell = Cell.objects.filter( + sheet=sheet, + table_type='top_left', + client=client_obj, + row_index=row_index, + ).first() + + if cell is None or cell.value in (None, ''): + return Decimal('0') + + try: + return Decimal(str(cell.value)) + except Exception: + return Decimal('0') +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", + ] + current_summary = MonthlySummary.objects.filter(sheet=sheet).first() + + # Get previous month summary (for Bottom Table 3: K46 = prev K44) + prev_month_info = self.get_prev_month(year, month) + prev_summary = None + if not is_start_sheet: + prev_sheet = MonthlySheet.objects.filter( + year=prev_month_info['year'], + month=prev_month_info['month'] + ).first() + if prev_sheet: + prev_summary = MonthlySummary.objects.filter(sheet=prev_sheet).first() + + context.update({ + # ... your existing context ... + 'current_summary': current_summary, + 'prev_summary': prev_summary, + }) + # Update row counts in build_group_rows function + # 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 + MERGED_ROWS = {2, 3, 5, 6, 7, 9, 10, 12, 14, 15} + MERGED_PAIRS = [ + ("Dr. Fohrer", "AG Buntk."), + ("AG Alff", "AG Gutfl."), + ] + rows = [] + + # Determine row count + row_counts = { + "top_left": 16, + "top_right": 16, # rows 0–15 + "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 + + # We will store row sums for Bezug (row 8) and Verbraucherverluste (row 14) + # for top_left / top_right so we can compute the overall % in row 15. + sum_bezug = None + sum_verbrauch = None + + # Build each row + 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 (includes editable + calculated cells) + sum_value = None + total = Decimal("0") + has_value = False + + merged_second_indices = set() + if table_type == 'top_right' and row_idx in MERGED_ROWS: + for left_name, right_name in MERGED_PAIRS: + try: + right_idx = client_names.index(right_name) + merged_second_indices.add(right_idx) + except ValueError: + # client not in this table; just ignore + pass + + for col_idx, cell in enumerate(display_cells): + # Skip the duplicate (second) column of each merged pair + if col_idx in merged_second_indices: + continue + + if cell and cell.value is not None: + try: + total += Decimal(str(cell.value)) + has_value = True + except Exception: + pass + + if has_value: + sum_value = total + + # Remember special rows for top tables + if table_type in ("top_left", "top_right"): + if row_idx == 8: # Bezug + sum_bezug = total + elif row_idx == 14: # Verbraucherverluste + sum_verbrauch = total + + rows.append( + { + "cells": display_cells, + "sum": sum_value, + "row_index": row_idx, + } + ) + + # Adjust the % row sum for top_left / top_right: + # Sum(%) = Sum(Verbraucherverluste) / Sum(Bezug) + if table_type in ("top_left", "top_right"): + perc_row_idx = 15 # % row + if 0 <= perc_row_idx < len(rows): + if sum_bezug is not None and sum_bezug != 0 and sum_verbrauch is not None: + rows[perc_row_idx]["sum"] = sum_verbrauch / sum_bezug + else: + rows[perc_row_idx]["sum"] = None + + 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) + + # --- Build combined summary of top-left + top-right Sum columns --- + + # Helper to safely get the Sum for a given row index + def get_row_sum(rows, row_index): + if 0 <= row_index < len(rows): + return rows[row_index].get('sum') + return None + + # Row definitions we want in the combined Σ table + # (row_index in top tables, label shown in the small table) + summary_row_defs = [ + (2, "Rückführung flüssig (Lit. L-He)"), + (3, "Sonderrückführungen (Lit. L-He)"), + (4, "Sammelrückführungen (Lit. L-He)"), + (5, "Bestand in Kannen-1 (Lit. L-He)"), + (6, "Summe Bestand (Lit. L-He)"), + (7, "Best. in Kannen Vormonat (Lit. L-He)"), + (8, "Bezug (Liter L-He)"), + (9, "Rückführ. Soll (Lit. L-He)"), + (10, "Verluste (Soll-Rückf.) (Lit. L-He)"), + (11, "Füllungen warm (Lit. L-He)"), + (12, "Kaltgas Rückgabe (Lit. L-He) – Faktor"), + (13, "Faktor 0.06"), + (14, "Verbraucherverluste (Liter L-He)"), + (15, "%"), + ] + + # Precompute totals for Bezug and Verbraucherverluste across both tables + bezug_left = get_row_sum(top_left_rows, 8) or Decimal('0') + bezug_right = get_row_sum(top_right_rows, 8) or Decimal('0') + total_bezug = bezug_left + bezug_right + + verb_left = get_row_sum(top_left_rows, 14) or Decimal('0') + verb_right = get_row_sum(top_right_rows, 14) or Decimal('0') + total_verbrauch = verb_left + verb_right + + summary_rows = [] + + for row_index, label in summary_row_defs: + # Faktor row: always fixed 0.06 + if row_index == 13: + summary_value = Decimal('0.06') + + # % row: total Verbraucherverluste / total Bezug + elif row_index == 15: + if total_bezug != 0: + summary_value = total_verbrauch / total_bezug + else: + summary_value = None + + else: + left_sum = get_row_sum(top_left_rows, row_index) + right_sum = get_row_sum(top_right_rows, row_index) + + # Sammelrückführungen: only from top-right table + if row_index == 4: + left_sum = None + + total = Decimal('0') + + has_any = False + if left_sum is not None: + total += Decimal(str(left_sum)) + has_any = True + if right_sum is not None: + total += Decimal(str(right_sum)) + has_any = True + + summary_value = total if has_any else None + + summary_rows.append({ + 'row_index': row_index, + 'label': label, + 'sum': summary_value, + }) + + # 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, + 'summary_rows': summary_rows, # 👈 NEW + '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 + + + 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) +ABRECHNUNG_COL_CLIENTS = { + # first 3 columns + "pkm_vogel": ["AG Vogel"], + "iap_halfmann": ["AG Halfm"], # or whatever exact name you use in Client admin + "ikp": ["IKP"], + + # your “5 columns” group (excluding Chemie/MaWi/Physik summaries) + "orgchem_thiele": ["M3 Thiele"], # if OrgChem Thiele corresponds to M3 Thiele in your DB + "phychem_m3_buntkow": ["AG Buntk.", "M3 Buntkowsky"], + "orgchem_fohrer": ["Dr. Fohrer"], + "mawi_m3_gutfl": ["AG Gutfl.", "M3 Gutfleisch"], + "mawi_alff": ["AG Alff"], + + # summary columns at the end + "chemie": ["Dr. Fohrer", "AG Buntk.", "M3 Buntkowsky", "M3 Thiele"], + "mawi": ["AG Alff", "AG Gutfl.", "M3 Gutfleisch"], + "physik": ["AG Vogel", "AG Halfm", "IKP"], + + # “Gesamt-summe” is computed as sum of all columns (not client-driven) + "gesamt_summe": [], +} + +class AbrechnungView(LoginRequiredMixin, TemplateView): + template_name = "abrechnung.html" + + # Columns EXACTLY from your screenshot + COLUMNS = [ + ("pkm_vogel", "PKM Vogel"), + ("iap_halfmann", "IAP Halfmann"), + ("ikp", "IKP"), + ("orgchem_thiele", "OrgChem Thiele"), + ("phychem_m3_buntkow", "PhyChem + M3 Buntkow"), + ("orgchem_fohrer", "OrgChem Fohrer"), + ("mawi_m3_gutfl", "MaWi + M3 Gutfl"), + ("mawi_alff", "MaWi Alff"), + ("chemie", "Chemie"), + ("mawi", "MaWi"), + ("physik", "Physik"), + ("gesamt_summe", "Gesamt-summe"), + ] + + # Rows EXACTLY from your screenshot (Excel row 11 is skipped) + ROWS = [ + ("bezogen_menge", "Bezogen. Menge", "LHe"), + ("kaltg_warmfuell", "Kaltg. Warmfüll.", "Anzahl"), + ("anzahl_15", "Anzahl", "LHe"), + ("he_verbrauch", "He-Verbrauch (LHe)", "LHe"), + ("umlage_instandhaltung_1", "1-Umlage Instandhaltung", "EUR"), + ("lhe_verluste", "LHe – Verluste", "LHe"), + ("umlage_heliumkosten_3", "3 Umlage Heliumkosten", "EUR"), + ("ghe_bezug", "GHe - Bezug", "m³"), + ("kosten_he_gas_bezug_4", "4-Kosten He-Gas-Bezug", "EUR"), + ("rel_anteil", "rel. Anteil", "Anteil"), + ("summe_anteile_1_4", "Summe Anteile 1-4", "EUR"), + ("sonstiges", "Sonstiges", "EUR"), + ("betrag", "Betrag", "EUR"), + ("umlage_personal_5", "5-Umlage Personal", "EUR"), + ("gutschriften", "Gutschriften", "EUR"), + ("rechnungsbetrag", "Rechnungsbetrag", "EUR"), + ("eff_lhe_preis", "eff. L-He-Preis", "EUR/L"), +] + + + # Editable rows = your yellow rows + EDITABLE_ROW_KEYS = {"ghe_bezug", "betrag", "gutschriften"} + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + + interval_year = self.request.session.get("halfyear_year") + interval_start = self.request.session.get("halfyear_start_month") + if not interval_year or not interval_start: + context["needs_interval"] = True + return context + + interval_year = int(interval_year) + interval_start = int(interval_start) + + qs = AbrechnungCell.objects.filter( + interval_year=interval_year, + interval_start_month=interval_start, + ) + value_map = {(c.row_key, c.col_key): c.value for c in qs} + def d(x): + """safe Decimal""" + if x in (None, ""): + return Decimal("0") + try: + return Decimal(str(x).replace(",", ".")) + except Exception: + return Decimal("0") + + def safe_div(a: Decimal, b: Decimal): + return (a / b) if b not in (None, 0, Decimal("0")) else Decimal("0") + + # ---- interval + window ---- + window = build_halfyear_window(interval_year, interval_start) + + # ---- BetriebskostenSummary values ---- + bs = BetriebskostenSummary.objects.first() + instandhaltung = d(bs.instandhaltung) if bs else Decimal("0") + heliumkosten = d(bs.heliumkosten) if bs else Decimal("0") + bezugskosten_gashe = d(bs.bezugskosten_gashe) if bs else Decimal("0") + umlage_personal_total = d(bs.umlage_personal) if bs else Decimal("0") + + # ---- helper: sum lhe_output in the 6-month window for a list of client names ---- + def sum_output_for_clients_exact(client_names): + total = Decimal("0") + if not client_names: + return total + + for (y, m) in window: + total += SecondTableEntry.objects.filter( + client__name__in=client_names, + date__year=y, + date__month=m, + ).aggregate( + total=Coalesce(Sum("lhe_output"), Value(0, output_field=DecimalField())) + )["total"] or Decimal("0") + + return total + # (NOTE: the date filter above is “wide”; if you prefer exact (y,m) matching, use the loop version below) + def sum_output_for_clients_exact(client_names): + total = Decimal("0") + if not client_names: + return total + for (y, m) in window: + total += SecondTableEntry.objects.filter( + client__name__in=client_names, + date__year=y, + date__month=m, + ).aggregate( + total=Coalesce(Sum("lhe_output"), Value(0, output_field=DecimalField())) + )["total"] or Decimal("0") + return total + + # Use exact to avoid edge cases + sum_output_for_clients = sum_output_for_clients_exact + + # ---- helper: warm fillings (from Halbjahresbilanz row “Füllungen warm”) ---- + # In your halfyear_balance logic, “Füllungen warm” is summed from MonthlySheet cells row_index=11 + # We replicate that part by reading stored top_left/top_right cells for each month. + def sum_fuellungen_warm(client_name): + total = Decimal("0") + for (y, m) in window: + sheet = MonthlySheet.objects.filter(year=y, month=m).first() + if not sheet: + continue + # warm row_index = 11 in both top_left and top_right tables in your existing code + # Try top_left first, then top_right + c = Cell.objects.filter(sheet=sheet, client__name=client_name, table_type="top_left", row_index=11).first() + if not c: + c = Cell.objects.filter(sheet=sheet, client__name=client_name, table_type="top_right", row_index=11).first() + total += d(c.value if c else None) + return total + + # ---- helper: verbraucherverluste (from Halbjahresbilanz) ---- + # In halfyear_balance view this is a *computed* metric, not directly stored. + # So we must either: + # A) replicate the whole halfyear_balance math (big), OR + # B) read it from stored cells *if you store it*. + # + # Right now your MonthlySheet stores the table rows, but “Verbraucherverluste” is row_index=15 + # in your debug mapping. If those cells are saved in DB (they usually are), we can sum row_index=15. + def sum_verbraucherverluste(client_name): + total = Decimal("0") + for (y, m) in window: + sheet = MonthlySheet.objects.filter(year=y, month=m).first() + if not sheet: + continue + # row_index=15 is “Verbraucherverluste” in your debug map + c = Cell.objects.filter(sheet=sheet, client__name=client_name, table_type="top_left", row_index=15).first() + if not c: + c = Cell.objects.filter(sheet=sheet, client__name=client_name, table_type="top_right", row_index=15).first() + total += d(c.value if c else None) + return total + + # ---------------------------------------------------------------------- + # Build computed row values per column + # ---------------------------------------------------------------------- + computed = {} # (row_key, col_key) -> Decimal OR special string + + # Gather base rows (bezug + warm fills) per column + bezogen = {} + warmfills = {} + + for col_key, _label in self.COLUMNS: + client_names = ABRECHNUNG_COL_CLIENTS.get(col_key, []) + bezogen[col_key] = sum_output_for_clients(client_names) + warmfills[col_key] = sum_fuellungen_warm(client_names[0]) if len(client_names) == 1 else sum(sum_fuellungen_warm(n) for n in client_names) + + # “Gesamt-summe” = sum of all “normal” columns (everything except itself) + def sum_all_cols(values_dict): + return sum(v for k, v in values_dict.items() if k != "gesamt_summe") + + bezogen["gesamt_summe"] = sum_all_cols(bezogen) + warmfills["gesamt_summe"] = sum_all_cols(warmfills) + + # ---- Row: Bezogen. Menge ---- + for col_key in bezogen: + computed[("bezogen_menge", col_key)] = bezogen[col_key] + + # ---- Row: Kaltg. Warmfüll. (á 15l L-He) ---- + for col_key in warmfills: + computed[("kaltg_warmfuell", col_key)] = warmfills[col_key] + + # ---- Row: Anzahl (15) = warmfills * 15 ---- + for col_key in warmfills: + computed[("anzahl_15", col_key)] = warmfills[col_key] * Decimal("15") + + # ---- Row: He-Verbrauch (LHe) = Bezogen + Anzahl_15 ---- + he_verbrauch = {} + for col_key in bezogen: + hv = bezogen[col_key] + (warmfills[col_key] * Decimal("15")) + he_verbrauch[col_key] = hv + computed[("he_verbrauch", col_key)] = hv + + # ---- Row: 1-Umlage Instandhaltung ---- + # Your rule (as you stated): + # - first 3 columns: instandhaltung * (He-Verbrauch of this column / He-Verbrauch of “Physik”) / 3 + # - next 3 columns: instandhaltung / 9 + # - next 2 columns: instandhaltung / 6 + # - next 2 columns: instandhaltung / 3 (if you later add more) + # - gesamt_summe: sum of first three columns (same row) + # + # We apply this to the current UI order. + first3 = ["pkm_vogel", "iap_halfmann", "ikp"] + next3 = ["orgchem_thiele", "phychem_m3_buntkow", "orgchem_fohrer"] + next2 = ["mawi_m3_gutfl", "mawi_alff"] + + physik_hv = he_verbrauch.get("physik", Decimal("0")) + + row1_vals = {} + for col_key in he_verbrauch: + if col_key in first3: + row1_vals[col_key] = instandhaltung * safe_div(he_verbrauch[col_key], physik_hv) / Decimal("3") + elif col_key in next3: + row1_vals[col_key] = instandhaltung / Decimal("9") + elif col_key in next2: + row1_vals[col_key] = instandhaltung / Decimal("6") + elif col_key == "gesamt_summe": + row1_vals[col_key] = sum(row1_vals.get(k, Decimal("0")) for k in first3) + else: + row1_vals[col_key] = Decimal("0") + + for col_key, v in row1_vals.items(): + computed[("umlage_instandhaltung_1", col_key)] = v + + # ---- Row: LHe – Verluste = Verbraucherverluste from Halbjahresbilanz (sum if combined) ---- + lhe_verluste = {} + for col_key, _label in self.COLUMNS: + if col_key in ("chemie", "mawi", "physik"): + # Special rules you stated: + if col_key == "chemie": + # Dr. Fohrer + AG Buntk. (NOT M3 here) + v = sum_verbraucherverluste("Dr. Fohrer") + sum_verbraucherverluste("AG Buntk.") + elif col_key == "mawi": + v = sum_verbraucherverluste("AG Alff") + sum_verbraucherverluste("AG Gutfl.") + else: # physik + v = sum_verbraucherverluste("M3 Thiele") + sum_verbraucherverluste("M3 Gutfleisch") + sum_verbraucherverluste("M3 Buntkowsky") + else: + client_names = ABRECHNUNG_COL_CLIENTS.get(col_key, []) + v = sum(sum_verbraucherverluste(n) for n in client_names) + + lhe_verluste[col_key] = v + computed[("lhe_verluste", col_key)] = v + + lhe_verluste["gesamt_summe"] = sum_all_cols(lhe_verluste) + computed[("lhe_verluste", "gesamt_summe")] = lhe_verluste["gesamt_summe"] + + # ---- Row: 3 Umlage Heliumkosten = heliumkosten * (LHe-Verluste col / LHe-Verluste gesamt_summe) ---- + for col_key in lhe_verluste: + computed[("umlage_heliumkosten_3", col_key)] = heliumkosten * safe_div(lhe_verluste[col_key], lhe_verluste["gesamt_summe"]) + + # ---- Row: 4-Kosten He-Gas-Bezug = Umlage Heliumkosten * Bezugskosten-GasHe ---- + for col_key in lhe_verluste: + computed[("kosten_he_gas_bezug_4", col_key)] = d(computed[("umlage_heliumkosten_3", col_key)]) * bezugskosten_gashe + + # ---- Row: Summe Anteile 1-4 = (1) + (3) + (4) ---- + summe_anteile = {} + for col_key in lhe_verluste: + v = ( + d(computed.get(("umlage_instandhaltung_1", col_key))) + + d(computed.get(("umlage_heliumkosten_3", col_key))) + + d(computed.get(("kosten_he_gas_bezug_4", col_key))) + ) + summe_anteile[col_key] = v + computed[("summe_anteile_1_4", col_key)] = v + + # ---- Row: rel. Anteil = summe_anteile col / summe_anteile gesamt ---- + for col_key in summe_anteile: + computed[("rel_anteil", col_key)] = safe_div(summe_anteile[col_key], summe_anteile.get("gesamt_summe", Decimal("0"))) + + # ---- Row: 5-Umlage Personal ---- + # Your rules: + # - first 3 clients => 0 + # - next columns up to (excluding Chemie): weighted over the 5 columns: + # OrgChem Thiele, PhyChem+M3 Buntkow, OrgChem Fohrer, MaWi+M3 Gutfl, MaWi Alff + # - for Chemie/MaWi/Physik: weighted over the 3 summary columns (Chemie, MaWi, Physik) + five_cols = ["orgchem_thiele", "phychem_m3_buntkow", "orgchem_fohrer", "mawi_m3_gutfl", "mawi_alff"] + sum5 = sum(he_verbrauch.get(k, Decimal("0")) for k in five_cols) + + sum3 = ( + he_verbrauch.get("chemie", Decimal("0")) + + he_verbrauch.get("mawi", Decimal("0")) + + he_verbrauch.get("physik", Decimal("0")) + ) + + for col_key in he_verbrauch: + if col_key in first3: + v = Decimal("0") + elif col_key in five_cols: + v = safe_div(he_verbrauch[col_key], sum5) * umlage_personal_total + elif col_key in ("chemie", "mawi", "physik"): + v = safe_div(he_verbrauch[col_key], sum3) * umlage_personal_total + elif col_key == "gesamt_summe": + v = sum(d(computed.get(("umlage_personal_5", k))) for k in he_verbrauch if k != "gesamt_summe") + else: + v = Decimal("0") + computed[("umlage_personal_5", col_key)] = v + + # ---- Row: Rechnungsbetrag = Summe Anteile 1-4 − Gutschriften + Betrag + 5-Umlage Personal ---- + for col_key in he_verbrauch: + gutsch = d(value_map.get(("gutschriften", col_key))) + betrag = d(value_map.get(("betrag", col_key))) + personal = d(computed.get(("umlage_personal_5", col_key))) + sa = d(computed.get(("summe_anteile_1_4", col_key))) + computed[("rechnungsbetrag", col_key)] = sa - gutsch + betrag + personal + + # ---- Row: eff. L-He-Preis = Rechnungsbetrag / He-Verbrauch ---- + for col_key in he_verbrauch: + rb = d(computed.get(("rechnungsbetrag", col_key))) + hv = he_verbrauch.get(col_key, Decimal("0")) + computed[("eff_lhe_preis", col_key)] = safe_div(rb, hv) + + # ---- Row: Sonstiges (TEXT) based on Betrag ---- + # IF(Betrag=0,"--------------",IF(Betrag<0,"Gutschrift","Nachzahlung")) + sonstiges_text = {} + for col_key in he_verbrauch: + b = d(value_map.get(("betrag", col_key))) + if b == 0: + sonstiges_text[col_key] = "--------------" + elif b < 0: + sonstiges_text[col_key] = "Gutschrift" + else: + sonstiges_text[col_key] = "Nachzahlung" + # overwrite display values for non-editable computed rows + non_editable_formula_rows = { + "bezogen_menge", + "kaltg_warmfuell", + "anzahl_15", + "he_verbrauch", + "umlage_instandhaltung_1", + "lhe_verluste", + "umlage_heliumkosten_3", + "kosten_he_gas_bezug_4", + "rel_anteil", + "summe_anteile_1_4", + "umlage_personal_5", + "rechnungsbetrag", + "eff_lhe_preis", + } + + for (rk, ck), val in computed.items(): + if rk in non_editable_formula_rows: + value_map[(rk, ck)] = val + rows = [] + for row_key, label, unit in self.ROWS: + rows.append({ + "row_key": row_key, + "label": label, + "unit": unit, # 👈 ADD THIS + "editable": row_key in self.EDITABLE_ROW_KEYS, + "cells": [ + { + "col_key": col_key, + "value": value_map.get((row_key, col_key)), + } + for col_key, _ in self.COLUMNS + ] + }) + start_year = interval_year + start_month = interval_start + # end month = start + 5 months + end_total = (start_month - 1) + 5 + end_year = start_year + (end_total // 12) + end_month = (end_total % 12) + 1 + + start_date_str = f"01.{start_month:02d}.{start_year}" + last_day = calendar.monthrange(end_year, end_month)[1] + end_date_str = f"{last_day:02d}.{end_month:02d}.{end_year}" + + context["interval_text"] = f"{start_date_str} bis {end_date_str}" + context["sonstiges_text"] = sonstiges_text + context.update({ + "page_title": "Abrechnung", + "columns": self.COLUMNS, + "rows": rows, + "interval_year": interval_year, + "interval_start": interval_start, + }) + + return context +@method_decorator(csrf_exempt, name="dispatch") +class SaveAbrechnungCellsView(LoginRequiredMixin, View): + def post(self, request): + interval_year = request.session.get("halfyear_year") + interval_start = request.session.get("halfyear_start_month") + if not interval_year or not interval_start: + return JsonResponse({"ok": False, "error": "No halfyear interval selected."}, status=400) + + interval_year = int(interval_year) + interval_start = int(interval_start) + + try: + payload = json.loads(request.body.decode("utf-8")) + except Exception: + return JsonResponse({"ok": False, "error": "Invalid JSON."}, status=400) + + changes = payload.get("changes", []) + + for item in changes: + row_key = item.get("row_key") + col_key = item.get("col_key") + raw_val = item.get("value") + + # allow empty -> NULL + if raw_val in ("", None): + val = None + else: + try: + val = Decimal(str(raw_val).replace(",", ".")) + except Exception: + continue + + AbrechnungCell.objects.update_or_create( + interval_year=interval_year, + interval_start_month=interval_start, + row_key=row_key, + col_key=col_key, + defaults={"value": val}, + ) + + return JsonResponse({"ok": True}) +def build_halfyear_window(interval_year: int, interval_start_month: int): + window = [] + for offset in range(6): + total_index = (interval_start_month - 1) + offset + y = interval_year + (total_index // 12) + m = (total_index % 12) + 1 + window.append((y, m)) + return window +class SaveCellsView(View): + + def calculate_bottom_3_dependents(self, sheet): + updated_cells = [] + + def get_cell(row_idx, col_idx): + return Cell.objects.filter( + sheet=sheet, + table_type='bottom_3', + row_index=row_idx, + column_index=col_idx + ).first() + + def set_cell(row_idx, col_idx, value): + cell, created = Cell.objects.get_or_create( + sheet=sheet, + table_type='bottom_3', + row_index=row_idx, + column_index=col_idx, + defaults={'value': value} + ) + 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 is not None else '', + 'is_calculated': True, + }) + + def dec(x): + if x in (None, ''): + return Decimal('0') + try: + return Decimal(str(x)) + except Exception: + return Decimal('0') + + # ---- current summary ---- + cur_sum = MonthlySummary.objects.filter(sheet=sheet).first() + curr_k44 = dec(cur_sum.gesamtbestand_neu_lhe) if cur_sum else Decimal('0') + total_verb = dec(cur_sum.verbraucherverlust_lhe) if cur_sum else Decimal('0') + + # ---- previous month summary ---- + year, month = sheet.year, sheet.month + if month == 1: + prev_year, prev_month = year - 1, 12 + else: + prev_year, prev_month = year, month - 1 + + prev_sheet = MonthlySheet.objects.filter(year=prev_year, month=prev_month).first() + if prev_sheet: + prev_sum = MonthlySummary.objects.filter(sheet=prev_sheet).first() + prev_k44 = dec(prev_sum.gesamtbestand_neu_lhe) if prev_sum else Decimal('0') + else: + prev_k44 = Decimal('0') + + # ---- read editable inputs from bottom_3: F47,G47,I47,I50 ---- + def get_val(r, c): + cell = get_cell(r, c) + return dec(cell.value if cell else None) + + f47 = get_val(1, 0) + g47 = get_val(1, 1) + i47 = get_val(1, 2) + i50 = get_val(4, 2) + + # Now apply your formulas using prev_k44, curr_k44, total_verb, f47,g47,i47,i50 + # Row indices: 0..7 correspond to 46..53 + # col 3 = J, col 4 = K + + # Row 46 + k46 = prev_k44 + j46 = k46 * Decimal('0.75') + set_cell(0, 3, j46) + set_cell(0, 4, k46) + + # Row 47 + g47 = self._dec((get_cell(1, 1) or {}).value if get_cell(1, 1) else None) + i47 = self._dec((get_cell(1, 2) or {}).value if get_cell(1, 2) else None) + + j47 = g47 + i47 + k47 = (j47 / Decimal('0.75')) + g47 if j47 != 0 else g47 + + set_cell(1, 3, j47) + set_cell(1, 4, k47) + + # Row 48 + k48 = k46 + k47 + j48 = k48 * Decimal('0.75') + set_cell(2, 3, j48) + set_cell(2, 4, k48) + + # Row 49 + k49 = curr_k44 + j49 = k49 * Decimal('0.75') + set_cell(3, 3, j49) + set_cell(3, 4, k49) + + # Row 50 + j50 = i50 + k50 = j50 / Decimal('0.75') if j50 != 0 else Decimal('0') + set_cell(4, 3, j50) + set_cell(4, 4, k50) + + # Row 51 + k51 = k48 - k49 - k50 + j51 = k51 * Decimal('0.75') + set_cell(5, 3, j51) + set_cell(5, 4, k51) + + # Row 52 + k52 = total_verb + j52 = k52 * Decimal('0.75') + set_cell(6, 3, j52) + set_cell(6, 4, k52) + + # Row 53 + j53 = j51 - j52 + k53 = k51 - k52 + set_cell(7, 3, j53) + set_cell(7, 4, k53) + + return updated_cells + + + def _dec(self, value): + """Convert value to Decimal or return 0.""" + if value is None or value == '': + return Decimal('0') + try: + return Decimal(str(value)) + except Exception: + return Decimal('0') + + 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) + ) + elif cell.table_type == 'bottom_1': + updated_cells.extend(self.calculate_bottom_1_dependents(sheet, cell)) + elif cell.table_type == 'bottom_3': + updated_cells.extend( + self.calculate_bottom_3_dependents(sheet) + ) + # bottom_1 / bottom_2 / bottom_3 currently have no formulas: + # they just save the new value. + updated_cells += self.calculate_bottom_3_dependents(sheet) + 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_bottom_1_dependents(self, sheet, changed_cell): + """ + Recalculate Bottom Table 1 (table_type='bottom_1'). + + Layout (row_index 0–9, col_index 0–4): + + Rows 0–8: + 0: Batterie 1 + 1: 2 + 2: 3 + 3: 4 + 4: 5 + 5: 6 + 6: 2 Bündel + 7: 2 Ballone + 8: Reingasspeicher + + Row 9: + 9: Gasbestand (totals row) + + Columns: + 0: Volumen (fixed values, not editable) + 1: bar (editable for rows 0–8) + 2: korrigiert = bar / (bar/2000 + 1) + 3: Nm³ = Volumen * korrigiert + 4: Lit. LHe = Nm³ / 0.75 + + Row 9: + - Volumen (col 0): empty + - bar (col 1): empty + - korrigiert (col 2): empty + - Nm³ (col 3): SUM Nm³ rows 0–8 + - Lit. LHe (col 4): SUM Lit. LHe rows 0–8 + """ + from decimal import Decimal, InvalidOperation + from .models import Cell + + updated_cells = [] + + DATA_ROWS = list(range(0, 9)) # 0–8 + TOTAL_ROW = 9 + + COL_VOL = 0 + COL_BAR = 1 + COL_KORR = 2 + COL_NM3 = 3 + COL_LHE = 4 + + # Fixed Volumen values for rows 0–8 + VOLUMES = [ + Decimal("2.4"), # row 0 + Decimal("5.1"), # row 1 + Decimal("4.0"), # row 2 + Decimal("1.0"), # row 3 + Decimal("4.0"), # row 4 + Decimal("0.4"), # row 5 + Decimal("1.2"), # row 6 + Decimal("20.0"), # row 7 + Decimal("5.0"), # row 8 + ] + + def get_cell(row_idx, col_idx): + return Cell.objects.filter( + sheet=sheet, + table_type="bottom_1", + row_index=row_idx, + column_index=col_idx, + ).first() + + def set_calc(row_idx, col_idx, value): + """ + Set a calculated cell and add it to updated_cells. + If value is None, we clear the cell. + """ + cell = get_cell(row_idx, col_idx) + if not cell: + cell = Cell( + sheet=sheet, + table_type="bottom_1", + row_index=row_idx, + column_index=col_idx, + value=value, + ) + else: + cell.value = value + cell.save() + + updated_cells.append( + { + "id": cell.id, + "value": "" if cell.value is None else str(cell.value), + "is_calculated": True, + } + ) + + # ---------- Rows 0–8: per-gasspeicher calculations ---------- + for row_idx in DATA_ROWS: + bar_cell = get_cell(row_idx, COL_BAR) + + # Volumen: fixed constant or, if present, value from DB + vol = VOLUMES[row_idx] + vol_cell = get_cell(row_idx, COL_VOL) + if vol_cell and vol_cell.value is not None: + try: + vol = Decimal(str(vol_cell.value)) + except (InvalidOperation, ValueError): + # fall back to fixed constant + vol = VOLUMES[row_idx] + + bar = None + try: + if bar_cell and bar_cell.value is not None: + bar = Decimal(str(bar_cell.value)) + except (InvalidOperation, ValueError): + bar = None + + # korrigiert = bar / (bar/2000 + 1) + if bar is not None and bar != 0: + korr = bar / (bar / Decimal("2000") + Decimal("1")) + else: + korr = None + + # Nm³ = Volumen * korrigiert + if korr is not None: + nm3 = vol * korr + else: + nm3 = None + + # Lit. LHe = Nm³ / 0.75 + if nm3 is not None: + lit_lhe = nm3 / Decimal("0.75") + else: + lit_lhe = None + + # Write calculated cells back (NOT Volumen or bar) + set_calc(row_idx, COL_KORR, korr) + set_calc(row_idx, COL_NM3, nm3) + set_calc(row_idx, COL_LHE, lit_lhe) + + # ---------- Row 9: totals (Gasbestand) ---------- + total_nm3 = Decimal("0") + total_lhe = Decimal("0") + has_nm3 = False + has_lhe = False + + for row_idx in DATA_ROWS: + nm3_cell = get_cell(row_idx, COL_NM3) + if nm3_cell and nm3_cell.value is not None: + try: + total_nm3 += Decimal(str(nm3_cell.value)) + has_nm3 = True + except (InvalidOperation, ValueError): + pass + + lhe_cell = get_cell(row_idx, COL_LHE) + if lhe_cell and lhe_cell.value is not None: + try: + total_lhe += Decimal(str(lhe_cell.value)) + has_lhe = True + except (InvalidOperation, ValueError): + pass + + # Volumen (0), bar (1), korrigiert (2) on total row stay empty + set_calc(TOTAL_ROW, COL_KORR, None) # explicitly clear korrigiert + set_calc(TOTAL_ROW, COL_NM3, total_nm3 if has_nm3 else None) + set_calc(TOTAL_ROW, COL_LHE, total_lhe if has_lhe else None) + + 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 Exception: + 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 '' + }) + # bottom_3 has no client, so this will just add None for those cells, + # which is harmless. Top-left cells still add their real client_id. + changed_clients.add(cell.client_id) + + except Cell.DoesNotExist: + continue + + # Recalculate for each changed client (top-left tables) + for client_id in changed_clients: + if client_id is not None: + self.recalculate_top_left_table(sheet, client_id) + + # --- NEW: recalc bottom_3 for the whole sheet, independent of clients --- + bottom3_updates = self.calculate_bottom_3_dependents(sheet) + + # Get all updated cells for response (top-left) + all_updated_cells = [] + for client_id in changed_clients: + if client_id is None: + continue # skip bottom_3 / non-client cells + 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 + }) + + # Add bottom_3 recalculated cells (J46..K53, etc.) + all_updated_cells.extend(bottom3_updates) + + 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() +@method_decorator(csrf_exempt, name='dispatch') +class SaveMonthSummaryView(View): + """ + Saves per-month summary values such as K44 (Gesamtbestand neu). + Called from JS after 'Save All' finishes. + """ + + def post(self, request, *args, **kwargs): + try: + data = json.loads(request.body.decode('utf-8')) + except json.JSONDecodeError: + return JsonResponse( + {'success': False, 'message': 'Invalid JSON'}, + status=400 + ) + + sheet_id = data.get('sheet_id') + if not sheet_id: + return JsonResponse( + {'success': False, 'message': 'Missing sheet_id'}, + status=400 + ) + + try: + sheet = MonthlySheet.objects.get(id=sheet_id) + except MonthlySheet.DoesNotExist: + return JsonResponse( + {'success': False, 'message': 'Sheet not found'}, + status=404 + ) + + # More tolerant decimal conversion: accepts "123.45" and "123,45" + def to_decimal(value): + if value is None: + return None + s = str(value).strip() + if s == '': + return None + s = s.replace(',', '.') + try: + return Decimal(s) + except (InvalidOperation, ValueError): + # Debug: show what failed in the dev server console + print("SaveMonthSummaryView to_decimal failed for:", repr(value)) + return None + + raw_k44 = data.get('gesamtbestand_neu_lhe') + raw_gas = data.get('gasbestand_lhe') + raw_verb = data.get('verbraucherverlust_lhe') + + gesamtbestand_neu_lhe = to_decimal(raw_k44) + gasbestand_lhe = to_decimal(raw_gas) + verbraucherverlust_lhe = to_decimal(raw_verb) + + summary, created = MonthlySummary.objects.get_or_create(sheet=sheet) + + if gesamtbestand_neu_lhe is not None: + summary.gesamtbestand_neu_lhe = gesamtbestand_neu_lhe + if gasbestand_lhe is not None: + summary.gasbestand_lhe = gasbestand_lhe + if verbraucherverlust_lhe is not None: + summary.verbraucherverlust_lhe = verbraucherverlust_lhe + + summary.save() + + # Small debug output so you can see in the server console what was saved + print( + f"Saved MonthlySummary for {sheet.year}-{sheet.month:02d}: " + f"K44={summary.gesamtbestand_neu_lhe}, " + f"Gasbestand={summary.gasbestand_lhe}, " + f"Verbraucherverlust={summary.verbraucherverlust_lhe}" + ) + + return JsonResponse({'success': True}) + + + +# 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): + # --- Clients for the yearly summary table --- + clients = Client.objects.all() + + # --- Available years for output data (same as before) --- + available_years_qs = SecondTableEntry.objects.dates('date', 'year', order='DESC') + available_years = [y.year for y in available_years_qs] + + # === 1) Year used for the "Helium Output Yearly Summary" table === + # Uses ?year=... in the dropdown + year_param = request.GET.get('year') + if year_param: + selected_year = int(year_param) + else: + selected_year = available_years[0] if available_years else datetime.now().year + + # === 2) GLOBAL half-year interval (shared with other pages) ======= + # Try GET params first + interval_year_param = request.GET.get('interval_year') + start_month_param = request.GET.get('interval_start_month') + + # Fallbacks from session + session_year = request.session.get('halfyear_year') + session_start_month = request.session.get('halfyear_start_month') + + # Determine final interval_year + if interval_year_param: + interval_year = int(interval_year_param) + elif session_year: + interval_year = int(session_year) + else: + # default: same as selected_year for summary + interval_year = selected_year + + # Determine final interval_start_month + if start_month_param: + interval_start_month = int(start_month_param) + elif session_start_month: + interval_start_month = int(session_start_month) + else: + interval_start_month = 1 # default Jan + + # Store back into the session so other views can read them + request.session['halfyear_year'] = interval_year + request.session['halfyear_start_month'] = interval_start_month + + # === 3) Build a 6-month window, allowing wrap into the next year === + # Example: interval_year=2025, interval_start_month=12 + # window = [(2025,12), (2026,1), (2026,2), (2026,3), (2026,4), (2026,5)] + window = [] + for offset in range(6): + total_index = (interval_start_month - 1) + offset # 0-based index + y = interval_year + (total_index // 12) + m = (total_index % 12) + 1 + window.append((y, m)) + + # === 4) Totals per client in that 6-month window ================== + monthly_data = [] + for client in clients: + monthly_totals = [] + for (y, m) in window: + total = SecondTableEntry.objects.filter( + client=client, + date__year=y, + date__month=m + ).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), + }) + + # === 5) Month labels for the header (only the 6-month window) ===== + month_labels = [calendar.month_abbr[m] for (y, m) in window] + + # === 6) FINALLY: return the response ============================== + return render(request, 'clients_table.html', { + 'available_years': available_years, + 'current_year': selected_year, # used by year dropdown + 'interval_year': interval_year, # used by "Global 6-Month Interval" form + 'interval_start_month': interval_start_month, + 'months': month_labels, + 'monthly_data': monthly_data, + }) + + # === 5) Month labels for the header (only the 6-month window) ===== + month_labels = [calendar.month_abbr[m] for (y, m) in window] + + +def set_halfyear_interval(request): + if request.method == 'POST': + year = int(request.POST.get('year')) + start_month = int(request.POST.get('start_month')) + + request.session['halfyear_year'] = year + request.session['halfyear_start_month'] = start_month + + return redirect(request.META.get('HTTP_REFERER', 'clients_list')) + + return redirect('clients_list') +# Table One View (ExcelEntry) +def table_one_view(request): + from .models import ExcelEntry, Client, Institute + + # --- Base queryset for the main Helium Input table --- + base_entries = ExcelEntry.objects.all().select_related('client', 'client__institute') + + # Read the global 6-month interval from the session + interval_year = request.session.get('halfyear_year') + interval_start = request.session.get('halfyear_start_month') + + if interval_year and interval_start: + interval_year = int(interval_year) + interval_start = int(interval_start) + + # Build the same 6-month window as on the main page (can cross year) + window = [] + for offset in range(6): + total_index = (interval_start - 1) + offset # 0-based + y = interval_year + (total_index // 12) + m = (total_index % 12) + 1 + window.append((y, m)) + + # Build Q filter: (year=m_year AND month=m_month) for any of those 6 + q = Q() + for (y, m) in window: + q |= Q(date__year=y, date__month=m) + + entries_table1 = base_entries.filter(q).order_by('-date') + else: + # Fallback: if no global interval yet, show everything + entries_table1 = base_entries.order_by('-date') + 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 + # default year/start month (if no global interval yet) + if available_years: + default_year = available_years[0] # newest year in ExcelEntry + else: + default_year = timezone.now().year + + # 🔸 Read global half-year interval from session (set on main page) + session_year = request.session.get('halfyear_year') + session_start = request.session.get('halfyear_start_month') + + # If the user has set a global interval, use it. + # Otherwise fall back to default year / January. + year = int(session_year) if session_year else int(default_year) + start_month = int(session_start) if session_start else 1 + + + # six-month window + # --- Build a 6-month window, allowing wrap into the next year --- + # Example: year=2025, start_month=10 + # window = [(2025,10), (2025,11), (2025,12), (2026,1), (2026,2), (2026,3)] + window = [] + for offset in range(6): + total_index = (start_month - 1) + offset # 0-based + y_for_month = year + (total_index // 12) + m_for_month = (total_index % 12) + 1 + window.append((y_for_month, m_for_month)) + + overview = None + + if window: + # 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 (y_m, m_m) in window: + total = ExcelEntry.objects.filter( + client__in=clients_qs, + date__year=y_m, + date__month=m_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(window)): + 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, (y_m, m_m) in enumerate(window): + row_values = [g['values'][idx] for g in groups_entries] + rows.append({ + 'month_number': m_m, + 'month_label': calendar.month_name[m_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] + + # Start/end for display – include years so wrap is clear + start_year = window[0][0] + start_month_disp = window[0][1] + end_year = window[-1][0] + end_month_disp = window[-1][1] + + overview = { + 'year': year, # keep for backwards compatibility if needed + 'start_month': start_month_disp, + 'start_year': start_year, + 'end_month': end_month_disp, + 'end_year': end_year, + '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: + clients = Client.objects.all().select_related('institute') + institutes = Institute.objects.all() + + # 🔸 Read global half-year interval from session + interval_year = request.session.get('halfyear_year') + interval_start = request.session.get('halfyear_start_month') + + if interval_year and interval_start: + interval_year = int(interval_year) + interval_start = int(interval_start) + + # Build the same 6-month window as in clients_list (can cross years) + window = [] + for offset in range(6): + total_index = (interval_start - 1) + offset # 0-based + y = interval_year + (total_index // 12) + m = (total_index % 12) + 1 + window.append((y, m)) + + # Build a Q object matching any of those (year, month) pairs + q = Q() + for (y, m) in window: + q |= Q(date__year=y, date__month=m) + + entries = SecondTableEntry.objects.filter(q).order_by('-date') + else: + # Fallback if no global interval yet: show all + entries = SecondTableEntry.objects.all().order_by('-date') + + return render(request, 'table_two.html', { + 'entries_table2': entries, + 'clients': clients, + 'institutes': institutes, + 'interval_year': interval_year, + 'interval_start_month': interval_start, + }) + + except Exception as e: + return render(request, 'table_two.html', { + 'error_message': f"Failed to load data: {str(e)}", + 'entries_table2': [], + 'clients': clients, + 'institutes': institutes, + }) + + +def monthly_sheet_root(request): + """ + Redirect /sheet/ to the sheet matching the globally selected + half-year start (year + month). If not set, fall back to latest. + """ + year = request.session.get('halfyear_year') + start_month = request.session.get('halfyear_start_month') + + if year and start_month: + try: + year = int(year) + start_month = int(start_month) + return redirect('monthly_sheet', year=year, month=start_month) + except ValueError: + pass # fall through + + # Fallback: latest MonthlySheet if exists + latest_sheet = MonthlySheet.objects.order_by('-year', '-month').first() + if latest_sheet: + return redirect('monthly_sheet', year=latest_sheet.year, month=latest_sheet.month) + else: + now = timezone.now() + return redirect('monthly_sheet', year=now.year, month=now.month) +# 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') + + summary = get_summary() + summary.recalculate() + + context = { + 'items': items, + 'summary': summary, + } + return render(request, 'betriebskosten_list.html', context) + +def betriebskosten_create(request): + if request.method != 'POST': + return JsonResponse({'status': 'error', 'message': 'Invalid request method'}) + + try: + entry_id = request.POST.get('id') + if entry_id: + entry = Betriebskosten.objects.get(id=entry_id) + else: + entry = Betriebskosten() + gegenstand = request.POST.get("gegenstand", "").strip() + buchungsdatum_str = request.POST.get('buchungsdatum') + rechnungsnummer = request.POST.get('rechnungsnummer') # UI label: Firma + kostentyp = request.POST.get('kostentyp') + betrag_raw = request.POST.get('betrag') + beschreibung = request.POST.get('beschreibung') or '' + gas_volume_raw = request.POST.get('gas_volume') # UI label: Gasvolumen (m³) + + # Validate required fields + if not all([buchungsdatum_str, rechnungsnummer, kostentyp, betrag_raw]): + return JsonResponse({'status': 'error', 'message': 'All required fields must be filled'}) + + allowed = {'sach', 'helium'} + if kostentyp not in allowed: + return JsonResponse({'status': 'error', 'message': 'Invalid Kostentyp'}) + + # Date + buchungsdatum = parse_date(buchungsdatum_str) + if not buchungsdatum: + return JsonResponse({'status': 'error', 'message': 'Invalid date format'}) + + # Betrag + try: + betrag = Decimal(str(betrag_raw)) + except Exception: + return JsonResponse({'status': 'error', 'message': 'Invalid Betrag'}) + + # Gas volume (m³) only for helium + gas_m3 = None + if kostentyp == 'helium': + if gas_volume_raw not in (None, '', 'None'): + try: + gas_m3 = Decimal(str(gas_volume_raw)) + except Exception: + return JsonResponse({'status': 'error', 'message': 'Invalid Gasvolumen (m³)'}) + entry.gegenstand = gegenstand + entry.buchungsdatum = buchungsdatum + entry.rechnungsnummer = rechnungsnummer + entry.kostentyp = kostentyp + entry.betrag = betrag + entry.beschreibung = beschreibung + entry.gas_volume = gas_m3 + + entry.save() + summary = get_summary() + summary.recalculate() + + # Computed values for UI (read-only fields) + gas_liter = None + price_per_m3 = None + price_per_liter = None + + if entry.kostentyp == 'helium' and entry.gas_volume: + # Liter = m³ / 0.75 + gas_liter = (Decimal(entry.gas_volume) / Decimal('0.75')) + if entry.gas_volume != 0: + price_per_m3 = (Decimal(entry.betrag) / Decimal(entry.gas_volume)) + if gas_liter != 0: + price_per_liter = (Decimal(entry.betrag) / gas_liter) + + return JsonResponse({ + 'status': 'success', + 'id': entry.id, + 'gegenstand': entry.gegenstand, + 'buchungsdatum': entry.buchungsdatum.strftime('%Y-%m-%d'), + 'rechnungsnummer': entry.rechnungsnummer, + 'kostentyp': entry.kostentyp, + 'kostentyp_display': entry.get_kostentyp_display(), + 'gas_volume_m3': str(entry.gas_volume) if entry.gas_volume is not None else '-', + 'gas_volume_liter': str(gas_liter.quantize(Decimal("0.01"))) if gas_liter is not None else '', + 'price_per_m3': str(price_per_m3.quantize(Decimal("0.01"))) if price_per_m3 is not None else '', + 'price_per_liter': str(price_per_liter.quantize(Decimal("0.01"))) if price_per_liter is not None else '', + 'betrag': str(entry.betrag), + 'beschreibung': entry.beschreibung or '', + }) + + except Betriebskosten.DoesNotExist: + return JsonResponse({'status': 'error', 'message': 'Entry not found'}, status=404) + except Exception as e: + return JsonResponse({'status': 'error', 'message': str(e)}) + + +def betriebskosten_delete(request): + if request.method == 'POST': + entry_id = request.POST.get('id') + + if not entry_id or not entry_id.isdigit(): + return JsonResponse({ + 'status': 'error', + 'message': 'Invalid ID' + }) + + try: + entry = Betriebskosten.objects.get(id=int(entry_id)) + entry.delete() + + summary = get_summary() + summary.recalculate() + + return JsonResponse({'status': 'success'}) + + except Betriebskosten.DoesNotExist: + return JsonResponse({ + 'status': 'error', + 'message': 'Entry not found' + }) + + return JsonResponse({'status': 'error'}) + + +def get_summary(): + summary, created = BetriebskostenSummary.objects.get_or_create(id=1) + return summary + +from django.http import JsonResponse + +def update_personalkosten(request): + if request.method == "POST": + value = request.POST.get("personalkosten") + summary = get_summary() + summary.personalkosten = Decimal(value or "0") + summary.recalculate() + return JsonResponse({ + "status": "success", + "umlage_personal": str(summary.umlage_personal), + }) + return JsonResponse({"status": "error"}) + + +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'}) + +def halfyear_settings(request): + """ + Global settings page: choose a year + first month for the 6-month interval. + These values are stored in the session and used by other views. + """ + # Determine available years from your data (use ExcelEntry or SecondTableEntry) + # Here I use SecondTableEntry; you can switch to ExcelEntry if you prefer. + available_years_qs = SecondTableEntry.objects.dates('date', 'year', order='DESC') + available_years = [d.year for d in available_years_qs] + + if not available_years: + available_years = [timezone.now().year] + + # Defaults (if nothing in session yet) + default_year = request.session.get('halfyear_year', available_years[0]) + default_start_month = request.session.get('halfyear_start_month', 1) + + if request.method == 'POST': + year = int(request.POST.get('year', default_year)) + start_month = int(request.POST.get('start_month', default_start_month)) + + request.session['halfyear_year'] = year + request.session['halfyear_start_month'] = start_month + + # Redirect back to where user came from, or to this page again + next_url = request.POST.get('next') or request.GET.get('next') or 'halfyear_settings' + return redirect(next_url) + + # Month choices for the dropdown + month_choices = [(i, calendar.month_name[i]) for i in range(1, 13)] + + context = { + 'available_years': available_years, + 'selected_year': int(default_year), + 'selected_start_month': int(default_start_month), + 'month_choices': month_choices, + } + return render(request, 'halfyear_settings.html', context) \ No newline at end of file diff --git a/sheets/old/halfyear_balance.html b/sheets/old/halfyear_balance.html new file mode 100644 index 0000000..1a429db --- /dev/null +++ b/sheets/old/halfyear_balance.html @@ -0,0 +1,353 @@ +{% extends "base.html" %} + +{% block content %} +

+ + + +
+ ← Helium Output Übersicht +

+ {% with first=window.0 last=window|last %} + Halbjahres-Bilanz ({{ first.1 }}/{{ first.0 }} – {{ last.1 }}/{{ last.0 }}) + {% endwith %} +

+ {% with first=window.0 %} + Monatsblätter + {% endwith %} +
+ +
+ +
+

Top Left – Halbjahresbilanz

+ + + + + {% for c in clients_left %} + + {% endfor %} + + + + + {% for row in rows_left %} + + + {% for v in row.values %} + + {% endfor %} + + + {% endfor %} + +
Bezeichnung{{ c }}Σ
{{ row.label }} + {% if row.is_percent and v is not None %} + {{ v|floatformat:4 }} + {% elif v is not None %} + {{ v|floatformat:2 }} + {% endif %} + + {% if row.is_percent and row.total %} + {{ row.total|floatformat:4 }} + {% elif row.total is not None %} + {{ row.total|floatformat:2 }} + {% endif %} +
+
+ + +
+

Top Right – Halbjahresbilanz

+ + + + + {% for c in clients_right %} + + {% endfor %} + + + + + {% for row in rows_right %} + + + {% for v in row.values %} + + {% endfor %} + + + {% endfor %} + +
Bezeichnung{{ c }}Σ
{{ row.label }} + {% if row.is_text_row %} + {{ v }} + {% elif row.is_percent and v is not None %} + {{ v|floatformat:4 }} + {% elif v is not None %} + {{ v|floatformat:2 }} + {% endif %} + + {% if row.is_text_row %} + {{ row.total }} + {% elif row.is_percent and row.total %} + {{ row.total|floatformat:4 }} + {% elif row.total is not None %} + {{ row.total|floatformat:2 }} + {% endif %} +
+
+
+

Summe

+ + + + + + + + + + + + + {% for r in rows_sum %} + + + + + + + + + + + + + + {% endfor %} + +
BezeichnungΣLicht-wieseChemieMaWiM3
{{ r.label }} + {% if r.is_percent %} + {{ r.total|floatformat:2 }}% + {% else %} + {{ r.total|floatformat:2 }} + {% endif %} + + {% if r.is_percent %} + {{ r.lichtwiese|floatformat:2 }}% + {% else %} + {{ r.lichtwiese|floatformat:2 }} + {% endif %} + + {% if r.is_percent %} + {{ r.chemie|floatformat:2 }}% + {% else %} + {{ r.chemie|floatformat:2 }} + {% endif %} + + {% if r.is_percent %} + {{ r.mawi|floatformat:2 }}% + {% else %} + {{ r.mawi|floatformat:2 }} + {% endif %} + + {% if r.is_percent %} + {{ r.m3|floatformat:2 }}% + {% else %} + {{ r.m3|floatformat:2 }} + {% endif %} +
+
+
+

Bottom Table 1 – Bilanz (read-only)

+ + + + + + + + + + + + + + + {% for r in bottom1_rows %} + + + + + + + + + {% endfor %} + +
GasspeicherVolumenbarkorrigiertNm³Lit. LHe
{{ r.label }}{{ r.volume|floatformat:1 }}{{ r.bar|floatformat:0 }}{{ r.korr|floatformat:1 }}{{ r.nm3|floatformat:0 }}{{ r.lhe|floatformat:0 }}
+
+

Bottom Table 2 – Verbraucherbestand L-He (read-only)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Verbraucherbestand{{ bottom2.j38|floatformat:2 }}{{ bottom2.k38|floatformat:2 }}
+ Anlage:Gefäss 2,5:{{ bottom2.g39|floatformat:2 }}Gefäss 1,0:{{ bottom2.i39|floatformat:2 }}{{ bottom2.j39|floatformat:2 }}{{ bottom2.k39|floatformat:2 }}
+ Kaltgas:Gefäss 2,5:{{ bottom2.g40|default_if_none:""|floatformat:2 }}Gefäss 1,0:{{ bottom2.i40|default_if_none:""|floatformat:2 }}{{ bottom2.j40|floatformat:2 }}{{ bottom2.k40|floatformat:2 }}
Bestand flüssig He{{ bottom2.j43|floatformat:2 }}{{ bottom2.k43|floatformat:2 }}
Gesamtbestand neu:{{ bottom2.j44|floatformat:2 }}{{ bottom2.k44|floatformat:2 }}
+
+ +
+ + +
+
{# closes .spreadsheet-container #} +{% endblock %} diff --git a/sheets/old/views.py b/sheets/old/views.py new file mode 100644 index 0000000..b40f726 --- /dev/null +++ b/sheets/old/views.py @@ -0,0 +1,4401 @@ +from django.shortcuts import render, redirect +from django.utils.decorators import method_decorator +from django.views.decorators.csrf import csrf_exempt +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, MonthlySummary +) +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 +import json +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 build_halfyear_window(interval_year: int, start_month: int): + """ + Build a list of (year, month) for the 6-month interval, possibly crossing into the next year. + Example: (2025, 10) -> [(2025,10), (2025,11), (2025,12), (2026,1), (2026,2), (2026,3)] + """ + window = [] + for offset in range(6): + total_index = (start_month - 1) + offset # 0-based + y = interval_year + (total_index // 12) + m = (total_index % 12) + 1 + window.append((y, m)) + return window +# --------------------------------------------------------------------------- +# Halbjahres-Bilanz helpers +# --------------------------------------------------------------------------- + +# You can adjust these indices if needed. +# Assuming: +# - bottom_1.table has row "Gasbestand" at some fixed row index, +# and columns: ... Nm³, Lit. LHe +GASBESTAND_ROW_INDEX = 9 # <-- adjust if your bottom_1 has a different row index +GASBESTAND_COL_NM3 = 3 # <-- adjust to the column index for Nm³ in bottom_1 + +# In top_left / top_right, "Bestand in Kannen-1 (Lit. L-He)" is row_index 5 +BESTAND_KANNEN_ROW_INDEX = 5 + +def pick_sheet_by_gasbestand(window, sheets_by_ym, prev_sheet): + """ + Returns the last sheet in the window whose Gasbestand (J36, Nm³ column) != 0. + If none found, returns prev_sheet (Übertrag_Dez__Vorjahr equivalent). + """ + for (y, m) in reversed(window): + sheet = sheets_by_ym.get((y, m)) + if not sheet: + continue + gasbestand_nm3 = get_bottom1_value(sheet, GASBESTAND_ROW_INDEX, GASBESTAND_COL_NM3) + if gasbestand_nm3 != 0: + return sheet + return prev_sheet +def get_bottom1_value(sheet, row_index: int, col_index: int) -> Decimal: + """Get a numeric value from bottom_1, or 0 if missing.""" + if sheet is None: + return Decimal('0') + + cell = Cell.objects.filter( + sheet=sheet, + table_type='bottom_1', + row_index=row_index, + column_index=col_index, + ).first() + + if cell is None or cell.value in (None, ''): + return Decimal('0') + + try: + return Decimal(str(cell.value)) + except Exception: + return Decimal('0') + +# MUST match the column order in your monthly_sheets top-right table + + + +def get_top_right_value(sheet, client_name: str, row_index: int) -> Decimal: + """ + Read a numeric value from the top_right table of a MonthlySheet for + a given client (by column) and row_index. + + top_right cells are keyed by (sheet, table_type='top_right', + row_index, column_index), where column_index is the position of the + client in HALFYEAR_RIGHT_CLIENTS. + """ + if sheet is None: + return Decimal('0') + + col_index = RIGHT_CLIENT_INDEX.get(client_name) + if col_index is None: + return Decimal('0') + + cell = Cell.objects.filter( + sheet=sheet, + table_type='top_right', + row_index=row_index, + column_index=col_index, + ).first() + + if cell is None or cell.value in (None, ''): + return Decimal('0') + + try: + return Decimal(str(cell.value)) + except Exception: + return Decimal('0') + +TR_RUECKF_FLUESSIG_ROW = 2 # confirmed by your March value 172.840560 +TR_BESTAND_KANNEN_ROW = 5 # confirmed by your earlier query +def get_bestand_kannen_for_month(sheet, client_name: str) -> Decimal: + """ + 'B9' in your description: Bestand in Kannen-1 (Lit. L-He) + For this implementation we take it from top_left row_index = 5 for that client. + """ + return get_top_left_value(sheet, client_name, row_index=BESTAND_KANNEN_ROW_INDEX) + +from decimal import Decimal +from django.db.models import Sum +from django.db.models.functions import Coalesce +from django.db.models import DecimalField, Value + +from .models import MonthlySheet, SecondTableEntry, Client, Cell +from django.shortcuts import redirect, render + +# You already have HALFYEAR_CLIENTS for the left table (AG Vogel, AG Halfm, IKP) +HALFYEAR_CLIENTS = ["AG Vogel", "AG Halfm", "IKP"] + +# NEW: clients for the top-right half-year table +HALFYEAR_RIGHT_CLIENTS = [ + "Dr. Fohrer", + "AG Buntk.", + "AG Alff", + "AG Gutfl.", + "M3 Thiele", + "M3 Buntkowsky", + "M3 Gutfleisch", +] +BOTTOM1_COL_VOLUME = 0 +BOTTOM1_COL_BAR = 1 +BOTTOM1_COL_KORR = 2 +BOTTOM1_COL_NM3 = 3 +BOTTOM1_COL_LHE = 4 +BOTTOM2_ROW_ANLAGE = 0 +BOTTOM2_COL_G39 = 0 # "Gefäss 2,5" (cell id shows column_index=0) +BOTTOM2_COL_I39 = 1 # "Gefäss 1,0" (cell id shows column_index=1) +BOTTOM2_ROW_INPUTS = { + "g39": (0, 0), # row_index=0, column_index=0 (your G39) + "i39": (0, 1), # row_index=0, column_index=1 (your I39) +} +FACTOR_NM3_TO_LHE = Decimal("0.75") +RIGHT_CLIENT_INDEX = {name: idx for idx, name in enumerate(HALFYEAR_RIGHT_CLIENTS)} +def halfyear_balance_view(request): + """ + Read-only Halbjahres-Bilanz view. + + LEFT table: AG Vogel / AG Halfm / IKP (exactly as in your last working version) + RIGHT table: Dr. Fohrer / AG Buntk. / AG Alff / AG Gutfl. / + M3 Thiele / M3 Buntkowsky / M3 Gutfleisch + using the Excel formulas you described. + + Uses the global 6-month interval from the main page (clients_list). + """ + # 1) Read half-year interval from the session + interval_year = request.session.get('halfyear_year') + interval_start = request.session.get('halfyear_start_month') + + if not interval_year or not interval_start: + # No interval chosen yet -> redirect to main page + return redirect('clients_list') + + interval_year = int(interval_year) + interval_start = int(interval_start) + + # You already have this helper in your code + window = build_halfyear_window(interval_year, interval_start) + # window = [(y1, m1), (y2, m2), ..., (y6, m6)] + + # (Year, month) of the first month + start_year, start_month = window[0] + + # Previous month (for "Stand ... (Vorjahr)" and "Best. in Kannen Vormonat") + prev_total_index = (start_month - 1) - 1 # one month back, 0-based + if prev_total_index >= 0: + prev_year = start_year + (prev_total_index // 12) + prev_month = (prev_total_index % 12) + 1 + else: + prev_year = start_year - 1 + prev_month = 12 + + # Load MonthlySheet objects for the window and for the previous month + sheets_by_ym = {} + for (y, m) in window: + sheet = MonthlySheet.objects.filter(year=y, month=m).first() + sheets_by_ym[(y, m)] = sheet + + prev_sheet = MonthlySheet.objects.filter(year=prev_year, month=prev_month).first() + def pick_bottom2_from_window(window, sheets_by_ym, prev_sheet): + # choose sheet (same logic you already use) + chosen = None + for (y, m) in reversed(window): + s = sheets_by_ym.get((y, m)) + # use your existing condition for choosing month + if s: + chosen = s + break + if chosen is None: + chosen = prev_sheet + + # Now read the two inputs safely + bottom2_inputs = {} + for key, (row_idx, col_idx) in BOTTOM2_ROW_INPUTS.items(): + bottom2_inputs[key] = get_bottom2_value(chosen, row_idx, col_idx) + + return chosen, bottom2_inputs + + + chosen_sheet_bottom2, bottom2_inputs = pick_bottom2_from_window(window, sheets_by_ym, prev_sheet) + bottom2_g39 = bottom2_inputs["g39"] + bottom2_i39 = bottom2_inputs["i39"] + # ---------------------------- + # HALF-YEAR BOTTOM TABLE 1 (Bilanz) - Read only + # ---------------------------- + chosen_sheet_bottom1 = pick_sheet_by_gasbestand(window, sheets_by_ym, prev_sheet) + + # IMPORTANT: define which bottom_1 row_index corresponds to Excel rows 27..35 + # If your bottom_1 starts at Excel row 27 => row_index 0 == Excel 27 + # then row_index = excel_row - 27 + BOTTOM1_EXCEL_START_ROW = 27 + + bottom1_excel_rows = list(range(27, 37)) # 27..36 + BOTTOM1_LABELS = [ + "Batterie 1", + "2", + "3", + "4", + "5", + "Batterie Links", + "2 Bündel", + "2 Ballone", + "Reingasspeicher", + "Gasbestand", + ] + + BOTTOM1_VOLUMES = [ + Decimal("2.4"), + Decimal("5.1"), + Decimal("4.0"), + Decimal("1.0"), + Decimal("4.0"), + Decimal("0.6"), + Decimal("1.2"), + Decimal("20.0"), + Decimal("5.0"), + None, # Gasbestand row has no volume + ] + nm3_sum_27_35 = Decimal("0") + lhe_sum_27_35 = Decimal("0") + bottom1_rows = [] + + for excel_row in bottom1_excel_rows: + row_index = excel_row - BOTTOM1_EXCEL_START_ROW + + chosen_sheet_bottom1 = None + for (y, m) in reversed(window): + s = sheets_by_ym.get((y, m)) + gasbestand = get_bottom1_value(s, GASBESTAND_ROW_INDEX, GASBESTAND_COL_NM3) # J36 (Nm3) + if gasbestand != 0: + chosen_sheet_bottom1 = s + break + + if chosen_sheet_bottom1 is None: + chosen_sheet_bottom1 = prev_sheet + + # Normal rows (27..35): read from chosen sheet and accumulate sums + if excel_row != 36: + nm3_val = get_bottom1_value(chosen_sheet_bottom1, row_index, BOTTOM1_COL_NM3) + lhe_val = get_bottom1_value(chosen_sheet_bottom1, row_index, BOTTOM1_COL_LHE) + + nm3_sum_27_35 += nm3_val + lhe_sum_27_35 += lhe_val + + bottom1_rows.append({ + "label": BOTTOM1_LABELS[row_index], + "volume": BOTTOM1_VOLUMES[row_index], + "bar": get_bottom1_value(chosen_sheet_bottom1, row_index, BOTTOM1_COL_BAR), + "korr": get_bottom1_value(chosen_sheet_bottom1, row_index, BOTTOM1_COL_KORR), + "nm3": nm3_val, + "lhe": lhe_val, + }) + + # Gasbestand row (36): show sums (J36 = SUM(J27:J35), K36 = SUM(K27:K35)) + else: + bottom1_rows.append({ + "label": "Gasbestand", + "volume": "", + "bar": "", + "korr": "", + "nm3": nm3_sum_27_35, + "lhe": lhe_sum_27_35, + }) + start_sheet = sheets_by_ym.get((start_year, start_month)) + # ------------------------------------------------------------ + # Bottom Table 2 (Halbjahres Bilanz) – server-side recalcBottom2() + # ------------------------------------------------------------ + + FACTOR_BT2 = Decimal("0.75") + + # 1) Helper: pick last-nonzero value of bottom_2 row0 col0/col1 from the window (fallback: prev_sheet) + def pick_last_nonzero_bottom2(row_index: int, col_index: int) -> Decimal: + # Scan from last month in window backwards + for (y, m) in reversed(window): + s = sheets_by_ym.get((y, m)) + if not s: + continue + v = get_bottom2_value(s, row_index, col_index) + if v is not None and v != 0: + return v + # fallback to month before window + v_prev = get_bottom2_value(prev_sheet, row_index, col_index) + return v_prev if v_prev is not None else Decimal("0") + + # 2) K38 comes from Overall Summary: "Summe Bestand (Lit. L-He)" + # Find it from your already built overall summary rows list. + + k38 = Decimal("0") + j38 = Decimal("0") + # 3) Inputs G39 / I39 (picked from last non-zero month in window) + g39 = pick_last_nonzero_bottom2(row_index=0, col_index=0) # G39 + i39 = pick_last_nonzero_bottom2(row_index=0, col_index=1) # I39 + + k39 = (g39 or Decimal("0")) + (i39 or Decimal("0")) + j39 = k39 * FACTOR_BT2 + + # 4) +Kaltgas (row 40) + # JS: + # g40 = (2500 - g39)/100*10 + # i40 = (1000 - i39)/100*10 + g40 = None + i40 = None + if g39 is not None: + g40 = (Decimal("2500") - g39) / Decimal("100") * Decimal("10") + if i39 is not None: + i40 = (Decimal("1000") - i39) / Decimal("100") * Decimal("10") + + k40 = (g40 or Decimal("0")) + (i40 or Decimal("0")) + j40 = k40 * FACTOR_BT2 + + # 5) Bestand flüssig He (row 43) + k43 = ( + (k38 or Decimal("0")) + + (k39 or Decimal("0")) + + (k40 or Decimal("0")) + ) + j43 = k43 * FACTOR_BT2 + + # 6) Gesamtbestand neu (row 44) = Gasbestand(Lit) from Bottom Table 1 + k43 + gasbestand_lit = Decimal("0") + for r in bottom1_rows: + if (r.get("label") or "").strip().startswith("Gasbestand"): + gasbestand_lit = r.get("lhe") or Decimal("0") + break + + k44 = (gasbestand_lit or Decimal("0")) + (k43 or Decimal("0")) + j44 = k44 * FACTOR_BT2 + + bottom2 = { + "j38": j38, "k38": k38, + "g39": g39, "i39": i39, "j39": j39, "k39": k39, + "g40": g40, "i40": i40, "j40": j40, "k40": k40, + "j43": j43, "k43": k43, + "j44": j44, "k44": k44, + } + + # ------------------------------------------------------------------ + # 2) LEFT TABLE (your existing, working logic) + # ------------------------------------------------------------------ + HALFYEAR_CLIENTS_LEFT = ["AG Vogel", "AG Halfm", "IKP"] + + # We'll collect client-wise values first for clarity. + client_data_left = {name: {} for name in HALFYEAR_CLIENTS_LEFT} + + # --- Row B3: Stand der Gaszähler (Nm³) + # = MAX(B3 from previous month, and B3 from each of the 6 months in the window) + # row_index 0 in top_left = "Stand der Gaszähler (Nm³)" + months_for_max = [(prev_year, prev_month)] + window + + for cname in HALFYEAR_CLIENTS_LEFT: + max_val = Decimal('0') + for (y, m) in months_for_max: + sheet = sheets_by_ym.get((y, m)) + if sheet is None and (y, m) == (prev_year, prev_month): + sheet = prev_sheet + val_b3 = get_top_left_value(sheet, cname, row_index=0) + if val_b3 > max_val: + max_val = val_b3 + client_data_left[cname]['stand_gas'] = max_val + + # --- Row B4: Stand der Gaszähler (Vorjahr) (Nm³) -> previous month same row --- + for cname in HALFYEAR_CLIENTS_LEFT: + val_b4 = get_top_left_value(prev_sheet, cname, row_index=0) + client_data_left[cname]['stand_gas_prev'] = val_b4 + + # --- Row B5: Gasrückführung (Nm³) = B3 - B4 --- + for cname in HALFYEAR_CLIENTS_LEFT: + b3 = client_data_left[cname]['stand_gas'] + b4 = client_data_left[cname]['stand_gas_prev'] + client_data_left[cname]['gasrueckf'] = b3 - b4 + + # --- Row B6: Rückführung flüssig (Lit. L-He) = B5 / 0.75 --- + for cname in HALFYEAR_CLIENTS_LEFT: + b5 = client_data_left[cname]['gasrueckf'] + client_data_left[cname]['rueckf_fluessig'] = (b5 / Decimal('0.75')) if b5 != 0 else Decimal('0') + + # --- Row B7: Sonderrückführungen (Lit. L-He) = sum over 6 months of that row --- + # That row index is 4 in your top_left table. + for cname in HALFYEAR_CLIENTS_LEFT: + sonder_total = Decimal('0') + for (y, m) in window: + sheet = sheets_by_ym.get((y, m)) + if sheet: + sonder_total += get_top_left_value(sheet, cname, row_index=4) + client_data_left[cname]['sonder'] = sonder_total + + # --- Row B8: Bestand in Kannen-1 (Lit. L-He) --- + # Excel-style logic with Gasbestand (J36) and fallback to previous month. + for cname in HALFYEAR_CLIENTS_LEFT: + chosen_value = None + + # Go from last month (window[5]) backwards to first (window[0]) + for (y, m) in reversed(window): + sheet = sheets_by_ym.get((y, m)) + gasbestand = get_bottom1_value(sheet, GASBESTAND_ROW_INDEX, GASBESTAND_COL_NM3) + if gasbestand != 0: + chosen_value = get_bestand_kannen_for_month(sheet, cname) + break + + # If still None -> use previous month (Übertrag_Dez__Vorjahr equivalent) + if chosen_value is None: + sheet_prev = prev_sheet + chosen_value = get_bestand_kannen_for_month(sheet_prev, cname) + + client_data_left[cname]['bestand_kannen'] = chosen_value if chosen_value is not None else Decimal('0') + + # --- Row B9: Summe Bestand (Lit. L-He) = equal to previous row --- + for cname in HALFYEAR_CLIENTS_LEFT: + client_data_left[cname]['summe_bestand'] = client_data_left[cname]['bestand_kannen'] + + # --- Row B10: Best. in Kannen Vormonat (Lit. L-He) + # = Bestand in Kannen-1 from the month BEFORE the window (prev_year, prev_month) + for cname in HALFYEAR_CLIENTS_LEFT: + client_data_left[cname]['best_kannen_vormonat'] = get_bestand_kannen_for_month(prev_sheet, cname) + + # --- Row B13: Bezug (Liter L-He) --- + for cname in HALFYEAR_CLIENTS_LEFT: + total_bezug = Decimal('0') + for (y, m) in window: + qs = SecondTableEntry.objects.filter( + client__name=cname, + date__year=y, + date__month=m, + ).aggregate( + total=Coalesce(Sum('lhe_output'), Value(0, output_field=DecimalField())) + ) + total_bezug += Decimal(str(qs['total'])) + client_data_left[cname]['bezug'] = total_bezug + + # --- Row B14: Rückführ. Soll (Lit. L-He) = Bezug - Summe Bestand + Best. in Kannen Vormonat --- + for cname in HALFYEAR_CLIENTS_LEFT: + b13 = client_data_left[cname]['bezug'] + b11 = client_data_left[cname]['summe_bestand'] + b12 = client_data_left[cname]['best_kannen_vormonat'] + client_data_left[cname]['rueckf_soll'] = b13 - b11 + b12 + + # --- Row B15: Verluste (Soll-Rückf.) (Lit. L-He) = B14 - B6 --- + for cname in HALFYEAR_CLIENTS_LEFT: + b14 = client_data_left[cname]['rueckf_soll'] + b6 = client_data_left[cname]['rueckf_fluessig'] + client_data_left[cname]['verluste'] = b14 - b6 + + # --- Row B16: Füllungen warm (Lit. L-He) = sum over 6 months (row_index=11) --- + for cname in HALFYEAR_CLIENTS_LEFT: + total_warm = Decimal('0') + for (y, m) in window: + sheet = sheets_by_ym.get((y, m)) + total_warm += get_top_left_value(sheet, cname, row_index=11) + client_data_left[cname]['fuellungen_warm'] = total_warm + + # --- Row B17: Kaltgas Rückgabe (Lit. L-He) = Bezug * 0.06 --- + factor = Decimal('0.06') + for cname in HALFYEAR_CLIENTS_LEFT: + b13 = client_data_left[cname]['bezug'] + client_data_left[cname]['kaltgas_rueckgabe'] = b13 * factor + + # --- Row B18: Verbraucherverluste (Liter L-He) = Verluste - Kaltgas Rückgabe --- + for cname in HALFYEAR_CLIENTS_LEFT: + b15 = client_data_left[cname]['verluste'] + b17 = client_data_left[cname]['kaltgas_rueckgabe'] + client_data_left[cname]['verbraucherverluste'] = b15 - b17 + + # --- Row B19: % = Verbraucherverluste / Bezug --- + for cname in HALFYEAR_CLIENTS_LEFT: + bezug = client_data_left[cname]['bezug'] + verb = client_data_left[cname]['verbraucherverluste'] + if bezug != 0: + client_data_left[cname]['percent'] = verb / bezug + else: + client_data_left[cname]['percent'] = None + + # Build LEFT rows structure + left_row_defs = [ + ('Stand der Gaszähler (Nm³)', 'stand_gas'), + ('Stand der Gaszähler (Vorjahr) (Nm³)', 'stand_gas_prev'), + ('Gasrückführung (Nm³)', 'gasrueckf'), + ('Rückführung flüssig (Lit. L-He)', 'rueckf_fluessig'), + ('Sonderrückführungen (Lit. L-He)', 'sonder'), + ('Bestand in Kannen-1 (Lit. L-He)', 'bestand_kannen'), + ('Summe Bestand (Lit. L-He)', 'summe_bestand'), + ('Best. in Kannen Vormonat (Lit. L-He)', 'best_kannen_vormonat'), + ('Bezug (Liter L-He)', 'bezug'), + ('Rückführ. Soll (Lit. L-He)', 'rueckf_soll'), + ('Verluste (Soll-Rückf.) (Lit. L-He)', 'verluste'), + ('Füllungen warm (Lit. L-He)', 'fuellungen_warm'), + ('Kaltgas Rückgabe (Lit. L-He) – Faktor', 'kaltgas_rueckgabe'), + ('Verbraucherverluste (Liter L-He)', 'verbraucherverluste'), + ('%', 'percent'), + ] + + rows_left = [] + for label, key in left_row_defs: + values = [client_data_left[cname][key] for cname in HALFYEAR_CLIENTS_LEFT] + if key == 'percent': + total_bezug = sum((client_data_left[c]['bezug'] for c in HALFYEAR_CLIENTS_LEFT), Decimal('0')) + total_verb = sum((client_data_left[c]['verbraucherverluste'] for c in HALFYEAR_CLIENTS_LEFT), Decimal('0')) + total = (total_verb / total_bezug) if total_bezug != 0 else None + else: + total = sum((v for v in values if v is not None), Decimal('0')) + rows_left.append({ + 'label': label, + 'values': values, + 'total': total, + 'is_percent': key == 'percent', + }) + + # ------------------------------------------------------------------ + # 3) RIGHT TABLE (top-right half-year aggregation) + # ------------------------------------------------------------------ + RIGHT_CLIENTS = HALFYEAR_RIGHT_CLIENTS # for brevity + + right_data = {name: {} for name in RIGHT_CLIENTS} + + # --- Bezug (Liter L-He) for each right client (same as for left) --- + for cname in RIGHT_CLIENTS: + total_bezug = Decimal('0') + for (y, m) in window: + qs = SecondTableEntry.objects.filter( + client__name=cname, + date__year=y, + date__month=m, + ).aggregate( + total=Coalesce(Sum('lhe_output'), Value(0, output_field=DecimalField())) + ) + total_bezug += Decimal(str(qs['total'])) + right_data[cname]['bezug'] = total_bezug + def find_bestand_from_window(reference_client: str) -> Decimal: + """ + Implements: + WENN(last_month!J36=0; WENN(prev_month!J36=0; ...; prev_sheet!9); last_month!9) + reference_client decides which column (L/N/P/Q/R) we read from monthly top_right row_index=5. + """ + # scan backward through window + for (y, m) in reversed(window): + sh = sheets_by_ym.get((y, m)) + if not sh: + continue + gasbestand = get_bottom1_value(sh, GASBESTAND_ROW_INDEX, GASBESTAND_COL_NM3) + if gasbestand != 0: + return get_top_right_value(sh, reference_client, TR_BESTAND_KANNEN_ROW) + + # fallback to previous month (Übertrag_Dez__Vorjahr equivalent) + return get_top_right_value(prev_sheet, reference_client, TR_BESTAND_KANNEN_ROW) + + # Fohrer+Buntk merged: BOTH use Fohrer column (L9) + val_L = find_bestand_from_window("Dr. Fohrer") + right_data["Dr. Fohrer"]["bestand_kannen"] = val_L + right_data["AG Buntk."]["bestand_kannen"] = val_L + + # Alff+Gutfl merged: BOTH use Alff column (N9) + val_N = find_bestand_from_window("AG Alff") + right_data["AG Alff"]["bestand_kannen"] = val_N + right_data["AG Gutfl."]["bestand_kannen"] = val_N + + # M3 each uses its own column (P9/Q9/R9) + right_data["M3 Thiele"]["bestand_kannen"] = find_bestand_from_window("M3 Thiele") + right_data["M3 Buntkowsky"]["bestand_kannen"] = find_bestand_from_window("M3 Buntkowsky") + right_data["M3 Gutfleisch"]["bestand_kannen"] = find_bestand_from_window("M3 Gutfleisch") + # Helper for pair shares (L13/($L13+$M13), etc.) + def pair_share(c1, c2): + total = right_data[c1]['bezug'] + right_data[c2]['bezug'] + if total == 0: + return (Decimal('0'), Decimal('0')) + return ( + right_data[c1]['bezug'] / total, + right_data[c2]['bezug'] / total, + ) + + # --- "Stand der Gaszähler (Vorjahr) (Nm³)" row: share based on Bezug --- + # Dr. Fohrer / AG Buntk. + s_fohrer, s_buntk = pair_share("Dr. Fohrer", "AG Buntk.") + right_data["Dr. Fohrer"]['stand_prev_share'] = s_fohrer + right_data["AG Buntk."]['stand_prev_share'] = s_buntk + + # AG Alff / AG Gutfl. + s_alff, s_gutfl = pair_share("AG Alff", "AG Gutfl.") + right_data["AG Alff"]['stand_prev_share'] = s_alff + right_data["AG Gutfl."]['stand_prev_share'] = s_gutfl + + # M3 Thiele / M3 Buntkowsky / M3 Gutfleisch → empty in Excel → None + for cname in ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"]: + right_data[cname]['stand_prev_share'] = None + + # --- Rückführung flüssig per month (raw sums) --- + # top_right row_index=2 is "Rückführung flüssig (Lit. L-He)" + + # --- Sonderrückführungen (row_index=3 in top_right) --- + for cname in RIGHT_CLIENTS: + sonder_total = Decimal('0') + for (y, m) in window: + sheet = sheets_by_ym.get((y, m)) + if sheet: + sonder_total += get_top_right_value(sheet, cname, row_index=3) + right_data[cname]['sonder'] = sonder_total + + # --- Sammelrückführung (row_index=4 in top_right), grouped & merged --- + # Group 1: Dr. Fohrer + AG Buntk. + group1_total = Decimal('0') + for (y, m) in window: + sheet = sheets_by_ym.get((y, m)) + if sheet: + group1_total += get_top_right_value(sheet, "Dr. Fohrer", row_index=4) + right_data["Dr. Fohrer"]['sammel'] = group1_total + right_data["AG Buntk."]['sammel'] = group1_total + + # Group 2: AG Alff + AG Gutfl. + group2_total = Decimal('0') + for (y, m) in window: + sheet = sheets_by_ym.get((y, m)) + if sheet: + group2_total += get_top_right_value(sheet, "AG Alff", row_index=4) + right_data["AG Alff"]['sammel'] = group2_total + right_data["AG Gutfl."]['sammel'] = group2_total + + # Group 3: M3 Thiele + M3 Buntkowsky + M3 Gutfleisch + group3_total = Decimal('0') + for (y, m) in window: + sheet = sheets_by_ym.get((y, m)) + if sheet: + group3_total += get_top_right_value(sheet, "M3 Thiele", row_index=4) + right_data["M3 Thiele"]['sammel'] = group3_total + right_data["M3 Buntkowsky"]['sammel'] = group3_total + right_data["M3 Gutfleisch"]['sammel'] = group3_total + def safe_div(a: Decimal, b: Decimal) -> Decimal: + return (a / b) if b != 0 else Decimal("0") + + # --- Rückführung flüssig (Lit. L-He) for Halbjahres-Bilanz top-right --- + # Uses your exact formulas. + + # 1) Fohrer / Buntk split by BEZUG share times group SAMMEL (L8) + L13 = right_data["Dr. Fohrer"]["bezug"] + M13 = right_data["AG Buntk."]["bezug"] + L8 = right_data["Dr. Fohrer"]["sammel"] # merged group total + + den = (L13 + M13) + right_data["Dr. Fohrer"]["rueckf_fluessig"] = (safe_div(L13, den) * L8) if den != 0 else Decimal("0") + right_data["AG Buntk."]["rueckf_fluessig"] = (safe_div(M13, den) * L8) if den != 0 else Decimal("0") + + # 2) Alff / Gutfl split by BEZUG share times group SAMMEL (N8) + N13 = right_data["AG Alff"]["bezug"] + O13 = right_data["AG Gutfl."]["bezug"] + N8 = right_data["AG Alff"]["sammel"] # merged group total + + den = (N13 + O13) + right_data["AG Alff"]["rueckf_fluessig"] = (safe_div(N13, den) * N8) if den != 0 else Decimal("0") + right_data["AG Gutfl."]["rueckf_fluessig"] = (safe_div(O13, den) * N8) if den != 0 else Decimal("0") + + # 3) M3 Thiele = sum of monthly Rückführung flüssig (monthly top_right row_index=2) over window + P6_sum = Decimal("0") + for (y, m) in window: + sh = sheets_by_ym.get((y, m)) + P6_sum += get_top_right_value(sh, "M3 Thiele", TR_RUECKF_FLUESSIG_ROW) + right_data["M3 Thiele"]["rueckf_fluessig"] = P6_sum + + # 4) M3 Buntkowsky / M3 Gutfleisch split by BEZUG share times M3-group SAMMEL (P8) + P13 = right_data["M3 Thiele"]["bezug"] + Q13 = right_data["M3 Buntkowsky"]["bezug"] + R13 = right_data["M3 Gutfleisch"]["bezug"] + P8 = right_data["M3 Thiele"]["sammel"] # merged group total + + den = (P13 + Q13 + R13) + right_data["M3 Buntkowsky"]["rueckf_fluessig"] = (safe_div(Q13, den) * P8) if den != 0 else Decimal("0") + right_data["M3 Gutfleisch"]["rueckf_fluessig"] = (safe_div(R13, den) * P8) if den != 0 else Decimal("0") + # --- Bestand in Kannen-1 (Lit. L-He) for right table (grouped) --- + # Use Gasbestand (J36) and fallback logic, but now reading top_right B9 for each group. + TOP_RIGHT_ROW_BESTAND_KANNEN = 6 # <-- most likely correct in your setup + + def pick_bestand_top_right(base_client: str) -> Decimal: + # Go from last month in window backwards: if Gasbestand != 0, use that month's Bestand in Kannen + for (y, m) in reversed(window): + sh = sheets_by_ym.get((y, m)) + if not sh: + continue + + gasbestand = get_bottom1_value(sh, GASBESTAND_ROW_INDEX, GASBESTAND_COL_NM3) + if gasbestand != 0: + return get_top_right_value(sh, base_client, TOP_RIGHT_ROW_BESTAND_KANNEN) + + # Fallback to previous month (Übertrag_Dez__Vorjahr equivalent) + return get_top_right_value(prev_sheet, base_client, TOP_RIGHT_ROW_BESTAND_KANNEN) + + # Group 1 merged (Fohrer + Buntk.) + g1_best = pick_bestand_top_right("Dr. Fohrer") + right_data["Dr. Fohrer"]["bestand_kannen"] = g1_best + right_data["AG Buntk."]["bestand_kannen"] = g1_best + + # Group 2 merged (Alff + Gutfl.) + g2_best = pick_bestand_top_right("AG Alff") + right_data["AG Alff"]["bestand_kannen"] = g2_best + right_data["AG Gutfl."]["bestand_kannen"] = g2_best + + # Group 3 merged (M3 Thiele + M3 Buntkowsky + M3 Gutfleisch) + g3_best = pick_bestand_top_right("M3 Thiele") + right_data["M3 Thiele"]["bestand_kannen"] = g3_best + right_data["M3 Buntkowsky"]["bestand_kannen"] = g3_best + right_data["M3 Gutfleisch"]["bestand_kannen"] = g3_best + + # Summe Bestand = same as previous row + for cname in RIGHT_CLIENTS: + right_data[cname]['summe_bestand'] = right_data[cname]['bestand_kannen'] + + # Best. in Kannen Vormonat (Lit. L-He) from previous month top_right row_index=7 + g1_prev = get_top_right_value(prev_sheet, "Dr. Fohrer", TOP_RIGHT_ROW_BESTAND_KANNEN) + right_data["Dr. Fohrer"]['best_kannen_vormonat'] = g1_prev + right_data["AG Buntk."]['best_kannen_vormonat'] = g1_prev + + g2_prev = get_top_right_value(prev_sheet, "AG Alff", TOP_RIGHT_ROW_BESTAND_KANNEN) + right_data["AG Alff"]['best_kannen_vormonat'] = g2_prev + right_data["AG Gutfl."]['best_kannen_vormonat'] = g2_prev + + + # Group 1 merged (Fohrer + Buntk.) + g1_prev = get_top_right_value(prev_sheet, "Dr. Fohrer", TOP_RIGHT_ROW_BESTAND_KANNEN) + right_data["Dr. Fohrer"]["best_kannen_vormonat"] = g1_prev + right_data["AG Buntk."]["best_kannen_vormonat"] = g1_prev + + # Group 2 merged (Alff + Gutfl.) + g2_prev = get_top_right_value(prev_sheet, "AG Alff", TOP_RIGHT_ROW_BESTAND_KANNEN) + right_data["AG Alff"]["best_kannen_vormonat"] = g2_prev + right_data["AG Gutfl."]["best_kannen_vormonat"] = g2_prev + + # Group 3 UNMERGED (each one reads its own cell) + right_data["M3 Thiele"]["best_kannen_vormonat"] = get_top_right_value(prev_sheet, "M3 Thiele", TOP_RIGHT_ROW_BESTAND_KANNEN) + right_data["M3 Buntkowsky"]["best_kannen_vormonat"] = get_top_right_value(prev_sheet, "M3 Buntkowsky", TOP_RIGHT_ROW_BESTAND_KANNEN) + right_data["M3 Gutfleisch"]["best_kannen_vormonat"] = get_top_right_value(prev_sheet, "M3 Gutfleisch", TOP_RIGHT_ROW_BESTAND_KANNEN) + + # --- Rückführ. Soll (Lit. L-He) according to your formulas --- + + # Group 1: Dr. Fohrer / AG Buntk. + total_bestand_1 = right_data["Dr. Fohrer"]['summe_bestand'] + best_vormonat_1 = right_data["Dr. Fohrer"]['best_kannen_vormonat'] + diff1 = total_bestand_1 - best_vormonat_1 + share_fohrer = right_data["Dr. Fohrer"]['stand_prev_share'] or Decimal('0') + + right_data["Dr. Fohrer"]['rueckf_soll'] = ( + right_data["Dr. Fohrer"]['bezug'] - diff1 * share_fohrer + ) + right_data["AG Buntk."]['rueckf_soll'] = ( + right_data["AG Buntk."]['bezug'] - total_bestand_1 + best_vormonat_1 + ) + + # Group 2: AG Alff / AG Gutfl. + total_bestand_2 = right_data["AG Alff"]['summe_bestand'] + best_vormonat_2 = right_data["AG Alff"]['best_kannen_vormonat'] + diff2 = total_bestand_2 - best_vormonat_2 + share_alff = right_data["AG Alff"]['stand_prev_share'] or Decimal('0') + share_gutfl = right_data["AG Gutfl."]['stand_prev_share'] or Decimal('0') + + right_data["AG Alff"]['rueckf_soll'] = ( + right_data["AG Alff"]['bezug'] - diff2 * share_alff + ) + right_data["AG Gutfl."]['rueckf_soll'] = ( + right_data["AG Gutfl."]['bezug'] - diff2 * share_gutfl + ) + + # Group 3: M3 Thiele / M3 Buntkowsky / M3 Gutfleisch + for cname in ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"]: + b13 = right_data[cname]['bezug'] + b12 = right_data[cname]['best_kannen_vormonat'] + b11 = right_data[cname]['summe_bestand'] + # Excel: P13+P12-P11 etc. + right_data[cname]['rueckf_soll'] = b13 + b12 - b11 + + # --- Verluste (Soll-Rückf.) (Lit. L-He) = B14 - B6 - B7 --- + for cname in RIGHT_CLIENTS: + b14 = right_data[cname]['rueckf_soll'] + b6 = right_data[cname]['rueckf_fluessig'] + b7 = right_data[cname]['sonder'] + right_data[cname]['verluste'] = b14 - b6 - b7 + + # --- Füllungen warm (Lit. L-He) = sum of monthly 'Füllungen warm' (row_index=11 top_right) --- + for cname in RIGHT_CLIENTS: + total_warm = Decimal('0') + for (y, m) in window: + sheet = sheets_by_ym.get((y, m)) + if sheet: + total_warm += get_top_right_value(sheet, cname, row_index=11) + right_data[cname]['fuellungen_warm'] = total_warm + + # --- Kaltgas Rückgabe (Lit. L-He) – Faktor = Bezug * 0.06 --- + for cname in RIGHT_CLIENTS: + b13 = right_data[cname]['bezug'] + right_data[cname]['kaltgas_rueckgabe'] = b13 * factor + + # --- Verbraucherverluste (Liter L-He) = Verluste - Kaltgas Rückgabe --- + for cname in RIGHT_CLIENTS: + b15 = right_data[cname]['verluste'] + b17 = right_data[cname]['kaltgas_rueckgabe'] + right_data[cname]['verbraucherverluste'] = b15 - b17 + + # --- % = Verbraucherverluste / Bezug --- + for cname in RIGHT_CLIENTS: + bezug = right_data[cname]['bezug'] + verb = right_data[cname]['verbraucherverluste'] + if bezug != 0: + right_data[cname]['percent'] = verb / bezug + else: + right_data[cname]['percent'] = None + + # Build RIGHT rows structure + right_row_defs = [ + ('Stand der Gaszähler (Vorjahr) (Nm³)', 'stand_prev_share'), + # We skip the pure-text "Gasrückführung (Nm³)" line here, + # because it’s only text (Aufteilung nach Verbrauch / Gaszähler) + # and easier to render directly in the template if needed. + ('Rückführung flüssig (Lit. L-He)', 'rueckf_fluessig'), + ('Sonderrückführungen (Lit. L-He)', 'sonder'), + ('Sammelrückführung (Lit. L-He)', 'sammel'), + ('Bestand in Kannen-1 (Lit. L-He)', 'bestand_kannen'), + ('Summe Bestand (Lit. L-He)', 'summe_bestand'), + ('Best. in Kannen Vormonat (Lit. L-He)', 'best_kannen_vormonat'), + ('Bezug (Liter L-He)', 'bezug'), + ('Rückführ. Soll (Lit. L-He)', 'rueckf_soll'), + ('Verluste (Soll-Rückf.) (Lit. L-He)', 'verluste'), + ('Füllungen warm (Lit. L-He)', 'fuellungen_warm'), + ('Kaltgas Rückgabe (Lit. L-He) – Faktor', 'kaltgas_rueckgabe'), + ('Verbraucherverluste (Liter L-He)', 'verbraucherverluste'), + ('%', 'percent'), + ] + + rows_right = [] + for label, key in right_row_defs: + values = [right_data[cname].get(key) for cname in RIGHT_CLIENTS] + + if key == 'percent': + total_bezug = sum((right_data[c]['bezug'] for c in RIGHT_CLIENTS), Decimal('0')) + total_verb = sum((right_data[c]['verbraucherverluste'] for c in RIGHT_CLIENTS), Decimal('0')) + total = (total_verb / total_bezug) if total_bezug != 0 else None + else: + total = sum((v for v in values if isinstance(v, Decimal)), Decimal('0')) + + rows_right.append({ + 'label': label, + 'values': values, + 'total': total, + 'is_percent': key == 'percent', + }) + SUM_TABLE_ROWS = [ + ("Rückführung flüssig (Lit. L-He)", "rueckf_fluessig"), + ("Sonderrückführungen (Lit. L-He)", "sonder"), + ("Sammelrückführungen (Lit. L-He)", "sammel"), + ("Bestand in Kannen-1 (Lit. L-He)", "bestand_kannen"), + ("Summe Bestand (Lit. L-He)", "summe_bestand"), + ("Best. in Kannen Vormonat (Lit. L-He)", "best_kannen_vormonat"), + ("Bezug (Liter L-He)", "bezug"), + ("Rückführ. Soll (Lit. L-He)", "rueckf_soll"), + ("Verluste (Soll-Rückf.) (Lit. L-He)", "verluste"), + ("Füllungen warm (Lit. L-He)", "fuellungen_warm"), + ("Kaltgas Rückgabe (Lit. L-He) – Faktor", "kaltgas_rueckgabe"), + ("Faktor 0.06", "factor_row"), + ("Verbraucherverluste (Liter L-He)", "verbraucherverluste"), + ("%", "percent"), + ] + + RIGHT_GROUPS = { + "chemie": ["Dr. Fohrer", "AG Buntk."], + "mawi": ["AG Alff", "AG Gutfl."], + "m3": ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"], + } + + RIGHT_ALL = ["Dr. Fohrer", "AG Buntk.", "AG Alff", "AG Gutfl.", "M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"] + LEFT_ALL = HALFYEAR_CLIENTS_LEFT + + def safe_pct(verb, bez): + return (verb / bez) if bez != 0 else None + rows_sum = [] + def d(x): + return x if isinstance(x, Decimal) else Decimal("0") + for label, key in SUM_TABLE_ROWS: + + if key == "factor_row": + lichtwiese = chemie = mawi = m3 = total = Decimal("0.06") + + elif key == "percent": + # Right totals + rw_bez = sum(d(right_data[c].get("bezug")) for c in RIGHT_ALL) + rw_verb = sum(d(right_data[c].get("verbraucherverluste")) for c in RIGHT_ALL) + lichtwiese = safe_pct(rw_verb, rw_bez) + + # Chemie + ch_bez = sum(d(right_data[c].get("bezug")) for c in RIGHT_GROUPS["chemie"]) + ch_verb = sum(d(right_data[c].get("verbraucherverluste")) for c in RIGHT_GROUPS["chemie"]) + chemie = safe_pct(ch_verb, ch_bez) + + # MaWi + mw_bez = sum(d(right_data[c].get("bezug")) for c in RIGHT_GROUPS["mawi"]) + mw_verb = sum(d(right_data[c].get("verbraucherverluste")) for c in RIGHT_GROUPS["mawi"]) + mawi = safe_pct(mw_verb, mw_bez) + + # M3 + m3_bez = sum(d(right_data[c].get("bezug")) for c in RIGHT_GROUPS["m3"]) + m3_verb = sum(d(right_data[c].get("verbraucherverluste")) for c in RIGHT_GROUPS["m3"]) + m3 = safe_pct(m3_verb, m3_bez) + + # Σ column = (left verb + right verb) / (left bez + right bez) + left_bez = sum(d(client_data_left[c].get("bezug")) for c in LEFT_ALL) + left_verb = sum(d(client_data_left[c].get("verbraucherverluste")) for c in LEFT_ALL) + total = safe_pct(left_verb + rw_verb, left_bez + rw_bez) + + else: + # normal rows = sums + lichtwiese = sum(d(right_data[c].get(key)) for c in RIGHT_ALL) + chemie = sum(d(right_data[c].get(key)) for c in RIGHT_GROUPS["chemie"]) + mawi = sum(d(right_data[c].get(key)) for c in RIGHT_GROUPS["mawi"]) + m3 = sum(d(right_data[c].get(key)) for c in RIGHT_GROUPS["m3"]) + + left_total = sum(d(client_data_left[c].get(key)) for c in LEFT_ALL) + total = left_total + lichtwiese + + rows_sum.append({ + "row_index": row_index, + "label": label, + "total": total, + "lichtwiese": lichtwiese, + "chemie": chemie, + "mawi": mawi, + "m3": m3, + "is_percent": (key == "percent"), + }) + def find_sum_row(rows, label_startswith: str): + for r in rows: + if str(r.get("label", "")).strip().startswith(label_startswith): + return r + return None + + summe_bestand_row = find_sum_row(rows_sum, "Summe Bestand") + k38 = (summe_bestand_row.get("total") if summe_bestand_row else Decimal("0")) or Decimal("0") + j38 = k38 * Decimal("0.75") + # --- FIX: now that k38 is known, update bottom2 + recompute dependent rows --- + bottom2["k38"] = k38 + bottom2["j38"] = j38 + + k39 = bottom2.get("k39") or Decimal("0") + k40 = bottom2.get("k40") or Decimal("0") + + # Row 43: Bestand flüssig He = SUMME(K38:K40) + k43 = (k38 or Decimal("0")) + k39 + k40 + j43 = k43 * Decimal("0.75") + + bottom2["k43"] = k43 + bottom2["j43"] = j43 + + # Row 44: Gesamtbestand neu = Gasbestand(Lit) from bottom table 1 + k43 + gasbestand_lit = Decimal("0") + for r in bottom1_rows: + if (r.get("label") or "").strip().startswith("Gasbestand"): + gasbestand_lit = r.get("lhe") or Decimal("0") + break + + k44 = gasbestand_lit + k43 + j44 = k44 * Decimal("0.75") + + bottom2["k44"] = k44 + bottom2["j44"] = j44 + def d(x): + return x if isinstance(x, Decimal) else Decimal("0") + + # ---- Bottom2: J38/K38 depend on rows_sum (overall summary), so do it HERE ---- + k38 = Decimal("0") + for r in rows_sum: + if r.get("label") == "Summe Bestand (Lit. L-He)": + k38 = r.get("total") or Decimal("0") + break + + j38 = k38 * FACTOR_NM3_TO_LHE # 0.75 + bottom2["k38"] = k38 + bottom2["j38"] = j38 + # ------------------------------------------------------------------ + # 4) Context – keep old keys AND new ones + # ------------------------------------------------------------------ + context = { + 'interval_year': interval_year, + 'interval_start_month': interval_start, + 'window': window, + + # Left table – old names (for your first template) + 'clients': HALFYEAR_CLIENTS_LEFT, + 'rows': rows_left, + + # Left table – explicit + 'clients_left': HALFYEAR_CLIENTS_LEFT, + 'rows_left': rows_left, + + # Right table + 'clients_right': RIGHT_CLIENTS, + 'rows_right': rows_right, + 'rows_sum': rows_sum, + 'bottom1_rows': bottom1_rows, + + } + context["bottom2"] = bottom2 + context["context_bottom2_g39"] = bottom2_inputs["g39"] + context["context_bottom2_i39"] = bottom2_inputs["i39"] + return render(request, 'halfyear_balance.html', context) + +def get_bottom2_value(sheet, row_index: int, col_index: int) -> Decimal: + """Get numeric value from bottom_2 or 0 if missing.""" + if sheet is None: + return Decimal("0") + cell = Cell.objects.filter( + sheet=sheet, + table_type="bottom_2", + row_index=row_index, + column_index=col_index, + ).first() + if cell is None or cell.value in (None, ""): + return Decimal("0") + try: + return Decimal(str(cell.value)) + except Exception: + return Decimal("0") + +def get_top_left_value(sheet, client_name: str, row_index: int) -> Decimal: + """ + Read a numeric value from the top_left table for a given month, client and row. + Does NOT use column_index, because top_left is keyed only by client + row_index. + """ + if sheet is None: + return Decimal('0') + + client_obj = Client.objects.filter(name=client_name).first() + if not client_obj: + return Decimal('0') + + cell = Cell.objects.filter( + sheet=sheet, + table_type='top_left', + client=client_obj, + row_index=row_index, + ).first() + + if cell is None or cell.value in (None, ''): + return Decimal('0') + + try: + return Decimal(str(cell.value)) + except Exception: + return Decimal('0') +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", + ] + current_summary = MonthlySummary.objects.filter(sheet=sheet).first() + + # Get previous month summary (for Bottom Table 3: K46 = prev K44) + prev_month_info = self.get_prev_month(year, month) + prev_summary = None + if not is_start_sheet: + prev_sheet = MonthlySheet.objects.filter( + year=prev_month_info['year'], + month=prev_month_info['month'] + ).first() + if prev_sheet: + prev_summary = MonthlySummary.objects.filter(sheet=prev_sheet).first() + + context.update({ + # ... your existing context ... + 'current_summary': current_summary, + 'prev_summary': prev_summary, + }) + # Update row counts in build_group_rows function + # 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 + MERGED_ROWS = {2, 3, 5, 6, 7, 9, 10, 12, 14, 15} + MERGED_PAIRS = [ + ("Dr. Fohrer", "AG Buntk."), + ("AG Alff", "AG Gutfl."), + ] + rows = [] + + # Determine row count + row_counts = { + "top_left": 16, + "top_right": 16, # rows 0–15 + "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 + + # We will store row sums for Bezug (row 8) and Verbraucherverluste (row 14) + # for top_left / top_right so we can compute the overall % in row 15. + sum_bezug = None + sum_verbrauch = None + + # Build each row + 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 (includes editable + calculated cells) + sum_value = None + total = Decimal("0") + has_value = False + + merged_second_indices = set() + if table_type == 'top_right' and row_idx in MERGED_ROWS: + for left_name, right_name in MERGED_PAIRS: + try: + right_idx = client_names.index(right_name) + merged_second_indices.add(right_idx) + except ValueError: + # client not in this table; just ignore + pass + + for col_idx, cell in enumerate(display_cells): + # Skip the duplicate (second) column of each merged pair + if col_idx in merged_second_indices: + continue + + if cell and cell.value is not None: + try: + total += Decimal(str(cell.value)) + has_value = True + except Exception: + pass + + if has_value: + sum_value = total + + # Remember special rows for top tables + if table_type in ("top_left", "top_right"): + if row_idx == 8: # Bezug + sum_bezug = total + elif row_idx == 14: # Verbraucherverluste + sum_verbrauch = total + + rows.append( + { + "cells": display_cells, + "sum": sum_value, + "row_index": row_idx, + } + ) + + # Adjust the % row sum for top_left / top_right: + # Sum(%) = Sum(Verbraucherverluste) / Sum(Bezug) + if table_type in ("top_left", "top_right"): + perc_row_idx = 15 # % row + if 0 <= perc_row_idx < len(rows): + if sum_bezug is not None and sum_bezug != 0 and sum_verbrauch is not None: + rows[perc_row_idx]["sum"] = sum_verbrauch / sum_bezug + else: + rows[perc_row_idx]["sum"] = None + + 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) + + # --- Build combined summary of top-left + top-right Sum columns --- + + # Helper to safely get the Sum for a given row index + def get_row_sum(rows, row_index): + if 0 <= row_index < len(rows): + return rows[row_index].get('sum') + return None + + # Row definitions we want in the combined Σ table + # (row_index in top tables, label shown in the small table) + summary_row_defs = [ + (2, "Rückführung flüssig (Lit. L-He)"), + (3, "Sonderrückführungen (Lit. L-He)"), + (4, "Sammelrückführungen (Lit. L-He)"), + (5, "Bestand in Kannen-1 (Lit. L-He)"), + (6, "Summe Bestand (Lit. L-He)"), + (7, "Best. in Kannen Vormonat (Lit. L-He)"), + (8, "Bezug (Liter L-He)"), + (9, "Rückführ. Soll (Lit. L-He)"), + (10, "Verluste (Soll-Rückf.) (Lit. L-He)"), + (11, "Füllungen warm (Lit. L-He)"), + (12, "Kaltgas Rückgabe (Lit. L-He) – Faktor"), + (13, "Faktor 0.06"), + (14, "Verbraucherverluste (Liter L-He)"), + (15, "%"), + ] + + # Precompute totals for Bezug and Verbraucherverluste across both tables + bezug_left = get_row_sum(top_left_rows, 8) or Decimal('0') + bezug_right = get_row_sum(top_right_rows, 8) or Decimal('0') + total_bezug = bezug_left + bezug_right + + verb_left = get_row_sum(top_left_rows, 14) or Decimal('0') + verb_right = get_row_sum(top_right_rows, 14) or Decimal('0') + total_verbrauch = verb_left + verb_right + + summary_rows = [] + + for row_index, label in summary_row_defs: + # Faktor row: always fixed 0.06 + if row_index == 13: + summary_value = Decimal('0.06') + + # % row: total Verbraucherverluste / total Bezug + elif row_index == 15: + if total_bezug != 0: + summary_value = total_verbrauch / total_bezug + else: + summary_value = None + + else: + left_sum = get_row_sum(top_left_rows, row_index) + right_sum = get_row_sum(top_right_rows, row_index) + + # Sammelrückführungen: only from top-right table + if row_index == 4: + left_sum = None + + total = Decimal('0') + + has_any = False + if left_sum is not None: + total += Decimal(str(left_sum)) + has_any = True + if right_sum is not None: + total += Decimal(str(right_sum)) + has_any = True + + summary_value = total if has_any else None + + summary_rows.append({ + 'row_index': row_index, + 'label': label, + 'sum': summary_value, + }) + + # 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, + 'summary_rows': summary_rows, # 👈 NEW + '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 + + + 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 calculate_bottom_3_dependents(self, sheet): + updated_cells = [] + + def get_cell(row_idx, col_idx): + return Cell.objects.filter( + sheet=sheet, + table_type='bottom_3', + row_index=row_idx, + column_index=col_idx + ).first() + + def set_cell(row_idx, col_idx, value): + cell, created = Cell.objects.get_or_create( + sheet=sheet, + table_type='bottom_3', + row_index=row_idx, + column_index=col_idx, + defaults={'value': value} + ) + 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 is not None else '', + 'is_calculated': True, + }) + + def dec(x): + if x in (None, ''): + return Decimal('0') + try: + return Decimal(str(x)) + except Exception: + return Decimal('0') + + # ---- current summary ---- + cur_sum = MonthlySummary.objects.filter(sheet=sheet).first() + curr_k44 = dec(cur_sum.gesamtbestand_neu_lhe) if cur_sum else Decimal('0') + total_verb = dec(cur_sum.verbraucherverlust_lhe) if cur_sum else Decimal('0') + + # ---- previous month summary ---- + year, month = sheet.year, sheet.month + if month == 1: + prev_year, prev_month = year - 1, 12 + else: + prev_year, prev_month = year, month - 1 + + prev_sheet = MonthlySheet.objects.filter(year=prev_year, month=prev_month).first() + if prev_sheet: + prev_sum = MonthlySummary.objects.filter(sheet=prev_sheet).first() + prev_k44 = dec(prev_sum.gesamtbestand_neu_lhe) if prev_sum else Decimal('0') + else: + prev_k44 = Decimal('0') + + # ---- read editable inputs from bottom_3: F47,G47,I47,I50 ---- + def get_val(r, c): + cell = get_cell(r, c) + return dec(cell.value if cell else None) + + f47 = get_val(1, 0) + g47 = get_val(1, 1) + i47 = get_val(1, 2) + i50 = get_val(4, 2) + + # Now apply your formulas using prev_k44, curr_k44, total_verb, f47,g47,i47,i50 + # Row indices: 0..7 correspond to 46..53 + # col 3 = J, col 4 = K + + # Row 46 + k46 = prev_k44 + j46 = k46 * Decimal('0.75') + set_cell(0, 3, j46) + set_cell(0, 4, k46) + + # Row 47 + g47 = self._dec((get_cell(1, 1) or {}).value if get_cell(1, 1) else None) + i47 = self._dec((get_cell(1, 2) or {}).value if get_cell(1, 2) else None) + + j47 = g47 + i47 + k47 = (j47 / Decimal('0.75')) + g47 if j47 != 0 else g47 + + set_cell(1, 3, j47) + set_cell(1, 4, k47) + + # Row 48 + k48 = k46 + k47 + j48 = k48 * Decimal('0.75') + set_cell(2, 3, j48) + set_cell(2, 4, k48) + + # Row 49 + k49 = curr_k44 + j49 = k49 * Decimal('0.75') + set_cell(3, 3, j49) + set_cell(3, 4, k49) + + # Row 50 + j50 = i50 + k50 = j50 / Decimal('0.75') if j50 != 0 else Decimal('0') + set_cell(4, 3, j50) + set_cell(4, 4, k50) + + # Row 51 + k51 = k48 - k49 - k50 + j51 = k51 * Decimal('0.75') + set_cell(5, 3, j51) + set_cell(5, 4, k51) + + # Row 52 + k52 = total_verb + j52 = k52 * Decimal('0.75') + set_cell(6, 3, j52) + set_cell(6, 4, k52) + + # Row 53 + j53 = j51 - j52 + k53 = k51 - k52 + set_cell(7, 3, j53) + set_cell(7, 4, k53) + + return updated_cells + + + def _dec(self, value): + """Convert value to Decimal or return 0.""" + if value is None or value == '': + return Decimal('0') + try: + return Decimal(str(value)) + except Exception: + return Decimal('0') + + 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) + ) + elif cell.table_type == 'bottom_1': + updated_cells.extend(self.calculate_bottom_1_dependents(sheet, cell)) + elif cell.table_type == 'bottom_3': + updated_cells.extend( + self.calculate_bottom_3_dependents(sheet) + ) + # bottom_1 / bottom_2 / bottom_3 currently have no formulas: + # they just save the new value. + updated_cells += self.calculate_bottom_3_dependents(sheet) + 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_bottom_1_dependents(self, sheet, changed_cell): + """ + Recalculate Bottom Table 1 (table_type='bottom_1'). + + Layout (row_index 0–9, col_index 0–4): + + Rows 0–8: + 0: Batterie 1 + 1: 2 + 2: 3 + 3: 4 + 4: 5 + 5: 6 + 6: 2 Bündel + 7: 2 Ballone + 8: Reingasspeicher + + Row 9: + 9: Gasbestand (totals row) + + Columns: + 0: Volumen (fixed values, not editable) + 1: bar (editable for rows 0–8) + 2: korrigiert = bar / (bar/2000 + 1) + 3: Nm³ = Volumen * korrigiert + 4: Lit. LHe = Nm³ / 0.75 + + Row 9: + - Volumen (col 0): empty + - bar (col 1): empty + - korrigiert (col 2): empty + - Nm³ (col 3): SUM Nm³ rows 0–8 + - Lit. LHe (col 4): SUM Lit. LHe rows 0–8 + """ + from decimal import Decimal, InvalidOperation + from .models import Cell + + updated_cells = [] + + DATA_ROWS = list(range(0, 9)) # 0–8 + TOTAL_ROW = 9 + + COL_VOL = 0 + COL_BAR = 1 + COL_KORR = 2 + COL_NM3 = 3 + COL_LHE = 4 + + # Fixed Volumen values for rows 0–8 + VOLUMES = [ + Decimal("2.4"), # row 0 + Decimal("5.1"), # row 1 + Decimal("4.0"), # row 2 + Decimal("1.0"), # row 3 + Decimal("4.0"), # row 4 + Decimal("0.4"), # row 5 + Decimal("1.2"), # row 6 + Decimal("20.0"), # row 7 + Decimal("5.0"), # row 8 + ] + + def get_cell(row_idx, col_idx): + return Cell.objects.filter( + sheet=sheet, + table_type="bottom_1", + row_index=row_idx, + column_index=col_idx, + ).first() + + def set_calc(row_idx, col_idx, value): + """ + Set a calculated cell and add it to updated_cells. + If value is None, we clear the cell. + """ + cell = get_cell(row_idx, col_idx) + if not cell: + cell = Cell( + sheet=sheet, + table_type="bottom_1", + row_index=row_idx, + column_index=col_idx, + value=value, + ) + else: + cell.value = value + cell.save() + + updated_cells.append( + { + "id": cell.id, + "value": "" if cell.value is None else str(cell.value), + "is_calculated": True, + } + ) + + # ---------- Rows 0–8: per-gasspeicher calculations ---------- + for row_idx in DATA_ROWS: + bar_cell = get_cell(row_idx, COL_BAR) + + # Volumen: fixed constant or, if present, value from DB + vol = VOLUMES[row_idx] + vol_cell = get_cell(row_idx, COL_VOL) + if vol_cell and vol_cell.value is not None: + try: + vol = Decimal(str(vol_cell.value)) + except (InvalidOperation, ValueError): + # fall back to fixed constant + vol = VOLUMES[row_idx] + + bar = None + try: + if bar_cell and bar_cell.value is not None: + bar = Decimal(str(bar_cell.value)) + except (InvalidOperation, ValueError): + bar = None + + # korrigiert = bar / (bar/2000 + 1) + if bar is not None and bar != 0: + korr = bar / (bar / Decimal("2000") + Decimal("1")) + else: + korr = None + + # Nm³ = Volumen * korrigiert + if korr is not None: + nm3 = vol * korr + else: + nm3 = None + + # Lit. LHe = Nm³ / 0.75 + if nm3 is not None: + lit_lhe = nm3 / Decimal("0.75") + else: + lit_lhe = None + + # Write calculated cells back (NOT Volumen or bar) + set_calc(row_idx, COL_KORR, korr) + set_calc(row_idx, COL_NM3, nm3) + set_calc(row_idx, COL_LHE, lit_lhe) + + # ---------- Row 9: totals (Gasbestand) ---------- + total_nm3 = Decimal("0") + total_lhe = Decimal("0") + has_nm3 = False + has_lhe = False + + for row_idx in DATA_ROWS: + nm3_cell = get_cell(row_idx, COL_NM3) + if nm3_cell and nm3_cell.value is not None: + try: + total_nm3 += Decimal(str(nm3_cell.value)) + has_nm3 = True + except (InvalidOperation, ValueError): + pass + + lhe_cell = get_cell(row_idx, COL_LHE) + if lhe_cell and lhe_cell.value is not None: + try: + total_lhe += Decimal(str(lhe_cell.value)) + has_lhe = True + except (InvalidOperation, ValueError): + pass + + # Volumen (0), bar (1), korrigiert (2) on total row stay empty + set_calc(TOTAL_ROW, COL_KORR, None) # explicitly clear korrigiert + set_calc(TOTAL_ROW, COL_NM3, total_nm3 if has_nm3 else None) + set_calc(TOTAL_ROW, COL_LHE, total_lhe if has_lhe else None) + + 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 Exception: + 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 '' + }) + # bottom_3 has no client, so this will just add None for those cells, + # which is harmless. Top-left cells still add their real client_id. + changed_clients.add(cell.client_id) + + except Cell.DoesNotExist: + continue + + # Recalculate for each changed client (top-left tables) + for client_id in changed_clients: + if client_id is not None: + self.recalculate_top_left_table(sheet, client_id) + + # --- NEW: recalc bottom_3 for the whole sheet, independent of clients --- + bottom3_updates = self.calculate_bottom_3_dependents(sheet) + + # Get all updated cells for response (top-left) + all_updated_cells = [] + for client_id in changed_clients: + if client_id is None: + continue # skip bottom_3 / non-client cells + 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 + }) + + # Add bottom_3 recalculated cells (J46..K53, etc.) + all_updated_cells.extend(bottom3_updates) + + 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() +@method_decorator(csrf_exempt, name='dispatch') +class SaveMonthSummaryView(View): + """ + Saves per-month summary values such as K44 (Gesamtbestand neu). + Called from JS after 'Save All' finishes. + """ + + def post(self, request, *args, **kwargs): + try: + data = json.loads(request.body.decode('utf-8')) + except json.JSONDecodeError: + return JsonResponse( + {'success': False, 'message': 'Invalid JSON'}, + status=400 + ) + + sheet_id = data.get('sheet_id') + if not sheet_id: + return JsonResponse( + {'success': False, 'message': 'Missing sheet_id'}, + status=400 + ) + + try: + sheet = MonthlySheet.objects.get(id=sheet_id) + except MonthlySheet.DoesNotExist: + return JsonResponse( + {'success': False, 'message': 'Sheet not found'}, + status=404 + ) + + # More tolerant decimal conversion: accepts "123.45" and "123,45" + def to_decimal(value): + if value is None: + return None + s = str(value).strip() + if s == '': + return None + s = s.replace(',', '.') + try: + return Decimal(s) + except (InvalidOperation, ValueError): + # Debug: show what failed in the dev server console + print("SaveMonthSummaryView to_decimal failed for:", repr(value)) + return None + + raw_k44 = data.get('gesamtbestand_neu_lhe') + raw_gas = data.get('gasbestand_lhe') + raw_verb = data.get('verbraucherverlust_lhe') + + gesamtbestand_neu_lhe = to_decimal(raw_k44) + gasbestand_lhe = to_decimal(raw_gas) + verbraucherverlust_lhe = to_decimal(raw_verb) + + summary, created = MonthlySummary.objects.get_or_create(sheet=sheet) + + if gesamtbestand_neu_lhe is not None: + summary.gesamtbestand_neu_lhe = gesamtbestand_neu_lhe + if gasbestand_lhe is not None: + summary.gasbestand_lhe = gasbestand_lhe + if verbraucherverlust_lhe is not None: + summary.verbraucherverlust_lhe = verbraucherverlust_lhe + + summary.save() + + # Small debug output so you can see in the server console what was saved + print( + f"Saved MonthlySummary for {sheet.year}-{sheet.month:02d}: " + f"K44={summary.gesamtbestand_neu_lhe}, " + f"Gasbestand={summary.gasbestand_lhe}, " + f"Verbraucherverlust={summary.verbraucherverlust_lhe}" + ) + + return JsonResponse({'success': True}) + + + +# 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): + # --- Clients for the yearly summary table --- + clients = Client.objects.all() + + # --- Available years for output data (same as before) --- + available_years_qs = SecondTableEntry.objects.dates('date', 'year', order='DESC') + available_years = [y.year for y in available_years_qs] + + # === 1) Year used for the "Helium Output Yearly Summary" table === + # Uses ?year=... in the dropdown + year_param = request.GET.get('year') + if year_param: + selected_year = int(year_param) + else: + selected_year = available_years[0] if available_years else datetime.now().year + + # === 2) GLOBAL half-year interval (shared with other pages) ======= + # Try GET params first + interval_year_param = request.GET.get('interval_year') + start_month_param = request.GET.get('interval_start_month') + + # Fallbacks from session + session_year = request.session.get('halfyear_year') + session_start_month = request.session.get('halfyear_start_month') + + # Determine final interval_year + if interval_year_param: + interval_year = int(interval_year_param) + elif session_year: + interval_year = int(session_year) + else: + # default: same as selected_year for summary + interval_year = selected_year + + # Determine final interval_start_month + if start_month_param: + interval_start_month = int(start_month_param) + elif session_start_month: + interval_start_month = int(session_start_month) + else: + interval_start_month = 1 # default Jan + + # Store back into the session so other views can read them + request.session['halfyear_year'] = interval_year + request.session['halfyear_start_month'] = interval_start_month + + # === 3) Build a 6-month window, allowing wrap into the next year === + # Example: interval_year=2025, interval_start_month=12 + # window = [(2025,12), (2026,1), (2026,2), (2026,3), (2026,4), (2026,5)] + window = [] + for offset in range(6): + total_index = (interval_start_month - 1) + offset # 0-based index + y = interval_year + (total_index // 12) + m = (total_index % 12) + 1 + window.append((y, m)) + + # === 4) Totals per client in that 6-month window ================== + monthly_data = [] + for client in clients: + monthly_totals = [] + for (y, m) in window: + total = SecondTableEntry.objects.filter( + client=client, + date__year=y, + date__month=m + ).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), + }) + + # === 5) Month labels for the header (only the 6-month window) ===== + month_labels = [calendar.month_abbr[m] for (y, m) in window] + + # === 6) FINALLY: return the response ============================== + return render(request, 'clients_table.html', { + 'available_years': available_years, + 'current_year': selected_year, # used by year dropdown + 'interval_year': interval_year, # used by "Global 6-Month Interval" form + 'interval_start_month': interval_start_month, + 'months': month_labels, + 'monthly_data': monthly_data, + }) + + # === 5) Month labels for the header (only the 6-month window) ===== + month_labels = [calendar.month_abbr[m] for (y, m) in window] + + +def set_halfyear_interval(request): + if request.method == 'POST': + year = int(request.POST.get('year')) + start_month = int(request.POST.get('start_month')) + + request.session['halfyear_year'] = year + request.session['halfyear_start_month'] = start_month + + return redirect(request.META.get('HTTP_REFERER', 'clients_list')) + + return redirect('clients_list') +# Table One View (ExcelEntry) +def table_one_view(request): + from .models import ExcelEntry, Client, Institute + + # --- Base queryset for the main Helium Input table --- + base_entries = ExcelEntry.objects.all().select_related('client', 'client__institute') + + # Read the global 6-month interval from the session + interval_year = request.session.get('halfyear_year') + interval_start = request.session.get('halfyear_start_month') + + if interval_year and interval_start: + interval_year = int(interval_year) + interval_start = int(interval_start) + + # Build the same 6-month window as on the main page (can cross year) + window = [] + for offset in range(6): + total_index = (interval_start - 1) + offset # 0-based + y = interval_year + (total_index // 12) + m = (total_index % 12) + 1 + window.append((y, m)) + + # Build Q filter: (year=m_year AND month=m_month) for any of those 6 + q = Q() + for (y, m) in window: + q |= Q(date__year=y, date__month=m) + + entries_table1 = base_entries.filter(q).order_by('-date') + else: + # Fallback: if no global interval yet, show everything + entries_table1 = base_entries.order_by('-date') + 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 + # default year/start month (if no global interval yet) + if available_years: + default_year = available_years[0] # newest year in ExcelEntry + else: + default_year = timezone.now().year + + # 🔸 Read global half-year interval from session (set on main page) + session_year = request.session.get('halfyear_year') + session_start = request.session.get('halfyear_start_month') + + # If the user has set a global interval, use it. + # Otherwise fall back to default year / January. + year = int(session_year) if session_year else int(default_year) + start_month = int(session_start) if session_start else 1 + + + # six-month window + # --- Build a 6-month window, allowing wrap into the next year --- + # Example: year=2025, start_month=10 + # window = [(2025,10), (2025,11), (2025,12), (2026,1), (2026,2), (2026,3)] + window = [] + for offset in range(6): + total_index = (start_month - 1) + offset # 0-based + y_for_month = year + (total_index // 12) + m_for_month = (total_index % 12) + 1 + window.append((y_for_month, m_for_month)) + + overview = None + + if window: + # 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 (y_m, m_m) in window: + total = ExcelEntry.objects.filter( + client__in=clients_qs, + date__year=y_m, + date__month=m_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(window)): + 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, (y_m, m_m) in enumerate(window): + row_values = [g['values'][idx] for g in groups_entries] + rows.append({ + 'month_number': m_m, + 'month_label': calendar.month_name[m_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] + + # Start/end for display – include years so wrap is clear + start_year = window[0][0] + start_month_disp = window[0][1] + end_year = window[-1][0] + end_month_disp = window[-1][1] + + overview = { + 'year': year, # keep for backwards compatibility if needed + 'start_month': start_month_disp, + 'start_year': start_year, + 'end_month': end_month_disp, + 'end_year': end_year, + '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: + clients = Client.objects.all().select_related('institute') + institutes = Institute.objects.all() + + # 🔸 Read global half-year interval from session + interval_year = request.session.get('halfyear_year') + interval_start = request.session.get('halfyear_start_month') + + if interval_year and interval_start: + interval_year = int(interval_year) + interval_start = int(interval_start) + + # Build the same 6-month window as in clients_list (can cross years) + window = [] + for offset in range(6): + total_index = (interval_start - 1) + offset # 0-based + y = interval_year + (total_index // 12) + m = (total_index % 12) + 1 + window.append((y, m)) + + # Build a Q object matching any of those (year, month) pairs + q = Q() + for (y, m) in window: + q |= Q(date__year=y, date__month=m) + + entries = SecondTableEntry.objects.filter(q).order_by('-date') + else: + # Fallback if no global interval yet: show all + entries = SecondTableEntry.objects.all().order_by('-date') + + return render(request, 'table_two.html', { + 'entries_table2': entries, + 'clients': clients, + 'institutes': institutes, + 'interval_year': interval_year, + 'interval_start_month': interval_start, + }) + + except Exception as e: + return render(request, 'table_two.html', { + 'error_message': f"Failed to load data: {str(e)}", + 'entries_table2': [], + 'clients': clients, + 'institutes': institutes, + }) + + +def monthly_sheet_root(request): + """ + Redirect /sheet/ to the sheet matching the globally selected + half-year start (year + month). If not set, fall back to latest. + """ + year = request.session.get('halfyear_year') + start_month = request.session.get('halfyear_start_month') + + if year and start_month: + try: + year = int(year) + start_month = int(start_month) + return redirect('monthly_sheet', year=year, month=start_month) + except ValueError: + pass # fall through + + # Fallback: latest MonthlySheet if exists + latest_sheet = MonthlySheet.objects.order_by('-year', '-month').first() + if latest_sheet: + return redirect('monthly_sheet', year=latest_sheet.year, month=latest_sheet.month) + else: + now = timezone.now() + return redirect('monthly_sheet', year=now.year, month=now.month) +# 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'}) + +def halfyear_settings(request): + """ + Global settings page: choose a year + first month for the 6-month interval. + These values are stored in the session and used by other views. + """ + # Determine available years from your data (use ExcelEntry or SecondTableEntry) + # Here I use SecondTableEntry; you can switch to ExcelEntry if you prefer. + available_years_qs = SecondTableEntry.objects.dates('date', 'year', order='DESC') + available_years = [d.year for d in available_years_qs] + + if not available_years: + available_years = [timezone.now().year] + + # Defaults (if nothing in session yet) + default_year = request.session.get('halfyear_year', available_years[0]) + default_start_month = request.session.get('halfyear_start_month', 1) + + if request.method == 'POST': + year = int(request.POST.get('year', default_year)) + start_month = int(request.POST.get('start_month', default_start_month)) + + request.session['halfyear_year'] = year + request.session['halfyear_start_month'] = start_month + + # Redirect back to where user came from, or to this page again + next_url = request.POST.get('next') or request.GET.get('next') or 'halfyear_settings' + return redirect(next_url) + + # Month choices for the dropdown + month_choices = [(i, calendar.month_name[i]) for i in range(1, 13)] + + context = { + 'available_years': available_years, + 'selected_year': int(default_year), + 'selected_start_month': int(default_start_month), + 'month_choices': month_choices, + } + return render(request, 'halfyear_settings.html', context) \ No newline at end of file diff --git a/sheets/old/views_v2.py b/sheets/old/views_v2.py new file mode 100644 index 0000000..086c96e --- /dev/null +++ b/sheets/old/views_v2.py @@ -0,0 +1,4520 @@ +from django.shortcuts import render, redirect +from django.utils.decorators import method_decorator +from django.views.decorators.csrf import csrf_exempt +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, MonthlySummary +) +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 +import json +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 build_halfyear_window(interval_year: int, start_month: int): + """ + Build a list of (year, month) for the 6-month interval, possibly crossing into the next year. + Example: (2025, 10) -> [(2025,10), (2025,11), (2025,12), (2026,1), (2026,2), (2026,3)] + """ + window = [] + for offset in range(6): + total_index = (start_month - 1) + offset # 0-based + y = interval_year + (total_index // 12) + m = (total_index % 12) + 1 + window.append((y, m)) + return window +# --------------------------------------------------------------------------- +# Halbjahres-Bilanz helpers +# --------------------------------------------------------------------------- + +# You can adjust these indices if needed. +# Assuming: +# - bottom_1.table has row "Gasbestand" at some fixed row index, +# and columns: ... Nm³, Lit. LHe +GASBESTAND_ROW_INDEX = 9 # <-- adjust if your bottom_1 has a different row index +GASBESTAND_COL_NM3 = 3 # <-- adjust to the column index for Nm³ in bottom_1 + +# In top_left / top_right, "Bestand in Kannen-1 (Lit. L-He)" is row_index 5 +BESTAND_KANNEN_ROW_INDEX = 5 + +def pick_sheet_by_gasbestand(window, sheets_by_ym, prev_sheet): + """ + Returns the last sheet in the window whose Gasbestand (J36, Nm³ column) != 0. + If none found, returns prev_sheet (Übertrag_Dez__Vorjahr equivalent). + """ + for (y, m) in reversed(window): + sheet = sheets_by_ym.get((y, m)) + if not sheet: + continue + gasbestand_nm3 = get_bottom1_value(sheet, GASBESTAND_ROW_INDEX, GASBESTAND_COL_NM3) + if gasbestand_nm3 != 0: + return sheet + return prev_sheet +def get_bottom1_value(sheet, row_index: int, col_index: int) -> Decimal: + """Get a numeric value from bottom_1, or 0 if missing.""" + if sheet is None: + return Decimal('0') + + cell = Cell.objects.filter( + sheet=sheet, + table_type='bottom_1', + row_index=row_index, + column_index=col_index, + ).first() + + if cell is None or cell.value in (None, ''): + return Decimal('0') + + try: + return Decimal(str(cell.value)) + except Exception: + return Decimal('0') + +# MUST match the column order in your monthly_sheets top-right table + + + +def get_top_right_value(sheet, client_name: str, row_index: int) -> Decimal: + """ + Read a numeric value from the top_right table of a MonthlySheet for + a given client (by column) and row_index. + + top_right cells are keyed by (sheet, table_type='top_right', + row_index, column_index), where column_index is the position of the + client in HALFYEAR_RIGHT_CLIENTS. + """ + if sheet is None: + return Decimal('0') + + col_index = RIGHT_CLIENT_INDEX.get(client_name) + if col_index is None: + return Decimal('0') + + cell = Cell.objects.filter( + sheet=sheet, + table_type='top_right', + row_index=row_index, + column_index=col_index, + ).first() + + if cell is None or cell.value in (None, ''): + return Decimal('0') + + try: + return Decimal(str(cell.value)) + except Exception: + return Decimal('0') + +TR_RUECKF_FLUESSIG_ROW = 2 # confirmed by your March value 172.840560 +TR_BESTAND_KANNEN_ROW = 5 # confirmed by your earlier query +def get_bestand_kannen_for_month(sheet, client_name: str) -> Decimal: + """ + 'B9' in your description: Bestand in Kannen-1 (Lit. L-He) + For this implementation we take it from top_left row_index = 5 for that client. + """ + return get_top_left_value(sheet, client_name, row_index=BESTAND_KANNEN_ROW_INDEX) + +from decimal import Decimal +from django.db.models import Sum +from django.db.models.functions import Coalesce +from django.db.models import DecimalField, Value + +from .models import MonthlySheet, SecondTableEntry, Client, Cell +from django.shortcuts import redirect, render + +# You already have HALFYEAR_CLIENTS for the left table (AG Vogel, AG Halfm, IKP) +HALFYEAR_CLIENTS = ["AG Vogel", "AG Halfm", "IKP"] + +# NEW: clients for the top-right half-year table +HALFYEAR_RIGHT_CLIENTS = [ + "Dr. Fohrer", + "AG Buntk.", + "AG Alff", + "AG Gutfl.", + "M3 Thiele", + "M3 Buntkowsky", + "M3 Gutfleisch", +] +BOTTOM1_COL_VOLUME = 0 +BOTTOM1_COL_BAR = 1 +BOTTOM1_COL_KORR = 2 +BOTTOM1_COL_NM3 = 3 +BOTTOM1_COL_LHE = 4 +BOTTOM2_ROW_ANLAGE = 0 +BOTTOM2_COL_G39 = 0 # "Gefäss 2,5" (cell id shows column_index=0) +BOTTOM2_COL_I39 = 1 # "Gefäss 1,0" (cell id shows column_index=1) +BOTTOM2_ROW_INPUTS = { + "g39": (0, 0), # row_index=0, column_index=0 (your G39) + "i39": (0, 1), # row_index=0, column_index=1 (your I39) +} +FACTOR_NM3_TO_LHE = Decimal("0.75") +RIGHT_CLIENT_INDEX = {name: idx for idx, name in enumerate(HALFYEAR_RIGHT_CLIENTS)} +def halfyear_balance_view(request): + """ + Read-only Halbjahres-Bilanz view. + + LEFT table: AG Vogel / AG Halfm / IKP (exactly as in your last working version) + RIGHT table: Dr. Fohrer / AG Buntk. / AG Alff / AG Gutfl. / + M3 Thiele / M3 Buntkowsky / M3 Gutfleisch + using the Excel formulas you described. + + Uses the global 6-month interval from the main page (clients_list). + """ + # 1) Read half-year interval from the session + interval_year = request.session.get('halfyear_year') + interval_start = request.session.get('halfyear_start_month') + + if not interval_year or not interval_start: + # No interval chosen yet -> redirect to main page + return redirect('clients_list') + + interval_year = int(interval_year) + interval_start = int(interval_start) + + # You already have this helper in your code + window = build_halfyear_window(interval_year, interval_start) + # window = [(y1, m1), (y2, m2), ..., (y6, m6)] + + # (Year, month) of the first month + start_year, start_month = window[0] + + # Previous month (for "Stand ... (Vorjahr)" and "Best. in Kannen Vormonat") + prev_total_index = (start_month - 1) - 1 # one month back, 0-based + if prev_total_index >= 0: + prev_year = start_year + (prev_total_index // 12) + prev_month = (prev_total_index % 12) + 1 + else: + prev_year = start_year - 1 + prev_month = 12 + + # Load MonthlySheet objects for the window and for the previous month + sheets_by_ym = {} + for (y, m) in window: + sheet = MonthlySheet.objects.filter(year=y, month=m).first() + sheets_by_ym[(y, m)] = sheet + + prev_sheet = MonthlySheet.objects.filter(year=prev_year, month=prev_month).first() + def pick_bottom2_from_window(window, sheets_by_ym, prev_sheet): + # choose sheet (same logic you already use) + chosen = None + for (y, m) in reversed(window): + s = sheets_by_ym.get((y, m)) + # use your existing condition for choosing month + if s: + chosen = s + break + if chosen is None: + chosen = prev_sheet + + # Now read the two inputs safely + bottom2_inputs = {} + for key, (row_idx, col_idx) in BOTTOM2_ROW_INPUTS.items(): + bottom2_inputs[key] = get_bottom2_value(chosen, row_idx, col_idx) + + return chosen, bottom2_inputs + + + chosen_sheet_bottom2, bottom2_inputs = pick_bottom2_from_window(window, sheets_by_ym, prev_sheet) + bottom2_g39 = bottom2_inputs["g39"] + bottom2_i39 = bottom2_inputs["i39"] + # ---------------------------- + # HALF-YEAR BOTTOM TABLE 1 (Bilanz) - Read only + # ---------------------------- + chosen_sheet_bottom1 = pick_sheet_by_gasbestand(window, sheets_by_ym, prev_sheet) + + # IMPORTANT: define which bottom_1 row_index corresponds to Excel rows 27..35 + # If your bottom_1 starts at Excel row 27 => row_index 0 == Excel 27 + # then row_index = excel_row - 27 + BOTTOM1_EXCEL_START_ROW = 27 + + bottom1_excel_rows = list(range(27, 37)) # 27..36 + BOTTOM1_LABELS = [ + "Batterie 1", + "2", + "3", + "4", + "5", + "Batterie Links", + "2 Bündel", + "2 Ballone", + "Reingasspeicher", + "Gasbestand", + ] + + BOTTOM1_VOLUMES = [ + Decimal("2.4"), + Decimal("5.1"), + Decimal("4.0"), + Decimal("1.0"), + Decimal("4.0"), + Decimal("0.6"), + Decimal("1.2"), + Decimal("20.0"), + Decimal("5.0"), + None, # Gasbestand row has no volume + ] + nm3_sum_27_35 = Decimal("0") + lhe_sum_27_35 = Decimal("0") + bottom1_rows = [] + + for excel_row in bottom1_excel_rows: + row_index = excel_row - BOTTOM1_EXCEL_START_ROW + + chosen_sheet_bottom1 = None + for (y, m) in reversed(window): + s = sheets_by_ym.get((y, m)) + gasbestand = get_bottom1_value(s, GASBESTAND_ROW_INDEX, GASBESTAND_COL_NM3) # J36 (Nm3) + if gasbestand != 0: + chosen_sheet_bottom1 = s + break + + if chosen_sheet_bottom1 is None: + chosen_sheet_bottom1 = prev_sheet + + # Normal rows (27..35): read from chosen sheet and accumulate sums + if excel_row != 36: + nm3_val = get_bottom1_value(chosen_sheet_bottom1, row_index, BOTTOM1_COL_NM3) + lhe_val = get_bottom1_value(chosen_sheet_bottom1, row_index, BOTTOM1_COL_LHE) + + nm3_sum_27_35 += nm3_val + lhe_sum_27_35 += lhe_val + + bottom1_rows.append({ + "label": BOTTOM1_LABELS[row_index], + "volume": BOTTOM1_VOLUMES[row_index], + "bar": get_bottom1_value(chosen_sheet_bottom1, row_index, BOTTOM1_COL_BAR), + "korr": get_bottom1_value(chosen_sheet_bottom1, row_index, BOTTOM1_COL_KORR), + "nm3": nm3_val, + "lhe": lhe_val, + }) + + # Gasbestand row (36): show sums (J36 = SUM(J27:J35), K36 = SUM(K27:K35)) + else: + bottom1_rows.append({ + "label": "Gasbestand", + "volume": "", + "bar": "", + "korr": "", + "nm3": nm3_sum_27_35, + "lhe": lhe_sum_27_35, + }) + start_sheet = sheets_by_ym.get((start_year, start_month)) + # ------------------------------------------------------------ + # Bottom Table 2 (Halbjahres Bilanz) – server-side recalcBottom2() + # ------------------------------------------------------------ + + FACTOR_BT2 = Decimal("0.75") + + # 1) Helper: pick last-nonzero value of bottom_2 row0 col0/col1 from the window (fallback: prev_sheet) + def pick_last_nonzero_bottom2(row_index: int, col_index: int) -> Decimal: + # Scan from last month in window backwards + for (y, m) in reversed(window): + s = sheets_by_ym.get((y, m)) + if not s: + continue + v = get_bottom2_value(s, row_index, col_index) + if v is not None and v != 0: + return v + # fallback to month before window + v_prev = get_bottom2_value(prev_sheet, row_index, col_index) + return v_prev if v_prev is not None else Decimal("0") + + # 2) K38 comes from Overall Summary: "Summe Bestand (Lit. L-He)" + # Find it from your already built overall summary rows list. + + k38 = Decimal("0") + j38 = Decimal("0") + # 3) Inputs G39 / I39 (picked from last non-zero month in window) + g39 = pick_last_nonzero_bottom2(row_index=0, col_index=0) # G39 + i39 = pick_last_nonzero_bottom2(row_index=0, col_index=1) # I39 + + k39 = (g39 or Decimal("0")) + (i39 or Decimal("0")) + j39 = k39 * FACTOR_BT2 + + # 4) +Kaltgas (row 40) + # JS: + # g40 = (2500 - g39)/100*10 + # i40 = (1000 - i39)/100*10 + g40 = None + i40 = None + if g39 is not None: + g40 = (Decimal("2500") - g39) / Decimal("100") * Decimal("10") + if i39 is not None: + i40 = (Decimal("1000") - i39) / Decimal("100") * Decimal("10") + + k40 = (g40 or Decimal("0")) + (i40 or Decimal("0")) + j40 = k40 * FACTOR_BT2 + + # 5) Bestand flüssig He (row 43) + k43 = ( + (k38 or Decimal("0")) + + (k39 or Decimal("0")) + + (k40 or Decimal("0")) + ) + j43 = k43 * FACTOR_BT2 + + # 6) Gesamtbestand neu (row 44) = Gasbestand(Lit) from Bottom Table 1 + k43 + gasbestand_lit = Decimal("0") + for r in bottom1_rows: + if (r.get("label") or "").strip().startswith("Gasbestand"): + gasbestand_lit = r.get("lhe") or Decimal("0") + break + + k44 = (gasbestand_lit or Decimal("0")) + (k43 or Decimal("0")) + j44 = k44 * FACTOR_BT2 + + bottom2 = { + "j38": j38, "k38": k38, + "g39": g39, "i39": i39, "j39": j39, "k39": k39, + "g40": g40, "i40": i40, "j40": j40, "k40": k40, + "j43": j43, "k43": k43, + "j44": j44, "k44": k44, + } + + # ------------------------------------------------------------------ + # 2) LEFT TABLE (your existing, working logic) + # ------------------------------------------------------------------ + HALFYEAR_CLIENTS_LEFT = ["AG Vogel", "AG Halfm", "IKP"] + + # We'll collect client-wise values first for clarity. + client_data_left = {name: {} for name in HALFYEAR_CLIENTS_LEFT} + + # --- Row B3: Stand der Gaszähler (Nm³) + # = MAX(B3 from previous month, and B3 from each of the 6 months in the window) + # row_index 0 in top_left = "Stand der Gaszähler (Nm³)" + months_for_max = [(prev_year, prev_month)] + window + + for cname in HALFYEAR_CLIENTS_LEFT: + max_val = Decimal('0') + for (y, m) in months_for_max: + sheet = sheets_by_ym.get((y, m)) + if sheet is None and (y, m) == (prev_year, prev_month): + sheet = prev_sheet + val_b3 = get_top_left_value(sheet, cname, row_index=0) + if val_b3 > max_val: + max_val = val_b3 + client_data_left[cname]['stand_gas'] = max_val + + # --- Row B4: Stand der Gaszähler (Vorjahr) (Nm³) -> previous month same row --- + for cname in HALFYEAR_CLIENTS_LEFT: + val_b4 = get_top_left_value(prev_sheet, cname, row_index=0) + client_data_left[cname]['stand_gas_prev'] = val_b4 + + # --- Row B5: Gasrückführung (Nm³) = B3 - B4 --- + for cname in HALFYEAR_CLIENTS_LEFT: + b3 = client_data_left[cname]['stand_gas'] + b4 = client_data_left[cname]['stand_gas_prev'] + client_data_left[cname]['gasrueckf'] = b3 - b4 + + # --- Row B6: Rückführung flüssig (Lit. L-He) = B5 / 0.75 --- + for cname in HALFYEAR_CLIENTS_LEFT: + b5 = client_data_left[cname]['gasrueckf'] + client_data_left[cname]['rueckf_fluessig'] = (b5 / Decimal('0.75')) if b5 != 0 else Decimal('0') + + # --- Row B7: Sonderrückführungen (Lit. L-He) = sum over 6 months of that row --- + # That row index is 4 in your top_left table. + for cname in HALFYEAR_CLIENTS_LEFT: + sonder_total = Decimal('0') + for (y, m) in window: + sheet = sheets_by_ym.get((y, m)) + if sheet: + sonder_total += get_top_left_value(sheet, cname, row_index=4) + client_data_left[cname]['sonder'] = sonder_total + + # --- Row B8: Bestand in Kannen-1 (Lit. L-He) --- + # Excel-style logic with Gasbestand (J36) and fallback to previous month. + for cname in HALFYEAR_CLIENTS_LEFT: + chosen_value = None + + # Go from last month (window[5]) backwards to first (window[0]) + for (y, m) in reversed(window): + sheet = sheets_by_ym.get((y, m)) + gasbestand = get_bottom1_value(sheet, GASBESTAND_ROW_INDEX, GASBESTAND_COL_NM3) + if gasbestand != 0: + chosen_value = get_bestand_kannen_for_month(sheet, cname) + break + + # If still None -> use previous month (Übertrag_Dez__Vorjahr equivalent) + if chosen_value is None: + sheet_prev = prev_sheet + chosen_value = get_bestand_kannen_for_month(sheet_prev, cname) + + client_data_left[cname]['bestand_kannen'] = chosen_value if chosen_value is not None else Decimal('0') + + # --- Row B9: Summe Bestand (Lit. L-He) = equal to previous row --- + for cname in HALFYEAR_CLIENTS_LEFT: + client_data_left[cname]['summe_bestand'] = client_data_left[cname]['bestand_kannen'] + + # --- Row B10: Best. in Kannen Vormonat (Lit. L-He) + # = Bestand in Kannen-1 from the month BEFORE the window (prev_year, prev_month) + for cname in HALFYEAR_CLIENTS_LEFT: + client_data_left[cname]['best_kannen_vormonat'] = get_bestand_kannen_for_month(prev_sheet, cname) + + # --- Row B13: Bezug (Liter L-He) --- + for cname in HALFYEAR_CLIENTS_LEFT: + total_bezug = Decimal('0') + for (y, m) in window: + qs = SecondTableEntry.objects.filter( + client__name=cname, + date__year=y, + date__month=m, + ).aggregate( + total=Coalesce(Sum('lhe_output'), Value(0, output_field=DecimalField())) + ) + total_bezug += Decimal(str(qs['total'])) + client_data_left[cname]['bezug'] = total_bezug + + # --- Row B14: Rückführ. Soll (Lit. L-He) = Bezug - Summe Bestand + Best. in Kannen Vormonat --- + for cname in HALFYEAR_CLIENTS_LEFT: + b13 = client_data_left[cname]['bezug'] + b11 = client_data_left[cname]['summe_bestand'] + b12 = client_data_left[cname]['best_kannen_vormonat'] + client_data_left[cname]['rueckf_soll'] = b13 - b11 + b12 + + # --- Row B15: Verluste (Soll-Rückf.) (Lit. L-He) = B14 - B6 --- + for cname in HALFYEAR_CLIENTS_LEFT: + b14 = client_data_left[cname]['rueckf_soll'] + b6 = client_data_left[cname]['rueckf_fluessig'] + client_data_left[cname]['verluste'] = b14 - b6 + + # --- Row B16: Füllungen warm (Lit. L-He) = sum over 6 months (row_index=11) --- + for cname in HALFYEAR_CLIENTS_LEFT: + total_warm = Decimal('0') + for (y, m) in window: + sheet = sheets_by_ym.get((y, m)) + total_warm += get_top_left_value(sheet, cname, row_index=11) + client_data_left[cname]['fuellungen_warm'] = total_warm + + # --- Row B17: Kaltgas Rückgabe (Lit. L-He) = Bezug * 0.06 --- + factor = Decimal('0.06') + for cname in HALFYEAR_CLIENTS_LEFT: + b13 = client_data_left[cname]['bezug'] + client_data_left[cname]['kaltgas_rueckgabe'] = b13 * factor + + # --- Row B18: Verbraucherverluste (Liter L-He) = Verluste - Kaltgas Rückgabe --- + for cname in HALFYEAR_CLIENTS_LEFT: + b15 = client_data_left[cname]['verluste'] + b17 = client_data_left[cname]['kaltgas_rueckgabe'] + client_data_left[cname]['verbraucherverluste'] = b15 - b17 + + # --- Row B19: % = Verbraucherverluste / Bezug --- + for cname in HALFYEAR_CLIENTS_LEFT: + bezug = client_data_left[cname]['bezug'] + verb = client_data_left[cname]['verbraucherverluste'] + if bezug != 0: + client_data_left[cname]['percent'] = verb / bezug + else: + client_data_left[cname]['percent'] = None + + # Build LEFT rows structure + left_row_defs = [ + ('Stand der Gaszähler (Nm³)', 'stand_gas'), + ('Stand der Gaszähler (Vorjahr) (Nm³)', 'stand_gas_prev'), + ('Gasrückführung (Nm³)', 'gasrueckf'), + ('Rückführung flüssig (Lit. L-He)', 'rueckf_fluessig'), + ('Sonderrückführungen (Lit. L-He)', 'sonder'), + ('Bestand in Kannen-1 (Lit. L-He)', 'bestand_kannen'), + ('Summe Bestand (Lit. L-He)', 'summe_bestand'), + ('Best. in Kannen Vormonat (Lit. L-He)', 'best_kannen_vormonat'), + ('Bezug (Liter L-He)', 'bezug'), + ('Rückführ. Soll (Lit. L-He)', 'rueckf_soll'), + ('Verluste (Soll-Rückf.) (Lit. L-He)', 'verluste'), + ('Füllungen warm (Lit. L-He)', 'fuellungen_warm'), + ('Kaltgas Rückgabe (Lit. L-He) – Faktor', 'kaltgas_rueckgabe'), + ('Verbraucherverluste (Liter L-He)', 'verbraucherverluste'), + ('%', 'percent'), + ] + + rows_left = [] + for label, key in left_row_defs: + values = [client_data_left[cname][key] for cname in HALFYEAR_CLIENTS_LEFT] + if key == 'percent': + total_bezug = sum((client_data_left[c]['bezug'] for c in HALFYEAR_CLIENTS_LEFT), Decimal('0')) + total_verb = sum((client_data_left[c]['verbraucherverluste'] for c in HALFYEAR_CLIENTS_LEFT), Decimal('0')) + total = (total_verb / total_bezug) if total_bezug != 0 else None + else: + total = sum((v for v in values if v is not None), Decimal('0')) + rows_left.append({ + 'label': label, + 'values': values, + 'total': total, + 'is_percent': key == 'percent', + }) + + # ------------------------------------------------------------------ + # 3) RIGHT TABLE (top-right half-year aggregation) + # ------------------------------------------------------------------ + RIGHT_CLIENTS = HALFYEAR_RIGHT_CLIENTS # for brevity + + right_data = {name: {} for name in RIGHT_CLIENTS} + + # --- Bezug (Liter L-He) for each right client (same as for left) --- + for cname in RIGHT_CLIENTS: + total_bezug = Decimal('0') + for (y, m) in window: + qs = SecondTableEntry.objects.filter( + client__name=cname, + date__year=y, + date__month=m, + ).aggregate( + total=Coalesce(Sum('lhe_output'), Value(0, output_field=DecimalField())) + ) + total_bezug += Decimal(str(qs['total'])) + right_data[cname]['bezug'] = total_bezug + def find_bestand_from_window(reference_client: str) -> Decimal: + """ + Implements: + WENN(last_month!J36=0; WENN(prev_month!J36=0; ...; prev_sheet!9); last_month!9) + reference_client decides which column (L/N/P/Q/R) we read from monthly top_right row_index=5. + """ + # scan backward through window + for (y, m) in reversed(window): + sh = sheets_by_ym.get((y, m)) + if not sh: + continue + gasbestand = get_bottom1_value(sh, GASBESTAND_ROW_INDEX, GASBESTAND_COL_NM3) + if gasbestand != 0: + return get_top_right_value(sh, reference_client, TR_BESTAND_KANNEN_ROW) + + # fallback to previous month (Übertrag_Dez__Vorjahr equivalent) + return get_top_right_value(prev_sheet, reference_client, TR_BESTAND_KANNEN_ROW) + + # Fohrer+Buntk merged: BOTH use Fohrer column (L9) + val_L = find_bestand_from_window("Dr. Fohrer") + right_data["Dr. Fohrer"]["bestand_kannen"] = val_L + right_data["AG Buntk."]["bestand_kannen"] = val_L + + # Alff+Gutfl merged: BOTH use Alff column (N9) + val_N = find_bestand_from_window("AG Alff") + right_data["AG Alff"]["bestand_kannen"] = val_N + right_data["AG Gutfl."]["bestand_kannen"] = val_N + + # M3 each uses its own column (P9/Q9/R9) + right_data["M3 Thiele"]["bestand_kannen"] = find_bestand_from_window("M3 Thiele") + right_data["M3 Buntkowsky"]["bestand_kannen"] = find_bestand_from_window("M3 Buntkowsky") + right_data["M3 Gutfleisch"]["bestand_kannen"] = find_bestand_from_window("M3 Gutfleisch") + # Helper for pair shares (L13/($L13+$M13), etc.) + def pair_share(c1, c2): + total = right_data[c1]['bezug'] + right_data[c2]['bezug'] + if total == 0: + return (Decimal('0'), Decimal('0')) + return ( + right_data[c1]['bezug'] / total, + right_data[c2]['bezug'] / total, + ) + + # --- "Stand der Gaszähler (Vorjahr) (Nm³)" row: share based on Bezug --- + # Dr. Fohrer / AG Buntk. + s_fohrer, s_buntk = pair_share("Dr. Fohrer", "AG Buntk.") + right_data["Dr. Fohrer"]['stand_prev_share'] = s_fohrer + right_data["AG Buntk."]['stand_prev_share'] = s_buntk + + # AG Alff / AG Gutfl. + s_alff, s_gutfl = pair_share("AG Alff", "AG Gutfl.") + right_data["AG Alff"]['stand_prev_share'] = s_alff + right_data["AG Gutfl."]['stand_prev_share'] = s_gutfl + + # M3 Thiele / M3 Buntkowsky / M3 Gutfleisch → empty in Excel → None + for cname in ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"]: + right_data[cname]['stand_prev_share'] = None + + # --- Rückführung flüssig per month (raw sums) --- + # top_right row_index=2 is "Rückführung flüssig (Lit. L-He)" + + # --- Sonderrückführungen (row_index=3 in top_right) --- + for cname in RIGHT_CLIENTS: + sonder_total = Decimal('0') + for (y, m) in window: + sheet = sheets_by_ym.get((y, m)) + if sheet: + sonder_total += get_top_right_value(sheet, cname, row_index=3) + right_data[cname]['sonder'] = sonder_total + + # --- Sammelrückführung (row_index=4 in top_right), grouped & merged --- + # Group 1: Dr. Fohrer + AG Buntk. + group1_total = Decimal('0') + for (y, m) in window: + sheet = sheets_by_ym.get((y, m)) + if sheet: + group1_total += get_top_right_value(sheet, "Dr. Fohrer", row_index=4) + right_data["Dr. Fohrer"]['sammel'] = group1_total + right_data["AG Buntk."]['sammel'] = group1_total + + # Group 2: AG Alff + AG Gutfl. + group2_total = Decimal('0') + for (y, m) in window: + sheet = sheets_by_ym.get((y, m)) + if sheet: + group2_total += get_top_right_value(sheet, "AG Alff", row_index=4) + right_data["AG Alff"]['sammel'] = group2_total + right_data["AG Gutfl."]['sammel'] = group2_total + + # Group 3: M3 Thiele + M3 Buntkowsky + M3 Gutfleisch + group3_total = Decimal('0') + for (y, m) in window: + sheet = sheets_by_ym.get((y, m)) + if sheet: + group3_total += get_top_right_value(sheet, "M3 Thiele", row_index=4) + right_data["M3 Thiele"]['sammel'] = group3_total + right_data["M3 Buntkowsky"]['sammel'] = group3_total + right_data["M3 Gutfleisch"]['sammel'] = group3_total + def safe_div(a: Decimal, b: Decimal) -> Decimal: + return (a / b) if b != 0 else Decimal("0") + + # --- Rückführung flüssig (Lit. L-He) for Halbjahres-Bilanz top-right --- + # Uses your exact formulas. + + # 1) Fohrer / Buntk split by BEZUG share times group SAMMEL (L8) + L13 = right_data["Dr. Fohrer"]["bezug"] + M13 = right_data["AG Buntk."]["bezug"] + L8 = right_data["Dr. Fohrer"]["sammel"] # merged group total + + den = (L13 + M13) + right_data["Dr. Fohrer"]["rueckf_fluessig"] = (safe_div(L13, den) * L8) if den != 0 else Decimal("0") + right_data["AG Buntk."]["rueckf_fluessig"] = (safe_div(M13, den) * L8) if den != 0 else Decimal("0") + + # 2) Alff / Gutfl split by BEZUG share times group SAMMEL (N8) + N13 = right_data["AG Alff"]["bezug"] + O13 = right_data["AG Gutfl."]["bezug"] + N8 = right_data["AG Alff"]["sammel"] # merged group total + + den = (N13 + O13) + right_data["AG Alff"]["rueckf_fluessig"] = (safe_div(N13, den) * N8) if den != 0 else Decimal("0") + right_data["AG Gutfl."]["rueckf_fluessig"] = (safe_div(O13, den) * N8) if den != 0 else Decimal("0") + + # 3) M3 Thiele = sum of monthly Rückführung flüssig (monthly top_right row_index=2) over window + P6_sum = Decimal("0") + for (y, m) in window: + sh = sheets_by_ym.get((y, m)) + P6_sum += get_top_right_value(sh, "M3 Thiele", TR_RUECKF_FLUESSIG_ROW) + right_data["M3 Thiele"]["rueckf_fluessig"] = P6_sum + + # 4) M3 Buntkowsky / M3 Gutfleisch split by BEZUG share times M3-group SAMMEL (P8) + P13 = right_data["M3 Thiele"]["bezug"] + Q13 = right_data["M3 Buntkowsky"]["bezug"] + R13 = right_data["M3 Gutfleisch"]["bezug"] + P8 = right_data["M3 Thiele"]["sammel"] # merged group total + + den = (P13 + Q13 + R13) + right_data["M3 Buntkowsky"]["rueckf_fluessig"] = (safe_div(Q13, den) * P8) if den != 0 else Decimal("0") + right_data["M3 Gutfleisch"]["rueckf_fluessig"] = (safe_div(R13, den) * P8) if den != 0 else Decimal("0") + # --- Bestand in Kannen-1 (Lit. L-He) for right table (grouped) --- + # Use Gasbestand (J36) and fallback logic, but now reading top_right B9 for each group. + TOP_RIGHT_ROW_BESTAND_KANNEN = 6 # <-- most likely correct in your setup + + def pick_bestand_top_right(base_client: str) -> Decimal: + # Go from last month in window backwards: if Gasbestand != 0, use that month's Bestand in Kannen + for (y, m) in reversed(window): + sh = sheets_by_ym.get((y, m)) + if not sh: + continue + + gasbestand = get_bottom1_value(sh, GASBESTAND_ROW_INDEX, GASBESTAND_COL_NM3) + if gasbestand != 0: + return get_top_right_value(sh, base_client, TOP_RIGHT_ROW_BESTAND_KANNEN) + + # Fallback to previous month (Übertrag_Dez__Vorjahr equivalent) + return get_top_right_value(prev_sheet, base_client, TOP_RIGHT_ROW_BESTAND_KANNEN) + + # Group 1 merged (Fohrer + Buntk.) + g1_best = pick_bestand_top_right("Dr. Fohrer") + right_data["Dr. Fohrer"]["bestand_kannen"] = g1_best + right_data["AG Buntk."]["bestand_kannen"] = g1_best + + # Group 2 merged (Alff + Gutfl.) + g2_best = pick_bestand_top_right("AG Alff") + right_data["AG Alff"]["bestand_kannen"] = g2_best + right_data["AG Gutfl."]["bestand_kannen"] = g2_best + + # Group 3 merged (M3 Thiele + M3 Buntkowsky + M3 Gutfleisch) + g3_best = pick_bestand_top_right("M3 Thiele") + right_data["M3 Thiele"]["bestand_kannen"] = g3_best + right_data["M3 Buntkowsky"]["bestand_kannen"] = g3_best + right_data["M3 Gutfleisch"]["bestand_kannen"] = g3_best + + # Summe Bestand = same as previous row + for cname in RIGHT_CLIENTS: + right_data[cname]['summe_bestand'] = right_data[cname]['bestand_kannen'] + + # Best. in Kannen Vormonat (Lit. L-He) from previous month top_right row_index=7 + g1_prev = get_top_right_value(prev_sheet, "Dr. Fohrer", TOP_RIGHT_ROW_BESTAND_KANNEN) + right_data["Dr. Fohrer"]['best_kannen_vormonat'] = g1_prev + right_data["AG Buntk."]['best_kannen_vormonat'] = g1_prev + + g2_prev = get_top_right_value(prev_sheet, "AG Alff", TOP_RIGHT_ROW_BESTAND_KANNEN) + right_data["AG Alff"]['best_kannen_vormonat'] = g2_prev + right_data["AG Gutfl."]['best_kannen_vormonat'] = g2_prev + + + # Group 1 merged (Fohrer + Buntk.) + g1_prev = get_top_right_value(prev_sheet, "Dr. Fohrer", TOP_RIGHT_ROW_BESTAND_KANNEN) + right_data["Dr. Fohrer"]["best_kannen_vormonat"] = g1_prev + right_data["AG Buntk."]["best_kannen_vormonat"] = g1_prev + + # Group 2 merged (Alff + Gutfl.) + g2_prev = get_top_right_value(prev_sheet, "AG Alff", TOP_RIGHT_ROW_BESTAND_KANNEN) + right_data["AG Alff"]["best_kannen_vormonat"] = g2_prev + right_data["AG Gutfl."]["best_kannen_vormonat"] = g2_prev + + # Group 3 UNMERGED (each one reads its own cell) + right_data["M3 Thiele"]["best_kannen_vormonat"] = get_top_right_value(prev_sheet, "M3 Thiele", TOP_RIGHT_ROW_BESTAND_KANNEN) + right_data["M3 Buntkowsky"]["best_kannen_vormonat"] = get_top_right_value(prev_sheet, "M3 Buntkowsky", TOP_RIGHT_ROW_BESTAND_KANNEN) + right_data["M3 Gutfleisch"]["best_kannen_vormonat"] = get_top_right_value(prev_sheet, "M3 Gutfleisch", TOP_RIGHT_ROW_BESTAND_KANNEN) + + # --- Rückführ. Soll (Lit. L-He) according to your formulas --- + + # Group 1: Dr. Fohrer / AG Buntk. + total_bestand_1 = right_data["Dr. Fohrer"]['summe_bestand'] + best_vormonat_1 = right_data["Dr. Fohrer"]['best_kannen_vormonat'] + diff1 = total_bestand_1 - best_vormonat_1 + share_fohrer = right_data["Dr. Fohrer"]['stand_prev_share'] or Decimal('0') + + right_data["Dr. Fohrer"]['rueckf_soll'] = ( + right_data["Dr. Fohrer"]['bezug'] - diff1 * share_fohrer + ) + right_data["AG Buntk."]['rueckf_soll'] = ( + right_data["AG Buntk."]['bezug'] - total_bestand_1 + best_vormonat_1 + ) + + # Group 2: AG Alff / AG Gutfl. + total_bestand_2 = right_data["AG Alff"]['summe_bestand'] + best_vormonat_2 = right_data["AG Alff"]['best_kannen_vormonat'] + diff2 = total_bestand_2 - best_vormonat_2 + share_alff = right_data["AG Alff"]['stand_prev_share'] or Decimal('0') + share_gutfl = right_data["AG Gutfl."]['stand_prev_share'] or Decimal('0') + + right_data["AG Alff"]['rueckf_soll'] = ( + right_data["AG Alff"]['bezug'] - diff2 * share_alff + ) + right_data["AG Gutfl."]['rueckf_soll'] = ( + right_data["AG Gutfl."]['bezug'] - diff2 * share_gutfl + ) + + # Group 3: M3 Thiele / M3 Buntkowsky / M3 Gutfleisch + for cname in ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"]: + b13 = right_data[cname]['bezug'] + b12 = right_data[cname]['best_kannen_vormonat'] + b11 = right_data[cname]['summe_bestand'] + # Excel: P13+P12-P11 etc. + right_data[cname]['rueckf_soll'] = b13 + b12 - b11 + + # --- Verluste (Soll-Rückf.) (Lit. L-He) = B14 - B6 - B7 --- + for cname in RIGHT_CLIENTS: + b14 = right_data[cname]['rueckf_soll'] + b6 = right_data[cname]['rueckf_fluessig'] + b7 = right_data[cname]['sonder'] + right_data[cname]['verluste'] = b14 - b6 - b7 + + # --- Füllungen warm (Lit. L-He) = sum of monthly 'Füllungen warm' (row_index=11 top_right) --- + for cname in RIGHT_CLIENTS: + total_warm = Decimal('0') + for (y, m) in window: + sheet = sheets_by_ym.get((y, m)) + if sheet: + total_warm += get_top_right_value(sheet, cname, row_index=11) + right_data[cname]['fuellungen_warm'] = total_warm + + # --- Kaltgas Rückgabe (Lit. L-He) – Faktor = Bezug * 0.06 --- + for cname in RIGHT_CLIENTS: + b13 = right_data[cname]['bezug'] + right_data[cname]['kaltgas_rueckgabe'] = b13 * factor + + # --- Verbraucherverluste (Liter L-He) = Verluste - Kaltgas Rückgabe --- + for cname in RIGHT_CLIENTS: + b15 = right_data[cname]['verluste'] + b17 = right_data[cname]['kaltgas_rueckgabe'] + right_data[cname]['verbraucherverluste'] = b15 - b17 + + # --- % = Verbraucherverluste / Bezug --- + for cname in RIGHT_CLIENTS: + bezug = right_data[cname]['bezug'] + verb = right_data[cname]['verbraucherverluste'] + if bezug != 0: + right_data[cname]['percent'] = verb / bezug + else: + right_data[cname]['percent'] = None + + # Build RIGHT rows structure + right_row_defs = [ + ('Stand der Gaszähler (Vorjahr) (Nm³)', 'stand_prev_share'), + # We skip the pure-text "Gasrückführung (Nm³)" line here, + # because it’s only text (Aufteilung nach Verbrauch / Gaszähler) + # and easier to render directly in the template if needed. + ('Rückführung flüssig (Lit. L-He)', 'rueckf_fluessig'), + ('Sonderrückführungen (Lit. L-He)', 'sonder'), + ('Sammelrückführung (Lit. L-He)', 'sammel'), + ('Bestand in Kannen-1 (Lit. L-He)', 'bestand_kannen'), + ('Summe Bestand (Lit. L-He)', 'summe_bestand'), + ('Best. in Kannen Vormonat (Lit. L-He)', 'best_kannen_vormonat'), + ('Bezug (Liter L-He)', 'bezug'), + ('Rückführ. Soll (Lit. L-He)', 'rueckf_soll'), + ('Verluste (Soll-Rückf.) (Lit. L-He)', 'verluste'), + ('Füllungen warm (Lit. L-He)', 'fuellungen_warm'), + ('Kaltgas Rückgabe (Lit. L-He) – Faktor', 'kaltgas_rueckgabe'), + ('Verbraucherverluste (Liter L-He)', 'verbraucherverluste'), + ('%', 'percent'), + ] + + rows_right = [] + for label, key in right_row_defs: + values = [right_data[cname].get(key) for cname in RIGHT_CLIENTS] + + if key == 'percent': + total_bezug = sum((right_data[c]['bezug'] for c in RIGHT_CLIENTS), Decimal('0')) + total_verb = sum((right_data[c]['verbraucherverluste'] for c in RIGHT_CLIENTS), Decimal('0')) + total = (total_verb / total_bezug) if total_bezug != 0 else None + else: + total = sum((v for v in values if isinstance(v, Decimal)), Decimal('0')) + + rows_right.append({ + 'label': label, + 'values': values, + 'total': total, + 'is_percent': key == 'percent', + }) + SUM_TABLE_ROWS = [ + ("Rückführung flüssig (Lit. L-He)", "rueckf_fluessig"), + ("Sonderrückführungen (Lit. L-He)", "sonder"), + ("Sammelrückführungen (Lit. L-He)", "sammel"), + ("Bestand in Kannen-1 (Lit. L-He)", "bestand_kannen"), + ("Summe Bestand (Lit. L-He)", "summe_bestand"), + ("Best. in Kannen Vormonat (Lit. L-He)", "best_kannen_vormonat"), + ("Bezug (Liter L-He)", "bezug"), + ("Rückführ. Soll (Lit. L-He)", "rueckf_soll"), + ("Verluste (Soll-Rückf.) (Lit. L-He)", "verluste"), + ("Füllungen warm (Lit. L-He)", "fuellungen_warm"), + ("Kaltgas Rückgabe (Lit. L-He) – Faktor", "kaltgas_rueckgabe"), + ("Faktor 0.06", "factor_row"), + ("Verbraucherverluste (Liter L-He)", "verbraucherverluste"), + ("%", "percent"), + ] + + RIGHT_GROUPS = { + "chemie": ["Dr. Fohrer", "AG Buntk."], + "mawi": ["AG Alff", "AG Gutfl."], + "m3": ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"], + } + + RIGHT_ALL = ["Dr. Fohrer", "AG Buntk.", "AG Alff", "AG Gutfl.", "M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"] + LEFT_ALL = HALFYEAR_CLIENTS_LEFT + + def safe_pct(verb, bez): + return (verb / bez) if bez != 0 else None + rows_sum = [] + def d(x): + return x if isinstance(x, Decimal) else Decimal("0") + for label, key in SUM_TABLE_ROWS: + + if key == "factor_row": + lichtwiese = chemie = mawi = m3 = total = Decimal("0.06") + + elif key == "percent": + # Right totals + rw_bez = sum(d(right_data[c].get("bezug")) for c in RIGHT_ALL) + rw_verb = sum(d(right_data[c].get("verbraucherverluste")) for c in RIGHT_ALL) + lichtwiese = safe_pct(rw_verb, rw_bez) + + # Chemie + ch_bez = sum(d(right_data[c].get("bezug")) for c in RIGHT_GROUPS["chemie"]) + ch_verb = sum(d(right_data[c].get("verbraucherverluste")) for c in RIGHT_GROUPS["chemie"]) + chemie = safe_pct(ch_verb, ch_bez) + + # MaWi + mw_bez = sum(d(right_data[c].get("bezug")) for c in RIGHT_GROUPS["mawi"]) + mw_verb = sum(d(right_data[c].get("verbraucherverluste")) for c in RIGHT_GROUPS["mawi"]) + mawi = safe_pct(mw_verb, mw_bez) + + # M3 + m3_bez = sum(d(right_data[c].get("bezug")) for c in RIGHT_GROUPS["m3"]) + m3_verb = sum(d(right_data[c].get("verbraucherverluste")) for c in RIGHT_GROUPS["m3"]) + m3 = safe_pct(m3_verb, m3_bez) + + # Σ column = (left verb + right verb) / (left bez + right bez) + left_bez = sum(d(client_data_left[c].get("bezug")) for c in LEFT_ALL) + left_verb = sum(d(client_data_left[c].get("verbraucherverluste")) for c in LEFT_ALL) + total = safe_pct(left_verb + rw_verb, left_bez + rw_bez) + + else: + # normal rows = sums + lichtwiese = sum(d(right_data[c].get(key)) for c in RIGHT_ALL) + chemie = sum(d(right_data[c].get(key)) for c in RIGHT_GROUPS["chemie"]) + mawi = sum(d(right_data[c].get(key)) for c in RIGHT_GROUPS["mawi"]) + m3 = sum(d(right_data[c].get(key)) for c in RIGHT_GROUPS["m3"]) + + left_total = sum(d(client_data_left[c].get(key)) for c in LEFT_ALL) + total = left_total + lichtwiese + + rows_sum.append({ + "row_index": row_index, + "label": label, + "total": total, + "lichtwiese": lichtwiese, + "chemie": chemie, + "mawi": mawi, + "m3": m3, + "is_percent": (key == "percent"), + }) + def find_sum_row(rows, label_startswith: str): + for r in rows: + if str(r.get("label", "")).strip().startswith(label_startswith): + return r + return None + + summe_bestand_row = find_sum_row(rows_sum, "Summe Bestand") + k38 = (summe_bestand_row.get("total") if summe_bestand_row else Decimal("0")) or Decimal("0") + j38 = k38 * Decimal("0.75") + # --- FIX: now that k38 is known, update bottom2 + recompute dependent rows --- + bottom2["k38"] = k38 + bottom2["j38"] = j38 + + k39 = bottom2.get("k39") or Decimal("0") + k40 = bottom2.get("k40") or Decimal("0") + + # Row 43: Bestand flüssig He = SUMME(K38:K40) + k43 = (k38 or Decimal("0")) + k39 + k40 + j43 = k43 * Decimal("0.75") + + bottom2["k43"] = k43 + bottom2["j43"] = j43 + + # Row 44: Gesamtbestand neu = Gasbestand(Lit) from bottom table 1 + k43 + gasbestand_lit = Decimal("0") + for r in bottom1_rows: + if (r.get("label") or "").strip().startswith("Gasbestand"): + gasbestand_lit = r.get("lhe") or Decimal("0") + break + + k44 = gasbestand_lit + k43 + j44 = k44 * Decimal("0.75") + + bottom2["k44"] = k44 + bottom2["j44"] = j44 + def d(x): + return x if isinstance(x, Decimal) else Decimal("0") + + # ---- Bottom2: J38/K38 depend on rows_sum (overall summary), so do it HERE ---- + k38 = Decimal("0") + for r in rows_sum: + if r.get("label") == "Summe Bestand (Lit. L-He)": + k38 = r.get("total") or Decimal("0") + break + + j38 = k38 * FACTOR_NM3_TO_LHE # 0.75 + bottom2["k38"] = k38 + bottom2["j38"] = j38 + factor = Decimal("0.75") + + # window = the 6-month list you already build in this view: [(y,m), (y,m), ...] + # bottom2 = dict with "k44" already computed in your view + # rows_sum = overall sum group rows list (your existing halfyear logic) + + # 1) K46 = K44 from the month BEFORE the global month (interval start) + start_year = interval_year + start_month = interval_start # whatever you named your start month variable + + if start_month == 1: + prev_year, prev_month = start_year - 1, 12 + else: + prev_year, prev_month = start_year, start_month - 1 + + prev_sheet = MonthlySheet.objects.filter(year=prev_year, month=prev_month).first() + + # This assumes you have MonthlySummary.gesamtbestand_neu_lhe as K44 equivalent. + # If your field name differs, tell me your MonthlySummary model fields. + prev_k44 = Decimal("0") + if prev_sheet: + prev_sum = MonthlySummary.objects.filter(sheet=prev_sheet).first() + if prev_sum and prev_sum.gesamtbestand_neu_lhe is not None: + prev_k44 = Decimal(str(prev_sum.gesamtbestand_neu_lhe)) + + # helpers: read bottom_3 values for a given sheet/month + def get_bottom3_value(sheet, row_index, col_index): + if not sheet: + return Decimal("0") + c = Cell.objects.filter( + sheet=sheet, table_type="bottom_3", + row_index=row_index, column_index=col_index + ).first() + if not c or c.value in (None, "", "None"): + return Decimal("0") + try: + return Decimal(str(c.value)) + except Exception: + return Decimal("0") + + # 2) Sum rows across the 6-month window + g47_sum = Decimal("0") + i47_sum = Decimal("0") + j47_sum = Decimal("0") + + g50_sum = Decimal("0") + i50_sum = Decimal("0") + + for (yy, mm) in window: + s = MonthlySheet.objects.filter(year=yy, month=mm).first() + ## Excel row 47 maps to bottom_3 row_index=1 in DB (see monthly_sheet.html) + g = get_bottom3_value(s, 1, 1) # G47 editable + i = get_bottom3_value(s, 1, 2) # I47 editable + + g47_sum += g + i47_sum += i + + # In monthly_sheet, J47 is CALCULATED as (G47 + I47), not stored as an editable cell + j47_sum += (g + i) + + # row 50: G(2), I(4) + g50_sum += get_bottom3_value(s, 50, 2) + i50_sum += get_bottom3_value(s, 50, 4) + + # 3) K52 = Verbraucherverlust from overall sum group first column (global Σ) + k52 = Decimal("0") + for r in rows_sum: + label = (r.get("label") or "") + if label.startswith("Verbraucherverluste"): + k52 = r.get("total") or Decimal("0") + break + + # --- apply the SAME monthly formulas, with your overrides --- + # Row 46 + k46 = prev_k44 + j46 = k46 * factor + + # Row 47 + g47 = g47_sum + i47 = i47_sum + j47 = j47_sum + k47 = (j47 / factor) + g47 if (j47 != 0 or g47 != 0) else Decimal("0") + + # Row 48 + k48 = k46 + k47 + j48 = k48 * factor + + # Row 49 (akt. Monat) -> in halfyear we use current bottom2 K44 + k49 = bottom2.get("k44") or Decimal("0") + j49 = k49 * factor + + # Row 50 + g50 = g50_sum + i50 = i50_sum + j50 = i50 + k50 = (j50 / factor) if j50 != 0 else Decimal("0") + + # Row 51 + k51 = k48 - k49 - k50 + j51 = k51 * factor + + # Row 52 + j52 = k52 * factor + + # Row 53 + k53 = k51 - k52 + j53 = j51 - j52 + + bottom3 = { + "j46": j46, "k46": k46, + "g47": g47, "i47": i47, "j47": j47, "k47": k47, + "j48": j48, "k48": k48, + "j49": j49, "k49": k49, + "g50": g50, "i50": i50, "j50": j50, "k50": k50, + "j51": j51, "k51": k51, + "j52": j52, "k52": k52, + "j53": j53, "k53": k53, + } + # ------------------------------------------------------------------ + # 4) Context – keep old keys AND new ones + # ------------------------------------------------------------------ + context = { + 'interval_year': interval_year, + 'interval_start_month': interval_start, + 'window': window, + + # Left table – old names (for your first template) + 'clients': HALFYEAR_CLIENTS_LEFT, + 'rows': rows_left, + + # Left table – explicit + 'clients_left': HALFYEAR_CLIENTS_LEFT, + 'rows_left': rows_left, + + # Right table + 'clients_right': RIGHT_CLIENTS, + 'rows_right': rows_right, + 'rows_sum': rows_sum, + 'bottom1_rows': bottom1_rows, + + } + context["bottom2"] = bottom2 + context["bottom3"] = bottom3 + context["context_bottom2_g39"] = bottom2_inputs["g39"] + context["context_bottom2_i39"] = bottom2_inputs["i39"] + return render(request, 'halfyear_balance.html', context) + +def get_bottom2_value(sheet, row_index: int, col_index: int) -> Decimal: + """Get numeric value from bottom_2 or 0 if missing.""" + if sheet is None: + return Decimal("0") + cell = Cell.objects.filter( + sheet=sheet, + table_type="bottom_2", + row_index=row_index, + column_index=col_index, + ).first() + if cell is None or cell.value in (None, ""): + return Decimal("0") + try: + return Decimal(str(cell.value)) + except Exception: + return Decimal("0") + +def get_top_left_value(sheet, client_name: str, row_index: int) -> Decimal: + """ + Read a numeric value from the top_left table for a given month, client and row. + Does NOT use column_index, because top_left is keyed only by client + row_index. + """ + if sheet is None: + return Decimal('0') + + client_obj = Client.objects.filter(name=client_name).first() + if not client_obj: + return Decimal('0') + + cell = Cell.objects.filter( + sheet=sheet, + table_type='top_left', + client=client_obj, + row_index=row_index, + ).first() + + if cell is None or cell.value in (None, ''): + return Decimal('0') + + try: + return Decimal(str(cell.value)) + except Exception: + return Decimal('0') +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", + ] + current_summary = MonthlySummary.objects.filter(sheet=sheet).first() + + # Get previous month summary (for Bottom Table 3: K46 = prev K44) + prev_month_info = self.get_prev_month(year, month) + prev_summary = None + if not is_start_sheet: + prev_sheet = MonthlySheet.objects.filter( + year=prev_month_info['year'], + month=prev_month_info['month'] + ).first() + if prev_sheet: + prev_summary = MonthlySummary.objects.filter(sheet=prev_sheet).first() + + context.update({ + # ... your existing context ... + 'current_summary': current_summary, + 'prev_summary': prev_summary, + }) + # Update row counts in build_group_rows function + # 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 + MERGED_ROWS = {2, 3, 5, 6, 7, 9, 10, 12, 14, 15} + MERGED_PAIRS = [ + ("Dr. Fohrer", "AG Buntk."), + ("AG Alff", "AG Gutfl."), + ] + rows = [] + + # Determine row count + row_counts = { + "top_left": 16, + "top_right": 16, # rows 0–15 + "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 + + # We will store row sums for Bezug (row 8) and Verbraucherverluste (row 14) + # for top_left / top_right so we can compute the overall % in row 15. + sum_bezug = None + sum_verbrauch = None + + # Build each row + 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 (includes editable + calculated cells) + sum_value = None + total = Decimal("0") + has_value = False + + merged_second_indices = set() + if table_type == 'top_right' and row_idx in MERGED_ROWS: + for left_name, right_name in MERGED_PAIRS: + try: + right_idx = client_names.index(right_name) + merged_second_indices.add(right_idx) + except ValueError: + # client not in this table; just ignore + pass + + for col_idx, cell in enumerate(display_cells): + # Skip the duplicate (second) column of each merged pair + if col_idx in merged_second_indices: + continue + + if cell and cell.value is not None: + try: + total += Decimal(str(cell.value)) + has_value = True + except Exception: + pass + + if has_value: + sum_value = total + + # Remember special rows for top tables + if table_type in ("top_left", "top_right"): + if row_idx == 8: # Bezug + sum_bezug = total + elif row_idx == 14: # Verbraucherverluste + sum_verbrauch = total + + rows.append( + { + "cells": display_cells, + "sum": sum_value, + "row_index": row_idx, + } + ) + + # Adjust the % row sum for top_left / top_right: + # Sum(%) = Sum(Verbraucherverluste) / Sum(Bezug) + if table_type in ("top_left", "top_right"): + perc_row_idx = 15 # % row + if 0 <= perc_row_idx < len(rows): + if sum_bezug is not None and sum_bezug != 0 and sum_verbrauch is not None: + rows[perc_row_idx]["sum"] = sum_verbrauch / sum_bezug + else: + rows[perc_row_idx]["sum"] = None + + 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) + + # --- Build combined summary of top-left + top-right Sum columns --- + + # Helper to safely get the Sum for a given row index + def get_row_sum(rows, row_index): + if 0 <= row_index < len(rows): + return rows[row_index].get('sum') + return None + + # Row definitions we want in the combined Σ table + # (row_index in top tables, label shown in the small table) + summary_row_defs = [ + (2, "Rückführung flüssig (Lit. L-He)"), + (3, "Sonderrückführungen (Lit. L-He)"), + (4, "Sammelrückführungen (Lit. L-He)"), + (5, "Bestand in Kannen-1 (Lit. L-He)"), + (6, "Summe Bestand (Lit. L-He)"), + (7, "Best. in Kannen Vormonat (Lit. L-He)"), + (8, "Bezug (Liter L-He)"), + (9, "Rückführ. Soll (Lit. L-He)"), + (10, "Verluste (Soll-Rückf.) (Lit. L-He)"), + (11, "Füllungen warm (Lit. L-He)"), + (12, "Kaltgas Rückgabe (Lit. L-He) – Faktor"), + (13, "Faktor 0.06"), + (14, "Verbraucherverluste (Liter L-He)"), + (15, "%"), + ] + + # Precompute totals for Bezug and Verbraucherverluste across both tables + bezug_left = get_row_sum(top_left_rows, 8) or Decimal('0') + bezug_right = get_row_sum(top_right_rows, 8) or Decimal('0') + total_bezug = bezug_left + bezug_right + + verb_left = get_row_sum(top_left_rows, 14) or Decimal('0') + verb_right = get_row_sum(top_right_rows, 14) or Decimal('0') + total_verbrauch = verb_left + verb_right + + summary_rows = [] + + for row_index, label in summary_row_defs: + # Faktor row: always fixed 0.06 + if row_index == 13: + summary_value = Decimal('0.06') + + # % row: total Verbraucherverluste / total Bezug + elif row_index == 15: + if total_bezug != 0: + summary_value = total_verbrauch / total_bezug + else: + summary_value = None + + else: + left_sum = get_row_sum(top_left_rows, row_index) + right_sum = get_row_sum(top_right_rows, row_index) + + # Sammelrückführungen: only from top-right table + if row_index == 4: + left_sum = None + + total = Decimal('0') + + has_any = False + if left_sum is not None: + total += Decimal(str(left_sum)) + has_any = True + if right_sum is not None: + total += Decimal(str(right_sum)) + has_any = True + + summary_value = total if has_any else None + + summary_rows.append({ + 'row_index': row_index, + 'label': label, + 'sum': summary_value, + }) + + # 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, + 'summary_rows': summary_rows, # 👈 NEW + '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 + + + 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 calculate_bottom_3_dependents(self, sheet): + updated_cells = [] + + def get_cell(row_idx, col_idx): + return Cell.objects.filter( + sheet=sheet, + table_type='bottom_3', + row_index=row_idx, + column_index=col_idx + ).first() + + def set_cell(row_idx, col_idx, value): + cell, created = Cell.objects.get_or_create( + sheet=sheet, + table_type='bottom_3', + row_index=row_idx, + column_index=col_idx, + defaults={'value': value} + ) + 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 is not None else '', + 'is_calculated': True, + }) + + def dec(x): + if x in (None, ''): + return Decimal('0') + try: + return Decimal(str(x)) + except Exception: + return Decimal('0') + + # ---- current summary ---- + cur_sum = MonthlySummary.objects.filter(sheet=sheet).first() + curr_k44 = dec(cur_sum.gesamtbestand_neu_lhe) if cur_sum else Decimal('0') + total_verb = dec(cur_sum.verbraucherverlust_lhe) if cur_sum else Decimal('0') + + # ---- previous month summary ---- + year, month = sheet.year, sheet.month + if month == 1: + prev_year, prev_month = year - 1, 12 + else: + prev_year, prev_month = year, month - 1 + + prev_sheet = MonthlySheet.objects.filter(year=prev_year, month=prev_month).first() + if prev_sheet: + prev_sum = MonthlySummary.objects.filter(sheet=prev_sheet).first() + prev_k44 = dec(prev_sum.gesamtbestand_neu_lhe) if prev_sum else Decimal('0') + else: + prev_k44 = Decimal('0') + + # ---- read editable inputs from bottom_3: F47,G47,I47,I50 ---- + def get_val(r, c): + cell = get_cell(r, c) + return dec(cell.value if cell else None) + + f47 = get_val(1, 0) + g47 = get_val(1, 1) + i47 = get_val(1, 2) + i50 = get_val(4, 2) + + # Now apply your formulas using prev_k44, curr_k44, total_verb, f47,g47,i47,i50 + # Row indices: 0..7 correspond to 46..53 + # col 3 = J, col 4 = K + + # Row 46 + k46 = prev_k44 + j46 = k46 * Decimal('0.75') + set_cell(0, 3, j46) + set_cell(0, 4, k46) + + # Row 47 + g47 = self._dec((get_cell(1, 1) or {}).value if get_cell(1, 1) else None) + i47 = self._dec((get_cell(1, 2) or {}).value if get_cell(1, 2) else None) + + j47 = g47 + i47 + k47 = (j47 / Decimal('0.75')) + g47 if j47 != 0 else g47 + + set_cell(1, 3, j47) + set_cell(1, 4, k47) + + # Row 48 + k48 = k46 + k47 + j48 = k48 * Decimal('0.75') + set_cell(2, 3, j48) + set_cell(2, 4, k48) + + # Row 49 + k49 = curr_k44 + j49 = k49 * Decimal('0.75') + set_cell(3, 3, j49) + set_cell(3, 4, k49) + + # Row 50 + j50 = i50 + k50 = j50 / Decimal('0.75') if j50 != 0 else Decimal('0') + set_cell(4, 3, j50) + set_cell(4, 4, k50) + + # Row 51 + k51 = k48 - k49 - k50 + j51 = k51 * Decimal('0.75') + set_cell(5, 3, j51) + set_cell(5, 4, k51) + + # Row 52 + k52 = total_verb + j52 = k52 * Decimal('0.75') + set_cell(6, 3, j52) + set_cell(6, 4, k52) + + # Row 53 + j53 = j51 - j52 + k53 = k51 - k52 + set_cell(7, 3, j53) + set_cell(7, 4, k53) + + return updated_cells + + + def _dec(self, value): + """Convert value to Decimal or return 0.""" + if value is None or value == '': + return Decimal('0') + try: + return Decimal(str(value)) + except Exception: + return Decimal('0') + + 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) + ) + elif cell.table_type == 'bottom_1': + updated_cells.extend(self.calculate_bottom_1_dependents(sheet, cell)) + elif cell.table_type == 'bottom_3': + updated_cells.extend( + self.calculate_bottom_3_dependents(sheet) + ) + # bottom_1 / bottom_2 / bottom_3 currently have no formulas: + # they just save the new value. + updated_cells += self.calculate_bottom_3_dependents(sheet) + 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_bottom_1_dependents(self, sheet, changed_cell): + """ + Recalculate Bottom Table 1 (table_type='bottom_1'). + + Layout (row_index 0–9, col_index 0–4): + + Rows 0–8: + 0: Batterie 1 + 1: 2 + 2: 3 + 3: 4 + 4: 5 + 5: 6 + 6: 2 Bündel + 7: 2 Ballone + 8: Reingasspeicher + + Row 9: + 9: Gasbestand (totals row) + + Columns: + 0: Volumen (fixed values, not editable) + 1: bar (editable for rows 0–8) + 2: korrigiert = bar / (bar/2000 + 1) + 3: Nm³ = Volumen * korrigiert + 4: Lit. LHe = Nm³ / 0.75 + + Row 9: + - Volumen (col 0): empty + - bar (col 1): empty + - korrigiert (col 2): empty + - Nm³ (col 3): SUM Nm³ rows 0–8 + - Lit. LHe (col 4): SUM Lit. LHe rows 0–8 + """ + from decimal import Decimal, InvalidOperation + from .models import Cell + + updated_cells = [] + + DATA_ROWS = list(range(0, 9)) # 0–8 + TOTAL_ROW = 9 + + COL_VOL = 0 + COL_BAR = 1 + COL_KORR = 2 + COL_NM3 = 3 + COL_LHE = 4 + + # Fixed Volumen values for rows 0–8 + VOLUMES = [ + Decimal("2.4"), # row 0 + Decimal("5.1"), # row 1 + Decimal("4.0"), # row 2 + Decimal("1.0"), # row 3 + Decimal("4.0"), # row 4 + Decimal("0.4"), # row 5 + Decimal("1.2"), # row 6 + Decimal("20.0"), # row 7 + Decimal("5.0"), # row 8 + ] + + def get_cell(row_idx, col_idx): + return Cell.objects.filter( + sheet=sheet, + table_type="bottom_1", + row_index=row_idx, + column_index=col_idx, + ).first() + + def set_calc(row_idx, col_idx, value): + """ + Set a calculated cell and add it to updated_cells. + If value is None, we clear the cell. + """ + cell = get_cell(row_idx, col_idx) + if not cell: + cell = Cell( + sheet=sheet, + table_type="bottom_1", + row_index=row_idx, + column_index=col_idx, + value=value, + ) + else: + cell.value = value + cell.save() + + updated_cells.append( + { + "id": cell.id, + "value": "" if cell.value is None else str(cell.value), + "is_calculated": True, + } + ) + + # ---------- Rows 0–8: per-gasspeicher calculations ---------- + for row_idx in DATA_ROWS: + bar_cell = get_cell(row_idx, COL_BAR) + + # Volumen: fixed constant or, if present, value from DB + vol = VOLUMES[row_idx] + vol_cell = get_cell(row_idx, COL_VOL) + if vol_cell and vol_cell.value is not None: + try: + vol = Decimal(str(vol_cell.value)) + except (InvalidOperation, ValueError): + # fall back to fixed constant + vol = VOLUMES[row_idx] + + bar = None + try: + if bar_cell and bar_cell.value is not None: + bar = Decimal(str(bar_cell.value)) + except (InvalidOperation, ValueError): + bar = None + + # korrigiert = bar / (bar/2000 + 1) + if bar is not None and bar != 0: + korr = bar / (bar / Decimal("2000") + Decimal("1")) + else: + korr = None + + # Nm³ = Volumen * korrigiert + if korr is not None: + nm3 = vol * korr + else: + nm3 = None + + # Lit. LHe = Nm³ / 0.75 + if nm3 is not None: + lit_lhe = nm3 / Decimal("0.75") + else: + lit_lhe = None + + # Write calculated cells back (NOT Volumen or bar) + set_calc(row_idx, COL_KORR, korr) + set_calc(row_idx, COL_NM3, nm3) + set_calc(row_idx, COL_LHE, lit_lhe) + + # ---------- Row 9: totals (Gasbestand) ---------- + total_nm3 = Decimal("0") + total_lhe = Decimal("0") + has_nm3 = False + has_lhe = False + + for row_idx in DATA_ROWS: + nm3_cell = get_cell(row_idx, COL_NM3) + if nm3_cell and nm3_cell.value is not None: + try: + total_nm3 += Decimal(str(nm3_cell.value)) + has_nm3 = True + except (InvalidOperation, ValueError): + pass + + lhe_cell = get_cell(row_idx, COL_LHE) + if lhe_cell and lhe_cell.value is not None: + try: + total_lhe += Decimal(str(lhe_cell.value)) + has_lhe = True + except (InvalidOperation, ValueError): + pass + + # Volumen (0), bar (1), korrigiert (2) on total row stay empty + set_calc(TOTAL_ROW, COL_KORR, None) # explicitly clear korrigiert + set_calc(TOTAL_ROW, COL_NM3, total_nm3 if has_nm3 else None) + set_calc(TOTAL_ROW, COL_LHE, total_lhe if has_lhe else None) + + 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 Exception: + 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 '' + }) + # bottom_3 has no client, so this will just add None for those cells, + # which is harmless. Top-left cells still add their real client_id. + changed_clients.add(cell.client_id) + + except Cell.DoesNotExist: + continue + + # Recalculate for each changed client (top-left tables) + for client_id in changed_clients: + if client_id is not None: + self.recalculate_top_left_table(sheet, client_id) + + # --- NEW: recalc bottom_3 for the whole sheet, independent of clients --- + bottom3_updates = self.calculate_bottom_3_dependents(sheet) + + # Get all updated cells for response (top-left) + all_updated_cells = [] + for client_id in changed_clients: + if client_id is None: + continue # skip bottom_3 / non-client cells + 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 + }) + + # Add bottom_3 recalculated cells (J46..K53, etc.) + all_updated_cells.extend(bottom3_updates) + + 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() +@method_decorator(csrf_exempt, name='dispatch') +class SaveMonthSummaryView(View): + """ + Saves per-month summary values such as K44 (Gesamtbestand neu). + Called from JS after 'Save All' finishes. + """ + + def post(self, request, *args, **kwargs): + try: + data = json.loads(request.body.decode('utf-8')) + except json.JSONDecodeError: + return JsonResponse( + {'success': False, 'message': 'Invalid JSON'}, + status=400 + ) + + sheet_id = data.get('sheet_id') + if not sheet_id: + return JsonResponse( + {'success': False, 'message': 'Missing sheet_id'}, + status=400 + ) + + try: + sheet = MonthlySheet.objects.get(id=sheet_id) + except MonthlySheet.DoesNotExist: + return JsonResponse( + {'success': False, 'message': 'Sheet not found'}, + status=404 + ) + + # More tolerant decimal conversion: accepts "123.45" and "123,45" + def to_decimal(value): + if value is None: + return None + s = str(value).strip() + if s == '': + return None + s = s.replace(',', '.') + try: + return Decimal(s) + except (InvalidOperation, ValueError): + # Debug: show what failed in the dev server console + print("SaveMonthSummaryView to_decimal failed for:", repr(value)) + return None + + raw_k44 = data.get('gesamtbestand_neu_lhe') + raw_gas = data.get('gasbestand_lhe') + raw_verb = data.get('verbraucherverlust_lhe') + + gesamtbestand_neu_lhe = to_decimal(raw_k44) + gasbestand_lhe = to_decimal(raw_gas) + verbraucherverlust_lhe = to_decimal(raw_verb) + + summary, created = MonthlySummary.objects.get_or_create(sheet=sheet) + + if gesamtbestand_neu_lhe is not None: + summary.gesamtbestand_neu_lhe = gesamtbestand_neu_lhe + if gasbestand_lhe is not None: + summary.gasbestand_lhe = gasbestand_lhe + if verbraucherverlust_lhe is not None: + summary.verbraucherverlust_lhe = verbraucherverlust_lhe + + summary.save() + + # Small debug output so you can see in the server console what was saved + print( + f"Saved MonthlySummary for {sheet.year}-{sheet.month:02d}: " + f"K44={summary.gesamtbestand_neu_lhe}, " + f"Gasbestand={summary.gasbestand_lhe}, " + f"Verbraucherverlust={summary.verbraucherverlust_lhe}" + ) + + return JsonResponse({'success': True}) + + + +# 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): + # --- Clients for the yearly summary table --- + clients = Client.objects.all() + + # --- Available years for output data (same as before) --- + available_years_qs = SecondTableEntry.objects.dates('date', 'year', order='DESC') + available_years = [y.year for y in available_years_qs] + + # === 1) Year used for the "Helium Output Yearly Summary" table === + # Uses ?year=... in the dropdown + year_param = request.GET.get('year') + if year_param: + selected_year = int(year_param) + else: + selected_year = available_years[0] if available_years else datetime.now().year + + # === 2) GLOBAL half-year interval (shared with other pages) ======= + # Try GET params first + interval_year_param = request.GET.get('interval_year') + start_month_param = request.GET.get('interval_start_month') + + # Fallbacks from session + session_year = request.session.get('halfyear_year') + session_start_month = request.session.get('halfyear_start_month') + + # Determine final interval_year + if interval_year_param: + interval_year = int(interval_year_param) + elif session_year: + interval_year = int(session_year) + else: + # default: same as selected_year for summary + interval_year = selected_year + + # Determine final interval_start_month + if start_month_param: + interval_start_month = int(start_month_param) + elif session_start_month: + interval_start_month = int(session_start_month) + else: + interval_start_month = 1 # default Jan + + # Store back into the session so other views can read them + request.session['halfyear_year'] = interval_year + request.session['halfyear_start_month'] = interval_start_month + + # === 3) Build a 6-month window, allowing wrap into the next year === + # Example: interval_year=2025, interval_start_month=12 + # window = [(2025,12), (2026,1), (2026,2), (2026,3), (2026,4), (2026,5)] + window = [] + for offset in range(6): + total_index = (interval_start_month - 1) + offset # 0-based index + y = interval_year + (total_index // 12) + m = (total_index % 12) + 1 + window.append((y, m)) + + # === 4) Totals per client in that 6-month window ================== + monthly_data = [] + for client in clients: + monthly_totals = [] + for (y, m) in window: + total = SecondTableEntry.objects.filter( + client=client, + date__year=y, + date__month=m + ).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), + }) + + # === 5) Month labels for the header (only the 6-month window) ===== + month_labels = [calendar.month_abbr[m] for (y, m) in window] + + # === 6) FINALLY: return the response ============================== + return render(request, 'clients_table.html', { + 'available_years': available_years, + 'current_year': selected_year, # used by year dropdown + 'interval_year': interval_year, # used by "Global 6-Month Interval" form + 'interval_start_month': interval_start_month, + 'months': month_labels, + 'monthly_data': monthly_data, + }) + + # === 5) Month labels for the header (only the 6-month window) ===== + month_labels = [calendar.month_abbr[m] for (y, m) in window] + + +def set_halfyear_interval(request): + if request.method == 'POST': + year = int(request.POST.get('year')) + start_month = int(request.POST.get('start_month')) + + request.session['halfyear_year'] = year + request.session['halfyear_start_month'] = start_month + + return redirect(request.META.get('HTTP_REFERER', 'clients_list')) + + return redirect('clients_list') +# Table One View (ExcelEntry) +def table_one_view(request): + from .models import ExcelEntry, Client, Institute + + # --- Base queryset for the main Helium Input table --- + base_entries = ExcelEntry.objects.all().select_related('client', 'client__institute') + + # Read the global 6-month interval from the session + interval_year = request.session.get('halfyear_year') + interval_start = request.session.get('halfyear_start_month') + + if interval_year and interval_start: + interval_year = int(interval_year) + interval_start = int(interval_start) + + # Build the same 6-month window as on the main page (can cross year) + window = [] + for offset in range(6): + total_index = (interval_start - 1) + offset # 0-based + y = interval_year + (total_index // 12) + m = (total_index % 12) + 1 + window.append((y, m)) + + # Build Q filter: (year=m_year AND month=m_month) for any of those 6 + q = Q() + for (y, m) in window: + q |= Q(date__year=y, date__month=m) + + entries_table1 = base_entries.filter(q).order_by('-date') + else: + # Fallback: if no global interval yet, show everything + entries_table1 = base_entries.order_by('-date') + 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 + # default year/start month (if no global interval yet) + if available_years: + default_year = available_years[0] # newest year in ExcelEntry + else: + default_year = timezone.now().year + + # 🔸 Read global half-year interval from session (set on main page) + session_year = request.session.get('halfyear_year') + session_start = request.session.get('halfyear_start_month') + + # If the user has set a global interval, use it. + # Otherwise fall back to default year / January. + year = int(session_year) if session_year else int(default_year) + start_month = int(session_start) if session_start else 1 + + + # six-month window + # --- Build a 6-month window, allowing wrap into the next year --- + # Example: year=2025, start_month=10 + # window = [(2025,10), (2025,11), (2025,12), (2026,1), (2026,2), (2026,3)] + window = [] + for offset in range(6): + total_index = (start_month - 1) + offset # 0-based + y_for_month = year + (total_index // 12) + m_for_month = (total_index % 12) + 1 + window.append((y_for_month, m_for_month)) + + overview = None + + if window: + # 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 (y_m, m_m) in window: + total = ExcelEntry.objects.filter( + client__in=clients_qs, + date__year=y_m, + date__month=m_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(window)): + 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, (y_m, m_m) in enumerate(window): + row_values = [g['values'][idx] for g in groups_entries] + rows.append({ + 'month_number': m_m, + 'month_label': calendar.month_name[m_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] + + # Start/end for display – include years so wrap is clear + start_year = window[0][0] + start_month_disp = window[0][1] + end_year = window[-1][0] + end_month_disp = window[-1][1] + + overview = { + 'year': year, # keep for backwards compatibility if needed + 'start_month': start_month_disp, + 'start_year': start_year, + 'end_month': end_month_disp, + 'end_year': end_year, + '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: + clients = Client.objects.all().select_related('institute') + institutes = Institute.objects.all() + + # 🔸 Read global half-year interval from session + interval_year = request.session.get('halfyear_year') + interval_start = request.session.get('halfyear_start_month') + + if interval_year and interval_start: + interval_year = int(interval_year) + interval_start = int(interval_start) + + # Build the same 6-month window as in clients_list (can cross years) + window = [] + for offset in range(6): + total_index = (interval_start - 1) + offset # 0-based + y = interval_year + (total_index // 12) + m = (total_index % 12) + 1 + window.append((y, m)) + + # Build a Q object matching any of those (year, month) pairs + q = Q() + for (y, m) in window: + q |= Q(date__year=y, date__month=m) + + entries = SecondTableEntry.objects.filter(q).order_by('-date') + else: + # Fallback if no global interval yet: show all + entries = SecondTableEntry.objects.all().order_by('-date') + + return render(request, 'table_two.html', { + 'entries_table2': entries, + 'clients': clients, + 'institutes': institutes, + 'interval_year': interval_year, + 'interval_start_month': interval_start, + }) + + except Exception as e: + return render(request, 'table_two.html', { + 'error_message': f"Failed to load data: {str(e)}", + 'entries_table2': [], + 'clients': clients, + 'institutes': institutes, + }) + + +def monthly_sheet_root(request): + """ + Redirect /sheet/ to the sheet matching the globally selected + half-year start (year + month). If not set, fall back to latest. + """ + year = request.session.get('halfyear_year') + start_month = request.session.get('halfyear_start_month') + + if year and start_month: + try: + year = int(year) + start_month = int(start_month) + return redirect('monthly_sheet', year=year, month=start_month) + except ValueError: + pass # fall through + + # Fallback: latest MonthlySheet if exists + latest_sheet = MonthlySheet.objects.order_by('-year', '-month').first() + if latest_sheet: + return redirect('monthly_sheet', year=latest_sheet.year, month=latest_sheet.month) + else: + now = timezone.now() + return redirect('monthly_sheet', year=now.year, month=now.month) +# 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'}) + +def halfyear_settings(request): + """ + Global settings page: choose a year + first month for the 6-month interval. + These values are stored in the session and used by other views. + """ + # Determine available years from your data (use ExcelEntry or SecondTableEntry) + # Here I use SecondTableEntry; you can switch to ExcelEntry if you prefer. + available_years_qs = SecondTableEntry.objects.dates('date', 'year', order='DESC') + available_years = [d.year for d in available_years_qs] + + if not available_years: + available_years = [timezone.now().year] + + # Defaults (if nothing in session yet) + default_year = request.session.get('halfyear_year', available_years[0]) + default_start_month = request.session.get('halfyear_start_month', 1) + + if request.method == 'POST': + year = int(request.POST.get('year', default_year)) + start_month = int(request.POST.get('start_month', default_start_month)) + + request.session['halfyear_year'] = year + request.session['halfyear_start_month'] = start_month + + # Redirect back to where user came from, or to this page again + next_url = request.POST.get('next') or request.GET.get('next') or 'halfyear_settings' + return redirect(next_url) + + # Month choices for the dropdown + month_choices = [(i, calendar.month_name[i]) for i in range(1, 13)] + + context = { + 'available_years': available_years, + 'selected_year': int(default_year), + 'selected_start_month': int(default_start_month), + 'month_choices': month_choices, + } + return render(request, 'halfyear_settings.html', context) \ No newline at end of file diff --git a/sheets/services/__init__.py b/sheets/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sheets/services/halfyear_calc.py b/sheets/services/halfyear_calc.py new file mode 100644 index 0000000..705809f --- /dev/null +++ b/sheets/services/halfyear_calc.py @@ -0,0 +1,1156 @@ +# sheets/services/halfyear_calc.py +from __future__ import annotations +from django.db.models.functions import Coalesce +from decimal import Decimal +from typing import Dict, Any +from django.shortcuts import redirect, render +from decimal import Decimal +from sheets.models import ( + Client, SecondTableEntry, Institute, ExcelEntry, + Betriebskosten, MonthlySheet, Cell, CellReference, MonthlySummary ,BetriebskostenSummary,AbrechnungCell +) +from django.db.models import Sum +from django.db.models.functions import Coalesce +from django.db.models import DecimalField, Value +HALFYEAR_CLIENTS = ["AG Vogel", "AG Halfm", "IKP"] +TR_RUECKF_FLUESSIG_ROW = 2 # confirmed by your March value 172.840560 +TR_BESTAND_KANNEN_ROW = 5 +def get_top_right_value(sheet, client_name: str, row_index: int) -> Decimal: + """ + Read a numeric value from the top_right table of a MonthlySheet for + a given client (by column) and row_index. + + top_right cells are keyed by (sheet, table_type='top_right', + row_index, column_index), where column_index is the position of the + client in HALFYEAR_RIGHT_CLIENTS. + """ + if sheet is None: + return Decimal('0') + + col_index = RIGHT_CLIENT_INDEX.get(client_name) + if col_index is None: + return Decimal('0') + + cell = Cell.objects.filter( + sheet=sheet, + table_type='top_right', + row_index=row_index, + column_index=col_index, + ).first() + + if cell is None or cell.value in (None, ''): + return Decimal('0') + + try: + return Decimal(str(cell.value)) + except Exception: + return Decimal('0') + +def get_bottom2_value(sheet, row_index: int, col_index: int) -> Decimal: + """Get numeric value from bottom_2 or 0 if missing.""" + if sheet is None: + return Decimal("0") + cell = Cell.objects.filter( + sheet=sheet, + table_type="bottom_2", + row_index=row_index, + column_index=col_index, + ).first() + if cell is None or cell.value in (None, ""): + return Decimal("0") + try: + return Decimal(str(cell.value)) + except Exception: + return Decimal("0") +def get_bottom1_value(sheet, row_index: int, col_index: int) -> Decimal: + """Get a numeric value from bottom_1, or 0 if missing.""" + if sheet is None: + return Decimal('0') + + cell = Cell.objects.filter( + sheet=sheet, + table_type='bottom_1', + row_index=row_index, + column_index=col_index, + ).first() + + if cell is None or cell.value in (None, ''): + return Decimal('0') + + try: + return Decimal(str(cell.value)) + except Exception: + return Decimal('0') +def get_top_left_value(sheet, client_name: str, row_index: int) -> Decimal: + """ + Read a numeric value from the top_left table for a given month, client and row. + Does NOT use column_index, because top_left is keyed only by client + row_index. + """ + if sheet is None: + return Decimal('0') + + client_obj = Client.objects.filter(name=client_name).first() + if not client_obj: + return Decimal('0') + + cell = Cell.objects.filter( + sheet=sheet, + table_type='top_left', + client=client_obj, + row_index=row_index, + ).first() + + if cell is None or cell.value in (None, ''): + return Decimal('0') + + try: + return Decimal(str(cell.value)) + except Exception: + return Decimal('0') +def get_bestand_kannen_for_month(sheet, client_name: str) -> Decimal: + """ + 'B9' in your description: Bestand in Kannen-1 (Lit. L-He) + For this implementation we take it from top_left row_index = 5 for that client. + """ + return get_top_left_value(sheet, client_name, row_index=BESTAND_KANNEN_ROW_INDEX) + +def pick_sheet_by_gasbestand(window, sheets_by_ym, prev_sheet): + """ + Returns the last sheet in the window whose Gasbestand (J36, Nm³ column) != 0. + If none found, returns prev_sheet (Übertrag_Dez__Vorjahr equivalent). + """ + for (y, m) in reversed(window): + sheet = sheets_by_ym.get((y, m)) + if not sheet: + continue + gasbestand_nm3 = get_bottom1_value(sheet, GASBESTAND_ROW_INDEX, GASBESTAND_COL_NM3) + if gasbestand_nm3 != 0: + return sheet + return prev_sheet +# NEW: clients for the top-right half-year table +GASBESTAND_ROW_INDEX = 9 # <-- adjust if your bottom_1 has a different row index +GASBESTAND_COL_NM3 = 3 # <-- adjust to the column index for Nm³ in bottom_1 + +# In top_left / top_right, "Bestand in Kannen-1 (Lit. L-He)" is row_index 5 +BESTAND_KANNEN_ROW_INDEX = 5 +HALFYEAR_RIGHT_CLIENTS = [ + "Dr. Fohrer", + "AG Buntk.", + "AG Alff", + "AG Gutfl.", + "M3 Thiele", + "M3 Buntkowsky", + "M3 Gutfleisch", +] +BOTTOM1_COL_VOLUME = 0 +BOTTOM1_COL_BAR = 1 +BOTTOM1_COL_KORR = 2 +BOTTOM1_COL_NM3 = 3 +BOTTOM1_COL_LHE = 4 +BOTTOM2_ROW_ANLAGE = 0 +BOTTOM2_COL_G39 = 0 # "Gefäss 2,5" (cell id shows column_index=0) +BOTTOM2_COL_I39 = 1 # "Gefäss 1,0" (cell id shows column_index=1) +BOTTOM2_ROW_INPUTS = { + "g39": (0, 0), # row_index=0, column_index=0 (your G39) + "i39": (0, 1), # row_index=0, column_index=1 (your I39) +} +FACTOR_NM3_TO_LHE = Decimal("0.75") +RIGHT_CLIENT_INDEX = {name: idx for idx, name in enumerate(HALFYEAR_RIGHT_CLIENTS)} + +def build_halfyear_window(interval_year: int, start_month: int): + """ + Build a list of (year, month) for the 6-month interval, possibly crossing into the next year. + Example: (2025, 10) -> [(2025,10), (2025,11), (2025,12), (2026,1), (2026,2), (2026,3)] + """ + window = [] + for offset in range(6): + total_index = (start_month - 1) + offset # 0-based + y = interval_year + (total_index // 12) + m = (total_index % 12) + 1 + window.append((y, m)) + return window +# Import ONLY models + pure helpers here +from sheets.models import MonthlySheet, Cell, Client, SecondTableEntry, BetriebskostenSummary + +def compute_halfyear_context(interval_year: int, interval_start: int) -> Dict[str, Any]: + """ + Returns a context dict with the SAME keys your current halfyear_balance.html expects. + """ + # ✅ Paste the pure calculation logic here in Step 2 + window = build_halfyear_window(interval_year, interval_start) + # window = [(y1, m1), (y2, m2), ..., (y6, m6)] + + # (Year, month) of the first month + start_year, start_month = window[0] + + # Previous month (for "Stand ... (Vorjahr)" and "Best. in Kannen Vormonat") + prev_total_index = (start_month - 1) - 1 # one month back, 0-based + if prev_total_index >= 0: + prev_year = start_year + (prev_total_index // 12) + prev_month = (prev_total_index % 12) + 1 + else: + prev_year = start_year - 1 + prev_month = 12 + + # Load MonthlySheet objects for the window and for the previous month + sheets_by_ym = {} + for (y, m) in window: + sheet = MonthlySheet.objects.filter(year=y, month=m).first() + sheets_by_ym[(y, m)] = sheet + + prev_sheet = MonthlySheet.objects.filter(year=prev_year, month=prev_month).first() + def pick_bottom2_from_window(window, sheets_by_ym, prev_sheet): + # choose sheet (same logic you already use) + chosen = None + for (y, m) in reversed(window): + s = sheets_by_ym.get((y, m)) + # use your existing condition for choosing month + if s: + chosen = s + break + if chosen is None: + chosen = prev_sheet + + # Now read the two inputs safely + bottom2_inputs = {} + for key, (row_idx, col_idx) in BOTTOM2_ROW_INPUTS.items(): + bottom2_inputs[key] = get_bottom2_value(chosen, row_idx, col_idx) + + return chosen, bottom2_inputs + + + chosen_sheet_bottom2, bottom2_inputs = pick_bottom2_from_window(window, sheets_by_ym, prev_sheet) + bottom2_g39 = bottom2_inputs["g39"] + bottom2_i39 = bottom2_inputs["i39"] + # ---------------------------- + # HALF-YEAR BOTTOM TABLE 1 (Bilanz) - Read only + # ---------------------------- + chosen_sheet_bottom1 = pick_sheet_by_gasbestand(window, sheets_by_ym, prev_sheet) + + # IMPORTANT: define which bottom_1 row_index corresponds to Excel rows 27..35 + # If your bottom_1 starts at Excel row 27 => row_index 0 == Excel 27 + # then row_index = excel_row - 27 + BOTTOM1_EXCEL_START_ROW = 27 + + bottom1_excel_rows = list(range(27, 37)) # 27..36 + BOTTOM1_LABELS = [ + "Batterie 1", + "2", + "3", + "4", + "5", + "Batterie Links", + "2 Bündel", + "2 Ballone", + "Reingasspeicher", + "Gasbestand", + ] + + BOTTOM1_VOLUMES = [ + Decimal("2.4"), + Decimal("5.1"), + Decimal("4.0"), + Decimal("1.0"), + Decimal("4.0"), + Decimal("0.6"), + Decimal("1.2"), + Decimal("20.0"), + Decimal("5.0"), + None, # Gasbestand row has no volume + ] + nm3_sum_27_35 = Decimal("0") + lhe_sum_27_35 = Decimal("0") + bottom1_rows = [] + + for excel_row in bottom1_excel_rows: + row_index = excel_row - BOTTOM1_EXCEL_START_ROW + + chosen_sheet_bottom1 = None + for (y, m) in reversed(window): + s = sheets_by_ym.get((y, m)) + gasbestand = get_bottom1_value(s, GASBESTAND_ROW_INDEX, GASBESTAND_COL_NM3) # J36 (Nm3) + if gasbestand != 0: + chosen_sheet_bottom1 = s + break + + if chosen_sheet_bottom1 is None: + chosen_sheet_bottom1 = prev_sheet + + # Normal rows (27..35): read from chosen sheet and accumulate sums + if excel_row != 36: + nm3_val = get_bottom1_value(chosen_sheet_bottom1, row_index, BOTTOM1_COL_NM3) + lhe_val = get_bottom1_value(chosen_sheet_bottom1, row_index, BOTTOM1_COL_LHE) + + nm3_sum_27_35 += nm3_val + lhe_sum_27_35 += lhe_val + + bottom1_rows.append({ + "label": BOTTOM1_LABELS[row_index], + "volume": BOTTOM1_VOLUMES[row_index], + "bar": get_bottom1_value(chosen_sheet_bottom1, row_index, BOTTOM1_COL_BAR), + "korr": get_bottom1_value(chosen_sheet_bottom1, row_index, BOTTOM1_COL_KORR), + "nm3": nm3_val, + "lhe": lhe_val, + }) + + # Gasbestand row (36): show sums (J36 = SUM(J27:J35), K36 = SUM(K27:K35)) + else: + bottom1_rows.append({ + "label": "Gasbestand", + "volume": "", + "bar": "", + "korr": "", + "nm3": nm3_sum_27_35, + "lhe": lhe_sum_27_35, + }) + start_sheet = sheets_by_ym.get((start_year, start_month)) + # ------------------------------------------------------------ + # Bottom Table 2 (Halbjahres Bilanz) – server-side recalcBottom2() + # ------------------------------------------------------------ + + FACTOR_BT2 = Decimal("0.75") + + # 1) Helper: pick last-nonzero value of bottom_2 row0 col0/col1 from the window (fallback: prev_sheet) + def pick_last_nonzero_bottom2(row_index: int, col_index: int) -> Decimal: + # Scan from last month in window backwards + for (y, m) in reversed(window): + s = sheets_by_ym.get((y, m)) + if not s: + continue + v = get_bottom2_value(s, row_index, col_index) + if v is not None and v != 0: + return v + # fallback to month before window + v_prev = get_bottom2_value(prev_sheet, row_index, col_index) + return v_prev if v_prev is not None else Decimal("0") + + # 2) K38 comes from Overall Summary: "Summe Bestand (Lit. L-He)" + # Find it from your already built overall summary rows list. + + k38 = Decimal("0") + j38 = Decimal("0") + # 3) Inputs G39 / I39 (picked from last non-zero month in window) + g39 = pick_last_nonzero_bottom2(row_index=0, col_index=0) # G39 + i39 = pick_last_nonzero_bottom2(row_index=0, col_index=1) # I39 + + k39 = (g39 or Decimal("0")) + (i39 or Decimal("0")) + j39 = k39 * FACTOR_BT2 + + # 4) +Kaltgas (row 40) + # JS: + # g40 = (2500 - g39)/100*10 + # i40 = (1000 - i39)/100*10 + g40 = None + i40 = None + if g39 is not None: + g40 = (Decimal("2500") - g39) / Decimal("100") * Decimal("10") + if i39 is not None: + i40 = (Decimal("1000") - i39) / Decimal("100") * Decimal("10") + + k40 = (g40 or Decimal("0")) + (i40 or Decimal("0")) + j40 = k40 * FACTOR_BT2 + + # 5) Bestand flüssig He (row 43) + k43 = ( + (k38 or Decimal("0")) + + (k39 or Decimal("0")) + + (k40 or Decimal("0")) + ) + j43 = k43 * FACTOR_BT2 + + # 6) Gesamtbestand neu (row 44) = Gasbestand(Lit) from Bottom Table 1 + k43 + gasbestand_lit = Decimal("0") + for r in bottom1_rows: + if (r.get("label") or "").strip().startswith("Gasbestand"): + gasbestand_lit = r.get("lhe") or Decimal("0") + break + + k44 = (gasbestand_lit or Decimal("0")) + (k43 or Decimal("0")) + j44 = k44 * FACTOR_BT2 + + bottom2 = { + "j38": j38, "k38": k38, + "g39": g39, "i39": i39, "j39": j39, "k39": k39, + "g40": g40, "i40": i40, "j40": j40, "k40": k40, + "j43": j43, "k43": k43, + "j44": j44, "k44": k44, + } + + # ------------------------------------------------------------------ + # 2) LEFT TABLE (your existing, working logic) + # ------------------------------------------------------------------ + HALFYEAR_CLIENTS_LEFT = ["AG Vogel", "AG Halfm", "IKP"] + + # We'll collect client-wise values first for clarity. + client_data_left = {name: {} for name in HALFYEAR_CLIENTS_LEFT} + + # --- Row B3: Stand der Gaszähler (Nm³) + # = MAX(B3 from previous month, and B3 from each of the 6 months in the window) + # row_index 0 in top_left = "Stand der Gaszähler (Nm³)" + months_for_max = [(prev_year, prev_month)] + window + + for cname in HALFYEAR_CLIENTS_LEFT: + max_val = Decimal('0') + for (y, m) in months_for_max: + sheet = sheets_by_ym.get((y, m)) + if sheet is None and (y, m) == (prev_year, prev_month): + sheet = prev_sheet + val_b3 = get_top_left_value(sheet, cname, row_index=0) + if val_b3 > max_val: + max_val = val_b3 + client_data_left[cname]['stand_gas'] = max_val + + # --- Row B4: Stand der Gaszähler (Vorjahr) (Nm³) -> previous month same row --- + for cname in HALFYEAR_CLIENTS_LEFT: + val_b4 = get_top_left_value(prev_sheet, cname, row_index=0) + client_data_left[cname]['stand_gas_prev'] = val_b4 + + # --- Row B5: Gasrückführung (Nm³) = B3 - B4 --- + for cname in HALFYEAR_CLIENTS_LEFT: + b3 = client_data_left[cname]['stand_gas'] + b4 = client_data_left[cname]['stand_gas_prev'] + client_data_left[cname]['gasrueckf'] = b3 - b4 + + # --- Row B6: Rückführung flüssig (Lit. L-He) = B5 / 0.75 --- + for cname in HALFYEAR_CLIENTS_LEFT: + b5 = client_data_left[cname]['gasrueckf'] + client_data_left[cname]['rueckf_fluessig'] = (b5 / Decimal('0.75')) if b5 != 0 else Decimal('0') + + # --- Row B7: Sonderrückführungen (Lit. L-He) = sum over 6 months of that row --- + # That row index is 4 in your top_left table. + for cname in HALFYEAR_CLIENTS_LEFT: + sonder_total = Decimal('0') + for (y, m) in window: + sheet = sheets_by_ym.get((y, m)) + if sheet: + sonder_total += get_top_left_value(sheet, cname, row_index=4) + client_data_left[cname]['sonder'] = sonder_total + + # --- Row B8: Bestand in Kannen-1 (Lit. L-He) --- + # Excel-style logic with Gasbestand (J36) and fallback to previous month. + for cname in HALFYEAR_CLIENTS_LEFT: + chosen_value = None + + # Go from last month (window[5]) backwards to first (window[0]) + for (y, m) in reversed(window): + sheet = sheets_by_ym.get((y, m)) + gasbestand = get_bottom1_value(sheet, GASBESTAND_ROW_INDEX, GASBESTAND_COL_NM3) + if gasbestand != 0: + chosen_value = get_bestand_kannen_for_month(sheet, cname) + break + + # If still None -> use previous month (Übertrag_Dez__Vorjahr equivalent) + if chosen_value is None: + sheet_prev = prev_sheet + chosen_value = get_bestand_kannen_for_month(sheet_prev, cname) + + client_data_left[cname]['bestand_kannen'] = chosen_value if chosen_value is not None else Decimal('0') + + # --- Row B9: Summe Bestand (Lit. L-He) = equal to previous row --- + for cname in HALFYEAR_CLIENTS_LEFT: + client_data_left[cname]['summe_bestand'] = client_data_left[cname]['bestand_kannen'] + + # --- Row B10: Best. in Kannen Vormonat (Lit. L-He) + # = Bestand in Kannen-1 from the month BEFORE the window (prev_year, prev_month) + for cname in HALFYEAR_CLIENTS_LEFT: + client_data_left[cname]['best_kannen_vormonat'] = get_bestand_kannen_for_month(prev_sheet, cname) + + # --- Row B13: Bezug (Liter L-He) --- + for cname in HALFYEAR_CLIENTS_LEFT: + total_bezug = Decimal('0') + for (y, m) in window: + qs = SecondTableEntry.objects.filter( + client__name=cname, + date__year=y, + date__month=m, + ).aggregate( + total=Coalesce(Sum('lhe_output'), Value(0, output_field=DecimalField())) + ) + total_bezug += Decimal(str(qs['total'])) + client_data_left[cname]['bezug'] = total_bezug + + # --- Row B14: Rückführ. Soll (Lit. L-He) = Bezug - Summe Bestand + Best. in Kannen Vormonat --- + for cname in HALFYEAR_CLIENTS_LEFT: + b13 = client_data_left[cname]['bezug'] + b11 = client_data_left[cname]['summe_bestand'] + b12 = client_data_left[cname]['best_kannen_vormonat'] + client_data_left[cname]['rueckf_soll'] = b13 - b11 + b12 + + # --- Row B15: Verluste (Soll-Rückf.) (Lit. L-He) + # AG Vogel, AG Halfm: B14 - B6 + # IKP: B14 - B6 - B7 (Sonderrückführungen) + for cname in HALFYEAR_CLIENTS_LEFT: + b14 = client_data_left[cname]['rueckf_soll'] + b6 = client_data_left[cname]['rueckf_fluessig'] + + if (cname or "").strip() == "IKP": + b7 = client_data_left[cname].get('sonder', Decimal('0')) + client_data_left[cname]['verluste'] = b14 - b6 - b7 + else: + client_data_left[cname]['verluste'] = b14 - b6 + # --- Row B16: Füllungen warm (Lit. L-He) = sum over 6 months (row_index=11) --- + for cname in HALFYEAR_CLIENTS_LEFT: + total_warm = Decimal('0') + for (y, m) in window: + sheet = sheets_by_ym.get((y, m)) + total_warm += get_top_left_value(sheet, cname, row_index=11) + client_data_left[cname]['fuellungen_warm'] = total_warm + + # --- Row B17: Kaltgas Rückgabe (Lit. L-He) = Bezug * 0.06 --- + factor = Decimal('0.06') + for cname in HALFYEAR_CLIENTS_LEFT: + b13 = client_data_left[cname]['bezug'] + client_data_left[cname]['kaltgas_rueckgabe'] = b13 * factor + + # --- Row B18: Verbraucherverluste (Liter L-He) = Verluste - Kaltgas Rückgabe --- + for cname in HALFYEAR_CLIENTS_LEFT: + b15 = client_data_left[cname]['verluste'] + b17 = client_data_left[cname]['kaltgas_rueckgabe'] + client_data_left[cname]['verbraucherverluste'] = b15 - b17 + + # --- Row B19: % = Verbraucherverluste / Bezug --- + for cname in HALFYEAR_CLIENTS_LEFT: + bezug = client_data_left[cname]['bezug'] + verb = client_data_left[cname]['verbraucherverluste'] + if bezug != 0: + client_data_left[cname]['percent'] = verb / bezug + else: + client_data_left[cname]['percent'] = None + + # Build LEFT rows structure + left_row_defs = [ + ('Stand der Gaszähler (Nm³)', 'stand_gas'), + ('Stand der Gaszähler (Vorjahr) (Nm³)', 'stand_gas_prev'), + ('Gasrückführung (Nm³)', 'gasrueckf'), + ('Rückführung flüssig (Lit. L-He)', 'rueckf_fluessig'), + ('Sonderrückführungen (Lit. L-He)', 'sonder'), + ('Bestand in Kannen-1 (Lit. L-He)', 'bestand_kannen'), + ('Summe Bestand (Lit. L-He)', 'summe_bestand'), + ('Best. in Kannen Vormonat (Lit. L-He)', 'best_kannen_vormonat'), + ('Bezug (Liter L-He)', 'bezug'), + ('Rückführ. Soll (Lit. L-He)', 'rueckf_soll'), + ('Verluste (Soll-Rückf.) (Lit. L-He)', 'verluste'), + ('Füllungen warm (Lit. L-He)', 'fuellungen_warm'), + ('Kaltgas Rückgabe (Lit. L-He) – Faktor', 'kaltgas_rueckgabe'), + ('Verbraucherverluste (Liter L-He)', 'verbraucherverluste'), + ('%', 'percent'), + ] + + rows_left = [] + for label, key in left_row_defs: + values = [client_data_left[cname][key] for cname in HALFYEAR_CLIENTS_LEFT] + if key == 'percent': + total_bezug = sum((client_data_left[c]['bezug'] for c in HALFYEAR_CLIENTS_LEFT), Decimal('0')) + total_verb = sum((client_data_left[c]['verbraucherverluste'] for c in HALFYEAR_CLIENTS_LEFT), Decimal('0')) + total = (total_verb / total_bezug) if total_bezug != 0 else None + else: + total = sum((v for v in values if v is not None), Decimal('0')) + rows_left.append({ + 'label': label, + 'values': values, + 'total': total, + 'is_percent': key == 'percent', + }) + + # ------------------------------------------------------------------ + # 3) RIGHT TABLE (top-right half-year aggregation) + # ------------------------------------------------------------------ + RIGHT_CLIENTS = HALFYEAR_RIGHT_CLIENTS # for brevity + + right_data = {name: {} for name in RIGHT_CLIENTS} + + # --- Bezug (Liter L-He) for each right client (same as for left) --- + for cname in RIGHT_CLIENTS: + total_bezug = Decimal('0') + for (y, m) in window: + qs = SecondTableEntry.objects.filter( + client__name=cname, + date__year=y, + date__month=m, + ).aggregate( + total=Coalesce(Sum('lhe_output'), Value(0, output_field=DecimalField())) + ) + total_bezug += Decimal(str(qs['total'])) + right_data[cname]['bezug'] = total_bezug + def find_bestand_from_window(reference_client: str) -> Decimal: + """ + Implements: + WENN(last_month!J36=0; WENN(prev_month!J36=0; ...; prev_sheet!9); last_month!9) + reference_client decides which column (L/N/P/Q/R) we read from monthly top_right row_index=5. + """ + # scan backward through window + for (y, m) in reversed(window): + sh = sheets_by_ym.get((y, m)) + if not sh: + continue + gasbestand = get_bottom1_value(sh, GASBESTAND_ROW_INDEX, GASBESTAND_COL_NM3) + if gasbestand != 0: + return get_top_right_value(sh, reference_client, TR_BESTAND_KANNEN_ROW) + + # fallback to previous month (Übertrag_Dez__Vorjahr equivalent) + return get_top_right_value(prev_sheet, reference_client, TR_BESTAND_KANNEN_ROW) + + # Fohrer+Buntk merged: BOTH use Fohrer column (L9) + val_L = find_bestand_from_window("Dr. Fohrer") + right_data["Dr. Fohrer"]["bestand_kannen"] = val_L + right_data["AG Buntk."]["bestand_kannen"] = val_L + + # Alff+Gutfl merged: BOTH use Alff column (N9) + val_N = find_bestand_from_window("AG Alff") + right_data["AG Alff"]["bestand_kannen"] = val_N + right_data["AG Gutfl."]["bestand_kannen"] = val_N + + # M3 each uses its own column (P9/Q9/R9) + right_data["M3 Thiele"]["bestand_kannen"] = find_bestand_from_window("M3 Thiele") + right_data["M3 Buntkowsky"]["bestand_kannen"] = find_bestand_from_window("M3 Buntkowsky") + right_data["M3 Gutfleisch"]["bestand_kannen"] = find_bestand_from_window("M3 Gutfleisch") + # Helper for pair shares (L13/($L13+$M13), etc.) + def pair_share(c1, c2): + total = right_data[c1]['bezug'] + right_data[c2]['bezug'] + if total == 0: + return (Decimal('0'), Decimal('0')) + return ( + right_data[c1]['bezug'] / total, + right_data[c2]['bezug'] / total, + ) + + # --- "Stand der Gaszähler (Vorjahr) (Nm³)" row: share based on Bezug --- + # Dr. Fohrer / AG Buntk. + s_fohrer, s_buntk = pair_share("Dr. Fohrer", "AG Buntk.") + right_data["Dr. Fohrer"]['stand_prev_share'] = s_fohrer + right_data["AG Buntk."]['stand_prev_share'] = s_buntk + + # AG Alff / AG Gutfl. + s_alff, s_gutfl = pair_share("AG Alff", "AG Gutfl.") + right_data["AG Alff"]['stand_prev_share'] = s_alff + right_data["AG Gutfl."]['stand_prev_share'] = s_gutfl + + # M3 Thiele / M3 Buntkowsky / M3 Gutfleisch → empty in Excel → None + for cname in ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"]: + right_data[cname]['stand_prev_share'] = None + + # --- Rückführung flüssig per month (raw sums) --- + # top_right row_index=2 is "Rückführung flüssig (Lit. L-He)" + + # --- Sonderrückführungen (row_index=3 in top_right) --- + for cname in RIGHT_CLIENTS: + sonder_total = Decimal('0') + for (y, m) in window: + sheet = sheets_by_ym.get((y, m)) + if sheet: + sonder_total += get_top_right_value(sheet, cname, row_index=3) + right_data[cname]['sonder'] = sonder_total + + # --- Sammelrückführung (row_index=4 in top_right), grouped & merged --- + # Group 1: Dr. Fohrer + AG Buntk. + group1_total = Decimal('0') + for (y, m) in window: + sheet = sheets_by_ym.get((y, m)) + if sheet: + group1_total += get_top_right_value(sheet, "Dr. Fohrer", row_index=4) + right_data["Dr. Fohrer"]['sammel'] = group1_total + right_data["AG Buntk."]['sammel'] = group1_total + + # Group 2: AG Alff + AG Gutfl. + group2_total = Decimal('0') + for (y, m) in window: + sheet = sheets_by_ym.get((y, m)) + if sheet: + group2_total += get_top_right_value(sheet, "AG Alff", row_index=4) + right_data["AG Alff"]['sammel'] = group2_total + right_data["AG Gutfl."]['sammel'] = group2_total + + # Group 3: M3 Thiele + M3 Buntkowsky + M3 Gutfleisch + group3_total = Decimal('0') + for (y, m) in window: + sheet = sheets_by_ym.get((y, m)) + if sheet: + group3_total += get_top_right_value(sheet, "M3 Thiele", row_index=4) + right_data["M3 Thiele"]['sammel'] = group3_total + right_data["M3 Buntkowsky"]['sammel'] = group3_total + right_data["M3 Gutfleisch"]['sammel'] = group3_total + def safe_div(a: Decimal, b: Decimal) -> Decimal: + return (a / b) if b != 0 else Decimal("0") + + # --- Rückführung flüssig (Lit. L-He) for Halbjahres-Bilanz top-right --- + # Uses your exact formulas. + + # 1) Fohrer / Buntk split by BEZUG share times group SAMMEL (L8) + L13 = right_data["Dr. Fohrer"]["bezug"] + M13 = right_data["AG Buntk."]["bezug"] + L8 = right_data["Dr. Fohrer"]["sammel"] # merged group total + + den = (L13 + M13) + right_data["Dr. Fohrer"]["rueckf_fluessig"] = (safe_div(L13, den) * L8) if den != 0 else Decimal("0") + right_data["AG Buntk."]["rueckf_fluessig"] = (safe_div(M13, den) * L8) if den != 0 else Decimal("0") + + # 2) Alff / Gutfl split by BEZUG share times group SAMMEL (N8) + N13 = right_data["AG Alff"]["bezug"] + O13 = right_data["AG Gutfl."]["bezug"] + N8 = right_data["AG Alff"]["sammel"] # merged group total + + den = (N13 + O13) + right_data["AG Alff"]["rueckf_fluessig"] = (safe_div(N13, den) * N8) if den != 0 else Decimal("0") + right_data["AG Gutfl."]["rueckf_fluessig"] = (safe_div(O13, den) * N8) if den != 0 else Decimal("0") + + # 3) M3 Thiele = sum of monthly Rückführung flüssig (monthly top_right row_index=2) over window + P6_sum = Decimal("0") + for (y, m) in window: + sh = sheets_by_ym.get((y, m)) + P6_sum += get_top_right_value(sh, "M3 Thiele", TR_RUECKF_FLUESSIG_ROW) + right_data["M3 Thiele"]["rueckf_fluessig"] = P6_sum + + # 4) M3 Buntkowsky / M3 Gutfleisch split by BEZUG share times M3-group SAMMEL (P8) + P13 = right_data["M3 Thiele"]["bezug"] + Q13 = right_data["M3 Buntkowsky"]["bezug"] + R13 = right_data["M3 Gutfleisch"]["bezug"] + P8 = right_data["M3 Thiele"]["sammel"] # merged group total + + den = (P13 + Q13 + R13) + right_data["M3 Buntkowsky"]["rueckf_fluessig"] = (safe_div(Q13, den) * P8) if den != 0 else Decimal("0") + right_data["M3 Gutfleisch"]["rueckf_fluessig"] = (safe_div(R13, den) * P8) if den != 0 else Decimal("0") + # --- Bestand in Kannen-1 (Lit. L-He) for right table (grouped) --- + # Use Gasbestand (J36) and fallback logic, but now reading top_right B9 for each group. + TOP_RIGHT_ROW_BESTAND_KANNEN = TR_BESTAND_KANNEN_ROW # <-- most likely correct in your setup + + def pick_bestand_top_right(base_client: str) -> Decimal: + # Go from last month in window backwards: if Gasbestand != 0, use that month's Bestand in Kannen + for (y, m) in reversed(window): + sh = sheets_by_ym.get((y, m)) + if not sh: + continue + + gasbestand = get_bottom1_value(sh, GASBESTAND_ROW_INDEX, GASBESTAND_COL_NM3) + if gasbestand != 0: + return get_top_right_value(sh, base_client, TOP_RIGHT_ROW_BESTAND_KANNEN) + + # Fallback to previous month (Übertrag_Dez__Vorjahr equivalent) + return get_top_right_value(prev_sheet, base_client, TOP_RIGHT_ROW_BESTAND_KANNEN) + + # Group 1 merged (Fohrer + Buntk.) + g1_best = pick_bestand_top_right("Dr. Fohrer") + right_data["Dr. Fohrer"]["bestand_kannen"] = g1_best + right_data["AG Buntk."]["bestand_kannen"] = g1_best + + # Group 2 merged (Alff + Gutfl.) + g2_best = pick_bestand_top_right("AG Alff") + right_data["AG Alff"]["bestand_kannen"] = g2_best + right_data["AG Gutfl."]["bestand_kannen"] = g2_best + + # Group 3 NOT merged: each M3 client uses its own most recent nonzero Bestand in Kannen-1 + right_data["M3 Thiele"]["bestand_kannen"] = pick_bestand_top_right("M3 Thiele") + right_data["M3 Buntkowsky"]["bestand_kannen"] = pick_bestand_top_right("M3 Buntkowsky") + right_data["M3 Gutfleisch"]["bestand_kannen"] = pick_bestand_top_right("M3 Gutfleisch") + + # Summe Bestand = same as previous row + for cname in RIGHT_CLIENTS: + right_data[cname]['summe_bestand'] = right_data[cname]['bestand_kannen'] + + # Best. in Kannen Vormonat (Lit. L-He) from previous month top_right row_index=7 + g1_prev = get_top_right_value(prev_sheet, "Dr. Fohrer", TOP_RIGHT_ROW_BESTAND_KANNEN) + right_data["Dr. Fohrer"]['best_kannen_vormonat'] = g1_prev + right_data["AG Buntk."]['best_kannen_vormonat'] = g1_prev + + g2_prev = get_top_right_value(prev_sheet, "AG Alff", TOP_RIGHT_ROW_BESTAND_KANNEN) + right_data["AG Alff"]['best_kannen_vormonat'] = g2_prev + right_data["AG Gutfl."]['best_kannen_vormonat'] = g2_prev + + + # Group 1 merged (Fohrer + Buntk.) + g1_prev = get_top_right_value(prev_sheet, "Dr. Fohrer", TOP_RIGHT_ROW_BESTAND_KANNEN) + right_data["Dr. Fohrer"]["best_kannen_vormonat"] = g1_prev + right_data["AG Buntk."]["best_kannen_vormonat"] = g1_prev + + # Group 2 merged (Alff + Gutfl.) + g2_prev = get_top_right_value(prev_sheet, "AG Alff", TOP_RIGHT_ROW_BESTAND_KANNEN) + right_data["AG Alff"]["best_kannen_vormonat"] = g2_prev + right_data["AG Gutfl."]["best_kannen_vormonat"] = g2_prev + + # Group 3 UNMERGED (each one reads its own cell) + right_data["M3 Thiele"]["best_kannen_vormonat"] = get_top_right_value(prev_sheet, "M3 Thiele", TOP_RIGHT_ROW_BESTAND_KANNEN) + right_data["M3 Buntkowsky"]["best_kannen_vormonat"] = get_top_right_value(prev_sheet, "M3 Buntkowsky", TOP_RIGHT_ROW_BESTAND_KANNEN) + right_data["M3 Gutfleisch"]["best_kannen_vormonat"] = get_top_right_value(prev_sheet, "M3 Gutfleisch", TOP_RIGHT_ROW_BESTAND_KANNEN) + + # --- Rückführ. Soll (Lit. L-He) according to your formulas --- + + # Group 1: Dr. Fohrer / AG Buntk. + total_bestand_1 = right_data["Dr. Fohrer"]['summe_bestand'] + best_vormonat_1 = right_data["Dr. Fohrer"]['best_kannen_vormonat'] + diff1 = total_bestand_1 - best_vormonat_1 + share_fohrer = right_data["Dr. Fohrer"]['stand_prev_share'] or Decimal('0') + + right_data["Dr. Fohrer"]['rueckf_soll'] = ( + right_data["Dr. Fohrer"]['bezug'] - diff1 * share_fohrer + ) + right_data["AG Buntk."]['rueckf_soll'] = ( + right_data["AG Buntk."]['bezug'] - total_bestand_1 + best_vormonat_1 + ) + + # Group 2: AG Alff / AG Gutfl. + total_bestand_2 = right_data["AG Alff"]['summe_bestand'] + best_vormonat_2 = right_data["AG Alff"]['best_kannen_vormonat'] + diff2 = total_bestand_2 - best_vormonat_2 + share_alff = right_data["AG Alff"]['stand_prev_share'] or Decimal('0') + share_gutfl = right_data["AG Gutfl."]['stand_prev_share'] or Decimal('0') + + right_data["AG Alff"]['rueckf_soll'] = ( + right_data["AG Alff"]['bezug'] - diff2 * share_alff + ) + right_data["AG Gutfl."]['rueckf_soll'] = ( + right_data["AG Gutfl."]['bezug'] - diff2 * share_gutfl + ) + + # Group 3: M3 Thiele / M3 Buntkowsky / M3 Gutfleisch + for cname in ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"]: + b13 = right_data[cname]['bezug'] + b12 = right_data[cname]['best_kannen_vormonat'] + b11 = right_data[cname]['summe_bestand'] + # Excel: P13+P12-P11 etc. + right_data[cname]['rueckf_soll'] = b13 + b12 - b11 + + # --- Verluste (Soll-Rückf.) (Lit. L-He) = B14 - B6 - B7 --- + for cname in RIGHT_CLIENTS: + b14 = right_data[cname]['rueckf_soll'] + b6 = right_data[cname]['rueckf_fluessig'] + b7 = right_data[cname]['sonder'] + right_data[cname]['verluste'] = b14 - b6 - b7 + + # --- Füllungen warm (Lit. L-He) = sum of monthly 'Füllungen warm' (row_index=11 top_right) --- + for cname in RIGHT_CLIENTS: + total_warm = Decimal('0') + for (y, m) in window: + sheet = sheets_by_ym.get((y, m)) + if sheet: + total_warm += get_top_right_value(sheet, cname, row_index=11) + right_data[cname]['fuellungen_warm'] = total_warm + + # --- Kaltgas Rückgabe (Lit. L-He) – Faktor = Bezug * 0.06 --- + for cname in RIGHT_CLIENTS: + b13 = right_data[cname]['bezug'] + right_data[cname]['kaltgas_rueckgabe'] = b13 * factor + + # --- Verbraucherverluste (Liter L-He) = Verluste - Kaltgas Rückgabe --- + for cname in RIGHT_CLIENTS: + b15 = right_data[cname]['verluste'] + b17 = right_data[cname]['kaltgas_rueckgabe'] + right_data[cname]['verbraucherverluste'] = b15 - b17 + + # --- % = Verbraucherverluste / Bezug --- + for cname in RIGHT_CLIENTS: + bezug = right_data[cname]['bezug'] + verb = right_data[cname]['verbraucherverluste'] + if bezug != 0: + right_data[cname]['percent'] = verb / bezug + else: + right_data[cname]['percent'] = None + + # Build RIGHT rows structure + right_row_defs = [ + ('Stand der Gaszähler (Vorjahr) (Nm³)', 'stand_prev_share'), + # We skip the pure-text "Gasrückführung (Nm³)" line here, + # because it’s only text (Aufteilung nach Verbrauch / Gaszähler) + # and easier to render directly in the template if needed. + ('Rückführung flüssig (Lit. L-He)', 'rueckf_fluessig'), + ('Sonderrückführungen (Lit. L-He)', 'sonder'), + ('Sammelrückführung (Lit. L-He)', 'sammel'), + ('Bestand in Kannen-1 (Lit. L-He)', 'bestand_kannen'), + ('Summe Bestand (Lit. L-He)', 'summe_bestand'), + ('Best. in Kannen Vormonat (Lit. L-He)', 'best_kannen_vormonat'), + ('Bezug (Liter L-He)', 'bezug'), + ('Rückführ. Soll (Lit. L-He)', 'rueckf_soll'), + ('Verluste (Soll-Rückf.) (Lit. L-He)', 'verluste'), + ('Füllungen warm (Lit. L-He)', 'fuellungen_warm'), + ('Kaltgas Rückgabe (Lit. L-He) – Faktor', 'kaltgas_rueckgabe'), + ('Verbraucherverluste (Liter L-He)', 'verbraucherverluste'), + ('%', 'percent'), + ] + + rows_right = [] + for label, key in right_row_defs: + values = [right_data[cname].get(key) for cname in RIGHT_CLIENTS] + + if key == 'percent': + total_bezug = sum((right_data[c]['bezug'] for c in RIGHT_CLIENTS), Decimal('0')) + total_verb = sum((right_data[c]['verbraucherverluste'] for c in RIGHT_CLIENTS), Decimal('0')) + total = (total_verb / total_bezug) if total_bezug != 0 else None + else: + total = sum((v for v in values if isinstance(v, Decimal)), Decimal('0')) + + rows_right.append({ + 'label': label, + 'values': values, + 'total': total, + 'is_percent': key == 'percent', + }) + SUM_TABLE_ROWS = [ + ("Rückführung flüssig (Lit. L-He)", "rueckf_fluessig"), + ("Sonderrückführungen (Lit. L-He)", "sonder"), + ("Sammelrückführungen (Lit. L-He)", "sammel"), + ("Bestand in Kannen-1 (Lit. L-He)", "bestand_kannen"), + ("Summe Bestand (Lit. L-He)", "summe_bestand"), + ("Best. in Kannen Vormonat (Lit. L-He)", "best_kannen_vormonat"), + ("Bezug (Liter L-He)", "bezug"), + ("Rückführ. Soll (Lit. L-He)", "rueckf_soll"), + ("Verluste (Soll-Rückf.) (Lit. L-He)", "verluste"), + ("Füllungen warm (Lit. L-He)", "fuellungen_warm"), + ("Kaltgas Rückgabe (Lit. L-He) – Faktor", "kaltgas_rueckgabe"), + ("Faktor 0.06", "factor_row"), + ("Verbraucherverluste (Liter L-He)", "verbraucherverluste"), + ("%", "percent"), + ] + + RIGHT_GROUPS = { + "chemie": ["Dr. Fohrer", "AG Buntk."], + "mawi": ["AG Alff", "AG Gutfl."], + "m3": ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"], + } + + RIGHT_ALL = ["Dr. Fohrer", "AG Buntk.", "AG Alff", "AG Gutfl.", "M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"] + LEFT_ALL = HALFYEAR_CLIENTS_LEFT + + def safe_pct(verb, bez): + return (verb / bez) if bez != 0 else None + rows_sum = [] + def d(x): + return x if isinstance(x, Decimal) else Decimal("0") + for label, key in SUM_TABLE_ROWS: + + if key == "factor_row": + lichtwiese = chemie = mawi = m3 = total = Decimal("0.06") + + elif key == "percent": + # Right totals + rw_bez = sum(d(right_data[c].get("bezug")) for c in RIGHT_ALL) + rw_verb = sum(d(right_data[c].get("verbraucherverluste")) for c in RIGHT_ALL) + lichtwiese = safe_pct(rw_verb, rw_bez) + + # Chemie + ch_bez = sum(d(right_data[c].get("bezug")) for c in RIGHT_GROUPS["chemie"]) + ch_verb = sum(d(right_data[c].get("verbraucherverluste")) for c in RIGHT_GROUPS["chemie"]) + chemie = safe_pct(ch_verb, ch_bez) + + # MaWi + mw_bez = sum(d(right_data[c].get("bezug")) for c in RIGHT_GROUPS["mawi"]) + mw_verb = sum(d(right_data[c].get("verbraucherverluste")) for c in RIGHT_GROUPS["mawi"]) + mawi = safe_pct(mw_verb, mw_bez) + + # M3 + m3_bez = sum(d(right_data[c].get("bezug")) for c in RIGHT_GROUPS["m3"]) + m3_verb = sum(d(right_data[c].get("verbraucherverluste")) for c in RIGHT_GROUPS["m3"]) + m3 = safe_pct(m3_verb, m3_bez) + + # Σ column = (left verb + right verb) / (left bez + right bez) + left_bez = sum(d(client_data_left[c].get("bezug")) for c in LEFT_ALL) + left_verb = sum(d(client_data_left[c].get("verbraucherverluste")) for c in LEFT_ALL) + total = safe_pct(left_verb + rw_verb, left_bez + rw_bez) + + else: + # normal rows = sums + lichtwiese = sum(d(right_data[c].get(key)) for c in RIGHT_ALL) + chemie = sum(d(right_data[c].get(key)) for c in RIGHT_GROUPS["chemie"]) + mawi = sum(d(right_data[c].get(key)) for c in RIGHT_GROUPS["mawi"]) + m3 = sum(d(right_data[c].get(key)) for c in RIGHT_GROUPS["m3"]) + + left_total = sum(d(client_data_left[c].get(key)) for c in LEFT_ALL) + total = left_total + lichtwiese + + rows_sum.append({ + "row_index": row_index, + "label": label, + "total": total, + "lichtwiese": lichtwiese, + "chemie": chemie, + "mawi": mawi, + "m3": m3, + "is_percent": (key == "percent"), + }) + def find_sum_row(rows, label_startswith: str): + for r in rows: + if str(r.get("label", "")).strip().startswith(label_startswith): + return r + return None + + summe_bestand_row = find_sum_row(rows_sum, "Summe Bestand") + k38 = (summe_bestand_row.get("total") if summe_bestand_row else Decimal("0")) or Decimal("0") + j38 = k38 * Decimal("0.75") + # --- FIX: now that k38 is known, update bottom2 + recompute dependent rows --- + bottom2["k38"] = k38 + bottom2["j38"] = j38 + + k39 = bottom2.get("k39") or Decimal("0") + k40 = bottom2.get("k40") or Decimal("0") + + # Row 43: Bestand flüssig He = SUMME(K38:K40) + k43 = (k38 or Decimal("0")) + k39 + k40 + j43 = k43 * Decimal("0.75") + + bottom2["k43"] = k43 + bottom2["j43"] = j43 + + # Row 44: Gesamtbestand neu = Gasbestand(Lit) from bottom table 1 + k43 + gasbestand_lit = Decimal("0") + for r in bottom1_rows: + if (r.get("label") or "").strip().startswith("Gasbestand"): + gasbestand_lit = r.get("lhe") or Decimal("0") + break + + k44 = gasbestand_lit + k43 + j44 = k44 * Decimal("0.75") + + bottom2["k44"] = k44 + bottom2["j44"] = j44 + def d(x): + return x if isinstance(x, Decimal) else Decimal("0") + + # ---- Bottom2: J38/K38 depend on rows_sum (overall summary), so do it HERE ---- + k38 = Decimal("0") + for r in rows_sum: + if r.get("label") == "Summe Bestand (Lit. L-He)": + k38 = r.get("total") or Decimal("0") + break + + j38 = k38 * FACTOR_NM3_TO_LHE # 0.75 + bottom2["k38"] = k38 + bottom2["j38"] = j38 + factor = Decimal("0.75") + + # window = the 6-month list you already build in this view: [(y,m), (y,m), ...] + # bottom2 = dict with "k44" already computed in your view + # rows_sum = overall sum group rows list (your existing halfyear logic) + + # 1) K46 = K44 from the month BEFORE the global month (interval start) + start_year = interval_year + start_month = interval_start # whatever you named your start month variable + + if start_month == 1: + prev_year, prev_month = start_year - 1, 12 + else: + prev_year, prev_month = start_year, start_month - 1 + + prev_sheet = MonthlySheet.objects.filter(year=prev_year, month=prev_month).first() + + # This assumes you have MonthlySummary.gesamtbestand_neu_lhe as K44 equivalent. + # If your field name differs, tell me your MonthlySummary model fields. + prev_k44 = Decimal("0") + if prev_sheet: + prev_sum = MonthlySummary.objects.filter(sheet=prev_sheet).first() + if prev_sum and prev_sum.gesamtbestand_neu_lhe is not None: + prev_k44 = Decimal(str(prev_sum.gesamtbestand_neu_lhe)) + + # helpers: read bottom_3 values for a given sheet/month + def get_bottom3_value(sheet, row_index, col_index): + if not sheet: + return Decimal("0") + c = Cell.objects.filter( + sheet=sheet, table_type="bottom_3", + row_index=row_index, column_index=col_index + ).first() + if not c or c.value in (None, "", "None"): + return Decimal("0") + try: + return Decimal(str(c.value)) + except Exception: + return Decimal("0") + + # 2) Sum rows across the 6-month window + g47_sum = Decimal("0") + i47_sum = Decimal("0") + j47_sum = Decimal("0") + + g50_sum = Decimal("0") + i50_sum = Decimal("0") + + for (yy, mm) in window: + s = MonthlySheet.objects.filter(year=yy, month=mm).first() + ## Excel row 47 maps to bottom_3 row_index=1 in DB (see monthly_sheet.html) + g = get_bottom3_value(s, 1, 1) # G47 editable + i = get_bottom3_value(s, 1, 2) # I47 editable + + g47_sum += g + i47_sum += i + + # In monthly_sheet, J47 is CALCULATED as (G47 + I47), not stored as an editable cell + j47_sum += (g + i) + + # row 50: G(2), I(4) + g50_sum += get_bottom3_value(s, 50, 2) + i50_sum += get_bottom3_value(s, 50, 4) + + # 3) K52 = Verbraucherverlust from overall sum group first column (global Σ) + k52 = Decimal("0") + for r in rows_sum: + label = (r.get("label") or "") + if label.startswith("Verbraucherverluste"): + k52 = r.get("total") or Decimal("0") + break + + # --- apply the SAME monthly formulas, with your overrides --- + # Row 46 + k46 = prev_k44 + j46 = k46 * factor + + # Row 47 + g47 = g47_sum + i47 = i47_sum + j47 = j47_sum + k47 = (j47 / factor) + g47 if (j47 != 0 or g47 != 0) else Decimal("0") + + # Row 48 + k48 = k46 + k47 + j48 = k48 * factor + + # Row 49 (akt. Monat) -> in halfyear we use current bottom2 K44 + k49 = bottom2.get("k44") or Decimal("0") + j49 = k49 * factor + + # Row 50 + g50 = g50_sum + i50 = i50_sum + j50 = i50 + k50 = (j50 / factor) if j50 != 0 else Decimal("0") + + # Row 51 + k51 = k48 - k49 - k50 + j51 = k51 * factor + + # Row 52 + j52 = k52 * factor + + # Row 53 + k53 = k51 - k52 + j53 = j51 - j52 + + bottom3 = { + "j46": j46, "k46": k46, + "g47": g47, "i47": i47, "j47": j47, "k47": k47, + "j48": j48, "k48": k48, + "j49": j49, "k49": k49, + "g50": g50, "i50": i50, "j50": j50, "k50": k50, + "j51": j51, "k51": k51, + "j52": j52, "k52": k52, + "j53": j53, "k53": k53, + } + # ------------------------------------------------------------------ + # 4) Context – keep old keys AND new ones + # ------------------------------------------------------------------ + context = { + 'interval_year': interval_year, + 'interval_start_month': interval_start, + 'window': window, + + 'clients': HALFYEAR_CLIENTS_LEFT, + 'rows': rows_left, + + 'clients_left': HALFYEAR_CLIENTS_LEFT, + 'rows_left': rows_left, + + 'clients_right': RIGHT_CLIENTS, + 'rows_right': rows_right, + 'rows_sum': rows_sum, + 'bottom1_rows': bottom1_rows, + } + context["bottom2"] = bottom2 + context["bottom3"] = bottom3 + context["context_bottom2_g39"] = bottom2_inputs["g39"] + context["context_bottom2_i39"] = bottom2_inputs["i39"] + return context \ No newline at end of file diff --git a/sheets/templates/Rechnung.html b/sheets/templates/Rechnung.html new file mode 100644 index 0000000..7f6679a --- /dev/null +++ b/sheets/templates/Rechnung.html @@ -0,0 +1,123 @@ +{% extends "base.html" %} +{% block content %} +
+ + +
+ ← Übersicht +

Rechnung

+ Abrechnung +
+ + {% if needs_interval %} +
+

Bitte zuerst ein 6-Monats-Intervall auf der Übersichtsseite auswählen.

+
+ {% else %} + +
+ + + + + + + + + + + + + + + + + + + + + + + + {% for r in rows %} + + + + + + + + + + + + + + + + + + + + + {% endfor %} + +
ArbeitsgruppeTabellenkürzelNameHeliumverluste (€)Heliumverluste (l)Verbr. u. Repar. (€)Gesamtverflüssigung (l)Preis (€/l)Bezogene Menge (l)Verflüssigungskosten (€)Verlustkosten (€)Gaskosten (€)Gutschriften (€)Personalkosten (€)Anteil Personalkosten (€)Gesamtkosten (€)
{{ r.arbeitsgruppe }}{{ r.tabellenkuerzel }}{{ r.name }}{{ r.heliumverluste_eur|floatformat:2 }}{{ r.heliumverluste_l|floatformat:2 }}{{ r.verbr_u_repar_eur|floatformat:2 }}{{ r.gesamtverfluessigung_l|floatformat:2 }}{{ r.preis_eur_l|floatformat:4 }}{{ r.bezogene_menge_l|floatformat:2 }}{{ r.verfluessigungskosten_eur|floatformat:2 }}{{ r.verlustkosten_eur|floatformat:2 }}{{ r.gaskosten_eur|floatformat:2 }}{{ r.gutschriften_eur|floatformat:2 }}{{ r.personalkosten_eur|floatformat:2 }}{{ r.anteil_personal_eur|floatformat:2 }}{{ r.gesamtkosten_eur|floatformat:2 }}
+
+ + {% endif %} +
+{% endblock %} diff --git a/sheets/templates/abrechnung.html b/sheets/templates/abrechnung.html new file mode 100644 index 0000000..015d8c1 --- /dev/null +++ b/sheets/templates/abrechnung.html @@ -0,0 +1,370 @@ +{% extends "base.html" %} +{% load dict_extras %} +{% block content %} +
+ + + +
+ ← Übersicht +

Abrechnung

+ Halbjahres-Bilanz +
+
+ {% if needs_interval %} +
+

Bitte zuerst ein Halbjahr auswählen (auf der Übersicht-Seite), damit der Zeitraum bekannt ist.

+
+ {% else %} +
+ +
+ Aufstellung Heliumverbrauch für Zeitraum: {{ interval_text }} +
+
+
+ + + + + + {% for col_key, col_label in columns %} + + {% endfor %} + + + + + + + {% for r in rows %} + + + + {% for c in r.cells %} + + {% endfor %} + + + + {% endfor %} + + + +
← Fachgebiet →{{ col_label }}Einheit
{{ r.label }} + {% if r.row_key == "sonstiges" %} + + {{ sonstiges_text|get_item:c.col_key }} + + {% else %} + {% if r.editable %} + + {% else %} + + {% if c.value is not None %}{{ c.value|floatformat:2 }}{% endif %} + + {% endif %} + {% endif %} + {{ r.unit }}
+
+ +
+ + + + + + + + + + + + + {% for r in right_summary_rows %} + + + + + + + + + + + + {% endfor %} + +
Summe Lichtwiese
IJKL
+ ← Fachgebiet → + Summe Lichtwiese
NOP (Check)
Summe
Stadtmitte
Summe
(Check)
+ {% if r.is_text %}{{ r.ijkl }}{% else %}{{ r.ijkl|floatformat:2 }}{% endif %} + + {{ r.label }} + {% if r.unit %} + {{ r.unit }} + {% endif %} + + {% if r.is_text %}{{ r.nop }}{% else %}{{ r.nop|floatformat:2 }}{% endif %} + + {% if r.is_text %}{{ r.stadt }}{% else %}{{ r.stadt|floatformat:2 }}{% endif %} + + {% if r.is_text %}{{ r.check }}{% else %}{{ r.check|floatformat:2 }}{% endif %} +
+ +
+ +
+ + + +
+ + + + + + +{% endif %} +
+{% endblock %} diff --git a/sheets/templates/betriebskosten_list.html b/sheets/templates/betriebskosten_list.html index a745799..7d9ab68 100644 --- a/sheets/templates/betriebskosten_list.html +++ b/sheets/templates/betriebskosten_list.html @@ -35,15 +35,19 @@ text-align: center; border-bottom: 1px solid #ddd; } - th:nth-child(1), td:nth-child(1) { width: 5%; } /* # column */ - th:nth-child(2), td:nth-child(2) { width: 10%; } /* Buchungsdatum */ - th:nth-child(3), td:nth-child(3) { width: 15%; } /* Rechnungsnummer */ - th:nth-child(4), td:nth-child(4) { width: 10%; } /* Kostentyp */ - th:nth-child(5), td:nth-child(5) { width: 10%; } /* Gasvolumen */ - th:nth-child(6), td:nth-child(6) { width: 10%; } /* Betrag */ - th:nth-child(7), td:nth-child(7) { width: 25%; } /* Beschreibung */ - th:nth-child(8), td:nth-child(8) { width: 15%; } /* Actions */ - + th:nth-child(1), td:nth-child(1) { width: 4%; } + th:nth-child(2), td:nth-child(2) { width: 12%; } + th:nth-child(3), td:nth-child(3) { width: 8%; } + th:nth-child(4), td:nth-child(4) { width: 12%; } + th:nth-child(5), td:nth-child(5) { width: 8%; } + th:nth-child(6), td:nth-child(6) { width: 8%; } + th:nth-child(7), td:nth-child(7) { width: 8%; } + th:nth-child(8), td:nth-child(8) { width: 8%; } + th:nth-child(9), td:nth-child(9) { width: 8%; } + th:nth-child(10), td:nth-child(10) { width: 8%; } + th:nth-child(11), td:nth-child(11) { width: 12%; } + th:nth-child(12), td:nth-child(12) { width: 8%; } + .actions { white-space: nowrap; /* Prevent buttons from wrapping */ } @@ -147,74 +151,126 @@

Betriebskosten

- - - - - - - - - - - - - - - - {% for item in items %} - - - - - - - - - - - {% empty %} - - - - {% endfor %} - +
+ + +
+ +
#BuchungsdatumRechnungsnummerKostentypGasvolumen (L)Betrag (€)BeschreibungActions
{{ forloop.counter }}{{ item.buchungsdatum|date:"Y-m-d" }}{{ item.rechnungsnummer }}{{ item.get_kostentyp_display }}{{ item.gas_volume|default_if_none:"-" }}{{ item.betrag }}{{ item.beschreibung|default:"" }} - - -
No entries found
+ + + + + + + + + + + + + + + + + + + +
Instandhaltung{{ summary.instandhaltung|floatformat:2 }} €
Verflüssigungskosten L-He{{ verfluessigungskosten_lhe|floatformat:6 }} €/L-He
Heliumkosten{{ summary.heliumkosten|floatformat:2 }} €
Bezugskosten -GasHe{{ summary.bezugskosten_gashe|floatformat:2 }} €/m³
Umlage Personal{{ summary.umlage_personal|floatformat:2 }} €
+ + + + + + + + + + + + + + + + + + + + + + {% for item in items %} + + + + + + + + + + + + + + + + + {% empty %} + + + + {% endfor %} + +
#GegenstandZahlungsdatumFirmaKostentypGasvolumen (m³)Gasvolumen (Liter)Preis pro m³ (€)Preis pro Liter (€)Betrag (€)BeschreibungActions
{{ forloop.counter }}{{ item.gegenstand }}{{ item.buchungsdatum|date:"Y-m-d" }}{{ item.rechnungsnummer }}{{ item.get_kostentyp_display }}{{ item.gas_volume|default_if_none:"-" }}{% if item.gas_volume %}{{ item.gas_volume_liter|floatformat:2 }}{% else %}-{% endif %}{% if item.price_per_m3 %}{{ item.price_per_m3|floatformat:2 }}{% else %}-{% endif %}{% if item.price_per_liter %}{{ item.price_per_liter|floatformat:2 }}{% else %}-{% endif %}{{ item.betrag }}{{ item.beschreibung|default:"" }} + + +
+ No entries found +
+