diff --git a/db.sqlite3 b/db.sqlite3
index 4121ae1..462e62b 100644
Binary files a/db.sqlite3 and b/db.sqlite3 differ
diff --git a/sheets/forms.py b/sheets/forms.py
index 5704562..9521965 100644
--- a/sheets/forms.py
+++ b/sheets/forms.py
@@ -1,7 +1,27 @@
from django import forms
-from .models import ExcelEntry
+from .models import ExcelEntry, Betriebskosten
class ExcelEntryForm(forms.ModelForm):
class Meta:
model = ExcelEntry
- fields = ['name', 'age', 'email'] # Include only the fields you want users to input
+ fields = '__all__' # Include only the fields you want users to input
+
+class BetriebskostenForm(forms.ModelForm):
+ class Meta:
+ model = Betriebskosten
+ fields = ['buchungsdatum', 'rechnungsnummer', 'kostentyp', 'gas_volume', 'betrag', 'beschreibung']
+ widgets = {
+ 'buchungsdatum': forms.DateInput(attrs={'type': 'date'}),
+ 'beschreibung': forms.Textarea(attrs={'rows': 3}),
+ }
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.fields['gas_volume'].required = False
+
+ # Add price per liter field (readonly)
+ self.fields['price_per_liter'] = forms.CharField(
+ label='Preis pro Liter (€)',
+ required=False,
+ widget=forms.TextInput(attrs={'readonly': 'readonly'})
+ )
\ No newline at end of file
diff --git a/sheets/migrations/0007_betriebskosten.py b/sheets/migrations/0007_betriebskosten.py
new file mode 100644
index 0000000..1f73ebe
--- /dev/null
+++ b/sheets/migrations/0007_betriebskosten.py
@@ -0,0 +1,25 @@
+# Generated by Django 5.1.5 on 2025-08-28 09:19
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('sheets', '0006_remove_excelentry_age_remove_excelentry_email_and_more'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Betriebskosten',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('buchungsdatum', models.DateField(verbose_name='Buchungsdatum')),
+ ('rechnungsnummer', models.CharField(max_length=50, verbose_name='Rechnungsnummer')),
+ ('kostentyp', models.CharField(choices=[('sach', 'Sachkosten'), ('ln2', 'LN2'), ('helium', 'Helium'), ('inv', 'Inventar')], max_length=10, verbose_name='Kostentyp')),
+ ('gas_volume', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='Gasvolumen (Liter)')),
+ ('betrag', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Betrag (€)')),
+ ('beschreibung', models.TextField(blank=True, verbose_name='Beschreibung')),
+ ],
+ ),
+ ]
diff --git a/sheets/models.py b/sheets/models.py
index 22cf221..8364335 100644
--- a/sheets/models.py
+++ b/sheets/models.py
@@ -3,6 +3,29 @@ from django.utils import timezone
from django.core.validators import MinValueValidator, MaxValueValidator
+class Betriebskosten(models.Model):
+ KOSTENTYP_CHOICES = [
+ ('sach', 'Sachkostöen'),
+ ('ln2', 'LN2'),
+ ('helium', 'Helium'),
+ ('inv', 'Inventar'),
+ ]
+
+ buchungsdatum = models.DateField('Buchungsdatum')
+ rechnungsnummer = models.CharField('Rechnungsnummer', max_length=50)
+ kostentyp = models.CharField('Kostentyp', max_length=10, choices=KOSTENTYP_CHOICES)
+ gas_volume = models.DecimalField('Gasvolumen (Liter)', max_digits=10, decimal_places=2, null=True, blank=True)
+ betrag = models.DecimalField('Betrag (€)', max_digits=10, decimal_places=2)
+ beschreibung = models.TextField('Beschreibung', blank=True)
+
+ @property
+ def price_per_liter(self):
+ if self.kostentyp == 'helium' and self.gas_volume:
+ return self.betrag / self.gas_volume
+ 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)
diff --git a/sheets/templates/base.html b/sheets/templates/base.html
index 151779f..8c92798 100644
--- a/sheets/templates/base.html
+++ b/sheets/templates/base.html
@@ -3,6 +3,9 @@
{% block title %}My App{% endblock %}
+
+
+
diff --git a/sheets/templates/betriebskosten_list.html b/sheets/templates/betriebskosten_list.html
new file mode 100644
index 0000000..a745799
--- /dev/null
+++ b/sheets/templates/betriebskosten_list.html
@@ -0,0 +1,479 @@
+
+
+
+
+
+ Betriebskosten
+
+
+
+
+
+
+
+
+ ⇦ Go to Clients
+
+
+ Betriebskosten
+
+
+
+
+
+ # |
+ Buchungsdatum |
+ Rechnungsnummer |
+ Kostentyp |
+ Gasvolumen (L) |
+ Betrag (€) |
+ Beschreibung |
+ Actions |
+
+
+
+ {% for item in items %}
+
+ {{ forloop.counter }} |
+ {{ item.buchungsdatum|date:"Y-m-d" }} |
+ {{ item.rechnungsnummer }} |
+ {{ item.get_kostentyp_display }} |
+ {{ item.gas_volume|default_if_none:"-" }} |
+ {{ item.betrag }} |
+ {{ item.beschreibung|default:"" }} |
+
+
+
+ |
+
+ {% empty %}
+
+ No entries found |
+
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/sheets/templates/clients_table.html b/sheets/templates/clients_table.html
index 8591a0c..985b14a 100644
--- a/sheets/templates/clients_table.html
+++ b/sheets/templates/clients_table.html
@@ -2,6 +2,9 @@
{% block content %}
+
+
Helium Output Yearly Summary
+
@@ -40,12 +43,10 @@
@@ -56,6 +57,12 @@
padding: 20px;
}
+ .page-title {
+ text-align: center;
+ color: #333;
+ margin-bottom: 20px;
+ }
+
.year-filter {
margin: 20px 0;
text-align: right;
@@ -120,5 +127,13 @@
.nav-button:hover {
background-color: #0056b3;
}
+
+ .admin-button {
+ background-color: #6c757d;
+ }
+
+ .admin-button:hover {
+ background-color: #5a6268;
+ }
{% endblock %}
\ No newline at end of file
diff --git a/sheets/templates/table_two.html b/sheets/templates/table_two.html
index 5c74be4..a740a29 100644
--- a/sheets/templates/table_two.html
+++ b/sheets/templates/table_two.html
@@ -316,7 +316,7 @@
$('#save-add-two').on('click', function() {
let formData = {
'client_id': $('#add-client-id').val(),
- 'date': $('#add-date').val(),
+ 'date': $('#add-date').val(), // Ensure this is in YYYY-MM-DD format
'is_warm': $('#add-is-warm').is(':checked'),
'lhe_delivery': $('#add-lhe-delivery').val(),
'lhe_output': $('#add-lhe-output').val(),
@@ -324,6 +324,12 @@
'csrfmiddlewaretoken': '{{ csrf_token }}'
};
+ // Validate date format
+ if (!formData.date) {
+ alert('Please select a date');
+ return;
+ }
+
// Validate LHe Output is a number
if (isNaN(parseFloat(formData.lhe_output))) {
alert('LHe Output must be a valid number');
@@ -335,16 +341,32 @@
method: 'POST',
data: formData,
success: function(response) {
- // Clear the form
- $('#add-popup-two').find('input, textarea, select').val('');
- $('#add-is-warm').prop('checked', false);
- $('#add-popup-two').fadeOut();
-
- // Reload the table data
- loadTableData();
+ if (response.status === 'success') {
+ // Add the new row to the table
+ let newRow = `
+
+ ${$('#table-two tbody tr').length + 1} |
+ ${response.id} |
+ ${response.client_name} |
+ ${response.date || ''} |
+ ${response.is_warm ? 'Yes' : 'No'} |
+ ${response.lhe_delivery} |
+ ${response.lhe_output || ''} |
+ ${response.notes || ''} |
+
+
+
+ |
+
+ `;
+ $('#table-two tbody').append(newRow);
+ $('#add-popup-two').fadeOut();
+ } else {
+ alert('Error: ' + (response.message || 'Failed to add entry'));
+ }
},
error: function(xhr) {
- alert('Error: ' + xhr.responseJSON?.message || 'Failed to add entry');
+ alert('Error: ' + (xhr.responseJSON?.message || 'Server error'));
}
});
});
@@ -366,18 +388,22 @@
}
// Open Edit Popup
- $(document).on('click', '.edit-btn-two', function () {
+ $(document).on('click', '.edit-btn-two', function() {
let row = $(this).closest('tr');
- currentTableId = row.closest('table').attr('id');
- currentModelName = 'SecondTableEntry';
-
- // Get data from the row
- let is_warm = row.find('td:eq(4)').text().trim() === 'Yes';
-
$('#edit-id').val(row.data('id'));
- $('#edit-client-id').val(row.find('td:eq(2)').text().trim());
- $('#edit-date').val(row.find('td:eq(3)').text().trim());
- $('#edit-is-warm').prop('checked', is_warm);
+
+ // Set client - find by name since we're showing names in the table
+ let clientName = row.find('td:eq(2)').text().trim();
+ $(`#edit-client-id option:contains("${clientName}")`).prop('selected', true);
+
+ // Set date - ensure proper format
+ let dateText = row.find('td:eq(3)').text().trim();
+ if (dateText) {
+ $('#edit-date').val(dateText);
+ }
+
+ // Set other fields
+ $('#edit-is-warm').prop('checked', row.find('td:eq(4)').text().trim() === 'Yes');
$('#edit-lhe-delivery').val(row.find('td:eq(5)').text().trim());
$('#edit-lhe-output').val(row.find('td:eq(6)').text().trim());
$('#edit-notes').val(row.find('td:eq(7)').text().trim());
@@ -386,45 +412,48 @@
});
// Save Edit Entry
- $('#save-edit-two').on('click', function () {
- let id = $('#edit-id').val();
- let client_id = $('#edit-client-id').val();
- let date = $('#edit-date').val();
- let is_warm = $('#edit-is-warm').is(':checked');
- let lhe_delivery = $('#edit-lhe-delivery').val();
- let lhe_output = $('#edit-lhe-output').val();
- let notes = $('#edit-notes').val();
+ $('#save-edit-two').on('click', function() {
+ let formData = {
+ 'id': $('#edit-id').val(),
+ 'client_id': $('#edit-client-id').val(),
+ 'date': $('#edit-date').val(), // Already in YYYY-MM-DD format
+ 'is_warm': $('#edit-is-warm').is(':checked'),
+ 'lhe_delivery': $('#edit-lhe-delivery').val(),
+ 'lhe_output': $('#edit-lhe-output').val(),
+ 'notes': $('#edit-notes').val(),
+ 'csrfmiddlewaretoken': '{{ csrf_token }}'
+ };
+
+ // Validate inputs
+ if (!formData.date) {
+ alert('Please select a date');
+ return;
+ }
+ if (isNaN(parseFloat(formData.lhe_output))) {
+ alert('Please enter a valid LHe Output value');
+ return;
+ }
$.ajax({
- url: `/update-entry/${currentModelName}/`,
+ url: '/update-entry/SecondTableEntry/',
method: 'POST',
- data: {
- 'id': id,
- 'client_id': client_id,
- 'date': date,
- 'is_warm': is_warm,
- 'lhe_delivery': lhe_delivery,
- 'lhe_output': lhe_output,
- 'notes': notes,
- 'csrfmiddlewaretoken': '{{ csrf_token }}'
- },
- success: function (response) {
+ data: formData,
+ success: function(response) {
if (response.status === 'success') {
let row = $(`tr[data-id="${response.id}"]`);
row.find('td:eq(2)').text(response.client_name);
- row.find('td:eq(3)').text(response.date);
+ row.find('td:eq(3)').text(response.date || '');
row.find('td:eq(4)').text(response.is_warm ? 'Yes' : 'No');
row.find('td:eq(5)').text(response.lhe_delivery);
- row.find('td:eq(6)').text(response.lhe_output);
- row.find('td:eq(7)').text(response.notes);
+ row.find('td:eq(6)').text(response.lhe_output || '');
+ row.find('td:eq(7)').text(response.notes || '');
$('#edit-popup-two').fadeOut();
} else {
- alert('Update failed: ' + (response.message || 'Please try again later'));
+ alert('Update failed: ' + (response.message || 'Unknown error'));
}
},
- error: function (xhr, status, error) {
- alert('Update failed: ' + (xhr.responseJSON?.message || 'Please try again later'));
- console.error('Error:', error);
+ error: function(xhr) {
+ alert('Error: ' + (xhr.responseJSON?.message || 'Server error'));
}
});
});
diff --git a/sheets/urls.py b/sheets/urls.py
index 9944e4a..ee7436f 100644
--- a/sheets/urls.py
+++ b/sheets/urls.py
@@ -8,4 +8,7 @@ urlpatterns = [
path('add-entry/
/', views.add_entry, name='add_entry'),
path('update-entry//', views.update_entry, name='update_entry'),
path('delete-entry//', 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'),
]
diff --git a/sheets/views.py b/sheets/views.py
index a909350..2365ff7 100644
--- a/sheets/views.py
+++ b/sheets/views.py
@@ -1,4 +1,5 @@
-from django.shortcuts import render
+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
@@ -7,41 +8,62 @@ from datetime import date, datetime
from django.utils import timezone
from .models import Client, SecondTableEntry
from django.db.models import Sum
+from django.urls import reverse
+from django.db.models.functions import Coalesce
+from .forms import BetriebskostenForm
+from .models import Betriebskosten
+from django.utils.dateparse import parse_date
# Clients Page (Main)
+from django.shortcuts import render
+from django.db.models import Sum
+from datetime import datetime
+from .models import Client, SecondTableEntry
+
def clients_list(request):
- # Annual summary data
- current_year = int(request.GET.get('year', datetime.now().year))
+ # 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=current_year,
+ date__year=selected_year,
date__month=month
- ).aggregate(total=Sum('lhe_output'))['total'] or 0
+ ).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)
})
-
- available_years = SecondTableEntry.objects.dates('date', 'year').distinct()
-
+
return render(request, 'clients_table.html', {
'monthly_data': monthly_data,
- 'current_year': current_year,
- 'available_years': [y.year for y in available_years],
+ '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):
ExcelEntry = apps.get_model('sheets', 'ExcelEntry')
@@ -80,20 +102,26 @@ def add_entry(request, model_name):
try:
model = apps.get_model('sheets', model_name)
- common_data = {
- 'client': Client.objects.get(id=request.POST.get('client_id')),
- 'date': request.POST.get('date'),
- 'notes': request.POST.get('notes', '')
- }
-
if model_name == 'SecondTableEntry':
+ # Handle date conversion
+ 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)
+
# Handle Helium Output (Table Two)
lhe_output = request.POST.get('lhe_output')
entry = model.objects.create(
- **common_data,
+ client=Client.objects.get(id=request.POST.get('client_id')),
+ date=date_obj,
is_warm=request.POST.get('is_warm') == 'true',
lhe_delivery=request.POST.get('lhe_delivery', ''),
- lhe_output=Decimal(lhe_output) if lhe_output else None
+ lhe_output=Decimal(lhe_output) if lhe_output else None,
+ notes=request.POST.get('notes', '')
)
return JsonResponse({
@@ -158,15 +186,33 @@ def update_entry(request, model_name):
# Common updates for both models
entry.client = Client.objects.get(id=request.POST.get('client_id'))
- entry.date = request.POST.get('date')
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 (Table Two)
- lhe_output = request.POST.get('lhe_output')
+ # Handle Helium Output specific fields
entry.is_warm = request.POST.get('is_warm') == 'true'
entry.lhe_delivery = request.POST.get('lhe_delivery', '')
- entry.lhe_output = Decimal(lhe_output) if lhe_output else None
+
+ 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({
@@ -181,29 +227,28 @@ def update_entry(request, model_name):
})
elif model_name == 'ExcelEntry':
- # Handle Helium Input (Table One)
- date_str = request.POST.get('date')
+ # Handle Helium Input specific fields
try:
- date_obj = datetime.strptime(date_str, '%Y-%m-%d').date() if date_str else None
- except (ValueError, TypeError):
- date_obj = None
+ entry.pressure = Decimal(request.POST.get('pressure', 0))
+ entry.purity = Decimal(request.POST.get('purity', 0))
+ except InvalidOperation:
+ return JsonResponse({
+ 'status': 'error',
+ 'message': 'Invalid pressure or purity value'
+ }, status=400)
- entry.client = Client.objects.get(id=request.POST.get('client_id'))
- entry.date = date_obj
- entry.pressure = Decimal(request.POST.get('pressure', 0))
- entry.purity = Decimal(request.POST.get('purity', 0))
- entry.notes = request.POST.get('notes', '')
entry.save()
return JsonResponse({
'status': 'success',
'id': entry.id,
'client_name': entry.client.name,
+ 'date': entry.date.strftime('%Y-%m-%d') if entry.date else '',
'pressure': str(entry.pressure),
'purity': str(entry.purity),
- 'date': entry.date.strftime('%Y-%m-%d') if entry.date else '',
'notes': entry.notes
})
+
except model.DoesNotExist:
return JsonResponse({'status': 'error', 'message': 'Entry not found'}, status=404)
except Exception as e:
@@ -225,4 +270,84 @@ def delete_entry(request, model_name):
except Exception as e:
return JsonResponse({'status': 'error', 'message': str(e)}, status=400)
- return JsonResponse({'status': 'error', 'message': 'Invalid request'}, status=400)
\ No newline at end of file
+ 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'})
\ No newline at end of file