diff --git a/db.sqlite3 b/db.sqlite3
index 3567da4..f930b33 100644
Binary files a/db.sqlite3 and b/db.sqlite3 differ
diff --git a/excel_mimic/pyproject.toml b/excel_mimic/pyproject.toml
new file mode 100644
index 0000000..a2e25a6
--- /dev/null
+++ b/excel_mimic/pyproject.toml
@@ -0,0 +1,17 @@
+[tool.ruff]
+line-length = 100
+target-version = "py312"
+exclude = [
+ "migrations",
+ ".venv",
+ "venv",
+ "__pycache__",
+]
+
+[tool.ruff.lint]
+select = ["E", "F", "I"]
+ignore = []
+
+[tool.ruff.format]
+quote-style = "double"
+indent-style = "space"
\ No newline at end of file
diff --git a/excel_mimic/urls.py b/excel_mimic/urls.py
index 1497463..ee3e395 100644
--- a/excel_mimic/urls.py
+++ b/excel_mimic/urls.py
@@ -15,7 +15,7 @@ Including another URLconftesting test
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
-from django.urls import path, include
+from django.urls import include, path
urlpatterns = [
path('admin/', admin.site.urls), # Admin site
diff --git a/round_entries.py b/round_entries.py
deleted file mode 100644
index 0c86f1b..0000000
--- a/round_entries.py
+++ /dev/null
@@ -1,48 +0,0 @@
-from decimal import Decimal, ROUND_HALF_UP
-import django
-import os
-
-os.environ.setdefault("DJANGO_SETTINGS_MODULE", "excel_mimic.settings")
-django.setup()
-
-from sheets.models import ExcelEntry
-
-
-TWO_DEC = Decimal("0.00")
-
-def q2(x):
- return Decimal(str(x)).quantize(TWO_DEC, rounding=ROUND_HALF_UP)
-
-fields = ["korrig_druck", "nm3", "lhe", "lhe_ges"]
-
-to_update = []
-total = 0
-changed_count = 0
-
-qs = ExcelEntry.objects.all().only("id", *fields)
-
-for e in qs.iterator(chunk_size=500):
- total += 1
- changed = False
-
- for f in fields:
- val = getattr(e, f)
- if val is not None:
- new = q2(val)
- if new != val:
- setattr(e, f, new)
- changed = True
-
- if changed:
- to_update.append(e)
- changed_count += 1
-
- if len(to_update) >= 500:
- ExcelEntry.objects.bulk_update(to_update, fields)
- to_update = []
- print(f"processed={total}, changed={changed_count}")
-
-if to_update:
- ExcelEntry.objects.bulk_update(to_update, fields)
-
-print(f"DONE. processed={total}, changed={changed_count}")
diff --git a/sheets/apps.py b/sheets/apps.py
index 519e7f8..cb22ba4 100644
--- a/sheets/apps.py
+++ b/sheets/apps.py
@@ -5,4 +5,4 @@ class SheetsConfig(AppConfig):
name = 'sheets'
def ready(self):
- import sheets.signals
+ pass
diff --git a/sheets/forms.py b/sheets/forms.py
index 1b4f3ba..0f06c43 100644
--- a/sheets/forms.py
+++ b/sheets/forms.py
@@ -1,5 +1,5 @@
from django import forms
-from .models import ExcelEntry, Betriebskosten, Institute
+from .models import ExcelEntry, Betriebskosten
diff --git a/sheets/models.py b/sheets/models.py
index 09dddcc..7a74ba0 100644
--- a/sheets/models.py
+++ b/sheets/models.py
@@ -1,7 +1,10 @@
from django.db import models
from django.utils import timezone
from django.core.validators import MinValueValidator, MaxValueValidator
-
+from decimal import Decimal
+from django.db.models import Sum
+from django.db.models.functions import Coalesce
+from django.db.models import DecimalField, Value
class Institute(models.Model):
name = models.CharField(max_length=100)
@@ -73,8 +76,7 @@ class CellReference(models.Model):
class Meta:
unique_together = ['source_cell', 'target_cell']
-from decimal import Decimal
-from django.db import models
+
class Betriebskosten(models.Model):
KOSTENTYP_CHOICES = [
@@ -263,10 +265,7 @@ class BetriebskostenSummary(models.Model):
umlage_personal = models.DecimalField(max_digits=12, decimal_places=2, default=0)
def recalculate(self):
- from django.db.models import Sum
- from django.db.models.functions import Coalesce
- from django.db.models import DecimalField, Value
- from .models import Betriebskosten
+
items = Betriebskosten.objects.all()
@@ -291,7 +290,7 @@ class BetriebskostenSummary(models.Model):
self.save()
# models.py
-from django.db import models
+
class AbrechnungCell(models.Model):
"""
diff --git a/sheets/new/views.py b/sheets/new/views.py
deleted file mode 100644
index d089af7..0000000
--- a/sheets/new/views.py
+++ /dev/null
@@ -1,5041 +0,0 @@
-from django.shortcuts import render, redirect
-from django.utils.decorators import method_decorator
-from django.views.decorators.csrf import csrf_exempt
-from django.db.models import Sum, Value, DecimalField
-from django.http import JsonResponse
-from django.db.models import Q
-from decimal import Decimal, InvalidOperation
-from django.apps import apps
-from datetime import date, datetime
-import calendar
-from django.utils import timezone
-from django.views.generic import TemplateView, View
-from .models import (
- Client, SecondTableEntry, Institute, ExcelEntry,
- Betriebskosten, MonthlySheet, Cell, CellReference, MonthlySummary ,BetriebskostenSummary,AbrechnungCell
-)
-from django.db.models import Sum
-from django.urls import reverse
-from django.db.models.functions import Coalesce
-from .forms import BetriebskostenForm
-from django.utils.dateparse import parse_date
-from django.contrib.auth.mixins import LoginRequiredMixin
-import json
-FIRST_SHEET_YEAR = 2025
-FIRST_SHEET_MONTH = 1
-
-CLIENT_GROUPS = {
- 'ikp': {
- 'label': 'IKP',
- # exactly as in the Clients admin
- 'names': ['IKP'],
- },
- 'phys_chem_bunt_fohrer': {
- 'label': 'Buntkowsky + Dr. Fohrer',
- # include all variants you might have used for Buntkowsky
- 'names': [
- 'AG Buntk.', # the one in your new entry
- 'AG Buntkowsky.', # from your original list
- 'AG Buntkowsky',
- 'Dr. Fohrer',
- ],
- },
- 'mawi_alff_gutfleisch': {
- 'label': 'Alff + AG Gutfleisch',
- # include both short and full forms
- 'names': [
- 'AG Alff',
- 'AG Gutfl.',
- 'AG Gutfleisch',
- ],
- },
- 'm3_group': {
- 'label': 'M3 Buntkowsky + M3 Thiele + M3 Gutfleisch',
- 'names': [
- 'M3 Buntkowsky',
- 'M3 Thiele',
- 'M3 Gutfleisch',
- ],
- },
-}
-
-# Add this CALCULATION_CONFIG at the top of views.py
-
-CALCULATION_CONFIG = {
- 'top_left': {
- # Row mappings: Django row_index (0-based) to Excel row
- # Excel B4 -> Django row_index 1 (UI row 2)
- # Excel B5 -> Django row_index 2 (UI row 3)
- # Excel B6 -> Django row_index 3 (UI row 4)
-
- # B6 (row_index 3) = B5 (row_index 2) / 0.75
- 3: "2 / 0.75",
-
- # B11 (row_index 10) = B9 (row_index 8)
- 10: "8",
-
- # B14 (row_index 13) = B13 (row_index 12) - B11 (row_index 10) + B12 (row_index 11)
- 13: "12 - 10 + 11",
-
- # Note: B5, B17, B19, B20 require IF logic, so they'll be handled separately
- },
-
- # other tables (top_right, bottom_1, ...) stay as they are
-
-
- ' top_right': {
- # UI Row 1 (Excel Row 4): Stand der Gaszähler (Vormonat) (Nm³)
- 0: {
- 'L': "9 / (9 + 9) if (9 + 9) > 0 else 0", # L4 = L13/(L13+M13)
- 'M': "9 / (9 + 9) if (9 + 9) > 0 else 0", # M4 = M13/(L13+M13)
- 'N': "9 / (9 + 9) if (9 + 9) > 0 else 0", # N4 = N13/(N13+O13)
- 'O': "9 / (9 + 9) if (9 + 9) > 0 else 0", # O4 = O13/(N13+O13)
- 'P': None, # Editable
- 'Q': None, # Editable
- 'R': None, # Editable
- },
-
- # UI Row 2 (Excel Row 5): Gasrückführung (Nm³)
- 1: {
- 'L': "4", # L5 = L8
- 'M': "4", # M5 = L8 (merged)
- 'N': "4", # N5 = N8
- 'O': "4", # O5 = N8 (merged)
- 'P': "4 * 0", # P5 = P8 * P4
- 'Q': "4 * 0", # Q5 = P8 * Q4
- 'R': "4 * 0", # R5 = P8 * R4
- },
-
- # UI Row 3 (Excel Row 6): Rückführung flüssig (Lit. L-He)
- 2: {
- 'L': "4", # L6 = L8 (Sammelrückführungen)
- 'M': "4",
- 'N': "4",
- 'O': "4",
- 'P': "4",
- 'Q': "4",
- 'R': "4",
-},
-
- # UI Row 4 (Excel Row 7): Sonderrückführungen (Lit. L-He) - EDITABLE
- 3: {
- 'L': None,
- 'M': None,
- 'N': None,
- 'O': None,
- 'P': None,
- 'Q': None,
- 'R': None,
- },
-
- # UI Row 5 (Excel Row 8): Sammelrückführungen (Lit. L-He)
- 4: {
- 'L': None, # Will be populated from ExcelEntry
- 'M': None,
- 'N': None,
- 'O': None,
- 'P': None,
- 'Q': None,
- 'R': None,
- },
-
- # UI Row 6 (Excel Row 9): Bestand in Kannen-1 (Lit. L-He) - EDITABLE
- 5: {
- 'L': None,
- 'M': None,
- 'N': None,
- 'O': None,
- 'P': None,
- 'Q': None,
- 'R': None,
- },
-
- # UI Row 7 (Excel Row 10): Summe Bestand (Lit. L-He)
- 6: {
- 'L': None,
- 'M': None,
- 'N': None,
- 'O': None,
- 'P': None,
- 'Q': None,
- 'R': None,
- },
-
- # UI Row 11 (Excel Row 14): Rückführ. Soll (Lit. L-He)
- # handled in calculate_top_right_dependents (merged pairs + M3)
- 10: {
- 'L': None,
- 'M': None,
- 'N': None,
- 'O': None,
- 'P': None,
- 'Q': None,
- 'R': None,
- },
-
- # UI Row 12 (Excel Row 15): Verluste (Soll-Rückf.) (Lit. L-He)
- # handled in calculate_top_right_dependents
- 11: {
- 'L': None,
- 'M': None,
- 'N': None,
- 'O': None,
- 'P': None,
- 'Q': None,
- 'R': None,
- },
-
- # UI Row 14 (Excel Row 17): Kaltgas Rückgabe (Lit. L-He) – Faktor
- # handled in calculate_top_right_dependents (different formulas for pair 1 vs pair 2 + M3)
- 13: {
- 'L': None,
- 'M': None,
- 'N': None,
- 'O': None,
- 'P': None,
- 'Q': None,
- 'R': None,
- },
-
- # UI Row 16 (Excel Row 19): Verbraucherverluste (Liter L-He)
- # handled in calculate_top_right_dependents
- 15: {
- 'L': None,
- 'M': None,
- 'N': None,
- 'O': None,
- 'P': None,
- 'Q': None,
- 'R': None,
- },
-
- # UI Row 17 (Excel Row 20): %
- # handled in calculate_top_right_dependents
- 16: {
- 'L': None,
- 'M': None,
- 'N': None,
- 'O': None,
- 'P': None,
- 'Q': None,
- 'R': None,
- }
-},
- 'bottom_1': {
- 5: "4 + 3 + 2",
- 8: "7 - 6",
- },
- 'bottom_2': {
- 3: "1 + 2",
- 6: "5 - 4",
- },
- 'bottom_3': {
- 2: "0 + 1",
- 5: "3 + 4",
- },
- # Special configuration for summation column (last column)
- 'summation_column': {
- # For each row that should be summed across columns
- 'rows_to_sum': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23], # All rows
- # OR specify specific rows:
- # 'rows_to_sum': [0, 5, 10, 15, 20], # Only specific rows
- # The last column index (0-based)
- 'sum_column_index': 5, # 6th column (0-5) since you have 6 clients
- }
-}
-def build_halfyear_window(interval_year: int, start_month: int):
- """
- Build a list of (year, month) for the 6-month interval, possibly crossing into the next year.
- Example: (2025, 10) -> [(2025,10), (2025,11), (2025,12), (2026,1), (2026,2), (2026,3)]
- """
- window = []
- for offset in range(6):
- total_index = (start_month - 1) + offset # 0-based
- y = interval_year + (total_index // 12)
- m = (total_index % 12) + 1
- window.append((y, m))
- return window
-# ---------------------------------------------------------------------------
-# Halbjahres-Bilanz helpers
-# ---------------------------------------------------------------------------
-
-# You can adjust these indices if needed.
-# Assuming:
-# - bottom_1.table has row "Gasbestand" at some fixed row index,
-# and columns: ... Nm³, Lit. LHe
-GASBESTAND_ROW_INDEX = 9 # <-- adjust if your bottom_1 has a different row index
-GASBESTAND_COL_NM3 = 3 # <-- adjust to the column index for Nm³ in bottom_1
-
-# In top_left / top_right, "Bestand in Kannen-1 (Lit. L-He)" is row_index 5
-BESTAND_KANNEN_ROW_INDEX = 5
-
-def pick_sheet_by_gasbestand(window, sheets_by_ym, prev_sheet):
- """
- Returns the last sheet in the window whose Gasbestand (J36, Nm³ column) != 0.
- If none found, returns prev_sheet (Übertrag_Dez__Vorjahr equivalent).
- """
- for (y, m) in reversed(window):
- sheet = sheets_by_ym.get((y, m))
- if not sheet:
- continue
- gasbestand_nm3 = get_bottom1_value(sheet, GASBESTAND_ROW_INDEX, GASBESTAND_COL_NM3)
- if gasbestand_nm3 != 0:
- return sheet
- return prev_sheet
-def get_bottom1_value(sheet, row_index: int, col_index: int) -> Decimal:
- """Get a numeric value from bottom_1, or 0 if missing."""
- if sheet is None:
- return Decimal('0')
-
- cell = Cell.objects.filter(
- sheet=sheet,
- table_type='bottom_1',
- row_index=row_index,
- column_index=col_index,
- ).first()
-
- if cell is None or cell.value in (None, ''):
- return Decimal('0')
-
- try:
- return Decimal(str(cell.value))
- except Exception:
- return Decimal('0')
-
-# MUST match the column order in your monthly_sheets top-right table
-
-
-
-def get_top_right_value(sheet, client_name: str, row_index: int) -> Decimal:
- """
- Read a numeric value from the top_right table of a MonthlySheet for
- a given client (by column) and row_index.
-
- top_right cells are keyed by (sheet, table_type='top_right',
- row_index, column_index), where column_index is the position of the
- client in HALFYEAR_RIGHT_CLIENTS.
- """
- if sheet is None:
- return Decimal('0')
-
- col_index = RIGHT_CLIENT_INDEX.get(client_name)
- if col_index is None:
- return Decimal('0')
-
- cell = Cell.objects.filter(
- sheet=sheet,
- table_type='top_right',
- row_index=row_index,
- column_index=col_index,
- ).first()
-
- if cell is None or cell.value in (None, ''):
- return Decimal('0')
-
- try:
- return Decimal(str(cell.value))
- except Exception:
- return Decimal('0')
-
-TR_RUECKF_FLUESSIG_ROW = 2 # confirmed by your March value 172.840560
-TR_BESTAND_KANNEN_ROW = 5 # confirmed by your earlier query
-def get_bestand_kannen_for_month(sheet, client_name: str) -> Decimal:
- """
- 'B9' in your description: Bestand in Kannen-1 (Lit. L-He)
- For this implementation we take it from top_left row_index = 5 for that client.
- """
- return get_top_left_value(sheet, client_name, row_index=BESTAND_KANNEN_ROW_INDEX)
-
-from decimal import Decimal
-from django.db.models import Sum
-from django.db.models.functions import Coalesce
-from django.db.models import DecimalField, Value
-
-from .models import MonthlySheet, SecondTableEntry, Client, Cell
-from django.shortcuts import redirect, render
-
-# You already have HALFYEAR_CLIENTS for the left table (AG Vogel, AG Halfm, IKP)
-HALFYEAR_CLIENTS = ["AG Vogel", "AG Halfm", "IKP"]
-
-# NEW: clients for the top-right half-year table
-HALFYEAR_RIGHT_CLIENTS = [
- "Dr. Fohrer",
- "AG Buntk.",
- "AG Alff",
- "AG Gutfl.",
- "M3 Thiele",
- "M3 Buntkowsky",
- "M3 Gutfleisch",
-]
-BOTTOM1_COL_VOLUME = 0
-BOTTOM1_COL_BAR = 1
-BOTTOM1_COL_KORR = 2
-BOTTOM1_COL_NM3 = 3
-BOTTOM1_COL_LHE = 4
-BOTTOM2_ROW_ANLAGE = 0
-BOTTOM2_COL_G39 = 0 # "Gefäss 2,5" (cell id shows column_index=0)
-BOTTOM2_COL_I39 = 1 # "Gefäss 1,0" (cell id shows column_index=1)
-BOTTOM2_ROW_INPUTS = {
- "g39": (0, 0), # row_index=0, column_index=0 (your G39)
- "i39": (0, 1), # row_index=0, column_index=1 (your I39)
-}
-FACTOR_NM3_TO_LHE = Decimal("0.75")
-RIGHT_CLIENT_INDEX = {name: idx for idx, name in enumerate(HALFYEAR_RIGHT_CLIENTS)}
-def halfyear_balance_view(request):
- """
- Read-only Halbjahres-Bilanz view.
-
- LEFT table: AG Vogel / AG Halfm / IKP (exactly as in your last working version)
- RIGHT table: Dr. Fohrer / AG Buntk. / AG Alff / AG Gutfl. /
- M3 Thiele / M3 Buntkowsky / M3 Gutfleisch
- using the Excel formulas you described.
-
- Uses the global 6-month interval from the main page (clients_list).
- """
- # 1) Read half-year interval from the session
- interval_year = request.session.get('halfyear_year')
- interval_start = request.session.get('halfyear_start_month')
-
- if not interval_year or not interval_start:
- # No interval chosen yet -> redirect to main page
- return redirect('clients_list')
-
- interval_year = int(interval_year)
- interval_start = int(interval_start)
-
- # You already have this helper in your code
- window = build_halfyear_window(interval_year, interval_start)
- # window = [(y1, m1), (y2, m2), ..., (y6, m6)]
-
- # (Year, month) of the first month
- start_year, start_month = window[0]
-
- # Previous month (for "Stand ... (Vorjahr)" and "Best. in Kannen Vormonat")
- prev_total_index = (start_month - 1) - 1 # one month back, 0-based
- if prev_total_index >= 0:
- prev_year = start_year + (prev_total_index // 12)
- prev_month = (prev_total_index % 12) + 1
- else:
- prev_year = start_year - 1
- prev_month = 12
-
- # Load MonthlySheet objects for the window and for the previous month
- sheets_by_ym = {}
- for (y, m) in window:
- sheet = MonthlySheet.objects.filter(year=y, month=m).first()
- sheets_by_ym[(y, m)] = sheet
-
- prev_sheet = MonthlySheet.objects.filter(year=prev_year, month=prev_month).first()
- def pick_bottom2_from_window(window, sheets_by_ym, prev_sheet):
- # choose sheet (same logic you already use)
- chosen = None
- for (y, m) in reversed(window):
- s = sheets_by_ym.get((y, m))
- # use your existing condition for choosing month
- if s:
- chosen = s
- break
- if chosen is None:
- chosen = prev_sheet
-
- # Now read the two inputs safely
- bottom2_inputs = {}
- for key, (row_idx, col_idx) in BOTTOM2_ROW_INPUTS.items():
- bottom2_inputs[key] = get_bottom2_value(chosen, row_idx, col_idx)
-
- return chosen, bottom2_inputs
-
-
- chosen_sheet_bottom2, bottom2_inputs = pick_bottom2_from_window(window, sheets_by_ym, prev_sheet)
- bottom2_g39 = bottom2_inputs["g39"]
- bottom2_i39 = bottom2_inputs["i39"]
- # ----------------------------
- # HALF-YEAR BOTTOM TABLE 1 (Bilanz) - Read only
- # ----------------------------
- chosen_sheet_bottom1 = pick_sheet_by_gasbestand(window, sheets_by_ym, prev_sheet)
-
- # IMPORTANT: define which bottom_1 row_index corresponds to Excel rows 27..35
- # If your bottom_1 starts at Excel row 27 => row_index 0 == Excel 27
- # then row_index = excel_row - 27
- BOTTOM1_EXCEL_START_ROW = 27
-
- bottom1_excel_rows = list(range(27, 37)) # 27..36
- BOTTOM1_LABELS = [
- "Batterie 1",
- "2",
- "3",
- "4",
- "5",
- "Batterie Links",
- "2 Bündel",
- "2 Ballone",
- "Reingasspeicher",
- "Gasbestand",
- ]
-
- BOTTOM1_VOLUMES = [
- Decimal("2.4"),
- Decimal("5.1"),
- Decimal("4.0"),
- Decimal("1.0"),
- Decimal("4.0"),
- Decimal("0.6"),
- Decimal("1.2"),
- Decimal("20.0"),
- Decimal("5.0"),
- None, # Gasbestand row has no volume
- ]
- nm3_sum_27_35 = Decimal("0")
- lhe_sum_27_35 = Decimal("0")
- bottom1_rows = []
-
- for excel_row in bottom1_excel_rows:
- row_index = excel_row - BOTTOM1_EXCEL_START_ROW
-
- chosen_sheet_bottom1 = None
- for (y, m) in reversed(window):
- s = sheets_by_ym.get((y, m))
- gasbestand = get_bottom1_value(s, GASBESTAND_ROW_INDEX, GASBESTAND_COL_NM3) # J36 (Nm3)
- if gasbestand != 0:
- chosen_sheet_bottom1 = s
- break
-
- if chosen_sheet_bottom1 is None:
- chosen_sheet_bottom1 = prev_sheet
-
- # Normal rows (27..35): read from chosen sheet and accumulate sums
- if excel_row != 36:
- nm3_val = get_bottom1_value(chosen_sheet_bottom1, row_index, BOTTOM1_COL_NM3)
- lhe_val = get_bottom1_value(chosen_sheet_bottom1, row_index, BOTTOM1_COL_LHE)
-
- nm3_sum_27_35 += nm3_val
- lhe_sum_27_35 += lhe_val
-
- bottom1_rows.append({
- "label": BOTTOM1_LABELS[row_index],
- "volume": BOTTOM1_VOLUMES[row_index],
- "bar": get_bottom1_value(chosen_sheet_bottom1, row_index, BOTTOM1_COL_BAR),
- "korr": get_bottom1_value(chosen_sheet_bottom1, row_index, BOTTOM1_COL_KORR),
- "nm3": nm3_val,
- "lhe": lhe_val,
- })
-
- # Gasbestand row (36): show sums (J36 = SUM(J27:J35), K36 = SUM(K27:K35))
- else:
- bottom1_rows.append({
- "label": "Gasbestand",
- "volume": "",
- "bar": "",
- "korr": "",
- "nm3": nm3_sum_27_35,
- "lhe": lhe_sum_27_35,
- })
- start_sheet = sheets_by_ym.get((start_year, start_month))
- # ------------------------------------------------------------
- # Bottom Table 2 (Halbjahres Bilanz) – server-side recalcBottom2()
- # ------------------------------------------------------------
-
- FACTOR_BT2 = Decimal("0.75")
-
- # 1) Helper: pick last-nonzero value of bottom_2 row0 col0/col1 from the window (fallback: prev_sheet)
- def pick_last_nonzero_bottom2(row_index: int, col_index: int) -> Decimal:
- # Scan from last month in window backwards
- for (y, m) in reversed(window):
- s = sheets_by_ym.get((y, m))
- if not s:
- continue
- v = get_bottom2_value(s, row_index, col_index)
- if v is not None and v != 0:
- return v
- # fallback to month before window
- v_prev = get_bottom2_value(prev_sheet, row_index, col_index)
- return v_prev if v_prev is not None else Decimal("0")
-
- # 2) K38 comes from Overall Summary: "Summe Bestand (Lit. L-He)"
- # Find it from your already built overall summary rows list.
-
- k38 = Decimal("0")
- j38 = Decimal("0")
- # 3) Inputs G39 / I39 (picked from last non-zero month in window)
- g39 = pick_last_nonzero_bottom2(row_index=0, col_index=0) # G39
- i39 = pick_last_nonzero_bottom2(row_index=0, col_index=1) # I39
-
- k39 = (g39 or Decimal("0")) + (i39 or Decimal("0"))
- j39 = k39 * FACTOR_BT2
-
- # 4) +Kaltgas (row 40)
- # JS:
- # g40 = (2500 - g39)/100*10
- # i40 = (1000 - i39)/100*10
- g40 = None
- i40 = None
- if g39 is not None:
- g40 = (Decimal("2500") - g39) / Decimal("100") * Decimal("10")
- if i39 is not None:
- i40 = (Decimal("1000") - i39) / Decimal("100") * Decimal("10")
-
- k40 = (g40 or Decimal("0")) + (i40 or Decimal("0"))
- j40 = k40 * FACTOR_BT2
-
- # 5) Bestand flüssig He (row 43)
- k43 = (
- (k38 or Decimal("0")) +
- (k39 or Decimal("0")) +
- (k40 or Decimal("0"))
- )
- j43 = k43 * FACTOR_BT2
-
- # 6) Gesamtbestand neu (row 44) = Gasbestand(Lit) from Bottom Table 1 + k43
- gasbestand_lit = Decimal("0")
- for r in bottom1_rows:
- if (r.get("label") or "").strip().startswith("Gasbestand"):
- gasbestand_lit = r.get("lhe") or Decimal("0")
- break
-
- k44 = (gasbestand_lit or Decimal("0")) + (k43 or Decimal("0"))
- j44 = k44 * FACTOR_BT2
-
- bottom2 = {
- "j38": j38, "k38": k38,
- "g39": g39, "i39": i39, "j39": j39, "k39": k39,
- "g40": g40, "i40": i40, "j40": j40, "k40": k40,
- "j43": j43, "k43": k43,
- "j44": j44, "k44": k44,
- }
-
- # ------------------------------------------------------------------
- # 2) LEFT TABLE (your existing, working logic)
- # ------------------------------------------------------------------
- HALFYEAR_CLIENTS_LEFT = ["AG Vogel", "AG Halfm", "IKP"]
-
- # We'll collect client-wise values first for clarity.
- client_data_left = {name: {} for name in HALFYEAR_CLIENTS_LEFT}
-
- # --- Row B3: Stand der Gaszähler (Nm³)
- # = MAX(B3 from previous month, and B3 from each of the 6 months in the window)
- # row_index 0 in top_left = "Stand der Gaszähler (Nm³)"
- months_for_max = [(prev_year, prev_month)] + window
-
- for cname in HALFYEAR_CLIENTS_LEFT:
- max_val = Decimal('0')
- for (y, m) in months_for_max:
- sheet = sheets_by_ym.get((y, m))
- if sheet is None and (y, m) == (prev_year, prev_month):
- sheet = prev_sheet
- val_b3 = get_top_left_value(sheet, cname, row_index=0)
- if val_b3 > max_val:
- max_val = val_b3
- client_data_left[cname]['stand_gas'] = max_val
-
- # --- Row B4: Stand der Gaszähler (Vorjahr) (Nm³) -> previous month same row ---
- for cname in HALFYEAR_CLIENTS_LEFT:
- val_b4 = get_top_left_value(prev_sheet, cname, row_index=0)
- client_data_left[cname]['stand_gas_prev'] = val_b4
-
- # --- Row B5: Gasrückführung (Nm³) = B3 - B4 ---
- for cname in HALFYEAR_CLIENTS_LEFT:
- b3 = client_data_left[cname]['stand_gas']
- b4 = client_data_left[cname]['stand_gas_prev']
- client_data_left[cname]['gasrueckf'] = b3 - b4
-
- # --- Row B6: Rückführung flüssig (Lit. L-He) = B5 / 0.75 ---
- for cname in HALFYEAR_CLIENTS_LEFT:
- b5 = client_data_left[cname]['gasrueckf']
- client_data_left[cname]['rueckf_fluessig'] = (b5 / Decimal('0.75')) if b5 != 0 else Decimal('0')
-
- # --- Row B7: Sonderrückführungen (Lit. L-He) = sum over 6 months of that row ---
- # That row index is 4 in your top_left table.
- for cname in HALFYEAR_CLIENTS_LEFT:
- sonder_total = Decimal('0')
- for (y, m) in window:
- sheet = sheets_by_ym.get((y, m))
- if sheet:
- sonder_total += get_top_left_value(sheet, cname, row_index=4)
- client_data_left[cname]['sonder'] = sonder_total
-
- # --- Row B8: Bestand in Kannen-1 (Lit. L-He) ---
- # Excel-style logic with Gasbestand (J36) and fallback to previous month.
- for cname in HALFYEAR_CLIENTS_LEFT:
- chosen_value = None
-
- # Go from last month (window[5]) backwards to first (window[0])
- for (y, m) in reversed(window):
- sheet = sheets_by_ym.get((y, m))
- gasbestand = get_bottom1_value(sheet, GASBESTAND_ROW_INDEX, GASBESTAND_COL_NM3)
- if gasbestand != 0:
- chosen_value = get_bestand_kannen_for_month(sheet, cname)
- break
-
- # If still None -> use previous month (Übertrag_Dez__Vorjahr equivalent)
- if chosen_value is None:
- sheet_prev = prev_sheet
- chosen_value = get_bestand_kannen_for_month(sheet_prev, cname)
-
- client_data_left[cname]['bestand_kannen'] = chosen_value if chosen_value is not None else Decimal('0')
-
- # --- Row B9: Summe Bestand (Lit. L-He) = equal to previous row ---
- for cname in HALFYEAR_CLIENTS_LEFT:
- client_data_left[cname]['summe_bestand'] = client_data_left[cname]['bestand_kannen']
-
- # --- Row B10: Best. in Kannen Vormonat (Lit. L-He)
- # = Bestand in Kannen-1 from the month BEFORE the window (prev_year, prev_month)
- for cname in HALFYEAR_CLIENTS_LEFT:
- client_data_left[cname]['best_kannen_vormonat'] = get_bestand_kannen_for_month(prev_sheet, cname)
-
- # --- Row B13: Bezug (Liter L-He) ---
- for cname in HALFYEAR_CLIENTS_LEFT:
- total_bezug = Decimal('0')
- for (y, m) in window:
- qs = SecondTableEntry.objects.filter(
- client__name=cname,
- date__year=y,
- date__month=m,
- ).aggregate(
- total=Coalesce(Sum('lhe_output'), Value(0, output_field=DecimalField()))
- )
- total_bezug += Decimal(str(qs['total']))
- client_data_left[cname]['bezug'] = total_bezug
-
- # --- Row B14: Rückführ. Soll (Lit. L-He) = Bezug - Summe Bestand + Best. in Kannen Vormonat ---
- for cname in HALFYEAR_CLIENTS_LEFT:
- b13 = client_data_left[cname]['bezug']
- b11 = client_data_left[cname]['summe_bestand']
- b12 = client_data_left[cname]['best_kannen_vormonat']
- client_data_left[cname]['rueckf_soll'] = b13 - b11 + b12
-
- # --- Row B15: Verluste (Soll-Rückf.) (Lit. L-He) = B14 - B6 ---
- for cname in HALFYEAR_CLIENTS_LEFT:
- b14 = client_data_left[cname]['rueckf_soll']
- b6 = client_data_left[cname]['rueckf_fluessig']
- client_data_left[cname]['verluste'] = b14 - b6
-
- # --- Row B16: Füllungen warm (Lit. L-He) = sum over 6 months (row_index=11) ---
- for cname in HALFYEAR_CLIENTS_LEFT:
- total_warm = Decimal('0')
- for (y, m) in window:
- sheet = sheets_by_ym.get((y, m))
- total_warm += get_top_left_value(sheet, cname, row_index=11)
- client_data_left[cname]['fuellungen_warm'] = total_warm
-
- # --- Row B17: Kaltgas Rückgabe (Lit. L-He) = Bezug * 0.06 ---
- factor = Decimal('0.06')
- for cname in HALFYEAR_CLIENTS_LEFT:
- b13 = client_data_left[cname]['bezug']
- client_data_left[cname]['kaltgas_rueckgabe'] = b13 * factor
-
- # --- Row B18: Verbraucherverluste (Liter L-He) = Verluste - Kaltgas Rückgabe ---
- for cname in HALFYEAR_CLIENTS_LEFT:
- b15 = client_data_left[cname]['verluste']
- b17 = client_data_left[cname]['kaltgas_rueckgabe']
- client_data_left[cname]['verbraucherverluste'] = b15 - b17
-
- # --- Row B19: % = Verbraucherverluste / Bezug ---
- for cname in HALFYEAR_CLIENTS_LEFT:
- bezug = client_data_left[cname]['bezug']
- verb = client_data_left[cname]['verbraucherverluste']
- if bezug != 0:
- client_data_left[cname]['percent'] = verb / bezug
- else:
- client_data_left[cname]['percent'] = None
-
- # Build LEFT rows structure
- left_row_defs = [
- ('Stand der Gaszähler (Nm³)', 'stand_gas'),
- ('Stand der Gaszähler (Vorjahr) (Nm³)', 'stand_gas_prev'),
- ('Gasrückführung (Nm³)', 'gasrueckf'),
- ('Rückführung flüssig (Lit. L-He)', 'rueckf_fluessig'),
- ('Sonderrückführungen (Lit. L-He)', 'sonder'),
- ('Bestand in Kannen-1 (Lit. L-He)', 'bestand_kannen'),
- ('Summe Bestand (Lit. L-He)', 'summe_bestand'),
- ('Best. in Kannen Vormonat (Lit. L-He)', 'best_kannen_vormonat'),
- ('Bezug (Liter L-He)', 'bezug'),
- ('Rückführ. Soll (Lit. L-He)', 'rueckf_soll'),
- ('Verluste (Soll-Rückf.) (Lit. L-He)', 'verluste'),
- ('Füllungen warm (Lit. L-He)', 'fuellungen_warm'),
- ('Kaltgas Rückgabe (Lit. L-He) – Faktor', 'kaltgas_rueckgabe'),
- ('Verbraucherverluste (Liter L-He)', 'verbraucherverluste'),
- ('%', 'percent'),
- ]
-
- rows_left = []
- for label, key in left_row_defs:
- values = [client_data_left[cname][key] for cname in HALFYEAR_CLIENTS_LEFT]
- if key == 'percent':
- total_bezug = sum((client_data_left[c]['bezug'] for c in HALFYEAR_CLIENTS_LEFT), Decimal('0'))
- total_verb = sum((client_data_left[c]['verbraucherverluste'] for c in HALFYEAR_CLIENTS_LEFT), Decimal('0'))
- total = (total_verb / total_bezug) if total_bezug != 0 else None
- else:
- total = sum((v for v in values if v is not None), Decimal('0'))
- rows_left.append({
- 'label': label,
- 'values': values,
- 'total': total,
- 'is_percent': key == 'percent',
- })
-
- # ------------------------------------------------------------------
- # 3) RIGHT TABLE (top-right half-year aggregation)
- # ------------------------------------------------------------------
- RIGHT_CLIENTS = HALFYEAR_RIGHT_CLIENTS # for brevity
-
- right_data = {name: {} for name in RIGHT_CLIENTS}
-
- # --- Bezug (Liter L-He) for each right client (same as for left) ---
- for cname in RIGHT_CLIENTS:
- total_bezug = Decimal('0')
- for (y, m) in window:
- qs = SecondTableEntry.objects.filter(
- client__name=cname,
- date__year=y,
- date__month=m,
- ).aggregate(
- total=Coalesce(Sum('lhe_output'), Value(0, output_field=DecimalField()))
- )
- total_bezug += Decimal(str(qs['total']))
- right_data[cname]['bezug'] = total_bezug
- def find_bestand_from_window(reference_client: str) -> Decimal:
- """
- Implements:
- WENN(last_month!J36=0; WENN(prev_month!J36=0; ...; prev_sheet!
9); last_month!9)
- reference_client decides which column (L/N/P/Q/R) we read from monthly top_right row_index=5.
- """
- # scan backward through window
- for (y, m) in reversed(window):
- sh = sheets_by_ym.get((y, m))
- if not sh:
- continue
- gasbestand = get_bottom1_value(sh, GASBESTAND_ROW_INDEX, GASBESTAND_COL_NM3)
- if gasbestand != 0:
- return get_top_right_value(sh, reference_client, TR_BESTAND_KANNEN_ROW)
-
- # fallback to previous month (Übertrag_Dez__Vorjahr equivalent)
- return get_top_right_value(prev_sheet, reference_client, TR_BESTAND_KANNEN_ROW)
-
- # Fohrer+Buntk merged: BOTH use Fohrer column (L9)
- val_L = find_bestand_from_window("Dr. Fohrer")
- right_data["Dr. Fohrer"]["bestand_kannen"] = val_L
- right_data["AG Buntk."]["bestand_kannen"] = val_L
-
- # Alff+Gutfl merged: BOTH use Alff column (N9)
- val_N = find_bestand_from_window("AG Alff")
- right_data["AG Alff"]["bestand_kannen"] = val_N
- right_data["AG Gutfl."]["bestand_kannen"] = val_N
-
- # M3 each uses its own column (P9/Q9/R9)
- right_data["M3 Thiele"]["bestand_kannen"] = find_bestand_from_window("M3 Thiele")
- right_data["M3 Buntkowsky"]["bestand_kannen"] = find_bestand_from_window("M3 Buntkowsky")
- right_data["M3 Gutfleisch"]["bestand_kannen"] = find_bestand_from_window("M3 Gutfleisch")
- # Helper for pair shares (L13/($L13+$M13), etc.)
- def pair_share(c1, c2):
- total = right_data[c1]['bezug'] + right_data[c2]['bezug']
- if total == 0:
- return (Decimal('0'), Decimal('0'))
- return (
- right_data[c1]['bezug'] / total,
- right_data[c2]['bezug'] / total,
- )
-
- # --- "Stand der Gaszähler (Vorjahr) (Nm³)" row: share based on Bezug ---
- # Dr. Fohrer / AG Buntk.
- s_fohrer, s_buntk = pair_share("Dr. Fohrer", "AG Buntk.")
- right_data["Dr. Fohrer"]['stand_prev_share'] = s_fohrer
- right_data["AG Buntk."]['stand_prev_share'] = s_buntk
-
- # AG Alff / AG Gutfl.
- s_alff, s_gutfl = pair_share("AG Alff", "AG Gutfl.")
- right_data["AG Alff"]['stand_prev_share'] = s_alff
- right_data["AG Gutfl."]['stand_prev_share'] = s_gutfl
-
- # M3 Thiele / M3 Buntkowsky / M3 Gutfleisch → empty in Excel → None
- for cname in ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"]:
- right_data[cname]['stand_prev_share'] = None
-
- # --- Rückführung flüssig per month (raw sums) ---
- # top_right row_index=2 is "Rückführung flüssig (Lit. L-He)"
-
- # --- Sonderrückführungen (row_index=3 in top_right) ---
- for cname in RIGHT_CLIENTS:
- sonder_total = Decimal('0')
- for (y, m) in window:
- sheet = sheets_by_ym.get((y, m))
- if sheet:
- sonder_total += get_top_right_value(sheet, cname, row_index=3)
- right_data[cname]['sonder'] = sonder_total
-
- # --- Sammelrückführung (row_index=4 in top_right), grouped & merged ---
- # Group 1: Dr. Fohrer + AG Buntk.
- group1_total = Decimal('0')
- for (y, m) in window:
- sheet = sheets_by_ym.get((y, m))
- if sheet:
- group1_total += get_top_right_value(sheet, "Dr. Fohrer", row_index=4)
- right_data["Dr. Fohrer"]['sammel'] = group1_total
- right_data["AG Buntk."]['sammel'] = group1_total
-
- # Group 2: AG Alff + AG Gutfl.
- group2_total = Decimal('0')
- for (y, m) in window:
- sheet = sheets_by_ym.get((y, m))
- if sheet:
- group2_total += get_top_right_value(sheet, "AG Alff", row_index=4)
- right_data["AG Alff"]['sammel'] = group2_total
- right_data["AG Gutfl."]['sammel'] = group2_total
-
- # Group 3: M3 Thiele + M3 Buntkowsky + M3 Gutfleisch
- group3_total = Decimal('0')
- for (y, m) in window:
- sheet = sheets_by_ym.get((y, m))
- if sheet:
- group3_total += get_top_right_value(sheet, "M3 Thiele", row_index=4)
- right_data["M3 Thiele"]['sammel'] = group3_total
- right_data["M3 Buntkowsky"]['sammel'] = group3_total
- right_data["M3 Gutfleisch"]['sammel'] = group3_total
- def safe_div(a: Decimal, b: Decimal) -> Decimal:
- return (a / b) if b != 0 else Decimal("0")
-
- # --- Rückführung flüssig (Lit. L-He) for Halbjahres-Bilanz top-right ---
- # Uses your exact formulas.
-
- # 1) Fohrer / Buntk split by BEZUG share times group SAMMEL (L8)
- L13 = right_data["Dr. Fohrer"]["bezug"]
- M13 = right_data["AG Buntk."]["bezug"]
- L8 = right_data["Dr. Fohrer"]["sammel"] # merged group total
-
- den = (L13 + M13)
- right_data["Dr. Fohrer"]["rueckf_fluessig"] = (safe_div(L13, den) * L8) if den != 0 else Decimal("0")
- right_data["AG Buntk."]["rueckf_fluessig"] = (safe_div(M13, den) * L8) if den != 0 else Decimal("0")
-
- # 2) Alff / Gutfl split by BEZUG share times group SAMMEL (N8)
- N13 = right_data["AG Alff"]["bezug"]
- O13 = right_data["AG Gutfl."]["bezug"]
- N8 = right_data["AG Alff"]["sammel"] # merged group total
-
- den = (N13 + O13)
- right_data["AG Alff"]["rueckf_fluessig"] = (safe_div(N13, den) * N8) if den != 0 else Decimal("0")
- right_data["AG Gutfl."]["rueckf_fluessig"] = (safe_div(O13, den) * N8) if den != 0 else Decimal("0")
-
- # 3) M3 Thiele = sum of monthly Rückführung flüssig (monthly top_right row_index=2) over window
- P6_sum = Decimal("0")
- for (y, m) in window:
- sh = sheets_by_ym.get((y, m))
- P6_sum += get_top_right_value(sh, "M3 Thiele", TR_RUECKF_FLUESSIG_ROW)
- right_data["M3 Thiele"]["rueckf_fluessig"] = P6_sum
-
- # 4) M3 Buntkowsky / M3 Gutfleisch split by BEZUG share times M3-group SAMMEL (P8)
- P13 = right_data["M3 Thiele"]["bezug"]
- Q13 = right_data["M3 Buntkowsky"]["bezug"]
- R13 = right_data["M3 Gutfleisch"]["bezug"]
- P8 = right_data["M3 Thiele"]["sammel"] # merged group total
-
- den = (P13 + Q13 + R13)
- right_data["M3 Buntkowsky"]["rueckf_fluessig"] = (safe_div(Q13, den) * P8) if den != 0 else Decimal("0")
- right_data["M3 Gutfleisch"]["rueckf_fluessig"] = (safe_div(R13, den) * P8) if den != 0 else Decimal("0")
- # --- Bestand in Kannen-1 (Lit. L-He) for right table (grouped) ---
- # Use Gasbestand (J36) and fallback logic, but now reading top_right B9 for each group.
- TOP_RIGHT_ROW_BESTAND_KANNEN = 6 # <-- most likely correct in your setup
-
- def pick_bestand_top_right(base_client: str) -> Decimal:
- # Go from last month in window backwards: if Gasbestand != 0, use that month's Bestand in Kannen
- for (y, m) in reversed(window):
- sh = sheets_by_ym.get((y, m))
- if not sh:
- continue
-
- gasbestand = get_bottom1_value(sh, GASBESTAND_ROW_INDEX, GASBESTAND_COL_NM3)
- if gasbestand != 0:
- return get_top_right_value(sh, base_client, TOP_RIGHT_ROW_BESTAND_KANNEN)
-
- # Fallback to previous month (Übertrag_Dez__Vorjahr equivalent)
- return get_top_right_value(prev_sheet, base_client, TOP_RIGHT_ROW_BESTAND_KANNEN)
-
- # Group 1 merged (Fohrer + Buntk.)
- g1_best = pick_bestand_top_right("Dr. Fohrer")
- right_data["Dr. Fohrer"]["bestand_kannen"] = g1_best
- right_data["AG Buntk."]["bestand_kannen"] = g1_best
-
- # Group 2 merged (Alff + Gutfl.)
- g2_best = pick_bestand_top_right("AG Alff")
- right_data["AG Alff"]["bestand_kannen"] = g2_best
- right_data["AG Gutfl."]["bestand_kannen"] = g2_best
-
- # Group 3 merged (M3 Thiele + M3 Buntkowsky + M3 Gutfleisch)
- g3_best = pick_bestand_top_right("M3 Thiele")
- right_data["M3 Thiele"]["bestand_kannen"] = g3_best
- right_data["M3 Buntkowsky"]["bestand_kannen"] = g3_best
- right_data["M3 Gutfleisch"]["bestand_kannen"] = g3_best
-
- # Summe Bestand = same as previous row
- for cname in RIGHT_CLIENTS:
- right_data[cname]['summe_bestand'] = right_data[cname]['bestand_kannen']
-
- # Best. in Kannen Vormonat (Lit. L-He) from previous month top_right row_index=7
- g1_prev = get_top_right_value(prev_sheet, "Dr. Fohrer", TOP_RIGHT_ROW_BESTAND_KANNEN)
- right_data["Dr. Fohrer"]['best_kannen_vormonat'] = g1_prev
- right_data["AG Buntk."]['best_kannen_vormonat'] = g1_prev
-
- g2_prev = get_top_right_value(prev_sheet, "AG Alff", TOP_RIGHT_ROW_BESTAND_KANNEN)
- right_data["AG Alff"]['best_kannen_vormonat'] = g2_prev
- right_data["AG Gutfl."]['best_kannen_vormonat'] = g2_prev
-
-
- # Group 1 merged (Fohrer + Buntk.)
- g1_prev = get_top_right_value(prev_sheet, "Dr. Fohrer", TOP_RIGHT_ROW_BESTAND_KANNEN)
- right_data["Dr. Fohrer"]["best_kannen_vormonat"] = g1_prev
- right_data["AG Buntk."]["best_kannen_vormonat"] = g1_prev
-
- # Group 2 merged (Alff + Gutfl.)
- g2_prev = get_top_right_value(prev_sheet, "AG Alff", TOP_RIGHT_ROW_BESTAND_KANNEN)
- right_data["AG Alff"]["best_kannen_vormonat"] = g2_prev
- right_data["AG Gutfl."]["best_kannen_vormonat"] = g2_prev
-
- # Group 3 UNMERGED (each one reads its own cell)
- right_data["M3 Thiele"]["best_kannen_vormonat"] = get_top_right_value(prev_sheet, "M3 Thiele", TOP_RIGHT_ROW_BESTAND_KANNEN)
- right_data["M3 Buntkowsky"]["best_kannen_vormonat"] = get_top_right_value(prev_sheet, "M3 Buntkowsky", TOP_RIGHT_ROW_BESTAND_KANNEN)
- right_data["M3 Gutfleisch"]["best_kannen_vormonat"] = get_top_right_value(prev_sheet, "M3 Gutfleisch", TOP_RIGHT_ROW_BESTAND_KANNEN)
-
- # --- Rückführ. Soll (Lit. L-He) according to your formulas ---
-
- # Group 1: Dr. Fohrer / AG Buntk.
- total_bestand_1 = right_data["Dr. Fohrer"]['summe_bestand']
- best_vormonat_1 = right_data["Dr. Fohrer"]['best_kannen_vormonat']
- diff1 = total_bestand_1 - best_vormonat_1
- share_fohrer = right_data["Dr. Fohrer"]['stand_prev_share'] or Decimal('0')
-
- right_data["Dr. Fohrer"]['rueckf_soll'] = (
- right_data["Dr. Fohrer"]['bezug'] - diff1 * share_fohrer
- )
- right_data["AG Buntk."]['rueckf_soll'] = (
- right_data["AG Buntk."]['bezug'] - total_bestand_1 + best_vormonat_1
- )
-
- # Group 2: AG Alff / AG Gutfl.
- total_bestand_2 = right_data["AG Alff"]['summe_bestand']
- best_vormonat_2 = right_data["AG Alff"]['best_kannen_vormonat']
- diff2 = total_bestand_2 - best_vormonat_2
- share_alff = right_data["AG Alff"]['stand_prev_share'] or Decimal('0')
- share_gutfl = right_data["AG Gutfl."]['stand_prev_share'] or Decimal('0')
-
- right_data["AG Alff"]['rueckf_soll'] = (
- right_data["AG Alff"]['bezug'] - diff2 * share_alff
- )
- right_data["AG Gutfl."]['rueckf_soll'] = (
- right_data["AG Gutfl."]['bezug'] - diff2 * share_gutfl
- )
-
- # Group 3: M3 Thiele / M3 Buntkowsky / M3 Gutfleisch
- for cname in ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"]:
- b13 = right_data[cname]['bezug']
- b12 = right_data[cname]['best_kannen_vormonat']
- b11 = right_data[cname]['summe_bestand']
- # Excel: P13+P12-P11 etc.
- right_data[cname]['rueckf_soll'] = b13 + b12 - b11
-
- # --- Verluste (Soll-Rückf.) (Lit. L-He) = B14 - B6 - B7 ---
- for cname in RIGHT_CLIENTS:
- b14 = right_data[cname]['rueckf_soll']
- b6 = right_data[cname]['rueckf_fluessig']
- b7 = right_data[cname]['sonder']
- right_data[cname]['verluste'] = b14 - b6 - b7
-
- # --- Füllungen warm (Lit. L-He) = sum of monthly 'Füllungen warm' (row_index=11 top_right) ---
- for cname in RIGHT_CLIENTS:
- total_warm = Decimal('0')
- for (y, m) in window:
- sheet = sheets_by_ym.get((y, m))
- if sheet:
- total_warm += get_top_right_value(sheet, cname, row_index=11)
- right_data[cname]['fuellungen_warm'] = total_warm
-
- # --- Kaltgas Rückgabe (Lit. L-He) – Faktor = Bezug * 0.06 ---
- for cname in RIGHT_CLIENTS:
- b13 = right_data[cname]['bezug']
- right_data[cname]['kaltgas_rueckgabe'] = b13 * factor
-
- # --- Verbraucherverluste (Liter L-He) = Verluste - Kaltgas Rückgabe ---
- for cname in RIGHT_CLIENTS:
- b15 = right_data[cname]['verluste']
- b17 = right_data[cname]['kaltgas_rueckgabe']
- right_data[cname]['verbraucherverluste'] = b15 - b17
-
- # --- % = Verbraucherverluste / Bezug ---
- for cname in RIGHT_CLIENTS:
- bezug = right_data[cname]['bezug']
- verb = right_data[cname]['verbraucherverluste']
- if bezug != 0:
- right_data[cname]['percent'] = verb / bezug
- else:
- right_data[cname]['percent'] = None
-
- # Build RIGHT rows structure
- right_row_defs = [
- ('Stand der Gaszähler (Vorjahr) (Nm³)', 'stand_prev_share'),
- # We skip the pure-text "Gasrückführung (Nm³)" line here,
- # because it’s only text (Aufteilung nach Verbrauch / Gaszähler)
- # and easier to render directly in the template if needed.
- ('Rückführung flüssig (Lit. L-He)', 'rueckf_fluessig'),
- ('Sonderrückführungen (Lit. L-He)', 'sonder'),
- ('Sammelrückführung (Lit. L-He)', 'sammel'),
- ('Bestand in Kannen-1 (Lit. L-He)', 'bestand_kannen'),
- ('Summe Bestand (Lit. L-He)', 'summe_bestand'),
- ('Best. in Kannen Vormonat (Lit. L-He)', 'best_kannen_vormonat'),
- ('Bezug (Liter L-He)', 'bezug'),
- ('Rückführ. Soll (Lit. L-He)', 'rueckf_soll'),
- ('Verluste (Soll-Rückf.) (Lit. L-He)', 'verluste'),
- ('Füllungen warm (Lit. L-He)', 'fuellungen_warm'),
- ('Kaltgas Rückgabe (Lit. L-He) – Faktor', 'kaltgas_rueckgabe'),
- ('Verbraucherverluste (Liter L-He)', 'verbraucherverluste'),
- ('%', 'percent'),
- ]
-
- rows_right = []
- for label, key in right_row_defs:
- values = [right_data[cname].get(key) for cname in RIGHT_CLIENTS]
-
- if key == 'percent':
- total_bezug = sum((right_data[c]['bezug'] for c in RIGHT_CLIENTS), Decimal('0'))
- total_verb = sum((right_data[c]['verbraucherverluste'] for c in RIGHT_CLIENTS), Decimal('0'))
- total = (total_verb / total_bezug) if total_bezug != 0 else None
- else:
- total = sum((v for v in values if isinstance(v, Decimal)), Decimal('0'))
-
- rows_right.append({
- 'label': label,
- 'values': values,
- 'total': total,
- 'is_percent': key == 'percent',
- })
- SUM_TABLE_ROWS = [
- ("Rückführung flüssig (Lit. L-He)", "rueckf_fluessig"),
- ("Sonderrückführungen (Lit. L-He)", "sonder"),
- ("Sammelrückführungen (Lit. L-He)", "sammel"),
- ("Bestand in Kannen-1 (Lit. L-He)", "bestand_kannen"),
- ("Summe Bestand (Lit. L-He)", "summe_bestand"),
- ("Best. in Kannen Vormonat (Lit. L-He)", "best_kannen_vormonat"),
- ("Bezug (Liter L-He)", "bezug"),
- ("Rückführ. Soll (Lit. L-He)", "rueckf_soll"),
- ("Verluste (Soll-Rückf.) (Lit. L-He)", "verluste"),
- ("Füllungen warm (Lit. L-He)", "fuellungen_warm"),
- ("Kaltgas Rückgabe (Lit. L-He) – Faktor", "kaltgas_rueckgabe"),
- ("Faktor 0.06", "factor_row"),
- ("Verbraucherverluste (Liter L-He)", "verbraucherverluste"),
- ("%", "percent"),
- ]
-
- RIGHT_GROUPS = {
- "chemie": ["Dr. Fohrer", "AG Buntk."],
- "mawi": ["AG Alff", "AG Gutfl."],
- "m3": ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"],
- }
-
- RIGHT_ALL = ["Dr. Fohrer", "AG Buntk.", "AG Alff", "AG Gutfl.", "M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"]
- LEFT_ALL = HALFYEAR_CLIENTS_LEFT
-
- def safe_pct(verb, bez):
- return (verb / bez) if bez != 0 else None
- rows_sum = []
- def d(x):
- return x if isinstance(x, Decimal) else Decimal("0")
- for label, key in SUM_TABLE_ROWS:
-
- if key == "factor_row":
- lichtwiese = chemie = mawi = m3 = total = Decimal("0.06")
-
- elif key == "percent":
- # Right totals
- rw_bez = sum(d(right_data[c].get("bezug")) for c in RIGHT_ALL)
- rw_verb = sum(d(right_data[c].get("verbraucherverluste")) for c in RIGHT_ALL)
- lichtwiese = safe_pct(rw_verb, rw_bez)
-
- # Chemie
- ch_bez = sum(d(right_data[c].get("bezug")) for c in RIGHT_GROUPS["chemie"])
- ch_verb = sum(d(right_data[c].get("verbraucherverluste")) for c in RIGHT_GROUPS["chemie"])
- chemie = safe_pct(ch_verb, ch_bez)
-
- # MaWi
- mw_bez = sum(d(right_data[c].get("bezug")) for c in RIGHT_GROUPS["mawi"])
- mw_verb = sum(d(right_data[c].get("verbraucherverluste")) for c in RIGHT_GROUPS["mawi"])
- mawi = safe_pct(mw_verb, mw_bez)
-
- # M3
- m3_bez = sum(d(right_data[c].get("bezug")) for c in RIGHT_GROUPS["m3"])
- m3_verb = sum(d(right_data[c].get("verbraucherverluste")) for c in RIGHT_GROUPS["m3"])
- m3 = safe_pct(m3_verb, m3_bez)
-
- # Σ column = (left verb + right verb) / (left bez + right bez)
- left_bez = sum(d(client_data_left[c].get("bezug")) for c in LEFT_ALL)
- left_verb = sum(d(client_data_left[c].get("verbraucherverluste")) for c in LEFT_ALL)
- total = safe_pct(left_verb + rw_verb, left_bez + rw_bez)
-
- else:
- # normal rows = sums
- lichtwiese = sum(d(right_data[c].get(key)) for c in RIGHT_ALL)
- chemie = sum(d(right_data[c].get(key)) for c in RIGHT_GROUPS["chemie"])
- mawi = sum(d(right_data[c].get(key)) for c in RIGHT_GROUPS["mawi"])
- m3 = sum(d(right_data[c].get(key)) for c in RIGHT_GROUPS["m3"])
-
- left_total = sum(d(client_data_left[c].get(key)) for c in LEFT_ALL)
- total = left_total + lichtwiese
-
- rows_sum.append({
- "row_index": row_index,
- "label": label,
- "total": total,
- "lichtwiese": lichtwiese,
- "chemie": chemie,
- "mawi": mawi,
- "m3": m3,
- "is_percent": (key == "percent"),
- })
- def find_sum_row(rows, label_startswith: str):
- for r in rows:
- if str(r.get("label", "")).strip().startswith(label_startswith):
- return r
- return None
-
- summe_bestand_row = find_sum_row(rows_sum, "Summe Bestand")
- k38 = (summe_bestand_row.get("total") if summe_bestand_row else Decimal("0")) or Decimal("0")
- j38 = k38 * Decimal("0.75")
- # --- FIX: now that k38 is known, update bottom2 + recompute dependent rows ---
- bottom2["k38"] = k38
- bottom2["j38"] = j38
-
- k39 = bottom2.get("k39") or Decimal("0")
- k40 = bottom2.get("k40") or Decimal("0")
-
- # Row 43: Bestand flüssig He = SUMME(K38:K40)
- k43 = (k38 or Decimal("0")) + k39 + k40
- j43 = k43 * Decimal("0.75")
-
- bottom2["k43"] = k43
- bottom2["j43"] = j43
-
- # Row 44: Gesamtbestand neu = Gasbestand(Lit) from bottom table 1 + k43
- gasbestand_lit = Decimal("0")
- for r in bottom1_rows:
- if (r.get("label") or "").strip().startswith("Gasbestand"):
- gasbestand_lit = r.get("lhe") or Decimal("0")
- break
-
- k44 = gasbestand_lit + k43
- j44 = k44 * Decimal("0.75")
-
- bottom2["k44"] = k44
- bottom2["j44"] = j44
- def d(x):
- return x if isinstance(x, Decimal) else Decimal("0")
-
- # ---- Bottom2: J38/K38 depend on rows_sum (overall summary), so do it HERE ----
- k38 = Decimal("0")
- for r in rows_sum:
- if r.get("label") == "Summe Bestand (Lit. L-He)":
- k38 = r.get("total") or Decimal("0")
- break
-
- j38 = k38 * FACTOR_NM3_TO_LHE # 0.75
- bottom2["k38"] = k38
- bottom2["j38"] = j38
- factor = Decimal("0.75")
-
- # window = the 6-month list you already build in this view: [(y,m), (y,m), ...]
- # bottom2 = dict with "k44" already computed in your view
- # rows_sum = overall sum group rows list (your existing halfyear logic)
-
- # 1) K46 = K44 from the month BEFORE the global month (interval start)
- start_year = interval_year
- start_month = interval_start # whatever you named your start month variable
-
- if start_month == 1:
- prev_year, prev_month = start_year - 1, 12
- else:
- prev_year, prev_month = start_year, start_month - 1
-
- prev_sheet = MonthlySheet.objects.filter(year=prev_year, month=prev_month).first()
-
- # This assumes you have MonthlySummary.gesamtbestand_neu_lhe as K44 equivalent.
- # If your field name differs, tell me your MonthlySummary model fields.
- prev_k44 = Decimal("0")
- if prev_sheet:
- prev_sum = MonthlySummary.objects.filter(sheet=prev_sheet).first()
- if prev_sum and prev_sum.gesamtbestand_neu_lhe is not None:
- prev_k44 = Decimal(str(prev_sum.gesamtbestand_neu_lhe))
-
- # helpers: read bottom_3 values for a given sheet/month
- def get_bottom3_value(sheet, row_index, col_index):
- if not sheet:
- return Decimal("0")
- c = Cell.objects.filter(
- sheet=sheet, table_type="bottom_3",
- row_index=row_index, column_index=col_index
- ).first()
- if not c or c.value in (None, "", "None"):
- return Decimal("0")
- try:
- return Decimal(str(c.value))
- except Exception:
- return Decimal("0")
-
- # 2) Sum rows across the 6-month window
- g47_sum = Decimal("0")
- i47_sum = Decimal("0")
- j47_sum = Decimal("0")
-
- g50_sum = Decimal("0")
- i50_sum = Decimal("0")
-
- for (yy, mm) in window:
- s = MonthlySheet.objects.filter(year=yy, month=mm).first()
- ## Excel row 47 maps to bottom_3 row_index=1 in DB (see monthly_sheet.html)
- g = get_bottom3_value(s, 1, 1) # G47 editable
- i = get_bottom3_value(s, 1, 2) # I47 editable
-
- g47_sum += g
- i47_sum += i
-
- # In monthly_sheet, J47 is CALCULATED as (G47 + I47), not stored as an editable cell
- j47_sum += (g + i)
-
- # row 50: G(2), I(4)
- g50_sum += get_bottom3_value(s, 50, 2)
- i50_sum += get_bottom3_value(s, 50, 4)
-
- # 3) K52 = Verbraucherverlust from overall sum group first column (global Σ)
- k52 = Decimal("0")
- for r in rows_sum:
- label = (r.get("label") or "")
- if label.startswith("Verbraucherverluste"):
- k52 = r.get("total") or Decimal("0")
- break
-
- # --- apply the SAME monthly formulas, with your overrides ---
- # Row 46
- k46 = prev_k44
- j46 = k46 * factor
-
- # Row 47
- g47 = g47_sum
- i47 = i47_sum
- j47 = j47_sum
- k47 = (j47 / factor) + g47 if (j47 != 0 or g47 != 0) else Decimal("0")
-
- # Row 48
- k48 = k46 + k47
- j48 = k48 * factor
-
- # Row 49 (akt. Monat) -> in halfyear we use current bottom2 K44
- k49 = bottom2.get("k44") or Decimal("0")
- j49 = k49 * factor
-
- # Row 50
- g50 = g50_sum
- i50 = i50_sum
- j50 = i50
- k50 = (j50 / factor) if j50 != 0 else Decimal("0")
-
- # Row 51
- k51 = k48 - k49 - k50
- j51 = k51 * factor
-
- # Row 52
- j52 = k52 * factor
-
- # Row 53
- k53 = k51 - k52
- j53 = j51 - j52
-
- bottom3 = {
- "j46": j46, "k46": k46,
- "g47": g47, "i47": i47, "j47": j47, "k47": k47,
- "j48": j48, "k48": k48,
- "j49": j49, "k49": k49,
- "g50": g50, "i50": i50, "j50": j50, "k50": k50,
- "j51": j51, "k51": k51,
- "j52": j52, "k52": k52,
- "j53": j53, "k53": k53,
- }
- # ------------------------------------------------------------------
- # 4) Context – keep old keys AND new ones
- # ------------------------------------------------------------------
- context = {
- 'interval_year': interval_year,
- 'interval_start_month': interval_start,
- 'window': window,
-
- # Left table – old names (for your first template)
- 'clients': HALFYEAR_CLIENTS_LEFT,
- 'rows': rows_left,
-
- # Left table – explicit
- 'clients_left': HALFYEAR_CLIENTS_LEFT,
- 'rows_left': rows_left,
-
- # Right table
- 'clients_right': RIGHT_CLIENTS,
- 'rows_right': rows_right,
- 'rows_sum': rows_sum,
- 'bottom1_rows': bottom1_rows,
-
- }
- context["bottom2"] = bottom2
- context["bottom3"] = bottom3
- context["context_bottom2_g39"] = bottom2_inputs["g39"]
- context["context_bottom2_i39"] = bottom2_inputs["i39"]
- return render(request, 'halfyear_balance.html', context)
-
-def get_bottom2_value(sheet, row_index: int, col_index: int) -> Decimal:
- """Get numeric value from bottom_2 or 0 if missing."""
- if sheet is None:
- return Decimal("0")
- cell = Cell.objects.filter(
- sheet=sheet,
- table_type="bottom_2",
- row_index=row_index,
- column_index=col_index,
- ).first()
- if cell is None or cell.value in (None, ""):
- return Decimal("0")
- try:
- return Decimal(str(cell.value))
- except Exception:
- return Decimal("0")
-
-def get_top_left_value(sheet, client_name: str, row_index: int) -> Decimal:
- """
- Read a numeric value from the top_left table for a given month, client and row.
- Does NOT use column_index, because top_left is keyed only by client + row_index.
- """
- if sheet is None:
- return Decimal('0')
-
- client_obj = Client.objects.filter(name=client_name).first()
- if not client_obj:
- return Decimal('0')
-
- cell = Cell.objects.filter(
- sheet=sheet,
- table_type='top_left',
- client=client_obj,
- row_index=row_index,
- ).first()
-
- if cell is None or cell.value in (None, ''):
- return Decimal('0')
-
- try:
- return Decimal(str(cell.value))
- except Exception:
- return Decimal('0')
-def get_group_clients(group_key):
- """Return queryset of clients that belong to a logical group."""
- from .models import Client # local import to avoid circulars
-
- group = CLIENT_GROUPS.get(group_key)
- if not group:
- return Client.objects.none()
- return Client.objects.filter(name__in=group['names'])
-
-def calculate_summation(sheet, table_type, row_index, sum_column_index):
- """Calculate summation for a row, with special handling for % row"""
- from decimal import Decimal
- from .models import Cell
-
- try:
- # Special case: top_left, % row (Excel B20 -> row_index 19)
- if table_type == 'top_left' and row_index == 19:
- # K13 = sum of row 13 (Excel B13 -> row_index 12) across all clients
- cells_row13 = Cell.objects.filter(
- sheet=sheet,
- table_type='top_left',
- row_index=12, # Excel B13 = row_index 12
- column_index__lt=sum_column_index # Exclude sum column itself
- )
- total_13 = Decimal('0')
- for cell in cells_row13:
- if cell.value is not None:
- total_13 += Decimal(str(cell.value))
-
- # K19 = sum of row 19 (Excel B19 -> row_index 18) across all clients
- cells_row19 = Cell.objects.filter(
- sheet=sheet,
- table_type='top_left',
- row_index=18, # Excel B19 = row_index 18
- column_index__lt=sum_column_index
- )
- total_19 = Decimal('0')
- for cell in cells_row19:
- if cell.value is not None:
- total_19 += Decimal(str(cell.value))
-
- # Calculate: IF(K13=0; 0; K19/K13)
- if total_13 == 0:
- return Decimal('0')
- return total_19 / total_13
-
- # Normal summation for other rows
- cells_in_row = Cell.objects.filter(
- sheet=sheet,
- table_type=table_type,
- row_index=row_index,
- column_index__lt=sum_column_index
- )
-
- total = Decimal('0')
- for cell in cells_in_row:
- if cell.value is not None:
- total += Decimal(str(cell.value))
-
- return total
-
- except Exception as e:
- print(f"Error calculating summation for {table_type}[{row_index}]: {e}")
- return None
-
-# Helper function for calculations
-def evaluate_formula(formula, values_dict):
- """
- Safely evaluate a formula like "10 + 9" where numbers are row indices
- values_dict: {row_index: decimal_value}
- """
- from decimal import Decimal
- import re
-
- try:
- # Create a copy of the formula to work with
- expr = formula
-
- # Find all row numbers in the formula
- row_refs = re.findall(r'\b\d+\b', expr)
-
- for row_ref in row_refs:
- row_num = int(row_ref)
- if row_num in values_dict and values_dict[row_num] is not None:
- # Replace row reference with actual value
- expr = expr.replace(row_ref, str(values_dict[row_num]))
- else:
- # Missing value - can't calculate
- return None
-
- # Evaluate the expression
- # Note: In production, use a safer evaluator like `asteval`
- result = eval(expr, {"__builtins__": {}}, {})
-
- # Convert to Decimal with proper rounding
- return Decimal(str(round(result, 6)))
-
- except Exception:
- return None
-
-# Monthly Sheet View
-class MonthlySheetView(TemplateView):
- template_name = 'monthly_sheet.html'
-
- def populate_helium_input_to_top_right(self, sheet):
- """Populate bezug data from SecondTableEntry to top-right table (row 8 = Excel row 12)"""
- from .models import SecondTableEntry, Cell, Client
- from django.db.models.functions import Coalesce
- from decimal import Decimal
-
- year = sheet.year
- month = sheet.month
-
- TOP_RIGHT_CLIENTS = [
- "Dr. Fohrer", # Column index 0 (L)
- "AG Buntk.", # Column index 1 (M)
- "AG Alff", # Column index 2 (N)
- "AG Gutfl.", # Column index 3 (O)
- "M3 Thiele", # Column index 4 (P)
- "M3 Buntkowsky", # Column index 5 (Q)
- "M3 Gutfleisch", # Column index 6 (R)
- ]
-
- # For each client in top-right table
- for client_name in TOP_RIGHT_CLIENTS:
- try:
- client = Client.objects.get(name=client_name)
- column_index = TOP_RIGHT_CLIENTS.index(client_name)
-
- # Calculate total LHe_output for this client in this month from SecondTableEntry
- total_lhe_output = SecondTableEntry.objects.filter(
- client=client,
- date__year=year,
- date__month=month
- ).aggregate(
- total=Coalesce(Sum('lhe_output'), Decimal('0'))
- )['total']
-
- # Get or create the cell for row_index 8 (Excel row 12) - Bezug
- cell, created = Cell.objects.get_or_create(
- sheet=sheet,
- table_type='top_right',
- client=client,
- row_index=8, # Bezug row (Excel row 12)
- column_index=column_index,
- defaults={'value': total_lhe_output}
- )
-
- if not created and cell.value != total_lhe_output:
- cell.value = total_lhe_output
- cell.save()
-
- except Client.DoesNotExist:
- continue
-
- # After populating bezug, trigger calculation for all dependent cells
- # Get any cell to start the calculation
- first_cell = Cell.objects.filter(
- sheet=sheet,
- table_type='top_right'
- ).first()
-
- if first_cell:
- save_view = SaveCellsView()
- save_view.calculate_top_right_dependents(sheet, first_cell)
-
- return True
- def calculate_bezug_from_entries(self, sheet, year, month):
- """Calculate B11 (Bezug) from SecondTableEntry for all clients - ONLY for non-start sheets"""
- from .models import SecondTableEntry, Cell, Client
- from django.db.models import Sum
- from django.db.models.functions import Coalesce
- from decimal import Decimal
-
- # Check if this is the start sheet
- if year == 2025 and month == 1:
- return # Don't auto-calculate for start sheet
-
- for client in Client.objects.all():
- # Calculate total LHe output for this client in this month
- lhe_output_sum = SecondTableEntry.objects.filter(
- client=client,
- date__year=year,
- date__month=month
- ).aggregate(
- total=Coalesce(Sum('lhe_output'), Decimal('0'))
- )['total']
-
- # Update B11 cell (row_index 8 = UI Row 9)
- b11_cell = Cell.objects.filter(
- sheet=sheet,
- table_type='top_left',
- client=client,
- row_index=8 # Excel B11
- ).first()
-
- if b11_cell and (b11_cell.value != lhe_output_sum or b11_cell.value is None):
- b11_cell.value = lhe_output_sum
- b11_cell.save()
-
- # Also trigger dependent calculations
- from .views import SaveCellsView
- save_view = SaveCellsView()
- save_view.calculate_top_left_dependents(sheet, b11_cell)
- # In MonthlySheetView.get_context_data() method, update the TOP_RIGHT_CLIENTS and row count:
-
-
-
- return True
- def get_context_data(self, **kwargs):
- from decimal import Decimal
- context = super().get_context_data(**kwargs)
- year = self.kwargs.get('year', datetime.now().year)
- month = self.kwargs.get('month', datetime.now().month)
- is_start_sheet = (year == FIRST_SHEET_YEAR and month == FIRST_SHEET_MONTH)
-
- # Get or create the monthly sheet
- sheet, created = MonthlySheet.objects.get_or_create(
- year=year, month=month
- )
-
- # All clients (used for bottom tables etc.)
- clients = Client.objects.all().order_by('name')
-
- # Pre-fill cells if creating new sheet
- if created:
- self.initialize_sheet_cells(sheet, clients)
-
- # Apply previous month links (for B4 and B12)
- self.apply_previous_month_links(sheet, year, month)
- self.calculate_bezug_from_entries(sheet, year, month)
- self.populate_helium_input_to_top_right(sheet)
- self.apply_previous_month_links_top_right(sheet, year, month)
-
- # Define client groups
- TOP_LEFT_CLIENTS = [
- "AG Vogel",
- "AG Halfm",
- "IKP",
- ]
-
- TOP_RIGHT_CLIENTS = [
- "Dr. Fohrer",
- "AG Buntk.",
- "AG Alff",
- "AG Gutfl.",
- "M3 Thiele",
- "M3 Buntkowsky",
- "M3 Gutfleisch",
- ]
- current_summary = MonthlySummary.objects.filter(sheet=sheet).first()
-
- # Get previous month summary (for Bottom Table 3: K46 = prev K44)
- prev_month_info = self.get_prev_month(year, month)
- prev_summary = None
- if not is_start_sheet:
- prev_sheet = MonthlySheet.objects.filter(
- year=prev_month_info['year'],
- month=prev_month_info['month']
- ).first()
- if prev_sheet:
- prev_summary = MonthlySummary.objects.filter(sheet=prev_sheet).first()
-
- context.update({
- # ... your existing context ...
- 'current_summary': current_summary,
- 'prev_summary': prev_summary,
- })
- # Update row counts in build_group_rows function
- # Update row counts in build_group_rows function
- def build_group_rows(sheet, table_type, client_names):
- """Build rows for display in monthly sheet."""
- from decimal import Decimal
- from .models import Cell
- MERGED_ROWS = {2, 3, 5, 6, 7, 9, 10, 12, 14, 15}
- MERGED_PAIRS = [
- ("Dr. Fohrer", "AG Buntk."),
- ("AG Alff", "AG Gutfl."),
- ]
- rows = []
-
- # Determine row count
- row_counts = {
- "top_left": 16,
- "top_right": 16, # rows 0–15
- "bottom_1": 10,
- "bottom_2": 10,
- "bottom_3": 10,
- }
- row_count = row_counts.get(table_type, 0)
-
- # Get all cells for this sheet and table
- all_cells = (
- Cell.objects.filter(
- sheet=sheet,
- table_type=table_type,
- ).select_related("client")
- )
-
- # Group cells by row index
- cells_by_row = {}
- for cell in all_cells:
- if cell.row_index not in cells_by_row:
- cells_by_row[cell.row_index] = {}
- cells_by_row[cell.row_index][cell.client.name] = cell
-
- # We will store row sums for Bezug (row 8) and Verbraucherverluste (row 14)
- # for top_left / top_right so we can compute the overall % in row 15.
- sum_bezug = None
- sum_verbrauch = None
-
- # Build each row
- for row_idx in range(row_count):
- display_cells = []
- row_cells_dict = cells_by_row.get(row_idx, {})
-
- # Build cells in the requested client order
- for name in client_names:
- cell = row_cells_dict.get(name)
- display_cells.append(cell)
-
- # Calculate sum for this row (includes editable + calculated cells)
- sum_value = None
- total = Decimal("0")
- has_value = False
-
- merged_second_indices = set()
- if table_type == 'top_right' and row_idx in MERGED_ROWS:
- for left_name, right_name in MERGED_PAIRS:
- try:
- right_idx = client_names.index(right_name)
- merged_second_indices.add(right_idx)
- except ValueError:
- # client not in this table; just ignore
- pass
-
- for col_idx, cell in enumerate(display_cells):
- # Skip the duplicate (second) column of each merged pair
- if col_idx in merged_second_indices:
- continue
-
- if cell and cell.value is not None:
- try:
- total += Decimal(str(cell.value))
- has_value = True
- except Exception:
- pass
-
- if has_value:
- sum_value = total
-
- # Remember special rows for top tables
- if table_type in ("top_left", "top_right"):
- if row_idx == 8: # Bezug
- sum_bezug = total
- elif row_idx == 14: # Verbraucherverluste
- sum_verbrauch = total
-
- rows.append(
- {
- "cells": display_cells,
- "sum": sum_value,
- "row_index": row_idx,
- }
- )
-
- # Adjust the % row sum for top_left / top_right:
- # Sum(%) = Sum(Verbraucherverluste) / Sum(Bezug)
- if table_type in ("top_left", "top_right"):
- perc_row_idx = 15 # % row
- if 0 <= perc_row_idx < len(rows):
- if sum_bezug is not None and sum_bezug != 0 and sum_verbrauch is not None:
- rows[perc_row_idx]["sum"] = sum_verbrauch / sum_bezug
- else:
- rows[perc_row_idx]["sum"] = None
-
- return rows
-
-
- # Now call the local function
- top_left_rows = build_group_rows(sheet, 'top_left', TOP_LEFT_CLIENTS)
- top_right_rows = build_group_rows(sheet, 'top_right', TOP_RIGHT_CLIENTS)
-
- # --- Build combined summary of top-left + top-right Sum columns ---
-
- # Helper to safely get the Sum for a given row index
- def get_row_sum(rows, row_index):
- if 0 <= row_index < len(rows):
- return rows[row_index].get('sum')
- return None
-
- # Row definitions we want in the combined Σ table
- # (row_index in top tables, label shown in the small table)
- summary_row_defs = [
- (2, "Rückführung flüssig (Lit. L-He)"),
- (3, "Sonderrückführungen (Lit. L-He)"),
- (4, "Sammelrückführungen (Lit. L-He)"),
- (5, "Bestand in Kannen-1 (Lit. L-He)"),
- (6, "Summe Bestand (Lit. L-He)"),
- (7, "Best. in Kannen Vormonat (Lit. L-He)"),
- (8, "Bezug (Liter L-He)"),
- (9, "Rückführ. Soll (Lit. L-He)"),
- (10, "Verluste (Soll-Rückf.) (Lit. L-He)"),
- (11, "Füllungen warm (Lit. L-He)"),
- (12, "Kaltgas Rückgabe (Lit. L-He) – Faktor"),
- (13, "Faktor 0.06"),
- (14, "Verbraucherverluste (Liter L-He)"),
- (15, "%"),
- ]
-
- # Precompute totals for Bezug and Verbraucherverluste across both tables
- bezug_left = get_row_sum(top_left_rows, 8) or Decimal('0')
- bezug_right = get_row_sum(top_right_rows, 8) or Decimal('0')
- total_bezug = bezug_left + bezug_right
-
- verb_left = get_row_sum(top_left_rows, 14) or Decimal('0')
- verb_right = get_row_sum(top_right_rows, 14) or Decimal('0')
- total_verbrauch = verb_left + verb_right
-
- summary_rows = []
-
- for row_index, label in summary_row_defs:
- # Faktor row: always fixed 0.06
- if row_index == 13:
- summary_value = Decimal('0.06')
-
- # % row: total Verbraucherverluste / total Bezug
- elif row_index == 15:
- if total_bezug != 0:
- summary_value = total_verbrauch / total_bezug
- else:
- summary_value = None
-
- else:
- left_sum = get_row_sum(top_left_rows, row_index)
- right_sum = get_row_sum(top_right_rows, row_index)
-
- # Sammelrückführungen: only from top-right table
- if row_index == 4:
- left_sum = None
-
- total = Decimal('0')
-
- has_any = False
- if left_sum is not None:
- total += Decimal(str(left_sum))
- has_any = True
- if right_sum is not None:
- total += Decimal(str(right_sum))
- has_any = True
-
- summary_value = total if has_any else None
-
- summary_rows.append({
- 'row_index': row_index,
- 'label': label,
- 'sum': summary_value,
- })
-
- # Get cells for bottom tables
- cells_by_table = self.get_cells_by_table(sheet)
-
- context.update({
- 'sheet': sheet,
- 'clients': clients,
- 'year': year,
- 'month': month,
- 'month_name': calendar.month_name[month],
- 'prev_month': self.get_prev_month(year, month),
- 'next_month': self.get_next_month(year, month),
- 'cells_by_table': cells_by_table,
- 'top_left_headers': TOP_LEFT_CLIENTS + ['Sum'],
- 'top_right_headers': TOP_RIGHT_CLIENTS + ['Sum'],
- 'top_left_rows': top_left_rows,
- 'top_right_rows': top_right_rows,
- 'summary_rows': summary_rows, # 👈 NEW
- 'is_start_sheet': is_start_sheet,
- })
- return context
-
- def get_cells_by_table(self, sheet):
- """Organize cells by table type for easy template rendering"""
- cells = sheet.cells.select_related('client').all()
- organized = {
- 'top_left': [[] for _ in range(16)],
- 'top_right': [[] for _ in range(16)], # now 16 rows
- 'bottom_1': [[] for _ in range(10)],
- 'bottom_2': [[] for _ in range(10)],
- 'bottom_3': [[] for _ in range(10)],
- }
-
- for cell in cells:
- if cell.table_type not in organized:
- continue
-
- max_rows = len(organized[cell.table_type])
- if cell.row_index < 0 or cell.row_index >= max_rows:
- # This is an "extra" cell from an older layout (e.g. top_left rows 18–23)
- continue
-
- row_list = organized[cell.table_type][cell.row_index]
- while len(row_list) <= cell.column_index:
- row_list.append(None)
-
- row_list[cell.column_index] = cell
-
- return organized
-
-
- def initialize_sheet_cells(self, sheet, clients):
- """Create all empty cells for a new monthly sheet"""
- cells_to_create = []
- summation_config = CALCULATION_CONFIG.get('summation_column', {})
- sum_column_index = summation_config.get('sum_column_index', len(clients) - 1) # Last column
-
- # For each table type and row
- table_configs = [
- ('top_left', 16),
- ('top_right', 16),
- ('bottom_1', 10),
- ('bottom_2', 10),
- ('bottom_3', 10),
- ]
-
- for table_type, row_count in table_configs:
- for row_idx in range(row_count):
- for col_idx, client in enumerate(clients):
- is_summation = (col_idx == sum_column_index)
- cells_to_create.append(Cell(
- sheet=sheet,
- client=client,
- table_type=table_type,
- row_index=row_idx,
- column_index=col_idx,
- value=None,
- is_formula=is_summation, # Mark summation cells as formulas
- ))
-
- # Bulk create all cells at once
- Cell.objects.bulk_create(cells_to_create)
- def get_prev_month(self, year, month):
- """Get previous month year and month"""
- if month == 1:
- return {'year': year - 1, 'month': 12}
- return {'year': year, 'month': month - 1}
-
- def get_next_month(self, year, month):
- """Get next month year and month"""
- if month == 12:
- return {'year': year + 1, 'month': 1}
- return {'year': year, 'month': month + 1}
- def apply_previous_month_links(self, sheet, year, month):
- """
- For non-start sheets:
- B4 (row 2) = previous sheet B3 (row 1)
- B10 (row 8) = previous sheet B9 (row 7)
- """
- # Do nothing on the first sheet
- if year == FIRST_SHEET_YEAR and month == FIRST_SHEET_MONTH:
- return
-
- # Figure out previous month
- if month == 1:
- prev_year = year - 1
- prev_month = 12
- else:
- prev_year = year
- prev_month = month - 1
-
- from .models import MonthlySheet, Cell, Client
-
- prev_sheet = MonthlySheet.objects.filter(
- year=prev_year,
- month=prev_month
- ).first()
-
- if not prev_sheet:
- # No previous sheet created yet → nothing to copy
- return
-
- # For each client, copy values
- for client in Client.objects.all():
- # B3(prev) → B4(curr): UI row 1 → row 2 → row_index 0 → 1
- prev_b3 = Cell.objects.filter(
- sheet=prev_sheet,
- table_type='top_left',
- client=client,
- row_index=0, # UI row 1
- ).first()
-
- curr_b4 = Cell.objects.filter(
- sheet=sheet,
- table_type='top_left',
- client=client,
- row_index=1, # UI row 2
- ).first()
-
- if prev_b3 and curr_b4:
- curr_b4.value = prev_b3.value
- curr_b4.save()
-
- # B9(prev) → B10(curr): UI row 7 → row 8 → row_index 6 → 7
- prev_b9 = Cell.objects.filter(
- sheet=prev_sheet,
- table_type='top_left',
- client=client,
- row_index=6, # UI row 7 (Summe Bestand)
- ).first()
-
- curr_b10 = Cell.objects.filter(
- sheet=sheet,
- table_type='top_left',
- client=client,
- row_index=7, # UI row 8 (Best. in Kannen Vormonat)
- ).first()
-
- if prev_b9 and curr_b10:
- curr_b10.value = prev_b9.value
- curr_b10.save()
-
- def apply_previous_month_links_top_right(self, sheet, year, month):
- """
- top_right row 7: Best. in Kannen Vormonat (Lit. L-He)
- = previous sheet's Summe Bestand (row 6).
-
- For merged pairs:
- - Dr. Fohrer + AG Buntk. share the SAME value (from previous month's AG Buntk. or Dr. Fohrer)
- - AG Alff + AG Gutfl. share the SAME value (from previous month's AG Alff or AG Gutfl.)
- M3 clients just copy their own value.
- """
- from .models import MonthlySheet, Cell, Client
- from decimal import Decimal
-
- # Do nothing on first sheet
- if year == FIRST_SHEET_YEAR and month == FIRST_SHEET_MONTH:
- return
-
- # find previous month
- if month == 1:
- prev_year = year - 1
- prev_month = 12
- else:
- prev_year = year
- prev_month = month - 1
-
- prev_sheet = MonthlySheet.objects.filter(
- year=prev_year,
- month=prev_month
- ).first()
-
- if not prev_sheet:
- return # nothing to copy from
-
- TOP_RIGHT_CLIENTS = [
- "Dr. Fohrer",
- "AG Buntk.",
- "AG Alff",
- "AG Gutfl.",
- "M3 Thiele",
- "M3 Buntkowsky",
- "M3 Gutfleisch",
- ]
-
- # Helper function to get a cell value
- def get_cell_value(sheet_obj, client_name, row_index):
- client_obj = Client.objects.filter(name=client_name).first()
- if not client_obj:
- return None
-
- try:
- col_idx = TOP_RIGHT_CLIENTS.index(client_name)
- except ValueError:
- return None
-
- cell = Cell.objects.filter(
- sheet=sheet_obj,
- table_type='top_right',
- client=client_obj,
- row_index=row_index,
- column_index=col_idx,
- ).first()
-
- return cell.value if cell else None
-
- # Helper function to set a cell value
- def set_cell_value(sheet_obj, client_name, row_index, value):
- client_obj = Client.objects.filter(name=client_name).first()
- if not client_obj:
- return False
-
- try:
- col_idx = TOP_RIGHT_CLIENTS.index(client_name)
- except ValueError:
- return False
-
- cell, created = Cell.objects.get_or_create(
- sheet=sheet_obj,
- table_type='top_right',
- client=client_obj,
- row_index=row_index,
- column_index=col_idx,
- defaults={'value': value}
- )
-
- if not created and cell.value != value:
- cell.value = value
- cell.save()
- elif created:
- cell.save()
-
- return True
-
- # ----- Pair 1: Dr. Fohrer + AG Buntk. -----
- # Get previous month's Summe Bestand (row 6) for either client in the pair
- pair1_prev_val = None
-
- # Try AG Buntk. first
- prev_buntk_val = get_cell_value(prev_sheet, "AG Buntk.", 6)
- if prev_buntk_val is not None:
- pair1_prev_val = prev_buntk_val
- else:
- # Try Dr. Fohrer if AG Buntk. is empty
- prev_fohrer_val = get_cell_value(prev_sheet, "Dr. Fohrer", 6)
- if prev_fohrer_val is not None:
- pair1_prev_val = prev_fohrer_val
-
- # Apply the value to both clients in the pair
- if pair1_prev_val is not None:
- set_cell_value(sheet, "Dr. Fohrer", 7, pair1_prev_val)
- set_cell_value(sheet, "AG Buntk.", 7, pair1_prev_val)
-
- # ----- Pair 2: AG Alff + AG Gutfl. -----
- pair2_prev_val = None
-
- # Try AG Alff first
- prev_alff_val = get_cell_value(prev_sheet, "AG Alff", 6)
- if prev_alff_val is not None:
- pair2_prev_val = prev_alff_val
- else:
- # Try AG Gutfl. if AG Alff is empty
- prev_gutfl_val = get_cell_value(prev_sheet, "AG Gutfl.", 6)
- if prev_gutfl_val is not None:
- pair2_prev_val = prev_gutfl_val
-
- # Apply the value to both clients in the pair
- if pair2_prev_val is not None:
- set_cell_value(sheet, "AG Alff", 7, pair2_prev_val)
- set_cell_value(sheet, "AG Gutfl.", 7, pair2_prev_val)
-
- # ----- M3 clients: copy their own Summe Bestand -----
- for name in ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"]:
- prev_val = get_cell_value(prev_sheet, name, 6)
- if prev_val is not None:
- set_cell_value(sheet, name, 7, prev_val)
-# Add this helper function to views.py
-def get_factor_value(table_type, row_index):
- """Get factor value (like 0.06 for top_left row 17)"""
- factors = {
- ('top_left', 17): Decimal('0.06'), # A18 in Excel (UI row 17, 0-based index 16)
- }
- return factors.get((table_type, row_index), Decimal('0'))
-# Save Cells View
-# views.py - Updated SaveCellsView
-# views.py - Update SaveCellsView class
-def debug_cell_values(self, sheet, client_id):
- """Debug method to check cell values"""
- from .models import Cell
- cells = Cell.objects.filter(
- sheet=sheet,
- table_type='top_left',
- client_id=client_id
- ).order_by('row_index')
-
- debug_info = {}
- for cell in cells:
- debug_info[f"row_{cell.row_index}"] = {
- 'value': str(cell.value) if cell.value else 'None',
- 'ui_row': cell.row_index + 1,
- 'excel_ref': f"B{cell.row_index + 3}"
- }
-
- return debug_info
-class DebugCalculationView(View):
- """Debug view to test calculations directly"""
- def get(self, request):
- sheet_id = request.GET.get('sheet_id', 1)
- client_name = request.GET.get('client', 'AG Vogel')
-
- try:
- sheet = MonthlySheet.objects.get(id=sheet_id)
- client = Client.objects.get(name=client_name)
-
- # Get SaveCellsView instance
- save_view = SaveCellsView()
-
- # Create a dummy cell to trigger calculations
- dummy_cell = Cell.objects.filter(
- sheet=sheet,
- table_type='top_left',
- client=client,
- row_index=0 # B3
- ).first()
-
- if not dummy_cell:
- return JsonResponse({'error': 'No cells found for this client'})
-
- # Trigger calculation
- updated = save_view.calculate_top_left_dependents(sheet, dummy_cell)
-
- # Get updated cell values
- cells = Cell.objects.filter(
- sheet=sheet,
- table_type='top_left',
- client=client
- ).order_by('row_index')
-
- cell_data = []
- for cell in cells:
- cell_data.append({
- 'row_index': cell.row_index,
- 'ui_row': cell.row_index + 1,
- 'excel_ref': f"B{cell.row_index + 3}",
- 'value': str(cell.value) if cell.value else 'None',
- 'description': self.get_row_description(cell.row_index)
- })
-
- return JsonResponse({
- 'sheet': f"{sheet.year}-{sheet.month:02d}",
- 'client': client.name,
- 'cells': cell_data,
- 'updated_count': len(updated),
- 'calculation': 'B5 = IF(B3>0; B3-B4; 0)'
- })
-
- except Exception as e:
- return JsonResponse({'error': str(e)}, status=400)
-
- def get_row_description(self, row_index):
- """Get description for row index"""
- descriptions = {
- 0: "B3: Stand der Gaszähler (Nm³)",
- 1: "B4: Stand der Gaszähler (Vormonat) (Nm³)",
- 2: "B5: Gasrückführung (Nm³)",
- 3: "B6: Rückführung flüssig (Lit. L-He)",
- 4: "B7: Sonderrückführungen (Lit. L-He)",
- 5: "B8: Bestand in Kannen-1 (Lit. L-He)",
- 6: "B9: Summe Bestand (Lit. L-He)",
- 7: "B10: Best. in Kannen Vormonat (Lit. L-He)",
- 8: "B11: Bezug (Liter L-He)",
- 9: "B12: Rückführ. Soll (Lit. L-He)",
- 10: "B13: Verluste (Soll-Rückf.) (Lit. L-He)",
- 11: "B14: Füllungen warm (Lit. L-He)",
- 12: "B15: Kaltgas Rückgabe (Lit. L-He) – Faktor",
- 13: "B16: Faktor",
- 14: "B17: Verbraucherverluste (Liter L-He)",
- 15: "B18: %"
- }
- return descriptions.get(row_index, f"Row {row_index}")
-def recalculate_stand_der_gaszahler(self, sheet):
- """Recalculate Stand der Gaszähler for all client pairs"""
- from decimal import Decimal
-
- # For Dr. Fohrer and AG Buntk. (L & M columns)
- try:
- # Get Dr. Fohrer's bezug
- dr_fohrer_client = Client.objects.get(name="Dr. Fohrer")
- dr_fohrer_cell = Cell.objects.filter(
- sheet=sheet,
- table_type='top_right',
- client=dr_fohrer_client,
- row_index=9 # Row 9 = Bezug
- ).first()
- L13 = Decimal(str(dr_fohrer_cell.value)) if dr_fohrer_cell and dr_fohrer_cell.value else Decimal('0')
-
- # Get AG Buntk.'s bezug
- ag_buntk_client = Client.objects.get(name="AG Buntk.")
- ag_buntk_cell = Cell.objects.filter(
- sheet=sheet,
- table_type='top_right',
- client=ag_buntk_client,
- row_index=9
- ).first()
- M13 = Decimal(str(ag_buntk_cell.value)) if ag_buntk_cell and ag_buntk_cell.value else Decimal('0')
-
- total = L13 + M13
- if total > 0:
- # Update Dr. Fohrer's row 0
- dr_fohrer_row0 = Cell.objects.filter(
- sheet=sheet,
- table_type='top_right',
- client=dr_fohrer_client,
- row_index=0
- ).first()
- if dr_fohrer_row0:
- dr_fohrer_row0.value = L13 / total
- dr_fohrer_row0.save()
-
- # Update AG Buntk.'s row 0
- ag_buntk_row0 = Cell.objects.filter(
- sheet=sheet,
- table_type='top_right',
- client=ag_buntk_client,
- row_index=0
- ).first()
- if ag_buntk_row0:
- ag_buntk_row0.value = M13 / total
- ag_buntk_row0.save()
- except Exception as e:
- print(f"Error recalculating Stand der Gaszähler for Dr. Fohrer/AG Buntk.: {e}")
-
- # For AG Alff and AG Gutfl. (N & O columns)
- try:
- # Get AG Alff's bezug
- ag_alff_client = Client.objects.get(name="AG Alff")
- ag_alff_cell = Cell.objects.filter(
- sheet=sheet,
- table_type='top_right',
- client=ag_alff_client,
- row_index=9
- ).first()
- N13 = Decimal(str(ag_alff_cell.value)) if ag_alff_cell and ag_alff_cell.value else Decimal('0')
-
- # Get AG Gutfl.'s bezug
- ag_gutfl_client = Client.objects.get(name="AG Gutfl.")
- ag_gutfl_cell = Cell.objects.filter(
- sheet=sheet,
- table_type='top_right',
- client=ag_gutfl_client,
- row_index=9
- ).first()
- O13 = Decimal(str(ag_gutfl_cell.value)) if ag_gutfl_cell and ag_gutfl_cell.value else Decimal('0')
-
- total = N13 + O13
- if total > 0:
- # Update AG Alff's row 0
- ag_alff_row0 = Cell.objects.filter(
- sheet=sheet,
- table_type='top_right',
- client=ag_alff_client,
- row_index=0
- ).first()
- if ag_alff_row0:
- ag_alff_row0.value = N13 / total
- ag_alff_row0.save()
-
- # Update AG Gutfl.'s row 0
- ag_gutfl_row0 = Cell.objects.filter(
- sheet=sheet,
- table_type='top_right',
- client=ag_gutfl_client,
- row_index=0
- ).first()
- if ag_gutfl_row0:
- ag_gutfl_row0.value = O13 / total
- ag_gutfl_row0.save()
- except Exception as e:
- print(f"Error recalculating Stand der Gaszähler for AG Alff/AG Gutfl.: {e}")
-# In your SaveCellsView class in views.py
-class DebugTopRightView(View):
- """Debug view to check top_right calculations"""
- def get(self, request):
- sheet_id = request.GET.get('sheet_id', 1)
- client_name = request.GET.get('client', 'Dr. Fohrer')
-
- try:
- sheet = MonthlySheet.objects.get(id=sheet_id)
- client = Client.objects.get(name=client_name)
-
- # Get all cells for this client in top_right
- cells = Cell.objects.filter(
- sheet=sheet,
- table_type='top_right',
- client=client
- ).order_by('row_index')
-
- cell_data = []
- descriptions = {
- 0: "Stand der Gaszähler (Vormonat)",
- 1: "Gasrückführung (Nm³)",
- 2: "Rückführung flüssig",
- 3: "Sonderrückführungen",
- 4: "Sammelrückführungen",
- 5: "Bestand in Kannen-1",
- 6: "Summe Bestand",
- 7: "Best. in Kannen Vormonat",
- 8: "Same as row 9 from prev sheet",
- 9: "Bezug",
- 10: "Rückführ. Soll",
- 11: "Verluste",
- 12: "Füllungen warm",
- 13: "Kaltgas Rückgabe",
- 14: "Faktor 0.06",
- 15: "Verbraucherverluste",
- 16: "%"
- }
-
- for cell in cells:
- cell_data.append({
- 'row_index': cell.row_index,
- 'ui_row': cell.row_index + 1,
- 'description': descriptions.get(cell.row_index, f"Row {cell.row_index}"),
- 'value': str(cell.value) if cell.value else 'Empty',
- 'cell_id': cell.id
- })
-
- # Test calculation
- row3_cell = cells.filter(row_index=3).first()
- row5_cell = cells.filter(row_index=5).first()
- row6_cell = cells.filter(row_index=6).first()
-
- calculation_info = {
- 'row3_value': str(row3_cell.value) if row3_cell and row3_cell.value else '0',
- 'row5_value': str(row5_cell.value) if row5_cell and row5_cell.value else '0',
- 'row6_value': str(row6_cell.value) if row6_cell and row6_cell.value else '0',
- 'expected_sum': '0'
- }
-
- if row3_cell and row5_cell and row6_cell:
- row3_val = Decimal(str(row3_cell.value)) if row3_cell.value else Decimal('0')
- row5_val = Decimal(str(row5_cell.value)) if row5_cell.value else Decimal('0')
- expected = row3_val + row5_val
- calculation_info['expected_sum'] = str(expected)
- calculation_info['is_correct'] = row6_cell.value == expected
-
- return JsonResponse({
- 'sheet': f"{sheet.year}-{sheet.month}",
- 'client': client.name,
- 'cells': cell_data,
- 'calculation': calculation_info
- })
-
- except Exception as e:
- return JsonResponse({'error': str(e)}, status=400)
-ABRECHNUNG_COL_CLIENTS = {
- # first 3 columns
- "pkm_vogel": ["AG Vogel"],
- "iap_halfmann": ["AG Halfm"], # or whatever exact name you use in Client admin
- "ikp": ["IKP"],
-
- # your “5 columns” group (excluding Chemie/MaWi/Physik summaries)
- "orgchem_thiele": ["M3 Thiele"], # if OrgChem Thiele corresponds to M3 Thiele in your DB
- "phychem_m3_buntkow": ["AG Buntk.", "M3 Buntkowsky"],
- "orgchem_fohrer": ["Dr. Fohrer"],
- "mawi_m3_gutfl": ["AG Gutfl.", "M3 Gutfleisch"],
- "mawi_alff": ["AG Alff"],
-
- # summary columns at the end
- "chemie": ["Dr. Fohrer", "AG Buntk.", "M3 Buntkowsky", "M3 Thiele"],
- "mawi": ["AG Alff", "AG Gutfl.", "M3 Gutfleisch"],
- "physik": ["AG Vogel", "AG Halfm", "IKP"],
-
- # “Gesamt-summe” is computed as sum of all columns (not client-driven)
- "gesamt_summe": [],
-}
-
-class AbrechnungView(LoginRequiredMixin, TemplateView):
- template_name = "abrechnung.html"
-
- # Columns EXACTLY from your screenshot
- COLUMNS = [
- ("pkm_vogel", "PKM Vogel"),
- ("iap_halfmann", "IAP Halfmann"),
- ("ikp", "IKP"),
- ("orgchem_thiele", "OrgChem Thiele"),
- ("phychem_m3_buntkow", "PhyChem + M3 Buntkow"),
- ("orgchem_fohrer", "OrgChem Fohrer"),
- ("mawi_m3_gutfl", "MaWi + M3 Gutfl"),
- ("mawi_alff", "MaWi Alff"),
- ("chemie", "Chemie"),
- ("mawi", "MaWi"),
- ("physik", "Physik"),
- ("gesamt_summe", "Gesamt-summe"),
- ]
-
- # Rows EXACTLY from your screenshot (Excel row 11 is skipped)
- ROWS = [
- ("bezogen_menge", "Bezogen. Menge", "LHe"),
- ("kaltg_warmfuell", "Kaltg. Warmfüll.", "Anzahl"),
- ("anzahl_15", "Anzahl", "LHe"),
- ("he_verbrauch", "He-Verbrauch (LHe)", "LHe"),
- ("umlage_instandhaltung_1", "1-Umlage Instandhaltung", "EUR"),
- ("lhe_verluste", "LHe – Verluste", "LHe"),
- ("umlage_heliumkosten_3", "3 Umlage Heliumkosten", "EUR"),
- ("ghe_bezug", "GHe - Bezug", "m³"),
- ("kosten_he_gas_bezug_4", "4-Kosten He-Gas-Bezug", "EUR"),
- ("rel_anteil", "rel. Anteil", "Anteil"),
- ("summe_anteile_1_4", "Summe Anteile 1-4", "EUR"),
- ("sonstiges", "Sonstiges", "EUR"),
- ("betrag", "Betrag", "EUR"),
- ("umlage_personal_5", "5-Umlage Personal", "EUR"),
- ("gutschriften", "Gutschriften", "EUR"),
- ("rechnungsbetrag", "Rechnungsbetrag", "EUR"),
- ("eff_lhe_preis", "eff. L-He-Preis", "EUR/L"),
-]
-
-
- # Editable rows = your yellow rows
- EDITABLE_ROW_KEYS = {"ghe_bezug", "betrag", "gutschriften"}
-
- def get_context_data(self, **kwargs):
- context = super().get_context_data(**kwargs)
-
- interval_year = self.request.session.get("halfyear_year")
- interval_start = self.request.session.get("halfyear_start_month")
- if not interval_year or not interval_start:
- context["needs_interval"] = True
- return context
-
- interval_year = int(interval_year)
- interval_start = int(interval_start)
-
- qs = AbrechnungCell.objects.filter(
- interval_year=interval_year,
- interval_start_month=interval_start,
- )
- value_map = {(c.row_key, c.col_key): c.value for c in qs}
- def d(x):
- """safe Decimal"""
- if x in (None, ""):
- return Decimal("0")
- try:
- return Decimal(str(x).replace(",", "."))
- except Exception:
- return Decimal("0")
-
- def safe_div(a: Decimal, b: Decimal):
- return (a / b) if b not in (None, 0, Decimal("0")) else Decimal("0")
-
- # ---- interval + window ----
- window = build_halfyear_window(interval_year, interval_start)
-
- # ---- BetriebskostenSummary values ----
- bs = BetriebskostenSummary.objects.first()
- instandhaltung = d(bs.instandhaltung) if bs else Decimal("0")
- heliumkosten = d(bs.heliumkosten) if bs else Decimal("0")
- bezugskosten_gashe = d(bs.bezugskosten_gashe) if bs else Decimal("0")
- umlage_personal_total = d(bs.umlage_personal) if bs else Decimal("0")
-
- # ---- helper: sum lhe_output in the 6-month window for a list of client names ----
- def sum_output_for_clients_exact(client_names):
- total = Decimal("0")
- if not client_names:
- return total
-
- for (y, m) in window:
- total += SecondTableEntry.objects.filter(
- client__name__in=client_names,
- date__year=y,
- date__month=m,
- ).aggregate(
- total=Coalesce(Sum("lhe_output"), Value(0, output_field=DecimalField()))
- )["total"] or Decimal("0")
-
- return total
- # (NOTE: the date filter above is “wide”; if you prefer exact (y,m) matching, use the loop version below)
- def sum_output_for_clients_exact(client_names):
- total = Decimal("0")
- if not client_names:
- return total
- for (y, m) in window:
- total += SecondTableEntry.objects.filter(
- client__name__in=client_names,
- date__year=y,
- date__month=m,
- ).aggregate(
- total=Coalesce(Sum("lhe_output"), Value(0, output_field=DecimalField()))
- )["total"] or Decimal("0")
- return total
-
- # Use exact to avoid edge cases
- sum_output_for_clients = sum_output_for_clients_exact
-
- # ---- helper: warm fillings (from Halbjahresbilanz row “Füllungen warm”) ----
- # In your halfyear_balance logic, “Füllungen warm” is summed from MonthlySheet cells row_index=11
- # We replicate that part by reading stored top_left/top_right cells for each month.
- def sum_fuellungen_warm(client_name):
- total = Decimal("0")
- for (y, m) in window:
- sheet = MonthlySheet.objects.filter(year=y, month=m).first()
- if not sheet:
- continue
- # warm row_index = 11 in both top_left and top_right tables in your existing code
- # Try top_left first, then top_right
- c = Cell.objects.filter(sheet=sheet, client__name=client_name, table_type="top_left", row_index=11).first()
- if not c:
- c = Cell.objects.filter(sheet=sheet, client__name=client_name, table_type="top_right", row_index=11).first()
- total += d(c.value if c else None)
- return total
-
- # ---- helper: verbraucherverluste (from Halbjahresbilanz) ----
- # In halfyear_balance view this is a *computed* metric, not directly stored.
- # So we must either:
- # A) replicate the whole halfyear_balance math (big), OR
- # B) read it from stored cells *if you store it*.
- #
- # Right now your MonthlySheet stores the table rows, but “Verbraucherverluste” is row_index=15
- # in your debug mapping. If those cells are saved in DB (they usually are), we can sum row_index=15.
- def sum_verbraucherverluste(client_name):
- total = Decimal("0")
- for (y, m) in window:
- sheet = MonthlySheet.objects.filter(year=y, month=m).first()
- if not sheet:
- continue
- # row_index=15 is “Verbraucherverluste” in your debug map
- c = Cell.objects.filter(sheet=sheet, client__name=client_name, table_type="top_left", row_index=15).first()
- if not c:
- c = Cell.objects.filter(sheet=sheet, client__name=client_name, table_type="top_right", row_index=15).first()
- total += d(c.value if c else None)
- return total
-
- # ----------------------------------------------------------------------
- # Build computed row values per column
- # ----------------------------------------------------------------------
- computed = {} # (row_key, col_key) -> Decimal OR special string
-
- # Gather base rows (bezug + warm fills) per column
- bezogen = {}
- warmfills = {}
-
- for col_key, _label in self.COLUMNS:
- client_names = ABRECHNUNG_COL_CLIENTS.get(col_key, [])
- bezogen[col_key] = sum_output_for_clients(client_names)
- warmfills[col_key] = sum_fuellungen_warm(client_names[0]) if len(client_names) == 1 else sum(sum_fuellungen_warm(n) for n in client_names)
-
- # “Gesamt-summe” = sum of all “normal” columns (everything except itself)
- def sum_all_cols(values_dict):
- return sum(v for k, v in values_dict.items() if k != "gesamt_summe")
-
- bezogen["gesamt_summe"] = sum_all_cols(bezogen)
- warmfills["gesamt_summe"] = sum_all_cols(warmfills)
-
- # ---- Row: Bezogen. Menge ----
- for col_key in bezogen:
- computed[("bezogen_menge", col_key)] = bezogen[col_key]
-
- # ---- Row: Kaltg. Warmfüll. (á 15l L-He) ----
- for col_key in warmfills:
- computed[("kaltg_warmfuell", col_key)] = warmfills[col_key]
-
- # ---- Row: Anzahl (15) = warmfills * 15 ----
- for col_key in warmfills:
- computed[("anzahl_15", col_key)] = warmfills[col_key] * Decimal("15")
-
- # ---- Row: He-Verbrauch (LHe) = Bezogen + Anzahl_15 ----
- he_verbrauch = {}
- for col_key in bezogen:
- hv = bezogen[col_key] + (warmfills[col_key] * Decimal("15"))
- he_verbrauch[col_key] = hv
- computed[("he_verbrauch", col_key)] = hv
-
- # ---- Row: 1-Umlage Instandhaltung ----
- # Your rule (as you stated):
- # - first 3 columns: instandhaltung * (He-Verbrauch of this column / He-Verbrauch of “Physik”) / 3
- # - next 3 columns: instandhaltung / 9
- # - next 2 columns: instandhaltung / 6
- # - next 2 columns: instandhaltung / 3 (if you later add more)
- # - gesamt_summe: sum of first three columns (same row)
- #
- # We apply this to the current UI order.
- first3 = ["pkm_vogel", "iap_halfmann", "ikp"]
- next3 = ["orgchem_thiele", "phychem_m3_buntkow", "orgchem_fohrer"]
- next2 = ["mawi_m3_gutfl", "mawi_alff"]
-
- physik_hv = he_verbrauch.get("physik", Decimal("0"))
-
- row1_vals = {}
- for col_key in he_verbrauch:
- if col_key in first3:
- row1_vals[col_key] = instandhaltung * safe_div(he_verbrauch[col_key], physik_hv) / Decimal("3")
- elif col_key in next3:
- row1_vals[col_key] = instandhaltung / Decimal("9")
- elif col_key in next2:
- row1_vals[col_key] = instandhaltung / Decimal("6")
- elif col_key == "gesamt_summe":
- row1_vals[col_key] = sum(row1_vals.get(k, Decimal("0")) for k in first3)
- else:
- row1_vals[col_key] = Decimal("0")
-
- for col_key, v in row1_vals.items():
- computed[("umlage_instandhaltung_1", col_key)] = v
-
- # ---- Row: LHe – Verluste = Verbraucherverluste from Halbjahresbilanz (sum if combined) ----
- lhe_verluste = {}
- for col_key, _label in self.COLUMNS:
- if col_key in ("chemie", "mawi", "physik"):
- # Special rules you stated:
- if col_key == "chemie":
- # Dr. Fohrer + AG Buntk. (NOT M3 here)
- v = sum_verbraucherverluste("Dr. Fohrer") + sum_verbraucherverluste("AG Buntk.")
- elif col_key == "mawi":
- v = sum_verbraucherverluste("AG Alff") + sum_verbraucherverluste("AG Gutfl.")
- else: # physik
- v = sum_verbraucherverluste("M3 Thiele") + sum_verbraucherverluste("M3 Gutfleisch") + sum_verbraucherverluste("M3 Buntkowsky")
- else:
- client_names = ABRECHNUNG_COL_CLIENTS.get(col_key, [])
- v = sum(sum_verbraucherverluste(n) for n in client_names)
-
- lhe_verluste[col_key] = v
- computed[("lhe_verluste", col_key)] = v
-
- lhe_verluste["gesamt_summe"] = sum_all_cols(lhe_verluste)
- computed[("lhe_verluste", "gesamt_summe")] = lhe_verluste["gesamt_summe"]
-
- # ---- Row: 3 Umlage Heliumkosten = heliumkosten * (LHe-Verluste col / LHe-Verluste gesamt_summe) ----
- for col_key in lhe_verluste:
- computed[("umlage_heliumkosten_3", col_key)] = heliumkosten * safe_div(lhe_verluste[col_key], lhe_verluste["gesamt_summe"])
-
- # ---- Row: 4-Kosten He-Gas-Bezug = Umlage Heliumkosten * Bezugskosten-GasHe ----
- for col_key in lhe_verluste:
- computed[("kosten_he_gas_bezug_4", col_key)] = d(computed[("umlage_heliumkosten_3", col_key)]) * bezugskosten_gashe
-
- # ---- Row: Summe Anteile 1-4 = (1) + (3) + (4) ----
- summe_anteile = {}
- for col_key in lhe_verluste:
- v = (
- d(computed.get(("umlage_instandhaltung_1", col_key))) +
- d(computed.get(("umlage_heliumkosten_3", col_key))) +
- d(computed.get(("kosten_he_gas_bezug_4", col_key)))
- )
- summe_anteile[col_key] = v
- computed[("summe_anteile_1_4", col_key)] = v
-
- # ---- Row: rel. Anteil = summe_anteile col / summe_anteile gesamt ----
- for col_key in summe_anteile:
- computed[("rel_anteil", col_key)] = safe_div(summe_anteile[col_key], summe_anteile.get("gesamt_summe", Decimal("0")))
-
- # ---- Row: 5-Umlage Personal ----
- # Your rules:
- # - first 3 clients => 0
- # - next columns up to (excluding Chemie): weighted over the 5 columns:
- # OrgChem Thiele, PhyChem+M3 Buntkow, OrgChem Fohrer, MaWi+M3 Gutfl, MaWi Alff
- # - for Chemie/MaWi/Physik: weighted over the 3 summary columns (Chemie, MaWi, Physik)
- five_cols = ["orgchem_thiele", "phychem_m3_buntkow", "orgchem_fohrer", "mawi_m3_gutfl", "mawi_alff"]
- sum5 = sum(he_verbrauch.get(k, Decimal("0")) for k in five_cols)
-
- sum3 = (
- he_verbrauch.get("chemie", Decimal("0")) +
- he_verbrauch.get("mawi", Decimal("0")) +
- he_verbrauch.get("physik", Decimal("0"))
- )
-
- for col_key in he_verbrauch:
- if col_key in first3:
- v = Decimal("0")
- elif col_key in five_cols:
- v = safe_div(he_verbrauch[col_key], sum5) * umlage_personal_total
- elif col_key in ("chemie", "mawi", "physik"):
- v = safe_div(he_verbrauch[col_key], sum3) * umlage_personal_total
- elif col_key == "gesamt_summe":
- v = sum(d(computed.get(("umlage_personal_5", k))) for k in he_verbrauch if k != "gesamt_summe")
- else:
- v = Decimal("0")
- computed[("umlage_personal_5", col_key)] = v
-
- # ---- Row: Rechnungsbetrag = Summe Anteile 1-4 − Gutschriften + Betrag + 5-Umlage Personal ----
- for col_key in he_verbrauch:
- gutsch = d(value_map.get(("gutschriften", col_key)))
- betrag = d(value_map.get(("betrag", col_key)))
- personal = d(computed.get(("umlage_personal_5", col_key)))
- sa = d(computed.get(("summe_anteile_1_4", col_key)))
- computed[("rechnungsbetrag", col_key)] = sa - gutsch + betrag + personal
-
- # ---- Row: eff. L-He-Preis = Rechnungsbetrag / He-Verbrauch ----
- for col_key in he_verbrauch:
- rb = d(computed.get(("rechnungsbetrag", col_key)))
- hv = he_verbrauch.get(col_key, Decimal("0"))
- computed[("eff_lhe_preis", col_key)] = safe_div(rb, hv)
-
- # ---- Row: Sonstiges (TEXT) based on Betrag ----
- # IF(Betrag=0,"--------------",IF(Betrag<0,"Gutschrift","Nachzahlung"))
- sonstiges_text = {}
- for col_key in he_verbrauch:
- b = d(value_map.get(("betrag", col_key)))
- if b == 0:
- sonstiges_text[col_key] = "--------------"
- elif b < 0:
- sonstiges_text[col_key] = "Gutschrift"
- else:
- sonstiges_text[col_key] = "Nachzahlung"
- # overwrite display values for non-editable computed rows
- non_editable_formula_rows = {
- "bezogen_menge",
- "kaltg_warmfuell",
- "anzahl_15",
- "he_verbrauch",
- "umlage_instandhaltung_1",
- "lhe_verluste",
- "umlage_heliumkosten_3",
- "kosten_he_gas_bezug_4",
- "rel_anteil",
- "summe_anteile_1_4",
- "umlage_personal_5",
- "rechnungsbetrag",
- "eff_lhe_preis",
- }
-
- for (rk, ck), val in computed.items():
- if rk in non_editable_formula_rows:
- value_map[(rk, ck)] = val
- rows = []
- for row_key, label, unit in self.ROWS:
- rows.append({
- "row_key": row_key,
- "label": label,
- "unit": unit, # 👈 ADD THIS
- "editable": row_key in self.EDITABLE_ROW_KEYS,
- "cells": [
- {
- "col_key": col_key,
- "value": value_map.get((row_key, col_key)),
- }
- for col_key, _ in self.COLUMNS
- ]
- })
- start_year = interval_year
- start_month = interval_start
- # end month = start + 5 months
- end_total = (start_month - 1) + 5
- end_year = start_year + (end_total // 12)
- end_month = (end_total % 12) + 1
-
- start_date_str = f"01.{start_month:02d}.{start_year}"
- last_day = calendar.monthrange(end_year, end_month)[1]
- end_date_str = f"{last_day:02d}.{end_month:02d}.{end_year}"
-
- context["interval_text"] = f"{start_date_str} bis {end_date_str}"
- context["sonstiges_text"] = sonstiges_text
- context.update({
- "page_title": "Abrechnung",
- "columns": self.COLUMNS,
- "rows": rows,
- "interval_year": interval_year,
- "interval_start": interval_start,
- })
-
- return context
-@method_decorator(csrf_exempt, name="dispatch")
-class SaveAbrechnungCellsView(LoginRequiredMixin, View):
- def post(self, request):
- interval_year = request.session.get("halfyear_year")
- interval_start = request.session.get("halfyear_start_month")
- if not interval_year or not interval_start:
- return JsonResponse({"ok": False, "error": "No halfyear interval selected."}, status=400)
-
- interval_year = int(interval_year)
- interval_start = int(interval_start)
-
- try:
- payload = json.loads(request.body.decode("utf-8"))
- except Exception:
- return JsonResponse({"ok": False, "error": "Invalid JSON."}, status=400)
-
- changes = payload.get("changes", [])
-
- for item in changes:
- row_key = item.get("row_key")
- col_key = item.get("col_key")
- raw_val = item.get("value")
-
- # allow empty -> NULL
- if raw_val in ("", None):
- val = None
- else:
- try:
- val = Decimal(str(raw_val).replace(",", "."))
- except Exception:
- continue
-
- AbrechnungCell.objects.update_or_create(
- interval_year=interval_year,
- interval_start_month=interval_start,
- row_key=row_key,
- col_key=col_key,
- defaults={"value": val},
- )
-
- return JsonResponse({"ok": True})
-def build_halfyear_window(interval_year: int, interval_start_month: int):
- window = []
- for offset in range(6):
- total_index = (interval_start_month - 1) + offset
- y = interval_year + (total_index // 12)
- m = (total_index % 12) + 1
- window.append((y, m))
- return window
-class SaveCellsView(View):
-
- def calculate_bottom_3_dependents(self, sheet):
- updated_cells = []
-
- def get_cell(row_idx, col_idx):
- return Cell.objects.filter(
- sheet=sheet,
- table_type='bottom_3',
- row_index=row_idx,
- column_index=col_idx
- ).first()
-
- def set_cell(row_idx, col_idx, value):
- cell, created = Cell.objects.get_or_create(
- sheet=sheet,
- table_type='bottom_3',
- row_index=row_idx,
- column_index=col_idx,
- defaults={'value': value}
- )
- if not created and cell.value != value:
- cell.value = value
- cell.save()
- elif created:
- cell.save()
-
- updated_cells.append({
- 'id': cell.id,
- 'value': str(cell.value) if cell.value is not None else '',
- 'is_calculated': True,
- })
-
- def dec(x):
- if x in (None, ''):
- return Decimal('0')
- try:
- return Decimal(str(x))
- except Exception:
- return Decimal('0')
-
- # ---- current summary ----
- cur_sum = MonthlySummary.objects.filter(sheet=sheet).first()
- curr_k44 = dec(cur_sum.gesamtbestand_neu_lhe) if cur_sum else Decimal('0')
- total_verb = dec(cur_sum.verbraucherverlust_lhe) if cur_sum else Decimal('0')
-
- # ---- previous month summary ----
- year, month = sheet.year, sheet.month
- if month == 1:
- prev_year, prev_month = year - 1, 12
- else:
- prev_year, prev_month = year, month - 1
-
- prev_sheet = MonthlySheet.objects.filter(year=prev_year, month=prev_month).first()
- if prev_sheet:
- prev_sum = MonthlySummary.objects.filter(sheet=prev_sheet).first()
- prev_k44 = dec(prev_sum.gesamtbestand_neu_lhe) if prev_sum else Decimal('0')
- else:
- prev_k44 = Decimal('0')
-
- # ---- read editable inputs from bottom_3: F47,G47,I47,I50 ----
- def get_val(r, c):
- cell = get_cell(r, c)
- return dec(cell.value if cell else None)
-
- f47 = get_val(1, 0)
- g47 = get_val(1, 1)
- i47 = get_val(1, 2)
- i50 = get_val(4, 2)
-
- # Now apply your formulas using prev_k44, curr_k44, total_verb, f47,g47,i47,i50
- # Row indices: 0..7 correspond to 46..53
- # col 3 = J, col 4 = K
-
- # Row 46
- k46 = prev_k44
- j46 = k46 * Decimal('0.75')
- set_cell(0, 3, j46)
- set_cell(0, 4, k46)
-
- # Row 47
- g47 = self._dec((get_cell(1, 1) or {}).value if get_cell(1, 1) else None)
- i47 = self._dec((get_cell(1, 2) or {}).value if get_cell(1, 2) else None)
-
- j47 = g47 + i47
- k47 = (j47 / Decimal('0.75')) + g47 if j47 != 0 else g47
-
- set_cell(1, 3, j47)
- set_cell(1, 4, k47)
-
- # Row 48
- k48 = k46 + k47
- j48 = k48 * Decimal('0.75')
- set_cell(2, 3, j48)
- set_cell(2, 4, k48)
-
- # Row 49
- k49 = curr_k44
- j49 = k49 * Decimal('0.75')
- set_cell(3, 3, j49)
- set_cell(3, 4, k49)
-
- # Row 50
- j50 = i50
- k50 = j50 / Decimal('0.75') if j50 != 0 else Decimal('0')
- set_cell(4, 3, j50)
- set_cell(4, 4, k50)
-
- # Row 51
- k51 = k48 - k49 - k50
- j51 = k51 * Decimal('0.75')
- set_cell(5, 3, j51)
- set_cell(5, 4, k51)
-
- # Row 52
- k52 = total_verb
- j52 = k52 * Decimal('0.75')
- set_cell(6, 3, j52)
- set_cell(6, 4, k52)
-
- # Row 53
- j53 = j51 - j52
- k53 = k51 - k52
- set_cell(7, 3, j53)
- set_cell(7, 4, k53)
-
- return updated_cells
-
-
- def _dec(self, value):
- """Convert value to Decimal or return 0."""
- if value is None or value == '':
- return Decimal('0')
- try:
- return Decimal(str(value))
- except Exception:
- return Decimal('0')
-
- def post(self, request, *args, **kwargs):
- """
- Handle AJAX saves from monthly_sheet.html
- - Single-cell save: when cell_id is present (blur on one cell)
- - Bulk save: when the 'Save All Cells' button is used (no cell_id)
- """
- try:
- sheet_id = request.POST.get('sheet_id')
- if not sheet_id:
- return JsonResponse({
- 'status': 'error',
- 'message': 'Missing sheet_id'
- })
-
- sheet = MonthlySheet.objects.get(id=sheet_id)
-
- # -------- Single-cell update (blur) --------
- cell_id = request.POST.get('cell_id')
- if cell_id:
- value_raw = (request.POST.get('value') or '').strip()
-
- try:
- cell = Cell.objects.get(id=cell_id, sheet=sheet)
- except Cell.DoesNotExist:
- return JsonResponse({
- 'status': 'error',
- 'message': 'Cell not found'
- })
-
- # Convert value to Decimal or None
- if value_raw == '':
- new_value = None
- else:
- try:
- # Allow comma or dot
- value_clean = value_raw.replace(',', '.')
- new_value = Decimal(value_clean)
- except (InvalidOperation, ValueError):
- # If conversion fails, treat as empty
- new_value = None
-
- old_value = cell.value
- cell.value = new_value
- cell.save()
-
- updated_cells = [{
- 'id': cell.id,
- 'value': '' if cell.value is None else str(cell.value),
- 'is_calculated': cell.is_formula, # model field
- }]
-
- # Recalculate dependents depending on table_type
- if cell.table_type == 'top_left':
- updated_cells.extend(
- self.calculate_top_left_dependents(sheet, cell)
- )
- elif cell.table_type == 'top_right':
- updated_cells.extend(
- self.calculate_top_right_dependents(sheet, cell)
- )
- elif cell.table_type == 'bottom_1':
- updated_cells.extend(self.calculate_bottom_1_dependents(sheet, cell))
- elif cell.table_type == 'bottom_3':
- updated_cells.extend(
- self.calculate_bottom_3_dependents(sheet)
- )
- # bottom_1 / bottom_2 / bottom_3 currently have no formulas:
- # they just save the new value.
- updated_cells += self.calculate_bottom_3_dependents(sheet)
- return JsonResponse({
- 'status': 'success',
- 'updated_cells': updated_cells
- })
-
-
- # -------- Bulk save (Save All button) --------
- return self.save_bulk_cells(request, sheet)
-
- except MonthlySheet.DoesNotExist:
- return JsonResponse({
- 'status': 'error',
- 'message': 'Sheet not found'
- })
- except Exception as e:
- # Generic safety net so the frontend sees an error message
- return JsonResponse({
- 'status': 'error',
- 'message': str(e)
- })
-
- # ... your other methods above ...
-
- # Update the calculate_top_right_dependents method in SaveCellsView class
-
- def calculate_top_right_dependents(self, sheet, changed_cell):
- """
- Recalculate all dependent cells in the top-right table according to Excel formulas.
-
- Excel rows (4-20) -> 0-based indices (0-15)
- Rows:
- 0: Stand der Gaszähler (Vormonat) (Nm³) - shares for M3 clients
- 1: Gasrückführung (Nm³)
- 2: Rückführung flüssig (Lit. L-He)
- 3: Sonderrückführungen (Lit. L-He) - editable
- 4: Sammelrückführungen (Lit. L-He) - from helium_input groups
- 5: Bestand in Kannen-1 (Lit. L-He) - editable, merged in pairs
- 6: Summe Bestand (Lit. L-He) = row 5
- 7: Best. in Kannen Vormonat (Lit. L-He) - from previous month
- 8: Bezug (Liter L-He) - from SecondTableEntry
- 9: Rückführ. Soll (Lit. L-He) - calculated
- 10: Verluste (Soll-Rückf.) (Lit. L-He) - calculated
- 11: Füllungen warm (Lit. L-He) - from SecondTableEntry warm outputs
- 12: Kaltgas Rückgabe (Lit. L-He) – Faktor - calculated
- 13: Faktor 0.06 - fixed
- 14: Verbraucherverluste (Liter L-He) - calculated
- 15: % - calculated
- """
- from decimal import Decimal
- from django.db.models import Sum, Count
- from django.db.models.functions import Coalesce
- from .models import Client, Cell, ExcelEntry, SecondTableEntry
-
- TOP_RIGHT_CLIENTS = [
- "Dr. Fohrer", # L
- "AG Buntk.", # M (merged with L)
- "AG Alff", # N
- "AG Gutfl.", # O (merged with N)
- "M3 Thiele", # P
- "M3 Buntkowsky", # Q
- "M3 Gutfleisch", # R
- ]
-
- # Define merged pairs
- MERGED_PAIRS = [
- ("Dr. Fohrer", "AG Buntk."), # L and M are merged
- ("AG Alff", "AG Gutfl."), # N and O are merged
- ]
-
- # M3 clients (not merged, calculated individually)
- M3_CLIENTS = ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"]
-
- # Groups for Sammelrückführungen (helium_input)
- HELIUM_INPUT_GROUPS = {
- "fohrer_buntk": ["Dr. Fohrer", "AG Buntk."],
- "alff_gutfl": ["AG Alff", "AG Gutfl."],
- "m3": ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"],
- }
-
- year = sheet.year
- month = sheet.month
- factor = Decimal('0.06') # Fixed factor from Excel
- updated_cells = []
-
- # Helper functions
- def get_val(client_name, row_idx):
- """Get cell value for a client and row"""
- try:
- client = Client.objects.get(name=client_name)
- col_idx = TOP_RIGHT_CLIENTS.index(client_name)
- cell = Cell.objects.filter(
- sheet=sheet,
- table_type='top_right',
- client=client,
- row_index=row_idx,
- column_index=col_idx
- ).first()
- if cell and cell.value is not None:
- return Decimal(str(cell.value))
- except (Client.DoesNotExist, ValueError, KeyError):
- pass
- return Decimal('0')
-
- def set_val(client_name, row_idx, value, is_calculated=True):
- """Set cell value for a client and row"""
- try:
- client = Client.objects.get(name=client_name)
- col_idx = TOP_RIGHT_CLIENTS.index(client_name)
-
- cell, created = Cell.objects.get_or_create(
- sheet=sheet,
- table_type='top_right',
- client=client,
- row_index=row_idx,
- column_index=col_idx,
- defaults={'value': value}
- )
-
- # Only update if value changed
- if not created and cell.value != value:
- cell.value = value
- cell.save()
- elif created:
- cell.save()
-
- updated_cells.append({
- 'id': cell.id,
- 'value': str(cell.value) if cell.value else '',
- 'is_calculated': is_calculated
- })
-
- return True
- except (Client.DoesNotExist, ValueError):
- return False
-
- # 1. Update Summe Bestand (row 6) from Bestand in Kannen-1 (row 5)
- # For merged pairs: copy value from changed cell to its pair
- if changed_cell and changed_cell.table_type == 'top_right':
- if changed_cell.row_index == 5: # Bestand in Kannen-1
- client_name = changed_cell.client.name
- new_value = changed_cell.value
-
- # Check if this client is in a merged pair
- for pair in MERGED_PAIRS:
- if client_name in pair:
- # Update both clients in the pair
- for client_in_pair in pair:
- if client_in_pair != client_name:
- set_val(client_in_pair, 5, new_value, is_calculated=False)
- break
-
- # 2. For all clients: Set Summe Bestand (row 6) = Bestand in Kannen-1 (row 5)
- for client_name in TOP_RIGHT_CLIENTS:
- bestand_value = get_val(client_name, 5)
- set_val(client_name, 6, bestand_value, is_calculated=True)
-
- # 3. Update Sammelrückführungen (row 4) from helium_input groups
- for group_name, client_names in HELIUM_INPUT_GROUPS.items():
- # Get total helium_input for this group
- clients_in_group = Client.objects.filter(name__in=client_names)
- total_helium = ExcelEntry.objects.filter(
- client__in=clients_in_group,
- date__year=year,
- date__month=month
- ).aggregate(total=Coalesce(Sum('lhe_ges'), Decimal('0')))['total']
-
- # Set same value for all clients in group
- for client_name in client_names:
- set_val(client_name, 4, total_helium, is_calculated=True)
-
- # 4. Calculate Rückführung flüssig (row 2)
- # For merged pairs: =L8 for Dr. Fohrer/AG Buntk., =N8 for AG Alff/AG Gutfl.
- # For M3 clients: =$P$8 * P4, $P$8 * Q4, $P$8 * R4
-
- # Get Sammelrückführungen values for groups
- sammel_fohrer_buntk = get_val("Dr. Fohrer", 4) # L8
- sammel_alff_gutfl = get_val("AG Alff", 4) # N8
- sammel_m3_group = get_val("M3 Thiele", 4) # P8 (same for all M3)
-
- # For merged pairs
- set_val("Dr. Fohrer", 2, sammel_fohrer_buntk, is_calculated=True)
- set_val("AG Buntk.", 2, sammel_fohrer_buntk, is_calculated=True)
- set_val("AG Alff", 2, sammel_alff_gutfl, is_calculated=True)
- set_val("AG Gutfl.", 2, sammel_alff_gutfl, is_calculated=True)
-
- # For M3 clients: =$P$8 * column4 (Stand der Gaszähler)
- for m3_client in M3_CLIENTS:
- stand_value = get_val(m3_client, 0) # Stand der Gaszähler (row 0)
- rueck_value = sammel_m3_group * stand_value
- set_val(m3_client, 2, rueck_value, is_calculated=True)
-
- # 5. Calculate Füllungen warm (row 11) from SecondTableEntry warm outputs
- # 5. Calculate Füllungen warm (row 11) as NUMBER of warm fillings
- # (sum of 1s where each warm SecondTableEntry is one filling)
- for client_name in TOP_RIGHT_CLIENTS:
- client = Client.objects.get(name=client_name)
- warm_count = SecondTableEntry.objects.filter(
- client=client,
- date__year=year,
- date__month=month,
- is_warm=True
- ).aggregate(
- total=Coalesce(Count('id'), 0)
- )['total']
-
- # store as Decimal so later formulas (warm * 15) still work nicely
- warm_value = Decimal(warm_count)
- set_val(client_name, 11, warm_value, is_calculated=True)
- # 6. Set Faktor row (13) to 0.06
- for client_name in TOP_RIGHT_CLIENTS:
- set_val(client_name, 13, factor, is_calculated=True)
-
- # 6a. Recalculate Stand der Gaszähler (row 0) for the merged pairs
- # according to Excel:
- # L4 = L13 / (L13 + M13), M4 = M13 / (L13 + M13)
- # N4 = N13 / (N13 + O13), O4 = O13 / (N13 + O13)
-
- # Pair 1: Dr. Fohrer / AG Buntk.
- bezug_dr = get_val("Dr. Fohrer", 8) # L13
- bezug_buntk = get_val("AG Buntk.", 8) # M13
- total_pair1 = bezug_dr + bezug_buntk
-
- if total_pair1 != 0:
- set_val("Dr. Fohrer", 0, bezug_dr / total_pair1, is_calculated=True)
- set_val("AG Buntk.", 0, bezug_buntk / total_pair1, is_calculated=True)
- else:
- # if no Bezug, both shares are 0
- set_val("Dr. Fohrer", 0, Decimal('0'), is_calculated=True)
- set_val("AG Buntk.", 0, Decimal('0'), is_calculated=True)
-
- # Pair 2: AG Alff / AG Gutfl.
- bezug_alff = get_val("AG Alff", 8) # N13
- bezug_gutfl = get_val("AG Gutfl.", 8) # O13
- total_pair2 = bezug_alff + bezug_gutfl
-
- if total_pair2 != 0:
- set_val("AG Alff", 0, bezug_alff / total_pair2, is_calculated=True)
- set_val("AG Gutfl.", 0, bezug_gutfl / total_pair2, is_calculated=True)
- else:
- set_val("AG Alff", 0, Decimal('0'), is_calculated=True)
- set_val("AG Gutfl.", 0, Decimal('0'), is_calculated=True)
-
-
- # 7. Calculate all other dependent rows for merged pairs
- for pair in MERGED_PAIRS:
- client1, client2 = pair
-
- # Get values for the pair
- bezug1 = get_val(client1, 8) # Bezug client1
- bezug2 = get_val(client2, 8) # Bezug client2
- total_bezug = bezug1 + bezug2 # L13+M13 or N13+O13
-
- summe_bestand = get_val(client1, 6) # L11 or N11 (merged, same value)
- best_vormonat = get_val(client1, 7) # L12 or N12 (merged, same value)
- rueck_fl = get_val(client1, 2) # L6 or N6 (merged, same value)
- warm1 = get_val(client1, 11) # L16 or N16
- warm2 = get_val(client2, 11) # M16 or O16
- total_warm = warm1 + warm2 # L16+M16 or N16+O16
-
- # Calculate Rückführ. Soll (row 9)
- # = L13+M13 - L11 + L12 for first pair
- # = N13+O13 - N11 + N12 for second pair
- rueck_soll = total_bezug - summe_bestand + best_vormonat
- set_val(client1, 9, rueck_soll, is_calculated=True)
- set_val(client2, 9, rueck_soll, is_calculated=True)
-
- # Calculate Verluste (row 10) = Rückführ. Soll - Rückführung flüssig
- verluste = rueck_soll - rueck_fl
- set_val(client1, 10, verluste, is_calculated=True)
- set_val(client2, 10, verluste, is_calculated=True)
-
- # Calculate Kaltgas Rückgabe (row 12)
- # = (L13+M13)*$A18 + (L16+M16)*15
- kaltgas = (total_bezug * factor) + (total_warm * Decimal('15'))
- set_val(client1, 12, kaltgas, is_calculated=True)
- set_val(client2, 12, kaltgas, is_calculated=True)
-
- # Calculate Verbraucherverluste (row 14) = Verluste - Kaltgas
- verbrauch = verluste - kaltgas
- set_val(client1, 14, verbrauch, is_calculated=True)
- set_val(client2, 14, verbrauch, is_calculated=True)
-
- # Calculate % (row 15) = Verbraucherverluste / (L13+M13)
- if total_bezug != 0:
- prozent = verbrauch / total_bezug
- else:
- prozent = Decimal('0')
- set_val(client1, 15, prozent, is_calculated=True)
- set_val(client2, 15, prozent, is_calculated=True)
-
- # 8. Calculate all dependent rows for M3 clients (individual calculations)
- for m3_client in M3_CLIENTS:
- # Get individual values
- bezug = get_val(m3_client, 8) # Bezug for this M3 client
- summe_bestand = get_val(m3_client, 6) # Summe Bestand
- best_vormonat = get_val(m3_client, 7) # Best. in Kannen Vormonat
- rueck_fl = get_val(m3_client, 2) # Rückführung flüssig
- warm = get_val(m3_client, 11) # Füllungen warm
-
- # Calculate Rückführ. Soll (row 9) = Bezug - Summe Bestand + Best. Vormonat
- rueck_soll = bezug - summe_bestand + best_vormonat
- set_val(m3_client, 9, rueck_soll, is_calculated=True)
-
- # Calculate Verluste (row 10) = Rückführ. Soll - Rückführung flüssig
- verluste = rueck_soll - rueck_fl
- set_val(m3_client, 10, verluste, is_calculated=True)
-
- # Calculate Kaltgas Rückgabe (row 12) = Bezug * factor + warm * 15
- kaltgas = (bezug * factor) + (warm * Decimal('15'))
- set_val(m3_client, 12, kaltgas, is_calculated=True)
-
- # Calculate Verbraucherverluste (row 14) = Verluste - Kaltgas
- verbrauch = verluste - kaltgas
- set_val(m3_client, 14, verbrauch, is_calculated=True)
-
- # Calculate % (row 15) = Verbraucherverluste / Bezug
- if bezug != 0:
- prozent = verbrauch / bezug
- else:
- prozent = Decimal('0')
- set_val(m3_client, 15, prozent, is_calculated=True)
-
- return updated_cells
-
-
-
- def calculate_bottom_1_dependents(self, sheet, changed_cell):
- """
- Recalculate Bottom Table 1 (table_type='bottom_1').
-
- Layout (row_index 0–9, col_index 0–4):
-
- Rows 0–8:
- 0: Batterie 1
- 1: 2
- 2: 3
- 3: 4
- 4: 5
- 5: 6
- 6: 2 Bündel
- 7: 2 Ballone
- 8: Reingasspeicher
-
- Row 9:
- 9: Gasbestand (totals row)
-
- Columns:
- 0: Volumen (fixed values, not editable)
- 1: bar (editable for rows 0–8)
- 2: korrigiert = bar / (bar/2000 + 1)
- 3: Nm³ = Volumen * korrigiert
- 4: Lit. LHe = Nm³ / 0.75
-
- Row 9:
- - Volumen (col 0): empty
- - bar (col 1): empty
- - korrigiert (col 2): empty
- - Nm³ (col 3): SUM Nm³ rows 0–8
- - Lit. LHe (col 4): SUM Lit. LHe rows 0–8
- """
- from decimal import Decimal, InvalidOperation
- from .models import Cell
-
- updated_cells = []
-
- DATA_ROWS = list(range(0, 9)) # 0–8
- TOTAL_ROW = 9
-
- COL_VOL = 0
- COL_BAR = 1
- COL_KORR = 2
- COL_NM3 = 3
- COL_LHE = 4
-
- # Fixed Volumen values for rows 0–8
- VOLUMES = [
- Decimal("2.4"), # row 0
- Decimal("5.1"), # row 1
- Decimal("4.0"), # row 2
- Decimal("1.0"), # row 3
- Decimal("4.0"), # row 4
- Decimal("0.4"), # row 5
- Decimal("1.2"), # row 6
- Decimal("20.0"), # row 7
- Decimal("5.0"), # row 8
- ]
-
- def get_cell(row_idx, col_idx):
- return Cell.objects.filter(
- sheet=sheet,
- table_type="bottom_1",
- row_index=row_idx,
- column_index=col_idx,
- ).first()
-
- def set_calc(row_idx, col_idx, value):
- """
- Set a calculated cell and add it to updated_cells.
- If value is None, we clear the cell.
- """
- cell = get_cell(row_idx, col_idx)
- if not cell:
- cell = Cell(
- sheet=sheet,
- table_type="bottom_1",
- row_index=row_idx,
- column_index=col_idx,
- value=value,
- )
- else:
- cell.value = value
- cell.save()
-
- updated_cells.append(
- {
- "id": cell.id,
- "value": "" if cell.value is None else str(cell.value),
- "is_calculated": True,
- }
- )
-
- # ---------- Rows 0–8: per-gasspeicher calculations ----------
- for row_idx in DATA_ROWS:
- bar_cell = get_cell(row_idx, COL_BAR)
-
- # Volumen: fixed constant or, if present, value from DB
- vol = VOLUMES[row_idx]
- vol_cell = get_cell(row_idx, COL_VOL)
- if vol_cell and vol_cell.value is not None:
- try:
- vol = Decimal(str(vol_cell.value))
- except (InvalidOperation, ValueError):
- # fall back to fixed constant
- vol = VOLUMES[row_idx]
-
- bar = None
- try:
- if bar_cell and bar_cell.value is not None:
- bar = Decimal(str(bar_cell.value))
- except (InvalidOperation, ValueError):
- bar = None
-
- # korrigiert = bar / (bar/2000 + 1)
- if bar is not None and bar != 0:
- korr = bar / (bar / Decimal("2000") + Decimal("1"))
- else:
- korr = None
-
- # Nm³ = Volumen * korrigiert
- if korr is not None:
- nm3 = vol * korr
- else:
- nm3 = None
-
- # Lit. LHe = Nm³ / 0.75
- if nm3 is not None:
- lit_lhe = nm3 / Decimal("0.75")
- else:
- lit_lhe = None
-
- # Write calculated cells back (NOT Volumen or bar)
- set_calc(row_idx, COL_KORR, korr)
- set_calc(row_idx, COL_NM3, nm3)
- set_calc(row_idx, COL_LHE, lit_lhe)
-
- # ---------- Row 9: totals (Gasbestand) ----------
- total_nm3 = Decimal("0")
- total_lhe = Decimal("0")
- has_nm3 = False
- has_lhe = False
-
- for row_idx in DATA_ROWS:
- nm3_cell = get_cell(row_idx, COL_NM3)
- if nm3_cell and nm3_cell.value is not None:
- try:
- total_nm3 += Decimal(str(nm3_cell.value))
- has_nm3 = True
- except (InvalidOperation, ValueError):
- pass
-
- lhe_cell = get_cell(row_idx, COL_LHE)
- if lhe_cell and lhe_cell.value is not None:
- try:
- total_lhe += Decimal(str(lhe_cell.value))
- has_lhe = True
- except (InvalidOperation, ValueError):
- pass
-
- # Volumen (0), bar (1), korrigiert (2) on total row stay empty
- set_calc(TOTAL_ROW, COL_KORR, None) # explicitly clear korrigiert
- set_calc(TOTAL_ROW, COL_NM3, total_nm3 if has_nm3 else None)
- set_calc(TOTAL_ROW, COL_LHE, total_lhe if has_lhe else None)
-
- return updated_cells
-
-
-
-
-
- def calculate_top_left_dependents(self, sheet, changed_cell):
- """Calculate dependent cells in top_left table"""
- from decimal import Decimal
- from django.db.models import Sum
- from django.db.models.functions import Coalesce
-
- client_id = changed_cell.client_id
- updated_cells = []
-
- # Get all cells for this client in top_left table
- client_cells = Cell.objects.filter(
- sheet=sheet,
- table_type='top_left',
- client_id=client_id
- )
-
- # Create a dict for easy access
- cell_dict = {}
- for cell in client_cells:
- cell_dict[cell.row_index] = cell
-
- # Get values with safe defaults
- def get_cell_value(row_idx):
- cell = cell_dict.get(row_idx)
- if cell and cell.value is not None:
- try:
- return Decimal(str(cell.value))
- except:
- return Decimal('0')
- return Decimal('0')
-
- # 1. B5 = IF(B3>0; B3-B4; 0) - Gasrückführung
- b3 = get_cell_value(0) # row_index 0 = Excel B3 (UI row 1)
- b4 = get_cell_value(1) # row_index 1 = Excel B4 (UI row 2)
-
- # Calculate B5
- if b3 > 0:
- b5_value = b3 - b4
- if b5_value < 0:
- b5_value = Decimal('0')
- else:
- b5_value = Decimal('0')
-
- # Update B5 - FORCE update even if value appears the same
- b5_cell = cell_dict.get(2) # row_index 2 = Excel B5 (UI row 3)
- if b5_cell:
- # Always update B5 when B3 or B4 changes
- b5_cell.value = b5_value
- b5_cell.save()
- updated_cells.append({
- 'id': b5_cell.id,
- 'value': str(b5_cell.value),
- 'is_calculated': True
- })
-
- # 2. B6 = B5 / 0.75 - Rückführung flüssig
- b6_value = b5_value / Decimal('0.75')
- b6_cell = cell_dict.get(3) # row_index 3 = Excel B6 (UI row 4)
- if b6_cell:
- b6_cell.value = b6_value
- b6_cell.save()
- updated_cells.append({
- 'id': b6_cell.id,
- 'value': str(b6_cell.value),
- 'is_calculated': True
- })
-
- # 3. B9 = B7 + B8 - Summe Bestand
- b7 = get_cell_value(4) # row_index 4 = Excel B7 (UI row 5)
- b8 = get_cell_value(5) # row_index 5 = Excel B8 (UI row 6)
- b9_value = b7 + b8
- b9_cell = cell_dict.get(6) # row_index 6 = Excel B9 (UI row 7)
- if b9_cell:
- b9_cell.value = b9_value
- b9_cell.save()
- updated_cells.append({
- 'id': b9_cell.id,
- 'value': str(b9_cell.value),
- 'is_calculated': True
- })
-
- # 4. B11 = Sum of LHe Output from SecondTableEntry for this client/month - Bezug
- from .models import SecondTableEntry
- client = changed_cell.client
-
- # Calculate total LHe output for this client in this month
- # 4. B11 = Bezug (Liter L-He)
- # For start sheet: manual entry, for other sheets: auto-calculated from SecondTableEntry
- from .models import SecondTableEntry
- client = changed_cell.client
-
- b11_cell = cell_dict.get(8) # row_index 8 = Excel B11 (UI row 9)
-
- # Check if this is the start sheet (2025-01)
- is_start_sheet = (sheet.year == 2025 and sheet.month == 1)
-
- # Get the B11 value for calculations
- b11_value = Decimal('0')
-
- if is_start_sheet:
- # For start sheet, keep whatever value is already there (manual entry)
- if b11_cell and b11_cell.value is not None:
- b11_value = Decimal(str(b11_cell.value))
- else:
- # For non-start sheets: auto-calculate from SecondTableEntry
- lhe_output_sum = SecondTableEntry.objects.filter(
- client=client,
- date__year=sheet.year,
- date__month=sheet.month
- ).aggregate(
- total=Coalesce(Sum('lhe_output'), Decimal('0'))
- )['total']
-
- b11_value = lhe_output_sum
-
- if b11_cell and (b11_cell.value != b11_value or b11_cell.value is None):
- b11_cell.value = b11_value
- b11_cell.save()
- updated_cells.append({
- 'id': b11_cell.id,
- 'value': str(b11_cell.value),
- 'is_calculated': True # Calculated from SecondTableEntry
- })
-
- # 5. B12 = B11 + B10 - B9 - Rückführ. Soll
- b10 = get_cell_value(7) # row_index 7 = Excel B10 (UI row 8)
- b12_value = b11_value + b10 - b9_value # Use b11_value instead of lhe_output_sum
- b12_cell = cell_dict.get(9) # row_index 9 = Excel B12 (UI row 10)
- if b12_cell:
- b12_cell.value = b12_value
- b12_cell.save()
- updated_cells.append({
- 'id': b12_cell.id,
- 'value': str(b12_cell.value),
- 'is_calculated': True
- })
-
- # 6. B13 = B12 - B6 - Verluste (Soll-Rückf.)
- b13_value = b12_value - b6_value
- b13_cell = cell_dict.get(10) # row_index 10 = Excel B13 (UI row 11)
- if b13_cell:
- b13_cell.value = b13_value
- b13_cell.save()
- updated_cells.append({
- 'id': b13_cell.id,
- 'value': str(b13_cell.value),
- 'is_calculated': True
- })
-
- # 7. B14 = Count of warm outputs
- warm_count = SecondTableEntry.objects.filter(
- client=client,
- date__year=sheet.year,
- date__month=sheet.month,
- is_warm=True
- ).count()
-
- b14_cell = cell_dict.get(11) # row_index 11 = Excel B14 (UI row 12)
- if b14_cell:
- b14_cell.value = Decimal(str(warm_count))
- b14_cell.save()
- updated_cells.append({
- 'id': b14_cell.id,
- 'value': str(b14_cell.value),
- 'is_calculated': True
- })
-
- # 8. B15 = IF(B11>0; B11 * factor + B14 * 15; 0) - Kaltgas Rückgabe
- factor = get_cell_value(13) # row_index 13 = Excel B16 (Faktor) (UI row 14)
- if factor == 0:
- factor = Decimal('0.06') # default factor
-
- if b11_value > 0: # Use b11_value
- b15_value = b11_value * factor + Decimal(str(warm_count)) * Decimal('15')
- else:
- b15_value = Decimal('0')
-
- b15_cell = cell_dict.get(12) # row_index 12 = Excel B15 (UI row 13)
- if b15_cell:
- b15_cell.value = b15_value
- b15_cell.save()
- updated_cells.append({
- 'id': b15_cell.id,
- 'value': str(b15_cell.value),
- 'is_calculated': True
- })
-
- # 9. B17 = B13 - B15 - Verbraucherverluste
- b17_value = b13_value - b15_value
- b17_cell = cell_dict.get(14) # row_index 14 = Excel B17 (UI row 15)
- if b17_cell:
- b17_cell.value = b17_value
- b17_cell.save()
- updated_cells.append({
- 'id': b17_cell.id,
- 'value': str(b17_cell.value),
- 'is_calculated': True
- })
-
- # 10. B18 = IF(B11=0; 0; B17/B11) - %
- if b11_value == 0: # Use b11_value
- b18_value = Decimal('0')
- else:
- b18_value = b17_value / b11_value # Use b11_value
-
- b18_cell = cell_dict.get(15) # row_index 15 = Excel B18 (UI row 16)
- if b18_cell:
- b18_cell.value = b18_value
- b18_cell.save()
- updated_cells.append({
- 'id': b18_cell.id,
- 'value': str(b18_cell.value),
- 'is_calculated': True
- })
-
- return updated_cells
- def save_bulk_cells(self, request, sheet):
- """Original bulk save logic (for backward compatibility)"""
- # Get all cell updates
- cell_updates = {}
- for key, value in request.POST.items():
- if key.startswith('cell_'):
- cell_id = key.replace('cell_', '')
- cell_updates[cell_id] = value
-
- # Update cells and track which ones changed
- updated_cells = []
- changed_clients = set()
-
- for cell_id, new_value in cell_updates.items():
- try:
- cell = Cell.objects.get(id=cell_id, sheet=sheet)
- old_value = cell.value
-
- # Convert new value
- try:
- if new_value.strip():
- cell.value = Decimal(new_value)
- else:
- cell.value = None
- except Exception:
- cell.value = None
-
- # Only save if value changed
- if cell.value != old_value:
- cell.save()
- updated_cells.append({
- 'id': cell.id,
- 'value': str(cell.value) if cell.value else ''
- })
- # bottom_3 has no client, so this will just add None for those cells,
- # which is harmless. Top-left cells still add their real client_id.
- changed_clients.add(cell.client_id)
-
- except Cell.DoesNotExist:
- continue
-
- # Recalculate for each changed client (top-left tables)
- for client_id in changed_clients:
- if client_id is not None:
- self.recalculate_top_left_table(sheet, client_id)
-
- # --- NEW: recalc bottom_3 for the whole sheet, independent of clients ---
- bottom3_updates = self.calculate_bottom_3_dependents(sheet)
-
- # Get all updated cells for response (top-left)
- all_updated_cells = []
- for client_id in changed_clients:
- if client_id is None:
- continue # skip bottom_3 / non-client cells
- client_cells = Cell.objects.filter(
- sheet=sheet,
- client_id=client_id
- )
- for cell in client_cells:
- all_updated_cells.append({
- 'id': cell.id,
- 'value': str(cell.value) if cell.value else '',
- 'is_calculated': cell.is_formula
- })
-
- # Add bottom_3 recalculated cells (J46..K53, etc.)
- all_updated_cells.extend(bottom3_updates)
-
- return JsonResponse({
- 'status': 'success',
- 'message': f'Saved {len(updated_cells)} cells',
- 'updated_cells': all_updated_cells
- })
-
-
- def recalculate_top_left_table(self, sheet, client_id):
- """Recalculate the top-left table for a specific client"""
- from decimal import Decimal
-
- # Get all cells for this client in top_left table
- cells = Cell.objects.filter(
- sheet=sheet,
- table_type='top_left',
- client_id=client_id
- ).order_by('row_index')
-
- # Create a dictionary of cell values
- cell_dict = {}
- for cell in cells:
- cell_dict[cell.row_index] = cell
-
- # Excel logic implementation for top-left table
- # B3 (row_index 0) = Stand der Gaszähler (Nm³) - manual
- # B4 (row_index 1) = Stand der Gaszähler (Vormonat) (Nm³) - from previous sheet
-
- # Get B3 and B4
- b3_cell = cell_dict.get(0) # UI Row 3
- b4_cell = cell_dict.get(1) # UI Row 4
-
- if b3_cell and b3_cell.value and b4_cell and b4_cell.value:
- # B5 = IF(B3>0; B3-B4; 0)
- b3 = Decimal(str(b3_cell.value))
- b4 = Decimal(str(b4_cell.value))
-
- if b3 > 0:
- b5 = b3 - b4
- if b5 < 0:
- b5 = Decimal('0')
- else:
- b5 = Decimal('0')
-
- # Update B5 (row_index 2)
- b5_cell = cell_dict.get(2)
- if b5_cell and (b5_cell.value != b5 or b5_cell.value is None):
- b5_cell.value = b5
- b5_cell.save()
-
- # B6 = B5 / 0.75 (row_index 3)
- b6 = b5 / Decimal('0.75')
- b6_cell = cell_dict.get(3)
- if b6_cell and (b6_cell.value != b6 or b6_cell.value is None):
- b6_cell.value = b6
- b6_cell.save()
-
- # Get previous month's sheet for B10
- if sheet.month == 1:
- prev_year = sheet.year - 1
- prev_month = 12
- else:
- prev_year = sheet.year
- prev_month = sheet.month - 1
-
- prev_sheet = MonthlySheet.objects.filter(
- year=prev_year,
- month=prev_month
- ).first()
-
- if prev_sheet:
- # Get B9 from previous sheet (row_index 7 in previous)
- prev_b9 = Cell.objects.filter(
- sheet=prev_sheet,
- table_type='top_left',
- client_id=client_id,
- row_index=7 # UI Row 9
- ).first()
-
- if prev_b9 and prev_b9.value:
- # Update B10 in current sheet (row_index 8)
- b10_cell = cell_dict.get(8)
- if b10_cell and (b10_cell.value != prev_b9.value or b10_cell.value is None):
- b10_cell.value = prev_b9.value
- b10_cell.save()
-@method_decorator(csrf_exempt, name='dispatch')
-class SaveMonthSummaryView(View):
- """
- Saves per-month summary values such as K44 (Gesamtbestand neu).
- Called from JS after 'Save All' finishes.
- """
-
- def post(self, request, *args, **kwargs):
- try:
- data = json.loads(request.body.decode('utf-8'))
- except json.JSONDecodeError:
- return JsonResponse(
- {'success': False, 'message': 'Invalid JSON'},
- status=400
- )
-
- sheet_id = data.get('sheet_id')
- if not sheet_id:
- return JsonResponse(
- {'success': False, 'message': 'Missing sheet_id'},
- status=400
- )
-
- try:
- sheet = MonthlySheet.objects.get(id=sheet_id)
- except MonthlySheet.DoesNotExist:
- return JsonResponse(
- {'success': False, 'message': 'Sheet not found'},
- status=404
- )
-
- # More tolerant decimal conversion: accepts "123.45" and "123,45"
- def to_decimal(value):
- if value is None:
- return None
- s = str(value).strip()
- if s == '':
- return None
- s = s.replace(',', '.')
- try:
- return Decimal(s)
- except (InvalidOperation, ValueError):
- # Debug: show what failed in the dev server console
- print("SaveMonthSummaryView to_decimal failed for:", repr(value))
- return None
-
- raw_k44 = data.get('gesamtbestand_neu_lhe')
- raw_gas = data.get('gasbestand_lhe')
- raw_verb = data.get('verbraucherverlust_lhe')
-
- gesamtbestand_neu_lhe = to_decimal(raw_k44)
- gasbestand_lhe = to_decimal(raw_gas)
- verbraucherverlust_lhe = to_decimal(raw_verb)
-
- summary, created = MonthlySummary.objects.get_or_create(sheet=sheet)
-
- if gesamtbestand_neu_lhe is not None:
- summary.gesamtbestand_neu_lhe = gesamtbestand_neu_lhe
- if gasbestand_lhe is not None:
- summary.gasbestand_lhe = gasbestand_lhe
- if verbraucherverlust_lhe is not None:
- summary.verbraucherverlust_lhe = verbraucherverlust_lhe
-
- summary.save()
-
- # Small debug output so you can see in the server console what was saved
- print(
- f"Saved MonthlySummary for {sheet.year}-{sheet.month:02d}: "
- f"K44={summary.gesamtbestand_neu_lhe}, "
- f"Gasbestand={summary.gasbestand_lhe}, "
- f"Verbraucherverlust={summary.verbraucherverlust_lhe}"
- )
-
- return JsonResponse({'success': True})
-
-
-
-# Calculate View (placeholder for calculations)
-class CalculateView(View):
- def post(self, request):
- # This will be implemented when you provide calculation rules
- return JsonResponse({
- 'status': 'success',
- 'message': 'Calculation endpoint ready'
- })
-
-# Summary Sheet View
-class SummarySheetView(TemplateView):
- template_name = 'summary_sheet.html'
-
- def get_context_data(self, **kwargs):
- context = super().get_context_data(**kwargs)
- start_month = int(self.kwargs.get('start_month', 1))
- year = int(self.kwargs.get('year', datetime.now().year))
-
- # Get 6 monthly sheets
- months = [(year, m) for m in range(start_month, start_month + 6)]
- sheets = MonthlySheet.objects.filter(
- year=year,
- month__in=list(range(start_month, start_month + 6))
- ).order_by('month')
-
- # Aggregate data across months
- summary_data = self.calculate_summary(sheets)
-
- context.update({
- 'year': year,
- 'start_month': start_month,
- 'end_month': start_month + 5,
- 'sheets': sheets,
- 'clients': Client.objects.all(),
- 'summary_data': summary_data,
- })
- return context
-
- def calculate_summary(self, sheets):
- """Calculate totals across 6 months"""
- summary = {}
-
- for client in Client.objects.all():
- client_total = 0
- for sheet in sheets:
- # Get specific cells and sum
- cells = sheet.cells.filter(
- client=client,
- table_type='top_left',
- row_index=0 # Example: first row
- )
- for cell in cells:
- if cell.value:
- try:
- client_total += float(cell.value)
- except (ValueError, TypeError):
- continue
-
- summary[client.id] = client_total
-
- return summary
-
-# Existing views below (keep all your existing functions)
-def clients_list(request):
- # --- Clients for the yearly summary table ---
- clients = Client.objects.all()
-
- # --- Available years for output data (same as before) ---
- available_years_qs = SecondTableEntry.objects.dates('date', 'year', order='DESC')
- available_years = [y.year for y in available_years_qs]
-
- # === 1) Year used for the "Helium Output Yearly Summary" table ===
- # Uses ?year=... in the dropdown
- year_param = request.GET.get('year')
- if year_param:
- selected_year = int(year_param)
- else:
- selected_year = available_years[0] if available_years else datetime.now().year
-
- # === 2) GLOBAL half-year interval (shared with other pages) =======
- # Try GET params first
- interval_year_param = request.GET.get('interval_year')
- start_month_param = request.GET.get('interval_start_month')
-
- # Fallbacks from session
- session_year = request.session.get('halfyear_year')
- session_start_month = request.session.get('halfyear_start_month')
-
- # Determine final interval_year
- if interval_year_param:
- interval_year = int(interval_year_param)
- elif session_year:
- interval_year = int(session_year)
- else:
- # default: same as selected_year for summary
- interval_year = selected_year
-
- # Determine final interval_start_month
- if start_month_param:
- interval_start_month = int(start_month_param)
- elif session_start_month:
- interval_start_month = int(session_start_month)
- else:
- interval_start_month = 1 # default Jan
-
- # Store back into the session so other views can read them
- request.session['halfyear_year'] = interval_year
- request.session['halfyear_start_month'] = interval_start_month
-
- # === 3) Build a 6-month window, allowing wrap into the next year ===
- # Example: interval_year=2025, interval_start_month=12
- # window = [(2025,12), (2026,1), (2026,2), (2026,3), (2026,4), (2026,5)]
- window = []
- for offset in range(6):
- total_index = (interval_start_month - 1) + offset # 0-based index
- y = interval_year + (total_index // 12)
- m = (total_index % 12) + 1
- window.append((y, m))
-
- # === 4) Totals per client in that 6-month window ==================
- monthly_data = []
- for client in clients:
- monthly_totals = []
- for (y, m) in window:
- total = SecondTableEntry.objects.filter(
- client=client,
- date__year=y,
- date__month=m
- ).aggregate(
- total=Coalesce(
- Sum('lhe_output'),
- Value(0, output_field=DecimalField())
- )
- )['total']
- monthly_totals.append(total)
-
- monthly_data.append({
- 'client': client,
- 'monthly_totals': monthly_totals,
- 'year_total': sum(monthly_totals),
- })
-
- # === 5) Month labels for the header (only the 6-month window) =====
- month_labels = [calendar.month_abbr[m] for (y, m) in window]
-
- # === 6) FINALLY: return the response ==============================
- return render(request, 'clients_table.html', {
- 'available_years': available_years,
- 'current_year': selected_year, # used by year dropdown
- 'interval_year': interval_year, # used by "Global 6-Month Interval" form
- 'interval_start_month': interval_start_month,
- 'months': month_labels,
- 'monthly_data': monthly_data,
- })
-
- # === 5) Month labels for the header (only the 6-month window) =====
- month_labels = [calendar.month_abbr[m] for (y, m) in window]
-
-
-def set_halfyear_interval(request):
- if request.method == 'POST':
- year = int(request.POST.get('year'))
- start_month = int(request.POST.get('start_month'))
-
- request.session['halfyear_year'] = year
- request.session['halfyear_start_month'] = start_month
-
- return redirect(request.META.get('HTTP_REFERER', 'clients_list'))
-
- return redirect('clients_list')
-# Table One View (ExcelEntry)
-def table_one_view(request):
- from .models import ExcelEntry, Client, Institute
-
- # --- Base queryset for the main Helium Input table ---
- base_entries = ExcelEntry.objects.all().select_related('client', 'client__institute')
-
- # Read the global 6-month interval from the session
- interval_year = request.session.get('halfyear_year')
- interval_start = request.session.get('halfyear_start_month')
-
- if interval_year and interval_start:
- interval_year = int(interval_year)
- interval_start = int(interval_start)
-
- # Build the same 6-month window as on the main page (can cross year)
- window = []
- for offset in range(6):
- total_index = (interval_start - 1) + offset # 0-based
- y = interval_year + (total_index // 12)
- m = (total_index % 12) + 1
- window.append((y, m))
-
- # Build Q filter: (year=m_year AND month=m_month) for any of those 6
- q = Q()
- for (y, m) in window:
- q |= Q(date__year=y, date__month=m)
-
- entries_table1 = base_entries.filter(q).order_by('-date')
- else:
- # Fallback: if no global interval yet, show everything
- entries_table1 = base_entries.order_by('-date')
- clients = Client.objects.all().select_related('institute')
- institutes = Institute.objects.all()
-
- # ---- Overview filters ----
- # years present in ExcelEntry.date
- year_qs = ExcelEntry.objects.dates('date', 'year', order='DESC')
- available_years = [d.year for d in year_qs]
-
- # default year/start month
- # default year/start month (if no global interval yet)
- if available_years:
- default_year = available_years[0] # newest year in ExcelEntry
- else:
- default_year = timezone.now().year
-
- # 🔸 Read global half-year interval from session (set on main page)
- session_year = request.session.get('halfyear_year')
- session_start = request.session.get('halfyear_start_month')
-
- # If the user has set a global interval, use it.
- # Otherwise fall back to default year / January.
- year = int(session_year) if session_year else int(default_year)
- start_month = int(session_start) if session_start else 1
-
-
- # six-month window
- # --- Build a 6-month window, allowing wrap into the next year ---
- # Example: year=2025, start_month=10
- # window = [(2025,10), (2025,11), (2025,12), (2026,1), (2026,2), (2026,3)]
- window = []
- for offset in range(6):
- total_index = (start_month - 1) + offset # 0-based
- y_for_month = year + (total_index // 12)
- m_for_month = (total_index % 12) + 1
- window.append((y_for_month, m_for_month))
-
- overview = None
-
- if window:
- # Build per-group data
- groups_entries = [] # for internal calculations
-
- for key, group in CLIENT_GROUPS.items():
- clients_qs = get_group_clients(key)
-
- values = []
- group_total = Decimal('0')
-
- for (y_m, m_m) in window:
- total = ExcelEntry.objects.filter(
- client__in=clients_qs,
- date__year=y_m,
- date__month=m_m
- ).aggregate(
- total=Coalesce(Sum('lhe_ges'), Decimal('0'))
- )['total']
-
- values.append(total)
- group_total += total
-
- groups_entries.append({
- 'key': key,
- 'label': group['label'],
- 'values': values,
- 'total': group_total,
- })
-
- # month totals across all groups
- month_totals = []
- for idx in range(len(window)):
- s = sum((g['values'][idx] for g in groups_entries), Decimal('0'))
- month_totals.append(s)
-
- grand_total = sum(month_totals, Decimal('0'))
-
- # Build rows for the template
- rows = []
- for idx, (y_m, m_m) in enumerate(window):
- row_values = [g['values'][idx] for g in groups_entries]
- rows.append({
- 'month_number': m_m,
- 'month_label': calendar.month_name[m_m],
- 'values': row_values,
- 'total': month_totals[idx],
- })
-
- groups_meta = [{'key': g['key'], 'label': g['label']} for g in groups_entries]
- group_totals = [g['total'] for g in groups_entries]
-
- # Start/end for display – include years so wrap is clear
- start_year = window[0][0]
- start_month_disp = window[0][1]
- end_year = window[-1][0]
- end_month_disp = window[-1][1]
-
- overview = {
- 'year': year, # keep for backwards compatibility if needed
- 'start_month': start_month_disp,
- 'start_year': start_year,
- 'end_month': end_month_disp,
- 'end_year': end_year,
- 'rows': rows,
- 'groups': groups_meta,
- 'group_totals': group_totals,
- 'grand_total': grand_total,
- }
-
-
- # Month dropdown labels
- MONTH_CHOICES = [
- (1, 'Jan'), (2, 'Feb'), (3, 'Mar'), (4, 'Apr'),
- (5, 'May'), (6, 'Jun'), (7, 'Jul'), (8, 'Aug'),
- (9, 'Sep'), (10, 'Oct'), (11, 'Nov'), (12, 'Dec'),
- ]
-
- return render(request, 'table_one.html', {
- 'entries_table1': entries_table1,
- 'clients': clients,
- 'institutes': institutes,
- 'available_years': available_years,
- 'month_choices': MONTH_CHOICES,
- 'overview': overview,
- })
-
-# Table Two View (SecondTableEntry)
-def table_two_view(request):
- try:
- clients = Client.objects.all().select_related('institute')
- institutes = Institute.objects.all()
-
- # 🔸 Read global half-year interval from session
- interval_year = request.session.get('halfyear_year')
- interval_start = request.session.get('halfyear_start_month')
-
- if interval_year and interval_start:
- interval_year = int(interval_year)
- interval_start = int(interval_start)
-
- # Build the same 6-month window as in clients_list (can cross years)
- window = []
- for offset in range(6):
- total_index = (interval_start - 1) + offset # 0-based
- y = interval_year + (total_index // 12)
- m = (total_index % 12) + 1
- window.append((y, m))
-
- # Build a Q object matching any of those (year, month) pairs
- q = Q()
- for (y, m) in window:
- q |= Q(date__year=y, date__month=m)
-
- entries = SecondTableEntry.objects.filter(q).order_by('-date')
- else:
- # Fallback if no global interval yet: show all
- entries = SecondTableEntry.objects.all().order_by('-date')
-
- return render(request, 'table_two.html', {
- 'entries_table2': entries,
- 'clients': clients,
- 'institutes': institutes,
- 'interval_year': interval_year,
- 'interval_start_month': interval_start,
- })
-
- except Exception as e:
- return render(request, 'table_two.html', {
- 'error_message': f"Failed to load data: {str(e)}",
- 'entries_table2': [],
- 'clients': clients,
- 'institutes': institutes,
- })
-
-
-def monthly_sheet_root(request):
- """
- Redirect /sheet/ to the sheet matching the globally selected
- half-year start (year + month). If not set, fall back to latest.
- """
- year = request.session.get('halfyear_year')
- start_month = request.session.get('halfyear_start_month')
-
- if year and start_month:
- try:
- year = int(year)
- start_month = int(start_month)
- return redirect('monthly_sheet', year=year, month=start_month)
- except ValueError:
- pass # fall through
-
- # Fallback: latest MonthlySheet if exists
- latest_sheet = MonthlySheet.objects.order_by('-year', '-month').first()
- if latest_sheet:
- return redirect('monthly_sheet', year=latest_sheet.year, month=latest_sheet.month)
- else:
- now = timezone.now()
- return redirect('monthly_sheet', year=now.year, month=now.month)
-# Add Entry (Generic)
-def add_entry(request, model_name):
- if request.method == 'POST':
- try:
- if model_name == 'SecondTableEntry':
- model = SecondTableEntry
-
- # Parse date
- date_str = request.POST.get('date')
- try:
- date_obj = datetime.strptime(date_str, '%Y-%m-%d').date() if date_str else None
- except (ValueError, TypeError):
- return JsonResponse({
- 'status': 'error',
- 'message': 'Invalid date format. Use YYYY-MM-DD'
- }, status=400)
-
- # NEW: robust parse of warm flag (handles 0/1, true/false, on/off)
- raw_warm = request.POST.get('is_warm')
- is_warm_bool = str(raw_warm).lower() in ('1', 'true', 'on', 'yes')
-
- # Handle Helium Output (Table Two)
- lhe_output = request.POST.get('lhe_output')
-
- entry = model.objects.create(
- client=Client.objects.get(id=request.POST.get('client_id')),
- date=date_obj,
- is_warm=is_warm_bool, # <-- use the boolean here
- lhe_delivery=request.POST.get('lhe_delivery', ''),
- lhe_output=Decimal(lhe_output) if lhe_output else None,
- notes=request.POST.get('notes', '')
- )
-
- return JsonResponse({
- 'status': 'success',
- 'id': entry.id,
- 'client_name': entry.client.name,
- 'institute_name': entry.client.institute.name,
- 'date': entry.date.strftime('%Y-%m-%d') if entry.date else '',
- 'is_warm': entry.is_warm,
- 'lhe_delivery': entry.lhe_delivery,
- 'lhe_output': str(entry.lhe_output) if entry.lhe_output else '',
- 'notes': entry.notes
- })
-
- elif model_name == 'ExcelEntry':
- model = ExcelEntry
-
- # Parse the date string into a date object
- date_str = request.POST.get('date')
- try:
- date_obj = datetime.strptime(date_str, '%Y-%m-%d').date() if date_str else None
- except (ValueError, TypeError):
- date_obj = None
-
- try:
- pressure = Decimal(request.POST.get('pressure', 0))
- purity = Decimal(request.POST.get('purity', 0))
- druckkorrektur = Decimal(request.POST.get('druckkorrektur', 1))
- lhe_zus = Decimal(request.POST.get('lhe_zus', 0))
- constant_300 = Decimal(request.POST.get('constant_300', 300))
- korrig_druck = Decimal(request.POST.get('korrig_druck', 0))
- nm3 = Decimal(request.POST.get('nm3', 0))
- lhe = Decimal(request.POST.get('lhe', 0))
- lhe_ges = Decimal(request.POST.get('lhe_ges', 0))
- except InvalidOperation:
- return JsonResponse({
- 'status': 'error',
- 'message': 'Invalid numeric value in Helium Input'
- }, status=400)
-
- # Create the entry with ALL fields
- entry = model.objects.create(
- client=Client.objects.get(id=request.POST.get('client_id')),
- date=date_obj,
- pressure=pressure,
- purity=purity,
- druckkorrektur=druckkorrektur,
- lhe_zus=lhe_zus,
- constant_300=constant_300,
- korrig_druck=korrig_druck,
- nm3=nm3,
- lhe=lhe,
- lhe_ges=lhe_ges,
- notes=request.POST.get('notes', '')
- )
-
- # Prepare the response
- response_data = {
- 'status': 'success',
- 'id': entry.id,
- 'client_name': entry.client.name,
- 'institute_name': entry.client.institute.name,
- 'pressure': str(entry.pressure),
- 'purity': str(entry.purity),
- 'druckkorrektur': str(entry.druckkorrektur),
- 'constant_300': str(entry.constant_300),
- 'korrig_druck': str(entry.korrig_druck),
- 'nm3': str(entry.nm3),
- 'lhe': str(entry.lhe),
- 'lhe_zus': str(entry.lhe_zus),
- 'lhe_ges': str(entry.lhe_ges),
- 'notes': entry.notes,
- }
-
- if entry.date:
- # JS uses this for the Date column and for the Month column
- response_data['date'] = entry.date.strftime('%Y-%m-%d')
- response_data['month'] = f"{entry.date.month:02d}"
- else:
- response_data['date'] = ''
- response_data['month'] = ''
-
- return JsonResponse(response_data)
-
-
- except Exception as e:
- return JsonResponse({'status': 'error', 'message': str(e)}, status=400)
-
- return JsonResponse({'status': 'error', 'message': 'Invalid request'}, status=400)
-
-# Update Entry (Generic)
-def update_entry(request, model_name):
- if request.method == 'POST':
- try:
- if model_name == 'SecondTableEntry':
- model = SecondTableEntry
- elif model_name == 'ExcelEntry':
- model = ExcelEntry
- else:
- return JsonResponse({'status': 'error', 'message': 'Invalid model'}, status=400)
-
- entry_id = int(request.POST.get('id'))
- entry = model.objects.get(id=entry_id)
-
- # Common updates for both models
- entry.client = Client.objects.get(id=request.POST.get('client_id'))
- entry.notes = request.POST.get('notes', '')
-
- # Handle date properly for both models
- date_str = request.POST.get('date')
- if date_str:
- try:
- entry.date = datetime.strptime(date_str, '%Y-%m-%d').date()
- except ValueError:
- return JsonResponse({
- 'status': 'error',
- 'message': 'Invalid date format. Use YYYY-MM-DD'
- }, status=400)
-
- if model_name == 'SecondTableEntry':
- # Handle Helium Output specific fields
-
- raw_warm = request.POST.get('is_warm')
- entry.is_warm = str(raw_warm).lower() in ('1', 'true', 'on', 'yes')
-
- entry.lhe_delivery = request.POST.get('lhe_delivery', '')
-
- lhe_output = request.POST.get('lhe_output')
- try:
- entry.lhe_output = Decimal(lhe_output) if lhe_output else None
- except InvalidOperation:
- return JsonResponse({
- 'status': 'error',
- 'message': 'Invalid LHe Output value'
- }, status=400)
-
- entry.save()
-
- return JsonResponse({
- 'status': 'success',
- 'id': entry.id,
- 'client_name': entry.client.name,
- 'institute_name': entry.client.institute.name,
- 'date': entry.date.strftime('%Y-%m-%d') if entry.date else '',
- 'is_warm': entry.is_warm,
- 'lhe_delivery': entry.lhe_delivery,
- 'lhe_output': str(entry.lhe_output) if entry.lhe_output else '',
- 'notes': entry.notes
- })
-
-
- elif model_name == 'ExcelEntry':
- # Handle Helium Input specific fields
- try:
- entry.pressure = Decimal(request.POST.get('pressure', 0))
- entry.purity = Decimal(request.POST.get('purity', 0))
- entry.druckkorrektur = Decimal(request.POST.get('druckkorrektur', 1))
- entry.lhe_zus = Decimal(request.POST.get('lhe_zus', 0))
- entry.constant_300 = Decimal(request.POST.get('constant_300', 300))
- entry.korrig_druck = Decimal(request.POST.get('korrig_druck', 0))
- entry.nm3 = Decimal(request.POST.get('nm3', 0))
- entry.lhe = Decimal(request.POST.get('lhe', 0))
- entry.lhe_ges = Decimal(request.POST.get('lhe_ges', 0))
- except InvalidOperation:
- return JsonResponse({
- 'status': 'error',
- 'message': 'Invalid numeric value in Helium Input'
- }, status=400)
-
- entry.save()
-
- return JsonResponse({
- 'status': 'success',
- 'id': entry.id,
- 'client_name': entry.client.name,
- 'institute_name': entry.client.institute.name,
- 'date': entry.date.strftime('%Y-%m-%d') if entry.date else '',
- 'month': f"{entry.date.month:02d}" if entry.date else '',
- 'pressure': str(entry.pressure),
- 'purity': str(entry.purity),
- 'druckkorrektur': str(entry.druckkorrektur),
- 'constant_300': str(entry.constant_300),
- 'korrig_druck': str(entry.korrig_druck),
- 'nm3': str(entry.nm3),
- 'lhe': str(entry.lhe),
- 'lhe_zus': str(entry.lhe_zus),
- 'lhe_ges': str(entry.lhe_ges),
- 'notes': entry.notes
- })
-
-
- except model.DoesNotExist:
- return JsonResponse({'status': 'error', 'message': 'Entry not found'}, status=404)
- except Exception as e:
- return JsonResponse({'status': 'error', 'message': str(e)}, status=400)
-
- return JsonResponse({'status': 'error', 'message': 'Invalid request method'}, status=400)
-
-# Delete Entry (Generic)
-def delete_entry(request, model_name):
- if request.method == 'POST':
- try:
- if model_name == 'SecondTableEntry':
- model = SecondTableEntry
- elif model_name == 'ExcelEntry':
- model = ExcelEntry
- else:
- return JsonResponse({'status': 'error', 'message': 'Invalid model'}, status=400)
-
- entry_id = request.POST.get('id')
- entry = model.objects.get(id=entry_id)
- entry.delete()
- return JsonResponse({'status': 'success', 'message': 'Entry deleted'})
-
- except model.DoesNotExist:
- return JsonResponse({'status': 'error', 'message': 'Entry not found'}, status=404)
- except Exception as e:
- return JsonResponse({'status': 'error', 'message': str(e)}, status=400)
-
- return JsonResponse({'status': 'error', 'message': 'Invalid request'}, status=400)
-
-def betriebskosten_list(request):
- items = Betriebskosten.objects.all().order_by('-buchungsdatum')
-
- summary = get_summary()
- summary.recalculate()
-
- context = {
- 'items': items,
- 'summary': summary,
- }
- return render(request, 'betriebskosten_list.html', context)
-
-def betriebskosten_create(request):
- if request.method != 'POST':
- return JsonResponse({'status': 'error', 'message': 'Invalid request method'})
-
- try:
- entry_id = request.POST.get('id')
- if entry_id:
- entry = Betriebskosten.objects.get(id=entry_id)
- else:
- entry = Betriebskosten()
- gegenstand = request.POST.get("gegenstand", "").strip()
- buchungsdatum_str = request.POST.get('buchungsdatum')
- rechnungsnummer = request.POST.get('rechnungsnummer') # UI label: Firma
- kostentyp = request.POST.get('kostentyp')
- betrag_raw = request.POST.get('betrag')
- beschreibung = request.POST.get('beschreibung') or ''
- gas_volume_raw = request.POST.get('gas_volume') # UI label: Gasvolumen (m³)
-
- # Validate required fields
- if not all([buchungsdatum_str, rechnungsnummer, kostentyp, betrag_raw]):
- return JsonResponse({'status': 'error', 'message': 'All required fields must be filled'})
-
- allowed = {'sach', 'helium'}
- if kostentyp not in allowed:
- return JsonResponse({'status': 'error', 'message': 'Invalid Kostentyp'})
-
- # Date
- buchungsdatum = parse_date(buchungsdatum_str)
- if not buchungsdatum:
- return JsonResponse({'status': 'error', 'message': 'Invalid date format'})
-
- # Betrag
- try:
- betrag = Decimal(str(betrag_raw))
- except Exception:
- return JsonResponse({'status': 'error', 'message': 'Invalid Betrag'})
-
- # Gas volume (m³) only for helium
- gas_m3 = None
- if kostentyp == 'helium':
- if gas_volume_raw not in (None, '', 'None'):
- try:
- gas_m3 = Decimal(str(gas_volume_raw))
- except Exception:
- return JsonResponse({'status': 'error', 'message': 'Invalid Gasvolumen (m³)'})
- entry.gegenstand = gegenstand
- entry.buchungsdatum = buchungsdatum
- entry.rechnungsnummer = rechnungsnummer
- entry.kostentyp = kostentyp
- entry.betrag = betrag
- entry.beschreibung = beschreibung
- entry.gas_volume = gas_m3
-
- entry.save()
- summary = get_summary()
- summary.recalculate()
-
- # Computed values for UI (read-only fields)
- gas_liter = None
- price_per_m3 = None
- price_per_liter = None
-
- if entry.kostentyp == 'helium' and entry.gas_volume:
- # Liter = m³ / 0.75
- gas_liter = (Decimal(entry.gas_volume) / Decimal('0.75'))
- if entry.gas_volume != 0:
- price_per_m3 = (Decimal(entry.betrag) / Decimal(entry.gas_volume))
- if gas_liter != 0:
- price_per_liter = (Decimal(entry.betrag) / gas_liter)
-
- return JsonResponse({
- 'status': 'success',
- 'id': entry.id,
- 'gegenstand': entry.gegenstand,
- 'buchungsdatum': entry.buchungsdatum.strftime('%Y-%m-%d'),
- 'rechnungsnummer': entry.rechnungsnummer,
- 'kostentyp': entry.kostentyp,
- 'kostentyp_display': entry.get_kostentyp_display(),
- 'gas_volume_m3': str(entry.gas_volume) if entry.gas_volume is not None else '-',
- 'gas_volume_liter': str(gas_liter.quantize(Decimal("0.01"))) if gas_liter is not None else '',
- 'price_per_m3': str(price_per_m3.quantize(Decimal("0.01"))) if price_per_m3 is not None else '',
- 'price_per_liter': str(price_per_liter.quantize(Decimal("0.01"))) if price_per_liter is not None else '',
- 'betrag': str(entry.betrag),
- 'beschreibung': entry.beschreibung or '',
- })
-
- except Betriebskosten.DoesNotExist:
- return JsonResponse({'status': 'error', 'message': 'Entry not found'}, status=404)
- except Exception as e:
- return JsonResponse({'status': 'error', 'message': str(e)})
-
-
-def betriebskosten_delete(request):
- if request.method == 'POST':
- entry_id = request.POST.get('id')
-
- if not entry_id or not entry_id.isdigit():
- return JsonResponse({
- 'status': 'error',
- 'message': 'Invalid ID'
- })
-
- try:
- entry = Betriebskosten.objects.get(id=int(entry_id))
- entry.delete()
-
- summary = get_summary()
- summary.recalculate()
-
- return JsonResponse({'status': 'success'})
-
- except Betriebskosten.DoesNotExist:
- return JsonResponse({
- 'status': 'error',
- 'message': 'Entry not found'
- })
-
- return JsonResponse({'status': 'error'})
-
-
-def get_summary():
- summary, created = BetriebskostenSummary.objects.get_or_create(id=1)
- return summary
-
-from django.http import JsonResponse
-
-def update_personalkosten(request):
- if request.method == "POST":
- value = request.POST.get("personalkosten")
- summary = get_summary()
- summary.personalkosten = Decimal(value or "0")
- summary.recalculate()
- return JsonResponse({
- "status": "success",
- "umlage_personal": str(summary.umlage_personal),
- })
- return JsonResponse({"status": "error"})
-
-
-class CheckSheetView(View):
- def get(self, request):
- # Get current month/year
- current_year = datetime.now().year
- current_month = datetime.now().month
-
- # Get all sheets
- sheets = MonthlySheet.objects.all()
-
- sheet_data = []
- for sheet in sheets:
- cells_count = sheet.cells.count()
- # Count non-empty cells
- non_empty = sheet.cells.exclude(value__isnull=True).count()
-
- sheet_data.append({
- 'id': sheet.id,
- 'year': sheet.year,
- 'month': sheet.month,
- 'month_name': calendar.month_name[sheet.month],
- 'total_cells': cells_count,
- 'non_empty_cells': non_empty,
- 'has_data': non_empty > 0
- })
-
- # Also check what the default view would show
- default_sheet = MonthlySheet.objects.filter(
- year=current_year,
- month=current_month
- ).first()
-
- return JsonResponse({
- 'current_year': current_year,
- 'current_month': current_month,
- 'current_month_name': calendar.month_name[current_month],
- 'default_sheet_exists': default_sheet is not None,
- 'default_sheet_id': default_sheet.id if default_sheet else None,
- 'sheets': sheet_data,
- 'total_sheets': len(sheet_data)
- })
-
-
-class QuickDebugView(View):
- def get(self, request):
- # Get ALL sheets
- sheets = MonthlySheet.objects.all().order_by('year', 'month')
-
- result = {
- 'sheets': []
- }
-
- for sheet in sheets:
- sheet_info = {
- 'id': sheet.id,
- 'display': f"{sheet.year}-{sheet.month:02d}",
- 'url': f"/sheet/{sheet.year}/{sheet.month}/", # CHANGED THIS LINE
- 'sheet_url_pattern': 'sheet/{year}/{month}/', # Add this for clarity
- }
-
- # Count cells with data for first client in top_left table
- first_client = Client.objects.first()
- if first_client:
- test_cells = sheet.cells.filter(
- client=first_client,
- table_type='top_left',
- row_index__in=[8, 9, 10] # Rows 9, 10, 11
- ).order_by('row_index')
-
- cell_values = {}
- for cell in test_cells:
- cell_values[f"row_{cell.row_index}"] = str(cell.value) if cell.value else "Empty"
-
- sheet_info['test_cells'] = cell_values
- else:
- sheet_info['test_cells'] = "No clients"
-
- result['sheets'].append(sheet_info)
-
- return JsonResponse(result)
-class TestFormulaView(View):
- def get(self, request):
- # Test the formula evaluation directly
- test_values = {
- 8: 2, # Row 9 value (0-based index 8)
- 9: 2, # Row 10 value (0-based index 9)
- }
-
- # Test formula "9 + 8" (using 0-based indices)
- formula = "9 + 8"
- result = evaluate_formula(formula, test_values)
-
- return JsonResponse({
- 'test_values': test_values,
- 'formula': formula,
- 'result': str(result),
- 'note': 'Formula uses 0-based indices. 9=Row10, 8=Row9'
- })
-
-
-class SimpleDebugView(View):
- """Simplest debug view to check if things are working"""
- def get(self, request):
- sheet_id = request.GET.get('sheet_id', 1)
-
- try:
- sheet = MonthlySheet.objects.get(id=sheet_id)
-
- # Get first client
- client = Client.objects.first()
- if not client:
- return JsonResponse({'error': 'No clients found'})
-
- # Check a few cells
- cells = Cell.objects.filter(
- sheet=sheet,
- client=client,
- table_type='top_left',
- row_index__in=[8, 9, 10]
- ).order_by('row_index')
-
- cell_data = []
- for cell in cells:
- cell_data.append({
- 'row_index': cell.row_index,
- 'ui_row': cell.row_index + 1,
- 'value': str(cell.value) if cell.value is not None else 'Empty',
- 'cell_id': cell.id
- })
-
- return JsonResponse({
- 'sheet': f"{sheet.year}-{sheet.month}",
- 'sheet_id': sheet.id,
- 'client': client.name,
- 'cells': cell_data,
- 'note': 'Row 8 = UI Row 9, Row 9 = UI Row 10, Row 10 = UI Row 11'
- })
-
- except MonthlySheet.DoesNotExist:
- return JsonResponse({'error': f'Sheet with id {sheet_id} not found'})
-
-def halfyear_settings(request):
- """
- Global settings page: choose a year + first month for the 6-month interval.
- These values are stored in the session and used by other views.
- """
- # Determine available years from your data (use ExcelEntry or SecondTableEntry)
- # Here I use SecondTableEntry; you can switch to ExcelEntry if you prefer.
- available_years_qs = SecondTableEntry.objects.dates('date', 'year', order='DESC')
- available_years = [d.year for d in available_years_qs]
-
- if not available_years:
- available_years = [timezone.now().year]
-
- # Defaults (if nothing in session yet)
- default_year = request.session.get('halfyear_year', available_years[0])
- default_start_month = request.session.get('halfyear_start_month', 1)
-
- if request.method == 'POST':
- year = int(request.POST.get('year', default_year))
- start_month = int(request.POST.get('start_month', default_start_month))
-
- request.session['halfyear_year'] = year
- request.session['halfyear_start_month'] = start_month
-
- # Redirect back to where user came from, or to this page again
- next_url = request.POST.get('next') or request.GET.get('next') or 'halfyear_settings'
- return redirect(next_url)
-
- # Month choices for the dropdown
- month_choices = [(i, calendar.month_name[i]) for i in range(1, 13)]
-
- context = {
- 'available_years': available_years,
- 'selected_year': int(default_year),
- 'selected_start_month': int(default_start_month),
- 'month_choices': month_choices,
- }
- return render(request, 'halfyear_settings.html', context)
\ No newline at end of file
diff --git a/sheets/old/halfyear_balance.html b/sheets/old/halfyear_balance.html
deleted file mode 100644
index 1a429db..0000000
--- a/sheets/old/halfyear_balance.html
+++ /dev/null
@@ -1,353 +0,0 @@
-{% 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 %}
-
-
-
-
Bottom Table 2 – Verbraucherbestand L-He (read-only)
-
-
-
-
-
- | Verbraucherbestand |
- {{ bottom2.j38|floatformat:2 }} |
- {{ bottom2.k38|floatformat:2 }} |
-
-
-
-
- | + Anlage: |
- Gefäss 2,5: |
- {{ bottom2.g39|floatformat:2 }} |
- Gefäss 1,0: |
- {{ bottom2.i39|floatformat:2 }} |
- {{ bottom2.j39|floatformat:2 }} |
- {{ bottom2.k39|floatformat:2 }} |
-
-
-
-
- | + Kaltgas: |
- Gefäss 2,5: |
- {{ bottom2.g40|default_if_none:""|floatformat:2 }} |
- Gefäss 1,0: |
- {{ bottom2.i40|default_if_none:""|floatformat:2 }} |
- {{ bottom2.j40|floatformat:2 }} |
- {{ bottom2.k40|floatformat:2 }} |
-
-
-
-
- | Bestand flüssig He |
- {{ bottom2.j43|floatformat:2 }} |
- {{ bottom2.k43|floatformat:2 }} |
-
-
-
-
- | Gesamtbestand neu: |
- {{ bottom2.j44|floatformat:2 }} |
- {{ bottom2.k44|floatformat:2 }} |
-
-
-
-
-
-
-
-
-
-
{# closes .spreadsheet-container #}
-{% endblock %}
diff --git a/sheets/old/views.py b/sheets/old/views.py
deleted file mode 100644
index b40f726..0000000
--- a/sheets/old/views.py
+++ /dev/null
@@ -1,4401 +0,0 @@
-from django.shortcuts import render, redirect
-from django.utils.decorators import method_decorator
-from django.views.decorators.csrf import csrf_exempt
-from django.db.models import Sum, Value, DecimalField
-from django.http import JsonResponse
-from django.db.models import Q
-from decimal import Decimal, InvalidOperation
-from django.apps import apps
-from datetime import date, datetime
-import calendar
-from django.utils import timezone
-from django.views.generic import TemplateView, View
-from .models import (
- Client, SecondTableEntry, Institute, ExcelEntry,
- Betriebskosten, MonthlySheet, Cell, CellReference, MonthlySummary
-)
-from django.db.models import Sum
-from django.urls import reverse
-from django.db.models.functions import Coalesce
-from .forms import BetriebskostenForm
-from django.utils.dateparse import parse_date
-from django.contrib.auth.mixins import LoginRequiredMixin
-import json
-FIRST_SHEET_YEAR = 2025
-FIRST_SHEET_MONTH = 1
-
-CLIENT_GROUPS = {
- 'ikp': {
- 'label': 'IKP',
- # exactly as in the Clients admin
- 'names': ['IKP'],
- },
- 'phys_chem_bunt_fohrer': {
- 'label': 'Buntkowsky + Dr. Fohrer',
- # include all variants you might have used for Buntkowsky
- 'names': [
- 'AG Buntk.', # the one in your new entry
- 'AG Buntkowsky.', # from your original list
- 'AG Buntkowsky',
- 'Dr. Fohrer',
- ],
- },
- 'mawi_alff_gutfleisch': {
- 'label': 'Alff + AG Gutfleisch',
- # include both short and full forms
- 'names': [
- 'AG Alff',
- 'AG Gutfl.',
- 'AG Gutfleisch',
- ],
- },
- 'm3_group': {
- 'label': 'M3 Buntkowsky + M3 Thiele + M3 Gutfleisch',
- 'names': [
- 'M3 Buntkowsky',
- 'M3 Thiele',
- 'M3 Gutfleisch',
- ],
- },
-}
-
-# Add this CALCULATION_CONFIG at the top of views.py
-
-CALCULATION_CONFIG = {
- 'top_left': {
- # Row mappings: Django row_index (0-based) to Excel row
- # Excel B4 -> Django row_index 1 (UI row 2)
- # Excel B5 -> Django row_index 2 (UI row 3)
- # Excel B6 -> Django row_index 3 (UI row 4)
-
- # B6 (row_index 3) = B5 (row_index 2) / 0.75
- 3: "2 / 0.75",
-
- # B11 (row_index 10) = B9 (row_index 8)
- 10: "8",
-
- # B14 (row_index 13) = B13 (row_index 12) - B11 (row_index 10) + B12 (row_index 11)
- 13: "12 - 10 + 11",
-
- # Note: B5, B17, B19, B20 require IF logic, so they'll be handled separately
- },
-
- # other tables (top_right, bottom_1, ...) stay as they are
-
-
- ' top_right': {
- # UI Row 1 (Excel Row 4): Stand der Gaszähler (Vormonat) (Nm³)
- 0: {
- 'L': "9 / (9 + 9) if (9 + 9) > 0 else 0", # L4 = L13/(L13+M13)
- 'M': "9 / (9 + 9) if (9 + 9) > 0 else 0", # M4 = M13/(L13+M13)
- 'N': "9 / (9 + 9) if (9 + 9) > 0 else 0", # N4 = N13/(N13+O13)
- 'O': "9 / (9 + 9) if (9 + 9) > 0 else 0", # O4 = O13/(N13+O13)
- 'P': None, # Editable
- 'Q': None, # Editable
- 'R': None, # Editable
- },
-
- # UI Row 2 (Excel Row 5): Gasrückführung (Nm³)
- 1: {
- 'L': "4", # L5 = L8
- 'M': "4", # M5 = L8 (merged)
- 'N': "4", # N5 = N8
- 'O': "4", # O5 = N8 (merged)
- 'P': "4 * 0", # P5 = P8 * P4
- 'Q': "4 * 0", # Q5 = P8 * Q4
- 'R': "4 * 0", # R5 = P8 * R4
- },
-
- # UI Row 3 (Excel Row 6): Rückführung flüssig (Lit. L-He)
- 2: {
- 'L': "4", # L6 = L8 (Sammelrückführungen)
- 'M': "4",
- 'N': "4",
- 'O': "4",
- 'P': "4",
- 'Q': "4",
- 'R': "4",
-},
-
- # UI Row 4 (Excel Row 7): Sonderrückführungen (Lit. L-He) - EDITABLE
- 3: {
- 'L': None,
- 'M': None,
- 'N': None,
- 'O': None,
- 'P': None,
- 'Q': None,
- 'R': None,
- },
-
- # UI Row 5 (Excel Row 8): Sammelrückführungen (Lit. L-He)
- 4: {
- 'L': None, # Will be populated from ExcelEntry
- 'M': None,
- 'N': None,
- 'O': None,
- 'P': None,
- 'Q': None,
- 'R': None,
- },
-
- # UI Row 6 (Excel Row 9): Bestand in Kannen-1 (Lit. L-He) - EDITABLE
- 5: {
- 'L': None,
- 'M': None,
- 'N': None,
- 'O': None,
- 'P': None,
- 'Q': None,
- 'R': None,
- },
-
- # UI Row 7 (Excel Row 10): Summe Bestand (Lit. L-He)
- 6: {
- 'L': None,
- 'M': None,
- 'N': None,
- 'O': None,
- 'P': None,
- 'Q': None,
- 'R': None,
- },
-
- # UI Row 11 (Excel Row 14): Rückführ. Soll (Lit. L-He)
- # handled in calculate_top_right_dependents (merged pairs + M3)
- 10: {
- 'L': None,
- 'M': None,
- 'N': None,
- 'O': None,
- 'P': None,
- 'Q': None,
- 'R': None,
- },
-
- # UI Row 12 (Excel Row 15): Verluste (Soll-Rückf.) (Lit. L-He)
- # handled in calculate_top_right_dependents
- 11: {
- 'L': None,
- 'M': None,
- 'N': None,
- 'O': None,
- 'P': None,
- 'Q': None,
- 'R': None,
- },
-
- # UI Row 14 (Excel Row 17): Kaltgas Rückgabe (Lit. L-He) – Faktor
- # handled in calculate_top_right_dependents (different formulas for pair 1 vs pair 2 + M3)
- 13: {
- 'L': None,
- 'M': None,
- 'N': None,
- 'O': None,
- 'P': None,
- 'Q': None,
- 'R': None,
- },
-
- # UI Row 16 (Excel Row 19): Verbraucherverluste (Liter L-He)
- # handled in calculate_top_right_dependents
- 15: {
- 'L': None,
- 'M': None,
- 'N': None,
- 'O': None,
- 'P': None,
- 'Q': None,
- 'R': None,
- },
-
- # UI Row 17 (Excel Row 20): %
- # handled in calculate_top_right_dependents
- 16: {
- 'L': None,
- 'M': None,
- 'N': None,
- 'O': None,
- 'P': None,
- 'Q': None,
- 'R': None,
- }
-},
- 'bottom_1': {
- 5: "4 + 3 + 2",
- 8: "7 - 6",
- },
- 'bottom_2': {
- 3: "1 + 2",
- 6: "5 - 4",
- },
- 'bottom_3': {
- 2: "0 + 1",
- 5: "3 + 4",
- },
- # Special configuration for summation column (last column)
- 'summation_column': {
- # For each row that should be summed across columns
- 'rows_to_sum': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23], # All rows
- # OR specify specific rows:
- # 'rows_to_sum': [0, 5, 10, 15, 20], # Only specific rows
- # The last column index (0-based)
- 'sum_column_index': 5, # 6th column (0-5) since you have 6 clients
- }
-}
-def build_halfyear_window(interval_year: int, start_month: int):
- """
- Build a list of (year, month) for the 6-month interval, possibly crossing into the next year.
- Example: (2025, 10) -> [(2025,10), (2025,11), (2025,12), (2026,1), (2026,2), (2026,3)]
- """
- window = []
- for offset in range(6):
- total_index = (start_month - 1) + offset # 0-based
- y = interval_year + (total_index // 12)
- m = (total_index % 12) + 1
- window.append((y, m))
- return window
-# ---------------------------------------------------------------------------
-# Halbjahres-Bilanz helpers
-# ---------------------------------------------------------------------------
-
-# You can adjust these indices if needed.
-# Assuming:
-# - bottom_1.table has row "Gasbestand" at some fixed row index,
-# and columns: ... Nm³, Lit. LHe
-GASBESTAND_ROW_INDEX = 9 # <-- adjust if your bottom_1 has a different row index
-GASBESTAND_COL_NM3 = 3 # <-- adjust to the column index for Nm³ in bottom_1
-
-# In top_left / top_right, "Bestand in Kannen-1 (Lit. L-He)" is row_index 5
-BESTAND_KANNEN_ROW_INDEX = 5
-
-def pick_sheet_by_gasbestand(window, sheets_by_ym, prev_sheet):
- """
- Returns the last sheet in the window whose Gasbestand (J36, Nm³ column) != 0.
- If none found, returns prev_sheet (Übertrag_Dez__Vorjahr equivalent).
- """
- for (y, m) in reversed(window):
- sheet = sheets_by_ym.get((y, m))
- if not sheet:
- continue
- gasbestand_nm3 = get_bottom1_value(sheet, GASBESTAND_ROW_INDEX, GASBESTAND_COL_NM3)
- if gasbestand_nm3 != 0:
- return sheet
- return prev_sheet
-def get_bottom1_value(sheet, row_index: int, col_index: int) -> Decimal:
- """Get a numeric value from bottom_1, or 0 if missing."""
- if sheet is None:
- return Decimal('0')
-
- cell = Cell.objects.filter(
- sheet=sheet,
- table_type='bottom_1',
- row_index=row_index,
- column_index=col_index,
- ).first()
-
- if cell is None or cell.value in (None, ''):
- return Decimal('0')
-
- try:
- return Decimal(str(cell.value))
- except Exception:
- return Decimal('0')
-
-# MUST match the column order in your monthly_sheets top-right table
-
-
-
-def get_top_right_value(sheet, client_name: str, row_index: int) -> Decimal:
- """
- Read a numeric value from the top_right table of a MonthlySheet for
- a given client (by column) and row_index.
-
- top_right cells are keyed by (sheet, table_type='top_right',
- row_index, column_index), where column_index is the position of the
- client in HALFYEAR_RIGHT_CLIENTS.
- """
- if sheet is None:
- return Decimal('0')
-
- col_index = RIGHT_CLIENT_INDEX.get(client_name)
- if col_index is None:
- return Decimal('0')
-
- cell = Cell.objects.filter(
- sheet=sheet,
- table_type='top_right',
- row_index=row_index,
- column_index=col_index,
- ).first()
-
- if cell is None or cell.value in (None, ''):
- return Decimal('0')
-
- try:
- return Decimal(str(cell.value))
- except Exception:
- return Decimal('0')
-
-TR_RUECKF_FLUESSIG_ROW = 2 # confirmed by your March value 172.840560
-TR_BESTAND_KANNEN_ROW = 5 # confirmed by your earlier query
-def get_bestand_kannen_for_month(sheet, client_name: str) -> Decimal:
- """
- 'B9' in your description: Bestand in Kannen-1 (Lit. L-He)
- For this implementation we take it from top_left row_index = 5 for that client.
- """
- return get_top_left_value(sheet, client_name, row_index=BESTAND_KANNEN_ROW_INDEX)
-
-from decimal import Decimal
-from django.db.models import Sum
-from django.db.models.functions import Coalesce
-from django.db.models import DecimalField, Value
-
-from .models import MonthlySheet, SecondTableEntry, Client, Cell
-from django.shortcuts import redirect, render
-
-# You already have HALFYEAR_CLIENTS for the left table (AG Vogel, AG Halfm, IKP)
-HALFYEAR_CLIENTS = ["AG Vogel", "AG Halfm", "IKP"]
-
-# NEW: clients for the top-right half-year table
-HALFYEAR_RIGHT_CLIENTS = [
- "Dr. Fohrer",
- "AG Buntk.",
- "AG Alff",
- "AG Gutfl.",
- "M3 Thiele",
- "M3 Buntkowsky",
- "M3 Gutfleisch",
-]
-BOTTOM1_COL_VOLUME = 0
-BOTTOM1_COL_BAR = 1
-BOTTOM1_COL_KORR = 2
-BOTTOM1_COL_NM3 = 3
-BOTTOM1_COL_LHE = 4
-BOTTOM2_ROW_ANLAGE = 0
-BOTTOM2_COL_G39 = 0 # "Gefäss 2,5" (cell id shows column_index=0)
-BOTTOM2_COL_I39 = 1 # "Gefäss 1,0" (cell id shows column_index=1)
-BOTTOM2_ROW_INPUTS = {
- "g39": (0, 0), # row_index=0, column_index=0 (your G39)
- "i39": (0, 1), # row_index=0, column_index=1 (your I39)
-}
-FACTOR_NM3_TO_LHE = Decimal("0.75")
-RIGHT_CLIENT_INDEX = {name: idx for idx, name in enumerate(HALFYEAR_RIGHT_CLIENTS)}
-def halfyear_balance_view(request):
- """
- Read-only Halbjahres-Bilanz view.
-
- LEFT table: AG Vogel / AG Halfm / IKP (exactly as in your last working version)
- RIGHT table: Dr. Fohrer / AG Buntk. / AG Alff / AG Gutfl. /
- M3 Thiele / M3 Buntkowsky / M3 Gutfleisch
- using the Excel formulas you described.
-
- Uses the global 6-month interval from the main page (clients_list).
- """
- # 1) Read half-year interval from the session
- interval_year = request.session.get('halfyear_year')
- interval_start = request.session.get('halfyear_start_month')
-
- if not interval_year or not interval_start:
- # No interval chosen yet -> redirect to main page
- return redirect('clients_list')
-
- interval_year = int(interval_year)
- interval_start = int(interval_start)
-
- # You already have this helper in your code
- window = build_halfyear_window(interval_year, interval_start)
- # window = [(y1, m1), (y2, m2), ..., (y6, m6)]
-
- # (Year, month) of the first month
- start_year, start_month = window[0]
-
- # Previous month (for "Stand ... (Vorjahr)" and "Best. in Kannen Vormonat")
- prev_total_index = (start_month - 1) - 1 # one month back, 0-based
- if prev_total_index >= 0:
- prev_year = start_year + (prev_total_index // 12)
- prev_month = (prev_total_index % 12) + 1
- else:
- prev_year = start_year - 1
- prev_month = 12
-
- # Load MonthlySheet objects for the window and for the previous month
- sheets_by_ym = {}
- for (y, m) in window:
- sheet = MonthlySheet.objects.filter(year=y, month=m).first()
- sheets_by_ym[(y, m)] = sheet
-
- prev_sheet = MonthlySheet.objects.filter(year=prev_year, month=prev_month).first()
- def pick_bottom2_from_window(window, sheets_by_ym, prev_sheet):
- # choose sheet (same logic you already use)
- chosen = None
- for (y, m) in reversed(window):
- s = sheets_by_ym.get((y, m))
- # use your existing condition for choosing month
- if s:
- chosen = s
- break
- if chosen is None:
- chosen = prev_sheet
-
- # Now read the two inputs safely
- bottom2_inputs = {}
- for key, (row_idx, col_idx) in BOTTOM2_ROW_INPUTS.items():
- bottom2_inputs[key] = get_bottom2_value(chosen, row_idx, col_idx)
-
- return chosen, bottom2_inputs
-
-
- chosen_sheet_bottom2, bottom2_inputs = pick_bottom2_from_window(window, sheets_by_ym, prev_sheet)
- bottom2_g39 = bottom2_inputs["g39"]
- bottom2_i39 = bottom2_inputs["i39"]
- # ----------------------------
- # HALF-YEAR BOTTOM TABLE 1 (Bilanz) - Read only
- # ----------------------------
- chosen_sheet_bottom1 = pick_sheet_by_gasbestand(window, sheets_by_ym, prev_sheet)
-
- # IMPORTANT: define which bottom_1 row_index corresponds to Excel rows 27..35
- # If your bottom_1 starts at Excel row 27 => row_index 0 == Excel 27
- # then row_index = excel_row - 27
- BOTTOM1_EXCEL_START_ROW = 27
-
- bottom1_excel_rows = list(range(27, 37)) # 27..36
- BOTTOM1_LABELS = [
- "Batterie 1",
- "2",
- "3",
- "4",
- "5",
- "Batterie Links",
- "2 Bündel",
- "2 Ballone",
- "Reingasspeicher",
- "Gasbestand",
- ]
-
- BOTTOM1_VOLUMES = [
- Decimal("2.4"),
- Decimal("5.1"),
- Decimal("4.0"),
- Decimal("1.0"),
- Decimal("4.0"),
- Decimal("0.6"),
- Decimal("1.2"),
- Decimal("20.0"),
- Decimal("5.0"),
- None, # Gasbestand row has no volume
- ]
- nm3_sum_27_35 = Decimal("0")
- lhe_sum_27_35 = Decimal("0")
- bottom1_rows = []
-
- for excel_row in bottom1_excel_rows:
- row_index = excel_row - BOTTOM1_EXCEL_START_ROW
-
- chosen_sheet_bottom1 = None
- for (y, m) in reversed(window):
- s = sheets_by_ym.get((y, m))
- gasbestand = get_bottom1_value(s, GASBESTAND_ROW_INDEX, GASBESTAND_COL_NM3) # J36 (Nm3)
- if gasbestand != 0:
- chosen_sheet_bottom1 = s
- break
-
- if chosen_sheet_bottom1 is None:
- chosen_sheet_bottom1 = prev_sheet
-
- # Normal rows (27..35): read from chosen sheet and accumulate sums
- if excel_row != 36:
- nm3_val = get_bottom1_value(chosen_sheet_bottom1, row_index, BOTTOM1_COL_NM3)
- lhe_val = get_bottom1_value(chosen_sheet_bottom1, row_index, BOTTOM1_COL_LHE)
-
- nm3_sum_27_35 += nm3_val
- lhe_sum_27_35 += lhe_val
-
- bottom1_rows.append({
- "label": BOTTOM1_LABELS[row_index],
- "volume": BOTTOM1_VOLUMES[row_index],
- "bar": get_bottom1_value(chosen_sheet_bottom1, row_index, BOTTOM1_COL_BAR),
- "korr": get_bottom1_value(chosen_sheet_bottom1, row_index, BOTTOM1_COL_KORR),
- "nm3": nm3_val,
- "lhe": lhe_val,
- })
-
- # Gasbestand row (36): show sums (J36 = SUM(J27:J35), K36 = SUM(K27:K35))
- else:
- bottom1_rows.append({
- "label": "Gasbestand",
- "volume": "",
- "bar": "",
- "korr": "",
- "nm3": nm3_sum_27_35,
- "lhe": lhe_sum_27_35,
- })
- start_sheet = sheets_by_ym.get((start_year, start_month))
- # ------------------------------------------------------------
- # Bottom Table 2 (Halbjahres Bilanz) – server-side recalcBottom2()
- # ------------------------------------------------------------
-
- FACTOR_BT2 = Decimal("0.75")
-
- # 1) Helper: pick last-nonzero value of bottom_2 row0 col0/col1 from the window (fallback: prev_sheet)
- def pick_last_nonzero_bottom2(row_index: int, col_index: int) -> Decimal:
- # Scan from last month in window backwards
- for (y, m) in reversed(window):
- s = sheets_by_ym.get((y, m))
- if not s:
- continue
- v = get_bottom2_value(s, row_index, col_index)
- if v is not None and v != 0:
- return v
- # fallback to month before window
- v_prev = get_bottom2_value(prev_sheet, row_index, col_index)
- return v_prev if v_prev is not None else Decimal("0")
-
- # 2) K38 comes from Overall Summary: "Summe Bestand (Lit. L-He)"
- # Find it from your already built overall summary rows list.
-
- k38 = Decimal("0")
- j38 = Decimal("0")
- # 3) Inputs G39 / I39 (picked from last non-zero month in window)
- g39 = pick_last_nonzero_bottom2(row_index=0, col_index=0) # G39
- i39 = pick_last_nonzero_bottom2(row_index=0, col_index=1) # I39
-
- k39 = (g39 or Decimal("0")) + (i39 or Decimal("0"))
- j39 = k39 * FACTOR_BT2
-
- # 4) +Kaltgas (row 40)
- # JS:
- # g40 = (2500 - g39)/100*10
- # i40 = (1000 - i39)/100*10
- g40 = None
- i40 = None
- if g39 is not None:
- g40 = (Decimal("2500") - g39) / Decimal("100") * Decimal("10")
- if i39 is not None:
- i40 = (Decimal("1000") - i39) / Decimal("100") * Decimal("10")
-
- k40 = (g40 or Decimal("0")) + (i40 or Decimal("0"))
- j40 = k40 * FACTOR_BT2
-
- # 5) Bestand flüssig He (row 43)
- k43 = (
- (k38 or Decimal("0")) +
- (k39 or Decimal("0")) +
- (k40 or Decimal("0"))
- )
- j43 = k43 * FACTOR_BT2
-
- # 6) Gesamtbestand neu (row 44) = Gasbestand(Lit) from Bottom Table 1 + k43
- gasbestand_lit = Decimal("0")
- for r in bottom1_rows:
- if (r.get("label") or "").strip().startswith("Gasbestand"):
- gasbestand_lit = r.get("lhe") or Decimal("0")
- break
-
- k44 = (gasbestand_lit or Decimal("0")) + (k43 or Decimal("0"))
- j44 = k44 * FACTOR_BT2
-
- bottom2 = {
- "j38": j38, "k38": k38,
- "g39": g39, "i39": i39, "j39": j39, "k39": k39,
- "g40": g40, "i40": i40, "j40": j40, "k40": k40,
- "j43": j43, "k43": k43,
- "j44": j44, "k44": k44,
- }
-
- # ------------------------------------------------------------------
- # 2) LEFT TABLE (your existing, working logic)
- # ------------------------------------------------------------------
- HALFYEAR_CLIENTS_LEFT = ["AG Vogel", "AG Halfm", "IKP"]
-
- # We'll collect client-wise values first for clarity.
- client_data_left = {name: {} for name in HALFYEAR_CLIENTS_LEFT}
-
- # --- Row B3: Stand der Gaszähler (Nm³)
- # = MAX(B3 from previous month, and B3 from each of the 6 months in the window)
- # row_index 0 in top_left = "Stand der Gaszähler (Nm³)"
- months_for_max = [(prev_year, prev_month)] + window
-
- for cname in HALFYEAR_CLIENTS_LEFT:
- max_val = Decimal('0')
- for (y, m) in months_for_max:
- sheet = sheets_by_ym.get((y, m))
- if sheet is None and (y, m) == (prev_year, prev_month):
- sheet = prev_sheet
- val_b3 = get_top_left_value(sheet, cname, row_index=0)
- if val_b3 > max_val:
- max_val = val_b3
- client_data_left[cname]['stand_gas'] = max_val
-
- # --- Row B4: Stand der Gaszähler (Vorjahr) (Nm³) -> previous month same row ---
- for cname in HALFYEAR_CLIENTS_LEFT:
- val_b4 = get_top_left_value(prev_sheet, cname, row_index=0)
- client_data_left[cname]['stand_gas_prev'] = val_b4
-
- # --- Row B5: Gasrückführung (Nm³) = B3 - B4 ---
- for cname in HALFYEAR_CLIENTS_LEFT:
- b3 = client_data_left[cname]['stand_gas']
- b4 = client_data_left[cname]['stand_gas_prev']
- client_data_left[cname]['gasrueckf'] = b3 - b4
-
- # --- Row B6: Rückführung flüssig (Lit. L-He) = B5 / 0.75 ---
- for cname in HALFYEAR_CLIENTS_LEFT:
- b5 = client_data_left[cname]['gasrueckf']
- client_data_left[cname]['rueckf_fluessig'] = (b5 / Decimal('0.75')) if b5 != 0 else Decimal('0')
-
- # --- Row B7: Sonderrückführungen (Lit. L-He) = sum over 6 months of that row ---
- # That row index is 4 in your top_left table.
- for cname in HALFYEAR_CLIENTS_LEFT:
- sonder_total = Decimal('0')
- for (y, m) in window:
- sheet = sheets_by_ym.get((y, m))
- if sheet:
- sonder_total += get_top_left_value(sheet, cname, row_index=4)
- client_data_left[cname]['sonder'] = sonder_total
-
- # --- Row B8: Bestand in Kannen-1 (Lit. L-He) ---
- # Excel-style logic with Gasbestand (J36) and fallback to previous month.
- for cname in HALFYEAR_CLIENTS_LEFT:
- chosen_value = None
-
- # Go from last month (window[5]) backwards to first (window[0])
- for (y, m) in reversed(window):
- sheet = sheets_by_ym.get((y, m))
- gasbestand = get_bottom1_value(sheet, GASBESTAND_ROW_INDEX, GASBESTAND_COL_NM3)
- if gasbestand != 0:
- chosen_value = get_bestand_kannen_for_month(sheet, cname)
- break
-
- # If still None -> use previous month (Übertrag_Dez__Vorjahr equivalent)
- if chosen_value is None:
- sheet_prev = prev_sheet
- chosen_value = get_bestand_kannen_for_month(sheet_prev, cname)
-
- client_data_left[cname]['bestand_kannen'] = chosen_value if chosen_value is not None else Decimal('0')
-
- # --- Row B9: Summe Bestand (Lit. L-He) = equal to previous row ---
- for cname in HALFYEAR_CLIENTS_LEFT:
- client_data_left[cname]['summe_bestand'] = client_data_left[cname]['bestand_kannen']
-
- # --- Row B10: Best. in Kannen Vormonat (Lit. L-He)
- # = Bestand in Kannen-1 from the month BEFORE the window (prev_year, prev_month)
- for cname in HALFYEAR_CLIENTS_LEFT:
- client_data_left[cname]['best_kannen_vormonat'] = get_bestand_kannen_for_month(prev_sheet, cname)
-
- # --- Row B13: Bezug (Liter L-He) ---
- for cname in HALFYEAR_CLIENTS_LEFT:
- total_bezug = Decimal('0')
- for (y, m) in window:
- qs = SecondTableEntry.objects.filter(
- client__name=cname,
- date__year=y,
- date__month=m,
- ).aggregate(
- total=Coalesce(Sum('lhe_output'), Value(0, output_field=DecimalField()))
- )
- total_bezug += Decimal(str(qs['total']))
- client_data_left[cname]['bezug'] = total_bezug
-
- # --- Row B14: Rückführ. Soll (Lit. L-He) = Bezug - Summe Bestand + Best. in Kannen Vormonat ---
- for cname in HALFYEAR_CLIENTS_LEFT:
- b13 = client_data_left[cname]['bezug']
- b11 = client_data_left[cname]['summe_bestand']
- b12 = client_data_left[cname]['best_kannen_vormonat']
- client_data_left[cname]['rueckf_soll'] = b13 - b11 + b12
-
- # --- Row B15: Verluste (Soll-Rückf.) (Lit. L-He) = B14 - B6 ---
- for cname in HALFYEAR_CLIENTS_LEFT:
- b14 = client_data_left[cname]['rueckf_soll']
- b6 = client_data_left[cname]['rueckf_fluessig']
- client_data_left[cname]['verluste'] = b14 - b6
-
- # --- Row B16: Füllungen warm (Lit. L-He) = sum over 6 months (row_index=11) ---
- for cname in HALFYEAR_CLIENTS_LEFT:
- total_warm = Decimal('0')
- for (y, m) in window:
- sheet = sheets_by_ym.get((y, m))
- total_warm += get_top_left_value(sheet, cname, row_index=11)
- client_data_left[cname]['fuellungen_warm'] = total_warm
-
- # --- Row B17: Kaltgas Rückgabe (Lit. L-He) = Bezug * 0.06 ---
- factor = Decimal('0.06')
- for cname in HALFYEAR_CLIENTS_LEFT:
- b13 = client_data_left[cname]['bezug']
- client_data_left[cname]['kaltgas_rueckgabe'] = b13 * factor
-
- # --- Row B18: Verbraucherverluste (Liter L-He) = Verluste - Kaltgas Rückgabe ---
- for cname in HALFYEAR_CLIENTS_LEFT:
- b15 = client_data_left[cname]['verluste']
- b17 = client_data_left[cname]['kaltgas_rueckgabe']
- client_data_left[cname]['verbraucherverluste'] = b15 - b17
-
- # --- Row B19: % = Verbraucherverluste / Bezug ---
- for cname in HALFYEAR_CLIENTS_LEFT:
- bezug = client_data_left[cname]['bezug']
- verb = client_data_left[cname]['verbraucherverluste']
- if bezug != 0:
- client_data_left[cname]['percent'] = verb / bezug
- else:
- client_data_left[cname]['percent'] = None
-
- # Build LEFT rows structure
- left_row_defs = [
- ('Stand der Gaszähler (Nm³)', 'stand_gas'),
- ('Stand der Gaszähler (Vorjahr) (Nm³)', 'stand_gas_prev'),
- ('Gasrückführung (Nm³)', 'gasrueckf'),
- ('Rückführung flüssig (Lit. L-He)', 'rueckf_fluessig'),
- ('Sonderrückführungen (Lit. L-He)', 'sonder'),
- ('Bestand in Kannen-1 (Lit. L-He)', 'bestand_kannen'),
- ('Summe Bestand (Lit. L-He)', 'summe_bestand'),
- ('Best. in Kannen Vormonat (Lit. L-He)', 'best_kannen_vormonat'),
- ('Bezug (Liter L-He)', 'bezug'),
- ('Rückführ. Soll (Lit. L-He)', 'rueckf_soll'),
- ('Verluste (Soll-Rückf.) (Lit. L-He)', 'verluste'),
- ('Füllungen warm (Lit. L-He)', 'fuellungen_warm'),
- ('Kaltgas Rückgabe (Lit. L-He) – Faktor', 'kaltgas_rueckgabe'),
- ('Verbraucherverluste (Liter L-He)', 'verbraucherverluste'),
- ('%', 'percent'),
- ]
-
- rows_left = []
- for label, key in left_row_defs:
- values = [client_data_left[cname][key] for cname in HALFYEAR_CLIENTS_LEFT]
- if key == 'percent':
- total_bezug = sum((client_data_left[c]['bezug'] for c in HALFYEAR_CLIENTS_LEFT), Decimal('0'))
- total_verb = sum((client_data_left[c]['verbraucherverluste'] for c in HALFYEAR_CLIENTS_LEFT), Decimal('0'))
- total = (total_verb / total_bezug) if total_bezug != 0 else None
- else:
- total = sum((v for v in values if v is not None), Decimal('0'))
- rows_left.append({
- 'label': label,
- 'values': values,
- 'total': total,
- 'is_percent': key == 'percent',
- })
-
- # ------------------------------------------------------------------
- # 3) RIGHT TABLE (top-right half-year aggregation)
- # ------------------------------------------------------------------
- RIGHT_CLIENTS = HALFYEAR_RIGHT_CLIENTS # for brevity
-
- right_data = {name: {} for name in RIGHT_CLIENTS}
-
- # --- Bezug (Liter L-He) for each right client (same as for left) ---
- for cname in RIGHT_CLIENTS:
- total_bezug = Decimal('0')
- for (y, m) in window:
- qs = SecondTableEntry.objects.filter(
- client__name=cname,
- date__year=y,
- date__month=m,
- ).aggregate(
- total=Coalesce(Sum('lhe_output'), Value(0, output_field=DecimalField()))
- )
- total_bezug += Decimal(str(qs['total']))
- right_data[cname]['bezug'] = total_bezug
- def find_bestand_from_window(reference_client: str) -> Decimal:
- """
- Implements:
- WENN(last_month!J36=0; WENN(prev_month!J36=0; ...; prev_sheet!9); last_month!9)
- reference_client decides which column (L/N/P/Q/R) we read from monthly top_right row_index=5.
- """
- # scan backward through window
- for (y, m) in reversed(window):
- sh = sheets_by_ym.get((y, m))
- if not sh:
- continue
- gasbestand = get_bottom1_value(sh, GASBESTAND_ROW_INDEX, GASBESTAND_COL_NM3)
- if gasbestand != 0:
- return get_top_right_value(sh, reference_client, TR_BESTAND_KANNEN_ROW)
-
- # fallback to previous month (Übertrag_Dez__Vorjahr equivalent)
- return get_top_right_value(prev_sheet, reference_client, TR_BESTAND_KANNEN_ROW)
-
- # Fohrer+Buntk merged: BOTH use Fohrer column (L9)
- val_L = find_bestand_from_window("Dr. Fohrer")
- right_data["Dr. Fohrer"]["bestand_kannen"] = val_L
- right_data["AG Buntk."]["bestand_kannen"] = val_L
-
- # Alff+Gutfl merged: BOTH use Alff column (N9)
- val_N = find_bestand_from_window("AG Alff")
- right_data["AG Alff"]["bestand_kannen"] = val_N
- right_data["AG Gutfl."]["bestand_kannen"] = val_N
-
- # M3 each uses its own column (P9/Q9/R9)
- right_data["M3 Thiele"]["bestand_kannen"] = find_bestand_from_window("M3 Thiele")
- right_data["M3 Buntkowsky"]["bestand_kannen"] = find_bestand_from_window("M3 Buntkowsky")
- right_data["M3 Gutfleisch"]["bestand_kannen"] = find_bestand_from_window("M3 Gutfleisch")
- # Helper for pair shares (L13/($L13+$M13), etc.)
- def pair_share(c1, c2):
- total = right_data[c1]['bezug'] + right_data[c2]['bezug']
- if total == 0:
- return (Decimal('0'), Decimal('0'))
- return (
- right_data[c1]['bezug'] / total,
- right_data[c2]['bezug'] / total,
- )
-
- # --- "Stand der Gaszähler (Vorjahr) (Nm³)" row: share based on Bezug ---
- # Dr. Fohrer / AG Buntk.
- s_fohrer, s_buntk = pair_share("Dr. Fohrer", "AG Buntk.")
- right_data["Dr. Fohrer"]['stand_prev_share'] = s_fohrer
- right_data["AG Buntk."]['stand_prev_share'] = s_buntk
-
- # AG Alff / AG Gutfl.
- s_alff, s_gutfl = pair_share("AG Alff", "AG Gutfl.")
- right_data["AG Alff"]['stand_prev_share'] = s_alff
- right_data["AG Gutfl."]['stand_prev_share'] = s_gutfl
-
- # M3 Thiele / M3 Buntkowsky / M3 Gutfleisch → empty in Excel → None
- for cname in ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"]:
- right_data[cname]['stand_prev_share'] = None
-
- # --- Rückführung flüssig per month (raw sums) ---
- # top_right row_index=2 is "Rückführung flüssig (Lit. L-He)"
-
- # --- Sonderrückführungen (row_index=3 in top_right) ---
- for cname in RIGHT_CLIENTS:
- sonder_total = Decimal('0')
- for (y, m) in window:
- sheet = sheets_by_ym.get((y, m))
- if sheet:
- sonder_total += get_top_right_value(sheet, cname, row_index=3)
- right_data[cname]['sonder'] = sonder_total
-
- # --- Sammelrückführung (row_index=4 in top_right), grouped & merged ---
- # Group 1: Dr. Fohrer + AG Buntk.
- group1_total = Decimal('0')
- for (y, m) in window:
- sheet = sheets_by_ym.get((y, m))
- if sheet:
- group1_total += get_top_right_value(sheet, "Dr. Fohrer", row_index=4)
- right_data["Dr. Fohrer"]['sammel'] = group1_total
- right_data["AG Buntk."]['sammel'] = group1_total
-
- # Group 2: AG Alff + AG Gutfl.
- group2_total = Decimal('0')
- for (y, m) in window:
- sheet = sheets_by_ym.get((y, m))
- if sheet:
- group2_total += get_top_right_value(sheet, "AG Alff", row_index=4)
- right_data["AG Alff"]['sammel'] = group2_total
- right_data["AG Gutfl."]['sammel'] = group2_total
-
- # Group 3: M3 Thiele + M3 Buntkowsky + M3 Gutfleisch
- group3_total = Decimal('0')
- for (y, m) in window:
- sheet = sheets_by_ym.get((y, m))
- if sheet:
- group3_total += get_top_right_value(sheet, "M3 Thiele", row_index=4)
- right_data["M3 Thiele"]['sammel'] = group3_total
- right_data["M3 Buntkowsky"]['sammel'] = group3_total
- right_data["M3 Gutfleisch"]['sammel'] = group3_total
- def safe_div(a: Decimal, b: Decimal) -> Decimal:
- return (a / b) if b != 0 else Decimal("0")
-
- # --- Rückführung flüssig (Lit. L-He) for Halbjahres-Bilanz top-right ---
- # Uses your exact formulas.
-
- # 1) Fohrer / Buntk split by BEZUG share times group SAMMEL (L8)
- L13 = right_data["Dr. Fohrer"]["bezug"]
- M13 = right_data["AG Buntk."]["bezug"]
- L8 = right_data["Dr. Fohrer"]["sammel"] # merged group total
-
- den = (L13 + M13)
- right_data["Dr. Fohrer"]["rueckf_fluessig"] = (safe_div(L13, den) * L8) if den != 0 else Decimal("0")
- right_data["AG Buntk."]["rueckf_fluessig"] = (safe_div(M13, den) * L8) if den != 0 else Decimal("0")
-
- # 2) Alff / Gutfl split by BEZUG share times group SAMMEL (N8)
- N13 = right_data["AG Alff"]["bezug"]
- O13 = right_data["AG Gutfl."]["bezug"]
- N8 = right_data["AG Alff"]["sammel"] # merged group total
-
- den = (N13 + O13)
- right_data["AG Alff"]["rueckf_fluessig"] = (safe_div(N13, den) * N8) if den != 0 else Decimal("0")
- right_data["AG Gutfl."]["rueckf_fluessig"] = (safe_div(O13, den) * N8) if den != 0 else Decimal("0")
-
- # 3) M3 Thiele = sum of monthly Rückführung flüssig (monthly top_right row_index=2) over window
- P6_sum = Decimal("0")
- for (y, m) in window:
- sh = sheets_by_ym.get((y, m))
- P6_sum += get_top_right_value(sh, "M3 Thiele", TR_RUECKF_FLUESSIG_ROW)
- right_data["M3 Thiele"]["rueckf_fluessig"] = P6_sum
-
- # 4) M3 Buntkowsky / M3 Gutfleisch split by BEZUG share times M3-group SAMMEL (P8)
- P13 = right_data["M3 Thiele"]["bezug"]
- Q13 = right_data["M3 Buntkowsky"]["bezug"]
- R13 = right_data["M3 Gutfleisch"]["bezug"]
- P8 = right_data["M3 Thiele"]["sammel"] # merged group total
-
- den = (P13 + Q13 + R13)
- right_data["M3 Buntkowsky"]["rueckf_fluessig"] = (safe_div(Q13, den) * P8) if den != 0 else Decimal("0")
- right_data["M3 Gutfleisch"]["rueckf_fluessig"] = (safe_div(R13, den) * P8) if den != 0 else Decimal("0")
- # --- Bestand in Kannen-1 (Lit. L-He) for right table (grouped) ---
- # Use Gasbestand (J36) and fallback logic, but now reading top_right B9 for each group.
- TOP_RIGHT_ROW_BESTAND_KANNEN = 6 # <-- most likely correct in your setup
-
- def pick_bestand_top_right(base_client: str) -> Decimal:
- # Go from last month in window backwards: if Gasbestand != 0, use that month's Bestand in Kannen
- for (y, m) in reversed(window):
- sh = sheets_by_ym.get((y, m))
- if not sh:
- continue
-
- gasbestand = get_bottom1_value(sh, GASBESTAND_ROW_INDEX, GASBESTAND_COL_NM3)
- if gasbestand != 0:
- return get_top_right_value(sh, base_client, TOP_RIGHT_ROW_BESTAND_KANNEN)
-
- # Fallback to previous month (Übertrag_Dez__Vorjahr equivalent)
- return get_top_right_value(prev_sheet, base_client, TOP_RIGHT_ROW_BESTAND_KANNEN)
-
- # Group 1 merged (Fohrer + Buntk.)
- g1_best = pick_bestand_top_right("Dr. Fohrer")
- right_data["Dr. Fohrer"]["bestand_kannen"] = g1_best
- right_data["AG Buntk."]["bestand_kannen"] = g1_best
-
- # Group 2 merged (Alff + Gutfl.)
- g2_best = pick_bestand_top_right("AG Alff")
- right_data["AG Alff"]["bestand_kannen"] = g2_best
- right_data["AG Gutfl."]["bestand_kannen"] = g2_best
-
- # Group 3 merged (M3 Thiele + M3 Buntkowsky + M3 Gutfleisch)
- g3_best = pick_bestand_top_right("M3 Thiele")
- right_data["M3 Thiele"]["bestand_kannen"] = g3_best
- right_data["M3 Buntkowsky"]["bestand_kannen"] = g3_best
- right_data["M3 Gutfleisch"]["bestand_kannen"] = g3_best
-
- # Summe Bestand = same as previous row
- for cname in RIGHT_CLIENTS:
- right_data[cname]['summe_bestand'] = right_data[cname]['bestand_kannen']
-
- # Best. in Kannen Vormonat (Lit. L-He) from previous month top_right row_index=7
- g1_prev = get_top_right_value(prev_sheet, "Dr. Fohrer", TOP_RIGHT_ROW_BESTAND_KANNEN)
- right_data["Dr. Fohrer"]['best_kannen_vormonat'] = g1_prev
- right_data["AG Buntk."]['best_kannen_vormonat'] = g1_prev
-
- g2_prev = get_top_right_value(prev_sheet, "AG Alff", TOP_RIGHT_ROW_BESTAND_KANNEN)
- right_data["AG Alff"]['best_kannen_vormonat'] = g2_prev
- right_data["AG Gutfl."]['best_kannen_vormonat'] = g2_prev
-
-
- # Group 1 merged (Fohrer + Buntk.)
- g1_prev = get_top_right_value(prev_sheet, "Dr. Fohrer", TOP_RIGHT_ROW_BESTAND_KANNEN)
- right_data["Dr. Fohrer"]["best_kannen_vormonat"] = g1_prev
- right_data["AG Buntk."]["best_kannen_vormonat"] = g1_prev
-
- # Group 2 merged (Alff + Gutfl.)
- g2_prev = get_top_right_value(prev_sheet, "AG Alff", TOP_RIGHT_ROW_BESTAND_KANNEN)
- right_data["AG Alff"]["best_kannen_vormonat"] = g2_prev
- right_data["AG Gutfl."]["best_kannen_vormonat"] = g2_prev
-
- # Group 3 UNMERGED (each one reads its own cell)
- right_data["M3 Thiele"]["best_kannen_vormonat"] = get_top_right_value(prev_sheet, "M3 Thiele", TOP_RIGHT_ROW_BESTAND_KANNEN)
- right_data["M3 Buntkowsky"]["best_kannen_vormonat"] = get_top_right_value(prev_sheet, "M3 Buntkowsky", TOP_RIGHT_ROW_BESTAND_KANNEN)
- right_data["M3 Gutfleisch"]["best_kannen_vormonat"] = get_top_right_value(prev_sheet, "M3 Gutfleisch", TOP_RIGHT_ROW_BESTAND_KANNEN)
-
- # --- Rückführ. Soll (Lit. L-He) according to your formulas ---
-
- # Group 1: Dr. Fohrer / AG Buntk.
- total_bestand_1 = right_data["Dr. Fohrer"]['summe_bestand']
- best_vormonat_1 = right_data["Dr. Fohrer"]['best_kannen_vormonat']
- diff1 = total_bestand_1 - best_vormonat_1
- share_fohrer = right_data["Dr. Fohrer"]['stand_prev_share'] or Decimal('0')
-
- right_data["Dr. Fohrer"]['rueckf_soll'] = (
- right_data["Dr. Fohrer"]['bezug'] - diff1 * share_fohrer
- )
- right_data["AG Buntk."]['rueckf_soll'] = (
- right_data["AG Buntk."]['bezug'] - total_bestand_1 + best_vormonat_1
- )
-
- # Group 2: AG Alff / AG Gutfl.
- total_bestand_2 = right_data["AG Alff"]['summe_bestand']
- best_vormonat_2 = right_data["AG Alff"]['best_kannen_vormonat']
- diff2 = total_bestand_2 - best_vormonat_2
- share_alff = right_data["AG Alff"]['stand_prev_share'] or Decimal('0')
- share_gutfl = right_data["AG Gutfl."]['stand_prev_share'] or Decimal('0')
-
- right_data["AG Alff"]['rueckf_soll'] = (
- right_data["AG Alff"]['bezug'] - diff2 * share_alff
- )
- right_data["AG Gutfl."]['rueckf_soll'] = (
- right_data["AG Gutfl."]['bezug'] - diff2 * share_gutfl
- )
-
- # Group 3: M3 Thiele / M3 Buntkowsky / M3 Gutfleisch
- for cname in ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"]:
- b13 = right_data[cname]['bezug']
- b12 = right_data[cname]['best_kannen_vormonat']
- b11 = right_data[cname]['summe_bestand']
- # Excel: P13+P12-P11 etc.
- right_data[cname]['rueckf_soll'] = b13 + b12 - b11
-
- # --- Verluste (Soll-Rückf.) (Lit. L-He) = B14 - B6 - B7 ---
- for cname in RIGHT_CLIENTS:
- b14 = right_data[cname]['rueckf_soll']
- b6 = right_data[cname]['rueckf_fluessig']
- b7 = right_data[cname]['sonder']
- right_data[cname]['verluste'] = b14 - b6 - b7
-
- # --- Füllungen warm (Lit. L-He) = sum of monthly 'Füllungen warm' (row_index=11 top_right) ---
- for cname in RIGHT_CLIENTS:
- total_warm = Decimal('0')
- for (y, m) in window:
- sheet = sheets_by_ym.get((y, m))
- if sheet:
- total_warm += get_top_right_value(sheet, cname, row_index=11)
- right_data[cname]['fuellungen_warm'] = total_warm
-
- # --- Kaltgas Rückgabe (Lit. L-He) – Faktor = Bezug * 0.06 ---
- for cname in RIGHT_CLIENTS:
- b13 = right_data[cname]['bezug']
- right_data[cname]['kaltgas_rueckgabe'] = b13 * factor
-
- # --- Verbraucherverluste (Liter L-He) = Verluste - Kaltgas Rückgabe ---
- for cname in RIGHT_CLIENTS:
- b15 = right_data[cname]['verluste']
- b17 = right_data[cname]['kaltgas_rueckgabe']
- right_data[cname]['verbraucherverluste'] = b15 - b17
-
- # --- % = Verbraucherverluste / Bezug ---
- for cname in RIGHT_CLIENTS:
- bezug = right_data[cname]['bezug']
- verb = right_data[cname]['verbraucherverluste']
- if bezug != 0:
- right_data[cname]['percent'] = verb / bezug
- else:
- right_data[cname]['percent'] = None
-
- # Build RIGHT rows structure
- right_row_defs = [
- ('Stand der Gaszähler (Vorjahr) (Nm³)', 'stand_prev_share'),
- # We skip the pure-text "Gasrückführung (Nm³)" line here,
- # because it’s only text (Aufteilung nach Verbrauch / Gaszähler)
- # and easier to render directly in the template if needed.
- ('Rückführung flüssig (Lit. L-He)', 'rueckf_fluessig'),
- ('Sonderrückführungen (Lit. L-He)', 'sonder'),
- ('Sammelrückführung (Lit. L-He)', 'sammel'),
- ('Bestand in Kannen-1 (Lit. L-He)', 'bestand_kannen'),
- ('Summe Bestand (Lit. L-He)', 'summe_bestand'),
- ('Best. in Kannen Vormonat (Lit. L-He)', 'best_kannen_vormonat'),
- ('Bezug (Liter L-He)', 'bezug'),
- ('Rückführ. Soll (Lit. L-He)', 'rueckf_soll'),
- ('Verluste (Soll-Rückf.) (Lit. L-He)', 'verluste'),
- ('Füllungen warm (Lit. L-He)', 'fuellungen_warm'),
- ('Kaltgas Rückgabe (Lit. L-He) – Faktor', 'kaltgas_rueckgabe'),
- ('Verbraucherverluste (Liter L-He)', 'verbraucherverluste'),
- ('%', 'percent'),
- ]
-
- rows_right = []
- for label, key in right_row_defs:
- values = [right_data[cname].get(key) for cname in RIGHT_CLIENTS]
-
- if key == 'percent':
- total_bezug = sum((right_data[c]['bezug'] for c in RIGHT_CLIENTS), Decimal('0'))
- total_verb = sum((right_data[c]['verbraucherverluste'] for c in RIGHT_CLIENTS), Decimal('0'))
- total = (total_verb / total_bezug) if total_bezug != 0 else None
- else:
- total = sum((v for v in values if isinstance(v, Decimal)), Decimal('0'))
-
- rows_right.append({
- 'label': label,
- 'values': values,
- 'total': total,
- 'is_percent': key == 'percent',
- })
- SUM_TABLE_ROWS = [
- ("Rückführung flüssig (Lit. L-He)", "rueckf_fluessig"),
- ("Sonderrückführungen (Lit. L-He)", "sonder"),
- ("Sammelrückführungen (Lit. L-He)", "sammel"),
- ("Bestand in Kannen-1 (Lit. L-He)", "bestand_kannen"),
- ("Summe Bestand (Lit. L-He)", "summe_bestand"),
- ("Best. in Kannen Vormonat (Lit. L-He)", "best_kannen_vormonat"),
- ("Bezug (Liter L-He)", "bezug"),
- ("Rückführ. Soll (Lit. L-He)", "rueckf_soll"),
- ("Verluste (Soll-Rückf.) (Lit. L-He)", "verluste"),
- ("Füllungen warm (Lit. L-He)", "fuellungen_warm"),
- ("Kaltgas Rückgabe (Lit. L-He) – Faktor", "kaltgas_rueckgabe"),
- ("Faktor 0.06", "factor_row"),
- ("Verbraucherverluste (Liter L-He)", "verbraucherverluste"),
- ("%", "percent"),
- ]
-
- RIGHT_GROUPS = {
- "chemie": ["Dr. Fohrer", "AG Buntk."],
- "mawi": ["AG Alff", "AG Gutfl."],
- "m3": ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"],
- }
-
- RIGHT_ALL = ["Dr. Fohrer", "AG Buntk.", "AG Alff", "AG Gutfl.", "M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"]
- LEFT_ALL = HALFYEAR_CLIENTS_LEFT
-
- def safe_pct(verb, bez):
- return (verb / bez) if bez != 0 else None
- rows_sum = []
- def d(x):
- return x if isinstance(x, Decimal) else Decimal("0")
- for label, key in SUM_TABLE_ROWS:
-
- if key == "factor_row":
- lichtwiese = chemie = mawi = m3 = total = Decimal("0.06")
-
- elif key == "percent":
- # Right totals
- rw_bez = sum(d(right_data[c].get("bezug")) for c in RIGHT_ALL)
- rw_verb = sum(d(right_data[c].get("verbraucherverluste")) for c in RIGHT_ALL)
- lichtwiese = safe_pct(rw_verb, rw_bez)
-
- # Chemie
- ch_bez = sum(d(right_data[c].get("bezug")) for c in RIGHT_GROUPS["chemie"])
- ch_verb = sum(d(right_data[c].get("verbraucherverluste")) for c in RIGHT_GROUPS["chemie"])
- chemie = safe_pct(ch_verb, ch_bez)
-
- # MaWi
- mw_bez = sum(d(right_data[c].get("bezug")) for c in RIGHT_GROUPS["mawi"])
- mw_verb = sum(d(right_data[c].get("verbraucherverluste")) for c in RIGHT_GROUPS["mawi"])
- mawi = safe_pct(mw_verb, mw_bez)
-
- # M3
- m3_bez = sum(d(right_data[c].get("bezug")) for c in RIGHT_GROUPS["m3"])
- m3_verb = sum(d(right_data[c].get("verbraucherverluste")) for c in RIGHT_GROUPS["m3"])
- m3 = safe_pct(m3_verb, m3_bez)
-
- # Σ column = (left verb + right verb) / (left bez + right bez)
- left_bez = sum(d(client_data_left[c].get("bezug")) for c in LEFT_ALL)
- left_verb = sum(d(client_data_left[c].get("verbraucherverluste")) for c in LEFT_ALL)
- total = safe_pct(left_verb + rw_verb, left_bez + rw_bez)
-
- else:
- # normal rows = sums
- lichtwiese = sum(d(right_data[c].get(key)) for c in RIGHT_ALL)
- chemie = sum(d(right_data[c].get(key)) for c in RIGHT_GROUPS["chemie"])
- mawi = sum(d(right_data[c].get(key)) for c in RIGHT_GROUPS["mawi"])
- m3 = sum(d(right_data[c].get(key)) for c in RIGHT_GROUPS["m3"])
-
- left_total = sum(d(client_data_left[c].get(key)) for c in LEFT_ALL)
- total = left_total + lichtwiese
-
- rows_sum.append({
- "row_index": row_index,
- "label": label,
- "total": total,
- "lichtwiese": lichtwiese,
- "chemie": chemie,
- "mawi": mawi,
- "m3": m3,
- "is_percent": (key == "percent"),
- })
- def find_sum_row(rows, label_startswith: str):
- for r in rows:
- if str(r.get("label", "")).strip().startswith(label_startswith):
- return r
- return None
-
- summe_bestand_row = find_sum_row(rows_sum, "Summe Bestand")
- k38 = (summe_bestand_row.get("total") if summe_bestand_row else Decimal("0")) or Decimal("0")
- j38 = k38 * Decimal("0.75")
- # --- FIX: now that k38 is known, update bottom2 + recompute dependent rows ---
- bottom2["k38"] = k38
- bottom2["j38"] = j38
-
- k39 = bottom2.get("k39") or Decimal("0")
- k40 = bottom2.get("k40") or Decimal("0")
-
- # Row 43: Bestand flüssig He = SUMME(K38:K40)
- k43 = (k38 or Decimal("0")) + k39 + k40
- j43 = k43 * Decimal("0.75")
-
- bottom2["k43"] = k43
- bottom2["j43"] = j43
-
- # Row 44: Gesamtbestand neu = Gasbestand(Lit) from bottom table 1 + k43
- gasbestand_lit = Decimal("0")
- for r in bottom1_rows:
- if (r.get("label") or "").strip().startswith("Gasbestand"):
- gasbestand_lit = r.get("lhe") or Decimal("0")
- break
-
- k44 = gasbestand_lit + k43
- j44 = k44 * Decimal("0.75")
-
- bottom2["k44"] = k44
- bottom2["j44"] = j44
- def d(x):
- return x if isinstance(x, Decimal) else Decimal("0")
-
- # ---- Bottom2: J38/K38 depend on rows_sum (overall summary), so do it HERE ----
- k38 = Decimal("0")
- for r in rows_sum:
- if r.get("label") == "Summe Bestand (Lit. L-He)":
- k38 = r.get("total") or Decimal("0")
- break
-
- j38 = k38 * FACTOR_NM3_TO_LHE # 0.75
- bottom2["k38"] = k38
- bottom2["j38"] = j38
- # ------------------------------------------------------------------
- # 4) Context – keep old keys AND new ones
- # ------------------------------------------------------------------
- context = {
- 'interval_year': interval_year,
- 'interval_start_month': interval_start,
- 'window': window,
-
- # Left table – old names (for your first template)
- 'clients': HALFYEAR_CLIENTS_LEFT,
- 'rows': rows_left,
-
- # Left table – explicit
- 'clients_left': HALFYEAR_CLIENTS_LEFT,
- 'rows_left': rows_left,
-
- # Right table
- 'clients_right': RIGHT_CLIENTS,
- 'rows_right': rows_right,
- 'rows_sum': rows_sum,
- 'bottom1_rows': bottom1_rows,
-
- }
- context["bottom2"] = bottom2
- context["context_bottom2_g39"] = bottom2_inputs["g39"]
- context["context_bottom2_i39"] = bottom2_inputs["i39"]
- return render(request, 'halfyear_balance.html', context)
-
-def get_bottom2_value(sheet, row_index: int, col_index: int) -> Decimal:
- """Get numeric value from bottom_2 or 0 if missing."""
- if sheet is None:
- return Decimal("0")
- cell = Cell.objects.filter(
- sheet=sheet,
- table_type="bottom_2",
- row_index=row_index,
- column_index=col_index,
- ).first()
- if cell is None or cell.value in (None, ""):
- return Decimal("0")
- try:
- return Decimal(str(cell.value))
- except Exception:
- return Decimal("0")
-
-def get_top_left_value(sheet, client_name: str, row_index: int) -> Decimal:
- """
- Read a numeric value from the top_left table for a given month, client and row.
- Does NOT use column_index, because top_left is keyed only by client + row_index.
- """
- if sheet is None:
- return Decimal('0')
-
- client_obj = Client.objects.filter(name=client_name).first()
- if not client_obj:
- return Decimal('0')
-
- cell = Cell.objects.filter(
- sheet=sheet,
- table_type='top_left',
- client=client_obj,
- row_index=row_index,
- ).first()
-
- if cell is None or cell.value in (None, ''):
- return Decimal('0')
-
- try:
- return Decimal(str(cell.value))
- except Exception:
- return Decimal('0')
-def get_group_clients(group_key):
- """Return queryset of clients that belong to a logical group."""
- from .models import Client # local import to avoid circulars
-
- group = CLIENT_GROUPS.get(group_key)
- if not group:
- return Client.objects.none()
- return Client.objects.filter(name__in=group['names'])
-
-def calculate_summation(sheet, table_type, row_index, sum_column_index):
- """Calculate summation for a row, with special handling for % row"""
- from decimal import Decimal
- from .models import Cell
-
- try:
- # Special case: top_left, % row (Excel B20 -> row_index 19)
- if table_type == 'top_left' and row_index == 19:
- # K13 = sum of row 13 (Excel B13 -> row_index 12) across all clients
- cells_row13 = Cell.objects.filter(
- sheet=sheet,
- table_type='top_left',
- row_index=12, # Excel B13 = row_index 12
- column_index__lt=sum_column_index # Exclude sum column itself
- )
- total_13 = Decimal('0')
- for cell in cells_row13:
- if cell.value is not None:
- total_13 += Decimal(str(cell.value))
-
- # K19 = sum of row 19 (Excel B19 -> row_index 18) across all clients
- cells_row19 = Cell.objects.filter(
- sheet=sheet,
- table_type='top_left',
- row_index=18, # Excel B19 = row_index 18
- column_index__lt=sum_column_index
- )
- total_19 = Decimal('0')
- for cell in cells_row19:
- if cell.value is not None:
- total_19 += Decimal(str(cell.value))
-
- # Calculate: IF(K13=0; 0; K19/K13)
- if total_13 == 0:
- return Decimal('0')
- return total_19 / total_13
-
- # Normal summation for other rows
- cells_in_row = Cell.objects.filter(
- sheet=sheet,
- table_type=table_type,
- row_index=row_index,
- column_index__lt=sum_column_index
- )
-
- total = Decimal('0')
- for cell in cells_in_row:
- if cell.value is not None:
- total += Decimal(str(cell.value))
-
- return total
-
- except Exception as e:
- print(f"Error calculating summation for {table_type}[{row_index}]: {e}")
- return None
-
-# Helper function for calculations
-def evaluate_formula(formula, values_dict):
- """
- Safely evaluate a formula like "10 + 9" where numbers are row indices
- values_dict: {row_index: decimal_value}
- """
- from decimal import Decimal
- import re
-
- try:
- # Create a copy of the formula to work with
- expr = formula
-
- # Find all row numbers in the formula
- row_refs = re.findall(r'\b\d+\b', expr)
-
- for row_ref in row_refs:
- row_num = int(row_ref)
- if row_num in values_dict and values_dict[row_num] is not None:
- # Replace row reference with actual value
- expr = expr.replace(row_ref, str(values_dict[row_num]))
- else:
- # Missing value - can't calculate
- return None
-
- # Evaluate the expression
- # Note: In production, use a safer evaluator like `asteval`
- result = eval(expr, {"__builtins__": {}}, {})
-
- # Convert to Decimal with proper rounding
- return Decimal(str(round(result, 6)))
-
- except Exception:
- return None
-
-# Monthly Sheet View
-class MonthlySheetView(TemplateView):
- template_name = 'monthly_sheet.html'
-
- def populate_helium_input_to_top_right(self, sheet):
- """Populate bezug data from SecondTableEntry to top-right table (row 8 = Excel row 12)"""
- from .models import SecondTableEntry, Cell, Client
- from django.db.models.functions import Coalesce
- from decimal import Decimal
-
- year = sheet.year
- month = sheet.month
-
- TOP_RIGHT_CLIENTS = [
- "Dr. Fohrer", # Column index 0 (L)
- "AG Buntk.", # Column index 1 (M)
- "AG Alff", # Column index 2 (N)
- "AG Gutfl.", # Column index 3 (O)
- "M3 Thiele", # Column index 4 (P)
- "M3 Buntkowsky", # Column index 5 (Q)
- "M3 Gutfleisch", # Column index 6 (R)
- ]
-
- # For each client in top-right table
- for client_name in TOP_RIGHT_CLIENTS:
- try:
- client = Client.objects.get(name=client_name)
- column_index = TOP_RIGHT_CLIENTS.index(client_name)
-
- # Calculate total LHe_output for this client in this month from SecondTableEntry
- total_lhe_output = SecondTableEntry.objects.filter(
- client=client,
- date__year=year,
- date__month=month
- ).aggregate(
- total=Coalesce(Sum('lhe_output'), Decimal('0'))
- )['total']
-
- # Get or create the cell for row_index 8 (Excel row 12) - Bezug
- cell, created = Cell.objects.get_or_create(
- sheet=sheet,
- table_type='top_right',
- client=client,
- row_index=8, # Bezug row (Excel row 12)
- column_index=column_index,
- defaults={'value': total_lhe_output}
- )
-
- if not created and cell.value != total_lhe_output:
- cell.value = total_lhe_output
- cell.save()
-
- except Client.DoesNotExist:
- continue
-
- # After populating bezug, trigger calculation for all dependent cells
- # Get any cell to start the calculation
- first_cell = Cell.objects.filter(
- sheet=sheet,
- table_type='top_right'
- ).first()
-
- if first_cell:
- save_view = SaveCellsView()
- save_view.calculate_top_right_dependents(sheet, first_cell)
-
- return True
- def calculate_bezug_from_entries(self, sheet, year, month):
- """Calculate B11 (Bezug) from SecondTableEntry for all clients - ONLY for non-start sheets"""
- from .models import SecondTableEntry, Cell, Client
- from django.db.models import Sum
- from django.db.models.functions import Coalesce
- from decimal import Decimal
-
- # Check if this is the start sheet
- if year == 2025 and month == 1:
- return # Don't auto-calculate for start sheet
-
- for client in Client.objects.all():
- # Calculate total LHe output for this client in this month
- lhe_output_sum = SecondTableEntry.objects.filter(
- client=client,
- date__year=year,
- date__month=month
- ).aggregate(
- total=Coalesce(Sum('lhe_output'), Decimal('0'))
- )['total']
-
- # Update B11 cell (row_index 8 = UI Row 9)
- b11_cell = Cell.objects.filter(
- sheet=sheet,
- table_type='top_left',
- client=client,
- row_index=8 # Excel B11
- ).first()
-
- if b11_cell and (b11_cell.value != lhe_output_sum or b11_cell.value is None):
- b11_cell.value = lhe_output_sum
- b11_cell.save()
-
- # Also trigger dependent calculations
- from .views import SaveCellsView
- save_view = SaveCellsView()
- save_view.calculate_top_left_dependents(sheet, b11_cell)
- # In MonthlySheetView.get_context_data() method, update the TOP_RIGHT_CLIENTS and row count:
-
-
-
- return True
- def get_context_data(self, **kwargs):
- from decimal import Decimal
- context = super().get_context_data(**kwargs)
- year = self.kwargs.get('year', datetime.now().year)
- month = self.kwargs.get('month', datetime.now().month)
- is_start_sheet = (year == FIRST_SHEET_YEAR and month == FIRST_SHEET_MONTH)
-
- # Get or create the monthly sheet
- sheet, created = MonthlySheet.objects.get_or_create(
- year=year, month=month
- )
-
- # All clients (used for bottom tables etc.)
- clients = Client.objects.all().order_by('name')
-
- # Pre-fill cells if creating new sheet
- if created:
- self.initialize_sheet_cells(sheet, clients)
-
- # Apply previous month links (for B4 and B12)
- self.apply_previous_month_links(sheet, year, month)
- self.calculate_bezug_from_entries(sheet, year, month)
- self.populate_helium_input_to_top_right(sheet)
- self.apply_previous_month_links_top_right(sheet, year, month)
-
- # Define client groups
- TOP_LEFT_CLIENTS = [
- "AG Vogel",
- "AG Halfm",
- "IKP",
- ]
-
- TOP_RIGHT_CLIENTS = [
- "Dr. Fohrer",
- "AG Buntk.",
- "AG Alff",
- "AG Gutfl.",
- "M3 Thiele",
- "M3 Buntkowsky",
- "M3 Gutfleisch",
- ]
- current_summary = MonthlySummary.objects.filter(sheet=sheet).first()
-
- # Get previous month summary (for Bottom Table 3: K46 = prev K44)
- prev_month_info = self.get_prev_month(year, month)
- prev_summary = None
- if not is_start_sheet:
- prev_sheet = MonthlySheet.objects.filter(
- year=prev_month_info['year'],
- month=prev_month_info['month']
- ).first()
- if prev_sheet:
- prev_summary = MonthlySummary.objects.filter(sheet=prev_sheet).first()
-
- context.update({
- # ... your existing context ...
- 'current_summary': current_summary,
- 'prev_summary': prev_summary,
- })
- # Update row counts in build_group_rows function
- # Update row counts in build_group_rows function
- def build_group_rows(sheet, table_type, client_names):
- """Build rows for display in monthly sheet."""
- from decimal import Decimal
- from .models import Cell
- MERGED_ROWS = {2, 3, 5, 6, 7, 9, 10, 12, 14, 15}
- MERGED_PAIRS = [
- ("Dr. Fohrer", "AG Buntk."),
- ("AG Alff", "AG Gutfl."),
- ]
- rows = []
-
- # Determine row count
- row_counts = {
- "top_left": 16,
- "top_right": 16, # rows 0–15
- "bottom_1": 10,
- "bottom_2": 10,
- "bottom_3": 10,
- }
- row_count = row_counts.get(table_type, 0)
-
- # Get all cells for this sheet and table
- all_cells = (
- Cell.objects.filter(
- sheet=sheet,
- table_type=table_type,
- ).select_related("client")
- )
-
- # Group cells by row index
- cells_by_row = {}
- for cell in all_cells:
- if cell.row_index not in cells_by_row:
- cells_by_row[cell.row_index] = {}
- cells_by_row[cell.row_index][cell.client.name] = cell
-
- # We will store row sums for Bezug (row 8) and Verbraucherverluste (row 14)
- # for top_left / top_right so we can compute the overall % in row 15.
- sum_bezug = None
- sum_verbrauch = None
-
- # Build each row
- for row_idx in range(row_count):
- display_cells = []
- row_cells_dict = cells_by_row.get(row_idx, {})
-
- # Build cells in the requested client order
- for name in client_names:
- cell = row_cells_dict.get(name)
- display_cells.append(cell)
-
- # Calculate sum for this row (includes editable + calculated cells)
- sum_value = None
- total = Decimal("0")
- has_value = False
-
- merged_second_indices = set()
- if table_type == 'top_right' and row_idx in MERGED_ROWS:
- for left_name, right_name in MERGED_PAIRS:
- try:
- right_idx = client_names.index(right_name)
- merged_second_indices.add(right_idx)
- except ValueError:
- # client not in this table; just ignore
- pass
-
- for col_idx, cell in enumerate(display_cells):
- # Skip the duplicate (second) column of each merged pair
- if col_idx in merged_second_indices:
- continue
-
- if cell and cell.value is not None:
- try:
- total += Decimal(str(cell.value))
- has_value = True
- except Exception:
- pass
-
- if has_value:
- sum_value = total
-
- # Remember special rows for top tables
- if table_type in ("top_left", "top_right"):
- if row_idx == 8: # Bezug
- sum_bezug = total
- elif row_idx == 14: # Verbraucherverluste
- sum_verbrauch = total
-
- rows.append(
- {
- "cells": display_cells,
- "sum": sum_value,
- "row_index": row_idx,
- }
- )
-
- # Adjust the % row sum for top_left / top_right:
- # Sum(%) = Sum(Verbraucherverluste) / Sum(Bezug)
- if table_type in ("top_left", "top_right"):
- perc_row_idx = 15 # % row
- if 0 <= perc_row_idx < len(rows):
- if sum_bezug is not None and sum_bezug != 0 and sum_verbrauch is not None:
- rows[perc_row_idx]["sum"] = sum_verbrauch / sum_bezug
- else:
- rows[perc_row_idx]["sum"] = None
-
- return rows
-
-
- # Now call the local function
- top_left_rows = build_group_rows(sheet, 'top_left', TOP_LEFT_CLIENTS)
- top_right_rows = build_group_rows(sheet, 'top_right', TOP_RIGHT_CLIENTS)
-
- # --- Build combined summary of top-left + top-right Sum columns ---
-
- # Helper to safely get the Sum for a given row index
- def get_row_sum(rows, row_index):
- if 0 <= row_index < len(rows):
- return rows[row_index].get('sum')
- return None
-
- # Row definitions we want in the combined Σ table
- # (row_index in top tables, label shown in the small table)
- summary_row_defs = [
- (2, "Rückführung flüssig (Lit. L-He)"),
- (3, "Sonderrückführungen (Lit. L-He)"),
- (4, "Sammelrückführungen (Lit. L-He)"),
- (5, "Bestand in Kannen-1 (Lit. L-He)"),
- (6, "Summe Bestand (Lit. L-He)"),
- (7, "Best. in Kannen Vormonat (Lit. L-He)"),
- (8, "Bezug (Liter L-He)"),
- (9, "Rückführ. Soll (Lit. L-He)"),
- (10, "Verluste (Soll-Rückf.) (Lit. L-He)"),
- (11, "Füllungen warm (Lit. L-He)"),
- (12, "Kaltgas Rückgabe (Lit. L-He) – Faktor"),
- (13, "Faktor 0.06"),
- (14, "Verbraucherverluste (Liter L-He)"),
- (15, "%"),
- ]
-
- # Precompute totals for Bezug and Verbraucherverluste across both tables
- bezug_left = get_row_sum(top_left_rows, 8) or Decimal('0')
- bezug_right = get_row_sum(top_right_rows, 8) or Decimal('0')
- total_bezug = bezug_left + bezug_right
-
- verb_left = get_row_sum(top_left_rows, 14) or Decimal('0')
- verb_right = get_row_sum(top_right_rows, 14) or Decimal('0')
- total_verbrauch = verb_left + verb_right
-
- summary_rows = []
-
- for row_index, label in summary_row_defs:
- # Faktor row: always fixed 0.06
- if row_index == 13:
- summary_value = Decimal('0.06')
-
- # % row: total Verbraucherverluste / total Bezug
- elif row_index == 15:
- if total_bezug != 0:
- summary_value = total_verbrauch / total_bezug
- else:
- summary_value = None
-
- else:
- left_sum = get_row_sum(top_left_rows, row_index)
- right_sum = get_row_sum(top_right_rows, row_index)
-
- # Sammelrückführungen: only from top-right table
- if row_index == 4:
- left_sum = None
-
- total = Decimal('0')
-
- has_any = False
- if left_sum is not None:
- total += Decimal(str(left_sum))
- has_any = True
- if right_sum is not None:
- total += Decimal(str(right_sum))
- has_any = True
-
- summary_value = total if has_any else None
-
- summary_rows.append({
- 'row_index': row_index,
- 'label': label,
- 'sum': summary_value,
- })
-
- # Get cells for bottom tables
- cells_by_table = self.get_cells_by_table(sheet)
-
- context.update({
- 'sheet': sheet,
- 'clients': clients,
- 'year': year,
- 'month': month,
- 'month_name': calendar.month_name[month],
- 'prev_month': self.get_prev_month(year, month),
- 'next_month': self.get_next_month(year, month),
- 'cells_by_table': cells_by_table,
- 'top_left_headers': TOP_LEFT_CLIENTS + ['Sum'],
- 'top_right_headers': TOP_RIGHT_CLIENTS + ['Sum'],
- 'top_left_rows': top_left_rows,
- 'top_right_rows': top_right_rows,
- 'summary_rows': summary_rows, # 👈 NEW
- 'is_start_sheet': is_start_sheet,
- })
- return context
-
- def get_cells_by_table(self, sheet):
- """Organize cells by table type for easy template rendering"""
- cells = sheet.cells.select_related('client').all()
- organized = {
- 'top_left': [[] for _ in range(16)],
- 'top_right': [[] for _ in range(16)], # now 16 rows
- 'bottom_1': [[] for _ in range(10)],
- 'bottom_2': [[] for _ in range(10)],
- 'bottom_3': [[] for _ in range(10)],
- }
-
- for cell in cells:
- if cell.table_type not in organized:
- continue
-
- max_rows = len(organized[cell.table_type])
- if cell.row_index < 0 or cell.row_index >= max_rows:
- # This is an "extra" cell from an older layout (e.g. top_left rows 18–23)
- continue
-
- row_list = organized[cell.table_type][cell.row_index]
- while len(row_list) <= cell.column_index:
- row_list.append(None)
-
- row_list[cell.column_index] = cell
-
- return organized
-
-
- def initialize_sheet_cells(self, sheet, clients):
- """Create all empty cells for a new monthly sheet"""
- cells_to_create = []
- summation_config = CALCULATION_CONFIG.get('summation_column', {})
- sum_column_index = summation_config.get('sum_column_index', len(clients) - 1) # Last column
-
- # For each table type and row
- table_configs = [
- ('top_left', 16),
- ('top_right', 16),
- ('bottom_1', 10),
- ('bottom_2', 10),
- ('bottom_3', 10),
- ]
-
- for table_type, row_count in table_configs:
- for row_idx in range(row_count):
- for col_idx, client in enumerate(clients):
- is_summation = (col_idx == sum_column_index)
- cells_to_create.append(Cell(
- sheet=sheet,
- client=client,
- table_type=table_type,
- row_index=row_idx,
- column_index=col_idx,
- value=None,
- is_formula=is_summation, # Mark summation cells as formulas
- ))
-
- # Bulk create all cells at once
- Cell.objects.bulk_create(cells_to_create)
- def get_prev_month(self, year, month):
- """Get previous month year and month"""
- if month == 1:
- return {'year': year - 1, 'month': 12}
- return {'year': year, 'month': month - 1}
-
- def get_next_month(self, year, month):
- """Get next month year and month"""
- if month == 12:
- return {'year': year + 1, 'month': 1}
- return {'year': year, 'month': month + 1}
- def apply_previous_month_links(self, sheet, year, month):
- """
- For non-start sheets:
- B4 (row 2) = previous sheet B3 (row 1)
- B10 (row 8) = previous sheet B9 (row 7)
- """
- # Do nothing on the first sheet
- if year == FIRST_SHEET_YEAR and month == FIRST_SHEET_MONTH:
- return
-
- # Figure out previous month
- if month == 1:
- prev_year = year - 1
- prev_month = 12
- else:
- prev_year = year
- prev_month = month - 1
-
- from .models import MonthlySheet, Cell, Client
-
- prev_sheet = MonthlySheet.objects.filter(
- year=prev_year,
- month=prev_month
- ).first()
-
- if not prev_sheet:
- # No previous sheet created yet → nothing to copy
- return
-
- # For each client, copy values
- for client in Client.objects.all():
- # B3(prev) → B4(curr): UI row 1 → row 2 → row_index 0 → 1
- prev_b3 = Cell.objects.filter(
- sheet=prev_sheet,
- table_type='top_left',
- client=client,
- row_index=0, # UI row 1
- ).first()
-
- curr_b4 = Cell.objects.filter(
- sheet=sheet,
- table_type='top_left',
- client=client,
- row_index=1, # UI row 2
- ).first()
-
- if prev_b3 and curr_b4:
- curr_b4.value = prev_b3.value
- curr_b4.save()
-
- # B9(prev) → B10(curr): UI row 7 → row 8 → row_index 6 → 7
- prev_b9 = Cell.objects.filter(
- sheet=prev_sheet,
- table_type='top_left',
- client=client,
- row_index=6, # UI row 7 (Summe Bestand)
- ).first()
-
- curr_b10 = Cell.objects.filter(
- sheet=sheet,
- table_type='top_left',
- client=client,
- row_index=7, # UI row 8 (Best. in Kannen Vormonat)
- ).first()
-
- if prev_b9 and curr_b10:
- curr_b10.value = prev_b9.value
- curr_b10.save()
-
- def apply_previous_month_links_top_right(self, sheet, year, month):
- """
- top_right row 7: Best. in Kannen Vormonat (Lit. L-He)
- = previous sheet's Summe Bestand (row 6).
-
- For merged pairs:
- - Dr. Fohrer + AG Buntk. share the SAME value (from previous month's AG Buntk. or Dr. Fohrer)
- - AG Alff + AG Gutfl. share the SAME value (from previous month's AG Alff or AG Gutfl.)
- M3 clients just copy their own value.
- """
- from .models import MonthlySheet, Cell, Client
- from decimal import Decimal
-
- # Do nothing on first sheet
- if year == FIRST_SHEET_YEAR and month == FIRST_SHEET_MONTH:
- return
-
- # find previous month
- if month == 1:
- prev_year = year - 1
- prev_month = 12
- else:
- prev_year = year
- prev_month = month - 1
-
- prev_sheet = MonthlySheet.objects.filter(
- year=prev_year,
- month=prev_month
- ).first()
-
- if not prev_sheet:
- return # nothing to copy from
-
- TOP_RIGHT_CLIENTS = [
- "Dr. Fohrer",
- "AG Buntk.",
- "AG Alff",
- "AG Gutfl.",
- "M3 Thiele",
- "M3 Buntkowsky",
- "M3 Gutfleisch",
- ]
-
- # Helper function to get a cell value
- def get_cell_value(sheet_obj, client_name, row_index):
- client_obj = Client.objects.filter(name=client_name).first()
- if not client_obj:
- return None
-
- try:
- col_idx = TOP_RIGHT_CLIENTS.index(client_name)
- except ValueError:
- return None
-
- cell = Cell.objects.filter(
- sheet=sheet_obj,
- table_type='top_right',
- client=client_obj,
- row_index=row_index,
- column_index=col_idx,
- ).first()
-
- return cell.value if cell else None
-
- # Helper function to set a cell value
- def set_cell_value(sheet_obj, client_name, row_index, value):
- client_obj = Client.objects.filter(name=client_name).first()
- if not client_obj:
- return False
-
- try:
- col_idx = TOP_RIGHT_CLIENTS.index(client_name)
- except ValueError:
- return False
-
- cell, created = Cell.objects.get_or_create(
- sheet=sheet_obj,
- table_type='top_right',
- client=client_obj,
- row_index=row_index,
- column_index=col_idx,
- defaults={'value': value}
- )
-
- if not created and cell.value != value:
- cell.value = value
- cell.save()
- elif created:
- cell.save()
-
- return True
-
- # ----- Pair 1: Dr. Fohrer + AG Buntk. -----
- # Get previous month's Summe Bestand (row 6) for either client in the pair
- pair1_prev_val = None
-
- # Try AG Buntk. first
- prev_buntk_val = get_cell_value(prev_sheet, "AG Buntk.", 6)
- if prev_buntk_val is not None:
- pair1_prev_val = prev_buntk_val
- else:
- # Try Dr. Fohrer if AG Buntk. is empty
- prev_fohrer_val = get_cell_value(prev_sheet, "Dr. Fohrer", 6)
- if prev_fohrer_val is not None:
- pair1_prev_val = prev_fohrer_val
-
- # Apply the value to both clients in the pair
- if pair1_prev_val is not None:
- set_cell_value(sheet, "Dr. Fohrer", 7, pair1_prev_val)
- set_cell_value(sheet, "AG Buntk.", 7, pair1_prev_val)
-
- # ----- Pair 2: AG Alff + AG Gutfl. -----
- pair2_prev_val = None
-
- # Try AG Alff first
- prev_alff_val = get_cell_value(prev_sheet, "AG Alff", 6)
- if prev_alff_val is not None:
- pair2_prev_val = prev_alff_val
- else:
- # Try AG Gutfl. if AG Alff is empty
- prev_gutfl_val = get_cell_value(prev_sheet, "AG Gutfl.", 6)
- if prev_gutfl_val is not None:
- pair2_prev_val = prev_gutfl_val
-
- # Apply the value to both clients in the pair
- if pair2_prev_val is not None:
- set_cell_value(sheet, "AG Alff", 7, pair2_prev_val)
- set_cell_value(sheet, "AG Gutfl.", 7, pair2_prev_val)
-
- # ----- M3 clients: copy their own Summe Bestand -----
- for name in ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"]:
- prev_val = get_cell_value(prev_sheet, name, 6)
- if prev_val is not None:
- set_cell_value(sheet, name, 7, prev_val)
-# Add this helper function to views.py
-def get_factor_value(table_type, row_index):
- """Get factor value (like 0.06 for top_left row 17)"""
- factors = {
- ('top_left', 17): Decimal('0.06'), # A18 in Excel (UI row 17, 0-based index 16)
- }
- return factors.get((table_type, row_index), Decimal('0'))
-# Save Cells View
-# views.py - Updated SaveCellsView
-# views.py - Update SaveCellsView class
-def debug_cell_values(self, sheet, client_id):
- """Debug method to check cell values"""
- from .models import Cell
- cells = Cell.objects.filter(
- sheet=sheet,
- table_type='top_left',
- client_id=client_id
- ).order_by('row_index')
-
- debug_info = {}
- for cell in cells:
- debug_info[f"row_{cell.row_index}"] = {
- 'value': str(cell.value) if cell.value else 'None',
- 'ui_row': cell.row_index + 1,
- 'excel_ref': f"B{cell.row_index + 3}"
- }
-
- return debug_info
-class DebugCalculationView(View):
- """Debug view to test calculations directly"""
- def get(self, request):
- sheet_id = request.GET.get('sheet_id', 1)
- client_name = request.GET.get('client', 'AG Vogel')
-
- try:
- sheet = MonthlySheet.objects.get(id=sheet_id)
- client = Client.objects.get(name=client_name)
-
- # Get SaveCellsView instance
- save_view = SaveCellsView()
-
- # Create a dummy cell to trigger calculations
- dummy_cell = Cell.objects.filter(
- sheet=sheet,
- table_type='top_left',
- client=client,
- row_index=0 # B3
- ).first()
-
- if not dummy_cell:
- return JsonResponse({'error': 'No cells found for this client'})
-
- # Trigger calculation
- updated = save_view.calculate_top_left_dependents(sheet, dummy_cell)
-
- # Get updated cell values
- cells = Cell.objects.filter(
- sheet=sheet,
- table_type='top_left',
- client=client
- ).order_by('row_index')
-
- cell_data = []
- for cell in cells:
- cell_data.append({
- 'row_index': cell.row_index,
- 'ui_row': cell.row_index + 1,
- 'excel_ref': f"B{cell.row_index + 3}",
- 'value': str(cell.value) if cell.value else 'None',
- 'description': self.get_row_description(cell.row_index)
- })
-
- return JsonResponse({
- 'sheet': f"{sheet.year}-{sheet.month:02d}",
- 'client': client.name,
- 'cells': cell_data,
- 'updated_count': len(updated),
- 'calculation': 'B5 = IF(B3>0; B3-B4; 0)'
- })
-
- except Exception as e:
- return JsonResponse({'error': str(e)}, status=400)
-
- def get_row_description(self, row_index):
- """Get description for row index"""
- descriptions = {
- 0: "B3: Stand der Gaszähler (Nm³)",
- 1: "B4: Stand der Gaszähler (Vormonat) (Nm³)",
- 2: "B5: Gasrückführung (Nm³)",
- 3: "B6: Rückführung flüssig (Lit. L-He)",
- 4: "B7: Sonderrückführungen (Lit. L-He)",
- 5: "B8: Bestand in Kannen-1 (Lit. L-He)",
- 6: "B9: Summe Bestand (Lit. L-He)",
- 7: "B10: Best. in Kannen Vormonat (Lit. L-He)",
- 8: "B11: Bezug (Liter L-He)",
- 9: "B12: Rückführ. Soll (Lit. L-He)",
- 10: "B13: Verluste (Soll-Rückf.) (Lit. L-He)",
- 11: "B14: Füllungen warm (Lit. L-He)",
- 12: "B15: Kaltgas Rückgabe (Lit. L-He) – Faktor",
- 13: "B16: Faktor",
- 14: "B17: Verbraucherverluste (Liter L-He)",
- 15: "B18: %"
- }
- return descriptions.get(row_index, f"Row {row_index}")
-def recalculate_stand_der_gaszahler(self, sheet):
- """Recalculate Stand der Gaszähler for all client pairs"""
- from decimal import Decimal
-
- # For Dr. Fohrer and AG Buntk. (L & M columns)
- try:
- # Get Dr. Fohrer's bezug
- dr_fohrer_client = Client.objects.get(name="Dr. Fohrer")
- dr_fohrer_cell = Cell.objects.filter(
- sheet=sheet,
- table_type='top_right',
- client=dr_fohrer_client,
- row_index=9 # Row 9 = Bezug
- ).first()
- L13 = Decimal(str(dr_fohrer_cell.value)) if dr_fohrer_cell and dr_fohrer_cell.value else Decimal('0')
-
- # Get AG Buntk.'s bezug
- ag_buntk_client = Client.objects.get(name="AG Buntk.")
- ag_buntk_cell = Cell.objects.filter(
- sheet=sheet,
- table_type='top_right',
- client=ag_buntk_client,
- row_index=9
- ).first()
- M13 = Decimal(str(ag_buntk_cell.value)) if ag_buntk_cell and ag_buntk_cell.value else Decimal('0')
-
- total = L13 + M13
- if total > 0:
- # Update Dr. Fohrer's row 0
- dr_fohrer_row0 = Cell.objects.filter(
- sheet=sheet,
- table_type='top_right',
- client=dr_fohrer_client,
- row_index=0
- ).first()
- if dr_fohrer_row0:
- dr_fohrer_row0.value = L13 / total
- dr_fohrer_row0.save()
-
- # Update AG Buntk.'s row 0
- ag_buntk_row0 = Cell.objects.filter(
- sheet=sheet,
- table_type='top_right',
- client=ag_buntk_client,
- row_index=0
- ).first()
- if ag_buntk_row0:
- ag_buntk_row0.value = M13 / total
- ag_buntk_row0.save()
- except Exception as e:
- print(f"Error recalculating Stand der Gaszähler for Dr. Fohrer/AG Buntk.: {e}")
-
- # For AG Alff and AG Gutfl. (N & O columns)
- try:
- # Get AG Alff's bezug
- ag_alff_client = Client.objects.get(name="AG Alff")
- ag_alff_cell = Cell.objects.filter(
- sheet=sheet,
- table_type='top_right',
- client=ag_alff_client,
- row_index=9
- ).first()
- N13 = Decimal(str(ag_alff_cell.value)) if ag_alff_cell and ag_alff_cell.value else Decimal('0')
-
- # Get AG Gutfl.'s bezug
- ag_gutfl_client = Client.objects.get(name="AG Gutfl.")
- ag_gutfl_cell = Cell.objects.filter(
- sheet=sheet,
- table_type='top_right',
- client=ag_gutfl_client,
- row_index=9
- ).first()
- O13 = Decimal(str(ag_gutfl_cell.value)) if ag_gutfl_cell and ag_gutfl_cell.value else Decimal('0')
-
- total = N13 + O13
- if total > 0:
- # Update AG Alff's row 0
- ag_alff_row0 = Cell.objects.filter(
- sheet=sheet,
- table_type='top_right',
- client=ag_alff_client,
- row_index=0
- ).first()
- if ag_alff_row0:
- ag_alff_row0.value = N13 / total
- ag_alff_row0.save()
-
- # Update AG Gutfl.'s row 0
- ag_gutfl_row0 = Cell.objects.filter(
- sheet=sheet,
- table_type='top_right',
- client=ag_gutfl_client,
- row_index=0
- ).first()
- if ag_gutfl_row0:
- ag_gutfl_row0.value = O13 / total
- ag_gutfl_row0.save()
- except Exception as e:
- print(f"Error recalculating Stand der Gaszähler for AG Alff/AG Gutfl.: {e}")
-# In your SaveCellsView class in views.py
-class DebugTopRightView(View):
- """Debug view to check top_right calculations"""
- def get(self, request):
- sheet_id = request.GET.get('sheet_id', 1)
- client_name = request.GET.get('client', 'Dr. Fohrer')
-
- try:
- sheet = MonthlySheet.objects.get(id=sheet_id)
- client = Client.objects.get(name=client_name)
-
- # Get all cells for this client in top_right
- cells = Cell.objects.filter(
- sheet=sheet,
- table_type='top_right',
- client=client
- ).order_by('row_index')
-
- cell_data = []
- descriptions = {
- 0: "Stand der Gaszähler (Vormonat)",
- 1: "Gasrückführung (Nm³)",
- 2: "Rückführung flüssig",
- 3: "Sonderrückführungen",
- 4: "Sammelrückführungen",
- 5: "Bestand in Kannen-1",
- 6: "Summe Bestand",
- 7: "Best. in Kannen Vormonat",
- 8: "Same as row 9 from prev sheet",
- 9: "Bezug",
- 10: "Rückführ. Soll",
- 11: "Verluste",
- 12: "Füllungen warm",
- 13: "Kaltgas Rückgabe",
- 14: "Faktor 0.06",
- 15: "Verbraucherverluste",
- 16: "%"
- }
-
- for cell in cells:
- cell_data.append({
- 'row_index': cell.row_index,
- 'ui_row': cell.row_index + 1,
- 'description': descriptions.get(cell.row_index, f"Row {cell.row_index}"),
- 'value': str(cell.value) if cell.value else 'Empty',
- 'cell_id': cell.id
- })
-
- # Test calculation
- row3_cell = cells.filter(row_index=3).first()
- row5_cell = cells.filter(row_index=5).first()
- row6_cell = cells.filter(row_index=6).first()
-
- calculation_info = {
- 'row3_value': str(row3_cell.value) if row3_cell and row3_cell.value else '0',
- 'row5_value': str(row5_cell.value) if row5_cell and row5_cell.value else '0',
- 'row6_value': str(row6_cell.value) if row6_cell and row6_cell.value else '0',
- 'expected_sum': '0'
- }
-
- if row3_cell and row5_cell and row6_cell:
- row3_val = Decimal(str(row3_cell.value)) if row3_cell.value else Decimal('0')
- row5_val = Decimal(str(row5_cell.value)) if row5_cell.value else Decimal('0')
- expected = row3_val + row5_val
- calculation_info['expected_sum'] = str(expected)
- calculation_info['is_correct'] = row6_cell.value == expected
-
- return JsonResponse({
- 'sheet': f"{sheet.year}-{sheet.month}",
- 'client': client.name,
- 'cells': cell_data,
- 'calculation': calculation_info
- })
-
- except Exception as e:
- return JsonResponse({'error': str(e)}, status=400)
-
-
-class SaveCellsView(View):
-
- def calculate_bottom_3_dependents(self, sheet):
- updated_cells = []
-
- def get_cell(row_idx, col_idx):
- return Cell.objects.filter(
- sheet=sheet,
- table_type='bottom_3',
- row_index=row_idx,
- column_index=col_idx
- ).first()
-
- def set_cell(row_idx, col_idx, value):
- cell, created = Cell.objects.get_or_create(
- sheet=sheet,
- table_type='bottom_3',
- row_index=row_idx,
- column_index=col_idx,
- defaults={'value': value}
- )
- if not created and cell.value != value:
- cell.value = value
- cell.save()
- elif created:
- cell.save()
-
- updated_cells.append({
- 'id': cell.id,
- 'value': str(cell.value) if cell.value is not None else '',
- 'is_calculated': True,
- })
-
- def dec(x):
- if x in (None, ''):
- return Decimal('0')
- try:
- return Decimal(str(x))
- except Exception:
- return Decimal('0')
-
- # ---- current summary ----
- cur_sum = MonthlySummary.objects.filter(sheet=sheet).first()
- curr_k44 = dec(cur_sum.gesamtbestand_neu_lhe) if cur_sum else Decimal('0')
- total_verb = dec(cur_sum.verbraucherverlust_lhe) if cur_sum else Decimal('0')
-
- # ---- previous month summary ----
- year, month = sheet.year, sheet.month
- if month == 1:
- prev_year, prev_month = year - 1, 12
- else:
- prev_year, prev_month = year, month - 1
-
- prev_sheet = MonthlySheet.objects.filter(year=prev_year, month=prev_month).first()
- if prev_sheet:
- prev_sum = MonthlySummary.objects.filter(sheet=prev_sheet).first()
- prev_k44 = dec(prev_sum.gesamtbestand_neu_lhe) if prev_sum else Decimal('0')
- else:
- prev_k44 = Decimal('0')
-
- # ---- read editable inputs from bottom_3: F47,G47,I47,I50 ----
- def get_val(r, c):
- cell = get_cell(r, c)
- return dec(cell.value if cell else None)
-
- f47 = get_val(1, 0)
- g47 = get_val(1, 1)
- i47 = get_val(1, 2)
- i50 = get_val(4, 2)
-
- # Now apply your formulas using prev_k44, curr_k44, total_verb, f47,g47,i47,i50
- # Row indices: 0..7 correspond to 46..53
- # col 3 = J, col 4 = K
-
- # Row 46
- k46 = prev_k44
- j46 = k46 * Decimal('0.75')
- set_cell(0, 3, j46)
- set_cell(0, 4, k46)
-
- # Row 47
- g47 = self._dec((get_cell(1, 1) or {}).value if get_cell(1, 1) else None)
- i47 = self._dec((get_cell(1, 2) or {}).value if get_cell(1, 2) else None)
-
- j47 = g47 + i47
- k47 = (j47 / Decimal('0.75')) + g47 if j47 != 0 else g47
-
- set_cell(1, 3, j47)
- set_cell(1, 4, k47)
-
- # Row 48
- k48 = k46 + k47
- j48 = k48 * Decimal('0.75')
- set_cell(2, 3, j48)
- set_cell(2, 4, k48)
-
- # Row 49
- k49 = curr_k44
- j49 = k49 * Decimal('0.75')
- set_cell(3, 3, j49)
- set_cell(3, 4, k49)
-
- # Row 50
- j50 = i50
- k50 = j50 / Decimal('0.75') if j50 != 0 else Decimal('0')
- set_cell(4, 3, j50)
- set_cell(4, 4, k50)
-
- # Row 51
- k51 = k48 - k49 - k50
- j51 = k51 * Decimal('0.75')
- set_cell(5, 3, j51)
- set_cell(5, 4, k51)
-
- # Row 52
- k52 = total_verb
- j52 = k52 * Decimal('0.75')
- set_cell(6, 3, j52)
- set_cell(6, 4, k52)
-
- # Row 53
- j53 = j51 - j52
- k53 = k51 - k52
- set_cell(7, 3, j53)
- set_cell(7, 4, k53)
-
- return updated_cells
-
-
- def _dec(self, value):
- """Convert value to Decimal or return 0."""
- if value is None or value == '':
- return Decimal('0')
- try:
- return Decimal(str(value))
- except Exception:
- return Decimal('0')
-
- def post(self, request, *args, **kwargs):
- """
- Handle AJAX saves from monthly_sheet.html
- - Single-cell save: when cell_id is present (blur on one cell)
- - Bulk save: when the 'Save All Cells' button is used (no cell_id)
- """
- try:
- sheet_id = request.POST.get('sheet_id')
- if not sheet_id:
- return JsonResponse({
- 'status': 'error',
- 'message': 'Missing sheet_id'
- })
-
- sheet = MonthlySheet.objects.get(id=sheet_id)
-
- # -------- Single-cell update (blur) --------
- cell_id = request.POST.get('cell_id')
- if cell_id:
- value_raw = (request.POST.get('value') or '').strip()
-
- try:
- cell = Cell.objects.get(id=cell_id, sheet=sheet)
- except Cell.DoesNotExist:
- return JsonResponse({
- 'status': 'error',
- 'message': 'Cell not found'
- })
-
- # Convert value to Decimal or None
- if value_raw == '':
- new_value = None
- else:
- try:
- # Allow comma or dot
- value_clean = value_raw.replace(',', '.')
- new_value = Decimal(value_clean)
- except (InvalidOperation, ValueError):
- # If conversion fails, treat as empty
- new_value = None
-
- old_value = cell.value
- cell.value = new_value
- cell.save()
-
- updated_cells = [{
- 'id': cell.id,
- 'value': '' if cell.value is None else str(cell.value),
- 'is_calculated': cell.is_formula, # model field
- }]
-
- # Recalculate dependents depending on table_type
- if cell.table_type == 'top_left':
- updated_cells.extend(
- self.calculate_top_left_dependents(sheet, cell)
- )
- elif cell.table_type == 'top_right':
- updated_cells.extend(
- self.calculate_top_right_dependents(sheet, cell)
- )
- elif cell.table_type == 'bottom_1':
- updated_cells.extend(self.calculate_bottom_1_dependents(sheet, cell))
- elif cell.table_type == 'bottom_3':
- updated_cells.extend(
- self.calculate_bottom_3_dependents(sheet)
- )
- # bottom_1 / bottom_2 / bottom_3 currently have no formulas:
- # they just save the new value.
- updated_cells += self.calculate_bottom_3_dependents(sheet)
- return JsonResponse({
- 'status': 'success',
- 'updated_cells': updated_cells
- })
-
-
- # -------- Bulk save (Save All button) --------
- return self.save_bulk_cells(request, sheet)
-
- except MonthlySheet.DoesNotExist:
- return JsonResponse({
- 'status': 'error',
- 'message': 'Sheet not found'
- })
- except Exception as e:
- # Generic safety net so the frontend sees an error message
- return JsonResponse({
- 'status': 'error',
- 'message': str(e)
- })
-
- # ... your other methods above ...
-
- # Update the calculate_top_right_dependents method in SaveCellsView class
-
- def calculate_top_right_dependents(self, sheet, changed_cell):
- """
- Recalculate all dependent cells in the top-right table according to Excel formulas.
-
- Excel rows (4-20) -> 0-based indices (0-15)
- Rows:
- 0: Stand der Gaszähler (Vormonat) (Nm³) - shares for M3 clients
- 1: Gasrückführung (Nm³)
- 2: Rückführung flüssig (Lit. L-He)
- 3: Sonderrückführungen (Lit. L-He) - editable
- 4: Sammelrückführungen (Lit. L-He) - from helium_input groups
- 5: Bestand in Kannen-1 (Lit. L-He) - editable, merged in pairs
- 6: Summe Bestand (Lit. L-He) = row 5
- 7: Best. in Kannen Vormonat (Lit. L-He) - from previous month
- 8: Bezug (Liter L-He) - from SecondTableEntry
- 9: Rückführ. Soll (Lit. L-He) - calculated
- 10: Verluste (Soll-Rückf.) (Lit. L-He) - calculated
- 11: Füllungen warm (Lit. L-He) - from SecondTableEntry warm outputs
- 12: Kaltgas Rückgabe (Lit. L-He) – Faktor - calculated
- 13: Faktor 0.06 - fixed
- 14: Verbraucherverluste (Liter L-He) - calculated
- 15: % - calculated
- """
- from decimal import Decimal
- from django.db.models import Sum, Count
- from django.db.models.functions import Coalesce
- from .models import Client, Cell, ExcelEntry, SecondTableEntry
-
- TOP_RIGHT_CLIENTS = [
- "Dr. Fohrer", # L
- "AG Buntk.", # M (merged with L)
- "AG Alff", # N
- "AG Gutfl.", # O (merged with N)
- "M3 Thiele", # P
- "M3 Buntkowsky", # Q
- "M3 Gutfleisch", # R
- ]
-
- # Define merged pairs
- MERGED_PAIRS = [
- ("Dr. Fohrer", "AG Buntk."), # L and M are merged
- ("AG Alff", "AG Gutfl."), # N and O are merged
- ]
-
- # M3 clients (not merged, calculated individually)
- M3_CLIENTS = ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"]
-
- # Groups for Sammelrückführungen (helium_input)
- HELIUM_INPUT_GROUPS = {
- "fohrer_buntk": ["Dr. Fohrer", "AG Buntk."],
- "alff_gutfl": ["AG Alff", "AG Gutfl."],
- "m3": ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"],
- }
-
- year = sheet.year
- month = sheet.month
- factor = Decimal('0.06') # Fixed factor from Excel
- updated_cells = []
-
- # Helper functions
- def get_val(client_name, row_idx):
- """Get cell value for a client and row"""
- try:
- client = Client.objects.get(name=client_name)
- col_idx = TOP_RIGHT_CLIENTS.index(client_name)
- cell = Cell.objects.filter(
- sheet=sheet,
- table_type='top_right',
- client=client,
- row_index=row_idx,
- column_index=col_idx
- ).first()
- if cell and cell.value is not None:
- return Decimal(str(cell.value))
- except (Client.DoesNotExist, ValueError, KeyError):
- pass
- return Decimal('0')
-
- def set_val(client_name, row_idx, value, is_calculated=True):
- """Set cell value for a client and row"""
- try:
- client = Client.objects.get(name=client_name)
- col_idx = TOP_RIGHT_CLIENTS.index(client_name)
-
- cell, created = Cell.objects.get_or_create(
- sheet=sheet,
- table_type='top_right',
- client=client,
- row_index=row_idx,
- column_index=col_idx,
- defaults={'value': value}
- )
-
- # Only update if value changed
- if not created and cell.value != value:
- cell.value = value
- cell.save()
- elif created:
- cell.save()
-
- updated_cells.append({
- 'id': cell.id,
- 'value': str(cell.value) if cell.value else '',
- 'is_calculated': is_calculated
- })
-
- return True
- except (Client.DoesNotExist, ValueError):
- return False
-
- # 1. Update Summe Bestand (row 6) from Bestand in Kannen-1 (row 5)
- # For merged pairs: copy value from changed cell to its pair
- if changed_cell and changed_cell.table_type == 'top_right':
- if changed_cell.row_index == 5: # Bestand in Kannen-1
- client_name = changed_cell.client.name
- new_value = changed_cell.value
-
- # Check if this client is in a merged pair
- for pair in MERGED_PAIRS:
- if client_name in pair:
- # Update both clients in the pair
- for client_in_pair in pair:
- if client_in_pair != client_name:
- set_val(client_in_pair, 5, new_value, is_calculated=False)
- break
-
- # 2. For all clients: Set Summe Bestand (row 6) = Bestand in Kannen-1 (row 5)
- for client_name in TOP_RIGHT_CLIENTS:
- bestand_value = get_val(client_name, 5)
- set_val(client_name, 6, bestand_value, is_calculated=True)
-
- # 3. Update Sammelrückführungen (row 4) from helium_input groups
- for group_name, client_names in HELIUM_INPUT_GROUPS.items():
- # Get total helium_input for this group
- clients_in_group = Client.objects.filter(name__in=client_names)
- total_helium = ExcelEntry.objects.filter(
- client__in=clients_in_group,
- date__year=year,
- date__month=month
- ).aggregate(total=Coalesce(Sum('lhe_ges'), Decimal('0')))['total']
-
- # Set same value for all clients in group
- for client_name in client_names:
- set_val(client_name, 4, total_helium, is_calculated=True)
-
- # 4. Calculate Rückführung flüssig (row 2)
- # For merged pairs: =L8 for Dr. Fohrer/AG Buntk., =N8 for AG Alff/AG Gutfl.
- # For M3 clients: =$P$8 * P4, $P$8 * Q4, $P$8 * R4
-
- # Get Sammelrückführungen values for groups
- sammel_fohrer_buntk = get_val("Dr. Fohrer", 4) # L8
- sammel_alff_gutfl = get_val("AG Alff", 4) # N8
- sammel_m3_group = get_val("M3 Thiele", 4) # P8 (same for all M3)
-
- # For merged pairs
- set_val("Dr. Fohrer", 2, sammel_fohrer_buntk, is_calculated=True)
- set_val("AG Buntk.", 2, sammel_fohrer_buntk, is_calculated=True)
- set_val("AG Alff", 2, sammel_alff_gutfl, is_calculated=True)
- set_val("AG Gutfl.", 2, sammel_alff_gutfl, is_calculated=True)
-
- # For M3 clients: =$P$8 * column4 (Stand der Gaszähler)
- for m3_client in M3_CLIENTS:
- stand_value = get_val(m3_client, 0) # Stand der Gaszähler (row 0)
- rueck_value = sammel_m3_group * stand_value
- set_val(m3_client, 2, rueck_value, is_calculated=True)
-
- # 5. Calculate Füllungen warm (row 11) from SecondTableEntry warm outputs
- # 5. Calculate Füllungen warm (row 11) as NUMBER of warm fillings
- # (sum of 1s where each warm SecondTableEntry is one filling)
- for client_name in TOP_RIGHT_CLIENTS:
- client = Client.objects.get(name=client_name)
- warm_count = SecondTableEntry.objects.filter(
- client=client,
- date__year=year,
- date__month=month,
- is_warm=True
- ).aggregate(
- total=Coalesce(Count('id'), 0)
- )['total']
-
- # store as Decimal so later formulas (warm * 15) still work nicely
- warm_value = Decimal(warm_count)
- set_val(client_name, 11, warm_value, is_calculated=True)
- # 6. Set Faktor row (13) to 0.06
- for client_name in TOP_RIGHT_CLIENTS:
- set_val(client_name, 13, factor, is_calculated=True)
-
- # 6a. Recalculate Stand der Gaszähler (row 0) for the merged pairs
- # according to Excel:
- # L4 = L13 / (L13 + M13), M4 = M13 / (L13 + M13)
- # N4 = N13 / (N13 + O13), O4 = O13 / (N13 + O13)
-
- # Pair 1: Dr. Fohrer / AG Buntk.
- bezug_dr = get_val("Dr. Fohrer", 8) # L13
- bezug_buntk = get_val("AG Buntk.", 8) # M13
- total_pair1 = bezug_dr + bezug_buntk
-
- if total_pair1 != 0:
- set_val("Dr. Fohrer", 0, bezug_dr / total_pair1, is_calculated=True)
- set_val("AG Buntk.", 0, bezug_buntk / total_pair1, is_calculated=True)
- else:
- # if no Bezug, both shares are 0
- set_val("Dr. Fohrer", 0, Decimal('0'), is_calculated=True)
- set_val("AG Buntk.", 0, Decimal('0'), is_calculated=True)
-
- # Pair 2: AG Alff / AG Gutfl.
- bezug_alff = get_val("AG Alff", 8) # N13
- bezug_gutfl = get_val("AG Gutfl.", 8) # O13
- total_pair2 = bezug_alff + bezug_gutfl
-
- if total_pair2 != 0:
- set_val("AG Alff", 0, bezug_alff / total_pair2, is_calculated=True)
- set_val("AG Gutfl.", 0, bezug_gutfl / total_pair2, is_calculated=True)
- else:
- set_val("AG Alff", 0, Decimal('0'), is_calculated=True)
- set_val("AG Gutfl.", 0, Decimal('0'), is_calculated=True)
-
-
- # 7. Calculate all other dependent rows for merged pairs
- for pair in MERGED_PAIRS:
- client1, client2 = pair
-
- # Get values for the pair
- bezug1 = get_val(client1, 8) # Bezug client1
- bezug2 = get_val(client2, 8) # Bezug client2
- total_bezug = bezug1 + bezug2 # L13+M13 or N13+O13
-
- summe_bestand = get_val(client1, 6) # L11 or N11 (merged, same value)
- best_vormonat = get_val(client1, 7) # L12 or N12 (merged, same value)
- rueck_fl = get_val(client1, 2) # L6 or N6 (merged, same value)
- warm1 = get_val(client1, 11) # L16 or N16
- warm2 = get_val(client2, 11) # M16 or O16
- total_warm = warm1 + warm2 # L16+M16 or N16+O16
-
- # Calculate Rückführ. Soll (row 9)
- # = L13+M13 - L11 + L12 for first pair
- # = N13+O13 - N11 + N12 for second pair
- rueck_soll = total_bezug - summe_bestand + best_vormonat
- set_val(client1, 9, rueck_soll, is_calculated=True)
- set_val(client2, 9, rueck_soll, is_calculated=True)
-
- # Calculate Verluste (row 10) = Rückführ. Soll - Rückführung flüssig
- verluste = rueck_soll - rueck_fl
- set_val(client1, 10, verluste, is_calculated=True)
- set_val(client2, 10, verluste, is_calculated=True)
-
- # Calculate Kaltgas Rückgabe (row 12)
- # = (L13+M13)*$A18 + (L16+M16)*15
- kaltgas = (total_bezug * factor) + (total_warm * Decimal('15'))
- set_val(client1, 12, kaltgas, is_calculated=True)
- set_val(client2, 12, kaltgas, is_calculated=True)
-
- # Calculate Verbraucherverluste (row 14) = Verluste - Kaltgas
- verbrauch = verluste - kaltgas
- set_val(client1, 14, verbrauch, is_calculated=True)
- set_val(client2, 14, verbrauch, is_calculated=True)
-
- # Calculate % (row 15) = Verbraucherverluste / (L13+M13)
- if total_bezug != 0:
- prozent = verbrauch / total_bezug
- else:
- prozent = Decimal('0')
- set_val(client1, 15, prozent, is_calculated=True)
- set_val(client2, 15, prozent, is_calculated=True)
-
- # 8. Calculate all dependent rows for M3 clients (individual calculations)
- for m3_client in M3_CLIENTS:
- # Get individual values
- bezug = get_val(m3_client, 8) # Bezug for this M3 client
- summe_bestand = get_val(m3_client, 6) # Summe Bestand
- best_vormonat = get_val(m3_client, 7) # Best. in Kannen Vormonat
- rueck_fl = get_val(m3_client, 2) # Rückführung flüssig
- warm = get_val(m3_client, 11) # Füllungen warm
-
- # Calculate Rückführ. Soll (row 9) = Bezug - Summe Bestand + Best. Vormonat
- rueck_soll = bezug - summe_bestand + best_vormonat
- set_val(m3_client, 9, rueck_soll, is_calculated=True)
-
- # Calculate Verluste (row 10) = Rückführ. Soll - Rückführung flüssig
- verluste = rueck_soll - rueck_fl
- set_val(m3_client, 10, verluste, is_calculated=True)
-
- # Calculate Kaltgas Rückgabe (row 12) = Bezug * factor + warm * 15
- kaltgas = (bezug * factor) + (warm * Decimal('15'))
- set_val(m3_client, 12, kaltgas, is_calculated=True)
-
- # Calculate Verbraucherverluste (row 14) = Verluste - Kaltgas
- verbrauch = verluste - kaltgas
- set_val(m3_client, 14, verbrauch, is_calculated=True)
-
- # Calculate % (row 15) = Verbraucherverluste / Bezug
- if bezug != 0:
- prozent = verbrauch / bezug
- else:
- prozent = Decimal('0')
- set_val(m3_client, 15, prozent, is_calculated=True)
-
- return updated_cells
-
-
-
- def calculate_bottom_1_dependents(self, sheet, changed_cell):
- """
- Recalculate Bottom Table 1 (table_type='bottom_1').
-
- Layout (row_index 0–9, col_index 0–4):
-
- Rows 0–8:
- 0: Batterie 1
- 1: 2
- 2: 3
- 3: 4
- 4: 5
- 5: 6
- 6: 2 Bündel
- 7: 2 Ballone
- 8: Reingasspeicher
-
- Row 9:
- 9: Gasbestand (totals row)
-
- Columns:
- 0: Volumen (fixed values, not editable)
- 1: bar (editable for rows 0–8)
- 2: korrigiert = bar / (bar/2000 + 1)
- 3: Nm³ = Volumen * korrigiert
- 4: Lit. LHe = Nm³ / 0.75
-
- Row 9:
- - Volumen (col 0): empty
- - bar (col 1): empty
- - korrigiert (col 2): empty
- - Nm³ (col 3): SUM Nm³ rows 0–8
- - Lit. LHe (col 4): SUM Lit. LHe rows 0–8
- """
- from decimal import Decimal, InvalidOperation
- from .models import Cell
-
- updated_cells = []
-
- DATA_ROWS = list(range(0, 9)) # 0–8
- TOTAL_ROW = 9
-
- COL_VOL = 0
- COL_BAR = 1
- COL_KORR = 2
- COL_NM3 = 3
- COL_LHE = 4
-
- # Fixed Volumen values for rows 0–8
- VOLUMES = [
- Decimal("2.4"), # row 0
- Decimal("5.1"), # row 1
- Decimal("4.0"), # row 2
- Decimal("1.0"), # row 3
- Decimal("4.0"), # row 4
- Decimal("0.4"), # row 5
- Decimal("1.2"), # row 6
- Decimal("20.0"), # row 7
- Decimal("5.0"), # row 8
- ]
-
- def get_cell(row_idx, col_idx):
- return Cell.objects.filter(
- sheet=sheet,
- table_type="bottom_1",
- row_index=row_idx,
- column_index=col_idx,
- ).first()
-
- def set_calc(row_idx, col_idx, value):
- """
- Set a calculated cell and add it to updated_cells.
- If value is None, we clear the cell.
- """
- cell = get_cell(row_idx, col_idx)
- if not cell:
- cell = Cell(
- sheet=sheet,
- table_type="bottom_1",
- row_index=row_idx,
- column_index=col_idx,
- value=value,
- )
- else:
- cell.value = value
- cell.save()
-
- updated_cells.append(
- {
- "id": cell.id,
- "value": "" if cell.value is None else str(cell.value),
- "is_calculated": True,
- }
- )
-
- # ---------- Rows 0–8: per-gasspeicher calculations ----------
- for row_idx in DATA_ROWS:
- bar_cell = get_cell(row_idx, COL_BAR)
-
- # Volumen: fixed constant or, if present, value from DB
- vol = VOLUMES[row_idx]
- vol_cell = get_cell(row_idx, COL_VOL)
- if vol_cell and vol_cell.value is not None:
- try:
- vol = Decimal(str(vol_cell.value))
- except (InvalidOperation, ValueError):
- # fall back to fixed constant
- vol = VOLUMES[row_idx]
-
- bar = None
- try:
- if bar_cell and bar_cell.value is not None:
- bar = Decimal(str(bar_cell.value))
- except (InvalidOperation, ValueError):
- bar = None
-
- # korrigiert = bar / (bar/2000 + 1)
- if bar is not None and bar != 0:
- korr = bar / (bar / Decimal("2000") + Decimal("1"))
- else:
- korr = None
-
- # Nm³ = Volumen * korrigiert
- if korr is not None:
- nm3 = vol * korr
- else:
- nm3 = None
-
- # Lit. LHe = Nm³ / 0.75
- if nm3 is not None:
- lit_lhe = nm3 / Decimal("0.75")
- else:
- lit_lhe = None
-
- # Write calculated cells back (NOT Volumen or bar)
- set_calc(row_idx, COL_KORR, korr)
- set_calc(row_idx, COL_NM3, nm3)
- set_calc(row_idx, COL_LHE, lit_lhe)
-
- # ---------- Row 9: totals (Gasbestand) ----------
- total_nm3 = Decimal("0")
- total_lhe = Decimal("0")
- has_nm3 = False
- has_lhe = False
-
- for row_idx in DATA_ROWS:
- nm3_cell = get_cell(row_idx, COL_NM3)
- if nm3_cell and nm3_cell.value is not None:
- try:
- total_nm3 += Decimal(str(nm3_cell.value))
- has_nm3 = True
- except (InvalidOperation, ValueError):
- pass
-
- lhe_cell = get_cell(row_idx, COL_LHE)
- if lhe_cell and lhe_cell.value is not None:
- try:
- total_lhe += Decimal(str(lhe_cell.value))
- has_lhe = True
- except (InvalidOperation, ValueError):
- pass
-
- # Volumen (0), bar (1), korrigiert (2) on total row stay empty
- set_calc(TOTAL_ROW, COL_KORR, None) # explicitly clear korrigiert
- set_calc(TOTAL_ROW, COL_NM3, total_nm3 if has_nm3 else None)
- set_calc(TOTAL_ROW, COL_LHE, total_lhe if has_lhe else None)
-
- return updated_cells
-
-
-
-
-
- def calculate_top_left_dependents(self, sheet, changed_cell):
- """Calculate dependent cells in top_left table"""
- from decimal import Decimal
- from django.db.models import Sum
- from django.db.models.functions import Coalesce
-
- client_id = changed_cell.client_id
- updated_cells = []
-
- # Get all cells for this client in top_left table
- client_cells = Cell.objects.filter(
- sheet=sheet,
- table_type='top_left',
- client_id=client_id
- )
-
- # Create a dict for easy access
- cell_dict = {}
- for cell in client_cells:
- cell_dict[cell.row_index] = cell
-
- # Get values with safe defaults
- def get_cell_value(row_idx):
- cell = cell_dict.get(row_idx)
- if cell and cell.value is not None:
- try:
- return Decimal(str(cell.value))
- except:
- return Decimal('0')
- return Decimal('0')
-
- # 1. B5 = IF(B3>0; B3-B4; 0) - Gasrückführung
- b3 = get_cell_value(0) # row_index 0 = Excel B3 (UI row 1)
- b4 = get_cell_value(1) # row_index 1 = Excel B4 (UI row 2)
-
- # Calculate B5
- if b3 > 0:
- b5_value = b3 - b4
- if b5_value < 0:
- b5_value = Decimal('0')
- else:
- b5_value = Decimal('0')
-
- # Update B5 - FORCE update even if value appears the same
- b5_cell = cell_dict.get(2) # row_index 2 = Excel B5 (UI row 3)
- if b5_cell:
- # Always update B5 when B3 or B4 changes
- b5_cell.value = b5_value
- b5_cell.save()
- updated_cells.append({
- 'id': b5_cell.id,
- 'value': str(b5_cell.value),
- 'is_calculated': True
- })
-
- # 2. B6 = B5 / 0.75 - Rückführung flüssig
- b6_value = b5_value / Decimal('0.75')
- b6_cell = cell_dict.get(3) # row_index 3 = Excel B6 (UI row 4)
- if b6_cell:
- b6_cell.value = b6_value
- b6_cell.save()
- updated_cells.append({
- 'id': b6_cell.id,
- 'value': str(b6_cell.value),
- 'is_calculated': True
- })
-
- # 3. B9 = B7 + B8 - Summe Bestand
- b7 = get_cell_value(4) # row_index 4 = Excel B7 (UI row 5)
- b8 = get_cell_value(5) # row_index 5 = Excel B8 (UI row 6)
- b9_value = b7 + b8
- b9_cell = cell_dict.get(6) # row_index 6 = Excel B9 (UI row 7)
- if b9_cell:
- b9_cell.value = b9_value
- b9_cell.save()
- updated_cells.append({
- 'id': b9_cell.id,
- 'value': str(b9_cell.value),
- 'is_calculated': True
- })
-
- # 4. B11 = Sum of LHe Output from SecondTableEntry for this client/month - Bezug
- from .models import SecondTableEntry
- client = changed_cell.client
-
- # Calculate total LHe output for this client in this month
- # 4. B11 = Bezug (Liter L-He)
- # For start sheet: manual entry, for other sheets: auto-calculated from SecondTableEntry
- from .models import SecondTableEntry
- client = changed_cell.client
-
- b11_cell = cell_dict.get(8) # row_index 8 = Excel B11 (UI row 9)
-
- # Check if this is the start sheet (2025-01)
- is_start_sheet = (sheet.year == 2025 and sheet.month == 1)
-
- # Get the B11 value for calculations
- b11_value = Decimal('0')
-
- if is_start_sheet:
- # For start sheet, keep whatever value is already there (manual entry)
- if b11_cell and b11_cell.value is not None:
- b11_value = Decimal(str(b11_cell.value))
- else:
- # For non-start sheets: auto-calculate from SecondTableEntry
- lhe_output_sum = SecondTableEntry.objects.filter(
- client=client,
- date__year=sheet.year,
- date__month=sheet.month
- ).aggregate(
- total=Coalesce(Sum('lhe_output'), Decimal('0'))
- )['total']
-
- b11_value = lhe_output_sum
-
- if b11_cell and (b11_cell.value != b11_value or b11_cell.value is None):
- b11_cell.value = b11_value
- b11_cell.save()
- updated_cells.append({
- 'id': b11_cell.id,
- 'value': str(b11_cell.value),
- 'is_calculated': True # Calculated from SecondTableEntry
- })
-
- # 5. B12 = B11 + B10 - B9 - Rückführ. Soll
- b10 = get_cell_value(7) # row_index 7 = Excel B10 (UI row 8)
- b12_value = b11_value + b10 - b9_value # Use b11_value instead of lhe_output_sum
- b12_cell = cell_dict.get(9) # row_index 9 = Excel B12 (UI row 10)
- if b12_cell:
- b12_cell.value = b12_value
- b12_cell.save()
- updated_cells.append({
- 'id': b12_cell.id,
- 'value': str(b12_cell.value),
- 'is_calculated': True
- })
-
- # 6. B13 = B12 - B6 - Verluste (Soll-Rückf.)
- b13_value = b12_value - b6_value
- b13_cell = cell_dict.get(10) # row_index 10 = Excel B13 (UI row 11)
- if b13_cell:
- b13_cell.value = b13_value
- b13_cell.save()
- updated_cells.append({
- 'id': b13_cell.id,
- 'value': str(b13_cell.value),
- 'is_calculated': True
- })
-
- # 7. B14 = Count of warm outputs
- warm_count = SecondTableEntry.objects.filter(
- client=client,
- date__year=sheet.year,
- date__month=sheet.month,
- is_warm=True
- ).count()
-
- b14_cell = cell_dict.get(11) # row_index 11 = Excel B14 (UI row 12)
- if b14_cell:
- b14_cell.value = Decimal(str(warm_count))
- b14_cell.save()
- updated_cells.append({
- 'id': b14_cell.id,
- 'value': str(b14_cell.value),
- 'is_calculated': True
- })
-
- # 8. B15 = IF(B11>0; B11 * factor + B14 * 15; 0) - Kaltgas Rückgabe
- factor = get_cell_value(13) # row_index 13 = Excel B16 (Faktor) (UI row 14)
- if factor == 0:
- factor = Decimal('0.06') # default factor
-
- if b11_value > 0: # Use b11_value
- b15_value = b11_value * factor + Decimal(str(warm_count)) * Decimal('15')
- else:
- b15_value = Decimal('0')
-
- b15_cell = cell_dict.get(12) # row_index 12 = Excel B15 (UI row 13)
- if b15_cell:
- b15_cell.value = b15_value
- b15_cell.save()
- updated_cells.append({
- 'id': b15_cell.id,
- 'value': str(b15_cell.value),
- 'is_calculated': True
- })
-
- # 9. B17 = B13 - B15 - Verbraucherverluste
- b17_value = b13_value - b15_value
- b17_cell = cell_dict.get(14) # row_index 14 = Excel B17 (UI row 15)
- if b17_cell:
- b17_cell.value = b17_value
- b17_cell.save()
- updated_cells.append({
- 'id': b17_cell.id,
- 'value': str(b17_cell.value),
- 'is_calculated': True
- })
-
- # 10. B18 = IF(B11=0; 0; B17/B11) - %
- if b11_value == 0: # Use b11_value
- b18_value = Decimal('0')
- else:
- b18_value = b17_value / b11_value # Use b11_value
-
- b18_cell = cell_dict.get(15) # row_index 15 = Excel B18 (UI row 16)
- if b18_cell:
- b18_cell.value = b18_value
- b18_cell.save()
- updated_cells.append({
- 'id': b18_cell.id,
- 'value': str(b18_cell.value),
- 'is_calculated': True
- })
-
- return updated_cells
- def save_bulk_cells(self, request, sheet):
- """Original bulk save logic (for backward compatibility)"""
- # Get all cell updates
- cell_updates = {}
- for key, value in request.POST.items():
- if key.startswith('cell_'):
- cell_id = key.replace('cell_', '')
- cell_updates[cell_id] = value
-
- # Update cells and track which ones changed
- updated_cells = []
- changed_clients = set()
-
- for cell_id, new_value in cell_updates.items():
- try:
- cell = Cell.objects.get(id=cell_id, sheet=sheet)
- old_value = cell.value
-
- # Convert new value
- try:
- if new_value.strip():
- cell.value = Decimal(new_value)
- else:
- cell.value = None
- except Exception:
- cell.value = None
-
- # Only save if value changed
- if cell.value != old_value:
- cell.save()
- updated_cells.append({
- 'id': cell.id,
- 'value': str(cell.value) if cell.value else ''
- })
- # bottom_3 has no client, so this will just add None for those cells,
- # which is harmless. Top-left cells still add their real client_id.
- changed_clients.add(cell.client_id)
-
- except Cell.DoesNotExist:
- continue
-
- # Recalculate for each changed client (top-left tables)
- for client_id in changed_clients:
- if client_id is not None:
- self.recalculate_top_left_table(sheet, client_id)
-
- # --- NEW: recalc bottom_3 for the whole sheet, independent of clients ---
- bottom3_updates = self.calculate_bottom_3_dependents(sheet)
-
- # Get all updated cells for response (top-left)
- all_updated_cells = []
- for client_id in changed_clients:
- if client_id is None:
- continue # skip bottom_3 / non-client cells
- client_cells = Cell.objects.filter(
- sheet=sheet,
- client_id=client_id
- )
- for cell in client_cells:
- all_updated_cells.append({
- 'id': cell.id,
- 'value': str(cell.value) if cell.value else '',
- 'is_calculated': cell.is_formula
- })
-
- # Add bottom_3 recalculated cells (J46..K53, etc.)
- all_updated_cells.extend(bottom3_updates)
-
- return JsonResponse({
- 'status': 'success',
- 'message': f'Saved {len(updated_cells)} cells',
- 'updated_cells': all_updated_cells
- })
-
-
- def recalculate_top_left_table(self, sheet, client_id):
- """Recalculate the top-left table for a specific client"""
- from decimal import Decimal
-
- # Get all cells for this client in top_left table
- cells = Cell.objects.filter(
- sheet=sheet,
- table_type='top_left',
- client_id=client_id
- ).order_by('row_index')
-
- # Create a dictionary of cell values
- cell_dict = {}
- for cell in cells:
- cell_dict[cell.row_index] = cell
-
- # Excel logic implementation for top-left table
- # B3 (row_index 0) = Stand der Gaszähler (Nm³) - manual
- # B4 (row_index 1) = Stand der Gaszähler (Vormonat) (Nm³) - from previous sheet
-
- # Get B3 and B4
- b3_cell = cell_dict.get(0) # UI Row 3
- b4_cell = cell_dict.get(1) # UI Row 4
-
- if b3_cell and b3_cell.value and b4_cell and b4_cell.value:
- # B5 = IF(B3>0; B3-B4; 0)
- b3 = Decimal(str(b3_cell.value))
- b4 = Decimal(str(b4_cell.value))
-
- if b3 > 0:
- b5 = b3 - b4
- if b5 < 0:
- b5 = Decimal('0')
- else:
- b5 = Decimal('0')
-
- # Update B5 (row_index 2)
- b5_cell = cell_dict.get(2)
- if b5_cell and (b5_cell.value != b5 or b5_cell.value is None):
- b5_cell.value = b5
- b5_cell.save()
-
- # B6 = B5 / 0.75 (row_index 3)
- b6 = b5 / Decimal('0.75')
- b6_cell = cell_dict.get(3)
- if b6_cell and (b6_cell.value != b6 or b6_cell.value is None):
- b6_cell.value = b6
- b6_cell.save()
-
- # Get previous month's sheet for B10
- if sheet.month == 1:
- prev_year = sheet.year - 1
- prev_month = 12
- else:
- prev_year = sheet.year
- prev_month = sheet.month - 1
-
- prev_sheet = MonthlySheet.objects.filter(
- year=prev_year,
- month=prev_month
- ).first()
-
- if prev_sheet:
- # Get B9 from previous sheet (row_index 7 in previous)
- prev_b9 = Cell.objects.filter(
- sheet=prev_sheet,
- table_type='top_left',
- client_id=client_id,
- row_index=7 # UI Row 9
- ).first()
-
- if prev_b9 and prev_b9.value:
- # Update B10 in current sheet (row_index 8)
- b10_cell = cell_dict.get(8)
- if b10_cell and (b10_cell.value != prev_b9.value or b10_cell.value is None):
- b10_cell.value = prev_b9.value
- b10_cell.save()
-@method_decorator(csrf_exempt, name='dispatch')
-class SaveMonthSummaryView(View):
- """
- Saves per-month summary values such as K44 (Gesamtbestand neu).
- Called from JS after 'Save All' finishes.
- """
-
- def post(self, request, *args, **kwargs):
- try:
- data = json.loads(request.body.decode('utf-8'))
- except json.JSONDecodeError:
- return JsonResponse(
- {'success': False, 'message': 'Invalid JSON'},
- status=400
- )
-
- sheet_id = data.get('sheet_id')
- if not sheet_id:
- return JsonResponse(
- {'success': False, 'message': 'Missing sheet_id'},
- status=400
- )
-
- try:
- sheet = MonthlySheet.objects.get(id=sheet_id)
- except MonthlySheet.DoesNotExist:
- return JsonResponse(
- {'success': False, 'message': 'Sheet not found'},
- status=404
- )
-
- # More tolerant decimal conversion: accepts "123.45" and "123,45"
- def to_decimal(value):
- if value is None:
- return None
- s = str(value).strip()
- if s == '':
- return None
- s = s.replace(',', '.')
- try:
- return Decimal(s)
- except (InvalidOperation, ValueError):
- # Debug: show what failed in the dev server console
- print("SaveMonthSummaryView to_decimal failed for:", repr(value))
- return None
-
- raw_k44 = data.get('gesamtbestand_neu_lhe')
- raw_gas = data.get('gasbestand_lhe')
- raw_verb = data.get('verbraucherverlust_lhe')
-
- gesamtbestand_neu_lhe = to_decimal(raw_k44)
- gasbestand_lhe = to_decimal(raw_gas)
- verbraucherverlust_lhe = to_decimal(raw_verb)
-
- summary, created = MonthlySummary.objects.get_or_create(sheet=sheet)
-
- if gesamtbestand_neu_lhe is not None:
- summary.gesamtbestand_neu_lhe = gesamtbestand_neu_lhe
- if gasbestand_lhe is not None:
- summary.gasbestand_lhe = gasbestand_lhe
- if verbraucherverlust_lhe is not None:
- summary.verbraucherverlust_lhe = verbraucherverlust_lhe
-
- summary.save()
-
- # Small debug output so you can see in the server console what was saved
- print(
- f"Saved MonthlySummary for {sheet.year}-{sheet.month:02d}: "
- f"K44={summary.gesamtbestand_neu_lhe}, "
- f"Gasbestand={summary.gasbestand_lhe}, "
- f"Verbraucherverlust={summary.verbraucherverlust_lhe}"
- )
-
- return JsonResponse({'success': True})
-
-
-
-# Calculate View (placeholder for calculations)
-class CalculateView(View):
- def post(self, request):
- # This will be implemented when you provide calculation rules
- return JsonResponse({
- 'status': 'success',
- 'message': 'Calculation endpoint ready'
- })
-
-# Summary Sheet View
-class SummarySheetView(TemplateView):
- template_name = 'summary_sheet.html'
-
- def get_context_data(self, **kwargs):
- context = super().get_context_data(**kwargs)
- start_month = int(self.kwargs.get('start_month', 1))
- year = int(self.kwargs.get('year', datetime.now().year))
-
- # Get 6 monthly sheets
- months = [(year, m) for m in range(start_month, start_month + 6)]
- sheets = MonthlySheet.objects.filter(
- year=year,
- month__in=list(range(start_month, start_month + 6))
- ).order_by('month')
-
- # Aggregate data across months
- summary_data = self.calculate_summary(sheets)
-
- context.update({
- 'year': year,
- 'start_month': start_month,
- 'end_month': start_month + 5,
- 'sheets': sheets,
- 'clients': Client.objects.all(),
- 'summary_data': summary_data,
- })
- return context
-
- def calculate_summary(self, sheets):
- """Calculate totals across 6 months"""
- summary = {}
-
- for client in Client.objects.all():
- client_total = 0
- for sheet in sheets:
- # Get specific cells and sum
- cells = sheet.cells.filter(
- client=client,
- table_type='top_left',
- row_index=0 # Example: first row
- )
- for cell in cells:
- if cell.value:
- try:
- client_total += float(cell.value)
- except (ValueError, TypeError):
- continue
-
- summary[client.id] = client_total
-
- return summary
-
-# Existing views below (keep all your existing functions)
-def clients_list(request):
- # --- Clients for the yearly summary table ---
- clients = Client.objects.all()
-
- # --- Available years for output data (same as before) ---
- available_years_qs = SecondTableEntry.objects.dates('date', 'year', order='DESC')
- available_years = [y.year for y in available_years_qs]
-
- # === 1) Year used for the "Helium Output Yearly Summary" table ===
- # Uses ?year=... in the dropdown
- year_param = request.GET.get('year')
- if year_param:
- selected_year = int(year_param)
- else:
- selected_year = available_years[0] if available_years else datetime.now().year
-
- # === 2) GLOBAL half-year interval (shared with other pages) =======
- # Try GET params first
- interval_year_param = request.GET.get('interval_year')
- start_month_param = request.GET.get('interval_start_month')
-
- # Fallbacks from session
- session_year = request.session.get('halfyear_year')
- session_start_month = request.session.get('halfyear_start_month')
-
- # Determine final interval_year
- if interval_year_param:
- interval_year = int(interval_year_param)
- elif session_year:
- interval_year = int(session_year)
- else:
- # default: same as selected_year for summary
- interval_year = selected_year
-
- # Determine final interval_start_month
- if start_month_param:
- interval_start_month = int(start_month_param)
- elif session_start_month:
- interval_start_month = int(session_start_month)
- else:
- interval_start_month = 1 # default Jan
-
- # Store back into the session so other views can read them
- request.session['halfyear_year'] = interval_year
- request.session['halfyear_start_month'] = interval_start_month
-
- # === 3) Build a 6-month window, allowing wrap into the next year ===
- # Example: interval_year=2025, interval_start_month=12
- # window = [(2025,12), (2026,1), (2026,2), (2026,3), (2026,4), (2026,5)]
- window = []
- for offset in range(6):
- total_index = (interval_start_month - 1) + offset # 0-based index
- y = interval_year + (total_index // 12)
- m = (total_index % 12) + 1
- window.append((y, m))
-
- # === 4) Totals per client in that 6-month window ==================
- monthly_data = []
- for client in clients:
- monthly_totals = []
- for (y, m) in window:
- total = SecondTableEntry.objects.filter(
- client=client,
- date__year=y,
- date__month=m
- ).aggregate(
- total=Coalesce(
- Sum('lhe_output'),
- Value(0, output_field=DecimalField())
- )
- )['total']
- monthly_totals.append(total)
-
- monthly_data.append({
- 'client': client,
- 'monthly_totals': monthly_totals,
- 'year_total': sum(monthly_totals),
- })
-
- # === 5) Month labels for the header (only the 6-month window) =====
- month_labels = [calendar.month_abbr[m] for (y, m) in window]
-
- # === 6) FINALLY: return the response ==============================
- return render(request, 'clients_table.html', {
- 'available_years': available_years,
- 'current_year': selected_year, # used by year dropdown
- 'interval_year': interval_year, # used by "Global 6-Month Interval" form
- 'interval_start_month': interval_start_month,
- 'months': month_labels,
- 'monthly_data': monthly_data,
- })
-
- # === 5) Month labels for the header (only the 6-month window) =====
- month_labels = [calendar.month_abbr[m] for (y, m) in window]
-
-
-def set_halfyear_interval(request):
- if request.method == 'POST':
- year = int(request.POST.get('year'))
- start_month = int(request.POST.get('start_month'))
-
- request.session['halfyear_year'] = year
- request.session['halfyear_start_month'] = start_month
-
- return redirect(request.META.get('HTTP_REFERER', 'clients_list'))
-
- return redirect('clients_list')
-# Table One View (ExcelEntry)
-def table_one_view(request):
- from .models import ExcelEntry, Client, Institute
-
- # --- Base queryset for the main Helium Input table ---
- base_entries = ExcelEntry.objects.all().select_related('client', 'client__institute')
-
- # Read the global 6-month interval from the session
- interval_year = request.session.get('halfyear_year')
- interval_start = request.session.get('halfyear_start_month')
-
- if interval_year and interval_start:
- interval_year = int(interval_year)
- interval_start = int(interval_start)
-
- # Build the same 6-month window as on the main page (can cross year)
- window = []
- for offset in range(6):
- total_index = (interval_start - 1) + offset # 0-based
- y = interval_year + (total_index // 12)
- m = (total_index % 12) + 1
- window.append((y, m))
-
- # Build Q filter: (year=m_year AND month=m_month) for any of those 6
- q = Q()
- for (y, m) in window:
- q |= Q(date__year=y, date__month=m)
-
- entries_table1 = base_entries.filter(q).order_by('-date')
- else:
- # Fallback: if no global interval yet, show everything
- entries_table1 = base_entries.order_by('-date')
- clients = Client.objects.all().select_related('institute')
- institutes = Institute.objects.all()
-
- # ---- Overview filters ----
- # years present in ExcelEntry.date
- year_qs = ExcelEntry.objects.dates('date', 'year', order='DESC')
- available_years = [d.year for d in year_qs]
-
- # default year/start month
- # default year/start month (if no global interval yet)
- if available_years:
- default_year = available_years[0] # newest year in ExcelEntry
- else:
- default_year = timezone.now().year
-
- # 🔸 Read global half-year interval from session (set on main page)
- session_year = request.session.get('halfyear_year')
- session_start = request.session.get('halfyear_start_month')
-
- # If the user has set a global interval, use it.
- # Otherwise fall back to default year / January.
- year = int(session_year) if session_year else int(default_year)
- start_month = int(session_start) if session_start else 1
-
-
- # six-month window
- # --- Build a 6-month window, allowing wrap into the next year ---
- # Example: year=2025, start_month=10
- # window = [(2025,10), (2025,11), (2025,12), (2026,1), (2026,2), (2026,3)]
- window = []
- for offset in range(6):
- total_index = (start_month - 1) + offset # 0-based
- y_for_month = year + (total_index // 12)
- m_for_month = (total_index % 12) + 1
- window.append((y_for_month, m_for_month))
-
- overview = None
-
- if window:
- # Build per-group data
- groups_entries = [] # for internal calculations
-
- for key, group in CLIENT_GROUPS.items():
- clients_qs = get_group_clients(key)
-
- values = []
- group_total = Decimal('0')
-
- for (y_m, m_m) in window:
- total = ExcelEntry.objects.filter(
- client__in=clients_qs,
- date__year=y_m,
- date__month=m_m
- ).aggregate(
- total=Coalesce(Sum('lhe_ges'), Decimal('0'))
- )['total']
-
- values.append(total)
- group_total += total
-
- groups_entries.append({
- 'key': key,
- 'label': group['label'],
- 'values': values,
- 'total': group_total,
- })
-
- # month totals across all groups
- month_totals = []
- for idx in range(len(window)):
- s = sum((g['values'][idx] for g in groups_entries), Decimal('0'))
- month_totals.append(s)
-
- grand_total = sum(month_totals, Decimal('0'))
-
- # Build rows for the template
- rows = []
- for idx, (y_m, m_m) in enumerate(window):
- row_values = [g['values'][idx] for g in groups_entries]
- rows.append({
- 'month_number': m_m,
- 'month_label': calendar.month_name[m_m],
- 'values': row_values,
- 'total': month_totals[idx],
- })
-
- groups_meta = [{'key': g['key'], 'label': g['label']} for g in groups_entries]
- group_totals = [g['total'] for g in groups_entries]
-
- # Start/end for display – include years so wrap is clear
- start_year = window[0][0]
- start_month_disp = window[0][1]
- end_year = window[-1][0]
- end_month_disp = window[-1][1]
-
- overview = {
- 'year': year, # keep for backwards compatibility if needed
- 'start_month': start_month_disp,
- 'start_year': start_year,
- 'end_month': end_month_disp,
- 'end_year': end_year,
- 'rows': rows,
- 'groups': groups_meta,
- 'group_totals': group_totals,
- 'grand_total': grand_total,
- }
-
-
- # Month dropdown labels
- MONTH_CHOICES = [
- (1, 'Jan'), (2, 'Feb'), (3, 'Mar'), (4, 'Apr'),
- (5, 'May'), (6, 'Jun'), (7, 'Jul'), (8, 'Aug'),
- (9, 'Sep'), (10, 'Oct'), (11, 'Nov'), (12, 'Dec'),
- ]
-
- return render(request, 'table_one.html', {
- 'entries_table1': entries_table1,
- 'clients': clients,
- 'institutes': institutes,
- 'available_years': available_years,
- 'month_choices': MONTH_CHOICES,
- 'overview': overview,
- })
-
-# Table Two View (SecondTableEntry)
-def table_two_view(request):
- try:
- clients = Client.objects.all().select_related('institute')
- institutes = Institute.objects.all()
-
- # 🔸 Read global half-year interval from session
- interval_year = request.session.get('halfyear_year')
- interval_start = request.session.get('halfyear_start_month')
-
- if interval_year and interval_start:
- interval_year = int(interval_year)
- interval_start = int(interval_start)
-
- # Build the same 6-month window as in clients_list (can cross years)
- window = []
- for offset in range(6):
- total_index = (interval_start - 1) + offset # 0-based
- y = interval_year + (total_index // 12)
- m = (total_index % 12) + 1
- window.append((y, m))
-
- # Build a Q object matching any of those (year, month) pairs
- q = Q()
- for (y, m) in window:
- q |= Q(date__year=y, date__month=m)
-
- entries = SecondTableEntry.objects.filter(q).order_by('-date')
- else:
- # Fallback if no global interval yet: show all
- entries = SecondTableEntry.objects.all().order_by('-date')
-
- return render(request, 'table_two.html', {
- 'entries_table2': entries,
- 'clients': clients,
- 'institutes': institutes,
- 'interval_year': interval_year,
- 'interval_start_month': interval_start,
- })
-
- except Exception as e:
- return render(request, 'table_two.html', {
- 'error_message': f"Failed to load data: {str(e)}",
- 'entries_table2': [],
- 'clients': clients,
- 'institutes': institutes,
- })
-
-
-def monthly_sheet_root(request):
- """
- Redirect /sheet/ to the sheet matching the globally selected
- half-year start (year + month). If not set, fall back to latest.
- """
- year = request.session.get('halfyear_year')
- start_month = request.session.get('halfyear_start_month')
-
- if year and start_month:
- try:
- year = int(year)
- start_month = int(start_month)
- return redirect('monthly_sheet', year=year, month=start_month)
- except ValueError:
- pass # fall through
-
- # Fallback: latest MonthlySheet if exists
- latest_sheet = MonthlySheet.objects.order_by('-year', '-month').first()
- if latest_sheet:
- return redirect('monthly_sheet', year=latest_sheet.year, month=latest_sheet.month)
- else:
- now = timezone.now()
- return redirect('monthly_sheet', year=now.year, month=now.month)
-# Add Entry (Generic)
-def add_entry(request, model_name):
- if request.method == 'POST':
- try:
- if model_name == 'SecondTableEntry':
- model = SecondTableEntry
-
- # Parse date
- date_str = request.POST.get('date')
- try:
- date_obj = datetime.strptime(date_str, '%Y-%m-%d').date() if date_str else None
- except (ValueError, TypeError):
- return JsonResponse({
- 'status': 'error',
- 'message': 'Invalid date format. Use YYYY-MM-DD'
- }, status=400)
-
- # NEW: robust parse of warm flag (handles 0/1, true/false, on/off)
- raw_warm = request.POST.get('is_warm')
- is_warm_bool = str(raw_warm).lower() in ('1', 'true', 'on', 'yes')
-
- # Handle Helium Output (Table Two)
- lhe_output = request.POST.get('lhe_output')
-
- entry = model.objects.create(
- client=Client.objects.get(id=request.POST.get('client_id')),
- date=date_obj,
- is_warm=is_warm_bool, # <-- use the boolean here
- lhe_delivery=request.POST.get('lhe_delivery', ''),
- lhe_output=Decimal(lhe_output) if lhe_output else None,
- notes=request.POST.get('notes', '')
- )
-
- return JsonResponse({
- 'status': 'success',
- 'id': entry.id,
- 'client_name': entry.client.name,
- 'institute_name': entry.client.institute.name,
- 'date': entry.date.strftime('%Y-%m-%d') if entry.date else '',
- 'is_warm': entry.is_warm,
- 'lhe_delivery': entry.lhe_delivery,
- 'lhe_output': str(entry.lhe_output) if entry.lhe_output else '',
- 'notes': entry.notes
- })
-
- elif model_name == 'ExcelEntry':
- model = ExcelEntry
-
- # Parse the date string into a date object
- date_str = request.POST.get('date')
- try:
- date_obj = datetime.strptime(date_str, '%Y-%m-%d').date() if date_str else None
- except (ValueError, TypeError):
- date_obj = None
-
- try:
- pressure = Decimal(request.POST.get('pressure', 0))
- purity = Decimal(request.POST.get('purity', 0))
- druckkorrektur = Decimal(request.POST.get('druckkorrektur', 1))
- lhe_zus = Decimal(request.POST.get('lhe_zus', 0))
- constant_300 = Decimal(request.POST.get('constant_300', 300))
- korrig_druck = Decimal(request.POST.get('korrig_druck', 0))
- nm3 = Decimal(request.POST.get('nm3', 0))
- lhe = Decimal(request.POST.get('lhe', 0))
- lhe_ges = Decimal(request.POST.get('lhe_ges', 0))
- except InvalidOperation:
- return JsonResponse({
- 'status': 'error',
- 'message': 'Invalid numeric value in Helium Input'
- }, status=400)
-
- # Create the entry with ALL fields
- entry = model.objects.create(
- client=Client.objects.get(id=request.POST.get('client_id')),
- date=date_obj,
- pressure=pressure,
- purity=purity,
- druckkorrektur=druckkorrektur,
- lhe_zus=lhe_zus,
- constant_300=constant_300,
- korrig_druck=korrig_druck,
- nm3=nm3,
- lhe=lhe,
- lhe_ges=lhe_ges,
- notes=request.POST.get('notes', '')
- )
-
- # Prepare the response
- response_data = {
- 'status': 'success',
- 'id': entry.id,
- 'client_name': entry.client.name,
- 'institute_name': entry.client.institute.name,
- 'pressure': str(entry.pressure),
- 'purity': str(entry.purity),
- 'druckkorrektur': str(entry.druckkorrektur),
- 'constant_300': str(entry.constant_300),
- 'korrig_druck': str(entry.korrig_druck),
- 'nm3': str(entry.nm3),
- 'lhe': str(entry.lhe),
- 'lhe_zus': str(entry.lhe_zus),
- 'lhe_ges': str(entry.lhe_ges),
- 'notes': entry.notes,
- }
-
- if entry.date:
- # JS uses this for the Date column and for the Month column
- response_data['date'] = entry.date.strftime('%Y-%m-%d')
- response_data['month'] = f"{entry.date.month:02d}"
- else:
- response_data['date'] = ''
- response_data['month'] = ''
-
- return JsonResponse(response_data)
-
-
- except Exception as e:
- return JsonResponse({'status': 'error', 'message': str(e)}, status=400)
-
- return JsonResponse({'status': 'error', 'message': 'Invalid request'}, status=400)
-
-# Update Entry (Generic)
-def update_entry(request, model_name):
- if request.method == 'POST':
- try:
- if model_name == 'SecondTableEntry':
- model = SecondTableEntry
- elif model_name == 'ExcelEntry':
- model = ExcelEntry
- else:
- return JsonResponse({'status': 'error', 'message': 'Invalid model'}, status=400)
-
- entry_id = int(request.POST.get('id'))
- entry = model.objects.get(id=entry_id)
-
- # Common updates for both models
- entry.client = Client.objects.get(id=request.POST.get('client_id'))
- entry.notes = request.POST.get('notes', '')
-
- # Handle date properly for both models
- date_str = request.POST.get('date')
- if date_str:
- try:
- entry.date = datetime.strptime(date_str, '%Y-%m-%d').date()
- except ValueError:
- return JsonResponse({
- 'status': 'error',
- 'message': 'Invalid date format. Use YYYY-MM-DD'
- }, status=400)
-
- if model_name == 'SecondTableEntry':
- # Handle Helium Output specific fields
-
- raw_warm = request.POST.get('is_warm')
- entry.is_warm = str(raw_warm).lower() in ('1', 'true', 'on', 'yes')
-
- entry.lhe_delivery = request.POST.get('lhe_delivery', '')
-
- lhe_output = request.POST.get('lhe_output')
- try:
- entry.lhe_output = Decimal(lhe_output) if lhe_output else None
- except InvalidOperation:
- return JsonResponse({
- 'status': 'error',
- 'message': 'Invalid LHe Output value'
- }, status=400)
-
- entry.save()
-
- return JsonResponse({
- 'status': 'success',
- 'id': entry.id,
- 'client_name': entry.client.name,
- 'institute_name': entry.client.institute.name,
- 'date': entry.date.strftime('%Y-%m-%d') if entry.date else '',
- 'is_warm': entry.is_warm,
- 'lhe_delivery': entry.lhe_delivery,
- 'lhe_output': str(entry.lhe_output) if entry.lhe_output else '',
- 'notes': entry.notes
- })
-
-
- elif model_name == 'ExcelEntry':
- # Handle Helium Input specific fields
- try:
- entry.pressure = Decimal(request.POST.get('pressure', 0))
- entry.purity = Decimal(request.POST.get('purity', 0))
- entry.druckkorrektur = Decimal(request.POST.get('druckkorrektur', 1))
- entry.lhe_zus = Decimal(request.POST.get('lhe_zus', 0))
- entry.constant_300 = Decimal(request.POST.get('constant_300', 300))
- entry.korrig_druck = Decimal(request.POST.get('korrig_druck', 0))
- entry.nm3 = Decimal(request.POST.get('nm3', 0))
- entry.lhe = Decimal(request.POST.get('lhe', 0))
- entry.lhe_ges = Decimal(request.POST.get('lhe_ges', 0))
- except InvalidOperation:
- return JsonResponse({
- 'status': 'error',
- 'message': 'Invalid numeric value in Helium Input'
- }, status=400)
-
- entry.save()
-
- return JsonResponse({
- 'status': 'success',
- 'id': entry.id,
- 'client_name': entry.client.name,
- 'institute_name': entry.client.institute.name,
- 'date': entry.date.strftime('%Y-%m-%d') if entry.date else '',
- 'month': f"{entry.date.month:02d}" if entry.date else '',
- 'pressure': str(entry.pressure),
- 'purity': str(entry.purity),
- 'druckkorrektur': str(entry.druckkorrektur),
- 'constant_300': str(entry.constant_300),
- 'korrig_druck': str(entry.korrig_druck),
- 'nm3': str(entry.nm3),
- 'lhe': str(entry.lhe),
- 'lhe_zus': str(entry.lhe_zus),
- 'lhe_ges': str(entry.lhe_ges),
- 'notes': entry.notes
- })
-
-
- except model.DoesNotExist:
- return JsonResponse({'status': 'error', 'message': 'Entry not found'}, status=404)
- except Exception as e:
- return JsonResponse({'status': 'error', 'message': str(e)}, status=400)
-
- return JsonResponse({'status': 'error', 'message': 'Invalid request method'}, status=400)
-
-# Delete Entry (Generic)
-def delete_entry(request, model_name):
- if request.method == 'POST':
- try:
- if model_name == 'SecondTableEntry':
- model = SecondTableEntry
- elif model_name == 'ExcelEntry':
- model = ExcelEntry
- else:
- return JsonResponse({'status': 'error', 'message': 'Invalid model'}, status=400)
-
- entry_id = request.POST.get('id')
- entry = model.objects.get(id=entry_id)
- entry.delete()
- return JsonResponse({'status': 'success', 'message': 'Entry deleted'})
-
- except model.DoesNotExist:
- return JsonResponse({'status': 'error', 'message': 'Entry not found'}, status=404)
- except Exception as e:
- return JsonResponse({'status': 'error', 'message': str(e)}, status=400)
-
- return JsonResponse({'status': 'error', 'message': 'Invalid request'}, status=400)
-
-def betriebskosten_list(request):
- items = Betriebskosten.objects.all().order_by('-buchungsdatum')
- return render(request, 'betriebskosten_list.html', {'items': items})
-
-def betriebskosten_create(request):
- if request.method == 'POST':
- try:
- entry_id = request.POST.get('id')
- if entry_id:
- # Update existing entry
- entry = Betriebskosten.objects.get(id=entry_id)
- else:
- # Create new entry
- entry = Betriebskosten()
-
- # Get form data
- buchungsdatum_str = request.POST.get('buchungsdatum')
- rechnungsnummer = request.POST.get('rechnungsnummer')
- kostentyp = request.POST.get('kostentyp')
- betrag = request.POST.get('betrag')
- beschreibung = request.POST.get('beschreibung')
- gas_volume = request.POST.get('gas_volume')
-
- # Validate required fields
- if not all([buchungsdatum_str, rechnungsnummer, kostentyp, betrag]):
- return JsonResponse({'status': 'error', 'message': 'All required fields must be filled'})
-
- # Convert date string to date object
- try:
- buchungsdatum = parse_date(buchungsdatum_str)
- if not buchungsdatum:
- return JsonResponse({'status': 'error', 'message': 'Invalid date format'})
- except (ValueError, TypeError):
- return JsonResponse({'status': 'error', 'message': 'Invalid date format'})
-
- # Set entry values
- entry.buchungsdatum = buchungsdatum
- entry.rechnungsnummer = rechnungsnummer
- entry.kostentyp = kostentyp
- entry.betrag = betrag
- entry.beschreibung = beschreibung
-
- # Only set gas_volume if kostentyp is helium and gas_volume is provided
- if kostentyp == 'helium' and gas_volume:
- entry.gas_volume = gas_volume
- else:
- entry.gas_volume = None
-
- entry.save()
-
- return JsonResponse({
- 'status': 'success',
- 'id': entry.id,
- 'buchungsdatum': entry.buchungsdatum.strftime('%Y-%m-%d'),
- 'rechnungsnummer': entry.rechnungsnummer,
- 'kostentyp_display': entry.get_kostentyp_display(),
- 'gas_volume': str(entry.gas_volume) if entry.gas_volume else '-',
- 'betrag': str(entry.betrag),
- 'beschreibung': entry.beschreibung or ''
- })
-
- except Exception as e:
- return JsonResponse({'status': 'error', 'message': str(e)})
-
- return JsonResponse({'status': 'error', 'message': 'Invalid request method'})
-
-def betriebskosten_delete(request):
- if request.method == 'POST':
- try:
- entry_id = request.POST.get('id')
- entry = Betriebskosten.objects.get(id=entry_id)
- entry.delete()
- return JsonResponse({'status': 'success'})
- except Betriebskosten.DoesNotExist:
- return JsonResponse({'status': 'error', 'message': 'Entry not found'})
- except Exception as e:
- return JsonResponse({'status': 'error', 'message': str(e)})
-
- return JsonResponse({'status': 'error', 'message': 'Invalid request method'})
-
-class CheckSheetView(View):
- def get(self, request):
- # Get current month/year
- current_year = datetime.now().year
- current_month = datetime.now().month
-
- # Get all sheets
- sheets = MonthlySheet.objects.all()
-
- sheet_data = []
- for sheet in sheets:
- cells_count = sheet.cells.count()
- # Count non-empty cells
- non_empty = sheet.cells.exclude(value__isnull=True).count()
-
- sheet_data.append({
- 'id': sheet.id,
- 'year': sheet.year,
- 'month': sheet.month,
- 'month_name': calendar.month_name[sheet.month],
- 'total_cells': cells_count,
- 'non_empty_cells': non_empty,
- 'has_data': non_empty > 0
- })
-
- # Also check what the default view would show
- default_sheet = MonthlySheet.objects.filter(
- year=current_year,
- month=current_month
- ).first()
-
- return JsonResponse({
- 'current_year': current_year,
- 'current_month': current_month,
- 'current_month_name': calendar.month_name[current_month],
- 'default_sheet_exists': default_sheet is not None,
- 'default_sheet_id': default_sheet.id if default_sheet else None,
- 'sheets': sheet_data,
- 'total_sheets': len(sheet_data)
- })
-
-
-class QuickDebugView(View):
- def get(self, request):
- # Get ALL sheets
- sheets = MonthlySheet.objects.all().order_by('year', 'month')
-
- result = {
- 'sheets': []
- }
-
- for sheet in sheets:
- sheet_info = {
- 'id': sheet.id,
- 'display': f"{sheet.year}-{sheet.month:02d}",
- 'url': f"/sheet/{sheet.year}/{sheet.month}/", # CHANGED THIS LINE
- 'sheet_url_pattern': 'sheet/{year}/{month}/', # Add this for clarity
- }
-
- # Count cells with data for first client in top_left table
- first_client = Client.objects.first()
- if first_client:
- test_cells = sheet.cells.filter(
- client=first_client,
- table_type='top_left',
- row_index__in=[8, 9, 10] # Rows 9, 10, 11
- ).order_by('row_index')
-
- cell_values = {}
- for cell in test_cells:
- cell_values[f"row_{cell.row_index}"] = str(cell.value) if cell.value else "Empty"
-
- sheet_info['test_cells'] = cell_values
- else:
- sheet_info['test_cells'] = "No clients"
-
- result['sheets'].append(sheet_info)
-
- return JsonResponse(result)
-class TestFormulaView(View):
- def get(self, request):
- # Test the formula evaluation directly
- test_values = {
- 8: 2, # Row 9 value (0-based index 8)
- 9: 2, # Row 10 value (0-based index 9)
- }
-
- # Test formula "9 + 8" (using 0-based indices)
- formula = "9 + 8"
- result = evaluate_formula(formula, test_values)
-
- return JsonResponse({
- 'test_values': test_values,
- 'formula': formula,
- 'result': str(result),
- 'note': 'Formula uses 0-based indices. 9=Row10, 8=Row9'
- })
-
-
-class SimpleDebugView(View):
- """Simplest debug view to check if things are working"""
- def get(self, request):
- sheet_id = request.GET.get('sheet_id', 1)
-
- try:
- sheet = MonthlySheet.objects.get(id=sheet_id)
-
- # Get first client
- client = Client.objects.first()
- if not client:
- return JsonResponse({'error': 'No clients found'})
-
- # Check a few cells
- cells = Cell.objects.filter(
- sheet=sheet,
- client=client,
- table_type='top_left',
- row_index__in=[8, 9, 10]
- ).order_by('row_index')
-
- cell_data = []
- for cell in cells:
- cell_data.append({
- 'row_index': cell.row_index,
- 'ui_row': cell.row_index + 1,
- 'value': str(cell.value) if cell.value is not None else 'Empty',
- 'cell_id': cell.id
- })
-
- return JsonResponse({
- 'sheet': f"{sheet.year}-{sheet.month}",
- 'sheet_id': sheet.id,
- 'client': client.name,
- 'cells': cell_data,
- 'note': 'Row 8 = UI Row 9, Row 9 = UI Row 10, Row 10 = UI Row 11'
- })
-
- except MonthlySheet.DoesNotExist:
- return JsonResponse({'error': f'Sheet with id {sheet_id} not found'})
-
-def halfyear_settings(request):
- """
- Global settings page: choose a year + first month for the 6-month interval.
- These values are stored in the session and used by other views.
- """
- # Determine available years from your data (use ExcelEntry or SecondTableEntry)
- # Here I use SecondTableEntry; you can switch to ExcelEntry if you prefer.
- available_years_qs = SecondTableEntry.objects.dates('date', 'year', order='DESC')
- available_years = [d.year for d in available_years_qs]
-
- if not available_years:
- available_years = [timezone.now().year]
-
- # Defaults (if nothing in session yet)
- default_year = request.session.get('halfyear_year', available_years[0])
- default_start_month = request.session.get('halfyear_start_month', 1)
-
- if request.method == 'POST':
- year = int(request.POST.get('year', default_year))
- start_month = int(request.POST.get('start_month', default_start_month))
-
- request.session['halfyear_year'] = year
- request.session['halfyear_start_month'] = start_month
-
- # Redirect back to where user came from, or to this page again
- next_url = request.POST.get('next') or request.GET.get('next') or 'halfyear_settings'
- return redirect(next_url)
-
- # Month choices for the dropdown
- month_choices = [(i, calendar.month_name[i]) for i in range(1, 13)]
-
- context = {
- 'available_years': available_years,
- 'selected_year': int(default_year),
- 'selected_start_month': int(default_start_month),
- 'month_choices': month_choices,
- }
- return render(request, 'halfyear_settings.html', context)
\ No newline at end of file
diff --git a/sheets/old/views_v2.py b/sheets/old/views_v2.py
deleted file mode 100644
index 086c96e..0000000
--- a/sheets/old/views_v2.py
+++ /dev/null
@@ -1,4520 +0,0 @@
-from django.shortcuts import render, redirect
-from django.utils.decorators import method_decorator
-from django.views.decorators.csrf import csrf_exempt
-from django.db.models import Sum, Value, DecimalField
-from django.http import JsonResponse
-from django.db.models import Q
-from decimal import Decimal, InvalidOperation
-from django.apps import apps
-from datetime import date, datetime
-import calendar
-from django.utils import timezone
-from django.views.generic import TemplateView, View
-from .models import (
- Client, SecondTableEntry, Institute, ExcelEntry,
- Betriebskosten, MonthlySheet, Cell, CellReference, MonthlySummary
-)
-from django.db.models import Sum
-from django.urls import reverse
-from django.db.models.functions import Coalesce
-from .forms import BetriebskostenForm
-from django.utils.dateparse import parse_date
-from django.contrib.auth.mixins import LoginRequiredMixin
-import json
-FIRST_SHEET_YEAR = 2025
-FIRST_SHEET_MONTH = 1
-
-CLIENT_GROUPS = {
- 'ikp': {
- 'label': 'IKP',
- # exactly as in the Clients admin
- 'names': ['IKP'],
- },
- 'phys_chem_bunt_fohrer': {
- 'label': 'Buntkowsky + Dr. Fohrer',
- # include all variants you might have used for Buntkowsky
- 'names': [
- 'AG Buntk.', # the one in your new entry
- 'AG Buntkowsky.', # from your original list
- 'AG Buntkowsky',
- 'Dr. Fohrer',
- ],
- },
- 'mawi_alff_gutfleisch': {
- 'label': 'Alff + AG Gutfleisch',
- # include both short and full forms
- 'names': [
- 'AG Alff',
- 'AG Gutfl.',
- 'AG Gutfleisch',
- ],
- },
- 'm3_group': {
- 'label': 'M3 Buntkowsky + M3 Thiele + M3 Gutfleisch',
- 'names': [
- 'M3 Buntkowsky',
- 'M3 Thiele',
- 'M3 Gutfleisch',
- ],
- },
-}
-
-# Add this CALCULATION_CONFIG at the top of views.py
-
-CALCULATION_CONFIG = {
- 'top_left': {
- # Row mappings: Django row_index (0-based) to Excel row
- # Excel B4 -> Django row_index 1 (UI row 2)
- # Excel B5 -> Django row_index 2 (UI row 3)
- # Excel B6 -> Django row_index 3 (UI row 4)
-
- # B6 (row_index 3) = B5 (row_index 2) / 0.75
- 3: "2 / 0.75",
-
- # B11 (row_index 10) = B9 (row_index 8)
- 10: "8",
-
- # B14 (row_index 13) = B13 (row_index 12) - B11 (row_index 10) + B12 (row_index 11)
- 13: "12 - 10 + 11",
-
- # Note: B5, B17, B19, B20 require IF logic, so they'll be handled separately
- },
-
- # other tables (top_right, bottom_1, ...) stay as they are
-
-
- ' top_right': {
- # UI Row 1 (Excel Row 4): Stand der Gaszähler (Vormonat) (Nm³)
- 0: {
- 'L': "9 / (9 + 9) if (9 + 9) > 0 else 0", # L4 = L13/(L13+M13)
- 'M': "9 / (9 + 9) if (9 + 9) > 0 else 0", # M4 = M13/(L13+M13)
- 'N': "9 / (9 + 9) if (9 + 9) > 0 else 0", # N4 = N13/(N13+O13)
- 'O': "9 / (9 + 9) if (9 + 9) > 0 else 0", # O4 = O13/(N13+O13)
- 'P': None, # Editable
- 'Q': None, # Editable
- 'R': None, # Editable
- },
-
- # UI Row 2 (Excel Row 5): Gasrückführung (Nm³)
- 1: {
- 'L': "4", # L5 = L8
- 'M': "4", # M5 = L8 (merged)
- 'N': "4", # N5 = N8
- 'O': "4", # O5 = N8 (merged)
- 'P': "4 * 0", # P5 = P8 * P4
- 'Q': "4 * 0", # Q5 = P8 * Q4
- 'R': "4 * 0", # R5 = P8 * R4
- },
-
- # UI Row 3 (Excel Row 6): Rückführung flüssig (Lit. L-He)
- 2: {
- 'L': "4", # L6 = L8 (Sammelrückführungen)
- 'M': "4",
- 'N': "4",
- 'O': "4",
- 'P': "4",
- 'Q': "4",
- 'R': "4",
-},
-
- # UI Row 4 (Excel Row 7): Sonderrückführungen (Lit. L-He) - EDITABLE
- 3: {
- 'L': None,
- 'M': None,
- 'N': None,
- 'O': None,
- 'P': None,
- 'Q': None,
- 'R': None,
- },
-
- # UI Row 5 (Excel Row 8): Sammelrückführungen (Lit. L-He)
- 4: {
- 'L': None, # Will be populated from ExcelEntry
- 'M': None,
- 'N': None,
- 'O': None,
- 'P': None,
- 'Q': None,
- 'R': None,
- },
-
- # UI Row 6 (Excel Row 9): Bestand in Kannen-1 (Lit. L-He) - EDITABLE
- 5: {
- 'L': None,
- 'M': None,
- 'N': None,
- 'O': None,
- 'P': None,
- 'Q': None,
- 'R': None,
- },
-
- # UI Row 7 (Excel Row 10): Summe Bestand (Lit. L-He)
- 6: {
- 'L': None,
- 'M': None,
- 'N': None,
- 'O': None,
- 'P': None,
- 'Q': None,
- 'R': None,
- },
-
- # UI Row 11 (Excel Row 14): Rückführ. Soll (Lit. L-He)
- # handled in calculate_top_right_dependents (merged pairs + M3)
- 10: {
- 'L': None,
- 'M': None,
- 'N': None,
- 'O': None,
- 'P': None,
- 'Q': None,
- 'R': None,
- },
-
- # UI Row 12 (Excel Row 15): Verluste (Soll-Rückf.) (Lit. L-He)
- # handled in calculate_top_right_dependents
- 11: {
- 'L': None,
- 'M': None,
- 'N': None,
- 'O': None,
- 'P': None,
- 'Q': None,
- 'R': None,
- },
-
- # UI Row 14 (Excel Row 17): Kaltgas Rückgabe (Lit. L-He) – Faktor
- # handled in calculate_top_right_dependents (different formulas for pair 1 vs pair 2 + M3)
- 13: {
- 'L': None,
- 'M': None,
- 'N': None,
- 'O': None,
- 'P': None,
- 'Q': None,
- 'R': None,
- },
-
- # UI Row 16 (Excel Row 19): Verbraucherverluste (Liter L-He)
- # handled in calculate_top_right_dependents
- 15: {
- 'L': None,
- 'M': None,
- 'N': None,
- 'O': None,
- 'P': None,
- 'Q': None,
- 'R': None,
- },
-
- # UI Row 17 (Excel Row 20): %
- # handled in calculate_top_right_dependents
- 16: {
- 'L': None,
- 'M': None,
- 'N': None,
- 'O': None,
- 'P': None,
- 'Q': None,
- 'R': None,
- }
-},
- 'bottom_1': {
- 5: "4 + 3 + 2",
- 8: "7 - 6",
- },
- 'bottom_2': {
- 3: "1 + 2",
- 6: "5 - 4",
- },
- 'bottom_3': {
- 2: "0 + 1",
- 5: "3 + 4",
- },
- # Special configuration for summation column (last column)
- 'summation_column': {
- # For each row that should be summed across columns
- 'rows_to_sum': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23], # All rows
- # OR specify specific rows:
- # 'rows_to_sum': [0, 5, 10, 15, 20], # Only specific rows
- # The last column index (0-based)
- 'sum_column_index': 5, # 6th column (0-5) since you have 6 clients
- }
-}
-def build_halfyear_window(interval_year: int, start_month: int):
- """
- Build a list of (year, month) for the 6-month interval, possibly crossing into the next year.
- Example: (2025, 10) -> [(2025,10), (2025,11), (2025,12), (2026,1), (2026,2), (2026,3)]
- """
- window = []
- for offset in range(6):
- total_index = (start_month - 1) + offset # 0-based
- y = interval_year + (total_index // 12)
- m = (total_index % 12) + 1
- window.append((y, m))
- return window
-# ---------------------------------------------------------------------------
-# Halbjahres-Bilanz helpers
-# ---------------------------------------------------------------------------
-
-# You can adjust these indices if needed.
-# Assuming:
-# - bottom_1.table has row "Gasbestand" at some fixed row index,
-# and columns: ... Nm³, Lit. LHe
-GASBESTAND_ROW_INDEX = 9 # <-- adjust if your bottom_1 has a different row index
-GASBESTAND_COL_NM3 = 3 # <-- adjust to the column index for Nm³ in bottom_1
-
-# In top_left / top_right, "Bestand in Kannen-1 (Lit. L-He)" is row_index 5
-BESTAND_KANNEN_ROW_INDEX = 5
-
-def pick_sheet_by_gasbestand(window, sheets_by_ym, prev_sheet):
- """
- Returns the last sheet in the window whose Gasbestand (J36, Nm³ column) != 0.
- If none found, returns prev_sheet (Übertrag_Dez__Vorjahr equivalent).
- """
- for (y, m) in reversed(window):
- sheet = sheets_by_ym.get((y, m))
- if not sheet:
- continue
- gasbestand_nm3 = get_bottom1_value(sheet, GASBESTAND_ROW_INDEX, GASBESTAND_COL_NM3)
- if gasbestand_nm3 != 0:
- return sheet
- return prev_sheet
-def get_bottom1_value(sheet, row_index: int, col_index: int) -> Decimal:
- """Get a numeric value from bottom_1, or 0 if missing."""
- if sheet is None:
- return Decimal('0')
-
- cell = Cell.objects.filter(
- sheet=sheet,
- table_type='bottom_1',
- row_index=row_index,
- column_index=col_index,
- ).first()
-
- if cell is None or cell.value in (None, ''):
- return Decimal('0')
-
- try:
- return Decimal(str(cell.value))
- except Exception:
- return Decimal('0')
-
-# MUST match the column order in your monthly_sheets top-right table
-
-
-
-def get_top_right_value(sheet, client_name: str, row_index: int) -> Decimal:
- """
- Read a numeric value from the top_right table of a MonthlySheet for
- a given client (by column) and row_index.
-
- top_right cells are keyed by (sheet, table_type='top_right',
- row_index, column_index), where column_index is the position of the
- client in HALFYEAR_RIGHT_CLIENTS.
- """
- if sheet is None:
- return Decimal('0')
-
- col_index = RIGHT_CLIENT_INDEX.get(client_name)
- if col_index is None:
- return Decimal('0')
-
- cell = Cell.objects.filter(
- sheet=sheet,
- table_type='top_right',
- row_index=row_index,
- column_index=col_index,
- ).first()
-
- if cell is None or cell.value in (None, ''):
- return Decimal('0')
-
- try:
- return Decimal(str(cell.value))
- except Exception:
- return Decimal('0')
-
-TR_RUECKF_FLUESSIG_ROW = 2 # confirmed by your March value 172.840560
-TR_BESTAND_KANNEN_ROW = 5 # confirmed by your earlier query
-def get_bestand_kannen_for_month(sheet, client_name: str) -> Decimal:
- """
- 'B9' in your description: Bestand in Kannen-1 (Lit. L-He)
- For this implementation we take it from top_left row_index = 5 for that client.
- """
- return get_top_left_value(sheet, client_name, row_index=BESTAND_KANNEN_ROW_INDEX)
-
-from decimal import Decimal
-from django.db.models import Sum
-from django.db.models.functions import Coalesce
-from django.db.models import DecimalField, Value
-
-from .models import MonthlySheet, SecondTableEntry, Client, Cell
-from django.shortcuts import redirect, render
-
-# You already have HALFYEAR_CLIENTS for the left table (AG Vogel, AG Halfm, IKP)
-HALFYEAR_CLIENTS = ["AG Vogel", "AG Halfm", "IKP"]
-
-# NEW: clients for the top-right half-year table
-HALFYEAR_RIGHT_CLIENTS = [
- "Dr. Fohrer",
- "AG Buntk.",
- "AG Alff",
- "AG Gutfl.",
- "M3 Thiele",
- "M3 Buntkowsky",
- "M3 Gutfleisch",
-]
-BOTTOM1_COL_VOLUME = 0
-BOTTOM1_COL_BAR = 1
-BOTTOM1_COL_KORR = 2
-BOTTOM1_COL_NM3 = 3
-BOTTOM1_COL_LHE = 4
-BOTTOM2_ROW_ANLAGE = 0
-BOTTOM2_COL_G39 = 0 # "Gefäss 2,5" (cell id shows column_index=0)
-BOTTOM2_COL_I39 = 1 # "Gefäss 1,0" (cell id shows column_index=1)
-BOTTOM2_ROW_INPUTS = {
- "g39": (0, 0), # row_index=0, column_index=0 (your G39)
- "i39": (0, 1), # row_index=0, column_index=1 (your I39)
-}
-FACTOR_NM3_TO_LHE = Decimal("0.75")
-RIGHT_CLIENT_INDEX = {name: idx for idx, name in enumerate(HALFYEAR_RIGHT_CLIENTS)}
-def halfyear_balance_view(request):
- """
- Read-only Halbjahres-Bilanz view.
-
- LEFT table: AG Vogel / AG Halfm / IKP (exactly as in your last working version)
- RIGHT table: Dr. Fohrer / AG Buntk. / AG Alff / AG Gutfl. /
- M3 Thiele / M3 Buntkowsky / M3 Gutfleisch
- using the Excel formulas you described.
-
- Uses the global 6-month interval from the main page (clients_list).
- """
- # 1) Read half-year interval from the session
- interval_year = request.session.get('halfyear_year')
- interval_start = request.session.get('halfyear_start_month')
-
- if not interval_year or not interval_start:
- # No interval chosen yet -> redirect to main page
- return redirect('clients_list')
-
- interval_year = int(interval_year)
- interval_start = int(interval_start)
-
- # You already have this helper in your code
- window = build_halfyear_window(interval_year, interval_start)
- # window = [(y1, m1), (y2, m2), ..., (y6, m6)]
-
- # (Year, month) of the first month
- start_year, start_month = window[0]
-
- # Previous month (for "Stand ... (Vorjahr)" and "Best. in Kannen Vormonat")
- prev_total_index = (start_month - 1) - 1 # one month back, 0-based
- if prev_total_index >= 0:
- prev_year = start_year + (prev_total_index // 12)
- prev_month = (prev_total_index % 12) + 1
- else:
- prev_year = start_year - 1
- prev_month = 12
-
- # Load MonthlySheet objects for the window and for the previous month
- sheets_by_ym = {}
- for (y, m) in window:
- sheet = MonthlySheet.objects.filter(year=y, month=m).first()
- sheets_by_ym[(y, m)] = sheet
-
- prev_sheet = MonthlySheet.objects.filter(year=prev_year, month=prev_month).first()
- def pick_bottom2_from_window(window, sheets_by_ym, prev_sheet):
- # choose sheet (same logic you already use)
- chosen = None
- for (y, m) in reversed(window):
- s = sheets_by_ym.get((y, m))
- # use your existing condition for choosing month
- if s:
- chosen = s
- break
- if chosen is None:
- chosen = prev_sheet
-
- # Now read the two inputs safely
- bottom2_inputs = {}
- for key, (row_idx, col_idx) in BOTTOM2_ROW_INPUTS.items():
- bottom2_inputs[key] = get_bottom2_value(chosen, row_idx, col_idx)
-
- return chosen, bottom2_inputs
-
-
- chosen_sheet_bottom2, bottom2_inputs = pick_bottom2_from_window(window, sheets_by_ym, prev_sheet)
- bottom2_g39 = bottom2_inputs["g39"]
- bottom2_i39 = bottom2_inputs["i39"]
- # ----------------------------
- # HALF-YEAR BOTTOM TABLE 1 (Bilanz) - Read only
- # ----------------------------
- chosen_sheet_bottom1 = pick_sheet_by_gasbestand(window, sheets_by_ym, prev_sheet)
-
- # IMPORTANT: define which bottom_1 row_index corresponds to Excel rows 27..35
- # If your bottom_1 starts at Excel row 27 => row_index 0 == Excel 27
- # then row_index = excel_row - 27
- BOTTOM1_EXCEL_START_ROW = 27
-
- bottom1_excel_rows = list(range(27, 37)) # 27..36
- BOTTOM1_LABELS = [
- "Batterie 1",
- "2",
- "3",
- "4",
- "5",
- "Batterie Links",
- "2 Bündel",
- "2 Ballone",
- "Reingasspeicher",
- "Gasbestand",
- ]
-
- BOTTOM1_VOLUMES = [
- Decimal("2.4"),
- Decimal("5.1"),
- Decimal("4.0"),
- Decimal("1.0"),
- Decimal("4.0"),
- Decimal("0.6"),
- Decimal("1.2"),
- Decimal("20.0"),
- Decimal("5.0"),
- None, # Gasbestand row has no volume
- ]
- nm3_sum_27_35 = Decimal("0")
- lhe_sum_27_35 = Decimal("0")
- bottom1_rows = []
-
- for excel_row in bottom1_excel_rows:
- row_index = excel_row - BOTTOM1_EXCEL_START_ROW
-
- chosen_sheet_bottom1 = None
- for (y, m) in reversed(window):
- s = sheets_by_ym.get((y, m))
- gasbestand = get_bottom1_value(s, GASBESTAND_ROW_INDEX, GASBESTAND_COL_NM3) # J36 (Nm3)
- if gasbestand != 0:
- chosen_sheet_bottom1 = s
- break
-
- if chosen_sheet_bottom1 is None:
- chosen_sheet_bottom1 = prev_sheet
-
- # Normal rows (27..35): read from chosen sheet and accumulate sums
- if excel_row != 36:
- nm3_val = get_bottom1_value(chosen_sheet_bottom1, row_index, BOTTOM1_COL_NM3)
- lhe_val = get_bottom1_value(chosen_sheet_bottom1, row_index, BOTTOM1_COL_LHE)
-
- nm3_sum_27_35 += nm3_val
- lhe_sum_27_35 += lhe_val
-
- bottom1_rows.append({
- "label": BOTTOM1_LABELS[row_index],
- "volume": BOTTOM1_VOLUMES[row_index],
- "bar": get_bottom1_value(chosen_sheet_bottom1, row_index, BOTTOM1_COL_BAR),
- "korr": get_bottom1_value(chosen_sheet_bottom1, row_index, BOTTOM1_COL_KORR),
- "nm3": nm3_val,
- "lhe": lhe_val,
- })
-
- # Gasbestand row (36): show sums (J36 = SUM(J27:J35), K36 = SUM(K27:K35))
- else:
- bottom1_rows.append({
- "label": "Gasbestand",
- "volume": "",
- "bar": "",
- "korr": "",
- "nm3": nm3_sum_27_35,
- "lhe": lhe_sum_27_35,
- })
- start_sheet = sheets_by_ym.get((start_year, start_month))
- # ------------------------------------------------------------
- # Bottom Table 2 (Halbjahres Bilanz) – server-side recalcBottom2()
- # ------------------------------------------------------------
-
- FACTOR_BT2 = Decimal("0.75")
-
- # 1) Helper: pick last-nonzero value of bottom_2 row0 col0/col1 from the window (fallback: prev_sheet)
- def pick_last_nonzero_bottom2(row_index: int, col_index: int) -> Decimal:
- # Scan from last month in window backwards
- for (y, m) in reversed(window):
- s = sheets_by_ym.get((y, m))
- if not s:
- continue
- v = get_bottom2_value(s, row_index, col_index)
- if v is not None and v != 0:
- return v
- # fallback to month before window
- v_prev = get_bottom2_value(prev_sheet, row_index, col_index)
- return v_prev if v_prev is not None else Decimal("0")
-
- # 2) K38 comes from Overall Summary: "Summe Bestand (Lit. L-He)"
- # Find it from your already built overall summary rows list.
-
- k38 = Decimal("0")
- j38 = Decimal("0")
- # 3) Inputs G39 / I39 (picked from last non-zero month in window)
- g39 = pick_last_nonzero_bottom2(row_index=0, col_index=0) # G39
- i39 = pick_last_nonzero_bottom2(row_index=0, col_index=1) # I39
-
- k39 = (g39 or Decimal("0")) + (i39 or Decimal("0"))
- j39 = k39 * FACTOR_BT2
-
- # 4) +Kaltgas (row 40)
- # JS:
- # g40 = (2500 - g39)/100*10
- # i40 = (1000 - i39)/100*10
- g40 = None
- i40 = None
- if g39 is not None:
- g40 = (Decimal("2500") - g39) / Decimal("100") * Decimal("10")
- if i39 is not None:
- i40 = (Decimal("1000") - i39) / Decimal("100") * Decimal("10")
-
- k40 = (g40 or Decimal("0")) + (i40 or Decimal("0"))
- j40 = k40 * FACTOR_BT2
-
- # 5) Bestand flüssig He (row 43)
- k43 = (
- (k38 or Decimal("0")) +
- (k39 or Decimal("0")) +
- (k40 or Decimal("0"))
- )
- j43 = k43 * FACTOR_BT2
-
- # 6) Gesamtbestand neu (row 44) = Gasbestand(Lit) from Bottom Table 1 + k43
- gasbestand_lit = Decimal("0")
- for r in bottom1_rows:
- if (r.get("label") or "").strip().startswith("Gasbestand"):
- gasbestand_lit = r.get("lhe") or Decimal("0")
- break
-
- k44 = (gasbestand_lit or Decimal("0")) + (k43 or Decimal("0"))
- j44 = k44 * FACTOR_BT2
-
- bottom2 = {
- "j38": j38, "k38": k38,
- "g39": g39, "i39": i39, "j39": j39, "k39": k39,
- "g40": g40, "i40": i40, "j40": j40, "k40": k40,
- "j43": j43, "k43": k43,
- "j44": j44, "k44": k44,
- }
-
- # ------------------------------------------------------------------
- # 2) LEFT TABLE (your existing, working logic)
- # ------------------------------------------------------------------
- HALFYEAR_CLIENTS_LEFT = ["AG Vogel", "AG Halfm", "IKP"]
-
- # We'll collect client-wise values first for clarity.
- client_data_left = {name: {} for name in HALFYEAR_CLIENTS_LEFT}
-
- # --- Row B3: Stand der Gaszähler (Nm³)
- # = MAX(B3 from previous month, and B3 from each of the 6 months in the window)
- # row_index 0 in top_left = "Stand der Gaszähler (Nm³)"
- months_for_max = [(prev_year, prev_month)] + window
-
- for cname in HALFYEAR_CLIENTS_LEFT:
- max_val = Decimal('0')
- for (y, m) in months_for_max:
- sheet = sheets_by_ym.get((y, m))
- if sheet is None and (y, m) == (prev_year, prev_month):
- sheet = prev_sheet
- val_b3 = get_top_left_value(sheet, cname, row_index=0)
- if val_b3 > max_val:
- max_val = val_b3
- client_data_left[cname]['stand_gas'] = max_val
-
- # --- Row B4: Stand der Gaszähler (Vorjahr) (Nm³) -> previous month same row ---
- for cname in HALFYEAR_CLIENTS_LEFT:
- val_b4 = get_top_left_value(prev_sheet, cname, row_index=0)
- client_data_left[cname]['stand_gas_prev'] = val_b4
-
- # --- Row B5: Gasrückführung (Nm³) = B3 - B4 ---
- for cname in HALFYEAR_CLIENTS_LEFT:
- b3 = client_data_left[cname]['stand_gas']
- b4 = client_data_left[cname]['stand_gas_prev']
- client_data_left[cname]['gasrueckf'] = b3 - b4
-
- # --- Row B6: Rückführung flüssig (Lit. L-He) = B5 / 0.75 ---
- for cname in HALFYEAR_CLIENTS_LEFT:
- b5 = client_data_left[cname]['gasrueckf']
- client_data_left[cname]['rueckf_fluessig'] = (b5 / Decimal('0.75')) if b5 != 0 else Decimal('0')
-
- # --- Row B7: Sonderrückführungen (Lit. L-He) = sum over 6 months of that row ---
- # That row index is 4 in your top_left table.
- for cname in HALFYEAR_CLIENTS_LEFT:
- sonder_total = Decimal('0')
- for (y, m) in window:
- sheet = sheets_by_ym.get((y, m))
- if sheet:
- sonder_total += get_top_left_value(sheet, cname, row_index=4)
- client_data_left[cname]['sonder'] = sonder_total
-
- # --- Row B8: Bestand in Kannen-1 (Lit. L-He) ---
- # Excel-style logic with Gasbestand (J36) and fallback to previous month.
- for cname in HALFYEAR_CLIENTS_LEFT:
- chosen_value = None
-
- # Go from last month (window[5]) backwards to first (window[0])
- for (y, m) in reversed(window):
- sheet = sheets_by_ym.get((y, m))
- gasbestand = get_bottom1_value(sheet, GASBESTAND_ROW_INDEX, GASBESTAND_COL_NM3)
- if gasbestand != 0:
- chosen_value = get_bestand_kannen_for_month(sheet, cname)
- break
-
- # If still None -> use previous month (Übertrag_Dez__Vorjahr equivalent)
- if chosen_value is None:
- sheet_prev = prev_sheet
- chosen_value = get_bestand_kannen_for_month(sheet_prev, cname)
-
- client_data_left[cname]['bestand_kannen'] = chosen_value if chosen_value is not None else Decimal('0')
-
- # --- Row B9: Summe Bestand (Lit. L-He) = equal to previous row ---
- for cname in HALFYEAR_CLIENTS_LEFT:
- client_data_left[cname]['summe_bestand'] = client_data_left[cname]['bestand_kannen']
-
- # --- Row B10: Best. in Kannen Vormonat (Lit. L-He)
- # = Bestand in Kannen-1 from the month BEFORE the window (prev_year, prev_month)
- for cname in HALFYEAR_CLIENTS_LEFT:
- client_data_left[cname]['best_kannen_vormonat'] = get_bestand_kannen_for_month(prev_sheet, cname)
-
- # --- Row B13: Bezug (Liter L-He) ---
- for cname in HALFYEAR_CLIENTS_LEFT:
- total_bezug = Decimal('0')
- for (y, m) in window:
- qs = SecondTableEntry.objects.filter(
- client__name=cname,
- date__year=y,
- date__month=m,
- ).aggregate(
- total=Coalesce(Sum('lhe_output'), Value(0, output_field=DecimalField()))
- )
- total_bezug += Decimal(str(qs['total']))
- client_data_left[cname]['bezug'] = total_bezug
-
- # --- Row B14: Rückführ. Soll (Lit. L-He) = Bezug - Summe Bestand + Best. in Kannen Vormonat ---
- for cname in HALFYEAR_CLIENTS_LEFT:
- b13 = client_data_left[cname]['bezug']
- b11 = client_data_left[cname]['summe_bestand']
- b12 = client_data_left[cname]['best_kannen_vormonat']
- client_data_left[cname]['rueckf_soll'] = b13 - b11 + b12
-
- # --- Row B15: Verluste (Soll-Rückf.) (Lit. L-He) = B14 - B6 ---
- for cname in HALFYEAR_CLIENTS_LEFT:
- b14 = client_data_left[cname]['rueckf_soll']
- b6 = client_data_left[cname]['rueckf_fluessig']
- client_data_left[cname]['verluste'] = b14 - b6
-
- # --- Row B16: Füllungen warm (Lit. L-He) = sum over 6 months (row_index=11) ---
- for cname in HALFYEAR_CLIENTS_LEFT:
- total_warm = Decimal('0')
- for (y, m) in window:
- sheet = sheets_by_ym.get((y, m))
- total_warm += get_top_left_value(sheet, cname, row_index=11)
- client_data_left[cname]['fuellungen_warm'] = total_warm
-
- # --- Row B17: Kaltgas Rückgabe (Lit. L-He) = Bezug * 0.06 ---
- factor = Decimal('0.06')
- for cname in HALFYEAR_CLIENTS_LEFT:
- b13 = client_data_left[cname]['bezug']
- client_data_left[cname]['kaltgas_rueckgabe'] = b13 * factor
-
- # --- Row B18: Verbraucherverluste (Liter L-He) = Verluste - Kaltgas Rückgabe ---
- for cname in HALFYEAR_CLIENTS_LEFT:
- b15 = client_data_left[cname]['verluste']
- b17 = client_data_left[cname]['kaltgas_rueckgabe']
- client_data_left[cname]['verbraucherverluste'] = b15 - b17
-
- # --- Row B19: % = Verbraucherverluste / Bezug ---
- for cname in HALFYEAR_CLIENTS_LEFT:
- bezug = client_data_left[cname]['bezug']
- verb = client_data_left[cname]['verbraucherverluste']
- if bezug != 0:
- client_data_left[cname]['percent'] = verb / bezug
- else:
- client_data_left[cname]['percent'] = None
-
- # Build LEFT rows structure
- left_row_defs = [
- ('Stand der Gaszähler (Nm³)', 'stand_gas'),
- ('Stand der Gaszähler (Vorjahr) (Nm³)', 'stand_gas_prev'),
- ('Gasrückführung (Nm³)', 'gasrueckf'),
- ('Rückführung flüssig (Lit. L-He)', 'rueckf_fluessig'),
- ('Sonderrückführungen (Lit. L-He)', 'sonder'),
- ('Bestand in Kannen-1 (Lit. L-He)', 'bestand_kannen'),
- ('Summe Bestand (Lit. L-He)', 'summe_bestand'),
- ('Best. in Kannen Vormonat (Lit. L-He)', 'best_kannen_vormonat'),
- ('Bezug (Liter L-He)', 'bezug'),
- ('Rückführ. Soll (Lit. L-He)', 'rueckf_soll'),
- ('Verluste (Soll-Rückf.) (Lit. L-He)', 'verluste'),
- ('Füllungen warm (Lit. L-He)', 'fuellungen_warm'),
- ('Kaltgas Rückgabe (Lit. L-He) – Faktor', 'kaltgas_rueckgabe'),
- ('Verbraucherverluste (Liter L-He)', 'verbraucherverluste'),
- ('%', 'percent'),
- ]
-
- rows_left = []
- for label, key in left_row_defs:
- values = [client_data_left[cname][key] for cname in HALFYEAR_CLIENTS_LEFT]
- if key == 'percent':
- total_bezug = sum((client_data_left[c]['bezug'] for c in HALFYEAR_CLIENTS_LEFT), Decimal('0'))
- total_verb = sum((client_data_left[c]['verbraucherverluste'] for c in HALFYEAR_CLIENTS_LEFT), Decimal('0'))
- total = (total_verb / total_bezug) if total_bezug != 0 else None
- else:
- total = sum((v for v in values if v is not None), Decimal('0'))
- rows_left.append({
- 'label': label,
- 'values': values,
- 'total': total,
- 'is_percent': key == 'percent',
- })
-
- # ------------------------------------------------------------------
- # 3) RIGHT TABLE (top-right half-year aggregation)
- # ------------------------------------------------------------------
- RIGHT_CLIENTS = HALFYEAR_RIGHT_CLIENTS # for brevity
-
- right_data = {name: {} for name in RIGHT_CLIENTS}
-
- # --- Bezug (Liter L-He) for each right client (same as for left) ---
- for cname in RIGHT_CLIENTS:
- total_bezug = Decimal('0')
- for (y, m) in window:
- qs = SecondTableEntry.objects.filter(
- client__name=cname,
- date__year=y,
- date__month=m,
- ).aggregate(
- total=Coalesce(Sum('lhe_output'), Value(0, output_field=DecimalField()))
- )
- total_bezug += Decimal(str(qs['total']))
- right_data[cname]['bezug'] = total_bezug
- def find_bestand_from_window(reference_client: str) -> Decimal:
- """
- Implements:
- WENN(last_month!J36=0; WENN(prev_month!J36=0; ...; prev_sheet!9); last_month!9)
- reference_client decides which column (L/N/P/Q/R) we read from monthly top_right row_index=5.
- """
- # scan backward through window
- for (y, m) in reversed(window):
- sh = sheets_by_ym.get((y, m))
- if not sh:
- continue
- gasbestand = get_bottom1_value(sh, GASBESTAND_ROW_INDEX, GASBESTAND_COL_NM3)
- if gasbestand != 0:
- return get_top_right_value(sh, reference_client, TR_BESTAND_KANNEN_ROW)
-
- # fallback to previous month (Übertrag_Dez__Vorjahr equivalent)
- return get_top_right_value(prev_sheet, reference_client, TR_BESTAND_KANNEN_ROW)
-
- # Fohrer+Buntk merged: BOTH use Fohrer column (L9)
- val_L = find_bestand_from_window("Dr. Fohrer")
- right_data["Dr. Fohrer"]["bestand_kannen"] = val_L
- right_data["AG Buntk."]["bestand_kannen"] = val_L
-
- # Alff+Gutfl merged: BOTH use Alff column (N9)
- val_N = find_bestand_from_window("AG Alff")
- right_data["AG Alff"]["bestand_kannen"] = val_N
- right_data["AG Gutfl."]["bestand_kannen"] = val_N
-
- # M3 each uses its own column (P9/Q9/R9)
- right_data["M3 Thiele"]["bestand_kannen"] = find_bestand_from_window("M3 Thiele")
- right_data["M3 Buntkowsky"]["bestand_kannen"] = find_bestand_from_window("M3 Buntkowsky")
- right_data["M3 Gutfleisch"]["bestand_kannen"] = find_bestand_from_window("M3 Gutfleisch")
- # Helper for pair shares (L13/($L13+$M13), etc.)
- def pair_share(c1, c2):
- total = right_data[c1]['bezug'] + right_data[c2]['bezug']
- if total == 0:
- return (Decimal('0'), Decimal('0'))
- return (
- right_data[c1]['bezug'] / total,
- right_data[c2]['bezug'] / total,
- )
-
- # --- "Stand der Gaszähler (Vorjahr) (Nm³)" row: share based on Bezug ---
- # Dr. Fohrer / AG Buntk.
- s_fohrer, s_buntk = pair_share("Dr. Fohrer", "AG Buntk.")
- right_data["Dr. Fohrer"]['stand_prev_share'] = s_fohrer
- right_data["AG Buntk."]['stand_prev_share'] = s_buntk
-
- # AG Alff / AG Gutfl.
- s_alff, s_gutfl = pair_share("AG Alff", "AG Gutfl.")
- right_data["AG Alff"]['stand_prev_share'] = s_alff
- right_data["AG Gutfl."]['stand_prev_share'] = s_gutfl
-
- # M3 Thiele / M3 Buntkowsky / M3 Gutfleisch → empty in Excel → None
- for cname in ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"]:
- right_data[cname]['stand_prev_share'] = None
-
- # --- Rückführung flüssig per month (raw sums) ---
- # top_right row_index=2 is "Rückführung flüssig (Lit. L-He)"
-
- # --- Sonderrückführungen (row_index=3 in top_right) ---
- for cname in RIGHT_CLIENTS:
- sonder_total = Decimal('0')
- for (y, m) in window:
- sheet = sheets_by_ym.get((y, m))
- if sheet:
- sonder_total += get_top_right_value(sheet, cname, row_index=3)
- right_data[cname]['sonder'] = sonder_total
-
- # --- Sammelrückführung (row_index=4 in top_right), grouped & merged ---
- # Group 1: Dr. Fohrer + AG Buntk.
- group1_total = Decimal('0')
- for (y, m) in window:
- sheet = sheets_by_ym.get((y, m))
- if sheet:
- group1_total += get_top_right_value(sheet, "Dr. Fohrer", row_index=4)
- right_data["Dr. Fohrer"]['sammel'] = group1_total
- right_data["AG Buntk."]['sammel'] = group1_total
-
- # Group 2: AG Alff + AG Gutfl.
- group2_total = Decimal('0')
- for (y, m) in window:
- sheet = sheets_by_ym.get((y, m))
- if sheet:
- group2_total += get_top_right_value(sheet, "AG Alff", row_index=4)
- right_data["AG Alff"]['sammel'] = group2_total
- right_data["AG Gutfl."]['sammel'] = group2_total
-
- # Group 3: M3 Thiele + M3 Buntkowsky + M3 Gutfleisch
- group3_total = Decimal('0')
- for (y, m) in window:
- sheet = sheets_by_ym.get((y, m))
- if sheet:
- group3_total += get_top_right_value(sheet, "M3 Thiele", row_index=4)
- right_data["M3 Thiele"]['sammel'] = group3_total
- right_data["M3 Buntkowsky"]['sammel'] = group3_total
- right_data["M3 Gutfleisch"]['sammel'] = group3_total
- def safe_div(a: Decimal, b: Decimal) -> Decimal:
- return (a / b) if b != 0 else Decimal("0")
-
- # --- Rückführung flüssig (Lit. L-He) for Halbjahres-Bilanz top-right ---
- # Uses your exact formulas.
-
- # 1) Fohrer / Buntk split by BEZUG share times group SAMMEL (L8)
- L13 = right_data["Dr. Fohrer"]["bezug"]
- M13 = right_data["AG Buntk."]["bezug"]
- L8 = right_data["Dr. Fohrer"]["sammel"] # merged group total
-
- den = (L13 + M13)
- right_data["Dr. Fohrer"]["rueckf_fluessig"] = (safe_div(L13, den) * L8) if den != 0 else Decimal("0")
- right_data["AG Buntk."]["rueckf_fluessig"] = (safe_div(M13, den) * L8) if den != 0 else Decimal("0")
-
- # 2) Alff / Gutfl split by BEZUG share times group SAMMEL (N8)
- N13 = right_data["AG Alff"]["bezug"]
- O13 = right_data["AG Gutfl."]["bezug"]
- N8 = right_data["AG Alff"]["sammel"] # merged group total
-
- den = (N13 + O13)
- right_data["AG Alff"]["rueckf_fluessig"] = (safe_div(N13, den) * N8) if den != 0 else Decimal("0")
- right_data["AG Gutfl."]["rueckf_fluessig"] = (safe_div(O13, den) * N8) if den != 0 else Decimal("0")
-
- # 3) M3 Thiele = sum of monthly Rückführung flüssig (monthly top_right row_index=2) over window
- P6_sum = Decimal("0")
- for (y, m) in window:
- sh = sheets_by_ym.get((y, m))
- P6_sum += get_top_right_value(sh, "M3 Thiele", TR_RUECKF_FLUESSIG_ROW)
- right_data["M3 Thiele"]["rueckf_fluessig"] = P6_sum
-
- # 4) M3 Buntkowsky / M3 Gutfleisch split by BEZUG share times M3-group SAMMEL (P8)
- P13 = right_data["M3 Thiele"]["bezug"]
- Q13 = right_data["M3 Buntkowsky"]["bezug"]
- R13 = right_data["M3 Gutfleisch"]["bezug"]
- P8 = right_data["M3 Thiele"]["sammel"] # merged group total
-
- den = (P13 + Q13 + R13)
- right_data["M3 Buntkowsky"]["rueckf_fluessig"] = (safe_div(Q13, den) * P8) if den != 0 else Decimal("0")
- right_data["M3 Gutfleisch"]["rueckf_fluessig"] = (safe_div(R13, den) * P8) if den != 0 else Decimal("0")
- # --- Bestand in Kannen-1 (Lit. L-He) for right table (grouped) ---
- # Use Gasbestand (J36) and fallback logic, but now reading top_right B9 for each group.
- TOP_RIGHT_ROW_BESTAND_KANNEN = 6 # <-- most likely correct in your setup
-
- def pick_bestand_top_right(base_client: str) -> Decimal:
- # Go from last month in window backwards: if Gasbestand != 0, use that month's Bestand in Kannen
- for (y, m) in reversed(window):
- sh = sheets_by_ym.get((y, m))
- if not sh:
- continue
-
- gasbestand = get_bottom1_value(sh, GASBESTAND_ROW_INDEX, GASBESTAND_COL_NM3)
- if gasbestand != 0:
- return get_top_right_value(sh, base_client, TOP_RIGHT_ROW_BESTAND_KANNEN)
-
- # Fallback to previous month (Übertrag_Dez__Vorjahr equivalent)
- return get_top_right_value(prev_sheet, base_client, TOP_RIGHT_ROW_BESTAND_KANNEN)
-
- # Group 1 merged (Fohrer + Buntk.)
- g1_best = pick_bestand_top_right("Dr. Fohrer")
- right_data["Dr. Fohrer"]["bestand_kannen"] = g1_best
- right_data["AG Buntk."]["bestand_kannen"] = g1_best
-
- # Group 2 merged (Alff + Gutfl.)
- g2_best = pick_bestand_top_right("AG Alff")
- right_data["AG Alff"]["bestand_kannen"] = g2_best
- right_data["AG Gutfl."]["bestand_kannen"] = g2_best
-
- # Group 3 merged (M3 Thiele + M3 Buntkowsky + M3 Gutfleisch)
- g3_best = pick_bestand_top_right("M3 Thiele")
- right_data["M3 Thiele"]["bestand_kannen"] = g3_best
- right_data["M3 Buntkowsky"]["bestand_kannen"] = g3_best
- right_data["M3 Gutfleisch"]["bestand_kannen"] = g3_best
-
- # Summe Bestand = same as previous row
- for cname in RIGHT_CLIENTS:
- right_data[cname]['summe_bestand'] = right_data[cname]['bestand_kannen']
-
- # Best. in Kannen Vormonat (Lit. L-He) from previous month top_right row_index=7
- g1_prev = get_top_right_value(prev_sheet, "Dr. Fohrer", TOP_RIGHT_ROW_BESTAND_KANNEN)
- right_data["Dr. Fohrer"]['best_kannen_vormonat'] = g1_prev
- right_data["AG Buntk."]['best_kannen_vormonat'] = g1_prev
-
- g2_prev = get_top_right_value(prev_sheet, "AG Alff", TOP_RIGHT_ROW_BESTAND_KANNEN)
- right_data["AG Alff"]['best_kannen_vormonat'] = g2_prev
- right_data["AG Gutfl."]['best_kannen_vormonat'] = g2_prev
-
-
- # Group 1 merged (Fohrer + Buntk.)
- g1_prev = get_top_right_value(prev_sheet, "Dr. Fohrer", TOP_RIGHT_ROW_BESTAND_KANNEN)
- right_data["Dr. Fohrer"]["best_kannen_vormonat"] = g1_prev
- right_data["AG Buntk."]["best_kannen_vormonat"] = g1_prev
-
- # Group 2 merged (Alff + Gutfl.)
- g2_prev = get_top_right_value(prev_sheet, "AG Alff", TOP_RIGHT_ROW_BESTAND_KANNEN)
- right_data["AG Alff"]["best_kannen_vormonat"] = g2_prev
- right_data["AG Gutfl."]["best_kannen_vormonat"] = g2_prev
-
- # Group 3 UNMERGED (each one reads its own cell)
- right_data["M3 Thiele"]["best_kannen_vormonat"] = get_top_right_value(prev_sheet, "M3 Thiele", TOP_RIGHT_ROW_BESTAND_KANNEN)
- right_data["M3 Buntkowsky"]["best_kannen_vormonat"] = get_top_right_value(prev_sheet, "M3 Buntkowsky", TOP_RIGHT_ROW_BESTAND_KANNEN)
- right_data["M3 Gutfleisch"]["best_kannen_vormonat"] = get_top_right_value(prev_sheet, "M3 Gutfleisch", TOP_RIGHT_ROW_BESTAND_KANNEN)
-
- # --- Rückführ. Soll (Lit. L-He) according to your formulas ---
-
- # Group 1: Dr. Fohrer / AG Buntk.
- total_bestand_1 = right_data["Dr. Fohrer"]['summe_bestand']
- best_vormonat_1 = right_data["Dr. Fohrer"]['best_kannen_vormonat']
- diff1 = total_bestand_1 - best_vormonat_1
- share_fohrer = right_data["Dr. Fohrer"]['stand_prev_share'] or Decimal('0')
-
- right_data["Dr. Fohrer"]['rueckf_soll'] = (
- right_data["Dr. Fohrer"]['bezug'] - diff1 * share_fohrer
- )
- right_data["AG Buntk."]['rueckf_soll'] = (
- right_data["AG Buntk."]['bezug'] - total_bestand_1 + best_vormonat_1
- )
-
- # Group 2: AG Alff / AG Gutfl.
- total_bestand_2 = right_data["AG Alff"]['summe_bestand']
- best_vormonat_2 = right_data["AG Alff"]['best_kannen_vormonat']
- diff2 = total_bestand_2 - best_vormonat_2
- share_alff = right_data["AG Alff"]['stand_prev_share'] or Decimal('0')
- share_gutfl = right_data["AG Gutfl."]['stand_prev_share'] or Decimal('0')
-
- right_data["AG Alff"]['rueckf_soll'] = (
- right_data["AG Alff"]['bezug'] - diff2 * share_alff
- )
- right_data["AG Gutfl."]['rueckf_soll'] = (
- right_data["AG Gutfl."]['bezug'] - diff2 * share_gutfl
- )
-
- # Group 3: M3 Thiele / M3 Buntkowsky / M3 Gutfleisch
- for cname in ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"]:
- b13 = right_data[cname]['bezug']
- b12 = right_data[cname]['best_kannen_vormonat']
- b11 = right_data[cname]['summe_bestand']
- # Excel: P13+P12-P11 etc.
- right_data[cname]['rueckf_soll'] = b13 + b12 - b11
-
- # --- Verluste (Soll-Rückf.) (Lit. L-He) = B14 - B6 - B7 ---
- for cname in RIGHT_CLIENTS:
- b14 = right_data[cname]['rueckf_soll']
- b6 = right_data[cname]['rueckf_fluessig']
- b7 = right_data[cname]['sonder']
- right_data[cname]['verluste'] = b14 - b6 - b7
-
- # --- Füllungen warm (Lit. L-He) = sum of monthly 'Füllungen warm' (row_index=11 top_right) ---
- for cname in RIGHT_CLIENTS:
- total_warm = Decimal('0')
- for (y, m) in window:
- sheet = sheets_by_ym.get((y, m))
- if sheet:
- total_warm += get_top_right_value(sheet, cname, row_index=11)
- right_data[cname]['fuellungen_warm'] = total_warm
-
- # --- Kaltgas Rückgabe (Lit. L-He) – Faktor = Bezug * 0.06 ---
- for cname in RIGHT_CLIENTS:
- b13 = right_data[cname]['bezug']
- right_data[cname]['kaltgas_rueckgabe'] = b13 * factor
-
- # --- Verbraucherverluste (Liter L-He) = Verluste - Kaltgas Rückgabe ---
- for cname in RIGHT_CLIENTS:
- b15 = right_data[cname]['verluste']
- b17 = right_data[cname]['kaltgas_rueckgabe']
- right_data[cname]['verbraucherverluste'] = b15 - b17
-
- # --- % = Verbraucherverluste / Bezug ---
- for cname in RIGHT_CLIENTS:
- bezug = right_data[cname]['bezug']
- verb = right_data[cname]['verbraucherverluste']
- if bezug != 0:
- right_data[cname]['percent'] = verb / bezug
- else:
- right_data[cname]['percent'] = None
-
- # Build RIGHT rows structure
- right_row_defs = [
- ('Stand der Gaszähler (Vorjahr) (Nm³)', 'stand_prev_share'),
- # We skip the pure-text "Gasrückführung (Nm³)" line here,
- # because it’s only text (Aufteilung nach Verbrauch / Gaszähler)
- # and easier to render directly in the template if needed.
- ('Rückführung flüssig (Lit. L-He)', 'rueckf_fluessig'),
- ('Sonderrückführungen (Lit. L-He)', 'sonder'),
- ('Sammelrückführung (Lit. L-He)', 'sammel'),
- ('Bestand in Kannen-1 (Lit. L-He)', 'bestand_kannen'),
- ('Summe Bestand (Lit. L-He)', 'summe_bestand'),
- ('Best. in Kannen Vormonat (Lit. L-He)', 'best_kannen_vormonat'),
- ('Bezug (Liter L-He)', 'bezug'),
- ('Rückführ. Soll (Lit. L-He)', 'rueckf_soll'),
- ('Verluste (Soll-Rückf.) (Lit. L-He)', 'verluste'),
- ('Füllungen warm (Lit. L-He)', 'fuellungen_warm'),
- ('Kaltgas Rückgabe (Lit. L-He) – Faktor', 'kaltgas_rueckgabe'),
- ('Verbraucherverluste (Liter L-He)', 'verbraucherverluste'),
- ('%', 'percent'),
- ]
-
- rows_right = []
- for label, key in right_row_defs:
- values = [right_data[cname].get(key) for cname in RIGHT_CLIENTS]
-
- if key == 'percent':
- total_bezug = sum((right_data[c]['bezug'] for c in RIGHT_CLIENTS), Decimal('0'))
- total_verb = sum((right_data[c]['verbraucherverluste'] for c in RIGHT_CLIENTS), Decimal('0'))
- total = (total_verb / total_bezug) if total_bezug != 0 else None
- else:
- total = sum((v for v in values if isinstance(v, Decimal)), Decimal('0'))
-
- rows_right.append({
- 'label': label,
- 'values': values,
- 'total': total,
- 'is_percent': key == 'percent',
- })
- SUM_TABLE_ROWS = [
- ("Rückführung flüssig (Lit. L-He)", "rueckf_fluessig"),
- ("Sonderrückführungen (Lit. L-He)", "sonder"),
- ("Sammelrückführungen (Lit. L-He)", "sammel"),
- ("Bestand in Kannen-1 (Lit. L-He)", "bestand_kannen"),
- ("Summe Bestand (Lit. L-He)", "summe_bestand"),
- ("Best. in Kannen Vormonat (Lit. L-He)", "best_kannen_vormonat"),
- ("Bezug (Liter L-He)", "bezug"),
- ("Rückführ. Soll (Lit. L-He)", "rueckf_soll"),
- ("Verluste (Soll-Rückf.) (Lit. L-He)", "verluste"),
- ("Füllungen warm (Lit. L-He)", "fuellungen_warm"),
- ("Kaltgas Rückgabe (Lit. L-He) – Faktor", "kaltgas_rueckgabe"),
- ("Faktor 0.06", "factor_row"),
- ("Verbraucherverluste (Liter L-He)", "verbraucherverluste"),
- ("%", "percent"),
- ]
-
- RIGHT_GROUPS = {
- "chemie": ["Dr. Fohrer", "AG Buntk."],
- "mawi": ["AG Alff", "AG Gutfl."],
- "m3": ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"],
- }
-
- RIGHT_ALL = ["Dr. Fohrer", "AG Buntk.", "AG Alff", "AG Gutfl.", "M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"]
- LEFT_ALL = HALFYEAR_CLIENTS_LEFT
-
- def safe_pct(verb, bez):
- return (verb / bez) if bez != 0 else None
- rows_sum = []
- def d(x):
- return x if isinstance(x, Decimal) else Decimal("0")
- for label, key in SUM_TABLE_ROWS:
-
- if key == "factor_row":
- lichtwiese = chemie = mawi = m3 = total = Decimal("0.06")
-
- elif key == "percent":
- # Right totals
- rw_bez = sum(d(right_data[c].get("bezug")) for c in RIGHT_ALL)
- rw_verb = sum(d(right_data[c].get("verbraucherverluste")) for c in RIGHT_ALL)
- lichtwiese = safe_pct(rw_verb, rw_bez)
-
- # Chemie
- ch_bez = sum(d(right_data[c].get("bezug")) for c in RIGHT_GROUPS["chemie"])
- ch_verb = sum(d(right_data[c].get("verbraucherverluste")) for c in RIGHT_GROUPS["chemie"])
- chemie = safe_pct(ch_verb, ch_bez)
-
- # MaWi
- mw_bez = sum(d(right_data[c].get("bezug")) for c in RIGHT_GROUPS["mawi"])
- mw_verb = sum(d(right_data[c].get("verbraucherverluste")) for c in RIGHT_GROUPS["mawi"])
- mawi = safe_pct(mw_verb, mw_bez)
-
- # M3
- m3_bez = sum(d(right_data[c].get("bezug")) for c in RIGHT_GROUPS["m3"])
- m3_verb = sum(d(right_data[c].get("verbraucherverluste")) for c in RIGHT_GROUPS["m3"])
- m3 = safe_pct(m3_verb, m3_bez)
-
- # Σ column = (left verb + right verb) / (left bez + right bez)
- left_bez = sum(d(client_data_left[c].get("bezug")) for c in LEFT_ALL)
- left_verb = sum(d(client_data_left[c].get("verbraucherverluste")) for c in LEFT_ALL)
- total = safe_pct(left_verb + rw_verb, left_bez + rw_bez)
-
- else:
- # normal rows = sums
- lichtwiese = sum(d(right_data[c].get(key)) for c in RIGHT_ALL)
- chemie = sum(d(right_data[c].get(key)) for c in RIGHT_GROUPS["chemie"])
- mawi = sum(d(right_data[c].get(key)) for c in RIGHT_GROUPS["mawi"])
- m3 = sum(d(right_data[c].get(key)) for c in RIGHT_GROUPS["m3"])
-
- left_total = sum(d(client_data_left[c].get(key)) for c in LEFT_ALL)
- total = left_total + lichtwiese
-
- rows_sum.append({
- "row_index": row_index,
- "label": label,
- "total": total,
- "lichtwiese": lichtwiese,
- "chemie": chemie,
- "mawi": mawi,
- "m3": m3,
- "is_percent": (key == "percent"),
- })
- def find_sum_row(rows, label_startswith: str):
- for r in rows:
- if str(r.get("label", "")).strip().startswith(label_startswith):
- return r
- return None
-
- summe_bestand_row = find_sum_row(rows_sum, "Summe Bestand")
- k38 = (summe_bestand_row.get("total") if summe_bestand_row else Decimal("0")) or Decimal("0")
- j38 = k38 * Decimal("0.75")
- # --- FIX: now that k38 is known, update bottom2 + recompute dependent rows ---
- bottom2["k38"] = k38
- bottom2["j38"] = j38
-
- k39 = bottom2.get("k39") or Decimal("0")
- k40 = bottom2.get("k40") or Decimal("0")
-
- # Row 43: Bestand flüssig He = SUMME(K38:K40)
- k43 = (k38 or Decimal("0")) + k39 + k40
- j43 = k43 * Decimal("0.75")
-
- bottom2["k43"] = k43
- bottom2["j43"] = j43
-
- # Row 44: Gesamtbestand neu = Gasbestand(Lit) from bottom table 1 + k43
- gasbestand_lit = Decimal("0")
- for r in bottom1_rows:
- if (r.get("label") or "").strip().startswith("Gasbestand"):
- gasbestand_lit = r.get("lhe") or Decimal("0")
- break
-
- k44 = gasbestand_lit + k43
- j44 = k44 * Decimal("0.75")
-
- bottom2["k44"] = k44
- bottom2["j44"] = j44
- def d(x):
- return x if isinstance(x, Decimal) else Decimal("0")
-
- # ---- Bottom2: J38/K38 depend on rows_sum (overall summary), so do it HERE ----
- k38 = Decimal("0")
- for r in rows_sum:
- if r.get("label") == "Summe Bestand (Lit. L-He)":
- k38 = r.get("total") or Decimal("0")
- break
-
- j38 = k38 * FACTOR_NM3_TO_LHE # 0.75
- bottom2["k38"] = k38
- bottom2["j38"] = j38
- factor = Decimal("0.75")
-
- # window = the 6-month list you already build in this view: [(y,m), (y,m), ...]
- # bottom2 = dict with "k44" already computed in your view
- # rows_sum = overall sum group rows list (your existing halfyear logic)
-
- # 1) K46 = K44 from the month BEFORE the global month (interval start)
- start_year = interval_year
- start_month = interval_start # whatever you named your start month variable
-
- if start_month == 1:
- prev_year, prev_month = start_year - 1, 12
- else:
- prev_year, prev_month = start_year, start_month - 1
-
- prev_sheet = MonthlySheet.objects.filter(year=prev_year, month=prev_month).first()
-
- # This assumes you have MonthlySummary.gesamtbestand_neu_lhe as K44 equivalent.
- # If your field name differs, tell me your MonthlySummary model fields.
- prev_k44 = Decimal("0")
- if prev_sheet:
- prev_sum = MonthlySummary.objects.filter(sheet=prev_sheet).first()
- if prev_sum and prev_sum.gesamtbestand_neu_lhe is not None:
- prev_k44 = Decimal(str(prev_sum.gesamtbestand_neu_lhe))
-
- # helpers: read bottom_3 values for a given sheet/month
- def get_bottom3_value(sheet, row_index, col_index):
- if not sheet:
- return Decimal("0")
- c = Cell.objects.filter(
- sheet=sheet, table_type="bottom_3",
- row_index=row_index, column_index=col_index
- ).first()
- if not c or c.value in (None, "", "None"):
- return Decimal("0")
- try:
- return Decimal(str(c.value))
- except Exception:
- return Decimal("0")
-
- # 2) Sum rows across the 6-month window
- g47_sum = Decimal("0")
- i47_sum = Decimal("0")
- j47_sum = Decimal("0")
-
- g50_sum = Decimal("0")
- i50_sum = Decimal("0")
-
- for (yy, mm) in window:
- s = MonthlySheet.objects.filter(year=yy, month=mm).first()
- ## Excel row 47 maps to bottom_3 row_index=1 in DB (see monthly_sheet.html)
- g = get_bottom3_value(s, 1, 1) # G47 editable
- i = get_bottom3_value(s, 1, 2) # I47 editable
-
- g47_sum += g
- i47_sum += i
-
- # In monthly_sheet, J47 is CALCULATED as (G47 + I47), not stored as an editable cell
- j47_sum += (g + i)
-
- # row 50: G(2), I(4)
- g50_sum += get_bottom3_value(s, 50, 2)
- i50_sum += get_bottom3_value(s, 50, 4)
-
- # 3) K52 = Verbraucherverlust from overall sum group first column (global Σ)
- k52 = Decimal("0")
- for r in rows_sum:
- label = (r.get("label") or "")
- if label.startswith("Verbraucherverluste"):
- k52 = r.get("total") or Decimal("0")
- break
-
- # --- apply the SAME monthly formulas, with your overrides ---
- # Row 46
- k46 = prev_k44
- j46 = k46 * factor
-
- # Row 47
- g47 = g47_sum
- i47 = i47_sum
- j47 = j47_sum
- k47 = (j47 / factor) + g47 if (j47 != 0 or g47 != 0) else Decimal("0")
-
- # Row 48
- k48 = k46 + k47
- j48 = k48 * factor
-
- # Row 49 (akt. Monat) -> in halfyear we use current bottom2 K44
- k49 = bottom2.get("k44") or Decimal("0")
- j49 = k49 * factor
-
- # Row 50
- g50 = g50_sum
- i50 = i50_sum
- j50 = i50
- k50 = (j50 / factor) if j50 != 0 else Decimal("0")
-
- # Row 51
- k51 = k48 - k49 - k50
- j51 = k51 * factor
-
- # Row 52
- j52 = k52 * factor
-
- # Row 53
- k53 = k51 - k52
- j53 = j51 - j52
-
- bottom3 = {
- "j46": j46, "k46": k46,
- "g47": g47, "i47": i47, "j47": j47, "k47": k47,
- "j48": j48, "k48": k48,
- "j49": j49, "k49": k49,
- "g50": g50, "i50": i50, "j50": j50, "k50": k50,
- "j51": j51, "k51": k51,
- "j52": j52, "k52": k52,
- "j53": j53, "k53": k53,
- }
- # ------------------------------------------------------------------
- # 4) Context – keep old keys AND new ones
- # ------------------------------------------------------------------
- context = {
- 'interval_year': interval_year,
- 'interval_start_month': interval_start,
- 'window': window,
-
- # Left table – old names (for your first template)
- 'clients': HALFYEAR_CLIENTS_LEFT,
- 'rows': rows_left,
-
- # Left table – explicit
- 'clients_left': HALFYEAR_CLIENTS_LEFT,
- 'rows_left': rows_left,
-
- # Right table
- 'clients_right': RIGHT_CLIENTS,
- 'rows_right': rows_right,
- 'rows_sum': rows_sum,
- 'bottom1_rows': bottom1_rows,
-
- }
- context["bottom2"] = bottom2
- context["bottom3"] = bottom3
- context["context_bottom2_g39"] = bottom2_inputs["g39"]
- context["context_bottom2_i39"] = bottom2_inputs["i39"]
- return render(request, 'halfyear_balance.html', context)
-
-def get_bottom2_value(sheet, row_index: int, col_index: int) -> Decimal:
- """Get numeric value from bottom_2 or 0 if missing."""
- if sheet is None:
- return Decimal("0")
- cell = Cell.objects.filter(
- sheet=sheet,
- table_type="bottom_2",
- row_index=row_index,
- column_index=col_index,
- ).first()
- if cell is None or cell.value in (None, ""):
- return Decimal("0")
- try:
- return Decimal(str(cell.value))
- except Exception:
- return Decimal("0")
-
-def get_top_left_value(sheet, client_name: str, row_index: int) -> Decimal:
- """
- Read a numeric value from the top_left table for a given month, client and row.
- Does NOT use column_index, because top_left is keyed only by client + row_index.
- """
- if sheet is None:
- return Decimal('0')
-
- client_obj = Client.objects.filter(name=client_name).first()
- if not client_obj:
- return Decimal('0')
-
- cell = Cell.objects.filter(
- sheet=sheet,
- table_type='top_left',
- client=client_obj,
- row_index=row_index,
- ).first()
-
- if cell is None or cell.value in (None, ''):
- return Decimal('0')
-
- try:
- return Decimal(str(cell.value))
- except Exception:
- return Decimal('0')
-def get_group_clients(group_key):
- """Return queryset of clients that belong to a logical group."""
- from .models import Client # local import to avoid circulars
-
- group = CLIENT_GROUPS.get(group_key)
- if not group:
- return Client.objects.none()
- return Client.objects.filter(name__in=group['names'])
-
-def calculate_summation(sheet, table_type, row_index, sum_column_index):
- """Calculate summation for a row, with special handling for % row"""
- from decimal import Decimal
- from .models import Cell
-
- try:
- # Special case: top_left, % row (Excel B20 -> row_index 19)
- if table_type == 'top_left' and row_index == 19:
- # K13 = sum of row 13 (Excel B13 -> row_index 12) across all clients
- cells_row13 = Cell.objects.filter(
- sheet=sheet,
- table_type='top_left',
- row_index=12, # Excel B13 = row_index 12
- column_index__lt=sum_column_index # Exclude sum column itself
- )
- total_13 = Decimal('0')
- for cell in cells_row13:
- if cell.value is not None:
- total_13 += Decimal(str(cell.value))
-
- # K19 = sum of row 19 (Excel B19 -> row_index 18) across all clients
- cells_row19 = Cell.objects.filter(
- sheet=sheet,
- table_type='top_left',
- row_index=18, # Excel B19 = row_index 18
- column_index__lt=sum_column_index
- )
- total_19 = Decimal('0')
- for cell in cells_row19:
- if cell.value is not None:
- total_19 += Decimal(str(cell.value))
-
- # Calculate: IF(K13=0; 0; K19/K13)
- if total_13 == 0:
- return Decimal('0')
- return total_19 / total_13
-
- # Normal summation for other rows
- cells_in_row = Cell.objects.filter(
- sheet=sheet,
- table_type=table_type,
- row_index=row_index,
- column_index__lt=sum_column_index
- )
-
- total = Decimal('0')
- for cell in cells_in_row:
- if cell.value is not None:
- total += Decimal(str(cell.value))
-
- return total
-
- except Exception as e:
- print(f"Error calculating summation for {table_type}[{row_index}]: {e}")
- return None
-
-# Helper function for calculations
-def evaluate_formula(formula, values_dict):
- """
- Safely evaluate a formula like "10 + 9" where numbers are row indices
- values_dict: {row_index: decimal_value}
- """
- from decimal import Decimal
- import re
-
- try:
- # Create a copy of the formula to work with
- expr = formula
-
- # Find all row numbers in the formula
- row_refs = re.findall(r'\b\d+\b', expr)
-
- for row_ref in row_refs:
- row_num = int(row_ref)
- if row_num in values_dict and values_dict[row_num] is not None:
- # Replace row reference with actual value
- expr = expr.replace(row_ref, str(values_dict[row_num]))
- else:
- # Missing value - can't calculate
- return None
-
- # Evaluate the expression
- # Note: In production, use a safer evaluator like `asteval`
- result = eval(expr, {"__builtins__": {}}, {})
-
- # Convert to Decimal with proper rounding
- return Decimal(str(round(result, 6)))
-
- except Exception:
- return None
-
-# Monthly Sheet View
-class MonthlySheetView(TemplateView):
- template_name = 'monthly_sheet.html'
-
- def populate_helium_input_to_top_right(self, sheet):
- """Populate bezug data from SecondTableEntry to top-right table (row 8 = Excel row 12)"""
- from .models import SecondTableEntry, Cell, Client
- from django.db.models.functions import Coalesce
- from decimal import Decimal
-
- year = sheet.year
- month = sheet.month
-
- TOP_RIGHT_CLIENTS = [
- "Dr. Fohrer", # Column index 0 (L)
- "AG Buntk.", # Column index 1 (M)
- "AG Alff", # Column index 2 (N)
- "AG Gutfl.", # Column index 3 (O)
- "M3 Thiele", # Column index 4 (P)
- "M3 Buntkowsky", # Column index 5 (Q)
- "M3 Gutfleisch", # Column index 6 (R)
- ]
-
- # For each client in top-right table
- for client_name in TOP_RIGHT_CLIENTS:
- try:
- client = Client.objects.get(name=client_name)
- column_index = TOP_RIGHT_CLIENTS.index(client_name)
-
- # Calculate total LHe_output for this client in this month from SecondTableEntry
- total_lhe_output = SecondTableEntry.objects.filter(
- client=client,
- date__year=year,
- date__month=month
- ).aggregate(
- total=Coalesce(Sum('lhe_output'), Decimal('0'))
- )['total']
-
- # Get or create the cell for row_index 8 (Excel row 12) - Bezug
- cell, created = Cell.objects.get_or_create(
- sheet=sheet,
- table_type='top_right',
- client=client,
- row_index=8, # Bezug row (Excel row 12)
- column_index=column_index,
- defaults={'value': total_lhe_output}
- )
-
- if not created and cell.value != total_lhe_output:
- cell.value = total_lhe_output
- cell.save()
-
- except Client.DoesNotExist:
- continue
-
- # After populating bezug, trigger calculation for all dependent cells
- # Get any cell to start the calculation
- first_cell = Cell.objects.filter(
- sheet=sheet,
- table_type='top_right'
- ).first()
-
- if first_cell:
- save_view = SaveCellsView()
- save_view.calculate_top_right_dependents(sheet, first_cell)
-
- return True
- def calculate_bezug_from_entries(self, sheet, year, month):
- """Calculate B11 (Bezug) from SecondTableEntry for all clients - ONLY for non-start sheets"""
- from .models import SecondTableEntry, Cell, Client
- from django.db.models import Sum
- from django.db.models.functions import Coalesce
- from decimal import Decimal
-
- # Check if this is the start sheet
- if year == 2025 and month == 1:
- return # Don't auto-calculate for start sheet
-
- for client in Client.objects.all():
- # Calculate total LHe output for this client in this month
- lhe_output_sum = SecondTableEntry.objects.filter(
- client=client,
- date__year=year,
- date__month=month
- ).aggregate(
- total=Coalesce(Sum('lhe_output'), Decimal('0'))
- )['total']
-
- # Update B11 cell (row_index 8 = UI Row 9)
- b11_cell = Cell.objects.filter(
- sheet=sheet,
- table_type='top_left',
- client=client,
- row_index=8 # Excel B11
- ).first()
-
- if b11_cell and (b11_cell.value != lhe_output_sum or b11_cell.value is None):
- b11_cell.value = lhe_output_sum
- b11_cell.save()
-
- # Also trigger dependent calculations
- from .views import SaveCellsView
- save_view = SaveCellsView()
- save_view.calculate_top_left_dependents(sheet, b11_cell)
- # In MonthlySheetView.get_context_data() method, update the TOP_RIGHT_CLIENTS and row count:
-
-
-
- return True
- def get_context_data(self, **kwargs):
- from decimal import Decimal
- context = super().get_context_data(**kwargs)
- year = self.kwargs.get('year', datetime.now().year)
- month = self.kwargs.get('month', datetime.now().month)
- is_start_sheet = (year == FIRST_SHEET_YEAR and month == FIRST_SHEET_MONTH)
-
- # Get or create the monthly sheet
- sheet, created = MonthlySheet.objects.get_or_create(
- year=year, month=month
- )
-
- # All clients (used for bottom tables etc.)
- clients = Client.objects.all().order_by('name')
-
- # Pre-fill cells if creating new sheet
- if created:
- self.initialize_sheet_cells(sheet, clients)
-
- # Apply previous month links (for B4 and B12)
- self.apply_previous_month_links(sheet, year, month)
- self.calculate_bezug_from_entries(sheet, year, month)
- self.populate_helium_input_to_top_right(sheet)
- self.apply_previous_month_links_top_right(sheet, year, month)
-
- # Define client groups
- TOP_LEFT_CLIENTS = [
- "AG Vogel",
- "AG Halfm",
- "IKP",
- ]
-
- TOP_RIGHT_CLIENTS = [
- "Dr. Fohrer",
- "AG Buntk.",
- "AG Alff",
- "AG Gutfl.",
- "M3 Thiele",
- "M3 Buntkowsky",
- "M3 Gutfleisch",
- ]
- current_summary = MonthlySummary.objects.filter(sheet=sheet).first()
-
- # Get previous month summary (for Bottom Table 3: K46 = prev K44)
- prev_month_info = self.get_prev_month(year, month)
- prev_summary = None
- if not is_start_sheet:
- prev_sheet = MonthlySheet.objects.filter(
- year=prev_month_info['year'],
- month=prev_month_info['month']
- ).first()
- if prev_sheet:
- prev_summary = MonthlySummary.objects.filter(sheet=prev_sheet).first()
-
- context.update({
- # ... your existing context ...
- 'current_summary': current_summary,
- 'prev_summary': prev_summary,
- })
- # Update row counts in build_group_rows function
- # Update row counts in build_group_rows function
- def build_group_rows(sheet, table_type, client_names):
- """Build rows for display in monthly sheet."""
- from decimal import Decimal
- from .models import Cell
- MERGED_ROWS = {2, 3, 5, 6, 7, 9, 10, 12, 14, 15}
- MERGED_PAIRS = [
- ("Dr. Fohrer", "AG Buntk."),
- ("AG Alff", "AG Gutfl."),
- ]
- rows = []
-
- # Determine row count
- row_counts = {
- "top_left": 16,
- "top_right": 16, # rows 0–15
- "bottom_1": 10,
- "bottom_2": 10,
- "bottom_3": 10,
- }
- row_count = row_counts.get(table_type, 0)
-
- # Get all cells for this sheet and table
- all_cells = (
- Cell.objects.filter(
- sheet=sheet,
- table_type=table_type,
- ).select_related("client")
- )
-
- # Group cells by row index
- cells_by_row = {}
- for cell in all_cells:
- if cell.row_index not in cells_by_row:
- cells_by_row[cell.row_index] = {}
- cells_by_row[cell.row_index][cell.client.name] = cell
-
- # We will store row sums for Bezug (row 8) and Verbraucherverluste (row 14)
- # for top_left / top_right so we can compute the overall % in row 15.
- sum_bezug = None
- sum_verbrauch = None
-
- # Build each row
- for row_idx in range(row_count):
- display_cells = []
- row_cells_dict = cells_by_row.get(row_idx, {})
-
- # Build cells in the requested client order
- for name in client_names:
- cell = row_cells_dict.get(name)
- display_cells.append(cell)
-
- # Calculate sum for this row (includes editable + calculated cells)
- sum_value = None
- total = Decimal("0")
- has_value = False
-
- merged_second_indices = set()
- if table_type == 'top_right' and row_idx in MERGED_ROWS:
- for left_name, right_name in MERGED_PAIRS:
- try:
- right_idx = client_names.index(right_name)
- merged_second_indices.add(right_idx)
- except ValueError:
- # client not in this table; just ignore
- pass
-
- for col_idx, cell in enumerate(display_cells):
- # Skip the duplicate (second) column of each merged pair
- if col_idx in merged_second_indices:
- continue
-
- if cell and cell.value is not None:
- try:
- total += Decimal(str(cell.value))
- has_value = True
- except Exception:
- pass
-
- if has_value:
- sum_value = total
-
- # Remember special rows for top tables
- if table_type in ("top_left", "top_right"):
- if row_idx == 8: # Bezug
- sum_bezug = total
- elif row_idx == 14: # Verbraucherverluste
- sum_verbrauch = total
-
- rows.append(
- {
- "cells": display_cells,
- "sum": sum_value,
- "row_index": row_idx,
- }
- )
-
- # Adjust the % row sum for top_left / top_right:
- # Sum(%) = Sum(Verbraucherverluste) / Sum(Bezug)
- if table_type in ("top_left", "top_right"):
- perc_row_idx = 15 # % row
- if 0 <= perc_row_idx < len(rows):
- if sum_bezug is not None and sum_bezug != 0 and sum_verbrauch is not None:
- rows[perc_row_idx]["sum"] = sum_verbrauch / sum_bezug
- else:
- rows[perc_row_idx]["sum"] = None
-
- return rows
-
-
- # Now call the local function
- top_left_rows = build_group_rows(sheet, 'top_left', TOP_LEFT_CLIENTS)
- top_right_rows = build_group_rows(sheet, 'top_right', TOP_RIGHT_CLIENTS)
-
- # --- Build combined summary of top-left + top-right Sum columns ---
-
- # Helper to safely get the Sum for a given row index
- def get_row_sum(rows, row_index):
- if 0 <= row_index < len(rows):
- return rows[row_index].get('sum')
- return None
-
- # Row definitions we want in the combined Σ table
- # (row_index in top tables, label shown in the small table)
- summary_row_defs = [
- (2, "Rückführung flüssig (Lit. L-He)"),
- (3, "Sonderrückführungen (Lit. L-He)"),
- (4, "Sammelrückführungen (Lit. L-He)"),
- (5, "Bestand in Kannen-1 (Lit. L-He)"),
- (6, "Summe Bestand (Lit. L-He)"),
- (7, "Best. in Kannen Vormonat (Lit. L-He)"),
- (8, "Bezug (Liter L-He)"),
- (9, "Rückführ. Soll (Lit. L-He)"),
- (10, "Verluste (Soll-Rückf.) (Lit. L-He)"),
- (11, "Füllungen warm (Lit. L-He)"),
- (12, "Kaltgas Rückgabe (Lit. L-He) – Faktor"),
- (13, "Faktor 0.06"),
- (14, "Verbraucherverluste (Liter L-He)"),
- (15, "%"),
- ]
-
- # Precompute totals for Bezug and Verbraucherverluste across both tables
- bezug_left = get_row_sum(top_left_rows, 8) or Decimal('0')
- bezug_right = get_row_sum(top_right_rows, 8) or Decimal('0')
- total_bezug = bezug_left + bezug_right
-
- verb_left = get_row_sum(top_left_rows, 14) or Decimal('0')
- verb_right = get_row_sum(top_right_rows, 14) or Decimal('0')
- total_verbrauch = verb_left + verb_right
-
- summary_rows = []
-
- for row_index, label in summary_row_defs:
- # Faktor row: always fixed 0.06
- if row_index == 13:
- summary_value = Decimal('0.06')
-
- # % row: total Verbraucherverluste / total Bezug
- elif row_index == 15:
- if total_bezug != 0:
- summary_value = total_verbrauch / total_bezug
- else:
- summary_value = None
-
- else:
- left_sum = get_row_sum(top_left_rows, row_index)
- right_sum = get_row_sum(top_right_rows, row_index)
-
- # Sammelrückführungen: only from top-right table
- if row_index == 4:
- left_sum = None
-
- total = Decimal('0')
-
- has_any = False
- if left_sum is not None:
- total += Decimal(str(left_sum))
- has_any = True
- if right_sum is not None:
- total += Decimal(str(right_sum))
- has_any = True
-
- summary_value = total if has_any else None
-
- summary_rows.append({
- 'row_index': row_index,
- 'label': label,
- 'sum': summary_value,
- })
-
- # Get cells for bottom tables
- cells_by_table = self.get_cells_by_table(sheet)
-
- context.update({
- 'sheet': sheet,
- 'clients': clients,
- 'year': year,
- 'month': month,
- 'month_name': calendar.month_name[month],
- 'prev_month': self.get_prev_month(year, month),
- 'next_month': self.get_next_month(year, month),
- 'cells_by_table': cells_by_table,
- 'top_left_headers': TOP_LEFT_CLIENTS + ['Sum'],
- 'top_right_headers': TOP_RIGHT_CLIENTS + ['Sum'],
- 'top_left_rows': top_left_rows,
- 'top_right_rows': top_right_rows,
- 'summary_rows': summary_rows, # 👈 NEW
- 'is_start_sheet': is_start_sheet,
- })
- return context
-
- def get_cells_by_table(self, sheet):
- """Organize cells by table type for easy template rendering"""
- cells = sheet.cells.select_related('client').all()
- organized = {
- 'top_left': [[] for _ in range(16)],
- 'top_right': [[] for _ in range(16)], # now 16 rows
- 'bottom_1': [[] for _ in range(10)],
- 'bottom_2': [[] for _ in range(10)],
- 'bottom_3': [[] for _ in range(10)],
- }
-
- for cell in cells:
- if cell.table_type not in organized:
- continue
-
- max_rows = len(organized[cell.table_type])
- if cell.row_index < 0 or cell.row_index >= max_rows:
- # This is an "extra" cell from an older layout (e.g. top_left rows 18–23)
- continue
-
- row_list = organized[cell.table_type][cell.row_index]
- while len(row_list) <= cell.column_index:
- row_list.append(None)
-
- row_list[cell.column_index] = cell
-
- return organized
-
-
- def initialize_sheet_cells(self, sheet, clients):
- """Create all empty cells for a new monthly sheet"""
- cells_to_create = []
- summation_config = CALCULATION_CONFIG.get('summation_column', {})
- sum_column_index = summation_config.get('sum_column_index', len(clients) - 1) # Last column
-
- # For each table type and row
- table_configs = [
- ('top_left', 16),
- ('top_right', 16),
- ('bottom_1', 10),
- ('bottom_2', 10),
- ('bottom_3', 10),
- ]
-
- for table_type, row_count in table_configs:
- for row_idx in range(row_count):
- for col_idx, client in enumerate(clients):
- is_summation = (col_idx == sum_column_index)
- cells_to_create.append(Cell(
- sheet=sheet,
- client=client,
- table_type=table_type,
- row_index=row_idx,
- column_index=col_idx,
- value=None,
- is_formula=is_summation, # Mark summation cells as formulas
- ))
-
- # Bulk create all cells at once
- Cell.objects.bulk_create(cells_to_create)
- def get_prev_month(self, year, month):
- """Get previous month year and month"""
- if month == 1:
- return {'year': year - 1, 'month': 12}
- return {'year': year, 'month': month - 1}
-
- def get_next_month(self, year, month):
- """Get next month year and month"""
- if month == 12:
- return {'year': year + 1, 'month': 1}
- return {'year': year, 'month': month + 1}
- def apply_previous_month_links(self, sheet, year, month):
- """
- For non-start sheets:
- B4 (row 2) = previous sheet B3 (row 1)
- B10 (row 8) = previous sheet B9 (row 7)
- """
- # Do nothing on the first sheet
- if year == FIRST_SHEET_YEAR and month == FIRST_SHEET_MONTH:
- return
-
- # Figure out previous month
- if month == 1:
- prev_year = year - 1
- prev_month = 12
- else:
- prev_year = year
- prev_month = month - 1
-
- from .models import MonthlySheet, Cell, Client
-
- prev_sheet = MonthlySheet.objects.filter(
- year=prev_year,
- month=prev_month
- ).first()
-
- if not prev_sheet:
- # No previous sheet created yet → nothing to copy
- return
-
- # For each client, copy values
- for client in Client.objects.all():
- # B3(prev) → B4(curr): UI row 1 → row 2 → row_index 0 → 1
- prev_b3 = Cell.objects.filter(
- sheet=prev_sheet,
- table_type='top_left',
- client=client,
- row_index=0, # UI row 1
- ).first()
-
- curr_b4 = Cell.objects.filter(
- sheet=sheet,
- table_type='top_left',
- client=client,
- row_index=1, # UI row 2
- ).first()
-
- if prev_b3 and curr_b4:
- curr_b4.value = prev_b3.value
- curr_b4.save()
-
- # B9(prev) → B10(curr): UI row 7 → row 8 → row_index 6 → 7
- prev_b9 = Cell.objects.filter(
- sheet=prev_sheet,
- table_type='top_left',
- client=client,
- row_index=6, # UI row 7 (Summe Bestand)
- ).first()
-
- curr_b10 = Cell.objects.filter(
- sheet=sheet,
- table_type='top_left',
- client=client,
- row_index=7, # UI row 8 (Best. in Kannen Vormonat)
- ).first()
-
- if prev_b9 and curr_b10:
- curr_b10.value = prev_b9.value
- curr_b10.save()
-
- def apply_previous_month_links_top_right(self, sheet, year, month):
- """
- top_right row 7: Best. in Kannen Vormonat (Lit. L-He)
- = previous sheet's Summe Bestand (row 6).
-
- For merged pairs:
- - Dr. Fohrer + AG Buntk. share the SAME value (from previous month's AG Buntk. or Dr. Fohrer)
- - AG Alff + AG Gutfl. share the SAME value (from previous month's AG Alff or AG Gutfl.)
- M3 clients just copy their own value.
- """
- from .models import MonthlySheet, Cell, Client
- from decimal import Decimal
-
- # Do nothing on first sheet
- if year == FIRST_SHEET_YEAR and month == FIRST_SHEET_MONTH:
- return
-
- # find previous month
- if month == 1:
- prev_year = year - 1
- prev_month = 12
- else:
- prev_year = year
- prev_month = month - 1
-
- prev_sheet = MonthlySheet.objects.filter(
- year=prev_year,
- month=prev_month
- ).first()
-
- if not prev_sheet:
- return # nothing to copy from
-
- TOP_RIGHT_CLIENTS = [
- "Dr. Fohrer",
- "AG Buntk.",
- "AG Alff",
- "AG Gutfl.",
- "M3 Thiele",
- "M3 Buntkowsky",
- "M3 Gutfleisch",
- ]
-
- # Helper function to get a cell value
- def get_cell_value(sheet_obj, client_name, row_index):
- client_obj = Client.objects.filter(name=client_name).first()
- if not client_obj:
- return None
-
- try:
- col_idx = TOP_RIGHT_CLIENTS.index(client_name)
- except ValueError:
- return None
-
- cell = Cell.objects.filter(
- sheet=sheet_obj,
- table_type='top_right',
- client=client_obj,
- row_index=row_index,
- column_index=col_idx,
- ).first()
-
- return cell.value if cell else None
-
- # Helper function to set a cell value
- def set_cell_value(sheet_obj, client_name, row_index, value):
- client_obj = Client.objects.filter(name=client_name).first()
- if not client_obj:
- return False
-
- try:
- col_idx = TOP_RIGHT_CLIENTS.index(client_name)
- except ValueError:
- return False
-
- cell, created = Cell.objects.get_or_create(
- sheet=sheet_obj,
- table_type='top_right',
- client=client_obj,
- row_index=row_index,
- column_index=col_idx,
- defaults={'value': value}
- )
-
- if not created and cell.value != value:
- cell.value = value
- cell.save()
- elif created:
- cell.save()
-
- return True
-
- # ----- Pair 1: Dr. Fohrer + AG Buntk. -----
- # Get previous month's Summe Bestand (row 6) for either client in the pair
- pair1_prev_val = None
-
- # Try AG Buntk. first
- prev_buntk_val = get_cell_value(prev_sheet, "AG Buntk.", 6)
- if prev_buntk_val is not None:
- pair1_prev_val = prev_buntk_val
- else:
- # Try Dr. Fohrer if AG Buntk. is empty
- prev_fohrer_val = get_cell_value(prev_sheet, "Dr. Fohrer", 6)
- if prev_fohrer_val is not None:
- pair1_prev_val = prev_fohrer_val
-
- # Apply the value to both clients in the pair
- if pair1_prev_val is not None:
- set_cell_value(sheet, "Dr. Fohrer", 7, pair1_prev_val)
- set_cell_value(sheet, "AG Buntk.", 7, pair1_prev_val)
-
- # ----- Pair 2: AG Alff + AG Gutfl. -----
- pair2_prev_val = None
-
- # Try AG Alff first
- prev_alff_val = get_cell_value(prev_sheet, "AG Alff", 6)
- if prev_alff_val is not None:
- pair2_prev_val = prev_alff_val
- else:
- # Try AG Gutfl. if AG Alff is empty
- prev_gutfl_val = get_cell_value(prev_sheet, "AG Gutfl.", 6)
- if prev_gutfl_val is not None:
- pair2_prev_val = prev_gutfl_val
-
- # Apply the value to both clients in the pair
- if pair2_prev_val is not None:
- set_cell_value(sheet, "AG Alff", 7, pair2_prev_val)
- set_cell_value(sheet, "AG Gutfl.", 7, pair2_prev_val)
-
- # ----- M3 clients: copy their own Summe Bestand -----
- for name in ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"]:
- prev_val = get_cell_value(prev_sheet, name, 6)
- if prev_val is not None:
- set_cell_value(sheet, name, 7, prev_val)
-# Add this helper function to views.py
-def get_factor_value(table_type, row_index):
- """Get factor value (like 0.06 for top_left row 17)"""
- factors = {
- ('top_left', 17): Decimal('0.06'), # A18 in Excel (UI row 17, 0-based index 16)
- }
- return factors.get((table_type, row_index), Decimal('0'))
-# Save Cells View
-# views.py - Updated SaveCellsView
-# views.py - Update SaveCellsView class
-def debug_cell_values(self, sheet, client_id):
- """Debug method to check cell values"""
- from .models import Cell
- cells = Cell.objects.filter(
- sheet=sheet,
- table_type='top_left',
- client_id=client_id
- ).order_by('row_index')
-
- debug_info = {}
- for cell in cells:
- debug_info[f"row_{cell.row_index}"] = {
- 'value': str(cell.value) if cell.value else 'None',
- 'ui_row': cell.row_index + 1,
- 'excel_ref': f"B{cell.row_index + 3}"
- }
-
- return debug_info
-class DebugCalculationView(View):
- """Debug view to test calculations directly"""
- def get(self, request):
- sheet_id = request.GET.get('sheet_id', 1)
- client_name = request.GET.get('client', 'AG Vogel')
-
- try:
- sheet = MonthlySheet.objects.get(id=sheet_id)
- client = Client.objects.get(name=client_name)
-
- # Get SaveCellsView instance
- save_view = SaveCellsView()
-
- # Create a dummy cell to trigger calculations
- dummy_cell = Cell.objects.filter(
- sheet=sheet,
- table_type='top_left',
- client=client,
- row_index=0 # B3
- ).first()
-
- if not dummy_cell:
- return JsonResponse({'error': 'No cells found for this client'})
-
- # Trigger calculation
- updated = save_view.calculate_top_left_dependents(sheet, dummy_cell)
-
- # Get updated cell values
- cells = Cell.objects.filter(
- sheet=sheet,
- table_type='top_left',
- client=client
- ).order_by('row_index')
-
- cell_data = []
- for cell in cells:
- cell_data.append({
- 'row_index': cell.row_index,
- 'ui_row': cell.row_index + 1,
- 'excel_ref': f"B{cell.row_index + 3}",
- 'value': str(cell.value) if cell.value else 'None',
- 'description': self.get_row_description(cell.row_index)
- })
-
- return JsonResponse({
- 'sheet': f"{sheet.year}-{sheet.month:02d}",
- 'client': client.name,
- 'cells': cell_data,
- 'updated_count': len(updated),
- 'calculation': 'B5 = IF(B3>0; B3-B4; 0)'
- })
-
- except Exception as e:
- return JsonResponse({'error': str(e)}, status=400)
-
- def get_row_description(self, row_index):
- """Get description for row index"""
- descriptions = {
- 0: "B3: Stand der Gaszähler (Nm³)",
- 1: "B4: Stand der Gaszähler (Vormonat) (Nm³)",
- 2: "B5: Gasrückführung (Nm³)",
- 3: "B6: Rückführung flüssig (Lit. L-He)",
- 4: "B7: Sonderrückführungen (Lit. L-He)",
- 5: "B8: Bestand in Kannen-1 (Lit. L-He)",
- 6: "B9: Summe Bestand (Lit. L-He)",
- 7: "B10: Best. in Kannen Vormonat (Lit. L-He)",
- 8: "B11: Bezug (Liter L-He)",
- 9: "B12: Rückführ. Soll (Lit. L-He)",
- 10: "B13: Verluste (Soll-Rückf.) (Lit. L-He)",
- 11: "B14: Füllungen warm (Lit. L-He)",
- 12: "B15: Kaltgas Rückgabe (Lit. L-He) – Faktor",
- 13: "B16: Faktor",
- 14: "B17: Verbraucherverluste (Liter L-He)",
- 15: "B18: %"
- }
- return descriptions.get(row_index, f"Row {row_index}")
-def recalculate_stand_der_gaszahler(self, sheet):
- """Recalculate Stand der Gaszähler for all client pairs"""
- from decimal import Decimal
-
- # For Dr. Fohrer and AG Buntk. (L & M columns)
- try:
- # Get Dr. Fohrer's bezug
- dr_fohrer_client = Client.objects.get(name="Dr. Fohrer")
- dr_fohrer_cell = Cell.objects.filter(
- sheet=sheet,
- table_type='top_right',
- client=dr_fohrer_client,
- row_index=9 # Row 9 = Bezug
- ).first()
- L13 = Decimal(str(dr_fohrer_cell.value)) if dr_fohrer_cell and dr_fohrer_cell.value else Decimal('0')
-
- # Get AG Buntk.'s bezug
- ag_buntk_client = Client.objects.get(name="AG Buntk.")
- ag_buntk_cell = Cell.objects.filter(
- sheet=sheet,
- table_type='top_right',
- client=ag_buntk_client,
- row_index=9
- ).first()
- M13 = Decimal(str(ag_buntk_cell.value)) if ag_buntk_cell and ag_buntk_cell.value else Decimal('0')
-
- total = L13 + M13
- if total > 0:
- # Update Dr. Fohrer's row 0
- dr_fohrer_row0 = Cell.objects.filter(
- sheet=sheet,
- table_type='top_right',
- client=dr_fohrer_client,
- row_index=0
- ).first()
- if dr_fohrer_row0:
- dr_fohrer_row0.value = L13 / total
- dr_fohrer_row0.save()
-
- # Update AG Buntk.'s row 0
- ag_buntk_row0 = Cell.objects.filter(
- sheet=sheet,
- table_type='top_right',
- client=ag_buntk_client,
- row_index=0
- ).first()
- if ag_buntk_row0:
- ag_buntk_row0.value = M13 / total
- ag_buntk_row0.save()
- except Exception as e:
- print(f"Error recalculating Stand der Gaszähler for Dr. Fohrer/AG Buntk.: {e}")
-
- # For AG Alff and AG Gutfl. (N & O columns)
- try:
- # Get AG Alff's bezug
- ag_alff_client = Client.objects.get(name="AG Alff")
- ag_alff_cell = Cell.objects.filter(
- sheet=sheet,
- table_type='top_right',
- client=ag_alff_client,
- row_index=9
- ).first()
- N13 = Decimal(str(ag_alff_cell.value)) if ag_alff_cell and ag_alff_cell.value else Decimal('0')
-
- # Get AG Gutfl.'s bezug
- ag_gutfl_client = Client.objects.get(name="AG Gutfl.")
- ag_gutfl_cell = Cell.objects.filter(
- sheet=sheet,
- table_type='top_right',
- client=ag_gutfl_client,
- row_index=9
- ).first()
- O13 = Decimal(str(ag_gutfl_cell.value)) if ag_gutfl_cell and ag_gutfl_cell.value else Decimal('0')
-
- total = N13 + O13
- if total > 0:
- # Update AG Alff's row 0
- ag_alff_row0 = Cell.objects.filter(
- sheet=sheet,
- table_type='top_right',
- client=ag_alff_client,
- row_index=0
- ).first()
- if ag_alff_row0:
- ag_alff_row0.value = N13 / total
- ag_alff_row0.save()
-
- # Update AG Gutfl.'s row 0
- ag_gutfl_row0 = Cell.objects.filter(
- sheet=sheet,
- table_type='top_right',
- client=ag_gutfl_client,
- row_index=0
- ).first()
- if ag_gutfl_row0:
- ag_gutfl_row0.value = O13 / total
- ag_gutfl_row0.save()
- except Exception as e:
- print(f"Error recalculating Stand der Gaszähler for AG Alff/AG Gutfl.: {e}")
-# In your SaveCellsView class in views.py
-class DebugTopRightView(View):
- """Debug view to check top_right calculations"""
- def get(self, request):
- sheet_id = request.GET.get('sheet_id', 1)
- client_name = request.GET.get('client', 'Dr. Fohrer')
-
- try:
- sheet = MonthlySheet.objects.get(id=sheet_id)
- client = Client.objects.get(name=client_name)
-
- # Get all cells for this client in top_right
- cells = Cell.objects.filter(
- sheet=sheet,
- table_type='top_right',
- client=client
- ).order_by('row_index')
-
- cell_data = []
- descriptions = {
- 0: "Stand der Gaszähler (Vormonat)",
- 1: "Gasrückführung (Nm³)",
- 2: "Rückführung flüssig",
- 3: "Sonderrückführungen",
- 4: "Sammelrückführungen",
- 5: "Bestand in Kannen-1",
- 6: "Summe Bestand",
- 7: "Best. in Kannen Vormonat",
- 8: "Same as row 9 from prev sheet",
- 9: "Bezug",
- 10: "Rückführ. Soll",
- 11: "Verluste",
- 12: "Füllungen warm",
- 13: "Kaltgas Rückgabe",
- 14: "Faktor 0.06",
- 15: "Verbraucherverluste",
- 16: "%"
- }
-
- for cell in cells:
- cell_data.append({
- 'row_index': cell.row_index,
- 'ui_row': cell.row_index + 1,
- 'description': descriptions.get(cell.row_index, f"Row {cell.row_index}"),
- 'value': str(cell.value) if cell.value else 'Empty',
- 'cell_id': cell.id
- })
-
- # Test calculation
- row3_cell = cells.filter(row_index=3).first()
- row5_cell = cells.filter(row_index=5).first()
- row6_cell = cells.filter(row_index=6).first()
-
- calculation_info = {
- 'row3_value': str(row3_cell.value) if row3_cell and row3_cell.value else '0',
- 'row5_value': str(row5_cell.value) if row5_cell and row5_cell.value else '0',
- 'row6_value': str(row6_cell.value) if row6_cell and row6_cell.value else '0',
- 'expected_sum': '0'
- }
-
- if row3_cell and row5_cell and row6_cell:
- row3_val = Decimal(str(row3_cell.value)) if row3_cell.value else Decimal('0')
- row5_val = Decimal(str(row5_cell.value)) if row5_cell.value else Decimal('0')
- expected = row3_val + row5_val
- calculation_info['expected_sum'] = str(expected)
- calculation_info['is_correct'] = row6_cell.value == expected
-
- return JsonResponse({
- 'sheet': f"{sheet.year}-{sheet.month}",
- 'client': client.name,
- 'cells': cell_data,
- 'calculation': calculation_info
- })
-
- except Exception as e:
- return JsonResponse({'error': str(e)}, status=400)
-
-
-class SaveCellsView(View):
-
- def calculate_bottom_3_dependents(self, sheet):
- updated_cells = []
-
- def get_cell(row_idx, col_idx):
- return Cell.objects.filter(
- sheet=sheet,
- table_type='bottom_3',
- row_index=row_idx,
- column_index=col_idx
- ).first()
-
- def set_cell(row_idx, col_idx, value):
- cell, created = Cell.objects.get_or_create(
- sheet=sheet,
- table_type='bottom_3',
- row_index=row_idx,
- column_index=col_idx,
- defaults={'value': value}
- )
- if not created and cell.value != value:
- cell.value = value
- cell.save()
- elif created:
- cell.save()
-
- updated_cells.append({
- 'id': cell.id,
- 'value': str(cell.value) if cell.value is not None else '',
- 'is_calculated': True,
- })
-
- def dec(x):
- if x in (None, ''):
- return Decimal('0')
- try:
- return Decimal(str(x))
- except Exception:
- return Decimal('0')
-
- # ---- current summary ----
- cur_sum = MonthlySummary.objects.filter(sheet=sheet).first()
- curr_k44 = dec(cur_sum.gesamtbestand_neu_lhe) if cur_sum else Decimal('0')
- total_verb = dec(cur_sum.verbraucherverlust_lhe) if cur_sum else Decimal('0')
-
- # ---- previous month summary ----
- year, month = sheet.year, sheet.month
- if month == 1:
- prev_year, prev_month = year - 1, 12
- else:
- prev_year, prev_month = year, month - 1
-
- prev_sheet = MonthlySheet.objects.filter(year=prev_year, month=prev_month).first()
- if prev_sheet:
- prev_sum = MonthlySummary.objects.filter(sheet=prev_sheet).first()
- prev_k44 = dec(prev_sum.gesamtbestand_neu_lhe) if prev_sum else Decimal('0')
- else:
- prev_k44 = Decimal('0')
-
- # ---- read editable inputs from bottom_3: F47,G47,I47,I50 ----
- def get_val(r, c):
- cell = get_cell(r, c)
- return dec(cell.value if cell else None)
-
- f47 = get_val(1, 0)
- g47 = get_val(1, 1)
- i47 = get_val(1, 2)
- i50 = get_val(4, 2)
-
- # Now apply your formulas using prev_k44, curr_k44, total_verb, f47,g47,i47,i50
- # Row indices: 0..7 correspond to 46..53
- # col 3 = J, col 4 = K
-
- # Row 46
- k46 = prev_k44
- j46 = k46 * Decimal('0.75')
- set_cell(0, 3, j46)
- set_cell(0, 4, k46)
-
- # Row 47
- g47 = self._dec((get_cell(1, 1) or {}).value if get_cell(1, 1) else None)
- i47 = self._dec((get_cell(1, 2) or {}).value if get_cell(1, 2) else None)
-
- j47 = g47 + i47
- k47 = (j47 / Decimal('0.75')) + g47 if j47 != 0 else g47
-
- set_cell(1, 3, j47)
- set_cell(1, 4, k47)
-
- # Row 48
- k48 = k46 + k47
- j48 = k48 * Decimal('0.75')
- set_cell(2, 3, j48)
- set_cell(2, 4, k48)
-
- # Row 49
- k49 = curr_k44
- j49 = k49 * Decimal('0.75')
- set_cell(3, 3, j49)
- set_cell(3, 4, k49)
-
- # Row 50
- j50 = i50
- k50 = j50 / Decimal('0.75') if j50 != 0 else Decimal('0')
- set_cell(4, 3, j50)
- set_cell(4, 4, k50)
-
- # Row 51
- k51 = k48 - k49 - k50
- j51 = k51 * Decimal('0.75')
- set_cell(5, 3, j51)
- set_cell(5, 4, k51)
-
- # Row 52
- k52 = total_verb
- j52 = k52 * Decimal('0.75')
- set_cell(6, 3, j52)
- set_cell(6, 4, k52)
-
- # Row 53
- j53 = j51 - j52
- k53 = k51 - k52
- set_cell(7, 3, j53)
- set_cell(7, 4, k53)
-
- return updated_cells
-
-
- def _dec(self, value):
- """Convert value to Decimal or return 0."""
- if value is None or value == '':
- return Decimal('0')
- try:
- return Decimal(str(value))
- except Exception:
- return Decimal('0')
-
- def post(self, request, *args, **kwargs):
- """
- Handle AJAX saves from monthly_sheet.html
- - Single-cell save: when cell_id is present (blur on one cell)
- - Bulk save: when the 'Save All Cells' button is used (no cell_id)
- """
- try:
- sheet_id = request.POST.get('sheet_id')
- if not sheet_id:
- return JsonResponse({
- 'status': 'error',
- 'message': 'Missing sheet_id'
- })
-
- sheet = MonthlySheet.objects.get(id=sheet_id)
-
- # -------- Single-cell update (blur) --------
- cell_id = request.POST.get('cell_id')
- if cell_id:
- value_raw = (request.POST.get('value') or '').strip()
-
- try:
- cell = Cell.objects.get(id=cell_id, sheet=sheet)
- except Cell.DoesNotExist:
- return JsonResponse({
- 'status': 'error',
- 'message': 'Cell not found'
- })
-
- # Convert value to Decimal or None
- if value_raw == '':
- new_value = None
- else:
- try:
- # Allow comma or dot
- value_clean = value_raw.replace(',', '.')
- new_value = Decimal(value_clean)
- except (InvalidOperation, ValueError):
- # If conversion fails, treat as empty
- new_value = None
-
- old_value = cell.value
- cell.value = new_value
- cell.save()
-
- updated_cells = [{
- 'id': cell.id,
- 'value': '' if cell.value is None else str(cell.value),
- 'is_calculated': cell.is_formula, # model field
- }]
-
- # Recalculate dependents depending on table_type
- if cell.table_type == 'top_left':
- updated_cells.extend(
- self.calculate_top_left_dependents(sheet, cell)
- )
- elif cell.table_type == 'top_right':
- updated_cells.extend(
- self.calculate_top_right_dependents(sheet, cell)
- )
- elif cell.table_type == 'bottom_1':
- updated_cells.extend(self.calculate_bottom_1_dependents(sheet, cell))
- elif cell.table_type == 'bottom_3':
- updated_cells.extend(
- self.calculate_bottom_3_dependents(sheet)
- )
- # bottom_1 / bottom_2 / bottom_3 currently have no formulas:
- # they just save the new value.
- updated_cells += self.calculate_bottom_3_dependents(sheet)
- return JsonResponse({
- 'status': 'success',
- 'updated_cells': updated_cells
- })
-
-
- # -------- Bulk save (Save All button) --------
- return self.save_bulk_cells(request, sheet)
-
- except MonthlySheet.DoesNotExist:
- return JsonResponse({
- 'status': 'error',
- 'message': 'Sheet not found'
- })
- except Exception as e:
- # Generic safety net so the frontend sees an error message
- return JsonResponse({
- 'status': 'error',
- 'message': str(e)
- })
-
- # ... your other methods above ...
-
- # Update the calculate_top_right_dependents method in SaveCellsView class
-
- def calculate_top_right_dependents(self, sheet, changed_cell):
- """
- Recalculate all dependent cells in the top-right table according to Excel formulas.
-
- Excel rows (4-20) -> 0-based indices (0-15)
- Rows:
- 0: Stand der Gaszähler (Vormonat) (Nm³) - shares for M3 clients
- 1: Gasrückführung (Nm³)
- 2: Rückführung flüssig (Lit. L-He)
- 3: Sonderrückführungen (Lit. L-He) - editable
- 4: Sammelrückführungen (Lit. L-He) - from helium_input groups
- 5: Bestand in Kannen-1 (Lit. L-He) - editable, merged in pairs
- 6: Summe Bestand (Lit. L-He) = row 5
- 7: Best. in Kannen Vormonat (Lit. L-He) - from previous month
- 8: Bezug (Liter L-He) - from SecondTableEntry
- 9: Rückführ. Soll (Lit. L-He) - calculated
- 10: Verluste (Soll-Rückf.) (Lit. L-He) - calculated
- 11: Füllungen warm (Lit. L-He) - from SecondTableEntry warm outputs
- 12: Kaltgas Rückgabe (Lit. L-He) – Faktor - calculated
- 13: Faktor 0.06 - fixed
- 14: Verbraucherverluste (Liter L-He) - calculated
- 15: % - calculated
- """
- from decimal import Decimal
- from django.db.models import Sum, Count
- from django.db.models.functions import Coalesce
- from .models import Client, Cell, ExcelEntry, SecondTableEntry
-
- TOP_RIGHT_CLIENTS = [
- "Dr. Fohrer", # L
- "AG Buntk.", # M (merged with L)
- "AG Alff", # N
- "AG Gutfl.", # O (merged with N)
- "M3 Thiele", # P
- "M3 Buntkowsky", # Q
- "M3 Gutfleisch", # R
- ]
-
- # Define merged pairs
- MERGED_PAIRS = [
- ("Dr. Fohrer", "AG Buntk."), # L and M are merged
- ("AG Alff", "AG Gutfl."), # N and O are merged
- ]
-
- # M3 clients (not merged, calculated individually)
- M3_CLIENTS = ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"]
-
- # Groups for Sammelrückführungen (helium_input)
- HELIUM_INPUT_GROUPS = {
- "fohrer_buntk": ["Dr. Fohrer", "AG Buntk."],
- "alff_gutfl": ["AG Alff", "AG Gutfl."],
- "m3": ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"],
- }
-
- year = sheet.year
- month = sheet.month
- factor = Decimal('0.06') # Fixed factor from Excel
- updated_cells = []
-
- # Helper functions
- def get_val(client_name, row_idx):
- """Get cell value for a client and row"""
- try:
- client = Client.objects.get(name=client_name)
- col_idx = TOP_RIGHT_CLIENTS.index(client_name)
- cell = Cell.objects.filter(
- sheet=sheet,
- table_type='top_right',
- client=client,
- row_index=row_idx,
- column_index=col_idx
- ).first()
- if cell and cell.value is not None:
- return Decimal(str(cell.value))
- except (Client.DoesNotExist, ValueError, KeyError):
- pass
- return Decimal('0')
-
- def set_val(client_name, row_idx, value, is_calculated=True):
- """Set cell value for a client and row"""
- try:
- client = Client.objects.get(name=client_name)
- col_idx = TOP_RIGHT_CLIENTS.index(client_name)
-
- cell, created = Cell.objects.get_or_create(
- sheet=sheet,
- table_type='top_right',
- client=client,
- row_index=row_idx,
- column_index=col_idx,
- defaults={'value': value}
- )
-
- # Only update if value changed
- if not created and cell.value != value:
- cell.value = value
- cell.save()
- elif created:
- cell.save()
-
- updated_cells.append({
- 'id': cell.id,
- 'value': str(cell.value) if cell.value else '',
- 'is_calculated': is_calculated
- })
-
- return True
- except (Client.DoesNotExist, ValueError):
- return False
-
- # 1. Update Summe Bestand (row 6) from Bestand in Kannen-1 (row 5)
- # For merged pairs: copy value from changed cell to its pair
- if changed_cell and changed_cell.table_type == 'top_right':
- if changed_cell.row_index == 5: # Bestand in Kannen-1
- client_name = changed_cell.client.name
- new_value = changed_cell.value
-
- # Check if this client is in a merged pair
- for pair in MERGED_PAIRS:
- if client_name in pair:
- # Update both clients in the pair
- for client_in_pair in pair:
- if client_in_pair != client_name:
- set_val(client_in_pair, 5, new_value, is_calculated=False)
- break
-
- # 2. For all clients: Set Summe Bestand (row 6) = Bestand in Kannen-1 (row 5)
- for client_name in TOP_RIGHT_CLIENTS:
- bestand_value = get_val(client_name, 5)
- set_val(client_name, 6, bestand_value, is_calculated=True)
-
- # 3. Update Sammelrückführungen (row 4) from helium_input groups
- for group_name, client_names in HELIUM_INPUT_GROUPS.items():
- # Get total helium_input for this group
- clients_in_group = Client.objects.filter(name__in=client_names)
- total_helium = ExcelEntry.objects.filter(
- client__in=clients_in_group,
- date__year=year,
- date__month=month
- ).aggregate(total=Coalesce(Sum('lhe_ges'), Decimal('0')))['total']
-
- # Set same value for all clients in group
- for client_name in client_names:
- set_val(client_name, 4, total_helium, is_calculated=True)
-
- # 4. Calculate Rückführung flüssig (row 2)
- # For merged pairs: =L8 for Dr. Fohrer/AG Buntk., =N8 for AG Alff/AG Gutfl.
- # For M3 clients: =$P$8 * P4, $P$8 * Q4, $P$8 * R4
-
- # Get Sammelrückführungen values for groups
- sammel_fohrer_buntk = get_val("Dr. Fohrer", 4) # L8
- sammel_alff_gutfl = get_val("AG Alff", 4) # N8
- sammel_m3_group = get_val("M3 Thiele", 4) # P8 (same for all M3)
-
- # For merged pairs
- set_val("Dr. Fohrer", 2, sammel_fohrer_buntk, is_calculated=True)
- set_val("AG Buntk.", 2, sammel_fohrer_buntk, is_calculated=True)
- set_val("AG Alff", 2, sammel_alff_gutfl, is_calculated=True)
- set_val("AG Gutfl.", 2, sammel_alff_gutfl, is_calculated=True)
-
- # For M3 clients: =$P$8 * column4 (Stand der Gaszähler)
- for m3_client in M3_CLIENTS:
- stand_value = get_val(m3_client, 0) # Stand der Gaszähler (row 0)
- rueck_value = sammel_m3_group * stand_value
- set_val(m3_client, 2, rueck_value, is_calculated=True)
-
- # 5. Calculate Füllungen warm (row 11) from SecondTableEntry warm outputs
- # 5. Calculate Füllungen warm (row 11) as NUMBER of warm fillings
- # (sum of 1s where each warm SecondTableEntry is one filling)
- for client_name in TOP_RIGHT_CLIENTS:
- client = Client.objects.get(name=client_name)
- warm_count = SecondTableEntry.objects.filter(
- client=client,
- date__year=year,
- date__month=month,
- is_warm=True
- ).aggregate(
- total=Coalesce(Count('id'), 0)
- )['total']
-
- # store as Decimal so later formulas (warm * 15) still work nicely
- warm_value = Decimal(warm_count)
- set_val(client_name, 11, warm_value, is_calculated=True)
- # 6. Set Faktor row (13) to 0.06
- for client_name in TOP_RIGHT_CLIENTS:
- set_val(client_name, 13, factor, is_calculated=True)
-
- # 6a. Recalculate Stand der Gaszähler (row 0) for the merged pairs
- # according to Excel:
- # L4 = L13 / (L13 + M13), M4 = M13 / (L13 + M13)
- # N4 = N13 / (N13 + O13), O4 = O13 / (N13 + O13)
-
- # Pair 1: Dr. Fohrer / AG Buntk.
- bezug_dr = get_val("Dr. Fohrer", 8) # L13
- bezug_buntk = get_val("AG Buntk.", 8) # M13
- total_pair1 = bezug_dr + bezug_buntk
-
- if total_pair1 != 0:
- set_val("Dr. Fohrer", 0, bezug_dr / total_pair1, is_calculated=True)
- set_val("AG Buntk.", 0, bezug_buntk / total_pair1, is_calculated=True)
- else:
- # if no Bezug, both shares are 0
- set_val("Dr. Fohrer", 0, Decimal('0'), is_calculated=True)
- set_val("AG Buntk.", 0, Decimal('0'), is_calculated=True)
-
- # Pair 2: AG Alff / AG Gutfl.
- bezug_alff = get_val("AG Alff", 8) # N13
- bezug_gutfl = get_val("AG Gutfl.", 8) # O13
- total_pair2 = bezug_alff + bezug_gutfl
-
- if total_pair2 != 0:
- set_val("AG Alff", 0, bezug_alff / total_pair2, is_calculated=True)
- set_val("AG Gutfl.", 0, bezug_gutfl / total_pair2, is_calculated=True)
- else:
- set_val("AG Alff", 0, Decimal('0'), is_calculated=True)
- set_val("AG Gutfl.", 0, Decimal('0'), is_calculated=True)
-
-
- # 7. Calculate all other dependent rows for merged pairs
- for pair in MERGED_PAIRS:
- client1, client2 = pair
-
- # Get values for the pair
- bezug1 = get_val(client1, 8) # Bezug client1
- bezug2 = get_val(client2, 8) # Bezug client2
- total_bezug = bezug1 + bezug2 # L13+M13 or N13+O13
-
- summe_bestand = get_val(client1, 6) # L11 or N11 (merged, same value)
- best_vormonat = get_val(client1, 7) # L12 or N12 (merged, same value)
- rueck_fl = get_val(client1, 2) # L6 or N6 (merged, same value)
- warm1 = get_val(client1, 11) # L16 or N16
- warm2 = get_val(client2, 11) # M16 or O16
- total_warm = warm1 + warm2 # L16+M16 or N16+O16
-
- # Calculate Rückführ. Soll (row 9)
- # = L13+M13 - L11 + L12 for first pair
- # = N13+O13 - N11 + N12 for second pair
- rueck_soll = total_bezug - summe_bestand + best_vormonat
- set_val(client1, 9, rueck_soll, is_calculated=True)
- set_val(client2, 9, rueck_soll, is_calculated=True)
-
- # Calculate Verluste (row 10) = Rückführ. Soll - Rückführung flüssig
- verluste = rueck_soll - rueck_fl
- set_val(client1, 10, verluste, is_calculated=True)
- set_val(client2, 10, verluste, is_calculated=True)
-
- # Calculate Kaltgas Rückgabe (row 12)
- # = (L13+M13)*$A18 + (L16+M16)*15
- kaltgas = (total_bezug * factor) + (total_warm * Decimal('15'))
- set_val(client1, 12, kaltgas, is_calculated=True)
- set_val(client2, 12, kaltgas, is_calculated=True)
-
- # Calculate Verbraucherverluste (row 14) = Verluste - Kaltgas
- verbrauch = verluste - kaltgas
- set_val(client1, 14, verbrauch, is_calculated=True)
- set_val(client2, 14, verbrauch, is_calculated=True)
-
- # Calculate % (row 15) = Verbraucherverluste / (L13+M13)
- if total_bezug != 0:
- prozent = verbrauch / total_bezug
- else:
- prozent = Decimal('0')
- set_val(client1, 15, prozent, is_calculated=True)
- set_val(client2, 15, prozent, is_calculated=True)
-
- # 8. Calculate all dependent rows for M3 clients (individual calculations)
- for m3_client in M3_CLIENTS:
- # Get individual values
- bezug = get_val(m3_client, 8) # Bezug for this M3 client
- summe_bestand = get_val(m3_client, 6) # Summe Bestand
- best_vormonat = get_val(m3_client, 7) # Best. in Kannen Vormonat
- rueck_fl = get_val(m3_client, 2) # Rückführung flüssig
- warm = get_val(m3_client, 11) # Füllungen warm
-
- # Calculate Rückführ. Soll (row 9) = Bezug - Summe Bestand + Best. Vormonat
- rueck_soll = bezug - summe_bestand + best_vormonat
- set_val(m3_client, 9, rueck_soll, is_calculated=True)
-
- # Calculate Verluste (row 10) = Rückführ. Soll - Rückführung flüssig
- verluste = rueck_soll - rueck_fl
- set_val(m3_client, 10, verluste, is_calculated=True)
-
- # Calculate Kaltgas Rückgabe (row 12) = Bezug * factor + warm * 15
- kaltgas = (bezug * factor) + (warm * Decimal('15'))
- set_val(m3_client, 12, kaltgas, is_calculated=True)
-
- # Calculate Verbraucherverluste (row 14) = Verluste - Kaltgas
- verbrauch = verluste - kaltgas
- set_val(m3_client, 14, verbrauch, is_calculated=True)
-
- # Calculate % (row 15) = Verbraucherverluste / Bezug
- if bezug != 0:
- prozent = verbrauch / bezug
- else:
- prozent = Decimal('0')
- set_val(m3_client, 15, prozent, is_calculated=True)
-
- return updated_cells
-
-
-
- def calculate_bottom_1_dependents(self, sheet, changed_cell):
- """
- Recalculate Bottom Table 1 (table_type='bottom_1').
-
- Layout (row_index 0–9, col_index 0–4):
-
- Rows 0–8:
- 0: Batterie 1
- 1: 2
- 2: 3
- 3: 4
- 4: 5
- 5: 6
- 6: 2 Bündel
- 7: 2 Ballone
- 8: Reingasspeicher
-
- Row 9:
- 9: Gasbestand (totals row)
-
- Columns:
- 0: Volumen (fixed values, not editable)
- 1: bar (editable for rows 0–8)
- 2: korrigiert = bar / (bar/2000 + 1)
- 3: Nm³ = Volumen * korrigiert
- 4: Lit. LHe = Nm³ / 0.75
-
- Row 9:
- - Volumen (col 0): empty
- - bar (col 1): empty
- - korrigiert (col 2): empty
- - Nm³ (col 3): SUM Nm³ rows 0–8
- - Lit. LHe (col 4): SUM Lit. LHe rows 0–8
- """
- from decimal import Decimal, InvalidOperation
- from .models import Cell
-
- updated_cells = []
-
- DATA_ROWS = list(range(0, 9)) # 0–8
- TOTAL_ROW = 9
-
- COL_VOL = 0
- COL_BAR = 1
- COL_KORR = 2
- COL_NM3 = 3
- COL_LHE = 4
-
- # Fixed Volumen values for rows 0–8
- VOLUMES = [
- Decimal("2.4"), # row 0
- Decimal("5.1"), # row 1
- Decimal("4.0"), # row 2
- Decimal("1.0"), # row 3
- Decimal("4.0"), # row 4
- Decimal("0.4"), # row 5
- Decimal("1.2"), # row 6
- Decimal("20.0"), # row 7
- Decimal("5.0"), # row 8
- ]
-
- def get_cell(row_idx, col_idx):
- return Cell.objects.filter(
- sheet=sheet,
- table_type="bottom_1",
- row_index=row_idx,
- column_index=col_idx,
- ).first()
-
- def set_calc(row_idx, col_idx, value):
- """
- Set a calculated cell and add it to updated_cells.
- If value is None, we clear the cell.
- """
- cell = get_cell(row_idx, col_idx)
- if not cell:
- cell = Cell(
- sheet=sheet,
- table_type="bottom_1",
- row_index=row_idx,
- column_index=col_idx,
- value=value,
- )
- else:
- cell.value = value
- cell.save()
-
- updated_cells.append(
- {
- "id": cell.id,
- "value": "" if cell.value is None else str(cell.value),
- "is_calculated": True,
- }
- )
-
- # ---------- Rows 0–8: per-gasspeicher calculations ----------
- for row_idx in DATA_ROWS:
- bar_cell = get_cell(row_idx, COL_BAR)
-
- # Volumen: fixed constant or, if present, value from DB
- vol = VOLUMES[row_idx]
- vol_cell = get_cell(row_idx, COL_VOL)
- if vol_cell and vol_cell.value is not None:
- try:
- vol = Decimal(str(vol_cell.value))
- except (InvalidOperation, ValueError):
- # fall back to fixed constant
- vol = VOLUMES[row_idx]
-
- bar = None
- try:
- if bar_cell and bar_cell.value is not None:
- bar = Decimal(str(bar_cell.value))
- except (InvalidOperation, ValueError):
- bar = None
-
- # korrigiert = bar / (bar/2000 + 1)
- if bar is not None and bar != 0:
- korr = bar / (bar / Decimal("2000") + Decimal("1"))
- else:
- korr = None
-
- # Nm³ = Volumen * korrigiert
- if korr is not None:
- nm3 = vol * korr
- else:
- nm3 = None
-
- # Lit. LHe = Nm³ / 0.75
- if nm3 is not None:
- lit_lhe = nm3 / Decimal("0.75")
- else:
- lit_lhe = None
-
- # Write calculated cells back (NOT Volumen or bar)
- set_calc(row_idx, COL_KORR, korr)
- set_calc(row_idx, COL_NM3, nm3)
- set_calc(row_idx, COL_LHE, lit_lhe)
-
- # ---------- Row 9: totals (Gasbestand) ----------
- total_nm3 = Decimal("0")
- total_lhe = Decimal("0")
- has_nm3 = False
- has_lhe = False
-
- for row_idx in DATA_ROWS:
- nm3_cell = get_cell(row_idx, COL_NM3)
- if nm3_cell and nm3_cell.value is not None:
- try:
- total_nm3 += Decimal(str(nm3_cell.value))
- has_nm3 = True
- except (InvalidOperation, ValueError):
- pass
-
- lhe_cell = get_cell(row_idx, COL_LHE)
- if lhe_cell and lhe_cell.value is not None:
- try:
- total_lhe += Decimal(str(lhe_cell.value))
- has_lhe = True
- except (InvalidOperation, ValueError):
- pass
-
- # Volumen (0), bar (1), korrigiert (2) on total row stay empty
- set_calc(TOTAL_ROW, COL_KORR, None) # explicitly clear korrigiert
- set_calc(TOTAL_ROW, COL_NM3, total_nm3 if has_nm3 else None)
- set_calc(TOTAL_ROW, COL_LHE, total_lhe if has_lhe else None)
-
- return updated_cells
-
-
-
-
-
- def calculate_top_left_dependents(self, sheet, changed_cell):
- """Calculate dependent cells in top_left table"""
- from decimal import Decimal
- from django.db.models import Sum
- from django.db.models.functions import Coalesce
-
- client_id = changed_cell.client_id
- updated_cells = []
-
- # Get all cells for this client in top_left table
- client_cells = Cell.objects.filter(
- sheet=sheet,
- table_type='top_left',
- client_id=client_id
- )
-
- # Create a dict for easy access
- cell_dict = {}
- for cell in client_cells:
- cell_dict[cell.row_index] = cell
-
- # Get values with safe defaults
- def get_cell_value(row_idx):
- cell = cell_dict.get(row_idx)
- if cell and cell.value is not None:
- try:
- return Decimal(str(cell.value))
- except:
- return Decimal('0')
- return Decimal('0')
-
- # 1. B5 = IF(B3>0; B3-B4; 0) - Gasrückführung
- b3 = get_cell_value(0) # row_index 0 = Excel B3 (UI row 1)
- b4 = get_cell_value(1) # row_index 1 = Excel B4 (UI row 2)
-
- # Calculate B5
- if b3 > 0:
- b5_value = b3 - b4
- if b5_value < 0:
- b5_value = Decimal('0')
- else:
- b5_value = Decimal('0')
-
- # Update B5 - FORCE update even if value appears the same
- b5_cell = cell_dict.get(2) # row_index 2 = Excel B5 (UI row 3)
- if b5_cell:
- # Always update B5 when B3 or B4 changes
- b5_cell.value = b5_value
- b5_cell.save()
- updated_cells.append({
- 'id': b5_cell.id,
- 'value': str(b5_cell.value),
- 'is_calculated': True
- })
-
- # 2. B6 = B5 / 0.75 - Rückführung flüssig
- b6_value = b5_value / Decimal('0.75')
- b6_cell = cell_dict.get(3) # row_index 3 = Excel B6 (UI row 4)
- if b6_cell:
- b6_cell.value = b6_value
- b6_cell.save()
- updated_cells.append({
- 'id': b6_cell.id,
- 'value': str(b6_cell.value),
- 'is_calculated': True
- })
-
- # 3. B9 = B7 + B8 - Summe Bestand
- b7 = get_cell_value(4) # row_index 4 = Excel B7 (UI row 5)
- b8 = get_cell_value(5) # row_index 5 = Excel B8 (UI row 6)
- b9_value = b7 + b8
- b9_cell = cell_dict.get(6) # row_index 6 = Excel B9 (UI row 7)
- if b9_cell:
- b9_cell.value = b9_value
- b9_cell.save()
- updated_cells.append({
- 'id': b9_cell.id,
- 'value': str(b9_cell.value),
- 'is_calculated': True
- })
-
- # 4. B11 = Sum of LHe Output from SecondTableEntry for this client/month - Bezug
- from .models import SecondTableEntry
- client = changed_cell.client
-
- # Calculate total LHe output for this client in this month
- # 4. B11 = Bezug (Liter L-He)
- # For start sheet: manual entry, for other sheets: auto-calculated from SecondTableEntry
- from .models import SecondTableEntry
- client = changed_cell.client
-
- b11_cell = cell_dict.get(8) # row_index 8 = Excel B11 (UI row 9)
-
- # Check if this is the start sheet (2025-01)
- is_start_sheet = (sheet.year == 2025 and sheet.month == 1)
-
- # Get the B11 value for calculations
- b11_value = Decimal('0')
-
- if is_start_sheet:
- # For start sheet, keep whatever value is already there (manual entry)
- if b11_cell and b11_cell.value is not None:
- b11_value = Decimal(str(b11_cell.value))
- else:
- # For non-start sheets: auto-calculate from SecondTableEntry
- lhe_output_sum = SecondTableEntry.objects.filter(
- client=client,
- date__year=sheet.year,
- date__month=sheet.month
- ).aggregate(
- total=Coalesce(Sum('lhe_output'), Decimal('0'))
- )['total']
-
- b11_value = lhe_output_sum
-
- if b11_cell and (b11_cell.value != b11_value or b11_cell.value is None):
- b11_cell.value = b11_value
- b11_cell.save()
- updated_cells.append({
- 'id': b11_cell.id,
- 'value': str(b11_cell.value),
- 'is_calculated': True # Calculated from SecondTableEntry
- })
-
- # 5. B12 = B11 + B10 - B9 - Rückführ. Soll
- b10 = get_cell_value(7) # row_index 7 = Excel B10 (UI row 8)
- b12_value = b11_value + b10 - b9_value # Use b11_value instead of lhe_output_sum
- b12_cell = cell_dict.get(9) # row_index 9 = Excel B12 (UI row 10)
- if b12_cell:
- b12_cell.value = b12_value
- b12_cell.save()
- updated_cells.append({
- 'id': b12_cell.id,
- 'value': str(b12_cell.value),
- 'is_calculated': True
- })
-
- # 6. B13 = B12 - B6 - Verluste (Soll-Rückf.)
- b13_value = b12_value - b6_value
- b13_cell = cell_dict.get(10) # row_index 10 = Excel B13 (UI row 11)
- if b13_cell:
- b13_cell.value = b13_value
- b13_cell.save()
- updated_cells.append({
- 'id': b13_cell.id,
- 'value': str(b13_cell.value),
- 'is_calculated': True
- })
-
- # 7. B14 = Count of warm outputs
- warm_count = SecondTableEntry.objects.filter(
- client=client,
- date__year=sheet.year,
- date__month=sheet.month,
- is_warm=True
- ).count()
-
- b14_cell = cell_dict.get(11) # row_index 11 = Excel B14 (UI row 12)
- if b14_cell:
- b14_cell.value = Decimal(str(warm_count))
- b14_cell.save()
- updated_cells.append({
- 'id': b14_cell.id,
- 'value': str(b14_cell.value),
- 'is_calculated': True
- })
-
- # 8. B15 = IF(B11>0; B11 * factor + B14 * 15; 0) - Kaltgas Rückgabe
- factor = get_cell_value(13) # row_index 13 = Excel B16 (Faktor) (UI row 14)
- if factor == 0:
- factor = Decimal('0.06') # default factor
-
- if b11_value > 0: # Use b11_value
- b15_value = b11_value * factor + Decimal(str(warm_count)) * Decimal('15')
- else:
- b15_value = Decimal('0')
-
- b15_cell = cell_dict.get(12) # row_index 12 = Excel B15 (UI row 13)
- if b15_cell:
- b15_cell.value = b15_value
- b15_cell.save()
- updated_cells.append({
- 'id': b15_cell.id,
- 'value': str(b15_cell.value),
- 'is_calculated': True
- })
-
- # 9. B17 = B13 - B15 - Verbraucherverluste
- b17_value = b13_value - b15_value
- b17_cell = cell_dict.get(14) # row_index 14 = Excel B17 (UI row 15)
- if b17_cell:
- b17_cell.value = b17_value
- b17_cell.save()
- updated_cells.append({
- 'id': b17_cell.id,
- 'value': str(b17_cell.value),
- 'is_calculated': True
- })
-
- # 10. B18 = IF(B11=0; 0; B17/B11) - %
- if b11_value == 0: # Use b11_value
- b18_value = Decimal('0')
- else:
- b18_value = b17_value / b11_value # Use b11_value
-
- b18_cell = cell_dict.get(15) # row_index 15 = Excel B18 (UI row 16)
- if b18_cell:
- b18_cell.value = b18_value
- b18_cell.save()
- updated_cells.append({
- 'id': b18_cell.id,
- 'value': str(b18_cell.value),
- 'is_calculated': True
- })
-
- return updated_cells
- def save_bulk_cells(self, request, sheet):
- """Original bulk save logic (for backward compatibility)"""
- # Get all cell updates
- cell_updates = {}
- for key, value in request.POST.items():
- if key.startswith('cell_'):
- cell_id = key.replace('cell_', '')
- cell_updates[cell_id] = value
-
- # Update cells and track which ones changed
- updated_cells = []
- changed_clients = set()
-
- for cell_id, new_value in cell_updates.items():
- try:
- cell = Cell.objects.get(id=cell_id, sheet=sheet)
- old_value = cell.value
-
- # Convert new value
- try:
- if new_value.strip():
- cell.value = Decimal(new_value)
- else:
- cell.value = None
- except Exception:
- cell.value = None
-
- # Only save if value changed
- if cell.value != old_value:
- cell.save()
- updated_cells.append({
- 'id': cell.id,
- 'value': str(cell.value) if cell.value else ''
- })
- # bottom_3 has no client, so this will just add None for those cells,
- # which is harmless. Top-left cells still add their real client_id.
- changed_clients.add(cell.client_id)
-
- except Cell.DoesNotExist:
- continue
-
- # Recalculate for each changed client (top-left tables)
- for client_id in changed_clients:
- if client_id is not None:
- self.recalculate_top_left_table(sheet, client_id)
-
- # --- NEW: recalc bottom_3 for the whole sheet, independent of clients ---
- bottom3_updates = self.calculate_bottom_3_dependents(sheet)
-
- # Get all updated cells for response (top-left)
- all_updated_cells = []
- for client_id in changed_clients:
- if client_id is None:
- continue # skip bottom_3 / non-client cells
- client_cells = Cell.objects.filter(
- sheet=sheet,
- client_id=client_id
- )
- for cell in client_cells:
- all_updated_cells.append({
- 'id': cell.id,
- 'value': str(cell.value) if cell.value else '',
- 'is_calculated': cell.is_formula
- })
-
- # Add bottom_3 recalculated cells (J46..K53, etc.)
- all_updated_cells.extend(bottom3_updates)
-
- return JsonResponse({
- 'status': 'success',
- 'message': f'Saved {len(updated_cells)} cells',
- 'updated_cells': all_updated_cells
- })
-
-
- def recalculate_top_left_table(self, sheet, client_id):
- """Recalculate the top-left table for a specific client"""
- from decimal import Decimal
-
- # Get all cells for this client in top_left table
- cells = Cell.objects.filter(
- sheet=sheet,
- table_type='top_left',
- client_id=client_id
- ).order_by('row_index')
-
- # Create a dictionary of cell values
- cell_dict = {}
- for cell in cells:
- cell_dict[cell.row_index] = cell
-
- # Excel logic implementation for top-left table
- # B3 (row_index 0) = Stand der Gaszähler (Nm³) - manual
- # B4 (row_index 1) = Stand der Gaszähler (Vormonat) (Nm³) - from previous sheet
-
- # Get B3 and B4
- b3_cell = cell_dict.get(0) # UI Row 3
- b4_cell = cell_dict.get(1) # UI Row 4
-
- if b3_cell and b3_cell.value and b4_cell and b4_cell.value:
- # B5 = IF(B3>0; B3-B4; 0)
- b3 = Decimal(str(b3_cell.value))
- b4 = Decimal(str(b4_cell.value))
-
- if b3 > 0:
- b5 = b3 - b4
- if b5 < 0:
- b5 = Decimal('0')
- else:
- b5 = Decimal('0')
-
- # Update B5 (row_index 2)
- b5_cell = cell_dict.get(2)
- if b5_cell and (b5_cell.value != b5 or b5_cell.value is None):
- b5_cell.value = b5
- b5_cell.save()
-
- # B6 = B5 / 0.75 (row_index 3)
- b6 = b5 / Decimal('0.75')
- b6_cell = cell_dict.get(3)
- if b6_cell and (b6_cell.value != b6 or b6_cell.value is None):
- b6_cell.value = b6
- b6_cell.save()
-
- # Get previous month's sheet for B10
- if sheet.month == 1:
- prev_year = sheet.year - 1
- prev_month = 12
- else:
- prev_year = sheet.year
- prev_month = sheet.month - 1
-
- prev_sheet = MonthlySheet.objects.filter(
- year=prev_year,
- month=prev_month
- ).first()
-
- if prev_sheet:
- # Get B9 from previous sheet (row_index 7 in previous)
- prev_b9 = Cell.objects.filter(
- sheet=prev_sheet,
- table_type='top_left',
- client_id=client_id,
- row_index=7 # UI Row 9
- ).first()
-
- if prev_b9 and prev_b9.value:
- # Update B10 in current sheet (row_index 8)
- b10_cell = cell_dict.get(8)
- if b10_cell and (b10_cell.value != prev_b9.value or b10_cell.value is None):
- b10_cell.value = prev_b9.value
- b10_cell.save()
-@method_decorator(csrf_exempt, name='dispatch')
-class SaveMonthSummaryView(View):
- """
- Saves per-month summary values such as K44 (Gesamtbestand neu).
- Called from JS after 'Save All' finishes.
- """
-
- def post(self, request, *args, **kwargs):
- try:
- data = json.loads(request.body.decode('utf-8'))
- except json.JSONDecodeError:
- return JsonResponse(
- {'success': False, 'message': 'Invalid JSON'},
- status=400
- )
-
- sheet_id = data.get('sheet_id')
- if not sheet_id:
- return JsonResponse(
- {'success': False, 'message': 'Missing sheet_id'},
- status=400
- )
-
- try:
- sheet = MonthlySheet.objects.get(id=sheet_id)
- except MonthlySheet.DoesNotExist:
- return JsonResponse(
- {'success': False, 'message': 'Sheet not found'},
- status=404
- )
-
- # More tolerant decimal conversion: accepts "123.45" and "123,45"
- def to_decimal(value):
- if value is None:
- return None
- s = str(value).strip()
- if s == '':
- return None
- s = s.replace(',', '.')
- try:
- return Decimal(s)
- except (InvalidOperation, ValueError):
- # Debug: show what failed in the dev server console
- print("SaveMonthSummaryView to_decimal failed for:", repr(value))
- return None
-
- raw_k44 = data.get('gesamtbestand_neu_lhe')
- raw_gas = data.get('gasbestand_lhe')
- raw_verb = data.get('verbraucherverlust_lhe')
-
- gesamtbestand_neu_lhe = to_decimal(raw_k44)
- gasbestand_lhe = to_decimal(raw_gas)
- verbraucherverlust_lhe = to_decimal(raw_verb)
-
- summary, created = MonthlySummary.objects.get_or_create(sheet=sheet)
-
- if gesamtbestand_neu_lhe is not None:
- summary.gesamtbestand_neu_lhe = gesamtbestand_neu_lhe
- if gasbestand_lhe is not None:
- summary.gasbestand_lhe = gasbestand_lhe
- if verbraucherverlust_lhe is not None:
- summary.verbraucherverlust_lhe = verbraucherverlust_lhe
-
- summary.save()
-
- # Small debug output so you can see in the server console what was saved
- print(
- f"Saved MonthlySummary for {sheet.year}-{sheet.month:02d}: "
- f"K44={summary.gesamtbestand_neu_lhe}, "
- f"Gasbestand={summary.gasbestand_lhe}, "
- f"Verbraucherverlust={summary.verbraucherverlust_lhe}"
- )
-
- return JsonResponse({'success': True})
-
-
-
-# Calculate View (placeholder for calculations)
-class CalculateView(View):
- def post(self, request):
- # This will be implemented when you provide calculation rules
- return JsonResponse({
- 'status': 'success',
- 'message': 'Calculation endpoint ready'
- })
-
-# Summary Sheet View
-class SummarySheetView(TemplateView):
- template_name = 'summary_sheet.html'
-
- def get_context_data(self, **kwargs):
- context = super().get_context_data(**kwargs)
- start_month = int(self.kwargs.get('start_month', 1))
- year = int(self.kwargs.get('year', datetime.now().year))
-
- # Get 6 monthly sheets
- months = [(year, m) for m in range(start_month, start_month + 6)]
- sheets = MonthlySheet.objects.filter(
- year=year,
- month__in=list(range(start_month, start_month + 6))
- ).order_by('month')
-
- # Aggregate data across months
- summary_data = self.calculate_summary(sheets)
-
- context.update({
- 'year': year,
- 'start_month': start_month,
- 'end_month': start_month + 5,
- 'sheets': sheets,
- 'clients': Client.objects.all(),
- 'summary_data': summary_data,
- })
- return context
-
- def calculate_summary(self, sheets):
- """Calculate totals across 6 months"""
- summary = {}
-
- for client in Client.objects.all():
- client_total = 0
- for sheet in sheets:
- # Get specific cells and sum
- cells = sheet.cells.filter(
- client=client,
- table_type='top_left',
- row_index=0 # Example: first row
- )
- for cell in cells:
- if cell.value:
- try:
- client_total += float(cell.value)
- except (ValueError, TypeError):
- continue
-
- summary[client.id] = client_total
-
- return summary
-
-# Existing views below (keep all your existing functions)
-def clients_list(request):
- # --- Clients for the yearly summary table ---
- clients = Client.objects.all()
-
- # --- Available years for output data (same as before) ---
- available_years_qs = SecondTableEntry.objects.dates('date', 'year', order='DESC')
- available_years = [y.year for y in available_years_qs]
-
- # === 1) Year used for the "Helium Output Yearly Summary" table ===
- # Uses ?year=... in the dropdown
- year_param = request.GET.get('year')
- if year_param:
- selected_year = int(year_param)
- else:
- selected_year = available_years[0] if available_years else datetime.now().year
-
- # === 2) GLOBAL half-year interval (shared with other pages) =======
- # Try GET params first
- interval_year_param = request.GET.get('interval_year')
- start_month_param = request.GET.get('interval_start_month')
-
- # Fallbacks from session
- session_year = request.session.get('halfyear_year')
- session_start_month = request.session.get('halfyear_start_month')
-
- # Determine final interval_year
- if interval_year_param:
- interval_year = int(interval_year_param)
- elif session_year:
- interval_year = int(session_year)
- else:
- # default: same as selected_year for summary
- interval_year = selected_year
-
- # Determine final interval_start_month
- if start_month_param:
- interval_start_month = int(start_month_param)
- elif session_start_month:
- interval_start_month = int(session_start_month)
- else:
- interval_start_month = 1 # default Jan
-
- # Store back into the session so other views can read them
- request.session['halfyear_year'] = interval_year
- request.session['halfyear_start_month'] = interval_start_month
-
- # === 3) Build a 6-month window, allowing wrap into the next year ===
- # Example: interval_year=2025, interval_start_month=12
- # window = [(2025,12), (2026,1), (2026,2), (2026,3), (2026,4), (2026,5)]
- window = []
- for offset in range(6):
- total_index = (interval_start_month - 1) + offset # 0-based index
- y = interval_year + (total_index // 12)
- m = (total_index % 12) + 1
- window.append((y, m))
-
- # === 4) Totals per client in that 6-month window ==================
- monthly_data = []
- for client in clients:
- monthly_totals = []
- for (y, m) in window:
- total = SecondTableEntry.objects.filter(
- client=client,
- date__year=y,
- date__month=m
- ).aggregate(
- total=Coalesce(
- Sum('lhe_output'),
- Value(0, output_field=DecimalField())
- )
- )['total']
- monthly_totals.append(total)
-
- monthly_data.append({
- 'client': client,
- 'monthly_totals': monthly_totals,
- 'year_total': sum(monthly_totals),
- })
-
- # === 5) Month labels for the header (only the 6-month window) =====
- month_labels = [calendar.month_abbr[m] for (y, m) in window]
-
- # === 6) FINALLY: return the response ==============================
- return render(request, 'clients_table.html', {
- 'available_years': available_years,
- 'current_year': selected_year, # used by year dropdown
- 'interval_year': interval_year, # used by "Global 6-Month Interval" form
- 'interval_start_month': interval_start_month,
- 'months': month_labels,
- 'monthly_data': monthly_data,
- })
-
- # === 5) Month labels for the header (only the 6-month window) =====
- month_labels = [calendar.month_abbr[m] for (y, m) in window]
-
-
-def set_halfyear_interval(request):
- if request.method == 'POST':
- year = int(request.POST.get('year'))
- start_month = int(request.POST.get('start_month'))
-
- request.session['halfyear_year'] = year
- request.session['halfyear_start_month'] = start_month
-
- return redirect(request.META.get('HTTP_REFERER', 'clients_list'))
-
- return redirect('clients_list')
-# Table One View (ExcelEntry)
-def table_one_view(request):
- from .models import ExcelEntry, Client, Institute
-
- # --- Base queryset for the main Helium Input table ---
- base_entries = ExcelEntry.objects.all().select_related('client', 'client__institute')
-
- # Read the global 6-month interval from the session
- interval_year = request.session.get('halfyear_year')
- interval_start = request.session.get('halfyear_start_month')
-
- if interval_year and interval_start:
- interval_year = int(interval_year)
- interval_start = int(interval_start)
-
- # Build the same 6-month window as on the main page (can cross year)
- window = []
- for offset in range(6):
- total_index = (interval_start - 1) + offset # 0-based
- y = interval_year + (total_index // 12)
- m = (total_index % 12) + 1
- window.append((y, m))
-
- # Build Q filter: (year=m_year AND month=m_month) for any of those 6
- q = Q()
- for (y, m) in window:
- q |= Q(date__year=y, date__month=m)
-
- entries_table1 = base_entries.filter(q).order_by('-date')
- else:
- # Fallback: if no global interval yet, show everything
- entries_table1 = base_entries.order_by('-date')
- clients = Client.objects.all().select_related('institute')
- institutes = Institute.objects.all()
-
- # ---- Overview filters ----
- # years present in ExcelEntry.date
- year_qs = ExcelEntry.objects.dates('date', 'year', order='DESC')
- available_years = [d.year for d in year_qs]
-
- # default year/start month
- # default year/start month (if no global interval yet)
- if available_years:
- default_year = available_years[0] # newest year in ExcelEntry
- else:
- default_year = timezone.now().year
-
- # 🔸 Read global half-year interval from session (set on main page)
- session_year = request.session.get('halfyear_year')
- session_start = request.session.get('halfyear_start_month')
-
- # If the user has set a global interval, use it.
- # Otherwise fall back to default year / January.
- year = int(session_year) if session_year else int(default_year)
- start_month = int(session_start) if session_start else 1
-
-
- # six-month window
- # --- Build a 6-month window, allowing wrap into the next year ---
- # Example: year=2025, start_month=10
- # window = [(2025,10), (2025,11), (2025,12), (2026,1), (2026,2), (2026,3)]
- window = []
- for offset in range(6):
- total_index = (start_month - 1) + offset # 0-based
- y_for_month = year + (total_index // 12)
- m_for_month = (total_index % 12) + 1
- window.append((y_for_month, m_for_month))
-
- overview = None
-
- if window:
- # Build per-group data
- groups_entries = [] # for internal calculations
-
- for key, group in CLIENT_GROUPS.items():
- clients_qs = get_group_clients(key)
-
- values = []
- group_total = Decimal('0')
-
- for (y_m, m_m) in window:
- total = ExcelEntry.objects.filter(
- client__in=clients_qs,
- date__year=y_m,
- date__month=m_m
- ).aggregate(
- total=Coalesce(Sum('lhe_ges'), Decimal('0'))
- )['total']
-
- values.append(total)
- group_total += total
-
- groups_entries.append({
- 'key': key,
- 'label': group['label'],
- 'values': values,
- 'total': group_total,
- })
-
- # month totals across all groups
- month_totals = []
- for idx in range(len(window)):
- s = sum((g['values'][idx] for g in groups_entries), Decimal('0'))
- month_totals.append(s)
-
- grand_total = sum(month_totals, Decimal('0'))
-
- # Build rows for the template
- rows = []
- for idx, (y_m, m_m) in enumerate(window):
- row_values = [g['values'][idx] for g in groups_entries]
- rows.append({
- 'month_number': m_m,
- 'month_label': calendar.month_name[m_m],
- 'values': row_values,
- 'total': month_totals[idx],
- })
-
- groups_meta = [{'key': g['key'], 'label': g['label']} for g in groups_entries]
- group_totals = [g['total'] for g in groups_entries]
-
- # Start/end for display – include years so wrap is clear
- start_year = window[0][0]
- start_month_disp = window[0][1]
- end_year = window[-1][0]
- end_month_disp = window[-1][1]
-
- overview = {
- 'year': year, # keep for backwards compatibility if needed
- 'start_month': start_month_disp,
- 'start_year': start_year,
- 'end_month': end_month_disp,
- 'end_year': end_year,
- 'rows': rows,
- 'groups': groups_meta,
- 'group_totals': group_totals,
- 'grand_total': grand_total,
- }
-
-
- # Month dropdown labels
- MONTH_CHOICES = [
- (1, 'Jan'), (2, 'Feb'), (3, 'Mar'), (4, 'Apr'),
- (5, 'May'), (6, 'Jun'), (7, 'Jul'), (8, 'Aug'),
- (9, 'Sep'), (10, 'Oct'), (11, 'Nov'), (12, 'Dec'),
- ]
-
- return render(request, 'table_one.html', {
- 'entries_table1': entries_table1,
- 'clients': clients,
- 'institutes': institutes,
- 'available_years': available_years,
- 'month_choices': MONTH_CHOICES,
- 'overview': overview,
- })
-
-# Table Two View (SecondTableEntry)
-def table_two_view(request):
- try:
- clients = Client.objects.all().select_related('institute')
- institutes = Institute.objects.all()
-
- # 🔸 Read global half-year interval from session
- interval_year = request.session.get('halfyear_year')
- interval_start = request.session.get('halfyear_start_month')
-
- if interval_year and interval_start:
- interval_year = int(interval_year)
- interval_start = int(interval_start)
-
- # Build the same 6-month window as in clients_list (can cross years)
- window = []
- for offset in range(6):
- total_index = (interval_start - 1) + offset # 0-based
- y = interval_year + (total_index // 12)
- m = (total_index % 12) + 1
- window.append((y, m))
-
- # Build a Q object matching any of those (year, month) pairs
- q = Q()
- for (y, m) in window:
- q |= Q(date__year=y, date__month=m)
-
- entries = SecondTableEntry.objects.filter(q).order_by('-date')
- else:
- # Fallback if no global interval yet: show all
- entries = SecondTableEntry.objects.all().order_by('-date')
-
- return render(request, 'table_two.html', {
- 'entries_table2': entries,
- 'clients': clients,
- 'institutes': institutes,
- 'interval_year': interval_year,
- 'interval_start_month': interval_start,
- })
-
- except Exception as e:
- return render(request, 'table_two.html', {
- 'error_message': f"Failed to load data: {str(e)}",
- 'entries_table2': [],
- 'clients': clients,
- 'institutes': institutes,
- })
-
-
-def monthly_sheet_root(request):
- """
- Redirect /sheet/ to the sheet matching the globally selected
- half-year start (year + month). If not set, fall back to latest.
- """
- year = request.session.get('halfyear_year')
- start_month = request.session.get('halfyear_start_month')
-
- if year and start_month:
- try:
- year = int(year)
- start_month = int(start_month)
- return redirect('monthly_sheet', year=year, month=start_month)
- except ValueError:
- pass # fall through
-
- # Fallback: latest MonthlySheet if exists
- latest_sheet = MonthlySheet.objects.order_by('-year', '-month').first()
- if latest_sheet:
- return redirect('monthly_sheet', year=latest_sheet.year, month=latest_sheet.month)
- else:
- now = timezone.now()
- return redirect('monthly_sheet', year=now.year, month=now.month)
-# Add Entry (Generic)
-def add_entry(request, model_name):
- if request.method == 'POST':
- try:
- if model_name == 'SecondTableEntry':
- model = SecondTableEntry
-
- # Parse date
- date_str = request.POST.get('date')
- try:
- date_obj = datetime.strptime(date_str, '%Y-%m-%d').date() if date_str else None
- except (ValueError, TypeError):
- return JsonResponse({
- 'status': 'error',
- 'message': 'Invalid date format. Use YYYY-MM-DD'
- }, status=400)
-
- # NEW: robust parse of warm flag (handles 0/1, true/false, on/off)
- raw_warm = request.POST.get('is_warm')
- is_warm_bool = str(raw_warm).lower() in ('1', 'true', 'on', 'yes')
-
- # Handle Helium Output (Table Two)
- lhe_output = request.POST.get('lhe_output')
-
- entry = model.objects.create(
- client=Client.objects.get(id=request.POST.get('client_id')),
- date=date_obj,
- is_warm=is_warm_bool, # <-- use the boolean here
- lhe_delivery=request.POST.get('lhe_delivery', ''),
- lhe_output=Decimal(lhe_output) if lhe_output else None,
- notes=request.POST.get('notes', '')
- )
-
- return JsonResponse({
- 'status': 'success',
- 'id': entry.id,
- 'client_name': entry.client.name,
- 'institute_name': entry.client.institute.name,
- 'date': entry.date.strftime('%Y-%m-%d') if entry.date else '',
- 'is_warm': entry.is_warm,
- 'lhe_delivery': entry.lhe_delivery,
- 'lhe_output': str(entry.lhe_output) if entry.lhe_output else '',
- 'notes': entry.notes
- })
-
- elif model_name == 'ExcelEntry':
- model = ExcelEntry
-
- # Parse the date string into a date object
- date_str = request.POST.get('date')
- try:
- date_obj = datetime.strptime(date_str, '%Y-%m-%d').date() if date_str else None
- except (ValueError, TypeError):
- date_obj = None
-
- try:
- pressure = Decimal(request.POST.get('pressure', 0))
- purity = Decimal(request.POST.get('purity', 0))
- druckkorrektur = Decimal(request.POST.get('druckkorrektur', 1))
- lhe_zus = Decimal(request.POST.get('lhe_zus', 0))
- constant_300 = Decimal(request.POST.get('constant_300', 300))
- korrig_druck = Decimal(request.POST.get('korrig_druck', 0))
- nm3 = Decimal(request.POST.get('nm3', 0))
- lhe = Decimal(request.POST.get('lhe', 0))
- lhe_ges = Decimal(request.POST.get('lhe_ges', 0))
- except InvalidOperation:
- return JsonResponse({
- 'status': 'error',
- 'message': 'Invalid numeric value in Helium Input'
- }, status=400)
-
- # Create the entry with ALL fields
- entry = model.objects.create(
- client=Client.objects.get(id=request.POST.get('client_id')),
- date=date_obj,
- pressure=pressure,
- purity=purity,
- druckkorrektur=druckkorrektur,
- lhe_zus=lhe_zus,
- constant_300=constant_300,
- korrig_druck=korrig_druck,
- nm3=nm3,
- lhe=lhe,
- lhe_ges=lhe_ges,
- notes=request.POST.get('notes', '')
- )
-
- # Prepare the response
- response_data = {
- 'status': 'success',
- 'id': entry.id,
- 'client_name': entry.client.name,
- 'institute_name': entry.client.institute.name,
- 'pressure': str(entry.pressure),
- 'purity': str(entry.purity),
- 'druckkorrektur': str(entry.druckkorrektur),
- 'constant_300': str(entry.constant_300),
- 'korrig_druck': str(entry.korrig_druck),
- 'nm3': str(entry.nm3),
- 'lhe': str(entry.lhe),
- 'lhe_zus': str(entry.lhe_zus),
- 'lhe_ges': str(entry.lhe_ges),
- 'notes': entry.notes,
- }
-
- if entry.date:
- # JS uses this for the Date column and for the Month column
- response_data['date'] = entry.date.strftime('%Y-%m-%d')
- response_data['month'] = f"{entry.date.month:02d}"
- else:
- response_data['date'] = ''
- response_data['month'] = ''
-
- return JsonResponse(response_data)
-
-
- except Exception as e:
- return JsonResponse({'status': 'error', 'message': str(e)}, status=400)
-
- return JsonResponse({'status': 'error', 'message': 'Invalid request'}, status=400)
-
-# Update Entry (Generic)
-def update_entry(request, model_name):
- if request.method == 'POST':
- try:
- if model_name == 'SecondTableEntry':
- model = SecondTableEntry
- elif model_name == 'ExcelEntry':
- model = ExcelEntry
- else:
- return JsonResponse({'status': 'error', 'message': 'Invalid model'}, status=400)
-
- entry_id = int(request.POST.get('id'))
- entry = model.objects.get(id=entry_id)
-
- # Common updates for both models
- entry.client = Client.objects.get(id=request.POST.get('client_id'))
- entry.notes = request.POST.get('notes', '')
-
- # Handle date properly for both models
- date_str = request.POST.get('date')
- if date_str:
- try:
- entry.date = datetime.strptime(date_str, '%Y-%m-%d').date()
- except ValueError:
- return JsonResponse({
- 'status': 'error',
- 'message': 'Invalid date format. Use YYYY-MM-DD'
- }, status=400)
-
- if model_name == 'SecondTableEntry':
- # Handle Helium Output specific fields
-
- raw_warm = request.POST.get('is_warm')
- entry.is_warm = str(raw_warm).lower() in ('1', 'true', 'on', 'yes')
-
- entry.lhe_delivery = request.POST.get('lhe_delivery', '')
-
- lhe_output = request.POST.get('lhe_output')
- try:
- entry.lhe_output = Decimal(lhe_output) if lhe_output else None
- except InvalidOperation:
- return JsonResponse({
- 'status': 'error',
- 'message': 'Invalid LHe Output value'
- }, status=400)
-
- entry.save()
-
- return JsonResponse({
- 'status': 'success',
- 'id': entry.id,
- 'client_name': entry.client.name,
- 'institute_name': entry.client.institute.name,
- 'date': entry.date.strftime('%Y-%m-%d') if entry.date else '',
- 'is_warm': entry.is_warm,
- 'lhe_delivery': entry.lhe_delivery,
- 'lhe_output': str(entry.lhe_output) if entry.lhe_output else '',
- 'notes': entry.notes
- })
-
-
- elif model_name == 'ExcelEntry':
- # Handle Helium Input specific fields
- try:
- entry.pressure = Decimal(request.POST.get('pressure', 0))
- entry.purity = Decimal(request.POST.get('purity', 0))
- entry.druckkorrektur = Decimal(request.POST.get('druckkorrektur', 1))
- entry.lhe_zus = Decimal(request.POST.get('lhe_zus', 0))
- entry.constant_300 = Decimal(request.POST.get('constant_300', 300))
- entry.korrig_druck = Decimal(request.POST.get('korrig_druck', 0))
- entry.nm3 = Decimal(request.POST.get('nm3', 0))
- entry.lhe = Decimal(request.POST.get('lhe', 0))
- entry.lhe_ges = Decimal(request.POST.get('lhe_ges', 0))
- except InvalidOperation:
- return JsonResponse({
- 'status': 'error',
- 'message': 'Invalid numeric value in Helium Input'
- }, status=400)
-
- entry.save()
-
- return JsonResponse({
- 'status': 'success',
- 'id': entry.id,
- 'client_name': entry.client.name,
- 'institute_name': entry.client.institute.name,
- 'date': entry.date.strftime('%Y-%m-%d') if entry.date else '',
- 'month': f"{entry.date.month:02d}" if entry.date else '',
- 'pressure': str(entry.pressure),
- 'purity': str(entry.purity),
- 'druckkorrektur': str(entry.druckkorrektur),
- 'constant_300': str(entry.constant_300),
- 'korrig_druck': str(entry.korrig_druck),
- 'nm3': str(entry.nm3),
- 'lhe': str(entry.lhe),
- 'lhe_zus': str(entry.lhe_zus),
- 'lhe_ges': str(entry.lhe_ges),
- 'notes': entry.notes
- })
-
-
- except model.DoesNotExist:
- return JsonResponse({'status': 'error', 'message': 'Entry not found'}, status=404)
- except Exception as e:
- return JsonResponse({'status': 'error', 'message': str(e)}, status=400)
-
- return JsonResponse({'status': 'error', 'message': 'Invalid request method'}, status=400)
-
-# Delete Entry (Generic)
-def delete_entry(request, model_name):
- if request.method == 'POST':
- try:
- if model_name == 'SecondTableEntry':
- model = SecondTableEntry
- elif model_name == 'ExcelEntry':
- model = ExcelEntry
- else:
- return JsonResponse({'status': 'error', 'message': 'Invalid model'}, status=400)
-
- entry_id = request.POST.get('id')
- entry = model.objects.get(id=entry_id)
- entry.delete()
- return JsonResponse({'status': 'success', 'message': 'Entry deleted'})
-
- except model.DoesNotExist:
- return JsonResponse({'status': 'error', 'message': 'Entry not found'}, status=404)
- except Exception as e:
- return JsonResponse({'status': 'error', 'message': str(e)}, status=400)
-
- return JsonResponse({'status': 'error', 'message': 'Invalid request'}, status=400)
-
-def betriebskosten_list(request):
- items = Betriebskosten.objects.all().order_by('-buchungsdatum')
- return render(request, 'betriebskosten_list.html', {'items': items})
-
-def betriebskosten_create(request):
- if request.method == 'POST':
- try:
- entry_id = request.POST.get('id')
- if entry_id:
- # Update existing entry
- entry = Betriebskosten.objects.get(id=entry_id)
- else:
- # Create new entry
- entry = Betriebskosten()
-
- # Get form data
- buchungsdatum_str = request.POST.get('buchungsdatum')
- rechnungsnummer = request.POST.get('rechnungsnummer')
- kostentyp = request.POST.get('kostentyp')
- betrag = request.POST.get('betrag')
- beschreibung = request.POST.get('beschreibung')
- gas_volume = request.POST.get('gas_volume')
-
- # Validate required fields
- if not all([buchungsdatum_str, rechnungsnummer, kostentyp, betrag]):
- return JsonResponse({'status': 'error', 'message': 'All required fields must be filled'})
-
- # Convert date string to date object
- try:
- buchungsdatum = parse_date(buchungsdatum_str)
- if not buchungsdatum:
- return JsonResponse({'status': 'error', 'message': 'Invalid date format'})
- except (ValueError, TypeError):
- return JsonResponse({'status': 'error', 'message': 'Invalid date format'})
-
- # Set entry values
- entry.buchungsdatum = buchungsdatum
- entry.rechnungsnummer = rechnungsnummer
- entry.kostentyp = kostentyp
- entry.betrag = betrag
- entry.beschreibung = beschreibung
-
- # Only set gas_volume if kostentyp is helium and gas_volume is provided
- if kostentyp == 'helium' and gas_volume:
- entry.gas_volume = gas_volume
- else:
- entry.gas_volume = None
-
- entry.save()
-
- return JsonResponse({
- 'status': 'success',
- 'id': entry.id,
- 'buchungsdatum': entry.buchungsdatum.strftime('%Y-%m-%d'),
- 'rechnungsnummer': entry.rechnungsnummer,
- 'kostentyp_display': entry.get_kostentyp_display(),
- 'gas_volume': str(entry.gas_volume) if entry.gas_volume else '-',
- 'betrag': str(entry.betrag),
- 'beschreibung': entry.beschreibung or ''
- })
-
- except Exception as e:
- return JsonResponse({'status': 'error', 'message': str(e)})
-
- return JsonResponse({'status': 'error', 'message': 'Invalid request method'})
-
-def betriebskosten_delete(request):
- if request.method == 'POST':
- try:
- entry_id = request.POST.get('id')
- entry = Betriebskosten.objects.get(id=entry_id)
- entry.delete()
- return JsonResponse({'status': 'success'})
- except Betriebskosten.DoesNotExist:
- return JsonResponse({'status': 'error', 'message': 'Entry not found'})
- except Exception as e:
- return JsonResponse({'status': 'error', 'message': str(e)})
-
- return JsonResponse({'status': 'error', 'message': 'Invalid request method'})
-
-class CheckSheetView(View):
- def get(self, request):
- # Get current month/year
- current_year = datetime.now().year
- current_month = datetime.now().month
-
- # Get all sheets
- sheets = MonthlySheet.objects.all()
-
- sheet_data = []
- for sheet in sheets:
- cells_count = sheet.cells.count()
- # Count non-empty cells
- non_empty = sheet.cells.exclude(value__isnull=True).count()
-
- sheet_data.append({
- 'id': sheet.id,
- 'year': sheet.year,
- 'month': sheet.month,
- 'month_name': calendar.month_name[sheet.month],
- 'total_cells': cells_count,
- 'non_empty_cells': non_empty,
- 'has_data': non_empty > 0
- })
-
- # Also check what the default view would show
- default_sheet = MonthlySheet.objects.filter(
- year=current_year,
- month=current_month
- ).first()
-
- return JsonResponse({
- 'current_year': current_year,
- 'current_month': current_month,
- 'current_month_name': calendar.month_name[current_month],
- 'default_sheet_exists': default_sheet is not None,
- 'default_sheet_id': default_sheet.id if default_sheet else None,
- 'sheets': sheet_data,
- 'total_sheets': len(sheet_data)
- })
-
-
-class QuickDebugView(View):
- def get(self, request):
- # Get ALL sheets
- sheets = MonthlySheet.objects.all().order_by('year', 'month')
-
- result = {
- 'sheets': []
- }
-
- for sheet in sheets:
- sheet_info = {
- 'id': sheet.id,
- 'display': f"{sheet.year}-{sheet.month:02d}",
- 'url': f"/sheet/{sheet.year}/{sheet.month}/", # CHANGED THIS LINE
- 'sheet_url_pattern': 'sheet/{year}/{month}/', # Add this for clarity
- }
-
- # Count cells with data for first client in top_left table
- first_client = Client.objects.first()
- if first_client:
- test_cells = sheet.cells.filter(
- client=first_client,
- table_type='top_left',
- row_index__in=[8, 9, 10] # Rows 9, 10, 11
- ).order_by('row_index')
-
- cell_values = {}
- for cell in test_cells:
- cell_values[f"row_{cell.row_index}"] = str(cell.value) if cell.value else "Empty"
-
- sheet_info['test_cells'] = cell_values
- else:
- sheet_info['test_cells'] = "No clients"
-
- result['sheets'].append(sheet_info)
-
- return JsonResponse(result)
-class TestFormulaView(View):
- def get(self, request):
- # Test the formula evaluation directly
- test_values = {
- 8: 2, # Row 9 value (0-based index 8)
- 9: 2, # Row 10 value (0-based index 9)
- }
-
- # Test formula "9 + 8" (using 0-based indices)
- formula = "9 + 8"
- result = evaluate_formula(formula, test_values)
-
- return JsonResponse({
- 'test_values': test_values,
- 'formula': formula,
- 'result': str(result),
- 'note': 'Formula uses 0-based indices. 9=Row10, 8=Row9'
- })
-
-
-class SimpleDebugView(View):
- """Simplest debug view to check if things are working"""
- def get(self, request):
- sheet_id = request.GET.get('sheet_id', 1)
-
- try:
- sheet = MonthlySheet.objects.get(id=sheet_id)
-
- # Get first client
- client = Client.objects.first()
- if not client:
- return JsonResponse({'error': 'No clients found'})
-
- # Check a few cells
- cells = Cell.objects.filter(
- sheet=sheet,
- client=client,
- table_type='top_left',
- row_index__in=[8, 9, 10]
- ).order_by('row_index')
-
- cell_data = []
- for cell in cells:
- cell_data.append({
- 'row_index': cell.row_index,
- 'ui_row': cell.row_index + 1,
- 'value': str(cell.value) if cell.value is not None else 'Empty',
- 'cell_id': cell.id
- })
-
- return JsonResponse({
- 'sheet': f"{sheet.year}-{sheet.month}",
- 'sheet_id': sheet.id,
- 'client': client.name,
- 'cells': cell_data,
- 'note': 'Row 8 = UI Row 9, Row 9 = UI Row 10, Row 10 = UI Row 11'
- })
-
- except MonthlySheet.DoesNotExist:
- return JsonResponse({'error': f'Sheet with id {sheet_id} not found'})
-
-def halfyear_settings(request):
- """
- Global settings page: choose a year + first month for the 6-month interval.
- These values are stored in the session and used by other views.
- """
- # Determine available years from your data (use ExcelEntry or SecondTableEntry)
- # Here I use SecondTableEntry; you can switch to ExcelEntry if you prefer.
- available_years_qs = SecondTableEntry.objects.dates('date', 'year', order='DESC')
- available_years = [d.year for d in available_years_qs]
-
- if not available_years:
- available_years = [timezone.now().year]
-
- # Defaults (if nothing in session yet)
- default_year = request.session.get('halfyear_year', available_years[0])
- default_start_month = request.session.get('halfyear_start_month', 1)
-
- if request.method == 'POST':
- year = int(request.POST.get('year', default_year))
- start_month = int(request.POST.get('start_month', default_start_month))
-
- request.session['halfyear_year'] = year
- request.session['halfyear_start_month'] = start_month
-
- # Redirect back to where user came from, or to this page again
- next_url = request.POST.get('next') or request.GET.get('next') or 'halfyear_settings'
- return redirect(next_url)
-
- # Month choices for the dropdown
- month_choices = [(i, calendar.month_name[i]) for i in range(1, 13)]
-
- context = {
- 'available_years': available_years,
- 'selected_year': int(default_year),
- 'selected_start_month': int(default_start_month),
- 'month_choices': month_choices,
- }
- return render(request, 'halfyear_settings.html', context)
\ No newline at end of file
diff --git a/sheets/services/halfyear_calc.py b/sheets/services/halfyear_calc.py
index e3ba478..f1e0766 100644
--- a/sheets/services/halfyear_calc.py
+++ b/sheets/services/halfyear_calc.py
@@ -1,14 +1,10 @@
# sheets/services/halfyear_calc.py
from __future__ import annotations
-from django.db.models.functions import Coalesce
-from decimal import Decimal
from typing import Dict, Any
-from django.shortcuts import redirect, render
from decimal import Decimal
from sheets.models import (
- Client, SecondTableEntry, Institute, ExcelEntry,
- Betriebskosten, MonthlySheet, Cell, CellReference, MonthlySummary ,BetriebskostenSummary,AbrechnungCell
-)
+ Client, SecondTableEntry, ExcelEntry,
+ MonthlySheet, Cell, MonthlySummary)
from django.db.models import Sum
from django.db.models.functions import Coalesce
from django.db.models import DecimalField, Value
@@ -172,7 +168,6 @@ def build_halfyear_window(interval_year: int, start_month: int):
window.append((y, m))
return window
# Import ONLY models + pure helpers here
-from sheets.models import MonthlySheet, Cell, Client, SecondTableEntry, BetriebskostenSummary
def sum_right_row_without_duplicates(label: str, clients_right: list[str], values: list):
"""
For specific rows, clients are logically merged, so we must only count one copy.
@@ -268,8 +263,7 @@ def compute_halfyear_context(interval_year: int, interval_start: int) -> Dict[st
chosen_sheet_bottom2, bottom2_inputs = pick_bottom2_from_window(window, sheets_by_ym, prev_sheet)
- bottom2_g39 = bottom2_inputs["g39"]
- bottom2_i39 = bottom2_inputs["i39"]
+
# ----------------------------
# HALF-YEAR BOTTOM TABLE 1 (Bilanz) - Read only
# ----------------------------
@@ -351,7 +345,6 @@ def compute_halfyear_context(interval_year: int, interval_start: int) -> Dict[st
"nm3": nm3_sum_27_35,
"lhe": lhe_sum_27_35,
})
- start_sheet = sheets_by_ym.get((start_year, start_month))
# ------------------------------------------------------------
# Bottom Table 2 (Halbjahres Bilanz) – server-side recalcBottom2()
# ------------------------------------------------------------
diff --git a/sheets/tests.py b/sheets/tests.py
index 7ce503c..4929020 100644
--- a/sheets/tests.py
+++ b/sheets/tests.py
@@ -1,3 +1,2 @@
-from django.test import TestCase
# Create your tests here.
diff --git a/sheets/urls.py b/sheets/urls.py
index 390b17b..76ea0ed 100644
--- a/sheets/urls.py
+++ b/sheets/urls.py
@@ -27,10 +27,10 @@ urlpatterns = [
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-month-summary/', SaveMonthSummaryView.as_view(), name='save_month_summary'),
+ path("save-cells/", SaveCellsView.as_view(), name="save_cells"),
path('save-month-summary/', SaveMonthSummaryView.as_view(), name='save_month_summary'),
path('calculate/', views.CalculateView.as_view(), name='calculate'),
diff --git a/sheets/views.py b/sheets/views.py
index d9d4903..c0efc65 100644
--- a/sheets/views.py
+++ b/sheets/views.py
@@ -4,24 +4,21 @@ from django.views.decorators.csrf import csrf_exempt
from django.db.models import Sum, Value, DecimalField
from django.http import JsonResponse
from django.db.models import Q
+from django.db.models import Count
from decimal import Decimal, ROUND_HALF_UP
-from decimal import Decimal, InvalidOperation
-from django.apps import apps
-from datetime import date, datetime
+from decimal import InvalidOperation
+from datetime import datetime
from sheets.services.halfyear_calc import compute_halfyear_context
import calendar
from django.utils import timezone
+import re
from django.views.generic import TemplateView, View
from .models import (
Client, SecondTableEntry, Institute, ExcelEntry,
- Betriebskosten, MonthlySheet, Cell, CellReference, MonthlySummary ,BetriebskostenSummary,AbrechnungCell
+ Betriebskosten, MonthlySheet, Cell, MonthlySummary ,BetriebskostenSummary,AbrechnungCell
)
-from django.db.models import Sum
-from django.urls import reverse
from django.db.models.functions import Coalesce
-from .forms import BetriebskostenForm
from django.utils.dateparse import parse_date
-from django.contrib.auth.mixins import LoginRequiredMixin
import json
FIRST_SHEET_YEAR = 2025
FIRST_SHEET_MONTH = 1
@@ -368,13 +365,7 @@ def get_bestand_kannen_for_month(sheet, client_name: str) -> Decimal:
"""
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"]
@@ -463,7 +454,7 @@ def get_top_left_value(sheet, client_name: str, row_index: int) -> Decimal:
return Decimal('0')
def get_group_clients(group_key):
"""Return queryset of clients that belong to a logical group."""
- from .models import Client # local import to avoid circulars
+
group = CLIENT_GROUPS.get(group_key)
if not group:
@@ -473,9 +464,7 @@ def get_group_clients(group_key):
def calculate_summation(sheet, table_type, row_index, sum_column_index):
"""Calculate summation for a row, with special handling for % row"""
- from decimal import Decimal
- from .models import Cell
-
+
try:
# Special case: top_left, % row (Excel B20 -> row_index 19)
if table_type == 'top_left' and row_index == 19:
@@ -533,9 +522,7 @@ def evaluate_formula(formula, values_dict):
Safely evaluate a formula like "10 + 9" where numbers are row indices
values_dict: {row_index: decimal_value}
"""
- from decimal import Decimal
- import re
-
+
try:
# Create a copy of the formula to work with
expr = formula
@@ -568,9 +555,7 @@ class MonthlySheetView(TemplateView):
def populate_helium_input_to_top_right(self, sheet):
"""Populate bezug data from SecondTableEntry to top-right table (row 8 = Excel row 12)"""
- from .models import SecondTableEntry, Cell, Client
- from django.db.models.functions import Coalesce
- from decimal import Decimal
+
year = sheet.year
month = sheet.month
@@ -632,10 +617,7 @@ class MonthlySheetView(TemplateView):
return True
def calculate_bezug_from_entries(self, sheet, year, month):
"""Calculate B11 (Bezug) from SecondTableEntry for all clients - ONLY for non-start sheets"""
- from .models import SecondTableEntry, Cell, Client
- from django.db.models import Sum
- from django.db.models.functions import Coalesce
- from decimal import Decimal
+
# Check if this is the start sheet
if year == 2025 and month == 1:
@@ -664,7 +646,7 @@ class MonthlySheetView(TemplateView):
b11_cell.save()
# Also trigger dependent calculations
- from .views import SaveCellsView
+
save_view = SaveCellsView()
save_view.calculate_top_left_dependents(sheet, b11_cell)
# In MonthlySheetView.get_context_data() method, update the TOP_RIGHT_CLIENTS and row count:
@@ -673,7 +655,7 @@ class MonthlySheetView(TemplateView):
return True
def get_context_data(self, **kwargs):
- from decimal import Decimal
+
context = super().get_context_data(**kwargs)
year = self.kwargs.get('year', datetime.now().year)
month = self.kwargs.get('month', datetime.now().month)
@@ -702,9 +684,7 @@ class MonthlySheetView(TemplateView):
# Recalculate dependents once when opening the sheet
# ----------------------------------------------------
- from .views import SaveCellsView
- from .models import Cell
-
+
save_view = SaveCellsView()
# ---- TOP LEFT ----
@@ -768,8 +748,7 @@ class MonthlySheetView(TemplateView):
# 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."),
@@ -1085,7 +1064,7 @@ class MonthlySheetView(TemplateView):
prev_year = year
prev_month = month - 1
- from .models import MonthlySheet, Cell, Client
+
prev_sheet = MonthlySheet.objects.filter(
year=prev_year,
@@ -1146,8 +1125,7 @@ class MonthlySheetView(TemplateView):
- AG Alff + AG Gutfl. share the SAME value (from previous month's AG Alff or AG Gutfl.)
M3 clients just copy their own value.
"""
- from .models import MonthlySheet, Cell, Client
- from decimal import Decimal
+
# Do nothing on first sheet
if year == FIRST_SHEET_YEAR and month == FIRST_SHEET_MONTH:
@@ -1284,7 +1262,7 @@ def get_factor_value(table_type, row_index):
def recalculate_stand_der_gaszahler(self, sheet):
"""Recalculate Stand der Gaszähler for all client pairs"""
- from decimal import Decimal
+
# For Dr. Fohrer and AG Buntk. (L & M columns)
try:
@@ -1456,7 +1434,7 @@ class AbrechnungView(TemplateView):
EDITABLE_ROW_KEYS = {"ghe_bezug", "betrag", "gutschriften"}
def get_context_data(self, **kwargs):
- from sheets.services.halfyear_calc import compute_halfyear_context
+
context = super().get_context_data(**kwargs)
interval_year = self.request.session.get("halfyear_year")
@@ -1496,22 +1474,6 @@ class AbrechnungView(TemplateView):
umlage_personal_total = d(bs.umlage_personal) if bs else Decimal("0")
# ---- helper: sum lhe_output in the 6-month window for a list of client names ----
- def sum_output_for_clients_exact(client_names):
- total = Decimal("0")
- if not client_names:
- return total
-
- for (y, m) in window:
- total += SecondTableEntry.objects.filter(
- client__name__in=client_names,
- date__year=y,
- date__month=m,
- ).aggregate(
- total=Coalesce(Sum("lhe_output"), Value(0, output_field=DecimalField()))
- )["total"] or Decimal("0")
-
- return total
- # (NOTE: the date filter above is “wide”; if you prefer exact (y,m) matching, use the loop version below)
def sum_output_for_clients_exact(client_names):
total = Decimal("0")
if not client_names:
@@ -1920,13 +1882,7 @@ class AbrechnungView(TemplateView):
GROUP_IJKL = ["orgchem_thiele", "phychem_m3_buntkow", "orgchem_fohrer", "mawi_m3_gutfl", "mawi_alff"]
GROUP_NOP = ["chemie", "mawi", "physik"]
GROUP_STADT = ["pkm_vogel", "iap_halfmann", "ikp"]
- def nop_cols_for_row(row_key: str):
- # For gutschriften, NOP should sum the 5 IJKL client columns
- if row_key == "gutschriften":
- return GROUP_IJKL
- # otherwise NOP is the 3 summary columns (Chemie/MaWi/Physik)
- return GROUP_NOP
-
+
def val(row_key, col_key):
"""Get the final numeric value for a cell (prefer computed, fallback to stored)."""
if (row_key, col_key) in computed:
@@ -2104,7 +2060,7 @@ class RechnungView(TemplateView):
# 3) Heliumverluste (l) from Halbjahresbilanz total Verbraucherverluste
# (sum all clients left+right)
# ------------------------------------------------------------
- from sheets.services.halfyear_calc import compute_halfyear_context
+
half_ctx = compute_halfyear_context(interval_year, interval_start)
heliumverluste_l = Decimal("0")
@@ -2223,14 +2179,7 @@ class SaveAbrechnungCellsView(View):
)
return JsonResponse({"ok": True})
-def build_halfyear_window(interval_year: int, interval_start_month: int):
- window = []
- for offset in range(6):
- total_index = (interval_start_month - 1) + offset
- y = interval_year + (total_index // 12)
- m = (total_index % 12) + 1
- window.append((y, m))
- return window
+
class SaveCellsView(View):
def calculate_bottom_3_dependents(self, sheet):
@@ -2296,7 +2245,7 @@ class SaveCellsView(View):
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)
@@ -2410,7 +2359,6 @@ class SaveCellsView(View):
# If conversion fails, treat as empty
new_value = None
- old_value = cell.value
cell.value = new_value
cell.save()
@@ -2486,10 +2434,7 @@ class SaveCellsView(View):
14: Verbraucherverluste (Liter L-He) - calculated
15: % - calculated
"""
- from decimal import Decimal
- from django.db.models import Sum, Count
- from django.db.models.functions import Coalesce
- from .models import Client, Cell, ExcelEntry, SecondTableEntry
+
TOP_RIGHT_CLIENTS = [
"Dr. Fohrer", # L
@@ -2820,9 +2765,7 @@ class SaveCellsView(View):
- 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
@@ -2961,9 +2904,7 @@ class SaveCellsView(View):
def calculate_top_left_dependents(self, sheet, changed_cell):
"""Calculate dependent cells in top_left table"""
- from decimal import Decimal
- from django.db.models import Sum
- from django.db.models.functions import Coalesce
+
client_id = changed_cell.client_id
updated_cells = []
@@ -2986,7 +2927,7 @@ class SaveCellsView(View):
if cell and cell.value is not None:
try:
return Decimal(str(cell.value))
- except:
+ except Exception:
return Decimal('0')
return Decimal('0')
@@ -3041,13 +2982,13 @@ class SaveCellsView(View):
})
# 4. B11 = Sum of LHe Output from SecondTableEntry for this client/month - Bezug
- from .models import SecondTableEntry
+
client = changed_cell.client
# Calculate total LHe output for this client in this month
# 4. B11 = Bezug (Liter L-He)
# For start sheet: manual entry, for other sheets: auto-calculated from SecondTableEntry
- from .models import SecondTableEntry
+
client = changed_cell.client
b11_cell = cell_dict.get(8) # row_index 8 = Excel B11 (UI row 9)
@@ -3258,7 +3199,7 @@ class SaveCellsView(View):
def recalculate_top_left_table(self, sheet, client_id):
"""Recalculate the top-left table for a specific client"""
- from decimal import Decimal
+
# Get all cells for this client in top_left table
cells = Cell.objects.filter(
@@ -3429,7 +3370,7 @@ class SummarySheetView(TemplateView):
year = int(self.kwargs.get('year', datetime.now().year))
# Get 6 monthly sheets
- months = [(year, m) for m in range(start_month, start_month + 6)]
+
sheets = MonthlySheet.objects.filter(
year=year,
month__in=list(range(start_month, start_month + 6))
@@ -3605,7 +3546,7 @@ def set_halfyear_interval(request):
return redirect('clients_list')
# Table One View (ExcelEntry)
def table_one_view(request):
- from .models import ExcelEntry, Client, Institute
+
# --- Base queryset for the main Helium Input table ---
base_entries = ExcelEntry.objects.all().select_related('client', 'client__institute')
@@ -4129,9 +4070,7 @@ def betriebskosten_list(request):
summary = get_summary()
summary.recalculate()
- from decimal import Decimal
- from django.db.models import Sum, DecimalField, Value
- from django.db.models.functions import Coalesce
+
interval_year = request.session.get("halfyear_year")
interval_start = request.session.get("halfyear_start_month")
@@ -4287,7 +4226,7 @@ def get_summary():
summary, created = BetriebskostenSummary.objects.get_or_create(id=1)
return summary
-from django.http import JsonResponse
+
def update_personalkosten(request):
if request.method == "POST":
@@ -4345,8 +4284,7 @@ class CheckSheetView(View):
-
- return JsonResponse(result)
+
class TestFormulaView(View):
def get(self, request):
# Test the formula evaluation directly