Finished months balance except the below tables

This commit is contained in:
2026-02-04 15:31:19 +01:00
parent 34f040df30
commit 567b9edc2d
40 changed files with 6357 additions and 58 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

36
fix_top_right_cells.py Normal file
View File

@@ -0,0 +1,36 @@
from sheets.models import MonthlySheet, Client, Cell
TOP_RIGHT_CLIENTS = [
"Dr. Fohrer", # L
"AG Buntk.", # M
"AG Alff", # N
"AG Gutfl.", # O
"M3 Thiele", # P
"M3 Buntkowsky", # Q
"M3 Gutfleisch", # R
]
ROW_COUNT = 16 # top_right rows 015
sheets = MonthlySheet.objects.all().order_by('year', 'month')
for sheet in sheets:
print(f"Fixing sheet {sheet.year}-{sheet.month}...")
for col_idx, name in enumerate(TOP_RIGHT_CLIENTS):
try:
client = Client.objects.get(name=name)
except Client.DoesNotExist:
print(f" WARNING: client {name!r} not found, skipping this column")
continue
for row_idx in range(ROW_COUNT):
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': None},
)
if created:
print(f" created cell: {name} row {row_idx}")

View File

@@ -0,0 +1,2 @@
# sheets/__init__.py
default_app_config = 'sheets.apps.SheetsConfig'

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,6 +1,8 @@
from django.apps import AppConfig
class SheetsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'sheets'
def ready(self):
import sheets.signals

View File

@@ -0,0 +1,67 @@
# Generated by Django 5.1.5 on 2026-01-07 11:29
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('sheets', '0010_excelentry_constant_300_excelentry_druckkorrektur_and_more'),
]
operations = [
migrations.AlterField(
model_name='betriebskosten',
name='kostentyp',
field=models.CharField(choices=[('sach', 'Sachkosten'), ('ln2', 'LN2'), ('helium', 'Helium'), ('inv', 'Inventar')], max_length=10, verbose_name='Kostentyp'),
),
migrations.CreateModel(
name='MonthlySheet',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('year', models.IntegerField()),
('month', models.IntegerField()),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'ordering': ['year', 'month'],
'unique_together': {('year', 'month')},
},
),
migrations.CreateModel(
name='Cell',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('table_type', models.CharField(choices=[('top_left', 'Top Left Table'), ('top_right', 'Top Right Table'), ('bottom_1', 'Bottom Table 1'), ('bottom_2', 'Bottom Table 2'), ('bottom_3', 'Bottom Table 3')], max_length=20)),
('row_index', models.IntegerField()),
('column_index', models.IntegerField()),
('value', models.DecimalField(blank=True, decimal_places=6, max_digits=15, null=True)),
('formula', models.TextField(blank=True)),
('is_formula', models.BooleanField(default=False)),
('data_type', models.CharField(choices=[('number', 'Number'), ('text', 'Text'), ('date', 'Date')], default='number', max_length=20)),
('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sheets.client')),
('sheet', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cells', to='sheets.monthlysheet')),
],
),
migrations.CreateModel(
name='CellReference',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('source_cell', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='dependents', to='sheets.cell')),
('target_cell', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='dependencies', to='sheets.cell')),
],
options={
'unique_together': {('source_cell', 'target_cell')},
},
),
migrations.AddIndex(
model_name='cell',
index=models.Index(fields=['sheet', 'table_type', 'row_index'], name='sheets_cell_sheet_i_511238_idx'),
),
migrations.AlterUniqueTogether(
name='cell',
unique_together={('sheet', 'client', 'table_type', 'row_index', 'column_index')},
),
]

View File

@@ -0,0 +1,34 @@
# Generated by Django 6.0.1 on 2026-02-01 11:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('sheets', '0011_alter_betriebskosten_kostentyp_monthlysheet_cell_and_more'),
]
operations = [
migrations.CreateModel(
name='TableConfig',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('table_type', models.CharField(choices=[('top_left', 'Top Left Table'), ('top_right', 'Top Right Table'), ('bottom_1', 'Bottom Table 1'), ('bottom_2', 'Bottom Table 2'), ('bottom_3', 'Bottom Table 3')], max_length=20, unique=True)),
('calculations', models.JSONField(default=dict)),
],
),
migrations.CreateModel(
name='RowCalculation',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('table_type', models.CharField(choices=[('top_left', 'Top Left Table'), ('top_right', 'Top Right Table'), ('bottom_1', 'Bottom Table 1'), ('bottom_2', 'Bottom Table 2'), ('bottom_3', 'Bottom Table 3')], max_length=20)),
('row_index', models.IntegerField()),
('formula', models.TextField()),
('description', models.CharField(blank=True, max_length=200)),
],
options={
'unique_together': {('table_type', 'row_index')},
},
),
]

