4232 lines
159 KiB
Python
4232 lines
159 KiB
Python
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 it’s only text (Aufteilung nach Verbrauch / Gaszähler)
|
||
# and easier to render directly in the template if needed.
|
||
('Rückführung flüssig (Lit. L-He)', 'rueckf_fluessig'),
|
||
('Sonderrückführungen (Lit. L-He)', 'sonder'),
|
||
('Sammelrückführung (Lit. L-He)', 'sammel'),
|
||
('Bestand in Kannen-1 (Lit. L-He)', 'bestand_kannen'),
|
||
('Summe Bestand (Lit. L-He)', 'summe_bestand'),
|
||
('Best. in Kannen Vormonat (Lit. L-He)', 'best_kannen_vormonat'),
|
||
('Bezug (Liter L-He)', 'bezug'),
|
||
('Rückführ. Soll (Lit. L-He)', 'rueckf_soll'),
|
||
('Verluste (Soll-Rückf.) (Lit. L-He)', 'verluste'),
|
||
('Füllungen warm (Lit. L-He)', 'fuellungen_warm'),
|
||
('Kaltgas Rückgabe (Lit. L-He) – Faktor', 'kaltgas_rueckgabe'),
|
||
('Verbraucherverluste (Liter L-He)', 'verbraucherverluste'),
|
||
('%', 'percent'),
|
||
]
|
||
|
||
rows_right = []
|
||
for label, key in right_row_defs:
|
||
values = [right_data[cname].get(key) for cname in RIGHT_CLIENTS]
|
||
|
||
if key == 'percent':
|
||
total_bezug = sum((right_data[c]['bezug'] for c in RIGHT_CLIENTS), Decimal('0'))
|
||
total_verb = sum((right_data[c]['verbraucherverluste'] for c in RIGHT_CLIENTS), Decimal('0'))
|
||
total = (total_verb / total_bezug) if total_bezug != 0 else None
|
||
else:
|
||
total = sum((v for v in values if isinstance(v, Decimal)), Decimal('0'))
|
||
|
||
rows_right.append({
|
||
'label': label,
|
||
'values': values,
|
||
'total': total,
|
||
'is_percent': key == 'percent',
|
||
})
|
||
SUM_TABLE_ROWS = [
|
||
("Rückführung flüssig (Lit. L-He)", "rueckf_fluessig"),
|
||
("Sonderrückführungen (Lit. L-He)", "sonder"),
|
||
("Sammelrückführungen (Lit. L-He)", "sammel"),
|
||
("Bestand in Kannen-1 (Lit. L-He)", "bestand_kannen"),
|
||
("Summe Bestand (Lit. L-He)", "summe_bestand"),
|
||
("Best. in Kannen Vormonat (Lit. L-He)", "best_kannen_vormonat"),
|
||
("Bezug (Liter L-He)", "bezug"),
|
||
("Rückführ. Soll (Lit. L-He)", "rueckf_soll"),
|
||
("Verluste (Soll-Rückf.) (Lit. L-He)", "verluste"),
|
||
("Füllungen warm (Lit. L-He)", "fuellungen_warm"),
|
||
("Kaltgas Rückgabe (Lit. L-He) – Faktor", "kaltgas_rueckgabe"),
|
||
("Faktor 0.06", "factor_row"),
|
||
("Verbraucherverluste (Liter L-He)", "verbraucherverluste"),
|
||
("%", "percent"),
|
||
]
|
||
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 0–15
|
||
"bottom_1": 10,
|
||
"bottom_2": 10,
|
||
"bottom_3": 10,
|
||
}
|
||
row_count = row_counts.get(table_type, 0)
|
||
|
||
# Get all cells for this sheet and table
|
||
all_cells = (
|
||
Cell.objects.filter(
|
||
sheet=sheet,
|
||
table_type=table_type,
|
||
).select_related("client")
|
||
)
|
||
|
||
# Group cells by row index
|
||
cells_by_row = {}
|
||
for cell in all_cells:
|
||
if cell.row_index not in cells_by_row:
|
||
cells_by_row[cell.row_index] = {}
|
||
cells_by_row[cell.row_index][cell.client.name] = cell
|
||
|
||
# We will store row sums for Bezug (row 8) and Verbraucherverluste (row 14)
|
||
# for top_left / top_right so we can compute the overall % in row 15.
|
||
sum_bezug = None
|
||
sum_verbrauch = None
|
||
|
||
# Build each row
|
||
for row_idx in range(row_count):
|
||
display_cells = []
|
||
row_cells_dict = cells_by_row.get(row_idx, {})
|
||
|
||
# Build cells in the requested client order
|
||
for name in client_names:
|
||
cell = row_cells_dict.get(name)
|
||
display_cells.append(cell)
|
||
|
||
# Calculate sum for this row (includes editable + calculated cells)
|
||
sum_value = None
|
||
total = Decimal("0")
|
||
has_value = False
|
||
|
||
merged_second_indices = set()
|
||
if table_type == 'top_right' and row_idx in MERGED_ROWS:
|
||
for left_name, right_name in MERGED_PAIRS:
|
||
try:
|
||
right_idx = client_names.index(right_name)
|
||
merged_second_indices.add(right_idx)
|
||
except ValueError:
|
||
# client not in this table; just ignore
|
||
pass
|
||
|
||
for col_idx, cell in enumerate(display_cells):
|
||
# Skip the duplicate (second) column of each merged pair
|
||
if col_idx in merged_second_indices:
|
||
continue
|
||
|
||
if cell and cell.value is not None:
|
||
try:
|
||
total += Decimal(str(cell.value))
|
||
has_value = True
|
||
except Exception:
|
||
pass
|
||
|
||
if has_value:
|
||
sum_value = total
|
||
|
||
# Remember special rows for top tables
|
||
if table_type in ("top_left", "top_right"):
|
||
if row_idx == 8: # Bezug
|
||
sum_bezug = total
|
||
elif row_idx == 14: # Verbraucherverluste
|
||
sum_verbrauch = total
|
||
|
||
rows.append(
|
||
{
|
||
"cells": display_cells,
|
||
"sum": sum_value,
|
||
"row_index": row_idx,
|
||
}
|
||
)
|
||
|
||
# Adjust the % row sum for top_left / top_right:
|
||
# Sum(%) = Sum(Verbraucherverluste) / Sum(Bezug)
|
||
if table_type in ("top_left", "top_right"):
|
||
perc_row_idx = 15 # % row
|
||
if 0 <= perc_row_idx < len(rows):
|
||
if sum_bezug is not None and sum_bezug != 0 and sum_verbrauch is not None:
|
||
rows[perc_row_idx]["sum"] = sum_verbrauch / sum_bezug
|
||
else:
|
||
rows[perc_row_idx]["sum"] = None
|
||
|
||
return rows
|
||
|
||
|
||
# Now call the local function
|
||
top_left_rows = build_group_rows(sheet, 'top_left', TOP_LEFT_CLIENTS)
|
||
top_right_rows = build_group_rows(sheet, 'top_right', TOP_RIGHT_CLIENTS)
|
||
|
||
# --- Build combined summary of top-left + top-right Sum columns ---
|
||
|
||
# Helper to safely get the Sum for a given row index
|
||
def get_row_sum(rows, row_index):
|
||
if 0 <= row_index < len(rows):
|
||
return rows[row_index].get('sum')
|
||
return None
|
||
|
||
# Row definitions we want in the combined Σ table
|
||
# (row_index in top tables, label shown in the small table)
|
||
summary_row_defs = [
|
||
(2, "Rückführung flüssig (Lit. L-He)"),
|
||
(3, "Sonderrückführungen (Lit. L-He)"),
|
||
(4, "Sammelrückführungen (Lit. L-He)"),
|
||
(5, "Bestand in Kannen-1 (Lit. L-He)"),
|
||
(6, "Summe Bestand (Lit. L-He)"),
|
||
(7, "Best. in Kannen Vormonat (Lit. L-He)"),
|
||
(8, "Bezug (Liter L-He)"),
|
||
(9, "Rückführ. Soll (Lit. L-He)"),
|
||
(10, "Verluste (Soll-Rückf.) (Lit. L-He)"),
|
||
(11, "Füllungen warm (Lit. L-He)"),
|
||
(12, "Kaltgas Rückgabe (Lit. L-He) – Faktor"),
|
||
(13, "Faktor 0.06"),
|
||
(14, "Verbraucherverluste (Liter L-He)"),
|
||
(15, "%"),
|
||
]
|
||
|
||
# Precompute totals for Bezug and Verbraucherverluste across both tables
|
||
bezug_left = get_row_sum(top_left_rows, 8) or Decimal('0')
|
||
bezug_right = get_row_sum(top_right_rows, 8) or Decimal('0')
|
||
total_bezug = bezug_left + bezug_right
|
||
|
||
verb_left = get_row_sum(top_left_rows, 14) or Decimal('0')
|
||
verb_right = get_row_sum(top_right_rows, 14) or Decimal('0')
|
||
total_verbrauch = verb_left + verb_right
|
||
|
||
summary_rows = []
|
||
|
||
for row_index, label in summary_row_defs:
|
||
# Faktor row: always fixed 0.06
|
||
if row_index == 13:
|
||
summary_value = Decimal('0.06')
|
||
|
||
# % row: total Verbraucherverluste / total Bezug
|
||
elif row_index == 15:
|
||
if total_bezug != 0:
|
||
summary_value = total_verbrauch / total_bezug
|
||
else:
|
||
summary_value = None
|
||
|
||
else:
|
||
left_sum = get_row_sum(top_left_rows, row_index)
|
||
right_sum = get_row_sum(top_right_rows, row_index)
|
||
|
||
# Sammelrückführungen: only from top-right table
|
||
if row_index == 4:
|
||
left_sum = None
|
||
|
||
total = Decimal('0')
|
||
|
||
has_any = False
|
||
if left_sum is not None:
|
||
total += Decimal(str(left_sum))
|
||
has_any = True
|
||
if right_sum is not None:
|
||
total += Decimal(str(right_sum))
|
||
has_any = True
|
||
|
||
summary_value = total if has_any else None
|
||
|
||
summary_rows.append({
|
||
'row_index': row_index,
|
||
'label': label,
|
||
'sum': summary_value,
|
||
})
|
||
|
||
# Get cells for bottom tables
|
||
cells_by_table = self.get_cells_by_table(sheet)
|
||
|
||
context.update({
|
||
'sheet': sheet,
|
||
'clients': clients,
|
||
'year': year,
|
||
'month': month,
|
||
'month_name': calendar.month_name[month],
|
||
'prev_month': self.get_prev_month(year, month),
|
||
'next_month': self.get_next_month(year, month),
|
||
'cells_by_table': cells_by_table,
|
||
'top_left_headers': TOP_LEFT_CLIENTS + ['Sum'],
|
||
'top_right_headers': TOP_RIGHT_CLIENTS + ['Sum'],
|
||
'top_left_rows': top_left_rows,
|
||
'top_right_rows': top_right_rows,
|
||
'summary_rows': summary_rows, # 👈 NEW
|
||
'is_start_sheet': is_start_sheet,
|
||
})
|
||
return context
|
||
|
||
def get_cells_by_table(self, sheet):
|
||
"""Organize cells by table type for easy template rendering"""
|
||
cells = sheet.cells.select_related('client').all()
|
||
organized = {
|
||
'top_left': [[] for _ in range(16)],
|
||
'top_right': [[] for _ in range(16)], # now 16 rows
|
||
'bottom_1': [[] for _ in range(10)],
|
||
'bottom_2': [[] for _ in range(10)],
|
||
'bottom_3': [[] for _ in range(10)],
|
||
}
|
||
|
||
for cell in cells:
|
||
if cell.table_type not in organized:
|
||
continue
|
||
|
||
max_rows = len(organized[cell.table_type])
|
||
if cell.row_index < 0 or cell.row_index >= max_rows:
|
||
# This is an "extra" cell from an older layout (e.g. top_left rows 18–23)
|
||
continue
|
||
|
||
row_list = organized[cell.table_type][cell.row_index]
|
||
while len(row_list) <= cell.column_index:
|
||
row_list.append(None)
|
||
|
||
row_list[cell.column_index] = cell
|
||
|
||
return organized
|
||
|
||
|
||
def initialize_sheet_cells(self, sheet, clients):
|
||
"""Create all empty cells for a new monthly sheet"""
|
||
cells_to_create = []
|
||
summation_config = CALCULATION_CONFIG.get('summation_column', {})
|
||
sum_column_index = summation_config.get('sum_column_index', len(clients) - 1) # Last column
|
||
|
||
# For each table type and row
|
||
table_configs = [
|
||
('top_left', 16),
|
||
('top_right', 16),
|
||
('bottom_1', 10),
|
||
('bottom_2', 10),
|
||
('bottom_3', 10),
|
||
]
|
||
|
||
for table_type, row_count in table_configs:
|
||
for row_idx in range(row_count):
|
||
for col_idx, client in enumerate(clients):
|
||
is_summation = (col_idx == sum_column_index)
|
||
cells_to_create.append(Cell(
|
||
sheet=sheet,
|
||
client=client,
|
||
table_type=table_type,
|
||
row_index=row_idx,
|
||
column_index=col_idx,
|
||
value=None,
|
||
is_formula=is_summation, # Mark summation cells as formulas
|
||
))
|
||
|
||
# Bulk create all cells at once
|
||
Cell.objects.bulk_create(cells_to_create)
|
||
def get_prev_month(self, year, month):
|
||
"""Get previous month year and month"""
|
||
if month == 1:
|
||
return {'year': year - 1, 'month': 12}
|
||
return {'year': year, 'month': month - 1}
|
||
|
||
def get_next_month(self, year, month):
|
||
"""Get next month year and month"""
|
||
if month == 12:
|
||
return {'year': year + 1, 'month': 1}
|
||
return {'year': year, 'month': month + 1}
|
||
def apply_previous_month_links(self, sheet, year, month):
|
||
"""
|
||
For non-start sheets:
|
||
B4 (row 2) = previous sheet B3 (row 1)
|
||
B10 (row 8) = previous sheet B9 (row 7)
|
||
"""
|
||
# Do nothing on the first sheet
|
||
if year == FIRST_SHEET_YEAR and month == FIRST_SHEET_MONTH:
|
||
return
|
||
|
||
# Figure out previous month
|
||
if month == 1:
|
||
prev_year = year - 1
|
||
prev_month = 12
|
||
else:
|
||
prev_year = year
|
||
prev_month = month - 1
|
||
|
||
from .models import MonthlySheet, Cell, Client
|
||
|
||
prev_sheet = MonthlySheet.objects.filter(
|
||
year=prev_year,
|
||
month=prev_month
|
||
).first()
|
||
|
||
if not prev_sheet:
|
||
# No previous sheet created yet → nothing to copy
|
||
return
|
||
|
||
# For each client, copy values
|
||
for client in Client.objects.all():
|
||
# B3(prev) → B4(curr): UI row 1 → row 2 → row_index 0 → 1
|
||
prev_b3 = Cell.objects.filter(
|
||
sheet=prev_sheet,
|
||
table_type='top_left',
|
||
client=client,
|
||
row_index=0, # UI row 1
|
||
).first()
|
||
|
||
curr_b4 = Cell.objects.filter(
|
||
sheet=sheet,
|
||
table_type='top_left',
|
||
client=client,
|
||
row_index=1, # UI row 2
|
||
).first()
|
||
|
||
if prev_b3 and curr_b4:
|
||
curr_b4.value = prev_b3.value
|
||
curr_b4.save()
|
||
|
||
# B9(prev) → B10(curr): UI row 7 → row 8 → row_index 6 → 7
|
||
prev_b9 = Cell.objects.filter(
|
||
sheet=prev_sheet,
|
||
table_type='top_left',
|
||
client=client,
|
||
row_index=6, # UI row 7 (Summe Bestand)
|
||
).first()
|
||
|
||
curr_b10 = Cell.objects.filter(
|
||
sheet=sheet,
|
||
table_type='top_left',
|
||
client=client,
|
||
row_index=7, # UI row 8 (Best. in Kannen Vormonat)
|
||
).first()
|
||
|
||
if prev_b9 and curr_b10:
|
||
curr_b10.value = prev_b9.value
|
||
curr_b10.save()
|
||
|
||
def apply_previous_month_links_top_right(self, sheet, year, month):
|
||
"""
|
||
top_right row 7: Best. in Kannen Vormonat (Lit. L-He)
|
||
= previous sheet's Summe Bestand (row 6).
|
||
|
||
For merged pairs:
|
||
- Dr. Fohrer + AG Buntk. share the SAME value (from previous month's AG Buntk. or Dr. Fohrer)
|
||
- AG Alff + AG Gutfl. share the SAME value (from previous month's AG Alff or AG Gutfl.)
|
||
M3 clients just copy their own value.
|
||
"""
|
||
from .models import MonthlySheet, Cell, Client
|
||
from decimal import Decimal
|
||
|
||
# Do nothing on first sheet
|
||
if year == FIRST_SHEET_YEAR and month == FIRST_SHEET_MONTH:
|
||
return
|
||
|
||
# find previous month
|
||
if month == 1:
|
||
prev_year = year - 1
|
||
prev_month = 12
|
||
else:
|
||
prev_year = year
|
||
prev_month = month - 1
|
||
|
||
prev_sheet = MonthlySheet.objects.filter(
|
||
year=prev_year,
|
||
month=prev_month
|
||
).first()
|
||
|
||
if not prev_sheet:
|
||
return # nothing to copy from
|
||
|
||
TOP_RIGHT_CLIENTS = [
|
||
"Dr. Fohrer",
|
||
"AG Buntk.",
|
||
"AG Alff",
|
||
"AG Gutfl.",
|
||
"M3 Thiele",
|
||
"M3 Buntkowsky",
|
||
"M3 Gutfleisch",
|
||
]
|
||
|
||
# Helper function to get a cell value
|
||
def get_cell_value(sheet_obj, client_name, row_index):
|
||
client_obj = Client.objects.filter(name=client_name).first()
|
||
if not client_obj:
|
||
return None
|
||
|
||
try:
|
||
col_idx = TOP_RIGHT_CLIENTS.index(client_name)
|
||
except ValueError:
|
||
return None
|
||
|
||
cell = Cell.objects.filter(
|
||
sheet=sheet_obj,
|
||
table_type='top_right',
|
||
client=client_obj,
|
||
row_index=row_index,
|
||
column_index=col_idx,
|
||
).first()
|
||
|
||
return cell.value if cell else None
|
||
|
||
# Helper function to set a cell value
|
||
def set_cell_value(sheet_obj, client_name, row_index, value):
|
||
client_obj = Client.objects.filter(name=client_name).first()
|
||
if not client_obj:
|
||
return False
|
||
|
||
try:
|
||
col_idx = TOP_RIGHT_CLIENTS.index(client_name)
|
||
except ValueError:
|
||
return False
|
||
|
||
cell, created = Cell.objects.get_or_create(
|
||
sheet=sheet_obj,
|
||
table_type='top_right',
|
||
client=client_obj,
|
||
row_index=row_index,
|
||
column_index=col_idx,
|
||
defaults={'value': value}
|
||
)
|
||
|
||
if not created and cell.value != value:
|
||
cell.value = value
|
||
cell.save()
|
||
elif created:
|
||
cell.save()
|
||
|
||
return True
|
||
|
||
# ----- Pair 1: Dr. Fohrer + AG Buntk. -----
|
||
# Get previous month's Summe Bestand (row 6) for either client in the pair
|
||
pair1_prev_val = None
|
||
|
||
# Try AG Buntk. first
|
||
prev_buntk_val = get_cell_value(prev_sheet, "AG Buntk.", 6)
|
||
if prev_buntk_val is not None:
|
||
pair1_prev_val = prev_buntk_val
|
||
else:
|
||
# Try Dr. Fohrer if AG Buntk. is empty
|
||
prev_fohrer_val = get_cell_value(prev_sheet, "Dr. Fohrer", 6)
|
||
if prev_fohrer_val is not None:
|
||
pair1_prev_val = prev_fohrer_val
|
||
|
||
# Apply the value to both clients in the pair
|
||
if pair1_prev_val is not None:
|
||
set_cell_value(sheet, "Dr. Fohrer", 7, pair1_prev_val)
|
||
set_cell_value(sheet, "AG Buntk.", 7, pair1_prev_val)
|
||
|
||
# ----- Pair 2: AG Alff + AG Gutfl. -----
|
||
pair2_prev_val = None
|
||
|
||
# Try AG Alff first
|
||
prev_alff_val = get_cell_value(prev_sheet, "AG Alff", 6)
|
||
if prev_alff_val is not None:
|
||
pair2_prev_val = prev_alff_val
|
||
else:
|
||
# Try AG Gutfl. if AG Alff is empty
|
||
prev_gutfl_val = get_cell_value(prev_sheet, "AG Gutfl.", 6)
|
||
if prev_gutfl_val is not None:
|
||
pair2_prev_val = prev_gutfl_val
|
||
|
||
# Apply the value to both clients in the pair
|
||
if pair2_prev_val is not None:
|
||
set_cell_value(sheet, "AG Alff", 7, pair2_prev_val)
|
||
set_cell_value(sheet, "AG Gutfl.", 7, pair2_prev_val)
|
||
|
||
# ----- M3 clients: copy their own Summe Bestand -----
|
||
for name in ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"]:
|
||
prev_val = get_cell_value(prev_sheet, name, 6)
|
||
if prev_val is not None:
|
||
set_cell_value(sheet, name, 7, prev_val)
|
||
# Add this helper function to views.py
|
||
def get_factor_value(table_type, row_index):
|
||
"""Get factor value (like 0.06 for top_left row 17)"""
|
||
factors = {
|
||
('top_left', 17): Decimal('0.06'), # A18 in Excel (UI row 17, 0-based index 16)
|
||
}
|
||
return factors.get((table_type, row_index), Decimal('0'))
|
||
# Save Cells View
|
||
# views.py - Updated SaveCellsView
|
||
# views.py - Update SaveCellsView class
|
||
def debug_cell_values(self, sheet, client_id):
|
||
"""Debug method to check cell values"""
|
||
from .models import Cell
|
||
cells = Cell.objects.filter(
|
||
sheet=sheet,
|
||
table_type='top_left',
|
||
client_id=client_id
|
||
).order_by('row_index')
|
||
|
||
debug_info = {}
|
||
for cell in cells:
|
||
debug_info[f"row_{cell.row_index}"] = {
|
||
'value': str(cell.value) if cell.value else 'None',
|
||
'ui_row': cell.row_index + 1,
|
||
'excel_ref': f"B{cell.row_index + 3}"
|
||
}
|
||
|
||
return debug_info
|
||
class DebugCalculationView(View):
|
||
"""Debug view to test calculations directly"""
|
||
def get(self, request):
|
||
sheet_id = request.GET.get('sheet_id', 1)
|
||
client_name = request.GET.get('client', 'AG Vogel')
|
||
|
||
try:
|
||
sheet = MonthlySheet.objects.get(id=sheet_id)
|
||
client = Client.objects.get(name=client_name)
|
||
|
||
# Get SaveCellsView instance
|
||
save_view = SaveCellsView()
|
||
|
||
# Create a dummy cell to trigger calculations
|
||
dummy_cell = Cell.objects.filter(
|
||
sheet=sheet,
|
||
table_type='top_left',
|
||
client=client,
|
||
row_index=0 # B3
|
||
).first()
|
||
|
||
if not dummy_cell:
|
||
return JsonResponse({'error': 'No cells found for this client'})
|
||
|
||
# Trigger calculation
|
||
updated = save_view.calculate_top_left_dependents(sheet, dummy_cell)
|
||
|
||
# Get updated cell values
|
||
cells = Cell.objects.filter(
|
||
sheet=sheet,
|
||
table_type='top_left',
|
||
client=client
|
||
).order_by('row_index')
|
||
|
||
cell_data = []
|
||
for cell in cells:
|
||
cell_data.append({
|
||
'row_index': cell.row_index,
|
||
'ui_row': cell.row_index + 1,
|
||
'excel_ref': f"B{cell.row_index + 3}",
|
||
'value': str(cell.value) if cell.value else 'None',
|
||
'description': self.get_row_description(cell.row_index)
|
||
})
|
||
|
||
return JsonResponse({
|
||
'sheet': f"{sheet.year}-{sheet.month:02d}",
|
||
'client': client.name,
|
||
'cells': cell_data,
|
||
'updated_count': len(updated),
|
||
'calculation': 'B5 = IF(B3>0; B3-B4; 0)'
|
||
})
|
||
|
||
except Exception as e:
|
||
return JsonResponse({'error': str(e)}, status=400)
|
||
|
||
def get_row_description(self, row_index):
|
||
"""Get description for row index"""
|
||
descriptions = {
|
||
0: "B3: Stand der Gaszähler (Nm³)",
|
||
1: "B4: Stand der Gaszähler (Vormonat) (Nm³)",
|
||
2: "B5: Gasrückführung (Nm³)",
|
||
3: "B6: Rückführung flüssig (Lit. L-He)",
|
||
4: "B7: Sonderrückführungen (Lit. L-He)",
|
||
5: "B8: Bestand in Kannen-1 (Lit. L-He)",
|
||
6: "B9: Summe Bestand (Lit. L-He)",
|
||
7: "B10: Best. in Kannen Vormonat (Lit. L-He)",
|
||
8: "B11: Bezug (Liter L-He)",
|
||
9: "B12: Rückführ. Soll (Lit. L-He)",
|
||
10: "B13: Verluste (Soll-Rückf.) (Lit. L-He)",
|
||
11: "B14: Füllungen warm (Lit. L-He)",
|
||
12: "B15: Kaltgas Rückgabe (Lit. L-He) – Faktor",
|
||
13: "B16: Faktor",
|
||
14: "B17: Verbraucherverluste (Liter L-He)",
|
||
15: "B18: %"
|
||
}
|
||
return descriptions.get(row_index, f"Row {row_index}")
|
||
def recalculate_stand_der_gaszahler(self, sheet):
|
||
"""Recalculate Stand der Gaszähler for all client pairs"""
|
||
from decimal import Decimal
|
||
|
||
# For Dr. Fohrer and AG Buntk. (L & M columns)
|
||
try:
|
||
# Get Dr. Fohrer's bezug
|
||
dr_fohrer_client = Client.objects.get(name="Dr. Fohrer")
|
||
dr_fohrer_cell = Cell.objects.filter(
|
||
sheet=sheet,
|
||
table_type='top_right',
|
||
client=dr_fohrer_client,
|
||
row_index=9 # Row 9 = Bezug
|
||
).first()
|
||
L13 = Decimal(str(dr_fohrer_cell.value)) if dr_fohrer_cell and dr_fohrer_cell.value else Decimal('0')
|
||
|
||
# Get AG Buntk.'s bezug
|
||
ag_buntk_client = Client.objects.get(name="AG Buntk.")
|
||
ag_buntk_cell = Cell.objects.filter(
|
||
sheet=sheet,
|
||
table_type='top_right',
|
||
client=ag_buntk_client,
|
||
row_index=9
|
||
).first()
|
||
M13 = Decimal(str(ag_buntk_cell.value)) if ag_buntk_cell and ag_buntk_cell.value else Decimal('0')
|
||
|
||
total = L13 + M13
|
||
if total > 0:
|
||
# Update Dr. Fohrer's row 0
|
||
dr_fohrer_row0 = Cell.objects.filter(
|
||
sheet=sheet,
|
||
table_type='top_right',
|
||
client=dr_fohrer_client,
|
||
row_index=0
|
||
).first()
|
||
if dr_fohrer_row0:
|
||
dr_fohrer_row0.value = L13 / total
|
||
dr_fohrer_row0.save()
|
||
|
||
# Update AG Buntk.'s row 0
|
||
ag_buntk_row0 = Cell.objects.filter(
|
||
sheet=sheet,
|
||
table_type='top_right',
|
||
client=ag_buntk_client,
|
||
row_index=0
|
||
).first()
|
||
if ag_buntk_row0:
|
||
ag_buntk_row0.value = M13 / total
|
||
ag_buntk_row0.save()
|
||
except Exception as e:
|
||
print(f"Error recalculating Stand der Gaszähler for Dr. Fohrer/AG Buntk.: {e}")
|
||
|
||
# For AG Alff and AG Gutfl. (N & O columns)
|
||
try:
|
||
# Get AG Alff's bezug
|
||
ag_alff_client = Client.objects.get(name="AG Alff")
|
||
ag_alff_cell = Cell.objects.filter(
|
||
sheet=sheet,
|
||
table_type='top_right',
|
||
client=ag_alff_client,
|
||
row_index=9
|
||
).first()
|
||
N13 = Decimal(str(ag_alff_cell.value)) if ag_alff_cell and ag_alff_cell.value else Decimal('0')
|
||
|
||
# Get AG Gutfl.'s bezug
|
||
ag_gutfl_client = Client.objects.get(name="AG Gutfl.")
|
||
ag_gutfl_cell = Cell.objects.filter(
|
||
sheet=sheet,
|
||
table_type='top_right',
|
||
client=ag_gutfl_client,
|
||
row_index=9
|
||
).first()
|
||
O13 = Decimal(str(ag_gutfl_cell.value)) if ag_gutfl_cell and ag_gutfl_cell.value else Decimal('0')
|
||
|
||
total = N13 + O13
|
||
if total > 0:
|
||
# Update AG Alff's row 0
|
||
ag_alff_row0 = Cell.objects.filter(
|
||
sheet=sheet,
|
||
table_type='top_right',
|
||
client=ag_alff_client,
|
||
row_index=0
|
||
).first()
|
||
if ag_alff_row0:
|
||
ag_alff_row0.value = N13 / total
|
||
ag_alff_row0.save()
|
||
|
||
# Update AG Gutfl.'s row 0
|
||
ag_gutfl_row0 = Cell.objects.filter(
|
||
sheet=sheet,
|
||
table_type='top_right',
|
||
client=ag_gutfl_client,
|
||
row_index=0
|
||
).first()
|
||
if ag_gutfl_row0:
|
||
ag_gutfl_row0.value = O13 / total
|
||
ag_gutfl_row0.save()
|
||
except Exception as e:
|
||
print(f"Error recalculating Stand der Gaszähler for AG Alff/AG Gutfl.: {e}")
|
||
# In your SaveCellsView class in views.py
|
||
class DebugTopRightView(View):
|
||
"""Debug view to check top_right calculations"""
|
||
def get(self, request):
|
||
sheet_id = request.GET.get('sheet_id', 1)
|
||
client_name = request.GET.get('client', 'Dr. Fohrer')
|
||
|
||
try:
|
||
sheet = MonthlySheet.objects.get(id=sheet_id)
|
||
client = Client.objects.get(name=client_name)
|
||
|
||
# Get all cells for this client in top_right
|
||
cells = Cell.objects.filter(
|
||
sheet=sheet,
|
||
table_type='top_right',
|
||
client=client
|
||
).order_by('row_index')
|
||
|
||
cell_data = []
|
||
descriptions = {
|
||
0: "Stand der Gaszähler (Vormonat)",
|
||
1: "Gasrückführung (Nm³)",
|
||
2: "Rückführung flüssig",
|
||
3: "Sonderrückführungen",
|
||
4: "Sammelrückführungen",
|
||
5: "Bestand in Kannen-1",
|
||
6: "Summe Bestand",
|
||
7: "Best. in Kannen Vormonat",
|
||
8: "Same as row 9 from prev sheet",
|
||
9: "Bezug",
|
||
10: "Rückführ. Soll",
|
||
11: "Verluste",
|
||
12: "Füllungen warm",
|
||
13: "Kaltgas Rückgabe",
|
||
14: "Faktor 0.06",
|
||
15: "Verbraucherverluste",
|
||
16: "%"
|
||
}
|
||
|
||
for cell in cells:
|
||
cell_data.append({
|
||
'row_index': cell.row_index,
|
||
'ui_row': cell.row_index + 1,
|
||
'description': descriptions.get(cell.row_index, f"Row {cell.row_index}"),
|
||
'value': str(cell.value) if cell.value else 'Empty',
|
||
'cell_id': cell.id
|
||
})
|
||
|
||
# Test calculation
|
||
row3_cell = cells.filter(row_index=3).first()
|
||
row5_cell = cells.filter(row_index=5).first()
|
||
row6_cell = cells.filter(row_index=6).first()
|
||
|
||
calculation_info = {
|
||
'row3_value': str(row3_cell.value) if row3_cell and row3_cell.value else '0',
|
||
'row5_value': str(row5_cell.value) if row5_cell and row5_cell.value else '0',
|
||
'row6_value': str(row6_cell.value) if row6_cell and row6_cell.value else '0',
|
||
'expected_sum': '0'
|
||
}
|
||
|
||
if row3_cell and row5_cell and row6_cell:
|
||
row3_val = Decimal(str(row3_cell.value)) if row3_cell.value else Decimal('0')
|
||
row5_val = Decimal(str(row5_cell.value)) if row5_cell.value else Decimal('0')
|
||
expected = row3_val + row5_val
|
||
calculation_info['expected_sum'] = str(expected)
|
||
calculation_info['is_correct'] = row6_cell.value == expected
|
||
|
||
return JsonResponse({
|
||
'sheet': f"{sheet.year}-{sheet.month}",
|
||
'client': client.name,
|
||
'cells': cell_data,
|
||
'calculation': calculation_info
|
||
})
|
||
|
||
except Exception as e:
|
||
return JsonResponse({'error': str(e)}, status=400)
|
||
|
||
|
||
class SaveCellsView(View):
|
||
|
||
def calculate_bottom_3_dependents(self, sheet):
|
||
updated_cells = []
|
||
|
||
def get_cell(row_idx, col_idx):
|
||
return Cell.objects.filter(
|
||
sheet=sheet,
|
||
table_type='bottom_3',
|
||
row_index=row_idx,
|
||
column_index=col_idx
|
||
).first()
|
||
|
||
def set_cell(row_idx, col_idx, value):
|
||
cell, created = Cell.objects.get_or_create(
|
||
sheet=sheet,
|
||
table_type='bottom_3',
|
||
row_index=row_idx,
|
||
column_index=col_idx,
|
||
defaults={'value': value}
|
||
)
|
||
if not created and cell.value != value:
|
||
cell.value = value
|
||
cell.save()
|
||
elif created:
|
||
cell.save()
|
||
|
||
updated_cells.append({
|
||
'id': cell.id,
|
||
'value': str(cell.value) if cell.value is not None else '',
|
||
'is_calculated': True,
|
||
})
|
||
|
||
def dec(x):
|
||
if x in (None, ''):
|
||
return Decimal('0')
|
||
try:
|
||
return Decimal(str(x))
|
||
except Exception:
|
||
return Decimal('0')
|
||
|
||
# ---- current summary ----
|
||
cur_sum = MonthlySummary.objects.filter(sheet=sheet).first()
|
||
curr_k44 = dec(cur_sum.gesamtbestand_neu_lhe) if cur_sum else Decimal('0')
|
||
total_verb = dec(cur_sum.verbraucherverlust_lhe) if cur_sum else Decimal('0')
|
||
|
||
# ---- previous month summary ----
|
||
year, month = sheet.year, sheet.month
|
||
if month == 1:
|
||
prev_year, prev_month = year - 1, 12
|
||
else:
|
||
prev_year, prev_month = year, month - 1
|
||
|
||
prev_sheet = MonthlySheet.objects.filter(year=prev_year, month=prev_month).first()
|
||
if prev_sheet:
|
||
prev_sum = MonthlySummary.objects.filter(sheet=prev_sheet).first()
|
||
prev_k44 = dec(prev_sum.gesamtbestand_neu_lhe) if prev_sum else Decimal('0')
|
||
else:
|
||
prev_k44 = Decimal('0')
|
||
|
||
# ---- read editable inputs from bottom_3: F47,G47,I47,I50 ----
|
||
def get_val(r, c):
|
||
cell = get_cell(r, c)
|
||
return dec(cell.value if cell else None)
|
||
|
||
f47 = get_val(1, 0)
|
||
g47 = get_val(1, 1)
|
||
i47 = get_val(1, 2)
|
||
i50 = get_val(4, 2)
|
||
|
||
# Now apply your formulas using prev_k44, curr_k44, total_verb, f47,g47,i47,i50
|
||
# Row indices: 0..7 correspond to 46..53
|
||
# col 3 = J, col 4 = K
|
||
|
||
# Row 46
|
||
k46 = prev_k44
|
||
j46 = k46 * Decimal('0.75')
|
||
set_cell(0, 3, j46)
|
||
set_cell(0, 4, k46)
|
||
|
||
# Row 47
|
||
g47 = self._dec((get_cell(1, 1) or {}).value if get_cell(1, 1) else None)
|
||
i47 = self._dec((get_cell(1, 2) or {}).value if get_cell(1, 2) else None)
|
||
|
||
j47 = g47 + i47
|
||
k47 = (j47 / Decimal('0.75')) + g47 if j47 != 0 else g47
|
||
|
||
set_cell(1, 3, j47)
|
||
set_cell(1, 4, k47)
|
||
|
||
# Row 48
|
||
k48 = k46 + k47
|
||
j48 = k48 * Decimal('0.75')
|
||
set_cell(2, 3, j48)
|
||
set_cell(2, 4, k48)
|
||
|
||
# Row 49
|
||
k49 = curr_k44
|
||
j49 = k49 * Decimal('0.75')
|
||
set_cell(3, 3, j49)
|
||
set_cell(3, 4, k49)
|
||
|
||
# Row 50
|
||
j50 = i50
|
||
k50 = j50 / Decimal('0.75') if j50 != 0 else Decimal('0')
|
||
set_cell(4, 3, j50)
|
||
set_cell(4, 4, k50)
|
||
|
||
# Row 51
|
||
k51 = k48 - k49 - k50
|
||
j51 = k51 * Decimal('0.75')
|
||
set_cell(5, 3, j51)
|
||
set_cell(5, 4, k51)
|
||
|
||
# Row 52
|
||
k52 = total_verb
|
||
j52 = k52 * Decimal('0.75')
|
||
set_cell(6, 3, j52)
|
||
set_cell(6, 4, k52)
|
||
|
||
# Row 53
|
||
j53 = j51 - j52
|
||
k53 = k51 - k52
|
||
set_cell(7, 3, j53)
|
||
set_cell(7, 4, k53)
|
||
|
||
return updated_cells
|
||
|
||
|
||
def _dec(self, value):
|
||
"""Convert value to Decimal or return 0."""
|
||
if value is None or value == '':
|
||
return Decimal('0')
|
||
try:
|
||
return Decimal(str(value))
|
||
except Exception:
|
||
return Decimal('0')
|
||
|
||
def post(self, request, *args, **kwargs):
|
||
"""
|
||
Handle AJAX saves from monthly_sheet.html
|
||
- Single-cell save: when cell_id is present (blur on one cell)
|
||
- Bulk save: when the 'Save All Cells' button is used (no cell_id)
|
||
"""
|
||
try:
|
||
sheet_id = request.POST.get('sheet_id')
|
||
if not sheet_id:
|
||
return JsonResponse({
|
||
'status': 'error',
|
||
'message': 'Missing sheet_id'
|
||
})
|
||
|
||
sheet = MonthlySheet.objects.get(id=sheet_id)
|
||
|
||
# -------- Single-cell update (blur) --------
|
||
cell_id = request.POST.get('cell_id')
|
||
if cell_id:
|
||
value_raw = (request.POST.get('value') or '').strip()
|
||
|
||
try:
|
||
cell = Cell.objects.get(id=cell_id, sheet=sheet)
|
||
except Cell.DoesNotExist:
|
||
return JsonResponse({
|
||
'status': 'error',
|
||
'message': 'Cell not found'
|
||
})
|
||
|
||
# Convert value to Decimal or None
|
||
if value_raw == '':
|
||
new_value = None
|
||
else:
|
||
try:
|
||
# Allow comma or dot
|
||
value_clean = value_raw.replace(',', '.')
|
||
new_value = Decimal(value_clean)
|
||
except (InvalidOperation, ValueError):
|
||
# If conversion fails, treat as empty
|
||
new_value = None
|
||
|
||
old_value = cell.value
|
||
cell.value = new_value
|
||
cell.save()
|
||
|
||
updated_cells = [{
|
||
'id': cell.id,
|
||
'value': '' if cell.value is None else str(cell.value),
|
||
'is_calculated': cell.is_formula, # model field
|
||
}]
|
||
|
||
# Recalculate dependents depending on table_type
|
||
if cell.table_type == 'top_left':
|
||
updated_cells.extend(
|
||
self.calculate_top_left_dependents(sheet, cell)
|
||
)
|
||
elif cell.table_type == 'top_right':
|
||
updated_cells.extend(
|
||
self.calculate_top_right_dependents(sheet, cell)
|
||
)
|
||
elif cell.table_type == 'bottom_1':
|
||
updated_cells.extend(self.calculate_bottom_1_dependents(sheet, cell))
|
||
elif cell.table_type == 'bottom_3':
|
||
updated_cells.extend(
|
||
self.calculate_bottom_3_dependents(sheet)
|
||
)
|
||
# bottom_1 / bottom_2 / bottom_3 currently have no formulas:
|
||
# they just save the new value.
|
||
updated_cells += self.calculate_bottom_3_dependents(sheet)
|
||
return JsonResponse({
|
||
'status': 'success',
|
||
'updated_cells': updated_cells
|
||
})
|
||
|
||
|
||
# -------- Bulk save (Save All button) --------
|
||
return self.save_bulk_cells(request, sheet)
|
||
|
||
except MonthlySheet.DoesNotExist:
|
||
return JsonResponse({
|
||
'status': 'error',
|
||
'message': 'Sheet not found'
|
||
})
|
||
except Exception as e:
|
||
# Generic safety net so the frontend sees an error message
|
||
return JsonResponse({
|
||
'status': 'error',
|
||
'message': str(e)
|
||
})
|
||
|
||
# ... your other methods above ...
|
||
|
||
# Update the calculate_top_right_dependents method in SaveCellsView class
|
||
|
||
def calculate_top_right_dependents(self, sheet, changed_cell):
|
||
"""
|
||
Recalculate all dependent cells in the top-right table according to Excel formulas.
|
||
|
||
Excel rows (4-20) -> 0-based indices (0-15)
|
||
Rows:
|
||
0: Stand der Gaszähler (Vormonat) (Nm³) - shares for M3 clients
|
||
1: Gasrückführung (Nm³)
|
||
2: Rückführung flüssig (Lit. L-He)
|
||
3: Sonderrückführungen (Lit. L-He) - editable
|
||
4: Sammelrückführungen (Lit. L-He) - from helium_input groups
|
||
5: Bestand in Kannen-1 (Lit. L-He) - editable, merged in pairs
|
||
6: Summe Bestand (Lit. L-He) = row 5
|
||
7: Best. in Kannen Vormonat (Lit. L-He) - from previous month
|
||
8: Bezug (Liter L-He) - from SecondTableEntry
|
||
9: Rückführ. Soll (Lit. L-He) - calculated
|
||
10: Verluste (Soll-Rückf.) (Lit. L-He) - calculated
|
||
11: Füllungen warm (Lit. L-He) - from SecondTableEntry warm outputs
|
||
12: Kaltgas Rückgabe (Lit. L-He) – Faktor - calculated
|
||
13: Faktor 0.06 - fixed
|
||
14: Verbraucherverluste (Liter L-He) - calculated
|
||
15: % - calculated
|
||
"""
|
||
from decimal import Decimal
|
||
from django.db.models import Sum, Count
|
||
from django.db.models.functions import Coalesce
|
||
from .models import Client, Cell, ExcelEntry, SecondTableEntry
|
||
|
||
TOP_RIGHT_CLIENTS = [
|
||
"Dr. Fohrer", # L
|
||
"AG Buntk.", # M (merged with L)
|
||
"AG Alff", # N
|
||
"AG Gutfl.", # O (merged with N)
|
||
"M3 Thiele", # P
|
||
"M3 Buntkowsky", # Q
|
||
"M3 Gutfleisch", # R
|
||
]
|
||
|
||
# Define merged pairs
|
||
MERGED_PAIRS = [
|
||
("Dr. Fohrer", "AG Buntk."), # L and M are merged
|
||
("AG Alff", "AG Gutfl."), # N and O are merged
|
||
]
|
||
|
||
# M3 clients (not merged, calculated individually)
|
||
M3_CLIENTS = ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"]
|
||
|
||
# Groups for Sammelrückführungen (helium_input)
|
||
HELIUM_INPUT_GROUPS = {
|
||
"fohrer_buntk": ["Dr. Fohrer", "AG Buntk."],
|
||
"alff_gutfl": ["AG Alff", "AG Gutfl."],
|
||
"m3": ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"],
|
||
}
|
||
|
||
year = sheet.year
|
||
month = sheet.month
|
||
factor = Decimal('0.06') # Fixed factor from Excel
|
||
updated_cells = []
|
||
|
||
# Helper functions
|
||
def get_val(client_name, row_idx):
|
||
"""Get cell value for a client and row"""
|
||
try:
|
||
client = Client.objects.get(name=client_name)
|
||
col_idx = TOP_RIGHT_CLIENTS.index(client_name)
|
||
cell = Cell.objects.filter(
|
||
sheet=sheet,
|
||
table_type='top_right',
|
||
client=client,
|
||
row_index=row_idx,
|
||
column_index=col_idx
|
||
).first()
|
||
if cell and cell.value is not None:
|
||
return Decimal(str(cell.value))
|
||
except (Client.DoesNotExist, ValueError, KeyError):
|
||
pass
|
||
return Decimal('0')
|
||
|
||
def set_val(client_name, row_idx, value, is_calculated=True):
|
||
"""Set cell value for a client and row"""
|
||
try:
|
||
client = Client.objects.get(name=client_name)
|
||
col_idx = TOP_RIGHT_CLIENTS.index(client_name)
|
||
|
||
cell, created = Cell.objects.get_or_create(
|
||
sheet=sheet,
|
||
table_type='top_right',
|
||
client=client,
|
||
row_index=row_idx,
|
||
column_index=col_idx,
|
||
defaults={'value': value}
|
||
)
|
||
|
||
# Only update if value changed
|
||
if not created and cell.value != value:
|
||
cell.value = value
|
||
cell.save()
|
||
elif created:
|
||
cell.save()
|
||
|
||
updated_cells.append({
|
||
'id': cell.id,
|
||
'value': str(cell.value) if cell.value else '',
|
||
'is_calculated': is_calculated
|
||
})
|
||
|
||
return True
|
||
except (Client.DoesNotExist, ValueError):
|
||
return False
|
||
|
||
# 1. Update Summe Bestand (row 6) from Bestand in Kannen-1 (row 5)
|
||
# For merged pairs: copy value from changed cell to its pair
|
||
if changed_cell and changed_cell.table_type == 'top_right':
|
||
if changed_cell.row_index == 5: # Bestand in Kannen-1
|
||
client_name = changed_cell.client.name
|
||
new_value = changed_cell.value
|
||
|
||
# Check if this client is in a merged pair
|
||
for pair in MERGED_PAIRS:
|
||
if client_name in pair:
|
||
# Update both clients in the pair
|
||
for client_in_pair in pair:
|
||
if client_in_pair != client_name:
|
||
set_val(client_in_pair, 5, new_value, is_calculated=False)
|
||
break
|
||
|
||
# 2. For all clients: Set Summe Bestand (row 6) = Bestand in Kannen-1 (row 5)
|
||
for client_name in TOP_RIGHT_CLIENTS:
|
||
bestand_value = get_val(client_name, 5)
|
||
set_val(client_name, 6, bestand_value, is_calculated=True)
|
||
|
||
# 3. Update Sammelrückführungen (row 4) from helium_input groups
|
||
for group_name, client_names in HELIUM_INPUT_GROUPS.items():
|
||
# Get total helium_input for this group
|
||
clients_in_group = Client.objects.filter(name__in=client_names)
|
||
total_helium = ExcelEntry.objects.filter(
|
||
client__in=clients_in_group,
|
||
date__year=year,
|
||
date__month=month
|
||
).aggregate(total=Coalesce(Sum('lhe_ges'), Decimal('0')))['total']
|
||
|
||
# Set same value for all clients in group
|
||
for client_name in client_names:
|
||
set_val(client_name, 4, total_helium, is_calculated=True)
|
||
|
||
# 4. Calculate Rückführung flüssig (row 2)
|
||
# For merged pairs: =L8 for Dr. Fohrer/AG Buntk., =N8 for AG Alff/AG Gutfl.
|
||
# For M3 clients: =$P$8 * P4, $P$8 * Q4, $P$8 * R4
|
||
|
||
# Get Sammelrückführungen values for groups
|
||
sammel_fohrer_buntk = get_val("Dr. Fohrer", 4) # L8
|
||
sammel_alff_gutfl = get_val("AG Alff", 4) # N8
|
||
sammel_m3_group = get_val("M3 Thiele", 4) # P8 (same for all M3)
|
||
|
||
# For merged pairs
|
||
set_val("Dr. Fohrer", 2, sammel_fohrer_buntk, is_calculated=True)
|
||
set_val("AG Buntk.", 2, sammel_fohrer_buntk, is_calculated=True)
|
||
set_val("AG Alff", 2, sammel_alff_gutfl, is_calculated=True)
|
||
set_val("AG Gutfl.", 2, sammel_alff_gutfl, is_calculated=True)
|
||
|
||
# For M3 clients: =$P$8 * column4 (Stand der Gaszähler)
|
||
for m3_client in M3_CLIENTS:
|
||
stand_value = get_val(m3_client, 0) # Stand der Gaszähler (row 0)
|
||
rueck_value = sammel_m3_group * stand_value
|
||
set_val(m3_client, 2, rueck_value, is_calculated=True)
|
||
|
||
# 5. Calculate Füllungen warm (row 11) from SecondTableEntry warm outputs
|
||
# 5. Calculate Füllungen warm (row 11) as NUMBER of warm fillings
|
||
# (sum of 1s where each warm SecondTableEntry is one filling)
|
||
for client_name in TOP_RIGHT_CLIENTS:
|
||
client = Client.objects.get(name=client_name)
|
||
warm_count = SecondTableEntry.objects.filter(
|
||
client=client,
|
||
date__year=year,
|
||
date__month=month,
|
||
is_warm=True
|
||
).aggregate(
|
||
total=Coalesce(Count('id'), 0)
|
||
)['total']
|
||
|
||
# store as Decimal so later formulas (warm * 15) still work nicely
|
||
warm_value = Decimal(warm_count)
|
||
set_val(client_name, 11, warm_value, is_calculated=True)
|
||
# 6. Set Faktor row (13) to 0.06
|
||
for client_name in TOP_RIGHT_CLIENTS:
|
||
set_val(client_name, 13, factor, is_calculated=True)
|
||
|
||
# 6a. Recalculate Stand der Gaszähler (row 0) for the merged pairs
|
||
# according to Excel:
|
||
# L4 = L13 / (L13 + M13), M4 = M13 / (L13 + M13)
|
||
# N4 = N13 / (N13 + O13), O4 = O13 / (N13 + O13)
|
||
|
||
# Pair 1: Dr. Fohrer / AG Buntk.
|
||
bezug_dr = get_val("Dr. Fohrer", 8) # L13
|
||
bezug_buntk = get_val("AG Buntk.", 8) # M13
|
||
total_pair1 = bezug_dr + bezug_buntk
|
||
|
||
if total_pair1 != 0:
|
||
set_val("Dr. Fohrer", 0, bezug_dr / total_pair1, is_calculated=True)
|
||
set_val("AG Buntk.", 0, bezug_buntk / total_pair1, is_calculated=True)
|
||
else:
|
||
# if no Bezug, both shares are 0
|
||
set_val("Dr. Fohrer", 0, Decimal('0'), is_calculated=True)
|
||
set_val("AG Buntk.", 0, Decimal('0'), is_calculated=True)
|
||
|
||
# Pair 2: AG Alff / AG Gutfl.
|
||
bezug_alff = get_val("AG Alff", 8) # N13
|
||
bezug_gutfl = get_val("AG Gutfl.", 8) # O13
|
||
total_pair2 = bezug_alff + bezug_gutfl
|
||
|
||
if total_pair2 != 0:
|
||
set_val("AG Alff", 0, bezug_alff / total_pair2, is_calculated=True)
|
||
set_val("AG Gutfl.", 0, bezug_gutfl / total_pair2, is_calculated=True)
|
||
else:
|
||
set_val("AG Alff", 0, Decimal('0'), is_calculated=True)
|
||
set_val("AG Gutfl.", 0, Decimal('0'), is_calculated=True)
|
||
|
||
|
||
# 7. Calculate all other dependent rows for merged pairs
|
||
for pair in MERGED_PAIRS:
|
||
client1, client2 = pair
|
||
|
||
# Get values for the pair
|
||
bezug1 = get_val(client1, 8) # Bezug client1
|
||
bezug2 = get_val(client2, 8) # Bezug client2
|
||
total_bezug = bezug1 + bezug2 # L13+M13 or N13+O13
|
||
|
||
summe_bestand = get_val(client1, 6) # L11 or N11 (merged, same value)
|
||
best_vormonat = get_val(client1, 7) # L12 or N12 (merged, same value)
|
||
rueck_fl = get_val(client1, 2) # L6 or N6 (merged, same value)
|
||
warm1 = get_val(client1, 11) # L16 or N16
|
||
warm2 = get_val(client2, 11) # M16 or O16
|
||
total_warm = warm1 + warm2 # L16+M16 or N16+O16
|
||
|
||
# Calculate Rückführ. Soll (row 9)
|
||
# = L13+M13 - L11 + L12 for first pair
|
||
# = N13+O13 - N11 + N12 for second pair
|
||
rueck_soll = total_bezug - summe_bestand + best_vormonat
|
||
set_val(client1, 9, rueck_soll, is_calculated=True)
|
||
set_val(client2, 9, rueck_soll, is_calculated=True)
|
||
|
||
# Calculate Verluste (row 10) = Rückführ. Soll - Rückführung flüssig
|
||
verluste = rueck_soll - rueck_fl
|
||
set_val(client1, 10, verluste, is_calculated=True)
|
||
set_val(client2, 10, verluste, is_calculated=True)
|
||
|
||
# Calculate Kaltgas Rückgabe (row 12)
|
||
# = (L13+M13)*$A18 + (L16+M16)*15
|
||
kaltgas = (total_bezug * factor) + (total_warm * Decimal('15'))
|
||
set_val(client1, 12, kaltgas, is_calculated=True)
|
||
set_val(client2, 12, kaltgas, is_calculated=True)
|
||
|
||
# Calculate Verbraucherverluste (row 14) = Verluste - Kaltgas
|
||
verbrauch = verluste - kaltgas
|
||
set_val(client1, 14, verbrauch, is_calculated=True)
|
||
set_val(client2, 14, verbrauch, is_calculated=True)
|
||
|
||
# Calculate % (row 15) = Verbraucherverluste / (L13+M13)
|
||
if total_bezug != 0:
|
||
prozent = verbrauch / total_bezug
|
||
else:
|
||
prozent = Decimal('0')
|
||
set_val(client1, 15, prozent, is_calculated=True)
|
||
set_val(client2, 15, prozent, is_calculated=True)
|
||
|
||
# 8. Calculate all dependent rows for M3 clients (individual calculations)
|
||
for m3_client in M3_CLIENTS:
|
||
# Get individual values
|
||
bezug = get_val(m3_client, 8) # Bezug for this M3 client
|
||
summe_bestand = get_val(m3_client, 6) # Summe Bestand
|
||
best_vormonat = get_val(m3_client, 7) # Best. in Kannen Vormonat
|
||
rueck_fl = get_val(m3_client, 2) # Rückführung flüssig
|
||
warm = get_val(m3_client, 11) # Füllungen warm
|
||
|
||
# Calculate Rückführ. Soll (row 9) = Bezug - Summe Bestand + Best. Vormonat
|
||
rueck_soll = bezug - summe_bestand + best_vormonat
|
||
set_val(m3_client, 9, rueck_soll, is_calculated=True)
|
||
|
||
# Calculate Verluste (row 10) = Rückführ. Soll - Rückführung flüssig
|
||
verluste = rueck_soll - rueck_fl
|
||
set_val(m3_client, 10, verluste, is_calculated=True)
|
||
|
||
# Calculate Kaltgas Rückgabe (row 12) = Bezug * factor + warm * 15
|
||
kaltgas = (bezug * factor) + (warm * Decimal('15'))
|
||
set_val(m3_client, 12, kaltgas, is_calculated=True)
|
||
|
||
# Calculate Verbraucherverluste (row 14) = Verluste - Kaltgas
|
||
verbrauch = verluste - kaltgas
|
||
set_val(m3_client, 14, verbrauch, is_calculated=True)
|
||
|
||
# Calculate % (row 15) = Verbraucherverluste / Bezug
|
||
if bezug != 0:
|
||
prozent = verbrauch / bezug
|
||
else:
|
||
prozent = Decimal('0')
|
||
set_val(m3_client, 15, prozent, is_calculated=True)
|
||
|
||
return updated_cells
|
||
|
||
|
||
|
||
def calculate_bottom_1_dependents(self, sheet, changed_cell):
|
||
"""
|
||
Recalculate Bottom Table 1 (table_type='bottom_1').
|
||
|
||
Layout (row_index 0–9, col_index 0–4):
|
||
|
||
Rows 0–8:
|
||
0: Batterie 1
|
||
1: 2
|
||
2: 3
|
||
3: 4
|
||
4: 5
|
||
5: 6
|
||
6: 2 Bündel
|
||
7: 2 Ballone
|
||
8: Reingasspeicher
|
||
|
||
Row 9:
|
||
9: Gasbestand (totals row)
|
||
|
||
Columns:
|
||
0: Volumen (fixed values, not editable)
|
||
1: bar (editable for rows 0–8)
|
||
2: korrigiert = bar / (bar/2000 + 1)
|
||
3: Nm³ = Volumen * korrigiert
|
||
4: Lit. LHe = Nm³ / 0.75
|
||
|
||
Row 9:
|
||
- Volumen (col 0): empty
|
||
- bar (col 1): empty
|
||
- korrigiert (col 2): empty
|
||
- Nm³ (col 3): SUM Nm³ rows 0–8
|
||
- Lit. LHe (col 4): SUM Lit. LHe rows 0–8
|
||
"""
|
||
from decimal import Decimal, InvalidOperation
|
||
from .models import Cell
|
||
|
||
updated_cells = []
|
||
|
||
DATA_ROWS = list(range(0, 9)) # 0–8
|
||
TOTAL_ROW = 9
|
||
|
||
COL_VOL = 0
|
||
COL_BAR = 1
|
||
COL_KORR = 2
|
||
COL_NM3 = 3
|
||
COL_LHE = 4
|
||
|
||
# Fixed Volumen values for rows 0–8
|
||
VOLUMES = [
|
||
Decimal("2.4"), # row 0
|
||
Decimal("5.1"), # row 1
|
||
Decimal("4.0"), # row 2
|
||
Decimal("1.0"), # row 3
|
||
Decimal("4.0"), # row 4
|
||
Decimal("0.4"), # row 5
|
||
Decimal("1.2"), # row 6
|
||
Decimal("20.0"), # row 7
|
||
Decimal("5.0"), # row 8
|
||
]
|
||
|
||
def get_cell(row_idx, col_idx):
|
||
return Cell.objects.filter(
|
||
sheet=sheet,
|
||
table_type="bottom_1",
|
||
row_index=row_idx,
|
||
column_index=col_idx,
|
||
).first()
|
||
|
||
def set_calc(row_idx, col_idx, value):
|
||
"""
|
||
Set a calculated cell and add it to updated_cells.
|
||
If value is None, we clear the cell.
|
||
"""
|
||
cell = get_cell(row_idx, col_idx)
|
||
if not cell:
|
||
cell = Cell(
|
||
sheet=sheet,
|
||
table_type="bottom_1",
|
||
row_index=row_idx,
|
||
column_index=col_idx,
|
||
value=value,
|
||
)
|
||
else:
|
||
cell.value = value
|
||
cell.save()
|
||
|
||
updated_cells.append(
|
||
{
|
||
"id": cell.id,
|
||
"value": "" if cell.value is None else str(cell.value),
|
||
"is_calculated": True,
|
||
}
|
||
)
|
||
|
||
# ---------- Rows 0–8: per-gasspeicher calculations ----------
|
||
for row_idx in DATA_ROWS:
|
||
bar_cell = get_cell(row_idx, COL_BAR)
|
||
|
||
# Volumen: fixed constant or, if present, value from DB
|
||
vol = VOLUMES[row_idx]
|
||
vol_cell = get_cell(row_idx, COL_VOL)
|
||
if vol_cell and vol_cell.value is not None:
|
||
try:
|
||
vol = Decimal(str(vol_cell.value))
|
||
except (InvalidOperation, ValueError):
|
||
# fall back to fixed constant
|
||
vol = VOLUMES[row_idx]
|
||
|
||
bar = None
|
||
try:
|
||
if bar_cell and bar_cell.value is not None:
|
||
bar = Decimal(str(bar_cell.value))
|
||
except (InvalidOperation, ValueError):
|
||
bar = None
|
||
|
||
# korrigiert = bar / (bar/2000 + 1)
|
||
if bar is not None and bar != 0:
|
||
korr = bar / (bar / Decimal("2000") + Decimal("1"))
|
||
else:
|
||
korr = None
|
||
|
||
# Nm³ = Volumen * korrigiert
|
||
if korr is not None:
|
||
nm3 = vol * korr
|
||
else:
|
||
nm3 = None
|
||
|
||
# Lit. LHe = Nm³ / 0.75
|
||
if nm3 is not None:
|
||
lit_lhe = nm3 / Decimal("0.75")
|
||
else:
|
||
lit_lhe = None
|
||
|
||
# Write calculated cells back (NOT Volumen or bar)
|
||
set_calc(row_idx, COL_KORR, korr)
|
||
set_calc(row_idx, COL_NM3, nm3)
|
||
set_calc(row_idx, COL_LHE, lit_lhe)
|
||
|
||
# ---------- Row 9: totals (Gasbestand) ----------
|
||
total_nm3 = Decimal("0")
|
||
total_lhe = Decimal("0")
|
||
has_nm3 = False
|
||
has_lhe = False
|
||
|
||
for row_idx in DATA_ROWS:
|
||
nm3_cell = get_cell(row_idx, COL_NM3)
|
||
if nm3_cell and nm3_cell.value is not None:
|
||
try:
|
||
total_nm3 += Decimal(str(nm3_cell.value))
|
||
has_nm3 = True
|
||
except (InvalidOperation, ValueError):
|
||
pass
|
||
|
||
lhe_cell = get_cell(row_idx, COL_LHE)
|
||
if lhe_cell and lhe_cell.value is not None:
|
||
try:
|
||
total_lhe += Decimal(str(lhe_cell.value))
|
||
has_lhe = True
|
||
except (InvalidOperation, ValueError):
|
||
pass
|
||
|
||
# Volumen (0), bar (1), korrigiert (2) on total row stay empty
|
||
set_calc(TOTAL_ROW, COL_KORR, None) # explicitly clear korrigiert
|
||
set_calc(TOTAL_ROW, COL_NM3, total_nm3 if has_nm3 else None)
|
||
set_calc(TOTAL_ROW, COL_LHE, total_lhe if has_lhe else None)
|
||
|
||
return updated_cells
|
||
|
||
|
||
|
||
|
||
|
||
def calculate_top_left_dependents(self, sheet, changed_cell):
|
||
"""Calculate dependent cells in top_left table"""
|
||
from decimal import Decimal
|
||
from django.db.models import Sum
|
||
from django.db.models.functions import Coalesce
|
||
|
||
client_id = changed_cell.client_id
|
||
updated_cells = []
|
||
|
||
# Get all cells for this client in top_left table
|
||
client_cells = Cell.objects.filter(
|
||
sheet=sheet,
|
||
table_type='top_left',
|
||
client_id=client_id
|
||
)
|
||
|
||
# Create a dict for easy access
|
||
cell_dict = {}
|
||
for cell in client_cells:
|
||
cell_dict[cell.row_index] = cell
|
||
|
||
# Get values with safe defaults
|
||
def get_cell_value(row_idx):
|
||
cell = cell_dict.get(row_idx)
|
||
if cell and cell.value is not None:
|
||
try:
|
||
return Decimal(str(cell.value))
|
||
except:
|
||
return Decimal('0')
|
||
return Decimal('0')
|
||
|
||
# 1. B5 = IF(B3>0; B3-B4; 0) - Gasrückführung
|
||
b3 = get_cell_value(0) # row_index 0 = Excel B3 (UI row 1)
|
||
b4 = get_cell_value(1) # row_index 1 = Excel B4 (UI row 2)
|
||
|
||
# Calculate B5
|
||
if b3 > 0:
|
||
b5_value = b3 - b4
|
||
if b5_value < 0:
|
||
b5_value = Decimal('0')
|
||
else:
|
||
b5_value = Decimal('0')
|
||
|
||
# Update B5 - FORCE update even if value appears the same
|
||
b5_cell = cell_dict.get(2) # row_index 2 = Excel B5 (UI row 3)
|
||
if b5_cell:
|
||
# Always update B5 when B3 or B4 changes
|
||
b5_cell.value = b5_value
|
||
b5_cell.save()
|
||
updated_cells.append({
|
||
'id': b5_cell.id,
|
||
'value': str(b5_cell.value),
|
||
'is_calculated': True
|
||
})
|
||
|
||
# 2. B6 = B5 / 0.75 - Rückführung flüssig
|
||
b6_value = b5_value / Decimal('0.75')
|
||
b6_cell = cell_dict.get(3) # row_index 3 = Excel B6 (UI row 4)
|
||
if b6_cell:
|
||
b6_cell.value = b6_value
|
||
b6_cell.save()
|
||
updated_cells.append({
|
||
'id': b6_cell.id,
|
||
'value': str(b6_cell.value),
|
||
'is_calculated': True
|
||
})
|
||
|
||
# 3. B9 = B7 + B8 - Summe Bestand
|
||
b7 = get_cell_value(4) # row_index 4 = Excel B7 (UI row 5)
|
||
b8 = get_cell_value(5) # row_index 5 = Excel B8 (UI row 6)
|
||
b9_value = b7 + b8
|
||
b9_cell = cell_dict.get(6) # row_index 6 = Excel B9 (UI row 7)
|
||
if b9_cell:
|
||
b9_cell.value = b9_value
|
||
b9_cell.save()
|
||
updated_cells.append({
|
||
'id': b9_cell.id,
|
||
'value': str(b9_cell.value),
|
||
'is_calculated': True
|
||
})
|
||
|
||
# 4. B11 = Sum of LHe Output from SecondTableEntry for this client/month - Bezug
|
||
from .models import SecondTableEntry
|
||
client = changed_cell.client
|
||
|
||
# Calculate total LHe output for this client in this month
|
||
# 4. B11 = Bezug (Liter L-He)
|
||
# For start sheet: manual entry, for other sheets: auto-calculated from SecondTableEntry
|
||
from .models import SecondTableEntry
|
||
client = changed_cell.client
|
||
|
||
b11_cell = cell_dict.get(8) # row_index 8 = Excel B11 (UI row 9)
|
||
|
||
# Check if this is the start sheet (2025-01)
|
||
is_start_sheet = (sheet.year == 2025 and sheet.month == 1)
|
||
|
||
# Get the B11 value for calculations
|
||
b11_value = Decimal('0')
|
||
|
||
if is_start_sheet:
|
||
# For start sheet, keep whatever value is already there (manual entry)
|
||
if b11_cell and b11_cell.value is not None:
|
||
b11_value = Decimal(str(b11_cell.value))
|
||
else:
|
||
# For non-start sheets: auto-calculate from SecondTableEntry
|
||
lhe_output_sum = SecondTableEntry.objects.filter(
|
||
client=client,
|
||
date__year=sheet.year,
|
||
date__month=sheet.month
|
||
).aggregate(
|
||
total=Coalesce(Sum('lhe_output'), Decimal('0'))
|
||
)['total']
|
||
|
||
b11_value = lhe_output_sum
|
||
|
||
if b11_cell and (b11_cell.value != b11_value or b11_cell.value is None):
|
||
b11_cell.value = b11_value
|
||
b11_cell.save()
|
||
updated_cells.append({
|
||
'id': b11_cell.id,
|
||
'value': str(b11_cell.value),
|
||
'is_calculated': True # Calculated from SecondTableEntry
|
||
})
|
||
|
||
# 5. B12 = B11 + B10 - B9 - Rückführ. Soll
|
||
b10 = get_cell_value(7) # row_index 7 = Excel B10 (UI row 8)
|
||
b12_value = b11_value + b10 - b9_value # Use b11_value instead of lhe_output_sum
|
||
b12_cell = cell_dict.get(9) # row_index 9 = Excel B12 (UI row 10)
|
||
if b12_cell:
|
||
b12_cell.value = b12_value
|
||
b12_cell.save()
|
||
updated_cells.append({
|
||
'id': b12_cell.id,
|
||
'value': str(b12_cell.value),
|
||
'is_calculated': True
|
||
})
|
||
|
||
# 6. B13 = B12 - B6 - Verluste (Soll-Rückf.)
|
||
b13_value = b12_value - b6_value
|
||
b13_cell = cell_dict.get(10) # row_index 10 = Excel B13 (UI row 11)
|
||
if b13_cell:
|
||
b13_cell.value = b13_value
|
||
b13_cell.save()
|
||
updated_cells.append({
|
||
'id': b13_cell.id,
|
||
'value': str(b13_cell.value),
|
||
'is_calculated': True
|
||
})
|
||
|
||
# 7. B14 = Count of warm outputs
|
||
warm_count = SecondTableEntry.objects.filter(
|
||
client=client,
|
||
date__year=sheet.year,
|
||
date__month=sheet.month,
|
||
is_warm=True
|
||
).count()
|
||
|
||
b14_cell = cell_dict.get(11) # row_index 11 = Excel B14 (UI row 12)
|
||
if b14_cell:
|
||
b14_cell.value = Decimal(str(warm_count))
|
||
b14_cell.save()
|
||
updated_cells.append({
|
||
'id': b14_cell.id,
|
||
'value': str(b14_cell.value),
|
||
'is_calculated': True
|
||
})
|
||
|
||
# 8. B15 = IF(B11>0; B11 * factor + B14 * 15; 0) - Kaltgas Rückgabe
|
||
factor = get_cell_value(13) # row_index 13 = Excel B16 (Faktor) (UI row 14)
|
||
if factor == 0:
|
||
factor = Decimal('0.06') # default factor
|
||
|
||
if b11_value > 0: # Use b11_value
|
||
b15_value = b11_value * factor + Decimal(str(warm_count)) * Decimal('15')
|
||
else:
|
||
b15_value = Decimal('0')
|
||
|
||
b15_cell = cell_dict.get(12) # row_index 12 = Excel B15 (UI row 13)
|
||
if b15_cell:
|
||
b15_cell.value = b15_value
|
||
b15_cell.save()
|
||
updated_cells.append({
|
||
'id': b15_cell.id,
|
||
'value': str(b15_cell.value),
|
||
'is_calculated': True
|
||
})
|
||
|
||
# 9. B17 = B13 - B15 - Verbraucherverluste
|
||
b17_value = b13_value - b15_value
|
||
b17_cell = cell_dict.get(14) # row_index 14 = Excel B17 (UI row 15)
|
||
if b17_cell:
|
||
b17_cell.value = b17_value
|
||
b17_cell.save()
|
||
updated_cells.append({
|
||
'id': b17_cell.id,
|
||
'value': str(b17_cell.value),
|
||
'is_calculated': True
|
||
})
|
||
|
||
# 10. B18 = IF(B11=0; 0; B17/B11) - %
|
||
if b11_value == 0: # Use b11_value
|
||
b18_value = Decimal('0')
|
||
else:
|
||
b18_value = b17_value / b11_value # Use b11_value
|
||
|
||
b18_cell = cell_dict.get(15) # row_index 15 = Excel B18 (UI row 16)
|
||
if b18_cell:
|
||
b18_cell.value = b18_value
|
||
b18_cell.save()
|
||
updated_cells.append({
|
||
'id': b18_cell.id,
|
||
'value': str(b18_cell.value),
|
||
'is_calculated': True
|
||
})
|
||
|
||
return updated_cells
|
||
def save_bulk_cells(self, request, sheet):
|
||
"""Original bulk save logic (for backward compatibility)"""
|
||
# Get all cell updates
|
||
cell_updates = {}
|
||
for key, value in request.POST.items():
|
||
if key.startswith('cell_'):
|
||
cell_id = key.replace('cell_', '')
|
||
cell_updates[cell_id] = value
|
||
|
||
# Update cells and track which ones changed
|
||
updated_cells = []
|
||
changed_clients = set()
|
||
|
||
for cell_id, new_value in cell_updates.items():
|
||
try:
|
||
cell = Cell.objects.get(id=cell_id, sheet=sheet)
|
||
old_value = cell.value
|
||
|
||
# Convert new value
|
||
try:
|
||
if new_value.strip():
|
||
cell.value = Decimal(new_value)
|
||
else:
|
||
cell.value = None
|
||
except Exception:
|
||
cell.value = None
|
||
|
||
# Only save if value changed
|
||
if cell.value != old_value:
|
||
cell.save()
|
||
updated_cells.append({
|
||
'id': cell.id,
|
||
'value': str(cell.value) if cell.value else ''
|
||
})
|
||
# bottom_3 has no client, so this will just add None for those cells,
|
||
# which is harmless. Top-left cells still add their real client_id.
|
||
changed_clients.add(cell.client_id)
|
||
|
||
except Cell.DoesNotExist:
|
||
continue
|
||
|
||
# Recalculate for each changed client (top-left tables)
|
||
for client_id in changed_clients:
|
||
if client_id is not None:
|
||
self.recalculate_top_left_table(sheet, client_id)
|
||
|
||
# --- NEW: recalc bottom_3 for the whole sheet, independent of clients ---
|
||
bottom3_updates = self.calculate_bottom_3_dependents(sheet)
|
||
|
||
# Get all updated cells for response (top-left)
|
||
all_updated_cells = []
|
||
for client_id in changed_clients:
|
||
if client_id is None:
|
||
continue # skip bottom_3 / non-client cells
|
||
client_cells = Cell.objects.filter(
|
||
sheet=sheet,
|
||
client_id=client_id
|
||
)
|
||
for cell in client_cells:
|
||
all_updated_cells.append({
|
||
'id': cell.id,
|
||
'value': str(cell.value) if cell.value else '',
|
||
'is_calculated': cell.is_formula
|
||
})
|
||
|
||
# Add bottom_3 recalculated cells (J46..K53, etc.)
|
||
all_updated_cells.extend(bottom3_updates)
|
||
|
||
return JsonResponse({
|
||
'status': 'success',
|
||
'message': f'Saved {len(updated_cells)} cells',
|
||
'updated_cells': all_updated_cells
|
||
})
|
||
|
||
|
||
def recalculate_top_left_table(self, sheet, client_id):
|
||
"""Recalculate the top-left table for a specific client"""
|
||
from decimal import Decimal
|
||
|
||
# Get all cells for this client in top_left table
|
||
cells = Cell.objects.filter(
|
||
sheet=sheet,
|
||
table_type='top_left',
|
||
client_id=client_id
|
||
).order_by('row_index')
|
||
|
||
# Create a dictionary of cell values
|
||
cell_dict = {}
|
||
for cell in cells:
|
||
cell_dict[cell.row_index] = cell
|
||
|
||
# Excel logic implementation for top-left table
|
||
# B3 (row_index 0) = Stand der Gaszähler (Nm³) - manual
|
||
# B4 (row_index 1) = Stand der Gaszähler (Vormonat) (Nm³) - from previous sheet
|
||
|
||
# Get B3 and B4
|
||
b3_cell = cell_dict.get(0) # UI Row 3
|
||
b4_cell = cell_dict.get(1) # UI Row 4
|
||
|
||
if b3_cell and b3_cell.value and b4_cell and b4_cell.value:
|
||
# B5 = IF(B3>0; B3-B4; 0)
|
||
b3 = Decimal(str(b3_cell.value))
|
||
b4 = Decimal(str(b4_cell.value))
|
||
|
||
if b3 > 0:
|
||
b5 = b3 - b4
|
||
if b5 < 0:
|
||
b5 = Decimal('0')
|
||
else:
|
||
b5 = Decimal('0')
|
||
|
||
# Update B5 (row_index 2)
|
||
b5_cell = cell_dict.get(2)
|
||
if b5_cell and (b5_cell.value != b5 or b5_cell.value is None):
|
||
b5_cell.value = b5
|
||
b5_cell.save()
|
||
|
||
# B6 = B5 / 0.75 (row_index 3)
|
||
b6 = b5 / Decimal('0.75')
|
||
b6_cell = cell_dict.get(3)
|
||
if b6_cell and (b6_cell.value != b6 or b6_cell.value is None):
|
||
b6_cell.value = b6
|
||
b6_cell.save()
|
||
|
||
# Get previous month's sheet for B10
|
||
if sheet.month == 1:
|
||
prev_year = sheet.year - 1
|
||
prev_month = 12
|
||
else:
|
||
prev_year = sheet.year
|
||
prev_month = sheet.month - 1
|
||
|
||
prev_sheet = MonthlySheet.objects.filter(
|
||
year=prev_year,
|
||
month=prev_month
|
||
).first()
|
||
|
||
if prev_sheet:
|
||
# Get B9 from previous sheet (row_index 7 in previous)
|
||
prev_b9 = Cell.objects.filter(
|
||
sheet=prev_sheet,
|
||
table_type='top_left',
|
||
client_id=client_id,
|
||
row_index=7 # UI Row 9
|
||
).first()
|
||
|
||
if prev_b9 and prev_b9.value:
|
||
# Update B10 in current sheet (row_index 8)
|
||
b10_cell = cell_dict.get(8)
|
||
if b10_cell and (b10_cell.value != prev_b9.value or b10_cell.value is None):
|
||
b10_cell.value = prev_b9.value
|
||
b10_cell.save()
|
||
@method_decorator(csrf_exempt, name='dispatch')
|
||
class SaveMonthSummaryView(View):
|
||
"""
|
||
Saves per-month summary values such as K44 (Gesamtbestand neu).
|
||
Called from JS after 'Save All' finishes.
|
||
"""
|
||
|
||
def post(self, request, *args, **kwargs):
|
||
try:
|
||
data = json.loads(request.body.decode('utf-8'))
|
||
except json.JSONDecodeError:
|
||
return JsonResponse(
|
||
{'success': False, 'message': 'Invalid JSON'},
|
||
status=400
|
||
)
|
||
|
||
sheet_id = data.get('sheet_id')
|
||
if not sheet_id:
|
||
return JsonResponse(
|
||
{'success': False, 'message': 'Missing sheet_id'},
|
||
status=400
|
||
)
|
||
|
||
try:
|
||
sheet = MonthlySheet.objects.get(id=sheet_id)
|
||
except MonthlySheet.DoesNotExist:
|
||
return JsonResponse(
|
||
{'success': False, 'message': 'Sheet not found'},
|
||
status=404
|
||
)
|
||
|
||
# More tolerant decimal conversion: accepts "123.45" and "123,45"
|
||
def to_decimal(value):
|
||
if value is None:
|
||
return None
|
||
s = str(value).strip()
|
||
if s == '':
|
||
return None
|
||
s = s.replace(',', '.')
|
||
try:
|
||
return Decimal(s)
|
||
except (InvalidOperation, ValueError):
|
||
# Debug: show what failed in the dev server console
|
||
print("SaveMonthSummaryView to_decimal failed for:", repr(value))
|
||
return None
|
||
|
||
raw_k44 = data.get('gesamtbestand_neu_lhe')
|
||
raw_gas = data.get('gasbestand_lhe')
|
||
raw_verb = data.get('verbraucherverlust_lhe')
|
||
|
||
gesamtbestand_neu_lhe = to_decimal(raw_k44)
|
||
gasbestand_lhe = to_decimal(raw_gas)
|
||
verbraucherverlust_lhe = to_decimal(raw_verb)
|
||
|
||
summary, created = MonthlySummary.objects.get_or_create(sheet=sheet)
|
||
|
||
if gesamtbestand_neu_lhe is not None:
|
||
summary.gesamtbestand_neu_lhe = gesamtbestand_neu_lhe
|
||
if gasbestand_lhe is not None:
|
||
summary.gasbestand_lhe = gasbestand_lhe
|
||
if verbraucherverlust_lhe is not None:
|
||
summary.verbraucherverlust_lhe = verbraucherverlust_lhe
|
||
|
||
summary.save()
|
||
|
||
# Small debug output so you can see in the server console what was saved
|
||
print(
|
||
f"Saved MonthlySummary for {sheet.year}-{sheet.month:02d}: "
|
||
f"K44={summary.gesamtbestand_neu_lhe}, "
|
||
f"Gasbestand={summary.gasbestand_lhe}, "
|
||
f"Verbraucherverlust={summary.verbraucherverlust_lhe}"
|
||
)
|
||
|
||
return JsonResponse({'success': True})
|
||
|
||
|
||
|
||
# Calculate View (placeholder for calculations)
|
||
class CalculateView(View):
|
||
def post(self, request):
|
||
# This will be implemented when you provide calculation rules
|
||
return JsonResponse({
|
||
'status': 'success',
|
||
'message': 'Calculation endpoint ready'
|
||
})
|
||
|
||
# Summary Sheet View
|
||
class SummarySheetView(TemplateView):
|
||
template_name = 'summary_sheet.html'
|
||
|
||
def get_context_data(self, **kwargs):
|
||
context = super().get_context_data(**kwargs)
|
||
start_month = int(self.kwargs.get('start_month', 1))
|
||
year = int(self.kwargs.get('year', datetime.now().year))
|
||
|
||
# Get 6 monthly sheets
|
||
months = [(year, m) for m in range(start_month, start_month + 6)]
|
||
sheets = MonthlySheet.objects.filter(
|
||
year=year,
|
||
month__in=list(range(start_month, start_month + 6))
|
||
).order_by('month')
|
||
|
||
# Aggregate data across months
|
||
summary_data = self.calculate_summary(sheets)
|
||
|
||
context.update({
|
||
'year': year,
|
||
'start_month': start_month,
|
||
'end_month': start_month + 5,
|
||
'sheets': sheets,
|
||
'clients': Client.objects.all(),
|
||
'summary_data': summary_data,
|
||
})
|
||
return context
|
||
|
||
def calculate_summary(self, sheets):
|
||
"""Calculate totals across 6 months"""
|
||
summary = {}
|
||
|
||
for client in Client.objects.all():
|
||
client_total = 0
|
||
for sheet in sheets:
|
||
# Get specific cells and sum
|
||
cells = sheet.cells.filter(
|
||
client=client,
|
||
table_type='top_left',
|
||
row_index=0 # Example: first row
|
||
)
|
||
for cell in cells:
|
||
if cell.value:
|
||
try:
|
||
client_total += float(cell.value)
|
||
except (ValueError, TypeError):
|
||
continue
|
||
|
||
summary[client.id] = client_total
|
||
|
||
return summary
|
||
|
||
# Existing views below (keep all your existing functions)
|
||
def clients_list(request):
|
||
# --- Clients for the yearly summary table ---
|
||
clients = Client.objects.all()
|
||
|
||
# --- Available years for output data (same as before) ---
|
||
available_years_qs = SecondTableEntry.objects.dates('date', 'year', order='DESC')
|
||
available_years = [y.year for y in available_years_qs]
|
||
|
||
# === 1) Year used for the "Helium Output Yearly Summary" table ===
|
||
# Uses ?year=... in the dropdown
|
||
year_param = request.GET.get('year')
|
||
if year_param:
|
||
selected_year = int(year_param)
|
||
else:
|
||
selected_year = available_years[0] if available_years else datetime.now().year
|
||
|
||
# === 2) GLOBAL half-year interval (shared with other pages) =======
|
||
# Try GET params first
|
||
interval_year_param = request.GET.get('interval_year')
|
||
start_month_param = request.GET.get('interval_start_month')
|
||
|
||
# Fallbacks from session
|
||
session_year = request.session.get('halfyear_year')
|
||
session_start_month = request.session.get('halfyear_start_month')
|
||
|
||
# Determine final interval_year
|
||
if interval_year_param:
|
||
interval_year = int(interval_year_param)
|
||
elif session_year:
|
||
interval_year = int(session_year)
|
||
else:
|
||
# default: same as selected_year for summary
|
||
interval_year = selected_year
|
||
|
||
# Determine final interval_start_month
|
||
if start_month_param:
|
||
interval_start_month = int(start_month_param)
|
||
elif session_start_month:
|
||
interval_start_month = int(session_start_month)
|
||
else:
|
||
interval_start_month = 1 # default Jan
|
||
|
||
# Store back into the session so other views can read them
|
||
request.session['halfyear_year'] = interval_year
|
||
request.session['halfyear_start_month'] = interval_start_month
|
||
|
||
# === 3) Build a 6-month window, allowing wrap into the next year ===
|
||
# Example: interval_year=2025, interval_start_month=12
|
||
# window = [(2025,12), (2026,1), (2026,2), (2026,3), (2026,4), (2026,5)]
|
||
window = []
|
||
for offset in range(6):
|
||
total_index = (interval_start_month - 1) + offset # 0-based index
|
||
y = interval_year + (total_index // 12)
|
||
m = (total_index % 12) + 1
|
||
window.append((y, m))
|
||
|
||
# === 4) Totals per client in that 6-month window ==================
|
||
monthly_data = []
|
||
for client in clients:
|
||
monthly_totals = []
|
||
for (y, m) in window:
|
||
total = SecondTableEntry.objects.filter(
|
||
client=client,
|
||
date__year=y,
|
||
date__month=m
|
||
).aggregate(
|
||
total=Coalesce(
|
||
Sum('lhe_output'),
|
||
Value(0, output_field=DecimalField())
|
||
)
|
||
)['total']
|
||
monthly_totals.append(total)
|
||
|
||
monthly_data.append({
|
||
'client': client,
|
||
'monthly_totals': monthly_totals,
|
||
'year_total': sum(monthly_totals),
|
||
})
|
||
|
||
# === 5) Month labels for the header (only the 6-month window) =====
|
||
month_labels = [calendar.month_abbr[m] for (y, m) in window]
|
||
|
||
# === 6) FINALLY: return the response ==============================
|
||
return render(request, 'clients_table.html', {
|
||
'available_years': available_years,
|
||
'current_year': selected_year, # used by year dropdown
|
||
'interval_year': interval_year, # used by "Global 6-Month Interval" form
|
||
'interval_start_month': interval_start_month,
|
||
'months': month_labels,
|
||
'monthly_data': monthly_data,
|
||
})
|
||
|
||
# === 5) Month labels for the header (only the 6-month window) =====
|
||
month_labels = [calendar.month_abbr[m] for (y, m) in window]
|
||
|
||
|
||
def set_halfyear_interval(request):
|
||
if request.method == 'POST':
|
||
year = int(request.POST.get('year'))
|
||
start_month = int(request.POST.get('start_month'))
|
||
|
||
request.session['halfyear_year'] = year
|
||
request.session['halfyear_start_month'] = start_month
|
||
|
||
return redirect(request.META.get('HTTP_REFERER', 'clients_list'))
|
||
|
||
return redirect('clients_list')
|
||
# Table One View (ExcelEntry)
|
||
def table_one_view(request):
|
||
from .models import ExcelEntry, Client, Institute
|
||
|
||
# --- Base queryset for the main Helium Input table ---
|
||
base_entries = ExcelEntry.objects.all().select_related('client', 'client__institute')
|
||
|
||
# Read the global 6-month interval from the session
|
||
interval_year = request.session.get('halfyear_year')
|
||
interval_start = request.session.get('halfyear_start_month')
|
||
|
||
if interval_year and interval_start:
|
||
interval_year = int(interval_year)
|
||
interval_start = int(interval_start)
|
||
|
||
# Build the same 6-month window as on the main page (can cross year)
|
||
window = []
|
||
for offset in range(6):
|
||
total_index = (interval_start - 1) + offset # 0-based
|
||
y = interval_year + (total_index // 12)
|
||
m = (total_index % 12) + 1
|
||
window.append((y, m))
|
||
|
||
# Build Q filter: (year=m_year AND month=m_month) for any of those 6
|
||
q = Q()
|
||
for (y, m) in window:
|
||
q |= Q(date__year=y, date__month=m)
|
||
|
||
entries_table1 = base_entries.filter(q).order_by('-date')
|
||
else:
|
||
# Fallback: if no global interval yet, show everything
|
||
entries_table1 = base_entries.order_by('-date')
|
||
clients = Client.objects.all().select_related('institute')
|
||
institutes = Institute.objects.all()
|
||
|
||
# ---- Overview filters ----
|
||
# years present in ExcelEntry.date
|
||
year_qs = ExcelEntry.objects.dates('date', 'year', order='DESC')
|
||
available_years = [d.year for d in year_qs]
|
||
|
||
# default year/start month
|
||
# default year/start month (if no global interval yet)
|
||
if available_years:
|
||
default_year = available_years[0] # newest year in ExcelEntry
|
||
else:
|
||
default_year = timezone.now().year
|
||
|
||
# 🔸 Read global half-year interval from session (set on main page)
|
||
session_year = request.session.get('halfyear_year')
|
||
session_start = request.session.get('halfyear_start_month')
|
||
|
||
# If the user has set a global interval, use it.
|
||
# Otherwise fall back to default year / January.
|
||
year = int(session_year) if session_year else int(default_year)
|
||
start_month = int(session_start) if session_start else 1
|
||
|
||
|
||
# six-month window
|
||
# --- Build a 6-month window, allowing wrap into the next year ---
|
||
# Example: year=2025, start_month=10
|
||
# window = [(2025,10), (2025,11), (2025,12), (2026,1), (2026,2), (2026,3)]
|
||
window = []
|
||
for offset in range(6):
|
||
total_index = (start_month - 1) + offset # 0-based
|
||
y_for_month = year + (total_index // 12)
|
||
m_for_month = (total_index % 12) + 1
|
||
window.append((y_for_month, m_for_month))
|
||
|
||
overview = None
|
||
|
||
if window:
|
||
# Build per-group data
|
||
groups_entries = [] # for internal calculations
|
||
|
||
for key, group in CLIENT_GROUPS.items():
|
||
clients_qs = get_group_clients(key)
|
||
|
||
values = []
|
||
group_total = Decimal('0')
|
||
|
||
for (y_m, m_m) in window:
|
||
total = ExcelEntry.objects.filter(
|
||
client__in=clients_qs,
|
||
date__year=y_m,
|
||
date__month=m_m
|
||
).aggregate(
|
||
total=Coalesce(Sum('lhe_ges'), Decimal('0'))
|
||
)['total']
|
||
|
||
values.append(total)
|
||
group_total += total
|
||
|
||
groups_entries.append({
|
||
'key': key,
|
||
'label': group['label'],
|
||
'values': values,
|
||
'total': group_total,
|
||
})
|
||
|
||
# month totals across all groups
|
||
month_totals = []
|
||
for idx in range(len(window)):
|
||
s = sum((g['values'][idx] for g in groups_entries), Decimal('0'))
|
||
month_totals.append(s)
|
||
|
||
grand_total = sum(month_totals, Decimal('0'))
|
||
|
||
# Build rows for the template
|
||
rows = []
|
||
for idx, (y_m, m_m) in enumerate(window):
|
||
row_values = [g['values'][idx] for g in groups_entries]
|
||
rows.append({
|
||
'month_number': m_m,
|
||
'month_label': calendar.month_name[m_m],
|
||
'values': row_values,
|
||
'total': month_totals[idx],
|
||
})
|
||
|
||
groups_meta = [{'key': g['key'], 'label': g['label']} for g in groups_entries]
|
||
group_totals = [g['total'] for g in groups_entries]
|
||
|
||
# Start/end for display – include years so wrap is clear
|
||
start_year = window[0][0]
|
||
start_month_disp = window[0][1]
|
||
end_year = window[-1][0]
|
||
end_month_disp = window[-1][1]
|
||
|
||
overview = {
|
||
'year': year, # keep for backwards compatibility if needed
|
||
'start_month': start_month_disp,
|
||
'start_year': start_year,
|
||
'end_month': end_month_disp,
|
||
'end_year': end_year,
|
||
'rows': rows,
|
||
'groups': groups_meta,
|
||
'group_totals': group_totals,
|
||
'grand_total': grand_total,
|
||
}
|
||
|
||
|
||
# Month dropdown labels
|
||
MONTH_CHOICES = [
|
||
(1, 'Jan'), (2, 'Feb'), (3, 'Mar'), (4, 'Apr'),
|
||
(5, 'May'), (6, 'Jun'), (7, 'Jul'), (8, 'Aug'),
|
||
(9, 'Sep'), (10, 'Oct'), (11, 'Nov'), (12, 'Dec'),
|
||
]
|
||
|
||
return render(request, 'table_one.html', {
|
||
'entries_table1': entries_table1,
|
||
'clients': clients,
|
||
'institutes': institutes,
|
||
'available_years': available_years,
|
||
'month_choices': MONTH_CHOICES,
|
||
'overview': overview,
|
||
})
|
||
|
||
# Table Two View (SecondTableEntry)
|
||
def table_two_view(request):
|
||
try:
|
||
clients = Client.objects.all().select_related('institute')
|
||
institutes = Institute.objects.all()
|
||
|
||
# 🔸 Read global half-year interval from session
|
||
interval_year = request.session.get('halfyear_year')
|
||
interval_start = request.session.get('halfyear_start_month')
|
||
|
||
if interval_year and interval_start:
|
||
interval_year = int(interval_year)
|
||
interval_start = int(interval_start)
|
||
|
||
# Build the same 6-month window as in clients_list (can cross years)
|
||
window = []
|
||
for offset in range(6):
|
||
total_index = (interval_start - 1) + offset # 0-based
|
||
y = interval_year + (total_index // 12)
|
||
m = (total_index % 12) + 1
|
||
window.append((y, m))
|
||
|
||
# Build a Q object matching any of those (year, month) pairs
|
||
q = Q()
|
||
for (y, m) in window:
|
||
q |= Q(date__year=y, date__month=m)
|
||
|
||
entries = SecondTableEntry.objects.filter(q).order_by('-date')
|
||
else:
|
||
# Fallback if no global interval yet: show all
|
||
entries = SecondTableEntry.objects.all().order_by('-date')
|
||
|
||
return render(request, 'table_two.html', {
|
||
'entries_table2': entries,
|
||
'clients': clients,
|
||
'institutes': institutes,
|
||
'interval_year': interval_year,
|
||
'interval_start_month': interval_start,
|
||
})
|
||
|
||
except Exception as e:
|
||
return render(request, 'table_two.html', {
|
||
'error_message': f"Failed to load data: {str(e)}",
|
||
'entries_table2': [],
|
||
'clients': clients,
|
||
'institutes': institutes,
|
||
})
|
||
|
||
|
||
def monthly_sheet_root(request):
|
||
"""
|
||
Redirect /sheet/ to the sheet matching the globally selected
|
||
half-year start (year + month). If not set, fall back to latest.
|
||
"""
|
||
year = request.session.get('halfyear_year')
|
||
start_month = request.session.get('halfyear_start_month')
|
||
|
||
if year and start_month:
|
||
try:
|
||
year = int(year)
|
||
start_month = int(start_month)
|
||
return redirect('monthly_sheet', year=year, month=start_month)
|
||
except ValueError:
|
||
pass # fall through
|
||
|
||
# Fallback: latest MonthlySheet if exists
|
||
latest_sheet = MonthlySheet.objects.order_by('-year', '-month').first()
|
||
if latest_sheet:
|
||
return redirect('monthly_sheet', year=latest_sheet.year, month=latest_sheet.month)
|
||
else:
|
||
now = timezone.now()
|
||
return redirect('monthly_sheet', year=now.year, month=now.month)
|
||
# Add Entry (Generic)
|
||
def add_entry(request, model_name):
|
||
if request.method == 'POST':
|
||
try:
|
||
if model_name == 'SecondTableEntry':
|
||
model = SecondTableEntry
|
||
|
||
# Parse date
|
||
date_str = request.POST.get('date')
|
||
try:
|
||
date_obj = datetime.strptime(date_str, '%Y-%m-%d').date() if date_str else None
|
||
except (ValueError, TypeError):
|
||
return JsonResponse({
|
||
'status': 'error',
|
||
'message': 'Invalid date format. Use YYYY-MM-DD'
|
||
}, status=400)
|
||
|
||
# NEW: robust parse of warm flag (handles 0/1, true/false, on/off)
|
||
raw_warm = request.POST.get('is_warm')
|
||
is_warm_bool = str(raw_warm).lower() in ('1', 'true', 'on', 'yes')
|
||
|
||
# Handle Helium Output (Table Two)
|
||
lhe_output = request.POST.get('lhe_output')
|
||
|
||
entry = model.objects.create(
|
||
client=Client.objects.get(id=request.POST.get('client_id')),
|
||
date=date_obj,
|
||
is_warm=is_warm_bool, # <-- use the boolean here
|
||
lhe_delivery=request.POST.get('lhe_delivery', ''),
|
||
lhe_output=Decimal(lhe_output) if lhe_output else None,
|
||
notes=request.POST.get('notes', '')
|
||
)
|
||
|
||
return JsonResponse({
|
||
'status': 'success',
|
||
'id': entry.id,
|
||
'client_name': entry.client.name,
|
||
'institute_name': entry.client.institute.name,
|
||
'date': entry.date.strftime('%Y-%m-%d') if entry.date else '',
|
||
'is_warm': entry.is_warm,
|
||
'lhe_delivery': entry.lhe_delivery,
|
||
'lhe_output': str(entry.lhe_output) if entry.lhe_output else '',
|
||
'notes': entry.notes
|
||
})
|
||
|
||
elif model_name == 'ExcelEntry':
|
||
model = ExcelEntry
|
||
|
||
# Parse the date string into a date object
|
||
date_str = request.POST.get('date')
|
||
try:
|
||
date_obj = datetime.strptime(date_str, '%Y-%m-%d').date() if date_str else None
|
||
except (ValueError, TypeError):
|
||
date_obj = None
|
||
|
||
try:
|
||
pressure = Decimal(request.POST.get('pressure', 0))
|
||
purity = Decimal(request.POST.get('purity', 0))
|
||
druckkorrektur = Decimal(request.POST.get('druckkorrektur', 1))
|
||
lhe_zus = Decimal(request.POST.get('lhe_zus', 0))
|
||
constant_300 = Decimal(request.POST.get('constant_300', 300))
|
||
korrig_druck = Decimal(request.POST.get('korrig_druck', 0))
|
||
nm3 = Decimal(request.POST.get('nm3', 0))
|
||
lhe = Decimal(request.POST.get('lhe', 0))
|
||
lhe_ges = Decimal(request.POST.get('lhe_ges', 0))
|
||
except InvalidOperation:
|
||
return JsonResponse({
|
||
'status': 'error',
|
||
'message': 'Invalid numeric value in Helium Input'
|
||
}, status=400)
|
||
|
||
# Create the entry with ALL fields
|
||
entry = model.objects.create(
|
||
client=Client.objects.get(id=request.POST.get('client_id')),
|
||
date=date_obj,
|
||
pressure=pressure,
|
||
purity=purity,
|
||
druckkorrektur=druckkorrektur,
|
||
lhe_zus=lhe_zus,
|
||
constant_300=constant_300,
|
||
korrig_druck=korrig_druck,
|
||
nm3=nm3,
|
||
lhe=lhe,
|
||
lhe_ges=lhe_ges,
|
||
notes=request.POST.get('notes', '')
|
||
)
|
||
|
||
# Prepare the response
|
||
response_data = {
|
||
'status': 'success',
|
||
'id': entry.id,
|
||
'client_name': entry.client.name,
|
||
'institute_name': entry.client.institute.name,
|
||
'pressure': str(entry.pressure),
|
||
'purity': str(entry.purity),
|
||
'druckkorrektur': str(entry.druckkorrektur),
|
||
'constant_300': str(entry.constant_300),
|
||
'korrig_druck': str(entry.korrig_druck),
|
||
'nm3': str(entry.nm3),
|
||
'lhe': str(entry.lhe),
|
||
'lhe_zus': str(entry.lhe_zus),
|
||
'lhe_ges': str(entry.lhe_ges),
|
||
'notes': entry.notes,
|
||
}
|
||
|
||
if entry.date:
|
||
# JS uses this for the Date column and for the Month column
|
||
response_data['date'] = entry.date.strftime('%Y-%m-%d')
|
||
response_data['month'] = f"{entry.date.month:02d}"
|
||
else:
|
||
response_data['date'] = ''
|
||
response_data['month'] = ''
|
||
|
||
return JsonResponse(response_data)
|
||
|
||
|
||
except Exception as e:
|
||
return JsonResponse({'status': 'error', 'message': str(e)}, status=400)
|
||
|
||
return JsonResponse({'status': 'error', 'message': 'Invalid request'}, status=400)
|
||
|
||
# Update Entry (Generic)
|
||
def update_entry(request, model_name):
|
||
if request.method == 'POST':
|
||
try:
|
||
if model_name == 'SecondTableEntry':
|
||
model = SecondTableEntry
|
||
elif model_name == 'ExcelEntry':
|
||
model = ExcelEntry
|
||
else:
|
||
return JsonResponse({'status': 'error', 'message': 'Invalid model'}, status=400)
|
||
|
||
entry_id = int(request.POST.get('id'))
|
||
entry = model.objects.get(id=entry_id)
|
||
|
||
# Common updates for both models
|
||
entry.client = Client.objects.get(id=request.POST.get('client_id'))
|
||
entry.notes = request.POST.get('notes', '')
|
||
|
||
# Handle date properly for both models
|
||
date_str = request.POST.get('date')
|
||
if date_str:
|
||
try:
|
||
entry.date = datetime.strptime(date_str, '%Y-%m-%d').date()
|
||
except ValueError:
|
||
return JsonResponse({
|
||
'status': 'error',
|
||
'message': 'Invalid date format. Use YYYY-MM-DD'
|
||
}, status=400)
|
||
|
||
if model_name == 'SecondTableEntry':
|
||
# Handle Helium Output specific fields
|
||
|
||
raw_warm = request.POST.get('is_warm')
|
||
entry.is_warm = str(raw_warm).lower() in ('1', 'true', 'on', 'yes')
|
||
|
||
entry.lhe_delivery = request.POST.get('lhe_delivery', '')
|
||
|
||
lhe_output = request.POST.get('lhe_output')
|
||
try:
|
||
entry.lhe_output = Decimal(lhe_output) if lhe_output else None
|
||
except InvalidOperation:
|
||
return JsonResponse({
|
||
'status': 'error',
|
||
'message': 'Invalid LHe Output value'
|
||
}, status=400)
|
||
|
||
entry.save()
|
||
|
||
return JsonResponse({
|
||
'status': 'success',
|
||
'id': entry.id,
|
||
'client_name': entry.client.name,
|
||
'institute_name': entry.client.institute.name,
|
||
'date': entry.date.strftime('%Y-%m-%d') if entry.date else '',
|
||
'is_warm': entry.is_warm,
|
||
'lhe_delivery': entry.lhe_delivery,
|
||
'lhe_output': str(entry.lhe_output) if entry.lhe_output else '',
|
||
'notes': entry.notes
|
||
})
|
||
|
||
|
||
elif model_name == 'ExcelEntry':
|
||
# Handle Helium Input specific fields
|
||
try:
|
||
entry.pressure = Decimal(request.POST.get('pressure', 0))
|
||
entry.purity = Decimal(request.POST.get('purity', 0))
|
||
entry.druckkorrektur = Decimal(request.POST.get('druckkorrektur', 1))
|
||
entry.lhe_zus = Decimal(request.POST.get('lhe_zus', 0))
|
||
entry.constant_300 = Decimal(request.POST.get('constant_300', 300))
|
||
entry.korrig_druck = Decimal(request.POST.get('korrig_druck', 0))
|
||
entry.nm3 = Decimal(request.POST.get('nm3', 0))
|
||
entry.lhe = Decimal(request.POST.get('lhe', 0))
|
||
entry.lhe_ges = Decimal(request.POST.get('lhe_ges', 0))
|
||
except InvalidOperation:
|
||
return JsonResponse({
|
||
'status': 'error',
|
||
'message': 'Invalid numeric value in Helium Input'
|
||
}, status=400)
|
||
|
||
entry.save()
|
||
|
||
return JsonResponse({
|
||
'status': 'success',
|
||
'id': entry.id,
|
||
'client_name': entry.client.name,
|
||
'institute_name': entry.client.institute.name,
|
||
'date': entry.date.strftime('%Y-%m-%d') if entry.date else '',
|
||
'month': f"{entry.date.month:02d}" if entry.date else '',
|
||
'pressure': str(entry.pressure),
|
||
'purity': str(entry.purity),
|
||
'druckkorrektur': str(entry.druckkorrektur),
|
||
'constant_300': str(entry.constant_300),
|
||
'korrig_druck': str(entry.korrig_druck),
|
||
'nm3': str(entry.nm3),
|
||
'lhe': str(entry.lhe),
|
||
'lhe_zus': str(entry.lhe_zus),
|
||
'lhe_ges': str(entry.lhe_ges),
|
||
'notes': entry.notes
|
||
})
|
||
|
||
|
||
except model.DoesNotExist:
|
||
return JsonResponse({'status': 'error', 'message': 'Entry not found'}, status=404)
|
||
except Exception as e:
|
||
return JsonResponse({'status': 'error', 'message': str(e)}, status=400)
|
||
|
||
return JsonResponse({'status': 'error', 'message': 'Invalid request method'}, status=400)
|
||
|
||
# Delete Entry (Generic)
|
||
def delete_entry(request, model_name):
|
||
if request.method == 'POST':
|
||
try:
|
||
if model_name == 'SecondTableEntry':
|
||
model = SecondTableEntry
|
||
elif model_name == 'ExcelEntry':
|
||
model = ExcelEntry
|
||
else:
|
||
return JsonResponse({'status': 'error', 'message': 'Invalid model'}, status=400)
|
||
|
||
entry_id = request.POST.get('id')
|
||
entry = model.objects.get(id=entry_id)
|
||
entry.delete()
|
||
return JsonResponse({'status': 'success', 'message': 'Entry deleted'})
|
||
|
||
except model.DoesNotExist:
|
||
return JsonResponse({'status': 'error', 'message': 'Entry not found'}, status=404)
|
||
except Exception as e:
|
||
return JsonResponse({'status': 'error', 'message': str(e)}, status=400)
|
||
|
||
return JsonResponse({'status': 'error', 'message': 'Invalid request'}, status=400)
|
||
|
||
def betriebskosten_list(request):
|
||
items = Betriebskosten.objects.all().order_by('-buchungsdatum')
|
||
return render(request, 'betriebskosten_list.html', {'items': items})
|
||
|
||
def betriebskosten_create(request):
|
||
if request.method == 'POST':
|
||
try:
|
||
entry_id = request.POST.get('id')
|
||
if entry_id:
|
||
# Update existing entry
|
||
entry = Betriebskosten.objects.get(id=entry_id)
|
||
else:
|
||
# Create new entry
|
||
entry = Betriebskosten()
|
||
|
||
# Get form data
|
||
buchungsdatum_str = request.POST.get('buchungsdatum')
|
||
rechnungsnummer = request.POST.get('rechnungsnummer')
|
||
kostentyp = request.POST.get('kostentyp')
|
||
betrag = request.POST.get('betrag')
|
||
beschreibung = request.POST.get('beschreibung')
|
||
gas_volume = request.POST.get('gas_volume')
|
||
|
||
# Validate required fields
|
||
if not all([buchungsdatum_str, rechnungsnummer, kostentyp, betrag]):
|
||
return JsonResponse({'status': 'error', 'message': 'All required fields must be filled'})
|
||
|
||
# Convert date string to date object
|
||
try:
|
||
buchungsdatum = parse_date(buchungsdatum_str)
|
||
if not buchungsdatum:
|
||
return JsonResponse({'status': 'error', 'message': 'Invalid date format'})
|
||
except (ValueError, TypeError):
|
||
return JsonResponse({'status': 'error', 'message': 'Invalid date format'})
|
||
|
||
# Set entry values
|
||
entry.buchungsdatum = buchungsdatum
|
||
entry.rechnungsnummer = rechnungsnummer
|
||
entry.kostentyp = kostentyp
|
||
entry.betrag = betrag
|
||
entry.beschreibung = beschreibung
|
||
|
||
# Only set gas_volume if kostentyp is helium and gas_volume is provided
|
||
if kostentyp == 'helium' and gas_volume:
|
||
entry.gas_volume = gas_volume
|
||
else:
|
||
entry.gas_volume = None
|
||
|
||
entry.save()
|
||
|
||
return JsonResponse({
|
||
'status': 'success',
|
||
'id': entry.id,
|
||
'buchungsdatum': entry.buchungsdatum.strftime('%Y-%m-%d'),
|
||
'rechnungsnummer': entry.rechnungsnummer,
|
||
'kostentyp_display': entry.get_kostentyp_display(),
|
||
'gas_volume': str(entry.gas_volume) if entry.gas_volume else '-',
|
||
'betrag': str(entry.betrag),
|
||
'beschreibung': entry.beschreibung or ''
|
||
})
|
||
|
||
except Exception as e:
|
||
return JsonResponse({'status': 'error', 'message': str(e)})
|
||
|
||
return JsonResponse({'status': 'error', 'message': 'Invalid request method'})
|
||
|
||
def betriebskosten_delete(request):
|
||
if request.method == 'POST':
|
||
try:
|
||
entry_id = request.POST.get('id')
|
||
entry = Betriebskosten.objects.get(id=entry_id)
|
||
entry.delete()
|
||
return JsonResponse({'status': 'success'})
|
||
except Betriebskosten.DoesNotExist:
|
||
return JsonResponse({'status': 'error', 'message': 'Entry not found'})
|
||
except Exception as e:
|
||
return JsonResponse({'status': 'error', 'message': str(e)})
|
||
|
||
return JsonResponse({'status': 'error', 'message': 'Invalid request method'})
|
||
|
||
class CheckSheetView(View):
|
||
def get(self, request):
|
||
# Get current month/year
|
||
current_year = datetime.now().year
|
||
current_month = datetime.now().month
|
||
|
||
# Get all sheets
|
||
sheets = MonthlySheet.objects.all()
|
||
|
||
sheet_data = []
|
||
for sheet in sheets:
|
||
cells_count = sheet.cells.count()
|
||
# Count non-empty cells
|
||
non_empty = sheet.cells.exclude(value__isnull=True).count()
|
||
|
||
sheet_data.append({
|
||
'id': sheet.id,
|
||
'year': sheet.year,
|
||
'month': sheet.month,
|
||
'month_name': calendar.month_name[sheet.month],
|
||
'total_cells': cells_count,
|
||
'non_empty_cells': non_empty,
|
||
'has_data': non_empty > 0
|
||
})
|
||
|
||
# Also check what the default view would show
|
||
default_sheet = MonthlySheet.objects.filter(
|
||
year=current_year,
|
||
month=current_month
|
||
).first()
|
||
|
||
return JsonResponse({
|
||
'current_year': current_year,
|
||
'current_month': current_month,
|
||
'current_month_name': calendar.month_name[current_month],
|
||
'default_sheet_exists': default_sheet is not None,
|
||
'default_sheet_id': default_sheet.id if default_sheet else None,
|
||
'sheets': sheet_data,
|
||
'total_sheets': len(sheet_data)
|
||
})
|
||
|
||
|
||
class QuickDebugView(View):
|
||
def get(self, request):
|
||
# Get ALL sheets
|
||
sheets = MonthlySheet.objects.all().order_by('year', 'month')
|
||
|
||
result = {
|
||
'sheets': []
|
||
}
|
||
|
||
for sheet in sheets:
|
||
sheet_info = {
|
||
'id': sheet.id,
|
||
'display': f"{sheet.year}-{sheet.month:02d}",
|
||
'url': f"/sheet/{sheet.year}/{sheet.month}/", # CHANGED THIS LINE
|
||
'sheet_url_pattern': 'sheet/{year}/{month}/', # Add this for clarity
|
||
}
|
||
|
||
# Count cells with data for first client in top_left table
|
||
first_client = Client.objects.first()
|
||
if first_client:
|
||
test_cells = sheet.cells.filter(
|
||
client=first_client,
|
||
table_type='top_left',
|
||
row_index__in=[8, 9, 10] # Rows 9, 10, 11
|
||
).order_by('row_index')
|
||
|
||
cell_values = {}
|
||
for cell in test_cells:
|
||
cell_values[f"row_{cell.row_index}"] = str(cell.value) if cell.value else "Empty"
|
||
|
||
sheet_info['test_cells'] = cell_values
|
||
else:
|
||
sheet_info['test_cells'] = "No clients"
|
||
|
||
result['sheets'].append(sheet_info)
|
||
|
||
return JsonResponse(result)
|
||
class TestFormulaView(View):
|
||
def get(self, request):
|
||
# Test the formula evaluation directly
|
||
test_values = {
|
||
8: 2, # Row 9 value (0-based index 8)
|
||
9: 2, # Row 10 value (0-based index 9)
|
||
}
|
||
|
||
# Test formula "9 + 8" (using 0-based indices)
|
||
formula = "9 + 8"
|
||
result = evaluate_formula(formula, test_values)
|
||
|
||
return JsonResponse({
|
||
'test_values': test_values,
|
||
'formula': formula,
|
||
'result': str(result),
|
||
'note': 'Formula uses 0-based indices. 9=Row10, 8=Row9'
|
||
})
|
||
|
||
|
||
class SimpleDebugView(View):
|
||
"""Simplest debug view to check if things are working"""
|
||
def get(self, request):
|
||
sheet_id = request.GET.get('sheet_id', 1)
|
||
|
||
try:
|
||
sheet = MonthlySheet.objects.get(id=sheet_id)
|
||
|
||
# Get first client
|
||
client = Client.objects.first()
|
||
if not client:
|
||
return JsonResponse({'error': 'No clients found'})
|
||
|
||
# Check a few cells
|
||
cells = Cell.objects.filter(
|
||
sheet=sheet,
|
||
client=client,
|
||
table_type='top_left',
|
||
row_index__in=[8, 9, 10]
|
||
).order_by('row_index')
|
||
|
||
cell_data = []
|
||
for cell in cells:
|
||
cell_data.append({
|
||
'row_index': cell.row_index,
|
||
'ui_row': cell.row_index + 1,
|
||
'value': str(cell.value) if cell.value is not None else 'Empty',
|
||
'cell_id': cell.id
|
||
})
|
||
|
||
return JsonResponse({
|
||
'sheet': f"{sheet.year}-{sheet.month}",
|
||
'sheet_id': sheet.id,
|
||
'client': client.name,
|
||
'cells': cell_data,
|
||
'note': 'Row 8 = UI Row 9, Row 9 = UI Row 10, Row 10 = UI Row 11'
|
||
})
|
||
|
||
except MonthlySheet.DoesNotExist:
|
||
return JsonResponse({'error': f'Sheet with id {sheet_id} not found'})
|
||
|
||
def halfyear_settings(request):
|
||
"""
|
||
Global settings page: choose a year + first month for the 6-month interval.
|
||
These values are stored in the session and used by other views.
|
||
"""
|
||
# Determine available years from your data (use ExcelEntry or SecondTableEntry)
|
||
# Here I use SecondTableEntry; you can switch to ExcelEntry if you prefer.
|
||
available_years_qs = SecondTableEntry.objects.dates('date', 'year', order='DESC')
|
||
available_years = [d.year for d in available_years_qs]
|
||
|
||
if not available_years:
|
||
available_years = [timezone.now().year]
|
||
|
||
# Defaults (if nothing in session yet)
|
||
default_year = request.session.get('halfyear_year', available_years[0])
|
||
default_start_month = request.session.get('halfyear_start_month', 1)
|
||
|
||
if request.method == 'POST':
|
||
year = int(request.POST.get('year', default_year))
|
||
start_month = int(request.POST.get('start_month', default_start_month))
|
||
|
||
request.session['halfyear_year'] = year
|
||
request.session['halfyear_start_month'] = start_month
|
||
|
||
# Redirect back to where user came from, or to this page again
|
||
next_url = request.POST.get('next') or request.GET.get('next') or 'halfyear_settings'
|
||
return redirect(next_url)
|
||
|
||
# Month choices for the dropdown
|
||
month_choices = [(i, calendar.month_name[i]) for i in range(1, 13)]
|
||
|
||
context = {
|
||
'available_years': available_years,
|
||
'selected_year': int(default_year),
|
||
'selected_start_month': int(default_start_month),
|
||
'month_choices': month_choices,
|
||
}
|
||
return render(request, 'halfyear_settings.html', context) |