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
+
+
@@ -16,6 +48,8 @@
{% endfor %}
+
+
@@ -48,6 +82,7 @@
Go to Admin Panel
Betriebskosten
Monthly 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
+
+
+
+ | Bezeichnung |
+ {% for c in clients_left %}
+ {{ c }} |
+ {% endfor %}
+ Σ |
+
+
+
+ {% for row in rows_left %}
+
+ | {{ row.label }} |
+ {% for v in row.values %}
+
+ {% if row.is_percent and v is not None %}
+ {{ v|floatformat:4 }}
+ {% elif v is not None %}
+ {{ v|floatformat:2 }}
+ {% endif %}
+ |
+ {% endfor %}
+
+ {% if row.is_percent and row.total %}
+ {{ row.total|floatformat:4 }}
+ {% elif row.total is not None %}
+ {{ row.total|floatformat:2 }}
+ {% endif %}
+ |
+
+ {% endfor %}
+
+
+
+
+
+
+
Top Right – Halbjahresbilanz
+
+
+
+ | Bezeichnung |
+ {% for c in clients_right %}
+ {{ c }} |
+ {% endfor %}
+ Σ |
+
+
+
+ {% for row in rows_right %}
+
+ | {{ row.label }} |
+ {% for v in row.values %}
+
+ {% 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 %}
+ |
+ {% endfor %}
+
+ {% 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 %}
+ |
+
+ {% endfor %}
+
+
+
+
+
Summe
+
+
+
+ | Bezeichnung |
+ Σ |
+ Licht-wiese |
+ Chemie |
+ MaWi |
+ M3 |
+
+
+
+ {% for r in rows_sum %}
+
+ | {{ 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 %}
+ |
+
+ {% endfor %}
+
+
+
+
+
Bottom Table 1 – Bilanz (read-only)
+
+
+
+
+ | Gasspeicher |
+ Volumen |
+ bar |
+ korrigiert |
+ Nm³ |
+ Lit. LHe |
+
+
+
+
+ {% for r in bottom1_rows %}
+
+ | {{ r.label }} |
+ {{ r.volume|floatformat:1 }} |
+ {{ r.bar|floatformat:0 }} |
+ {{ r.korr|floatformat:1 }} |
+ {{ r.nm3|floatformat:0 }} |
+ {{ r.lhe|floatformat:0 }} |
+
+ {% endfor %}
+
+
+
+
+
+
+
{# 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 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)
+
+
+
+ | Row Label |
+ Σ |
+
+
+
+ {% for row in summary_rows %}
+
+ | {{ row.label }} |
+
+ {{ row.sum|default_if_none:"" }}
+ |
+
+ {% endfor %}
+
+
+
-
Bottom Table 1
+
Bottom Table 1 – Gasspeicher
+
+
+
- | Row Label |
- {% for client in clients %}
- {{ client.name }} |
- {% endfor %}
+ Gasspeicher |
+ Volumen |
+ bar |
+ korrigiert |
+ Nm³ |
+ Lit. LHe |
+
{% for row in cells_by_table.bottom_1 %}
+ {% with rownum=forloop.counter %}
- | Row {{ forloop.counter }} |
- {% for cell in row %}
-
- {{ 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 %}
|
+
+ {% for cell in row %}
+ {% if forloop.counter0 < 5 %}
+ {% if forloop.counter0 == 0 %}
+ {# Volumen – fixed, not editable #}
+
+ {% 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 %}
+ |
+
+ {% elif forloop.counter0 == 1 %}
+ {# bar – editable only for rows 1–9 (not Gasbestand) #}
+ {% if rownum < 10 %}
+
+ {{ cell.value|default:"" }}
+ |
+ {% else %}
+
+ {{ cell.value|default:"" }}
+ |
+ {% endif %}
+
+ {% else %}
+ {# korrigiert, Nm³, Lit. LHe – calculated, not editable #}
+
+ {{ cell.value|default:"" }}
+ |
+ {% endif %}
+ {% endif %}
{% endfor %}
+ {% endwith %}
{% endfor %}
+
+
+
-
Bottom Table 2
+
Bottom Table 2 – Verbraucherbestand L-He
+
-
-
- | Row Label |
- {% for client in clients %}
- {{ client.name }} |
- {% endfor %}
-
-
- {% for row in cells_by_table.bottom_2 %}
-
- | Row {{ forloop.counter }} |
- {% for cell in row %}
-
+ | Verbraucherbestand L-He |
+ |
+ |
+
+
+ {# Excel row 39: + Anlage – G39 / I39 are the ONLY editable inputs #}
+
+ | + Anlage |
+ Gefäss 2,5 |
+ {% with cell=cells_by_table.bottom_2.0.0 %}
+
{{ cell.value|default:"" }}
|
- {% endfor %}
+ {% endwith %}
+ Gefäss 1,0 |
+ {% with cell=cells_by_table.bottom_2.0.1 %}
+
+ {{ cell.value|default:"" }}
+ |
+ {% endwith %}
+ |
+ |
+
+
+ {# Excel row 40: + Kaltgas – all calculated #}
+
+ | + Kaltgas |
+ Gefäss 2,5 |
+ |
+ Gefäss 1,0 |
+ |
+ |
+ |
+
+
+ {# Excel row 43: Bestand flüssig He – uses K38..K40 #}
+
+ | Bestand flüssig He |
+ |
+ |
+
+
+ {# Excel row 44: Gesamtbestand neu – Gasbestand (bottom 1) + K43 #}
+
+ | Gesamtbestand neu |
+ |
+ |
- {% endfor %}
-
-
-
Bottom Table 3
+
+
+
Bottom Table 3 – Bilanz
+
- | Row Label |
- {% for client in clients %}
- {{ client.name }} |
- {% endfor %}
+ Bezeichnung |
+ F |
+ G |
+ I |
+ Nm³ (J) |
+ Lit. L-He (K) |
- {% for row in cells_by_table.bottom_3 %}
-
- | Row {{ forloop.counter }} |
- {% for cell in row %}
-
+ | Gesamtbestand Vormonat |
+ |
+ |
+ |
+
+ {# J46 comes from bottom_3 row 0, col 3 #}
+ {% with cell=cells_by_table.bottom_3.0.3 %}
+
+ {{ cell.value|default:"" }}
+ |
+ {% endwith %}
+
+ {# K46 comes from bottom_3 row 0, col 4 (prev month K44) #}
+ {% with cell=cells_by_table.bottom_3.0.4 %}
+
+ {% if prev_summary %}
+ {{ prev_summary.gesamtbestand_neu_lhe|default_if_none:'' }}
+ {% endif %}
+ |
+ {% endwith %}
+
+
+
+
+ {# Row 47: + Verbrauch / Anlage #}
+
+ | + Verbrauch / Anlage |
+
+
+ |
+
+
+ {% with cell=cells_by_table.bottom_3.1.1 %}
+
{{ cell.value|default:"" }}
|
- {% endfor %}
+ {% endwith %}
+
+
+ {% with cell=cells_by_table.bottom_3.1.2 %}
+
+ {{ cell.value|default:"" }}
+ |
+ {% endwith %}
+
+ |
+ |
+
+
+ {# Row 48: K48 = K46 + K47, J48 = K48 * 0.75 #}
+
+ | Gesamtbestand inkl. Anlage |
+ |
+ |
+ |
+ |
+ |
+
+
+ {# Row 49: K49 = current month K44, J49 = K49 * 0.75 (will fill via JS) #}
+
+ | Gesamtbestand (akt. Monat) |
+ |
+ |
+ |
+ |
+ |
+
+
+ {# Row 50: I50 editable, J50/K50 readonly #}
+
+ | Verbrauch Sonstiges |
+ |
+ |
+
+ {% with cell=cells_by_table.bottom_3.4.2 %}
+
+ {{ cell.value|default:"" }}
+ |
+ {% endwith %}
+
+ |
+ |
+
+
+ {# Row 51: K51 = K48 - K49 - K50, J51 = K51 * 0.75 #}
+
+ | Differenz Bestand |
+ |
+ |
+ |
+ |
+ |
+
+
+ {# Row 52: K52 = Verbraucherverluste (aus Übersicht), J52 = K52 * 0.75 #}
+
+ | Verbraucherverluste (aus Übersicht) |
+ |
+ |
+ |
+ |
+ |
+
+
+ {# Row 53: J53 = J51 - J52, K53 = K51 - K52 #}
+
+ | Differenz Bilanz |
+ |
+ |
+ |
+ |
+ |
- {% endfor %}
+
+
+
@@ -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)
- {% 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