From fbc11a361f23349896cc0a4057518977e442ded6 Mon Sep 17 00:00:00 2001 From: Mohamad Hesham Jenbaz Date: Tue, 14 Apr 2026 10:03:33 +0200 Subject: [PATCH] All checks passed! --- db.sqlite3 | Bin 4034560 -> 4034560 bytes excel_mimic/pyproject.toml | 17 + excel_mimic/urls.py | 2 +- round_entries.py | 48 - sheets/apps.py | 2 +- sheets/forms.py | 2 +- sheets/models.py | 15 +- sheets/new/views.py | 5041 ------------------------------ sheets/old/halfyear_balance.html | 353 --- sheets/old/views.py | 4401 -------------------------- sheets/old/views_v2.py | 4520 --------------------------- sheets/services/halfyear_calc.py | 13 +- sheets/tests.py | 1 - sheets/urls.py | 4 +- sheets/views.py | 130 +- 15 files changed, 66 insertions(+), 14483 deletions(-) create mode 100644 excel_mimic/pyproject.toml delete mode 100644 round_entries.py delete mode 100644 sheets/new/views.py delete mode 100644 sheets/old/halfyear_balance.html delete mode 100644 sheets/old/views.py delete mode 100644 sheets/old/views_v2.py diff --git a/db.sqlite3 b/db.sqlite3 index 3567da489e7621464bf8b165be44d8330c77a7b7..f930b335e40e7eb96597596ba0dde4f6dfc86827 100644 GIT binary patch delta 1856 zcmZwHc}!GC7y$4$Gw*R-kFq$W)usr|#GCN)*dAJtgaRO()SX5XmEr$O$ws`XuW{)JW(u-2fJYH3=e`c(Z%y^5Bpz3KtAT3w?qQWfP9(v=&^C8bAcRkkTP zN;DP}#&XYc-Ez+Ip=GC~2sK&UmN5Ab`A7Ln`D1w>x-XZ@d2+lgN)Mz-+TkN!j9ckv zKC-HPm=qHavEje)E<egDY@8j>qv(F2lH@EsPT(K^5mqQcFW)+0N=6Erj!pUD>#? z`0G#K-My=#y4e|S{s_XR;`T9?vCKnw(3dm2le#xF4D|mBzXD z%u$BMT^pPFPZ^G);6!(F$!ZpA-$0Y{NO6K|(;Q>vSI4%|_4&kVT*@P*spN8BoA ziw#rrK5jF&oU^lk zv$xrC_9WZH7P3oNl{_Lh$S65PDo8eQ;%WRd{ss@=PF#%(a3Yq_12l;)qGRYZdLO-w zRv@Rj`7@UjK6eOpt|!gL!;8d*Xz>LSO``ls^!Na=)^Ndn<{bV+bEypM9+D~=T3Q-* zm3depm?jQ?!dz)SnC7GW3ty(CnbFTDkMqaV+(9@d-w??paeh~@)iyvvY3KlnrAG(I zd(iI;^xJ64AjyDMRIp_qA|bSTker0BdBK7A`$!q}z(80=sJRUKdp{9q&k)&0Yx+rZ zq{A;mJ(3i7Ze~`)9HC{yq@1qpBThO#OmgV0eiBXV`bZXy&m(*Bejgnhz*gh%77`(% zSYxo9{DP5U9IParY?5SWZ18+x{Csy(3Bfi&La+Aa7-_X+B13#}1cum4rpr)WkHDT_ zQJs+7bC#V*i0jNTw=D2_yetu>^o{P14U7!#ET-Lvt!1x`Zn;DML9EM+3>RjLka+FN z#Ld8qWb7sEJrwq$85}4&I^Uo_2&|QMaBLqI!l{dAALoh|CL-`sinH$;*STV@Vp!kj zsAHxWm^0g))2J8NPegQt+CtdJsLK#T*}DQOe}v8&zeTdAkewqEW;$3RAznVi@WC*} zlEiRbeRdWEzIh-35lBD=7N7tXXg~)cAQXfFE3koiARI)1NH8DRK@@NRCx|xs>@k%+ z#H#tIFd7q&HQ!~AJ4{nFE6)fFBL!AR*Itd85_#x>kJObp9s^>H%<F<1hUK?+!E^o+Rkn$$Q|QF)b7o+?i) z(Z*D)=U0&|LK}?llRU+!lp_f7tIVqo|I=^rtKhnc55y&ATxiCxoAF9B&NO4M880#8 ng=QRU#`dnp#U9`VsbCpM1L+_GWP;@&3uJ>FqjB+y?y7$QsdNYj delta 2308 zcmbu9X>1f_7{}jhcJA5Ulx26@QffKey4{`>DlJGt5KD^)1!GzYg+dBFSXzo=yI6%7 zf`zueKsgq{2%_OK83GCTL81u|&TNT_pWMxFcK*-v9{>5z z%=Sco$2~xw$H?7>5WS9^&-U@fBzND5`h|Ho>D1}G70t6NLxlxpU0v&1rgv?qUbG-k zGCQYMaNeU?5^*R)@7vG%2QQXACPYm2q1 znxBXoQg5jj)WhoAYO`8})77Awq}*4oDQA>D%Io+irCyn?n!O6u?S`c>q^80*q#Sq`_f)-^XS4^x4y zS+}~OvE7^Ygf0=N-5ukCuJn(&@us)q2~!Q>va|34yPNR0eHGy{zfei*`NM^@3vc8# zGib7Tayp%-vuRQX;tAFC^=`J~e}31p`K*v!d*b&r*-5&{Vp2vtco=_!&*CBc8h!@avG?!>!8udjw`Q1H6s^+o_E^nkm`3g&sI1V%ug=y1|#g zM80j8At` zjgZe{*UIZ0Bjf|y2pKU#w%8+H9WrDFYcx4)JT^gQ#YQBO$0o@5SRmUW5bFV{u^*5q zSW>_TzJoGkjQFE|BUPrLFIt-W2aL2_7s!ud?U7fpedUU#@Q5G0EjHN7T9#mNOH8&| zl7Os^No!*X+~RzY{BFBG3X;Idmbd~f%LKZQG9<@@1SZhepm{4dMvVs#$c%OW!vor7 z&$Vu_Wdd!mG`CDBU;@2txAzag^??gC6|~Gxg!NPjd>}taejElf!5e{x?I1}ux7N~8 z64uN^_4Ip!v&=0^X)oiMO*Az{_MjB_Pb8$FB^+*9T~|OTF^ih$#S%%!2T=i@BbZW@ zKCHuigCgr^Vv(3OxQ|`r!RrB7VlK<(eV8?|isv#B{}Z$9*qND4SQVBkW`FfkZPtXD zW*23*a8g{_FPP-*eV$(xSu*bt*a0G`Jln@sSC*ULI&*^M8_Pk%L=Aoz`4nqQko7=;vjPtC+ z<^vuE2!IO^0SS-+1yBJE&;bK*0|`JPkOU+HDL^Wa28;sIfzf~m7z20#AK*6+crum_ z(j=pri^nr^P9?UEGkEp6jGGccB6bab`tIh^^BF*=R9s8Dhd$qWE z@Yq-&8yE-V08atAz<6K+FcFvpJPibZJm48~@K|tqo8GKzx=Tmeu=bm}+`OHc|D#0P w@sN3?tDIv+pcuQRS;x(-lk$NOPyiGHML;o70+a%ifihqUFx70GRNlAjFZps~_5c6? diff --git a/excel_mimic/pyproject.toml b/excel_mimic/pyproject.toml new file mode 100644 index 0000000..a2e25a6 --- /dev/null +++ b/excel_mimic/pyproject.toml @@ -0,0 +1,17 @@ +[tool.ruff] +line-length = 100 +target-version = "py312" +exclude = [ + "migrations", + ".venv", + "venv", + "__pycache__", +] + +[tool.ruff.lint] +select = ["E", "F", "I"] +ignore = [] + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" \ No newline at end of file diff --git a/excel_mimic/urls.py b/excel_mimic/urls.py index 1497463..ee3e395 100644 --- a/excel_mimic/urls.py +++ b/excel_mimic/urls.py @@ -15,7 +15,7 @@ Including another URLconftesting test 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin -from django.urls import path, include +from django.urls import include, path urlpatterns = [ path('admin/', admin.site.urls), # Admin site diff --git a/round_entries.py b/round_entries.py deleted file mode 100644 index 0c86f1b..0000000 --- a/round_entries.py +++ /dev/null @@ -1,48 +0,0 @@ -from decimal import Decimal, ROUND_HALF_UP -import django -import os - -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "excel_mimic.settings") -django.setup() - -from sheets.models import ExcelEntry - - -TWO_DEC = Decimal("0.00") - -def q2(x): - return Decimal(str(x)).quantize(TWO_DEC, rounding=ROUND_HALF_UP) - -fields = ["korrig_druck", "nm3", "lhe", "lhe_ges"] - -to_update = [] -total = 0 -changed_count = 0 - -qs = ExcelEntry.objects.all().only("id", *fields) - -for e in qs.iterator(chunk_size=500): - total += 1 - changed = False - - for f in fields: - val = getattr(e, f) - if val is not None: - new = q2(val) - if new != val: - setattr(e, f, new) - changed = True - - if changed: - to_update.append(e) - changed_count += 1 - - if len(to_update) >= 500: - ExcelEntry.objects.bulk_update(to_update, fields) - to_update = [] - print(f"processed={total}, changed={changed_count}") - -if to_update: - ExcelEntry.objects.bulk_update(to_update, fields) - -print(f"DONE. processed={total}, changed={changed_count}") diff --git a/sheets/apps.py b/sheets/apps.py index 519e7f8..cb22ba4 100644 --- a/sheets/apps.py +++ b/sheets/apps.py @@ -5,4 +5,4 @@ class SheetsConfig(AppConfig): name = 'sheets' def ready(self): - import sheets.signals + pass diff --git a/sheets/forms.py b/sheets/forms.py index 1b4f3ba..0f06c43 100644 --- a/sheets/forms.py +++ b/sheets/forms.py @@ -1,5 +1,5 @@ from django import forms -from .models import ExcelEntry, Betriebskosten, Institute +from .models import ExcelEntry, Betriebskosten diff --git a/sheets/models.py b/sheets/models.py index 09dddcc..7a74ba0 100644 --- a/sheets/models.py +++ b/sheets/models.py @@ -1,7 +1,10 @@ from django.db import models from django.utils import timezone from django.core.validators import MinValueValidator, MaxValueValidator - +from decimal import Decimal +from django.db.models import Sum +from django.db.models.functions import Coalesce +from django.db.models import DecimalField, Value class Institute(models.Model): name = models.CharField(max_length=100) @@ -73,8 +76,7 @@ class CellReference(models.Model): class Meta: unique_together = ['source_cell', 'target_cell'] -from decimal import Decimal -from django.db import models + class Betriebskosten(models.Model): KOSTENTYP_CHOICES = [ @@ -263,10 +265,7 @@ class BetriebskostenSummary(models.Model): umlage_personal = models.DecimalField(max_digits=12, decimal_places=2, default=0) def recalculate(self): - from django.db.models import Sum - from django.db.models.functions import Coalesce - from django.db.models import DecimalField, Value - from .models import Betriebskosten + items = Betriebskosten.objects.all() @@ -291,7 +290,7 @@ class BetriebskostenSummary(models.Model): self.save() # models.py -from django.db import models + class AbrechnungCell(models.Model): """ diff --git a/sheets/new/views.py b/sheets/new/views.py deleted file mode 100644 index d089af7..0000000 --- a/sheets/new/views.py +++ /dev/null @@ -1,5041 +0,0 @@ -from django.shortcuts import render, redirect -from django.utils.decorators import method_decorator -from django.views.decorators.csrf import csrf_exempt -from django.db.models import Sum, Value, DecimalField -from django.http import JsonResponse -from django.db.models import Q -from decimal import Decimal, InvalidOperation -from django.apps import apps -from datetime import date, datetime -import calendar -from django.utils import timezone -from django.views.generic import TemplateView, View -from .models import ( - Client, SecondTableEntry, Institute, ExcelEntry, - Betriebskosten, MonthlySheet, Cell, CellReference, MonthlySummary ,BetriebskostenSummary,AbrechnungCell -) -from django.db.models import Sum -from django.urls import reverse -from django.db.models.functions import Coalesce -from .forms import BetriebskostenForm -from django.utils.dateparse import parse_date -from django.contrib.auth.mixins import LoginRequiredMixin -import json -FIRST_SHEET_YEAR = 2025 -FIRST_SHEET_MONTH = 1 - -CLIENT_GROUPS = { - 'ikp': { - 'label': 'IKP', - # exactly as in the Clients admin - 'names': ['IKP'], - }, - 'phys_chem_bunt_fohrer': { - 'label': 'Buntkowsky + Dr. Fohrer', - # include all variants you might have used for Buntkowsky - 'names': [ - 'AG Buntk.', # the one in your new entry - 'AG Buntkowsky.', # from your original list - 'AG Buntkowsky', - 'Dr. Fohrer', - ], - }, - 'mawi_alff_gutfleisch': { - 'label': 'Alff + AG Gutfleisch', - # include both short and full forms - 'names': [ - 'AG Alff', - 'AG Gutfl.', - 'AG Gutfleisch', - ], - }, - 'm3_group': { - 'label': 'M3 Buntkowsky + M3 Thiele + M3 Gutfleisch', - 'names': [ - 'M3 Buntkowsky', - 'M3 Thiele', - 'M3 Gutfleisch', - ], - }, -} - -# Add this CALCULATION_CONFIG at the top of views.py - -CALCULATION_CONFIG = { - 'top_left': { - # Row mappings: Django row_index (0-based) to Excel row - # Excel B4 -> Django row_index 1 (UI row 2) - # Excel B5 -> Django row_index 2 (UI row 3) - # Excel B6 -> Django row_index 3 (UI row 4) - - # B6 (row_index 3) = B5 (row_index 2) / 0.75 - 3: "2 / 0.75", - - # B11 (row_index 10) = B9 (row_index 8) - 10: "8", - - # B14 (row_index 13) = B13 (row_index 12) - B11 (row_index 10) + B12 (row_index 11) - 13: "12 - 10 + 11", - - # Note: B5, B17, B19, B20 require IF logic, so they'll be handled separately - }, - - # other tables (top_right, bottom_1, ...) stay as they are - - - ' top_right': { - # UI Row 1 (Excel Row 4): Stand der Gaszähler (Vormonat) (Nm³) - 0: { - 'L': "9 / (9 + 9) if (9 + 9) > 0 else 0", # L4 = L13/(L13+M13) - 'M': "9 / (9 + 9) if (9 + 9) > 0 else 0", # M4 = M13/(L13+M13) - 'N': "9 / (9 + 9) if (9 + 9) > 0 else 0", # N4 = N13/(N13+O13) - 'O': "9 / (9 + 9) if (9 + 9) > 0 else 0", # O4 = O13/(N13+O13) - 'P': None, # Editable - 'Q': None, # Editable - 'R': None, # Editable - }, - - # UI Row 2 (Excel Row 5): Gasrückführung (Nm³) - 1: { - 'L': "4", # L5 = L8 - 'M': "4", # M5 = L8 (merged) - 'N': "4", # N5 = N8 - 'O': "4", # O5 = N8 (merged) - 'P': "4 * 0", # P5 = P8 * P4 - 'Q': "4 * 0", # Q5 = P8 * Q4 - 'R': "4 * 0", # R5 = P8 * R4 - }, - - # UI Row 3 (Excel Row 6): Rückführung flüssig (Lit. L-He) - 2: { - 'L': "4", # L6 = L8 (Sammelrückführungen) - 'M': "4", - 'N': "4", - 'O': "4", - 'P': "4", - 'Q': "4", - 'R': "4", -}, - - # UI Row 4 (Excel Row 7): Sonderrückführungen (Lit. L-He) - EDITABLE - 3: { - 'L': None, - 'M': None, - 'N': None, - 'O': None, - 'P': None, - 'Q': None, - 'R': None, - }, - - # UI Row 5 (Excel Row 8): Sammelrückführungen (Lit. L-He) - 4: { - 'L': None, # Will be populated from ExcelEntry - 'M': None, - 'N': None, - 'O': None, - 'P': None, - 'Q': None, - 'R': None, - }, - - # UI Row 6 (Excel Row 9): Bestand in Kannen-1 (Lit. L-He) - EDITABLE - 5: { - 'L': None, - 'M': None, - 'N': None, - 'O': None, - 'P': None, - 'Q': None, - 'R': None, - }, - - # UI Row 7 (Excel Row 10): Summe Bestand (Lit. L-He) - 6: { - 'L': None, - 'M': None, - 'N': None, - 'O': None, - 'P': None, - 'Q': None, - 'R': None, - }, - - # UI Row 11 (Excel Row 14): Rückführ. Soll (Lit. L-He) - # handled in calculate_top_right_dependents (merged pairs + M3) - 10: { - 'L': None, - 'M': None, - 'N': None, - 'O': None, - 'P': None, - 'Q': None, - 'R': None, - }, - - # UI Row 12 (Excel Row 15): Verluste (Soll-Rückf.) (Lit. L-He) - # handled in calculate_top_right_dependents - 11: { - 'L': None, - 'M': None, - 'N': None, - 'O': None, - 'P': None, - 'Q': None, - 'R': None, - }, - - # UI Row 14 (Excel Row 17): Kaltgas Rückgabe (Lit. L-He) – Faktor - # handled in calculate_top_right_dependents (different formulas for pair 1 vs pair 2 + M3) - 13: { - 'L': None, - 'M': None, - 'N': None, - 'O': None, - 'P': None, - 'Q': None, - 'R': None, - }, - - # UI Row 16 (Excel Row 19): Verbraucherverluste (Liter L-He) - # handled in calculate_top_right_dependents - 15: { - 'L': None, - 'M': None, - 'N': None, - 'O': None, - 'P': None, - 'Q': None, - 'R': None, - }, - - # UI Row 17 (Excel Row 20): % - # handled in calculate_top_right_dependents - 16: { - 'L': None, - 'M': None, - 'N': None, - 'O': None, - 'P': None, - 'Q': None, - 'R': None, - } -}, - 'bottom_1': { - 5: "4 + 3 + 2", - 8: "7 - 6", - }, - 'bottom_2': { - 3: "1 + 2", - 6: "5 - 4", - }, - 'bottom_3': { - 2: "0 + 1", - 5: "3 + 4", - }, - # Special configuration for summation column (last column) - 'summation_column': { - # For each row that should be summed across columns - 'rows_to_sum': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23], # All rows - # OR specify specific rows: - # 'rows_to_sum': [0, 5, 10, 15, 20], # Only specific rows - # The last column index (0-based) - 'sum_column_index': 5, # 6th column (0-5) since you have 6 clients - } -} -def build_halfyear_window(interval_year: int, start_month: int): - """ - Build a list of (year, month) for the 6-month interval, possibly crossing into the next year. - Example: (2025, 10) -> [(2025,10), (2025,11), (2025,12), (2026,1), (2026,2), (2026,3)] - """ - window = [] - for offset in range(6): - total_index = (start_month - 1) + offset # 0-based - y = interval_year + (total_index // 12) - m = (total_index % 12) + 1 - window.append((y, m)) - return window -# --------------------------------------------------------------------------- -# Halbjahres-Bilanz helpers -# --------------------------------------------------------------------------- - -# You can adjust these indices if needed. -# Assuming: -# - bottom_1.table has row "Gasbestand" at some fixed row index, -# and columns: ... Nm³, Lit. LHe -GASBESTAND_ROW_INDEX = 9 # <-- adjust if your bottom_1 has a different row index -GASBESTAND_COL_NM3 = 3 # <-- adjust to the column index for Nm³ in bottom_1 - -# In top_left / top_right, "Bestand in Kannen-1 (Lit. L-He)" is row_index 5 -BESTAND_KANNEN_ROW_INDEX = 5 - -def pick_sheet_by_gasbestand(window, sheets_by_ym, prev_sheet): - """ - Returns the last sheet in the window whose Gasbestand (J36, Nm³ column) != 0. - If none found, returns prev_sheet (Übertrag_Dez__Vorjahr equivalent). - """ - for (y, m) in reversed(window): - sheet = sheets_by_ym.get((y, m)) - if not sheet: - continue - gasbestand_nm3 = get_bottom1_value(sheet, GASBESTAND_ROW_INDEX, GASBESTAND_COL_NM3) - if gasbestand_nm3 != 0: - return sheet - return prev_sheet -def get_bottom1_value(sheet, row_index: int, col_index: int) -> Decimal: - """Get a numeric value from bottom_1, or 0 if missing.""" - if sheet is None: - return Decimal('0') - - cell = Cell.objects.filter( - sheet=sheet, - table_type='bottom_1', - row_index=row_index, - column_index=col_index, - ).first() - - if cell is None or cell.value in (None, ''): - return Decimal('0') - - try: - return Decimal(str(cell.value)) - except Exception: - return Decimal('0') - -# MUST match the column order in your monthly_sheets top-right table - - - -def get_top_right_value(sheet, client_name: str, row_index: int) -> Decimal: - """ - Read a numeric value from the top_right table of a MonthlySheet for - a given client (by column) and row_index. - - top_right cells are keyed by (sheet, table_type='top_right', - row_index, column_index), where column_index is the position of the - client in HALFYEAR_RIGHT_CLIENTS. - """ - if sheet is None: - return Decimal('0') - - col_index = RIGHT_CLIENT_INDEX.get(client_name) - if col_index is None: - return Decimal('0') - - cell = Cell.objects.filter( - sheet=sheet, - table_type='top_right', - row_index=row_index, - column_index=col_index, - ).first() - - if cell is None or cell.value in (None, ''): - return Decimal('0') - - try: - return Decimal(str(cell.value)) - except Exception: - return Decimal('0') - -TR_RUECKF_FLUESSIG_ROW = 2 # confirmed by your March value 172.840560 -TR_BESTAND_KANNEN_ROW = 5 # confirmed by your earlier query -def get_bestand_kannen_for_month(sheet, client_name: str) -> Decimal: - """ - 'B9' in your description: Bestand in Kannen-1 (Lit. L-He) - For this implementation we take it from top_left row_index = 5 for that client. - """ - return get_top_left_value(sheet, client_name, row_index=BESTAND_KANNEN_ROW_INDEX) - -from decimal import Decimal -from django.db.models import Sum -from django.db.models.functions import Coalesce -from django.db.models import DecimalField, Value - -from .models import MonthlySheet, SecondTableEntry, Client, Cell -from django.shortcuts import redirect, render - -# You already have HALFYEAR_CLIENTS for the left table (AG Vogel, AG Halfm, IKP) -HALFYEAR_CLIENTS = ["AG Vogel", "AG Halfm", "IKP"] - -# NEW: clients for the top-right half-year table -HALFYEAR_RIGHT_CLIENTS = [ - "Dr. Fohrer", - "AG Buntk.", - "AG Alff", - "AG Gutfl.", - "M3 Thiele", - "M3 Buntkowsky", - "M3 Gutfleisch", -] -BOTTOM1_COL_VOLUME = 0 -BOTTOM1_COL_BAR = 1 -BOTTOM1_COL_KORR = 2 -BOTTOM1_COL_NM3 = 3 -BOTTOM1_COL_LHE = 4 -BOTTOM2_ROW_ANLAGE = 0 -BOTTOM2_COL_G39 = 0 # "Gefäss 2,5" (cell id shows column_index=0) -BOTTOM2_COL_I39 = 1 # "Gefäss 1,0" (cell id shows column_index=1) -BOTTOM2_ROW_INPUTS = { - "g39": (0, 0), # row_index=0, column_index=0 (your G39) - "i39": (0, 1), # row_index=0, column_index=1 (your I39) -} -FACTOR_NM3_TO_LHE = Decimal("0.75") -RIGHT_CLIENT_INDEX = {name: idx for idx, name in enumerate(HALFYEAR_RIGHT_CLIENTS)} -def halfyear_balance_view(request): - """ - Read-only Halbjahres-Bilanz view. - - LEFT table: AG Vogel / AG Halfm / IKP (exactly as in your last working version) - RIGHT table: Dr. Fohrer / AG Buntk. / AG Alff / AG Gutfl. / - M3 Thiele / M3 Buntkowsky / M3 Gutfleisch - using the Excel formulas you described. - - Uses the global 6-month interval from the main page (clients_list). - """ - # 1) Read half-year interval from the session - interval_year = request.session.get('halfyear_year') - interval_start = request.session.get('halfyear_start_month') - - if not interval_year or not interval_start: - # No interval chosen yet -> redirect to main page - return redirect('clients_list') - - interval_year = int(interval_year) - interval_start = int(interval_start) - - # You already have this helper in your code - window = build_halfyear_window(interval_year, interval_start) - # window = [(y1, m1), (y2, m2), ..., (y6, m6)] - - # (Year, month) of the first month - start_year, start_month = window[0] - - # Previous month (for "Stand ... (Vorjahr)" and "Best. in Kannen Vormonat") - prev_total_index = (start_month - 1) - 1 # one month back, 0-based - if prev_total_index >= 0: - prev_year = start_year + (prev_total_index // 12) - prev_month = (prev_total_index % 12) + 1 - else: - prev_year = start_year - 1 - prev_month = 12 - - # Load MonthlySheet objects for the window and for the previous month - sheets_by_ym = {} - for (y, m) in window: - sheet = MonthlySheet.objects.filter(year=y, month=m).first() - sheets_by_ym[(y, m)] = sheet - - prev_sheet = MonthlySheet.objects.filter(year=prev_year, month=prev_month).first() - def pick_bottom2_from_window(window, sheets_by_ym, prev_sheet): - # choose sheet (same logic you already use) - chosen = None - for (y, m) in reversed(window): - s = sheets_by_ym.get((y, m)) - # use your existing condition for choosing month - if s: - chosen = s - break - if chosen is None: - chosen = prev_sheet - - # Now read the two inputs safely - bottom2_inputs = {} - for key, (row_idx, col_idx) in BOTTOM2_ROW_INPUTS.items(): - bottom2_inputs[key] = get_bottom2_value(chosen, row_idx, col_idx) - - return chosen, bottom2_inputs - - - chosen_sheet_bottom2, bottom2_inputs = pick_bottom2_from_window(window, sheets_by_ym, prev_sheet) - bottom2_g39 = bottom2_inputs["g39"] - bottom2_i39 = bottom2_inputs["i39"] - # ---------------------------- - # HALF-YEAR BOTTOM TABLE 1 (Bilanz) - Read only - # ---------------------------- - chosen_sheet_bottom1 = pick_sheet_by_gasbestand(window, sheets_by_ym, prev_sheet) - - # IMPORTANT: define which bottom_1 row_index corresponds to Excel rows 27..35 - # If your bottom_1 starts at Excel row 27 => row_index 0 == Excel 27 - # then row_index = excel_row - 27 - BOTTOM1_EXCEL_START_ROW = 27 - - bottom1_excel_rows = list(range(27, 37)) # 27..36 - BOTTOM1_LABELS = [ - "Batterie 1", - "2", - "3", - "4", - "5", - "Batterie Links", - "2 Bündel", - "2 Ballone", - "Reingasspeicher", - "Gasbestand", - ] - - BOTTOM1_VOLUMES = [ - Decimal("2.4"), - Decimal("5.1"), - Decimal("4.0"), - Decimal("1.0"), - Decimal("4.0"), - Decimal("0.6"), - Decimal("1.2"), - Decimal("20.0"), - Decimal("5.0"), - None, # Gasbestand row has no volume - ] - nm3_sum_27_35 = Decimal("0") - lhe_sum_27_35 = Decimal("0") - bottom1_rows = [] - - for excel_row in bottom1_excel_rows: - row_index = excel_row - BOTTOM1_EXCEL_START_ROW - - chosen_sheet_bottom1 = None - for (y, m) in reversed(window): - s = sheets_by_ym.get((y, m)) - gasbestand = get_bottom1_value(s, GASBESTAND_ROW_INDEX, GASBESTAND_COL_NM3) # J36 (Nm3) - if gasbestand != 0: - chosen_sheet_bottom1 = s - break - - if chosen_sheet_bottom1 is None: - chosen_sheet_bottom1 = prev_sheet - - # Normal rows (27..35): read from chosen sheet and accumulate sums - if excel_row != 36: - nm3_val = get_bottom1_value(chosen_sheet_bottom1, row_index, BOTTOM1_COL_NM3) - lhe_val = get_bottom1_value(chosen_sheet_bottom1, row_index, BOTTOM1_COL_LHE) - - nm3_sum_27_35 += nm3_val - lhe_sum_27_35 += lhe_val - - bottom1_rows.append({ - "label": BOTTOM1_LABELS[row_index], - "volume": BOTTOM1_VOLUMES[row_index], - "bar": get_bottom1_value(chosen_sheet_bottom1, row_index, BOTTOM1_COL_BAR), - "korr": get_bottom1_value(chosen_sheet_bottom1, row_index, BOTTOM1_COL_KORR), - "nm3": nm3_val, - "lhe": lhe_val, - }) - - # Gasbestand row (36): show sums (J36 = SUM(J27:J35), K36 = SUM(K27:K35)) - else: - bottom1_rows.append({ - "label": "Gasbestand", - "volume": "", - "bar": "", - "korr": "", - "nm3": nm3_sum_27_35, - "lhe": lhe_sum_27_35, - }) - start_sheet = sheets_by_ym.get((start_year, start_month)) - # ------------------------------------------------------------ - # Bottom Table 2 (Halbjahres Bilanz) – server-side recalcBottom2() - # ------------------------------------------------------------ - - FACTOR_BT2 = Decimal("0.75") - - # 1) Helper: pick last-nonzero value of bottom_2 row0 col0/col1 from the window (fallback: prev_sheet) - def pick_last_nonzero_bottom2(row_index: int, col_index: int) -> Decimal: - # Scan from last month in window backwards - for (y, m) in reversed(window): - s = sheets_by_ym.get((y, m)) - if not s: - continue - v = get_bottom2_value(s, row_index, col_index) - if v is not None and v != 0: - return v - # fallback to month before window - v_prev = get_bottom2_value(prev_sheet, row_index, col_index) - return v_prev if v_prev is not None else Decimal("0") - - # 2) K38 comes from Overall Summary: "Summe Bestand (Lit. L-He)" - # Find it from your already built overall summary rows list. - - k38 = Decimal("0") - j38 = Decimal("0") - # 3) Inputs G39 / I39 (picked from last non-zero month in window) - g39 = pick_last_nonzero_bottom2(row_index=0, col_index=0) # G39 - i39 = pick_last_nonzero_bottom2(row_index=0, col_index=1) # I39 - - k39 = (g39 or Decimal("0")) + (i39 or Decimal("0")) - j39 = k39 * FACTOR_BT2 - - # 4) +Kaltgas (row 40) - # JS: - # g40 = (2500 - g39)/100*10 - # i40 = (1000 - i39)/100*10 - g40 = None - i40 = None - if g39 is not None: - g40 = (Decimal("2500") - g39) / Decimal("100") * Decimal("10") - if i39 is not None: - i40 = (Decimal("1000") - i39) / Decimal("100") * Decimal("10") - - k40 = (g40 or Decimal("0")) + (i40 or Decimal("0")) - j40 = k40 * FACTOR_BT2 - - # 5) Bestand flüssig He (row 43) - k43 = ( - (k38 or Decimal("0")) + - (k39 or Decimal("0")) + - (k40 or Decimal("0")) - ) - j43 = k43 * FACTOR_BT2 - - # 6) Gesamtbestand neu (row 44) = Gasbestand(Lit) from Bottom Table 1 + k43 - gasbestand_lit = Decimal("0") - for r in bottom1_rows: - if (r.get("label") or "").strip().startswith("Gasbestand"): - gasbestand_lit = r.get("lhe") or Decimal("0") - break - - k44 = (gasbestand_lit or Decimal("0")) + (k43 or Decimal("0")) - j44 = k44 * FACTOR_BT2 - - bottom2 = { - "j38": j38, "k38": k38, - "g39": g39, "i39": i39, "j39": j39, "k39": k39, - "g40": g40, "i40": i40, "j40": j40, "k40": k40, - "j43": j43, "k43": k43, - "j44": j44, "k44": k44, - } - - # ------------------------------------------------------------------ - # 2) LEFT TABLE (your existing, working logic) - # ------------------------------------------------------------------ - HALFYEAR_CLIENTS_LEFT = ["AG Vogel", "AG Halfm", "IKP"] - - # We'll collect client-wise values first for clarity. - client_data_left = {name: {} for name in HALFYEAR_CLIENTS_LEFT} - - # --- Row B3: Stand der Gaszähler (Nm³) - # = MAX(B3 from previous month, and B3 from each of the 6 months in the window) - # row_index 0 in top_left = "Stand der Gaszähler (Nm³)" - months_for_max = [(prev_year, prev_month)] + window - - for cname in HALFYEAR_CLIENTS_LEFT: - max_val = Decimal('0') - for (y, m) in months_for_max: - sheet = sheets_by_ym.get((y, m)) - if sheet is None and (y, m) == (prev_year, prev_month): - sheet = prev_sheet - val_b3 = get_top_left_value(sheet, cname, row_index=0) - if val_b3 > max_val: - max_val = val_b3 - client_data_left[cname]['stand_gas'] = max_val - - # --- Row B4: Stand der Gaszähler (Vorjahr) (Nm³) -> previous month same row --- - for cname in HALFYEAR_CLIENTS_LEFT: - val_b4 = get_top_left_value(prev_sheet, cname, row_index=0) - client_data_left[cname]['stand_gas_prev'] = val_b4 - - # --- Row B5: Gasrückführung (Nm³) = B3 - B4 --- - for cname in HALFYEAR_CLIENTS_LEFT: - b3 = client_data_left[cname]['stand_gas'] - b4 = client_data_left[cname]['stand_gas_prev'] - client_data_left[cname]['gasrueckf'] = b3 - b4 - - # --- Row B6: Rückführung flüssig (Lit. L-He) = B5 / 0.75 --- - for cname in HALFYEAR_CLIENTS_LEFT: - b5 = client_data_left[cname]['gasrueckf'] - client_data_left[cname]['rueckf_fluessig'] = (b5 / Decimal('0.75')) if b5 != 0 else Decimal('0') - - # --- Row B7: Sonderrückführungen (Lit. L-He) = sum over 6 months of that row --- - # That row index is 4 in your top_left table. - for cname in HALFYEAR_CLIENTS_LEFT: - sonder_total = Decimal('0') - for (y, m) in window: - sheet = sheets_by_ym.get((y, m)) - if sheet: - sonder_total += get_top_left_value(sheet, cname, row_index=4) - client_data_left[cname]['sonder'] = sonder_total - - # --- Row B8: Bestand in Kannen-1 (Lit. L-He) --- - # Excel-style logic with Gasbestand (J36) and fallback to previous month. - for cname in HALFYEAR_CLIENTS_LEFT: - chosen_value = None - - # Go from last month (window[5]) backwards to first (window[0]) - for (y, m) in reversed(window): - sheet = sheets_by_ym.get((y, m)) - gasbestand = get_bottom1_value(sheet, GASBESTAND_ROW_INDEX, GASBESTAND_COL_NM3) - if gasbestand != 0: - chosen_value = get_bestand_kannen_for_month(sheet, cname) - break - - # If still None -> use previous month (Übertrag_Dez__Vorjahr equivalent) - if chosen_value is None: - sheet_prev = prev_sheet - chosen_value = get_bestand_kannen_for_month(sheet_prev, cname) - - client_data_left[cname]['bestand_kannen'] = chosen_value if chosen_value is not None else Decimal('0') - - # --- Row B9: Summe Bestand (Lit. L-He) = equal to previous row --- - for cname in HALFYEAR_CLIENTS_LEFT: - client_data_left[cname]['summe_bestand'] = client_data_left[cname]['bestand_kannen'] - - # --- Row B10: Best. in Kannen Vormonat (Lit. L-He) - # = Bestand in Kannen-1 from the month BEFORE the window (prev_year, prev_month) - for cname in HALFYEAR_CLIENTS_LEFT: - client_data_left[cname]['best_kannen_vormonat'] = get_bestand_kannen_for_month(prev_sheet, cname) - - # --- Row B13: Bezug (Liter L-He) --- - for cname in HALFYEAR_CLIENTS_LEFT: - total_bezug = Decimal('0') - for (y, m) in window: - qs = SecondTableEntry.objects.filter( - client__name=cname, - date__year=y, - date__month=m, - ).aggregate( - total=Coalesce(Sum('lhe_output'), Value(0, output_field=DecimalField())) - ) - total_bezug += Decimal(str(qs['total'])) - client_data_left[cname]['bezug'] = total_bezug - - # --- Row B14: Rückführ. Soll (Lit. L-He) = Bezug - Summe Bestand + Best. in Kannen Vormonat --- - for cname in HALFYEAR_CLIENTS_LEFT: - b13 = client_data_left[cname]['bezug'] - b11 = client_data_left[cname]['summe_bestand'] - b12 = client_data_left[cname]['best_kannen_vormonat'] - client_data_left[cname]['rueckf_soll'] = b13 - b11 + b12 - - # --- Row B15: Verluste (Soll-Rückf.) (Lit. L-He) = B14 - B6 --- - for cname in HALFYEAR_CLIENTS_LEFT: - b14 = client_data_left[cname]['rueckf_soll'] - b6 = client_data_left[cname]['rueckf_fluessig'] - client_data_left[cname]['verluste'] = b14 - b6 - - # --- Row B16: Füllungen warm (Lit. L-He) = sum over 6 months (row_index=11) --- - for cname in HALFYEAR_CLIENTS_LEFT: - total_warm = Decimal('0') - for (y, m) in window: - sheet = sheets_by_ym.get((y, m)) - total_warm += get_top_left_value(sheet, cname, row_index=11) - client_data_left[cname]['fuellungen_warm'] = total_warm - - # --- Row B17: Kaltgas Rückgabe (Lit. L-He) = Bezug * 0.06 --- - factor = Decimal('0.06') - for cname in HALFYEAR_CLIENTS_LEFT: - b13 = client_data_left[cname]['bezug'] - client_data_left[cname]['kaltgas_rueckgabe'] = b13 * factor - - # --- Row B18: Verbraucherverluste (Liter L-He) = Verluste - Kaltgas Rückgabe --- - for cname in HALFYEAR_CLIENTS_LEFT: - b15 = client_data_left[cname]['verluste'] - b17 = client_data_left[cname]['kaltgas_rueckgabe'] - client_data_left[cname]['verbraucherverluste'] = b15 - b17 - - # --- Row B19: % = Verbraucherverluste / Bezug --- - for cname in HALFYEAR_CLIENTS_LEFT: - bezug = client_data_left[cname]['bezug'] - verb = client_data_left[cname]['verbraucherverluste'] - if bezug != 0: - client_data_left[cname]['percent'] = verb / bezug - else: - client_data_left[cname]['percent'] = None - - # Build LEFT rows structure - left_row_defs = [ - ('Stand der Gaszähler (Nm³)', 'stand_gas'), - ('Stand der Gaszähler (Vorjahr) (Nm³)', 'stand_gas_prev'), - ('Gasrückführung (Nm³)', 'gasrueckf'), - ('Rückführung flüssig (Lit. L-He)', 'rueckf_fluessig'), - ('Sonderrückführungen (Lit. L-He)', 'sonder'), - ('Bestand in Kannen-1 (Lit. L-He)', 'bestand_kannen'), - ('Summe Bestand (Lit. L-He)', 'summe_bestand'), - ('Best. in Kannen Vormonat (Lit. L-He)', 'best_kannen_vormonat'), - ('Bezug (Liter L-He)', 'bezug'), - ('Rückführ. Soll (Lit. L-He)', 'rueckf_soll'), - ('Verluste (Soll-Rückf.) (Lit. L-He)', 'verluste'), - ('Füllungen warm (Lit. L-He)', 'fuellungen_warm'), - ('Kaltgas Rückgabe (Lit. L-He) – Faktor', 'kaltgas_rueckgabe'), - ('Verbraucherverluste (Liter L-He)', 'verbraucherverluste'), - ('%', 'percent'), - ] - - rows_left = [] - for label, key in left_row_defs: - values = [client_data_left[cname][key] for cname in HALFYEAR_CLIENTS_LEFT] - if key == 'percent': - total_bezug = sum((client_data_left[c]['bezug'] for c in HALFYEAR_CLIENTS_LEFT), Decimal('0')) - total_verb = sum((client_data_left[c]['verbraucherverluste'] for c in HALFYEAR_CLIENTS_LEFT), Decimal('0')) - total = (total_verb / total_bezug) if total_bezug != 0 else None - else: - total = sum((v for v in values if v is not None), Decimal('0')) - rows_left.append({ - 'label': label, - 'values': values, - 'total': total, - 'is_percent': key == 'percent', - }) - - # ------------------------------------------------------------------ - # 3) RIGHT TABLE (top-right half-year aggregation) - # ------------------------------------------------------------------ - RIGHT_CLIENTS = HALFYEAR_RIGHT_CLIENTS # for brevity - - right_data = {name: {} for name in RIGHT_CLIENTS} - - # --- Bezug (Liter L-He) for each right client (same as for left) --- - for cname in RIGHT_CLIENTS: - total_bezug = Decimal('0') - for (y, m) in window: - qs = SecondTableEntry.objects.filter( - client__name=cname, - date__year=y, - date__month=m, - ).aggregate( - total=Coalesce(Sum('lhe_output'), Value(0, output_field=DecimalField())) - ) - total_bezug += Decimal(str(qs['total'])) - right_data[cname]['bezug'] = total_bezug - def find_bestand_from_window(reference_client: str) -> Decimal: - """ - Implements: - WENN(last_month!J36=0; WENN(prev_month!J36=0; ...; prev_sheet!9); last_month!9) - reference_client decides which column (L/N/P/Q/R) we read from monthly top_right row_index=5. - """ - # scan backward through window - for (y, m) in reversed(window): - sh = sheets_by_ym.get((y, m)) - if not sh: - continue - gasbestand = get_bottom1_value(sh, GASBESTAND_ROW_INDEX, GASBESTAND_COL_NM3) - if gasbestand != 0: - return get_top_right_value(sh, reference_client, TR_BESTAND_KANNEN_ROW) - - # fallback to previous month (Übertrag_Dez__Vorjahr equivalent) - return get_top_right_value(prev_sheet, reference_client, TR_BESTAND_KANNEN_ROW) - - # Fohrer+Buntk merged: BOTH use Fohrer column (L9) - val_L = find_bestand_from_window("Dr. Fohrer") - right_data["Dr. Fohrer"]["bestand_kannen"] = val_L - right_data["AG Buntk."]["bestand_kannen"] = val_L - - # Alff+Gutfl merged: BOTH use Alff column (N9) - val_N = find_bestand_from_window("AG Alff") - right_data["AG Alff"]["bestand_kannen"] = val_N - right_data["AG Gutfl."]["bestand_kannen"] = val_N - - # M3 each uses its own column (P9/Q9/R9) - right_data["M3 Thiele"]["bestand_kannen"] = find_bestand_from_window("M3 Thiele") - right_data["M3 Buntkowsky"]["bestand_kannen"] = find_bestand_from_window("M3 Buntkowsky") - right_data["M3 Gutfleisch"]["bestand_kannen"] = find_bestand_from_window("M3 Gutfleisch") - # Helper for pair shares (L13/($L13+$M13), etc.) - def pair_share(c1, c2): - total = right_data[c1]['bezug'] + right_data[c2]['bezug'] - if total == 0: - return (Decimal('0'), Decimal('0')) - return ( - right_data[c1]['bezug'] / total, - right_data[c2]['bezug'] / total, - ) - - # --- "Stand der Gaszähler (Vorjahr) (Nm³)" row: share based on Bezug --- - # Dr. Fohrer / AG Buntk. - s_fohrer, s_buntk = pair_share("Dr. Fohrer", "AG Buntk.") - right_data["Dr. Fohrer"]['stand_prev_share'] = s_fohrer - right_data["AG Buntk."]['stand_prev_share'] = s_buntk - - # AG Alff / AG Gutfl. - s_alff, s_gutfl = pair_share("AG Alff", "AG Gutfl.") - right_data["AG Alff"]['stand_prev_share'] = s_alff - right_data["AG Gutfl."]['stand_prev_share'] = s_gutfl - - # M3 Thiele / M3 Buntkowsky / M3 Gutfleisch → empty in Excel → None - for cname in ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"]: - right_data[cname]['stand_prev_share'] = None - - # --- Rückführung flüssig per month (raw sums) --- - # top_right row_index=2 is "Rückführung flüssig (Lit. L-He)" - - # --- Sonderrückführungen (row_index=3 in top_right) --- - for cname in RIGHT_CLIENTS: - sonder_total = Decimal('0') - for (y, m) in window: - sheet = sheets_by_ym.get((y, m)) - if sheet: - sonder_total += get_top_right_value(sheet, cname, row_index=3) - right_data[cname]['sonder'] = sonder_total - - # --- Sammelrückführung (row_index=4 in top_right), grouped & merged --- - # Group 1: Dr. Fohrer + AG Buntk. - group1_total = Decimal('0') - for (y, m) in window: - sheet = sheets_by_ym.get((y, m)) - if sheet: - group1_total += get_top_right_value(sheet, "Dr. Fohrer", row_index=4) - right_data["Dr. Fohrer"]['sammel'] = group1_total - right_data["AG Buntk."]['sammel'] = group1_total - - # Group 2: AG Alff + AG Gutfl. - group2_total = Decimal('0') - for (y, m) in window: - sheet = sheets_by_ym.get((y, m)) - if sheet: - group2_total += get_top_right_value(sheet, "AG Alff", row_index=4) - right_data["AG Alff"]['sammel'] = group2_total - right_data["AG Gutfl."]['sammel'] = group2_total - - # Group 3: M3 Thiele + M3 Buntkowsky + M3 Gutfleisch - group3_total = Decimal('0') - for (y, m) in window: - sheet = sheets_by_ym.get((y, m)) - if sheet: - group3_total += get_top_right_value(sheet, "M3 Thiele", row_index=4) - right_data["M3 Thiele"]['sammel'] = group3_total - right_data["M3 Buntkowsky"]['sammel'] = group3_total - right_data["M3 Gutfleisch"]['sammel'] = group3_total - def safe_div(a: Decimal, b: Decimal) -> Decimal: - return (a / b) if b != 0 else Decimal("0") - - # --- Rückführung flüssig (Lit. L-He) for Halbjahres-Bilanz top-right --- - # Uses your exact formulas. - - # 1) Fohrer / Buntk split by BEZUG share times group SAMMEL (L8) - L13 = right_data["Dr. Fohrer"]["bezug"] - M13 = right_data["AG Buntk."]["bezug"] - L8 = right_data["Dr. Fohrer"]["sammel"] # merged group total - - den = (L13 + M13) - right_data["Dr. Fohrer"]["rueckf_fluessig"] = (safe_div(L13, den) * L8) if den != 0 else Decimal("0") - right_data["AG Buntk."]["rueckf_fluessig"] = (safe_div(M13, den) * L8) if den != 0 else Decimal("0") - - # 2) Alff / Gutfl split by BEZUG share times group SAMMEL (N8) - N13 = right_data["AG Alff"]["bezug"] - O13 = right_data["AG Gutfl."]["bezug"] - N8 = right_data["AG Alff"]["sammel"] # merged group total - - den = (N13 + O13) - right_data["AG Alff"]["rueckf_fluessig"] = (safe_div(N13, den) * N8) if den != 0 else Decimal("0") - right_data["AG Gutfl."]["rueckf_fluessig"] = (safe_div(O13, den) * N8) if den != 0 else Decimal("0") - - # 3) M3 Thiele = sum of monthly Rückführung flüssig (monthly top_right row_index=2) over window - P6_sum = Decimal("0") - for (y, m) in window: - sh = sheets_by_ym.get((y, m)) - P6_sum += get_top_right_value(sh, "M3 Thiele", TR_RUECKF_FLUESSIG_ROW) - right_data["M3 Thiele"]["rueckf_fluessig"] = P6_sum - - # 4) M3 Buntkowsky / M3 Gutfleisch split by BEZUG share times M3-group SAMMEL (P8) - P13 = right_data["M3 Thiele"]["bezug"] - Q13 = right_data["M3 Buntkowsky"]["bezug"] - R13 = right_data["M3 Gutfleisch"]["bezug"] - P8 = right_data["M3 Thiele"]["sammel"] # merged group total - - den = (P13 + Q13 + R13) - right_data["M3 Buntkowsky"]["rueckf_fluessig"] = (safe_div(Q13, den) * P8) if den != 0 else Decimal("0") - right_data["M3 Gutfleisch"]["rueckf_fluessig"] = (safe_div(R13, den) * P8) if den != 0 else Decimal("0") - # --- Bestand in Kannen-1 (Lit. L-He) for right table (grouped) --- - # Use Gasbestand (J36) and fallback logic, but now reading top_right B9 for each group. - TOP_RIGHT_ROW_BESTAND_KANNEN = 6 # <-- most likely correct in your setup - - def pick_bestand_top_right(base_client: str) -> Decimal: - # Go from last month in window backwards: if Gasbestand != 0, use that month's Bestand in Kannen - for (y, m) in reversed(window): - sh = sheets_by_ym.get((y, m)) - if not sh: - continue - - gasbestand = get_bottom1_value(sh, GASBESTAND_ROW_INDEX, GASBESTAND_COL_NM3) - if gasbestand != 0: - return get_top_right_value(sh, base_client, TOP_RIGHT_ROW_BESTAND_KANNEN) - - # Fallback to previous month (Übertrag_Dez__Vorjahr equivalent) - return get_top_right_value(prev_sheet, base_client, TOP_RIGHT_ROW_BESTAND_KANNEN) - - # Group 1 merged (Fohrer + Buntk.) - g1_best = pick_bestand_top_right("Dr. Fohrer") - right_data["Dr. Fohrer"]["bestand_kannen"] = g1_best - right_data["AG Buntk."]["bestand_kannen"] = g1_best - - # Group 2 merged (Alff + Gutfl.) - g2_best = pick_bestand_top_right("AG Alff") - right_data["AG Alff"]["bestand_kannen"] = g2_best - right_data["AG Gutfl."]["bestand_kannen"] = g2_best - - # Group 3 merged (M3 Thiele + M3 Buntkowsky + M3 Gutfleisch) - g3_best = pick_bestand_top_right("M3 Thiele") - right_data["M3 Thiele"]["bestand_kannen"] = g3_best - right_data["M3 Buntkowsky"]["bestand_kannen"] = g3_best - right_data["M3 Gutfleisch"]["bestand_kannen"] = g3_best - - # Summe Bestand = same as previous row - for cname in RIGHT_CLIENTS: - right_data[cname]['summe_bestand'] = right_data[cname]['bestand_kannen'] - - # Best. in Kannen Vormonat (Lit. L-He) from previous month top_right row_index=7 - g1_prev = get_top_right_value(prev_sheet, "Dr. Fohrer", TOP_RIGHT_ROW_BESTAND_KANNEN) - right_data["Dr. Fohrer"]['best_kannen_vormonat'] = g1_prev - right_data["AG Buntk."]['best_kannen_vormonat'] = g1_prev - - g2_prev = get_top_right_value(prev_sheet, "AG Alff", TOP_RIGHT_ROW_BESTAND_KANNEN) - right_data["AG Alff"]['best_kannen_vormonat'] = g2_prev - right_data["AG Gutfl."]['best_kannen_vormonat'] = g2_prev - - - # Group 1 merged (Fohrer + Buntk.) - g1_prev = get_top_right_value(prev_sheet, "Dr. Fohrer", TOP_RIGHT_ROW_BESTAND_KANNEN) - right_data["Dr. Fohrer"]["best_kannen_vormonat"] = g1_prev - right_data["AG Buntk."]["best_kannen_vormonat"] = g1_prev - - # Group 2 merged (Alff + Gutfl.) - g2_prev = get_top_right_value(prev_sheet, "AG Alff", TOP_RIGHT_ROW_BESTAND_KANNEN) - right_data["AG Alff"]["best_kannen_vormonat"] = g2_prev - right_data["AG Gutfl."]["best_kannen_vormonat"] = g2_prev - - # Group 3 UNMERGED (each one reads its own cell) - right_data["M3 Thiele"]["best_kannen_vormonat"] = get_top_right_value(prev_sheet, "M3 Thiele", TOP_RIGHT_ROW_BESTAND_KANNEN) - right_data["M3 Buntkowsky"]["best_kannen_vormonat"] = get_top_right_value(prev_sheet, "M3 Buntkowsky", TOP_RIGHT_ROW_BESTAND_KANNEN) - right_data["M3 Gutfleisch"]["best_kannen_vormonat"] = get_top_right_value(prev_sheet, "M3 Gutfleisch", TOP_RIGHT_ROW_BESTAND_KANNEN) - - # --- Rückführ. Soll (Lit. L-He) according to your formulas --- - - # Group 1: Dr. Fohrer / AG Buntk. - total_bestand_1 = right_data["Dr. Fohrer"]['summe_bestand'] - best_vormonat_1 = right_data["Dr. Fohrer"]['best_kannen_vormonat'] - diff1 = total_bestand_1 - best_vormonat_1 - share_fohrer = right_data["Dr. Fohrer"]['stand_prev_share'] or Decimal('0') - - right_data["Dr. Fohrer"]['rueckf_soll'] = ( - right_data["Dr. Fohrer"]['bezug'] - diff1 * share_fohrer - ) - right_data["AG Buntk."]['rueckf_soll'] = ( - right_data["AG Buntk."]['bezug'] - total_bestand_1 + best_vormonat_1 - ) - - # Group 2: AG Alff / AG Gutfl. - total_bestand_2 = right_data["AG Alff"]['summe_bestand'] - best_vormonat_2 = right_data["AG Alff"]['best_kannen_vormonat'] - diff2 = total_bestand_2 - best_vormonat_2 - share_alff = right_data["AG Alff"]['stand_prev_share'] or Decimal('0') - share_gutfl = right_data["AG Gutfl."]['stand_prev_share'] or Decimal('0') - - right_data["AG Alff"]['rueckf_soll'] = ( - right_data["AG Alff"]['bezug'] - diff2 * share_alff - ) - right_data["AG Gutfl."]['rueckf_soll'] = ( - right_data["AG Gutfl."]['bezug'] - diff2 * share_gutfl - ) - - # Group 3: M3 Thiele / M3 Buntkowsky / M3 Gutfleisch - for cname in ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"]: - b13 = right_data[cname]['bezug'] - b12 = right_data[cname]['best_kannen_vormonat'] - b11 = right_data[cname]['summe_bestand'] - # Excel: P13+P12-P11 etc. - right_data[cname]['rueckf_soll'] = b13 + b12 - b11 - - # --- Verluste (Soll-Rückf.) (Lit. L-He) = B14 - B6 - B7 --- - for cname in RIGHT_CLIENTS: - b14 = right_data[cname]['rueckf_soll'] - b6 = right_data[cname]['rueckf_fluessig'] - b7 = right_data[cname]['sonder'] - right_data[cname]['verluste'] = b14 - b6 - b7 - - # --- Füllungen warm (Lit. L-He) = sum of monthly 'Füllungen warm' (row_index=11 top_right) --- - for cname in RIGHT_CLIENTS: - total_warm = Decimal('0') - for (y, m) in window: - sheet = sheets_by_ym.get((y, m)) - if sheet: - total_warm += get_top_right_value(sheet, cname, row_index=11) - right_data[cname]['fuellungen_warm'] = total_warm - - # --- Kaltgas Rückgabe (Lit. L-He) – Faktor = Bezug * 0.06 --- - for cname in RIGHT_CLIENTS: - b13 = right_data[cname]['bezug'] - right_data[cname]['kaltgas_rueckgabe'] = b13 * factor - - # --- Verbraucherverluste (Liter L-He) = Verluste - Kaltgas Rückgabe --- - for cname in RIGHT_CLIENTS: - b15 = right_data[cname]['verluste'] - b17 = right_data[cname]['kaltgas_rueckgabe'] - right_data[cname]['verbraucherverluste'] = b15 - b17 - - # --- % = Verbraucherverluste / Bezug --- - for cname in RIGHT_CLIENTS: - bezug = right_data[cname]['bezug'] - verb = right_data[cname]['verbraucherverluste'] - if bezug != 0: - right_data[cname]['percent'] = verb / bezug - else: - right_data[cname]['percent'] = None - - # Build RIGHT rows structure - right_row_defs = [ - ('Stand der Gaszähler (Vorjahr) (Nm³)', 'stand_prev_share'), - # We skip the pure-text "Gasrückführung (Nm³)" line here, - # because it’s only text (Aufteilung nach Verbrauch / Gaszähler) - # and easier to render directly in the template if needed. - ('Rückführung flüssig (Lit. L-He)', 'rueckf_fluessig'), - ('Sonderrückführungen (Lit. L-He)', 'sonder'), - ('Sammelrückführung (Lit. L-He)', 'sammel'), - ('Bestand in Kannen-1 (Lit. L-He)', 'bestand_kannen'), - ('Summe Bestand (Lit. L-He)', 'summe_bestand'), - ('Best. in Kannen Vormonat (Lit. L-He)', 'best_kannen_vormonat'), - ('Bezug (Liter L-He)', 'bezug'), - ('Rückführ. Soll (Lit. L-He)', 'rueckf_soll'), - ('Verluste (Soll-Rückf.) (Lit. L-He)', 'verluste'), - ('Füllungen warm (Lit. L-He)', 'fuellungen_warm'), - ('Kaltgas Rückgabe (Lit. L-He) – Faktor', 'kaltgas_rueckgabe'), - ('Verbraucherverluste (Liter L-He)', 'verbraucherverluste'), - ('%', 'percent'), - ] - - rows_right = [] - for label, key in right_row_defs: - values = [right_data[cname].get(key) for cname in RIGHT_CLIENTS] - - if key == 'percent': - total_bezug = sum((right_data[c]['bezug'] for c in RIGHT_CLIENTS), Decimal('0')) - total_verb = sum((right_data[c]['verbraucherverluste'] for c in RIGHT_CLIENTS), Decimal('0')) - total = (total_verb / total_bezug) if total_bezug != 0 else None - else: - total = sum((v for v in values if isinstance(v, Decimal)), Decimal('0')) - - rows_right.append({ - 'label': label, - 'values': values, - 'total': total, - 'is_percent': key == 'percent', - }) - SUM_TABLE_ROWS = [ - ("Rückführung flüssig (Lit. L-He)", "rueckf_fluessig"), - ("Sonderrückführungen (Lit. L-He)", "sonder"), - ("Sammelrückführungen (Lit. L-He)", "sammel"), - ("Bestand in Kannen-1 (Lit. L-He)", "bestand_kannen"), - ("Summe Bestand (Lit. L-He)", "summe_bestand"), - ("Best. in Kannen Vormonat (Lit. L-He)", "best_kannen_vormonat"), - ("Bezug (Liter L-He)", "bezug"), - ("Rückführ. Soll (Lit. L-He)", "rueckf_soll"), - ("Verluste (Soll-Rückf.) (Lit. L-He)", "verluste"), - ("Füllungen warm (Lit. L-He)", "fuellungen_warm"), - ("Kaltgas Rückgabe (Lit. L-He) – Faktor", "kaltgas_rueckgabe"), - ("Faktor 0.06", "factor_row"), - ("Verbraucherverluste (Liter L-He)", "verbraucherverluste"), - ("%", "percent"), - ] - - RIGHT_GROUPS = { - "chemie": ["Dr. Fohrer", "AG Buntk."], - "mawi": ["AG Alff", "AG Gutfl."], - "m3": ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"], - } - - RIGHT_ALL = ["Dr. Fohrer", "AG Buntk.", "AG Alff", "AG Gutfl.", "M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"] - LEFT_ALL = HALFYEAR_CLIENTS_LEFT - - def safe_pct(verb, bez): - return (verb / bez) if bez != 0 else None - rows_sum = [] - def d(x): - return x if isinstance(x, Decimal) else Decimal("0") - for label, key in SUM_TABLE_ROWS: - - if key == "factor_row": - lichtwiese = chemie = mawi = m3 = total = Decimal("0.06") - - elif key == "percent": - # Right totals - rw_bez = sum(d(right_data[c].get("bezug")) for c in RIGHT_ALL) - rw_verb = sum(d(right_data[c].get("verbraucherverluste")) for c in RIGHT_ALL) - lichtwiese = safe_pct(rw_verb, rw_bez) - - # Chemie - ch_bez = sum(d(right_data[c].get("bezug")) for c in RIGHT_GROUPS["chemie"]) - ch_verb = sum(d(right_data[c].get("verbraucherverluste")) for c in RIGHT_GROUPS["chemie"]) - chemie = safe_pct(ch_verb, ch_bez) - - # MaWi - mw_bez = sum(d(right_data[c].get("bezug")) for c in RIGHT_GROUPS["mawi"]) - mw_verb = sum(d(right_data[c].get("verbraucherverluste")) for c in RIGHT_GROUPS["mawi"]) - mawi = safe_pct(mw_verb, mw_bez) - - # M3 - m3_bez = sum(d(right_data[c].get("bezug")) for c in RIGHT_GROUPS["m3"]) - m3_verb = sum(d(right_data[c].get("verbraucherverluste")) for c in RIGHT_GROUPS["m3"]) - m3 = safe_pct(m3_verb, m3_bez) - - # Σ column = (left verb + right verb) / (left bez + right bez) - left_bez = sum(d(client_data_left[c].get("bezug")) for c in LEFT_ALL) - left_verb = sum(d(client_data_left[c].get("verbraucherverluste")) for c in LEFT_ALL) - total = safe_pct(left_verb + rw_verb, left_bez + rw_bez) - - else: - # normal rows = sums - lichtwiese = sum(d(right_data[c].get(key)) for c in RIGHT_ALL) - chemie = sum(d(right_data[c].get(key)) for c in RIGHT_GROUPS["chemie"]) - mawi = sum(d(right_data[c].get(key)) for c in RIGHT_GROUPS["mawi"]) - m3 = sum(d(right_data[c].get(key)) for c in RIGHT_GROUPS["m3"]) - - left_total = sum(d(client_data_left[c].get(key)) for c in LEFT_ALL) - total = left_total + lichtwiese - - rows_sum.append({ - "row_index": row_index, - "label": label, - "total": total, - "lichtwiese": lichtwiese, - "chemie": chemie, - "mawi": mawi, - "m3": m3, - "is_percent": (key == "percent"), - }) - def find_sum_row(rows, label_startswith: str): - for r in rows: - if str(r.get("label", "")).strip().startswith(label_startswith): - return r - return None - - summe_bestand_row = find_sum_row(rows_sum, "Summe Bestand") - k38 = (summe_bestand_row.get("total") if summe_bestand_row else Decimal("0")) or Decimal("0") - j38 = k38 * Decimal("0.75") - # --- FIX: now that k38 is known, update bottom2 + recompute dependent rows --- - bottom2["k38"] = k38 - bottom2["j38"] = j38 - - k39 = bottom2.get("k39") or Decimal("0") - k40 = bottom2.get("k40") or Decimal("0") - - # Row 43: Bestand flüssig He = SUMME(K38:K40) - k43 = (k38 or Decimal("0")) + k39 + k40 - j43 = k43 * Decimal("0.75") - - bottom2["k43"] = k43 - bottom2["j43"] = j43 - - # Row 44: Gesamtbestand neu = Gasbestand(Lit) from bottom table 1 + k43 - gasbestand_lit = Decimal("0") - for r in bottom1_rows: - if (r.get("label") or "").strip().startswith("Gasbestand"): - gasbestand_lit = r.get("lhe") or Decimal("0") - break - - k44 = gasbestand_lit + k43 - j44 = k44 * Decimal("0.75") - - bottom2["k44"] = k44 - bottom2["j44"] = j44 - def d(x): - return x if isinstance(x, Decimal) else Decimal("0") - - # ---- Bottom2: J38/K38 depend on rows_sum (overall summary), so do it HERE ---- - k38 = Decimal("0") - for r in rows_sum: - if r.get("label") == "Summe Bestand (Lit. L-He)": - k38 = r.get("total") or Decimal("0") - break - - j38 = k38 * FACTOR_NM3_TO_LHE # 0.75 - bottom2["k38"] = k38 - bottom2["j38"] = j38 - factor = Decimal("0.75") - - # window = the 6-month list you already build in this view: [(y,m), (y,m), ...] - # bottom2 = dict with "k44" already computed in your view - # rows_sum = overall sum group rows list (your existing halfyear logic) - - # 1) K46 = K44 from the month BEFORE the global month (interval start) - start_year = interval_year - start_month = interval_start # whatever you named your start month variable - - if start_month == 1: - prev_year, prev_month = start_year - 1, 12 - else: - prev_year, prev_month = start_year, start_month - 1 - - prev_sheet = MonthlySheet.objects.filter(year=prev_year, month=prev_month).first() - - # This assumes you have MonthlySummary.gesamtbestand_neu_lhe as K44 equivalent. - # If your field name differs, tell me your MonthlySummary model fields. - prev_k44 = Decimal("0") - if prev_sheet: - prev_sum = MonthlySummary.objects.filter(sheet=prev_sheet).first() - if prev_sum and prev_sum.gesamtbestand_neu_lhe is not None: - prev_k44 = Decimal(str(prev_sum.gesamtbestand_neu_lhe)) - - # helpers: read bottom_3 values for a given sheet/month - def get_bottom3_value(sheet, row_index, col_index): - if not sheet: - return Decimal("0") - c = Cell.objects.filter( - sheet=sheet, table_type="bottom_3", - row_index=row_index, column_index=col_index - ).first() - if not c or c.value in (None, "", "None"): - return Decimal("0") - try: - return Decimal(str(c.value)) - except Exception: - return Decimal("0") - - # 2) Sum rows across the 6-month window - g47_sum = Decimal("0") - i47_sum = Decimal("0") - j47_sum = Decimal("0") - - g50_sum = Decimal("0") - i50_sum = Decimal("0") - - for (yy, mm) in window: - s = MonthlySheet.objects.filter(year=yy, month=mm).first() - ## Excel row 47 maps to bottom_3 row_index=1 in DB (see monthly_sheet.html) - g = get_bottom3_value(s, 1, 1) # G47 editable - i = get_bottom3_value(s, 1, 2) # I47 editable - - g47_sum += g - i47_sum += i - - # In monthly_sheet, J47 is CALCULATED as (G47 + I47), not stored as an editable cell - j47_sum += (g + i) - - # row 50: G(2), I(4) - g50_sum += get_bottom3_value(s, 50, 2) - i50_sum += get_bottom3_value(s, 50, 4) - - # 3) K52 = Verbraucherverlust from overall sum group first column (global Σ) - k52 = Decimal("0") - for r in rows_sum: - label = (r.get("label") or "") - if label.startswith("Verbraucherverluste"): - k52 = r.get("total") or Decimal("0") - break - - # --- apply the SAME monthly formulas, with your overrides --- - # Row 46 - k46 = prev_k44 - j46 = k46 * factor - - # Row 47 - g47 = g47_sum - i47 = i47_sum - j47 = j47_sum - k47 = (j47 / factor) + g47 if (j47 != 0 or g47 != 0) else Decimal("0") - - # Row 48 - k48 = k46 + k47 - j48 = k48 * factor - - # Row 49 (akt. Monat) -> in halfyear we use current bottom2 K44 - k49 = bottom2.get("k44") or Decimal("0") - j49 = k49 * factor - - # Row 50 - g50 = g50_sum - i50 = i50_sum - j50 = i50 - k50 = (j50 / factor) if j50 != 0 else Decimal("0") - - # Row 51 - k51 = k48 - k49 - k50 - j51 = k51 * factor - - # Row 52 - j52 = k52 * factor - - # Row 53 - k53 = k51 - k52 - j53 = j51 - j52 - - bottom3 = { - "j46": j46, "k46": k46, - "g47": g47, "i47": i47, "j47": j47, "k47": k47, - "j48": j48, "k48": k48, - "j49": j49, "k49": k49, - "g50": g50, "i50": i50, "j50": j50, "k50": k50, - "j51": j51, "k51": k51, - "j52": j52, "k52": k52, - "j53": j53, "k53": k53, - } - # ------------------------------------------------------------------ - # 4) Context – keep old keys AND new ones - # ------------------------------------------------------------------ - context = { - 'interval_year': interval_year, - 'interval_start_month': interval_start, - 'window': window, - - # Left table – old names (for your first template) - 'clients': HALFYEAR_CLIENTS_LEFT, - 'rows': rows_left, - - # Left table – explicit - 'clients_left': HALFYEAR_CLIENTS_LEFT, - 'rows_left': rows_left, - - # Right table - 'clients_right': RIGHT_CLIENTS, - 'rows_right': rows_right, - 'rows_sum': rows_sum, - 'bottom1_rows': bottom1_rows, - - } - context["bottom2"] = bottom2 - context["bottom3"] = bottom3 - context["context_bottom2_g39"] = bottom2_inputs["g39"] - context["context_bottom2_i39"] = bottom2_inputs["i39"] - return render(request, 'halfyear_balance.html', context) - -def get_bottom2_value(sheet, row_index: int, col_index: int) -> Decimal: - """Get numeric value from bottom_2 or 0 if missing.""" - if sheet is None: - return Decimal("0") - cell = Cell.objects.filter( - sheet=sheet, - table_type="bottom_2", - row_index=row_index, - column_index=col_index, - ).first() - if cell is None or cell.value in (None, ""): - return Decimal("0") - try: - return Decimal(str(cell.value)) - except Exception: - return Decimal("0") - -def get_top_left_value(sheet, client_name: str, row_index: int) -> Decimal: - """ - Read a numeric value from the top_left table for a given month, client and row. - Does NOT use column_index, because top_left is keyed only by client + row_index. - """ - if sheet is None: - return Decimal('0') - - client_obj = Client.objects.filter(name=client_name).first() - if not client_obj: - return Decimal('0') - - cell = Cell.objects.filter( - sheet=sheet, - table_type='top_left', - client=client_obj, - row_index=row_index, - ).first() - - if cell is None or cell.value in (None, ''): - return Decimal('0') - - try: - return Decimal(str(cell.value)) - except Exception: - return Decimal('0') -def get_group_clients(group_key): - """Return queryset of clients that belong to a logical group.""" - from .models import Client # local import to avoid circulars - - group = CLIENT_GROUPS.get(group_key) - if not group: - return Client.objects.none() - return Client.objects.filter(name__in=group['names']) - -def calculate_summation(sheet, table_type, row_index, sum_column_index): - """Calculate summation for a row, with special handling for % row""" - from decimal import Decimal - from .models import Cell - - try: - # Special case: top_left, % row (Excel B20 -> row_index 19) - if table_type == 'top_left' and row_index == 19: - # K13 = sum of row 13 (Excel B13 -> row_index 12) across all clients - cells_row13 = Cell.objects.filter( - sheet=sheet, - table_type='top_left', - row_index=12, # Excel B13 = row_index 12 - column_index__lt=sum_column_index # Exclude sum column itself - ) - total_13 = Decimal('0') - for cell in cells_row13: - if cell.value is not None: - total_13 += Decimal(str(cell.value)) - - # K19 = sum of row 19 (Excel B19 -> row_index 18) across all clients - cells_row19 = Cell.objects.filter( - sheet=sheet, - table_type='top_left', - row_index=18, # Excel B19 = row_index 18 - column_index__lt=sum_column_index - ) - total_19 = Decimal('0') - for cell in cells_row19: - if cell.value is not None: - total_19 += Decimal(str(cell.value)) - - # Calculate: IF(K13=0; 0; K19/K13) - if total_13 == 0: - return Decimal('0') - return total_19 / total_13 - - # Normal summation for other rows - cells_in_row = Cell.objects.filter( - sheet=sheet, - table_type=table_type, - row_index=row_index, - column_index__lt=sum_column_index - ) - - total = Decimal('0') - for cell in cells_in_row: - if cell.value is not None: - total += Decimal(str(cell.value)) - - return total - - except Exception as e: - print(f"Error calculating summation for {table_type}[{row_index}]: {e}") - return None - -# Helper function for calculations -def evaluate_formula(formula, values_dict): - """ - Safely evaluate a formula like "10 + 9" where numbers are row indices - values_dict: {row_index: decimal_value} - """ - from decimal import Decimal - import re - - try: - # Create a copy of the formula to work with - expr = formula - - # Find all row numbers in the formula - row_refs = re.findall(r'\b\d+\b', expr) - - for row_ref in row_refs: - row_num = int(row_ref) - if row_num in values_dict and values_dict[row_num] is not None: - # Replace row reference with actual value - expr = expr.replace(row_ref, str(values_dict[row_num])) - else: - # Missing value - can't calculate - return None - - # Evaluate the expression - # Note: In production, use a safer evaluator like `asteval` - result = eval(expr, {"__builtins__": {}}, {}) - - # Convert to Decimal with proper rounding - return Decimal(str(round(result, 6))) - - except Exception: - return None - -# Monthly Sheet View -class MonthlySheetView(TemplateView): - template_name = 'monthly_sheet.html' - - def populate_helium_input_to_top_right(self, sheet): - """Populate bezug data from SecondTableEntry to top-right table (row 8 = Excel row 12)""" - from .models import SecondTableEntry, Cell, Client - from django.db.models.functions import Coalesce - from decimal import Decimal - - year = sheet.year - month = sheet.month - - TOP_RIGHT_CLIENTS = [ - "Dr. Fohrer", # Column index 0 (L) - "AG Buntk.", # Column index 1 (M) - "AG Alff", # Column index 2 (N) - "AG Gutfl.", # Column index 3 (O) - "M3 Thiele", # Column index 4 (P) - "M3 Buntkowsky", # Column index 5 (Q) - "M3 Gutfleisch", # Column index 6 (R) - ] - - # For each client in top-right table - for client_name in TOP_RIGHT_CLIENTS: - try: - client = Client.objects.get(name=client_name) - column_index = TOP_RIGHT_CLIENTS.index(client_name) - - # Calculate total LHe_output for this client in this month from SecondTableEntry - total_lhe_output = SecondTableEntry.objects.filter( - client=client, - date__year=year, - date__month=month - ).aggregate( - total=Coalesce(Sum('lhe_output'), Decimal('0')) - )['total'] - - # Get or create the cell for row_index 8 (Excel row 12) - Bezug - cell, created = Cell.objects.get_or_create( - sheet=sheet, - table_type='top_right', - client=client, - row_index=8, # Bezug row (Excel row 12) - column_index=column_index, - defaults={'value': total_lhe_output} - ) - - if not created and cell.value != total_lhe_output: - cell.value = total_lhe_output - cell.save() - - except Client.DoesNotExist: - continue - - # After populating bezug, trigger calculation for all dependent cells - # Get any cell to start the calculation - first_cell = Cell.objects.filter( - sheet=sheet, - table_type='top_right' - ).first() - - if first_cell: - save_view = SaveCellsView() - save_view.calculate_top_right_dependents(sheet, first_cell) - - return True - def calculate_bezug_from_entries(self, sheet, year, month): - """Calculate B11 (Bezug) from SecondTableEntry for all clients - ONLY for non-start sheets""" - from .models import SecondTableEntry, Cell, Client - from django.db.models import Sum - from django.db.models.functions import Coalesce - from decimal import Decimal - - # Check if this is the start sheet - if year == 2025 and month == 1: - return # Don't auto-calculate for start sheet - - for client in Client.objects.all(): - # Calculate total LHe output for this client in this month - lhe_output_sum = SecondTableEntry.objects.filter( - client=client, - date__year=year, - date__month=month - ).aggregate( - total=Coalesce(Sum('lhe_output'), Decimal('0')) - )['total'] - - # Update B11 cell (row_index 8 = UI Row 9) - b11_cell = Cell.objects.filter( - sheet=sheet, - table_type='top_left', - client=client, - row_index=8 # Excel B11 - ).first() - - if b11_cell and (b11_cell.value != lhe_output_sum or b11_cell.value is None): - b11_cell.value = lhe_output_sum - b11_cell.save() - - # Also trigger dependent calculations - from .views import SaveCellsView - save_view = SaveCellsView() - save_view.calculate_top_left_dependents(sheet, b11_cell) - # In MonthlySheetView.get_context_data() method, update the TOP_RIGHT_CLIENTS and row count: - - - - return True - def get_context_data(self, **kwargs): - from decimal import Decimal - context = super().get_context_data(**kwargs) - year = self.kwargs.get('year', datetime.now().year) - month = self.kwargs.get('month', datetime.now().month) - is_start_sheet = (year == FIRST_SHEET_YEAR and month == FIRST_SHEET_MONTH) - - # Get or create the monthly sheet - sheet, created = MonthlySheet.objects.get_or_create( - year=year, month=month - ) - - # All clients (used for bottom tables etc.) - clients = Client.objects.all().order_by('name') - - # Pre-fill cells if creating new sheet - if created: - self.initialize_sheet_cells(sheet, clients) - - # Apply previous month links (for B4 and B12) - self.apply_previous_month_links(sheet, year, month) - self.calculate_bezug_from_entries(sheet, year, month) - self.populate_helium_input_to_top_right(sheet) - self.apply_previous_month_links_top_right(sheet, year, month) - - # Define client groups - TOP_LEFT_CLIENTS = [ - "AG Vogel", - "AG Halfm", - "IKP", - ] - - TOP_RIGHT_CLIENTS = [ - "Dr. Fohrer", - "AG Buntk.", - "AG Alff", - "AG Gutfl.", - "M3 Thiele", - "M3 Buntkowsky", - "M3 Gutfleisch", - ] - current_summary = MonthlySummary.objects.filter(sheet=sheet).first() - - # Get previous month summary (for Bottom Table 3: K46 = prev K44) - prev_month_info = self.get_prev_month(year, month) - prev_summary = None - if not is_start_sheet: - prev_sheet = MonthlySheet.objects.filter( - year=prev_month_info['year'], - month=prev_month_info['month'] - ).first() - if prev_sheet: - prev_summary = MonthlySummary.objects.filter(sheet=prev_sheet).first() - - context.update({ - # ... your existing context ... - 'current_summary': current_summary, - 'prev_summary': prev_summary, - }) - # Update row counts in build_group_rows function - # Update row counts in build_group_rows function - def build_group_rows(sheet, table_type, client_names): - """Build rows for display in monthly sheet.""" - from decimal import Decimal - from .models import Cell - MERGED_ROWS = {2, 3, 5, 6, 7, 9, 10, 12, 14, 15} - MERGED_PAIRS = [ - ("Dr. Fohrer", "AG Buntk."), - ("AG Alff", "AG Gutfl."), - ] - rows = [] - - # Determine row count - row_counts = { - "top_left": 16, - "top_right": 16, # rows 0–15 - "bottom_1": 10, - "bottom_2": 10, - "bottom_3": 10, - } - row_count = row_counts.get(table_type, 0) - - # Get all cells for this sheet and table - all_cells = ( - Cell.objects.filter( - sheet=sheet, - table_type=table_type, - ).select_related("client") - ) - - # Group cells by row index - cells_by_row = {} - for cell in all_cells: - if cell.row_index not in cells_by_row: - cells_by_row[cell.row_index] = {} - cells_by_row[cell.row_index][cell.client.name] = cell - - # We will store row sums for Bezug (row 8) and Verbraucherverluste (row 14) - # for top_left / top_right so we can compute the overall % in row 15. - sum_bezug = None - sum_verbrauch = None - - # Build each row - for row_idx in range(row_count): - display_cells = [] - row_cells_dict = cells_by_row.get(row_idx, {}) - - # Build cells in the requested client order - for name in client_names: - cell = row_cells_dict.get(name) - display_cells.append(cell) - - # Calculate sum for this row (includes editable + calculated cells) - sum_value = None - total = Decimal("0") - has_value = False - - merged_second_indices = set() - if table_type == 'top_right' and row_idx in MERGED_ROWS: - for left_name, right_name in MERGED_PAIRS: - try: - right_idx = client_names.index(right_name) - merged_second_indices.add(right_idx) - except ValueError: - # client not in this table; just ignore - pass - - for col_idx, cell in enumerate(display_cells): - # Skip the duplicate (second) column of each merged pair - if col_idx in merged_second_indices: - continue - - if cell and cell.value is not None: - try: - total += Decimal(str(cell.value)) - has_value = True - except Exception: - pass - - if has_value: - sum_value = total - - # Remember special rows for top tables - if table_type in ("top_left", "top_right"): - if row_idx == 8: # Bezug - sum_bezug = total - elif row_idx == 14: # Verbraucherverluste - sum_verbrauch = total - - rows.append( - { - "cells": display_cells, - "sum": sum_value, - "row_index": row_idx, - } - ) - - # Adjust the % row sum for top_left / top_right: - # Sum(%) = Sum(Verbraucherverluste) / Sum(Bezug) - if table_type in ("top_left", "top_right"): - perc_row_idx = 15 # % row - if 0 <= perc_row_idx < len(rows): - if sum_bezug is not None and sum_bezug != 0 and sum_verbrauch is not None: - rows[perc_row_idx]["sum"] = sum_verbrauch / sum_bezug - else: - rows[perc_row_idx]["sum"] = None - - return rows - - - # Now call the local function - top_left_rows = build_group_rows(sheet, 'top_left', TOP_LEFT_CLIENTS) - top_right_rows = build_group_rows(sheet, 'top_right', TOP_RIGHT_CLIENTS) - - # --- Build combined summary of top-left + top-right Sum columns --- - - # Helper to safely get the Sum for a given row index - def get_row_sum(rows, row_index): - if 0 <= row_index < len(rows): - return rows[row_index].get('sum') - return None - - # Row definitions we want in the combined Σ table - # (row_index in top tables, label shown in the small table) - summary_row_defs = [ - (2, "Rückführung flüssig (Lit. L-He)"), - (3, "Sonderrückführungen (Lit. L-He)"), - (4, "Sammelrückführungen (Lit. L-He)"), - (5, "Bestand in Kannen-1 (Lit. L-He)"), - (6, "Summe Bestand (Lit. L-He)"), - (7, "Best. in Kannen Vormonat (Lit. L-He)"), - (8, "Bezug (Liter L-He)"), - (9, "Rückführ. Soll (Lit. L-He)"), - (10, "Verluste (Soll-Rückf.) (Lit. L-He)"), - (11, "Füllungen warm (Lit. L-He)"), - (12, "Kaltgas Rückgabe (Lit. L-He) – Faktor"), - (13, "Faktor 0.06"), - (14, "Verbraucherverluste (Liter L-He)"), - (15, "%"), - ] - - # Precompute totals for Bezug and Verbraucherverluste across both tables - bezug_left = get_row_sum(top_left_rows, 8) or Decimal('0') - bezug_right = get_row_sum(top_right_rows, 8) or Decimal('0') - total_bezug = bezug_left + bezug_right - - verb_left = get_row_sum(top_left_rows, 14) or Decimal('0') - verb_right = get_row_sum(top_right_rows, 14) or Decimal('0') - total_verbrauch = verb_left + verb_right - - summary_rows = [] - - for row_index, label in summary_row_defs: - # Faktor row: always fixed 0.06 - if row_index == 13: - summary_value = Decimal('0.06') - - # % row: total Verbraucherverluste / total Bezug - elif row_index == 15: - if total_bezug != 0: - summary_value = total_verbrauch / total_bezug - else: - summary_value = None - - else: - left_sum = get_row_sum(top_left_rows, row_index) - right_sum = get_row_sum(top_right_rows, row_index) - - # Sammelrückführungen: only from top-right table - if row_index == 4: - left_sum = None - - total = Decimal('0') - - has_any = False - if left_sum is not None: - total += Decimal(str(left_sum)) - has_any = True - if right_sum is not None: - total += Decimal(str(right_sum)) - has_any = True - - summary_value = total if has_any else None - - summary_rows.append({ - 'row_index': row_index, - 'label': label, - 'sum': summary_value, - }) - - # Get cells for bottom tables - cells_by_table = self.get_cells_by_table(sheet) - - context.update({ - 'sheet': sheet, - 'clients': clients, - 'year': year, - 'month': month, - 'month_name': calendar.month_name[month], - 'prev_month': self.get_prev_month(year, month), - 'next_month': self.get_next_month(year, month), - 'cells_by_table': cells_by_table, - 'top_left_headers': TOP_LEFT_CLIENTS + ['Sum'], - 'top_right_headers': TOP_RIGHT_CLIENTS + ['Sum'], - 'top_left_rows': top_left_rows, - 'top_right_rows': top_right_rows, - 'summary_rows': summary_rows, # 👈 NEW - 'is_start_sheet': is_start_sheet, - }) - return context - - def get_cells_by_table(self, sheet): - """Organize cells by table type for easy template rendering""" - cells = sheet.cells.select_related('client').all() - organized = { - 'top_left': [[] for _ in range(16)], - 'top_right': [[] for _ in range(16)], # now 16 rows - 'bottom_1': [[] for _ in range(10)], - 'bottom_2': [[] for _ in range(10)], - 'bottom_3': [[] for _ in range(10)], - } - - for cell in cells: - if cell.table_type not in organized: - continue - - max_rows = len(organized[cell.table_type]) - if cell.row_index < 0 or cell.row_index >= max_rows: - # This is an "extra" cell from an older layout (e.g. top_left rows 18–23) - continue - - row_list = organized[cell.table_type][cell.row_index] - while len(row_list) <= cell.column_index: - row_list.append(None) - - row_list[cell.column_index] = cell - - return organized - - - def initialize_sheet_cells(self, sheet, clients): - """Create all empty cells for a new monthly sheet""" - cells_to_create = [] - summation_config = CALCULATION_CONFIG.get('summation_column', {}) - sum_column_index = summation_config.get('sum_column_index', len(clients) - 1) # Last column - - # For each table type and row - table_configs = [ - ('top_left', 16), - ('top_right', 16), - ('bottom_1', 10), - ('bottom_2', 10), - ('bottom_3', 10), - ] - - for table_type, row_count in table_configs: - for row_idx in range(row_count): - for col_idx, client in enumerate(clients): - is_summation = (col_idx == sum_column_index) - cells_to_create.append(Cell( - sheet=sheet, - client=client, - table_type=table_type, - row_index=row_idx, - column_index=col_idx, - value=None, - is_formula=is_summation, # Mark summation cells as formulas - )) - - # Bulk create all cells at once - Cell.objects.bulk_create(cells_to_create) - def get_prev_month(self, year, month): - """Get previous month year and month""" - if month == 1: - return {'year': year - 1, 'month': 12} - return {'year': year, 'month': month - 1} - - def get_next_month(self, year, month): - """Get next month year and month""" - if month == 12: - return {'year': year + 1, 'month': 1} - return {'year': year, 'month': month + 1} - def apply_previous_month_links(self, sheet, year, month): - """ - For non-start sheets: - B4 (row 2) = previous sheet B3 (row 1) - B10 (row 8) = previous sheet B9 (row 7) - """ - # Do nothing on the first sheet - if year == FIRST_SHEET_YEAR and month == FIRST_SHEET_MONTH: - return - - # Figure out previous month - if month == 1: - prev_year = year - 1 - prev_month = 12 - else: - prev_year = year - prev_month = month - 1 - - from .models import MonthlySheet, Cell, Client - - prev_sheet = MonthlySheet.objects.filter( - year=prev_year, - month=prev_month - ).first() - - if not prev_sheet: - # No previous sheet created yet → nothing to copy - return - - # For each client, copy values - for client in Client.objects.all(): - # B3(prev) → B4(curr): UI row 1 → row 2 → row_index 0 → 1 - prev_b3 = Cell.objects.filter( - sheet=prev_sheet, - table_type='top_left', - client=client, - row_index=0, # UI row 1 - ).first() - - curr_b4 = Cell.objects.filter( - sheet=sheet, - table_type='top_left', - client=client, - row_index=1, # UI row 2 - ).first() - - if prev_b3 and curr_b4: - curr_b4.value = prev_b3.value - curr_b4.save() - - # B9(prev) → B10(curr): UI row 7 → row 8 → row_index 6 → 7 - prev_b9 = Cell.objects.filter( - sheet=prev_sheet, - table_type='top_left', - client=client, - row_index=6, # UI row 7 (Summe Bestand) - ).first() - - curr_b10 = Cell.objects.filter( - sheet=sheet, - table_type='top_left', - client=client, - row_index=7, # UI row 8 (Best. in Kannen Vormonat) - ).first() - - if prev_b9 and curr_b10: - curr_b10.value = prev_b9.value - curr_b10.save() - - def apply_previous_month_links_top_right(self, sheet, year, month): - """ - top_right row 7: Best. in Kannen Vormonat (Lit. L-He) - = previous sheet's Summe Bestand (row 6). - - For merged pairs: - - Dr. Fohrer + AG Buntk. share the SAME value (from previous month's AG Buntk. or Dr. Fohrer) - - AG Alff + AG Gutfl. share the SAME value (from previous month's AG Alff or AG Gutfl.) - M3 clients just copy their own value. - """ - from .models import MonthlySheet, Cell, Client - from decimal import Decimal - - # Do nothing on first sheet - if year == FIRST_SHEET_YEAR and month == FIRST_SHEET_MONTH: - return - - # find previous month - if month == 1: - prev_year = year - 1 - prev_month = 12 - else: - prev_year = year - prev_month = month - 1 - - prev_sheet = MonthlySheet.objects.filter( - year=prev_year, - month=prev_month - ).first() - - if not prev_sheet: - return # nothing to copy from - - TOP_RIGHT_CLIENTS = [ - "Dr. Fohrer", - "AG Buntk.", - "AG Alff", - "AG Gutfl.", - "M3 Thiele", - "M3 Buntkowsky", - "M3 Gutfleisch", - ] - - # Helper function to get a cell value - def get_cell_value(sheet_obj, client_name, row_index): - client_obj = Client.objects.filter(name=client_name).first() - if not client_obj: - return None - - try: - col_idx = TOP_RIGHT_CLIENTS.index(client_name) - except ValueError: - return None - - cell = Cell.objects.filter( - sheet=sheet_obj, - table_type='top_right', - client=client_obj, - row_index=row_index, - column_index=col_idx, - ).first() - - return cell.value if cell else None - - # Helper function to set a cell value - def set_cell_value(sheet_obj, client_name, row_index, value): - client_obj = Client.objects.filter(name=client_name).first() - if not client_obj: - return False - - try: - col_idx = TOP_RIGHT_CLIENTS.index(client_name) - except ValueError: - return False - - cell, created = Cell.objects.get_or_create( - sheet=sheet_obj, - table_type='top_right', - client=client_obj, - row_index=row_index, - column_index=col_idx, - defaults={'value': value} - ) - - if not created and cell.value != value: - cell.value = value - cell.save() - elif created: - cell.save() - - return True - - # ----- Pair 1: Dr. Fohrer + AG Buntk. ----- - # Get previous month's Summe Bestand (row 6) for either client in the pair - pair1_prev_val = None - - # Try AG Buntk. first - prev_buntk_val = get_cell_value(prev_sheet, "AG Buntk.", 6) - if prev_buntk_val is not None: - pair1_prev_val = prev_buntk_val - else: - # Try Dr. Fohrer if AG Buntk. is empty - prev_fohrer_val = get_cell_value(prev_sheet, "Dr. Fohrer", 6) - if prev_fohrer_val is not None: - pair1_prev_val = prev_fohrer_val - - # Apply the value to both clients in the pair - if pair1_prev_val is not None: - set_cell_value(sheet, "Dr. Fohrer", 7, pair1_prev_val) - set_cell_value(sheet, "AG Buntk.", 7, pair1_prev_val) - - # ----- Pair 2: AG Alff + AG Gutfl. ----- - pair2_prev_val = None - - # Try AG Alff first - prev_alff_val = get_cell_value(prev_sheet, "AG Alff", 6) - if prev_alff_val is not None: - pair2_prev_val = prev_alff_val - else: - # Try AG Gutfl. if AG Alff is empty - prev_gutfl_val = get_cell_value(prev_sheet, "AG Gutfl.", 6) - if prev_gutfl_val is not None: - pair2_prev_val = prev_gutfl_val - - # Apply the value to both clients in the pair - if pair2_prev_val is not None: - set_cell_value(sheet, "AG Alff", 7, pair2_prev_val) - set_cell_value(sheet, "AG Gutfl.", 7, pair2_prev_val) - - # ----- M3 clients: copy their own Summe Bestand ----- - for name in ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"]: - prev_val = get_cell_value(prev_sheet, name, 6) - if prev_val is not None: - set_cell_value(sheet, name, 7, prev_val) -# Add this helper function to views.py -def get_factor_value(table_type, row_index): - """Get factor value (like 0.06 for top_left row 17)""" - factors = { - ('top_left', 17): Decimal('0.06'), # A18 in Excel (UI row 17, 0-based index 16) - } - return factors.get((table_type, row_index), Decimal('0')) -# Save Cells View -# views.py - Updated SaveCellsView -# views.py - Update SaveCellsView class -def debug_cell_values(self, sheet, client_id): - """Debug method to check cell values""" - from .models import Cell - cells = Cell.objects.filter( - sheet=sheet, - table_type='top_left', - client_id=client_id - ).order_by('row_index') - - debug_info = {} - for cell in cells: - debug_info[f"row_{cell.row_index}"] = { - 'value': str(cell.value) if cell.value else 'None', - 'ui_row': cell.row_index + 1, - 'excel_ref': f"B{cell.row_index + 3}" - } - - return debug_info -class DebugCalculationView(View): - """Debug view to test calculations directly""" - def get(self, request): - sheet_id = request.GET.get('sheet_id', 1) - client_name = request.GET.get('client', 'AG Vogel') - - try: - sheet = MonthlySheet.objects.get(id=sheet_id) - client = Client.objects.get(name=client_name) - - # Get SaveCellsView instance - save_view = SaveCellsView() - - # Create a dummy cell to trigger calculations - dummy_cell = Cell.objects.filter( - sheet=sheet, - table_type='top_left', - client=client, - row_index=0 # B3 - ).first() - - if not dummy_cell: - return JsonResponse({'error': 'No cells found for this client'}) - - # Trigger calculation - updated = save_view.calculate_top_left_dependents(sheet, dummy_cell) - - # Get updated cell values - cells = Cell.objects.filter( - sheet=sheet, - table_type='top_left', - client=client - ).order_by('row_index') - - cell_data = [] - for cell in cells: - cell_data.append({ - 'row_index': cell.row_index, - 'ui_row': cell.row_index + 1, - 'excel_ref': f"B{cell.row_index + 3}", - 'value': str(cell.value) if cell.value else 'None', - 'description': self.get_row_description(cell.row_index) - }) - - return JsonResponse({ - 'sheet': f"{sheet.year}-{sheet.month:02d}", - 'client': client.name, - 'cells': cell_data, - 'updated_count': len(updated), - 'calculation': 'B5 = IF(B3>0; B3-B4; 0)' - }) - - except Exception as e: - return JsonResponse({'error': str(e)}, status=400) - - def get_row_description(self, row_index): - """Get description for row index""" - descriptions = { - 0: "B3: Stand der Gaszähler (Nm³)", - 1: "B4: Stand der Gaszähler (Vormonat) (Nm³)", - 2: "B5: Gasrückführung (Nm³)", - 3: "B6: Rückführung flüssig (Lit. L-He)", - 4: "B7: Sonderrückführungen (Lit. L-He)", - 5: "B8: Bestand in Kannen-1 (Lit. L-He)", - 6: "B9: Summe Bestand (Lit. L-He)", - 7: "B10: Best. in Kannen Vormonat (Lit. L-He)", - 8: "B11: Bezug (Liter L-He)", - 9: "B12: Rückführ. Soll (Lit. L-He)", - 10: "B13: Verluste (Soll-Rückf.) (Lit. L-He)", - 11: "B14: Füllungen warm (Lit. L-He)", - 12: "B15: Kaltgas Rückgabe (Lit. L-He) – Faktor", - 13: "B16: Faktor", - 14: "B17: Verbraucherverluste (Liter L-He)", - 15: "B18: %" - } - return descriptions.get(row_index, f"Row {row_index}") -def recalculate_stand_der_gaszahler(self, sheet): - """Recalculate Stand der Gaszähler for all client pairs""" - from decimal import Decimal - - # For Dr. Fohrer and AG Buntk. (L & M columns) - try: - # Get Dr. Fohrer's bezug - dr_fohrer_client = Client.objects.get(name="Dr. Fohrer") - dr_fohrer_cell = Cell.objects.filter( - sheet=sheet, - table_type='top_right', - client=dr_fohrer_client, - row_index=9 # Row 9 = Bezug - ).first() - L13 = Decimal(str(dr_fohrer_cell.value)) if dr_fohrer_cell and dr_fohrer_cell.value else Decimal('0') - - # Get AG Buntk.'s bezug - ag_buntk_client = Client.objects.get(name="AG Buntk.") - ag_buntk_cell = Cell.objects.filter( - sheet=sheet, - table_type='top_right', - client=ag_buntk_client, - row_index=9 - ).first() - M13 = Decimal(str(ag_buntk_cell.value)) if ag_buntk_cell and ag_buntk_cell.value else Decimal('0') - - total = L13 + M13 - if total > 0: - # Update Dr. Fohrer's row 0 - dr_fohrer_row0 = Cell.objects.filter( - sheet=sheet, - table_type='top_right', - client=dr_fohrer_client, - row_index=0 - ).first() - if dr_fohrer_row0: - dr_fohrer_row0.value = L13 / total - dr_fohrer_row0.save() - - # Update AG Buntk.'s row 0 - ag_buntk_row0 = Cell.objects.filter( - sheet=sheet, - table_type='top_right', - client=ag_buntk_client, - row_index=0 - ).first() - if ag_buntk_row0: - ag_buntk_row0.value = M13 / total - ag_buntk_row0.save() - except Exception as e: - print(f"Error recalculating Stand der Gaszähler for Dr. Fohrer/AG Buntk.: {e}") - - # For AG Alff and AG Gutfl. (N & O columns) - try: - # Get AG Alff's bezug - ag_alff_client = Client.objects.get(name="AG Alff") - ag_alff_cell = Cell.objects.filter( - sheet=sheet, - table_type='top_right', - client=ag_alff_client, - row_index=9 - ).first() - N13 = Decimal(str(ag_alff_cell.value)) if ag_alff_cell and ag_alff_cell.value else Decimal('0') - - # Get AG Gutfl.'s bezug - ag_gutfl_client = Client.objects.get(name="AG Gutfl.") - ag_gutfl_cell = Cell.objects.filter( - sheet=sheet, - table_type='top_right', - client=ag_gutfl_client, - row_index=9 - ).first() - O13 = Decimal(str(ag_gutfl_cell.value)) if ag_gutfl_cell and ag_gutfl_cell.value else Decimal('0') - - total = N13 + O13 - if total > 0: - # Update AG Alff's row 0 - ag_alff_row0 = Cell.objects.filter( - sheet=sheet, - table_type='top_right', - client=ag_alff_client, - row_index=0 - ).first() - if ag_alff_row0: - ag_alff_row0.value = N13 / total - ag_alff_row0.save() - - # Update AG Gutfl.'s row 0 - ag_gutfl_row0 = Cell.objects.filter( - sheet=sheet, - table_type='top_right', - client=ag_gutfl_client, - row_index=0 - ).first() - if ag_gutfl_row0: - ag_gutfl_row0.value = O13 / total - ag_gutfl_row0.save() - except Exception as e: - print(f"Error recalculating Stand der Gaszähler for AG Alff/AG Gutfl.: {e}") -# In your SaveCellsView class in views.py -class DebugTopRightView(View): - """Debug view to check top_right calculations""" - def get(self, request): - sheet_id = request.GET.get('sheet_id', 1) - client_name = request.GET.get('client', 'Dr. Fohrer') - - try: - sheet = MonthlySheet.objects.get(id=sheet_id) - client = Client.objects.get(name=client_name) - - # Get all cells for this client in top_right - cells = Cell.objects.filter( - sheet=sheet, - table_type='top_right', - client=client - ).order_by('row_index') - - cell_data = [] - descriptions = { - 0: "Stand der Gaszähler (Vormonat)", - 1: "Gasrückführung (Nm³)", - 2: "Rückführung flüssig", - 3: "Sonderrückführungen", - 4: "Sammelrückführungen", - 5: "Bestand in Kannen-1", - 6: "Summe Bestand", - 7: "Best. in Kannen Vormonat", - 8: "Same as row 9 from prev sheet", - 9: "Bezug", - 10: "Rückführ. Soll", - 11: "Verluste", - 12: "Füllungen warm", - 13: "Kaltgas Rückgabe", - 14: "Faktor 0.06", - 15: "Verbraucherverluste", - 16: "%" - } - - for cell in cells: - cell_data.append({ - 'row_index': cell.row_index, - 'ui_row': cell.row_index + 1, - 'description': descriptions.get(cell.row_index, f"Row {cell.row_index}"), - 'value': str(cell.value) if cell.value else 'Empty', - 'cell_id': cell.id - }) - - # Test calculation - row3_cell = cells.filter(row_index=3).first() - row5_cell = cells.filter(row_index=5).first() - row6_cell = cells.filter(row_index=6).first() - - calculation_info = { - 'row3_value': str(row3_cell.value) if row3_cell and row3_cell.value else '0', - 'row5_value': str(row5_cell.value) if row5_cell and row5_cell.value else '0', - 'row6_value': str(row6_cell.value) if row6_cell and row6_cell.value else '0', - 'expected_sum': '0' - } - - if row3_cell and row5_cell and row6_cell: - row3_val = Decimal(str(row3_cell.value)) if row3_cell.value else Decimal('0') - row5_val = Decimal(str(row5_cell.value)) if row5_cell.value else Decimal('0') - expected = row3_val + row5_val - calculation_info['expected_sum'] = str(expected) - calculation_info['is_correct'] = row6_cell.value == expected - - return JsonResponse({ - 'sheet': f"{sheet.year}-{sheet.month}", - 'client': client.name, - 'cells': cell_data, - 'calculation': calculation_info - }) - - except Exception as e: - return JsonResponse({'error': str(e)}, status=400) -ABRECHNUNG_COL_CLIENTS = { - # first 3 columns - "pkm_vogel": ["AG Vogel"], - "iap_halfmann": ["AG Halfm"], # or whatever exact name you use in Client admin - "ikp": ["IKP"], - - # your “5 columns” group (excluding Chemie/MaWi/Physik summaries) - "orgchem_thiele": ["M3 Thiele"], # if OrgChem Thiele corresponds to M3 Thiele in your DB - "phychem_m3_buntkow": ["AG Buntk.", "M3 Buntkowsky"], - "orgchem_fohrer": ["Dr. Fohrer"], - "mawi_m3_gutfl": ["AG Gutfl.", "M3 Gutfleisch"], - "mawi_alff": ["AG Alff"], - - # summary columns at the end - "chemie": ["Dr. Fohrer", "AG Buntk.", "M3 Buntkowsky", "M3 Thiele"], - "mawi": ["AG Alff", "AG Gutfl.", "M3 Gutfleisch"], - "physik": ["AG Vogel", "AG Halfm", "IKP"], - - # “Gesamt-summe” is computed as sum of all columns (not client-driven) - "gesamt_summe": [], -} - -class AbrechnungView(LoginRequiredMixin, TemplateView): - template_name = "abrechnung.html" - - # Columns EXACTLY from your screenshot - COLUMNS = [ - ("pkm_vogel", "PKM Vogel"), - ("iap_halfmann", "IAP Halfmann"), - ("ikp", "IKP"), - ("orgchem_thiele", "OrgChem Thiele"), - ("phychem_m3_buntkow", "PhyChem + M3 Buntkow"), - ("orgchem_fohrer", "OrgChem Fohrer"), - ("mawi_m3_gutfl", "MaWi + M3 Gutfl"), - ("mawi_alff", "MaWi Alff"), - ("chemie", "Chemie"), - ("mawi", "MaWi"), - ("physik", "Physik"), - ("gesamt_summe", "Gesamt-summe"), - ] - - # Rows EXACTLY from your screenshot (Excel row 11 is skipped) - ROWS = [ - ("bezogen_menge", "Bezogen. Menge", "LHe"), - ("kaltg_warmfuell", "Kaltg. Warmfüll.", "Anzahl"), - ("anzahl_15", "Anzahl", "LHe"), - ("he_verbrauch", "He-Verbrauch (LHe)", "LHe"), - ("umlage_instandhaltung_1", "1-Umlage Instandhaltung", "EUR"), - ("lhe_verluste", "LHe – Verluste", "LHe"), - ("umlage_heliumkosten_3", "3 Umlage Heliumkosten", "EUR"), - ("ghe_bezug", "GHe - Bezug", "m³"), - ("kosten_he_gas_bezug_4", "4-Kosten He-Gas-Bezug", "EUR"), - ("rel_anteil", "rel. Anteil", "Anteil"), - ("summe_anteile_1_4", "Summe Anteile 1-4", "EUR"), - ("sonstiges", "Sonstiges", "EUR"), - ("betrag", "Betrag", "EUR"), - ("umlage_personal_5", "5-Umlage Personal", "EUR"), - ("gutschriften", "Gutschriften", "EUR"), - ("rechnungsbetrag", "Rechnungsbetrag", "EUR"), - ("eff_lhe_preis", "eff. L-He-Preis", "EUR/L"), -] - - - # Editable rows = your yellow rows - EDITABLE_ROW_KEYS = {"ghe_bezug", "betrag", "gutschriften"} - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - - interval_year = self.request.session.get("halfyear_year") - interval_start = self.request.session.get("halfyear_start_month") - if not interval_year or not interval_start: - context["needs_interval"] = True - return context - - interval_year = int(interval_year) - interval_start = int(interval_start) - - qs = AbrechnungCell.objects.filter( - interval_year=interval_year, - interval_start_month=interval_start, - ) - value_map = {(c.row_key, c.col_key): c.value for c in qs} - def d(x): - """safe Decimal""" - if x in (None, ""): - return Decimal("0") - try: - return Decimal(str(x).replace(",", ".")) - except Exception: - return Decimal("0") - - def safe_div(a: Decimal, b: Decimal): - return (a / b) if b not in (None, 0, Decimal("0")) else Decimal("0") - - # ---- interval + window ---- - window = build_halfyear_window(interval_year, interval_start) - - # ---- BetriebskostenSummary values ---- - bs = BetriebskostenSummary.objects.first() - instandhaltung = d(bs.instandhaltung) if bs else Decimal("0") - heliumkosten = d(bs.heliumkosten) if bs else Decimal("0") - bezugskosten_gashe = d(bs.bezugskosten_gashe) if bs else Decimal("0") - umlage_personal_total = d(bs.umlage_personal) if bs else Decimal("0") - - # ---- helper: sum lhe_output in the 6-month window for a list of client names ---- - def sum_output_for_clients_exact(client_names): - total = Decimal("0") - if not client_names: - return total - - for (y, m) in window: - total += SecondTableEntry.objects.filter( - client__name__in=client_names, - date__year=y, - date__month=m, - ).aggregate( - total=Coalesce(Sum("lhe_output"), Value(0, output_field=DecimalField())) - )["total"] or Decimal("0") - - return total - # (NOTE: the date filter above is “wide”; if you prefer exact (y,m) matching, use the loop version below) - def sum_output_for_clients_exact(client_names): - total = Decimal("0") - if not client_names: - return total - for (y, m) in window: - total += SecondTableEntry.objects.filter( - client__name__in=client_names, - date__year=y, - date__month=m, - ).aggregate( - total=Coalesce(Sum("lhe_output"), Value(0, output_field=DecimalField())) - )["total"] or Decimal("0") - return total - - # Use exact to avoid edge cases - sum_output_for_clients = sum_output_for_clients_exact - - # ---- helper: warm fillings (from Halbjahresbilanz row “Füllungen warm”) ---- - # In your halfyear_balance logic, “Füllungen warm” is summed from MonthlySheet cells row_index=11 - # We replicate that part by reading stored top_left/top_right cells for each month. - def sum_fuellungen_warm(client_name): - total = Decimal("0") - for (y, m) in window: - sheet = MonthlySheet.objects.filter(year=y, month=m).first() - if not sheet: - continue - # warm row_index = 11 in both top_left and top_right tables in your existing code - # Try top_left first, then top_right - c = Cell.objects.filter(sheet=sheet, client__name=client_name, table_type="top_left", row_index=11).first() - if not c: - c = Cell.objects.filter(sheet=sheet, client__name=client_name, table_type="top_right", row_index=11).first() - total += d(c.value if c else None) - return total - - # ---- helper: verbraucherverluste (from Halbjahresbilanz) ---- - # In halfyear_balance view this is a *computed* metric, not directly stored. - # So we must either: - # A) replicate the whole halfyear_balance math (big), OR - # B) read it from stored cells *if you store it*. - # - # Right now your MonthlySheet stores the table rows, but “Verbraucherverluste” is row_index=15 - # in your debug mapping. If those cells are saved in DB (they usually are), we can sum row_index=15. - def sum_verbraucherverluste(client_name): - total = Decimal("0") - for (y, m) in window: - sheet = MonthlySheet.objects.filter(year=y, month=m).first() - if not sheet: - continue - # row_index=15 is “Verbraucherverluste” in your debug map - c = Cell.objects.filter(sheet=sheet, client__name=client_name, table_type="top_left", row_index=15).first() - if not c: - c = Cell.objects.filter(sheet=sheet, client__name=client_name, table_type="top_right", row_index=15).first() - total += d(c.value if c else None) - return total - - # ---------------------------------------------------------------------- - # Build computed row values per column - # ---------------------------------------------------------------------- - computed = {} # (row_key, col_key) -> Decimal OR special string - - # Gather base rows (bezug + warm fills) per column - bezogen = {} - warmfills = {} - - for col_key, _label in self.COLUMNS: - client_names = ABRECHNUNG_COL_CLIENTS.get(col_key, []) - bezogen[col_key] = sum_output_for_clients(client_names) - warmfills[col_key] = sum_fuellungen_warm(client_names[0]) if len(client_names) == 1 else sum(sum_fuellungen_warm(n) for n in client_names) - - # “Gesamt-summe” = sum of all “normal” columns (everything except itself) - def sum_all_cols(values_dict): - return sum(v for k, v in values_dict.items() if k != "gesamt_summe") - - bezogen["gesamt_summe"] = sum_all_cols(bezogen) - warmfills["gesamt_summe"] = sum_all_cols(warmfills) - - # ---- Row: Bezogen. Menge ---- - for col_key in bezogen: - computed[("bezogen_menge", col_key)] = bezogen[col_key] - - # ---- Row: Kaltg. Warmfüll. (á 15l L-He) ---- - for col_key in warmfills: - computed[("kaltg_warmfuell", col_key)] = warmfills[col_key] - - # ---- Row: Anzahl (15) = warmfills * 15 ---- - for col_key in warmfills: - computed[("anzahl_15", col_key)] = warmfills[col_key] * Decimal("15") - - # ---- Row: He-Verbrauch (LHe) = Bezogen + Anzahl_15 ---- - he_verbrauch = {} - for col_key in bezogen: - hv = bezogen[col_key] + (warmfills[col_key] * Decimal("15")) - he_verbrauch[col_key] = hv - computed[("he_verbrauch", col_key)] = hv - - # ---- Row: 1-Umlage Instandhaltung ---- - # Your rule (as you stated): - # - first 3 columns: instandhaltung * (He-Verbrauch of this column / He-Verbrauch of “Physik”) / 3 - # - next 3 columns: instandhaltung / 9 - # - next 2 columns: instandhaltung / 6 - # - next 2 columns: instandhaltung / 3 (if you later add more) - # - gesamt_summe: sum of first three columns (same row) - # - # We apply this to the current UI order. - first3 = ["pkm_vogel", "iap_halfmann", "ikp"] - next3 = ["orgchem_thiele", "phychem_m3_buntkow", "orgchem_fohrer"] - next2 = ["mawi_m3_gutfl", "mawi_alff"] - - physik_hv = he_verbrauch.get("physik", Decimal("0")) - - row1_vals = {} - for col_key in he_verbrauch: - if col_key in first3: - row1_vals[col_key] = instandhaltung * safe_div(he_verbrauch[col_key], physik_hv) / Decimal("3") - elif col_key in next3: - row1_vals[col_key] = instandhaltung / Decimal("9") - elif col_key in next2: - row1_vals[col_key] = instandhaltung / Decimal("6") - elif col_key == "gesamt_summe": - row1_vals[col_key] = sum(row1_vals.get(k, Decimal("0")) for k in first3) - else: - row1_vals[col_key] = Decimal("0") - - for col_key, v in row1_vals.items(): - computed[("umlage_instandhaltung_1", col_key)] = v - - # ---- Row: LHe – Verluste = Verbraucherverluste from Halbjahresbilanz (sum if combined) ---- - lhe_verluste = {} - for col_key, _label in self.COLUMNS: - if col_key in ("chemie", "mawi", "physik"): - # Special rules you stated: - if col_key == "chemie": - # Dr. Fohrer + AG Buntk. (NOT M3 here) - v = sum_verbraucherverluste("Dr. Fohrer") + sum_verbraucherverluste("AG Buntk.") - elif col_key == "mawi": - v = sum_verbraucherverluste("AG Alff") + sum_verbraucherverluste("AG Gutfl.") - else: # physik - v = sum_verbraucherverluste("M3 Thiele") + sum_verbraucherverluste("M3 Gutfleisch") + sum_verbraucherverluste("M3 Buntkowsky") - else: - client_names = ABRECHNUNG_COL_CLIENTS.get(col_key, []) - v = sum(sum_verbraucherverluste(n) for n in client_names) - - lhe_verluste[col_key] = v - computed[("lhe_verluste", col_key)] = v - - lhe_verluste["gesamt_summe"] = sum_all_cols(lhe_verluste) - computed[("lhe_verluste", "gesamt_summe")] = lhe_verluste["gesamt_summe"] - - # ---- Row: 3 Umlage Heliumkosten = heliumkosten * (LHe-Verluste col / LHe-Verluste gesamt_summe) ---- - for col_key in lhe_verluste: - computed[("umlage_heliumkosten_3", col_key)] = heliumkosten * safe_div(lhe_verluste[col_key], lhe_verluste["gesamt_summe"]) - - # ---- Row: 4-Kosten He-Gas-Bezug = Umlage Heliumkosten * Bezugskosten-GasHe ---- - for col_key in lhe_verluste: - computed[("kosten_he_gas_bezug_4", col_key)] = d(computed[("umlage_heliumkosten_3", col_key)]) * bezugskosten_gashe - - # ---- Row: Summe Anteile 1-4 = (1) + (3) + (4) ---- - summe_anteile = {} - for col_key in lhe_verluste: - v = ( - d(computed.get(("umlage_instandhaltung_1", col_key))) + - d(computed.get(("umlage_heliumkosten_3", col_key))) + - d(computed.get(("kosten_he_gas_bezug_4", col_key))) - ) - summe_anteile[col_key] = v - computed[("summe_anteile_1_4", col_key)] = v - - # ---- Row: rel. Anteil = summe_anteile col / summe_anteile gesamt ---- - for col_key in summe_anteile: - computed[("rel_anteil", col_key)] = safe_div(summe_anteile[col_key], summe_anteile.get("gesamt_summe", Decimal("0"))) - - # ---- Row: 5-Umlage Personal ---- - # Your rules: - # - first 3 clients => 0 - # - next columns up to (excluding Chemie): weighted over the 5 columns: - # OrgChem Thiele, PhyChem+M3 Buntkow, OrgChem Fohrer, MaWi+M3 Gutfl, MaWi Alff - # - for Chemie/MaWi/Physik: weighted over the 3 summary columns (Chemie, MaWi, Physik) - five_cols = ["orgchem_thiele", "phychem_m3_buntkow", "orgchem_fohrer", "mawi_m3_gutfl", "mawi_alff"] - sum5 = sum(he_verbrauch.get(k, Decimal("0")) for k in five_cols) - - sum3 = ( - he_verbrauch.get("chemie", Decimal("0")) + - he_verbrauch.get("mawi", Decimal("0")) + - he_verbrauch.get("physik", Decimal("0")) - ) - - for col_key in he_verbrauch: - if col_key in first3: - v = Decimal("0") - elif col_key in five_cols: - v = safe_div(he_verbrauch[col_key], sum5) * umlage_personal_total - elif col_key in ("chemie", "mawi", "physik"): - v = safe_div(he_verbrauch[col_key], sum3) * umlage_personal_total - elif col_key == "gesamt_summe": - v = sum(d(computed.get(("umlage_personal_5", k))) for k in he_verbrauch if k != "gesamt_summe") - else: - v = Decimal("0") - computed[("umlage_personal_5", col_key)] = v - - # ---- Row: Rechnungsbetrag = Summe Anteile 1-4 − Gutschriften + Betrag + 5-Umlage Personal ---- - for col_key in he_verbrauch: - gutsch = d(value_map.get(("gutschriften", col_key))) - betrag = d(value_map.get(("betrag", col_key))) - personal = d(computed.get(("umlage_personal_5", col_key))) - sa = d(computed.get(("summe_anteile_1_4", col_key))) - computed[("rechnungsbetrag", col_key)] = sa - gutsch + betrag + personal - - # ---- Row: eff. L-He-Preis = Rechnungsbetrag / He-Verbrauch ---- - for col_key in he_verbrauch: - rb = d(computed.get(("rechnungsbetrag", col_key))) - hv = he_verbrauch.get(col_key, Decimal("0")) - computed[("eff_lhe_preis", col_key)] = safe_div(rb, hv) - - # ---- Row: Sonstiges (TEXT) based on Betrag ---- - # IF(Betrag=0,"--------------",IF(Betrag<0,"Gutschrift","Nachzahlung")) - sonstiges_text = {} - for col_key in he_verbrauch: - b = d(value_map.get(("betrag", col_key))) - if b == 0: - sonstiges_text[col_key] = "--------------" - elif b < 0: - sonstiges_text[col_key] = "Gutschrift" - else: - sonstiges_text[col_key] = "Nachzahlung" - # overwrite display values for non-editable computed rows - non_editable_formula_rows = { - "bezogen_menge", - "kaltg_warmfuell", - "anzahl_15", - "he_verbrauch", - "umlage_instandhaltung_1", - "lhe_verluste", - "umlage_heliumkosten_3", - "kosten_he_gas_bezug_4", - "rel_anteil", - "summe_anteile_1_4", - "umlage_personal_5", - "rechnungsbetrag", - "eff_lhe_preis", - } - - for (rk, ck), val in computed.items(): - if rk in non_editable_formula_rows: - value_map[(rk, ck)] = val - rows = [] - for row_key, label, unit in self.ROWS: - rows.append({ - "row_key": row_key, - "label": label, - "unit": unit, # 👈 ADD THIS - "editable": row_key in self.EDITABLE_ROW_KEYS, - "cells": [ - { - "col_key": col_key, - "value": value_map.get((row_key, col_key)), - } - for col_key, _ in self.COLUMNS - ] - }) - start_year = interval_year - start_month = interval_start - # end month = start + 5 months - end_total = (start_month - 1) + 5 - end_year = start_year + (end_total // 12) - end_month = (end_total % 12) + 1 - - start_date_str = f"01.{start_month:02d}.{start_year}" - last_day = calendar.monthrange(end_year, end_month)[1] - end_date_str = f"{last_day:02d}.{end_month:02d}.{end_year}" - - context["interval_text"] = f"{start_date_str} bis {end_date_str}" - context["sonstiges_text"] = sonstiges_text - context.update({ - "page_title": "Abrechnung", - "columns": self.COLUMNS, - "rows": rows, - "interval_year": interval_year, - "interval_start": interval_start, - }) - - return context -@method_decorator(csrf_exempt, name="dispatch") -class SaveAbrechnungCellsView(LoginRequiredMixin, View): - def post(self, request): - interval_year = request.session.get("halfyear_year") - interval_start = request.session.get("halfyear_start_month") - if not interval_year or not interval_start: - return JsonResponse({"ok": False, "error": "No halfyear interval selected."}, status=400) - - interval_year = int(interval_year) - interval_start = int(interval_start) - - try: - payload = json.loads(request.body.decode("utf-8")) - except Exception: - return JsonResponse({"ok": False, "error": "Invalid JSON."}, status=400) - - changes = payload.get("changes", []) - - for item in changes: - row_key = item.get("row_key") - col_key = item.get("col_key") - raw_val = item.get("value") - - # allow empty -> NULL - if raw_val in ("", None): - val = None - else: - try: - val = Decimal(str(raw_val).replace(",", ".")) - except Exception: - continue - - AbrechnungCell.objects.update_or_create( - interval_year=interval_year, - interval_start_month=interval_start, - row_key=row_key, - col_key=col_key, - defaults={"value": val}, - ) - - return JsonResponse({"ok": True}) -def build_halfyear_window(interval_year: int, interval_start_month: int): - window = [] - for offset in range(6): - total_index = (interval_start_month - 1) + offset - y = interval_year + (total_index // 12) - m = (total_index % 12) + 1 - window.append((y, m)) - return window -class SaveCellsView(View): - - def calculate_bottom_3_dependents(self, sheet): - updated_cells = [] - - def get_cell(row_idx, col_idx): - return Cell.objects.filter( - sheet=sheet, - table_type='bottom_3', - row_index=row_idx, - column_index=col_idx - ).first() - - def set_cell(row_idx, col_idx, value): - cell, created = Cell.objects.get_or_create( - sheet=sheet, - table_type='bottom_3', - row_index=row_idx, - column_index=col_idx, - defaults={'value': value} - ) - if not created and cell.value != value: - cell.value = value - cell.save() - elif created: - cell.save() - - updated_cells.append({ - 'id': cell.id, - 'value': str(cell.value) if cell.value is not None else '', - 'is_calculated': True, - }) - - def dec(x): - if x in (None, ''): - return Decimal('0') - try: - return Decimal(str(x)) - except Exception: - return Decimal('0') - - # ---- current summary ---- - cur_sum = MonthlySummary.objects.filter(sheet=sheet).first() - curr_k44 = dec(cur_sum.gesamtbestand_neu_lhe) if cur_sum else Decimal('0') - total_verb = dec(cur_sum.verbraucherverlust_lhe) if cur_sum else Decimal('0') - - # ---- previous month summary ---- - year, month = sheet.year, sheet.month - if month == 1: - prev_year, prev_month = year - 1, 12 - else: - prev_year, prev_month = year, month - 1 - - prev_sheet = MonthlySheet.objects.filter(year=prev_year, month=prev_month).first() - if prev_sheet: - prev_sum = MonthlySummary.objects.filter(sheet=prev_sheet).first() - prev_k44 = dec(prev_sum.gesamtbestand_neu_lhe) if prev_sum else Decimal('0') - else: - prev_k44 = Decimal('0') - - # ---- read editable inputs from bottom_3: F47,G47,I47,I50 ---- - def get_val(r, c): - cell = get_cell(r, c) - return dec(cell.value if cell else None) - - f47 = get_val(1, 0) - g47 = get_val(1, 1) - i47 = get_val(1, 2) - i50 = get_val(4, 2) - - # Now apply your formulas using prev_k44, curr_k44, total_verb, f47,g47,i47,i50 - # Row indices: 0..7 correspond to 46..53 - # col 3 = J, col 4 = K - - # Row 46 - k46 = prev_k44 - j46 = k46 * Decimal('0.75') - set_cell(0, 3, j46) - set_cell(0, 4, k46) - - # Row 47 - g47 = self._dec((get_cell(1, 1) or {}).value if get_cell(1, 1) else None) - i47 = self._dec((get_cell(1, 2) or {}).value if get_cell(1, 2) else None) - - j47 = g47 + i47 - k47 = (j47 / Decimal('0.75')) + g47 if j47 != 0 else g47 - - set_cell(1, 3, j47) - set_cell(1, 4, k47) - - # Row 48 - k48 = k46 + k47 - j48 = k48 * Decimal('0.75') - set_cell(2, 3, j48) - set_cell(2, 4, k48) - - # Row 49 - k49 = curr_k44 - j49 = k49 * Decimal('0.75') - set_cell(3, 3, j49) - set_cell(3, 4, k49) - - # Row 50 - j50 = i50 - k50 = j50 / Decimal('0.75') if j50 != 0 else Decimal('0') - set_cell(4, 3, j50) - set_cell(4, 4, k50) - - # Row 51 - k51 = k48 - k49 - k50 - j51 = k51 * Decimal('0.75') - set_cell(5, 3, j51) - set_cell(5, 4, k51) - - # Row 52 - k52 = total_verb - j52 = k52 * Decimal('0.75') - set_cell(6, 3, j52) - set_cell(6, 4, k52) - - # Row 53 - j53 = j51 - j52 - k53 = k51 - k52 - set_cell(7, 3, j53) - set_cell(7, 4, k53) - - return updated_cells - - - def _dec(self, value): - """Convert value to Decimal or return 0.""" - if value is None or value == '': - return Decimal('0') - try: - return Decimal(str(value)) - except Exception: - return Decimal('0') - - def post(self, request, *args, **kwargs): - """ - Handle AJAX saves from monthly_sheet.html - - Single-cell save: when cell_id is present (blur on one cell) - - Bulk save: when the 'Save All Cells' button is used (no cell_id) - """ - try: - sheet_id = request.POST.get('sheet_id') - if not sheet_id: - return JsonResponse({ - 'status': 'error', - 'message': 'Missing sheet_id' - }) - - sheet = MonthlySheet.objects.get(id=sheet_id) - - # -------- Single-cell update (blur) -------- - cell_id = request.POST.get('cell_id') - if cell_id: - value_raw = (request.POST.get('value') or '').strip() - - try: - cell = Cell.objects.get(id=cell_id, sheet=sheet) - except Cell.DoesNotExist: - return JsonResponse({ - 'status': 'error', - 'message': 'Cell not found' - }) - - # Convert value to Decimal or None - if value_raw == '': - new_value = None - else: - try: - # Allow comma or dot - value_clean = value_raw.replace(',', '.') - new_value = Decimal(value_clean) - except (InvalidOperation, ValueError): - # If conversion fails, treat as empty - new_value = None - - old_value = cell.value - cell.value = new_value - cell.save() - - updated_cells = [{ - 'id': cell.id, - 'value': '' if cell.value is None else str(cell.value), - 'is_calculated': cell.is_formula, # model field - }] - - # Recalculate dependents depending on table_type - if cell.table_type == 'top_left': - updated_cells.extend( - self.calculate_top_left_dependents(sheet, cell) - ) - elif cell.table_type == 'top_right': - updated_cells.extend( - self.calculate_top_right_dependents(sheet, cell) - ) - elif cell.table_type == 'bottom_1': - updated_cells.extend(self.calculate_bottom_1_dependents(sheet, cell)) - elif cell.table_type == 'bottom_3': - updated_cells.extend( - self.calculate_bottom_3_dependents(sheet) - ) - # bottom_1 / bottom_2 / bottom_3 currently have no formulas: - # they just save the new value. - updated_cells += self.calculate_bottom_3_dependents(sheet) - return JsonResponse({ - 'status': 'success', - 'updated_cells': updated_cells - }) - - - # -------- Bulk save (Save All button) -------- - return self.save_bulk_cells(request, sheet) - - except MonthlySheet.DoesNotExist: - return JsonResponse({ - 'status': 'error', - 'message': 'Sheet not found' - }) - except Exception as e: - # Generic safety net so the frontend sees an error message - return JsonResponse({ - 'status': 'error', - 'message': str(e) - }) - - # ... your other methods above ... - - # Update the calculate_top_right_dependents method in SaveCellsView class - - def calculate_top_right_dependents(self, sheet, changed_cell): - """ - Recalculate all dependent cells in the top-right table according to Excel formulas. - - Excel rows (4-20) -> 0-based indices (0-15) - Rows: - 0: Stand der Gaszähler (Vormonat) (Nm³) - shares for M3 clients - 1: Gasrückführung (Nm³) - 2: Rückführung flüssig (Lit. L-He) - 3: Sonderrückführungen (Lit. L-He) - editable - 4: Sammelrückführungen (Lit. L-He) - from helium_input groups - 5: Bestand in Kannen-1 (Lit. L-He) - editable, merged in pairs - 6: Summe Bestand (Lit. L-He) = row 5 - 7: Best. in Kannen Vormonat (Lit. L-He) - from previous month - 8: Bezug (Liter L-He) - from SecondTableEntry - 9: Rückführ. Soll (Lit. L-He) - calculated - 10: Verluste (Soll-Rückf.) (Lit. L-He) - calculated - 11: Füllungen warm (Lit. L-He) - from SecondTableEntry warm outputs - 12: Kaltgas Rückgabe (Lit. L-He) – Faktor - calculated - 13: Faktor 0.06 - fixed - 14: Verbraucherverluste (Liter L-He) - calculated - 15: % - calculated - """ - from decimal import Decimal - from django.db.models import Sum, Count - from django.db.models.functions import Coalesce - from .models import Client, Cell, ExcelEntry, SecondTableEntry - - TOP_RIGHT_CLIENTS = [ - "Dr. Fohrer", # L - "AG Buntk.", # M (merged with L) - "AG Alff", # N - "AG Gutfl.", # O (merged with N) - "M3 Thiele", # P - "M3 Buntkowsky", # Q - "M3 Gutfleisch", # R - ] - - # Define merged pairs - MERGED_PAIRS = [ - ("Dr. Fohrer", "AG Buntk."), # L and M are merged - ("AG Alff", "AG Gutfl."), # N and O are merged - ] - - # M3 clients (not merged, calculated individually) - M3_CLIENTS = ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"] - - # Groups for Sammelrückführungen (helium_input) - HELIUM_INPUT_GROUPS = { - "fohrer_buntk": ["Dr. Fohrer", "AG Buntk."], - "alff_gutfl": ["AG Alff", "AG Gutfl."], - "m3": ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"], - } - - year = sheet.year - month = sheet.month - factor = Decimal('0.06') # Fixed factor from Excel - updated_cells = [] - - # Helper functions - def get_val(client_name, row_idx): - """Get cell value for a client and row""" - try: - client = Client.objects.get(name=client_name) - col_idx = TOP_RIGHT_CLIENTS.index(client_name) - cell = Cell.objects.filter( - sheet=sheet, - table_type='top_right', - client=client, - row_index=row_idx, - column_index=col_idx - ).first() - if cell and cell.value is not None: - return Decimal(str(cell.value)) - except (Client.DoesNotExist, ValueError, KeyError): - pass - return Decimal('0') - - def set_val(client_name, row_idx, value, is_calculated=True): - """Set cell value for a client and row""" - try: - client = Client.objects.get(name=client_name) - col_idx = TOP_RIGHT_CLIENTS.index(client_name) - - cell, created = Cell.objects.get_or_create( - sheet=sheet, - table_type='top_right', - client=client, - row_index=row_idx, - column_index=col_idx, - defaults={'value': value} - ) - - # Only update if value changed - if not created and cell.value != value: - cell.value = value - cell.save() - elif created: - cell.save() - - updated_cells.append({ - 'id': cell.id, - 'value': str(cell.value) if cell.value else '', - 'is_calculated': is_calculated - }) - - return True - except (Client.DoesNotExist, ValueError): - return False - - # 1. Update Summe Bestand (row 6) from Bestand in Kannen-1 (row 5) - # For merged pairs: copy value from changed cell to its pair - if changed_cell and changed_cell.table_type == 'top_right': - if changed_cell.row_index == 5: # Bestand in Kannen-1 - client_name = changed_cell.client.name - new_value = changed_cell.value - - # Check if this client is in a merged pair - for pair in MERGED_PAIRS: - if client_name in pair: - # Update both clients in the pair - for client_in_pair in pair: - if client_in_pair != client_name: - set_val(client_in_pair, 5, new_value, is_calculated=False) - break - - # 2. For all clients: Set Summe Bestand (row 6) = Bestand in Kannen-1 (row 5) - for client_name in TOP_RIGHT_CLIENTS: - bestand_value = get_val(client_name, 5) - set_val(client_name, 6, bestand_value, is_calculated=True) - - # 3. Update Sammelrückführungen (row 4) from helium_input groups - for group_name, client_names in HELIUM_INPUT_GROUPS.items(): - # Get total helium_input for this group - clients_in_group = Client.objects.filter(name__in=client_names) - total_helium = ExcelEntry.objects.filter( - client__in=clients_in_group, - date__year=year, - date__month=month - ).aggregate(total=Coalesce(Sum('lhe_ges'), Decimal('0')))['total'] - - # Set same value for all clients in group - for client_name in client_names: - set_val(client_name, 4, total_helium, is_calculated=True) - - # 4. Calculate Rückführung flüssig (row 2) - # For merged pairs: =L8 for Dr. Fohrer/AG Buntk., =N8 for AG Alff/AG Gutfl. - # For M3 clients: =$P$8 * P4, $P$8 * Q4, $P$8 * R4 - - # Get Sammelrückführungen values for groups - sammel_fohrer_buntk = get_val("Dr. Fohrer", 4) # L8 - sammel_alff_gutfl = get_val("AG Alff", 4) # N8 - sammel_m3_group = get_val("M3 Thiele", 4) # P8 (same for all M3) - - # For merged pairs - set_val("Dr. Fohrer", 2, sammel_fohrer_buntk, is_calculated=True) - set_val("AG Buntk.", 2, sammel_fohrer_buntk, is_calculated=True) - set_val("AG Alff", 2, sammel_alff_gutfl, is_calculated=True) - set_val("AG Gutfl.", 2, sammel_alff_gutfl, is_calculated=True) - - # For M3 clients: =$P$8 * column4 (Stand der Gaszähler) - for m3_client in M3_CLIENTS: - stand_value = get_val(m3_client, 0) # Stand der Gaszähler (row 0) - rueck_value = sammel_m3_group * stand_value - set_val(m3_client, 2, rueck_value, is_calculated=True) - - # 5. Calculate Füllungen warm (row 11) from SecondTableEntry warm outputs - # 5. Calculate Füllungen warm (row 11) as NUMBER of warm fillings - # (sum of 1s where each warm SecondTableEntry is one filling) - for client_name in TOP_RIGHT_CLIENTS: - client = Client.objects.get(name=client_name) - warm_count = SecondTableEntry.objects.filter( - client=client, - date__year=year, - date__month=month, - is_warm=True - ).aggregate( - total=Coalesce(Count('id'), 0) - )['total'] - - # store as Decimal so later formulas (warm * 15) still work nicely - warm_value = Decimal(warm_count) - set_val(client_name, 11, warm_value, is_calculated=True) - # 6. Set Faktor row (13) to 0.06 - for client_name in TOP_RIGHT_CLIENTS: - set_val(client_name, 13, factor, is_calculated=True) - - # 6a. Recalculate Stand der Gaszähler (row 0) for the merged pairs - # according to Excel: - # L4 = L13 / (L13 + M13), M4 = M13 / (L13 + M13) - # N4 = N13 / (N13 + O13), O4 = O13 / (N13 + O13) - - # Pair 1: Dr. Fohrer / AG Buntk. - bezug_dr = get_val("Dr. Fohrer", 8) # L13 - bezug_buntk = get_val("AG Buntk.", 8) # M13 - total_pair1 = bezug_dr + bezug_buntk - - if total_pair1 != 0: - set_val("Dr. Fohrer", 0, bezug_dr / total_pair1, is_calculated=True) - set_val("AG Buntk.", 0, bezug_buntk / total_pair1, is_calculated=True) - else: - # if no Bezug, both shares are 0 - set_val("Dr. Fohrer", 0, Decimal('0'), is_calculated=True) - set_val("AG Buntk.", 0, Decimal('0'), is_calculated=True) - - # Pair 2: AG Alff / AG Gutfl. - bezug_alff = get_val("AG Alff", 8) # N13 - bezug_gutfl = get_val("AG Gutfl.", 8) # O13 - total_pair2 = bezug_alff + bezug_gutfl - - if total_pair2 != 0: - set_val("AG Alff", 0, bezug_alff / total_pair2, is_calculated=True) - set_val("AG Gutfl.", 0, bezug_gutfl / total_pair2, is_calculated=True) - else: - set_val("AG Alff", 0, Decimal('0'), is_calculated=True) - set_val("AG Gutfl.", 0, Decimal('0'), is_calculated=True) - - - # 7. Calculate all other dependent rows for merged pairs - for pair in MERGED_PAIRS: - client1, client2 = pair - - # Get values for the pair - bezug1 = get_val(client1, 8) # Bezug client1 - bezug2 = get_val(client2, 8) # Bezug client2 - total_bezug = bezug1 + bezug2 # L13+M13 or N13+O13 - - summe_bestand = get_val(client1, 6) # L11 or N11 (merged, same value) - best_vormonat = get_val(client1, 7) # L12 or N12 (merged, same value) - rueck_fl = get_val(client1, 2) # L6 or N6 (merged, same value) - warm1 = get_val(client1, 11) # L16 or N16 - warm2 = get_val(client2, 11) # M16 or O16 - total_warm = warm1 + warm2 # L16+M16 or N16+O16 - - # Calculate Rückführ. Soll (row 9) - # = L13+M13 - L11 + L12 for first pair - # = N13+O13 - N11 + N12 for second pair - rueck_soll = total_bezug - summe_bestand + best_vormonat - set_val(client1, 9, rueck_soll, is_calculated=True) - set_val(client2, 9, rueck_soll, is_calculated=True) - - # Calculate Verluste (row 10) = Rückführ. Soll - Rückführung flüssig - verluste = rueck_soll - rueck_fl - set_val(client1, 10, verluste, is_calculated=True) - set_val(client2, 10, verluste, is_calculated=True) - - # Calculate Kaltgas Rückgabe (row 12) - # = (L13+M13)*$A18 + (L16+M16)*15 - kaltgas = (total_bezug * factor) + (total_warm * Decimal('15')) - set_val(client1, 12, kaltgas, is_calculated=True) - set_val(client2, 12, kaltgas, is_calculated=True) - - # Calculate Verbraucherverluste (row 14) = Verluste - Kaltgas - verbrauch = verluste - kaltgas - set_val(client1, 14, verbrauch, is_calculated=True) - set_val(client2, 14, verbrauch, is_calculated=True) - - # Calculate % (row 15) = Verbraucherverluste / (L13+M13) - if total_bezug != 0: - prozent = verbrauch / total_bezug - else: - prozent = Decimal('0') - set_val(client1, 15, prozent, is_calculated=True) - set_val(client2, 15, prozent, is_calculated=True) - - # 8. Calculate all dependent rows for M3 clients (individual calculations) - for m3_client in M3_CLIENTS: - # Get individual values - bezug = get_val(m3_client, 8) # Bezug for this M3 client - summe_bestand = get_val(m3_client, 6) # Summe Bestand - best_vormonat = get_val(m3_client, 7) # Best. in Kannen Vormonat - rueck_fl = get_val(m3_client, 2) # Rückführung flüssig - warm = get_val(m3_client, 11) # Füllungen warm - - # Calculate Rückführ. Soll (row 9) = Bezug - Summe Bestand + Best. Vormonat - rueck_soll = bezug - summe_bestand + best_vormonat - set_val(m3_client, 9, rueck_soll, is_calculated=True) - - # Calculate Verluste (row 10) = Rückführ. Soll - Rückführung flüssig - verluste = rueck_soll - rueck_fl - set_val(m3_client, 10, verluste, is_calculated=True) - - # Calculate Kaltgas Rückgabe (row 12) = Bezug * factor + warm * 15 - kaltgas = (bezug * factor) + (warm * Decimal('15')) - set_val(m3_client, 12, kaltgas, is_calculated=True) - - # Calculate Verbraucherverluste (row 14) = Verluste - Kaltgas - verbrauch = verluste - kaltgas - set_val(m3_client, 14, verbrauch, is_calculated=True) - - # Calculate % (row 15) = Verbraucherverluste / Bezug - if bezug != 0: - prozent = verbrauch / bezug - else: - prozent = Decimal('0') - set_val(m3_client, 15, prozent, is_calculated=True) - - return updated_cells - - - - def calculate_bottom_1_dependents(self, sheet, changed_cell): - """ - Recalculate Bottom Table 1 (table_type='bottom_1'). - - Layout (row_index 0–9, col_index 0–4): - - Rows 0–8: - 0: Batterie 1 - 1: 2 - 2: 3 - 3: 4 - 4: 5 - 5: 6 - 6: 2 Bündel - 7: 2 Ballone - 8: Reingasspeicher - - Row 9: - 9: Gasbestand (totals row) - - Columns: - 0: Volumen (fixed values, not editable) - 1: bar (editable for rows 0–8) - 2: korrigiert = bar / (bar/2000 + 1) - 3: Nm³ = Volumen * korrigiert - 4: Lit. LHe = Nm³ / 0.75 - - Row 9: - - Volumen (col 0): empty - - bar (col 1): empty - - korrigiert (col 2): empty - - Nm³ (col 3): SUM Nm³ rows 0–8 - - Lit. LHe (col 4): SUM Lit. LHe rows 0–8 - """ - from decimal import Decimal, InvalidOperation - from .models import Cell - - updated_cells = [] - - DATA_ROWS = list(range(0, 9)) # 0–8 - TOTAL_ROW = 9 - - COL_VOL = 0 - COL_BAR = 1 - COL_KORR = 2 - COL_NM3 = 3 - COL_LHE = 4 - - # Fixed Volumen values for rows 0–8 - VOLUMES = [ - Decimal("2.4"), # row 0 - Decimal("5.1"), # row 1 - Decimal("4.0"), # row 2 - Decimal("1.0"), # row 3 - Decimal("4.0"), # row 4 - Decimal("0.4"), # row 5 - Decimal("1.2"), # row 6 - Decimal("20.0"), # row 7 - Decimal("5.0"), # row 8 - ] - - def get_cell(row_idx, col_idx): - return Cell.objects.filter( - sheet=sheet, - table_type="bottom_1", - row_index=row_idx, - column_index=col_idx, - ).first() - - def set_calc(row_idx, col_idx, value): - """ - Set a calculated cell and add it to updated_cells. - If value is None, we clear the cell. - """ - cell = get_cell(row_idx, col_idx) - if not cell: - cell = Cell( - sheet=sheet, - table_type="bottom_1", - row_index=row_idx, - column_index=col_idx, - value=value, - ) - else: - cell.value = value - cell.save() - - updated_cells.append( - { - "id": cell.id, - "value": "" if cell.value is None else str(cell.value), - "is_calculated": True, - } - ) - - # ---------- Rows 0–8: per-gasspeicher calculations ---------- - for row_idx in DATA_ROWS: - bar_cell = get_cell(row_idx, COL_BAR) - - # Volumen: fixed constant or, if present, value from DB - vol = VOLUMES[row_idx] - vol_cell = get_cell(row_idx, COL_VOL) - if vol_cell and vol_cell.value is not None: - try: - vol = Decimal(str(vol_cell.value)) - except (InvalidOperation, ValueError): - # fall back to fixed constant - vol = VOLUMES[row_idx] - - bar = None - try: - if bar_cell and bar_cell.value is not None: - bar = Decimal(str(bar_cell.value)) - except (InvalidOperation, ValueError): - bar = None - - # korrigiert = bar / (bar/2000 + 1) - if bar is not None and bar != 0: - korr = bar / (bar / Decimal("2000") + Decimal("1")) - else: - korr = None - - # Nm³ = Volumen * korrigiert - if korr is not None: - nm3 = vol * korr - else: - nm3 = None - - # Lit. LHe = Nm³ / 0.75 - if nm3 is not None: - lit_lhe = nm3 / Decimal("0.75") - else: - lit_lhe = None - - # Write calculated cells back (NOT Volumen or bar) - set_calc(row_idx, COL_KORR, korr) - set_calc(row_idx, COL_NM3, nm3) - set_calc(row_idx, COL_LHE, lit_lhe) - - # ---------- Row 9: totals (Gasbestand) ---------- - total_nm3 = Decimal("0") - total_lhe = Decimal("0") - has_nm3 = False - has_lhe = False - - for row_idx in DATA_ROWS: - nm3_cell = get_cell(row_idx, COL_NM3) - if nm3_cell and nm3_cell.value is not None: - try: - total_nm3 += Decimal(str(nm3_cell.value)) - has_nm3 = True - except (InvalidOperation, ValueError): - pass - - lhe_cell = get_cell(row_idx, COL_LHE) - if lhe_cell and lhe_cell.value is not None: - try: - total_lhe += Decimal(str(lhe_cell.value)) - has_lhe = True - except (InvalidOperation, ValueError): - pass - - # Volumen (0), bar (1), korrigiert (2) on total row stay empty - set_calc(TOTAL_ROW, COL_KORR, None) # explicitly clear korrigiert - set_calc(TOTAL_ROW, COL_NM3, total_nm3 if has_nm3 else None) - set_calc(TOTAL_ROW, COL_LHE, total_lhe if has_lhe else None) - - return updated_cells - - - - - - def calculate_top_left_dependents(self, sheet, changed_cell): - """Calculate dependent cells in top_left table""" - from decimal import Decimal - from django.db.models import Sum - from django.db.models.functions import Coalesce - - client_id = changed_cell.client_id - updated_cells = [] - - # Get all cells for this client in top_left table - client_cells = Cell.objects.filter( - sheet=sheet, - table_type='top_left', - client_id=client_id - ) - - # Create a dict for easy access - cell_dict = {} - for cell in client_cells: - cell_dict[cell.row_index] = cell - - # Get values with safe defaults - def get_cell_value(row_idx): - cell = cell_dict.get(row_idx) - if cell and cell.value is not None: - try: - return Decimal(str(cell.value)) - except: - return Decimal('0') - return Decimal('0') - - # 1. B5 = IF(B3>0; B3-B4; 0) - Gasrückführung - b3 = get_cell_value(0) # row_index 0 = Excel B3 (UI row 1) - b4 = get_cell_value(1) # row_index 1 = Excel B4 (UI row 2) - - # Calculate B5 - if b3 > 0: - b5_value = b3 - b4 - if b5_value < 0: - b5_value = Decimal('0') - else: - b5_value = Decimal('0') - - # Update B5 - FORCE update even if value appears the same - b5_cell = cell_dict.get(2) # row_index 2 = Excel B5 (UI row 3) - if b5_cell: - # Always update B5 when B3 or B4 changes - b5_cell.value = b5_value - b5_cell.save() - updated_cells.append({ - 'id': b5_cell.id, - 'value': str(b5_cell.value), - 'is_calculated': True - }) - - # 2. B6 = B5 / 0.75 - Rückführung flüssig - b6_value = b5_value / Decimal('0.75') - b6_cell = cell_dict.get(3) # row_index 3 = Excel B6 (UI row 4) - if b6_cell: - b6_cell.value = b6_value - b6_cell.save() - updated_cells.append({ - 'id': b6_cell.id, - 'value': str(b6_cell.value), - 'is_calculated': True - }) - - # 3. B9 = B7 + B8 - Summe Bestand - b7 = get_cell_value(4) # row_index 4 = Excel B7 (UI row 5) - b8 = get_cell_value(5) # row_index 5 = Excel B8 (UI row 6) - b9_value = b7 + b8 - b9_cell = cell_dict.get(6) # row_index 6 = Excel B9 (UI row 7) - if b9_cell: - b9_cell.value = b9_value - b9_cell.save() - updated_cells.append({ - 'id': b9_cell.id, - 'value': str(b9_cell.value), - 'is_calculated': True - }) - - # 4. B11 = Sum of LHe Output from SecondTableEntry for this client/month - Bezug - from .models import SecondTableEntry - client = changed_cell.client - - # Calculate total LHe output for this client in this month - # 4. B11 = Bezug (Liter L-He) - # For start sheet: manual entry, for other sheets: auto-calculated from SecondTableEntry - from .models import SecondTableEntry - client = changed_cell.client - - b11_cell = cell_dict.get(8) # row_index 8 = Excel B11 (UI row 9) - - # Check if this is the start sheet (2025-01) - is_start_sheet = (sheet.year == 2025 and sheet.month == 1) - - # Get the B11 value for calculations - b11_value = Decimal('0') - - if is_start_sheet: - # For start sheet, keep whatever value is already there (manual entry) - if b11_cell and b11_cell.value is not None: - b11_value = Decimal(str(b11_cell.value)) - else: - # For non-start sheets: auto-calculate from SecondTableEntry - lhe_output_sum = SecondTableEntry.objects.filter( - client=client, - date__year=sheet.year, - date__month=sheet.month - ).aggregate( - total=Coalesce(Sum('lhe_output'), Decimal('0')) - )['total'] - - b11_value = lhe_output_sum - - if b11_cell and (b11_cell.value != b11_value or b11_cell.value is None): - b11_cell.value = b11_value - b11_cell.save() - updated_cells.append({ - 'id': b11_cell.id, - 'value': str(b11_cell.value), - 'is_calculated': True # Calculated from SecondTableEntry - }) - - # 5. B12 = B11 + B10 - B9 - Rückführ. Soll - b10 = get_cell_value(7) # row_index 7 = Excel B10 (UI row 8) - b12_value = b11_value + b10 - b9_value # Use b11_value instead of lhe_output_sum - b12_cell = cell_dict.get(9) # row_index 9 = Excel B12 (UI row 10) - if b12_cell: - b12_cell.value = b12_value - b12_cell.save() - updated_cells.append({ - 'id': b12_cell.id, - 'value': str(b12_cell.value), - 'is_calculated': True - }) - - # 6. B13 = B12 - B6 - Verluste (Soll-Rückf.) - b13_value = b12_value - b6_value - b13_cell = cell_dict.get(10) # row_index 10 = Excel B13 (UI row 11) - if b13_cell: - b13_cell.value = b13_value - b13_cell.save() - updated_cells.append({ - 'id': b13_cell.id, - 'value': str(b13_cell.value), - 'is_calculated': True - }) - - # 7. B14 = Count of warm outputs - warm_count = SecondTableEntry.objects.filter( - client=client, - date__year=sheet.year, - date__month=sheet.month, - is_warm=True - ).count() - - b14_cell = cell_dict.get(11) # row_index 11 = Excel B14 (UI row 12) - if b14_cell: - b14_cell.value = Decimal(str(warm_count)) - b14_cell.save() - updated_cells.append({ - 'id': b14_cell.id, - 'value': str(b14_cell.value), - 'is_calculated': True - }) - - # 8. B15 = IF(B11>0; B11 * factor + B14 * 15; 0) - Kaltgas Rückgabe - factor = get_cell_value(13) # row_index 13 = Excel B16 (Faktor) (UI row 14) - if factor == 0: - factor = Decimal('0.06') # default factor - - if b11_value > 0: # Use b11_value - b15_value = b11_value * factor + Decimal(str(warm_count)) * Decimal('15') - else: - b15_value = Decimal('0') - - b15_cell = cell_dict.get(12) # row_index 12 = Excel B15 (UI row 13) - if b15_cell: - b15_cell.value = b15_value - b15_cell.save() - updated_cells.append({ - 'id': b15_cell.id, - 'value': str(b15_cell.value), - 'is_calculated': True - }) - - # 9. B17 = B13 - B15 - Verbraucherverluste - b17_value = b13_value - b15_value - b17_cell = cell_dict.get(14) # row_index 14 = Excel B17 (UI row 15) - if b17_cell: - b17_cell.value = b17_value - b17_cell.save() - updated_cells.append({ - 'id': b17_cell.id, - 'value': str(b17_cell.value), - 'is_calculated': True - }) - - # 10. B18 = IF(B11=0; 0; B17/B11) - % - if b11_value == 0: # Use b11_value - b18_value = Decimal('0') - else: - b18_value = b17_value / b11_value # Use b11_value - - b18_cell = cell_dict.get(15) # row_index 15 = Excel B18 (UI row 16) - if b18_cell: - b18_cell.value = b18_value - b18_cell.save() - updated_cells.append({ - 'id': b18_cell.id, - 'value': str(b18_cell.value), - 'is_calculated': True - }) - - return updated_cells - def save_bulk_cells(self, request, sheet): - """Original bulk save logic (for backward compatibility)""" - # Get all cell updates - cell_updates = {} - for key, value in request.POST.items(): - if key.startswith('cell_'): - cell_id = key.replace('cell_', '') - cell_updates[cell_id] = value - - # Update cells and track which ones changed - updated_cells = [] - changed_clients = set() - - for cell_id, new_value in cell_updates.items(): - try: - cell = Cell.objects.get(id=cell_id, sheet=sheet) - old_value = cell.value - - # Convert new value - try: - if new_value.strip(): - cell.value = Decimal(new_value) - else: - cell.value = None - except Exception: - cell.value = None - - # Only save if value changed - if cell.value != old_value: - cell.save() - updated_cells.append({ - 'id': cell.id, - 'value': str(cell.value) if cell.value else '' - }) - # bottom_3 has no client, so this will just add None for those cells, - # which is harmless. Top-left cells still add their real client_id. - changed_clients.add(cell.client_id) - - except Cell.DoesNotExist: - continue - - # Recalculate for each changed client (top-left tables) - for client_id in changed_clients: - if client_id is not None: - self.recalculate_top_left_table(sheet, client_id) - - # --- NEW: recalc bottom_3 for the whole sheet, independent of clients --- - bottom3_updates = self.calculate_bottom_3_dependents(sheet) - - # Get all updated cells for response (top-left) - all_updated_cells = [] - for client_id in changed_clients: - if client_id is None: - continue # skip bottom_3 / non-client cells - client_cells = Cell.objects.filter( - sheet=sheet, - client_id=client_id - ) - for cell in client_cells: - all_updated_cells.append({ - 'id': cell.id, - 'value': str(cell.value) if cell.value else '', - 'is_calculated': cell.is_formula - }) - - # Add bottom_3 recalculated cells (J46..K53, etc.) - all_updated_cells.extend(bottom3_updates) - - return JsonResponse({ - 'status': 'success', - 'message': f'Saved {len(updated_cells)} cells', - 'updated_cells': all_updated_cells - }) - - - def recalculate_top_left_table(self, sheet, client_id): - """Recalculate the top-left table for a specific client""" - from decimal import Decimal - - # Get all cells for this client in top_left table - cells = Cell.objects.filter( - sheet=sheet, - table_type='top_left', - client_id=client_id - ).order_by('row_index') - - # Create a dictionary of cell values - cell_dict = {} - for cell in cells: - cell_dict[cell.row_index] = cell - - # Excel logic implementation for top-left table - # B3 (row_index 0) = Stand der Gaszähler (Nm³) - manual - # B4 (row_index 1) = Stand der Gaszähler (Vormonat) (Nm³) - from previous sheet - - # Get B3 and B4 - b3_cell = cell_dict.get(0) # UI Row 3 - b4_cell = cell_dict.get(1) # UI Row 4 - - if b3_cell and b3_cell.value and b4_cell and b4_cell.value: - # B5 = IF(B3>0; B3-B4; 0) - b3 = Decimal(str(b3_cell.value)) - b4 = Decimal(str(b4_cell.value)) - - if b3 > 0: - b5 = b3 - b4 - if b5 < 0: - b5 = Decimal('0') - else: - b5 = Decimal('0') - - # Update B5 (row_index 2) - b5_cell = cell_dict.get(2) - if b5_cell and (b5_cell.value != b5 or b5_cell.value is None): - b5_cell.value = b5 - b5_cell.save() - - # B6 = B5 / 0.75 (row_index 3) - b6 = b5 / Decimal('0.75') - b6_cell = cell_dict.get(3) - if b6_cell and (b6_cell.value != b6 or b6_cell.value is None): - b6_cell.value = b6 - b6_cell.save() - - # Get previous month's sheet for B10 - if sheet.month == 1: - prev_year = sheet.year - 1 - prev_month = 12 - else: - prev_year = sheet.year - prev_month = sheet.month - 1 - - prev_sheet = MonthlySheet.objects.filter( - year=prev_year, - month=prev_month - ).first() - - if prev_sheet: - # Get B9 from previous sheet (row_index 7 in previous) - prev_b9 = Cell.objects.filter( - sheet=prev_sheet, - table_type='top_left', - client_id=client_id, - row_index=7 # UI Row 9 - ).first() - - if prev_b9 and prev_b9.value: - # Update B10 in current sheet (row_index 8) - b10_cell = cell_dict.get(8) - if b10_cell and (b10_cell.value != prev_b9.value or b10_cell.value is None): - b10_cell.value = prev_b9.value - b10_cell.save() -@method_decorator(csrf_exempt, name='dispatch') -class SaveMonthSummaryView(View): - """ - Saves per-month summary values such as K44 (Gesamtbestand neu). - Called from JS after 'Save All' finishes. - """ - - def post(self, request, *args, **kwargs): - try: - data = json.loads(request.body.decode('utf-8')) - except json.JSONDecodeError: - return JsonResponse( - {'success': False, 'message': 'Invalid JSON'}, - status=400 - ) - - sheet_id = data.get('sheet_id') - if not sheet_id: - return JsonResponse( - {'success': False, 'message': 'Missing sheet_id'}, - status=400 - ) - - try: - sheet = MonthlySheet.objects.get(id=sheet_id) - except MonthlySheet.DoesNotExist: - return JsonResponse( - {'success': False, 'message': 'Sheet not found'}, - status=404 - ) - - # More tolerant decimal conversion: accepts "123.45" and "123,45" - def to_decimal(value): - if value is None: - return None - s = str(value).strip() - if s == '': - return None - s = s.replace(',', '.') - try: - return Decimal(s) - except (InvalidOperation, ValueError): - # Debug: show what failed in the dev server console - print("SaveMonthSummaryView to_decimal failed for:", repr(value)) - return None - - raw_k44 = data.get('gesamtbestand_neu_lhe') - raw_gas = data.get('gasbestand_lhe') - raw_verb = data.get('verbraucherverlust_lhe') - - gesamtbestand_neu_lhe = to_decimal(raw_k44) - gasbestand_lhe = to_decimal(raw_gas) - verbraucherverlust_lhe = to_decimal(raw_verb) - - summary, created = MonthlySummary.objects.get_or_create(sheet=sheet) - - if gesamtbestand_neu_lhe is not None: - summary.gesamtbestand_neu_lhe = gesamtbestand_neu_lhe - if gasbestand_lhe is not None: - summary.gasbestand_lhe = gasbestand_lhe - if verbraucherverlust_lhe is not None: - summary.verbraucherverlust_lhe = verbraucherverlust_lhe - - summary.save() - - # Small debug output so you can see in the server console what was saved - print( - f"Saved MonthlySummary for {sheet.year}-{sheet.month:02d}: " - f"K44={summary.gesamtbestand_neu_lhe}, " - f"Gasbestand={summary.gasbestand_lhe}, " - f"Verbraucherverlust={summary.verbraucherverlust_lhe}" - ) - - return JsonResponse({'success': True}) - - - -# Calculate View (placeholder for calculations) -class CalculateView(View): - def post(self, request): - # This will be implemented when you provide calculation rules - return JsonResponse({ - 'status': 'success', - 'message': 'Calculation endpoint ready' - }) - -# Summary Sheet View -class SummarySheetView(TemplateView): - template_name = 'summary_sheet.html' - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - start_month = int(self.kwargs.get('start_month', 1)) - year = int(self.kwargs.get('year', datetime.now().year)) - - # Get 6 monthly sheets - months = [(year, m) for m in range(start_month, start_month + 6)] - sheets = MonthlySheet.objects.filter( - year=year, - month__in=list(range(start_month, start_month + 6)) - ).order_by('month') - - # Aggregate data across months - summary_data = self.calculate_summary(sheets) - - context.update({ - 'year': year, - 'start_month': start_month, - 'end_month': start_month + 5, - 'sheets': sheets, - 'clients': Client.objects.all(), - 'summary_data': summary_data, - }) - return context - - def calculate_summary(self, sheets): - """Calculate totals across 6 months""" - summary = {} - - for client in Client.objects.all(): - client_total = 0 - for sheet in sheets: - # Get specific cells and sum - cells = sheet.cells.filter( - client=client, - table_type='top_left', - row_index=0 # Example: first row - ) - for cell in cells: - if cell.value: - try: - client_total += float(cell.value) - except (ValueError, TypeError): - continue - - summary[client.id] = client_total - - return summary - -# Existing views below (keep all your existing functions) -def clients_list(request): - # --- Clients for the yearly summary table --- - clients = Client.objects.all() - - # --- Available years for output data (same as before) --- - available_years_qs = SecondTableEntry.objects.dates('date', 'year', order='DESC') - available_years = [y.year for y in available_years_qs] - - # === 1) Year used for the "Helium Output Yearly Summary" table === - # Uses ?year=... in the dropdown - year_param = request.GET.get('year') - if year_param: - selected_year = int(year_param) - else: - selected_year = available_years[0] if available_years else datetime.now().year - - # === 2) GLOBAL half-year interval (shared with other pages) ======= - # Try GET params first - interval_year_param = request.GET.get('interval_year') - start_month_param = request.GET.get('interval_start_month') - - # Fallbacks from session - session_year = request.session.get('halfyear_year') - session_start_month = request.session.get('halfyear_start_month') - - # Determine final interval_year - if interval_year_param: - interval_year = int(interval_year_param) - elif session_year: - interval_year = int(session_year) - else: - # default: same as selected_year for summary - interval_year = selected_year - - # Determine final interval_start_month - if start_month_param: - interval_start_month = int(start_month_param) - elif session_start_month: - interval_start_month = int(session_start_month) - else: - interval_start_month = 1 # default Jan - - # Store back into the session so other views can read them - request.session['halfyear_year'] = interval_year - request.session['halfyear_start_month'] = interval_start_month - - # === 3) Build a 6-month window, allowing wrap into the next year === - # Example: interval_year=2025, interval_start_month=12 - # window = [(2025,12), (2026,1), (2026,2), (2026,3), (2026,4), (2026,5)] - window = [] - for offset in range(6): - total_index = (interval_start_month - 1) + offset # 0-based index - y = interval_year + (total_index // 12) - m = (total_index % 12) + 1 - window.append((y, m)) - - # === 4) Totals per client in that 6-month window ================== - monthly_data = [] - for client in clients: - monthly_totals = [] - for (y, m) in window: - total = SecondTableEntry.objects.filter( - client=client, - date__year=y, - date__month=m - ).aggregate( - total=Coalesce( - Sum('lhe_output'), - Value(0, output_field=DecimalField()) - ) - )['total'] - monthly_totals.append(total) - - monthly_data.append({ - 'client': client, - 'monthly_totals': monthly_totals, - 'year_total': sum(monthly_totals), - }) - - # === 5) Month labels for the header (only the 6-month window) ===== - month_labels = [calendar.month_abbr[m] for (y, m) in window] - - # === 6) FINALLY: return the response ============================== - return render(request, 'clients_table.html', { - 'available_years': available_years, - 'current_year': selected_year, # used by year dropdown - 'interval_year': interval_year, # used by "Global 6-Month Interval" form - 'interval_start_month': interval_start_month, - 'months': month_labels, - 'monthly_data': monthly_data, - }) - - # === 5) Month labels for the header (only the 6-month window) ===== - month_labels = [calendar.month_abbr[m] for (y, m) in window] - - -def set_halfyear_interval(request): - if request.method == 'POST': - year = int(request.POST.get('year')) - start_month = int(request.POST.get('start_month')) - - request.session['halfyear_year'] = year - request.session['halfyear_start_month'] = start_month - - return redirect(request.META.get('HTTP_REFERER', 'clients_list')) - - return redirect('clients_list') -# Table One View (ExcelEntry) -def table_one_view(request): - from .models import ExcelEntry, Client, Institute - - # --- Base queryset for the main Helium Input table --- - base_entries = ExcelEntry.objects.all().select_related('client', 'client__institute') - - # Read the global 6-month interval from the session - interval_year = request.session.get('halfyear_year') - interval_start = request.session.get('halfyear_start_month') - - if interval_year and interval_start: - interval_year = int(interval_year) - interval_start = int(interval_start) - - # Build the same 6-month window as on the main page (can cross year) - window = [] - for offset in range(6): - total_index = (interval_start - 1) + offset # 0-based - y = interval_year + (total_index // 12) - m = (total_index % 12) + 1 - window.append((y, m)) - - # Build Q filter: (year=m_year AND month=m_month) for any of those 6 - q = Q() - for (y, m) in window: - q |= Q(date__year=y, date__month=m) - - entries_table1 = base_entries.filter(q).order_by('-date') - else: - # Fallback: if no global interval yet, show everything - entries_table1 = base_entries.order_by('-date') - clients = Client.objects.all().select_related('institute') - institutes = Institute.objects.all() - - # ---- Overview filters ---- - # years present in ExcelEntry.date - year_qs = ExcelEntry.objects.dates('date', 'year', order='DESC') - available_years = [d.year for d in year_qs] - - # default year/start month - # default year/start month (if no global interval yet) - if available_years: - default_year = available_years[0] # newest year in ExcelEntry - else: - default_year = timezone.now().year - - # 🔸 Read global half-year interval from session (set on main page) - session_year = request.session.get('halfyear_year') - session_start = request.session.get('halfyear_start_month') - - # If the user has set a global interval, use it. - # Otherwise fall back to default year / January. - year = int(session_year) if session_year else int(default_year) - start_month = int(session_start) if session_start else 1 - - - # six-month window - # --- Build a 6-month window, allowing wrap into the next year --- - # Example: year=2025, start_month=10 - # window = [(2025,10), (2025,11), (2025,12), (2026,1), (2026,2), (2026,3)] - window = [] - for offset in range(6): - total_index = (start_month - 1) + offset # 0-based - y_for_month = year + (total_index // 12) - m_for_month = (total_index % 12) + 1 - window.append((y_for_month, m_for_month)) - - overview = None - - if window: - # Build per-group data - groups_entries = [] # for internal calculations - - for key, group in CLIENT_GROUPS.items(): - clients_qs = get_group_clients(key) - - values = [] - group_total = Decimal('0') - - for (y_m, m_m) in window: - total = ExcelEntry.objects.filter( - client__in=clients_qs, - date__year=y_m, - date__month=m_m - ).aggregate( - total=Coalesce(Sum('lhe_ges'), Decimal('0')) - )['total'] - - values.append(total) - group_total += total - - groups_entries.append({ - 'key': key, - 'label': group['label'], - 'values': values, - 'total': group_total, - }) - - # month totals across all groups - month_totals = [] - for idx in range(len(window)): - s = sum((g['values'][idx] for g in groups_entries), Decimal('0')) - month_totals.append(s) - - grand_total = sum(month_totals, Decimal('0')) - - # Build rows for the template - rows = [] - for idx, (y_m, m_m) in enumerate(window): - row_values = [g['values'][idx] for g in groups_entries] - rows.append({ - 'month_number': m_m, - 'month_label': calendar.month_name[m_m], - 'values': row_values, - 'total': month_totals[idx], - }) - - groups_meta = [{'key': g['key'], 'label': g['label']} for g in groups_entries] - group_totals = [g['total'] for g in groups_entries] - - # Start/end for display – include years so wrap is clear - start_year = window[0][0] - start_month_disp = window[0][1] - end_year = window[-1][0] - end_month_disp = window[-1][1] - - overview = { - 'year': year, # keep for backwards compatibility if needed - 'start_month': start_month_disp, - 'start_year': start_year, - 'end_month': end_month_disp, - 'end_year': end_year, - 'rows': rows, - 'groups': groups_meta, - 'group_totals': group_totals, - 'grand_total': grand_total, - } - - - # Month dropdown labels - MONTH_CHOICES = [ - (1, 'Jan'), (2, 'Feb'), (3, 'Mar'), (4, 'Apr'), - (5, 'May'), (6, 'Jun'), (7, 'Jul'), (8, 'Aug'), - (9, 'Sep'), (10, 'Oct'), (11, 'Nov'), (12, 'Dec'), - ] - - return render(request, 'table_one.html', { - 'entries_table1': entries_table1, - 'clients': clients, - 'institutes': institutes, - 'available_years': available_years, - 'month_choices': MONTH_CHOICES, - 'overview': overview, - }) - -# Table Two View (SecondTableEntry) -def table_two_view(request): - try: - clients = Client.objects.all().select_related('institute') - institutes = Institute.objects.all() - - # 🔸 Read global half-year interval from session - interval_year = request.session.get('halfyear_year') - interval_start = request.session.get('halfyear_start_month') - - if interval_year and interval_start: - interval_year = int(interval_year) - interval_start = int(interval_start) - - # Build the same 6-month window as in clients_list (can cross years) - window = [] - for offset in range(6): - total_index = (interval_start - 1) + offset # 0-based - y = interval_year + (total_index // 12) - m = (total_index % 12) + 1 - window.append((y, m)) - - # Build a Q object matching any of those (year, month) pairs - q = Q() - for (y, m) in window: - q |= Q(date__year=y, date__month=m) - - entries = SecondTableEntry.objects.filter(q).order_by('-date') - else: - # Fallback if no global interval yet: show all - entries = SecondTableEntry.objects.all().order_by('-date') - - return render(request, 'table_two.html', { - 'entries_table2': entries, - 'clients': clients, - 'institutes': institutes, - 'interval_year': interval_year, - 'interval_start_month': interval_start, - }) - - except Exception as e: - return render(request, 'table_two.html', { - 'error_message': f"Failed to load data: {str(e)}", - 'entries_table2': [], - 'clients': clients, - 'institutes': institutes, - }) - - -def monthly_sheet_root(request): - """ - Redirect /sheet/ to the sheet matching the globally selected - half-year start (year + month). If not set, fall back to latest. - """ - year = request.session.get('halfyear_year') - start_month = request.session.get('halfyear_start_month') - - if year and start_month: - try: - year = int(year) - start_month = int(start_month) - return redirect('monthly_sheet', year=year, month=start_month) - except ValueError: - pass # fall through - - # Fallback: latest MonthlySheet if exists - latest_sheet = MonthlySheet.objects.order_by('-year', '-month').first() - if latest_sheet: - return redirect('monthly_sheet', year=latest_sheet.year, month=latest_sheet.month) - else: - now = timezone.now() - return redirect('monthly_sheet', year=now.year, month=now.month) -# Add Entry (Generic) -def add_entry(request, model_name): - if request.method == 'POST': - try: - if model_name == 'SecondTableEntry': - model = SecondTableEntry - - # Parse date - date_str = request.POST.get('date') - try: - date_obj = datetime.strptime(date_str, '%Y-%m-%d').date() if date_str else None - except (ValueError, TypeError): - return JsonResponse({ - 'status': 'error', - 'message': 'Invalid date format. Use YYYY-MM-DD' - }, status=400) - - # NEW: robust parse of warm flag (handles 0/1, true/false, on/off) - raw_warm = request.POST.get('is_warm') - is_warm_bool = str(raw_warm).lower() in ('1', 'true', 'on', 'yes') - - # Handle Helium Output (Table Two) - lhe_output = request.POST.get('lhe_output') - - entry = model.objects.create( - client=Client.objects.get(id=request.POST.get('client_id')), - date=date_obj, - is_warm=is_warm_bool, # <-- use the boolean here - lhe_delivery=request.POST.get('lhe_delivery', ''), - lhe_output=Decimal(lhe_output) if lhe_output else None, - notes=request.POST.get('notes', '') - ) - - return JsonResponse({ - 'status': 'success', - 'id': entry.id, - 'client_name': entry.client.name, - 'institute_name': entry.client.institute.name, - 'date': entry.date.strftime('%Y-%m-%d') if entry.date else '', - 'is_warm': entry.is_warm, - 'lhe_delivery': entry.lhe_delivery, - 'lhe_output': str(entry.lhe_output) if entry.lhe_output else '', - 'notes': entry.notes - }) - - elif model_name == 'ExcelEntry': - model = ExcelEntry - - # Parse the date string into a date object - date_str = request.POST.get('date') - try: - date_obj = datetime.strptime(date_str, '%Y-%m-%d').date() if date_str else None - except (ValueError, TypeError): - date_obj = None - - try: - pressure = Decimal(request.POST.get('pressure', 0)) - purity = Decimal(request.POST.get('purity', 0)) - druckkorrektur = Decimal(request.POST.get('druckkorrektur', 1)) - lhe_zus = Decimal(request.POST.get('lhe_zus', 0)) - constant_300 = Decimal(request.POST.get('constant_300', 300)) - korrig_druck = Decimal(request.POST.get('korrig_druck', 0)) - nm3 = Decimal(request.POST.get('nm3', 0)) - lhe = Decimal(request.POST.get('lhe', 0)) - lhe_ges = Decimal(request.POST.get('lhe_ges', 0)) - except InvalidOperation: - return JsonResponse({ - 'status': 'error', - 'message': 'Invalid numeric value in Helium Input' - }, status=400) - - # Create the entry with ALL fields - entry = model.objects.create( - client=Client.objects.get(id=request.POST.get('client_id')), - date=date_obj, - pressure=pressure, - purity=purity, - druckkorrektur=druckkorrektur, - lhe_zus=lhe_zus, - constant_300=constant_300, - korrig_druck=korrig_druck, - nm3=nm3, - lhe=lhe, - lhe_ges=lhe_ges, - notes=request.POST.get('notes', '') - ) - - # Prepare the response - response_data = { - 'status': 'success', - 'id': entry.id, - 'client_name': entry.client.name, - 'institute_name': entry.client.institute.name, - 'pressure': str(entry.pressure), - 'purity': str(entry.purity), - 'druckkorrektur': str(entry.druckkorrektur), - 'constant_300': str(entry.constant_300), - 'korrig_druck': str(entry.korrig_druck), - 'nm3': str(entry.nm3), - 'lhe': str(entry.lhe), - 'lhe_zus': str(entry.lhe_zus), - 'lhe_ges': str(entry.lhe_ges), - 'notes': entry.notes, - } - - if entry.date: - # JS uses this for the Date column and for the Month column - response_data['date'] = entry.date.strftime('%Y-%m-%d') - response_data['month'] = f"{entry.date.month:02d}" - else: - response_data['date'] = '' - response_data['month'] = '' - - return JsonResponse(response_data) - - - except Exception as e: - return JsonResponse({'status': 'error', 'message': str(e)}, status=400) - - return JsonResponse({'status': 'error', 'message': 'Invalid request'}, status=400) - -# Update Entry (Generic) -def update_entry(request, model_name): - if request.method == 'POST': - try: - if model_name == 'SecondTableEntry': - model = SecondTableEntry - elif model_name == 'ExcelEntry': - model = ExcelEntry - else: - return JsonResponse({'status': 'error', 'message': 'Invalid model'}, status=400) - - entry_id = int(request.POST.get('id')) - entry = model.objects.get(id=entry_id) - - # Common updates for both models - entry.client = Client.objects.get(id=request.POST.get('client_id')) - entry.notes = request.POST.get('notes', '') - - # Handle date properly for both models - date_str = request.POST.get('date') - if date_str: - try: - entry.date = datetime.strptime(date_str, '%Y-%m-%d').date() - except ValueError: - return JsonResponse({ - 'status': 'error', - 'message': 'Invalid date format. Use YYYY-MM-DD' - }, status=400) - - if model_name == 'SecondTableEntry': - # Handle Helium Output specific fields - - raw_warm = request.POST.get('is_warm') - entry.is_warm = str(raw_warm).lower() in ('1', 'true', 'on', 'yes') - - entry.lhe_delivery = request.POST.get('lhe_delivery', '') - - lhe_output = request.POST.get('lhe_output') - try: - entry.lhe_output = Decimal(lhe_output) if lhe_output else None - except InvalidOperation: - return JsonResponse({ - 'status': 'error', - 'message': 'Invalid LHe Output value' - }, status=400) - - entry.save() - - return JsonResponse({ - 'status': 'success', - 'id': entry.id, - 'client_name': entry.client.name, - 'institute_name': entry.client.institute.name, - 'date': entry.date.strftime('%Y-%m-%d') if entry.date else '', - 'is_warm': entry.is_warm, - 'lhe_delivery': entry.lhe_delivery, - 'lhe_output': str(entry.lhe_output) if entry.lhe_output else '', - 'notes': entry.notes - }) - - - elif model_name == 'ExcelEntry': - # Handle Helium Input specific fields - try: - entry.pressure = Decimal(request.POST.get('pressure', 0)) - entry.purity = Decimal(request.POST.get('purity', 0)) - entry.druckkorrektur = Decimal(request.POST.get('druckkorrektur', 1)) - entry.lhe_zus = Decimal(request.POST.get('lhe_zus', 0)) - entry.constant_300 = Decimal(request.POST.get('constant_300', 300)) - entry.korrig_druck = Decimal(request.POST.get('korrig_druck', 0)) - entry.nm3 = Decimal(request.POST.get('nm3', 0)) - entry.lhe = Decimal(request.POST.get('lhe', 0)) - entry.lhe_ges = Decimal(request.POST.get('lhe_ges', 0)) - except InvalidOperation: - return JsonResponse({ - 'status': 'error', - 'message': 'Invalid numeric value in Helium Input' - }, status=400) - - entry.save() - - return JsonResponse({ - 'status': 'success', - 'id': entry.id, - 'client_name': entry.client.name, - 'institute_name': entry.client.institute.name, - 'date': entry.date.strftime('%Y-%m-%d') if entry.date else '', - 'month': f"{entry.date.month:02d}" if entry.date else '', - 'pressure': str(entry.pressure), - 'purity': str(entry.purity), - 'druckkorrektur': str(entry.druckkorrektur), - 'constant_300': str(entry.constant_300), - 'korrig_druck': str(entry.korrig_druck), - 'nm3': str(entry.nm3), - 'lhe': str(entry.lhe), - 'lhe_zus': str(entry.lhe_zus), - 'lhe_ges': str(entry.lhe_ges), - 'notes': entry.notes - }) - - - except model.DoesNotExist: - return JsonResponse({'status': 'error', 'message': 'Entry not found'}, status=404) - except Exception as e: - return JsonResponse({'status': 'error', 'message': str(e)}, status=400) - - return JsonResponse({'status': 'error', 'message': 'Invalid request method'}, status=400) - -# Delete Entry (Generic) -def delete_entry(request, model_name): - if request.method == 'POST': - try: - if model_name == 'SecondTableEntry': - model = SecondTableEntry - elif model_name == 'ExcelEntry': - model = ExcelEntry - else: - return JsonResponse({'status': 'error', 'message': 'Invalid model'}, status=400) - - entry_id = request.POST.get('id') - entry = model.objects.get(id=entry_id) - entry.delete() - return JsonResponse({'status': 'success', 'message': 'Entry deleted'}) - - except model.DoesNotExist: - return JsonResponse({'status': 'error', 'message': 'Entry not found'}, status=404) - except Exception as e: - return JsonResponse({'status': 'error', 'message': str(e)}, status=400) - - return JsonResponse({'status': 'error', 'message': 'Invalid request'}, status=400) - -def betriebskosten_list(request): - items = Betriebskosten.objects.all().order_by('-buchungsdatum') - - summary = get_summary() - summary.recalculate() - - context = { - 'items': items, - 'summary': summary, - } - return render(request, 'betriebskosten_list.html', context) - -def betriebskosten_create(request): - if request.method != 'POST': - return JsonResponse({'status': 'error', 'message': 'Invalid request method'}) - - try: - entry_id = request.POST.get('id') - if entry_id: - entry = Betriebskosten.objects.get(id=entry_id) - else: - entry = Betriebskosten() - gegenstand = request.POST.get("gegenstand", "").strip() - buchungsdatum_str = request.POST.get('buchungsdatum') - rechnungsnummer = request.POST.get('rechnungsnummer') # UI label: Firma - kostentyp = request.POST.get('kostentyp') - betrag_raw = request.POST.get('betrag') - beschreibung = request.POST.get('beschreibung') or '' - gas_volume_raw = request.POST.get('gas_volume') # UI label: Gasvolumen (m³) - - # Validate required fields - if not all([buchungsdatum_str, rechnungsnummer, kostentyp, betrag_raw]): - return JsonResponse({'status': 'error', 'message': 'All required fields must be filled'}) - - allowed = {'sach', 'helium'} - if kostentyp not in allowed: - return JsonResponse({'status': 'error', 'message': 'Invalid Kostentyp'}) - - # Date - buchungsdatum = parse_date(buchungsdatum_str) - if not buchungsdatum: - return JsonResponse({'status': 'error', 'message': 'Invalid date format'}) - - # Betrag - try: - betrag = Decimal(str(betrag_raw)) - except Exception: - return JsonResponse({'status': 'error', 'message': 'Invalid Betrag'}) - - # Gas volume (m³) only for helium - gas_m3 = None - if kostentyp == 'helium': - if gas_volume_raw not in (None, '', 'None'): - try: - gas_m3 = Decimal(str(gas_volume_raw)) - except Exception: - return JsonResponse({'status': 'error', 'message': 'Invalid Gasvolumen (m³)'}) - entry.gegenstand = gegenstand - entry.buchungsdatum = buchungsdatum - entry.rechnungsnummer = rechnungsnummer - entry.kostentyp = kostentyp - entry.betrag = betrag - entry.beschreibung = beschreibung - entry.gas_volume = gas_m3 - - entry.save() - summary = get_summary() - summary.recalculate() - - # Computed values for UI (read-only fields) - gas_liter = None - price_per_m3 = None - price_per_liter = None - - if entry.kostentyp == 'helium' and entry.gas_volume: - # Liter = m³ / 0.75 - gas_liter = (Decimal(entry.gas_volume) / Decimal('0.75')) - if entry.gas_volume != 0: - price_per_m3 = (Decimal(entry.betrag) / Decimal(entry.gas_volume)) - if gas_liter != 0: - price_per_liter = (Decimal(entry.betrag) / gas_liter) - - return JsonResponse({ - 'status': 'success', - 'id': entry.id, - 'gegenstand': entry.gegenstand, - 'buchungsdatum': entry.buchungsdatum.strftime('%Y-%m-%d'), - 'rechnungsnummer': entry.rechnungsnummer, - 'kostentyp': entry.kostentyp, - 'kostentyp_display': entry.get_kostentyp_display(), - 'gas_volume_m3': str(entry.gas_volume) if entry.gas_volume is not None else '-', - 'gas_volume_liter': str(gas_liter.quantize(Decimal("0.01"))) if gas_liter is not None else '', - 'price_per_m3': str(price_per_m3.quantize(Decimal("0.01"))) if price_per_m3 is not None else '', - 'price_per_liter': str(price_per_liter.quantize(Decimal("0.01"))) if price_per_liter is not None else '', - 'betrag': str(entry.betrag), - 'beschreibung': entry.beschreibung or '', - }) - - except Betriebskosten.DoesNotExist: - return JsonResponse({'status': 'error', 'message': 'Entry not found'}, status=404) - except Exception as e: - return JsonResponse({'status': 'error', 'message': str(e)}) - - -def betriebskosten_delete(request): - if request.method == 'POST': - entry_id = request.POST.get('id') - - if not entry_id or not entry_id.isdigit(): - return JsonResponse({ - 'status': 'error', - 'message': 'Invalid ID' - }) - - try: - entry = Betriebskosten.objects.get(id=int(entry_id)) - entry.delete() - - summary = get_summary() - summary.recalculate() - - return JsonResponse({'status': 'success'}) - - except Betriebskosten.DoesNotExist: - return JsonResponse({ - 'status': 'error', - 'message': 'Entry not found' - }) - - return JsonResponse({'status': 'error'}) - - -def get_summary(): - summary, created = BetriebskostenSummary.objects.get_or_create(id=1) - return summary - -from django.http import JsonResponse - -def update_personalkosten(request): - if request.method == "POST": - value = request.POST.get("personalkosten") - summary = get_summary() - summary.personalkosten = Decimal(value or "0") - summary.recalculate() - return JsonResponse({ - "status": "success", - "umlage_personal": str(summary.umlage_personal), - }) - return JsonResponse({"status": "error"}) - - -class CheckSheetView(View): - def get(self, request): - # Get current month/year - current_year = datetime.now().year - current_month = datetime.now().month - - # Get all sheets - sheets = MonthlySheet.objects.all() - - sheet_data = [] - for sheet in sheets: - cells_count = sheet.cells.count() - # Count non-empty cells - non_empty = sheet.cells.exclude(value__isnull=True).count() - - sheet_data.append({ - 'id': sheet.id, - 'year': sheet.year, - 'month': sheet.month, - 'month_name': calendar.month_name[sheet.month], - 'total_cells': cells_count, - 'non_empty_cells': non_empty, - 'has_data': non_empty > 0 - }) - - # Also check what the default view would show - default_sheet = MonthlySheet.objects.filter( - year=current_year, - month=current_month - ).first() - - return JsonResponse({ - 'current_year': current_year, - 'current_month': current_month, - 'current_month_name': calendar.month_name[current_month], - 'default_sheet_exists': default_sheet is not None, - 'default_sheet_id': default_sheet.id if default_sheet else None, - 'sheets': sheet_data, - 'total_sheets': len(sheet_data) - }) - - -class QuickDebugView(View): - def get(self, request): - # Get ALL sheets - sheets = MonthlySheet.objects.all().order_by('year', 'month') - - result = { - 'sheets': [] - } - - for sheet in sheets: - sheet_info = { - 'id': sheet.id, - 'display': f"{sheet.year}-{sheet.month:02d}", - 'url': f"/sheet/{sheet.year}/{sheet.month}/", # CHANGED THIS LINE - 'sheet_url_pattern': 'sheet/{year}/{month}/', # Add this for clarity - } - - # Count cells with data for first client in top_left table - first_client = Client.objects.first() - if first_client: - test_cells = sheet.cells.filter( - client=first_client, - table_type='top_left', - row_index__in=[8, 9, 10] # Rows 9, 10, 11 - ).order_by('row_index') - - cell_values = {} - for cell in test_cells: - cell_values[f"row_{cell.row_index}"] = str(cell.value) if cell.value else "Empty" - - sheet_info['test_cells'] = cell_values - else: - sheet_info['test_cells'] = "No clients" - - result['sheets'].append(sheet_info) - - return JsonResponse(result) -class TestFormulaView(View): - def get(self, request): - # Test the formula evaluation directly - test_values = { - 8: 2, # Row 9 value (0-based index 8) - 9: 2, # Row 10 value (0-based index 9) - } - - # Test formula "9 + 8" (using 0-based indices) - formula = "9 + 8" - result = evaluate_formula(formula, test_values) - - return JsonResponse({ - 'test_values': test_values, - 'formula': formula, - 'result': str(result), - 'note': 'Formula uses 0-based indices. 9=Row10, 8=Row9' - }) - - -class SimpleDebugView(View): - """Simplest debug view to check if things are working""" - def get(self, request): - sheet_id = request.GET.get('sheet_id', 1) - - try: - sheet = MonthlySheet.objects.get(id=sheet_id) - - # Get first client - client = Client.objects.first() - if not client: - return JsonResponse({'error': 'No clients found'}) - - # Check a few cells - cells = Cell.objects.filter( - sheet=sheet, - client=client, - table_type='top_left', - row_index__in=[8, 9, 10] - ).order_by('row_index') - - cell_data = [] - for cell in cells: - cell_data.append({ - 'row_index': cell.row_index, - 'ui_row': cell.row_index + 1, - 'value': str(cell.value) if cell.value is not None else 'Empty', - 'cell_id': cell.id - }) - - return JsonResponse({ - 'sheet': f"{sheet.year}-{sheet.month}", - 'sheet_id': sheet.id, - 'client': client.name, - 'cells': cell_data, - 'note': 'Row 8 = UI Row 9, Row 9 = UI Row 10, Row 10 = UI Row 11' - }) - - except MonthlySheet.DoesNotExist: - return JsonResponse({'error': f'Sheet with id {sheet_id} not found'}) - -def halfyear_settings(request): - """ - Global settings page: choose a year + first month for the 6-month interval. - These values are stored in the session and used by other views. - """ - # Determine available years from your data (use ExcelEntry or SecondTableEntry) - # Here I use SecondTableEntry; you can switch to ExcelEntry if you prefer. - available_years_qs = SecondTableEntry.objects.dates('date', 'year', order='DESC') - available_years = [d.year for d in available_years_qs] - - if not available_years: - available_years = [timezone.now().year] - - # Defaults (if nothing in session yet) - default_year = request.session.get('halfyear_year', available_years[0]) - default_start_month = request.session.get('halfyear_start_month', 1) - - if request.method == 'POST': - year = int(request.POST.get('year', default_year)) - start_month = int(request.POST.get('start_month', default_start_month)) - - request.session['halfyear_year'] = year - request.session['halfyear_start_month'] = start_month - - # Redirect back to where user came from, or to this page again - next_url = request.POST.get('next') or request.GET.get('next') or 'halfyear_settings' - return redirect(next_url) - - # Month choices for the dropdown - month_choices = [(i, calendar.month_name[i]) for i in range(1, 13)] - - context = { - 'available_years': available_years, - 'selected_year': int(default_year), - 'selected_start_month': int(default_start_month), - 'month_choices': month_choices, - } - return render(request, 'halfyear_settings.html', context) \ No newline at end of file diff --git a/sheets/old/halfyear_balance.html b/sheets/old/halfyear_balance.html deleted file mode 100644 index 1a429db..0000000 --- a/sheets/old/halfyear_balance.html +++ /dev/null @@ -1,353 +0,0 @@ -{% extends "base.html" %} - -{% block content %} -
- - - -
- ← Helium Output Übersicht -

- {% with first=window.0 last=window|last %} - Halbjahres-Bilanz ({{ first.1 }}/{{ first.0 }} – {{ last.1 }}/{{ last.0 }}) - {% endwith %} -

- {% with first=window.0 %} - Monatsblätter - {% endwith %} -
- -
- -
-

Top Left – Halbjahresbilanz

- - - - - {% for c in clients_left %} - - {% endfor %} - - - - - {% for row in rows_left %} - - - {% for v in row.values %} - - {% endfor %} - - - {% endfor %} - -
Bezeichnung{{ c }}Σ
{{ row.label }} - {% if row.is_percent and v is not None %} - {{ v|floatformat:4 }} - {% elif v is not None %} - {{ v|floatformat:2 }} - {% endif %} - - {% if row.is_percent and row.total %} - {{ row.total|floatformat:4 }} - {% elif row.total is not None %} - {{ row.total|floatformat:2 }} - {% endif %} -
-
- - -
-

Top Right – Halbjahresbilanz

- - - - - {% for c in clients_right %} - - {% endfor %} - - - - - {% for row in rows_right %} - - - {% for v in row.values %} - - {% endfor %} - - - {% endfor %} - -
Bezeichnung{{ c }}Σ
{{ row.label }} - {% if row.is_text_row %} - {{ v }} - {% elif row.is_percent and v is not None %} - {{ v|floatformat:4 }} - {% elif v is not None %} - {{ v|floatformat:2 }} - {% endif %} - - {% if row.is_text_row %} - {{ row.total }} - {% elif row.is_percent and row.total %} - {{ row.total|floatformat:4 }} - {% elif row.total is not None %} - {{ row.total|floatformat:2 }} - {% endif %} -
-
-
-

Summe

- - - - - - - - - - - - - {% for r in rows_sum %} - - - - - - - - - - - - - - {% endfor %} - -
BezeichnungΣLicht-wieseChemieMaWiM3
{{ r.label }} - {% if r.is_percent %} - {{ r.total|floatformat:2 }}% - {% else %} - {{ r.total|floatformat:2 }} - {% endif %} - - {% if r.is_percent %} - {{ r.lichtwiese|floatformat:2 }}% - {% else %} - {{ r.lichtwiese|floatformat:2 }} - {% endif %} - - {% if r.is_percent %} - {{ r.chemie|floatformat:2 }}% - {% else %} - {{ r.chemie|floatformat:2 }} - {% endif %} - - {% if r.is_percent %} - {{ r.mawi|floatformat:2 }}% - {% else %} - {{ r.mawi|floatformat:2 }} - {% endif %} - - {% if r.is_percent %} - {{ r.m3|floatformat:2 }}% - {% else %} - {{ r.m3|floatformat:2 }} - {% endif %} -
-
-
-

Bottom Table 1 – Bilanz (read-only)

- - - - - - - - - - - - - - - {% for r in bottom1_rows %} - - - - - - - - - {% endfor %} - -
GasspeicherVolumenbarkorrigiertNm³Lit. LHe
{{ r.label }}{{ r.volume|floatformat:1 }}{{ r.bar|floatformat:0 }}{{ r.korr|floatformat:1 }}{{ r.nm3|floatformat:0 }}{{ r.lhe|floatformat:0 }}
-
-

Bottom Table 2 – Verbraucherbestand L-He (read-only)

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Verbraucherbestand{{ bottom2.j38|floatformat:2 }}{{ bottom2.k38|floatformat:2 }}
+ Anlage:Gefäss 2,5:{{ bottom2.g39|floatformat:2 }}Gefäss 1,0:{{ bottom2.i39|floatformat:2 }}{{ bottom2.j39|floatformat:2 }}{{ bottom2.k39|floatformat:2 }}
+ Kaltgas:Gefäss 2,5:{{ bottom2.g40|default_if_none:""|floatformat:2 }}Gefäss 1,0:{{ bottom2.i40|default_if_none:""|floatformat:2 }}{{ bottom2.j40|floatformat:2 }}{{ bottom2.k40|floatformat:2 }}
Bestand flüssig He{{ bottom2.j43|floatformat:2 }}{{ bottom2.k43|floatformat:2 }}
Gesamtbestand neu:{{ bottom2.j44|floatformat:2 }}{{ bottom2.k44|floatformat:2 }}
-
- -
- - -
-
{# closes .spreadsheet-container #} -{% endblock %} diff --git a/sheets/old/views.py b/sheets/old/views.py deleted file mode 100644 index b40f726..0000000 --- a/sheets/old/views.py +++ /dev/null @@ -1,4401 +0,0 @@ -from django.shortcuts import render, redirect -from django.utils.decorators import method_decorator -from django.views.decorators.csrf import csrf_exempt -from django.db.models import Sum, Value, DecimalField -from django.http import JsonResponse -from django.db.models import Q -from decimal import Decimal, InvalidOperation -from django.apps import apps -from datetime import date, datetime -import calendar -from django.utils import timezone -from django.views.generic import TemplateView, View -from .models import ( - Client, SecondTableEntry, Institute, ExcelEntry, - Betriebskosten, MonthlySheet, Cell, CellReference, MonthlySummary -) -from django.db.models import Sum -from django.urls import reverse -from django.db.models.functions import Coalesce -from .forms import BetriebskostenForm -from django.utils.dateparse import parse_date -from django.contrib.auth.mixins import LoginRequiredMixin -import json -FIRST_SHEET_YEAR = 2025 -FIRST_SHEET_MONTH = 1 - -CLIENT_GROUPS = { - 'ikp': { - 'label': 'IKP', - # exactly as in the Clients admin - 'names': ['IKP'], - }, - 'phys_chem_bunt_fohrer': { - 'label': 'Buntkowsky + Dr. Fohrer', - # include all variants you might have used for Buntkowsky - 'names': [ - 'AG Buntk.', # the one in your new entry - 'AG Buntkowsky.', # from your original list - 'AG Buntkowsky', - 'Dr. Fohrer', - ], - }, - 'mawi_alff_gutfleisch': { - 'label': 'Alff + AG Gutfleisch', - # include both short and full forms - 'names': [ - 'AG Alff', - 'AG Gutfl.', - 'AG Gutfleisch', - ], - }, - 'm3_group': { - 'label': 'M3 Buntkowsky + M3 Thiele + M3 Gutfleisch', - 'names': [ - 'M3 Buntkowsky', - 'M3 Thiele', - 'M3 Gutfleisch', - ], - }, -} - -# Add this CALCULATION_CONFIG at the top of views.py - -CALCULATION_CONFIG = { - 'top_left': { - # Row mappings: Django row_index (0-based) to Excel row - # Excel B4 -> Django row_index 1 (UI row 2) - # Excel B5 -> Django row_index 2 (UI row 3) - # Excel B6 -> Django row_index 3 (UI row 4) - - # B6 (row_index 3) = B5 (row_index 2) / 0.75 - 3: "2 / 0.75", - - # B11 (row_index 10) = B9 (row_index 8) - 10: "8", - - # B14 (row_index 13) = B13 (row_index 12) - B11 (row_index 10) + B12 (row_index 11) - 13: "12 - 10 + 11", - - # Note: B5, B17, B19, B20 require IF logic, so they'll be handled separately - }, - - # other tables (top_right, bottom_1, ...) stay as they are - - - ' top_right': { - # UI Row 1 (Excel Row 4): Stand der Gaszähler (Vormonat) (Nm³) - 0: { - 'L': "9 / (9 + 9) if (9 + 9) > 0 else 0", # L4 = L13/(L13+M13) - 'M': "9 / (9 + 9) if (9 + 9) > 0 else 0", # M4 = M13/(L13+M13) - 'N': "9 / (9 + 9) if (9 + 9) > 0 else 0", # N4 = N13/(N13+O13) - 'O': "9 / (9 + 9) if (9 + 9) > 0 else 0", # O4 = O13/(N13+O13) - 'P': None, # Editable - 'Q': None, # Editable - 'R': None, # Editable - }, - - # UI Row 2 (Excel Row 5): Gasrückführung (Nm³) - 1: { - 'L': "4", # L5 = L8 - 'M': "4", # M5 = L8 (merged) - 'N': "4", # N5 = N8 - 'O': "4", # O5 = N8 (merged) - 'P': "4 * 0", # P5 = P8 * P4 - 'Q': "4 * 0", # Q5 = P8 * Q4 - 'R': "4 * 0", # R5 = P8 * R4 - }, - - # UI Row 3 (Excel Row 6): Rückführung flüssig (Lit. L-He) - 2: { - 'L': "4", # L6 = L8 (Sammelrückführungen) - 'M': "4", - 'N': "4", - 'O': "4", - 'P': "4", - 'Q': "4", - 'R': "4", -}, - - # UI Row 4 (Excel Row 7): Sonderrückführungen (Lit. L-He) - EDITABLE - 3: { - 'L': None, - 'M': None, - 'N': None, - 'O': None, - 'P': None, - 'Q': None, - 'R': None, - }, - - # UI Row 5 (Excel Row 8): Sammelrückführungen (Lit. L-He) - 4: { - 'L': None, # Will be populated from ExcelEntry - 'M': None, - 'N': None, - 'O': None, - 'P': None, - 'Q': None, - 'R': None, - }, - - # UI Row 6 (Excel Row 9): Bestand in Kannen-1 (Lit. L-He) - EDITABLE - 5: { - 'L': None, - 'M': None, - 'N': None, - 'O': None, - 'P': None, - 'Q': None, - 'R': None, - }, - - # UI Row 7 (Excel Row 10): Summe Bestand (Lit. L-He) - 6: { - 'L': None, - 'M': None, - 'N': None, - 'O': None, - 'P': None, - 'Q': None, - 'R': None, - }, - - # UI Row 11 (Excel Row 14): Rückführ. Soll (Lit. L-He) - # handled in calculate_top_right_dependents (merged pairs + M3) - 10: { - 'L': None, - 'M': None, - 'N': None, - 'O': None, - 'P': None, - 'Q': None, - 'R': None, - }, - - # UI Row 12 (Excel Row 15): Verluste (Soll-Rückf.) (Lit. L-He) - # handled in calculate_top_right_dependents - 11: { - 'L': None, - 'M': None, - 'N': None, - 'O': None, - 'P': None, - 'Q': None, - 'R': None, - }, - - # UI Row 14 (Excel Row 17): Kaltgas Rückgabe (Lit. L-He) – Faktor - # handled in calculate_top_right_dependents (different formulas for pair 1 vs pair 2 + M3) - 13: { - 'L': None, - 'M': None, - 'N': None, - 'O': None, - 'P': None, - 'Q': None, - 'R': None, - }, - - # UI Row 16 (Excel Row 19): Verbraucherverluste (Liter L-He) - # handled in calculate_top_right_dependents - 15: { - 'L': None, - 'M': None, - 'N': None, - 'O': None, - 'P': None, - 'Q': None, - 'R': None, - }, - - # UI Row 17 (Excel Row 20): % - # handled in calculate_top_right_dependents - 16: { - 'L': None, - 'M': None, - 'N': None, - 'O': None, - 'P': None, - 'Q': None, - 'R': None, - } -}, - 'bottom_1': { - 5: "4 + 3 + 2", - 8: "7 - 6", - }, - 'bottom_2': { - 3: "1 + 2", - 6: "5 - 4", - }, - 'bottom_3': { - 2: "0 + 1", - 5: "3 + 4", - }, - # Special configuration for summation column (last column) - 'summation_column': { - # For each row that should be summed across columns - 'rows_to_sum': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23], # All rows - # OR specify specific rows: - # 'rows_to_sum': [0, 5, 10, 15, 20], # Only specific rows - # The last column index (0-based) - 'sum_column_index': 5, # 6th column (0-5) since you have 6 clients - } -} -def build_halfyear_window(interval_year: int, start_month: int): - """ - Build a list of (year, month) for the 6-month interval, possibly crossing into the next year. - Example: (2025, 10) -> [(2025,10), (2025,11), (2025,12), (2026,1), (2026,2), (2026,3)] - """ - window = [] - for offset in range(6): - total_index = (start_month - 1) + offset # 0-based - y = interval_year + (total_index // 12) - m = (total_index % 12) + 1 - window.append((y, m)) - return window -# --------------------------------------------------------------------------- -# Halbjahres-Bilanz helpers -# --------------------------------------------------------------------------- - -# You can adjust these indices if needed. -# Assuming: -# - bottom_1.table has row "Gasbestand" at some fixed row index, -# and columns: ... Nm³, Lit. LHe -GASBESTAND_ROW_INDEX = 9 # <-- adjust if your bottom_1 has a different row index -GASBESTAND_COL_NM3 = 3 # <-- adjust to the column index for Nm³ in bottom_1 - -# In top_left / top_right, "Bestand in Kannen-1 (Lit. L-He)" is row_index 5 -BESTAND_KANNEN_ROW_INDEX = 5 - -def pick_sheet_by_gasbestand(window, sheets_by_ym, prev_sheet): - """ - Returns the last sheet in the window whose Gasbestand (J36, Nm³ column) != 0. - If none found, returns prev_sheet (Übertrag_Dez__Vorjahr equivalent). - """ - for (y, m) in reversed(window): - sheet = sheets_by_ym.get((y, m)) - if not sheet: - continue - gasbestand_nm3 = get_bottom1_value(sheet, GASBESTAND_ROW_INDEX, GASBESTAND_COL_NM3) - if gasbestand_nm3 != 0: - return sheet - return prev_sheet -def get_bottom1_value(sheet, row_index: int, col_index: int) -> Decimal: - """Get a numeric value from bottom_1, or 0 if missing.""" - if sheet is None: - return Decimal('0') - - cell = Cell.objects.filter( - sheet=sheet, - table_type='bottom_1', - row_index=row_index, - column_index=col_index, - ).first() - - if cell is None or cell.value in (None, ''): - return Decimal('0') - - try: - return Decimal(str(cell.value)) - except Exception: - return Decimal('0') - -# MUST match the column order in your monthly_sheets top-right table - - - -def get_top_right_value(sheet, client_name: str, row_index: int) -> Decimal: - """ - Read a numeric value from the top_right table of a MonthlySheet for - a given client (by column) and row_index. - - top_right cells are keyed by (sheet, table_type='top_right', - row_index, column_index), where column_index is the position of the - client in HALFYEAR_RIGHT_CLIENTS. - """ - if sheet is None: - return Decimal('0') - - col_index = RIGHT_CLIENT_INDEX.get(client_name) - if col_index is None: - return Decimal('0') - - cell = Cell.objects.filter( - sheet=sheet, - table_type='top_right', - row_index=row_index, - column_index=col_index, - ).first() - - if cell is None or cell.value in (None, ''): - return Decimal('0') - - try: - return Decimal(str(cell.value)) - except Exception: - return Decimal('0') - -TR_RUECKF_FLUESSIG_ROW = 2 # confirmed by your March value 172.840560 -TR_BESTAND_KANNEN_ROW = 5 # confirmed by your earlier query -def get_bestand_kannen_for_month(sheet, client_name: str) -> Decimal: - """ - 'B9' in your description: Bestand in Kannen-1 (Lit. L-He) - For this implementation we take it from top_left row_index = 5 for that client. - """ - return get_top_left_value(sheet, client_name, row_index=BESTAND_KANNEN_ROW_INDEX) - -from decimal import Decimal -from django.db.models import Sum -from django.db.models.functions import Coalesce -from django.db.models import DecimalField, Value - -from .models import MonthlySheet, SecondTableEntry, Client, Cell -from django.shortcuts import redirect, render - -# You already have HALFYEAR_CLIENTS for the left table (AG Vogel, AG Halfm, IKP) -HALFYEAR_CLIENTS = ["AG Vogel", "AG Halfm", "IKP"] - -# NEW: clients for the top-right half-year table -HALFYEAR_RIGHT_CLIENTS = [ - "Dr. Fohrer", - "AG Buntk.", - "AG Alff", - "AG Gutfl.", - "M3 Thiele", - "M3 Buntkowsky", - "M3 Gutfleisch", -] -BOTTOM1_COL_VOLUME = 0 -BOTTOM1_COL_BAR = 1 -BOTTOM1_COL_KORR = 2 -BOTTOM1_COL_NM3 = 3 -BOTTOM1_COL_LHE = 4 -BOTTOM2_ROW_ANLAGE = 0 -BOTTOM2_COL_G39 = 0 # "Gefäss 2,5" (cell id shows column_index=0) -BOTTOM2_COL_I39 = 1 # "Gefäss 1,0" (cell id shows column_index=1) -BOTTOM2_ROW_INPUTS = { - "g39": (0, 0), # row_index=0, column_index=0 (your G39) - "i39": (0, 1), # row_index=0, column_index=1 (your I39) -} -FACTOR_NM3_TO_LHE = Decimal("0.75") -RIGHT_CLIENT_INDEX = {name: idx for idx, name in enumerate(HALFYEAR_RIGHT_CLIENTS)} -def halfyear_balance_view(request): - """ - Read-only Halbjahres-Bilanz view. - - LEFT table: AG Vogel / AG Halfm / IKP (exactly as in your last working version) - RIGHT table: Dr. Fohrer / AG Buntk. / AG Alff / AG Gutfl. / - M3 Thiele / M3 Buntkowsky / M3 Gutfleisch - using the Excel formulas you described. - - Uses the global 6-month interval from the main page (clients_list). - """ - # 1) Read half-year interval from the session - interval_year = request.session.get('halfyear_year') - interval_start = request.session.get('halfyear_start_month') - - if not interval_year or not interval_start: - # No interval chosen yet -> redirect to main page - return redirect('clients_list') - - interval_year = int(interval_year) - interval_start = int(interval_start) - - # You already have this helper in your code - window = build_halfyear_window(interval_year, interval_start) - # window = [(y1, m1), (y2, m2), ..., (y6, m6)] - - # (Year, month) of the first month - start_year, start_month = window[0] - - # Previous month (for "Stand ... (Vorjahr)" and "Best. in Kannen Vormonat") - prev_total_index = (start_month - 1) - 1 # one month back, 0-based - if prev_total_index >= 0: - prev_year = start_year + (prev_total_index // 12) - prev_month = (prev_total_index % 12) + 1 - else: - prev_year = start_year - 1 - prev_month = 12 - - # Load MonthlySheet objects for the window and for the previous month - sheets_by_ym = {} - for (y, m) in window: - sheet = MonthlySheet.objects.filter(year=y, month=m).first() - sheets_by_ym[(y, m)] = sheet - - prev_sheet = MonthlySheet.objects.filter(year=prev_year, month=prev_month).first() - def pick_bottom2_from_window(window, sheets_by_ym, prev_sheet): - # choose sheet (same logic you already use) - chosen = None - for (y, m) in reversed(window): - s = sheets_by_ym.get((y, m)) - # use your existing condition for choosing month - if s: - chosen = s - break - if chosen is None: - chosen = prev_sheet - - # Now read the two inputs safely - bottom2_inputs = {} - for key, (row_idx, col_idx) in BOTTOM2_ROW_INPUTS.items(): - bottom2_inputs[key] = get_bottom2_value(chosen, row_idx, col_idx) - - return chosen, bottom2_inputs - - - chosen_sheet_bottom2, bottom2_inputs = pick_bottom2_from_window(window, sheets_by_ym, prev_sheet) - bottom2_g39 = bottom2_inputs["g39"] - bottom2_i39 = bottom2_inputs["i39"] - # ---------------------------- - # HALF-YEAR BOTTOM TABLE 1 (Bilanz) - Read only - # ---------------------------- - chosen_sheet_bottom1 = pick_sheet_by_gasbestand(window, sheets_by_ym, prev_sheet) - - # IMPORTANT: define which bottom_1 row_index corresponds to Excel rows 27..35 - # If your bottom_1 starts at Excel row 27 => row_index 0 == Excel 27 - # then row_index = excel_row - 27 - BOTTOM1_EXCEL_START_ROW = 27 - - bottom1_excel_rows = list(range(27, 37)) # 27..36 - BOTTOM1_LABELS = [ - "Batterie 1", - "2", - "3", - "4", - "5", - "Batterie Links", - "2 Bündel", - "2 Ballone", - "Reingasspeicher", - "Gasbestand", - ] - - BOTTOM1_VOLUMES = [ - Decimal("2.4"), - Decimal("5.1"), - Decimal("4.0"), - Decimal("1.0"), - Decimal("4.0"), - Decimal("0.6"), - Decimal("1.2"), - Decimal("20.0"), - Decimal("5.0"), - None, # Gasbestand row has no volume - ] - nm3_sum_27_35 = Decimal("0") - lhe_sum_27_35 = Decimal("0") - bottom1_rows = [] - - for excel_row in bottom1_excel_rows: - row_index = excel_row - BOTTOM1_EXCEL_START_ROW - - chosen_sheet_bottom1 = None - for (y, m) in reversed(window): - s = sheets_by_ym.get((y, m)) - gasbestand = get_bottom1_value(s, GASBESTAND_ROW_INDEX, GASBESTAND_COL_NM3) # J36 (Nm3) - if gasbestand != 0: - chosen_sheet_bottom1 = s - break - - if chosen_sheet_bottom1 is None: - chosen_sheet_bottom1 = prev_sheet - - # Normal rows (27..35): read from chosen sheet and accumulate sums - if excel_row != 36: - nm3_val = get_bottom1_value(chosen_sheet_bottom1, row_index, BOTTOM1_COL_NM3) - lhe_val = get_bottom1_value(chosen_sheet_bottom1, row_index, BOTTOM1_COL_LHE) - - nm3_sum_27_35 += nm3_val - lhe_sum_27_35 += lhe_val - - bottom1_rows.append({ - "label": BOTTOM1_LABELS[row_index], - "volume": BOTTOM1_VOLUMES[row_index], - "bar": get_bottom1_value(chosen_sheet_bottom1, row_index, BOTTOM1_COL_BAR), - "korr": get_bottom1_value(chosen_sheet_bottom1, row_index, BOTTOM1_COL_KORR), - "nm3": nm3_val, - "lhe": lhe_val, - }) - - # Gasbestand row (36): show sums (J36 = SUM(J27:J35), K36 = SUM(K27:K35)) - else: - bottom1_rows.append({ - "label": "Gasbestand", - "volume": "", - "bar": "", - "korr": "", - "nm3": nm3_sum_27_35, - "lhe": lhe_sum_27_35, - }) - start_sheet = sheets_by_ym.get((start_year, start_month)) - # ------------------------------------------------------------ - # Bottom Table 2 (Halbjahres Bilanz) – server-side recalcBottom2() - # ------------------------------------------------------------ - - FACTOR_BT2 = Decimal("0.75") - - # 1) Helper: pick last-nonzero value of bottom_2 row0 col0/col1 from the window (fallback: prev_sheet) - def pick_last_nonzero_bottom2(row_index: int, col_index: int) -> Decimal: - # Scan from last month in window backwards - for (y, m) in reversed(window): - s = sheets_by_ym.get((y, m)) - if not s: - continue - v = get_bottom2_value(s, row_index, col_index) - if v is not None and v != 0: - return v - # fallback to month before window - v_prev = get_bottom2_value(prev_sheet, row_index, col_index) - return v_prev if v_prev is not None else Decimal("0") - - # 2) K38 comes from Overall Summary: "Summe Bestand (Lit. L-He)" - # Find it from your already built overall summary rows list. - - k38 = Decimal("0") - j38 = Decimal("0") - # 3) Inputs G39 / I39 (picked from last non-zero month in window) - g39 = pick_last_nonzero_bottom2(row_index=0, col_index=0) # G39 - i39 = pick_last_nonzero_bottom2(row_index=0, col_index=1) # I39 - - k39 = (g39 or Decimal("0")) + (i39 or Decimal("0")) - j39 = k39 * FACTOR_BT2 - - # 4) +Kaltgas (row 40) - # JS: - # g40 = (2500 - g39)/100*10 - # i40 = (1000 - i39)/100*10 - g40 = None - i40 = None - if g39 is not None: - g40 = (Decimal("2500") - g39) / Decimal("100") * Decimal("10") - if i39 is not None: - i40 = (Decimal("1000") - i39) / Decimal("100") * Decimal("10") - - k40 = (g40 or Decimal("0")) + (i40 or Decimal("0")) - j40 = k40 * FACTOR_BT2 - - # 5) Bestand flüssig He (row 43) - k43 = ( - (k38 or Decimal("0")) + - (k39 or Decimal("0")) + - (k40 or Decimal("0")) - ) - j43 = k43 * FACTOR_BT2 - - # 6) Gesamtbestand neu (row 44) = Gasbestand(Lit) from Bottom Table 1 + k43 - gasbestand_lit = Decimal("0") - for r in bottom1_rows: - if (r.get("label") or "").strip().startswith("Gasbestand"): - gasbestand_lit = r.get("lhe") or Decimal("0") - break - - k44 = (gasbestand_lit or Decimal("0")) + (k43 or Decimal("0")) - j44 = k44 * FACTOR_BT2 - - bottom2 = { - "j38": j38, "k38": k38, - "g39": g39, "i39": i39, "j39": j39, "k39": k39, - "g40": g40, "i40": i40, "j40": j40, "k40": k40, - "j43": j43, "k43": k43, - "j44": j44, "k44": k44, - } - - # ------------------------------------------------------------------ - # 2) LEFT TABLE (your existing, working logic) - # ------------------------------------------------------------------ - HALFYEAR_CLIENTS_LEFT = ["AG Vogel", "AG Halfm", "IKP"] - - # We'll collect client-wise values first for clarity. - client_data_left = {name: {} for name in HALFYEAR_CLIENTS_LEFT} - - # --- Row B3: Stand der Gaszähler (Nm³) - # = MAX(B3 from previous month, and B3 from each of the 6 months in the window) - # row_index 0 in top_left = "Stand der Gaszähler (Nm³)" - months_for_max = [(prev_year, prev_month)] + window - - for cname in HALFYEAR_CLIENTS_LEFT: - max_val = Decimal('0') - for (y, m) in months_for_max: - sheet = sheets_by_ym.get((y, m)) - if sheet is None and (y, m) == (prev_year, prev_month): - sheet = prev_sheet - val_b3 = get_top_left_value(sheet, cname, row_index=0) - if val_b3 > max_val: - max_val = val_b3 - client_data_left[cname]['stand_gas'] = max_val - - # --- Row B4: Stand der Gaszähler (Vorjahr) (Nm³) -> previous month same row --- - for cname in HALFYEAR_CLIENTS_LEFT: - val_b4 = get_top_left_value(prev_sheet, cname, row_index=0) - client_data_left[cname]['stand_gas_prev'] = val_b4 - - # --- Row B5: Gasrückführung (Nm³) = B3 - B4 --- - for cname in HALFYEAR_CLIENTS_LEFT: - b3 = client_data_left[cname]['stand_gas'] - b4 = client_data_left[cname]['stand_gas_prev'] - client_data_left[cname]['gasrueckf'] = b3 - b4 - - # --- Row B6: Rückführung flüssig (Lit. L-He) = B5 / 0.75 --- - for cname in HALFYEAR_CLIENTS_LEFT: - b5 = client_data_left[cname]['gasrueckf'] - client_data_left[cname]['rueckf_fluessig'] = (b5 / Decimal('0.75')) if b5 != 0 else Decimal('0') - - # --- Row B7: Sonderrückführungen (Lit. L-He) = sum over 6 months of that row --- - # That row index is 4 in your top_left table. - for cname in HALFYEAR_CLIENTS_LEFT: - sonder_total = Decimal('0') - for (y, m) in window: - sheet = sheets_by_ym.get((y, m)) - if sheet: - sonder_total += get_top_left_value(sheet, cname, row_index=4) - client_data_left[cname]['sonder'] = sonder_total - - # --- Row B8: Bestand in Kannen-1 (Lit. L-He) --- - # Excel-style logic with Gasbestand (J36) and fallback to previous month. - for cname in HALFYEAR_CLIENTS_LEFT: - chosen_value = None - - # Go from last month (window[5]) backwards to first (window[0]) - for (y, m) in reversed(window): - sheet = sheets_by_ym.get((y, m)) - gasbestand = get_bottom1_value(sheet, GASBESTAND_ROW_INDEX, GASBESTAND_COL_NM3) - if gasbestand != 0: - chosen_value = get_bestand_kannen_for_month(sheet, cname) - break - - # If still None -> use previous month (Übertrag_Dez__Vorjahr equivalent) - if chosen_value is None: - sheet_prev = prev_sheet - chosen_value = get_bestand_kannen_for_month(sheet_prev, cname) - - client_data_left[cname]['bestand_kannen'] = chosen_value if chosen_value is not None else Decimal('0') - - # --- Row B9: Summe Bestand (Lit. L-He) = equal to previous row --- - for cname in HALFYEAR_CLIENTS_LEFT: - client_data_left[cname]['summe_bestand'] = client_data_left[cname]['bestand_kannen'] - - # --- Row B10: Best. in Kannen Vormonat (Lit. L-He) - # = Bestand in Kannen-1 from the month BEFORE the window (prev_year, prev_month) - for cname in HALFYEAR_CLIENTS_LEFT: - client_data_left[cname]['best_kannen_vormonat'] = get_bestand_kannen_for_month(prev_sheet, cname) - - # --- Row B13: Bezug (Liter L-He) --- - for cname in HALFYEAR_CLIENTS_LEFT: - total_bezug = Decimal('0') - for (y, m) in window: - qs = SecondTableEntry.objects.filter( - client__name=cname, - date__year=y, - date__month=m, - ).aggregate( - total=Coalesce(Sum('lhe_output'), Value(0, output_field=DecimalField())) - ) - total_bezug += Decimal(str(qs['total'])) - client_data_left[cname]['bezug'] = total_bezug - - # --- Row B14: Rückführ. Soll (Lit. L-He) = Bezug - Summe Bestand + Best. in Kannen Vormonat --- - for cname in HALFYEAR_CLIENTS_LEFT: - b13 = client_data_left[cname]['bezug'] - b11 = client_data_left[cname]['summe_bestand'] - b12 = client_data_left[cname]['best_kannen_vormonat'] - client_data_left[cname]['rueckf_soll'] = b13 - b11 + b12 - - # --- Row B15: Verluste (Soll-Rückf.) (Lit. L-He) = B14 - B6 --- - for cname in HALFYEAR_CLIENTS_LEFT: - b14 = client_data_left[cname]['rueckf_soll'] - b6 = client_data_left[cname]['rueckf_fluessig'] - client_data_left[cname]['verluste'] = b14 - b6 - - # --- Row B16: Füllungen warm (Lit. L-He) = sum over 6 months (row_index=11) --- - for cname in HALFYEAR_CLIENTS_LEFT: - total_warm = Decimal('0') - for (y, m) in window: - sheet = sheets_by_ym.get((y, m)) - total_warm += get_top_left_value(sheet, cname, row_index=11) - client_data_left[cname]['fuellungen_warm'] = total_warm - - # --- Row B17: Kaltgas Rückgabe (Lit. L-He) = Bezug * 0.06 --- - factor = Decimal('0.06') - for cname in HALFYEAR_CLIENTS_LEFT: - b13 = client_data_left[cname]['bezug'] - client_data_left[cname]['kaltgas_rueckgabe'] = b13 * factor - - # --- Row B18: Verbraucherverluste (Liter L-He) = Verluste - Kaltgas Rückgabe --- - for cname in HALFYEAR_CLIENTS_LEFT: - b15 = client_data_left[cname]['verluste'] - b17 = client_data_left[cname]['kaltgas_rueckgabe'] - client_data_left[cname]['verbraucherverluste'] = b15 - b17 - - # --- Row B19: % = Verbraucherverluste / Bezug --- - for cname in HALFYEAR_CLIENTS_LEFT: - bezug = client_data_left[cname]['bezug'] - verb = client_data_left[cname]['verbraucherverluste'] - if bezug != 0: - client_data_left[cname]['percent'] = verb / bezug - else: - client_data_left[cname]['percent'] = None - - # Build LEFT rows structure - left_row_defs = [ - ('Stand der Gaszähler (Nm³)', 'stand_gas'), - ('Stand der Gaszähler (Vorjahr) (Nm³)', 'stand_gas_prev'), - ('Gasrückführung (Nm³)', 'gasrueckf'), - ('Rückführung flüssig (Lit. L-He)', 'rueckf_fluessig'), - ('Sonderrückführungen (Lit. L-He)', 'sonder'), - ('Bestand in Kannen-1 (Lit. L-He)', 'bestand_kannen'), - ('Summe Bestand (Lit. L-He)', 'summe_bestand'), - ('Best. in Kannen Vormonat (Lit. L-He)', 'best_kannen_vormonat'), - ('Bezug (Liter L-He)', 'bezug'), - ('Rückführ. Soll (Lit. L-He)', 'rueckf_soll'), - ('Verluste (Soll-Rückf.) (Lit. L-He)', 'verluste'), - ('Füllungen warm (Lit. L-He)', 'fuellungen_warm'), - ('Kaltgas Rückgabe (Lit. L-He) – Faktor', 'kaltgas_rueckgabe'), - ('Verbraucherverluste (Liter L-He)', 'verbraucherverluste'), - ('%', 'percent'), - ] - - rows_left = [] - for label, key in left_row_defs: - values = [client_data_left[cname][key] for cname in HALFYEAR_CLIENTS_LEFT] - if key == 'percent': - total_bezug = sum((client_data_left[c]['bezug'] for c in HALFYEAR_CLIENTS_LEFT), Decimal('0')) - total_verb = sum((client_data_left[c]['verbraucherverluste'] for c in HALFYEAR_CLIENTS_LEFT), Decimal('0')) - total = (total_verb / total_bezug) if total_bezug != 0 else None - else: - total = sum((v for v in values if v is not None), Decimal('0')) - rows_left.append({ - 'label': label, - 'values': values, - 'total': total, - 'is_percent': key == 'percent', - }) - - # ------------------------------------------------------------------ - # 3) RIGHT TABLE (top-right half-year aggregation) - # ------------------------------------------------------------------ - RIGHT_CLIENTS = HALFYEAR_RIGHT_CLIENTS # for brevity - - right_data = {name: {} for name in RIGHT_CLIENTS} - - # --- Bezug (Liter L-He) for each right client (same as for left) --- - for cname in RIGHT_CLIENTS: - total_bezug = Decimal('0') - for (y, m) in window: - qs = SecondTableEntry.objects.filter( - client__name=cname, - date__year=y, - date__month=m, - ).aggregate( - total=Coalesce(Sum('lhe_output'), Value(0, output_field=DecimalField())) - ) - total_bezug += Decimal(str(qs['total'])) - right_data[cname]['bezug'] = total_bezug - def find_bestand_from_window(reference_client: str) -> Decimal: - """ - Implements: - WENN(last_month!J36=0; WENN(prev_month!J36=0; ...; prev_sheet!9); last_month!9) - reference_client decides which column (L/N/P/Q/R) we read from monthly top_right row_index=5. - """ - # scan backward through window - for (y, m) in reversed(window): - sh = sheets_by_ym.get((y, m)) - if not sh: - continue - gasbestand = get_bottom1_value(sh, GASBESTAND_ROW_INDEX, GASBESTAND_COL_NM3) - if gasbestand != 0: - return get_top_right_value(sh, reference_client, TR_BESTAND_KANNEN_ROW) - - # fallback to previous month (Übertrag_Dez__Vorjahr equivalent) - return get_top_right_value(prev_sheet, reference_client, TR_BESTAND_KANNEN_ROW) - - # Fohrer+Buntk merged: BOTH use Fohrer column (L9) - val_L = find_bestand_from_window("Dr. Fohrer") - right_data["Dr. Fohrer"]["bestand_kannen"] = val_L - right_data["AG Buntk."]["bestand_kannen"] = val_L - - # Alff+Gutfl merged: BOTH use Alff column (N9) - val_N = find_bestand_from_window("AG Alff") - right_data["AG Alff"]["bestand_kannen"] = val_N - right_data["AG Gutfl."]["bestand_kannen"] = val_N - - # M3 each uses its own column (P9/Q9/R9) - right_data["M3 Thiele"]["bestand_kannen"] = find_bestand_from_window("M3 Thiele") - right_data["M3 Buntkowsky"]["bestand_kannen"] = find_bestand_from_window("M3 Buntkowsky") - right_data["M3 Gutfleisch"]["bestand_kannen"] = find_bestand_from_window("M3 Gutfleisch") - # Helper for pair shares (L13/($L13+$M13), etc.) - def pair_share(c1, c2): - total = right_data[c1]['bezug'] + right_data[c2]['bezug'] - if total == 0: - return (Decimal('0'), Decimal('0')) - return ( - right_data[c1]['bezug'] / total, - right_data[c2]['bezug'] / total, - ) - - # --- "Stand der Gaszähler (Vorjahr) (Nm³)" row: share based on Bezug --- - # Dr. Fohrer / AG Buntk. - s_fohrer, s_buntk = pair_share("Dr. Fohrer", "AG Buntk.") - right_data["Dr. Fohrer"]['stand_prev_share'] = s_fohrer - right_data["AG Buntk."]['stand_prev_share'] = s_buntk - - # AG Alff / AG Gutfl. - s_alff, s_gutfl = pair_share("AG Alff", "AG Gutfl.") - right_data["AG Alff"]['stand_prev_share'] = s_alff - right_data["AG Gutfl."]['stand_prev_share'] = s_gutfl - - # M3 Thiele / M3 Buntkowsky / M3 Gutfleisch → empty in Excel → None - for cname in ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"]: - right_data[cname]['stand_prev_share'] = None - - # --- Rückführung flüssig per month (raw sums) --- - # top_right row_index=2 is "Rückführung flüssig (Lit. L-He)" - - # --- Sonderrückführungen (row_index=3 in top_right) --- - for cname in RIGHT_CLIENTS: - sonder_total = Decimal('0') - for (y, m) in window: - sheet = sheets_by_ym.get((y, m)) - if sheet: - sonder_total += get_top_right_value(sheet, cname, row_index=3) - right_data[cname]['sonder'] = sonder_total - - # --- Sammelrückführung (row_index=4 in top_right), grouped & merged --- - # Group 1: Dr. Fohrer + AG Buntk. - group1_total = Decimal('0') - for (y, m) in window: - sheet = sheets_by_ym.get((y, m)) - if sheet: - group1_total += get_top_right_value(sheet, "Dr. Fohrer", row_index=4) - right_data["Dr. Fohrer"]['sammel'] = group1_total - right_data["AG Buntk."]['sammel'] = group1_total - - # Group 2: AG Alff + AG Gutfl. - group2_total = Decimal('0') - for (y, m) in window: - sheet = sheets_by_ym.get((y, m)) - if sheet: - group2_total += get_top_right_value(sheet, "AG Alff", row_index=4) - right_data["AG Alff"]['sammel'] = group2_total - right_data["AG Gutfl."]['sammel'] = group2_total - - # Group 3: M3 Thiele + M3 Buntkowsky + M3 Gutfleisch - group3_total = Decimal('0') - for (y, m) in window: - sheet = sheets_by_ym.get((y, m)) - if sheet: - group3_total += get_top_right_value(sheet, "M3 Thiele", row_index=4) - right_data["M3 Thiele"]['sammel'] = group3_total - right_data["M3 Buntkowsky"]['sammel'] = group3_total - right_data["M3 Gutfleisch"]['sammel'] = group3_total - def safe_div(a: Decimal, b: Decimal) -> Decimal: - return (a / b) if b != 0 else Decimal("0") - - # --- Rückführung flüssig (Lit. L-He) for Halbjahres-Bilanz top-right --- - # Uses your exact formulas. - - # 1) Fohrer / Buntk split by BEZUG share times group SAMMEL (L8) - L13 = right_data["Dr. Fohrer"]["bezug"] - M13 = right_data["AG Buntk."]["bezug"] - L8 = right_data["Dr. Fohrer"]["sammel"] # merged group total - - den = (L13 + M13) - right_data["Dr. Fohrer"]["rueckf_fluessig"] = (safe_div(L13, den) * L8) if den != 0 else Decimal("0") - right_data["AG Buntk."]["rueckf_fluessig"] = (safe_div(M13, den) * L8) if den != 0 else Decimal("0") - - # 2) Alff / Gutfl split by BEZUG share times group SAMMEL (N8) - N13 = right_data["AG Alff"]["bezug"] - O13 = right_data["AG Gutfl."]["bezug"] - N8 = right_data["AG Alff"]["sammel"] # merged group total - - den = (N13 + O13) - right_data["AG Alff"]["rueckf_fluessig"] = (safe_div(N13, den) * N8) if den != 0 else Decimal("0") - right_data["AG Gutfl."]["rueckf_fluessig"] = (safe_div(O13, den) * N8) if den != 0 else Decimal("0") - - # 3) M3 Thiele = sum of monthly Rückführung flüssig (monthly top_right row_index=2) over window - P6_sum = Decimal("0") - for (y, m) in window: - sh = sheets_by_ym.get((y, m)) - P6_sum += get_top_right_value(sh, "M3 Thiele", TR_RUECKF_FLUESSIG_ROW) - right_data["M3 Thiele"]["rueckf_fluessig"] = P6_sum - - # 4) M3 Buntkowsky / M3 Gutfleisch split by BEZUG share times M3-group SAMMEL (P8) - P13 = right_data["M3 Thiele"]["bezug"] - Q13 = right_data["M3 Buntkowsky"]["bezug"] - R13 = right_data["M3 Gutfleisch"]["bezug"] - P8 = right_data["M3 Thiele"]["sammel"] # merged group total - - den = (P13 + Q13 + R13) - right_data["M3 Buntkowsky"]["rueckf_fluessig"] = (safe_div(Q13, den) * P8) if den != 0 else Decimal("0") - right_data["M3 Gutfleisch"]["rueckf_fluessig"] = (safe_div(R13, den) * P8) if den != 0 else Decimal("0") - # --- Bestand in Kannen-1 (Lit. L-He) for right table (grouped) --- - # Use Gasbestand (J36) and fallback logic, but now reading top_right B9 for each group. - TOP_RIGHT_ROW_BESTAND_KANNEN = 6 # <-- most likely correct in your setup - - def pick_bestand_top_right(base_client: str) -> Decimal: - # Go from last month in window backwards: if Gasbestand != 0, use that month's Bestand in Kannen - for (y, m) in reversed(window): - sh = sheets_by_ym.get((y, m)) - if not sh: - continue - - gasbestand = get_bottom1_value(sh, GASBESTAND_ROW_INDEX, GASBESTAND_COL_NM3) - if gasbestand != 0: - return get_top_right_value(sh, base_client, TOP_RIGHT_ROW_BESTAND_KANNEN) - - # Fallback to previous month (Übertrag_Dez__Vorjahr equivalent) - return get_top_right_value(prev_sheet, base_client, TOP_RIGHT_ROW_BESTAND_KANNEN) - - # Group 1 merged (Fohrer + Buntk.) - g1_best = pick_bestand_top_right("Dr. Fohrer") - right_data["Dr. Fohrer"]["bestand_kannen"] = g1_best - right_data["AG Buntk."]["bestand_kannen"] = g1_best - - # Group 2 merged (Alff + Gutfl.) - g2_best = pick_bestand_top_right("AG Alff") - right_data["AG Alff"]["bestand_kannen"] = g2_best - right_data["AG Gutfl."]["bestand_kannen"] = g2_best - - # Group 3 merged (M3 Thiele + M3 Buntkowsky + M3 Gutfleisch) - g3_best = pick_bestand_top_right("M3 Thiele") - right_data["M3 Thiele"]["bestand_kannen"] = g3_best - right_data["M3 Buntkowsky"]["bestand_kannen"] = g3_best - right_data["M3 Gutfleisch"]["bestand_kannen"] = g3_best - - # Summe Bestand = same as previous row - for cname in RIGHT_CLIENTS: - right_data[cname]['summe_bestand'] = right_data[cname]['bestand_kannen'] - - # Best. in Kannen Vormonat (Lit. L-He) from previous month top_right row_index=7 - g1_prev = get_top_right_value(prev_sheet, "Dr. Fohrer", TOP_RIGHT_ROW_BESTAND_KANNEN) - right_data["Dr. Fohrer"]['best_kannen_vormonat'] = g1_prev - right_data["AG Buntk."]['best_kannen_vormonat'] = g1_prev - - g2_prev = get_top_right_value(prev_sheet, "AG Alff", TOP_RIGHT_ROW_BESTAND_KANNEN) - right_data["AG Alff"]['best_kannen_vormonat'] = g2_prev - right_data["AG Gutfl."]['best_kannen_vormonat'] = g2_prev - - - # Group 1 merged (Fohrer + Buntk.) - g1_prev = get_top_right_value(prev_sheet, "Dr. Fohrer", TOP_RIGHT_ROW_BESTAND_KANNEN) - right_data["Dr. Fohrer"]["best_kannen_vormonat"] = g1_prev - right_data["AG Buntk."]["best_kannen_vormonat"] = g1_prev - - # Group 2 merged (Alff + Gutfl.) - g2_prev = get_top_right_value(prev_sheet, "AG Alff", TOP_RIGHT_ROW_BESTAND_KANNEN) - right_data["AG Alff"]["best_kannen_vormonat"] = g2_prev - right_data["AG Gutfl."]["best_kannen_vormonat"] = g2_prev - - # Group 3 UNMERGED (each one reads its own cell) - right_data["M3 Thiele"]["best_kannen_vormonat"] = get_top_right_value(prev_sheet, "M3 Thiele", TOP_RIGHT_ROW_BESTAND_KANNEN) - right_data["M3 Buntkowsky"]["best_kannen_vormonat"] = get_top_right_value(prev_sheet, "M3 Buntkowsky", TOP_RIGHT_ROW_BESTAND_KANNEN) - right_data["M3 Gutfleisch"]["best_kannen_vormonat"] = get_top_right_value(prev_sheet, "M3 Gutfleisch", TOP_RIGHT_ROW_BESTAND_KANNEN) - - # --- Rückführ. Soll (Lit. L-He) according to your formulas --- - - # Group 1: Dr. Fohrer / AG Buntk. - total_bestand_1 = right_data["Dr. Fohrer"]['summe_bestand'] - best_vormonat_1 = right_data["Dr. Fohrer"]['best_kannen_vormonat'] - diff1 = total_bestand_1 - best_vormonat_1 - share_fohrer = right_data["Dr. Fohrer"]['stand_prev_share'] or Decimal('0') - - right_data["Dr. Fohrer"]['rueckf_soll'] = ( - right_data["Dr. Fohrer"]['bezug'] - diff1 * share_fohrer - ) - right_data["AG Buntk."]['rueckf_soll'] = ( - right_data["AG Buntk."]['bezug'] - total_bestand_1 + best_vormonat_1 - ) - - # Group 2: AG Alff / AG Gutfl. - total_bestand_2 = right_data["AG Alff"]['summe_bestand'] - best_vormonat_2 = right_data["AG Alff"]['best_kannen_vormonat'] - diff2 = total_bestand_2 - best_vormonat_2 - share_alff = right_data["AG Alff"]['stand_prev_share'] or Decimal('0') - share_gutfl = right_data["AG Gutfl."]['stand_prev_share'] or Decimal('0') - - right_data["AG Alff"]['rueckf_soll'] = ( - right_data["AG Alff"]['bezug'] - diff2 * share_alff - ) - right_data["AG Gutfl."]['rueckf_soll'] = ( - right_data["AG Gutfl."]['bezug'] - diff2 * share_gutfl - ) - - # Group 3: M3 Thiele / M3 Buntkowsky / M3 Gutfleisch - for cname in ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"]: - b13 = right_data[cname]['bezug'] - b12 = right_data[cname]['best_kannen_vormonat'] - b11 = right_data[cname]['summe_bestand'] - # Excel: P13+P12-P11 etc. - right_data[cname]['rueckf_soll'] = b13 + b12 - b11 - - # --- Verluste (Soll-Rückf.) (Lit. L-He) = B14 - B6 - B7 --- - for cname in RIGHT_CLIENTS: - b14 = right_data[cname]['rueckf_soll'] - b6 = right_data[cname]['rueckf_fluessig'] - b7 = right_data[cname]['sonder'] - right_data[cname]['verluste'] = b14 - b6 - b7 - - # --- Füllungen warm (Lit. L-He) = sum of monthly 'Füllungen warm' (row_index=11 top_right) --- - for cname in RIGHT_CLIENTS: - total_warm = Decimal('0') - for (y, m) in window: - sheet = sheets_by_ym.get((y, m)) - if sheet: - total_warm += get_top_right_value(sheet, cname, row_index=11) - right_data[cname]['fuellungen_warm'] = total_warm - - # --- Kaltgas Rückgabe (Lit. L-He) – Faktor = Bezug * 0.06 --- - for cname in RIGHT_CLIENTS: - b13 = right_data[cname]['bezug'] - right_data[cname]['kaltgas_rueckgabe'] = b13 * factor - - # --- Verbraucherverluste (Liter L-He) = Verluste - Kaltgas Rückgabe --- - for cname in RIGHT_CLIENTS: - b15 = right_data[cname]['verluste'] - b17 = right_data[cname]['kaltgas_rueckgabe'] - right_data[cname]['verbraucherverluste'] = b15 - b17 - - # --- % = Verbraucherverluste / Bezug --- - for cname in RIGHT_CLIENTS: - bezug = right_data[cname]['bezug'] - verb = right_data[cname]['verbraucherverluste'] - if bezug != 0: - right_data[cname]['percent'] = verb / bezug - else: - right_data[cname]['percent'] = None - - # Build RIGHT rows structure - right_row_defs = [ - ('Stand der Gaszähler (Vorjahr) (Nm³)', 'stand_prev_share'), - # We skip the pure-text "Gasrückführung (Nm³)" line here, - # because it’s only text (Aufteilung nach Verbrauch / Gaszähler) - # and easier to render directly in the template if needed. - ('Rückführung flüssig (Lit. L-He)', 'rueckf_fluessig'), - ('Sonderrückführungen (Lit. L-He)', 'sonder'), - ('Sammelrückführung (Lit. L-He)', 'sammel'), - ('Bestand in Kannen-1 (Lit. L-He)', 'bestand_kannen'), - ('Summe Bestand (Lit. L-He)', 'summe_bestand'), - ('Best. in Kannen Vormonat (Lit. L-He)', 'best_kannen_vormonat'), - ('Bezug (Liter L-He)', 'bezug'), - ('Rückführ. Soll (Lit. L-He)', 'rueckf_soll'), - ('Verluste (Soll-Rückf.) (Lit. L-He)', 'verluste'), - ('Füllungen warm (Lit. L-He)', 'fuellungen_warm'), - ('Kaltgas Rückgabe (Lit. L-He) – Faktor', 'kaltgas_rueckgabe'), - ('Verbraucherverluste (Liter L-He)', 'verbraucherverluste'), - ('%', 'percent'), - ] - - rows_right = [] - for label, key in right_row_defs: - values = [right_data[cname].get(key) for cname in RIGHT_CLIENTS] - - if key == 'percent': - total_bezug = sum((right_data[c]['bezug'] for c in RIGHT_CLIENTS), Decimal('0')) - total_verb = sum((right_data[c]['verbraucherverluste'] for c in RIGHT_CLIENTS), Decimal('0')) - total = (total_verb / total_bezug) if total_bezug != 0 else None - else: - total = sum((v for v in values if isinstance(v, Decimal)), Decimal('0')) - - rows_right.append({ - 'label': label, - 'values': values, - 'total': total, - 'is_percent': key == 'percent', - }) - SUM_TABLE_ROWS = [ - ("Rückführung flüssig (Lit. L-He)", "rueckf_fluessig"), - ("Sonderrückführungen (Lit. L-He)", "sonder"), - ("Sammelrückführungen (Lit. L-He)", "sammel"), - ("Bestand in Kannen-1 (Lit. L-He)", "bestand_kannen"), - ("Summe Bestand (Lit. L-He)", "summe_bestand"), - ("Best. in Kannen Vormonat (Lit. L-He)", "best_kannen_vormonat"), - ("Bezug (Liter L-He)", "bezug"), - ("Rückführ. Soll (Lit. L-He)", "rueckf_soll"), - ("Verluste (Soll-Rückf.) (Lit. L-He)", "verluste"), - ("Füllungen warm (Lit. L-He)", "fuellungen_warm"), - ("Kaltgas Rückgabe (Lit. L-He) – Faktor", "kaltgas_rueckgabe"), - ("Faktor 0.06", "factor_row"), - ("Verbraucherverluste (Liter L-He)", "verbraucherverluste"), - ("%", "percent"), - ] - - RIGHT_GROUPS = { - "chemie": ["Dr. Fohrer", "AG Buntk."], - "mawi": ["AG Alff", "AG Gutfl."], - "m3": ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"], - } - - RIGHT_ALL = ["Dr. Fohrer", "AG Buntk.", "AG Alff", "AG Gutfl.", "M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"] - LEFT_ALL = HALFYEAR_CLIENTS_LEFT - - def safe_pct(verb, bez): - return (verb / bez) if bez != 0 else None - rows_sum = [] - def d(x): - return x if isinstance(x, Decimal) else Decimal("0") - for label, key in SUM_TABLE_ROWS: - - if key == "factor_row": - lichtwiese = chemie = mawi = m3 = total = Decimal("0.06") - - elif key == "percent": - # Right totals - rw_bez = sum(d(right_data[c].get("bezug")) for c in RIGHT_ALL) - rw_verb = sum(d(right_data[c].get("verbraucherverluste")) for c in RIGHT_ALL) - lichtwiese = safe_pct(rw_verb, rw_bez) - - # Chemie - ch_bez = sum(d(right_data[c].get("bezug")) for c in RIGHT_GROUPS["chemie"]) - ch_verb = sum(d(right_data[c].get("verbraucherverluste")) for c in RIGHT_GROUPS["chemie"]) - chemie = safe_pct(ch_verb, ch_bez) - - # MaWi - mw_bez = sum(d(right_data[c].get("bezug")) for c in RIGHT_GROUPS["mawi"]) - mw_verb = sum(d(right_data[c].get("verbraucherverluste")) for c in RIGHT_GROUPS["mawi"]) - mawi = safe_pct(mw_verb, mw_bez) - - # M3 - m3_bez = sum(d(right_data[c].get("bezug")) for c in RIGHT_GROUPS["m3"]) - m3_verb = sum(d(right_data[c].get("verbraucherverluste")) for c in RIGHT_GROUPS["m3"]) - m3 = safe_pct(m3_verb, m3_bez) - - # Σ column = (left verb + right verb) / (left bez + right bez) - left_bez = sum(d(client_data_left[c].get("bezug")) for c in LEFT_ALL) - left_verb = sum(d(client_data_left[c].get("verbraucherverluste")) for c in LEFT_ALL) - total = safe_pct(left_verb + rw_verb, left_bez + rw_bez) - - else: - # normal rows = sums - lichtwiese = sum(d(right_data[c].get(key)) for c in RIGHT_ALL) - chemie = sum(d(right_data[c].get(key)) for c in RIGHT_GROUPS["chemie"]) - mawi = sum(d(right_data[c].get(key)) for c in RIGHT_GROUPS["mawi"]) - m3 = sum(d(right_data[c].get(key)) for c in RIGHT_GROUPS["m3"]) - - left_total = sum(d(client_data_left[c].get(key)) for c in LEFT_ALL) - total = left_total + lichtwiese - - rows_sum.append({ - "row_index": row_index, - "label": label, - "total": total, - "lichtwiese": lichtwiese, - "chemie": chemie, - "mawi": mawi, - "m3": m3, - "is_percent": (key == "percent"), - }) - def find_sum_row(rows, label_startswith: str): - for r in rows: - if str(r.get("label", "")).strip().startswith(label_startswith): - return r - return None - - summe_bestand_row = find_sum_row(rows_sum, "Summe Bestand") - k38 = (summe_bestand_row.get("total") if summe_bestand_row else Decimal("0")) or Decimal("0") - j38 = k38 * Decimal("0.75") - # --- FIX: now that k38 is known, update bottom2 + recompute dependent rows --- - bottom2["k38"] = k38 - bottom2["j38"] = j38 - - k39 = bottom2.get("k39") or Decimal("0") - k40 = bottom2.get("k40") or Decimal("0") - - # Row 43: Bestand flüssig He = SUMME(K38:K40) - k43 = (k38 or Decimal("0")) + k39 + k40 - j43 = k43 * Decimal("0.75") - - bottom2["k43"] = k43 - bottom2["j43"] = j43 - - # Row 44: Gesamtbestand neu = Gasbestand(Lit) from bottom table 1 + k43 - gasbestand_lit = Decimal("0") - for r in bottom1_rows: - if (r.get("label") or "").strip().startswith("Gasbestand"): - gasbestand_lit = r.get("lhe") or Decimal("0") - break - - k44 = gasbestand_lit + k43 - j44 = k44 * Decimal("0.75") - - bottom2["k44"] = k44 - bottom2["j44"] = j44 - def d(x): - return x if isinstance(x, Decimal) else Decimal("0") - - # ---- Bottom2: J38/K38 depend on rows_sum (overall summary), so do it HERE ---- - k38 = Decimal("0") - for r in rows_sum: - if r.get("label") == "Summe Bestand (Lit. L-He)": - k38 = r.get("total") or Decimal("0") - break - - j38 = k38 * FACTOR_NM3_TO_LHE # 0.75 - bottom2["k38"] = k38 - bottom2["j38"] = j38 - # ------------------------------------------------------------------ - # 4) Context – keep old keys AND new ones - # ------------------------------------------------------------------ - context = { - 'interval_year': interval_year, - 'interval_start_month': interval_start, - 'window': window, - - # Left table – old names (for your first template) - 'clients': HALFYEAR_CLIENTS_LEFT, - 'rows': rows_left, - - # Left table – explicit - 'clients_left': HALFYEAR_CLIENTS_LEFT, - 'rows_left': rows_left, - - # Right table - 'clients_right': RIGHT_CLIENTS, - 'rows_right': rows_right, - 'rows_sum': rows_sum, - 'bottom1_rows': bottom1_rows, - - } - context["bottom2"] = bottom2 - context["context_bottom2_g39"] = bottom2_inputs["g39"] - context["context_bottom2_i39"] = bottom2_inputs["i39"] - return render(request, 'halfyear_balance.html', context) - -def get_bottom2_value(sheet, row_index: int, col_index: int) -> Decimal: - """Get numeric value from bottom_2 or 0 if missing.""" - if sheet is None: - return Decimal("0") - cell = Cell.objects.filter( - sheet=sheet, - table_type="bottom_2", - row_index=row_index, - column_index=col_index, - ).first() - if cell is None or cell.value in (None, ""): - return Decimal("0") - try: - return Decimal(str(cell.value)) - except Exception: - return Decimal("0") - -def get_top_left_value(sheet, client_name: str, row_index: int) -> Decimal: - """ - Read a numeric value from the top_left table for a given month, client and row. - Does NOT use column_index, because top_left is keyed only by client + row_index. - """ - if sheet is None: - return Decimal('0') - - client_obj = Client.objects.filter(name=client_name).first() - if not client_obj: - return Decimal('0') - - cell = Cell.objects.filter( - sheet=sheet, - table_type='top_left', - client=client_obj, - row_index=row_index, - ).first() - - if cell is None or cell.value in (None, ''): - return Decimal('0') - - try: - return Decimal(str(cell.value)) - except Exception: - return Decimal('0') -def get_group_clients(group_key): - """Return queryset of clients that belong to a logical group.""" - from .models import Client # local import to avoid circulars - - group = CLIENT_GROUPS.get(group_key) - if not group: - return Client.objects.none() - return Client.objects.filter(name__in=group['names']) - -def calculate_summation(sheet, table_type, row_index, sum_column_index): - """Calculate summation for a row, with special handling for % row""" - from decimal import Decimal - from .models import Cell - - try: - # Special case: top_left, % row (Excel B20 -> row_index 19) - if table_type == 'top_left' and row_index == 19: - # K13 = sum of row 13 (Excel B13 -> row_index 12) across all clients - cells_row13 = Cell.objects.filter( - sheet=sheet, - table_type='top_left', - row_index=12, # Excel B13 = row_index 12 - column_index__lt=sum_column_index # Exclude sum column itself - ) - total_13 = Decimal('0') - for cell in cells_row13: - if cell.value is not None: - total_13 += Decimal(str(cell.value)) - - # K19 = sum of row 19 (Excel B19 -> row_index 18) across all clients - cells_row19 = Cell.objects.filter( - sheet=sheet, - table_type='top_left', - row_index=18, # Excel B19 = row_index 18 - column_index__lt=sum_column_index - ) - total_19 = Decimal('0') - for cell in cells_row19: - if cell.value is not None: - total_19 += Decimal(str(cell.value)) - - # Calculate: IF(K13=0; 0; K19/K13) - if total_13 == 0: - return Decimal('0') - return total_19 / total_13 - - # Normal summation for other rows - cells_in_row = Cell.objects.filter( - sheet=sheet, - table_type=table_type, - row_index=row_index, - column_index__lt=sum_column_index - ) - - total = Decimal('0') - for cell in cells_in_row: - if cell.value is not None: - total += Decimal(str(cell.value)) - - return total - - except Exception as e: - print(f"Error calculating summation for {table_type}[{row_index}]: {e}") - return None - -# Helper function for calculations -def evaluate_formula(formula, values_dict): - """ - Safely evaluate a formula like "10 + 9" where numbers are row indices - values_dict: {row_index: decimal_value} - """ - from decimal import Decimal - import re - - try: - # Create a copy of the formula to work with - expr = formula - - # Find all row numbers in the formula - row_refs = re.findall(r'\b\d+\b', expr) - - for row_ref in row_refs: - row_num = int(row_ref) - if row_num in values_dict and values_dict[row_num] is not None: - # Replace row reference with actual value - expr = expr.replace(row_ref, str(values_dict[row_num])) - else: - # Missing value - can't calculate - return None - - # Evaluate the expression - # Note: In production, use a safer evaluator like `asteval` - result = eval(expr, {"__builtins__": {}}, {}) - - # Convert to Decimal with proper rounding - return Decimal(str(round(result, 6))) - - except Exception: - return None - -# Monthly Sheet View -class MonthlySheetView(TemplateView): - template_name = 'monthly_sheet.html' - - def populate_helium_input_to_top_right(self, sheet): - """Populate bezug data from SecondTableEntry to top-right table (row 8 = Excel row 12)""" - from .models import SecondTableEntry, Cell, Client - from django.db.models.functions import Coalesce - from decimal import Decimal - - year = sheet.year - month = sheet.month - - TOP_RIGHT_CLIENTS = [ - "Dr. Fohrer", # Column index 0 (L) - "AG Buntk.", # Column index 1 (M) - "AG Alff", # Column index 2 (N) - "AG Gutfl.", # Column index 3 (O) - "M3 Thiele", # Column index 4 (P) - "M3 Buntkowsky", # Column index 5 (Q) - "M3 Gutfleisch", # Column index 6 (R) - ] - - # For each client in top-right table - for client_name in TOP_RIGHT_CLIENTS: - try: - client = Client.objects.get(name=client_name) - column_index = TOP_RIGHT_CLIENTS.index(client_name) - - # Calculate total LHe_output for this client in this month from SecondTableEntry - total_lhe_output = SecondTableEntry.objects.filter( - client=client, - date__year=year, - date__month=month - ).aggregate( - total=Coalesce(Sum('lhe_output'), Decimal('0')) - )['total'] - - # Get or create the cell for row_index 8 (Excel row 12) - Bezug - cell, created = Cell.objects.get_or_create( - sheet=sheet, - table_type='top_right', - client=client, - row_index=8, # Bezug row (Excel row 12) - column_index=column_index, - defaults={'value': total_lhe_output} - ) - - if not created and cell.value != total_lhe_output: - cell.value = total_lhe_output - cell.save() - - except Client.DoesNotExist: - continue - - # After populating bezug, trigger calculation for all dependent cells - # Get any cell to start the calculation - first_cell = Cell.objects.filter( - sheet=sheet, - table_type='top_right' - ).first() - - if first_cell: - save_view = SaveCellsView() - save_view.calculate_top_right_dependents(sheet, first_cell) - - return True - def calculate_bezug_from_entries(self, sheet, year, month): - """Calculate B11 (Bezug) from SecondTableEntry for all clients - ONLY for non-start sheets""" - from .models import SecondTableEntry, Cell, Client - from django.db.models import Sum - from django.db.models.functions import Coalesce - from decimal import Decimal - - # Check if this is the start sheet - if year == 2025 and month == 1: - return # Don't auto-calculate for start sheet - - for client in Client.objects.all(): - # Calculate total LHe output for this client in this month - lhe_output_sum = SecondTableEntry.objects.filter( - client=client, - date__year=year, - date__month=month - ).aggregate( - total=Coalesce(Sum('lhe_output'), Decimal('0')) - )['total'] - - # Update B11 cell (row_index 8 = UI Row 9) - b11_cell = Cell.objects.filter( - sheet=sheet, - table_type='top_left', - client=client, - row_index=8 # Excel B11 - ).first() - - if b11_cell and (b11_cell.value != lhe_output_sum or b11_cell.value is None): - b11_cell.value = lhe_output_sum - b11_cell.save() - - # Also trigger dependent calculations - from .views import SaveCellsView - save_view = SaveCellsView() - save_view.calculate_top_left_dependents(sheet, b11_cell) - # In MonthlySheetView.get_context_data() method, update the TOP_RIGHT_CLIENTS and row count: - - - - return True - def get_context_data(self, **kwargs): - from decimal import Decimal - context = super().get_context_data(**kwargs) - year = self.kwargs.get('year', datetime.now().year) - month = self.kwargs.get('month', datetime.now().month) - is_start_sheet = (year == FIRST_SHEET_YEAR and month == FIRST_SHEET_MONTH) - - # Get or create the monthly sheet - sheet, created = MonthlySheet.objects.get_or_create( - year=year, month=month - ) - - # All clients (used for bottom tables etc.) - clients = Client.objects.all().order_by('name') - - # Pre-fill cells if creating new sheet - if created: - self.initialize_sheet_cells(sheet, clients) - - # Apply previous month links (for B4 and B12) - self.apply_previous_month_links(sheet, year, month) - self.calculate_bezug_from_entries(sheet, year, month) - self.populate_helium_input_to_top_right(sheet) - self.apply_previous_month_links_top_right(sheet, year, month) - - # Define client groups - TOP_LEFT_CLIENTS = [ - "AG Vogel", - "AG Halfm", - "IKP", - ] - - TOP_RIGHT_CLIENTS = [ - "Dr. Fohrer", - "AG Buntk.", - "AG Alff", - "AG Gutfl.", - "M3 Thiele", - "M3 Buntkowsky", - "M3 Gutfleisch", - ] - current_summary = MonthlySummary.objects.filter(sheet=sheet).first() - - # Get previous month summary (for Bottom Table 3: K46 = prev K44) - prev_month_info = self.get_prev_month(year, month) - prev_summary = None - if not is_start_sheet: - prev_sheet = MonthlySheet.objects.filter( - year=prev_month_info['year'], - month=prev_month_info['month'] - ).first() - if prev_sheet: - prev_summary = MonthlySummary.objects.filter(sheet=prev_sheet).first() - - context.update({ - # ... your existing context ... - 'current_summary': current_summary, - 'prev_summary': prev_summary, - }) - # Update row counts in build_group_rows function - # Update row counts in build_group_rows function - def build_group_rows(sheet, table_type, client_names): - """Build rows for display in monthly sheet.""" - from decimal import Decimal - from .models import Cell - MERGED_ROWS = {2, 3, 5, 6, 7, 9, 10, 12, 14, 15} - MERGED_PAIRS = [ - ("Dr. Fohrer", "AG Buntk."), - ("AG Alff", "AG Gutfl."), - ] - rows = [] - - # Determine row count - row_counts = { - "top_left": 16, - "top_right": 16, # rows 0–15 - "bottom_1": 10, - "bottom_2": 10, - "bottom_3": 10, - } - row_count = row_counts.get(table_type, 0) - - # Get all cells for this sheet and table - all_cells = ( - Cell.objects.filter( - sheet=sheet, - table_type=table_type, - ).select_related("client") - ) - - # Group cells by row index - cells_by_row = {} - for cell in all_cells: - if cell.row_index not in cells_by_row: - cells_by_row[cell.row_index] = {} - cells_by_row[cell.row_index][cell.client.name] = cell - - # We will store row sums for Bezug (row 8) and Verbraucherverluste (row 14) - # for top_left / top_right so we can compute the overall % in row 15. - sum_bezug = None - sum_verbrauch = None - - # Build each row - for row_idx in range(row_count): - display_cells = [] - row_cells_dict = cells_by_row.get(row_idx, {}) - - # Build cells in the requested client order - for name in client_names: - cell = row_cells_dict.get(name) - display_cells.append(cell) - - # Calculate sum for this row (includes editable + calculated cells) - sum_value = None - total = Decimal("0") - has_value = False - - merged_second_indices = set() - if table_type == 'top_right' and row_idx in MERGED_ROWS: - for left_name, right_name in MERGED_PAIRS: - try: - right_idx = client_names.index(right_name) - merged_second_indices.add(right_idx) - except ValueError: - # client not in this table; just ignore - pass - - for col_idx, cell in enumerate(display_cells): - # Skip the duplicate (second) column of each merged pair - if col_idx in merged_second_indices: - continue - - if cell and cell.value is not None: - try: - total += Decimal(str(cell.value)) - has_value = True - except Exception: - pass - - if has_value: - sum_value = total - - # Remember special rows for top tables - if table_type in ("top_left", "top_right"): - if row_idx == 8: # Bezug - sum_bezug = total - elif row_idx == 14: # Verbraucherverluste - sum_verbrauch = total - - rows.append( - { - "cells": display_cells, - "sum": sum_value, - "row_index": row_idx, - } - ) - - # Adjust the % row sum for top_left / top_right: - # Sum(%) = Sum(Verbraucherverluste) / Sum(Bezug) - if table_type in ("top_left", "top_right"): - perc_row_idx = 15 # % row - if 0 <= perc_row_idx < len(rows): - if sum_bezug is not None and sum_bezug != 0 and sum_verbrauch is not None: - rows[perc_row_idx]["sum"] = sum_verbrauch / sum_bezug - else: - rows[perc_row_idx]["sum"] = None - - return rows - - - # Now call the local function - top_left_rows = build_group_rows(sheet, 'top_left', TOP_LEFT_CLIENTS) - top_right_rows = build_group_rows(sheet, 'top_right', TOP_RIGHT_CLIENTS) - - # --- Build combined summary of top-left + top-right Sum columns --- - - # Helper to safely get the Sum for a given row index - def get_row_sum(rows, row_index): - if 0 <= row_index < len(rows): - return rows[row_index].get('sum') - return None - - # Row definitions we want in the combined Σ table - # (row_index in top tables, label shown in the small table) - summary_row_defs = [ - (2, "Rückführung flüssig (Lit. L-He)"), - (3, "Sonderrückführungen (Lit. L-He)"), - (4, "Sammelrückführungen (Lit. L-He)"), - (5, "Bestand in Kannen-1 (Lit. L-He)"), - (6, "Summe Bestand (Lit. L-He)"), - (7, "Best. in Kannen Vormonat (Lit. L-He)"), - (8, "Bezug (Liter L-He)"), - (9, "Rückführ. Soll (Lit. L-He)"), - (10, "Verluste (Soll-Rückf.) (Lit. L-He)"), - (11, "Füllungen warm (Lit. L-He)"), - (12, "Kaltgas Rückgabe (Lit. L-He) – Faktor"), - (13, "Faktor 0.06"), - (14, "Verbraucherverluste (Liter L-He)"), - (15, "%"), - ] - - # Precompute totals for Bezug and Verbraucherverluste across both tables - bezug_left = get_row_sum(top_left_rows, 8) or Decimal('0') - bezug_right = get_row_sum(top_right_rows, 8) or Decimal('0') - total_bezug = bezug_left + bezug_right - - verb_left = get_row_sum(top_left_rows, 14) or Decimal('0') - verb_right = get_row_sum(top_right_rows, 14) or Decimal('0') - total_verbrauch = verb_left + verb_right - - summary_rows = [] - - for row_index, label in summary_row_defs: - # Faktor row: always fixed 0.06 - if row_index == 13: - summary_value = Decimal('0.06') - - # % row: total Verbraucherverluste / total Bezug - elif row_index == 15: - if total_bezug != 0: - summary_value = total_verbrauch / total_bezug - else: - summary_value = None - - else: - left_sum = get_row_sum(top_left_rows, row_index) - right_sum = get_row_sum(top_right_rows, row_index) - - # Sammelrückführungen: only from top-right table - if row_index == 4: - left_sum = None - - total = Decimal('0') - - has_any = False - if left_sum is not None: - total += Decimal(str(left_sum)) - has_any = True - if right_sum is not None: - total += Decimal(str(right_sum)) - has_any = True - - summary_value = total if has_any else None - - summary_rows.append({ - 'row_index': row_index, - 'label': label, - 'sum': summary_value, - }) - - # Get cells for bottom tables - cells_by_table = self.get_cells_by_table(sheet) - - context.update({ - 'sheet': sheet, - 'clients': clients, - 'year': year, - 'month': month, - 'month_name': calendar.month_name[month], - 'prev_month': self.get_prev_month(year, month), - 'next_month': self.get_next_month(year, month), - 'cells_by_table': cells_by_table, - 'top_left_headers': TOP_LEFT_CLIENTS + ['Sum'], - 'top_right_headers': TOP_RIGHT_CLIENTS + ['Sum'], - 'top_left_rows': top_left_rows, - 'top_right_rows': top_right_rows, - 'summary_rows': summary_rows, # 👈 NEW - 'is_start_sheet': is_start_sheet, - }) - return context - - def get_cells_by_table(self, sheet): - """Organize cells by table type for easy template rendering""" - cells = sheet.cells.select_related('client').all() - organized = { - 'top_left': [[] for _ in range(16)], - 'top_right': [[] for _ in range(16)], # now 16 rows - 'bottom_1': [[] for _ in range(10)], - 'bottom_2': [[] for _ in range(10)], - 'bottom_3': [[] for _ in range(10)], - } - - for cell in cells: - if cell.table_type not in organized: - continue - - max_rows = len(organized[cell.table_type]) - if cell.row_index < 0 or cell.row_index >= max_rows: - # This is an "extra" cell from an older layout (e.g. top_left rows 18–23) - continue - - row_list = organized[cell.table_type][cell.row_index] - while len(row_list) <= cell.column_index: - row_list.append(None) - - row_list[cell.column_index] = cell - - return organized - - - def initialize_sheet_cells(self, sheet, clients): - """Create all empty cells for a new monthly sheet""" - cells_to_create = [] - summation_config = CALCULATION_CONFIG.get('summation_column', {}) - sum_column_index = summation_config.get('sum_column_index', len(clients) - 1) # Last column - - # For each table type and row - table_configs = [ - ('top_left', 16), - ('top_right', 16), - ('bottom_1', 10), - ('bottom_2', 10), - ('bottom_3', 10), - ] - - for table_type, row_count in table_configs: - for row_idx in range(row_count): - for col_idx, client in enumerate(clients): - is_summation = (col_idx == sum_column_index) - cells_to_create.append(Cell( - sheet=sheet, - client=client, - table_type=table_type, - row_index=row_idx, - column_index=col_idx, - value=None, - is_formula=is_summation, # Mark summation cells as formulas - )) - - # Bulk create all cells at once - Cell.objects.bulk_create(cells_to_create) - def get_prev_month(self, year, month): - """Get previous month year and month""" - if month == 1: - return {'year': year - 1, 'month': 12} - return {'year': year, 'month': month - 1} - - def get_next_month(self, year, month): - """Get next month year and month""" - if month == 12: - return {'year': year + 1, 'month': 1} - return {'year': year, 'month': month + 1} - def apply_previous_month_links(self, sheet, year, month): - """ - For non-start sheets: - B4 (row 2) = previous sheet B3 (row 1) - B10 (row 8) = previous sheet B9 (row 7) - """ - # Do nothing on the first sheet - if year == FIRST_SHEET_YEAR and month == FIRST_SHEET_MONTH: - return - - # Figure out previous month - if month == 1: - prev_year = year - 1 - prev_month = 12 - else: - prev_year = year - prev_month = month - 1 - - from .models import MonthlySheet, Cell, Client - - prev_sheet = MonthlySheet.objects.filter( - year=prev_year, - month=prev_month - ).first() - - if not prev_sheet: - # No previous sheet created yet → nothing to copy - return - - # For each client, copy values - for client in Client.objects.all(): - # B3(prev) → B4(curr): UI row 1 → row 2 → row_index 0 → 1 - prev_b3 = Cell.objects.filter( - sheet=prev_sheet, - table_type='top_left', - client=client, - row_index=0, # UI row 1 - ).first() - - curr_b4 = Cell.objects.filter( - sheet=sheet, - table_type='top_left', - client=client, - row_index=1, # UI row 2 - ).first() - - if prev_b3 and curr_b4: - curr_b4.value = prev_b3.value - curr_b4.save() - - # B9(prev) → B10(curr): UI row 7 → row 8 → row_index 6 → 7 - prev_b9 = Cell.objects.filter( - sheet=prev_sheet, - table_type='top_left', - client=client, - row_index=6, # UI row 7 (Summe Bestand) - ).first() - - curr_b10 = Cell.objects.filter( - sheet=sheet, - table_type='top_left', - client=client, - row_index=7, # UI row 8 (Best. in Kannen Vormonat) - ).first() - - if prev_b9 and curr_b10: - curr_b10.value = prev_b9.value - curr_b10.save() - - def apply_previous_month_links_top_right(self, sheet, year, month): - """ - top_right row 7: Best. in Kannen Vormonat (Lit. L-He) - = previous sheet's Summe Bestand (row 6). - - For merged pairs: - - Dr. Fohrer + AG Buntk. share the SAME value (from previous month's AG Buntk. or Dr. Fohrer) - - AG Alff + AG Gutfl. share the SAME value (from previous month's AG Alff or AG Gutfl.) - M3 clients just copy their own value. - """ - from .models import MonthlySheet, Cell, Client - from decimal import Decimal - - # Do nothing on first sheet - if year == FIRST_SHEET_YEAR and month == FIRST_SHEET_MONTH: - return - - # find previous month - if month == 1: - prev_year = year - 1 - prev_month = 12 - else: - prev_year = year - prev_month = month - 1 - - prev_sheet = MonthlySheet.objects.filter( - year=prev_year, - month=prev_month - ).first() - - if not prev_sheet: - return # nothing to copy from - - TOP_RIGHT_CLIENTS = [ - "Dr. Fohrer", - "AG Buntk.", - "AG Alff", - "AG Gutfl.", - "M3 Thiele", - "M3 Buntkowsky", - "M3 Gutfleisch", - ] - - # Helper function to get a cell value - def get_cell_value(sheet_obj, client_name, row_index): - client_obj = Client.objects.filter(name=client_name).first() - if not client_obj: - return None - - try: - col_idx = TOP_RIGHT_CLIENTS.index(client_name) - except ValueError: - return None - - cell = Cell.objects.filter( - sheet=sheet_obj, - table_type='top_right', - client=client_obj, - row_index=row_index, - column_index=col_idx, - ).first() - - return cell.value if cell else None - - # Helper function to set a cell value - def set_cell_value(sheet_obj, client_name, row_index, value): - client_obj = Client.objects.filter(name=client_name).first() - if not client_obj: - return False - - try: - col_idx = TOP_RIGHT_CLIENTS.index(client_name) - except ValueError: - return False - - cell, created = Cell.objects.get_or_create( - sheet=sheet_obj, - table_type='top_right', - client=client_obj, - row_index=row_index, - column_index=col_idx, - defaults={'value': value} - ) - - if not created and cell.value != value: - cell.value = value - cell.save() - elif created: - cell.save() - - return True - - # ----- Pair 1: Dr. Fohrer + AG Buntk. ----- - # Get previous month's Summe Bestand (row 6) for either client in the pair - pair1_prev_val = None - - # Try AG Buntk. first - prev_buntk_val = get_cell_value(prev_sheet, "AG Buntk.", 6) - if prev_buntk_val is not None: - pair1_prev_val = prev_buntk_val - else: - # Try Dr. Fohrer if AG Buntk. is empty - prev_fohrer_val = get_cell_value(prev_sheet, "Dr. Fohrer", 6) - if prev_fohrer_val is not None: - pair1_prev_val = prev_fohrer_val - - # Apply the value to both clients in the pair - if pair1_prev_val is not None: - set_cell_value(sheet, "Dr. Fohrer", 7, pair1_prev_val) - set_cell_value(sheet, "AG Buntk.", 7, pair1_prev_val) - - # ----- Pair 2: AG Alff + AG Gutfl. ----- - pair2_prev_val = None - - # Try AG Alff first - prev_alff_val = get_cell_value(prev_sheet, "AG Alff", 6) - if prev_alff_val is not None: - pair2_prev_val = prev_alff_val - else: - # Try AG Gutfl. if AG Alff is empty - prev_gutfl_val = get_cell_value(prev_sheet, "AG Gutfl.", 6) - if prev_gutfl_val is not None: - pair2_prev_val = prev_gutfl_val - - # Apply the value to both clients in the pair - if pair2_prev_val is not None: - set_cell_value(sheet, "AG Alff", 7, pair2_prev_val) - set_cell_value(sheet, "AG Gutfl.", 7, pair2_prev_val) - - # ----- M3 clients: copy their own Summe Bestand ----- - for name in ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"]: - prev_val = get_cell_value(prev_sheet, name, 6) - if prev_val is not None: - set_cell_value(sheet, name, 7, prev_val) -# Add this helper function to views.py -def get_factor_value(table_type, row_index): - """Get factor value (like 0.06 for top_left row 17)""" - factors = { - ('top_left', 17): Decimal('0.06'), # A18 in Excel (UI row 17, 0-based index 16) - } - return factors.get((table_type, row_index), Decimal('0')) -# Save Cells View -# views.py - Updated SaveCellsView -# views.py - Update SaveCellsView class -def debug_cell_values(self, sheet, client_id): - """Debug method to check cell values""" - from .models import Cell - cells = Cell.objects.filter( - sheet=sheet, - table_type='top_left', - client_id=client_id - ).order_by('row_index') - - debug_info = {} - for cell in cells: - debug_info[f"row_{cell.row_index}"] = { - 'value': str(cell.value) if cell.value else 'None', - 'ui_row': cell.row_index + 1, - 'excel_ref': f"B{cell.row_index + 3}" - } - - return debug_info -class DebugCalculationView(View): - """Debug view to test calculations directly""" - def get(self, request): - sheet_id = request.GET.get('sheet_id', 1) - client_name = request.GET.get('client', 'AG Vogel') - - try: - sheet = MonthlySheet.objects.get(id=sheet_id) - client = Client.objects.get(name=client_name) - - # Get SaveCellsView instance - save_view = SaveCellsView() - - # Create a dummy cell to trigger calculations - dummy_cell = Cell.objects.filter( - sheet=sheet, - table_type='top_left', - client=client, - row_index=0 # B3 - ).first() - - if not dummy_cell: - return JsonResponse({'error': 'No cells found for this client'}) - - # Trigger calculation - updated = save_view.calculate_top_left_dependents(sheet, dummy_cell) - - # Get updated cell values - cells = Cell.objects.filter( - sheet=sheet, - table_type='top_left', - client=client - ).order_by('row_index') - - cell_data = [] - for cell in cells: - cell_data.append({ - 'row_index': cell.row_index, - 'ui_row': cell.row_index + 1, - 'excel_ref': f"B{cell.row_index + 3}", - 'value': str(cell.value) if cell.value else 'None', - 'description': self.get_row_description(cell.row_index) - }) - - return JsonResponse({ - 'sheet': f"{sheet.year}-{sheet.month:02d}", - 'client': client.name, - 'cells': cell_data, - 'updated_count': len(updated), - 'calculation': 'B5 = IF(B3>0; B3-B4; 0)' - }) - - except Exception as e: - return JsonResponse({'error': str(e)}, status=400) - - def get_row_description(self, row_index): - """Get description for row index""" - descriptions = { - 0: "B3: Stand der Gaszähler (Nm³)", - 1: "B4: Stand der Gaszähler (Vormonat) (Nm³)", - 2: "B5: Gasrückführung (Nm³)", - 3: "B6: Rückführung flüssig (Lit. L-He)", - 4: "B7: Sonderrückführungen (Lit. L-He)", - 5: "B8: Bestand in Kannen-1 (Lit. L-He)", - 6: "B9: Summe Bestand (Lit. L-He)", - 7: "B10: Best. in Kannen Vormonat (Lit. L-He)", - 8: "B11: Bezug (Liter L-He)", - 9: "B12: Rückführ. Soll (Lit. L-He)", - 10: "B13: Verluste (Soll-Rückf.) (Lit. L-He)", - 11: "B14: Füllungen warm (Lit. L-He)", - 12: "B15: Kaltgas Rückgabe (Lit. L-He) – Faktor", - 13: "B16: Faktor", - 14: "B17: Verbraucherverluste (Liter L-He)", - 15: "B18: %" - } - return descriptions.get(row_index, f"Row {row_index}") -def recalculate_stand_der_gaszahler(self, sheet): - """Recalculate Stand der Gaszähler for all client pairs""" - from decimal import Decimal - - # For Dr. Fohrer and AG Buntk. (L & M columns) - try: - # Get Dr. Fohrer's bezug - dr_fohrer_client = Client.objects.get(name="Dr. Fohrer") - dr_fohrer_cell = Cell.objects.filter( - sheet=sheet, - table_type='top_right', - client=dr_fohrer_client, - row_index=9 # Row 9 = Bezug - ).first() - L13 = Decimal(str(dr_fohrer_cell.value)) if dr_fohrer_cell and dr_fohrer_cell.value else Decimal('0') - - # Get AG Buntk.'s bezug - ag_buntk_client = Client.objects.get(name="AG Buntk.") - ag_buntk_cell = Cell.objects.filter( - sheet=sheet, - table_type='top_right', - client=ag_buntk_client, - row_index=9 - ).first() - M13 = Decimal(str(ag_buntk_cell.value)) if ag_buntk_cell and ag_buntk_cell.value else Decimal('0') - - total = L13 + M13 - if total > 0: - # Update Dr. Fohrer's row 0 - dr_fohrer_row0 = Cell.objects.filter( - sheet=sheet, - table_type='top_right', - client=dr_fohrer_client, - row_index=0 - ).first() - if dr_fohrer_row0: - dr_fohrer_row0.value = L13 / total - dr_fohrer_row0.save() - - # Update AG Buntk.'s row 0 - ag_buntk_row0 = Cell.objects.filter( - sheet=sheet, - table_type='top_right', - client=ag_buntk_client, - row_index=0 - ).first() - if ag_buntk_row0: - ag_buntk_row0.value = M13 / total - ag_buntk_row0.save() - except Exception as e: - print(f"Error recalculating Stand der Gaszähler for Dr. Fohrer/AG Buntk.: {e}") - - # For AG Alff and AG Gutfl. (N & O columns) - try: - # Get AG Alff's bezug - ag_alff_client = Client.objects.get(name="AG Alff") - ag_alff_cell = Cell.objects.filter( - sheet=sheet, - table_type='top_right', - client=ag_alff_client, - row_index=9 - ).first() - N13 = Decimal(str(ag_alff_cell.value)) if ag_alff_cell and ag_alff_cell.value else Decimal('0') - - # Get AG Gutfl.'s bezug - ag_gutfl_client = Client.objects.get(name="AG Gutfl.") - ag_gutfl_cell = Cell.objects.filter( - sheet=sheet, - table_type='top_right', - client=ag_gutfl_client, - row_index=9 - ).first() - O13 = Decimal(str(ag_gutfl_cell.value)) if ag_gutfl_cell and ag_gutfl_cell.value else Decimal('0') - - total = N13 + O13 - if total > 0: - # Update AG Alff's row 0 - ag_alff_row0 = Cell.objects.filter( - sheet=sheet, - table_type='top_right', - client=ag_alff_client, - row_index=0 - ).first() - if ag_alff_row0: - ag_alff_row0.value = N13 / total - ag_alff_row0.save() - - # Update AG Gutfl.'s row 0 - ag_gutfl_row0 = Cell.objects.filter( - sheet=sheet, - table_type='top_right', - client=ag_gutfl_client, - row_index=0 - ).first() - if ag_gutfl_row0: - ag_gutfl_row0.value = O13 / total - ag_gutfl_row0.save() - except Exception as e: - print(f"Error recalculating Stand der Gaszähler for AG Alff/AG Gutfl.: {e}") -# In your SaveCellsView class in views.py -class DebugTopRightView(View): - """Debug view to check top_right calculations""" - def get(self, request): - sheet_id = request.GET.get('sheet_id', 1) - client_name = request.GET.get('client', 'Dr. Fohrer') - - try: - sheet = MonthlySheet.objects.get(id=sheet_id) - client = Client.objects.get(name=client_name) - - # Get all cells for this client in top_right - cells = Cell.objects.filter( - sheet=sheet, - table_type='top_right', - client=client - ).order_by('row_index') - - cell_data = [] - descriptions = { - 0: "Stand der Gaszähler (Vormonat)", - 1: "Gasrückführung (Nm³)", - 2: "Rückführung flüssig", - 3: "Sonderrückführungen", - 4: "Sammelrückführungen", - 5: "Bestand in Kannen-1", - 6: "Summe Bestand", - 7: "Best. in Kannen Vormonat", - 8: "Same as row 9 from prev sheet", - 9: "Bezug", - 10: "Rückführ. Soll", - 11: "Verluste", - 12: "Füllungen warm", - 13: "Kaltgas Rückgabe", - 14: "Faktor 0.06", - 15: "Verbraucherverluste", - 16: "%" - } - - for cell in cells: - cell_data.append({ - 'row_index': cell.row_index, - 'ui_row': cell.row_index + 1, - 'description': descriptions.get(cell.row_index, f"Row {cell.row_index}"), - 'value': str(cell.value) if cell.value else 'Empty', - 'cell_id': cell.id - }) - - # Test calculation - row3_cell = cells.filter(row_index=3).first() - row5_cell = cells.filter(row_index=5).first() - row6_cell = cells.filter(row_index=6).first() - - calculation_info = { - 'row3_value': str(row3_cell.value) if row3_cell and row3_cell.value else '0', - 'row5_value': str(row5_cell.value) if row5_cell and row5_cell.value else '0', - 'row6_value': str(row6_cell.value) if row6_cell and row6_cell.value else '0', - 'expected_sum': '0' - } - - if row3_cell and row5_cell and row6_cell: - row3_val = Decimal(str(row3_cell.value)) if row3_cell.value else Decimal('0') - row5_val = Decimal(str(row5_cell.value)) if row5_cell.value else Decimal('0') - expected = row3_val + row5_val - calculation_info['expected_sum'] = str(expected) - calculation_info['is_correct'] = row6_cell.value == expected - - return JsonResponse({ - 'sheet': f"{sheet.year}-{sheet.month}", - 'client': client.name, - 'cells': cell_data, - 'calculation': calculation_info - }) - - except Exception as e: - return JsonResponse({'error': str(e)}, status=400) - - -class SaveCellsView(View): - - def calculate_bottom_3_dependents(self, sheet): - updated_cells = [] - - def get_cell(row_idx, col_idx): - return Cell.objects.filter( - sheet=sheet, - table_type='bottom_3', - row_index=row_idx, - column_index=col_idx - ).first() - - def set_cell(row_idx, col_idx, value): - cell, created = Cell.objects.get_or_create( - sheet=sheet, - table_type='bottom_3', - row_index=row_idx, - column_index=col_idx, - defaults={'value': value} - ) - if not created and cell.value != value: - cell.value = value - cell.save() - elif created: - cell.save() - - updated_cells.append({ - 'id': cell.id, - 'value': str(cell.value) if cell.value is not None else '', - 'is_calculated': True, - }) - - def dec(x): - if x in (None, ''): - return Decimal('0') - try: - return Decimal(str(x)) - except Exception: - return Decimal('0') - - # ---- current summary ---- - cur_sum = MonthlySummary.objects.filter(sheet=sheet).first() - curr_k44 = dec(cur_sum.gesamtbestand_neu_lhe) if cur_sum else Decimal('0') - total_verb = dec(cur_sum.verbraucherverlust_lhe) if cur_sum else Decimal('0') - - # ---- previous month summary ---- - year, month = sheet.year, sheet.month - if month == 1: - prev_year, prev_month = year - 1, 12 - else: - prev_year, prev_month = year, month - 1 - - prev_sheet = MonthlySheet.objects.filter(year=prev_year, month=prev_month).first() - if prev_sheet: - prev_sum = MonthlySummary.objects.filter(sheet=prev_sheet).first() - prev_k44 = dec(prev_sum.gesamtbestand_neu_lhe) if prev_sum else Decimal('0') - else: - prev_k44 = Decimal('0') - - # ---- read editable inputs from bottom_3: F47,G47,I47,I50 ---- - def get_val(r, c): - cell = get_cell(r, c) - return dec(cell.value if cell else None) - - f47 = get_val(1, 0) - g47 = get_val(1, 1) - i47 = get_val(1, 2) - i50 = get_val(4, 2) - - # Now apply your formulas using prev_k44, curr_k44, total_verb, f47,g47,i47,i50 - # Row indices: 0..7 correspond to 46..53 - # col 3 = J, col 4 = K - - # Row 46 - k46 = prev_k44 - j46 = k46 * Decimal('0.75') - set_cell(0, 3, j46) - set_cell(0, 4, k46) - - # Row 47 - g47 = self._dec((get_cell(1, 1) or {}).value if get_cell(1, 1) else None) - i47 = self._dec((get_cell(1, 2) or {}).value if get_cell(1, 2) else None) - - j47 = g47 + i47 - k47 = (j47 / Decimal('0.75')) + g47 if j47 != 0 else g47 - - set_cell(1, 3, j47) - set_cell(1, 4, k47) - - # Row 48 - k48 = k46 + k47 - j48 = k48 * Decimal('0.75') - set_cell(2, 3, j48) - set_cell(2, 4, k48) - - # Row 49 - k49 = curr_k44 - j49 = k49 * Decimal('0.75') - set_cell(3, 3, j49) - set_cell(3, 4, k49) - - # Row 50 - j50 = i50 - k50 = j50 / Decimal('0.75') if j50 != 0 else Decimal('0') - set_cell(4, 3, j50) - set_cell(4, 4, k50) - - # Row 51 - k51 = k48 - k49 - k50 - j51 = k51 * Decimal('0.75') - set_cell(5, 3, j51) - set_cell(5, 4, k51) - - # Row 52 - k52 = total_verb - j52 = k52 * Decimal('0.75') - set_cell(6, 3, j52) - set_cell(6, 4, k52) - - # Row 53 - j53 = j51 - j52 - k53 = k51 - k52 - set_cell(7, 3, j53) - set_cell(7, 4, k53) - - return updated_cells - - - def _dec(self, value): - """Convert value to Decimal or return 0.""" - if value is None or value == '': - return Decimal('0') - try: - return Decimal(str(value)) - except Exception: - return Decimal('0') - - def post(self, request, *args, **kwargs): - """ - Handle AJAX saves from monthly_sheet.html - - Single-cell save: when cell_id is present (blur on one cell) - - Bulk save: when the 'Save All Cells' button is used (no cell_id) - """ - try: - sheet_id = request.POST.get('sheet_id') - if not sheet_id: - return JsonResponse({ - 'status': 'error', - 'message': 'Missing sheet_id' - }) - - sheet = MonthlySheet.objects.get(id=sheet_id) - - # -------- Single-cell update (blur) -------- - cell_id = request.POST.get('cell_id') - if cell_id: - value_raw = (request.POST.get('value') or '').strip() - - try: - cell = Cell.objects.get(id=cell_id, sheet=sheet) - except Cell.DoesNotExist: - return JsonResponse({ - 'status': 'error', - 'message': 'Cell not found' - }) - - # Convert value to Decimal or None - if value_raw == '': - new_value = None - else: - try: - # Allow comma or dot - value_clean = value_raw.replace(',', '.') - new_value = Decimal(value_clean) - except (InvalidOperation, ValueError): - # If conversion fails, treat as empty - new_value = None - - old_value = cell.value - cell.value = new_value - cell.save() - - updated_cells = [{ - 'id': cell.id, - 'value': '' if cell.value is None else str(cell.value), - 'is_calculated': cell.is_formula, # model field - }] - - # Recalculate dependents depending on table_type - if cell.table_type == 'top_left': - updated_cells.extend( - self.calculate_top_left_dependents(sheet, cell) - ) - elif cell.table_type == 'top_right': - updated_cells.extend( - self.calculate_top_right_dependents(sheet, cell) - ) - elif cell.table_type == 'bottom_1': - updated_cells.extend(self.calculate_bottom_1_dependents(sheet, cell)) - elif cell.table_type == 'bottom_3': - updated_cells.extend( - self.calculate_bottom_3_dependents(sheet) - ) - # bottom_1 / bottom_2 / bottom_3 currently have no formulas: - # they just save the new value. - updated_cells += self.calculate_bottom_3_dependents(sheet) - return JsonResponse({ - 'status': 'success', - 'updated_cells': updated_cells - }) - - - # -------- Bulk save (Save All button) -------- - return self.save_bulk_cells(request, sheet) - - except MonthlySheet.DoesNotExist: - return JsonResponse({ - 'status': 'error', - 'message': 'Sheet not found' - }) - except Exception as e: - # Generic safety net so the frontend sees an error message - return JsonResponse({ - 'status': 'error', - 'message': str(e) - }) - - # ... your other methods above ... - - # Update the calculate_top_right_dependents method in SaveCellsView class - - def calculate_top_right_dependents(self, sheet, changed_cell): - """ - Recalculate all dependent cells in the top-right table according to Excel formulas. - - Excel rows (4-20) -> 0-based indices (0-15) - Rows: - 0: Stand der Gaszähler (Vormonat) (Nm³) - shares for M3 clients - 1: Gasrückführung (Nm³) - 2: Rückführung flüssig (Lit. L-He) - 3: Sonderrückführungen (Lit. L-He) - editable - 4: Sammelrückführungen (Lit. L-He) - from helium_input groups - 5: Bestand in Kannen-1 (Lit. L-He) - editable, merged in pairs - 6: Summe Bestand (Lit. L-He) = row 5 - 7: Best. in Kannen Vormonat (Lit. L-He) - from previous month - 8: Bezug (Liter L-He) - from SecondTableEntry - 9: Rückführ. Soll (Lit. L-He) - calculated - 10: Verluste (Soll-Rückf.) (Lit. L-He) - calculated - 11: Füllungen warm (Lit. L-He) - from SecondTableEntry warm outputs - 12: Kaltgas Rückgabe (Lit. L-He) – Faktor - calculated - 13: Faktor 0.06 - fixed - 14: Verbraucherverluste (Liter L-He) - calculated - 15: % - calculated - """ - from decimal import Decimal - from django.db.models import Sum, Count - from django.db.models.functions import Coalesce - from .models import Client, Cell, ExcelEntry, SecondTableEntry - - TOP_RIGHT_CLIENTS = [ - "Dr. Fohrer", # L - "AG Buntk.", # M (merged with L) - "AG Alff", # N - "AG Gutfl.", # O (merged with N) - "M3 Thiele", # P - "M3 Buntkowsky", # Q - "M3 Gutfleisch", # R - ] - - # Define merged pairs - MERGED_PAIRS = [ - ("Dr. Fohrer", "AG Buntk."), # L and M are merged - ("AG Alff", "AG Gutfl."), # N and O are merged - ] - - # M3 clients (not merged, calculated individually) - M3_CLIENTS = ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"] - - # Groups for Sammelrückführungen (helium_input) - HELIUM_INPUT_GROUPS = { - "fohrer_buntk": ["Dr. Fohrer", "AG Buntk."], - "alff_gutfl": ["AG Alff", "AG Gutfl."], - "m3": ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"], - } - - year = sheet.year - month = sheet.month - factor = Decimal('0.06') # Fixed factor from Excel - updated_cells = [] - - # Helper functions - def get_val(client_name, row_idx): - """Get cell value for a client and row""" - try: - client = Client.objects.get(name=client_name) - col_idx = TOP_RIGHT_CLIENTS.index(client_name) - cell = Cell.objects.filter( - sheet=sheet, - table_type='top_right', - client=client, - row_index=row_idx, - column_index=col_idx - ).first() - if cell and cell.value is not None: - return Decimal(str(cell.value)) - except (Client.DoesNotExist, ValueError, KeyError): - pass - return Decimal('0') - - def set_val(client_name, row_idx, value, is_calculated=True): - """Set cell value for a client and row""" - try: - client = Client.objects.get(name=client_name) - col_idx = TOP_RIGHT_CLIENTS.index(client_name) - - cell, created = Cell.objects.get_or_create( - sheet=sheet, - table_type='top_right', - client=client, - row_index=row_idx, - column_index=col_idx, - defaults={'value': value} - ) - - # Only update if value changed - if not created and cell.value != value: - cell.value = value - cell.save() - elif created: - cell.save() - - updated_cells.append({ - 'id': cell.id, - 'value': str(cell.value) if cell.value else '', - 'is_calculated': is_calculated - }) - - return True - except (Client.DoesNotExist, ValueError): - return False - - # 1. Update Summe Bestand (row 6) from Bestand in Kannen-1 (row 5) - # For merged pairs: copy value from changed cell to its pair - if changed_cell and changed_cell.table_type == 'top_right': - if changed_cell.row_index == 5: # Bestand in Kannen-1 - client_name = changed_cell.client.name - new_value = changed_cell.value - - # Check if this client is in a merged pair - for pair in MERGED_PAIRS: - if client_name in pair: - # Update both clients in the pair - for client_in_pair in pair: - if client_in_pair != client_name: - set_val(client_in_pair, 5, new_value, is_calculated=False) - break - - # 2. For all clients: Set Summe Bestand (row 6) = Bestand in Kannen-1 (row 5) - for client_name in TOP_RIGHT_CLIENTS: - bestand_value = get_val(client_name, 5) - set_val(client_name, 6, bestand_value, is_calculated=True) - - # 3. Update Sammelrückführungen (row 4) from helium_input groups - for group_name, client_names in HELIUM_INPUT_GROUPS.items(): - # Get total helium_input for this group - clients_in_group = Client.objects.filter(name__in=client_names) - total_helium = ExcelEntry.objects.filter( - client__in=clients_in_group, - date__year=year, - date__month=month - ).aggregate(total=Coalesce(Sum('lhe_ges'), Decimal('0')))['total'] - - # Set same value for all clients in group - for client_name in client_names: - set_val(client_name, 4, total_helium, is_calculated=True) - - # 4. Calculate Rückführung flüssig (row 2) - # For merged pairs: =L8 for Dr. Fohrer/AG Buntk., =N8 for AG Alff/AG Gutfl. - # For M3 clients: =$P$8 * P4, $P$8 * Q4, $P$8 * R4 - - # Get Sammelrückführungen values for groups - sammel_fohrer_buntk = get_val("Dr. Fohrer", 4) # L8 - sammel_alff_gutfl = get_val("AG Alff", 4) # N8 - sammel_m3_group = get_val("M3 Thiele", 4) # P8 (same for all M3) - - # For merged pairs - set_val("Dr. Fohrer", 2, sammel_fohrer_buntk, is_calculated=True) - set_val("AG Buntk.", 2, sammel_fohrer_buntk, is_calculated=True) - set_val("AG Alff", 2, sammel_alff_gutfl, is_calculated=True) - set_val("AG Gutfl.", 2, sammel_alff_gutfl, is_calculated=True) - - # For M3 clients: =$P$8 * column4 (Stand der Gaszähler) - for m3_client in M3_CLIENTS: - stand_value = get_val(m3_client, 0) # Stand der Gaszähler (row 0) - rueck_value = sammel_m3_group * stand_value - set_val(m3_client, 2, rueck_value, is_calculated=True) - - # 5. Calculate Füllungen warm (row 11) from SecondTableEntry warm outputs - # 5. Calculate Füllungen warm (row 11) as NUMBER of warm fillings - # (sum of 1s where each warm SecondTableEntry is one filling) - for client_name in TOP_RIGHT_CLIENTS: - client = Client.objects.get(name=client_name) - warm_count = SecondTableEntry.objects.filter( - client=client, - date__year=year, - date__month=month, - is_warm=True - ).aggregate( - total=Coalesce(Count('id'), 0) - )['total'] - - # store as Decimal so later formulas (warm * 15) still work nicely - warm_value = Decimal(warm_count) - set_val(client_name, 11, warm_value, is_calculated=True) - # 6. Set Faktor row (13) to 0.06 - for client_name in TOP_RIGHT_CLIENTS: - set_val(client_name, 13, factor, is_calculated=True) - - # 6a. Recalculate Stand der Gaszähler (row 0) for the merged pairs - # according to Excel: - # L4 = L13 / (L13 + M13), M4 = M13 / (L13 + M13) - # N4 = N13 / (N13 + O13), O4 = O13 / (N13 + O13) - - # Pair 1: Dr. Fohrer / AG Buntk. - bezug_dr = get_val("Dr. Fohrer", 8) # L13 - bezug_buntk = get_val("AG Buntk.", 8) # M13 - total_pair1 = bezug_dr + bezug_buntk - - if total_pair1 != 0: - set_val("Dr. Fohrer", 0, bezug_dr / total_pair1, is_calculated=True) - set_val("AG Buntk.", 0, bezug_buntk / total_pair1, is_calculated=True) - else: - # if no Bezug, both shares are 0 - set_val("Dr. Fohrer", 0, Decimal('0'), is_calculated=True) - set_val("AG Buntk.", 0, Decimal('0'), is_calculated=True) - - # Pair 2: AG Alff / AG Gutfl. - bezug_alff = get_val("AG Alff", 8) # N13 - bezug_gutfl = get_val("AG Gutfl.", 8) # O13 - total_pair2 = bezug_alff + bezug_gutfl - - if total_pair2 != 0: - set_val("AG Alff", 0, bezug_alff / total_pair2, is_calculated=True) - set_val("AG Gutfl.", 0, bezug_gutfl / total_pair2, is_calculated=True) - else: - set_val("AG Alff", 0, Decimal('0'), is_calculated=True) - set_val("AG Gutfl.", 0, Decimal('0'), is_calculated=True) - - - # 7. Calculate all other dependent rows for merged pairs - for pair in MERGED_PAIRS: - client1, client2 = pair - - # Get values for the pair - bezug1 = get_val(client1, 8) # Bezug client1 - bezug2 = get_val(client2, 8) # Bezug client2 - total_bezug = bezug1 + bezug2 # L13+M13 or N13+O13 - - summe_bestand = get_val(client1, 6) # L11 or N11 (merged, same value) - best_vormonat = get_val(client1, 7) # L12 or N12 (merged, same value) - rueck_fl = get_val(client1, 2) # L6 or N6 (merged, same value) - warm1 = get_val(client1, 11) # L16 or N16 - warm2 = get_val(client2, 11) # M16 or O16 - total_warm = warm1 + warm2 # L16+M16 or N16+O16 - - # Calculate Rückführ. Soll (row 9) - # = L13+M13 - L11 + L12 for first pair - # = N13+O13 - N11 + N12 for second pair - rueck_soll = total_bezug - summe_bestand + best_vormonat - set_val(client1, 9, rueck_soll, is_calculated=True) - set_val(client2, 9, rueck_soll, is_calculated=True) - - # Calculate Verluste (row 10) = Rückführ. Soll - Rückführung flüssig - verluste = rueck_soll - rueck_fl - set_val(client1, 10, verluste, is_calculated=True) - set_val(client2, 10, verluste, is_calculated=True) - - # Calculate Kaltgas Rückgabe (row 12) - # = (L13+M13)*$A18 + (L16+M16)*15 - kaltgas = (total_bezug * factor) + (total_warm * Decimal('15')) - set_val(client1, 12, kaltgas, is_calculated=True) - set_val(client2, 12, kaltgas, is_calculated=True) - - # Calculate Verbraucherverluste (row 14) = Verluste - Kaltgas - verbrauch = verluste - kaltgas - set_val(client1, 14, verbrauch, is_calculated=True) - set_val(client2, 14, verbrauch, is_calculated=True) - - # Calculate % (row 15) = Verbraucherverluste / (L13+M13) - if total_bezug != 0: - prozent = verbrauch / total_bezug - else: - prozent = Decimal('0') - set_val(client1, 15, prozent, is_calculated=True) - set_val(client2, 15, prozent, is_calculated=True) - - # 8. Calculate all dependent rows for M3 clients (individual calculations) - for m3_client in M3_CLIENTS: - # Get individual values - bezug = get_val(m3_client, 8) # Bezug for this M3 client - summe_bestand = get_val(m3_client, 6) # Summe Bestand - best_vormonat = get_val(m3_client, 7) # Best. in Kannen Vormonat - rueck_fl = get_val(m3_client, 2) # Rückführung flüssig - warm = get_val(m3_client, 11) # Füllungen warm - - # Calculate Rückführ. Soll (row 9) = Bezug - Summe Bestand + Best. Vormonat - rueck_soll = bezug - summe_bestand + best_vormonat - set_val(m3_client, 9, rueck_soll, is_calculated=True) - - # Calculate Verluste (row 10) = Rückführ. Soll - Rückführung flüssig - verluste = rueck_soll - rueck_fl - set_val(m3_client, 10, verluste, is_calculated=True) - - # Calculate Kaltgas Rückgabe (row 12) = Bezug * factor + warm * 15 - kaltgas = (bezug * factor) + (warm * Decimal('15')) - set_val(m3_client, 12, kaltgas, is_calculated=True) - - # Calculate Verbraucherverluste (row 14) = Verluste - Kaltgas - verbrauch = verluste - kaltgas - set_val(m3_client, 14, verbrauch, is_calculated=True) - - # Calculate % (row 15) = Verbraucherverluste / Bezug - if bezug != 0: - prozent = verbrauch / bezug - else: - prozent = Decimal('0') - set_val(m3_client, 15, prozent, is_calculated=True) - - return updated_cells - - - - def calculate_bottom_1_dependents(self, sheet, changed_cell): - """ - Recalculate Bottom Table 1 (table_type='bottom_1'). - - Layout (row_index 0–9, col_index 0–4): - - Rows 0–8: - 0: Batterie 1 - 1: 2 - 2: 3 - 3: 4 - 4: 5 - 5: 6 - 6: 2 Bündel - 7: 2 Ballone - 8: Reingasspeicher - - Row 9: - 9: Gasbestand (totals row) - - Columns: - 0: Volumen (fixed values, not editable) - 1: bar (editable for rows 0–8) - 2: korrigiert = bar / (bar/2000 + 1) - 3: Nm³ = Volumen * korrigiert - 4: Lit. LHe = Nm³ / 0.75 - - Row 9: - - Volumen (col 0): empty - - bar (col 1): empty - - korrigiert (col 2): empty - - Nm³ (col 3): SUM Nm³ rows 0–8 - - Lit. LHe (col 4): SUM Lit. LHe rows 0–8 - """ - from decimal import Decimal, InvalidOperation - from .models import Cell - - updated_cells = [] - - DATA_ROWS = list(range(0, 9)) # 0–8 - TOTAL_ROW = 9 - - COL_VOL = 0 - COL_BAR = 1 - COL_KORR = 2 - COL_NM3 = 3 - COL_LHE = 4 - - # Fixed Volumen values for rows 0–8 - VOLUMES = [ - Decimal("2.4"), # row 0 - Decimal("5.1"), # row 1 - Decimal("4.0"), # row 2 - Decimal("1.0"), # row 3 - Decimal("4.0"), # row 4 - Decimal("0.4"), # row 5 - Decimal("1.2"), # row 6 - Decimal("20.0"), # row 7 - Decimal("5.0"), # row 8 - ] - - def get_cell(row_idx, col_idx): - return Cell.objects.filter( - sheet=sheet, - table_type="bottom_1", - row_index=row_idx, - column_index=col_idx, - ).first() - - def set_calc(row_idx, col_idx, value): - """ - Set a calculated cell and add it to updated_cells. - If value is None, we clear the cell. - """ - cell = get_cell(row_idx, col_idx) - if not cell: - cell = Cell( - sheet=sheet, - table_type="bottom_1", - row_index=row_idx, - column_index=col_idx, - value=value, - ) - else: - cell.value = value - cell.save() - - updated_cells.append( - { - "id": cell.id, - "value": "" if cell.value is None else str(cell.value), - "is_calculated": True, - } - ) - - # ---------- Rows 0–8: per-gasspeicher calculations ---------- - for row_idx in DATA_ROWS: - bar_cell = get_cell(row_idx, COL_BAR) - - # Volumen: fixed constant or, if present, value from DB - vol = VOLUMES[row_idx] - vol_cell = get_cell(row_idx, COL_VOL) - if vol_cell and vol_cell.value is not None: - try: - vol = Decimal(str(vol_cell.value)) - except (InvalidOperation, ValueError): - # fall back to fixed constant - vol = VOLUMES[row_idx] - - bar = None - try: - if bar_cell and bar_cell.value is not None: - bar = Decimal(str(bar_cell.value)) - except (InvalidOperation, ValueError): - bar = None - - # korrigiert = bar / (bar/2000 + 1) - if bar is not None and bar != 0: - korr = bar / (bar / Decimal("2000") + Decimal("1")) - else: - korr = None - - # Nm³ = Volumen * korrigiert - if korr is not None: - nm3 = vol * korr - else: - nm3 = None - - # Lit. LHe = Nm³ / 0.75 - if nm3 is not None: - lit_lhe = nm3 / Decimal("0.75") - else: - lit_lhe = None - - # Write calculated cells back (NOT Volumen or bar) - set_calc(row_idx, COL_KORR, korr) - set_calc(row_idx, COL_NM3, nm3) - set_calc(row_idx, COL_LHE, lit_lhe) - - # ---------- Row 9: totals (Gasbestand) ---------- - total_nm3 = Decimal("0") - total_lhe = Decimal("0") - has_nm3 = False - has_lhe = False - - for row_idx in DATA_ROWS: - nm3_cell = get_cell(row_idx, COL_NM3) - if nm3_cell and nm3_cell.value is not None: - try: - total_nm3 += Decimal(str(nm3_cell.value)) - has_nm3 = True - except (InvalidOperation, ValueError): - pass - - lhe_cell = get_cell(row_idx, COL_LHE) - if lhe_cell and lhe_cell.value is not None: - try: - total_lhe += Decimal(str(lhe_cell.value)) - has_lhe = True - except (InvalidOperation, ValueError): - pass - - # Volumen (0), bar (1), korrigiert (2) on total row stay empty - set_calc(TOTAL_ROW, COL_KORR, None) # explicitly clear korrigiert - set_calc(TOTAL_ROW, COL_NM3, total_nm3 if has_nm3 else None) - set_calc(TOTAL_ROW, COL_LHE, total_lhe if has_lhe else None) - - return updated_cells - - - - - - def calculate_top_left_dependents(self, sheet, changed_cell): - """Calculate dependent cells in top_left table""" - from decimal import Decimal - from django.db.models import Sum - from django.db.models.functions import Coalesce - - client_id = changed_cell.client_id - updated_cells = [] - - # Get all cells for this client in top_left table - client_cells = Cell.objects.filter( - sheet=sheet, - table_type='top_left', - client_id=client_id - ) - - # Create a dict for easy access - cell_dict = {} - for cell in client_cells: - cell_dict[cell.row_index] = cell - - # Get values with safe defaults - def get_cell_value(row_idx): - cell = cell_dict.get(row_idx) - if cell and cell.value is not None: - try: - return Decimal(str(cell.value)) - except: - return Decimal('0') - return Decimal('0') - - # 1. B5 = IF(B3>0; B3-B4; 0) - Gasrückführung - b3 = get_cell_value(0) # row_index 0 = Excel B3 (UI row 1) - b4 = get_cell_value(1) # row_index 1 = Excel B4 (UI row 2) - - # Calculate B5 - if b3 > 0: - b5_value = b3 - b4 - if b5_value < 0: - b5_value = Decimal('0') - else: - b5_value = Decimal('0') - - # Update B5 - FORCE update even if value appears the same - b5_cell = cell_dict.get(2) # row_index 2 = Excel B5 (UI row 3) - if b5_cell: - # Always update B5 when B3 or B4 changes - b5_cell.value = b5_value - b5_cell.save() - updated_cells.append({ - 'id': b5_cell.id, - 'value': str(b5_cell.value), - 'is_calculated': True - }) - - # 2. B6 = B5 / 0.75 - Rückführung flüssig - b6_value = b5_value / Decimal('0.75') - b6_cell = cell_dict.get(3) # row_index 3 = Excel B6 (UI row 4) - if b6_cell: - b6_cell.value = b6_value - b6_cell.save() - updated_cells.append({ - 'id': b6_cell.id, - 'value': str(b6_cell.value), - 'is_calculated': True - }) - - # 3. B9 = B7 + B8 - Summe Bestand - b7 = get_cell_value(4) # row_index 4 = Excel B7 (UI row 5) - b8 = get_cell_value(5) # row_index 5 = Excel B8 (UI row 6) - b9_value = b7 + b8 - b9_cell = cell_dict.get(6) # row_index 6 = Excel B9 (UI row 7) - if b9_cell: - b9_cell.value = b9_value - b9_cell.save() - updated_cells.append({ - 'id': b9_cell.id, - 'value': str(b9_cell.value), - 'is_calculated': True - }) - - # 4. B11 = Sum of LHe Output from SecondTableEntry for this client/month - Bezug - from .models import SecondTableEntry - client = changed_cell.client - - # Calculate total LHe output for this client in this month - # 4. B11 = Bezug (Liter L-He) - # For start sheet: manual entry, for other sheets: auto-calculated from SecondTableEntry - from .models import SecondTableEntry - client = changed_cell.client - - b11_cell = cell_dict.get(8) # row_index 8 = Excel B11 (UI row 9) - - # Check if this is the start sheet (2025-01) - is_start_sheet = (sheet.year == 2025 and sheet.month == 1) - - # Get the B11 value for calculations - b11_value = Decimal('0') - - if is_start_sheet: - # For start sheet, keep whatever value is already there (manual entry) - if b11_cell and b11_cell.value is not None: - b11_value = Decimal(str(b11_cell.value)) - else: - # For non-start sheets: auto-calculate from SecondTableEntry - lhe_output_sum = SecondTableEntry.objects.filter( - client=client, - date__year=sheet.year, - date__month=sheet.month - ).aggregate( - total=Coalesce(Sum('lhe_output'), Decimal('0')) - )['total'] - - b11_value = lhe_output_sum - - if b11_cell and (b11_cell.value != b11_value or b11_cell.value is None): - b11_cell.value = b11_value - b11_cell.save() - updated_cells.append({ - 'id': b11_cell.id, - 'value': str(b11_cell.value), - 'is_calculated': True # Calculated from SecondTableEntry - }) - - # 5. B12 = B11 + B10 - B9 - Rückführ. Soll - b10 = get_cell_value(7) # row_index 7 = Excel B10 (UI row 8) - b12_value = b11_value + b10 - b9_value # Use b11_value instead of lhe_output_sum - b12_cell = cell_dict.get(9) # row_index 9 = Excel B12 (UI row 10) - if b12_cell: - b12_cell.value = b12_value - b12_cell.save() - updated_cells.append({ - 'id': b12_cell.id, - 'value': str(b12_cell.value), - 'is_calculated': True - }) - - # 6. B13 = B12 - B6 - Verluste (Soll-Rückf.) - b13_value = b12_value - b6_value - b13_cell = cell_dict.get(10) # row_index 10 = Excel B13 (UI row 11) - if b13_cell: - b13_cell.value = b13_value - b13_cell.save() - updated_cells.append({ - 'id': b13_cell.id, - 'value': str(b13_cell.value), - 'is_calculated': True - }) - - # 7. B14 = Count of warm outputs - warm_count = SecondTableEntry.objects.filter( - client=client, - date__year=sheet.year, - date__month=sheet.month, - is_warm=True - ).count() - - b14_cell = cell_dict.get(11) # row_index 11 = Excel B14 (UI row 12) - if b14_cell: - b14_cell.value = Decimal(str(warm_count)) - b14_cell.save() - updated_cells.append({ - 'id': b14_cell.id, - 'value': str(b14_cell.value), - 'is_calculated': True - }) - - # 8. B15 = IF(B11>0; B11 * factor + B14 * 15; 0) - Kaltgas Rückgabe - factor = get_cell_value(13) # row_index 13 = Excel B16 (Faktor) (UI row 14) - if factor == 0: - factor = Decimal('0.06') # default factor - - if b11_value > 0: # Use b11_value - b15_value = b11_value * factor + Decimal(str(warm_count)) * Decimal('15') - else: - b15_value = Decimal('0') - - b15_cell = cell_dict.get(12) # row_index 12 = Excel B15 (UI row 13) - if b15_cell: - b15_cell.value = b15_value - b15_cell.save() - updated_cells.append({ - 'id': b15_cell.id, - 'value': str(b15_cell.value), - 'is_calculated': True - }) - - # 9. B17 = B13 - B15 - Verbraucherverluste - b17_value = b13_value - b15_value - b17_cell = cell_dict.get(14) # row_index 14 = Excel B17 (UI row 15) - if b17_cell: - b17_cell.value = b17_value - b17_cell.save() - updated_cells.append({ - 'id': b17_cell.id, - 'value': str(b17_cell.value), - 'is_calculated': True - }) - - # 10. B18 = IF(B11=0; 0; B17/B11) - % - if b11_value == 0: # Use b11_value - b18_value = Decimal('0') - else: - b18_value = b17_value / b11_value # Use b11_value - - b18_cell = cell_dict.get(15) # row_index 15 = Excel B18 (UI row 16) - if b18_cell: - b18_cell.value = b18_value - b18_cell.save() - updated_cells.append({ - 'id': b18_cell.id, - 'value': str(b18_cell.value), - 'is_calculated': True - }) - - return updated_cells - def save_bulk_cells(self, request, sheet): - """Original bulk save logic (for backward compatibility)""" - # Get all cell updates - cell_updates = {} - for key, value in request.POST.items(): - if key.startswith('cell_'): - cell_id = key.replace('cell_', '') - cell_updates[cell_id] = value - - # Update cells and track which ones changed - updated_cells = [] - changed_clients = set() - - for cell_id, new_value in cell_updates.items(): - try: - cell = Cell.objects.get(id=cell_id, sheet=sheet) - old_value = cell.value - - # Convert new value - try: - if new_value.strip(): - cell.value = Decimal(new_value) - else: - cell.value = None - except Exception: - cell.value = None - - # Only save if value changed - if cell.value != old_value: - cell.save() - updated_cells.append({ - 'id': cell.id, - 'value': str(cell.value) if cell.value else '' - }) - # bottom_3 has no client, so this will just add None for those cells, - # which is harmless. Top-left cells still add their real client_id. - changed_clients.add(cell.client_id) - - except Cell.DoesNotExist: - continue - - # Recalculate for each changed client (top-left tables) - for client_id in changed_clients: - if client_id is not None: - self.recalculate_top_left_table(sheet, client_id) - - # --- NEW: recalc bottom_3 for the whole sheet, independent of clients --- - bottom3_updates = self.calculate_bottom_3_dependents(sheet) - - # Get all updated cells for response (top-left) - all_updated_cells = [] - for client_id in changed_clients: - if client_id is None: - continue # skip bottom_3 / non-client cells - client_cells = Cell.objects.filter( - sheet=sheet, - client_id=client_id - ) - for cell in client_cells: - all_updated_cells.append({ - 'id': cell.id, - 'value': str(cell.value) if cell.value else '', - 'is_calculated': cell.is_formula - }) - - # Add bottom_3 recalculated cells (J46..K53, etc.) - all_updated_cells.extend(bottom3_updates) - - return JsonResponse({ - 'status': 'success', - 'message': f'Saved {len(updated_cells)} cells', - 'updated_cells': all_updated_cells - }) - - - def recalculate_top_left_table(self, sheet, client_id): - """Recalculate the top-left table for a specific client""" - from decimal import Decimal - - # Get all cells for this client in top_left table - cells = Cell.objects.filter( - sheet=sheet, - table_type='top_left', - client_id=client_id - ).order_by('row_index') - - # Create a dictionary of cell values - cell_dict = {} - for cell in cells: - cell_dict[cell.row_index] = cell - - # Excel logic implementation for top-left table - # B3 (row_index 0) = Stand der Gaszähler (Nm³) - manual - # B4 (row_index 1) = Stand der Gaszähler (Vormonat) (Nm³) - from previous sheet - - # Get B3 and B4 - b3_cell = cell_dict.get(0) # UI Row 3 - b4_cell = cell_dict.get(1) # UI Row 4 - - if b3_cell and b3_cell.value and b4_cell and b4_cell.value: - # B5 = IF(B3>0; B3-B4; 0) - b3 = Decimal(str(b3_cell.value)) - b4 = Decimal(str(b4_cell.value)) - - if b3 > 0: - b5 = b3 - b4 - if b5 < 0: - b5 = Decimal('0') - else: - b5 = Decimal('0') - - # Update B5 (row_index 2) - b5_cell = cell_dict.get(2) - if b5_cell and (b5_cell.value != b5 or b5_cell.value is None): - b5_cell.value = b5 - b5_cell.save() - - # B6 = B5 / 0.75 (row_index 3) - b6 = b5 / Decimal('0.75') - b6_cell = cell_dict.get(3) - if b6_cell and (b6_cell.value != b6 or b6_cell.value is None): - b6_cell.value = b6 - b6_cell.save() - - # Get previous month's sheet for B10 - if sheet.month == 1: - prev_year = sheet.year - 1 - prev_month = 12 - else: - prev_year = sheet.year - prev_month = sheet.month - 1 - - prev_sheet = MonthlySheet.objects.filter( - year=prev_year, - month=prev_month - ).first() - - if prev_sheet: - # Get B9 from previous sheet (row_index 7 in previous) - prev_b9 = Cell.objects.filter( - sheet=prev_sheet, - table_type='top_left', - client_id=client_id, - row_index=7 # UI Row 9 - ).first() - - if prev_b9 and prev_b9.value: - # Update B10 in current sheet (row_index 8) - b10_cell = cell_dict.get(8) - if b10_cell and (b10_cell.value != prev_b9.value or b10_cell.value is None): - b10_cell.value = prev_b9.value - b10_cell.save() -@method_decorator(csrf_exempt, name='dispatch') -class SaveMonthSummaryView(View): - """ - Saves per-month summary values such as K44 (Gesamtbestand neu). - Called from JS after 'Save All' finishes. - """ - - def post(self, request, *args, **kwargs): - try: - data = json.loads(request.body.decode('utf-8')) - except json.JSONDecodeError: - return JsonResponse( - {'success': False, 'message': 'Invalid JSON'}, - status=400 - ) - - sheet_id = data.get('sheet_id') - if not sheet_id: - return JsonResponse( - {'success': False, 'message': 'Missing sheet_id'}, - status=400 - ) - - try: - sheet = MonthlySheet.objects.get(id=sheet_id) - except MonthlySheet.DoesNotExist: - return JsonResponse( - {'success': False, 'message': 'Sheet not found'}, - status=404 - ) - - # More tolerant decimal conversion: accepts "123.45" and "123,45" - def to_decimal(value): - if value is None: - return None - s = str(value).strip() - if s == '': - return None - s = s.replace(',', '.') - try: - return Decimal(s) - except (InvalidOperation, ValueError): - # Debug: show what failed in the dev server console - print("SaveMonthSummaryView to_decimal failed for:", repr(value)) - return None - - raw_k44 = data.get('gesamtbestand_neu_lhe') - raw_gas = data.get('gasbestand_lhe') - raw_verb = data.get('verbraucherverlust_lhe') - - gesamtbestand_neu_lhe = to_decimal(raw_k44) - gasbestand_lhe = to_decimal(raw_gas) - verbraucherverlust_lhe = to_decimal(raw_verb) - - summary, created = MonthlySummary.objects.get_or_create(sheet=sheet) - - if gesamtbestand_neu_lhe is not None: - summary.gesamtbestand_neu_lhe = gesamtbestand_neu_lhe - if gasbestand_lhe is not None: - summary.gasbestand_lhe = gasbestand_lhe - if verbraucherverlust_lhe is not None: - summary.verbraucherverlust_lhe = verbraucherverlust_lhe - - summary.save() - - # Small debug output so you can see in the server console what was saved - print( - f"Saved MonthlySummary for {sheet.year}-{sheet.month:02d}: " - f"K44={summary.gesamtbestand_neu_lhe}, " - f"Gasbestand={summary.gasbestand_lhe}, " - f"Verbraucherverlust={summary.verbraucherverlust_lhe}" - ) - - return JsonResponse({'success': True}) - - - -# Calculate View (placeholder for calculations) -class CalculateView(View): - def post(self, request): - # This will be implemented when you provide calculation rules - return JsonResponse({ - 'status': 'success', - 'message': 'Calculation endpoint ready' - }) - -# Summary Sheet View -class SummarySheetView(TemplateView): - template_name = 'summary_sheet.html' - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - start_month = int(self.kwargs.get('start_month', 1)) - year = int(self.kwargs.get('year', datetime.now().year)) - - # Get 6 monthly sheets - months = [(year, m) for m in range(start_month, start_month + 6)] - sheets = MonthlySheet.objects.filter( - year=year, - month__in=list(range(start_month, start_month + 6)) - ).order_by('month') - - # Aggregate data across months - summary_data = self.calculate_summary(sheets) - - context.update({ - 'year': year, - 'start_month': start_month, - 'end_month': start_month + 5, - 'sheets': sheets, - 'clients': Client.objects.all(), - 'summary_data': summary_data, - }) - return context - - def calculate_summary(self, sheets): - """Calculate totals across 6 months""" - summary = {} - - for client in Client.objects.all(): - client_total = 0 - for sheet in sheets: - # Get specific cells and sum - cells = sheet.cells.filter( - client=client, - table_type='top_left', - row_index=0 # Example: first row - ) - for cell in cells: - if cell.value: - try: - client_total += float(cell.value) - except (ValueError, TypeError): - continue - - summary[client.id] = client_total - - return summary - -# Existing views below (keep all your existing functions) -def clients_list(request): - # --- Clients for the yearly summary table --- - clients = Client.objects.all() - - # --- Available years for output data (same as before) --- - available_years_qs = SecondTableEntry.objects.dates('date', 'year', order='DESC') - available_years = [y.year for y in available_years_qs] - - # === 1) Year used for the "Helium Output Yearly Summary" table === - # Uses ?year=... in the dropdown - year_param = request.GET.get('year') - if year_param: - selected_year = int(year_param) - else: - selected_year = available_years[0] if available_years else datetime.now().year - - # === 2) GLOBAL half-year interval (shared with other pages) ======= - # Try GET params first - interval_year_param = request.GET.get('interval_year') - start_month_param = request.GET.get('interval_start_month') - - # Fallbacks from session - session_year = request.session.get('halfyear_year') - session_start_month = request.session.get('halfyear_start_month') - - # Determine final interval_year - if interval_year_param: - interval_year = int(interval_year_param) - elif session_year: - interval_year = int(session_year) - else: - # default: same as selected_year for summary - interval_year = selected_year - - # Determine final interval_start_month - if start_month_param: - interval_start_month = int(start_month_param) - elif session_start_month: - interval_start_month = int(session_start_month) - else: - interval_start_month = 1 # default Jan - - # Store back into the session so other views can read them - request.session['halfyear_year'] = interval_year - request.session['halfyear_start_month'] = interval_start_month - - # === 3) Build a 6-month window, allowing wrap into the next year === - # Example: interval_year=2025, interval_start_month=12 - # window = [(2025,12), (2026,1), (2026,2), (2026,3), (2026,4), (2026,5)] - window = [] - for offset in range(6): - total_index = (interval_start_month - 1) + offset # 0-based index - y = interval_year + (total_index // 12) - m = (total_index % 12) + 1 - window.append((y, m)) - - # === 4) Totals per client in that 6-month window ================== - monthly_data = [] - for client in clients: - monthly_totals = [] - for (y, m) in window: - total = SecondTableEntry.objects.filter( - client=client, - date__year=y, - date__month=m - ).aggregate( - total=Coalesce( - Sum('lhe_output'), - Value(0, output_field=DecimalField()) - ) - )['total'] - monthly_totals.append(total) - - monthly_data.append({ - 'client': client, - 'monthly_totals': monthly_totals, - 'year_total': sum(monthly_totals), - }) - - # === 5) Month labels for the header (only the 6-month window) ===== - month_labels = [calendar.month_abbr[m] for (y, m) in window] - - # === 6) FINALLY: return the response ============================== - return render(request, 'clients_table.html', { - 'available_years': available_years, - 'current_year': selected_year, # used by year dropdown - 'interval_year': interval_year, # used by "Global 6-Month Interval" form - 'interval_start_month': interval_start_month, - 'months': month_labels, - 'monthly_data': monthly_data, - }) - - # === 5) Month labels for the header (only the 6-month window) ===== - month_labels = [calendar.month_abbr[m] for (y, m) in window] - - -def set_halfyear_interval(request): - if request.method == 'POST': - year = int(request.POST.get('year')) - start_month = int(request.POST.get('start_month')) - - request.session['halfyear_year'] = year - request.session['halfyear_start_month'] = start_month - - return redirect(request.META.get('HTTP_REFERER', 'clients_list')) - - return redirect('clients_list') -# Table One View (ExcelEntry) -def table_one_view(request): - from .models import ExcelEntry, Client, Institute - - # --- Base queryset for the main Helium Input table --- - base_entries = ExcelEntry.objects.all().select_related('client', 'client__institute') - - # Read the global 6-month interval from the session - interval_year = request.session.get('halfyear_year') - interval_start = request.session.get('halfyear_start_month') - - if interval_year and interval_start: - interval_year = int(interval_year) - interval_start = int(interval_start) - - # Build the same 6-month window as on the main page (can cross year) - window = [] - for offset in range(6): - total_index = (interval_start - 1) + offset # 0-based - y = interval_year + (total_index // 12) - m = (total_index % 12) + 1 - window.append((y, m)) - - # Build Q filter: (year=m_year AND month=m_month) for any of those 6 - q = Q() - for (y, m) in window: - q |= Q(date__year=y, date__month=m) - - entries_table1 = base_entries.filter(q).order_by('-date') - else: - # Fallback: if no global interval yet, show everything - entries_table1 = base_entries.order_by('-date') - clients = Client.objects.all().select_related('institute') - institutes = Institute.objects.all() - - # ---- Overview filters ---- - # years present in ExcelEntry.date - year_qs = ExcelEntry.objects.dates('date', 'year', order='DESC') - available_years = [d.year for d in year_qs] - - # default year/start month - # default year/start month (if no global interval yet) - if available_years: - default_year = available_years[0] # newest year in ExcelEntry - else: - default_year = timezone.now().year - - # 🔸 Read global half-year interval from session (set on main page) - session_year = request.session.get('halfyear_year') - session_start = request.session.get('halfyear_start_month') - - # If the user has set a global interval, use it. - # Otherwise fall back to default year / January. - year = int(session_year) if session_year else int(default_year) - start_month = int(session_start) if session_start else 1 - - - # six-month window - # --- Build a 6-month window, allowing wrap into the next year --- - # Example: year=2025, start_month=10 - # window = [(2025,10), (2025,11), (2025,12), (2026,1), (2026,2), (2026,3)] - window = [] - for offset in range(6): - total_index = (start_month - 1) + offset # 0-based - y_for_month = year + (total_index // 12) - m_for_month = (total_index % 12) + 1 - window.append((y_for_month, m_for_month)) - - overview = None - - if window: - # Build per-group data - groups_entries = [] # for internal calculations - - for key, group in CLIENT_GROUPS.items(): - clients_qs = get_group_clients(key) - - values = [] - group_total = Decimal('0') - - for (y_m, m_m) in window: - total = ExcelEntry.objects.filter( - client__in=clients_qs, - date__year=y_m, - date__month=m_m - ).aggregate( - total=Coalesce(Sum('lhe_ges'), Decimal('0')) - )['total'] - - values.append(total) - group_total += total - - groups_entries.append({ - 'key': key, - 'label': group['label'], - 'values': values, - 'total': group_total, - }) - - # month totals across all groups - month_totals = [] - for idx in range(len(window)): - s = sum((g['values'][idx] for g in groups_entries), Decimal('0')) - month_totals.append(s) - - grand_total = sum(month_totals, Decimal('0')) - - # Build rows for the template - rows = [] - for idx, (y_m, m_m) in enumerate(window): - row_values = [g['values'][idx] for g in groups_entries] - rows.append({ - 'month_number': m_m, - 'month_label': calendar.month_name[m_m], - 'values': row_values, - 'total': month_totals[idx], - }) - - groups_meta = [{'key': g['key'], 'label': g['label']} for g in groups_entries] - group_totals = [g['total'] for g in groups_entries] - - # Start/end for display – include years so wrap is clear - start_year = window[0][0] - start_month_disp = window[0][1] - end_year = window[-1][0] - end_month_disp = window[-1][1] - - overview = { - 'year': year, # keep for backwards compatibility if needed - 'start_month': start_month_disp, - 'start_year': start_year, - 'end_month': end_month_disp, - 'end_year': end_year, - 'rows': rows, - 'groups': groups_meta, - 'group_totals': group_totals, - 'grand_total': grand_total, - } - - - # Month dropdown labels - MONTH_CHOICES = [ - (1, 'Jan'), (2, 'Feb'), (3, 'Mar'), (4, 'Apr'), - (5, 'May'), (6, 'Jun'), (7, 'Jul'), (8, 'Aug'), - (9, 'Sep'), (10, 'Oct'), (11, 'Nov'), (12, 'Dec'), - ] - - return render(request, 'table_one.html', { - 'entries_table1': entries_table1, - 'clients': clients, - 'institutes': institutes, - 'available_years': available_years, - 'month_choices': MONTH_CHOICES, - 'overview': overview, - }) - -# Table Two View (SecondTableEntry) -def table_two_view(request): - try: - clients = Client.objects.all().select_related('institute') - institutes = Institute.objects.all() - - # 🔸 Read global half-year interval from session - interval_year = request.session.get('halfyear_year') - interval_start = request.session.get('halfyear_start_month') - - if interval_year and interval_start: - interval_year = int(interval_year) - interval_start = int(interval_start) - - # Build the same 6-month window as in clients_list (can cross years) - window = [] - for offset in range(6): - total_index = (interval_start - 1) + offset # 0-based - y = interval_year + (total_index // 12) - m = (total_index % 12) + 1 - window.append((y, m)) - - # Build a Q object matching any of those (year, month) pairs - q = Q() - for (y, m) in window: - q |= Q(date__year=y, date__month=m) - - entries = SecondTableEntry.objects.filter(q).order_by('-date') - else: - # Fallback if no global interval yet: show all - entries = SecondTableEntry.objects.all().order_by('-date') - - return render(request, 'table_two.html', { - 'entries_table2': entries, - 'clients': clients, - 'institutes': institutes, - 'interval_year': interval_year, - 'interval_start_month': interval_start, - }) - - except Exception as e: - return render(request, 'table_two.html', { - 'error_message': f"Failed to load data: {str(e)}", - 'entries_table2': [], - 'clients': clients, - 'institutes': institutes, - }) - - -def monthly_sheet_root(request): - """ - Redirect /sheet/ to the sheet matching the globally selected - half-year start (year + month). If not set, fall back to latest. - """ - year = request.session.get('halfyear_year') - start_month = request.session.get('halfyear_start_month') - - if year and start_month: - try: - year = int(year) - start_month = int(start_month) - return redirect('monthly_sheet', year=year, month=start_month) - except ValueError: - pass # fall through - - # Fallback: latest MonthlySheet if exists - latest_sheet = MonthlySheet.objects.order_by('-year', '-month').first() - if latest_sheet: - return redirect('monthly_sheet', year=latest_sheet.year, month=latest_sheet.month) - else: - now = timezone.now() - return redirect('monthly_sheet', year=now.year, month=now.month) -# Add Entry (Generic) -def add_entry(request, model_name): - if request.method == 'POST': - try: - if model_name == 'SecondTableEntry': - model = SecondTableEntry - - # Parse date - date_str = request.POST.get('date') - try: - date_obj = datetime.strptime(date_str, '%Y-%m-%d').date() if date_str else None - except (ValueError, TypeError): - return JsonResponse({ - 'status': 'error', - 'message': 'Invalid date format. Use YYYY-MM-DD' - }, status=400) - - # NEW: robust parse of warm flag (handles 0/1, true/false, on/off) - raw_warm = request.POST.get('is_warm') - is_warm_bool = str(raw_warm).lower() in ('1', 'true', 'on', 'yes') - - # Handle Helium Output (Table Two) - lhe_output = request.POST.get('lhe_output') - - entry = model.objects.create( - client=Client.objects.get(id=request.POST.get('client_id')), - date=date_obj, - is_warm=is_warm_bool, # <-- use the boolean here - lhe_delivery=request.POST.get('lhe_delivery', ''), - lhe_output=Decimal(lhe_output) if lhe_output else None, - notes=request.POST.get('notes', '') - ) - - return JsonResponse({ - 'status': 'success', - 'id': entry.id, - 'client_name': entry.client.name, - 'institute_name': entry.client.institute.name, - 'date': entry.date.strftime('%Y-%m-%d') if entry.date else '', - 'is_warm': entry.is_warm, - 'lhe_delivery': entry.lhe_delivery, - 'lhe_output': str(entry.lhe_output) if entry.lhe_output else '', - 'notes': entry.notes - }) - - elif model_name == 'ExcelEntry': - model = ExcelEntry - - # Parse the date string into a date object - date_str = request.POST.get('date') - try: - date_obj = datetime.strptime(date_str, '%Y-%m-%d').date() if date_str else None - except (ValueError, TypeError): - date_obj = None - - try: - pressure = Decimal(request.POST.get('pressure', 0)) - purity = Decimal(request.POST.get('purity', 0)) - druckkorrektur = Decimal(request.POST.get('druckkorrektur', 1)) - lhe_zus = Decimal(request.POST.get('lhe_zus', 0)) - constant_300 = Decimal(request.POST.get('constant_300', 300)) - korrig_druck = Decimal(request.POST.get('korrig_druck', 0)) - nm3 = Decimal(request.POST.get('nm3', 0)) - lhe = Decimal(request.POST.get('lhe', 0)) - lhe_ges = Decimal(request.POST.get('lhe_ges', 0)) - except InvalidOperation: - return JsonResponse({ - 'status': 'error', - 'message': 'Invalid numeric value in Helium Input' - }, status=400) - - # Create the entry with ALL fields - entry = model.objects.create( - client=Client.objects.get(id=request.POST.get('client_id')), - date=date_obj, - pressure=pressure, - purity=purity, - druckkorrektur=druckkorrektur, - lhe_zus=lhe_zus, - constant_300=constant_300, - korrig_druck=korrig_druck, - nm3=nm3, - lhe=lhe, - lhe_ges=lhe_ges, - notes=request.POST.get('notes', '') - ) - - # Prepare the response - response_data = { - 'status': 'success', - 'id': entry.id, - 'client_name': entry.client.name, - 'institute_name': entry.client.institute.name, - 'pressure': str(entry.pressure), - 'purity': str(entry.purity), - 'druckkorrektur': str(entry.druckkorrektur), - 'constant_300': str(entry.constant_300), - 'korrig_druck': str(entry.korrig_druck), - 'nm3': str(entry.nm3), - 'lhe': str(entry.lhe), - 'lhe_zus': str(entry.lhe_zus), - 'lhe_ges': str(entry.lhe_ges), - 'notes': entry.notes, - } - - if entry.date: - # JS uses this for the Date column and for the Month column - response_data['date'] = entry.date.strftime('%Y-%m-%d') - response_data['month'] = f"{entry.date.month:02d}" - else: - response_data['date'] = '' - response_data['month'] = '' - - return JsonResponse(response_data) - - - except Exception as e: - return JsonResponse({'status': 'error', 'message': str(e)}, status=400) - - return JsonResponse({'status': 'error', 'message': 'Invalid request'}, status=400) - -# Update Entry (Generic) -def update_entry(request, model_name): - if request.method == 'POST': - try: - if model_name == 'SecondTableEntry': - model = SecondTableEntry - elif model_name == 'ExcelEntry': - model = ExcelEntry - else: - return JsonResponse({'status': 'error', 'message': 'Invalid model'}, status=400) - - entry_id = int(request.POST.get('id')) - entry = model.objects.get(id=entry_id) - - # Common updates for both models - entry.client = Client.objects.get(id=request.POST.get('client_id')) - entry.notes = request.POST.get('notes', '') - - # Handle date properly for both models - date_str = request.POST.get('date') - if date_str: - try: - entry.date = datetime.strptime(date_str, '%Y-%m-%d').date() - except ValueError: - return JsonResponse({ - 'status': 'error', - 'message': 'Invalid date format. Use YYYY-MM-DD' - }, status=400) - - if model_name == 'SecondTableEntry': - # Handle Helium Output specific fields - - raw_warm = request.POST.get('is_warm') - entry.is_warm = str(raw_warm).lower() in ('1', 'true', 'on', 'yes') - - entry.lhe_delivery = request.POST.get('lhe_delivery', '') - - lhe_output = request.POST.get('lhe_output') - try: - entry.lhe_output = Decimal(lhe_output) if lhe_output else None - except InvalidOperation: - return JsonResponse({ - 'status': 'error', - 'message': 'Invalid LHe Output value' - }, status=400) - - entry.save() - - return JsonResponse({ - 'status': 'success', - 'id': entry.id, - 'client_name': entry.client.name, - 'institute_name': entry.client.institute.name, - 'date': entry.date.strftime('%Y-%m-%d') if entry.date else '', - 'is_warm': entry.is_warm, - 'lhe_delivery': entry.lhe_delivery, - 'lhe_output': str(entry.lhe_output) if entry.lhe_output else '', - 'notes': entry.notes - }) - - - elif model_name == 'ExcelEntry': - # Handle Helium Input specific fields - try: - entry.pressure = Decimal(request.POST.get('pressure', 0)) - entry.purity = Decimal(request.POST.get('purity', 0)) - entry.druckkorrektur = Decimal(request.POST.get('druckkorrektur', 1)) - entry.lhe_zus = Decimal(request.POST.get('lhe_zus', 0)) - entry.constant_300 = Decimal(request.POST.get('constant_300', 300)) - entry.korrig_druck = Decimal(request.POST.get('korrig_druck', 0)) - entry.nm3 = Decimal(request.POST.get('nm3', 0)) - entry.lhe = Decimal(request.POST.get('lhe', 0)) - entry.lhe_ges = Decimal(request.POST.get('lhe_ges', 0)) - except InvalidOperation: - return JsonResponse({ - 'status': 'error', - 'message': 'Invalid numeric value in Helium Input' - }, status=400) - - entry.save() - - return JsonResponse({ - 'status': 'success', - 'id': entry.id, - 'client_name': entry.client.name, - 'institute_name': entry.client.institute.name, - 'date': entry.date.strftime('%Y-%m-%d') if entry.date else '', - 'month': f"{entry.date.month:02d}" if entry.date else '', - 'pressure': str(entry.pressure), - 'purity': str(entry.purity), - 'druckkorrektur': str(entry.druckkorrektur), - 'constant_300': str(entry.constant_300), - 'korrig_druck': str(entry.korrig_druck), - 'nm3': str(entry.nm3), - 'lhe': str(entry.lhe), - 'lhe_zus': str(entry.lhe_zus), - 'lhe_ges': str(entry.lhe_ges), - 'notes': entry.notes - }) - - - except model.DoesNotExist: - return JsonResponse({'status': 'error', 'message': 'Entry not found'}, status=404) - except Exception as e: - return JsonResponse({'status': 'error', 'message': str(e)}, status=400) - - return JsonResponse({'status': 'error', 'message': 'Invalid request method'}, status=400) - -# Delete Entry (Generic) -def delete_entry(request, model_name): - if request.method == 'POST': - try: - if model_name == 'SecondTableEntry': - model = SecondTableEntry - elif model_name == 'ExcelEntry': - model = ExcelEntry - else: - return JsonResponse({'status': 'error', 'message': 'Invalid model'}, status=400) - - entry_id = request.POST.get('id') - entry = model.objects.get(id=entry_id) - entry.delete() - return JsonResponse({'status': 'success', 'message': 'Entry deleted'}) - - except model.DoesNotExist: - return JsonResponse({'status': 'error', 'message': 'Entry not found'}, status=404) - except Exception as e: - return JsonResponse({'status': 'error', 'message': str(e)}, status=400) - - return JsonResponse({'status': 'error', 'message': 'Invalid request'}, status=400) - -def betriebskosten_list(request): - items = Betriebskosten.objects.all().order_by('-buchungsdatum') - return render(request, 'betriebskosten_list.html', {'items': items}) - -def betriebskosten_create(request): - if request.method == 'POST': - try: - entry_id = request.POST.get('id') - if entry_id: - # Update existing entry - entry = Betriebskosten.objects.get(id=entry_id) - else: - # Create new entry - entry = Betriebskosten() - - # Get form data - buchungsdatum_str = request.POST.get('buchungsdatum') - rechnungsnummer = request.POST.get('rechnungsnummer') - kostentyp = request.POST.get('kostentyp') - betrag = request.POST.get('betrag') - beschreibung = request.POST.get('beschreibung') - gas_volume = request.POST.get('gas_volume') - - # Validate required fields - if not all([buchungsdatum_str, rechnungsnummer, kostentyp, betrag]): - return JsonResponse({'status': 'error', 'message': 'All required fields must be filled'}) - - # Convert date string to date object - try: - buchungsdatum = parse_date(buchungsdatum_str) - if not buchungsdatum: - return JsonResponse({'status': 'error', 'message': 'Invalid date format'}) - except (ValueError, TypeError): - return JsonResponse({'status': 'error', 'message': 'Invalid date format'}) - - # Set entry values - entry.buchungsdatum = buchungsdatum - entry.rechnungsnummer = rechnungsnummer - entry.kostentyp = kostentyp - entry.betrag = betrag - entry.beschreibung = beschreibung - - # Only set gas_volume if kostentyp is helium and gas_volume is provided - if kostentyp == 'helium' and gas_volume: - entry.gas_volume = gas_volume - else: - entry.gas_volume = None - - entry.save() - - return JsonResponse({ - 'status': 'success', - 'id': entry.id, - 'buchungsdatum': entry.buchungsdatum.strftime('%Y-%m-%d'), - 'rechnungsnummer': entry.rechnungsnummer, - 'kostentyp_display': entry.get_kostentyp_display(), - 'gas_volume': str(entry.gas_volume) if entry.gas_volume else '-', - 'betrag': str(entry.betrag), - 'beschreibung': entry.beschreibung or '' - }) - - except Exception as e: - return JsonResponse({'status': 'error', 'message': str(e)}) - - return JsonResponse({'status': 'error', 'message': 'Invalid request method'}) - -def betriebskosten_delete(request): - if request.method == 'POST': - try: - entry_id = request.POST.get('id') - entry = Betriebskosten.objects.get(id=entry_id) - entry.delete() - return JsonResponse({'status': 'success'}) - except Betriebskosten.DoesNotExist: - return JsonResponse({'status': 'error', 'message': 'Entry not found'}) - except Exception as e: - return JsonResponse({'status': 'error', 'message': str(e)}) - - return JsonResponse({'status': 'error', 'message': 'Invalid request method'}) - -class CheckSheetView(View): - def get(self, request): - # Get current month/year - current_year = datetime.now().year - current_month = datetime.now().month - - # Get all sheets - sheets = MonthlySheet.objects.all() - - sheet_data = [] - for sheet in sheets: - cells_count = sheet.cells.count() - # Count non-empty cells - non_empty = sheet.cells.exclude(value__isnull=True).count() - - sheet_data.append({ - 'id': sheet.id, - 'year': sheet.year, - 'month': sheet.month, - 'month_name': calendar.month_name[sheet.month], - 'total_cells': cells_count, - 'non_empty_cells': non_empty, - 'has_data': non_empty > 0 - }) - - # Also check what the default view would show - default_sheet = MonthlySheet.objects.filter( - year=current_year, - month=current_month - ).first() - - return JsonResponse({ - 'current_year': current_year, - 'current_month': current_month, - 'current_month_name': calendar.month_name[current_month], - 'default_sheet_exists': default_sheet is not None, - 'default_sheet_id': default_sheet.id if default_sheet else None, - 'sheets': sheet_data, - 'total_sheets': len(sheet_data) - }) - - -class QuickDebugView(View): - def get(self, request): - # Get ALL sheets - sheets = MonthlySheet.objects.all().order_by('year', 'month') - - result = { - 'sheets': [] - } - - for sheet in sheets: - sheet_info = { - 'id': sheet.id, - 'display': f"{sheet.year}-{sheet.month:02d}", - 'url': f"/sheet/{sheet.year}/{sheet.month}/", # CHANGED THIS LINE - 'sheet_url_pattern': 'sheet/{year}/{month}/', # Add this for clarity - } - - # Count cells with data for first client in top_left table - first_client = Client.objects.first() - if first_client: - test_cells = sheet.cells.filter( - client=first_client, - table_type='top_left', - row_index__in=[8, 9, 10] # Rows 9, 10, 11 - ).order_by('row_index') - - cell_values = {} - for cell in test_cells: - cell_values[f"row_{cell.row_index}"] = str(cell.value) if cell.value else "Empty" - - sheet_info['test_cells'] = cell_values - else: - sheet_info['test_cells'] = "No clients" - - result['sheets'].append(sheet_info) - - return JsonResponse(result) -class TestFormulaView(View): - def get(self, request): - # Test the formula evaluation directly - test_values = { - 8: 2, # Row 9 value (0-based index 8) - 9: 2, # Row 10 value (0-based index 9) - } - - # Test formula "9 + 8" (using 0-based indices) - formula = "9 + 8" - result = evaluate_formula(formula, test_values) - - return JsonResponse({ - 'test_values': test_values, - 'formula': formula, - 'result': str(result), - 'note': 'Formula uses 0-based indices. 9=Row10, 8=Row9' - }) - - -class SimpleDebugView(View): - """Simplest debug view to check if things are working""" - def get(self, request): - sheet_id = request.GET.get('sheet_id', 1) - - try: - sheet = MonthlySheet.objects.get(id=sheet_id) - - # Get first client - client = Client.objects.first() - if not client: - return JsonResponse({'error': 'No clients found'}) - - # Check a few cells - cells = Cell.objects.filter( - sheet=sheet, - client=client, - table_type='top_left', - row_index__in=[8, 9, 10] - ).order_by('row_index') - - cell_data = [] - for cell in cells: - cell_data.append({ - 'row_index': cell.row_index, - 'ui_row': cell.row_index + 1, - 'value': str(cell.value) if cell.value is not None else 'Empty', - 'cell_id': cell.id - }) - - return JsonResponse({ - 'sheet': f"{sheet.year}-{sheet.month}", - 'sheet_id': sheet.id, - 'client': client.name, - 'cells': cell_data, - 'note': 'Row 8 = UI Row 9, Row 9 = UI Row 10, Row 10 = UI Row 11' - }) - - except MonthlySheet.DoesNotExist: - return JsonResponse({'error': f'Sheet with id {sheet_id} not found'}) - -def halfyear_settings(request): - """ - Global settings page: choose a year + first month for the 6-month interval. - These values are stored in the session and used by other views. - """ - # Determine available years from your data (use ExcelEntry or SecondTableEntry) - # Here I use SecondTableEntry; you can switch to ExcelEntry if you prefer. - available_years_qs = SecondTableEntry.objects.dates('date', 'year', order='DESC') - available_years = [d.year for d in available_years_qs] - - if not available_years: - available_years = [timezone.now().year] - - # Defaults (if nothing in session yet) - default_year = request.session.get('halfyear_year', available_years[0]) - default_start_month = request.session.get('halfyear_start_month', 1) - - if request.method == 'POST': - year = int(request.POST.get('year', default_year)) - start_month = int(request.POST.get('start_month', default_start_month)) - - request.session['halfyear_year'] = year - request.session['halfyear_start_month'] = start_month - - # Redirect back to where user came from, or to this page again - next_url = request.POST.get('next') or request.GET.get('next') or 'halfyear_settings' - return redirect(next_url) - - # Month choices for the dropdown - month_choices = [(i, calendar.month_name[i]) for i in range(1, 13)] - - context = { - 'available_years': available_years, - 'selected_year': int(default_year), - 'selected_start_month': int(default_start_month), - 'month_choices': month_choices, - } - return render(request, 'halfyear_settings.html', context) \ No newline at end of file diff --git a/sheets/old/views_v2.py b/sheets/old/views_v2.py deleted file mode 100644 index 086c96e..0000000 --- a/sheets/old/views_v2.py +++ /dev/null @@ -1,4520 +0,0 @@ -from django.shortcuts import render, redirect -from django.utils.decorators import method_decorator -from django.views.decorators.csrf import csrf_exempt -from django.db.models import Sum, Value, DecimalField -from django.http import JsonResponse -from django.db.models import Q -from decimal import Decimal, InvalidOperation -from django.apps import apps -from datetime import date, datetime -import calendar -from django.utils import timezone -from django.views.generic import TemplateView, View -from .models import ( - Client, SecondTableEntry, Institute, ExcelEntry, - Betriebskosten, MonthlySheet, Cell, CellReference, MonthlySummary -) -from django.db.models import Sum -from django.urls import reverse -from django.db.models.functions import Coalesce -from .forms import BetriebskostenForm -from django.utils.dateparse import parse_date -from django.contrib.auth.mixins import LoginRequiredMixin -import json -FIRST_SHEET_YEAR = 2025 -FIRST_SHEET_MONTH = 1 - -CLIENT_GROUPS = { - 'ikp': { - 'label': 'IKP', - # exactly as in the Clients admin - 'names': ['IKP'], - }, - 'phys_chem_bunt_fohrer': { - 'label': 'Buntkowsky + Dr. Fohrer', - # include all variants you might have used for Buntkowsky - 'names': [ - 'AG Buntk.', # the one in your new entry - 'AG Buntkowsky.', # from your original list - 'AG Buntkowsky', - 'Dr. Fohrer', - ], - }, - 'mawi_alff_gutfleisch': { - 'label': 'Alff + AG Gutfleisch', - # include both short and full forms - 'names': [ - 'AG Alff', - 'AG Gutfl.', - 'AG Gutfleisch', - ], - }, - 'm3_group': { - 'label': 'M3 Buntkowsky + M3 Thiele + M3 Gutfleisch', - 'names': [ - 'M3 Buntkowsky', - 'M3 Thiele', - 'M3 Gutfleisch', - ], - }, -} - -# Add this CALCULATION_CONFIG at the top of views.py - -CALCULATION_CONFIG = { - 'top_left': { - # Row mappings: Django row_index (0-based) to Excel row - # Excel B4 -> Django row_index 1 (UI row 2) - # Excel B5 -> Django row_index 2 (UI row 3) - # Excel B6 -> Django row_index 3 (UI row 4) - - # B6 (row_index 3) = B5 (row_index 2) / 0.75 - 3: "2 / 0.75", - - # B11 (row_index 10) = B9 (row_index 8) - 10: "8", - - # B14 (row_index 13) = B13 (row_index 12) - B11 (row_index 10) + B12 (row_index 11) - 13: "12 - 10 + 11", - - # Note: B5, B17, B19, B20 require IF logic, so they'll be handled separately - }, - - # other tables (top_right, bottom_1, ...) stay as they are - - - ' top_right': { - # UI Row 1 (Excel Row 4): Stand der Gaszähler (Vormonat) (Nm³) - 0: { - 'L': "9 / (9 + 9) if (9 + 9) > 0 else 0", # L4 = L13/(L13+M13) - 'M': "9 / (9 + 9) if (9 + 9) > 0 else 0", # M4 = M13/(L13+M13) - 'N': "9 / (9 + 9) if (9 + 9) > 0 else 0", # N4 = N13/(N13+O13) - 'O': "9 / (9 + 9) if (9 + 9) > 0 else 0", # O4 = O13/(N13+O13) - 'P': None, # Editable - 'Q': None, # Editable - 'R': None, # Editable - }, - - # UI Row 2 (Excel Row 5): Gasrückführung (Nm³) - 1: { - 'L': "4", # L5 = L8 - 'M': "4", # M5 = L8 (merged) - 'N': "4", # N5 = N8 - 'O': "4", # O5 = N8 (merged) - 'P': "4 * 0", # P5 = P8 * P4 - 'Q': "4 * 0", # Q5 = P8 * Q4 - 'R': "4 * 0", # R5 = P8 * R4 - }, - - # UI Row 3 (Excel Row 6): Rückführung flüssig (Lit. L-He) - 2: { - 'L': "4", # L6 = L8 (Sammelrückführungen) - 'M': "4", - 'N': "4", - 'O': "4", - 'P': "4", - 'Q': "4", - 'R': "4", -}, - - # UI Row 4 (Excel Row 7): Sonderrückführungen (Lit. L-He) - EDITABLE - 3: { - 'L': None, - 'M': None, - 'N': None, - 'O': None, - 'P': None, - 'Q': None, - 'R': None, - }, - - # UI Row 5 (Excel Row 8): Sammelrückführungen (Lit. L-He) - 4: { - 'L': None, # Will be populated from ExcelEntry - 'M': None, - 'N': None, - 'O': None, - 'P': None, - 'Q': None, - 'R': None, - }, - - # UI Row 6 (Excel Row 9): Bestand in Kannen-1 (Lit. L-He) - EDITABLE - 5: { - 'L': None, - 'M': None, - 'N': None, - 'O': None, - 'P': None, - 'Q': None, - 'R': None, - }, - - # UI Row 7 (Excel Row 10): Summe Bestand (Lit. L-He) - 6: { - 'L': None, - 'M': None, - 'N': None, - 'O': None, - 'P': None, - 'Q': None, - 'R': None, - }, - - # UI Row 11 (Excel Row 14): Rückführ. Soll (Lit. L-He) - # handled in calculate_top_right_dependents (merged pairs + M3) - 10: { - 'L': None, - 'M': None, - 'N': None, - 'O': None, - 'P': None, - 'Q': None, - 'R': None, - }, - - # UI Row 12 (Excel Row 15): Verluste (Soll-Rückf.) (Lit. L-He) - # handled in calculate_top_right_dependents - 11: { - 'L': None, - 'M': None, - 'N': None, - 'O': None, - 'P': None, - 'Q': None, - 'R': None, - }, - - # UI Row 14 (Excel Row 17): Kaltgas Rückgabe (Lit. L-He) – Faktor - # handled in calculate_top_right_dependents (different formulas for pair 1 vs pair 2 + M3) - 13: { - 'L': None, - 'M': None, - 'N': None, - 'O': None, - 'P': None, - 'Q': None, - 'R': None, - }, - - # UI Row 16 (Excel Row 19): Verbraucherverluste (Liter L-He) - # handled in calculate_top_right_dependents - 15: { - 'L': None, - 'M': None, - 'N': None, - 'O': None, - 'P': None, - 'Q': None, - 'R': None, - }, - - # UI Row 17 (Excel Row 20): % - # handled in calculate_top_right_dependents - 16: { - 'L': None, - 'M': None, - 'N': None, - 'O': None, - 'P': None, - 'Q': None, - 'R': None, - } -}, - 'bottom_1': { - 5: "4 + 3 + 2", - 8: "7 - 6", - }, - 'bottom_2': { - 3: "1 + 2", - 6: "5 - 4", - }, - 'bottom_3': { - 2: "0 + 1", - 5: "3 + 4", - }, - # Special configuration for summation column (last column) - 'summation_column': { - # For each row that should be summed across columns - 'rows_to_sum': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23], # All rows - # OR specify specific rows: - # 'rows_to_sum': [0, 5, 10, 15, 20], # Only specific rows - # The last column index (0-based) - 'sum_column_index': 5, # 6th column (0-5) since you have 6 clients - } -} -def build_halfyear_window(interval_year: int, start_month: int): - """ - Build a list of (year, month) for the 6-month interval, possibly crossing into the next year. - Example: (2025, 10) -> [(2025,10), (2025,11), (2025,12), (2026,1), (2026,2), (2026,3)] - """ - window = [] - for offset in range(6): - total_index = (start_month - 1) + offset # 0-based - y = interval_year + (total_index // 12) - m = (total_index % 12) + 1 - window.append((y, m)) - return window -# --------------------------------------------------------------------------- -# Halbjahres-Bilanz helpers -# --------------------------------------------------------------------------- - -# You can adjust these indices if needed. -# Assuming: -# - bottom_1.table has row "Gasbestand" at some fixed row index, -# and columns: ... Nm³, Lit. LHe -GASBESTAND_ROW_INDEX = 9 # <-- adjust if your bottom_1 has a different row index -GASBESTAND_COL_NM3 = 3 # <-- adjust to the column index for Nm³ in bottom_1 - -# In top_left / top_right, "Bestand in Kannen-1 (Lit. L-He)" is row_index 5 -BESTAND_KANNEN_ROW_INDEX = 5 - -def pick_sheet_by_gasbestand(window, sheets_by_ym, prev_sheet): - """ - Returns the last sheet in the window whose Gasbestand (J36, Nm³ column) != 0. - If none found, returns prev_sheet (Übertrag_Dez__Vorjahr equivalent). - """ - for (y, m) in reversed(window): - sheet = sheets_by_ym.get((y, m)) - if not sheet: - continue - gasbestand_nm3 = get_bottom1_value(sheet, GASBESTAND_ROW_INDEX, GASBESTAND_COL_NM3) - if gasbestand_nm3 != 0: - return sheet - return prev_sheet -def get_bottom1_value(sheet, row_index: int, col_index: int) -> Decimal: - """Get a numeric value from bottom_1, or 0 if missing.""" - if sheet is None: - return Decimal('0') - - cell = Cell.objects.filter( - sheet=sheet, - table_type='bottom_1', - row_index=row_index, - column_index=col_index, - ).first() - - if cell is None or cell.value in (None, ''): - return Decimal('0') - - try: - return Decimal(str(cell.value)) - except Exception: - return Decimal('0') - -# MUST match the column order in your monthly_sheets top-right table - - - -def get_top_right_value(sheet, client_name: str, row_index: int) -> Decimal: - """ - Read a numeric value from the top_right table of a MonthlySheet for - a given client (by column) and row_index. - - top_right cells are keyed by (sheet, table_type='top_right', - row_index, column_index), where column_index is the position of the - client in HALFYEAR_RIGHT_CLIENTS. - """ - if sheet is None: - return Decimal('0') - - col_index = RIGHT_CLIENT_INDEX.get(client_name) - if col_index is None: - return Decimal('0') - - cell = Cell.objects.filter( - sheet=sheet, - table_type='top_right', - row_index=row_index, - column_index=col_index, - ).first() - - if cell is None or cell.value in (None, ''): - return Decimal('0') - - try: - return Decimal(str(cell.value)) - except Exception: - return Decimal('0') - -TR_RUECKF_FLUESSIG_ROW = 2 # confirmed by your March value 172.840560 -TR_BESTAND_KANNEN_ROW = 5 # confirmed by your earlier query -def get_bestand_kannen_for_month(sheet, client_name: str) -> Decimal: - """ - 'B9' in your description: Bestand in Kannen-1 (Lit. L-He) - For this implementation we take it from top_left row_index = 5 for that client. - """ - return get_top_left_value(sheet, client_name, row_index=BESTAND_KANNEN_ROW_INDEX) - -from decimal import Decimal -from django.db.models import Sum -from django.db.models.functions import Coalesce -from django.db.models import DecimalField, Value - -from .models import MonthlySheet, SecondTableEntry, Client, Cell -from django.shortcuts import redirect, render - -# You already have HALFYEAR_CLIENTS for the left table (AG Vogel, AG Halfm, IKP) -HALFYEAR_CLIENTS = ["AG Vogel", "AG Halfm", "IKP"] - -# NEW: clients for the top-right half-year table -HALFYEAR_RIGHT_CLIENTS = [ - "Dr. Fohrer", - "AG Buntk.", - "AG Alff", - "AG Gutfl.", - "M3 Thiele", - "M3 Buntkowsky", - "M3 Gutfleisch", -] -BOTTOM1_COL_VOLUME = 0 -BOTTOM1_COL_BAR = 1 -BOTTOM1_COL_KORR = 2 -BOTTOM1_COL_NM3 = 3 -BOTTOM1_COL_LHE = 4 -BOTTOM2_ROW_ANLAGE = 0 -BOTTOM2_COL_G39 = 0 # "Gefäss 2,5" (cell id shows column_index=0) -BOTTOM2_COL_I39 = 1 # "Gefäss 1,0" (cell id shows column_index=1) -BOTTOM2_ROW_INPUTS = { - "g39": (0, 0), # row_index=0, column_index=0 (your G39) - "i39": (0, 1), # row_index=0, column_index=1 (your I39) -} -FACTOR_NM3_TO_LHE = Decimal("0.75") -RIGHT_CLIENT_INDEX = {name: idx for idx, name in enumerate(HALFYEAR_RIGHT_CLIENTS)} -def halfyear_balance_view(request): - """ - Read-only Halbjahres-Bilanz view. - - LEFT table: AG Vogel / AG Halfm / IKP (exactly as in your last working version) - RIGHT table: Dr. Fohrer / AG Buntk. / AG Alff / AG Gutfl. / - M3 Thiele / M3 Buntkowsky / M3 Gutfleisch - using the Excel formulas you described. - - Uses the global 6-month interval from the main page (clients_list). - """ - # 1) Read half-year interval from the session - interval_year = request.session.get('halfyear_year') - interval_start = request.session.get('halfyear_start_month') - - if not interval_year or not interval_start: - # No interval chosen yet -> redirect to main page - return redirect('clients_list') - - interval_year = int(interval_year) - interval_start = int(interval_start) - - # You already have this helper in your code - window = build_halfyear_window(interval_year, interval_start) - # window = [(y1, m1), (y2, m2), ..., (y6, m6)] - - # (Year, month) of the first month - start_year, start_month = window[0] - - # Previous month (for "Stand ... (Vorjahr)" and "Best. in Kannen Vormonat") - prev_total_index = (start_month - 1) - 1 # one month back, 0-based - if prev_total_index >= 0: - prev_year = start_year + (prev_total_index // 12) - prev_month = (prev_total_index % 12) + 1 - else: - prev_year = start_year - 1 - prev_month = 12 - - # Load MonthlySheet objects for the window and for the previous month - sheets_by_ym = {} - for (y, m) in window: - sheet = MonthlySheet.objects.filter(year=y, month=m).first() - sheets_by_ym[(y, m)] = sheet - - prev_sheet = MonthlySheet.objects.filter(year=prev_year, month=prev_month).first() - def pick_bottom2_from_window(window, sheets_by_ym, prev_sheet): - # choose sheet (same logic you already use) - chosen = None - for (y, m) in reversed(window): - s = sheets_by_ym.get((y, m)) - # use your existing condition for choosing month - if s: - chosen = s - break - if chosen is None: - chosen = prev_sheet - - # Now read the two inputs safely - bottom2_inputs = {} - for key, (row_idx, col_idx) in BOTTOM2_ROW_INPUTS.items(): - bottom2_inputs[key] = get_bottom2_value(chosen, row_idx, col_idx) - - return chosen, bottom2_inputs - - - chosen_sheet_bottom2, bottom2_inputs = pick_bottom2_from_window(window, sheets_by_ym, prev_sheet) - bottom2_g39 = bottom2_inputs["g39"] - bottom2_i39 = bottom2_inputs["i39"] - # ---------------------------- - # HALF-YEAR BOTTOM TABLE 1 (Bilanz) - Read only - # ---------------------------- - chosen_sheet_bottom1 = pick_sheet_by_gasbestand(window, sheets_by_ym, prev_sheet) - - # IMPORTANT: define which bottom_1 row_index corresponds to Excel rows 27..35 - # If your bottom_1 starts at Excel row 27 => row_index 0 == Excel 27 - # then row_index = excel_row - 27 - BOTTOM1_EXCEL_START_ROW = 27 - - bottom1_excel_rows = list(range(27, 37)) # 27..36 - BOTTOM1_LABELS = [ - "Batterie 1", - "2", - "3", - "4", - "5", - "Batterie Links", - "2 Bündel", - "2 Ballone", - "Reingasspeicher", - "Gasbestand", - ] - - BOTTOM1_VOLUMES = [ - Decimal("2.4"), - Decimal("5.1"), - Decimal("4.0"), - Decimal("1.0"), - Decimal("4.0"), - Decimal("0.6"), - Decimal("1.2"), - Decimal("20.0"), - Decimal("5.0"), - None, # Gasbestand row has no volume - ] - nm3_sum_27_35 = Decimal("0") - lhe_sum_27_35 = Decimal("0") - bottom1_rows = [] - - for excel_row in bottom1_excel_rows: - row_index = excel_row - BOTTOM1_EXCEL_START_ROW - - chosen_sheet_bottom1 = None - for (y, m) in reversed(window): - s = sheets_by_ym.get((y, m)) - gasbestand = get_bottom1_value(s, GASBESTAND_ROW_INDEX, GASBESTAND_COL_NM3) # J36 (Nm3) - if gasbestand != 0: - chosen_sheet_bottom1 = s - break - - if chosen_sheet_bottom1 is None: - chosen_sheet_bottom1 = prev_sheet - - # Normal rows (27..35): read from chosen sheet and accumulate sums - if excel_row != 36: - nm3_val = get_bottom1_value(chosen_sheet_bottom1, row_index, BOTTOM1_COL_NM3) - lhe_val = get_bottom1_value(chosen_sheet_bottom1, row_index, BOTTOM1_COL_LHE) - - nm3_sum_27_35 += nm3_val - lhe_sum_27_35 += lhe_val - - bottom1_rows.append({ - "label": BOTTOM1_LABELS[row_index], - "volume": BOTTOM1_VOLUMES[row_index], - "bar": get_bottom1_value(chosen_sheet_bottom1, row_index, BOTTOM1_COL_BAR), - "korr": get_bottom1_value(chosen_sheet_bottom1, row_index, BOTTOM1_COL_KORR), - "nm3": nm3_val, - "lhe": lhe_val, - }) - - # Gasbestand row (36): show sums (J36 = SUM(J27:J35), K36 = SUM(K27:K35)) - else: - bottom1_rows.append({ - "label": "Gasbestand", - "volume": "", - "bar": "", - "korr": "", - "nm3": nm3_sum_27_35, - "lhe": lhe_sum_27_35, - }) - start_sheet = sheets_by_ym.get((start_year, start_month)) - # ------------------------------------------------------------ - # Bottom Table 2 (Halbjahres Bilanz) – server-side recalcBottom2() - # ------------------------------------------------------------ - - FACTOR_BT2 = Decimal("0.75") - - # 1) Helper: pick last-nonzero value of bottom_2 row0 col0/col1 from the window (fallback: prev_sheet) - def pick_last_nonzero_bottom2(row_index: int, col_index: int) -> Decimal: - # Scan from last month in window backwards - for (y, m) in reversed(window): - s = sheets_by_ym.get((y, m)) - if not s: - continue - v = get_bottom2_value(s, row_index, col_index) - if v is not None and v != 0: - return v - # fallback to month before window - v_prev = get_bottom2_value(prev_sheet, row_index, col_index) - return v_prev if v_prev is not None else Decimal("0") - - # 2) K38 comes from Overall Summary: "Summe Bestand (Lit. L-He)" - # Find it from your already built overall summary rows list. - - k38 = Decimal("0") - j38 = Decimal("0") - # 3) Inputs G39 / I39 (picked from last non-zero month in window) - g39 = pick_last_nonzero_bottom2(row_index=0, col_index=0) # G39 - i39 = pick_last_nonzero_bottom2(row_index=0, col_index=1) # I39 - - k39 = (g39 or Decimal("0")) + (i39 or Decimal("0")) - j39 = k39 * FACTOR_BT2 - - # 4) +Kaltgas (row 40) - # JS: - # g40 = (2500 - g39)/100*10 - # i40 = (1000 - i39)/100*10 - g40 = None - i40 = None - if g39 is not None: - g40 = (Decimal("2500") - g39) / Decimal("100") * Decimal("10") - if i39 is not None: - i40 = (Decimal("1000") - i39) / Decimal("100") * Decimal("10") - - k40 = (g40 or Decimal("0")) + (i40 or Decimal("0")) - j40 = k40 * FACTOR_BT2 - - # 5) Bestand flüssig He (row 43) - k43 = ( - (k38 or Decimal("0")) + - (k39 or Decimal("0")) + - (k40 or Decimal("0")) - ) - j43 = k43 * FACTOR_BT2 - - # 6) Gesamtbestand neu (row 44) = Gasbestand(Lit) from Bottom Table 1 + k43 - gasbestand_lit = Decimal("0") - for r in bottom1_rows: - if (r.get("label") or "").strip().startswith("Gasbestand"): - gasbestand_lit = r.get("lhe") or Decimal("0") - break - - k44 = (gasbestand_lit or Decimal("0")) + (k43 or Decimal("0")) - j44 = k44 * FACTOR_BT2 - - bottom2 = { - "j38": j38, "k38": k38, - "g39": g39, "i39": i39, "j39": j39, "k39": k39, - "g40": g40, "i40": i40, "j40": j40, "k40": k40, - "j43": j43, "k43": k43, - "j44": j44, "k44": k44, - } - - # ------------------------------------------------------------------ - # 2) LEFT TABLE (your existing, working logic) - # ------------------------------------------------------------------ - HALFYEAR_CLIENTS_LEFT = ["AG Vogel", "AG Halfm", "IKP"] - - # We'll collect client-wise values first for clarity. - client_data_left = {name: {} for name in HALFYEAR_CLIENTS_LEFT} - - # --- Row B3: Stand der Gaszähler (Nm³) - # = MAX(B3 from previous month, and B3 from each of the 6 months in the window) - # row_index 0 in top_left = "Stand der Gaszähler (Nm³)" - months_for_max = [(prev_year, prev_month)] + window - - for cname in HALFYEAR_CLIENTS_LEFT: - max_val = Decimal('0') - for (y, m) in months_for_max: - sheet = sheets_by_ym.get((y, m)) - if sheet is None and (y, m) == (prev_year, prev_month): - sheet = prev_sheet - val_b3 = get_top_left_value(sheet, cname, row_index=0) - if val_b3 > max_val: - max_val = val_b3 - client_data_left[cname]['stand_gas'] = max_val - - # --- Row B4: Stand der Gaszähler (Vorjahr) (Nm³) -> previous month same row --- - for cname in HALFYEAR_CLIENTS_LEFT: - val_b4 = get_top_left_value(prev_sheet, cname, row_index=0) - client_data_left[cname]['stand_gas_prev'] = val_b4 - - # --- Row B5: Gasrückführung (Nm³) = B3 - B4 --- - for cname in HALFYEAR_CLIENTS_LEFT: - b3 = client_data_left[cname]['stand_gas'] - b4 = client_data_left[cname]['stand_gas_prev'] - client_data_left[cname]['gasrueckf'] = b3 - b4 - - # --- Row B6: Rückführung flüssig (Lit. L-He) = B5 / 0.75 --- - for cname in HALFYEAR_CLIENTS_LEFT: - b5 = client_data_left[cname]['gasrueckf'] - client_data_left[cname]['rueckf_fluessig'] = (b5 / Decimal('0.75')) if b5 != 0 else Decimal('0') - - # --- Row B7: Sonderrückführungen (Lit. L-He) = sum over 6 months of that row --- - # That row index is 4 in your top_left table. - for cname in HALFYEAR_CLIENTS_LEFT: - sonder_total = Decimal('0') - for (y, m) in window: - sheet = sheets_by_ym.get((y, m)) - if sheet: - sonder_total += get_top_left_value(sheet, cname, row_index=4) - client_data_left[cname]['sonder'] = sonder_total - - # --- Row B8: Bestand in Kannen-1 (Lit. L-He) --- - # Excel-style logic with Gasbestand (J36) and fallback to previous month. - for cname in HALFYEAR_CLIENTS_LEFT: - chosen_value = None - - # Go from last month (window[5]) backwards to first (window[0]) - for (y, m) in reversed(window): - sheet = sheets_by_ym.get((y, m)) - gasbestand = get_bottom1_value(sheet, GASBESTAND_ROW_INDEX, GASBESTAND_COL_NM3) - if gasbestand != 0: - chosen_value = get_bestand_kannen_for_month(sheet, cname) - break - - # If still None -> use previous month (Übertrag_Dez__Vorjahr equivalent) - if chosen_value is None: - sheet_prev = prev_sheet - chosen_value = get_bestand_kannen_for_month(sheet_prev, cname) - - client_data_left[cname]['bestand_kannen'] = chosen_value if chosen_value is not None else Decimal('0') - - # --- Row B9: Summe Bestand (Lit. L-He) = equal to previous row --- - for cname in HALFYEAR_CLIENTS_LEFT: - client_data_left[cname]['summe_bestand'] = client_data_left[cname]['bestand_kannen'] - - # --- Row B10: Best. in Kannen Vormonat (Lit. L-He) - # = Bestand in Kannen-1 from the month BEFORE the window (prev_year, prev_month) - for cname in HALFYEAR_CLIENTS_LEFT: - client_data_left[cname]['best_kannen_vormonat'] = get_bestand_kannen_for_month(prev_sheet, cname) - - # --- Row B13: Bezug (Liter L-He) --- - for cname in HALFYEAR_CLIENTS_LEFT: - total_bezug = Decimal('0') - for (y, m) in window: - qs = SecondTableEntry.objects.filter( - client__name=cname, - date__year=y, - date__month=m, - ).aggregate( - total=Coalesce(Sum('lhe_output'), Value(0, output_field=DecimalField())) - ) - total_bezug += Decimal(str(qs['total'])) - client_data_left[cname]['bezug'] = total_bezug - - # --- Row B14: Rückführ. Soll (Lit. L-He) = Bezug - Summe Bestand + Best. in Kannen Vormonat --- - for cname in HALFYEAR_CLIENTS_LEFT: - b13 = client_data_left[cname]['bezug'] - b11 = client_data_left[cname]['summe_bestand'] - b12 = client_data_left[cname]['best_kannen_vormonat'] - client_data_left[cname]['rueckf_soll'] = b13 - b11 + b12 - - # --- Row B15: Verluste (Soll-Rückf.) (Lit. L-He) = B14 - B6 --- - for cname in HALFYEAR_CLIENTS_LEFT: - b14 = client_data_left[cname]['rueckf_soll'] - b6 = client_data_left[cname]['rueckf_fluessig'] - client_data_left[cname]['verluste'] = b14 - b6 - - # --- Row B16: Füllungen warm (Lit. L-He) = sum over 6 months (row_index=11) --- - for cname in HALFYEAR_CLIENTS_LEFT: - total_warm = Decimal('0') - for (y, m) in window: - sheet = sheets_by_ym.get((y, m)) - total_warm += get_top_left_value(sheet, cname, row_index=11) - client_data_left[cname]['fuellungen_warm'] = total_warm - - # --- Row B17: Kaltgas Rückgabe (Lit. L-He) = Bezug * 0.06 --- - factor = Decimal('0.06') - for cname in HALFYEAR_CLIENTS_LEFT: - b13 = client_data_left[cname]['bezug'] - client_data_left[cname]['kaltgas_rueckgabe'] = b13 * factor - - # --- Row B18: Verbraucherverluste (Liter L-He) = Verluste - Kaltgas Rückgabe --- - for cname in HALFYEAR_CLIENTS_LEFT: - b15 = client_data_left[cname]['verluste'] - b17 = client_data_left[cname]['kaltgas_rueckgabe'] - client_data_left[cname]['verbraucherverluste'] = b15 - b17 - - # --- Row B19: % = Verbraucherverluste / Bezug --- - for cname in HALFYEAR_CLIENTS_LEFT: - bezug = client_data_left[cname]['bezug'] - verb = client_data_left[cname]['verbraucherverluste'] - if bezug != 0: - client_data_left[cname]['percent'] = verb / bezug - else: - client_data_left[cname]['percent'] = None - - # Build LEFT rows structure - left_row_defs = [ - ('Stand der Gaszähler (Nm³)', 'stand_gas'), - ('Stand der Gaszähler (Vorjahr) (Nm³)', 'stand_gas_prev'), - ('Gasrückführung (Nm³)', 'gasrueckf'), - ('Rückführung flüssig (Lit. L-He)', 'rueckf_fluessig'), - ('Sonderrückführungen (Lit. L-He)', 'sonder'), - ('Bestand in Kannen-1 (Lit. L-He)', 'bestand_kannen'), - ('Summe Bestand (Lit. L-He)', 'summe_bestand'), - ('Best. in Kannen Vormonat (Lit. L-He)', 'best_kannen_vormonat'), - ('Bezug (Liter L-He)', 'bezug'), - ('Rückführ. Soll (Lit. L-He)', 'rueckf_soll'), - ('Verluste (Soll-Rückf.) (Lit. L-He)', 'verluste'), - ('Füllungen warm (Lit. L-He)', 'fuellungen_warm'), - ('Kaltgas Rückgabe (Lit. L-He) – Faktor', 'kaltgas_rueckgabe'), - ('Verbraucherverluste (Liter L-He)', 'verbraucherverluste'), - ('%', 'percent'), - ] - - rows_left = [] - for label, key in left_row_defs: - values = [client_data_left[cname][key] for cname in HALFYEAR_CLIENTS_LEFT] - if key == 'percent': - total_bezug = sum((client_data_left[c]['bezug'] for c in HALFYEAR_CLIENTS_LEFT), Decimal('0')) - total_verb = sum((client_data_left[c]['verbraucherverluste'] for c in HALFYEAR_CLIENTS_LEFT), Decimal('0')) - total = (total_verb / total_bezug) if total_bezug != 0 else None - else: - total = sum((v for v in values if v is not None), Decimal('0')) - rows_left.append({ - 'label': label, - 'values': values, - 'total': total, - 'is_percent': key == 'percent', - }) - - # ------------------------------------------------------------------ - # 3) RIGHT TABLE (top-right half-year aggregation) - # ------------------------------------------------------------------ - RIGHT_CLIENTS = HALFYEAR_RIGHT_CLIENTS # for brevity - - right_data = {name: {} for name in RIGHT_CLIENTS} - - # --- Bezug (Liter L-He) for each right client (same as for left) --- - for cname in RIGHT_CLIENTS: - total_bezug = Decimal('0') - for (y, m) in window: - qs = SecondTableEntry.objects.filter( - client__name=cname, - date__year=y, - date__month=m, - ).aggregate( - total=Coalesce(Sum('lhe_output'), Value(0, output_field=DecimalField())) - ) - total_bezug += Decimal(str(qs['total'])) - right_data[cname]['bezug'] = total_bezug - def find_bestand_from_window(reference_client: str) -> Decimal: - """ - Implements: - WENN(last_month!J36=0; WENN(prev_month!J36=0; ...; prev_sheet!9); last_month!9) - reference_client decides which column (L/N/P/Q/R) we read from monthly top_right row_index=5. - """ - # scan backward through window - for (y, m) in reversed(window): - sh = sheets_by_ym.get((y, m)) - if not sh: - continue - gasbestand = get_bottom1_value(sh, GASBESTAND_ROW_INDEX, GASBESTAND_COL_NM3) - if gasbestand != 0: - return get_top_right_value(sh, reference_client, TR_BESTAND_KANNEN_ROW) - - # fallback to previous month (Übertrag_Dez__Vorjahr equivalent) - return get_top_right_value(prev_sheet, reference_client, TR_BESTAND_KANNEN_ROW) - - # Fohrer+Buntk merged: BOTH use Fohrer column (L9) - val_L = find_bestand_from_window("Dr. Fohrer") - right_data["Dr. Fohrer"]["bestand_kannen"] = val_L - right_data["AG Buntk."]["bestand_kannen"] = val_L - - # Alff+Gutfl merged: BOTH use Alff column (N9) - val_N = find_bestand_from_window("AG Alff") - right_data["AG Alff"]["bestand_kannen"] = val_N - right_data["AG Gutfl."]["bestand_kannen"] = val_N - - # M3 each uses its own column (P9/Q9/R9) - right_data["M3 Thiele"]["bestand_kannen"] = find_bestand_from_window("M3 Thiele") - right_data["M3 Buntkowsky"]["bestand_kannen"] = find_bestand_from_window("M3 Buntkowsky") - right_data["M3 Gutfleisch"]["bestand_kannen"] = find_bestand_from_window("M3 Gutfleisch") - # Helper for pair shares (L13/($L13+$M13), etc.) - def pair_share(c1, c2): - total = right_data[c1]['bezug'] + right_data[c2]['bezug'] - if total == 0: - return (Decimal('0'), Decimal('0')) - return ( - right_data[c1]['bezug'] / total, - right_data[c2]['bezug'] / total, - ) - - # --- "Stand der Gaszähler (Vorjahr) (Nm³)" row: share based on Bezug --- - # Dr. Fohrer / AG Buntk. - s_fohrer, s_buntk = pair_share("Dr. Fohrer", "AG Buntk.") - right_data["Dr. Fohrer"]['stand_prev_share'] = s_fohrer - right_data["AG Buntk."]['stand_prev_share'] = s_buntk - - # AG Alff / AG Gutfl. - s_alff, s_gutfl = pair_share("AG Alff", "AG Gutfl.") - right_data["AG Alff"]['stand_prev_share'] = s_alff - right_data["AG Gutfl."]['stand_prev_share'] = s_gutfl - - # M3 Thiele / M3 Buntkowsky / M3 Gutfleisch → empty in Excel → None - for cname in ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"]: - right_data[cname]['stand_prev_share'] = None - - # --- Rückführung flüssig per month (raw sums) --- - # top_right row_index=2 is "Rückführung flüssig (Lit. L-He)" - - # --- Sonderrückführungen (row_index=3 in top_right) --- - for cname in RIGHT_CLIENTS: - sonder_total = Decimal('0') - for (y, m) in window: - sheet = sheets_by_ym.get((y, m)) - if sheet: - sonder_total += get_top_right_value(sheet, cname, row_index=3) - right_data[cname]['sonder'] = sonder_total - - # --- Sammelrückführung (row_index=4 in top_right), grouped & merged --- - # Group 1: Dr. Fohrer + AG Buntk. - group1_total = Decimal('0') - for (y, m) in window: - sheet = sheets_by_ym.get((y, m)) - if sheet: - group1_total += get_top_right_value(sheet, "Dr. Fohrer", row_index=4) - right_data["Dr. Fohrer"]['sammel'] = group1_total - right_data["AG Buntk."]['sammel'] = group1_total - - # Group 2: AG Alff + AG Gutfl. - group2_total = Decimal('0') - for (y, m) in window: - sheet = sheets_by_ym.get((y, m)) - if sheet: - group2_total += get_top_right_value(sheet, "AG Alff", row_index=4) - right_data["AG Alff"]['sammel'] = group2_total - right_data["AG Gutfl."]['sammel'] = group2_total - - # Group 3: M3 Thiele + M3 Buntkowsky + M3 Gutfleisch - group3_total = Decimal('0') - for (y, m) in window: - sheet = sheets_by_ym.get((y, m)) - if sheet: - group3_total += get_top_right_value(sheet, "M3 Thiele", row_index=4) - right_data["M3 Thiele"]['sammel'] = group3_total - right_data["M3 Buntkowsky"]['sammel'] = group3_total - right_data["M3 Gutfleisch"]['sammel'] = group3_total - def safe_div(a: Decimal, b: Decimal) -> Decimal: - return (a / b) if b != 0 else Decimal("0") - - # --- Rückführung flüssig (Lit. L-He) for Halbjahres-Bilanz top-right --- - # Uses your exact formulas. - - # 1) Fohrer / Buntk split by BEZUG share times group SAMMEL (L8) - L13 = right_data["Dr. Fohrer"]["bezug"] - M13 = right_data["AG Buntk."]["bezug"] - L8 = right_data["Dr. Fohrer"]["sammel"] # merged group total - - den = (L13 + M13) - right_data["Dr. Fohrer"]["rueckf_fluessig"] = (safe_div(L13, den) * L8) if den != 0 else Decimal("0") - right_data["AG Buntk."]["rueckf_fluessig"] = (safe_div(M13, den) * L8) if den != 0 else Decimal("0") - - # 2) Alff / Gutfl split by BEZUG share times group SAMMEL (N8) - N13 = right_data["AG Alff"]["bezug"] - O13 = right_data["AG Gutfl."]["bezug"] - N8 = right_data["AG Alff"]["sammel"] # merged group total - - den = (N13 + O13) - right_data["AG Alff"]["rueckf_fluessig"] = (safe_div(N13, den) * N8) if den != 0 else Decimal("0") - right_data["AG Gutfl."]["rueckf_fluessig"] = (safe_div(O13, den) * N8) if den != 0 else Decimal("0") - - # 3) M3 Thiele = sum of monthly Rückführung flüssig (monthly top_right row_index=2) over window - P6_sum = Decimal("0") - for (y, m) in window: - sh = sheets_by_ym.get((y, m)) - P6_sum += get_top_right_value(sh, "M3 Thiele", TR_RUECKF_FLUESSIG_ROW) - right_data["M3 Thiele"]["rueckf_fluessig"] = P6_sum - - # 4) M3 Buntkowsky / M3 Gutfleisch split by BEZUG share times M3-group SAMMEL (P8) - P13 = right_data["M3 Thiele"]["bezug"] - Q13 = right_data["M3 Buntkowsky"]["bezug"] - R13 = right_data["M3 Gutfleisch"]["bezug"] - P8 = right_data["M3 Thiele"]["sammel"] # merged group total - - den = (P13 + Q13 + R13) - right_data["M3 Buntkowsky"]["rueckf_fluessig"] = (safe_div(Q13, den) * P8) if den != 0 else Decimal("0") - right_data["M3 Gutfleisch"]["rueckf_fluessig"] = (safe_div(R13, den) * P8) if den != 0 else Decimal("0") - # --- Bestand in Kannen-1 (Lit. L-He) for right table (grouped) --- - # Use Gasbestand (J36) and fallback logic, but now reading top_right B9 for each group. - TOP_RIGHT_ROW_BESTAND_KANNEN = 6 # <-- most likely correct in your setup - - def pick_bestand_top_right(base_client: str) -> Decimal: - # Go from last month in window backwards: if Gasbestand != 0, use that month's Bestand in Kannen - for (y, m) in reversed(window): - sh = sheets_by_ym.get((y, m)) - if not sh: - continue - - gasbestand = get_bottom1_value(sh, GASBESTAND_ROW_INDEX, GASBESTAND_COL_NM3) - if gasbestand != 0: - return get_top_right_value(sh, base_client, TOP_RIGHT_ROW_BESTAND_KANNEN) - - # Fallback to previous month (Übertrag_Dez__Vorjahr equivalent) - return get_top_right_value(prev_sheet, base_client, TOP_RIGHT_ROW_BESTAND_KANNEN) - - # Group 1 merged (Fohrer + Buntk.) - g1_best = pick_bestand_top_right("Dr. Fohrer") - right_data["Dr. Fohrer"]["bestand_kannen"] = g1_best - right_data["AG Buntk."]["bestand_kannen"] = g1_best - - # Group 2 merged (Alff + Gutfl.) - g2_best = pick_bestand_top_right("AG Alff") - right_data["AG Alff"]["bestand_kannen"] = g2_best - right_data["AG Gutfl."]["bestand_kannen"] = g2_best - - # Group 3 merged (M3 Thiele + M3 Buntkowsky + M3 Gutfleisch) - g3_best = pick_bestand_top_right("M3 Thiele") - right_data["M3 Thiele"]["bestand_kannen"] = g3_best - right_data["M3 Buntkowsky"]["bestand_kannen"] = g3_best - right_data["M3 Gutfleisch"]["bestand_kannen"] = g3_best - - # Summe Bestand = same as previous row - for cname in RIGHT_CLIENTS: - right_data[cname]['summe_bestand'] = right_data[cname]['bestand_kannen'] - - # Best. in Kannen Vormonat (Lit. L-He) from previous month top_right row_index=7 - g1_prev = get_top_right_value(prev_sheet, "Dr. Fohrer", TOP_RIGHT_ROW_BESTAND_KANNEN) - right_data["Dr. Fohrer"]['best_kannen_vormonat'] = g1_prev - right_data["AG Buntk."]['best_kannen_vormonat'] = g1_prev - - g2_prev = get_top_right_value(prev_sheet, "AG Alff", TOP_RIGHT_ROW_BESTAND_KANNEN) - right_data["AG Alff"]['best_kannen_vormonat'] = g2_prev - right_data["AG Gutfl."]['best_kannen_vormonat'] = g2_prev - - - # Group 1 merged (Fohrer + Buntk.) - g1_prev = get_top_right_value(prev_sheet, "Dr. Fohrer", TOP_RIGHT_ROW_BESTAND_KANNEN) - right_data["Dr. Fohrer"]["best_kannen_vormonat"] = g1_prev - right_data["AG Buntk."]["best_kannen_vormonat"] = g1_prev - - # Group 2 merged (Alff + Gutfl.) - g2_prev = get_top_right_value(prev_sheet, "AG Alff", TOP_RIGHT_ROW_BESTAND_KANNEN) - right_data["AG Alff"]["best_kannen_vormonat"] = g2_prev - right_data["AG Gutfl."]["best_kannen_vormonat"] = g2_prev - - # Group 3 UNMERGED (each one reads its own cell) - right_data["M3 Thiele"]["best_kannen_vormonat"] = get_top_right_value(prev_sheet, "M3 Thiele", TOP_RIGHT_ROW_BESTAND_KANNEN) - right_data["M3 Buntkowsky"]["best_kannen_vormonat"] = get_top_right_value(prev_sheet, "M3 Buntkowsky", TOP_RIGHT_ROW_BESTAND_KANNEN) - right_data["M3 Gutfleisch"]["best_kannen_vormonat"] = get_top_right_value(prev_sheet, "M3 Gutfleisch", TOP_RIGHT_ROW_BESTAND_KANNEN) - - # --- Rückführ. Soll (Lit. L-He) according to your formulas --- - - # Group 1: Dr. Fohrer / AG Buntk. - total_bestand_1 = right_data["Dr. Fohrer"]['summe_bestand'] - best_vormonat_1 = right_data["Dr. Fohrer"]['best_kannen_vormonat'] - diff1 = total_bestand_1 - best_vormonat_1 - share_fohrer = right_data["Dr. Fohrer"]['stand_prev_share'] or Decimal('0') - - right_data["Dr. Fohrer"]['rueckf_soll'] = ( - right_data["Dr. Fohrer"]['bezug'] - diff1 * share_fohrer - ) - right_data["AG Buntk."]['rueckf_soll'] = ( - right_data["AG Buntk."]['bezug'] - total_bestand_1 + best_vormonat_1 - ) - - # Group 2: AG Alff / AG Gutfl. - total_bestand_2 = right_data["AG Alff"]['summe_bestand'] - best_vormonat_2 = right_data["AG Alff"]['best_kannen_vormonat'] - diff2 = total_bestand_2 - best_vormonat_2 - share_alff = right_data["AG Alff"]['stand_prev_share'] or Decimal('0') - share_gutfl = right_data["AG Gutfl."]['stand_prev_share'] or Decimal('0') - - right_data["AG Alff"]['rueckf_soll'] = ( - right_data["AG Alff"]['bezug'] - diff2 * share_alff - ) - right_data["AG Gutfl."]['rueckf_soll'] = ( - right_data["AG Gutfl."]['bezug'] - diff2 * share_gutfl - ) - - # Group 3: M3 Thiele / M3 Buntkowsky / M3 Gutfleisch - for cname in ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"]: - b13 = right_data[cname]['bezug'] - b12 = right_data[cname]['best_kannen_vormonat'] - b11 = right_data[cname]['summe_bestand'] - # Excel: P13+P12-P11 etc. - right_data[cname]['rueckf_soll'] = b13 + b12 - b11 - - # --- Verluste (Soll-Rückf.) (Lit. L-He) = B14 - B6 - B7 --- - for cname in RIGHT_CLIENTS: - b14 = right_data[cname]['rueckf_soll'] - b6 = right_data[cname]['rueckf_fluessig'] - b7 = right_data[cname]['sonder'] - right_data[cname]['verluste'] = b14 - b6 - b7 - - # --- Füllungen warm (Lit. L-He) = sum of monthly 'Füllungen warm' (row_index=11 top_right) --- - for cname in RIGHT_CLIENTS: - total_warm = Decimal('0') - for (y, m) in window: - sheet = sheets_by_ym.get((y, m)) - if sheet: - total_warm += get_top_right_value(sheet, cname, row_index=11) - right_data[cname]['fuellungen_warm'] = total_warm - - # --- Kaltgas Rückgabe (Lit. L-He) – Faktor = Bezug * 0.06 --- - for cname in RIGHT_CLIENTS: - b13 = right_data[cname]['bezug'] - right_data[cname]['kaltgas_rueckgabe'] = b13 * factor - - # --- Verbraucherverluste (Liter L-He) = Verluste - Kaltgas Rückgabe --- - for cname in RIGHT_CLIENTS: - b15 = right_data[cname]['verluste'] - b17 = right_data[cname]['kaltgas_rueckgabe'] - right_data[cname]['verbraucherverluste'] = b15 - b17 - - # --- % = Verbraucherverluste / Bezug --- - for cname in RIGHT_CLIENTS: - bezug = right_data[cname]['bezug'] - verb = right_data[cname]['verbraucherverluste'] - if bezug != 0: - right_data[cname]['percent'] = verb / bezug - else: - right_data[cname]['percent'] = None - - # Build RIGHT rows structure - right_row_defs = [ - ('Stand der Gaszähler (Vorjahr) (Nm³)', 'stand_prev_share'), - # We skip the pure-text "Gasrückführung (Nm³)" line here, - # because it’s only text (Aufteilung nach Verbrauch / Gaszähler) - # and easier to render directly in the template if needed. - ('Rückführung flüssig (Lit. L-He)', 'rueckf_fluessig'), - ('Sonderrückführungen (Lit. L-He)', 'sonder'), - ('Sammelrückführung (Lit. L-He)', 'sammel'), - ('Bestand in Kannen-1 (Lit. L-He)', 'bestand_kannen'), - ('Summe Bestand (Lit. L-He)', 'summe_bestand'), - ('Best. in Kannen Vormonat (Lit. L-He)', 'best_kannen_vormonat'), - ('Bezug (Liter L-He)', 'bezug'), - ('Rückführ. Soll (Lit. L-He)', 'rueckf_soll'), - ('Verluste (Soll-Rückf.) (Lit. L-He)', 'verluste'), - ('Füllungen warm (Lit. L-He)', 'fuellungen_warm'), - ('Kaltgas Rückgabe (Lit. L-He) – Faktor', 'kaltgas_rueckgabe'), - ('Verbraucherverluste (Liter L-He)', 'verbraucherverluste'), - ('%', 'percent'), - ] - - rows_right = [] - for label, key in right_row_defs: - values = [right_data[cname].get(key) for cname in RIGHT_CLIENTS] - - if key == 'percent': - total_bezug = sum((right_data[c]['bezug'] for c in RIGHT_CLIENTS), Decimal('0')) - total_verb = sum((right_data[c]['verbraucherverluste'] for c in RIGHT_CLIENTS), Decimal('0')) - total = (total_verb / total_bezug) if total_bezug != 0 else None - else: - total = sum((v for v in values if isinstance(v, Decimal)), Decimal('0')) - - rows_right.append({ - 'label': label, - 'values': values, - 'total': total, - 'is_percent': key == 'percent', - }) - SUM_TABLE_ROWS = [ - ("Rückführung flüssig (Lit. L-He)", "rueckf_fluessig"), - ("Sonderrückführungen (Lit. L-He)", "sonder"), - ("Sammelrückführungen (Lit. L-He)", "sammel"), - ("Bestand in Kannen-1 (Lit. L-He)", "bestand_kannen"), - ("Summe Bestand (Lit. L-He)", "summe_bestand"), - ("Best. in Kannen Vormonat (Lit. L-He)", "best_kannen_vormonat"), - ("Bezug (Liter L-He)", "bezug"), - ("Rückführ. Soll (Lit. L-He)", "rueckf_soll"), - ("Verluste (Soll-Rückf.) (Lit. L-He)", "verluste"), - ("Füllungen warm (Lit. L-He)", "fuellungen_warm"), - ("Kaltgas Rückgabe (Lit. L-He) – Faktor", "kaltgas_rueckgabe"), - ("Faktor 0.06", "factor_row"), - ("Verbraucherverluste (Liter L-He)", "verbraucherverluste"), - ("%", "percent"), - ] - - RIGHT_GROUPS = { - "chemie": ["Dr. Fohrer", "AG Buntk."], - "mawi": ["AG Alff", "AG Gutfl."], - "m3": ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"], - } - - RIGHT_ALL = ["Dr. Fohrer", "AG Buntk.", "AG Alff", "AG Gutfl.", "M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"] - LEFT_ALL = HALFYEAR_CLIENTS_LEFT - - def safe_pct(verb, bez): - return (verb / bez) if bez != 0 else None - rows_sum = [] - def d(x): - return x if isinstance(x, Decimal) else Decimal("0") - for label, key in SUM_TABLE_ROWS: - - if key == "factor_row": - lichtwiese = chemie = mawi = m3 = total = Decimal("0.06") - - elif key == "percent": - # Right totals - rw_bez = sum(d(right_data[c].get("bezug")) for c in RIGHT_ALL) - rw_verb = sum(d(right_data[c].get("verbraucherverluste")) for c in RIGHT_ALL) - lichtwiese = safe_pct(rw_verb, rw_bez) - - # Chemie - ch_bez = sum(d(right_data[c].get("bezug")) for c in RIGHT_GROUPS["chemie"]) - ch_verb = sum(d(right_data[c].get("verbraucherverluste")) for c in RIGHT_GROUPS["chemie"]) - chemie = safe_pct(ch_verb, ch_bez) - - # MaWi - mw_bez = sum(d(right_data[c].get("bezug")) for c in RIGHT_GROUPS["mawi"]) - mw_verb = sum(d(right_data[c].get("verbraucherverluste")) for c in RIGHT_GROUPS["mawi"]) - mawi = safe_pct(mw_verb, mw_bez) - - # M3 - m3_bez = sum(d(right_data[c].get("bezug")) for c in RIGHT_GROUPS["m3"]) - m3_verb = sum(d(right_data[c].get("verbraucherverluste")) for c in RIGHT_GROUPS["m3"]) - m3 = safe_pct(m3_verb, m3_bez) - - # Σ column = (left verb + right verb) / (left bez + right bez) - left_bez = sum(d(client_data_left[c].get("bezug")) for c in LEFT_ALL) - left_verb = sum(d(client_data_left[c].get("verbraucherverluste")) for c in LEFT_ALL) - total = safe_pct(left_verb + rw_verb, left_bez + rw_bez) - - else: - # normal rows = sums - lichtwiese = sum(d(right_data[c].get(key)) for c in RIGHT_ALL) - chemie = sum(d(right_data[c].get(key)) for c in RIGHT_GROUPS["chemie"]) - mawi = sum(d(right_data[c].get(key)) for c in RIGHT_GROUPS["mawi"]) - m3 = sum(d(right_data[c].get(key)) for c in RIGHT_GROUPS["m3"]) - - left_total = sum(d(client_data_left[c].get(key)) for c in LEFT_ALL) - total = left_total + lichtwiese - - rows_sum.append({ - "row_index": row_index, - "label": label, - "total": total, - "lichtwiese": lichtwiese, - "chemie": chemie, - "mawi": mawi, - "m3": m3, - "is_percent": (key == "percent"), - }) - def find_sum_row(rows, label_startswith: str): - for r in rows: - if str(r.get("label", "")).strip().startswith(label_startswith): - return r - return None - - summe_bestand_row = find_sum_row(rows_sum, "Summe Bestand") - k38 = (summe_bestand_row.get("total") if summe_bestand_row else Decimal("0")) or Decimal("0") - j38 = k38 * Decimal("0.75") - # --- FIX: now that k38 is known, update bottom2 + recompute dependent rows --- - bottom2["k38"] = k38 - bottom2["j38"] = j38 - - k39 = bottom2.get("k39") or Decimal("0") - k40 = bottom2.get("k40") or Decimal("0") - - # Row 43: Bestand flüssig He = SUMME(K38:K40) - k43 = (k38 or Decimal("0")) + k39 + k40 - j43 = k43 * Decimal("0.75") - - bottom2["k43"] = k43 - bottom2["j43"] = j43 - - # Row 44: Gesamtbestand neu = Gasbestand(Lit) from bottom table 1 + k43 - gasbestand_lit = Decimal("0") - for r in bottom1_rows: - if (r.get("label") or "").strip().startswith("Gasbestand"): - gasbestand_lit = r.get("lhe") or Decimal("0") - break - - k44 = gasbestand_lit + k43 - j44 = k44 * Decimal("0.75") - - bottom2["k44"] = k44 - bottom2["j44"] = j44 - def d(x): - return x if isinstance(x, Decimal) else Decimal("0") - - # ---- Bottom2: J38/K38 depend on rows_sum (overall summary), so do it HERE ---- - k38 = Decimal("0") - for r in rows_sum: - if r.get("label") == "Summe Bestand (Lit. L-He)": - k38 = r.get("total") or Decimal("0") - break - - j38 = k38 * FACTOR_NM3_TO_LHE # 0.75 - bottom2["k38"] = k38 - bottom2["j38"] = j38 - factor = Decimal("0.75") - - # window = the 6-month list you already build in this view: [(y,m), (y,m), ...] - # bottom2 = dict with "k44" already computed in your view - # rows_sum = overall sum group rows list (your existing halfyear logic) - - # 1) K46 = K44 from the month BEFORE the global month (interval start) - start_year = interval_year - start_month = interval_start # whatever you named your start month variable - - if start_month == 1: - prev_year, prev_month = start_year - 1, 12 - else: - prev_year, prev_month = start_year, start_month - 1 - - prev_sheet = MonthlySheet.objects.filter(year=prev_year, month=prev_month).first() - - # This assumes you have MonthlySummary.gesamtbestand_neu_lhe as K44 equivalent. - # If your field name differs, tell me your MonthlySummary model fields. - prev_k44 = Decimal("0") - if prev_sheet: - prev_sum = MonthlySummary.objects.filter(sheet=prev_sheet).first() - if prev_sum and prev_sum.gesamtbestand_neu_lhe is not None: - prev_k44 = Decimal(str(prev_sum.gesamtbestand_neu_lhe)) - - # helpers: read bottom_3 values for a given sheet/month - def get_bottom3_value(sheet, row_index, col_index): - if not sheet: - return Decimal("0") - c = Cell.objects.filter( - sheet=sheet, table_type="bottom_3", - row_index=row_index, column_index=col_index - ).first() - if not c or c.value in (None, "", "None"): - return Decimal("0") - try: - return Decimal(str(c.value)) - except Exception: - return Decimal("0") - - # 2) Sum rows across the 6-month window - g47_sum = Decimal("0") - i47_sum = Decimal("0") - j47_sum = Decimal("0") - - g50_sum = Decimal("0") - i50_sum = Decimal("0") - - for (yy, mm) in window: - s = MonthlySheet.objects.filter(year=yy, month=mm).first() - ## Excel row 47 maps to bottom_3 row_index=1 in DB (see monthly_sheet.html) - g = get_bottom3_value(s, 1, 1) # G47 editable - i = get_bottom3_value(s, 1, 2) # I47 editable - - g47_sum += g - i47_sum += i - - # In monthly_sheet, J47 is CALCULATED as (G47 + I47), not stored as an editable cell - j47_sum += (g + i) - - # row 50: G(2), I(4) - g50_sum += get_bottom3_value(s, 50, 2) - i50_sum += get_bottom3_value(s, 50, 4) - - # 3) K52 = Verbraucherverlust from overall sum group first column (global Σ) - k52 = Decimal("0") - for r in rows_sum: - label = (r.get("label") or "") - if label.startswith("Verbraucherverluste"): - k52 = r.get("total") or Decimal("0") - break - - # --- apply the SAME monthly formulas, with your overrides --- - # Row 46 - k46 = prev_k44 - j46 = k46 * factor - - # Row 47 - g47 = g47_sum - i47 = i47_sum - j47 = j47_sum - k47 = (j47 / factor) + g47 if (j47 != 0 or g47 != 0) else Decimal("0") - - # Row 48 - k48 = k46 + k47 - j48 = k48 * factor - - # Row 49 (akt. Monat) -> in halfyear we use current bottom2 K44 - k49 = bottom2.get("k44") or Decimal("0") - j49 = k49 * factor - - # Row 50 - g50 = g50_sum - i50 = i50_sum - j50 = i50 - k50 = (j50 / factor) if j50 != 0 else Decimal("0") - - # Row 51 - k51 = k48 - k49 - k50 - j51 = k51 * factor - - # Row 52 - j52 = k52 * factor - - # Row 53 - k53 = k51 - k52 - j53 = j51 - j52 - - bottom3 = { - "j46": j46, "k46": k46, - "g47": g47, "i47": i47, "j47": j47, "k47": k47, - "j48": j48, "k48": k48, - "j49": j49, "k49": k49, - "g50": g50, "i50": i50, "j50": j50, "k50": k50, - "j51": j51, "k51": k51, - "j52": j52, "k52": k52, - "j53": j53, "k53": k53, - } - # ------------------------------------------------------------------ - # 4) Context – keep old keys AND new ones - # ------------------------------------------------------------------ - context = { - 'interval_year': interval_year, - 'interval_start_month': interval_start, - 'window': window, - - # Left table – old names (for your first template) - 'clients': HALFYEAR_CLIENTS_LEFT, - 'rows': rows_left, - - # Left table – explicit - 'clients_left': HALFYEAR_CLIENTS_LEFT, - 'rows_left': rows_left, - - # Right table - 'clients_right': RIGHT_CLIENTS, - 'rows_right': rows_right, - 'rows_sum': rows_sum, - 'bottom1_rows': bottom1_rows, - - } - context["bottom2"] = bottom2 - context["bottom3"] = bottom3 - context["context_bottom2_g39"] = bottom2_inputs["g39"] - context["context_bottom2_i39"] = bottom2_inputs["i39"] - return render(request, 'halfyear_balance.html', context) - -def get_bottom2_value(sheet, row_index: int, col_index: int) -> Decimal: - """Get numeric value from bottom_2 or 0 if missing.""" - if sheet is None: - return Decimal("0") - cell = Cell.objects.filter( - sheet=sheet, - table_type="bottom_2", - row_index=row_index, - column_index=col_index, - ).first() - if cell is None or cell.value in (None, ""): - return Decimal("0") - try: - return Decimal(str(cell.value)) - except Exception: - return Decimal("0") - -def get_top_left_value(sheet, client_name: str, row_index: int) -> Decimal: - """ - Read a numeric value from the top_left table for a given month, client and row. - Does NOT use column_index, because top_left is keyed only by client + row_index. - """ - if sheet is None: - return Decimal('0') - - client_obj = Client.objects.filter(name=client_name).first() - if not client_obj: - return Decimal('0') - - cell = Cell.objects.filter( - sheet=sheet, - table_type='top_left', - client=client_obj, - row_index=row_index, - ).first() - - if cell is None or cell.value in (None, ''): - return Decimal('0') - - try: - return Decimal(str(cell.value)) - except Exception: - return Decimal('0') -def get_group_clients(group_key): - """Return queryset of clients that belong to a logical group.""" - from .models import Client # local import to avoid circulars - - group = CLIENT_GROUPS.get(group_key) - if not group: - return Client.objects.none() - return Client.objects.filter(name__in=group['names']) - -def calculate_summation(sheet, table_type, row_index, sum_column_index): - """Calculate summation for a row, with special handling for % row""" - from decimal import Decimal - from .models import Cell - - try: - # Special case: top_left, % row (Excel B20 -> row_index 19) - if table_type == 'top_left' and row_index == 19: - # K13 = sum of row 13 (Excel B13 -> row_index 12) across all clients - cells_row13 = Cell.objects.filter( - sheet=sheet, - table_type='top_left', - row_index=12, # Excel B13 = row_index 12 - column_index__lt=sum_column_index # Exclude sum column itself - ) - total_13 = Decimal('0') - for cell in cells_row13: - if cell.value is not None: - total_13 += Decimal(str(cell.value)) - - # K19 = sum of row 19 (Excel B19 -> row_index 18) across all clients - cells_row19 = Cell.objects.filter( - sheet=sheet, - table_type='top_left', - row_index=18, # Excel B19 = row_index 18 - column_index__lt=sum_column_index - ) - total_19 = Decimal('0') - for cell in cells_row19: - if cell.value is not None: - total_19 += Decimal(str(cell.value)) - - # Calculate: IF(K13=0; 0; K19/K13) - if total_13 == 0: - return Decimal('0') - return total_19 / total_13 - - # Normal summation for other rows - cells_in_row = Cell.objects.filter( - sheet=sheet, - table_type=table_type, - row_index=row_index, - column_index__lt=sum_column_index - ) - - total = Decimal('0') - for cell in cells_in_row: - if cell.value is not None: - total += Decimal(str(cell.value)) - - return total - - except Exception as e: - print(f"Error calculating summation for {table_type}[{row_index}]: {e}") - return None - -# Helper function for calculations -def evaluate_formula(formula, values_dict): - """ - Safely evaluate a formula like "10 + 9" where numbers are row indices - values_dict: {row_index: decimal_value} - """ - from decimal import Decimal - import re - - try: - # Create a copy of the formula to work with - expr = formula - - # Find all row numbers in the formula - row_refs = re.findall(r'\b\d+\b', expr) - - for row_ref in row_refs: - row_num = int(row_ref) - if row_num in values_dict and values_dict[row_num] is not None: - # Replace row reference with actual value - expr = expr.replace(row_ref, str(values_dict[row_num])) - else: - # Missing value - can't calculate - return None - - # Evaluate the expression - # Note: In production, use a safer evaluator like `asteval` - result = eval(expr, {"__builtins__": {}}, {}) - - # Convert to Decimal with proper rounding - return Decimal(str(round(result, 6))) - - except Exception: - return None - -# Monthly Sheet View -class MonthlySheetView(TemplateView): - template_name = 'monthly_sheet.html' - - def populate_helium_input_to_top_right(self, sheet): - """Populate bezug data from SecondTableEntry to top-right table (row 8 = Excel row 12)""" - from .models import SecondTableEntry, Cell, Client - from django.db.models.functions import Coalesce - from decimal import Decimal - - year = sheet.year - month = sheet.month - - TOP_RIGHT_CLIENTS = [ - "Dr. Fohrer", # Column index 0 (L) - "AG Buntk.", # Column index 1 (M) - "AG Alff", # Column index 2 (N) - "AG Gutfl.", # Column index 3 (O) - "M3 Thiele", # Column index 4 (P) - "M3 Buntkowsky", # Column index 5 (Q) - "M3 Gutfleisch", # Column index 6 (R) - ] - - # For each client in top-right table - for client_name in TOP_RIGHT_CLIENTS: - try: - client = Client.objects.get(name=client_name) - column_index = TOP_RIGHT_CLIENTS.index(client_name) - - # Calculate total LHe_output for this client in this month from SecondTableEntry - total_lhe_output = SecondTableEntry.objects.filter( - client=client, - date__year=year, - date__month=month - ).aggregate( - total=Coalesce(Sum('lhe_output'), Decimal('0')) - )['total'] - - # Get or create the cell for row_index 8 (Excel row 12) - Bezug - cell, created = Cell.objects.get_or_create( - sheet=sheet, - table_type='top_right', - client=client, - row_index=8, # Bezug row (Excel row 12) - column_index=column_index, - defaults={'value': total_lhe_output} - ) - - if not created and cell.value != total_lhe_output: - cell.value = total_lhe_output - cell.save() - - except Client.DoesNotExist: - continue - - # After populating bezug, trigger calculation for all dependent cells - # Get any cell to start the calculation - first_cell = Cell.objects.filter( - sheet=sheet, - table_type='top_right' - ).first() - - if first_cell: - save_view = SaveCellsView() - save_view.calculate_top_right_dependents(sheet, first_cell) - - return True - def calculate_bezug_from_entries(self, sheet, year, month): - """Calculate B11 (Bezug) from SecondTableEntry for all clients - ONLY for non-start sheets""" - from .models import SecondTableEntry, Cell, Client - from django.db.models import Sum - from django.db.models.functions import Coalesce - from decimal import Decimal - - # Check if this is the start sheet - if year == 2025 and month == 1: - return # Don't auto-calculate for start sheet - - for client in Client.objects.all(): - # Calculate total LHe output for this client in this month - lhe_output_sum = SecondTableEntry.objects.filter( - client=client, - date__year=year, - date__month=month - ).aggregate( - total=Coalesce(Sum('lhe_output'), Decimal('0')) - )['total'] - - # Update B11 cell (row_index 8 = UI Row 9) - b11_cell = Cell.objects.filter( - sheet=sheet, - table_type='top_left', - client=client, - row_index=8 # Excel B11 - ).first() - - if b11_cell and (b11_cell.value != lhe_output_sum or b11_cell.value is None): - b11_cell.value = lhe_output_sum - b11_cell.save() - - # Also trigger dependent calculations - from .views import SaveCellsView - save_view = SaveCellsView() - save_view.calculate_top_left_dependents(sheet, b11_cell) - # In MonthlySheetView.get_context_data() method, update the TOP_RIGHT_CLIENTS and row count: - - - - return True - def get_context_data(self, **kwargs): - from decimal import Decimal - context = super().get_context_data(**kwargs) - year = self.kwargs.get('year', datetime.now().year) - month = self.kwargs.get('month', datetime.now().month) - is_start_sheet = (year == FIRST_SHEET_YEAR and month == FIRST_SHEET_MONTH) - - # Get or create the monthly sheet - sheet, created = MonthlySheet.objects.get_or_create( - year=year, month=month - ) - - # All clients (used for bottom tables etc.) - clients = Client.objects.all().order_by('name') - - # Pre-fill cells if creating new sheet - if created: - self.initialize_sheet_cells(sheet, clients) - - # Apply previous month links (for B4 and B12) - self.apply_previous_month_links(sheet, year, month) - self.calculate_bezug_from_entries(sheet, year, month) - self.populate_helium_input_to_top_right(sheet) - self.apply_previous_month_links_top_right(sheet, year, month) - - # Define client groups - TOP_LEFT_CLIENTS = [ - "AG Vogel", - "AG Halfm", - "IKP", - ] - - TOP_RIGHT_CLIENTS = [ - "Dr. Fohrer", - "AG Buntk.", - "AG Alff", - "AG Gutfl.", - "M3 Thiele", - "M3 Buntkowsky", - "M3 Gutfleisch", - ] - current_summary = MonthlySummary.objects.filter(sheet=sheet).first() - - # Get previous month summary (for Bottom Table 3: K46 = prev K44) - prev_month_info = self.get_prev_month(year, month) - prev_summary = None - if not is_start_sheet: - prev_sheet = MonthlySheet.objects.filter( - year=prev_month_info['year'], - month=prev_month_info['month'] - ).first() - if prev_sheet: - prev_summary = MonthlySummary.objects.filter(sheet=prev_sheet).first() - - context.update({ - # ... your existing context ... - 'current_summary': current_summary, - 'prev_summary': prev_summary, - }) - # Update row counts in build_group_rows function - # Update row counts in build_group_rows function - def build_group_rows(sheet, table_type, client_names): - """Build rows for display in monthly sheet.""" - from decimal import Decimal - from .models import Cell - MERGED_ROWS = {2, 3, 5, 6, 7, 9, 10, 12, 14, 15} - MERGED_PAIRS = [ - ("Dr. Fohrer", "AG Buntk."), - ("AG Alff", "AG Gutfl."), - ] - rows = [] - - # Determine row count - row_counts = { - "top_left": 16, - "top_right": 16, # rows 0–15 - "bottom_1": 10, - "bottom_2": 10, - "bottom_3": 10, - } - row_count = row_counts.get(table_type, 0) - - # Get all cells for this sheet and table - all_cells = ( - Cell.objects.filter( - sheet=sheet, - table_type=table_type, - ).select_related("client") - ) - - # Group cells by row index - cells_by_row = {} - for cell in all_cells: - if cell.row_index not in cells_by_row: - cells_by_row[cell.row_index] = {} - cells_by_row[cell.row_index][cell.client.name] = cell - - # We will store row sums for Bezug (row 8) and Verbraucherverluste (row 14) - # for top_left / top_right so we can compute the overall % in row 15. - sum_bezug = None - sum_verbrauch = None - - # Build each row - for row_idx in range(row_count): - display_cells = [] - row_cells_dict = cells_by_row.get(row_idx, {}) - - # Build cells in the requested client order - for name in client_names: - cell = row_cells_dict.get(name) - display_cells.append(cell) - - # Calculate sum for this row (includes editable + calculated cells) - sum_value = None - total = Decimal("0") - has_value = False - - merged_second_indices = set() - if table_type == 'top_right' and row_idx in MERGED_ROWS: - for left_name, right_name in MERGED_PAIRS: - try: - right_idx = client_names.index(right_name) - merged_second_indices.add(right_idx) - except ValueError: - # client not in this table; just ignore - pass - - for col_idx, cell in enumerate(display_cells): - # Skip the duplicate (second) column of each merged pair - if col_idx in merged_second_indices: - continue - - if cell and cell.value is not None: - try: - total += Decimal(str(cell.value)) - has_value = True - except Exception: - pass - - if has_value: - sum_value = total - - # Remember special rows for top tables - if table_type in ("top_left", "top_right"): - if row_idx == 8: # Bezug - sum_bezug = total - elif row_idx == 14: # Verbraucherverluste - sum_verbrauch = total - - rows.append( - { - "cells": display_cells, - "sum": sum_value, - "row_index": row_idx, - } - ) - - # Adjust the % row sum for top_left / top_right: - # Sum(%) = Sum(Verbraucherverluste) / Sum(Bezug) - if table_type in ("top_left", "top_right"): - perc_row_idx = 15 # % row - if 0 <= perc_row_idx < len(rows): - if sum_bezug is not None and sum_bezug != 0 and sum_verbrauch is not None: - rows[perc_row_idx]["sum"] = sum_verbrauch / sum_bezug - else: - rows[perc_row_idx]["sum"] = None - - return rows - - - # Now call the local function - top_left_rows = build_group_rows(sheet, 'top_left', TOP_LEFT_CLIENTS) - top_right_rows = build_group_rows(sheet, 'top_right', TOP_RIGHT_CLIENTS) - - # --- Build combined summary of top-left + top-right Sum columns --- - - # Helper to safely get the Sum for a given row index - def get_row_sum(rows, row_index): - if 0 <= row_index < len(rows): - return rows[row_index].get('sum') - return None - - # Row definitions we want in the combined Σ table - # (row_index in top tables, label shown in the small table) - summary_row_defs = [ - (2, "Rückführung flüssig (Lit. L-He)"), - (3, "Sonderrückführungen (Lit. L-He)"), - (4, "Sammelrückführungen (Lit. L-He)"), - (5, "Bestand in Kannen-1 (Lit. L-He)"), - (6, "Summe Bestand (Lit. L-He)"), - (7, "Best. in Kannen Vormonat (Lit. L-He)"), - (8, "Bezug (Liter L-He)"), - (9, "Rückführ. Soll (Lit. L-He)"), - (10, "Verluste (Soll-Rückf.) (Lit. L-He)"), - (11, "Füllungen warm (Lit. L-He)"), - (12, "Kaltgas Rückgabe (Lit. L-He) – Faktor"), - (13, "Faktor 0.06"), - (14, "Verbraucherverluste (Liter L-He)"), - (15, "%"), - ] - - # Precompute totals for Bezug and Verbraucherverluste across both tables - bezug_left = get_row_sum(top_left_rows, 8) or Decimal('0') - bezug_right = get_row_sum(top_right_rows, 8) or Decimal('0') - total_bezug = bezug_left + bezug_right - - verb_left = get_row_sum(top_left_rows, 14) or Decimal('0') - verb_right = get_row_sum(top_right_rows, 14) or Decimal('0') - total_verbrauch = verb_left + verb_right - - summary_rows = [] - - for row_index, label in summary_row_defs: - # Faktor row: always fixed 0.06 - if row_index == 13: - summary_value = Decimal('0.06') - - # % row: total Verbraucherverluste / total Bezug - elif row_index == 15: - if total_bezug != 0: - summary_value = total_verbrauch / total_bezug - else: - summary_value = None - - else: - left_sum = get_row_sum(top_left_rows, row_index) - right_sum = get_row_sum(top_right_rows, row_index) - - # Sammelrückführungen: only from top-right table - if row_index == 4: - left_sum = None - - total = Decimal('0') - - has_any = False - if left_sum is not None: - total += Decimal(str(left_sum)) - has_any = True - if right_sum is not None: - total += Decimal(str(right_sum)) - has_any = True - - summary_value = total if has_any else None - - summary_rows.append({ - 'row_index': row_index, - 'label': label, - 'sum': summary_value, - }) - - # Get cells for bottom tables - cells_by_table = self.get_cells_by_table(sheet) - - context.update({ - 'sheet': sheet, - 'clients': clients, - 'year': year, - 'month': month, - 'month_name': calendar.month_name[month], - 'prev_month': self.get_prev_month(year, month), - 'next_month': self.get_next_month(year, month), - 'cells_by_table': cells_by_table, - 'top_left_headers': TOP_LEFT_CLIENTS + ['Sum'], - 'top_right_headers': TOP_RIGHT_CLIENTS + ['Sum'], - 'top_left_rows': top_left_rows, - 'top_right_rows': top_right_rows, - 'summary_rows': summary_rows, # 👈 NEW - 'is_start_sheet': is_start_sheet, - }) - return context - - def get_cells_by_table(self, sheet): - """Organize cells by table type for easy template rendering""" - cells = sheet.cells.select_related('client').all() - organized = { - 'top_left': [[] for _ in range(16)], - 'top_right': [[] for _ in range(16)], # now 16 rows - 'bottom_1': [[] for _ in range(10)], - 'bottom_2': [[] for _ in range(10)], - 'bottom_3': [[] for _ in range(10)], - } - - for cell in cells: - if cell.table_type not in organized: - continue - - max_rows = len(organized[cell.table_type]) - if cell.row_index < 0 or cell.row_index >= max_rows: - # This is an "extra" cell from an older layout (e.g. top_left rows 18–23) - continue - - row_list = organized[cell.table_type][cell.row_index] - while len(row_list) <= cell.column_index: - row_list.append(None) - - row_list[cell.column_index] = cell - - return organized - - - def initialize_sheet_cells(self, sheet, clients): - """Create all empty cells for a new monthly sheet""" - cells_to_create = [] - summation_config = CALCULATION_CONFIG.get('summation_column', {}) - sum_column_index = summation_config.get('sum_column_index', len(clients) - 1) # Last column - - # For each table type and row - table_configs = [ - ('top_left', 16), - ('top_right', 16), - ('bottom_1', 10), - ('bottom_2', 10), - ('bottom_3', 10), - ] - - for table_type, row_count in table_configs: - for row_idx in range(row_count): - for col_idx, client in enumerate(clients): - is_summation = (col_idx == sum_column_index) - cells_to_create.append(Cell( - sheet=sheet, - client=client, - table_type=table_type, - row_index=row_idx, - column_index=col_idx, - value=None, - is_formula=is_summation, # Mark summation cells as formulas - )) - - # Bulk create all cells at once - Cell.objects.bulk_create(cells_to_create) - def get_prev_month(self, year, month): - """Get previous month year and month""" - if month == 1: - return {'year': year - 1, 'month': 12} - return {'year': year, 'month': month - 1} - - def get_next_month(self, year, month): - """Get next month year and month""" - if month == 12: - return {'year': year + 1, 'month': 1} - return {'year': year, 'month': month + 1} - def apply_previous_month_links(self, sheet, year, month): - """ - For non-start sheets: - B4 (row 2) = previous sheet B3 (row 1) - B10 (row 8) = previous sheet B9 (row 7) - """ - # Do nothing on the first sheet - if year == FIRST_SHEET_YEAR and month == FIRST_SHEET_MONTH: - return - - # Figure out previous month - if month == 1: - prev_year = year - 1 - prev_month = 12 - else: - prev_year = year - prev_month = month - 1 - - from .models import MonthlySheet, Cell, Client - - prev_sheet = MonthlySheet.objects.filter( - year=prev_year, - month=prev_month - ).first() - - if not prev_sheet: - # No previous sheet created yet → nothing to copy - return - - # For each client, copy values - for client in Client.objects.all(): - # B3(prev) → B4(curr): UI row 1 → row 2 → row_index 0 → 1 - prev_b3 = Cell.objects.filter( - sheet=prev_sheet, - table_type='top_left', - client=client, - row_index=0, # UI row 1 - ).first() - - curr_b4 = Cell.objects.filter( - sheet=sheet, - table_type='top_left', - client=client, - row_index=1, # UI row 2 - ).first() - - if prev_b3 and curr_b4: - curr_b4.value = prev_b3.value - curr_b4.save() - - # B9(prev) → B10(curr): UI row 7 → row 8 → row_index 6 → 7 - prev_b9 = Cell.objects.filter( - sheet=prev_sheet, - table_type='top_left', - client=client, - row_index=6, # UI row 7 (Summe Bestand) - ).first() - - curr_b10 = Cell.objects.filter( - sheet=sheet, - table_type='top_left', - client=client, - row_index=7, # UI row 8 (Best. in Kannen Vormonat) - ).first() - - if prev_b9 and curr_b10: - curr_b10.value = prev_b9.value - curr_b10.save() - - def apply_previous_month_links_top_right(self, sheet, year, month): - """ - top_right row 7: Best. in Kannen Vormonat (Lit. L-He) - = previous sheet's Summe Bestand (row 6). - - For merged pairs: - - Dr. Fohrer + AG Buntk. share the SAME value (from previous month's AG Buntk. or Dr. Fohrer) - - AG Alff + AG Gutfl. share the SAME value (from previous month's AG Alff or AG Gutfl.) - M3 clients just copy their own value. - """ - from .models import MonthlySheet, Cell, Client - from decimal import Decimal - - # Do nothing on first sheet - if year == FIRST_SHEET_YEAR and month == FIRST_SHEET_MONTH: - return - - # find previous month - if month == 1: - prev_year = year - 1 - prev_month = 12 - else: - prev_year = year - prev_month = month - 1 - - prev_sheet = MonthlySheet.objects.filter( - year=prev_year, - month=prev_month - ).first() - - if not prev_sheet: - return # nothing to copy from - - TOP_RIGHT_CLIENTS = [ - "Dr. Fohrer", - "AG Buntk.", - "AG Alff", - "AG Gutfl.", - "M3 Thiele", - "M3 Buntkowsky", - "M3 Gutfleisch", - ] - - # Helper function to get a cell value - def get_cell_value(sheet_obj, client_name, row_index): - client_obj = Client.objects.filter(name=client_name).first() - if not client_obj: - return None - - try: - col_idx = TOP_RIGHT_CLIENTS.index(client_name) - except ValueError: - return None - - cell = Cell.objects.filter( - sheet=sheet_obj, - table_type='top_right', - client=client_obj, - row_index=row_index, - column_index=col_idx, - ).first() - - return cell.value if cell else None - - # Helper function to set a cell value - def set_cell_value(sheet_obj, client_name, row_index, value): - client_obj = Client.objects.filter(name=client_name).first() - if not client_obj: - return False - - try: - col_idx = TOP_RIGHT_CLIENTS.index(client_name) - except ValueError: - return False - - cell, created = Cell.objects.get_or_create( - sheet=sheet_obj, - table_type='top_right', - client=client_obj, - row_index=row_index, - column_index=col_idx, - defaults={'value': value} - ) - - if not created and cell.value != value: - cell.value = value - cell.save() - elif created: - cell.save() - - return True - - # ----- Pair 1: Dr. Fohrer + AG Buntk. ----- - # Get previous month's Summe Bestand (row 6) for either client in the pair - pair1_prev_val = None - - # Try AG Buntk. first - prev_buntk_val = get_cell_value(prev_sheet, "AG Buntk.", 6) - if prev_buntk_val is not None: - pair1_prev_val = prev_buntk_val - else: - # Try Dr. Fohrer if AG Buntk. is empty - prev_fohrer_val = get_cell_value(prev_sheet, "Dr. Fohrer", 6) - if prev_fohrer_val is not None: - pair1_prev_val = prev_fohrer_val - - # Apply the value to both clients in the pair - if pair1_prev_val is not None: - set_cell_value(sheet, "Dr. Fohrer", 7, pair1_prev_val) - set_cell_value(sheet, "AG Buntk.", 7, pair1_prev_val) - - # ----- Pair 2: AG Alff + AG Gutfl. ----- - pair2_prev_val = None - - # Try AG Alff first - prev_alff_val = get_cell_value(prev_sheet, "AG Alff", 6) - if prev_alff_val is not None: - pair2_prev_val = prev_alff_val - else: - # Try AG Gutfl. if AG Alff is empty - prev_gutfl_val = get_cell_value(prev_sheet, "AG Gutfl.", 6) - if prev_gutfl_val is not None: - pair2_prev_val = prev_gutfl_val - - # Apply the value to both clients in the pair - if pair2_prev_val is not None: - set_cell_value(sheet, "AG Alff", 7, pair2_prev_val) - set_cell_value(sheet, "AG Gutfl.", 7, pair2_prev_val) - - # ----- M3 clients: copy their own Summe Bestand ----- - for name in ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"]: - prev_val = get_cell_value(prev_sheet, name, 6) - if prev_val is not None: - set_cell_value(sheet, name, 7, prev_val) -# Add this helper function to views.py -def get_factor_value(table_type, row_index): - """Get factor value (like 0.06 for top_left row 17)""" - factors = { - ('top_left', 17): Decimal('0.06'), # A18 in Excel (UI row 17, 0-based index 16) - } - return factors.get((table_type, row_index), Decimal('0')) -# Save Cells View -# views.py - Updated SaveCellsView -# views.py - Update SaveCellsView class -def debug_cell_values(self, sheet, client_id): - """Debug method to check cell values""" - from .models import Cell - cells = Cell.objects.filter( - sheet=sheet, - table_type='top_left', - client_id=client_id - ).order_by('row_index') - - debug_info = {} - for cell in cells: - debug_info[f"row_{cell.row_index}"] = { - 'value': str(cell.value) if cell.value else 'None', - 'ui_row': cell.row_index + 1, - 'excel_ref': f"B{cell.row_index + 3}" - } - - return debug_info -class DebugCalculationView(View): - """Debug view to test calculations directly""" - def get(self, request): - sheet_id = request.GET.get('sheet_id', 1) - client_name = request.GET.get('client', 'AG Vogel') - - try: - sheet = MonthlySheet.objects.get(id=sheet_id) - client = Client.objects.get(name=client_name) - - # Get SaveCellsView instance - save_view = SaveCellsView() - - # Create a dummy cell to trigger calculations - dummy_cell = Cell.objects.filter( - sheet=sheet, - table_type='top_left', - client=client, - row_index=0 # B3 - ).first() - - if not dummy_cell: - return JsonResponse({'error': 'No cells found for this client'}) - - # Trigger calculation - updated = save_view.calculate_top_left_dependents(sheet, dummy_cell) - - # Get updated cell values - cells = Cell.objects.filter( - sheet=sheet, - table_type='top_left', - client=client - ).order_by('row_index') - - cell_data = [] - for cell in cells: - cell_data.append({ - 'row_index': cell.row_index, - 'ui_row': cell.row_index + 1, - 'excel_ref': f"B{cell.row_index + 3}", - 'value': str(cell.value) if cell.value else 'None', - 'description': self.get_row_description(cell.row_index) - }) - - return JsonResponse({ - 'sheet': f"{sheet.year}-{sheet.month:02d}", - 'client': client.name, - 'cells': cell_data, - 'updated_count': len(updated), - 'calculation': 'B5 = IF(B3>0; B3-B4; 0)' - }) - - except Exception as e: - return JsonResponse({'error': str(e)}, status=400) - - def get_row_description(self, row_index): - """Get description for row index""" - descriptions = { - 0: "B3: Stand der Gaszähler (Nm³)", - 1: "B4: Stand der Gaszähler (Vormonat) (Nm³)", - 2: "B5: Gasrückführung (Nm³)", - 3: "B6: Rückführung flüssig (Lit. L-He)", - 4: "B7: Sonderrückführungen (Lit. L-He)", - 5: "B8: Bestand in Kannen-1 (Lit. L-He)", - 6: "B9: Summe Bestand (Lit. L-He)", - 7: "B10: Best. in Kannen Vormonat (Lit. L-He)", - 8: "B11: Bezug (Liter L-He)", - 9: "B12: Rückführ. Soll (Lit. L-He)", - 10: "B13: Verluste (Soll-Rückf.) (Lit. L-He)", - 11: "B14: Füllungen warm (Lit. L-He)", - 12: "B15: Kaltgas Rückgabe (Lit. L-He) – Faktor", - 13: "B16: Faktor", - 14: "B17: Verbraucherverluste (Liter L-He)", - 15: "B18: %" - } - return descriptions.get(row_index, f"Row {row_index}") -def recalculate_stand_der_gaszahler(self, sheet): - """Recalculate Stand der Gaszähler for all client pairs""" - from decimal import Decimal - - # For Dr. Fohrer and AG Buntk. (L & M columns) - try: - # Get Dr. Fohrer's bezug - dr_fohrer_client = Client.objects.get(name="Dr. Fohrer") - dr_fohrer_cell = Cell.objects.filter( - sheet=sheet, - table_type='top_right', - client=dr_fohrer_client, - row_index=9 # Row 9 = Bezug - ).first() - L13 = Decimal(str(dr_fohrer_cell.value)) if dr_fohrer_cell and dr_fohrer_cell.value else Decimal('0') - - # Get AG Buntk.'s bezug - ag_buntk_client = Client.objects.get(name="AG Buntk.") - ag_buntk_cell = Cell.objects.filter( - sheet=sheet, - table_type='top_right', - client=ag_buntk_client, - row_index=9 - ).first() - M13 = Decimal(str(ag_buntk_cell.value)) if ag_buntk_cell and ag_buntk_cell.value else Decimal('0') - - total = L13 + M13 - if total > 0: - # Update Dr. Fohrer's row 0 - dr_fohrer_row0 = Cell.objects.filter( - sheet=sheet, - table_type='top_right', - client=dr_fohrer_client, - row_index=0 - ).first() - if dr_fohrer_row0: - dr_fohrer_row0.value = L13 / total - dr_fohrer_row0.save() - - # Update AG Buntk.'s row 0 - ag_buntk_row0 = Cell.objects.filter( - sheet=sheet, - table_type='top_right', - client=ag_buntk_client, - row_index=0 - ).first() - if ag_buntk_row0: - ag_buntk_row0.value = M13 / total - ag_buntk_row0.save() - except Exception as e: - print(f"Error recalculating Stand der Gaszähler for Dr. Fohrer/AG Buntk.: {e}") - - # For AG Alff and AG Gutfl. (N & O columns) - try: - # Get AG Alff's bezug - ag_alff_client = Client.objects.get(name="AG Alff") - ag_alff_cell = Cell.objects.filter( - sheet=sheet, - table_type='top_right', - client=ag_alff_client, - row_index=9 - ).first() - N13 = Decimal(str(ag_alff_cell.value)) if ag_alff_cell and ag_alff_cell.value else Decimal('0') - - # Get AG Gutfl.'s bezug - ag_gutfl_client = Client.objects.get(name="AG Gutfl.") - ag_gutfl_cell = Cell.objects.filter( - sheet=sheet, - table_type='top_right', - client=ag_gutfl_client, - row_index=9 - ).first() - O13 = Decimal(str(ag_gutfl_cell.value)) if ag_gutfl_cell and ag_gutfl_cell.value else Decimal('0') - - total = N13 + O13 - if total > 0: - # Update AG Alff's row 0 - ag_alff_row0 = Cell.objects.filter( - sheet=sheet, - table_type='top_right', - client=ag_alff_client, - row_index=0 - ).first() - if ag_alff_row0: - ag_alff_row0.value = N13 / total - ag_alff_row0.save() - - # Update AG Gutfl.'s row 0 - ag_gutfl_row0 = Cell.objects.filter( - sheet=sheet, - table_type='top_right', - client=ag_gutfl_client, - row_index=0 - ).first() - if ag_gutfl_row0: - ag_gutfl_row0.value = O13 / total - ag_gutfl_row0.save() - except Exception as e: - print(f"Error recalculating Stand der Gaszähler for AG Alff/AG Gutfl.: {e}") -# In your SaveCellsView class in views.py -class DebugTopRightView(View): - """Debug view to check top_right calculations""" - def get(self, request): - sheet_id = request.GET.get('sheet_id', 1) - client_name = request.GET.get('client', 'Dr. Fohrer') - - try: - sheet = MonthlySheet.objects.get(id=sheet_id) - client = Client.objects.get(name=client_name) - - # Get all cells for this client in top_right - cells = Cell.objects.filter( - sheet=sheet, - table_type='top_right', - client=client - ).order_by('row_index') - - cell_data = [] - descriptions = { - 0: "Stand der Gaszähler (Vormonat)", - 1: "Gasrückführung (Nm³)", - 2: "Rückführung flüssig", - 3: "Sonderrückführungen", - 4: "Sammelrückführungen", - 5: "Bestand in Kannen-1", - 6: "Summe Bestand", - 7: "Best. in Kannen Vormonat", - 8: "Same as row 9 from prev sheet", - 9: "Bezug", - 10: "Rückführ. Soll", - 11: "Verluste", - 12: "Füllungen warm", - 13: "Kaltgas Rückgabe", - 14: "Faktor 0.06", - 15: "Verbraucherverluste", - 16: "%" - } - - for cell in cells: - cell_data.append({ - 'row_index': cell.row_index, - 'ui_row': cell.row_index + 1, - 'description': descriptions.get(cell.row_index, f"Row {cell.row_index}"), - 'value': str(cell.value) if cell.value else 'Empty', - 'cell_id': cell.id - }) - - # Test calculation - row3_cell = cells.filter(row_index=3).first() - row5_cell = cells.filter(row_index=5).first() - row6_cell = cells.filter(row_index=6).first() - - calculation_info = { - 'row3_value': str(row3_cell.value) if row3_cell and row3_cell.value else '0', - 'row5_value': str(row5_cell.value) if row5_cell and row5_cell.value else '0', - 'row6_value': str(row6_cell.value) if row6_cell and row6_cell.value else '0', - 'expected_sum': '0' - } - - if row3_cell and row5_cell and row6_cell: - row3_val = Decimal(str(row3_cell.value)) if row3_cell.value else Decimal('0') - row5_val = Decimal(str(row5_cell.value)) if row5_cell.value else Decimal('0') - expected = row3_val + row5_val - calculation_info['expected_sum'] = str(expected) - calculation_info['is_correct'] = row6_cell.value == expected - - return JsonResponse({ - 'sheet': f"{sheet.year}-{sheet.month}", - 'client': client.name, - 'cells': cell_data, - 'calculation': calculation_info - }) - - except Exception as e: - return JsonResponse({'error': str(e)}, status=400) - - -class SaveCellsView(View): - - def calculate_bottom_3_dependents(self, sheet): - updated_cells = [] - - def get_cell(row_idx, col_idx): - return Cell.objects.filter( - sheet=sheet, - table_type='bottom_3', - row_index=row_idx, - column_index=col_idx - ).first() - - def set_cell(row_idx, col_idx, value): - cell, created = Cell.objects.get_or_create( - sheet=sheet, - table_type='bottom_3', - row_index=row_idx, - column_index=col_idx, - defaults={'value': value} - ) - if not created and cell.value != value: - cell.value = value - cell.save() - elif created: - cell.save() - - updated_cells.append({ - 'id': cell.id, - 'value': str(cell.value) if cell.value is not None else '', - 'is_calculated': True, - }) - - def dec(x): - if x in (None, ''): - return Decimal('0') - try: - return Decimal(str(x)) - except Exception: - return Decimal('0') - - # ---- current summary ---- - cur_sum = MonthlySummary.objects.filter(sheet=sheet).first() - curr_k44 = dec(cur_sum.gesamtbestand_neu_lhe) if cur_sum else Decimal('0') - total_verb = dec(cur_sum.verbraucherverlust_lhe) if cur_sum else Decimal('0') - - # ---- previous month summary ---- - year, month = sheet.year, sheet.month - if month == 1: - prev_year, prev_month = year - 1, 12 - else: - prev_year, prev_month = year, month - 1 - - prev_sheet = MonthlySheet.objects.filter(year=prev_year, month=prev_month).first() - if prev_sheet: - prev_sum = MonthlySummary.objects.filter(sheet=prev_sheet).first() - prev_k44 = dec(prev_sum.gesamtbestand_neu_lhe) if prev_sum else Decimal('0') - else: - prev_k44 = Decimal('0') - - # ---- read editable inputs from bottom_3: F47,G47,I47,I50 ---- - def get_val(r, c): - cell = get_cell(r, c) - return dec(cell.value if cell else None) - - f47 = get_val(1, 0) - g47 = get_val(1, 1) - i47 = get_val(1, 2) - i50 = get_val(4, 2) - - # Now apply your formulas using prev_k44, curr_k44, total_verb, f47,g47,i47,i50 - # Row indices: 0..7 correspond to 46..53 - # col 3 = J, col 4 = K - - # Row 46 - k46 = prev_k44 - j46 = k46 * Decimal('0.75') - set_cell(0, 3, j46) - set_cell(0, 4, k46) - - # Row 47 - g47 = self._dec((get_cell(1, 1) or {}).value if get_cell(1, 1) else None) - i47 = self._dec((get_cell(1, 2) or {}).value if get_cell(1, 2) else None) - - j47 = g47 + i47 - k47 = (j47 / Decimal('0.75')) + g47 if j47 != 0 else g47 - - set_cell(1, 3, j47) - set_cell(1, 4, k47) - - # Row 48 - k48 = k46 + k47 - j48 = k48 * Decimal('0.75') - set_cell(2, 3, j48) - set_cell(2, 4, k48) - - # Row 49 - k49 = curr_k44 - j49 = k49 * Decimal('0.75') - set_cell(3, 3, j49) - set_cell(3, 4, k49) - - # Row 50 - j50 = i50 - k50 = j50 / Decimal('0.75') if j50 != 0 else Decimal('0') - set_cell(4, 3, j50) - set_cell(4, 4, k50) - - # Row 51 - k51 = k48 - k49 - k50 - j51 = k51 * Decimal('0.75') - set_cell(5, 3, j51) - set_cell(5, 4, k51) - - # Row 52 - k52 = total_verb - j52 = k52 * Decimal('0.75') - set_cell(6, 3, j52) - set_cell(6, 4, k52) - - # Row 53 - j53 = j51 - j52 - k53 = k51 - k52 - set_cell(7, 3, j53) - set_cell(7, 4, k53) - - return updated_cells - - - def _dec(self, value): - """Convert value to Decimal or return 0.""" - if value is None or value == '': - return Decimal('0') - try: - return Decimal(str(value)) - except Exception: - return Decimal('0') - - def post(self, request, *args, **kwargs): - """ - Handle AJAX saves from monthly_sheet.html - - Single-cell save: when cell_id is present (blur on one cell) - - Bulk save: when the 'Save All Cells' button is used (no cell_id) - """ - try: - sheet_id = request.POST.get('sheet_id') - if not sheet_id: - return JsonResponse({ - 'status': 'error', - 'message': 'Missing sheet_id' - }) - - sheet = MonthlySheet.objects.get(id=sheet_id) - - # -------- Single-cell update (blur) -------- - cell_id = request.POST.get('cell_id') - if cell_id: - value_raw = (request.POST.get('value') or '').strip() - - try: - cell = Cell.objects.get(id=cell_id, sheet=sheet) - except Cell.DoesNotExist: - return JsonResponse({ - 'status': 'error', - 'message': 'Cell not found' - }) - - # Convert value to Decimal or None - if value_raw == '': - new_value = None - else: - try: - # Allow comma or dot - value_clean = value_raw.replace(',', '.') - new_value = Decimal(value_clean) - except (InvalidOperation, ValueError): - # If conversion fails, treat as empty - new_value = None - - old_value = cell.value - cell.value = new_value - cell.save() - - updated_cells = [{ - 'id': cell.id, - 'value': '' if cell.value is None else str(cell.value), - 'is_calculated': cell.is_formula, # model field - }] - - # Recalculate dependents depending on table_type - if cell.table_type == 'top_left': - updated_cells.extend( - self.calculate_top_left_dependents(sheet, cell) - ) - elif cell.table_type == 'top_right': - updated_cells.extend( - self.calculate_top_right_dependents(sheet, cell) - ) - elif cell.table_type == 'bottom_1': - updated_cells.extend(self.calculate_bottom_1_dependents(sheet, cell)) - elif cell.table_type == 'bottom_3': - updated_cells.extend( - self.calculate_bottom_3_dependents(sheet) - ) - # bottom_1 / bottom_2 / bottom_3 currently have no formulas: - # they just save the new value. - updated_cells += self.calculate_bottom_3_dependents(sheet) - return JsonResponse({ - 'status': 'success', - 'updated_cells': updated_cells - }) - - - # -------- Bulk save (Save All button) -------- - return self.save_bulk_cells(request, sheet) - - except MonthlySheet.DoesNotExist: - return JsonResponse({ - 'status': 'error', - 'message': 'Sheet not found' - }) - except Exception as e: - # Generic safety net so the frontend sees an error message - return JsonResponse({ - 'status': 'error', - 'message': str(e) - }) - - # ... your other methods above ... - - # Update the calculate_top_right_dependents method in SaveCellsView class - - def calculate_top_right_dependents(self, sheet, changed_cell): - """ - Recalculate all dependent cells in the top-right table according to Excel formulas. - - Excel rows (4-20) -> 0-based indices (0-15) - Rows: - 0: Stand der Gaszähler (Vormonat) (Nm³) - shares for M3 clients - 1: Gasrückführung (Nm³) - 2: Rückführung flüssig (Lit. L-He) - 3: Sonderrückführungen (Lit. L-He) - editable - 4: Sammelrückführungen (Lit. L-He) - from helium_input groups - 5: Bestand in Kannen-1 (Lit. L-He) - editable, merged in pairs - 6: Summe Bestand (Lit. L-He) = row 5 - 7: Best. in Kannen Vormonat (Lit. L-He) - from previous month - 8: Bezug (Liter L-He) - from SecondTableEntry - 9: Rückführ. Soll (Lit. L-He) - calculated - 10: Verluste (Soll-Rückf.) (Lit. L-He) - calculated - 11: Füllungen warm (Lit. L-He) - from SecondTableEntry warm outputs - 12: Kaltgas Rückgabe (Lit. L-He) – Faktor - calculated - 13: Faktor 0.06 - fixed - 14: Verbraucherverluste (Liter L-He) - calculated - 15: % - calculated - """ - from decimal import Decimal - from django.db.models import Sum, Count - from django.db.models.functions import Coalesce - from .models import Client, Cell, ExcelEntry, SecondTableEntry - - TOP_RIGHT_CLIENTS = [ - "Dr. Fohrer", # L - "AG Buntk.", # M (merged with L) - "AG Alff", # N - "AG Gutfl.", # O (merged with N) - "M3 Thiele", # P - "M3 Buntkowsky", # Q - "M3 Gutfleisch", # R - ] - - # Define merged pairs - MERGED_PAIRS = [ - ("Dr. Fohrer", "AG Buntk."), # L and M are merged - ("AG Alff", "AG Gutfl."), # N and O are merged - ] - - # M3 clients (not merged, calculated individually) - M3_CLIENTS = ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"] - - # Groups for Sammelrückführungen (helium_input) - HELIUM_INPUT_GROUPS = { - "fohrer_buntk": ["Dr. Fohrer", "AG Buntk."], - "alff_gutfl": ["AG Alff", "AG Gutfl."], - "m3": ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"], - } - - year = sheet.year - month = sheet.month - factor = Decimal('0.06') # Fixed factor from Excel - updated_cells = [] - - # Helper functions - def get_val(client_name, row_idx): - """Get cell value for a client and row""" - try: - client = Client.objects.get(name=client_name) - col_idx = TOP_RIGHT_CLIENTS.index(client_name) - cell = Cell.objects.filter( - sheet=sheet, - table_type='top_right', - client=client, - row_index=row_idx, - column_index=col_idx - ).first() - if cell and cell.value is not None: - return Decimal(str(cell.value)) - except (Client.DoesNotExist, ValueError, KeyError): - pass - return Decimal('0') - - def set_val(client_name, row_idx, value, is_calculated=True): - """Set cell value for a client and row""" - try: - client = Client.objects.get(name=client_name) - col_idx = TOP_RIGHT_CLIENTS.index(client_name) - - cell, created = Cell.objects.get_or_create( - sheet=sheet, - table_type='top_right', - client=client, - row_index=row_idx, - column_index=col_idx, - defaults={'value': value} - ) - - # Only update if value changed - if not created and cell.value != value: - cell.value = value - cell.save() - elif created: - cell.save() - - updated_cells.append({ - 'id': cell.id, - 'value': str(cell.value) if cell.value else '', - 'is_calculated': is_calculated - }) - - return True - except (Client.DoesNotExist, ValueError): - return False - - # 1. Update Summe Bestand (row 6) from Bestand in Kannen-1 (row 5) - # For merged pairs: copy value from changed cell to its pair - if changed_cell and changed_cell.table_type == 'top_right': - if changed_cell.row_index == 5: # Bestand in Kannen-1 - client_name = changed_cell.client.name - new_value = changed_cell.value - - # Check if this client is in a merged pair - for pair in MERGED_PAIRS: - if client_name in pair: - # Update both clients in the pair - for client_in_pair in pair: - if client_in_pair != client_name: - set_val(client_in_pair, 5, new_value, is_calculated=False) - break - - # 2. For all clients: Set Summe Bestand (row 6) = Bestand in Kannen-1 (row 5) - for client_name in TOP_RIGHT_CLIENTS: - bestand_value = get_val(client_name, 5) - set_val(client_name, 6, bestand_value, is_calculated=True) - - # 3. Update Sammelrückführungen (row 4) from helium_input groups - for group_name, client_names in HELIUM_INPUT_GROUPS.items(): - # Get total helium_input for this group - clients_in_group = Client.objects.filter(name__in=client_names) - total_helium = ExcelEntry.objects.filter( - client__in=clients_in_group, - date__year=year, - date__month=month - ).aggregate(total=Coalesce(Sum('lhe_ges'), Decimal('0')))['total'] - - # Set same value for all clients in group - for client_name in client_names: - set_val(client_name, 4, total_helium, is_calculated=True) - - # 4. Calculate Rückführung flüssig (row 2) - # For merged pairs: =L8 for Dr. Fohrer/AG Buntk., =N8 for AG Alff/AG Gutfl. - # For M3 clients: =$P$8 * P4, $P$8 * Q4, $P$8 * R4 - - # Get Sammelrückführungen values for groups - sammel_fohrer_buntk = get_val("Dr. Fohrer", 4) # L8 - sammel_alff_gutfl = get_val("AG Alff", 4) # N8 - sammel_m3_group = get_val("M3 Thiele", 4) # P8 (same for all M3) - - # For merged pairs - set_val("Dr. Fohrer", 2, sammel_fohrer_buntk, is_calculated=True) - set_val("AG Buntk.", 2, sammel_fohrer_buntk, is_calculated=True) - set_val("AG Alff", 2, sammel_alff_gutfl, is_calculated=True) - set_val("AG Gutfl.", 2, sammel_alff_gutfl, is_calculated=True) - - # For M3 clients: =$P$8 * column4 (Stand der Gaszähler) - for m3_client in M3_CLIENTS: - stand_value = get_val(m3_client, 0) # Stand der Gaszähler (row 0) - rueck_value = sammel_m3_group * stand_value - set_val(m3_client, 2, rueck_value, is_calculated=True) - - # 5. Calculate Füllungen warm (row 11) from SecondTableEntry warm outputs - # 5. Calculate Füllungen warm (row 11) as NUMBER of warm fillings - # (sum of 1s where each warm SecondTableEntry is one filling) - for client_name in TOP_RIGHT_CLIENTS: - client = Client.objects.get(name=client_name) - warm_count = SecondTableEntry.objects.filter( - client=client, - date__year=year, - date__month=month, - is_warm=True - ).aggregate( - total=Coalesce(Count('id'), 0) - )['total'] - - # store as Decimal so later formulas (warm * 15) still work nicely - warm_value = Decimal(warm_count) - set_val(client_name, 11, warm_value, is_calculated=True) - # 6. Set Faktor row (13) to 0.06 - for client_name in TOP_RIGHT_CLIENTS: - set_val(client_name, 13, factor, is_calculated=True) - - # 6a. Recalculate Stand der Gaszähler (row 0) for the merged pairs - # according to Excel: - # L4 = L13 / (L13 + M13), M4 = M13 / (L13 + M13) - # N4 = N13 / (N13 + O13), O4 = O13 / (N13 + O13) - - # Pair 1: Dr. Fohrer / AG Buntk. - bezug_dr = get_val("Dr. Fohrer", 8) # L13 - bezug_buntk = get_val("AG Buntk.", 8) # M13 - total_pair1 = bezug_dr + bezug_buntk - - if total_pair1 != 0: - set_val("Dr. Fohrer", 0, bezug_dr / total_pair1, is_calculated=True) - set_val("AG Buntk.", 0, bezug_buntk / total_pair1, is_calculated=True) - else: - # if no Bezug, both shares are 0 - set_val("Dr. Fohrer", 0, Decimal('0'), is_calculated=True) - set_val("AG Buntk.", 0, Decimal('0'), is_calculated=True) - - # Pair 2: AG Alff / AG Gutfl. - bezug_alff = get_val("AG Alff", 8) # N13 - bezug_gutfl = get_val("AG Gutfl.", 8) # O13 - total_pair2 = bezug_alff + bezug_gutfl - - if total_pair2 != 0: - set_val("AG Alff", 0, bezug_alff / total_pair2, is_calculated=True) - set_val("AG Gutfl.", 0, bezug_gutfl / total_pair2, is_calculated=True) - else: - set_val("AG Alff", 0, Decimal('0'), is_calculated=True) - set_val("AG Gutfl.", 0, Decimal('0'), is_calculated=True) - - - # 7. Calculate all other dependent rows for merged pairs - for pair in MERGED_PAIRS: - client1, client2 = pair - - # Get values for the pair - bezug1 = get_val(client1, 8) # Bezug client1 - bezug2 = get_val(client2, 8) # Bezug client2 - total_bezug = bezug1 + bezug2 # L13+M13 or N13+O13 - - summe_bestand = get_val(client1, 6) # L11 or N11 (merged, same value) - best_vormonat = get_val(client1, 7) # L12 or N12 (merged, same value) - rueck_fl = get_val(client1, 2) # L6 or N6 (merged, same value) - warm1 = get_val(client1, 11) # L16 or N16 - warm2 = get_val(client2, 11) # M16 or O16 - total_warm = warm1 + warm2 # L16+M16 or N16+O16 - - # Calculate Rückführ. Soll (row 9) - # = L13+M13 - L11 + L12 for first pair - # = N13+O13 - N11 + N12 for second pair - rueck_soll = total_bezug - summe_bestand + best_vormonat - set_val(client1, 9, rueck_soll, is_calculated=True) - set_val(client2, 9, rueck_soll, is_calculated=True) - - # Calculate Verluste (row 10) = Rückführ. Soll - Rückführung flüssig - verluste = rueck_soll - rueck_fl - set_val(client1, 10, verluste, is_calculated=True) - set_val(client2, 10, verluste, is_calculated=True) - - # Calculate Kaltgas Rückgabe (row 12) - # = (L13+M13)*$A18 + (L16+M16)*15 - kaltgas = (total_bezug * factor) + (total_warm * Decimal('15')) - set_val(client1, 12, kaltgas, is_calculated=True) - set_val(client2, 12, kaltgas, is_calculated=True) - - # Calculate Verbraucherverluste (row 14) = Verluste - Kaltgas - verbrauch = verluste - kaltgas - set_val(client1, 14, verbrauch, is_calculated=True) - set_val(client2, 14, verbrauch, is_calculated=True) - - # Calculate % (row 15) = Verbraucherverluste / (L13+M13) - if total_bezug != 0: - prozent = verbrauch / total_bezug - else: - prozent = Decimal('0') - set_val(client1, 15, prozent, is_calculated=True) - set_val(client2, 15, prozent, is_calculated=True) - - # 8. Calculate all dependent rows for M3 clients (individual calculations) - for m3_client in M3_CLIENTS: - # Get individual values - bezug = get_val(m3_client, 8) # Bezug for this M3 client - summe_bestand = get_val(m3_client, 6) # Summe Bestand - best_vormonat = get_val(m3_client, 7) # Best. in Kannen Vormonat - rueck_fl = get_val(m3_client, 2) # Rückführung flüssig - warm = get_val(m3_client, 11) # Füllungen warm - - # Calculate Rückführ. Soll (row 9) = Bezug - Summe Bestand + Best. Vormonat - rueck_soll = bezug - summe_bestand + best_vormonat - set_val(m3_client, 9, rueck_soll, is_calculated=True) - - # Calculate Verluste (row 10) = Rückführ. Soll - Rückführung flüssig - verluste = rueck_soll - rueck_fl - set_val(m3_client, 10, verluste, is_calculated=True) - - # Calculate Kaltgas Rückgabe (row 12) = Bezug * factor + warm * 15 - kaltgas = (bezug * factor) + (warm * Decimal('15')) - set_val(m3_client, 12, kaltgas, is_calculated=True) - - # Calculate Verbraucherverluste (row 14) = Verluste - Kaltgas - verbrauch = verluste - kaltgas - set_val(m3_client, 14, verbrauch, is_calculated=True) - - # Calculate % (row 15) = Verbraucherverluste / Bezug - if bezug != 0: - prozent = verbrauch / bezug - else: - prozent = Decimal('0') - set_val(m3_client, 15, prozent, is_calculated=True) - - return updated_cells - - - - def calculate_bottom_1_dependents(self, sheet, changed_cell): - """ - Recalculate Bottom Table 1 (table_type='bottom_1'). - - Layout (row_index 0–9, col_index 0–4): - - Rows 0–8: - 0: Batterie 1 - 1: 2 - 2: 3 - 3: 4 - 4: 5 - 5: 6 - 6: 2 Bündel - 7: 2 Ballone - 8: Reingasspeicher - - Row 9: - 9: Gasbestand (totals row) - - Columns: - 0: Volumen (fixed values, not editable) - 1: bar (editable for rows 0–8) - 2: korrigiert = bar / (bar/2000 + 1) - 3: Nm³ = Volumen * korrigiert - 4: Lit. LHe = Nm³ / 0.75 - - Row 9: - - Volumen (col 0): empty - - bar (col 1): empty - - korrigiert (col 2): empty - - Nm³ (col 3): SUM Nm³ rows 0–8 - - Lit. LHe (col 4): SUM Lit. LHe rows 0–8 - """ - from decimal import Decimal, InvalidOperation - from .models import Cell - - updated_cells = [] - - DATA_ROWS = list(range(0, 9)) # 0–8 - TOTAL_ROW = 9 - - COL_VOL = 0 - COL_BAR = 1 - COL_KORR = 2 - COL_NM3 = 3 - COL_LHE = 4 - - # Fixed Volumen values for rows 0–8 - VOLUMES = [ - Decimal("2.4"), # row 0 - Decimal("5.1"), # row 1 - Decimal("4.0"), # row 2 - Decimal("1.0"), # row 3 - Decimal("4.0"), # row 4 - Decimal("0.4"), # row 5 - Decimal("1.2"), # row 6 - Decimal("20.0"), # row 7 - Decimal("5.0"), # row 8 - ] - - def get_cell(row_idx, col_idx): - return Cell.objects.filter( - sheet=sheet, - table_type="bottom_1", - row_index=row_idx, - column_index=col_idx, - ).first() - - def set_calc(row_idx, col_idx, value): - """ - Set a calculated cell and add it to updated_cells. - If value is None, we clear the cell. - """ - cell = get_cell(row_idx, col_idx) - if not cell: - cell = Cell( - sheet=sheet, - table_type="bottom_1", - row_index=row_idx, - column_index=col_idx, - value=value, - ) - else: - cell.value = value - cell.save() - - updated_cells.append( - { - "id": cell.id, - "value": "" if cell.value is None else str(cell.value), - "is_calculated": True, - } - ) - - # ---------- Rows 0–8: per-gasspeicher calculations ---------- - for row_idx in DATA_ROWS: - bar_cell = get_cell(row_idx, COL_BAR) - - # Volumen: fixed constant or, if present, value from DB - vol = VOLUMES[row_idx] - vol_cell = get_cell(row_idx, COL_VOL) - if vol_cell and vol_cell.value is not None: - try: - vol = Decimal(str(vol_cell.value)) - except (InvalidOperation, ValueError): - # fall back to fixed constant - vol = VOLUMES[row_idx] - - bar = None - try: - if bar_cell and bar_cell.value is not None: - bar = Decimal(str(bar_cell.value)) - except (InvalidOperation, ValueError): - bar = None - - # korrigiert = bar / (bar/2000 + 1) - if bar is not None and bar != 0: - korr = bar / (bar / Decimal("2000") + Decimal("1")) - else: - korr = None - - # Nm³ = Volumen * korrigiert - if korr is not None: - nm3 = vol * korr - else: - nm3 = None - - # Lit. LHe = Nm³ / 0.75 - if nm3 is not None: - lit_lhe = nm3 / Decimal("0.75") - else: - lit_lhe = None - - # Write calculated cells back (NOT Volumen or bar) - set_calc(row_idx, COL_KORR, korr) - set_calc(row_idx, COL_NM3, nm3) - set_calc(row_idx, COL_LHE, lit_lhe) - - # ---------- Row 9: totals (Gasbestand) ---------- - total_nm3 = Decimal("0") - total_lhe = Decimal("0") - has_nm3 = False - has_lhe = False - - for row_idx in DATA_ROWS: - nm3_cell = get_cell(row_idx, COL_NM3) - if nm3_cell and nm3_cell.value is not None: - try: - total_nm3 += Decimal(str(nm3_cell.value)) - has_nm3 = True - except (InvalidOperation, ValueError): - pass - - lhe_cell = get_cell(row_idx, COL_LHE) - if lhe_cell and lhe_cell.value is not None: - try: - total_lhe += Decimal(str(lhe_cell.value)) - has_lhe = True - except (InvalidOperation, ValueError): - pass - - # Volumen (0), bar (1), korrigiert (2) on total row stay empty - set_calc(TOTAL_ROW, COL_KORR, None) # explicitly clear korrigiert - set_calc(TOTAL_ROW, COL_NM3, total_nm3 if has_nm3 else None) - set_calc(TOTAL_ROW, COL_LHE, total_lhe if has_lhe else None) - - return updated_cells - - - - - - def calculate_top_left_dependents(self, sheet, changed_cell): - """Calculate dependent cells in top_left table""" - from decimal import Decimal - from django.db.models import Sum - from django.db.models.functions import Coalesce - - client_id = changed_cell.client_id - updated_cells = [] - - # Get all cells for this client in top_left table - client_cells = Cell.objects.filter( - sheet=sheet, - table_type='top_left', - client_id=client_id - ) - - # Create a dict for easy access - cell_dict = {} - for cell in client_cells: - cell_dict[cell.row_index] = cell - - # Get values with safe defaults - def get_cell_value(row_idx): - cell = cell_dict.get(row_idx) - if cell and cell.value is not None: - try: - return Decimal(str(cell.value)) - except: - return Decimal('0') - return Decimal('0') - - # 1. B5 = IF(B3>0; B3-B4; 0) - Gasrückführung - b3 = get_cell_value(0) # row_index 0 = Excel B3 (UI row 1) - b4 = get_cell_value(1) # row_index 1 = Excel B4 (UI row 2) - - # Calculate B5 - if b3 > 0: - b5_value = b3 - b4 - if b5_value < 0: - b5_value = Decimal('0') - else: - b5_value = Decimal('0') - - # Update B5 - FORCE update even if value appears the same - b5_cell = cell_dict.get(2) # row_index 2 = Excel B5 (UI row 3) - if b5_cell: - # Always update B5 when B3 or B4 changes - b5_cell.value = b5_value - b5_cell.save() - updated_cells.append({ - 'id': b5_cell.id, - 'value': str(b5_cell.value), - 'is_calculated': True - }) - - # 2. B6 = B5 / 0.75 - Rückführung flüssig - b6_value = b5_value / Decimal('0.75') - b6_cell = cell_dict.get(3) # row_index 3 = Excel B6 (UI row 4) - if b6_cell: - b6_cell.value = b6_value - b6_cell.save() - updated_cells.append({ - 'id': b6_cell.id, - 'value': str(b6_cell.value), - 'is_calculated': True - }) - - # 3. B9 = B7 + B8 - Summe Bestand - b7 = get_cell_value(4) # row_index 4 = Excel B7 (UI row 5) - b8 = get_cell_value(5) # row_index 5 = Excel B8 (UI row 6) - b9_value = b7 + b8 - b9_cell = cell_dict.get(6) # row_index 6 = Excel B9 (UI row 7) - if b9_cell: - b9_cell.value = b9_value - b9_cell.save() - updated_cells.append({ - 'id': b9_cell.id, - 'value': str(b9_cell.value), - 'is_calculated': True - }) - - # 4. B11 = Sum of LHe Output from SecondTableEntry for this client/month - Bezug - from .models import SecondTableEntry - client = changed_cell.client - - # Calculate total LHe output for this client in this month - # 4. B11 = Bezug (Liter L-He) - # For start sheet: manual entry, for other sheets: auto-calculated from SecondTableEntry - from .models import SecondTableEntry - client = changed_cell.client - - b11_cell = cell_dict.get(8) # row_index 8 = Excel B11 (UI row 9) - - # Check if this is the start sheet (2025-01) - is_start_sheet = (sheet.year == 2025 and sheet.month == 1) - - # Get the B11 value for calculations - b11_value = Decimal('0') - - if is_start_sheet: - # For start sheet, keep whatever value is already there (manual entry) - if b11_cell and b11_cell.value is not None: - b11_value = Decimal(str(b11_cell.value)) - else: - # For non-start sheets: auto-calculate from SecondTableEntry - lhe_output_sum = SecondTableEntry.objects.filter( - client=client, - date__year=sheet.year, - date__month=sheet.month - ).aggregate( - total=Coalesce(Sum('lhe_output'), Decimal('0')) - )['total'] - - b11_value = lhe_output_sum - - if b11_cell and (b11_cell.value != b11_value or b11_cell.value is None): - b11_cell.value = b11_value - b11_cell.save() - updated_cells.append({ - 'id': b11_cell.id, - 'value': str(b11_cell.value), - 'is_calculated': True # Calculated from SecondTableEntry - }) - - # 5. B12 = B11 + B10 - B9 - Rückführ. Soll - b10 = get_cell_value(7) # row_index 7 = Excel B10 (UI row 8) - b12_value = b11_value + b10 - b9_value # Use b11_value instead of lhe_output_sum - b12_cell = cell_dict.get(9) # row_index 9 = Excel B12 (UI row 10) - if b12_cell: - b12_cell.value = b12_value - b12_cell.save() - updated_cells.append({ - 'id': b12_cell.id, - 'value': str(b12_cell.value), - 'is_calculated': True - }) - - # 6. B13 = B12 - B6 - Verluste (Soll-Rückf.) - b13_value = b12_value - b6_value - b13_cell = cell_dict.get(10) # row_index 10 = Excel B13 (UI row 11) - if b13_cell: - b13_cell.value = b13_value - b13_cell.save() - updated_cells.append({ - 'id': b13_cell.id, - 'value': str(b13_cell.value), - 'is_calculated': True - }) - - # 7. B14 = Count of warm outputs - warm_count = SecondTableEntry.objects.filter( - client=client, - date__year=sheet.year, - date__month=sheet.month, - is_warm=True - ).count() - - b14_cell = cell_dict.get(11) # row_index 11 = Excel B14 (UI row 12) - if b14_cell: - b14_cell.value = Decimal(str(warm_count)) - b14_cell.save() - updated_cells.append({ - 'id': b14_cell.id, - 'value': str(b14_cell.value), - 'is_calculated': True - }) - - # 8. B15 = IF(B11>0; B11 * factor + B14 * 15; 0) - Kaltgas Rückgabe - factor = get_cell_value(13) # row_index 13 = Excel B16 (Faktor) (UI row 14) - if factor == 0: - factor = Decimal('0.06') # default factor - - if b11_value > 0: # Use b11_value - b15_value = b11_value * factor + Decimal(str(warm_count)) * Decimal('15') - else: - b15_value = Decimal('0') - - b15_cell = cell_dict.get(12) # row_index 12 = Excel B15 (UI row 13) - if b15_cell: - b15_cell.value = b15_value - b15_cell.save() - updated_cells.append({ - 'id': b15_cell.id, - 'value': str(b15_cell.value), - 'is_calculated': True - }) - - # 9. B17 = B13 - B15 - Verbraucherverluste - b17_value = b13_value - b15_value - b17_cell = cell_dict.get(14) # row_index 14 = Excel B17 (UI row 15) - if b17_cell: - b17_cell.value = b17_value - b17_cell.save() - updated_cells.append({ - 'id': b17_cell.id, - 'value': str(b17_cell.value), - 'is_calculated': True - }) - - # 10. B18 = IF(B11=0; 0; B17/B11) - % - if b11_value == 0: # Use b11_value - b18_value = Decimal('0') - else: - b18_value = b17_value / b11_value # Use b11_value - - b18_cell = cell_dict.get(15) # row_index 15 = Excel B18 (UI row 16) - if b18_cell: - b18_cell.value = b18_value - b18_cell.save() - updated_cells.append({ - 'id': b18_cell.id, - 'value': str(b18_cell.value), - 'is_calculated': True - }) - - return updated_cells - def save_bulk_cells(self, request, sheet): - """Original bulk save logic (for backward compatibility)""" - # Get all cell updates - cell_updates = {} - for key, value in request.POST.items(): - if key.startswith('cell_'): - cell_id = key.replace('cell_', '') - cell_updates[cell_id] = value - - # Update cells and track which ones changed - updated_cells = [] - changed_clients = set() - - for cell_id, new_value in cell_updates.items(): - try: - cell = Cell.objects.get(id=cell_id, sheet=sheet) - old_value = cell.value - - # Convert new value - try: - if new_value.strip(): - cell.value = Decimal(new_value) - else: - cell.value = None - except Exception: - cell.value = None - - # Only save if value changed - if cell.value != old_value: - cell.save() - updated_cells.append({ - 'id': cell.id, - 'value': str(cell.value) if cell.value else '' - }) - # bottom_3 has no client, so this will just add None for those cells, - # which is harmless. Top-left cells still add their real client_id. - changed_clients.add(cell.client_id) - - except Cell.DoesNotExist: - continue - - # Recalculate for each changed client (top-left tables) - for client_id in changed_clients: - if client_id is not None: - self.recalculate_top_left_table(sheet, client_id) - - # --- NEW: recalc bottom_3 for the whole sheet, independent of clients --- - bottom3_updates = self.calculate_bottom_3_dependents(sheet) - - # Get all updated cells for response (top-left) - all_updated_cells = [] - for client_id in changed_clients: - if client_id is None: - continue # skip bottom_3 / non-client cells - client_cells = Cell.objects.filter( - sheet=sheet, - client_id=client_id - ) - for cell in client_cells: - all_updated_cells.append({ - 'id': cell.id, - 'value': str(cell.value) if cell.value else '', - 'is_calculated': cell.is_formula - }) - - # Add bottom_3 recalculated cells (J46..K53, etc.) - all_updated_cells.extend(bottom3_updates) - - return JsonResponse({ - 'status': 'success', - 'message': f'Saved {len(updated_cells)} cells', - 'updated_cells': all_updated_cells - }) - - - def recalculate_top_left_table(self, sheet, client_id): - """Recalculate the top-left table for a specific client""" - from decimal import Decimal - - # Get all cells for this client in top_left table - cells = Cell.objects.filter( - sheet=sheet, - table_type='top_left', - client_id=client_id - ).order_by('row_index') - - # Create a dictionary of cell values - cell_dict = {} - for cell in cells: - cell_dict[cell.row_index] = cell - - # Excel logic implementation for top-left table - # B3 (row_index 0) = Stand der Gaszähler (Nm³) - manual - # B4 (row_index 1) = Stand der Gaszähler (Vormonat) (Nm³) - from previous sheet - - # Get B3 and B4 - b3_cell = cell_dict.get(0) # UI Row 3 - b4_cell = cell_dict.get(1) # UI Row 4 - - if b3_cell and b3_cell.value and b4_cell and b4_cell.value: - # B5 = IF(B3>0; B3-B4; 0) - b3 = Decimal(str(b3_cell.value)) - b4 = Decimal(str(b4_cell.value)) - - if b3 > 0: - b5 = b3 - b4 - if b5 < 0: - b5 = Decimal('0') - else: - b5 = Decimal('0') - - # Update B5 (row_index 2) - b5_cell = cell_dict.get(2) - if b5_cell and (b5_cell.value != b5 or b5_cell.value is None): - b5_cell.value = b5 - b5_cell.save() - - # B6 = B5 / 0.75 (row_index 3) - b6 = b5 / Decimal('0.75') - b6_cell = cell_dict.get(3) - if b6_cell and (b6_cell.value != b6 or b6_cell.value is None): - b6_cell.value = b6 - b6_cell.save() - - # Get previous month's sheet for B10 - if sheet.month == 1: - prev_year = sheet.year - 1 - prev_month = 12 - else: - prev_year = sheet.year - prev_month = sheet.month - 1 - - prev_sheet = MonthlySheet.objects.filter( - year=prev_year, - month=prev_month - ).first() - - if prev_sheet: - # Get B9 from previous sheet (row_index 7 in previous) - prev_b9 = Cell.objects.filter( - sheet=prev_sheet, - table_type='top_left', - client_id=client_id, - row_index=7 # UI Row 9 - ).first() - - if prev_b9 and prev_b9.value: - # Update B10 in current sheet (row_index 8) - b10_cell = cell_dict.get(8) - if b10_cell and (b10_cell.value != prev_b9.value or b10_cell.value is None): - b10_cell.value = prev_b9.value - b10_cell.save() -@method_decorator(csrf_exempt, name='dispatch') -class SaveMonthSummaryView(View): - """ - Saves per-month summary values such as K44 (Gesamtbestand neu). - Called from JS after 'Save All' finishes. - """ - - def post(self, request, *args, **kwargs): - try: - data = json.loads(request.body.decode('utf-8')) - except json.JSONDecodeError: - return JsonResponse( - {'success': False, 'message': 'Invalid JSON'}, - status=400 - ) - - sheet_id = data.get('sheet_id') - if not sheet_id: - return JsonResponse( - {'success': False, 'message': 'Missing sheet_id'}, - status=400 - ) - - try: - sheet = MonthlySheet.objects.get(id=sheet_id) - except MonthlySheet.DoesNotExist: - return JsonResponse( - {'success': False, 'message': 'Sheet not found'}, - status=404 - ) - - # More tolerant decimal conversion: accepts "123.45" and "123,45" - def to_decimal(value): - if value is None: - return None - s = str(value).strip() - if s == '': - return None - s = s.replace(',', '.') - try: - return Decimal(s) - except (InvalidOperation, ValueError): - # Debug: show what failed in the dev server console - print("SaveMonthSummaryView to_decimal failed for:", repr(value)) - return None - - raw_k44 = data.get('gesamtbestand_neu_lhe') - raw_gas = data.get('gasbestand_lhe') - raw_verb = data.get('verbraucherverlust_lhe') - - gesamtbestand_neu_lhe = to_decimal(raw_k44) - gasbestand_lhe = to_decimal(raw_gas) - verbraucherverlust_lhe = to_decimal(raw_verb) - - summary, created = MonthlySummary.objects.get_or_create(sheet=sheet) - - if gesamtbestand_neu_lhe is not None: - summary.gesamtbestand_neu_lhe = gesamtbestand_neu_lhe - if gasbestand_lhe is not None: - summary.gasbestand_lhe = gasbestand_lhe - if verbraucherverlust_lhe is not None: - summary.verbraucherverlust_lhe = verbraucherverlust_lhe - - summary.save() - - # Small debug output so you can see in the server console what was saved - print( - f"Saved MonthlySummary for {sheet.year}-{sheet.month:02d}: " - f"K44={summary.gesamtbestand_neu_lhe}, " - f"Gasbestand={summary.gasbestand_lhe}, " - f"Verbraucherverlust={summary.verbraucherverlust_lhe}" - ) - - return JsonResponse({'success': True}) - - - -# Calculate View (placeholder for calculations) -class CalculateView(View): - def post(self, request): - # This will be implemented when you provide calculation rules - return JsonResponse({ - 'status': 'success', - 'message': 'Calculation endpoint ready' - }) - -# Summary Sheet View -class SummarySheetView(TemplateView): - template_name = 'summary_sheet.html' - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - start_month = int(self.kwargs.get('start_month', 1)) - year = int(self.kwargs.get('year', datetime.now().year)) - - # Get 6 monthly sheets - months = [(year, m) for m in range(start_month, start_month + 6)] - sheets = MonthlySheet.objects.filter( - year=year, - month__in=list(range(start_month, start_month + 6)) - ).order_by('month') - - # Aggregate data across months - summary_data = self.calculate_summary(sheets) - - context.update({ - 'year': year, - 'start_month': start_month, - 'end_month': start_month + 5, - 'sheets': sheets, - 'clients': Client.objects.all(), - 'summary_data': summary_data, - }) - return context - - def calculate_summary(self, sheets): - """Calculate totals across 6 months""" - summary = {} - - for client in Client.objects.all(): - client_total = 0 - for sheet in sheets: - # Get specific cells and sum - cells = sheet.cells.filter( - client=client, - table_type='top_left', - row_index=0 # Example: first row - ) - for cell in cells: - if cell.value: - try: - client_total += float(cell.value) - except (ValueError, TypeError): - continue - - summary[client.id] = client_total - - return summary - -# Existing views below (keep all your existing functions) -def clients_list(request): - # --- Clients for the yearly summary table --- - clients = Client.objects.all() - - # --- Available years for output data (same as before) --- - available_years_qs = SecondTableEntry.objects.dates('date', 'year', order='DESC') - available_years = [y.year for y in available_years_qs] - - # === 1) Year used for the "Helium Output Yearly Summary" table === - # Uses ?year=... in the dropdown - year_param = request.GET.get('year') - if year_param: - selected_year = int(year_param) - else: - selected_year = available_years[0] if available_years else datetime.now().year - - # === 2) GLOBAL half-year interval (shared with other pages) ======= - # Try GET params first - interval_year_param = request.GET.get('interval_year') - start_month_param = request.GET.get('interval_start_month') - - # Fallbacks from session - session_year = request.session.get('halfyear_year') - session_start_month = request.session.get('halfyear_start_month') - - # Determine final interval_year - if interval_year_param: - interval_year = int(interval_year_param) - elif session_year: - interval_year = int(session_year) - else: - # default: same as selected_year for summary - interval_year = selected_year - - # Determine final interval_start_month - if start_month_param: - interval_start_month = int(start_month_param) - elif session_start_month: - interval_start_month = int(session_start_month) - else: - interval_start_month = 1 # default Jan - - # Store back into the session so other views can read them - request.session['halfyear_year'] = interval_year - request.session['halfyear_start_month'] = interval_start_month - - # === 3) Build a 6-month window, allowing wrap into the next year === - # Example: interval_year=2025, interval_start_month=12 - # window = [(2025,12), (2026,1), (2026,2), (2026,3), (2026,4), (2026,5)] - window = [] - for offset in range(6): - total_index = (interval_start_month - 1) + offset # 0-based index - y = interval_year + (total_index // 12) - m = (total_index % 12) + 1 - window.append((y, m)) - - # === 4) Totals per client in that 6-month window ================== - monthly_data = [] - for client in clients: - monthly_totals = [] - for (y, m) in window: - total = SecondTableEntry.objects.filter( - client=client, - date__year=y, - date__month=m - ).aggregate( - total=Coalesce( - Sum('lhe_output'), - Value(0, output_field=DecimalField()) - ) - )['total'] - monthly_totals.append(total) - - monthly_data.append({ - 'client': client, - 'monthly_totals': monthly_totals, - 'year_total': sum(monthly_totals), - }) - - # === 5) Month labels for the header (only the 6-month window) ===== - month_labels = [calendar.month_abbr[m] for (y, m) in window] - - # === 6) FINALLY: return the response ============================== - return render(request, 'clients_table.html', { - 'available_years': available_years, - 'current_year': selected_year, # used by year dropdown - 'interval_year': interval_year, # used by "Global 6-Month Interval" form - 'interval_start_month': interval_start_month, - 'months': month_labels, - 'monthly_data': monthly_data, - }) - - # === 5) Month labels for the header (only the 6-month window) ===== - month_labels = [calendar.month_abbr[m] for (y, m) in window] - - -def set_halfyear_interval(request): - if request.method == 'POST': - year = int(request.POST.get('year')) - start_month = int(request.POST.get('start_month')) - - request.session['halfyear_year'] = year - request.session['halfyear_start_month'] = start_month - - return redirect(request.META.get('HTTP_REFERER', 'clients_list')) - - return redirect('clients_list') -# Table One View (ExcelEntry) -def table_one_view(request): - from .models import ExcelEntry, Client, Institute - - # --- Base queryset for the main Helium Input table --- - base_entries = ExcelEntry.objects.all().select_related('client', 'client__institute') - - # Read the global 6-month interval from the session - interval_year = request.session.get('halfyear_year') - interval_start = request.session.get('halfyear_start_month') - - if interval_year and interval_start: - interval_year = int(interval_year) - interval_start = int(interval_start) - - # Build the same 6-month window as on the main page (can cross year) - window = [] - for offset in range(6): - total_index = (interval_start - 1) + offset # 0-based - y = interval_year + (total_index // 12) - m = (total_index % 12) + 1 - window.append((y, m)) - - # Build Q filter: (year=m_year AND month=m_month) for any of those 6 - q = Q() - for (y, m) in window: - q |= Q(date__year=y, date__month=m) - - entries_table1 = base_entries.filter(q).order_by('-date') - else: - # Fallback: if no global interval yet, show everything - entries_table1 = base_entries.order_by('-date') - clients = Client.objects.all().select_related('institute') - institutes = Institute.objects.all() - - # ---- Overview filters ---- - # years present in ExcelEntry.date - year_qs = ExcelEntry.objects.dates('date', 'year', order='DESC') - available_years = [d.year for d in year_qs] - - # default year/start month - # default year/start month (if no global interval yet) - if available_years: - default_year = available_years[0] # newest year in ExcelEntry - else: - default_year = timezone.now().year - - # 🔸 Read global half-year interval from session (set on main page) - session_year = request.session.get('halfyear_year') - session_start = request.session.get('halfyear_start_month') - - # If the user has set a global interval, use it. - # Otherwise fall back to default year / January. - year = int(session_year) if session_year else int(default_year) - start_month = int(session_start) if session_start else 1 - - - # six-month window - # --- Build a 6-month window, allowing wrap into the next year --- - # Example: year=2025, start_month=10 - # window = [(2025,10), (2025,11), (2025,12), (2026,1), (2026,2), (2026,3)] - window = [] - for offset in range(6): - total_index = (start_month - 1) + offset # 0-based - y_for_month = year + (total_index // 12) - m_for_month = (total_index % 12) + 1 - window.append((y_for_month, m_for_month)) - - overview = None - - if window: - # Build per-group data - groups_entries = [] # for internal calculations - - for key, group in CLIENT_GROUPS.items(): - clients_qs = get_group_clients(key) - - values = [] - group_total = Decimal('0') - - for (y_m, m_m) in window: - total = ExcelEntry.objects.filter( - client__in=clients_qs, - date__year=y_m, - date__month=m_m - ).aggregate( - total=Coalesce(Sum('lhe_ges'), Decimal('0')) - )['total'] - - values.append(total) - group_total += total - - groups_entries.append({ - 'key': key, - 'label': group['label'], - 'values': values, - 'total': group_total, - }) - - # month totals across all groups - month_totals = [] - for idx in range(len(window)): - s = sum((g['values'][idx] for g in groups_entries), Decimal('0')) - month_totals.append(s) - - grand_total = sum(month_totals, Decimal('0')) - - # Build rows for the template - rows = [] - for idx, (y_m, m_m) in enumerate(window): - row_values = [g['values'][idx] for g in groups_entries] - rows.append({ - 'month_number': m_m, - 'month_label': calendar.month_name[m_m], - 'values': row_values, - 'total': month_totals[idx], - }) - - groups_meta = [{'key': g['key'], 'label': g['label']} for g in groups_entries] - group_totals = [g['total'] for g in groups_entries] - - # Start/end for display – include years so wrap is clear - start_year = window[0][0] - start_month_disp = window[0][1] - end_year = window[-1][0] - end_month_disp = window[-1][1] - - overview = { - 'year': year, # keep for backwards compatibility if needed - 'start_month': start_month_disp, - 'start_year': start_year, - 'end_month': end_month_disp, - 'end_year': end_year, - 'rows': rows, - 'groups': groups_meta, - 'group_totals': group_totals, - 'grand_total': grand_total, - } - - - # Month dropdown labels - MONTH_CHOICES = [ - (1, 'Jan'), (2, 'Feb'), (3, 'Mar'), (4, 'Apr'), - (5, 'May'), (6, 'Jun'), (7, 'Jul'), (8, 'Aug'), - (9, 'Sep'), (10, 'Oct'), (11, 'Nov'), (12, 'Dec'), - ] - - return render(request, 'table_one.html', { - 'entries_table1': entries_table1, - 'clients': clients, - 'institutes': institutes, - 'available_years': available_years, - 'month_choices': MONTH_CHOICES, - 'overview': overview, - }) - -# Table Two View (SecondTableEntry) -def table_two_view(request): - try: - clients = Client.objects.all().select_related('institute') - institutes = Institute.objects.all() - - # 🔸 Read global half-year interval from session - interval_year = request.session.get('halfyear_year') - interval_start = request.session.get('halfyear_start_month') - - if interval_year and interval_start: - interval_year = int(interval_year) - interval_start = int(interval_start) - - # Build the same 6-month window as in clients_list (can cross years) - window = [] - for offset in range(6): - total_index = (interval_start - 1) + offset # 0-based - y = interval_year + (total_index // 12) - m = (total_index % 12) + 1 - window.append((y, m)) - - # Build a Q object matching any of those (year, month) pairs - q = Q() - for (y, m) in window: - q |= Q(date__year=y, date__month=m) - - entries = SecondTableEntry.objects.filter(q).order_by('-date') - else: - # Fallback if no global interval yet: show all - entries = SecondTableEntry.objects.all().order_by('-date') - - return render(request, 'table_two.html', { - 'entries_table2': entries, - 'clients': clients, - 'institutes': institutes, - 'interval_year': interval_year, - 'interval_start_month': interval_start, - }) - - except Exception as e: - return render(request, 'table_two.html', { - 'error_message': f"Failed to load data: {str(e)}", - 'entries_table2': [], - 'clients': clients, - 'institutes': institutes, - }) - - -def monthly_sheet_root(request): - """ - Redirect /sheet/ to the sheet matching the globally selected - half-year start (year + month). If not set, fall back to latest. - """ - year = request.session.get('halfyear_year') - start_month = request.session.get('halfyear_start_month') - - if year and start_month: - try: - year = int(year) - start_month = int(start_month) - return redirect('monthly_sheet', year=year, month=start_month) - except ValueError: - pass # fall through - - # Fallback: latest MonthlySheet if exists - latest_sheet = MonthlySheet.objects.order_by('-year', '-month').first() - if latest_sheet: - return redirect('monthly_sheet', year=latest_sheet.year, month=latest_sheet.month) - else: - now = timezone.now() - return redirect('monthly_sheet', year=now.year, month=now.month) -# Add Entry (Generic) -def add_entry(request, model_name): - if request.method == 'POST': - try: - if model_name == 'SecondTableEntry': - model = SecondTableEntry - - # Parse date - date_str = request.POST.get('date') - try: - date_obj = datetime.strptime(date_str, '%Y-%m-%d').date() if date_str else None - except (ValueError, TypeError): - return JsonResponse({ - 'status': 'error', - 'message': 'Invalid date format. Use YYYY-MM-DD' - }, status=400) - - # NEW: robust parse of warm flag (handles 0/1, true/false, on/off) - raw_warm = request.POST.get('is_warm') - is_warm_bool = str(raw_warm).lower() in ('1', 'true', 'on', 'yes') - - # Handle Helium Output (Table Two) - lhe_output = request.POST.get('lhe_output') - - entry = model.objects.create( - client=Client.objects.get(id=request.POST.get('client_id')), - date=date_obj, - is_warm=is_warm_bool, # <-- use the boolean here - lhe_delivery=request.POST.get('lhe_delivery', ''), - lhe_output=Decimal(lhe_output) if lhe_output else None, - notes=request.POST.get('notes', '') - ) - - return JsonResponse({ - 'status': 'success', - 'id': entry.id, - 'client_name': entry.client.name, - 'institute_name': entry.client.institute.name, - 'date': entry.date.strftime('%Y-%m-%d') if entry.date else '', - 'is_warm': entry.is_warm, - 'lhe_delivery': entry.lhe_delivery, - 'lhe_output': str(entry.lhe_output) if entry.lhe_output else '', - 'notes': entry.notes - }) - - elif model_name == 'ExcelEntry': - model = ExcelEntry - - # Parse the date string into a date object - date_str = request.POST.get('date') - try: - date_obj = datetime.strptime(date_str, '%Y-%m-%d').date() if date_str else None - except (ValueError, TypeError): - date_obj = None - - try: - pressure = Decimal(request.POST.get('pressure', 0)) - purity = Decimal(request.POST.get('purity', 0)) - druckkorrektur = Decimal(request.POST.get('druckkorrektur', 1)) - lhe_zus = Decimal(request.POST.get('lhe_zus', 0)) - constant_300 = Decimal(request.POST.get('constant_300', 300)) - korrig_druck = Decimal(request.POST.get('korrig_druck', 0)) - nm3 = Decimal(request.POST.get('nm3', 0)) - lhe = Decimal(request.POST.get('lhe', 0)) - lhe_ges = Decimal(request.POST.get('lhe_ges', 0)) - except InvalidOperation: - return JsonResponse({ - 'status': 'error', - 'message': 'Invalid numeric value in Helium Input' - }, status=400) - - # Create the entry with ALL fields - entry = model.objects.create( - client=Client.objects.get(id=request.POST.get('client_id')), - date=date_obj, - pressure=pressure, - purity=purity, - druckkorrektur=druckkorrektur, - lhe_zus=lhe_zus, - constant_300=constant_300, - korrig_druck=korrig_druck, - nm3=nm3, - lhe=lhe, - lhe_ges=lhe_ges, - notes=request.POST.get('notes', '') - ) - - # Prepare the response - response_data = { - 'status': 'success', - 'id': entry.id, - 'client_name': entry.client.name, - 'institute_name': entry.client.institute.name, - 'pressure': str(entry.pressure), - 'purity': str(entry.purity), - 'druckkorrektur': str(entry.druckkorrektur), - 'constant_300': str(entry.constant_300), - 'korrig_druck': str(entry.korrig_druck), - 'nm3': str(entry.nm3), - 'lhe': str(entry.lhe), - 'lhe_zus': str(entry.lhe_zus), - 'lhe_ges': str(entry.lhe_ges), - 'notes': entry.notes, - } - - if entry.date: - # JS uses this for the Date column and for the Month column - response_data['date'] = entry.date.strftime('%Y-%m-%d') - response_data['month'] = f"{entry.date.month:02d}" - else: - response_data['date'] = '' - response_data['month'] = '' - - return JsonResponse(response_data) - - - except Exception as e: - return JsonResponse({'status': 'error', 'message': str(e)}, status=400) - - return JsonResponse({'status': 'error', 'message': 'Invalid request'}, status=400) - -# Update Entry (Generic) -def update_entry(request, model_name): - if request.method == 'POST': - try: - if model_name == 'SecondTableEntry': - model = SecondTableEntry - elif model_name == 'ExcelEntry': - model = ExcelEntry - else: - return JsonResponse({'status': 'error', 'message': 'Invalid model'}, status=400) - - entry_id = int(request.POST.get('id')) - entry = model.objects.get(id=entry_id) - - # Common updates for both models - entry.client = Client.objects.get(id=request.POST.get('client_id')) - entry.notes = request.POST.get('notes', '') - - # Handle date properly for both models - date_str = request.POST.get('date') - if date_str: - try: - entry.date = datetime.strptime(date_str, '%Y-%m-%d').date() - except ValueError: - return JsonResponse({ - 'status': 'error', - 'message': 'Invalid date format. Use YYYY-MM-DD' - }, status=400) - - if model_name == 'SecondTableEntry': - # Handle Helium Output specific fields - - raw_warm = request.POST.get('is_warm') - entry.is_warm = str(raw_warm).lower() in ('1', 'true', 'on', 'yes') - - entry.lhe_delivery = request.POST.get('lhe_delivery', '') - - lhe_output = request.POST.get('lhe_output') - try: - entry.lhe_output = Decimal(lhe_output) if lhe_output else None - except InvalidOperation: - return JsonResponse({ - 'status': 'error', - 'message': 'Invalid LHe Output value' - }, status=400) - - entry.save() - - return JsonResponse({ - 'status': 'success', - 'id': entry.id, - 'client_name': entry.client.name, - 'institute_name': entry.client.institute.name, - 'date': entry.date.strftime('%Y-%m-%d') if entry.date else '', - 'is_warm': entry.is_warm, - 'lhe_delivery': entry.lhe_delivery, - 'lhe_output': str(entry.lhe_output) if entry.lhe_output else '', - 'notes': entry.notes - }) - - - elif model_name == 'ExcelEntry': - # Handle Helium Input specific fields - try: - entry.pressure = Decimal(request.POST.get('pressure', 0)) - entry.purity = Decimal(request.POST.get('purity', 0)) - entry.druckkorrektur = Decimal(request.POST.get('druckkorrektur', 1)) - entry.lhe_zus = Decimal(request.POST.get('lhe_zus', 0)) - entry.constant_300 = Decimal(request.POST.get('constant_300', 300)) - entry.korrig_druck = Decimal(request.POST.get('korrig_druck', 0)) - entry.nm3 = Decimal(request.POST.get('nm3', 0)) - entry.lhe = Decimal(request.POST.get('lhe', 0)) - entry.lhe_ges = Decimal(request.POST.get('lhe_ges', 0)) - except InvalidOperation: - return JsonResponse({ - 'status': 'error', - 'message': 'Invalid numeric value in Helium Input' - }, status=400) - - entry.save() - - return JsonResponse({ - 'status': 'success', - 'id': entry.id, - 'client_name': entry.client.name, - 'institute_name': entry.client.institute.name, - 'date': entry.date.strftime('%Y-%m-%d') if entry.date else '', - 'month': f"{entry.date.month:02d}" if entry.date else '', - 'pressure': str(entry.pressure), - 'purity': str(entry.purity), - 'druckkorrektur': str(entry.druckkorrektur), - 'constant_300': str(entry.constant_300), - 'korrig_druck': str(entry.korrig_druck), - 'nm3': str(entry.nm3), - 'lhe': str(entry.lhe), - 'lhe_zus': str(entry.lhe_zus), - 'lhe_ges': str(entry.lhe_ges), - 'notes': entry.notes - }) - - - except model.DoesNotExist: - return JsonResponse({'status': 'error', 'message': 'Entry not found'}, status=404) - except Exception as e: - return JsonResponse({'status': 'error', 'message': str(e)}, status=400) - - return JsonResponse({'status': 'error', 'message': 'Invalid request method'}, status=400) - -# Delete Entry (Generic) -def delete_entry(request, model_name): - if request.method == 'POST': - try: - if model_name == 'SecondTableEntry': - model = SecondTableEntry - elif model_name == 'ExcelEntry': - model = ExcelEntry - else: - return JsonResponse({'status': 'error', 'message': 'Invalid model'}, status=400) - - entry_id = request.POST.get('id') - entry = model.objects.get(id=entry_id) - entry.delete() - return JsonResponse({'status': 'success', 'message': 'Entry deleted'}) - - except model.DoesNotExist: - return JsonResponse({'status': 'error', 'message': 'Entry not found'}, status=404) - except Exception as e: - return JsonResponse({'status': 'error', 'message': str(e)}, status=400) - - return JsonResponse({'status': 'error', 'message': 'Invalid request'}, status=400) - -def betriebskosten_list(request): - items = Betriebskosten.objects.all().order_by('-buchungsdatum') - return render(request, 'betriebskosten_list.html', {'items': items}) - -def betriebskosten_create(request): - if request.method == 'POST': - try: - entry_id = request.POST.get('id') - if entry_id: - # Update existing entry - entry = Betriebskosten.objects.get(id=entry_id) - else: - # Create new entry - entry = Betriebskosten() - - # Get form data - buchungsdatum_str = request.POST.get('buchungsdatum') - rechnungsnummer = request.POST.get('rechnungsnummer') - kostentyp = request.POST.get('kostentyp') - betrag = request.POST.get('betrag') - beschreibung = request.POST.get('beschreibung') - gas_volume = request.POST.get('gas_volume') - - # Validate required fields - if not all([buchungsdatum_str, rechnungsnummer, kostentyp, betrag]): - return JsonResponse({'status': 'error', 'message': 'All required fields must be filled'}) - - # Convert date string to date object - try: - buchungsdatum = parse_date(buchungsdatum_str) - if not buchungsdatum: - return JsonResponse({'status': 'error', 'message': 'Invalid date format'}) - except (ValueError, TypeError): - return JsonResponse({'status': 'error', 'message': 'Invalid date format'}) - - # Set entry values - entry.buchungsdatum = buchungsdatum - entry.rechnungsnummer = rechnungsnummer - entry.kostentyp = kostentyp - entry.betrag = betrag - entry.beschreibung = beschreibung - - # Only set gas_volume if kostentyp is helium and gas_volume is provided - if kostentyp == 'helium' and gas_volume: - entry.gas_volume = gas_volume - else: - entry.gas_volume = None - - entry.save() - - return JsonResponse({ - 'status': 'success', - 'id': entry.id, - 'buchungsdatum': entry.buchungsdatum.strftime('%Y-%m-%d'), - 'rechnungsnummer': entry.rechnungsnummer, - 'kostentyp_display': entry.get_kostentyp_display(), - 'gas_volume': str(entry.gas_volume) if entry.gas_volume else '-', - 'betrag': str(entry.betrag), - 'beschreibung': entry.beschreibung or '' - }) - - except Exception as e: - return JsonResponse({'status': 'error', 'message': str(e)}) - - return JsonResponse({'status': 'error', 'message': 'Invalid request method'}) - -def betriebskosten_delete(request): - if request.method == 'POST': - try: - entry_id = request.POST.get('id') - entry = Betriebskosten.objects.get(id=entry_id) - entry.delete() - return JsonResponse({'status': 'success'}) - except Betriebskosten.DoesNotExist: - return JsonResponse({'status': 'error', 'message': 'Entry not found'}) - except Exception as e: - return JsonResponse({'status': 'error', 'message': str(e)}) - - return JsonResponse({'status': 'error', 'message': 'Invalid request method'}) - -class CheckSheetView(View): - def get(self, request): - # Get current month/year - current_year = datetime.now().year - current_month = datetime.now().month - - # Get all sheets - sheets = MonthlySheet.objects.all() - - sheet_data = [] - for sheet in sheets: - cells_count = sheet.cells.count() - # Count non-empty cells - non_empty = sheet.cells.exclude(value__isnull=True).count() - - sheet_data.append({ - 'id': sheet.id, - 'year': sheet.year, - 'month': sheet.month, - 'month_name': calendar.month_name[sheet.month], - 'total_cells': cells_count, - 'non_empty_cells': non_empty, - 'has_data': non_empty > 0 - }) - - # Also check what the default view would show - default_sheet = MonthlySheet.objects.filter( - year=current_year, - month=current_month - ).first() - - return JsonResponse({ - 'current_year': current_year, - 'current_month': current_month, - 'current_month_name': calendar.month_name[current_month], - 'default_sheet_exists': default_sheet is not None, - 'default_sheet_id': default_sheet.id if default_sheet else None, - 'sheets': sheet_data, - 'total_sheets': len(sheet_data) - }) - - -class QuickDebugView(View): - def get(self, request): - # Get ALL sheets - sheets = MonthlySheet.objects.all().order_by('year', 'month') - - result = { - 'sheets': [] - } - - for sheet in sheets: - sheet_info = { - 'id': sheet.id, - 'display': f"{sheet.year}-{sheet.month:02d}", - 'url': f"/sheet/{sheet.year}/{sheet.month}/", # CHANGED THIS LINE - 'sheet_url_pattern': 'sheet/{year}/{month}/', # Add this for clarity - } - - # Count cells with data for first client in top_left table - first_client = Client.objects.first() - if first_client: - test_cells = sheet.cells.filter( - client=first_client, - table_type='top_left', - row_index__in=[8, 9, 10] # Rows 9, 10, 11 - ).order_by('row_index') - - cell_values = {} - for cell in test_cells: - cell_values[f"row_{cell.row_index}"] = str(cell.value) if cell.value else "Empty" - - sheet_info['test_cells'] = cell_values - else: - sheet_info['test_cells'] = "No clients" - - result['sheets'].append(sheet_info) - - return JsonResponse(result) -class TestFormulaView(View): - def get(self, request): - # Test the formula evaluation directly - test_values = { - 8: 2, # Row 9 value (0-based index 8) - 9: 2, # Row 10 value (0-based index 9) - } - - # Test formula "9 + 8" (using 0-based indices) - formula = "9 + 8" - result = evaluate_formula(formula, test_values) - - return JsonResponse({ - 'test_values': test_values, - 'formula': formula, - 'result': str(result), - 'note': 'Formula uses 0-based indices. 9=Row10, 8=Row9' - }) - - -class SimpleDebugView(View): - """Simplest debug view to check if things are working""" - def get(self, request): - sheet_id = request.GET.get('sheet_id', 1) - - try: - sheet = MonthlySheet.objects.get(id=sheet_id) - - # Get first client - client = Client.objects.first() - if not client: - return JsonResponse({'error': 'No clients found'}) - - # Check a few cells - cells = Cell.objects.filter( - sheet=sheet, - client=client, - table_type='top_left', - row_index__in=[8, 9, 10] - ).order_by('row_index') - - cell_data = [] - for cell in cells: - cell_data.append({ - 'row_index': cell.row_index, - 'ui_row': cell.row_index + 1, - 'value': str(cell.value) if cell.value is not None else 'Empty', - 'cell_id': cell.id - }) - - return JsonResponse({ - 'sheet': f"{sheet.year}-{sheet.month}", - 'sheet_id': sheet.id, - 'client': client.name, - 'cells': cell_data, - 'note': 'Row 8 = UI Row 9, Row 9 = UI Row 10, Row 10 = UI Row 11' - }) - - except MonthlySheet.DoesNotExist: - return JsonResponse({'error': f'Sheet with id {sheet_id} not found'}) - -def halfyear_settings(request): - """ - Global settings page: choose a year + first month for the 6-month interval. - These values are stored in the session and used by other views. - """ - # Determine available years from your data (use ExcelEntry or SecondTableEntry) - # Here I use SecondTableEntry; you can switch to ExcelEntry if you prefer. - available_years_qs = SecondTableEntry.objects.dates('date', 'year', order='DESC') - available_years = [d.year for d in available_years_qs] - - if not available_years: - available_years = [timezone.now().year] - - # Defaults (if nothing in session yet) - default_year = request.session.get('halfyear_year', available_years[0]) - default_start_month = request.session.get('halfyear_start_month', 1) - - if request.method == 'POST': - year = int(request.POST.get('year', default_year)) - start_month = int(request.POST.get('start_month', default_start_month)) - - request.session['halfyear_year'] = year - request.session['halfyear_start_month'] = start_month - - # Redirect back to where user came from, or to this page again - next_url = request.POST.get('next') or request.GET.get('next') or 'halfyear_settings' - return redirect(next_url) - - # Month choices for the dropdown - month_choices = [(i, calendar.month_name[i]) for i in range(1, 13)] - - context = { - 'available_years': available_years, - 'selected_year': int(default_year), - 'selected_start_month': int(default_start_month), - 'month_choices': month_choices, - } - return render(request, 'halfyear_settings.html', context) \ No newline at end of file diff --git a/sheets/services/halfyear_calc.py b/sheets/services/halfyear_calc.py index e3ba478..f1e0766 100644 --- a/sheets/services/halfyear_calc.py +++ b/sheets/services/halfyear_calc.py @@ -1,14 +1,10 @@ # sheets/services/halfyear_calc.py from __future__ import annotations -from django.db.models.functions import Coalesce -from decimal import Decimal from typing import Dict, Any -from django.shortcuts import redirect, render from decimal import Decimal from sheets.models import ( - Client, SecondTableEntry, Institute, ExcelEntry, - Betriebskosten, MonthlySheet, Cell, CellReference, MonthlySummary ,BetriebskostenSummary,AbrechnungCell -) + Client, SecondTableEntry, ExcelEntry, + MonthlySheet, Cell, MonthlySummary) from django.db.models import Sum from django.db.models.functions import Coalesce from django.db.models import DecimalField, Value @@ -172,7 +168,6 @@ def build_halfyear_window(interval_year: int, start_month: int): window.append((y, m)) return window # Import ONLY models + pure helpers here -from sheets.models import MonthlySheet, Cell, Client, SecondTableEntry, BetriebskostenSummary def sum_right_row_without_duplicates(label: str, clients_right: list[str], values: list): """ For specific rows, clients are logically merged, so we must only count one copy. @@ -268,8 +263,7 @@ def compute_halfyear_context(interval_year: int, interval_start: int) -> Dict[st chosen_sheet_bottom2, bottom2_inputs = pick_bottom2_from_window(window, sheets_by_ym, prev_sheet) - bottom2_g39 = bottom2_inputs["g39"] - bottom2_i39 = bottom2_inputs["i39"] + # ---------------------------- # HALF-YEAR BOTTOM TABLE 1 (Bilanz) - Read only # ---------------------------- @@ -351,7 +345,6 @@ def compute_halfyear_context(interval_year: int, interval_start: int) -> Dict[st "nm3": nm3_sum_27_35, "lhe": lhe_sum_27_35, }) - start_sheet = sheets_by_ym.get((start_year, start_month)) # ------------------------------------------------------------ # Bottom Table 2 (Halbjahres Bilanz) – server-side recalcBottom2() # ------------------------------------------------------------ diff --git a/sheets/tests.py b/sheets/tests.py index 7ce503c..4929020 100644 --- a/sheets/tests.py +++ b/sheets/tests.py @@ -1,3 +1,2 @@ -from django.test import TestCase # Create your tests here. diff --git a/sheets/urls.py b/sheets/urls.py index 390b17b..76ea0ed 100644 --- a/sheets/urls.py +++ b/sheets/urls.py @@ -27,10 +27,10 @@ urlpatterns = [ path('set-halfyear-interval/', set_halfyear_interval, name='set_halfyear_interval'), path('halfyear-bilanz/', views.halfyear_balance_view, name='halfyear_balance'), path('sheet///', MonthlySheetView.as_view(), name='monthly_sheet'), + path('settings/halfyear/', halfyear_settings, name='halfyear_settings'), - path('sheet///', views.MonthlySheetView.as_view(), name='monthly_sheet'), path('summary///', views.SummarySheetView.as_view(), name='summary_sheet'), - path("save-cells/", SaveCellsView.as_view(), name="save_cells"), path("save-cells/", SaveCellsView.as_view(), name="save_cells"),path('save-month-summary/', SaveMonthSummaryView.as_view(), name='save_month_summary'), + path("save-cells/", SaveCellsView.as_view(), name="save_cells"), path('save-month-summary/', SaveMonthSummaryView.as_view(), name='save_month_summary'), path('calculate/', views.CalculateView.as_view(), name='calculate'), diff --git a/sheets/views.py b/sheets/views.py index d9d4903..c0efc65 100644 --- a/sheets/views.py +++ b/sheets/views.py @@ -4,24 +4,21 @@ from django.views.decorators.csrf import csrf_exempt from django.db.models import Sum, Value, DecimalField from django.http import JsonResponse from django.db.models import Q +from django.db.models import Count from decimal import Decimal, ROUND_HALF_UP -from decimal import Decimal, InvalidOperation -from django.apps import apps -from datetime import date, datetime +from decimal import InvalidOperation +from datetime import datetime from sheets.services.halfyear_calc import compute_halfyear_context import calendar from django.utils import timezone +import re from django.views.generic import TemplateView, View from .models import ( Client, SecondTableEntry, Institute, ExcelEntry, - Betriebskosten, MonthlySheet, Cell, CellReference, MonthlySummary ,BetriebskostenSummary,AbrechnungCell + Betriebskosten, MonthlySheet, Cell, MonthlySummary ,BetriebskostenSummary,AbrechnungCell ) -from django.db.models import Sum -from django.urls import reverse from django.db.models.functions import Coalesce -from .forms import BetriebskostenForm from django.utils.dateparse import parse_date -from django.contrib.auth.mixins import LoginRequiredMixin import json FIRST_SHEET_YEAR = 2025 FIRST_SHEET_MONTH = 1 @@ -368,13 +365,7 @@ def get_bestand_kannen_for_month(sheet, client_name: str) -> Decimal: """ return get_top_left_value(sheet, client_name, row_index=BESTAND_KANNEN_ROW_INDEX) -from decimal import Decimal -from django.db.models import Sum -from django.db.models.functions import Coalesce -from django.db.models import DecimalField, Value -from .models import MonthlySheet, SecondTableEntry, Client, Cell -from django.shortcuts import redirect, render # You already have HALFYEAR_CLIENTS for the left table (AG Vogel, AG Halfm, IKP) HALFYEAR_CLIENTS = ["AG Vogel", "AG Halfm", "IKP"] @@ -463,7 +454,7 @@ def get_top_left_value(sheet, client_name: str, row_index: int) -> Decimal: return Decimal('0') def get_group_clients(group_key): """Return queryset of clients that belong to a logical group.""" - from .models import Client # local import to avoid circulars + group = CLIENT_GROUPS.get(group_key) if not group: @@ -473,9 +464,7 @@ def get_group_clients(group_key): def calculate_summation(sheet, table_type, row_index, sum_column_index): """Calculate summation for a row, with special handling for % row""" - from decimal import Decimal - from .models import Cell - + try: # Special case: top_left, % row (Excel B20 -> row_index 19) if table_type == 'top_left' and row_index == 19: @@ -533,9 +522,7 @@ def evaluate_formula(formula, values_dict): Safely evaluate a formula like "10 + 9" where numbers are row indices values_dict: {row_index: decimal_value} """ - from decimal import Decimal - import re - + try: # Create a copy of the formula to work with expr = formula @@ -568,9 +555,7 @@ class MonthlySheetView(TemplateView): def populate_helium_input_to_top_right(self, sheet): """Populate bezug data from SecondTableEntry to top-right table (row 8 = Excel row 12)""" - from .models import SecondTableEntry, Cell, Client - from django.db.models.functions import Coalesce - from decimal import Decimal + year = sheet.year month = sheet.month @@ -632,10 +617,7 @@ class MonthlySheetView(TemplateView): return True def calculate_bezug_from_entries(self, sheet, year, month): """Calculate B11 (Bezug) from SecondTableEntry for all clients - ONLY for non-start sheets""" - from .models import SecondTableEntry, Cell, Client - from django.db.models import Sum - from django.db.models.functions import Coalesce - from decimal import Decimal + # Check if this is the start sheet if year == 2025 and month == 1: @@ -664,7 +646,7 @@ class MonthlySheetView(TemplateView): b11_cell.save() # Also trigger dependent calculations - from .views import SaveCellsView + save_view = SaveCellsView() save_view.calculate_top_left_dependents(sheet, b11_cell) # In MonthlySheetView.get_context_data() method, update the TOP_RIGHT_CLIENTS and row count: @@ -673,7 +655,7 @@ class MonthlySheetView(TemplateView): return True def get_context_data(self, **kwargs): - from decimal import Decimal + context = super().get_context_data(**kwargs) year = self.kwargs.get('year', datetime.now().year) month = self.kwargs.get('month', datetime.now().month) @@ -702,9 +684,7 @@ class MonthlySheetView(TemplateView): # Recalculate dependents once when opening the sheet # ---------------------------------------------------- - from .views import SaveCellsView - from .models import Cell - + save_view = SaveCellsView() # ---- TOP LEFT ---- @@ -768,8 +748,7 @@ class MonthlySheetView(TemplateView): # Update row counts in build_group_rows function def build_group_rows(sheet, table_type, client_names): """Build rows for display in monthly sheet.""" - from decimal import Decimal - from .models import Cell + MERGED_ROWS = {2, 3, 5, 6, 7, 9, 10, 12, 14, 15} MERGED_PAIRS = [ ("Dr. Fohrer", "AG Buntk."), @@ -1085,7 +1064,7 @@ class MonthlySheetView(TemplateView): prev_year = year prev_month = month - 1 - from .models import MonthlySheet, Cell, Client + prev_sheet = MonthlySheet.objects.filter( year=prev_year, @@ -1146,8 +1125,7 @@ class MonthlySheetView(TemplateView): - AG Alff + AG Gutfl. share the SAME value (from previous month's AG Alff or AG Gutfl.) M3 clients just copy their own value. """ - from .models import MonthlySheet, Cell, Client - from decimal import Decimal + # Do nothing on first sheet if year == FIRST_SHEET_YEAR and month == FIRST_SHEET_MONTH: @@ -1284,7 +1262,7 @@ def get_factor_value(table_type, row_index): def recalculate_stand_der_gaszahler(self, sheet): """Recalculate Stand der Gaszähler for all client pairs""" - from decimal import Decimal + # For Dr. Fohrer and AG Buntk. (L & M columns) try: @@ -1456,7 +1434,7 @@ class AbrechnungView(TemplateView): EDITABLE_ROW_KEYS = {"ghe_bezug", "betrag", "gutschriften"} def get_context_data(self, **kwargs): - from sheets.services.halfyear_calc import compute_halfyear_context + context = super().get_context_data(**kwargs) interval_year = self.request.session.get("halfyear_year") @@ -1496,22 +1474,6 @@ class AbrechnungView(TemplateView): umlage_personal_total = d(bs.umlage_personal) if bs else Decimal("0") # ---- helper: sum lhe_output in the 6-month window for a list of client names ---- - def sum_output_for_clients_exact(client_names): - total = Decimal("0") - if not client_names: - return total - - for (y, m) in window: - total += SecondTableEntry.objects.filter( - client__name__in=client_names, - date__year=y, - date__month=m, - ).aggregate( - total=Coalesce(Sum("lhe_output"), Value(0, output_field=DecimalField())) - )["total"] or Decimal("0") - - return total - # (NOTE: the date filter above is “wide”; if you prefer exact (y,m) matching, use the loop version below) def sum_output_for_clients_exact(client_names): total = Decimal("0") if not client_names: @@ -1920,13 +1882,7 @@ class AbrechnungView(TemplateView): GROUP_IJKL = ["orgchem_thiele", "phychem_m3_buntkow", "orgchem_fohrer", "mawi_m3_gutfl", "mawi_alff"] GROUP_NOP = ["chemie", "mawi", "physik"] GROUP_STADT = ["pkm_vogel", "iap_halfmann", "ikp"] - def nop_cols_for_row(row_key: str): - # For gutschriften, NOP should sum the 5 IJKL client columns - if row_key == "gutschriften": - return GROUP_IJKL - # otherwise NOP is the 3 summary columns (Chemie/MaWi/Physik) - return GROUP_NOP - + def val(row_key, col_key): """Get the final numeric value for a cell (prefer computed, fallback to stored).""" if (row_key, col_key) in computed: @@ -2104,7 +2060,7 @@ class RechnungView(TemplateView): # 3) Heliumverluste (l) from Halbjahresbilanz total Verbraucherverluste # (sum all clients left+right) # ------------------------------------------------------------ - from sheets.services.halfyear_calc import compute_halfyear_context + half_ctx = compute_halfyear_context(interval_year, interval_start) heliumverluste_l = Decimal("0") @@ -2223,14 +2179,7 @@ class SaveAbrechnungCellsView(View): ) return JsonResponse({"ok": True}) -def build_halfyear_window(interval_year: int, interval_start_month: int): - window = [] - for offset in range(6): - total_index = (interval_start_month - 1) + offset - y = interval_year + (total_index // 12) - m = (total_index % 12) + 1 - window.append((y, m)) - return window + class SaveCellsView(View): def calculate_bottom_3_dependents(self, sheet): @@ -2296,7 +2245,7 @@ class SaveCellsView(View): cell = get_cell(r, c) return dec(cell.value if cell else None) - f47 = get_val(1, 0) + g47 = get_val(1, 1) i47 = get_val(1, 2) i50 = get_val(4, 2) @@ -2410,7 +2359,6 @@ class SaveCellsView(View): # If conversion fails, treat as empty new_value = None - old_value = cell.value cell.value = new_value cell.save() @@ -2486,10 +2434,7 @@ class SaveCellsView(View): 14: Verbraucherverluste (Liter L-He) - calculated 15: % - calculated """ - from decimal import Decimal - from django.db.models import Sum, Count - from django.db.models.functions import Coalesce - from .models import Client, Cell, ExcelEntry, SecondTableEntry + TOP_RIGHT_CLIENTS = [ "Dr. Fohrer", # L @@ -2820,9 +2765,7 @@ class SaveCellsView(View): - Nm³ (col 3): SUM Nm³ rows 0–8 - Lit. LHe (col 4): SUM Lit. LHe rows 0–8 """ - from decimal import Decimal, InvalidOperation - from .models import Cell - + updated_cells = [] DATA_ROWS = list(range(0, 9)) # 0–8 @@ -2961,9 +2904,7 @@ class SaveCellsView(View): def calculate_top_left_dependents(self, sheet, changed_cell): """Calculate dependent cells in top_left table""" - from decimal import Decimal - from django.db.models import Sum - from django.db.models.functions import Coalesce + client_id = changed_cell.client_id updated_cells = [] @@ -2986,7 +2927,7 @@ class SaveCellsView(View): if cell and cell.value is not None: try: return Decimal(str(cell.value)) - except: + except Exception: return Decimal('0') return Decimal('0') @@ -3041,13 +2982,13 @@ class SaveCellsView(View): }) # 4. B11 = Sum of LHe Output from SecondTableEntry for this client/month - Bezug - from .models import SecondTableEntry + client = changed_cell.client # Calculate total LHe output for this client in this month # 4. B11 = Bezug (Liter L-He) # For start sheet: manual entry, for other sheets: auto-calculated from SecondTableEntry - from .models import SecondTableEntry + client = changed_cell.client b11_cell = cell_dict.get(8) # row_index 8 = Excel B11 (UI row 9) @@ -3258,7 +3199,7 @@ class SaveCellsView(View): def recalculate_top_left_table(self, sheet, client_id): """Recalculate the top-left table for a specific client""" - from decimal import Decimal + # Get all cells for this client in top_left table cells = Cell.objects.filter( @@ -3429,7 +3370,7 @@ class SummarySheetView(TemplateView): year = int(self.kwargs.get('year', datetime.now().year)) # Get 6 monthly sheets - months = [(year, m) for m in range(start_month, start_month + 6)] + sheets = MonthlySheet.objects.filter( year=year, month__in=list(range(start_month, start_month + 6)) @@ -3605,7 +3546,7 @@ def set_halfyear_interval(request): return redirect('clients_list') # Table One View (ExcelEntry) def table_one_view(request): - from .models import ExcelEntry, Client, Institute + # --- Base queryset for the main Helium Input table --- base_entries = ExcelEntry.objects.all().select_related('client', 'client__institute') @@ -4129,9 +4070,7 @@ def betriebskosten_list(request): summary = get_summary() summary.recalculate() - from decimal import Decimal - from django.db.models import Sum, DecimalField, Value - from django.db.models.functions import Coalesce + interval_year = request.session.get("halfyear_year") interval_start = request.session.get("halfyear_start_month") @@ -4287,7 +4226,7 @@ def get_summary(): summary, created = BetriebskostenSummary.objects.get_or_create(id=1) return summary -from django.http import JsonResponse + def update_personalkosten(request): if request.method == "POST": @@ -4345,8 +4284,7 @@ class CheckSheetView(View): - - return JsonResponse(result) + class TestFormulaView(View): def get(self, request): # Test the formula evaluation directly