# sheets/services/halfyear_calc.py from __future__ import annotations from django.db.models.functions import Coalesce from decimal import Decimal from typing import Dict, Any from django.shortcuts import redirect, render from decimal import Decimal from sheets.models import ( Client, SecondTableEntry, Institute, ExcelEntry, Betriebskosten, MonthlySheet, Cell, CellReference, MonthlySummary ,BetriebskostenSummary,AbrechnungCell ) from django.db.models import Sum from django.db.models.functions import Coalesce from django.db.models import DecimalField, Value HALFYEAR_CLIENTS = ["AG Vogel", "AG Halfm", "IKP"] TR_RUECKF_FLUESSIG_ROW = 2 # confirmed by your March value 172.840560 TR_BESTAND_KANNEN_ROW = 5 def get_top_right_value(sheet, client_name: str, row_index: int) -> Decimal: """ Read a numeric value from the top_right table of a MonthlySheet for a given client (by column) and row_index. top_right cells are keyed by (sheet, table_type='top_right', row_index, column_index), where column_index is the position of the client in HALFYEAR_RIGHT_CLIENTS. """ if sheet is None: return Decimal('0') col_index = RIGHT_CLIENT_INDEX.get(client_name) if col_index is None: return Decimal('0') cell = Cell.objects.filter( sheet=sheet, table_type='top_right', row_index=row_index, column_index=col_index, ).first() if cell is None or cell.value in (None, ''): return Decimal('0') try: return Decimal(str(cell.value)) except Exception: return Decimal('0') def get_bottom2_value(sheet, row_index: int, col_index: int) -> Decimal: """Get numeric value from bottom_2 or 0 if missing.""" if sheet is None: return Decimal("0") cell = Cell.objects.filter( sheet=sheet, table_type="bottom_2", row_index=row_index, column_index=col_index, ).first() if cell is None or cell.value in (None, ""): return Decimal("0") try: return Decimal(str(cell.value)) except Exception: return Decimal("0") def get_bottom1_value(sheet, row_index: int, col_index: int) -> Decimal: """Get a numeric value from bottom_1, or 0 if missing.""" if sheet is None: return Decimal('0') cell = Cell.objects.filter( sheet=sheet, table_type='bottom_1', row_index=row_index, column_index=col_index, ).first() if cell is None or cell.value in (None, ''): return Decimal('0') try: return Decimal(str(cell.value)) except Exception: return Decimal('0') def get_top_left_value(sheet, client_name: str, row_index: int) -> Decimal: """ Read a numeric value from the top_left table for a given month, client and row. Does NOT use column_index, because top_left is keyed only by client + row_index. """ if sheet is None: return Decimal('0') client_obj = Client.objects.filter(name=client_name).first() if not client_obj: return Decimal('0') cell = Cell.objects.filter( sheet=sheet, table_type='top_left', client=client_obj, row_index=row_index, ).first() if cell is None or cell.value in (None, ''): return Decimal('0') try: return Decimal(str(cell.value)) except Exception: return Decimal('0') def get_bestand_kannen_for_month(sheet, client_name: str) -> Decimal: """ 'B9' in your description: Bestand in Kannen-1 (Lit. L-He) For this implementation we take it from top_left row_index = 5 for that client. """ return get_top_left_value(sheet, client_name, row_index=BESTAND_KANNEN_ROW_INDEX) def pick_sheet_by_gasbestand(window, sheets_by_ym, prev_sheet): """ Returns the last sheet in the window whose Gasbestand (J36, Nm³ column) != 0. If none found, returns prev_sheet (Übertrag_Dez__Vorjahr equivalent). """ for (y, m) in reversed(window): sheet = sheets_by_ym.get((y, m)) if not sheet: continue gasbestand_nm3 = get_bottom1_value(sheet, GASBESTAND_ROW_INDEX, GASBESTAND_COL_NM3) if gasbestand_nm3 != 0: return sheet return prev_sheet # NEW: clients for the top-right half-year table GASBESTAND_ROW_INDEX = 9 # <-- adjust if your bottom_1 has a different row index GASBESTAND_COL_NM3 = 3 # <-- adjust to the column index for Nm³ in bottom_1 # In top_left / top_right, "Bestand in Kannen-1 (Lit. L-He)" is row_index 5 BESTAND_KANNEN_ROW_INDEX = 5 HALFYEAR_RIGHT_CLIENTS = [ "Dr. Fohrer", "AG Buntk.", "AG Alff", "AG Gutfl.", "M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch", ] BOTTOM1_COL_VOLUME = 0 BOTTOM1_COL_BAR = 1 BOTTOM1_COL_KORR = 2 BOTTOM1_COL_NM3 = 3 BOTTOM1_COL_LHE = 4 BOTTOM2_ROW_ANLAGE = 0 BOTTOM2_COL_G39 = 0 # "Gefäss 2,5" (cell id shows column_index=0) BOTTOM2_COL_I39 = 1 # "Gefäss 1,0" (cell id shows column_index=1) BOTTOM2_ROW_INPUTS = { "g39": (0, 0), # row_index=0, column_index=0 (your G39) "i39": (0, 1), # row_index=0, column_index=1 (your I39) } FACTOR_NM3_TO_LHE = Decimal("0.75") RIGHT_CLIENT_INDEX = {name: idx for idx, name in enumerate(HALFYEAR_RIGHT_CLIENTS)} def build_halfyear_window(interval_year: int, start_month: int): """ Build a list of (year, month) for the 6-month interval, possibly crossing into the next year. Example: (2025, 10) -> [(2025,10), (2025,11), (2025,12), (2026,1), (2026,2), (2026,3)] """ window = [] for offset in range(6): total_index = (start_month - 1) + offset # 0-based y = interval_year + (total_index // 12) m = (total_index % 12) + 1 window.append((y, m)) return window # Import ONLY models + pure helpers here from sheets.models import MonthlySheet, Cell, Client, SecondTableEntry, BetriebskostenSummary def compute_halfyear_context(interval_year: int, interval_start: int) -> Dict[str, Any]: """ Returns a context dict with the SAME keys your current halfyear_balance.html expects. """ # ✅ Paste the pure calculation logic here in Step 2 window = build_halfyear_window(interval_year, interval_start) # window = [(y1, m1), (y2, m2), ..., (y6, m6)] # (Year, month) of the first month start_year, start_month = window[0] # Previous month (for "Stand ... (Vorjahr)" and "Best. in Kannen Vormonat") prev_total_index = (start_month - 1) - 1 # one month back, 0-based if prev_total_index >= 0: prev_year = start_year + (prev_total_index // 12) prev_month = (prev_total_index % 12) + 1 else: prev_year = start_year - 1 prev_month = 12 # Load MonthlySheet objects for the window and for the previous month sheets_by_ym = {} for (y, m) in window: sheet = MonthlySheet.objects.filter(year=y, month=m).first() sheets_by_ym[(y, m)] = sheet prev_sheet = MonthlySheet.objects.filter(year=prev_year, month=prev_month).first() def pick_bottom2_from_window(window, sheets_by_ym, prev_sheet): # choose sheet (same logic you already use) chosen = None for (y, m) in reversed(window): s = sheets_by_ym.get((y, m)) # use your existing condition for choosing month if s: chosen = s break if chosen is None: chosen = prev_sheet # Now read the two inputs safely bottom2_inputs = {} for key, (row_idx, col_idx) in BOTTOM2_ROW_INPUTS.items(): bottom2_inputs[key] = get_bottom2_value(chosen, row_idx, col_idx) return chosen, bottom2_inputs chosen_sheet_bottom2, bottom2_inputs = pick_bottom2_from_window(window, sheets_by_ym, prev_sheet) bottom2_g39 = bottom2_inputs["g39"] bottom2_i39 = bottom2_inputs["i39"] # ---------------------------- # HALF-YEAR BOTTOM TABLE 1 (Bilanz) - Read only # ---------------------------- chosen_sheet_bottom1 = pick_sheet_by_gasbestand(window, sheets_by_ym, prev_sheet) # IMPORTANT: define which bottom_1 row_index corresponds to Excel rows 27..35 # If your bottom_1 starts at Excel row 27 => row_index 0 == Excel 27 # then row_index = excel_row - 27 BOTTOM1_EXCEL_START_ROW = 27 bottom1_excel_rows = list(range(27, 37)) # 27..36 BOTTOM1_LABELS = [ "Batterie 1", "2", "3", "4", "5", "Batterie Links", "2 Bündel", "2 Ballone", "Reingasspeicher", "Gasbestand", ] BOTTOM1_VOLUMES = [ Decimal("2.4"), Decimal("5.1"), Decimal("4.0"), Decimal("1.0"), Decimal("4.0"), Decimal("0.6"), Decimal("1.2"), Decimal("20.0"), Decimal("5.0"), None, # Gasbestand row has no volume ] nm3_sum_27_35 = Decimal("0") lhe_sum_27_35 = Decimal("0") bottom1_rows = [] for excel_row in bottom1_excel_rows: row_index = excel_row - BOTTOM1_EXCEL_START_ROW chosen_sheet_bottom1 = None for (y, m) in reversed(window): s = sheets_by_ym.get((y, m)) gasbestand = get_bottom1_value(s, GASBESTAND_ROW_INDEX, GASBESTAND_COL_NM3) # J36 (Nm3) if gasbestand != 0: chosen_sheet_bottom1 = s break if chosen_sheet_bottom1 is None: chosen_sheet_bottom1 = prev_sheet # Normal rows (27..35): read from chosen sheet and accumulate sums if excel_row != 36: nm3_val = get_bottom1_value(chosen_sheet_bottom1, row_index, BOTTOM1_COL_NM3) lhe_val = get_bottom1_value(chosen_sheet_bottom1, row_index, BOTTOM1_COL_LHE) nm3_sum_27_35 += nm3_val lhe_sum_27_35 += lhe_val bottom1_rows.append({ "label": BOTTOM1_LABELS[row_index], "volume": BOTTOM1_VOLUMES[row_index], "bar": get_bottom1_value(chosen_sheet_bottom1, row_index, BOTTOM1_COL_BAR), "korr": get_bottom1_value(chosen_sheet_bottom1, row_index, BOTTOM1_COL_KORR), "nm3": nm3_val, "lhe": lhe_val, }) # Gasbestand row (36): show sums (J36 = SUM(J27:J35), K36 = SUM(K27:K35)) else: bottom1_rows.append({ "label": "Gasbestand", "volume": "", "bar": "", "korr": "", "nm3": nm3_sum_27_35, "lhe": lhe_sum_27_35, }) start_sheet = sheets_by_ym.get((start_year, start_month)) # ------------------------------------------------------------ # Bottom Table 2 (Halbjahres Bilanz) – server-side recalcBottom2() # ------------------------------------------------------------ FACTOR_BT2 = Decimal("0.75") # 1) Helper: pick last-nonzero value of bottom_2 row0 col0/col1 from the window (fallback: prev_sheet) def pick_last_nonzero_bottom2(row_index: int, col_index: int) -> Decimal: # Scan from last month in window backwards for (y, m) in reversed(window): s = sheets_by_ym.get((y, m)) if not s: continue v = get_bottom2_value(s, row_index, col_index) if v is not None and v != 0: return v # fallback to month before window v_prev = get_bottom2_value(prev_sheet, row_index, col_index) return v_prev if v_prev is not None else Decimal("0") # 2) K38 comes from Overall Summary: "Summe Bestand (Lit. L-He)" # Find it from your already built overall summary rows list. k38 = Decimal("0") j38 = Decimal("0") # 3) Inputs G39 / I39 (picked from last non-zero month in window) g39 = pick_last_nonzero_bottom2(row_index=0, col_index=0) # G39 i39 = pick_last_nonzero_bottom2(row_index=0, col_index=1) # I39 k39 = (g39 or Decimal("0")) + (i39 or Decimal("0")) j39 = k39 * FACTOR_BT2 # 4) +Kaltgas (row 40) # JS: # g40 = (2500 - g39)/100*10 # i40 = (1000 - i39)/100*10 g40 = None i40 = None if g39 is not None: g40 = (Decimal("2500") - g39) / Decimal("100") * Decimal("10") if i39 is not None: i40 = (Decimal("1000") - i39) / Decimal("100") * Decimal("10") k40 = (g40 or Decimal("0")) + (i40 or Decimal("0")) j40 = k40 * FACTOR_BT2 # 5) Bestand flüssig He (row 43) k43 = ( (k38 or Decimal("0")) + (k39 or Decimal("0")) + (k40 or Decimal("0")) ) j43 = k43 * FACTOR_BT2 # 6) Gesamtbestand neu (row 44) = Gasbestand(Lit) from Bottom Table 1 + k43 gasbestand_lit = Decimal("0") for r in bottom1_rows: if (r.get("label") or "").strip().startswith("Gasbestand"): gasbestand_lit = r.get("lhe") or Decimal("0") break k44 = (gasbestand_lit or Decimal("0")) + (k43 or Decimal("0")) j44 = k44 * FACTOR_BT2 bottom2 = { "j38": j38, "k38": k38, "g39": g39, "i39": i39, "j39": j39, "k39": k39, "g40": g40, "i40": i40, "j40": j40, "k40": k40, "j43": j43, "k43": k43, "j44": j44, "k44": k44, } # ------------------------------------------------------------------ # 2) LEFT TABLE (your existing, working logic) # ------------------------------------------------------------------ HALFYEAR_CLIENTS_LEFT = ["AG Vogel", "AG Halfm", "IKP"] # We'll collect client-wise values first for clarity. client_data_left = {name: {} for name in HALFYEAR_CLIENTS_LEFT} # --- Row B3: Stand der Gaszähler (Nm³) # = MAX(B3 from previous month, and B3 from each of the 6 months in the window) # row_index 0 in top_left = "Stand der Gaszähler (Nm³)" months_for_max = [(prev_year, prev_month)] + window for cname in HALFYEAR_CLIENTS_LEFT: max_val = Decimal('0') for (y, m) in months_for_max: sheet = sheets_by_ym.get((y, m)) if sheet is None and (y, m) == (prev_year, prev_month): sheet = prev_sheet val_b3 = get_top_left_value(sheet, cname, row_index=0) if val_b3 > max_val: max_val = val_b3 client_data_left[cname]['stand_gas'] = max_val # --- Row B4: Stand der Gaszähler (Vorjahr) (Nm³) -> previous month same row --- for cname in HALFYEAR_CLIENTS_LEFT: val_b4 = get_top_left_value(prev_sheet, cname, row_index=0) client_data_left[cname]['stand_gas_prev'] = val_b4 # --- Row B5: Gasrückführung (Nm³) = B3 - B4 --- for cname in HALFYEAR_CLIENTS_LEFT: b3 = client_data_left[cname]['stand_gas'] b4 = client_data_left[cname]['stand_gas_prev'] client_data_left[cname]['gasrueckf'] = b3 - b4 # --- Row B6: Rückführung flüssig (Lit. L-He) = B5 / 0.75 --- for cname in HALFYEAR_CLIENTS_LEFT: b5 = client_data_left[cname]['gasrueckf'] client_data_left[cname]['rueckf_fluessig'] = (b5 / Decimal('0.75')) if b5 != 0 else Decimal('0') # --- Row B7: Sonderrückführungen (Lit. L-He) = sum over 6 months of that row --- # That row index is 4 in your top_left table. for cname in HALFYEAR_CLIENTS_LEFT: sonder_total = Decimal('0') for (y, m) in window: sheet = sheets_by_ym.get((y, m)) if sheet: sonder_total += get_top_left_value(sheet, cname, row_index=4) client_data_left[cname]['sonder'] = sonder_total # --- Row B8: Bestand in Kannen-1 (Lit. L-He) --- # Excel-style logic with Gasbestand (J36) and fallback to previous month. for cname in HALFYEAR_CLIENTS_LEFT: chosen_value = None # Go from last month (window[5]) backwards to first (window[0]) for (y, m) in reversed(window): sheet = sheets_by_ym.get((y, m)) gasbestand = get_bottom1_value(sheet, GASBESTAND_ROW_INDEX, GASBESTAND_COL_NM3) if gasbestand != 0: chosen_value = get_bestand_kannen_for_month(sheet, cname) break # If still None -> use previous month (Übertrag_Dez__Vorjahr equivalent) if chosen_value is None: sheet_prev = prev_sheet chosen_value = get_bestand_kannen_for_month(sheet_prev, cname) client_data_left[cname]['bestand_kannen'] = chosen_value if chosen_value is not None else Decimal('0') # --- Row B9: Summe Bestand (Lit. L-He) = equal to previous row --- for cname in HALFYEAR_CLIENTS_LEFT: client_data_left[cname]['summe_bestand'] = client_data_left[cname]['bestand_kannen'] # --- Row B10: Best. in Kannen Vormonat (Lit. L-He) # = Bestand in Kannen-1 from the month BEFORE the window (prev_year, prev_month) for cname in HALFYEAR_CLIENTS_LEFT: client_data_left[cname]['best_kannen_vormonat'] = get_bestand_kannen_for_month(prev_sheet, cname) # --- Row B13: Bezug (Liter L-He) --- for cname in HALFYEAR_CLIENTS_LEFT: total_bezug = Decimal('0') for (y, m) in window: qs = SecondTableEntry.objects.filter( client__name=cname, date__year=y, date__month=m, ).aggregate( total=Coalesce(Sum('lhe_output'), Value(0, output_field=DecimalField())) ) total_bezug += Decimal(str(qs['total'])) client_data_left[cname]['bezug'] = total_bezug # --- Row B14: Rückführ. Soll (Lit. L-He) = Bezug - Summe Bestand + Best. in Kannen Vormonat --- for cname in HALFYEAR_CLIENTS_LEFT: b13 = client_data_left[cname]['bezug'] b11 = client_data_left[cname]['summe_bestand'] b12 = client_data_left[cname]['best_kannen_vormonat'] client_data_left[cname]['rueckf_soll'] = b13 - b11 + b12 # --- Row B15: Verluste (Soll-Rückf.) (Lit. L-He) # AG Vogel, AG Halfm: B14 - B6 # IKP: B14 - B6 - B7 (Sonderrückführungen) for cname in HALFYEAR_CLIENTS_LEFT: b14 = client_data_left[cname]['rueckf_soll'] b6 = client_data_left[cname]['rueckf_fluessig'] if (cname or "").strip() == "IKP": b7 = client_data_left[cname].get('sonder', Decimal('0')) client_data_left[cname]['verluste'] = b14 - b6 - b7 else: client_data_left[cname]['verluste'] = b14 - b6 # --- Row B16: Füllungen warm (Lit. L-He) = sum over 6 months (row_index=11) --- for cname in HALFYEAR_CLIENTS_LEFT: total_warm = Decimal('0') for (y, m) in window: sheet = sheets_by_ym.get((y, m)) total_warm += get_top_left_value(sheet, cname, row_index=11) client_data_left[cname]['fuellungen_warm'] = total_warm # --- Row B17: Kaltgas Rückgabe (Lit. L-He) = Bezug * 0.06 --- factor = Decimal('0.06') for cname in HALFYEAR_CLIENTS_LEFT: b13 = client_data_left[cname]['bezug'] client_data_left[cname]['kaltgas_rueckgabe'] = b13 * factor # --- Row B18: Verbraucherverluste (Liter L-He) = Verluste - Kaltgas Rückgabe --- for cname in HALFYEAR_CLIENTS_LEFT: b15 = client_data_left[cname]['verluste'] b17 = client_data_left[cname]['kaltgas_rueckgabe'] client_data_left[cname]['verbraucherverluste'] = b15 - b17 # --- Row B19: % = Verbraucherverluste / Bezug --- for cname in HALFYEAR_CLIENTS_LEFT: bezug = client_data_left[cname]['bezug'] verb = client_data_left[cname]['verbraucherverluste'] if bezug != 0: client_data_left[cname]['percent'] = verb / bezug else: client_data_left[cname]['percent'] = None # Build LEFT rows structure left_row_defs = [ ('Stand der Gaszähler (Nm³)', 'stand_gas'), ('Stand der Gaszähler (Vorjahr) (Nm³)', 'stand_gas_prev'), ('Gasrückführung (Nm³)', 'gasrueckf'), ('Rückführung flüssig (Lit. L-He)', 'rueckf_fluessig'), ('Sonderrückführungen (Lit. L-He)', 'sonder'), ('Bestand in Kannen-1 (Lit. L-He)', 'bestand_kannen'), ('Summe Bestand (Lit. L-He)', 'summe_bestand'), ('Best. in Kannen Vormonat (Lit. L-He)', 'best_kannen_vormonat'), ('Bezug (Liter L-He)', 'bezug'), ('Rückführ. Soll (Lit. L-He)', 'rueckf_soll'), ('Verluste (Soll-Rückf.) (Lit. L-He)', 'verluste'), ('Füllungen warm (Lit. L-He)', 'fuellungen_warm'), ('Kaltgas Rückgabe (Lit. L-He) – Faktor', 'kaltgas_rueckgabe'), ('Verbraucherverluste (Liter L-He)', 'verbraucherverluste'), ('%', 'percent'), ] rows_left = [] for label, key in left_row_defs: values = [client_data_left[cname][key] for cname in HALFYEAR_CLIENTS_LEFT] if key == 'percent': total_bezug = sum((client_data_left[c]['bezug'] for c in HALFYEAR_CLIENTS_LEFT), Decimal('0')) total_verb = sum((client_data_left[c]['verbraucherverluste'] for c in HALFYEAR_CLIENTS_LEFT), Decimal('0')) total = (total_verb / total_bezug) if total_bezug != 0 else None else: total = sum((v for v in values if v is not None), Decimal('0')) rows_left.append({ 'label': label, 'values': values, 'total': total, 'is_percent': key == 'percent', }) # ------------------------------------------------------------------ # 3) RIGHT TABLE (top-right half-year aggregation) # ------------------------------------------------------------------ RIGHT_CLIENTS = HALFYEAR_RIGHT_CLIENTS # for brevity right_data = {name: {} for name in RIGHT_CLIENTS} # --- Bezug (Liter L-He) for each right client (same as for left) --- for cname in RIGHT_CLIENTS: total_bezug = Decimal('0') for (y, m) in window: qs = SecondTableEntry.objects.filter( client__name=cname, date__year=y, date__month=m, ).aggregate( total=Coalesce(Sum('lhe_output'), Value(0, output_field=DecimalField())) ) total_bezug += Decimal(str(qs['total'])) right_data[cname]['bezug'] = total_bezug def find_bestand_from_window(reference_client: str) -> Decimal: """ Implements: WENN(last_month!J36=0; WENN(prev_month!J36=0; ...; prev_sheet!9); last_month!9) reference_client decides which column (L/N/P/Q/R) we read from monthly top_right row_index=5. """ # scan backward through window for (y, m) in reversed(window): sh = sheets_by_ym.get((y, m)) if not sh: continue gasbestand = get_bottom1_value(sh, GASBESTAND_ROW_INDEX, GASBESTAND_COL_NM3) if gasbestand != 0: return get_top_right_value(sh, reference_client, TR_BESTAND_KANNEN_ROW) # fallback to previous month (Übertrag_Dez__Vorjahr equivalent) return get_top_right_value(prev_sheet, reference_client, TR_BESTAND_KANNEN_ROW) # Fohrer+Buntk merged: BOTH use Fohrer column (L9) val_L = find_bestand_from_window("Dr. Fohrer") right_data["Dr. Fohrer"]["bestand_kannen"] = val_L right_data["AG Buntk."]["bestand_kannen"] = val_L # Alff+Gutfl merged: BOTH use Alff column (N9) val_N = find_bestand_from_window("AG Alff") right_data["AG Alff"]["bestand_kannen"] = val_N right_data["AG Gutfl."]["bestand_kannen"] = val_N # M3 each uses its own column (P9/Q9/R9) right_data["M3 Thiele"]["bestand_kannen"] = find_bestand_from_window("M3 Thiele") right_data["M3 Buntkowsky"]["bestand_kannen"] = find_bestand_from_window("M3 Buntkowsky") right_data["M3 Gutfleisch"]["bestand_kannen"] = find_bestand_from_window("M3 Gutfleisch") # Helper for pair shares (L13/($L13+$M13), etc.) def pair_share(c1, c2): total = right_data[c1]['bezug'] + right_data[c2]['bezug'] if total == 0: return (Decimal('0'), Decimal('0')) return ( right_data[c1]['bezug'] / total, right_data[c2]['bezug'] / total, ) # --- "Stand der Gaszähler (Vorjahr) (Nm³)" row: share based on Bezug --- # Dr. Fohrer / AG Buntk. s_fohrer, s_buntk = pair_share("Dr. Fohrer", "AG Buntk.") right_data["Dr. Fohrer"]['stand_prev_share'] = s_fohrer right_data["AG Buntk."]['stand_prev_share'] = s_buntk # AG Alff / AG Gutfl. s_alff, s_gutfl = pair_share("AG Alff", "AG Gutfl.") right_data["AG Alff"]['stand_prev_share'] = s_alff right_data["AG Gutfl."]['stand_prev_share'] = s_gutfl # M3 Thiele / M3 Buntkowsky / M3 Gutfleisch → empty in Excel → None for cname in ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"]: right_data[cname]['stand_prev_share'] = None # --- Rückführung flüssig per month (raw sums) --- # top_right row_index=2 is "Rückführung flüssig (Lit. L-He)" # --- Sonderrückführungen (row_index=3 in top_right) --- for cname in RIGHT_CLIENTS: sonder_total = Decimal('0') for (y, m) in window: sheet = sheets_by_ym.get((y, m)) if sheet: sonder_total += get_top_right_value(sheet, cname, row_index=3) right_data[cname]['sonder'] = sonder_total # --- Sammelrückführung (row_index=4 in top_right), grouped & merged --- # Group 1: Dr. Fohrer + AG Buntk. group1_total = Decimal('0') for (y, m) in window: sheet = sheets_by_ym.get((y, m)) if sheet: group1_total += get_top_right_value(sheet, "Dr. Fohrer", row_index=4) right_data["Dr. Fohrer"]['sammel'] = group1_total right_data["AG Buntk."]['sammel'] = group1_total # Group 2: AG Alff + AG Gutfl. group2_total = Decimal('0') for (y, m) in window: sheet = sheets_by_ym.get((y, m)) if sheet: group2_total += get_top_right_value(sheet, "AG Alff", row_index=4) right_data["AG Alff"]['sammel'] = group2_total right_data["AG Gutfl."]['sammel'] = group2_total # Group 3: M3 Thiele + M3 Buntkowsky + M3 Gutfleisch group3_total = Decimal('0') for (y, m) in window: sheet = sheets_by_ym.get((y, m)) if sheet: group3_total += get_top_right_value(sheet, "M3 Thiele", row_index=4) right_data["M3 Thiele"]['sammel'] = group3_total right_data["M3 Buntkowsky"]['sammel'] = group3_total right_data["M3 Gutfleisch"]['sammel'] = group3_total def safe_div(a: Decimal, b: Decimal) -> Decimal: return (a / b) if b != 0 else Decimal("0") # --- Rückführung flüssig (Lit. L-He) for Halbjahres-Bilanz top-right --- # Uses your exact formulas. # 1) Fohrer / Buntk split by BEZUG share times group SAMMEL (L8) L13 = right_data["Dr. Fohrer"]["bezug"] M13 = right_data["AG Buntk."]["bezug"] L8 = right_data["Dr. Fohrer"]["sammel"] # merged group total den = (L13 + M13) right_data["Dr. Fohrer"]["rueckf_fluessig"] = (safe_div(L13, den) * L8) if den != 0 else Decimal("0") right_data["AG Buntk."]["rueckf_fluessig"] = (safe_div(M13, den) * L8) if den != 0 else Decimal("0") # 2) Alff / Gutfl split by BEZUG share times group SAMMEL (N8) N13 = right_data["AG Alff"]["bezug"] O13 = right_data["AG Gutfl."]["bezug"] N8 = right_data["AG Alff"]["sammel"] # merged group total den = (N13 + O13) right_data["AG Alff"]["rueckf_fluessig"] = (safe_div(N13, den) * N8) if den != 0 else Decimal("0") right_data["AG Gutfl."]["rueckf_fluessig"] = (safe_div(O13, den) * N8) if den != 0 else Decimal("0") # 3) M3 Thiele = sum of monthly Rückführung flüssig (monthly top_right row_index=2) over window P6_sum = Decimal("0") for (y, m) in window: sh = sheets_by_ym.get((y, m)) P6_sum += get_top_right_value(sh, "M3 Thiele", TR_RUECKF_FLUESSIG_ROW) right_data["M3 Thiele"]["rueckf_fluessig"] = P6_sum # 4) M3 Buntkowsky / M3 Gutfleisch split by BEZUG share times M3-group SAMMEL (P8) P13 = right_data["M3 Thiele"]["bezug"] Q13 = right_data["M3 Buntkowsky"]["bezug"] R13 = right_data["M3 Gutfleisch"]["bezug"] P8 = right_data["M3 Thiele"]["sammel"] # merged group total den = (P13 + Q13 + R13) right_data["M3 Buntkowsky"]["rueckf_fluessig"] = (safe_div(Q13, den) * P8) if den != 0 else Decimal("0") right_data["M3 Gutfleisch"]["rueckf_fluessig"] = (safe_div(R13, den) * P8) if den != 0 else Decimal("0") # --- Bestand in Kannen-1 (Lit. L-He) for right table (grouped) --- # Use Gasbestand (J36) and fallback logic, but now reading top_right B9 for each group. TOP_RIGHT_ROW_BESTAND_KANNEN = TR_BESTAND_KANNEN_ROW # <-- most likely correct in your setup def pick_bestand_top_right(base_client: str) -> Decimal: # Go from last month in window backwards: if Gasbestand != 0, use that month's Bestand in Kannen for (y, m) in reversed(window): sh = sheets_by_ym.get((y, m)) if not sh: continue gasbestand = get_bottom1_value(sh, GASBESTAND_ROW_INDEX, GASBESTAND_COL_NM3) if gasbestand != 0: return get_top_right_value(sh, base_client, TOP_RIGHT_ROW_BESTAND_KANNEN) # Fallback to previous month (Übertrag_Dez__Vorjahr equivalent) return get_top_right_value(prev_sheet, base_client, TOP_RIGHT_ROW_BESTAND_KANNEN) # Group 1 merged (Fohrer + Buntk.) g1_best = pick_bestand_top_right("Dr. Fohrer") right_data["Dr. Fohrer"]["bestand_kannen"] = g1_best right_data["AG Buntk."]["bestand_kannen"] = g1_best # Group 2 merged (Alff + Gutfl.) g2_best = pick_bestand_top_right("AG Alff") right_data["AG Alff"]["bestand_kannen"] = g2_best right_data["AG Gutfl."]["bestand_kannen"] = g2_best # Group 3 NOT merged: each M3 client uses its own most recent nonzero Bestand in Kannen-1 right_data["M3 Thiele"]["bestand_kannen"] = pick_bestand_top_right("M3 Thiele") right_data["M3 Buntkowsky"]["bestand_kannen"] = pick_bestand_top_right("M3 Buntkowsky") right_data["M3 Gutfleisch"]["bestand_kannen"] = pick_bestand_top_right("M3 Gutfleisch") # Summe Bestand = same as previous row for cname in RIGHT_CLIENTS: right_data[cname]['summe_bestand'] = right_data[cname]['bestand_kannen'] # Best. in Kannen Vormonat (Lit. L-He) from previous month top_right row_index=7 g1_prev = get_top_right_value(prev_sheet, "Dr. Fohrer", TOP_RIGHT_ROW_BESTAND_KANNEN) right_data["Dr. Fohrer"]['best_kannen_vormonat'] = g1_prev right_data["AG Buntk."]['best_kannen_vormonat'] = g1_prev g2_prev = get_top_right_value(prev_sheet, "AG Alff", TOP_RIGHT_ROW_BESTAND_KANNEN) right_data["AG Alff"]['best_kannen_vormonat'] = g2_prev right_data["AG Gutfl."]['best_kannen_vormonat'] = g2_prev # Group 1 merged (Fohrer + Buntk.) g1_prev = get_top_right_value(prev_sheet, "Dr. Fohrer", TOP_RIGHT_ROW_BESTAND_KANNEN) right_data["Dr. Fohrer"]["best_kannen_vormonat"] = g1_prev right_data["AG Buntk."]["best_kannen_vormonat"] = g1_prev # Group 2 merged (Alff + Gutfl.) g2_prev = get_top_right_value(prev_sheet, "AG Alff", TOP_RIGHT_ROW_BESTAND_KANNEN) right_data["AG Alff"]["best_kannen_vormonat"] = g2_prev right_data["AG Gutfl."]["best_kannen_vormonat"] = g2_prev # Group 3 UNMERGED (each one reads its own cell) right_data["M3 Thiele"]["best_kannen_vormonat"] = get_top_right_value(prev_sheet, "M3 Thiele", TOP_RIGHT_ROW_BESTAND_KANNEN) right_data["M3 Buntkowsky"]["best_kannen_vormonat"] = get_top_right_value(prev_sheet, "M3 Buntkowsky", TOP_RIGHT_ROW_BESTAND_KANNEN) right_data["M3 Gutfleisch"]["best_kannen_vormonat"] = get_top_right_value(prev_sheet, "M3 Gutfleisch", TOP_RIGHT_ROW_BESTAND_KANNEN) # --- Rückführ. Soll (Lit. L-He) according to your formulas --- # Group 1: Dr. Fohrer / AG Buntk. total_bestand_1 = right_data["Dr. Fohrer"]['summe_bestand'] best_vormonat_1 = right_data["Dr. Fohrer"]['best_kannen_vormonat'] diff1 = total_bestand_1 - best_vormonat_1 share_fohrer = right_data["Dr. Fohrer"]['stand_prev_share'] or Decimal('0') right_data["Dr. Fohrer"]['rueckf_soll'] = ( right_data["Dr. Fohrer"]['bezug'] - diff1 * share_fohrer ) right_data["AG Buntk."]['rueckf_soll'] = ( right_data["AG Buntk."]['bezug'] - total_bestand_1 + best_vormonat_1 ) # Group 2: AG Alff / AG Gutfl. total_bestand_2 = right_data["AG Alff"]['summe_bestand'] best_vormonat_2 = right_data["AG Alff"]['best_kannen_vormonat'] diff2 = total_bestand_2 - best_vormonat_2 share_alff = right_data["AG Alff"]['stand_prev_share'] or Decimal('0') share_gutfl = right_data["AG Gutfl."]['stand_prev_share'] or Decimal('0') right_data["AG Alff"]['rueckf_soll'] = ( right_data["AG Alff"]['bezug'] - diff2 * share_alff ) right_data["AG Gutfl."]['rueckf_soll'] = ( right_data["AG Gutfl."]['bezug'] - diff2 * share_gutfl ) # Group 3: M3 Thiele / M3 Buntkowsky / M3 Gutfleisch for cname in ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"]: b13 = right_data[cname]['bezug'] b12 = right_data[cname]['best_kannen_vormonat'] b11 = right_data[cname]['summe_bestand'] # Excel: P13+P12-P11 etc. right_data[cname]['rueckf_soll'] = b13 + b12 - b11 # --- Verluste (Soll-Rückf.) (Lit. L-He) = B14 - B6 - B7 --- for cname in RIGHT_CLIENTS: b14 = right_data[cname]['rueckf_soll'] b6 = right_data[cname]['rueckf_fluessig'] b7 = right_data[cname]['sonder'] right_data[cname]['verluste'] = b14 - b6 - b7 # --- Füllungen warm (Lit. L-He) = sum of monthly 'Füllungen warm' (row_index=11 top_right) --- for cname in RIGHT_CLIENTS: total_warm = Decimal('0') for (y, m) in window: sheet = sheets_by_ym.get((y, m)) if sheet: total_warm += get_top_right_value(sheet, cname, row_index=11) right_data[cname]['fuellungen_warm'] = total_warm # --- Kaltgas Rückgabe (Lit. L-He) – Faktor = Bezug * 0.06 --- for cname in RIGHT_CLIENTS: b13 = right_data[cname]['bezug'] right_data[cname]['kaltgas_rueckgabe'] = b13 * factor # --- Verbraucherverluste (Liter L-He) = Verluste - Kaltgas Rückgabe --- for cname in RIGHT_CLIENTS: b15 = right_data[cname]['verluste'] b17 = right_data[cname]['kaltgas_rueckgabe'] right_data[cname]['verbraucherverluste'] = b15 - b17 # --- % = Verbraucherverluste / Bezug --- for cname in RIGHT_CLIENTS: bezug = right_data[cname]['bezug'] verb = right_data[cname]['verbraucherverluste'] if bezug != 0: right_data[cname]['percent'] = verb / bezug else: right_data[cname]['percent'] = None # Build RIGHT rows structure right_row_defs = [ ('Stand der Gaszähler (Vorjahr) (Nm³)', 'stand_prev_share'), # We skip the pure-text "Gasrückführung (Nm³)" line here, # because it’s only text (Aufteilung nach Verbrauch / Gaszähler) # and easier to render directly in the template if needed. ('Rückführung flüssig (Lit. L-He)', 'rueckf_fluessig'), ('Sonderrückführungen (Lit. L-He)', 'sonder'), ('Sammelrückführung (Lit. L-He)', 'sammel'), ('Bestand in Kannen-1 (Lit. L-He)', 'bestand_kannen'), ('Summe Bestand (Lit. L-He)', 'summe_bestand'), ('Best. in Kannen Vormonat (Lit. L-He)', 'best_kannen_vormonat'), ('Bezug (Liter L-He)', 'bezug'), ('Rückführ. Soll (Lit. L-He)', 'rueckf_soll'), ('Verluste (Soll-Rückf.) (Lit. L-He)', 'verluste'), ('Füllungen warm (Lit. L-He)', 'fuellungen_warm'), ('Kaltgas Rückgabe (Lit. L-He) – Faktor', 'kaltgas_rueckgabe'), ('Verbraucherverluste (Liter L-He)', 'verbraucherverluste'), ('%', 'percent'), ] rows_right = [] for label, key in right_row_defs: values = [right_data[cname].get(key) for cname in RIGHT_CLIENTS] if key == 'percent': total_bezug = sum((right_data[c]['bezug'] for c in RIGHT_CLIENTS), Decimal('0')) total_verb = sum((right_data[c]['verbraucherverluste'] for c in RIGHT_CLIENTS), Decimal('0')) total = (total_verb / total_bezug) if total_bezug != 0 else None else: total = sum((v for v in values if isinstance(v, Decimal)), Decimal('0')) rows_right.append({ 'label': label, 'values': values, 'total': total, 'is_percent': key == 'percent', }) SUM_TABLE_ROWS = [ ("Rückführung flüssig (Lit. L-He)", "rueckf_fluessig"), ("Sonderrückführungen (Lit. L-He)", "sonder"), ("Sammelrückführungen (Lit. L-He)", "sammel"), ("Bestand in Kannen-1 (Lit. L-He)", "bestand_kannen"), ("Summe Bestand (Lit. L-He)", "summe_bestand"), ("Best. in Kannen Vormonat (Lit. L-He)", "best_kannen_vormonat"), ("Bezug (Liter L-He)", "bezug"), ("Rückführ. Soll (Lit. L-He)", "rueckf_soll"), ("Verluste (Soll-Rückf.) (Lit. L-He)", "verluste"), ("Füllungen warm (Lit. L-He)", "fuellungen_warm"), ("Kaltgas Rückgabe (Lit. L-He) – Faktor", "kaltgas_rueckgabe"), ("Faktor 0.06", "factor_row"), ("Verbraucherverluste (Liter L-He)", "verbraucherverluste"), ("%", "percent"), ] RIGHT_GROUPS = { "chemie": ["Dr. Fohrer", "AG Buntk."], "mawi": ["AG Alff", "AG Gutfl."], "m3": ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"], } RIGHT_ALL = ["Dr. Fohrer", "AG Buntk.", "AG Alff", "AG Gutfl.", "M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"] LEFT_ALL = HALFYEAR_CLIENTS_LEFT def safe_pct(verb, bez): return (verb / bez) if bez != 0 else None rows_sum = [] def d(x): return x if isinstance(x, Decimal) else Decimal("0") for label, key in SUM_TABLE_ROWS: if key == "factor_row": lichtwiese = chemie = mawi = m3 = total = Decimal("0.06") elif key == "percent": # Right totals rw_bez = sum(d(right_data[c].get("bezug")) for c in RIGHT_ALL) rw_verb = sum(d(right_data[c].get("verbraucherverluste")) for c in RIGHT_ALL) lichtwiese = safe_pct(rw_verb, rw_bez) # Chemie ch_bez = sum(d(right_data[c].get("bezug")) for c in RIGHT_GROUPS["chemie"]) ch_verb = sum(d(right_data[c].get("verbraucherverluste")) for c in RIGHT_GROUPS["chemie"]) chemie = safe_pct(ch_verb, ch_bez) # MaWi mw_bez = sum(d(right_data[c].get("bezug")) for c in RIGHT_GROUPS["mawi"]) mw_verb = sum(d(right_data[c].get("verbraucherverluste")) for c in RIGHT_GROUPS["mawi"]) mawi = safe_pct(mw_verb, mw_bez) # M3 m3_bez = sum(d(right_data[c].get("bezug")) for c in RIGHT_GROUPS["m3"]) m3_verb = sum(d(right_data[c].get("verbraucherverluste")) for c in RIGHT_GROUPS["m3"]) m3 = safe_pct(m3_verb, m3_bez) # Σ column = (left verb + right verb) / (left bez + right bez) left_bez = sum(d(client_data_left[c].get("bezug")) for c in LEFT_ALL) left_verb = sum(d(client_data_left[c].get("verbraucherverluste")) for c in LEFT_ALL) total = safe_pct(left_verb + rw_verb, left_bez + rw_bez) else: # normal rows = sums lichtwiese = sum(d(right_data[c].get(key)) for c in RIGHT_ALL) chemie = sum(d(right_data[c].get(key)) for c in RIGHT_GROUPS["chemie"]) mawi = sum(d(right_data[c].get(key)) for c in RIGHT_GROUPS["mawi"]) m3 = sum(d(right_data[c].get(key)) for c in RIGHT_GROUPS["m3"]) left_total = sum(d(client_data_left[c].get(key)) for c in LEFT_ALL) total = left_total + lichtwiese rows_sum.append({ "row_index": row_index, "label": label, "total": total, "lichtwiese": lichtwiese, "chemie": chemie, "mawi": mawi, "m3": m3, "is_percent": (key == "percent"), }) def find_sum_row(rows, label_startswith: str): for r in rows: if str(r.get("label", "")).strip().startswith(label_startswith): return r return None summe_bestand_row = find_sum_row(rows_sum, "Summe Bestand") k38 = (summe_bestand_row.get("total") if summe_bestand_row else Decimal("0")) or Decimal("0") j38 = k38 * Decimal("0.75") # --- FIX: now that k38 is known, update bottom2 + recompute dependent rows --- bottom2["k38"] = k38 bottom2["j38"] = j38 k39 = bottom2.get("k39") or Decimal("0") k40 = bottom2.get("k40") or Decimal("0") # Row 43: Bestand flüssig He = SUMME(K38:K40) k43 = (k38 or Decimal("0")) + k39 + k40 j43 = k43 * Decimal("0.75") bottom2["k43"] = k43 bottom2["j43"] = j43 # Row 44: Gesamtbestand neu = Gasbestand(Lit) from bottom table 1 + k43 gasbestand_lit = Decimal("0") for r in bottom1_rows: if (r.get("label") or "").strip().startswith("Gasbestand"): gasbestand_lit = r.get("lhe") or Decimal("0") break k44 = gasbestand_lit + k43 j44 = k44 * Decimal("0.75") bottom2["k44"] = k44 bottom2["j44"] = j44 def d(x): return x if isinstance(x, Decimal) else Decimal("0") # ---- Bottom2: J38/K38 depend on rows_sum (overall summary), so do it HERE ---- k38 = Decimal("0") for r in rows_sum: if r.get("label") == "Summe Bestand (Lit. L-He)": k38 = r.get("total") or Decimal("0") break j38 = k38 * FACTOR_NM3_TO_LHE # 0.75 bottom2["k38"] = k38 bottom2["j38"] = j38 factor = Decimal("0.75") # window = the 6-month list you already build in this view: [(y,m), (y,m), ...] # bottom2 = dict with "k44" already computed in your view # rows_sum = overall sum group rows list (your existing halfyear logic) # 1) K46 = K44 from the month BEFORE the global month (interval start) start_year = interval_year start_month = interval_start # whatever you named your start month variable if start_month == 1: prev_year, prev_month = start_year - 1, 12 else: prev_year, prev_month = start_year, start_month - 1 prev_sheet = MonthlySheet.objects.filter(year=prev_year, month=prev_month).first() # This assumes you have MonthlySummary.gesamtbestand_neu_lhe as K44 equivalent. # If your field name differs, tell me your MonthlySummary model fields. prev_k44 = Decimal("0") if prev_sheet: prev_sum = MonthlySummary.objects.filter(sheet=prev_sheet).first() if prev_sum and prev_sum.gesamtbestand_neu_lhe is not None: prev_k44 = Decimal(str(prev_sum.gesamtbestand_neu_lhe)) # helpers: read bottom_3 values for a given sheet/month def get_bottom3_value(sheet, row_index, col_index): if not sheet: return Decimal("0") c = Cell.objects.filter( sheet=sheet, table_type="bottom_3", row_index=row_index, column_index=col_index ).first() if not c or c.value in (None, "", "None"): return Decimal("0") try: return Decimal(str(c.value)) except Exception: return Decimal("0") # 2) Sum rows across the 6-month window g47_sum = Decimal("0") i47_sum = Decimal("0") j47_sum = Decimal("0") g50_sum = Decimal("0") i50_sum = Decimal("0") for (yy, mm) in window: s = MonthlySheet.objects.filter(year=yy, month=mm).first() ## Excel row 47 maps to bottom_3 row_index=1 in DB (see monthly_sheet.html) g = get_bottom3_value(s, 1, 1) # G47 editable i = get_bottom3_value(s, 1, 2) # I47 editable g47_sum += g i47_sum += i # In monthly_sheet, J47 is CALCULATED as (G47 + I47), not stored as an editable cell j47_sum += (g + i) # row 50: G(2), I(4) g50_sum += get_bottom3_value(s, 50, 2) i50_sum += get_bottom3_value(s, 50, 4) # 3) K52 = Verbraucherverlust from overall sum group first column (global Σ) k52 = Decimal("0") for r in rows_sum: label = (r.get("label") or "") if label.startswith("Verbraucherverluste"): k52 = r.get("total") or Decimal("0") break # --- apply the SAME monthly formulas, with your overrides --- # Row 46 k46 = prev_k44 j46 = k46 * factor # Row 47 g47 = g47_sum i47 = i47_sum j47 = j47_sum k47 = (j47 / factor) + g47 if (j47 != 0 or g47 != 0) else Decimal("0") # Row 48 k48 = k46 + k47 j48 = k48 * factor # Row 49 (akt. Monat) -> in halfyear we use current bottom2 K44 k49 = bottom2.get("k44") or Decimal("0") j49 = k49 * factor # Row 50 g50 = g50_sum i50 = i50_sum j50 = i50 k50 = (j50 / factor) if j50 != 0 else Decimal("0") # Row 51 k51 = k48 - k49 - k50 j51 = k51 * factor # Row 52 j52 = k52 * factor # Row 53 k53 = k51 - k52 j53 = j51 - j52 bottom3 = { "j46": j46, "k46": k46, "g47": g47, "i47": i47, "j47": j47, "k47": k47, "j48": j48, "k48": k48, "j49": j49, "k49": k49, "g50": g50, "i50": i50, "j50": j50, "k50": k50, "j51": j51, "k51": k51, "j52": j52, "k52": k52, "j53": j53, "k53": k53, } # ------------------------------------------------------------------ # 4) Context – keep old keys AND new ones # ------------------------------------------------------------------ context = { 'interval_year': interval_year, 'interval_start_month': interval_start, 'window': window, 'clients': HALFYEAR_CLIENTS_LEFT, 'rows': rows_left, 'clients_left': HALFYEAR_CLIENTS_LEFT, 'rows_left': rows_left, 'clients_right': RIGHT_CLIENTS, 'rows_right': rows_right, 'rows_sum': rows_sum, 'bottom1_rows': bottom1_rows, } context["bottom2"] = bottom2 context["bottom3"] = bottom3 context["context_bottom2_g39"] = bottom2_inputs["g39"] context["context_bottom2_i39"] = bottom2_inputs["i39"] return context