Binary file not shown.

View File

@@ -8,9 +8,74 @@ class Institute(models.Model):
def __str__(self):
return self.name
class Client(models.Model):
name = models.CharField(max_length=100)
address = models.TextField()
institute = models.ForeignKey(Institute, on_delete=models.CASCADE)
def __str__(self):
return f"{self.name} ({self.institute.name})"
class MonthlySheet(models.Model):
"""Represents one monthly page"""
year = models.IntegerField()
month = models.IntegerField() # 1-12
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
unique_together = ['year', 'month']
ordering = ['year', 'month']
def __str__(self):
return f"{self.year}-{self.month:02d}"
class Cell(models.Model):
"""A single cell in the spreadsheet"""
sheet = models.ForeignKey(MonthlySheet, on_delete=models.CASCADE, related_name='cells')
client = models.ForeignKey(Client, on_delete=models.CASCADE)
table_type = models.CharField(max_length=20, choices=[
('top_left', 'Top Left Table'),
('top_right', 'Top Right Table'),
('bottom_1', 'Bottom Table 1'),
('bottom_2', 'Bottom Table 2'),
('bottom_3', 'Bottom Table 3'),
])
row_index = models.IntegerField() # 0-23 for top tables, 0-9 for bottom
column_index = models.IntegerField() # Actually client index (0-5)
is_formula = models.BooleanField(default=False)
# Cell content
value = models.DecimalField(max_digits=15, decimal_places=6, null=True, blank=True)
formula = models.TextField(blank=True)
# Metadata
data_type = models.CharField(max_length=20, default='number', choices=[
('number', 'Number'),
('text', 'Text'),
('date', 'Date'),
])
class Meta:
unique_together = ['sheet', 'client', 'table_type', 'row_index', 'column_index']
indexes = [
models.Index(fields=['sheet', 'table_type', 'row_index']),
]
def __str__(self):
return f"{self.sheet} - {self.client.name} - {self.table_type}[{self.row_index}][{self.column_index}]"
class CellReference(models.Model):
"""Track dependencies between cells for calculations"""
source_cell = models.ForeignKey(Cell, on_delete=models.CASCADE, related_name='dependents')
target_cell = models.ForeignKey(Cell, on_delete=models.CASCADE, related_name='dependencies')
class Meta:
unique_together = ['source_cell', 'target_cell']
class Betriebskosten(models.Model):
KOSTENTYP_CHOICES = [
('sach', 'Sachkostöen'),
('sach', 'Sachkosten'),
('ln2', 'LN2'),
('helium', 'Helium'),
('inv', 'Inventar'),
@@ -30,16 +95,40 @@ class Betriebskosten(models.Model):
return None
def __str__(self):
return f"{self.buchungsdatum} - {self.get_kostentyp_display()} - {self.betrag}" # Fixed the missing quote
class Client(models.Model):
name = models.CharField(max_length=100)
address = models.TextField()
institute = models.ForeignKey(Institute, on_delete=models.CASCADE) # Remove null=True, blank=True
return f"{self.buchungsdatum} - {self.get_kostentyp_display()} - {self.betrag}"
class RowCalculation(models.Model):
"""Define calculations for specific rows"""
table_type = models.CharField(max_length=20, choices=[
('top_left', 'Top Left Table'),
('top_right', 'Top Right Table'),
('bottom_1', 'Bottom Table 1'),
('bottom_2', 'Bottom Table 2'),
('bottom_3', 'Bottom Table 3'),
])
row_index = models.IntegerField() # Which row has the formula
formula = models.TextField() # e.g., "row_10 + row_9"
description = models.CharField(max_length=200, blank=True)
class Meta:
unique_together = ['table_type', 'row_index']
def __str__(self):
return f"{self.name} ({self.institute.name})"
return f"{self.table_type}[{self.row_index}]: {self.formula}"
# Or simpler: Just store row calculations in a JSONField
class TableConfig(models.Model):
"""Configuration for table calculations"""
table_type = models.CharField(max_length=20, unique=True, choices=[
('top_left', 'Top Left Table'),
('top_right', 'Top Right Table'),
('bottom_1', 'Bottom Table 1'),
('bottom_2', 'Bottom Table 2'),
('bottom_3', 'Bottom Table 3'),
])
calculations = models.JSONField(default=dict) # {11: "10 + 9", 15: "14 - 13"}
def __str__(self):
return f"{self.get_table_type_display()} Config"
class ExcelEntry(models.Model):
client = models.ForeignKey(Client, on_delete=models.CASCADE)
date = models.DateField(default=timezone.now)
@@ -103,6 +192,10 @@ class ExcelEntry(models.Model):
decimal_places=6,
default=0.0
)
def __str__(self):
return f"{self.client.name} - {self.date}"
class SecondTableEntry(models.Model):
client = models.ForeignKey(Client, on_delete=models.CASCADE)
date = models.DateField(default=timezone.now)
@@ -119,4 +212,4 @@ class SecondTableEntry(models.Model):
date_joined = models.DateField(auto_now_add=True)
def __str__(self):
return f"{self.client.name} - {self.date}"
return f"{self.client.name} - {self.date}"

