diff --git a/db.sqlite3 b/db.sqlite3 index 10e7ffb..95ff630 100644 Binary files a/db.sqlite3 and b/db.sqlite3 differ diff --git a/excel_mimic/__pycache__/__init__.cpython-313.pyc b/excel_mimic/__pycache__/__init__.cpython-313.pyc index b2de94a..9c331f8 100644 Binary files a/excel_mimic/__pycache__/__init__.cpython-313.pyc and b/excel_mimic/__pycache__/__init__.cpython-313.pyc differ diff --git a/excel_mimic/__pycache__/urls.cpython-313.pyc b/excel_mimic/__pycache__/urls.cpython-313.pyc index 124f410..e4871ae 100644 Binary files a/excel_mimic/__pycache__/urls.cpython-313.pyc and b/excel_mimic/__pycache__/urls.cpython-313.pyc differ diff --git a/excel_mimic/__pycache__/wsgi.cpython-313.pyc b/excel_mimic/__pycache__/wsgi.cpython-313.pyc index d62485e..4a6e0b4 100644 Binary files a/excel_mimic/__pycache__/wsgi.cpython-313.pyc and b/excel_mimic/__pycache__/wsgi.cpython-313.pyc differ diff --git a/sheets/__pycache__/__init__.cpython-313.pyc b/sheets/__pycache__/__init__.cpython-313.pyc index 839958d..ef09e1a 100644 Binary files a/sheets/__pycache__/__init__.cpython-313.pyc and b/sheets/__pycache__/__init__.cpython-313.pyc differ diff --git a/sheets/__pycache__/admin.cpython-313.pyc b/sheets/__pycache__/admin.cpython-313.pyc index b7545a9..f3a533d 100644 Binary files a/sheets/__pycache__/admin.cpython-313.pyc and b/sheets/__pycache__/admin.cpython-313.pyc differ diff --git a/sheets/__pycache__/apps.cpython-313.pyc b/sheets/__pycache__/apps.cpython-313.pyc index 076120a..04d4936 100644 Binary files a/sheets/__pycache__/apps.cpython-313.pyc and b/sheets/__pycache__/apps.cpython-313.pyc differ diff --git a/sheets/__pycache__/models.cpython-313.pyc b/sheets/__pycache__/models.cpython-313.pyc index ea468c7..e56e1d3 100644 Binary files a/sheets/__pycache__/models.cpython-313.pyc and b/sheets/__pycache__/models.cpython-313.pyc differ diff --git a/sheets/__pycache__/signals.cpython-313.pyc b/sheets/__pycache__/signals.cpython-313.pyc new file mode 100644 index 0000000..e513283 Binary files /dev/null and b/sheets/__pycache__/signals.cpython-313.pyc differ diff --git a/sheets/__pycache__/urls.cpython-313.pyc b/sheets/__pycache__/urls.cpython-313.pyc index dc17ed7..aab0abc 100644 Binary files a/sheets/__pycache__/urls.cpython-313.pyc and b/sheets/__pycache__/urls.cpython-313.pyc differ diff --git a/sheets/__pycache__/views.cpython-313.pyc b/sheets/__pycache__/views.cpython-313.pyc index 58cd178..6f84df9 100644 Binary files a/sheets/__pycache__/views.cpython-313.pyc and b/sheets/__pycache__/views.cpython-313.pyc differ diff --git a/sheets/migrations/0013_monthlysummary.py b/sheets/migrations/0013_monthlysummary.py new file mode 100644 index 0000000..6630830 --- /dev/null +++ b/sheets/migrations/0013_monthlysummary.py @@ -0,0 +1,24 @@ +# Generated by Django 5.1.5 on 2026-02-09 07:01 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('sheets', '0012_tableconfig_rowcalculation'), + ] + + operations = [ + migrations.CreateModel( + name='MonthlySummary', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('gesamtbestand_neu_lhe', models.DecimalField(blank=True, decimal_places=6, max_digits=18, null=True)), + ('gasbestand_lhe', models.DecimalField(blank=True, decimal_places=6, max_digits=18, null=True)), + ('verbraucherverlust_lhe', models.DecimalField(blank=True, decimal_places=6, max_digits=18, null=True)), + ('sheet', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='summary', to='sheets.monthlysheet')), + ], + ), + ] diff --git a/sheets/migrations/__pycache__/0001_initial.cpython-313.pyc b/sheets/migrations/__pycache__/0001_initial.cpython-313.pyc index 82ac4dc..7419aae 100644 Binary files a/sheets/migrations/__pycache__/0001_initial.cpython-313.pyc and b/sheets/migrations/__pycache__/0001_initial.cpython-313.pyc differ diff --git a/sheets/migrations/__pycache__/0002_remove_secondtableentry_age_and_more.cpython-313.pyc b/sheets/migrations/__pycache__/0002_remove_secondtableentry_age_and_more.cpython-313.pyc index fa8a409..dc90b87 100644 Binary files a/sheets/migrations/__pycache__/0002_remove_secondtableentry_age_and_more.cpython-313.pyc and b/sheets/migrations/__pycache__/0002_remove_secondtableentry_age_and_more.cpython-313.pyc differ diff --git a/sheets/migrations/__pycache__/0003_alter_secondtableentry_lhe_output.cpython-313.pyc b/sheets/migrations/__pycache__/0003_alter_secondtableentry_lhe_output.cpython-313.pyc index 5f440ff..e79eff9 100644 Binary files a/sheets/migrations/__pycache__/0003_alter_secondtableentry_lhe_output.cpython-313.pyc and b/sheets/migrations/__pycache__/0003_alter_secondtableentry_lhe_output.cpython-313.pyc differ diff --git a/sheets/migrations/__pycache__/0004_alter_secondtableentry_lhe_output.cpython-313.pyc b/sheets/migrations/__pycache__/0004_alter_secondtableentry_lhe_output.cpython-313.pyc index 494393b..0b7ce9b 100644 Binary files a/sheets/migrations/__pycache__/0004_alter_secondtableentry_lhe_output.cpython-313.pyc and b/sheets/migrations/__pycache__/0004_alter_secondtableentry_lhe_output.cpython-313.pyc differ diff --git a/sheets/migrations/__pycache__/0005_alter_secondtableentry_lhe_output.cpython-313.pyc b/sheets/migrations/__pycache__/0005_alter_secondtableentry_lhe_output.cpython-313.pyc index a35dee6..9a8f8a4 100644 Binary files a/sheets/migrations/__pycache__/0005_alter_secondtableentry_lhe_output.cpython-313.pyc and b/sheets/migrations/__pycache__/0005_alter_secondtableentry_lhe_output.cpython-313.pyc differ diff --git a/sheets/migrations/__pycache__/0006_remove_excelentry_age_remove_excelentry_email_and_more.cpython-313.pyc b/sheets/migrations/__pycache__/0006_remove_excelentry_age_remove_excelentry_email_and_more.cpython-313.pyc index de75832..7043f1a 100644 Binary files a/sheets/migrations/__pycache__/0006_remove_excelentry_age_remove_excelentry_email_and_more.cpython-313.pyc and b/sheets/migrations/__pycache__/0006_remove_excelentry_age_remove_excelentry_email_and_more.cpython-313.pyc differ diff --git a/sheets/migrations/__pycache__/0007_betriebskosten.cpython-313.pyc b/sheets/migrations/__pycache__/0007_betriebskosten.cpython-313.pyc index 7a4fd09..031505b 100644 Binary files a/sheets/migrations/__pycache__/0007_betriebskosten.cpython-313.pyc and b/sheets/migrations/__pycache__/0007_betriebskosten.cpython-313.pyc differ diff --git a/sheets/migrations/__pycache__/0013_monthlysummary.cpython-313.pyc b/sheets/migrations/__pycache__/0013_monthlysummary.cpython-313.pyc new file mode 100644 index 0000000..0860c36 Binary files /dev/null and b/sheets/migrations/__pycache__/0013_monthlysummary.cpython-313.pyc differ diff --git a/sheets/models.py b/sheets/models.py index d1990bc..2cb7f8f 100644 --- a/sheets/models.py +++ b/sheets/models.py @@ -212,4 +212,42 @@ class SecondTableEntry(models.Model): date_joined = models.DateField(auto_now_add=True) def __str__(self): - return f"{self.client.name} - {self.date}" \ No newline at end of file + return f"{self.client.name} - {self.date}" + +class MonthlySummary(models.Model): + """ + Stores per-month summary values that we need in other months + or in the half-year overview. + """ + sheet = models.OneToOneField( + MonthlySheet, + on_delete=models.CASCADE, + related_name='summary', + ) + + # K44: Gesamtbestand neu (Lit. LHe) from Bottom Table 2 + gesamtbestand_neu_lhe = models.DecimalField( + max_digits=18, + decimal_places=6, + null=True, + blank=True, + ) + + # Gasbestand (Lit. LHe) from Bottom Table 1 + gasbestand_lhe = models.DecimalField( + max_digits=18, + decimal_places=6, + null=True, + blank=True, + ) + + # Verbraucherverluste (Lit. L-He) from overall summary + verbraucherverlust_lhe = models.DecimalField( + max_digits=18, + decimal_places=6, + null=True, + blank=True, + ) + + def __str__(self): + return f"Summary {self.sheet.year}-{self.sheet.month:02d}" \ No newline at end of file diff --git a/sheets/templates/clients_table.html b/sheets/templates/clients_table.html index cd65cba..d47d16d 100644 --- a/sheets/templates/clients_table.html +++ b/sheets/templates/clients_table.html @@ -4,6 +4,38 @@

