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': None, 'M': None, 'N': None, 'O': None, 'P': None, 'Q': None, 'R': None, }, # UI Row 11 (Excel Row 14): Rückführ. Soll (Lit. L-He) # handled in calculate_top_right_dependents (merged pairs + M3) 10: { 'L': None, 'M': None, 'N': None, 'O': None, 'P': None, 'Q': None, 'R': None, }, # UI Row 12 (Excel Row 15): Verluste (Soll-Rückf.) (Lit. L-He) # handled in calculate_top_right_dependents 11: { 'L': None, 'M': None, 'N': None, 'O': None, 'P': None, 'Q': None, 'R': None, }, # UI Row 14 (Excel Row 17): Kaltgas Rückgabe (Lit. L-He) – Faktor # handled in calculate_top_right_dependents (different formulas for pair 1 vs pair 2 + M3) 13: { 'L': None, 'M': None, 'N': None, 'O': None, 'P': None, 'Q': None, 'R': None, }, # UI Row 16 (Excel Row 19): Verbraucherverluste (Liter L-He) # handled in calculate_top_right_dependents 15: { 'L': None, 'M': None, 'N': None, 'O': None, 'P': None, 'Q': None, 'R': None, }, # UI Row 17 (Excel Row 20): % # handled in calculate_top_right_dependents 16: { 'L': None, 'M': None, 'N': None, 'O': None, 'P': None, 'Q': None, 'R': None, } }, 'bottom_1': { 5: "4 + 3 + 2", 8: "7 - 6", }, 'bottom_2': { 3: "1 + 2", 6: "5 - 4", }, 'bottom_3': { 2: "0 + 1", 5: "3 + 4", }, # Special configuration for summation column (last column) 'summation_column': { # For each row that should be summed across columns 'rows_to_sum': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23], # All rows # OR specify specific rows: # 'rows_to_sum': [0, 5, 10, 15, 20], # Only specific rows # The last column index (0-based) 'sum_column_index': 5, # 6th column (0-5) since you have 6 clients } } def get_group_clients(group_key): """Return queryset of clients that belong to a logical group.""" from .models import Client # local import to avoid circulars group = CLIENT_GROUPS.get(group_key) if not group: return Client.objects.none() return Client.objects.filter(name__in=group['names']) def calculate_summation(sheet, table_type, row_index, sum_column_index): """Calculate summation for a row, with special handling for % row""" from decimal import Decimal from .models import Cell try: # Special case: top_left, % row (Excel B20 -> row_index 19) if table_type == 'top_left' and row_index == 19: # K13 = sum of row 13 (Excel B13 -> row_index 12) across all clients cells_row13 = Cell.objects.filter( sheet=sheet, table_type='top_left', row_index=12, # Excel B13 = row_index 12 column_index__lt=sum_column_index # Exclude sum column itself ) total_13 = Decimal('0') for cell in cells_row13: if cell.value is not None: total_13 += Decimal(str(cell.value)) # K19 = sum of row 19 (Excel B19 -> row_index 18) across all clients cells_row19 = Cell.objects.filter( sheet=sheet, table_type='top_left', row_index=18, # Excel B19 = row_index 18 column_index__lt=sum_column_index ) total_19 = Decimal('0') for cell in cells_row19: if cell.value is not None: total_19 += Decimal(str(cell.value)) # Calculate: IF(K13=0; 0; K19/K13) if total_13 == 0: return Decimal('0') return total_19 / total_13 # Normal summation for other rows cells_in_row = Cell.objects.filter( sheet=sheet, table_type=table_type, row_index=row_index, column_index__lt=sum_column_index ) total = Decimal('0') for cell in cells_in_row: if cell.value is not None: total += Decimal(str(cell.value)) return total except Exception as e: print(f"Error calculating summation for {table_type}[{row_index}]: {e}") return None # Helper function for calculations def evaluate_formula(formula, values_dict): """ Safely evaluate a formula like "10 + 9" where numbers are row indices values_dict: {row_index: decimal_value} """ from decimal import Decimal import re try: # Create a copy of the formula to work with expr = formula # Find all row numbers in the formula row_refs = re.findall(r'\b\d+\b', expr) for row_ref in row_refs: row_num = int(row_ref) if row_num in values_dict and values_dict[row_num] is not None: # Replace row reference with actual value expr = expr.replace(row_ref, str(values_dict[row_num])) else: # Missing value - can't calculate return None # Evaluate the expression # Note: In production, use a safer evaluator like `asteval` result = eval(expr, {"__builtins__": {}}, {}) # Convert to Decimal with proper rounding return Decimal(str(round(result, 6))) except Exception: return None # Monthly Sheet View class MonthlySheetView(TemplateView): template_name = 'monthly_sheet.html' def populate_helium_input_to_top_right(self, sheet): """Populate bezug data from SecondTableEntry to top-right table (row 8 = Excel row 12)""" from .models import SecondTableEntry, Cell, Client from django.db.models.functions import Coalesce from decimal import Decimal year = sheet.year month = sheet.month TOP_RIGHT_CLIENTS = [ "Dr. Fohrer", # Column index 0 (L) "AG Buntk.", # Column index 1 (M) "AG Alff", # Column index 2 (N) "AG Gutfl.", # Column index 3 (O) "M3 Thiele", # Column index 4 (P) "M3 Buntkowsky", # Column index 5 (Q) "M3 Gutfleisch", # Column index 6 (R) ] # For each client in top-right table for client_name in TOP_RIGHT_CLIENTS: try: client = Client.objects.get(name=client_name) column_index = TOP_RIGHT_CLIENTS.index(client_name) # Calculate total LHe_output for this client in this month from SecondTableEntry total_lhe_output = SecondTableEntry.objects.filter( client=client, date__year=year, date__month=month ).aggregate( total=Coalesce(Sum('lhe_output'), Decimal('0')) )['total'] # Get or create the cell for row_index 8 (Excel row 12) - Bezug cell, created = Cell.objects.get_or_create( sheet=sheet, table_type='top_right', client=client, row_index=8, # Bezug row (Excel row 12) column_index=column_index, defaults={'value': total_lhe_output} ) if not created and cell.value != total_lhe_output: cell.value = total_lhe_output cell.save() except Client.DoesNotExist: continue # After populating bezug, trigger calculation for all dependent cells # Get any cell to start the calculation first_cell = Cell.objects.filter( sheet=sheet, table_type='top_right' ).first() if first_cell: save_view = SaveCellsView() save_view.calculate_top_right_dependents(sheet, first_cell) return True def calculate_bezug_from_entries(self, sheet, year, month): """Calculate B11 (Bezug) from SecondTableEntry for all clients - ONLY for non-start sheets""" from .models import SecondTableEntry, Cell, Client from django.db.models import Sum from django.db.models.functions import Coalesce from decimal import Decimal # Check if this is the start sheet if year == 2025 and month == 1: return # Don't auto-calculate for start sheet for client in Client.objects.all(): # Calculate total LHe output for this client in this month lhe_output_sum = SecondTableEntry.objects.filter( client=client, date__year=year, date__month=month ).aggregate( total=Coalesce(Sum('lhe_output'), Decimal('0')) )['total'] # Update B11 cell (row_index 8 = UI Row 9) b11_cell = Cell.objects.filter( sheet=sheet, table_type='top_left', client=client, row_index=8 # Excel B11 ).first() if b11_cell and (b11_cell.value != lhe_output_sum or b11_cell.value is None): b11_cell.value = lhe_output_sum b11_cell.save() # Also trigger dependent calculations from .views import SaveCellsView save_view = SaveCellsView() save_view.calculate_top_left_dependents(sheet, b11_cell) # In MonthlySheetView.get_context_data() method, update the TOP_RIGHT_CLIENTS and row count: return True def get_context_data(self, **kwargs): from decimal import Decimal context = super().get_context_data(**kwargs) year = self.kwargs.get('year', datetime.now().year) month = self.kwargs.get('month', datetime.now().month) is_start_sheet = (year == FIRST_SHEET_YEAR and month == FIRST_SHEET_MONTH) # Get or create the monthly sheet sheet, created = MonthlySheet.objects.get_or_create( year=year, month=month ) # All clients (used for bottom tables etc.) clients = Client.objects.all().order_by('name') # Pre-fill cells if creating new sheet if created: self.initialize_sheet_cells(sheet, clients) # Apply previous month links (for B4 and B12) self.apply_previous_month_links(sheet, year, month) self.calculate_bezug_from_entries(sheet, year, month) self.populate_helium_input_to_top_right(sheet) self.apply_previous_month_links_top_right(sheet, year, month) # Define client groups TOP_LEFT_CLIENTS = [ "AG Vogel", "AG Halfm", "IKP", ] TOP_RIGHT_CLIENTS = [ "Dr. Fohrer", "AG Buntk.", "AG Alff", "AG Gutfl.", "M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch", ] # 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': 16, # 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)], 'top_right': [[] for _ in range(16)], # now 16 rows 'bottom_1': [[] for _ in range(10)], 'bottom_2': [[] for _ in range(10)], 'bottom_3': [[] for _ in range(10)], } for cell in cells: if cell.table_type not in organized: continue max_rows = len(organized[cell.table_type]) if cell.row_index < 0 or cell.row_index >= max_rows: # This is an "extra" cell from an older layout (e.g. top_left rows 18–23) continue row_list = organized[cell.table_type][cell.row_index] while len(row_list) <= cell.column_index: row_list.append(None) row_list[cell.column_index] = cell return organized def initialize_sheet_cells(self, sheet, clients): """Create all empty cells for a new monthly sheet""" cells_to_create = [] summation_config = CALCULATION_CONFIG.get('summation_column', {}) sum_column_index = summation_config.get('sum_column_index', len(clients) - 1) # Last column # For each table type and row table_configs = [ ('top_left', 16), ('top_right', 16), ('bottom_1', 10), ('bottom_2', 10), ('bottom_3', 10), ] for table_type, row_count in table_configs: for row_idx in range(row_count): for col_idx, client in enumerate(clients): is_summation = (col_idx == sum_column_index) cells_to_create.append(Cell( sheet=sheet, client=client, table_type=table_type, row_index=row_idx, column_index=col_idx, value=None, is_formula=is_summation, # Mark summation cells as formulas )) # Bulk create all cells at once Cell.objects.bulk_create(cells_to_create) def get_prev_month(self, year, month): """Get previous month year and month""" if month == 1: return {'year': year - 1, 'month': 12} return {'year': year, 'month': month - 1} def get_next_month(self, year, month): """Get next month year and month""" if month == 12: return {'year': year + 1, 'month': 1} return {'year': year, 'month': month + 1} def apply_previous_month_links(self, sheet, year, month): """ For non-start sheets: B4 (row 2) = previous sheet B3 (row 1) B10 (row 8) = previous sheet B9 (row 7) """ # Do nothing on the first sheet if year == FIRST_SHEET_YEAR and month == FIRST_SHEET_MONTH: return # Figure out previous month if month == 1: prev_year = year - 1 prev_month = 12 else: prev_year = year prev_month = month - 1 from .models import MonthlySheet, Cell, Client prev_sheet = MonthlySheet.objects.filter( year=prev_year, month=prev_month ).first() if not prev_sheet: # No previous sheet created yet → nothing to copy return # For each client, copy values for client in Client.objects.all(): # B3(prev) → B4(curr): UI row 1 → row 2 → row_index 0 → 1 prev_b3 = Cell.objects.filter( sheet=prev_sheet, table_type='top_left', client=client, row_index=0, # UI row 1 ).first() curr_b4 = Cell.objects.filter( sheet=sheet, table_type='top_left', client=client, row_index=1, # UI row 2 ).first() if prev_b3 and curr_b4: curr_b4.value = prev_b3.value curr_b4.save() # B9(prev) → B10(curr): UI row 7 → row 8 → row_index 6 → 7 prev_b9 = Cell.objects.filter( sheet=prev_sheet, table_type='top_left', client=client, row_index=6, # UI row 7 (Summe Bestand) ).first() curr_b10 = Cell.objects.filter( sheet=sheet, table_type='top_left', client=client, row_index=7, # UI row 8 (Best. in Kannen Vormonat) ).first() if prev_b9 and curr_b10: curr_b10.value = prev_b9.value curr_b10.save() def apply_previous_month_links_top_right(self, sheet, year, month): """ top_right row 7: Best. in Kannen Vormonat (Lit. L-He) = previous sheet's Summe Bestand (row 6). For merged pairs: - Dr. Fohrer + AG Buntk. share the SAME value (from previous month's AG Buntk. or Dr. Fohrer) - AG Alff + AG Gutfl. share the SAME value (from previous month's AG Alff or AG Gutfl.) M3 clients just copy their own value. """ from .models import MonthlySheet, Cell, Client from decimal import Decimal # Do nothing on first sheet if year == FIRST_SHEET_YEAR and month == FIRST_SHEET_MONTH: return # find previous month if month == 1: prev_year = year - 1 prev_month = 12 else: prev_year = year prev_month = month - 1 prev_sheet = MonthlySheet.objects.filter( year=prev_year, month=prev_month ).first() if not prev_sheet: return # nothing to copy from TOP_RIGHT_CLIENTS = [ "Dr. Fohrer", "AG Buntk.", "AG Alff", "AG Gutfl.", "M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch", ] # Helper function to get a cell value def get_cell_value(sheet_obj, client_name, row_index): client_obj = Client.objects.filter(name=client_name).first() if not client_obj: return None try: col_idx = TOP_RIGHT_CLIENTS.index(client_name) except ValueError: return None cell = Cell.objects.filter( sheet=sheet_obj, table_type='top_right', client=client_obj, row_index=row_index, column_index=col_idx, ).first() return cell.value if cell else None # Helper function to set a cell value def set_cell_value(sheet_obj, client_name, row_index, value): client_obj = Client.objects.filter(name=client_name).first() if not client_obj: return False try: col_idx = TOP_RIGHT_CLIENTS.index(client_name) except ValueError: return False cell, created = Cell.objects.get_or_create( sheet=sheet_obj, table_type='top_right', client=client_obj, row_index=row_index, column_index=col_idx, defaults={'value': value} ) if not created and cell.value != value: cell.value = value cell.save() elif created: cell.save() return True # ----- Pair 1: Dr. Fohrer + AG Buntk. ----- # Get previous month's Summe Bestand (row 6) for either client in the pair pair1_prev_val = None # Try AG Buntk. first prev_buntk_val = get_cell_value(prev_sheet, "AG Buntk.", 6) if prev_buntk_val is not None: pair1_prev_val = prev_buntk_val else: # Try Dr. Fohrer if AG Buntk. is empty prev_fohrer_val = get_cell_value(prev_sheet, "Dr. Fohrer", 6) if prev_fohrer_val is not None: pair1_prev_val = prev_fohrer_val # Apply the value to both clients in the pair if pair1_prev_val is not None: set_cell_value(sheet, "Dr. Fohrer", 7, pair1_prev_val) set_cell_value(sheet, "AG Buntk.", 7, pair1_prev_val) # ----- Pair 2: AG Alff + AG Gutfl. ----- pair2_prev_val = None # Try AG Alff first prev_alff_val = get_cell_value(prev_sheet, "AG Alff", 6) if prev_alff_val is not None: pair2_prev_val = prev_alff_val else: # Try AG Gutfl. if AG Alff is empty prev_gutfl_val = get_cell_value(prev_sheet, "AG Gutfl.", 6) if prev_gutfl_val is not None: pair2_prev_val = prev_gutfl_val # Apply the value to both clients in the pair if pair2_prev_val is not None: set_cell_value(sheet, "AG Alff", 7, pair2_prev_val) set_cell_value(sheet, "AG Gutfl.", 7, pair2_prev_val) # ----- M3 clients: copy their own Summe Bestand ----- for name in ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"]: prev_val = get_cell_value(prev_sheet, name, 6) if prev_val is not None: set_cell_value(sheet, name, 7, prev_val) # Add this helper function to views.py def get_factor_value(table_type, row_index): """Get factor value (like 0.06 for top_left row 17)""" factors = { ('top_left', 17): Decimal('0.06'), # A18 in Excel (UI row 17, 0-based index 16) } return factors.get((table_type, row_index), Decimal('0')) # Save Cells View # views.py - Updated SaveCellsView # views.py - Update SaveCellsView class def debug_cell_values(self, sheet, client_id): """Debug method to check cell values""" from .models import Cell cells = Cell.objects.filter( sheet=sheet, table_type='top_left', client_id=client_id ).order_by('row_index') debug_info = {} for cell in cells: debug_info[f"row_{cell.row_index}"] = { 'value': str(cell.value) if cell.value else 'None', 'ui_row': cell.row_index + 1, 'excel_ref': f"B{cell.row_index + 3}" } return debug_info class DebugCalculationView(View): """Debug view to test calculations directly""" def get(self, request): sheet_id = request.GET.get('sheet_id', 1) client_name = request.GET.get('client', 'AG Vogel') try: sheet = MonthlySheet.objects.get(id=sheet_id) client = Client.objects.get(name=client_name) # Get SaveCellsView instance save_view = SaveCellsView() # Create a dummy cell to trigger calculations dummy_cell = Cell.objects.filter( sheet=sheet, table_type='top_left', client=client, row_index=0 # B3 ).first() if not dummy_cell: return JsonResponse({'error': 'No cells found for this client'}) # Trigger calculation updated = save_view.calculate_top_left_dependents(sheet, dummy_cell) # Get updated cell values cells = Cell.objects.filter( sheet=sheet, table_type='top_left', client=client ).order_by('row_index') cell_data = [] for cell in cells: cell_data.append({ 'row_index': cell.row_index, 'ui_row': cell.row_index + 1, 'excel_ref': f"B{cell.row_index + 3}", 'value': str(cell.value) if cell.value else 'None', 'description': self.get_row_description(cell.row_index) }) return JsonResponse({ 'sheet': f"{sheet.year}-{sheet.month:02d}", 'client': client.name, 'cells': cell_data, 'updated_count': len(updated), 'calculation': 'B5 = IF(B3>0; B3-B4; 0)' }) except Exception as e: return JsonResponse({'error': str(e)}, status=400) def get_row_description(self, row_index): """Get description for row index""" descriptions = { 0: "B3: Stand der Gaszähler (Nm³)", 1: "B4: Stand der Gaszähler (Vormonat) (Nm³)", 2: "B5: Gasrückführung (Nm³)", 3: "B6: Rückführung flüssig (Lit. L-He)", 4: "B7: Sonderrückführungen (Lit. L-He)", 5: "B8: Bestand in Kannen-1 (Lit. L-He)", 6: "B9: Summe Bestand (Lit. L-He)", 7: "B10: Best. in Kannen Vormonat (Lit. L-He)", 8: "B11: Bezug (Liter L-He)", 9: "B12: Rückführ. Soll (Lit. L-He)", 10: "B13: Verluste (Soll-Rückf.) (Lit. L-He)", 11: "B14: Füllungen warm (Lit. L-He)", 12: "B15: Kaltgas Rückgabe (Lit. L-He) – Faktor", 13: "B16: Faktor", 14: "B17: Verbraucherverluste (Liter L-He)", 15: "B18: %" } return descriptions.get(row_index, f"Row {row_index}") def recalculate_stand_der_gaszahler(self, sheet): """Recalculate Stand der Gaszähler for all client pairs""" from decimal import Decimal # For Dr. Fohrer and AG Buntk. (L & M columns) try: # Get Dr. Fohrer's bezug dr_fohrer_client = Client.objects.get(name="Dr. Fohrer") dr_fohrer_cell = Cell.objects.filter( sheet=sheet, table_type='top_right', client=dr_fohrer_client, row_index=9 # Row 9 = Bezug ).first() L13 = Decimal(str(dr_fohrer_cell.value)) if dr_fohrer_cell and dr_fohrer_cell.value else Decimal('0') # Get AG Buntk.'s bezug ag_buntk_client = Client.objects.get(name="AG Buntk.") ag_buntk_cell = Cell.objects.filter( sheet=sheet, table_type='top_right', client=ag_buntk_client, row_index=9 ).first() M13 = Decimal(str(ag_buntk_cell.value)) if ag_buntk_cell and ag_buntk_cell.value else Decimal('0') total = L13 + M13 if total > 0: # Update Dr. Fohrer's row 0 dr_fohrer_row0 = Cell.objects.filter( sheet=sheet, table_type='top_right', client=dr_fohrer_client, row_index=0 ).first() if dr_fohrer_row0: dr_fohrer_row0.value = L13 / total dr_fohrer_row0.save() # Update AG Buntk.'s row 0 ag_buntk_row0 = Cell.objects.filter( sheet=sheet, table_type='top_right', client=ag_buntk_client, row_index=0 ).first() if ag_buntk_row0: ag_buntk_row0.value = M13 / total ag_buntk_row0.save() except Exception as e: print(f"Error recalculating Stand der Gaszähler for Dr. Fohrer/AG Buntk.: {e}") # For AG Alff and AG Gutfl. (N & O columns) try: # Get AG Alff's bezug ag_alff_client = Client.objects.get(name="AG Alff") ag_alff_cell = Cell.objects.filter( sheet=sheet, table_type='top_right', client=ag_alff_client, row_index=9 ).first() N13 = Decimal(str(ag_alff_cell.value)) if ag_alff_cell and ag_alff_cell.value else Decimal('0') # Get AG Gutfl.'s bezug ag_gutfl_client = Client.objects.get(name="AG Gutfl.") ag_gutfl_cell = Cell.objects.filter( sheet=sheet, table_type='top_right', client=ag_gutfl_client, row_index=9 ).first() O13 = Decimal(str(ag_gutfl_cell.value)) if ag_gutfl_cell and ag_gutfl_cell.value else Decimal('0') total = N13 + O13 if total > 0: # Update AG Alff's row 0 ag_alff_row0 = Cell.objects.filter( sheet=sheet, table_type='top_right', client=ag_alff_client, row_index=0 ).first() if ag_alff_row0: ag_alff_row0.value = N13 / total ag_alff_row0.save() # Update AG Gutfl.'s row 0 ag_gutfl_row0 = Cell.objects.filter( sheet=sheet, table_type='top_right', client=ag_gutfl_client, row_index=0 ).first() if ag_gutfl_row0: ag_gutfl_row0.value = O13 / total ag_gutfl_row0.save() except Exception as e: print(f"Error recalculating Stand der Gaszähler for AG Alff/AG Gutfl.: {e}") # In your SaveCellsView class in views.py class DebugTopRightView(View): """Debug view to check top_right calculations""" def get(self, request): sheet_id = request.GET.get('sheet_id', 1) client_name = request.GET.get('client', 'Dr. Fohrer') try: sheet = MonthlySheet.objects.get(id=sheet_id) client = Client.objects.get(name=client_name) # Get all cells for this client in top_right cells = Cell.objects.filter( sheet=sheet, table_type='top_right', client=client ).order_by('row_index') cell_data = [] descriptions = { 0: "Stand der Gaszähler (Vormonat)", 1: "Gasrückführung (Nm³)", 2: "Rückführung flüssig", 3: "Sonderrückführungen", 4: "Sammelrückführungen", 5: "Bestand in Kannen-1", 6: "Summe Bestand", 7: "Best. in Kannen Vormonat", 8: "Same as row 9 from prev sheet", 9: "Bezug", 10: "Rückführ. Soll", 11: "Verluste", 12: "Füllungen warm", 13: "Kaltgas Rückgabe", 14: "Faktor 0.06", 15: "Verbraucherverluste", 16: "%" } for cell in cells: cell_data.append({ 'row_index': cell.row_index, 'ui_row': cell.row_index + 1, 'description': descriptions.get(cell.row_index, f"Row {cell.row_index}"), 'value': str(cell.value) if cell.value else 'Empty', 'cell_id': cell.id }) # Test calculation row3_cell = cells.filter(row_index=3).first() row5_cell = cells.filter(row_index=5).first() row6_cell = cells.filter(row_index=6).first() calculation_info = { 'row3_value': str(row3_cell.value) if row3_cell and row3_cell.value else '0', 'row5_value': str(row5_cell.value) if row5_cell and row5_cell.value else '0', 'row6_value': str(row6_cell.value) if row6_cell and row6_cell.value else '0', 'expected_sum': '0' } if row3_cell and row5_cell and row6_cell: row3_val = Decimal(str(row3_cell.value)) if row3_cell.value else Decimal('0') row5_val = Decimal(str(row5_cell.value)) if row5_cell.value else Decimal('0') expected = row3_val + row5_val calculation_info['expected_sum'] = str(expected) calculation_info['is_correct'] = row6_cell.value == expected return JsonResponse({ 'sheet': f"{sheet.year}-{sheet.month}", 'client': client.name, 'cells': cell_data, 'calculation': calculation_info }) except Exception as e: return JsonResponse({'error': str(e)}, status=400) class SaveCellsView(View): def post(self, request, *args, **kwargs): """ Handle AJAX saves from monthly_sheet.html - Single-cell save: when cell_id is present (blur on one cell) - Bulk save: when the 'Save All Cells' button is used (no cell_id) """ try: sheet_id = request.POST.get('sheet_id') if not sheet_id: return JsonResponse({ 'status': 'error', 'message': 'Missing sheet_id' }) sheet = MonthlySheet.objects.get(id=sheet_id) # -------- Single-cell update (blur) -------- cell_id = request.POST.get('cell_id') if cell_id: value_raw = (request.POST.get('value') or '').strip() try: cell = Cell.objects.get(id=cell_id, sheet=sheet) except Cell.DoesNotExist: return JsonResponse({ 'status': 'error', 'message': 'Cell not found' }) # Convert value to Decimal or None if value_raw == '': new_value = None else: try: # Allow comma or dot value_clean = value_raw.replace(',', '.') new_value = Decimal(value_clean) except (InvalidOperation, ValueError): # If conversion fails, treat as empty new_value = None old_value = cell.value cell.value = new_value cell.save() updated_cells = [{ 'id': cell.id, 'value': '' if cell.value is None else str(cell.value), 'is_calculated': cell.is_formula, # model field }] # Recalculate dependents depending on table_type if cell.table_type == 'top_left': updated_cells.extend( self.calculate_top_left_dependents(sheet, cell) ) elif cell.table_type == 'top_right': updated_cells.extend( self.calculate_top_right_dependents(sheet, cell) ) # bottom_1 / bottom_2 / bottom_3 currently have no formulas: # they just save the new value. return JsonResponse({ 'status': 'success', 'updated_cells': updated_cells }) # -------- Bulk save (Save All button) -------- return self.save_bulk_cells(request, sheet) except MonthlySheet.DoesNotExist: return JsonResponse({ 'status': 'error', 'message': 'Sheet not found' }) except Exception as e: # Generic safety net so the frontend sees an error message return JsonResponse({ 'status': 'error', 'message': str(e) }) # ... your other methods above ... # Update the calculate_top_right_dependents method in SaveCellsView class def calculate_top_right_dependents(self, sheet, changed_cell): """ Recalculate all dependent cells in the top-right table according to Excel formulas. Excel rows (4-20) -> 0-based indices (0-15) Rows: 0: Stand der Gaszähler (Vormonat) (Nm³) - shares for M3 clients 1: Gasrückführung (Nm³) 2: Rückführung flüssig (Lit. L-He) 3: Sonderrückführungen (Lit. L-He) - editable 4: Sammelrückführungen (Lit. L-He) - from helium_input groups 5: Bestand in Kannen-1 (Lit. L-He) - editable, merged in pairs 6: Summe Bestand (Lit. L-He) = row 5 7: Best. in Kannen Vormonat (Lit. L-He) - from previous month 8: Bezug (Liter L-He) - from SecondTableEntry 9: Rückführ. Soll (Lit. L-He) - calculated 10: Verluste (Soll-Rückf.) (Lit. L-He) - calculated 11: Füllungen warm (Lit. L-He) - from SecondTableEntry warm outputs 12: Kaltgas Rückgabe (Lit. L-He) – Faktor - calculated 13: Faktor 0.06 - fixed 14: Verbraucherverluste (Liter L-He) - calculated 15: % - calculated """ from decimal import Decimal from django.db.models import Sum, Count from django.db.models.functions import Coalesce from .models import Client, Cell, ExcelEntry, SecondTableEntry TOP_RIGHT_CLIENTS = [ "Dr. Fohrer", # L "AG Buntk.", # M (merged with L) "AG Alff", # N "AG Gutfl.", # O (merged with N) "M3 Thiele", # P "M3 Buntkowsky", # Q "M3 Gutfleisch", # R ] # Define merged pairs MERGED_PAIRS = [ ("Dr. Fohrer", "AG Buntk."), # L and M are merged ("AG Alff", "AG Gutfl."), # N and O are merged ] # M3 clients (not merged, calculated individually) M3_CLIENTS = ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"] # Groups for Sammelrückführungen (helium_input) HELIUM_INPUT_GROUPS = { "fohrer_buntk": ["Dr. Fohrer", "AG Buntk."], "alff_gutfl": ["AG Alff", "AG Gutfl."], "m3": ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"], } year = sheet.year month = sheet.month factor = Decimal('0.06') # Fixed factor from Excel updated_cells = [] # Helper functions def get_val(client_name, row_idx): """Get cell value for a client and row""" try: client = Client.objects.get(name=client_name) col_idx = TOP_RIGHT_CLIENTS.index(client_name) cell = Cell.objects.filter( sheet=sheet, table_type='top_right', client=client, row_index=row_idx, column_index=col_idx ).first() if cell and cell.value is not None: return Decimal(str(cell.value)) except (Client.DoesNotExist, ValueError, KeyError): pass return Decimal('0') def set_val(client_name, row_idx, value, is_calculated=True): """Set cell value for a client and row""" try: client = Client.objects.get(name=client_name) col_idx = TOP_RIGHT_CLIENTS.index(client_name) cell, created = Cell.objects.get_or_create( sheet=sheet, table_type='top_right', client=client, row_index=row_idx, column_index=col_idx, defaults={'value': value} ) # Only update if value changed if not created and cell.value != value: cell.value = value cell.save() elif created: cell.save() updated_cells.append({ 'id': cell.id, 'value': str(cell.value) if cell.value else '', 'is_calculated': is_calculated }) return True except (Client.DoesNotExist, ValueError): return False # 1. Update Summe Bestand (row 6) from Bestand in Kannen-1 (row 5) # For merged pairs: copy value from changed cell to its pair if changed_cell and changed_cell.table_type == 'top_right': if changed_cell.row_index == 5: # Bestand in Kannen-1 client_name = changed_cell.client.name new_value = changed_cell.value # Check if this client is in a merged pair for pair in MERGED_PAIRS: if client_name in pair: # Update both clients in the pair for client_in_pair in pair: if client_in_pair != client_name: set_val(client_in_pair, 5, new_value, is_calculated=False) break # 2. For all clients: Set Summe Bestand (row 6) = Bestand in Kannen-1 (row 5) for client_name in TOP_RIGHT_CLIENTS: bestand_value = get_val(client_name, 5) set_val(client_name, 6, bestand_value, is_calculated=True) # 3. Update Sammelrückführungen (row 4) from helium_input groups for group_name, client_names in HELIUM_INPUT_GROUPS.items(): # Get total helium_input for this group clients_in_group = Client.objects.filter(name__in=client_names) total_helium = ExcelEntry.objects.filter( client__in=clients_in_group, date__year=year, date__month=month ).aggregate(total=Coalesce(Sum('lhe_ges'), Decimal('0')))['total'] # Set same value for all clients in group for client_name in client_names: set_val(client_name, 4, total_helium, is_calculated=True) # 4. Calculate Rückführung flüssig (row 2) # For merged pairs: =L8 for Dr. Fohrer/AG Buntk., =N8 for AG Alff/AG Gutfl. # For M3 clients: =$P$8 * P4, $P$8 * Q4, $P$8 * R4 # Get Sammelrückführungen values for groups sammel_fohrer_buntk = get_val("Dr. Fohrer", 4) # L8 sammel_alff_gutfl = get_val("AG Alff", 4) # N8 sammel_m3_group = get_val("M3 Thiele", 4) # P8 (same for all M3) # For merged pairs set_val("Dr. Fohrer", 2, sammel_fohrer_buntk, is_calculated=True) set_val("AG Buntk.", 2, sammel_fohrer_buntk, is_calculated=True) set_val("AG Alff", 2, sammel_alff_gutfl, is_calculated=True) set_val("AG Gutfl.", 2, sammel_alff_gutfl, is_calculated=True) # For M3 clients: =$P$8 * column4 (Stand der Gaszähler) for m3_client in M3_CLIENTS: stand_value = get_val(m3_client, 0) # Stand der Gaszähler (row 0) rueck_value = sammel_m3_group * stand_value set_val(m3_client, 2, rueck_value, is_calculated=True) # 5. Calculate Füllungen warm (row 11) from SecondTableEntry warm outputs # 5. Calculate Füllungen warm (row 11) as NUMBER of warm fillings # (sum of 1s where each warm SecondTableEntry is one filling) for client_name in TOP_RIGHT_CLIENTS: client = Client.objects.get(name=client_name) warm_count = SecondTableEntry.objects.filter( client=client, date__year=year, date__month=month, is_warm=True ).aggregate( total=Coalesce(Count('id'), 0) )['total'] # store as Decimal so later formulas (warm * 15) still work nicely warm_value = Decimal(warm_count) set_val(client_name, 11, warm_value, is_calculated=True) # 6. Set Faktor row (13) to 0.06 for client_name in TOP_RIGHT_CLIENTS: set_val(client_name, 13, factor, is_calculated=True) # 6a. Recalculate Stand der Gaszähler (row 0) for the merged pairs # according to Excel: # L4 = L13 / (L13 + M13), M4 = M13 / (L13 + M13) # N4 = N13 / (N13 + O13), O4 = O13 / (N13 + O13) # Pair 1: Dr. Fohrer / AG Buntk. bezug_dr = get_val("Dr. Fohrer", 8) # L13 bezug_buntk = get_val("AG Buntk.", 8) # M13 total_pair1 = bezug_dr + bezug_buntk if total_pair1 != 0: set_val("Dr. Fohrer", 0, bezug_dr / total_pair1, is_calculated=True) set_val("AG Buntk.", 0, bezug_buntk / total_pair1, is_calculated=True) else: # if no Bezug, both shares are 0 set_val("Dr. Fohrer", 0, Decimal('0'), is_calculated=True) set_val("AG Buntk.", 0, Decimal('0'), is_calculated=True) # Pair 2: AG Alff / AG Gutfl. bezug_alff = get_val("AG Alff", 8) # N13 bezug_gutfl = get_val("AG Gutfl.", 8) # O13 total_pair2 = bezug_alff + bezug_gutfl if total_pair2 != 0: set_val("AG Alff", 0, bezug_alff / total_pair2, is_calculated=True) set_val("AG Gutfl.", 0, bezug_gutfl / total_pair2, is_calculated=True) else: set_val("AG Alff", 0, Decimal('0'), is_calculated=True) set_val("AG Gutfl.", 0, Decimal('0'), is_calculated=True) # 7. Calculate all other dependent rows for merged pairs for pair in MERGED_PAIRS: client1, client2 = pair # Get values for the pair bezug1 = get_val(client1, 8) # Bezug client1 bezug2 = get_val(client2, 8) # Bezug client2 total_bezug = bezug1 + bezug2 # L13+M13 or N13+O13 summe_bestand = get_val(client1, 6) # L11 or N11 (merged, same value) best_vormonat = get_val(client1, 7) # L12 or N12 (merged, same value) rueck_fl = get_val(client1, 2) # L6 or N6 (merged, same value) warm1 = get_val(client1, 11) # L16 or N16 warm2 = get_val(client2, 11) # M16 or O16 total_warm = warm1 + warm2 # L16+M16 or N16+O16 # Calculate Rückführ. Soll (row 9) # = L13+M13 - L11 + L12 for first pair # = N13+O13 - N11 + N12 for second pair rueck_soll = total_bezug - summe_bestand + best_vormonat set_val(client1, 9, rueck_soll, is_calculated=True) set_val(client2, 9, rueck_soll, is_calculated=True) # Calculate Verluste (row 10) = Rückführ. Soll - Rückführung flüssig verluste = rueck_soll - rueck_fl set_val(client1, 10, verluste, is_calculated=True) set_val(client2, 10, verluste, is_calculated=True) # Calculate Kaltgas Rückgabe (row 12) # = (L13+M13)*$A18 + (L16+M16)*15 kaltgas = (total_bezug * factor) + (total_warm * Decimal('15')) set_val(client1, 12, kaltgas, is_calculated=True) set_val(client2, 12, kaltgas, is_calculated=True) # Calculate Verbraucherverluste (row 14) = Verluste - Kaltgas verbrauch = verluste - kaltgas set_val(client1, 14, verbrauch, is_calculated=True) set_val(client2, 14, verbrauch, is_calculated=True) # Calculate % (row 15) = Verbraucherverluste / (L13+M13) if total_bezug != 0: prozent = verbrauch / total_bezug else: prozent = Decimal('0') set_val(client1, 15, prozent, is_calculated=True) set_val(client2, 15, prozent, is_calculated=True) # 8. Calculate all dependent rows for M3 clients (individual calculations) for m3_client in M3_CLIENTS: # Get individual values bezug = get_val(m3_client, 8) # Bezug for this M3 client summe_bestand = get_val(m3_client, 6) # Summe Bestand best_vormonat = get_val(m3_client, 7) # Best. in Kannen Vormonat rueck_fl = get_val(m3_client, 2) # Rückführung flüssig warm = get_val(m3_client, 11) # Füllungen warm # Calculate Rückführ. Soll (row 9) = Bezug - Summe Bestand + Best. Vormonat rueck_soll = bezug - summe_bestand + best_vormonat set_val(m3_client, 9, rueck_soll, is_calculated=True) # Calculate Verluste (row 10) = Rückführ. Soll - Rückführung flüssig verluste = rueck_soll - rueck_fl set_val(m3_client, 10, verluste, is_calculated=True) # Calculate Kaltgas Rückgabe (row 12) = Bezug * factor + warm * 15 kaltgas = (bezug * factor) + (warm * Decimal('15')) set_val(m3_client, 12, kaltgas, is_calculated=True) # Calculate Verbraucherverluste (row 14) = Verluste - Kaltgas verbrauch = verluste - kaltgas set_val(m3_client, 14, verbrauch, is_calculated=True) # Calculate % (row 15) = Verbraucherverluste / Bezug if bezug != 0: prozent = verbrauch / bezug else: prozent = Decimal('0') set_val(m3_client, 15, prozent, is_calculated=True) return updated_cells def calculate_top_left_dependents(self, sheet, changed_cell): """Calculate dependent cells in top_left table""" from decimal import Decimal from django.db.models import Sum from django.db.models.functions import Coalesce client_id = changed_cell.client_id updated_cells = [] # Get all cells for this client in top_left table client_cells = Cell.objects.filter( sheet=sheet, table_type='top_left', client_id=client_id ) # Create a dict for easy access cell_dict = {} for cell in client_cells: cell_dict[cell.row_index] = cell # Get values with safe defaults def get_cell_value(row_idx): cell = cell_dict.get(row_idx) if cell and cell.value is not None: try: return Decimal(str(cell.value)) except: return Decimal('0') return Decimal('0') # 1. B5 = IF(B3>0; B3-B4; 0) - Gasrückführung b3 = get_cell_value(0) # row_index 0 = Excel B3 (UI row 1) b4 = get_cell_value(1) # row_index 1 = Excel B4 (UI row 2) # Calculate B5 if b3 > 0: b5_value = b3 - b4 if b5_value < 0: b5_value = Decimal('0') else: b5_value = Decimal('0') # Update B5 - FORCE update even if value appears the same b5_cell = cell_dict.get(2) # row_index 2 = Excel B5 (UI row 3) if b5_cell: # Always update B5 when B3 or B4 changes b5_cell.value = b5_value b5_cell.save() updated_cells.append({ 'id': b5_cell.id, 'value': str(b5_cell.value), 'is_calculated': True }) # 2. B6 = B5 / 0.75 - Rückführung flüssig b6_value = b5_value / Decimal('0.75') b6_cell = cell_dict.get(3) # row_index 3 = Excel B6 (UI row 4) if b6_cell: b6_cell.value = b6_value b6_cell.save() updated_cells.append({ 'id': b6_cell.id, 'value': str(b6_cell.value), 'is_calculated': True }) # 3. B9 = B7 + B8 - Summe Bestand b7 = get_cell_value(4) # row_index 4 = Excel B7 (UI row 5) b8 = get_cell_value(5) # row_index 5 = Excel B8 (UI row 6) b9_value = b7 + b8 b9_cell = cell_dict.get(6) # row_index 6 = Excel B9 (UI row 7) if b9_cell: b9_cell.value = b9_value b9_cell.save() updated_cells.append({ 'id': b9_cell.id, 'value': str(b9_cell.value), 'is_calculated': True }) # 4. B11 = Sum of LHe Output from SecondTableEntry for this client/month - Bezug from .models import SecondTableEntry client = changed_cell.client # Calculate total LHe output for this client in this month # 4. B11 = Bezug (Liter L-He) # For start sheet: manual entry, for other sheets: auto-calculated from SecondTableEntry from .models import SecondTableEntry client = changed_cell.client b11_cell = cell_dict.get(8) # row_index 8 = Excel B11 (UI row 9) # Check if this is the start sheet (2025-01) is_start_sheet = (sheet.year == 2025 and sheet.month == 1) # Get the B11 value for calculations b11_value = Decimal('0') if is_start_sheet: # For start sheet, keep whatever value is already there (manual entry) if b11_cell and b11_cell.value is not None: b11_value = Decimal(str(b11_cell.value)) else: # For non-start sheets: auto-calculate from SecondTableEntry lhe_output_sum = SecondTableEntry.objects.filter( client=client, date__year=sheet.year, date__month=sheet.month ).aggregate( total=Coalesce(Sum('lhe_output'), Decimal('0')) )['total'] b11_value = lhe_output_sum if b11_cell and (b11_cell.value != b11_value or b11_cell.value is None): b11_cell.value = b11_value b11_cell.save() updated_cells.append({ 'id': b11_cell.id, 'value': str(b11_cell.value), 'is_calculated': True # Calculated from SecondTableEntry }) # 5. B12 = B11 + B10 - B9 - Rückführ. Soll b10 = get_cell_value(7) # row_index 7 = Excel B10 (UI row 8) b12_value = b11_value + b10 - b9_value # Use b11_value instead of lhe_output_sum b12_cell = cell_dict.get(9) # row_index 9 = Excel B12 (UI row 10) if b12_cell: b12_cell.value = b12_value b12_cell.save() updated_cells.append({ 'id': b12_cell.id, 'value': str(b12_cell.value), 'is_calculated': True }) # 6. B13 = B12 - B6 - Verluste (Soll-Rückf.) b13_value = b12_value - b6_value b13_cell = cell_dict.get(10) # row_index 10 = Excel B13 (UI row 11) if b13_cell: b13_cell.value = b13_value b13_cell.save() updated_cells.append({ 'id': b13_cell.id, 'value': str(b13_cell.value), 'is_calculated': True }) # 7. B14 = Count of warm outputs warm_count = SecondTableEntry.objects.filter( client=client, date__year=sheet.year, date__month=sheet.month, is_warm=True ).count() b14_cell = cell_dict.get(11) # row_index 11 = Excel B14 (UI row 12) if b14_cell: b14_cell.value = Decimal(str(warm_count)) b14_cell.save() updated_cells.append({ 'id': b14_cell.id, 'value': str(b14_cell.value), 'is_calculated': True }) # 8. B15 = IF(B11>0; B11 * factor + B14 * 15; 0) - Kaltgas Rückgabe factor = get_cell_value(13) # row_index 13 = Excel B16 (Faktor) (UI row 14) if factor == 0: factor = Decimal('0.06') # default factor if b11_value > 0: # Use b11_value b15_value = b11_value * factor + Decimal(str(warm_count)) * Decimal('15') else: b15_value = Decimal('0') b15_cell = cell_dict.get(12) # row_index 12 = Excel B15 (UI row 13) if b15_cell: b15_cell.value = b15_value b15_cell.save() updated_cells.append({ 'id': b15_cell.id, 'value': str(b15_cell.value), 'is_calculated': True }) # 9. B17 = B13 - B15 - Verbraucherverluste b17_value = b13_value - b15_value b17_cell = cell_dict.get(14) # row_index 14 = Excel B17 (UI row 15) if b17_cell: b17_cell.value = b17_value b17_cell.save() updated_cells.append({ 'id': b17_cell.id, 'value': str(b17_cell.value), 'is_calculated': True }) # 10. B18 = IF(B11=0; 0; B17/B11) - % if b11_value == 0: # Use b11_value b18_value = Decimal('0') else: b18_value = b17_value / b11_value # Use b11_value b18_cell = cell_dict.get(15) # row_index 15 = Excel B18 (UI row 16) if b18_cell: b18_cell.value = b18_value b18_cell.save() updated_cells.append({ 'id': b18_cell.id, 'value': str(b18_cell.value), 'is_calculated': True }) return updated_cells def save_bulk_cells(self, request, sheet): """Original bulk save logic (for backward compatibility)""" # Get all cell updates cell_updates = {} for key, value in request.POST.items(): if key.startswith('cell_'): cell_id = key.replace('cell_', '') cell_updates[cell_id] = value # Update cells and track which ones changed updated_cells = [] changed_clients = set() for cell_id, new_value in cell_updates.items(): try: cell = Cell.objects.get(id=cell_id, sheet=sheet) old_value = cell.value # Convert new value try: if new_value.strip(): cell.value = Decimal(new_value) else: cell.value = None except: 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'})