This commit is contained in:
2026-04-13 13:41:28 +02:00
parent e1da4fa041
commit cfbb5c2fa2
7 changed files with 465 additions and 403 deletions
+187 -277
View File
@@ -59,6 +59,12 @@ CLIENT_GROUPS = {
'M3 Gutfleisch',
],
},
'merck': {
'label': 'Merck',
'names': [
'Merck',
],
},
}
# Add this CALCULATION_CONFIG at the top of views.py
@@ -275,12 +281,12 @@ def build_halfyear_window(interval_year: int, start_month: int):
# Halbjahres-Bilanz helpers
# ---------------------------------------------------------------------------
# You can adjust these indices if needed.
# Assuming:
# - bottom_1.table has row "Gasbestand" at some fixed row index,
# and columns: ... Nm³, Lit. LHe
GASBESTAND_ROW_INDEX = 9 # <-- adjust if your bottom_1 has a different row index
GASBESTAND_COL_NM3 = 3 # <-- adjust to the column index for Nm³ in bottom_1
GASBESTAND_ROW_INDEX = 9
GASBESTAND_COL_NM3 = 3
# In top_left / top_right, "Bestand in Kannen-1 (Lit. L-He)" is row_index 5
BESTAND_KANNEN_ROW_INDEX = 5
@@ -382,6 +388,7 @@ HALFYEAR_RIGHT_CLIENTS = [
"M3 Thiele",
"M3 Buntkowsky",
"M3 Gutfleisch",
"Merck",
]
BOTTOM1_COL_VOLUME = 0
BOTTOM1_COL_BAR = 1
@@ -461,6 +468,7 @@ def get_group_clients(group_key):
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):
@@ -575,6 +583,7 @@ class MonthlySheetView(TemplateView):
"M3 Thiele", # Column index 4 (P)
"M3 Buntkowsky", # Column index 5 (Q)
"M3 Gutfleisch", # Column index 6 (R)
"Merck",
]
# For each client in top-right table
@@ -735,6 +744,7 @@ class MonthlySheetView(TemplateView):
"M3 Thiele",
"M3 Buntkowsky",
"M3 Gutfleisch",
"Merck",
]
current_summary = MonthlySummary.objects.filter(sheet=sheet).first()
@@ -765,6 +775,10 @@ class MonthlySheetView(TemplateView):
("Dr. Fohrer", "AG Buntk."),
("AG Alff", "AG Gutfl."),
]
MERGED_TRIPLES = [
("M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"),
]
rows = []
# Determine row count
@@ -813,13 +827,29 @@ class MonthlySheetView(TemplateView):
has_value = False
merged_second_indices = set()
if table_type == 'top_right' and row_idx in MERGED_ROWS:
# Handle pairs
for left_name, right_name in MERGED_PAIRS:
try:
right_idx = client_names.index(right_name)
merged_second_indices.add(right_idx)
except ValueError:
# client not in this table; just ignore
pass
# Handle M3 triple group
MERGED_TRIPLES = [
("M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"),
]
for a, b, c in MERGED_TRIPLES:
try:
b_idx = client_names.index(b)
c_idx = client_names.index(c)
merged_second_indices.add(b_idx)
merged_second_indices.add(c_idx)
except ValueError:
pass
for col_idx, cell in enumerate(display_cells):
@@ -1250,100 +1280,8 @@ def get_factor_value(table_type, row_index):
# 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
@@ -1443,82 +1381,7 @@ def recalculate_stand_der_gaszahler(self, sheet):
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)
ABRECHNUNG_COL_CLIENTS = {
# first 3 columns
"pkm_vogel": ["AG Vogel"],
@@ -1582,7 +1445,12 @@ class AbrechnungView(TemplateView):
("rechnungsbetrag", "Rechnungsbetrag", "EUR"),
("eff_lhe_preis", "eff. L-He-Preis", "EUR/L"),
]
SUMMARY_ROWS = {
"bezogen_menge",
"kaltg_warmfuell",
"he_verbrauch",
"lhe_verluste",
}
# Editable rows = your yellow rows
EDITABLE_ROW_KEYS = {"ghe_bezug", "betrag", "gutschriften"}
@@ -1706,9 +1574,12 @@ class AbrechnungView(TemplateView):
def sum_all_cols(values_dict):
return sum(v for k, v in values_dict.items() if k != "gesamt_summe")
bezogen["gesamt_summe"] = sum_all_cols(bezogen)
warmfills["gesamt_summe"] = sum_all_cols(warmfills)
bezogen["gesamt_summe"] = (
d(bezogen.get("chemie")) + d(bezogen.get("mawi")) + d(bezogen.get("physik"))
)
warmfills["gesamt_summe"] = (
d(warmfills.get("chemie")) + d(warmfills.get("mawi")) + d(warmfills.get("physik"))
)
# ---- Row: Bezogen. Menge ----
for col_key in bezogen:
computed[("bezogen_menge", col_key)] = bezogen[col_key]
@@ -1765,12 +1636,30 @@ class AbrechnungView(TemplateView):
for col_key in he_verbrauch:
if col_key in first3:
row1_vals[col_key] = instandhaltung * safe_div(he_verbrauch[col_key], physik_hv) / Decimal("3")
elif col_key in next3:
row1_vals[col_key] = instandhaltung / Decimal("9")
elif col_key in next2:
row1_vals[col_key] = instandhaltung / Decimal("6")
# ✅ NEW: summary columns
elif col_key in ("chemie", "mawi"):
row1_vals[col_key] = instandhaltung / Decimal("3")
elif col_key == "physik":
row1_vals[col_key] = (
d(row1_vals.get("pkm_vogel")) +
d(row1_vals.get("iap_halfmann")) +
d(row1_vals.get("ikp"))
)
elif col_key == "gesamt_summe":
row1_vals[col_key] = sum(row1_vals.get(k, Decimal("0")) for k in first3)
# keep whatever you want here; example: chemie+mawi+physik
row1_vals[col_key] = (
d(row1_vals.get("chemie")) + d(row1_vals.get("mawi")) + d(row1_vals.get("physik"))
)
else:
row1_vals[col_key] = Decimal("0")
@@ -1820,23 +1709,21 @@ class AbrechnungView(TemplateView):
elif col_key == "chemie":
v = (
d(verbrauch_right.get("Dr. Fohrer")) +
d(verbrauch_right.get("AG Buntk.")) +
d(verbrauch_right.get("M3 Buntkowsky")) +
d(verbrauch_right.get("M3 Thiele"))
d(verbrauch_right.get("AG Buntk."))
)
elif col_key == "mawi":
# MaWi = MaWi Gutfl + MaWi Alff (Abrechnung columns)
v = (
d(verbrauch_right.get("AG Alff")) +
d(verbrauch_right.get("AG Gutfl.")) +
d(verbrauch_right.get("M3 Gutfleisch"))
d(lhe_verluste.get("AG Gutfl")) +
d(lhe_verluste.get("mawi_alff"))
)
elif col_key == "physik":
v = (
d(verbrauch_left.get("AG Vogel")) +
d(verbrauch_left.get("AG Halfm")) +
d(verbrauch_left.get("IKP"))
v = (
d(verbrauch_right.get("M3 Thiele")) +
d(verbrauch_right.get("M3 Buntkowsky")) +
d(verbrauch_right.get("M3 Gutfleisch"))
)
elif col_key == "gesamt_summe":
@@ -1851,14 +1738,41 @@ class AbrechnungView(TemplateView):
# --- Gesamt-summe ---
lhe_verluste["gesamt_summe"] = sum(v for k, v in lhe_verluste.items())
lhe_verluste["gesamt_summe"] = (
d(lhe_verluste.get("chemie")) + d(lhe_verluste.get("mawi")) + d(lhe_verluste.get("physik"))
)
computed[("lhe_verluste", "gesamt_summe")] = lhe_verluste["gesamt_summe"]
# ---- Row: 3 Umlage Heliumkosten = heliumkosten * (LHe-Verluste col / LHe-Verluste gesamt_summe) ----
for col_key in lhe_verluste:
computed[("umlage_heliumkosten_3", col_key)] = heliumkosten * safe_div(lhe_verluste[col_key], lhe_verluste["gesamt_summe"])
FIRST8 = [
"pkm_vogel",
"iap_halfmann",
"ikp",
"orgchem_thiele",
"phychem_m3_buntkow",
"orgchem_fohrer",
"mawi_m3_gutfl",
"mawi_alff",
]
den = sum(d(lhe_verluste.get(k)) for k in FIRST8) # denominator = sum of first 8 LHeVerluste
# 1) compute first 8 columns
for k in FIRST8:
computed[("umlage_heliumkosten_3", k)] = heliumkosten * safe_div(d(lhe_verluste.get(k)), den)
# 2) compute the 3 summary columns from those first 8 (so they stay consistent)
# 2) compute the 3 summary columns directly from their LHeVerluste shares (Excel logic)
computed[("umlage_heliumkosten_3", "chemie")] = heliumkosten * safe_div(d(lhe_verluste.get("chemie")), den)
computed[("umlage_heliumkosten_3", "mawi")] = heliumkosten * safe_div(d(lhe_verluste.get("mawi")), den)
computed[("umlage_heliumkosten_3", "physik")] = heliumkosten * safe_div(d(lhe_verluste.get("physik")), den)
# 3) last column = sum of first 8 columns (your requirement)
computed[("umlage_heliumkosten_3", "gesamt_summe")] = sum(
d(computed.get(("umlage_heliumkosten_3", k))) for k in FIRST8
)
# ---- Row: 4-Kosten He-Gas-Bezug = Umlage Heliumkosten * Bezugskosten-GasHe ----
for col_key, _label in self.COLUMNS:
ghe = d(value_map.get(("ghe_bezug", col_key)))
@@ -1901,7 +1815,17 @@ class AbrechnungView(TemplateView):
elif col_key in ("chemie", "mawi", "physik"):
v = safe_div(he_verbrauch[col_key], sum3) * umlage_personal_total
elif col_key == "gesamt_summe":
v = sum(d(computed.get(("umlage_personal_5", k))) for k in he_verbrauch if k != "gesamt_summe")
# sum of the FIRST 8 columns only (exclude chemie/mawi/physik/gesamt_summe)
v = (
d(computed.get(("umlage_personal_5", "pkm_vogel"))) +
d(computed.get(("umlage_personal_5", "iap_halfmann"))) +
d(computed.get(("umlage_personal_5", "ikp"))) +
d(computed.get(("umlage_personal_5", "orgchem_thiele"))) +
d(computed.get(("umlage_personal_5", "phychem_m3_buntkow"))) +
d(computed.get(("umlage_personal_5", "orgchem_fohrer"))) +
d(computed.get(("umlage_personal_5", "mawi_m3_gutfl"))) +
d(computed.get(("umlage_personal_5", "mawi_alff")))
)
else:
v = Decimal("0")
computed[("umlage_personal_5", col_key)] = v
@@ -1932,6 +1856,19 @@ class AbrechnungView(TemplateView):
else:
sonstiges_text[col_key] = "Nachzahlung"
# overwrite display values for non-editable computed rows
FIRST4 = {"bezogen_menge", "kaltg_warmfuell", "anzahl_15", "he_verbrauch"}
FIRST8 = ["pkm_vogel", "iap_halfmann", "ikp", "orgchem_thiele",
"phychem_m3_buntkow", "orgchem_fohrer", "mawi_m3_gutfl", "mawi_alff"]
for rk, _label, _unit in self.ROWS:
if rk in FIRST4:
computed[(rk, "gesamt_summe")] = (
d(computed.get((rk, "chemie"))) +
d(computed.get((rk, "mawi"))) +
d(computed.get((rk, "physik")))
)
else:
computed[(rk, "gesamt_summe")] = sum(d(computed.get((rk, ck))) for ck in FIRST8)
non_editable_formula_rows = {
"bezogen_menge",
"kaltg_warmfuell",
@@ -1981,19 +1918,31 @@ class AbrechnungView(TemplateView):
# -------------------------------
GROUP_IJKL = ["orgchem_thiele", "phychem_m3_buntkow", "orgchem_fohrer", "mawi_m3_gutfl", "mawi_alff"]
GROUP_NOP = GROUP_IJKL + ["chemie", "mawi", "physik"]
GROUP_NOP = ["chemie", "mawi", "physik"]
GROUP_STADT = ["pkm_vogel", "iap_halfmann", "ikp"]
def nop_cols_for_row(row_key: str):
# For gutschriften, NOP should sum the 5 IJKL client columns
if row_key == "gutschriften":
return GROUP_IJKL
# otherwise NOP is the 3 summary columns (Chemie/MaWi/Physik)
return GROUP_NOP
def val(row_key, col_key):
"""Get the final numeric value for a cell (computed if available, else stored)."""
v = value_map.get((row_key, col_key))
return d(v)
"""Get the final numeric value for a cell (prefer computed, fallback to stored)."""
if (row_key, col_key) in computed:
return d(computed.get((row_key, col_key)))
return d(value_map.get((row_key, col_key)))
def sum_group(row_key, cols):
return sum(val(row_key, ck) for ck in cols)
right_rows = []
def nop_cols_for_row(row_key: str):
# For gutschriften, NOP should sum the 5 IJKL client columns
if row_key == "gutschriften":
return GROUP_IJKL
# otherwise NOP is the 3 summary columns
return GROUP_NOP
# These are your normal rows (same order as UI)
# We will create one summary row per Abrechnung row_key.
for row_key, label, unit in self.ROWS:
@@ -2011,14 +1960,27 @@ class AbrechnungView(TemplateView):
continue
ijkl = sum_group(row_key, GROUP_IJKL)
nop = sum_group(row_key, GROUP_NOP)
if row_key == "umlage_heliumkosten_3":
nop = (
val(row_key, "chemie") +
val(row_key, "mawi") +
val(row_key, "physik")
)
else:
nop = sum_group(row_key, nop_cols_for_row(row_key))
stadt = sum_group(row_key, GROUP_STADT)
check = nop + stadt
# Special rule: Rechnungsbetrag row gets +Betrag +Gutschriften (for NOP only)
if row_key == "rechnungsbetrag":
nop_extra = sum_group("betrag", GROUP_NOP) + sum_group("gutschriften", GROUP_NOP)
nop = nop + nop_extra
# Rechnungsbetrag (NOP) = (Chemie+MaWi+Physik Rechnungsbetrag)
# + (Chemie+MaWi+Physik Betrag)
# + (IJKL Gutschriften) <-- because you want gutschriften NOP from IJKL
nop = (
sum_group("rechnungsbetrag", GROUP_NOP)
+ sum_group("betrag", GROUP_NOP)
+ sum_group("gutschriften", GROUP_IJKL)
)
check = nop + stadt
right_rows.append({
@@ -2180,7 +2142,7 @@ class RechnungView(TemplateView):
bs = BetriebskostenSummary.objects.first()
instandhaltung = d(bs.instandhaltung) if bs else Decimal("0")
personalkosten = d(bs.personalkosten) if bs else Decimal("0")
personalkosten = d(bs.umlage_personal) if bs else Decimal("0")
preis_eur_pro_l = (instandhaltung / interval_total_output_lhe) if interval_total_output_lhe != 0 else Decimal("0")
@@ -2537,6 +2499,7 @@ class SaveCellsView(View):
"M3 Thiele", # P
"M3 Buntkowsky", # Q
"M3 Gutfleisch", # R
"Merck"
]
# Define merged pairs
@@ -2553,6 +2516,7 @@ class SaveCellsView(View):
"fohrer_buntk": ["Dr. Fohrer", "AG Buntk."],
"alff_gutfl": ["AG Alff", "AG Gutfl."],
"m3": ["M3 Thiele", "M3 Buntkowsky", "M3 Gutfleisch"],
"merck": ["Merck"],
}
year = sheet.year
@@ -2797,6 +2761,26 @@ class SaveCellsView(View):
else:
prozent = Decimal('0')
set_val(m3_client, 15, prozent, is_calculated=True)
# 9. Simple client(s): only Bezug, Sammelrückführungen, Verbraucherverluste = Sammel - Bezug
SIMPLE_RIGHT_CLIENTS = ["Merck"] # use the exact database name here
for client_name in SIMPLE_RIGHT_CLIENTS:
# Bezug (row 8)
bezug = get_val(client_name, 8)
# Sammelrückführungen (row 4)
sammel = get_val(client_name, 4)
# Verbraucherverluste (row 14) = Sammelrückführungen - Bezug
verbrauch = bezug - sammel
set_val(client_name, 14, verbrauch, is_calculated=True)
# % (row 15) = Verbraucherverluste / Bezug
if bezug != 0:
prozent = verbrauch / bezug
else:
prozent = Decimal("0")
set_val(client_name, 15, prozent, is_calculated=True)
return updated_cells
@@ -4360,41 +4344,7 @@ class CheckSheetView(View):
})
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):
@@ -4417,46 +4367,6 @@ class TestFormulaView(View):
})
class SimpleDebugView(View):
"""Simplest debug view to check if things are working"""
def get(self, request):
sheet_id = request.GET.get('sheet_id', 1)
try:
sheet = MonthlySheet.objects.get(id=sheet_id)
# Get first client
client = Client.objects.first()
if not client:
return JsonResponse({'error': 'No clients found'})
# Check a few cells
cells = Cell.objects.filter(
sheet=sheet,
client=client,
table_type='top_left',
row_index__in=[8, 9, 10]
).order_by('row_index')
cell_data = []
for cell in cells:
cell_data.append({
'row_index': cell.row_index,
'ui_row': cell.row_index + 1,
'value': str(cell.value) if cell.value is not None else 'Empty',
'cell_id': cell.id
})
return JsonResponse({
'sheet': f"{sheet.year}-{sheet.month}",
'sheet_id': sheet.id,
'client': client.name,
'cells': cell_data,
'note': 'Row 8 = UI Row 9, Row 9 = UI Row 10, Row 10 = UI Row 11'
})
except MonthlySheet.DoesNotExist:
return JsonResponse({'error': f'Sheet with id {sheet_id} not found'})
def halfyear_settings(request):
"""