2873 lines
106 KiB
Plaintext
2873 lines
106 KiB
Plaintext
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 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', 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'}) |