Helium Output Yearly Summary

+
+ {% csrf_token %} +

Global 6-Month Interval

+ + + + + + + + +
+
@@ -16,6 +48,8 @@ {% endfor %}
+ + @@ -48,6 +82,7 @@ Go to Admin PanelBetriebskostenMonthly Sheets + Halbjahres Bilanz @@ -136,5 +171,22 @@ .admin-button:hover { background-color: #5a6268; } + .interval-filter { + margin: 10px 0 25px 0; + text-align: left; + } + .interval-filter h3 { + margin-bottom: 8px; + } + .interval-filter label { + margin-right: 6px; + } + .interval-filter select { + margin-right: 12px; + padding: 4px 6px; + } + .interval-filter button { + padding: 6px 12px; + } {% endblock %} \ No newline at end of file diff --git a/sheets/templates/halfyear_balance.html b/sheets/templates/halfyear_balance.html new file mode 100644 index 0000000..951d950 --- /dev/null +++ b/sheets/templates/halfyear_balance.html @@ -0,0 +1,301 @@ +{% 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 }}
+
+ + + + {# closes .spreadsheet-container #} +{% endblock %} diff --git a/sheets/templates/monthly_sheet.html b/sheets/templates/monthly_sheet.html index 896b8db..35f73ce 100644 --- a/sheets/templates/monthly_sheet.html +++ b/sheets/templates/monthly_sheet.html @@ -4,11 +4,28 @@
- ← Previous -

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

- Next → - Go to Summary -
+ {# Previous month link #} + {% with pm=prev_month %} + {% if pm.year and pm.month %} + ← Previous + {% else %} + ← Previous + {% endif %} + {% endwith %} + +

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

+ + {# Next month link #} + {% with nm=next_month %} + {% if nm.year and nm.month %} + Next → + {% else %} + Next → + {% endif %} + {% endwith %} + + Go to Summary +
@@ -211,104 +228,367 @@
- +
+

Gesamtsumme (Top Left + Top Right)

+ + + + + + + + + {% for row in summary_rows %} + + + + + {% endfor %} + +
Row LabelΣ
{{ row.label }} + {{ row.sum|default_if_none:"" }} +
+
-

Bottom Table 1

+

Bottom Table 1 – Gasspeicher

+ + + - - {% for client in clients %} - - {% endfor %} + + + + + + + {% for row in cells_by_table.bottom_1 %} + {% with rownum=forloop.counter %} - - {% for cell in row %} - + + {% for cell in row %} + {% if forloop.counter0 < 5 %} + {% if forloop.counter0 == 0 %} + {# Volumen – fixed, not editable #} + + + {% elif forloop.counter0 == 1 %} + {# bar – editable only for rows 1–9 (not Gasbestand) #} + {% if rownum < 10 %} + + {% else %} + + {% endif %} + + {% else %} + {# korrigiert, Nm³, Lit. LHe – calculated, not editable #} + + {% endif %} + {% endif %} {% endfor %} + {% endwith %} {% endfor %}
Row Label{{ client.name }}GasspeicherVolumenbarkorrigiertNm³Lit. LHe
Row {{ forloop.counter }} - {{ cell.value|default:"" }} + + {% if rownum == 1 %}Batterie 1 + {% elif rownum == 2 %}2 + {% elif rownum == 3 %}3 + {% elif rownum == 4 %}4 + {% elif rownum == 5 %}5 + {% elif rownum == 6 %}6 + {% elif rownum == 7 %}2 Bündel + {% elif rownum == 8 %}2 Ballone + {% elif rownum == 9 %}Reingasspeicher + {% elif rownum == 10 %}Gasbestand + {% endif %} + {% if cell.value %} + {{ cell.value }} + {% else %} + {% if rownum == 1 %}2,4 + {% elif rownum == 2 %}5,1 + {% elif rownum == 3 %}4,0 + {% elif rownum == 4 %}1,0 + {% elif rownum == 5 %}4,0 + {% elif rownum == 6 %}0,4 + {% elif rownum == 7 %}1,2 + {% elif rownum == 8 %}20,0 + {% elif rownum == 9 %}5,0 + {% endif %} + {% endif %} + + {{ cell.value|default:"" }} + + {{ cell.value|default:"" }} + + {{ cell.value|default:"" }} +
+ + +
-

Bottom Table 2

+

Bottom Table 2 – Verbraucherbestand L-He

+ - - - - {% for client in clients %} - - {% endfor %} - - - {% for row in cells_by_table.bottom_2 %} - - - {% for cell in row %} - + + + + + {# Excel row 39: + Anlage – G39 / I39 are the ONLY editable inputs #} + + + + {% with cell=cells_by_table.bottom_2.0.0 %} + - {% endfor %} + {% endwith %} + + {% with cell=cells_by_table.bottom_2.0.1 %} + + {% endwith %} + + + + + {# Excel row 40: + Kaltgas – all calculated #} + + + + + + + + + + + {# Excel row 43: Bestand flüssig He – uses K38..K40 #} + + + + + + + {# Excel row 44: Gesamtbestand neu – Gasbestand (bottom 1) + K43 #} + + + + - {% endfor %}
Row Label{{ client.name }}
Row {{ forloop.counter }} + Verbraucherbestand L-He
+ AnlageGefäss 2,5 {{ cell.value|default:"" }} Gefäss 1,0 + {{ cell.value|default:"" }} +
+ KaltgasGefäss 2,5Gefäss 1,0
Bestand flüssig He
Gesamtbestand neu
- -
-

Bottom Table 3

+ +
+

Bottom Table 3 – Bilanz

+ - - {% for client in clients %} - - {% endfor %} + + + + + + - {% for row in cells_by_table.bottom_3 %} - - - {% for cell in row %} - + + + + + {# J46 comes from bottom_3 row 0, col 3 #} + {% with cell=cells_by_table.bottom_3.0.3 %} + + {% endwith %} + + {# K46 comes from bottom_3 row 0, col 4 (prev month K44) #} + {% with cell=cells_by_table.bottom_3.0.4 %} + + {% endwith %} + + + + + {# Row 47: + Verbrauch / Anlage #} + + + + + + + + {% with cell=cells_by_table.bottom_3.1.1 %} + - {% endfor %} + {% endwith %} + + + {% with cell=cells_by_table.bottom_3.1.2 %} + + {% endwith %} + + + + + + {# Row 48: K48 = K46 + K47, J48 = K48 * 0.75 #} + + + + + + + + + + {# Row 49: K49 = current month K44, J49 = K49 * 0.75 (will fill via JS) #} + + + + + + + + + + {# Row 50: I50 editable, J50/K50 readonly #} + + + + + + {% with cell=cells_by_table.bottom_3.4.2 %} + + {% endwith %} + + + + + + {# Row 51: K51 = K48 - K49 - K50, J51 = K51 * 0.75 #} + + + + + + + + + + {# Row 52: K52 = Verbraucherverluste (aus Übersicht), J52 = K52 * 0.75 #} + + + + + + + + + + {# Row 53: J53 = J51 - J52, K53 = K51 - K52 #} + + + + + + + - {% endfor %}
Row Label{{ client.name }}BezeichnungFGINm³ (J)Lit. L-He (K)
Row {{ forloop.counter }} + Gesamtbestand Vormonat + {{ cell.value|default:"" }} + + {% if prev_summary %} + {{ prev_summary.gesamtbestand_neu_lhe|default_if_none:'' }} + {% endif %} +
+ Verbrauch / Anlage {{ cell.value|default:"" }} + {{ cell.value|default:"" }} +
Gesamtbestand inkl. Anlage
Gesamtbestand (akt. Monat)
Verbrauch Sonstiges + {{ cell.value|default:"" }} +
Differenz Bestand
Verbraucherverluste (aus Übersicht)
Differenz Bilanz
+ + +
@@ -511,17 +791,403 @@ {% endblock %} \ No newline at end of file diff --git a/sheets/templates/table_one.html b/sheets/templates/table_one.html index aac8fba..9f41a2c 100644 --- a/sheets/templates/table_one.html +++ b/sheets/templates/table_one.html @@ -291,42 +291,16 @@
-
Helium Input – 6 Month Overview
- -
-
-
- -
- -
-
- -
- -
- -
-
+
Helium Input – 6 Month Overview
{% if overview %}
Period: - {{ overview.start_month }}–{{ overview.end_month }} / {{ overview.year }} + + {{ overview.start_month }}/{{ overview.start_year }} + – {{ overview.end_month }}/{{ overview.end_year }} + + (selected on the main page)
@@ -361,11 +335,8 @@
- {% else %} -
- No data yet – choose a year and start month and click “Show overview”. -
{% endif %} +
diff --git a/sheets/urls.py b/sheets/urls.py index 4aa8172..c098587 100644 --- a/sheets/urls.py +++ b/sheets/urls.py @@ -1,6 +1,6 @@ from django.urls import path from .views import DebugTopRightView -from .views import SaveCellsView +from .views import SaveCellsView , SaveMonthSummaryView, halfyear_settings,MonthlySheetView, monthly_sheet_root,set_halfyear_interval from . import views urlpatterns = [ @@ -17,9 +17,15 @@ urlpatterns = [ path('quick-debug/', views.QuickDebugView.as_view(), name='quick_debug'), path('test-formula/', views.TestFormulaView.as_view(), name='test_formula'), path('simple-debug/', views.SimpleDebugView.as_view(), name='simple_debug'), + path('sheet/', monthly_sheet_root, name='monthly_sheet_root'), + path('set-halfyear-interval/', set_halfyear_interval, name='set_halfyear_interval'), + path('halfyear-bilanz/', views.halfyear_balance_view, name='halfyear_balance'), + path('sheet///', MonthlySheetView.as_view(), name='monthly_sheet'), + path('settings/halfyear/', halfyear_settings, name='halfyear_settings'), path('sheet///', views.MonthlySheetView.as_view(), name='monthly_sheet'), path('summary///', views.SummarySheetView.as_view(), name='summary_sheet'), - path("save-cells/", SaveCellsView.as_view(), name="save_cells"), + path("save-cells/", SaveCellsView.as_view(), name="save_cells"), path("save-cells/", SaveCellsView.as_view(), name="save_cells"),path('save-month-summary/', SaveMonthSummaryView.as_view(), name='save_month_summary'), + path('save-month-summary/', SaveMonthSummaryView.as_view(), name='save_month_summary'), path('calculate/', views.CalculateView.as_view(), name='calculate'), path('debug-calculation/', views.DebugCalculationView.as_view(), name='debug_calculation'), path('debug-top-right/', DebugTopRightView.as_view(), name='debug_top_right'), diff --git a/sheets/views.py b/sheets/views.py index 4a7898f..e5b7255 100644 --- a/sheets/views.py +++ b/sheets/views.py @@ -1,4 +1,6 @@ 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 @@ -10,7 +12,7 @@ from django.utils import timezone from django.views.generic import TemplateView, View from .models import ( Client, SecondTableEntry, Institute, ExcelEntry, - Betriebskosten, MonthlySheet, Cell, CellReference + Betriebskosten, MonthlySheet, Cell, CellReference, MonthlySummary ) from django.db.models import Sum from django.urls import reverse @@ -18,8 +20,14 @@ 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 +BOTTOM1_COL_VOLUME = 0 +BOTTOM1_COL_BAR = 1 +BOTTOM1_COL_KORR = 2 +BOTTOM1_COL_NM3 = 3 +BOTTOM1_COL_LHE = 4 CLIENT_GROUPS = { 'ikp': { 'label': 'IKP', @@ -239,7 +247,895 @@ CALCULATION_CONFIG = { '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", +] + +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() + # ---------------------------- + # 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)) + + # ------------------------------------------------------------------ + # 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"), + ] + def d(x): + return x if isinstance(x, Decimal) else Decimal("0") + + 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 = [] + + 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({ + "label": label, + "total": total, + "lichtwiese": lichtwiese, + "chemie": chemie, + "mawi": mawi, + "m3": m3, + "is_percent": (key == "percent"), + }) + # ------------------------------------------------------------------ + # 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, + } + return render(request, 'halfyear_balance.html', context) + + + +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 @@ -343,6 +1239,7 @@ def evaluate_formula(formula, values_dict): # 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 @@ -489,82 +1386,218 @@ class MonthlySheetView(TemplateView): "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 - UPDATED for top_right + + # Determine row count row_counts = { - 'top_left': 16, - 'top_right': 16, # Changed from 16 to 17 to include all rows (0-16) - 'bottom_1': 10, - 'bottom_2': 10, - 'bottom_3': 10, + "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') - + all_cells = ( + Cell.objects.filter( + sheet=sheet, + table_type=table_type, + ).select_related("client") + ) + # Group cells by row index cells_by_row = {} for cell in all_cells: if cell.row_index not in cells_by_row: cells_by_row[cell.row_index] = {} cells_by_row[cell.row_index][cell.client.name] = cell - - # For each row (including row 7) + + # 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 - sum_column_index = len(client_names) - - # Calculate sum for the row + + # Calculate sum for this row (includes editable + calculated cells) sum_value = None - total = Decimal('0') + total = Decimal("0") has_value = False - - for cell in display_cells: + + 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: + except Exception: pass - + if has_value: sum_value = total - - rows.append({ - 'cells': display_cells, - 'sum': sum_value, - 'row_index': row_idx, # Add this for debugging - }) - + + # 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, @@ -578,6 +1611,7 @@ class MonthlySheetView(TemplateView): '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 @@ -1138,6 +2172,143 @@ class DebugTopRightView(View): 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 @@ -1198,13 +2369,20 @@ class SaveCellsView(View): 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) @@ -1526,6 +2704,175 @@ class SaveCellsView(View): + 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 + @@ -1770,7 +3117,7 @@ class SaveCellsView(View): cell.value = Decimal(new_value) else: cell.value = None - except: + except Exception: cell.value = None # Only save if value changed @@ -1780,18 +3127,26 @@ class SaveCellsView(View): '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 + # Recalculate for each changed client (top-left tables) for client_id in changed_clients: - self.recalculate_top_left_table(sheet, client_id) + if client_id is not None: + self.recalculate_top_left_table(sheet, client_id) - # Get all updated cells for response + # --- 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 @@ -1803,11 +3158,15 @@ class SaveCellsView(View): '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""" @@ -1886,7 +3245,83 @@ class SaveCellsView(View): 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): @@ -1951,55 +3386,144 @@ class SummarySheetView(TemplateView): # Existing views below (keep all your existing functions) def clients_list(request): - # Get all clients + # --- Clients for the yearly summary table --- clients = Client.objects.all() - # Get all years available in SecondTableEntry + # --- 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] - # Determine selected year + # === 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: - # If no year in GET, default to latest available year or current year if DB empty selected_year = available_years[0] if available_years else datetime.now().year - # Prepare monthly totals per client + # === 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 month in range(1, 13): + for (y, m) in window: total = SecondTableEntry.objects.filter( client=client, - date__year=selected_year, - date__month=month + date__year=y, + date__month=m ).aggregate( - total=Coalesce(Sum('lhe_output'), Value(0, output_field=DecimalField())) + 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) + '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', { - 'monthly_data': monthly_data, - 'current_year': selected_year, 'available_years': available_years, - 'months': ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', - 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] + '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 - # Main table (unchanged) - entries_table1 = ExcelEntry.objects.all().select_related('client', '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() @@ -2009,27 +3533,36 @@ def table_one_view(request): 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 + default_year = available_years[0] # newest year in ExcelEntry else: default_year = timezone.now().year - year_param = request.GET.get('overview_year') - start_month_param = request.GET.get('overview_start_month') + # 🔸 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 - year = int(year_param) if year_param else int(default_year) - start_month = int(start_month_param) if start_month_param else 1 # six-month window - end_month = start_month + 5 - if end_month > 12: - end_month = 12 - - months = list(range(start_month, end_month + 1)) + # --- 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 months: + if window: # Build per-group data groups_entries = [] # for internal calculations @@ -2039,11 +3572,11 @@ def table_one_view(request): values = [] group_total = Decimal('0') - for m in months: + for (y_m, m_m) in window: total = ExcelEntry.objects.filter( client__in=clients_qs, - date__year=year, - date__month=m + date__year=y_m, + date__month=m_m ).aggregate( total=Coalesce(Sum('lhe_ges'), Decimal('0')) )['total'] @@ -2060,7 +3593,7 @@ def table_one_view(request): # month totals across all groups month_totals = [] - for idx in range(len(months)): + for idx in range(len(window)): s = sum((g['values'][idx] for g in groups_entries), Decimal('0')) month_totals.append(s) @@ -2068,11 +3601,11 @@ def table_one_view(request): # Build rows for the template rows = [] - for idx, m in enumerate(months): + for idx, (y_m, m_m) in enumerate(window): row_values = [g['values'][idx] for g in groups_entries] rows.append({ - 'month_number': m, - 'month_label': calendar.month_name[m], + 'month_number': m_m, + 'month_label': calendar.month_name[m_m], 'values': row_values, 'total': month_totals[idx], }) @@ -2080,16 +3613,25 @@ def table_one_view(request): 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, - 'start_month': start_month, - 'end_month': end_month, + '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'), @@ -2109,24 +3651,75 @@ def table_one_view(request): # Table Two View (SecondTableEntry) def table_two_view(request): try: - entries = SecondTableEntry.objects.all().order_by('-date') clients = Client.objects.all().select_related('institute') institutes = Institute.objects.all() - + + # 🔸 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': Client.objects.all().select_related('institute'), - 'institutes': Institute.objects.all() + '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': @@ -2597,4 +4190,43 @@ class SimpleDebugView(View): }) except MonthlySheet.DoesNotExist: - return JsonResponse({'error': f'Sheet with id {sheet_id} not found'}) \ No newline at end of file + 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