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

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