diff --git a/db.sqlite3 b/db.sqlite3 index 35a321a..3567da4 100644 Binary files a/db.sqlite3 and b/db.sqlite3 differ diff --git a/sheets/services/halfyear_calc.py b/sheets/services/halfyear_calc.py index 705809f..e3ba478 100644 --- a/sheets/services/halfyear_calc.py +++ b/sheets/services/halfyear_calc.py @@ -13,8 +13,37 @@ 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_RUECKF_FLUESSIG_ROW = 2 TR_BESTAND_KANNEN_ROW = 5 +GASBESTAND_ROW_INDEX = 9 +GASBESTAND_COL_NM3 = 3 + +# 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", + "Merck" +] +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 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 @@ -128,34 +157,7 @@ def pick_sheet_by_gasbestand(window, sheets_by_ym, prev_sheet): 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): """ @@ -171,7 +173,53 @@ def build_halfyear_window(interval_year: int, start_month: int): return window # Import ONLY models + pure helpers here from sheets.models import MonthlySheet, Cell, Client, SecondTableEntry, BetriebskostenSummary +def sum_right_row_without_duplicates(label: str, clients_right: list[str], values: list): + """ + For specific rows, clients are logically merged, so we must only count one copy. + """ + # rows where the PAIRS are merged: + # - Bestand in Kannen-1 + # - Summe Bestand + # - Best. in Kannen Vormonat + merged_pair_labels = { + "Bestand in Kannen-1 (Lit. L-He)", + "Summe Bestand (Lit. L-He)", + "Best. in Kannen Vormonat (Lit. L-He)", + "Verbraucherverluste (Liter L-He)", + # ✅ Sammel is also duplicated for the merged PAIRS (Fohrer+Buntk, Alff+Gutfl) + "Sammelrückführungen (Lit. L-He)", + "Sammelrückführung (Lit. L-He)", # safety (singular) +} + + merged_m3_labels = { + # ✅ Sammel is duplicated for the merged M3 triple + "Sammelrückführungen (Lit. L-He)", + "Sammelrückführung (Lit. L-He)", # safety (singular) + } + + skip_indices = set() + + # skip second column of each merged PAIR + if label in merged_pair_labels: + for right_name in ("AG Buntk.", "AG Gutfl."): + if right_name in clients_right: + skip_indices.add(clients_right.index(right_name)) + + # skip 2nd+3rd of the merged TRIPLE + if label in merged_m3_labels: + for name in ("M3 Buntkowsky", "M3 Gutfleisch"): + if name in clients_right: + skip_indices.add(clients_right.index(name)) + + total = Decimal("0") + for i, v in enumerate(values): + if i in skip_indices: + continue + if v in (None, ""): + continue + total += Decimal(str(v).replace(",", ".")) + return total 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. @@ -227,8 +275,8 @@ def compute_halfyear_context(interval_year: int, interval_start: int) -> Dict[st # ---------------------------- 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 + # define which bottom_1 row_index corresponds to Excel rows 27..35 + # If bottom_1 starts at Excel row 27 => row_index 0 == Excel 27 # then row_index = excel_row - 27 BOTTOM1_EXCEL_START_ROW = 27 @@ -671,6 +719,20 @@ def compute_halfyear_context(interval_year: int, interval_start: int) -> Dict[st right_data["M3 Gutfleisch"]['sammel'] = group3_total def safe_div(a: Decimal, b: Decimal) -> Decimal: return (a / b) if b != 0 else Decimal("0") + + # Merck: Sammelrückführung = total helium input (ExcelEntry.lhe_ges) over the 6-month window + merck_sammel_total = Decimal("0") + for (y, m) in window: + qs = ExcelEntry.objects.filter( + client__name="Merck", + date__year=y, + date__month=m, + ).aggregate( + total=Coalesce(Sum("lhe_ges"), Value(0, output_field=DecimalField())) + ) + merck_sammel_total += Decimal(str(qs["total"])) + + right_data["Merck"]["sammel"] = merck_sammel_total # --- Rückführung flüssig (Lit. L-He) for Halbjahres-Bilanz top-right --- # Uses your exact formulas. @@ -741,6 +803,9 @@ def compute_halfyear_context(interval_year: int, interval_start: int) -> Dict[st 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") + # Merck should stay empty in Bestand in Kannen-1 + right_data["Merck"]["bestand_kannen"] = None + right_data["Merck"]["best_kannen_vormonat"] = None # Summe Bestand = same as previous row for cname in RIGHT_CLIENTS: @@ -807,9 +872,23 @@ def compute_halfyear_context(interval_year: int, interval_start: int) -> Dict[st b11 = right_data[cname]['summe_bestand'] # Excel: P13+P12-P11 etc. right_data[cname]['rueckf_soll'] = b13 + b12 - b11 - + # Merck: only selected rows should have values in Top Right Halbjahresbilanz + right_data["Merck"]["stand_prev_share"] = None + right_data["Merck"]["rueckf_fluessig"] = None + right_data["Merck"]["sonder"] = None + right_data["Merck"]["bestand_kannen"] = None + right_data["Merck"]["summe_bestand"] = None + right_data["Merck"]["best_kannen_vormonat"] = None + right_data["Merck"]["rueckf_soll"] = None + right_data["Merck"]["verluste"] = None + right_data["Merck"]["fuellungen_warm"] = None + right_data["Merck"]["kaltgas_rueckgabe"] = None # --- Verluste (Soll-Rückf.) (Lit. L-He) = B14 - B6 - B7 --- for cname in RIGHT_CLIENTS: + if cname == "Merck": + right_data[cname]['verluste'] = None + continue + b14 = right_data[cname]['rueckf_soll'] b6 = right_data[cname]['rueckf_fluessig'] b7 = right_data[cname]['sonder'] @@ -829,17 +908,22 @@ def compute_halfyear_context(interval_year: int, interval_start: int) -> Dict[st b13 = right_data[cname]['bezug'] right_data[cname]['kaltgas_rueckgabe'] = b13 * factor - # --- Verbraucherverluste (Liter L-He) = Verluste - Kaltgas Rückgabe --- + # --- Verbraucherverluste (Liter L-He) --- for cname in RIGHT_CLIENTS: - b15 = right_data[cname]['verluste'] - b17 = right_data[cname]['kaltgas_rueckgabe'] - right_data[cname]['verbraucherverluste'] = b15 - b17 + if cname == "Merck": + bezug = right_data[cname].get("bezug") or Decimal("0") + sammel = right_data[cname].get("sammel") or Decimal("0") + right_data[cname]["verbraucherverluste"] = bezug - sammel + else: + 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: + bezug = right_data[cname].get('bezug') or Decimal("0") + verb = right_data[cname].get('verbraucherverluste') + if bezug != 0 and verb is not None: right_data[cname]['percent'] = verb / bezug else: right_data[cname]['percent'] = None @@ -910,50 +994,78 @@ def compute_halfyear_context(interval_year: int, interval_start: int) -> Dict[st def safe_pct(verb, bez): return (verb / bez) if bez != 0 else None + def sum_right_key(label_for_merge: str, clients: list[str], key: str) -> Decimal: + vals = [right_data[c].get(key) for c in clients] + return sum_right_row_without_duplicates(label_for_merge, clients, vals) rows_sum = [] def d(x): return x if isinstance(x, Decimal) else Decimal("0") - for label, key in SUM_TABLE_ROWS: + for row_index, (label, key) in enumerate(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) + # We want % = Verbraucherverluste / Bezug (for each group) + + # LEFT totals (no merge on left) + 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) + + # RIGHT totals (must skip merged duplicates!) + rw_bez = sum(d(right_data[c].get("bezug")) for c in RIGHT_ALL) + + rw_verb = sum_right_key("Verbraucherverluste (Liter L-He)", RIGHT_ALL, "verbraucherverluste") + 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 group (Fohrer+Buntk) + ch_clients = RIGHT_GROUPS["chemie"] + ch_bez = sum_right_key("Bezug (Liter L-He)", ch_clients, "bezug") + ch_verb = sum_right_key("Verbraucherverluste (Liter L-He)", ch_clients, "verbraucherverluste") 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 group (Alff+Gutfl) + mw_clients = RIGHT_GROUPS["mawi"] + mw_bez = sum_right_key("Bezug (Liter L-He)", mw_clients, "bezug") + mw_verb = sum_right_key("Verbraucherverluste (Liter L-He)", mw_clients, "verbraucherverluste") 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 group (triple) + m3_clients = RIGHT_GROUPS["m3"] + m3_bez = sum_right_key("Bezug (Liter L-He)", m3_clients, "bezug") + m3_verb = sum_right_key("Verbraucherverluste (Liter L-He)", m3_clients, "verbraucherverluste") 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) + # Overall Σ (%) 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"]) + # normal rows = sums (BUT skip duplicates for merged logical cells) + rw_values = [right_data[c].get(key) for c in RIGHT_ALL] + lichtwiese = sum_right_row_without_duplicates(label, RIGHT_ALL, rw_values) + + ch_clients = RIGHT_GROUPS["chemie"] + ch_values = [right_data[c].get(key) for c in ch_clients] + chemie = sum_right_row_without_duplicates(label, ch_clients, ch_values) + + mw_clients = RIGHT_GROUPS["mawi"] + mw_values = [right_data[c].get(key) for c in mw_clients] + mawi = sum_right_row_without_duplicates(label, mw_clients, mw_values) + + m3_clients = RIGHT_GROUPS["m3"] + m3_values = [right_data[c].get(key) for c in m3_clients] + m3 = sum_right_row_without_duplicates(label, m3_clients, m3_values) left_total = sum(d(client_data_left[c].get(key)) for c in LEFT_ALL) - total = left_total + lichtwiese + # Merck should count ONLY in the overall total, not in Lichtwiese + merck_val = d(right_data["Merck"].get(key)) + + total = left_total + lichtwiese + merck_val + + # ✅ THIS MUST BE OUTSIDE THE IF/ELIF/ELSE rows_sum.append({ "row_index": row_index, "label": label, @@ -964,6 +1076,7 @@ def compute_halfyear_context(interval_year: int, interval_start: int) -> Dict[st "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): @@ -999,8 +1112,8 @@ def compute_halfyear_context(interval_year: int, interval_start: int) -> Dict[st bottom2["k44"] = k44 bottom2["j44"] = j44 - def d(x): - return x if isinstance(x, Decimal) else Decimal("0") + def d_or_none(x): + return x if isinstance(x, Decimal) else None # ---- Bottom2: J38/K38 depend on rows_sum (overall summary), so do it HERE ---- k38 = Decimal("0") diff --git a/sheets/templates/clients_table.html b/sheets/templates/clients_table.html index 28078dc..4af89dc 100644 --- a/sheets/templates/clients_table.html +++ b/sheets/templates/clients_table.html @@ -3,10 +3,10 @@ {% block content %}
-

Helium Output Yearly Summary

+

Heliumabgabe - Halbjahresübersicht

{% csrf_token %} -

Global 6-Month Interval

+

Allgemeines 6-Monats-Intervall