update
This commit is contained in:
BIN
db.sqlite3
BIN
db.sqlite3
Binary file not shown.
@@ -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'})
|
||||
)
|
25
sheets/migrations/0007_betriebskosten.py
Normal file
25
sheets/migrations/0007_betriebskosten.py
Normal 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')),
|
||||
],
|
||||
),
|
||||
]
|
@@ -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)
|
||||
|
@@ -3,6 +3,9 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<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" />
|
||||
<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>
|
||||
|
479
sheets/templates/betriebskosten_list.html
Normal file
479
sheets/templates/betriebskosten_list.html
Normal 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">
|
||||
⇦ 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">×</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">×</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>
|
@@ -2,6 +2,9 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<!-- Title -->
|
||||
<h1 class="page-title">Helium Output Yearly Summary</h1>
|
||||
|
||||
<!-- Year Filter -->
|
||||
<div class="year-filter">
|
||||
<label for="year-select">Year:</label>
|
||||
@@ -40,12 +43,10 @@
|
||||
|
||||
<!-- Navigation Buttons -->
|
||||
<div class="navigation-buttons">
|
||||
<a href="{% url 'table_one' %}" class="nav-button">
|
||||
Go to Helium Input
|
||||
</a>
|
||||
<a href="{% url 'table_two' %}" class="nav-button">
|
||||
Go to Helium Output
|
||||
</a>
|
||||
<a href="{% url 'table_one' %}" class="nav-button">Go to Helium Input</a>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
@@ -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 = `
|
||||
<tr data-id="${response.id}">
|
||||
<td>${$('#table-two tbody tr').length + 1}</td>
|
||||
<td>${response.id}</td>
|
||||
<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) {
|
||||
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'));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@@ -8,4 +8,7 @@ urlpatterns = [
|
||||
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'),
|
||||
]
|
||||
|
199
sheets/views.py
199
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)
|
||||
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'})
|
Reference in New Issue
Block a user