Files
he-database/sheets/views.txt

2873 lines
106 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from django.shortcuts import render, redirect
from django.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
)
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
FIRST_SHEET_YEAR = 2025
FIRST_SHEET_MONTH = 1
CLIENT_GROUPS = {
'ikp': {
'label': 'IKP',
# exactly as in the Clients admin
'names': ['IKP'],
},
'phys_chem_bunt_fohrer': {
'label': 'Buntkowsky + Dr. Fohrer',
# include all variants you might have used for Buntkowsky
'names': [
'AG Buntk.', # the one in your new entry
'AG Buntkowsky.', # from your original list
'AG Buntkowsky',
'Dr. Fohrer',
],
},
'mawi_alff_gutfleisch': {
'label': 'Alff + AG Gutfleisch',
# include both short and full forms
'names': [
'AG Alff',
'AG Gutfl.',
'AG Gutfleisch',
],
},
'm3_group': {
'label': 'M3 Buntkowsky + M3 Thiele + M3 Gutfleisch',
'names': [
'M3 Buntkowsky',
'M3 Thiele',
'M3 Gutfleisch',
],
},
}
# Add this CALCULATION_CONFIG at the top of views.py
CALCULATION_CONFIG = {
'top_left': {
# Row mappings: Django row_index (0-based) to Excel row
# Excel B4 -> Django row_index 1 (UI row 2)
# Excel B5 -> Django row_index 2 (UI row 3)
# Excel B6 -> Django row_index 3 (UI row 4)
# B6 (row_index 3) = B5 (row_index 2) / 0.75
3: "2 / 0.75",
# B11 (row_index 10) = B9 (row_index 8)
10: "8",
# B14 (row_index 13) = B13 (row_index 12) - B11 (row_index 10) + B12 (row_index 11)
13: "12 - 10 + 11",
# Note: B5, B17, B19, B20 require IF logic, so they'll be handled separately
},
# other tables (top_right, bottom_1, ...) stay as they are
' top_right': {
# UI Row 1 (Excel Row 4): Stand der Gaszähler (Vormonat) (Nm³)
0: {
'L': "9 / (9 + 9) if (9 + 9) > 0 else 0", # L4 = L13/(L13+M13)
'M': "9 / (9 + 9) if (9 + 9) > 0 else 0", # M4 = M13/(L13+M13)
'N': "9 / (9 + 9) if (9 + 9) > 0 else 0", # N4 = N13/(N13+O13)
'O': "9 / (9 + 9) if (9 + 9) > 0 else 0", # O4 = O13/(N13+O13)
'P': None, # Editable
'Q': None, # Editable
'R': None, # Editable
},
# UI Row 2 (Excel Row 5): Gasrückführung (Nm³)
1: {
'L': "4", # L5 = L8
'M': "4", # M5 = L8 (merged)
'N': "4", # N5 = N8
'O': "4", # O5 = N8 (merged)
'P': "4 * 0", # P5 = P8 * P4
'Q': "4 * 0", # Q5 = P8 * Q4
'R': "4 * 0", # R5 = P8 * R4
},
# UI Row 3 (Excel Row 6): Rückführung flüssig (Lit. L-He)
2: {
'L': "4", # L6 = L8 (Sammelrückführungen)
'M': "4",
'N': "4",
'O': "4",
'P': "4",
'Q': "4",
'R': "4",
},
# UI Row 4 (Excel Row 7): Sonderrückführungen (Lit. L-He) - EDITABLE
3: {
'L': None,
'M': None,
'N': None,
'O': None,
'P': None,
'Q': None,
'R': None,
},
# UI Row 5 (Excel Row 8): Sammelrückführungen (Lit. L-He)
4: {
'L': None, # Will be populated from ExcelEntry
'M': None,
'N': None,
'O': None,
'P': None,
'Q': None,
'R': None,
},
# UI Row 6 (Excel Row 9): Bestand in Kannen-1 (Lit. L-He) - EDITABLE
5: {
'L': None,
'M': None,
'N': None,
'O': None,
'P': None,
'Q': None,
'R': None,
},
# UI Row 7 (Excel Row 10): Summe Bestand (Lit. L-He)
6: {
'L': "3 + 5", # L10 = L7 + L9
'M': "3 + 5",
'N': "3 + 5",
'O': "3 + 5",
'P': "3 + 5",
'Q': "3 + 5",
'R': "3 + 5",
},
# UI Row 8 (Excel Row 11): Best. in Kannen Vormonat (Lit. L-He)
7: {
'L': None, # From previous sheet
'M': None,
'N': None,
'O': None,
'P': None,
'Q': None,
'R': None,
},
# UI Row 9 (Excel Row 12): same as row 9 from previous sheet
8: {
'L': None,
'M': None,
'N': None,
'O': None,
'P': None,
'Q': None,
'R': None,
},
# UI Row 10 (Excel Row 13): Bezug (Liter L-He)
9: {
'L': None, # From SecondTableEntry
'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)
10: {
'L': "9 + 9 + 7 - 8", # L14 = L13 + M13 - L11 + L12
'M': "9 + 9 + 7 - 8",
'N': "9 + 9 + 7 - 8", # N14 = N13 + O13 - N11 + N12
'O': "9 + 9 + 7 - 8",
'P': "9 + 7 - 8", # P14 = P13 + P12 - P11
'Q': "9 + 7 - 8", # Q14 = Q13 + Q12 - Q11
'R': "9 + 7 - 8", # R14 = R13 + R12 - R11
},
# UI Row 12 (Excel Row 15): Verluste (Soll-Rückf.) (Lit. L-He)
11: {
'L': "10 - 2", # L15 = L14 - L6
'M': "10 - 2",
'N': "10 - 2", # N15 = N14 - N6
'O': "10 - 2",
'P': "10 - 2", # P15 = P14 - P6
'Q': "10 - 2", # Q15 = Q14 - Q6
'R': "10 - 2", # R15 = R14 - R6
},
# UI Row 13 (Excel Row 16): Füllungen warm (Lit. L-He)
12: {
'L': None, # From SecondTableEntry warm count
'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
13: {
'L': "(9 + 9) * 14 + 12 * 15", # L17 = (L13+M13)*0.06 + (L16+M16)*15
'M': "(9 + 9) * 14 + 12 * 15",
'N': "(9 + 9) * 14 + 12 * 15", # N17 = (N13+O13)*0.06 + (N16+O16)*15
'O': "(9 + 9) * 14 + 12 * 15",
'P': "9 * 14 + 12 * 15", # P17 = P13*0.06 + P16*15
'Q': "9 * 14 + 12 * 15", # Q17 = Q13*0.06 + Q16*15
'R': "9 * 14 + 12 * 15", # R17 = R13*0.06 + R16*15
},
# UI Row 15 (Excel Row 18): Faktor 0.06
14: {
'L': "0.06",
'M': "0.06",
'N': "0.06",
'O': "0.06",
'P': "0.06",
'Q': "0.06",
'R': "0.06",
},
# UI Row 16 (Excel Row 19): Verbraucherverluste (Liter L-He)
15: {
'L': "11 - 13", # L19 = L15 - L17
'M': "11 - 13",
'N': "11 - 13", # N19 = N15 - N17
'O': "11 - 13",
'P': "11 - 13", # P19 = P15 - P17
'Q': "11 - 13", # Q19 = Q15 - Q17
'R': "11 - 13", # R19 = R15 - R17
},
# UI Row 17 (Excel Row 20): %
16: {
'L': "15 / (9 + 9) if (9 + 9) > 0 else 0", # L20 = L19/(L13+M13)
'M': "15 / (9 + 9) if (9 + 9) > 0 else 0",
'N': "15 / (9 + 9) if (9 + 9) > 0 else 0", # N20 = N19/(N13+O13)
'O': "15 / (9 + 9) if (9 + 9) > 0 else 0",
'P': "15 / 9 if 9 > 0 else 0", # P20 = P19/P13
'Q': "15 / 9 if 9 > 0 else 0", # Q20 = Q19/Q13
'R': "15 / 9 if 9 > 0 else 0", # R20 = R19/R13
},
},
'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 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 9 = Excel row 13)"""
from .models import SecondTableEntry, Cell, Client
from django.db.models import Sum
from django.db.models.functions import Coalesce
from decimal import Decimal
year = sheet.year
month = sheet.month
# Define the client groups for top-right table IN ORDER
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)
]
# Clear and update row 9 (Excel row 13 = Bezug)
Cell.objects.filter(
sheet=sheet,
table_type='top_right',
row_index=9
).update(value=None)
# 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 9 (Excel row 13) - Bezug
cell, created = Cell.objects.get_or_create(
sheet=sheet,
table_type='top_right',
client=client,
row_index=9, # Bezug row
column_index=column_index,
defaults={'value': total_lhe_output}
)
if not created:
# Only update if the value is actually different
if 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
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",
]
# 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
rows = []
# Determine row count - UPDATED for top_right
row_counts = {
'top_left': 16,
'top_right': 17, # Changed from 16 to 17 to include all rows (0-16)
'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
# For each row (including row 7)
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
sum_column_index = len(client_names)
# Calculate sum for the row
sum_value = None
total = Decimal('0')
has_value = False
for cell in display_cells:
if cell and cell.value is not None:
try:
total += Decimal(str(cell.value))
has_value = True
except:
pass
if has_value:
sum_value = total
rows.append({
'cells': display_cells,
'sum': sum_value,
'row_index': row_idx, # Add this for debugging
})
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)
# 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,
'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)], # 16 rows now
'top_right': [[] for _ in range(24)],
'bottom_1': [[] for _ in range(10)],
'bottom_2': [[] for _ in range(10)],
'bottom_3': [[] for _ in range(10)],
}
for cell in cells:
if cell.table_type not in organized:
continue
max_rows = len(organized[cell.table_type])
if cell.row_index < 0 or cell.row_index >= max_rows:
# This is an "extra" cell from an older layout (e.g. top_left rows 1823)
continue
row_list = organized[cell.table_type][cell.row_index]
while len(row_list) <= cell.column_index:
row_list.append(None)
row_list[cell.column_index] = cell
return organized
def initialize_sheet_cells(self, sheet, clients):
"""Create all empty cells for a new monthly sheet"""
cells_to_create = []
summation_config = CALCULATION_CONFIG.get('summation_column', {})
sum_column_index = summation_config.get('sum_column_index', len(clients) - 1) # Last column
# For each table type and row
table_configs = [
('top_left', 16),
('top_right', 24),
('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 recalculate_summe_bestand_for_all(self, sheet):
"""Recalculate Summe Bestand for all clients in top_right table"""
from decimal import Decimal
from .models import Cell, Client
# Get all clients in top_right table
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)
]
updated_cells = []
for client_name in TOP_RIGHT_CLIENTS:
try:
client = Client.objects.get(name=client_name)
# Get cells for this client
cells = Cell.objects.filter(
sheet=sheet,
table_type='top_right',
client=client,
row_index__in=[3, 5, 6] # Row 3, 5, 6
)
# Create dict
cell_dict = {}
for cell in cells:
cell_dict[cell.row_index] = cell
# Get values
row3_cell = cell_dict.get(3) # Sonderrückführungen
row5_cell = cell_dict.get(5) # Bestand in Kannen-1
row6_cell = cell_dict.get(6) # Summe Bestand
if row3_cell and row5_cell and row6_cell:
row5_value = Decimal(str(row5_cell.value)) if row5_cell.value else Decimal('0')
# Calculate Summe Bestand
summe_bestand = row3_value + row5_value
# Update if different
if row6_cell.value != summe_bestand:
row6_cell.value = summe_bestand
row6_cell.save()
updated_cells.append({
'id': row6_cell.id,
'value': str(row6_cell.value),
'is_calculated': True
})
except Client.DoesNotExist:
continue
return updated_cells
class SaveCellsView(View):
# In your SaveCellsView class, update the calculate_top_right_dependents method:
def calculate_top_right_dependents(self, sheet, changed_cell):
"""
Recalculate ALL dependent cells in the top_right table whenever one cell changes.
This implements the Excel logic for merged client pairs with correct formulas.
"""
from decimal import Decimal
from django.db.models import Sum
from django.db.models.functions import Coalesce
from .models import SecondTableEntry, ExcelEntry, Client, Cell
updated_cells = []
TOP_RIGHT_CLIENTS = [
"Dr. Fohrer",
"AG Buntk.",
"AG Alff",
"AG Gutfl.",
"M3 Thiele",
"M3 Buntkowsky",
"M3 Gutfleisch",
]
MERGED_PAIRS = [
("Dr. Fohrer", "AG Buntk."), # pair 1: L/M
("AG Alff", "AG Gutfl."), # pair 2: N/O
]
M3_CLIENTS = ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"]
factor = Decimal("0.06")
# --------- helper functions ---------
def get_cell(client_name: str, row_idx: int):
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
return Cell.objects.filter(
sheet=sheet,
table_type="top_right",
client=client_obj,
row_index=row_idx,
column_index=col_idx,
).first()
def get_val(client_name: str, row_idx: int) -> Decimal:
cell = get_cell(client_name, row_idx)
if cell and cell.value is not None:
try:
return Decimal(str(cell.value))
except Exception:
return Decimal("0")
return Decimal("0")
def set_val(client_name: str, row_idx: int, value: Decimal, is_calculated=True):
client_obj = 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_obj,
row_index=row_idx,
column_index=col_idx,
defaults={"value": value},
)
if not created and cell.value == value:
return
cell.value = value
cell.save()
updated_cells.append(
{"id": cell.id, "value": str(cell.value), "is_calculated": is_calculated}
)
# --------- Get all current values ---------
all_cells = Cell.objects.filter(
sheet=sheet,
table_type="top_right"
).select_related('client')
client_values = {}
for cell in all_cells:
if cell.client.name not in client_values:
client_values[cell.client.name] = {}
client_values[cell.client.name][cell.row_index] = cell.value
def get_cached_val(client_name: str, row_idx: int) -> Decimal:
if client_name in client_values and row_idx in client_values[client_name]:
val = client_values[client_name][row_idx]
if val is not None:
try:
return Decimal(str(val))
except Exception:
return Decimal("0")
return Decimal("0")
# --------- 1. Füllungen warm (row 12) for ALL clients ---------
for name in TOP_RIGHT_CLIENTS:
client_obj = Client.objects.filter(name=name).first()
if not client_obj:
continue
warm_count = SecondTableEntry.objects.filter(
client=client_obj,
date__year=sheet.year,
date__month=sheet.month,
is_warm=True,
).count()
set_val(name, 12, Decimal(str(warm_count)))
client_values[name][12] = Decimal(str(warm_count))
# --------- 2. Calculate Summe Bestand (row 6) ---------
# Summe Bestand = Sonderrückführungen (row 3) + Bestand in Kannen-1 (row 5)
for name in TOP_RIGHT_CLIENTS:
row3_val = get_cached_val(name, 3) # Sonderrückführungen
row5_val = get_cached_val(name, 5) # Bestand in Kannen-1
summe_bestand = row3_val + row5_val
set_val(name, 6, summe_bestand, is_calculated=True)
client_values[name][6] = summe_bestand
# --------- 3. Process merged pairs ---------
for left_name, right_name in MERGED_PAIRS:
# Get values for the pair
L9 = get_cached_val(left_name, 9) # Bezug (left)
R9 = get_cached_val(right_name, 9) # Bezug (right)
L8 = get_cached_val(left_name, 8) # same as row 9 from prev sheet
L7 = get_cached_val(left_name, 7) # Best. in Kannen Vormonat
L2 = get_cached_val(left_name, 2) # Rückführung flüssig
L12 = get_cached_val(left_name, 12) # Füllungen warm (left)
R12 = get_cached_val(right_name, 12) # Füllungen warm (right)
# ----- 1. Rückführ. Soll (row 10) -----
# For Dr. Fohrer/AG Buntk: L14 = L13 - L11 + L12
# For AG Alff/AG Gutfl: N14 = N13 + N12 - N11 (which is the same formula: bezug + Best.Vormonat - PrevSheet)
rueckf_soll = L9 - L8 + L7
# Set same value for both clients in the pair
set_val(left_name, 10, rueckf_soll)
set_val(right_name, 10, rueckf_soll)
client_values[left_name][10] = rueckf_soll
client_values[right_name][10] = rueckf_soll
# ----- 2. Verluste (row 11) -----
# Formula: = L14 - L6 (where L14 is Rückführ. Soll, L6 is Rückführung flüssig)
verluste = rueckf_soll - L2
set_val(left_name, 11, verluste)
set_val(right_name, 11, verluste)
client_values[left_name][11] = verluste
client_values[right_name][11] = verluste
# ----- 3. Kaltgas Rückgabe (row 13) -----
# Formula: = L13 * factor + (L16 + M16) * 15
# Note: For AG Alff/AG Gutfl, it should be (N13+O13)*factor + (N16+O16)*15
if left_name == "Dr. Fohrer":
# For Dr. Fohrer/AG Buntk: use only Dr. Fohrer's bezug
kaltgas = L9 * factor + (L12 + R12) * Decimal("15")
else:
# For AG Alff/AG Gutfl: use sum of both bezugs
kaltgas = (L9 + R9) * factor + (L12 + R12) * Decimal("15")
set_val(left_name, 13, kaltgas)
set_val(right_name, 13, kaltgas)
client_values[left_name][13] = kaltgas
client_values[right_name][13] = kaltgas
# ----- 4. Faktor 0.06 (row 14) -----
set_val(left_name, 14, factor)
set_val(right_name, 14, factor)
client_values[left_name][14] = factor
client_values[right_name][14] = factor
# ----- 5. Verbraucherverluste (row 15) -----
# Formula: = L15 - L17
verbrauch = verluste - kaltgas
set_val(left_name, 15, verbrauch)
set_val(right_name, 15, verbrauch)
client_values[left_name][15] = verbrauch
client_values[right_name][15] = verbrauch
# ----- 6. % (row 16) -----
if left_name == "Dr. Fohrer":
# For Dr. Fohrer/AG Buntk: L20 = L19 / L13
if L9 > 0:
prozent = verbrauch / L9
else:
prozent = Decimal("0")
else:
# For AG Alff/AG Gutfl: N20 = N19 / (N13 + O13)
total_bezug = L9 + R9
if total_bezug > 0:
prozent = verbrauch / total_bezug
else:
prozent = Decimal("0")
set_val(left_name, 16, prozent)
set_val(right_name, 16, prozent)
client_values[left_name][16] = prozent
client_values[right_name][16] = prozent
# --------- 4. M3 clients (no merging, each individually) ---------
for name in M3_CLIENTS:
P9 = get_cached_val(name, 9) # Bezug (row 9)
P8 = get_cached_val(name, 8) # prev sheet (row 8)
P7 = get_cached_val(name, 7) # Best. in Kannen Vormonat (row 7)
P2 = get_cached_val(name, 2) # Rückführung flüssig (row 2)
P12 = get_cached_val(name, 12) # Füllungen warm (row 12)
# Rückführ. Soll = P9 - P8 + P7
rueckf_soll_m3 = P9 - P8 + P7
set_val(name, 10, rueckf_soll_m3)
client_values[name][10] = rueckf_soll_m3
# Verluste = P10 - P2
verluste_m3 = rueckf_soll_m3 - P2
set_val(name, 11, verluste_m3)
client_values[name][11] = verluste_m3
# Kaltgas = P9 * factor + P12 * 15
kaltgas_m3 = P9 * factor + P12 * Decimal("15")
set_val(name, 13, kaltgas_m3)
client_values[name][13] = kaltgas_m3
# Faktor 0.06
set_val(name, 14, factor)
client_values[name][14] = factor
# Verbraucherverluste = P11 - P13
verbrauch_m3 = verluste_m3 - kaltgas_m3
set_val(name, 15, verbrauch_m3)
client_values[name][15] = verbrauch_m3
# % = P15 / P9
if P9 > 0:
prozent_m3 = verbrauch_m3 / P9
else:
prozent_m3 = Decimal("0")
set_val(name, 16, prozent_m3)
client_values[name][16] = prozent_m3
return updated_cells
def post(self, request):
try:
sheet_id = request.POST.get('sheet_id')
sheet = MonthlySheet.objects.get(id=sheet_id)
# Check if it's a single cell save or bulk save
single_cell_id = request.POST.get('cell_id')
if single_cell_id:
# Single cell save
return self.save_single_cell(request, sheet, single_cell_id)
else:
# Bulk save (for backward compatibility)
return self.save_bulk_cells(request, sheet)
except MonthlySheet.DoesNotExist:
return JsonResponse({
'status': 'error',
'message': 'Sheet not found'
}, status=404)
except Exception as e:
return JsonResponse({
'status': 'error',
'message': str(e)
}, status=400)
def apply_top_right_group_links(self, sheet):
"""
Top-right merging logic for client pairs:
1. Bestand in Kannen-1 (row 5) is shared within each pair
2. Summe Bestand (row 6) = Bestand in Kannen-1 (row 5) only (not row3+row5)
3. Several other calculations are merged for each pair
Merged pairs:
- (Dr. Fohrer, AG Buntk.)
- (AG Alff, AG Gutfl.)
"""
from decimal import Decimal
from .models import Client, Cell
TOP_RIGHT_CLIENTS = [
"Dr. Fohrer",
"AG Buntk.",
"AG Alff",
"AG Gutfl.",
"M3 Thiele",
"M3 Buntkowsky",
"M3 Gutfleisch",
]
MERGED_PAIRS = [
("Dr. Fohrer", "AG Buntk."),
("AG Alff", "AG Gutfl."),
]
updated_cells = []
def get_cell(client_name, row_index):
client = Client.objects.filter(name=client_name).first()
if not client:
return None
try:
col_index = TOP_RIGHT_CLIENTS.index(client_name)
except ValueError:
return None
return Cell.objects.filter(
sheet=sheet,
table_type='top_right',
client=client,
row_index=row_index,
column_index=col_index,
).first()
def calculate_rueckf_soll_for_pair(left_name, right_name):
"""Calculate Rückführ. Soll for a merged pair: L13+M13-L11+L12"""
left_13 = get_cell(left_name, 9) # Bezug (row 9)
right_13 = get_cell(right_name, 9) # Bezug (row 9)
left_11 = get_cell(left_name, 8) # Same as row 9 from prev sheet (row 8)
left_12 = get_cell(left_name, 7) # Best. in Kannen Vormonat (row 7)
L13 = Decimal(str(left_13.value)) if left_13 and left_13.value else Decimal('0')
M13 = Decimal(str(right_13.value)) if right_13 and right_13.value else Decimal('0')
L11 = Decimal(str(left_11.value)) if left_11 and left_11.value else Decimal('0')
L12 = Decimal(str(left_12.value)) if left_12 and left_12.value else Decimal('0')
return L13 + M13 - L11 + L12
def calculate_verluste_for_pair(left_name, right_name, rueckf_soll_value):
"""Calculate Verluste for a merged pair: (L14+M14)-L6"""
left_14 = get_cell(left_name, 10) # Rückführ. Soll (row 10)
right_14 = get_cell(right_name, 10) # Rückführ. Soll (row 10)
left_6 = get_cell(left_name, 2) # Rückführung flüssig (row 2)
# Use provided rueckf_soll_value for calculation
L6 = Decimal(str(left_6.value)) if left_6 and left_6.value else Decimal('0')
return rueckf_soll_value - L6
def calculate_kaltgas_for_pair(left_name, right_name):
"""Calculate Kaltgas Rückgabe for a merged pair: (L13+M13)*0.06 + (L16+M16)*15"""
left_13 = get_cell(left_name, 9) # Bezug (row 9)
right_13 = get_cell(right_name, 9) # Bezug (row 9)
left_16 = get_cell(left_name, 12) # Füllungen warm (row 12)
right_16 = get_cell(right_name, 12) # Füllungen warm (row 12)
L13 = Decimal(str(left_13.value)) if left_13 and left_13.value else Decimal('0')
M13 = Decimal(str(right_13.value)) if right_13 and right_13.value else Decimal('0')
L16 = Decimal(str(left_16.value)) if left_16 and left_16.value else Decimal('0')
M16 = Decimal(str(right_16.value)) if right_16 and right_16.value else Decimal('0')
return (L13 + M13) * Decimal('0.06') + (L16 + M16) * Decimal('15')
def calculate_verbraucherverluste_for_pair(verluste_value, kaltgas_value):
"""Calculate Verbraucherverluste for a merged pair: L15-L17"""
return verluste_value - kaltgas_value
def calculate_prozent_for_pair(verbraucher_value, left_name, right_name):
"""Calculate % for a merged pair: L19/(L13+M13)"""
left_13 = get_cell(left_name, 9) # Bezug (row 9)
right_13 = get_cell(right_name, 9) # Bezug (row 9)
L13 = Decimal(str(left_13.value)) if left_13 and left_13.value else Decimal('0')
M13 = Decimal(str(right_13.value)) if right_13 and right_13.value else Decimal('0')
total_bezug = L13 + M13
if total_bezug > 0:
return verbraucher_value / total_bezug
else:
return Decimal('0')
# Process each merged pair
for left_name, right_name in MERGED_PAIRS:
# ----- 1) Sync Bestand in Kannen-1 (row 5) between the two clients -----
left_best = get_cell(left_name, 5)
right_best = get_cell(right_name, 5)
# Choose a value to propagate (prefer left if present, else right)
bestand_value = None
if left_best and left_best.value is not None:
bestand_value = left_best.value
elif right_best and right_best.value is not None:
bestand_value = right_best.value
# Update both cells with the same value
if bestand_value is not None:
for cell in (left_best, right_best):
if cell and cell.value != bestand_value:
cell.value = bestand_value
cell.save()
updated_cells.append({
'id': cell.id,
'value': str(cell.value),
'is_calculated': False,
})
# ----- 2) Update Summe Bestand (row 6) = Bestand in Kannen-1 (row 5) -----
# Summe Bestand should only equal row 5 (not row3+row5)
for name in (left_name, right_name):
bestand_cell = get_cell(name, 5)
summe_cell = get_cell(name, 6)
if bestand_cell and summe_cell:
new_sum = bestand_cell.value if bestand_cell.value is not None else Decimal('0')
if summe_cell.value != new_sum:
summe_cell.value = new_sum
summe_cell.save()
updated_cells.append({
'id': summe_cell.id,
'value': str(summe_cell.value),
'is_calculated': True,
})
# ----- 3) Calculate merged values for the pair -----
# Calculate Rückführ. Soll (row 10)
rueckf_soll_value = calculate_rueckf_soll_for_pair(left_name, right_name)
# Update both clients with the same Rückführ. Soll value
for name in (left_name, right_name):
rueckf_cell = get_cell(name, 10)
if rueckf_cell and rueckf_cell.value != rueckf_soll_value:
rueckf_cell.value = rueckf_soll_value
rueckf_cell.save()
updated_cells.append({
'id': rueckf_cell.id,
'value': str(rueckf_cell.value),
'is_calculated': True,
})
# Calculate Verluste (row 11)
verluste_value = calculate_verluste_for_pair(left_name, right_name, rueckf_soll_value)
# Update both clients with the same Verluste value
for name in (left_name, right_name):
verluste_cell = get_cell(name, 11)
if verluste_cell and verluste_cell.value != verluste_value:
verluste_cell.value = verluste_value
verluste_cell.save()
updated_cells.append({
'id': verluste_cell.id,
'value': str(verluste_cell.value),
'is_calculated': True,
})
# Calculate Kaltgas Rückgabe (row 13)
kaltgas_value = calculate_kaltgas_for_pair(left_name, right_name)
# Update both clients with the same Kaltgas value
for name in (left_name, right_name):
kaltgas_cell = get_cell(name, 13)
if kaltgas_cell and kaltgas_cell.value != kaltgas_value:
kaltgas_cell.value = kaltgas_value
kaltgas_cell.save()
updated_cells.append({
'id': kaltgas_cell.id,
'value': str(kaltgas_cell.value),
'is_calculated': True,
})
# Calculate Verbraucherverluste (row 15)
verbraucher_value = calculate_verbraucherverluste_for_pair(verluste_value, kaltgas_value)
# Update both clients with the same Verbraucherverluste value
for name in (left_name, right_name):
verbraucher_cell = get_cell(name, 15)
if verbraucher_cell and verbraucher_cell.value != verbraucher_value:
verbraucher_cell.value = verbraucher_value
verbraucher_cell.save()
updated_cells.append({
'id': verbraucher_cell.id,
'value': str(verbraucher_cell.value),
'is_calculated': True,
})
# Calculate % (row 16)
prozent_value = calculate_prozent_for_pair(verbraucher_value, left_name, right_name)
# Update both clients with the same % value
for name in (left_name, right_name):
prozent_cell = get_cell(name, 16)
if prozent_cell and prozent_cell.value != prozent_value:
prozent_cell.value = prozent_value
prozent_cell.save()
updated_cells.append({
'id': prozent_cell.id,
'value': str(prozent_cell.value),
'is_calculated': True,
})
return updated_cells
def save_single_cell(self, request, sheet, cell_id):
"""Save a single cell and calculate dependents"""
try:
# Get the cell
cell = Cell.objects.get(id=cell_id, sheet=sheet)
old_value = cell.value
new_value_str = request.POST.get('value', '').strip()
# Convert new value
try:
if new_value_str:
cell.value = Decimal(new_value_str)
else:
cell.value = None
except Exception:
cell.value = None
# Save the cell
cell.save()
# Start with this cell in the updated list
updated_cells = [{
'id': cell.id,
'value': str(cell.value) if cell.value else '',
'is_calculated': False
}]
# Calculate dependent cells based on table type
if cell.table_type == 'top_left':
dependent_updates = self.calculate_top_left_dependents(sheet, cell)
updated_cells.extend(dependent_updates)
elif cell.table_type == 'top_right':
# Always use the unified calculation logic for the top-right table
dependent_updates = self.calculate_top_right_dependents(sheet, cell)
updated_cells.extend(dependent_updates)
return JsonResponse({
'status': 'success',
'updated_cells': updated_cells
})
except Cell.DoesNotExist:
return JsonResponse({
'status': 'error',
'message': 'Cell not found'
}, status=404)
except Exception as e:
return JsonResponse({
'status': 'error',
'message': str(e)
}, status=400)
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 = Bezug (Liter L-He)
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
from .models import 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
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
from .models import SecondTableEntry
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:
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 ''
})
changed_clients.add(cell.client_id)
except Cell.DoesNotExist:
continue
# Recalculate for each changed client
for client_id in changed_clients:
self.recalculate_top_left_table(sheet, client_id)
# Get all updated cells for response
all_updated_cells = []
for client_id in changed_clients:
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
})
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()
# 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):
# Get all clients
clients = Client.objects.all()
# Get all years available in SecondTableEntry
available_years_qs = SecondTableEntry.objects.dates('date', 'year', order='DESC')
available_years = [y.year for y in available_years_qs]
# Determine selected year
year_param = request.GET.get('year')
if year_param:
selected_year = int(year_param)
else:
# If no year in GET, default to latest available year or current year if DB empty
selected_year = available_years[0] if available_years else datetime.now().year
# Prepare monthly totals per client
monthly_data = []
for client in clients:
monthly_totals = []
for month in range(1, 13):
total = SecondTableEntry.objects.filter(
client=client,
date__year=selected_year,
date__month=month
).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)
})
return render(request, 'clients_table.html', {
'monthly_data': monthly_data,
'current_year': selected_year,
'available_years': available_years,
'months': ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
})
# Table One View (ExcelEntry)
def table_one_view(request):
from .models import ExcelEntry, Client, Institute
# Main table (unchanged)
entries_table1 = ExcelEntry.objects.all().select_related('client', 'client__institute')
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
if available_years:
default_year = available_years[0] # newest year
else:
default_year = timezone.now().year
year_param = request.GET.get('overview_year')
start_month_param = request.GET.get('overview_start_month')
year = int(year_param) if year_param else int(default_year)
start_month = int(start_month_param) if start_month_param else 1
# six-month window
end_month = start_month + 5
if end_month > 12:
end_month = 12
months = list(range(start_month, end_month + 1))
overview = None
if months:
# 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 m in months:
total = ExcelEntry.objects.filter(
client__in=clients_qs,
date__year=year,
date__month=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(months)):
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, m in enumerate(months):
row_values = [g['values'][idx] for g in groups_entries]
rows.append({
'month_number': m,
'month_label': calendar.month_name[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]
overview = {
'year': year,
'start_month': start_month,
'end_month': end_month,
'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:
entries = SecondTableEntry.objects.all().order_by('-date')
clients = Client.objects.all().select_related('institute')
institutes = Institute.objects.all()
return render(request, 'table_two.html', {
'entries_table2': entries,
'clients': clients,
'institutes': institutes,
})
except Exception as e:
return render(request, 'table_two.html', {
'error_message': f"Failed to load data: {str(e)}",
'entries_table2': [],
'clients': Client.objects.all().select_related('institute'),
'institutes': Institute.objects.all()
})
# 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'})