Files
he-database/sheets/views.py

4232 lines
159 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
BOTTOM1_COL_VOLUME = 0
BOTTOM1_COL_BAR = 1
BOTTOM1_COL_KORR = 2
BOTTOM1_COL_NM3 = 3
BOTTOM1_COL_LHE = 4
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",
]
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()
# ----------------------------
# 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))
# ------------------------------------------------------------------
# 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!<col>9); last_month!<col>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 its 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"),
]
def d(x):
return x if isinstance(x, Decimal) else Decimal("0")
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 = []
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({
"label": label,
"total": total,
"lichtwiese": lichtwiese,
"chemie": chemie,
"mawi": mawi,
"m3": m3,
"is_percent": (key == "percent"),
})
# ------------------------------------------------------------------
# 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,
}
return render(request, 'halfyear_balance.html', context)
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 015
"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 1823)
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 09, col_index 04):
Rows 08:
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 08)
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 08
- Lit. LHe (col 4): SUM Lit. LHe rows 08
"""
from decimal import Decimal, InvalidOperation
from .models import Cell
updated_cells = []
DATA_ROWS = list(range(0, 9)) # 08
TOTAL_ROW = 9
COL_VOL = 0
COL_BAR = 1
COL_KORR = 2
COL_NM3 = 3
COL_LHE = 4
# Fixed Volumen values for rows 08
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 08: 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)