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'})