82
sheets/signals.py Normal file
View File

@@ -0,0 +1,82 @@
# sheets/signals.py
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
from django.db.models import Sum
from django.db.models.functions import Coalesce
from decimal import Decimal
# IMPORT THE MODELS AT THE TOP
from .models import ExcelEntry, SecondTableEntry, MonthlySheet, Cell
@receiver([post_save, post_delete], sender=ExcelEntry)
def update_top_right_helium_input(sender, instance, **kwargs):
"""Update top-right table when ExcelEntry changes"""
if instance.date:
# Get the monthly sheet for this date
sheet = MonthlySheet.objects.filter(
year=instance.date.year,
month=instance.date.month
).first()
if sheet:
# Re-populate helium input data
from .views import MonthlySheetView
view = MonthlySheetView()
view.populate_helium_input_to_top_right(sheet)
@receiver([post_save, post_delete], sender=SecondTableEntry)
def update_monthly_sheet_bezug(sender, instance, **kwargs):
"""Update B11 (Bezug) in monthly sheet when SecondTableEntry changes - ONLY for non-start sheets"""
if instance.date and instance.client:
# Check if this is the start sheet (2025-01)
if instance.date.year == 2025 and instance.date.month == 1:
return # Don't auto-update for start sheet
# Get or create the monthly sheet for this date
sheet, created = MonthlySheet.objects.get_or_create(
year=instance.date.year,
month=instance.date.month
)
# Calculate total LHe output for this client in this month
lhe_output_sum = SecondTableEntry.objects.filter(
client=instance.client,
date__year=instance.date.year,
date__month=instance.date.month
).aggregate(
total=Coalesce(Sum('lhe_output'), Decimal('0'))
)['total']
# Update B11 cell (row_index 8 = Excel B11) in TOP-LEFT table
b11_cell_left = Cell.objects.filter(
sheet=sheet,
table_type='top_left',
client=instance.client,
row_index=8 # Excel B11
).first()
if b11_cell_left and (b11_cell_left.value != lhe_output_sum or b11_cell_left.value is None):
b11_cell_left.value = lhe_output_sum
b11_cell_left.save()
# Import here to avoid circular imports
from .views import SaveCellsView
save_view = SaveCellsView()
save_view.calculate_top_left_dependents(sheet, b11_cell_left)
# ALSO update Bezug (row_index 8) in TOP-RIGHT table
b11_cell_right = Cell.objects.filter(
sheet=sheet,
table_type='top_right',
client=instance.client,
row_index=8 # Excel row 12 = Bezug
).first()
if b11_cell_right and (b11_cell_right.value != lhe_output_sum or b11_cell_right.value is None):
b11_cell_right.value = lhe_output_sum
b11_cell_right.save()
# Also trigger top-right calculations
from .views import SaveCellsView
save_view = SaveCellsView()
save_view.calculate_top_right_dependents(sheet, b11_cell_right)

View File

@@ -47,6 +47,7 @@
<a href="{% url 'table_two' %}" class="nav-button">Go to Helium Output</a>
<a href="/admin/" class="nav-button admin-button">Go to Admin Panel</a>
<a href="{% url 'betriebskosten_list' %}" class="nav-button">Betriebskosten</a>
<a href="{% url 'monthly_sheet' year=2024 month=1 %}">Monthly Sheets</a>
</div>
</div>

View File

