This commit is contained in:
2025-10-08 10:13:07 +02:00
parent 70e055d20b
commit b74cf45c5c
10 changed files with 813 additions and 91 deletions

Binary file not shown.

View File

@@ -1,7 +1,27 @@
from django import forms from django import forms
from .models import ExcelEntry from .models import ExcelEntry, Betriebskosten
class ExcelEntryForm(forms.ModelForm): class ExcelEntryForm(forms.ModelForm):
class Meta: class Meta:
model = ExcelEntry 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'})
)

View File

@@ -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')),
],
),
]

View File

@@ -3,6 +3,29 @@ from django.utils import timezone
from django.core.validators import MinValueValidator, MaxValueValidator 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): class Client(models.Model):
name = models.CharField(max_length=100) name = models.CharField(max_length=100)

View File

@@ -3,6 +3,9 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>{% block title %}My App{% endblock %}</title> <title>{% block title %}My App{% endblock %}</title>
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jquery-modal/0.9.1/jquery.modal.min.css" /> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jquery-modal/0.9.1/jquery.modal.min.css" />
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-modal/0.9.1/jquery.modal.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-modal/0.9.1/jquery.modal.min.js"></script>

View File

@@ -0,0 +1,479 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Betriebskosten</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-modal/0.9.1/jquery.modal.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jquery-modal/0.9.1/jquery.modal.min.css" />
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
background-color: #f4f4f9;
}
.table-container {
width: 100%;
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
h2 {
text-align: center;
color: #333;
}
table {
width: 100%;
table-layout: fixed;
border-collapse: collapse;
}
th, td {
padding: 12px;
text-align: center;
border-bottom: 1px solid #ddd;
}
th:nth-child(1), td:nth-child(1) { width: 5%; } /* # column */
th:nth-child(2), td:nth-child(2) { width: 10%; } /* Buchungsdatum */
th:nth-child(3), td:nth-child(3) { width: 15%; } /* Rechnungsnummer */
th:nth-child(4), td:nth-child(4) { width: 10%; } /* Kostentyp */
th:nth-child(5), td:nth-child(5) { width: 10%; } /* Gasvolumen */
th:nth-child(6), td:nth-child(6) { width: 10%; } /* Betrag */
th:nth-child(7), td:nth-child(7) { width: 25%; } /* Beschreibung */
th:nth-child(8), td:nth-child(8) { width: 15%; } /* Actions */
.actions {
white-space: nowrap; /* Prevent buttons from wrapping */
}
th {
background-color: #007bff;
color: white;
font-weight: bold;
position: sticky;
top: 0;
}
tr:hover {
background-color: #f1f1f1;
}
.actions button {
margin: 2px;
padding: 5px 10px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.edit-btn {
background-color: #28a745;
color: white;
}
.delete-btn {
background-color: #dc3545;
color: white;
}
.popup {
display: none;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 20px;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
z-index: 1000;
width: 400px;
}
.popup input, .popup select, .popup textarea {
display: block;
margin-bottom: 10px;
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
.popup button {
margin-top: 10px;
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.close-btn {
cursor: pointer;
float: right;
font-size: 18px;
color: #333;
}
.add-row-btn {
padding: 10px 20px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
margin-bottom: 20px;
}
.add-row-btn:hover {
background-color: #0056b3;
}
.save-btn {
background-color: #28a745;
color: white;
}
.cancel-btn {
background-color: #6c757d;
color: white;
}
.help-btn {
background-color: #17a2b8;
color: white;
}
#price-per-liter-group {
display: none;
}
</style>
</head>
<body>
<a href="{% url 'clients_list' %}" class="btn btn-outline-primary">
&#8678; Go to Clients
</a>
<h2>Betriebskosten</h2>
<div class="table-container">
<button class="add-row-btn" id="add-row-btn">Add Row</button>
<table id="betriebskosten-table">
<thead>
<tr>
<th>#</th>
<th>Buchungsdatum</th>
<th>Rechnungsnummer</th>
<th>Kostentyp</th>
<th>Gasvolumen (L)</th>
<th>Betrag (€)</th>
<th>Beschreibung</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for item in items %}
<tr data-id="{{ item.id }}">
<td>{{ forloop.counter }}</td>
<td>{{ item.buchungsdatum|date:"Y-m-d" }}</td>
<td>{{ item.rechnungsnummer }}</td>
<td>{{ item.get_kostentyp_display }}</td>
<td>{{ item.gas_volume|default_if_none:"-" }}</td>
<td>{{ item.betrag }}</td>
<td>{{ item.beschreibung|default:"" }}</td>
<td class="actions">
<button class="edit-btn">Edit</button>
<button class="delete-btn">Delete</button>
</td>
</tr>
{% empty %}
<tr>
<td colspan="8" style="text-align: center;">No entries found</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Add Popup -->
<div id="add-popup" class="popup">
<span class="close-btn">&times;</span>
<h3>Betriebskosten Eintrag</h3>
<label for="add-buchungsdatum">Buchungsdatum:</label>
<input type="date" id="add-buchungsdatum" required>
<label for="add-rechnungsnummer">Rechnungsnummer:</label>
<input type="text" id="add-rechnungsnummer" required>
<label for="add-kostentyp">Kostentyp:</label>
<select id="add-kostentyp" required onchange="toggleGasVolume('add')">
<option value="">Bitte auswählen</option>
<option value="sach">Sachkosten</option>
<option value="ln2">LN2</option>
<option value="helium">Helium</option>
<option value="inv">Inventar</option>
</select>
<div id="add-gas-volume-group" style="display: none;">
<label for="add-gas-volume">Gasvolumen (Liter):</label>
<input type="number" id="add-gas-volume" step="0.01" min="0" oninput="calculatePrice('add')">
</div>
<label for="add-betrag">Betrag (€):</label>
<input type="number" id="add-betrag" step="0.01" min="0" required oninput="calculatePrice('add')">
<div id="add-price-per-liter-group" style="display: none;">
<label for="add-price-per-liter">Preis pro Liter (€):</label>
<input type="text" id="add-price-per-liter" readonly>
</div>
<label for="add-beschreibung">Beschreibung:</label>
<textarea id="add-beschreibung" placeholder="Optionale Beschreibung"></textarea>
<div class="popup-buttons">
<button class="save-btn" id="save-add">Save</button>
<button class="cancel-btn">Cancel</button>
<button class="help-btn">Help</button>
</div>
</div>
<!-- Edit Popup -->
<div id="edit-popup" class="popup">
<span class="close-btn">&times;</span>
<h3>Betriebskosten Eintrag</h3>
<input type="hidden" id="edit-id">
<label for="edit-buchungsdatum">Buchungsdatum:</label>
<input type="date" id="edit-buchungsdatum" required>
<label for="edit-rechnungsnummer">Rechnungsnummer:</label>
<input type="text" id="edit-rechnungsnummer" required>
<label for="edit-kostentyp">Kostentyp:</label>
<select id="edit-kostentyp" required onchange="toggleGasVolume('edit')">
<option value="">Bitte auswählen</option>
<option value="sach">Sachkosten</option>
<option value="ln2">LN2</option>
<option value="helium">Helium</option>
<option value="inv">Inventar</option>
</select>
<div id="edit-gas-volume-group" style="display: none;">
<label for="edit-gas-volume">Gasvolumen (Liter):</label>
<input type="number" id="edit-gas-volume" step="0.01" min="0" oninput="calculatePrice('edit')">
</div>
<label for="edit-betrag">Betrag (€):</label>
<input type="number" id="edit-betrag" step="0.01" min="0" required oninput="calculatePrice('edit')">
<div id="edit-price-per-liter-group" style="display: none;">
<label for="edit-price-per-liter">Preis pro Liter (€):</label>
<input type="text" id="edit-price-per-liter" readonly>
</div>
<label for="edit-beschreibung">Beschreibung:</label>
<textarea id="edit-beschreibung" placeholder="Optionale Beschreibung"></textarea>
<div class="popup-buttons">
<button class="save-btn" id="save-edit">Save</button>
<button class="cancel-btn">Cancel</button>
<button class="help-btn">Help</button>
</div>
</div>
<script>
$(document).ready(function () {
// Open add popup
$('#add-row-btn').on('click', function () {
$('#add-popup').fadeIn();
});
// Close popups
$('.close-btn, .cancel-btn').on('click', function () {
$('.popup').fadeOut();
});
// Add new entry
$('#save-add').on('click', function() {
const formData = {
'buchungsdatum': $('#add-buchungsdatum').val(),
'rechnungsnummer': $('#add-rechnungsnummer').val(),
'kostentyp': $('#add-kostentyp').val(),
'gas_volume': $('#add-gas-volume').val(),
'betrag': $('#add-betrag').val(),
'beschreibung': $('#add-beschreibung').val(),
'csrfmiddlewaretoken': '{{ csrf_token }}'
};
// Validate required fields
if (!formData.buchungsdatum || !formData.rechnungsnummer || !formData.kostentyp || !formData.betrag) {
alert('Bitte füllen Sie alle erforderlichen Felder aus');
return;
}
$.ajax({
url: "{% url 'betriebskosten_create' %}",
method: 'POST',
data: formData,
success: function(response) {
if (response.status === 'success') {
// Add the new row to the table
const newRow = `
<tr data-id="${response.id}">
<td>${$('#betriebskosten-table tbody tr').length + 1}</td>
<td>${response.buchungsdatum}</td>
<td>${response.rechnungsnummer}</td>
<td>${response.kostentyp_display}</td>
<td>${response.gas_volume || '-'}</td>
<td>${response.betrag}</td>
<td>${response.beschreibung || ''}</td>
<td class="actions">
<button class="edit-btn">Edit</button>
<button class="delete-btn">Delete</button>
</td>
</tr>
`;
$('#betriebskosten-table tbody').append(newRow);
$('#add-popup').fadeOut();
$('#add-popup input, #add-popup select, #add-popup textarea').val('');
} else {
alert('Error: ' + (response.message || 'Failed to add entry'));
}
},
error: function(xhr) {
alert('Error: ' + (xhr.responseJSON?.message || 'Server error'));
}
});
});
// Edit entry
$(document).on('click', '.edit-btn', function() {
const row = $(this).closest('tr');
const id = row.data('id');
// Fill the edit form with current data
$('#edit-id').val(id);
$('#edit-buchungsdatum').val(row.find('td:eq(1)').text());
$('#edit-rechnungsnummer').val(row.find('td:eq(2)').text());
// Set kostentyp based on display text
const kostentypText = row.find('td:eq(3)').text();
const kostentypMap = {
'Sachkosten': 'sach',
'LN2': 'ln2',
'Helium': 'helium',
'Inventar': 'inv'
};
$('#edit-kostentyp').val(kostentypMap[kostentypText] || '');
$('#edit-gas-volume').val(row.find('td:eq(4)').text() === '-' ? '' : row.find('td:eq(4)').text());
$('#edit-betrag').val(row.find('td:eq(5)').text());
$('#edit-beschreibung').val(row.find('td:eq(6)').text());
// Show/hide gas volume based on kostentyp
toggleGasVolume('edit');
calculatePrice('edit');
$('#edit-popup').fadeIn();
});
// Save edit
$('#save-edit').on('click', function() {
const formData = {
'id': $('#edit-id').val(),
'buchungsdatum': $('#edit-buchungsdatum').val(),
'rechnungsnummer': $('#edit-rechnungsnummer').val(),
'kostentyp': $('#edit-kostentyp').val(),
'gas_volume': $('#edit-gas-volume').val(),
'betrag': $('#edit-betrag').val(),
'beschreibung': $('#edit-beschreibung').val(),
'csrfmiddlewaretoken': '{{ csrf_token }}'
};
// Validate required fields
if (!formData.buchungsdatum || !formData.rechnungsnummer || !formData.kostentyp || !formData.betrag) {
alert('Bitte füllen Sie alle erforderlichen Felder aus');
return;
}
$.ajax({
url: "{% url 'betriebskosten_create' %}",
method: 'POST',
data: formData,
success: function(response) {
if (response.status === 'success') {
// Update the row in the table
const row = $(`tr[data-id="${response.id}"]`);
row.find('td:eq(1)').text(response.buchungsdatum);
row.find('td:eq(2)').text(response.rechnungsnummer);
row.find('td:eq(3)').text(response.kostentyp_display);
row.find('td:eq(4)').text(response.gas_volume || '-');
row.find('td:eq(5)').text(response.betrag);
row.find('td:eq(6)').text(response.beschreibung || '');
$('#edit-popup').fadeOut();
} else {
alert('Error: ' + (response.message || 'Failed to update entry'));
}
},
error: function(xhr) {
alert('Error: ' + (xhr.responseJSON?.message || 'Server error'));
}
});
});
// Delete entry
$(document).on('click', '.delete-btn', function () {
const row = $(this).closest('tr');
const id = row.data('id');
if (!confirm('Are you sure you want to delete this entry?')) return;
$.ajax({
url: "{% url 'betriebskosten_delete' %}",
method: 'POST',
data: {
'id': id,
'csrfmiddlewaretoken': '{{ csrf_token }}'
},
success: function (response) {
if (response.status === 'success') {
row.fadeOut(300, function () { $(this).remove(); });
// Update row numbers
$('#betriebskosten-table tbody tr').each(function(index) {
$(this).find('td:first').text(index + 1);
});
} else {
alert('Failed to delete entry: ' + response.message);
}
},
error: function () {
alert('Failed to delete entry. Please try again.');
}
});
});
});
// Toggle gas volume field based on kostentyp
function toggleGasVolume(type) {
const kostentyp = $(`#${type}-kostentyp`).val();
const gasVolumeGroup = $(`#${type}-gas-volume-group`);
const pricePerLiterGroup = $(`#${type}-price-per-liter-group`);
if (kostentyp === 'helium') {
gasVolumeGroup.show();
pricePerLiterGroup.show();
} else {
gasVolumeGroup.hide();
pricePerLiterGroup.hide();
$(`#${type}-gas-volume`).val('');
$(`#${type}-price-per-liter`).val('');
}
}
// Calculate price per liter
function calculatePrice(type) {
const kostentyp = $(`#${type}-kostentyp`).val();
const volume = parseFloat($(`#${type}-gas-volume`).val()) || 0;
const betrag = parseFloat($(`#${type}-betrag`).val()) || 0;
if (kostentyp === 'helium' && volume > 0 && betrag > 0) {
$(`#${type}-price-per-liter`).val((betrag / volume).toFixed(2) + ' €/L');
} else {
$(`#${type}-price-per-liter`).val('');
}
}
</script>
</body>
</html>

View File

@@ -2,6 +2,9 @@
{% block content %} {% block content %}
<div class="container"> <div class="container">
<!-- Title -->
<h1 class="page-title">Helium Output Yearly Summary</h1>
<!-- Year Filter --> <!-- Year Filter -->
<div class="year-filter"> <div class="year-filter">
<label for="year-select">Year:</label> <label for="year-select">Year:</label>
@@ -40,12 +43,10 @@
<!-- Navigation Buttons --> <!-- Navigation Buttons -->
<div class="navigation-buttons"> <div class="navigation-buttons">
<a href="{% url 'table_one' %}" class="nav-button"> <a href="{% url 'table_one' %}" class="nav-button">Go to Helium Input</a>
Go to Helium Input <a href="{% url 'table_two' %}" class="nav-button">Go to Helium Output</a>
</a> <a href="/admin/" class="nav-button admin-button">Go to Admin Panel</a>
<a href="{% url 'table_two' %}" class="nav-button"> <a href="{% url 'betriebskosten_list' %}" class="nav-button">Betriebskosten</a>
Go to Helium Output
</a>
</div> </div>
</div> </div>
@@ -56,6 +57,12 @@
padding: 20px; padding: 20px;
} }
.page-title {
text-align: center;
color: #333;
margin-bottom: 20px;
}
.year-filter { .year-filter {
margin: 20px 0; margin: 20px 0;
text-align: right; text-align: right;
@@ -120,5 +127,13 @@
.nav-button:hover { .nav-button:hover {
background-color: #0056b3; background-color: #0056b3;
} }
.admin-button {
background-color: #6c757d;
}
.admin-button:hover {
background-color: #5a6268;
}
</style> </style>
{% endblock %} {% endblock %}

View File

@@ -316,7 +316,7 @@
$('#save-add-two').on('click', function() { $('#save-add-two').on('click', function() {
let formData = { let formData = {
'client_id': $('#add-client-id').val(), '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'), 'is_warm': $('#add-is-warm').is(':checked'),
'lhe_delivery': $('#add-lhe-delivery').val(), 'lhe_delivery': $('#add-lhe-delivery').val(),
'lhe_output': $('#add-lhe-output').val(), 'lhe_output': $('#add-lhe-output').val(),
@@ -324,6 +324,12 @@
'csrfmiddlewaretoken': '{{ csrf_token }}' 'csrfmiddlewaretoken': '{{ csrf_token }}'
}; };
// Validate date format
if (!formData.date) {
alert('Please select a date');
return;
}
// Validate LHe Output is a number // Validate LHe Output is a number
if (isNaN(parseFloat(formData.lhe_output))) { if (isNaN(parseFloat(formData.lhe_output))) {
alert('LHe Output must be a valid number'); alert('LHe Output must be a valid number');
@@ -335,16 +341,32 @@
method: 'POST', method: 'POST',
data: formData, data: formData,
success: function(response) { success: function(response) {
// Clear the form if (response.status === 'success') {
$('#add-popup-two').find('input, textarea, select').val(''); // Add the new row to the table
$('#add-is-warm').prop('checked', false); let newRow = `
$('#add-popup-two').fadeOut(); <tr data-id="${response.id}">
<td>${$('#table-two tbody tr').length + 1}</td>
// Reload the table data <td>${response.id}</td>
loadTableData(); <td>${response.client_name}</td>
<td>${response.date || ''}</td>
<td>${response.is_warm ? 'Yes' : 'No'}</td>
<td>${response.lhe_delivery}</td>
<td>${response.lhe_output || ''}</td>
<td>${response.notes || ''}</td>
<td class="actions">
<button class="edit-btn-two">Edit</button>
<button class="delete-btn-two">Delete</button>
</td>
</tr>
`;
$('#table-two tbody').append(newRow);
$('#add-popup-two').fadeOut();
} else {
alert('Error: ' + (response.message || 'Failed to add entry'));
}
}, },
error: function(xhr) { 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 // Open Edit Popup
$(document).on('click', '.edit-btn-two', function () { $(document).on('click', '.edit-btn-two', function() {
let row = $(this).closest('tr'); 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-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()); // Set client - find by name since we're showing names in the table
$('#edit-is-warm').prop('checked', is_warm); 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-delivery').val(row.find('td:eq(5)').text().trim());
$('#edit-lhe-output').val(row.find('td:eq(6)').text().trim()); $('#edit-lhe-output').val(row.find('td:eq(6)').text().trim());
$('#edit-notes').val(row.find('td:eq(7)').text().trim()); $('#edit-notes').val(row.find('td:eq(7)').text().trim());
@@ -386,45 +412,48 @@
}); });
// Save Edit Entry // Save Edit Entry
$('#save-edit-two').on('click', function () { $('#save-edit-two').on('click', function() {
let id = $('#edit-id').val(); let formData = {
let client_id = $('#edit-client-id').val(); 'id': $('#edit-id').val(),
let date = $('#edit-date').val(); 'client_id': $('#edit-client-id').val(),
let is_warm = $('#edit-is-warm').is(':checked'); 'date': $('#edit-date').val(), // Already in YYYY-MM-DD format
let lhe_delivery = $('#edit-lhe-delivery').val(); 'is_warm': $('#edit-is-warm').is(':checked'),
let lhe_output = $('#edit-lhe-output').val(); 'lhe_delivery': $('#edit-lhe-delivery').val(),
let notes = $('#edit-notes').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({ $.ajax({
url: `/update-entry/${currentModelName}/`, url: '/update-entry/SecondTableEntry/',
method: 'POST', method: 'POST',
data: { data: formData,
'id': id, success: function(response) {
'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) {
if (response.status === 'success') { if (response.status === 'success') {
let row = $(`tr[data-id="${response.id}"]`); let row = $(`tr[data-id="${response.id}"]`);
row.find('td:eq(2)').text(response.client_name); 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(4)').text(response.is_warm ? 'Yes' : 'No');
row.find('td:eq(5)').text(response.lhe_delivery); row.find('td:eq(5)').text(response.lhe_delivery);
row.find('td:eq(6)').text(response.lhe_output); row.find('td:eq(6)').text(response.lhe_output || '');
row.find('td:eq(7)').text(response.notes); row.find('td:eq(7)').text(response.notes || '');
$('#edit-popup-two').fadeOut(); $('#edit-popup-two').fadeOut();
} else { } else {
alert('Update failed: ' + (response.message || 'Please try again later')); alert('Update failed: ' + (response.message || 'Unknown error'));
} }
}, },
error: function (xhr, status, error) { error: function(xhr) {
alert('Update failed: ' + (xhr.responseJSON?.message || 'Please try again later')); alert('Error: ' + (xhr.responseJSON?.message || 'Server error'));
console.error('Error:', error);
} }
}); });
}); });

View File

@@ -8,4 +8,7 @@ urlpatterns = [
path('add-entry/<str:model_name>/', views.add_entry, name='add_entry'), 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('update-entry/<str:model_name>/', views.update_entry, name='update_entry'),
path('delete-entry/<str:model_name>/', views.delete_entry, name='delete_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'),
] ]

View File

@@ -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.http import JsonResponse
from django.db.models import Q from django.db.models import Q
from decimal import Decimal, InvalidOperation from decimal import Decimal, InvalidOperation
@@ -7,23 +8,47 @@ from datetime import date, datetime
from django.utils import timezone from django.utils import timezone
from .models import Client, SecondTableEntry from .models import Client, SecondTableEntry
from django.db.models import Sum 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) # 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): def clients_list(request):
# Annual summary data # Get all clients
current_year = int(request.GET.get('year', datetime.now().year))
clients = Client.objects.all() 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 = [] monthly_data = []
for client in clients: for client in clients:
monthly_totals = [] monthly_totals = []
for month in range(1, 13): for month in range(1, 13):
total = SecondTableEntry.objects.filter( total = SecondTableEntry.objects.filter(
client=client, client=client,
date__year=current_year, date__year=selected_year,
date__month=month 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_totals.append(total)
monthly_data.append({ monthly_data.append({
@@ -32,16 +57,13 @@ def clients_list(request):
'year_total': sum(monthly_totals) 'year_total': sum(monthly_totals)
}) })
available_years = SecondTableEntry.objects.dates('date', 'year').distinct()
return render(request, 'clients_table.html', { return render(request, 'clients_table.html', {
'monthly_data': monthly_data, 'monthly_data': monthly_data,
'current_year': current_year, 'current_year': selected_year,
'available_years': [y.year for y in available_years], 'available_years': available_years,
'months': ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'months': ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
}) })
# Table One View (ExcelEntry) # Table One View (ExcelEntry)
def table_one_view(request): def table_one_view(request):
ExcelEntry = apps.get_model('sheets', 'ExcelEntry') ExcelEntry = apps.get_model('sheets', 'ExcelEntry')
@@ -80,20 +102,26 @@ def add_entry(request, model_name):
try: try:
model = apps.get_model('sheets', model_name) 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': 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) # Handle Helium Output (Table Two)
lhe_output = request.POST.get('lhe_output') lhe_output = request.POST.get('lhe_output')
entry = model.objects.create( 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', is_warm=request.POST.get('is_warm') == 'true',
lhe_delivery=request.POST.get('lhe_delivery', ''), 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({ return JsonResponse({
@@ -158,15 +186,33 @@ def update_entry(request, model_name):
# Common updates for both models # Common updates for both models
entry.client = Client.objects.get(id=request.POST.get('client_id')) entry.client = Client.objects.get(id=request.POST.get('client_id'))
entry.date = request.POST.get('date')
entry.notes = request.POST.get('notes', '') 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': if model_name == 'SecondTableEntry':
# Handle Helium Output (Table Two) # Handle Helium Output specific fields
lhe_output = request.POST.get('lhe_output')
entry.is_warm = request.POST.get('is_warm') == 'true' entry.is_warm = request.POST.get('is_warm') == 'true'
entry.lhe_delivery = request.POST.get('lhe_delivery', '') 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() entry.save()
return JsonResponse({ return JsonResponse({
@@ -181,29 +227,28 @@ def update_entry(request, model_name):
}) })
elif model_name == 'ExcelEntry': elif model_name == 'ExcelEntry':
# Handle Helium Input (Table One) # Handle Helium Input specific fields
date_str = request.POST.get('date')
try: try:
date_obj = datetime.strptime(date_str, '%Y-%m-%d').date() if date_str else None entry.pressure = Decimal(request.POST.get('pressure', 0))
except (ValueError, TypeError): entry.purity = Decimal(request.POST.get('purity', 0))
date_obj = None 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() entry.save()
return JsonResponse({ return JsonResponse({
'status': 'success', 'status': 'success',
'id': entry.id, 'id': entry.id,
'client_name': entry.client.name, 'client_name': entry.client.name,
'date': entry.date.strftime('%Y-%m-%d') if entry.date else '',
'pressure': str(entry.pressure), 'pressure': str(entry.pressure),
'purity': str(entry.purity), 'purity': str(entry.purity),
'date': entry.date.strftime('%Y-%m-%d') if entry.date else '',
'notes': entry.notes 'notes': entry.notes
}) })
except model.DoesNotExist: except model.DoesNotExist:
return JsonResponse({'status': 'error', 'message': 'Entry not found'}, status=404) return JsonResponse({'status': 'error', 'message': 'Entry not found'}, status=404)
except Exception as e: except Exception as e:
@@ -226,3 +271,83 @@ def delete_entry(request, model_name):
return JsonResponse({'status': 'error', 'message': str(e)}, status=400) return JsonResponse({'status': 'error', 'message': str(e)}, status=400)
return JsonResponse({'status': 'error', 'message': 'Invalid request'}, status=400) return JsonResponse({'status': 'error', 'message': 'Invalid request'}, status=400)
def betriebskosten_list(request):
items = Betriebskosten.objects.all().order_by('-buchungsdatum')
return render(request, 'betriebskosten_list.html', {'items': items})
def betriebskosten_create(request):
if request.method == 'POST':
try:
entry_id = request.POST.get('id')
if entry_id:
# Update existing entry
entry = Betriebskosten.objects.get(id=entry_id)
else:
# Create new entry
entry = Betriebskosten()
# Get form data
buchungsdatum_str = request.POST.get('buchungsdatum')
rechnungsnummer = request.POST.get('rechnungsnummer')
kostentyp = request.POST.get('kostentyp')
betrag = request.POST.get('betrag')
beschreibung = request.POST.get('beschreibung')
gas_volume = request.POST.get('gas_volume')
# Validate required fields
if not all([buchungsdatum_str, rechnungsnummer, kostentyp, betrag]):
return JsonResponse({'status': 'error', 'message': 'All required fields must be filled'})
# Convert date string to date object
try:
buchungsdatum = parse_date(buchungsdatum_str)
if not buchungsdatum:
return JsonResponse({'status': 'error', 'message': 'Invalid date format'})
except (ValueError, TypeError):
return JsonResponse({'status': 'error', 'message': 'Invalid date format'})
# Set entry values
entry.buchungsdatum = buchungsdatum
entry.rechnungsnummer = rechnungsnummer
entry.kostentyp = kostentyp
entry.betrag = betrag
entry.beschreibung = beschreibung
# Only set gas_volume if kostentyp is helium and gas_volume is provided
if kostentyp == 'helium' and gas_volume:
entry.gas_volume = gas_volume
else:
entry.gas_volume = None
entry.save()
return JsonResponse({
'status': 'success',
'id': entry.id,
'buchungsdatum': entry.buchungsdatum.strftime('%Y-%m-%d'),
'rechnungsnummer': entry.rechnungsnummer,
'kostentyp_display': entry.get_kostentyp_display(),
'gas_volume': str(entry.gas_volume) if entry.gas_volume else '-',
'betrag': str(entry.betrag),
'beschreibung': entry.beschreibung or ''
})
except Exception as e:
return JsonResponse({'status': 'error', 'message': str(e)})
return JsonResponse({'status': 'error', 'message': 'Invalid request method'})
def betriebskosten_delete(request):
if request.method == 'POST':
try:
entry_id = request.POST.get('id')
entry = Betriebskosten.objects.get(id=entry_id)
entry.delete()
return JsonResponse({'status': 'success'})
except Betriebskosten.DoesNotExist:
return JsonResponse({'status': 'error', 'message': 'Entry not found'})
except Exception as e:
return JsonResponse({'status': 'error', 'message': str(e)})
return JsonResponse({'status': 'error', 'message': 'Invalid request method'})