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)