@@ -0,0 +1,674 @@
{% extends "base.html" %}
{% block content %}
<div class="spreadsheet-container">
<!-- Navigation Header -->
<div class="sheet-navigation">
<a href="{% url 'monthly_sheet' prev_month.year prev_month.month %}">← Previous</a>
<h2>{{ year }} - {{ month_name }} (Sheet {{ month }}/6)</h2>
<a href="{% url 'monthly_sheet' next_month.year next_month.month %}">Next →</a>
<a href="{% url 'summary_sheet' year month %}">Go to Summary</a>
</div>
<!-- Top Tables Container -->
<div class="top-tables">
<!-- Left Table (18 rows × clients) -->
<div class="table-container top-left-table">
<h3>Table 1: Top Left</h3>
<table class="spreadsheet-table">
<thead>
<tr>
<th>Row Label</th>
{% for header in top_left_headers %}
<th>{{ header }}</th>
{% endfor %}
<th>Sum</th>
</tr>
</thead>
<tbody>
{% for row in top_left_rows %}
{% with rownum=forloop.counter %}
<tr data-row="{{ forloop.counter0 }}">
<td class="row-label">
{% if rownum == 1 %}
Stand der Gaszähler (Nm³)
{% elif rownum == 2 %}
Stand der Gaszähler (Vormonat) (Nm³)
{% elif rownum == 3 %}
Gasrückführung (Nm³)
{% elif rownum == 4 %}
Rückführung flüssig (Lit. L-He)
{% elif rownum == 5 %}
Sonderrückführungen (Lit. L-He)
{% elif rownum == 6 %}
Bestand in Kannen-1 (Lit. L-He)
{% elif rownum == 7 %}
Summe Bestand (Lit. L-He)
{% elif rownum == 8 %}
Best. in Kannen Vormonat (Lit. L-He)
{% elif rownum == 9 %}
Bezug (Liter L-He)
{% elif rownum == 10 %}
Rückführ. Soll (Lit. L-He)
{% elif rownum == 11 %}
Verluste (Soll-Rückf.) (Lit. L-He)
{% elif rownum == 12 %}
Füllungen warm (Lit. L-He)
{% elif rownum == 13 %}
Kaltgas Rückgabe (Lit. L-He) Faktor
{% elif rownum == 14 %}
Faktor 0.06
{% elif rownum == 15 %}
Verbraucherverluste (Liter L-He)
{% elif rownum == 16 %}
%
{% elif rownum == 17 %}
Gesamtrückführung (Nm³)
{% elif rownum == 18 %}
Aufgeteilte Verluste (Liter L-He)
{% endif %}
</td>
{% for cell in row.cells %}
{% if is_start_sheet or rownum == 1 or rownum == 5 or rownum == 6 %}
{# Editable in start sheet OR always editable rows (B3, B7, B8) #}
<td class="editable-cell"
data-cell-id="{{ cell.id|default:'' }}"
data-table="top_left"
data-row="{{ forloop.parentloop.counter0 }}"
data-col="{{ forloop.counter0 }}"
data-client-id="{{ cell.client.id|default:'' }}"
contenteditable="true">
{{ cell.value|default:"" }}
</td>
{% else %}
{# Readonly for non-start sheets #}
<td class="readonly-cell"
data-cell-id="{{ cell.id|default:'' }}"
data-table="top_left"
data-row="{{ forloop.parentloop.counter0 }}"
data-col="{{ forloop.counter0 }}"
data-client-id="{{ cell.client.id|default:'' }}"
contenteditable="false">
{{ cell.value|default:"" }}
</td>
{% endif %}
{% endfor %}
<td class="sum-cell">
{{ row.sum|default_if_none:"" }}
</td>
</tr>
{% endwith %}
{% endfor %}
</tbody>
</table>
</div>
<!-- Right Table (24 rows × 6 clients) -->
<!-- Update the top-right table section in monthly_sheet.html -->
<div class="table-container top-right-table">
<h3>Table 2: Top Right</h3>
<table class="spreadsheet-table">
<thead>
<tr>
<th>Row Label</th>
{% for header in top_right_headers %}
<th>{{ header }}</th>
{% endfor %}
<th>Sum</th>
</tr>
</thead>
<tbody>
{% for row in top_right_rows %}
{% with rownum=forloop.counter %}
<tr data-row="{{ forloop.counter0 }}">
<td class="row-label">
{% if rownum == 1 %}
Stand der Gaszähler (Vormonat) (Nm³)
{% elif rownum == 2 %}
Gasrückführung (Nm³)
{% elif rownum == 3 %}
Rückführung flüssig (Lit. L-He)
{% elif rownum == 4 %}
Sonderrückführungen (Lit. L-He)
{% elif rownum == 5 %}
Sammelrückführungen (Lit. L-He)
{% elif rownum == 6 %}
Bestand in Kannen-1 (Lit. L-He)
{% elif rownum == 7 %}
Summe Bestand (Lit. L-He)
{% elif rownum == 8 %}
Best. in Kannen Vormonat (Lit. L-He)
{% elif rownum == 9 %}
Bezug (Liter L-He)
{% elif rownum == 10 %}
Rückführ. Soll (Lit. L-He)
{% elif rownum == 11 %}
Verluste (Soll-Rückf.) (Lit. L-He)
{% elif rownum == 12 %}
Füllungen warm (Lit. L-He)
{% elif rownum == 13 %}
Kaltgas Rückgabe (Lit. L-He) Faktor
{% elif rownum == 14 %}
Faktor 0.06
{% elif rownum == 15 %}
Verbraucherverluste (Liter L-He)
{% elif rownum == 16 %}
%
{% endif %}
</td>
{% for cell in row.cells %}
{% with client_name=cell.client.name|default:"" %}
{# Determine if this cell should be editable #}
{% if is_start_sheet %}
<td class="editable-cell"
data-cell-id="{{ cell.id|default:'' }}"
data-table="top_right"
data-row="{{ forloop.parentloop.counter0 }}"
data-col="{{ forloop.counter0 }}"
data-client-id="{{ cell.client.id|default:'' }}"
contenteditable="true">
{{ cell.value|default:"" }}
</td>
{% elif rownum == 4 or rownum == 6 or rownum == 1 and client_name in "M3 Thiele,M3 Buntkowsky,M3 Gutfleisch" %}
<td class="editable-cell"
data-cell-id="{{ cell.id|default:'' }}"
data-table="top_right"
data-row="{{ forloop.parentloop.counter0 }}"
data-col="{{ forloop.counter0 }}"
data-client-id="{{ cell.client.id|default:'' }}"
contenteditable="true">
{{ cell.value|default:"" }}
</td>
{% else %}
<td class="readonly-cell"
data-cell-id="{{ cell.id|default:'' }}"
data-table="top_right"
data-row="{{ forloop.parentloop.counter0 }}"
data-col="{{ forloop.counter0 }}"
data-client-id="{{ cell.client.id|default:'' }}"
contenteditable="false">
{% if rownum == 2 %}
Aufteilung Nach Verbrauch
{% else %}
{{ cell.value|default:"" }}
{% endif %}
</td>
{% endif %}
{% endwith %}
{% endfor %}
<td class="sum-cell">
{{ row.sum|default_if_none:"" }}
</td>
</tr>
{% endwith %}
{% endfor %}
</tbody>
</table>
</div>
<!-- Bottom Tables -->
<div class="bottom-tables">
<div class="table-container bottom-table-1">
<h3>Bottom Table 1</h3>
<table class="spreadsheet-table">
<thead>
<tr>
<th>Row Label</th>
{% for client in clients %}
<th>{{ client.name }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for row in cells_by_table.bottom_1 %}
<tr data-row="{{ forloop.counter0 }}">
<td class="row-label">Row {{ forloop.counter }}</td>
{% for cell in row %}
<td class="editable-cell"
data-cell-id="{{ cell.id|default:'' }}"
data-table="bottom_1"
data-row="{{ forloop.parentloop.counter0 }}"
data-col="{{ forloop.counter0 }}"
data-client-id="{{ cell.client.id|default:'' }}"
contenteditable="true">
{{ cell.value|default:"" }}
</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="table-container bottom-table-2">
<h3>Bottom Table 2</h3>
<table class="spreadsheet-table">
<thead>
<tr>
<th>Row Label</th>
{% for client in clients %}
<th>{{ client.name }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for row in cells_by_table.bottom_2 %}
<tr data-row="{{ forloop.counter0 }}">
<td class="row-label">Row {{ forloop.counter }}</td>
{% for cell in row %}
<td class="editable-cell"
data-cell-id="{{ cell.id|default:'' }}"
data-table="bottom_2"
data-row="{{ forloop.parentloop.counter0 }}"
data-col="{{ forloop.counter0 }}"
data-client-id="{{ cell.client.id|default:'' }}"
contenteditable="true">
{{ cell.value|default:"" }}
</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="table-container bottom-table-3">
<h3>Bottom Table 3</h3>
<table class="spreadsheet-table">
<thead>
<tr>
<th>Row Label</th>
{% for client in clients %}
<th>{{ client.name }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for row in cells_by_table.bottom_3 %}
<tr data-row="{{ forloop.counter0 }}">
<td class="row-label">Row {{ forloop.counter }}</td>
{% for cell in row %}
<td class="editable-cell"
data-cell-id="{{ cell.id|default:'' }}"
data-table="bottom_3"
data-row="{{ forloop.parentloop.counter0 }}"
data-col="{{ forloop.counter0 }}"
data-client-id="{{ cell.client.id|default:'' }}"
contenteditable="true">
{{ cell.value|default:"" }}
</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- Save Button -->
<div class="action-bar">
<button id="save-all-btn" class="btn btn-primary">Save All Cells</button>
<div id="save-status"></div>
<div class="legend">
<small>
<span style="background-color: #d1ecf1; padding: 2px 5px;">Saved cells</span>
<span style="background-color: #d4edda; padding: 2px 5px;">Calculated cells</span>
</small>
</div>
</div>
</div>
<!-- Hidden form for cell data -->
<form id="cell-data-form" style="display: none;">
{% csrf_token %}
<input type="hidden" name="sheet_id" value="{{ sheet.id }}">
</form>
<style>
.spreadsheet-container {
padding: 20px;
}
.sheet-navigation {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding: 10px;
background-color: #f8f9fa;
border-radius: 5px;
}
.sheet-navigation h2 {
margin: 0;
}
.top-tables {
display: flex;
gap: 20px;
margin-bottom: 20px;
}
.top-tables .table-container {
flex: 1;
border: 1px solid #ddd;
border-radius: 5px;
padding: 10px;
background-color: white;
}
.merged-cell-left {
border-right-width: 0;
}
.merged-cell-right {
border-left-width: 0;
}
.cell-group-LM {
background-color: #f0f8ff; /* Light blue for L&M group */
}
.cell-group-NO {
background-color: #f0fff0; /* Light green for N&O group */
}
.cell-group-PQR {
background-color: #fff0f0; /* Light red for P,Q,R group */
}
.bottom-tables {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
margin-bottom: 20px;
}
.bottom-tables .table-container {
border: 1px solid #ddd;
border-radius: 5px;
padding: 10px;
background-color: white;
}
.spreadsheet-table {
width: 100%;
border-collapse: collapse;
}
.spreadsheet-table th,
.spreadsheet-table td {
border: 1px solid #ddd;
padding: 8px;
text-align: center;
}
.spreadsheet-table th {
background-color: #f2f2f2;
font-weight: bold;
}
.row-label {
background-color: #f9f9f9;
font-weight: bold;
}
.editable-cell {
min-width: 100px;
min-height: 30px;
background-color: white;
cursor: text;
}
.editable-cell:focus {
outline: 2px solid #007bff;
background-color: #f0f8ff;
}
.editable-cell:hover {
background-color: #f1f1f1;
}
.readonly-cell {
background-color: #f8f9fa;
color: #495057;
cursor: default;
min-height: 30px;
}
.cell-calculated {
font-style: italic;
font-weight: bold;
background-color: #e8f4f8;
}
.sum-cell {
font-weight: bold;
background-color: #f0f0f0;
padding: 5px;
}
.action-bar {
display: flex;
gap: 10px;
align-items: center;
padding: 10px;
background-color: #f8f9fa;
border-radius: 5px;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.btn-primary {
background-color: #007bff;
color: white;
}
#save-status {
margin-left: 10px;
color: #28a745;
font-weight: bold;
}
.saving-text {
position: absolute;
font-size: 11px;
color: #666;
margin-left: 5px;
}
.saving {
background-color: #fff3cd !important;
font-style: italic;
}
.saved-success {
background-color: #d4edda !important;
transition: background-color 0.5s ease;
}
.calculated-cell {
background-color: #e8f4f8 !important;
font-style: italic;
color: #0c5460;
}
.editable-cell[contenteditable="false"] {
background-color: #f8f9fa !important;
cursor: not-allowed;
}
</style>
<script>
$(document).ready(function() {
// Enable editing on focus
$('.editable-cell').on('focus', function() {
$(this).data('original-value', $(this).text().trim());
});
// Save on blur
$('.editable-cell').on('blur', function() {
saveCell($(this));
});
// Save on Enter key
$('.editable-cell').on('keydown', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
$(this).blur();
}
});
// Prevent editing of readonly cells
$('.readonly-cell').on('focus click', function(e) {
e.preventDefault();
$(this).blur();
});
function saveCell($cell) {
const cellId = $cell.data('cell-id');
const newValue = $cell.text().trim();
const originalValue = $cell.data('original-value') || '';
// Skip if no change
if (newValue === originalValue) {
return;
}
// Skip if no cell ID
if (!cellId) {
console.error('No cell ID found');
return;
}
// Show saving indicator
$cell.addClass('saving');
// Prepare AJAX request
const csrfToken = $('[name=csrfmiddlewaretoken]').val();
$.ajax({
url: '{% url "save_cells" %}',
method: 'POST',
headers: {
'X-CSRFToken': csrfToken
},
data: {
'sheet_id': '{{ sheet.id }}',
'cell_id': cellId,
'value': newValue
},
success: function(response) {
$cell.removeClass('saving');
if (response.status === 'success') {
// Update all cells returned by the server
if (response.updated_cells) {
response.updated_cells.forEach(function(cellData) {
const targetCell = $('[data-cell-id="' + cellData.id + '"]');
if (targetCell.length) {
targetCell.text(cellData.value || '');
targetCell.data('original-value', cellData.value || '');
// Style calculated cells differently
// Style calculated cells differently
if (cellData.is_calculated) {
targetCell.addClass('calculated-cell');
// Leave contenteditable as set by the template
}
}
});
}
// Show success message
showStatus('Saved successfully!', 'success');
// Highlight the saved cell briefly
$cell.addClass('saved-success');
setTimeout(function() {
$cell.removeClass('saved-success');
}, 1000);
} else {
// Restore original value on error
$cell.text(originalValue);
showStatus('Error: ' + response.message, 'error');
}
},
error: function(xhr, status, error) {
$cell.removeClass('saving');
$cell.text(originalValue);
showStatus('Error: ' + error, 'error');
console.error('Save error:', error);
}
});
}
// Save All button
$('#save-all-btn').on('click', function() {
const csrfToken = $('[name=csrfmiddlewaretoken]').val();
const formData = new FormData();
formData.append('sheet_id', '{{ sheet.id }}');
formData.append('csrfmiddlewaretoken', csrfToken);
// Collect all cell values
$('.editable-cell').each(function() {
const cellId = $(this).data('cell-id');
if (cellId) {
formData.append('cell_' + cellId, $(this).text().trim());
}
});
$.ajax({
url: '{% url "save_cells" %}',
method: 'POST',
data: formData,
processData: false,
contentType: false,
headers: {
'X-CSRFToken': csrfToken
},
success: function(response) {
if (response.status === 'success') {
if (response.updated_cells) {
response.updated_cells.forEach(function(cellData) {
const targetCell = $('[data-cell-id="' + cellData.id + '"]');
if (targetCell.length) {
targetCell.text(cellData.value || '');
targetCell.data('original-value', cellData.value || '');
}
});
}
showStatus('All cells saved!', 'success');
} else {
showStatus('Error: ' + response.message, 'error');
}
},
error: function() {
showStatus('Error saving all cells', 'error');
}
});
});
function showStatus(message, type) {
const statusEl = $('#save-status');
statusEl.text(message);
statusEl.css('color', type === 'success' ? '#28a745' : '#dc3545');
setTimeout(function() {
statusEl.text('');
}, 3000);
}
});
</script>
{% endblock %}

View File

@@ -191,15 +191,184 @@
.readonly-field {
background-color: #e9ecef;
color: #6c757d;
}
/* ---- 6-month overview card ---- */
.overview-card {
background-color: #ffffff;
padding: 16px 20px;
margin: 20px 0;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.08);
}
.overview-header {
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: flex-end;
margin-bottom: 12px;
}
.overview-header label {
font-size: 0.9rem;
color: #555;
}
.overview-header select {
padding: 6px 8px;
border-radius: 4px;
border: 1px solid #ccc;
}
.overview-header button {
padding: 7px 14px;
border-radius: 4px;
border: none;
background-color: #007bff;
color: #fff;
cursor: pointer;
}
.overview-header button:hover {
background-color: #0056b3;
}
.overview-title {
font-size: 1.1rem;
font-weight: bold;
margin-bottom: 8px;
color: #333;
}
.overview-subtitle {
font-size: 0.9rem;
color: #555;
margin-bottom: 8px;
}
.overview-table {
width: 100%;
border-collapse: collapse;
}
.overview-table th,
.overview-table td {
padding: 6px 8px;
border-bottom: 1px solid #eee;
font-size: 0.9rem;
}
.overview-table thead th {
background-color: #007bff;
color: #fff;
text-align: right;
}
.overview-table thead th:first-child {
text-align: left;
}
.overview-table tbody tr:hover {
background-color: #f8f9ff;
}
.overview-table .number-cell {
text-align: right;
}
.overview-table .summary-row {
background-color: #f0f4ff;
font-weight: bold;
}
</style>
</head>
<body>
<!-- This link should ONLY wrap the text below -->
<a href="{% url 'clients_list' %}" class="btn btn-outline-primary">
&#8678; Go to Clients
</a>
<!-- 6-Month overview card (OUTSIDE any <a>) -->
<div class="overview-card">
<div class="overview-title">Helium Input 6 Month Overview</div>
<form method="get" class="overview-header">
<div>
<label for="overview-year">Year</label><br>
<select name="overview_year" id="overview-year">
{% for y in available_years %}
<option value="{{ y }}"
{% if overview and overview.year == y %}selected{% endif %}>
{{ y }}
</option>
{% endfor %}
</select>
</div>
<div>
<label for="overview-start-month">Start month</label><br>
<select name="overview_start_month" id="overview-start-month">
{% for m, label in month_choices %}
<option value="{{ m }}"
{% if overview and overview.start_month == m %}selected{% endif %}>
{{ m }} - {{ label }}
</option>
{% endfor %}
</select>
</div>
<div>
<button type="submit">Show overview</button>
</div>
</form>
{% if overview %}
<div class="overview-subtitle">
Period:
<strong>{{ overview.start_month }}{{ overview.end_month }} / {{ overview.year }}</strong>
</div>
<table class="overview-table">
<thead>
<tr>
<th>Month</th>
{% for g in overview.groups %}
<th>{{ g.label }}</th>
{% endfor %}
<th>Month total</th>
</tr>
</thead>
<tbody>
{% for row in overview.rows %}
<tr>
<td>
{{ row.month_number }} - {{ row.month_label|slice:":3" }}
</td>
{% for value in row.values %}
<td class="number-cell">{{ value|floatformat:2 }}</td>
{% endfor %}
<td class="number-cell">{{ row.total|floatformat:2 }}</td>
</tr>
{% endfor %}
<tr class="summary-row">
<td>Summe</td>
{% for total in overview.group_totals %}
<td class="number-cell">{{ total|floatformat:2 }}</td>
{% endfor %}
<td class="number-cell">{{ overview.grand_total|floatformat:2 }}</td>
</tr>
</tbody>
</table>
{% else %}
<div class="overview-subtitle">
No data yet choose a year and start month and click “Show overview”.
</div>
{% endif %}
</div>
<h2>Helium Input</h2>
<div class="table-container">
<button class="add-row-btn" id="add-row-one">Add Row</button>
@@ -218,7 +387,8 @@
<col style="width: 5%"> <!-- L-He -->
<col style="width: 5%"> <!-- L-He zus. -->
<col style="width: 6%"> <!-- L-He ges. -->
<col style="width: 6%"> <!-- Date Joined -->
<col style="width: 7%"> <!-- Date -->
<col style="width: 5%"> <!-- Month -->
<col style="width: 8%"> <!-- Actions -->
</colgroup>
<thead>
@@ -236,7 +406,8 @@
<th>L-He</th>
<th>L-He zus.</th>
<th>L-He ges.</th>
<th>Date Joined</th>
<th>Date</th>
<th>Month</th>
<th>Actions</th>
</tr>
</thead>
@@ -256,7 +427,16 @@
<td>{{ entry.lhe|floatformat:6 }}</td>
<td>{{ entry.lhe_zus|floatformat:3 }}</td>
<td>{{ entry.lhe_ges|floatformat:6 }}</td>
<td>{{ entry.date_joined|date:"Y-m-d" }}</td>
<td>
{% if entry.date %}
{{ entry.date|date:"d.m.Y" }}
{% endif %}
</td>
<td>
{% if entry.date %}
{{ entry.date|date:"m" }} {# e.g. 0112 #}
{% endif %}
</td>
<td class="actions">
<button class="edit-btn-one">Edit</button>
<button class="delete-btn-one">Delete</button>
@@ -645,6 +825,7 @@
<td>${response.lhe_zus}</td>
<td>${response.lhe_ges}</td>
<td>${response.date || ''}</td>
<td>${response.month || ''}</td>
<td class="actions">
<button class="edit-btn-one">Edit</button>
<button class="delete-btn-one">Delete</button>
@@ -805,6 +986,7 @@
row.find('td:eq(11)').text(response.lhe_zus);
row.find('td:eq(12)').text(response.lhe_ges);
row.find('td:eq(13)').text(response.date || '');
row.find('td:eq(14)').text(response.month || '');
$('#edit-popup-one').fadeOut();
} else {
alert('Error: ' + (response.message || 'Failed to update entry'));

View File

@@ -1,15 +1,26 @@
from django.urls import path
from .views import DebugTopRightView
from .views import SaveCellsView
from . import views
# Create your URLs here.
urlpatterns = [
path('', views.clients_list, name='clients_list'), # Main page
path('table-one/', views.table_one_view, name='table_one'), # Table One
path('table-two/', views.table_two_view, name='table_two'), # Table Two
path('', views.clients_list, name='clients_list'),
path('table-one/', views.table_one_view, name='table_one'),
path('table-two/', views.table_two_view, name='table_two'),
path('add-entry/<str:model_name>/', views.add_entry, name='add_entry'),
path('update-entry/<str:model_name>/', views.update_entry, name='update_entry'),
path('delete-entry/<str:model_name>/', views.delete_entry, name='delete_entry'),
path('betriebskosten/', views.betriebskosten_list, name='betriebskosten_list'),
path('betriebskosten/create/', views.betriebskosten_create, name='betriebskosten_create'),
path('betriebskosten/delete/', views.betriebskosten_delete, name='betriebskosten_delete'),
]
path('check-sheets/', views.CheckSheetView.as_view(), name='check_sheets'),
path('quick-debug/', views.QuickDebugView.as_view(), name='quick_debug'),
path('test-formula/', views.TestFormulaView.as_view(), name='test_formula'),
path('simple-debug/', views.SimpleDebugView.as_view(), name='simple_debug'),
path('sheet/<int:year>/<int:month>/', views.MonthlySheetView.as_view(), name='monthly_sheet'),
path('summary/<int:year>/<int:start_month>/', views.SummarySheetView.as_view(), name='summary_sheet'),
path("save-cells/", SaveCellsView.as_view(), name="save_cells"),
path('calculate/', views.CalculateView.as_view(), name='calculate'),
path('debug-calculation/', views.DebugCalculationView.as_view(), name='debug_calculation'),
path('debug-top-right/', DebugTopRightView.as_view(), name='debug_top_right'),
]

File diff suppressed because it is too large Load Diff

2873
sheets/views.txt Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.