v1.0
This commit is contained in:
@@ -3,7 +3,10 @@ __pycache__/
|
|||||||
*.pyo
|
*.pyo
|
||||||
.env
|
.env
|
||||||
db.sqlite3
|
db.sqlite3
|
||||||
|
*.sqlite3
|
||||||
media/
|
media/
|
||||||
staticfiles/
|
staticfiles/
|
||||||
venv/
|
venv/
|
||||||
.venv/
|
.venv/
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 6.0.6 on 2026-06-27 17:19
|
# Generated by Django 6.0.6 on 2026-06-28 10:19
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@@ -46,12 +46,15 @@ class Migration(migrations.Migration):
|
|||||||
('projects_inventory_shopping_add', models.BooleanField(default=False, help_text='Benutzer darf Einkaufspositionen erstellen.', verbose_name='Einkauf hinzufügen')),
|
('projects_inventory_shopping_add', models.BooleanField(default=False, help_text='Benutzer darf Einkaufspositionen erstellen.', verbose_name='Einkauf hinzufügen')),
|
||||||
('projects_inventory_shopping_edit', models.BooleanField(default=False, help_text='Benutzer darf Einkaufspositionen ändern.', verbose_name='Einkauf bearbeiten')),
|
('projects_inventory_shopping_edit', models.BooleanField(default=False, help_text='Benutzer darf Einkaufspositionen ändern.', verbose_name='Einkauf bearbeiten')),
|
||||||
('projects_inventory_shopping_delete', models.BooleanField(default=False, help_text='Benutzer darf Einkaufspositionen löschen.', verbose_name='Einkauf löschen')),
|
('projects_inventory_shopping_delete', models.BooleanField(default=False, help_text='Benutzer darf Einkaufspositionen löschen.', verbose_name='Einkauf löschen')),
|
||||||
|
('projects_inventory_shopping_to_expense', models.BooleanField(default=False, help_text='Benutzer darf Einkäufe als erledigt markieren.', verbose_name='Einkäufe durchführen')),
|
||||||
('inventory_view', models.BooleanField(default=False, help_text='Benutzer darf Inventar einsehen.', verbose_name='Inventar anzeigen')),
|
('inventory_view', models.BooleanField(default=False, help_text='Benutzer darf Inventar einsehen.', verbose_name='Inventar anzeigen')),
|
||||||
('inventory_add', models.BooleanField(default=False, help_text='Benutzer darf Inventareinträge erstellen.', verbose_name='Inventar hinzufügen')),
|
('inventory_add', models.BooleanField(default=False, help_text='Benutzer darf Inventareinträge erstellen.', verbose_name='Inventar hinzufügen')),
|
||||||
('inventory_edit', models.BooleanField(default=False, help_text='Benutzer darf Inventareinträge ändern.', verbose_name='Inventar bearbeiten')),
|
('inventory_edit', models.BooleanField(default=False, help_text='Benutzer darf Inventareinträge ändern.', verbose_name='Inventar bearbeiten')),
|
||||||
('inventory_delete', models.BooleanField(default=False, help_text='Benutzer darf Inventareinträge löschen.', verbose_name='Inventar löschen')),
|
('inventory_delete', models.BooleanField(default=False, help_text='Benutzer darf Inventareinträge löschen.', verbose_name='Inventar löschen')),
|
||||||
('invoices_view', models.BooleanField(default=False, help_text='Benutzer darf Rechnungen verwalten.', verbose_name='Rechnungsbereich anzeigen')),
|
('invoices_view', models.BooleanField(default=False, help_text='Benutzer darf Rechnungen verwalten.', verbose_name='Rechnungsbereich anzeigen')),
|
||||||
|
('invoices_info', models.BooleanField(default=False, help_text='Benutzer darf Statistiken sehen.', verbose_name='Statistiken ansehen')),
|
||||||
('invoices_customers_view', models.BooleanField(default=False, help_text='Benutzer darf Kunden sehen.', verbose_name='Kunden anzeigen')),
|
('invoices_customers_view', models.BooleanField(default=False, help_text='Benutzer darf Kunden sehen.', verbose_name='Kunden anzeigen')),
|
||||||
|
('invoices_customers_info', models.BooleanField(default=False, help_text='Benutzer darf Kundenstatistiken sehen.', verbose_name='Kundenstatistiken ansehen')),
|
||||||
('invoices_customers_detail_view', models.BooleanField(default=False, help_text='Benutzer darf Details des Kunden sehen.', verbose_name='Kundendetails anzeigen')),
|
('invoices_customers_detail_view', models.BooleanField(default=False, help_text='Benutzer darf Details des Kunden sehen.', verbose_name='Kundendetails anzeigen')),
|
||||||
('invoices_customers_add', models.BooleanField(default=False, help_text='Benutzer darf Kunden anlegen.', verbose_name='Kunden erstellen')),
|
('invoices_customers_add', models.BooleanField(default=False, help_text='Benutzer darf Kunden anlegen.', verbose_name='Kunden erstellen')),
|
||||||
('invoices_customers_edit', models.BooleanField(default=False, help_text='Benutzer darf Kunden ändern.', verbose_name='Kunden bearbeiten')),
|
('invoices_customers_edit', models.BooleanField(default=False, help_text='Benutzer darf Kunden ändern.', verbose_name='Kunden bearbeiten')),
|
||||||
@@ -62,6 +65,7 @@ class Migration(migrations.Migration):
|
|||||||
('invoices_items_edit', models.BooleanField(default=False, help_text='Benutzer darf Artikel ändern.', verbose_name='Artikel bearbeiten')),
|
('invoices_items_edit', models.BooleanField(default=False, help_text='Benutzer darf Artikel ändern.', verbose_name='Artikel bearbeiten')),
|
||||||
('invoices_items_delete', models.BooleanField(default=False, help_text='Benutzer darf Artikel löschen.', verbose_name='Artikel löschen')),
|
('invoices_items_delete', models.BooleanField(default=False, help_text='Benutzer darf Artikel löschen.', verbose_name='Artikel löschen')),
|
||||||
('invoices_invoices_view', models.BooleanField(default=False, help_text='Benutzer darf Rechnungen sehen.', verbose_name='Rechnungen anzeigen')),
|
('invoices_invoices_view', models.BooleanField(default=False, help_text='Benutzer darf Rechnungen sehen.', verbose_name='Rechnungen anzeigen')),
|
||||||
|
('invoices_invoices_info', models.BooleanField(default=False, help_text='Benutzer darf Rechnungsstatistiken sehen.', verbose_name='Rechnungsstatistiken ansehen')),
|
||||||
('invoices_invoices_project_add', models.BooleanField(default=False, help_text='Benutzer darf Rechnungen aus Projekten erstellen.', verbose_name='Projekt-Rechnung erstellen')),
|
('invoices_invoices_project_add', models.BooleanField(default=False, help_text='Benutzer darf Rechnungen aus Projekten erstellen.', verbose_name='Projekt-Rechnung erstellen')),
|
||||||
('invoices_invoices_customer_add', models.BooleanField(default=False, help_text='Benutzer darf Rechnungen für Kunden erstellen.', verbose_name='Kunden-Rechnung erstellen')),
|
('invoices_invoices_customer_add', models.BooleanField(default=False, help_text='Benutzer darf Rechnungen für Kunden erstellen.', verbose_name='Kunden-Rechnung erstellen')),
|
||||||
('invoices_invoices_invoices_view', models.BooleanField(default=False, help_text='Benutzer darf Rechnungsdetails sehen.', verbose_name='Rechnungsdetails anzeigen')),
|
('invoices_invoices_invoices_view', models.BooleanField(default=False, help_text='Benutzer darf Rechnungsdetails sehen.', verbose_name='Rechnungsdetails anzeigen')),
|
||||||
@@ -72,24 +76,23 @@ class Migration(migrations.Migration):
|
|||||||
('invoices_invoices_invoices_cancel', models.BooleanField(default=False, help_text='Benutzer darf Rechnungen stornieren.', verbose_name='Rechnung stornieren')),
|
('invoices_invoices_invoices_cancel', models.BooleanField(default=False, help_text='Benutzer darf Rechnungen stornieren.', verbose_name='Rechnung stornieren')),
|
||||||
('invoices_invoices_invoices_paid', models.BooleanField(default=False, help_text='Benutzer darf Rechnungsstatus ändern.', verbose_name='Rechnung auf bezahlt setzen')),
|
('invoices_invoices_invoices_paid', models.BooleanField(default=False, help_text='Benutzer darf Rechnungsstatus ändern.', verbose_name='Rechnung auf bezahlt setzen')),
|
||||||
('invoices_invoices_invoices_delete', models.BooleanField(default=False, help_text='Benutzer darf Rechnungen löschen.', verbose_name='Rechnungen löschen')),
|
('invoices_invoices_invoices_delete', models.BooleanField(default=False, help_text='Benutzer darf Rechnungen löschen.', verbose_name='Rechnungen löschen')),
|
||||||
('invoices_invoices_invoices_overdue', models.BooleanField(default=False, help_text='Benutzer darf überfällige Rechnungen einsehen.', verbose_name='Überfällige Rechnungen anzeigen')),
|
|
||||||
('invoices_offers_view', models.BooleanField(default=False, help_text='Benutzer darf Angebote sehen.', verbose_name='Angebote anzeigen')),
|
('invoices_offers_view', models.BooleanField(default=False, help_text='Benutzer darf Angebote sehen.', verbose_name='Angebote anzeigen')),
|
||||||
|
('invoices_offers_info', models.BooleanField(default=False, help_text='Benutzer darf Angebotsstatistiken sehen.', verbose_name='Angebotsstatistiken ansehen')),
|
||||||
('invoices_offers_add', models.BooleanField(default=False, help_text='Benutzer darf Angebote für Kunden erstellen.', verbose_name='Angebot erstellen')),
|
('invoices_offers_add', models.BooleanField(default=False, help_text='Benutzer darf Angebote für Kunden erstellen.', verbose_name='Angebot erstellen')),
|
||||||
('invoices_offers_offers_view', models.BooleanField(default=False, help_text='Benutzer darf Angebotsdetails sehen.', verbose_name='Angebotsdetails anzeigen')),
|
('invoices_offers_offers_view', models.BooleanField(default=False, help_text='Benutzer darf Angebotsdetails sehen.', verbose_name='Angebotsdetails anzeigen')),
|
||||||
('invoices_offers_offers_item_add', models.BooleanField(default=False, help_text='Benutzer darf Positionen hinzufügen.', verbose_name='Angebotsposition hinzufügen')),
|
('invoices_offers_offers_item_add', models.BooleanField(default=False, help_text='Benutzer darf Positionen hinzufügen.', verbose_name='Angebotsposition hinzufügen')),
|
||||||
('invoices_offers_offers_item_delete', models.BooleanField(default=False, help_text='Benutzer darf Positionen löschen.', verbose_name='Angebotsposition löschen')),
|
('invoices_offers_offers_item_delete', models.BooleanField(default=False, help_text='Benutzer darf Positionen löschen.', verbose_name='Angebotsposition löschen')),
|
||||||
('invoices_offers_offers_preview', models.BooleanField(default=False, help_text='Benutzer darf Vorschauen öffnen.', verbose_name='Angebotsvorschau anzeigen')),
|
('invoices_offers_offers_preview', models.BooleanField(default=False, help_text='Benutzer darf Vorschauen öffnen.', verbose_name='Angebotsvorschau anzeigen')),
|
||||||
('invoices_offers_offers_download', models.BooleanField(default=False, help_text='Benutzer darf Angebote herunterladen.', verbose_name='Angebot herunterladen')),
|
('invoices_offers_offers_download', models.BooleanField(default=False, help_text='Benutzer darf Angebote herunterladen.', verbose_name='Angebot herunterladen')),
|
||||||
('invoices_offers_offers_status', models.BooleanField(default=False, help_text='Benutzer darf Angebotsstatus ändern.', verbose_name='Angebotsstatus bearbeiten')),
|
|
||||||
('invoices_offers_offers_delete', models.BooleanField(default=False, help_text='Benutzer darf Angebote löschen.', verbose_name='Angebote löschen')),
|
('invoices_offers_offers_delete', models.BooleanField(default=False, help_text='Benutzer darf Angebote löschen.', verbose_name='Angebote löschen')),
|
||||||
('invoices_reminders_view', models.BooleanField(default=False, help_text='Benutzer darf Mahnungen einsehen.', verbose_name='Mahnungen anzeigen')),
|
('invoices_reminders_view', models.BooleanField(default=False, help_text='Benutzer darf Mahnungen einsehen.', verbose_name='Mahnungen anzeigen')),
|
||||||
|
('invoices_reminders_info', models.BooleanField(default=False, help_text='Benutzer darf Mahnungsstatistiken sehen.', verbose_name='Mahnungsstatistiken ansehen')),
|
||||||
('invoices_reminders_add', models.BooleanField(default=False, help_text='Benutzer darf Mahnungen erstellen.', verbose_name='Mahnungen hinzufügen')),
|
('invoices_reminders_add', models.BooleanField(default=False, help_text='Benutzer darf Mahnungen erstellen.', verbose_name='Mahnungen hinzufügen')),
|
||||||
('invoices_reminders_reminders_view', models.BooleanField(default=False, help_text='Benutzer darf Zahlungserinnerungen einsehen.', verbose_name='Erinnerungen anzeigen')),
|
('invoices_reminders_reminders_view', models.BooleanField(default=False, help_text='Benutzer darf Zahlungserinnerungen einsehen.', verbose_name='Erinnerungen anzeigen')),
|
||||||
('invoices_reminders_reminders_cancel', models.BooleanField(default=False, help_text='Benutzer darf Zahlungserinnerungen stornieren.', verbose_name='Erinnerungen stornieren')),
|
('invoices_reminders_reminders_cancel', models.BooleanField(default=False, help_text='Benutzer darf Zahlungserinnerungen stornieren.', verbose_name='Erinnerungen stornieren')),
|
||||||
('invoices_reminders_reminders_delete', models.BooleanField(default=False, help_text='Benutzer darf Zahlungserinnerungen löschen.', verbose_name='Erinnerungen löschen')),
|
('invoices_reminders_reminders_delete', models.BooleanField(default=False, help_text='Benutzer darf Zahlungserinnerungen löschen.', verbose_name='Erinnerungen löschen')),
|
||||||
('invoices_reminders_reminders_download', models.BooleanField(default=False, help_text='Benutzer darf Zahlungserinnerungen herunterladen.', verbose_name='Erinnerungen herunterladen')),
|
('invoices_reminders_reminders_download', models.BooleanField(default=False, help_text='Benutzer darf Zahlungserinnerungen herunterladen.', verbose_name='Erinnerungen herunterladen')),
|
||||||
('invoices_reminders_reminders_paid', models.BooleanField(default=False, help_text='Benutzer darf Zahlungserinnerungen als bezahlt markieren.', verbose_name='Erinnerungen als bezahlt markieren')),
|
('invoices_payments_view', models.BooleanField(default=False, help_text='Benutzer darf Zahlungen einsehen.', verbose_name='Zahlungen anzeigen')),
|
||||||
('invoices_reminders_reminders_expire', models.BooleanField(default=False, help_text='Benutzer darf Zahlungserinnerungen als abgelaufen markieren.', verbose_name='Erinnerungen ablaufen lassen')),
|
|
||||||
('users_view', models.BooleanField(default=False, help_text='Benutzer darf Mitarbeiter sehen.', verbose_name='Benutzer anzeigen')),
|
('users_view', models.BooleanField(default=False, help_text='Benutzer darf Mitarbeiter sehen.', verbose_name='Benutzer anzeigen')),
|
||||||
('users_add', models.BooleanField(default=False, help_text='Benutzer darf neue Mitarbeiter anlegen.', verbose_name='Benutzer erstellen')),
|
('users_add', models.BooleanField(default=False, help_text='Benutzer darf neue Mitarbeiter anlegen.', verbose_name='Benutzer erstellen')),
|
||||||
('users_edit', models.BooleanField(default=False, help_text='Benutzer darf Mitarbeiter bearbeiten.', verbose_name='Benutzer bearbeiten')),
|
('users_edit', models.BooleanField(default=False, help_text='Benutzer darf Mitarbeiter bearbeiten.', verbose_name='Benutzer bearbeiten')),
|
||||||
|
|||||||
+7
-5
@@ -42,7 +42,7 @@ class Role(models.Model):
|
|||||||
projects_inventory_shopping_add = models.BooleanField(default=False, verbose_name="Einkauf hinzufügen", help_text="Benutzer darf Einkaufspositionen erstellen.")
|
projects_inventory_shopping_add = models.BooleanField(default=False, verbose_name="Einkauf hinzufügen", help_text="Benutzer darf Einkaufspositionen erstellen.")
|
||||||
projects_inventory_shopping_edit = models.BooleanField(default=False, verbose_name="Einkauf bearbeiten", help_text="Benutzer darf Einkaufspositionen ändern.")
|
projects_inventory_shopping_edit = models.BooleanField(default=False, verbose_name="Einkauf bearbeiten", help_text="Benutzer darf Einkaufspositionen ändern.")
|
||||||
projects_inventory_shopping_delete = models.BooleanField(default=False, verbose_name="Einkauf löschen", help_text="Benutzer darf Einkaufspositionen löschen.")
|
projects_inventory_shopping_delete = models.BooleanField(default=False, verbose_name="Einkauf löschen", help_text="Benutzer darf Einkaufspositionen löschen.")
|
||||||
#projects_inventory_shopping_buy = models.BooleanField(default=False, verbose_name="Einkäufe durchführen", help_text="Benutzer darf Einkäufe als erledigt markieren.")
|
projects_inventory_shopping_to_expense = models.BooleanField(default=False, verbose_name="Einkäufe durchführen", help_text="Benutzer darf Einkäufe als erledigt markieren.")
|
||||||
|
|
||||||
|
|
||||||
# =====================
|
# =====================
|
||||||
@@ -58,8 +58,10 @@ class Role(models.Model):
|
|||||||
# Rechnungen
|
# Rechnungen
|
||||||
# =====================
|
# =====================
|
||||||
invoices_view = models.BooleanField(default=False, verbose_name="Rechnungsbereich anzeigen", help_text="Benutzer darf Rechnungen verwalten.")
|
invoices_view = models.BooleanField(default=False, verbose_name="Rechnungsbereich anzeigen", help_text="Benutzer darf Rechnungen verwalten.")
|
||||||
|
invoices_info = models.BooleanField(default=False, verbose_name="Statistiken ansehen", help_text="Benutzer darf Statistiken sehen.")
|
||||||
|
|
||||||
invoices_customers_view = models.BooleanField(default=False, verbose_name="Kunden anzeigen", help_text="Benutzer darf Kunden sehen.")
|
invoices_customers_view = models.BooleanField(default=False, verbose_name="Kunden anzeigen", help_text="Benutzer darf Kunden sehen.")
|
||||||
|
invoices_customers_info = models.BooleanField(default=False, verbose_name="Kundenstatistiken ansehen", help_text="Benutzer darf Kundenstatistiken sehen.")
|
||||||
invoices_customers_detail_view = models.BooleanField(default=False, verbose_name="Kundendetails anzeigen", help_text="Benutzer darf Details des Kunden sehen.")
|
invoices_customers_detail_view = models.BooleanField(default=False, verbose_name="Kundendetails anzeigen", help_text="Benutzer darf Details des Kunden sehen.")
|
||||||
invoices_customers_add = models.BooleanField(default=False, verbose_name="Kunden erstellen", help_text="Benutzer darf Kunden anlegen.")
|
invoices_customers_add = models.BooleanField(default=False, verbose_name="Kunden erstellen", help_text="Benutzer darf Kunden anlegen.")
|
||||||
invoices_customers_edit = models.BooleanField(default=False, verbose_name="Kunden bearbeiten", help_text="Benutzer darf Kunden ändern.")
|
invoices_customers_edit = models.BooleanField(default=False, verbose_name="Kunden bearbeiten", help_text="Benutzer darf Kunden ändern.")
|
||||||
@@ -72,6 +74,7 @@ class Role(models.Model):
|
|||||||
invoices_items_delete = models.BooleanField(default=False, verbose_name="Artikel löschen", help_text="Benutzer darf Artikel löschen.")
|
invoices_items_delete = models.BooleanField(default=False, verbose_name="Artikel löschen", help_text="Benutzer darf Artikel löschen.")
|
||||||
|
|
||||||
invoices_invoices_view = models.BooleanField(default=False, verbose_name="Rechnungen anzeigen", help_text="Benutzer darf Rechnungen sehen.")
|
invoices_invoices_view = models.BooleanField(default=False, verbose_name="Rechnungen anzeigen", help_text="Benutzer darf Rechnungen sehen.")
|
||||||
|
invoices_invoices_info = models.BooleanField(default=False, verbose_name="Rechnungsstatistiken ansehen", help_text="Benutzer darf Rechnungsstatistiken sehen.")
|
||||||
invoices_invoices_project_add = models.BooleanField(default=False, verbose_name="Projekt-Rechnung erstellen", help_text="Benutzer darf Rechnungen aus Projekten erstellen.")
|
invoices_invoices_project_add = models.BooleanField(default=False, verbose_name="Projekt-Rechnung erstellen", help_text="Benutzer darf Rechnungen aus Projekten erstellen.")
|
||||||
invoices_invoices_customer_add = models.BooleanField(default=False, verbose_name="Kunden-Rechnung erstellen", help_text="Benutzer darf Rechnungen für Kunden erstellen.")
|
invoices_invoices_customer_add = models.BooleanField(default=False, verbose_name="Kunden-Rechnung erstellen", help_text="Benutzer darf Rechnungen für Kunden erstellen.")
|
||||||
invoices_invoices_invoices_view = models.BooleanField(default=False, verbose_name="Rechnungsdetails anzeigen", help_text="Benutzer darf Rechnungsdetails sehen.")
|
invoices_invoices_invoices_view = models.BooleanField(default=False, verbose_name="Rechnungsdetails anzeigen", help_text="Benutzer darf Rechnungsdetails sehen.")
|
||||||
@@ -82,27 +85,26 @@ class Role(models.Model):
|
|||||||
invoices_invoices_invoices_cancel = models.BooleanField(default=False, verbose_name="Rechnung stornieren", help_text="Benutzer darf Rechnungen stornieren.")
|
invoices_invoices_invoices_cancel = models.BooleanField(default=False, verbose_name="Rechnung stornieren", help_text="Benutzer darf Rechnungen stornieren.")
|
||||||
invoices_invoices_invoices_paid = models.BooleanField(default=False, verbose_name="Rechnung auf bezahlt setzen", help_text="Benutzer darf Rechnungsstatus ändern.")
|
invoices_invoices_invoices_paid = models.BooleanField(default=False, verbose_name="Rechnung auf bezahlt setzen", help_text="Benutzer darf Rechnungsstatus ändern.")
|
||||||
invoices_invoices_invoices_delete = models.BooleanField(default=False, verbose_name="Rechnungen löschen", help_text="Benutzer darf Rechnungen löschen.")
|
invoices_invoices_invoices_delete = models.BooleanField(default=False, verbose_name="Rechnungen löschen", help_text="Benutzer darf Rechnungen löschen.")
|
||||||
invoices_invoices_invoices_overdue = models.BooleanField(default=False, verbose_name="Überfällige Rechnungen anzeigen", help_text="Benutzer darf überfällige Rechnungen einsehen.")
|
|
||||||
|
|
||||||
invoices_offers_view = models.BooleanField(default=False, verbose_name="Angebote anzeigen", help_text="Benutzer darf Angebote sehen.")
|
invoices_offers_view = models.BooleanField(default=False, verbose_name="Angebote anzeigen", help_text="Benutzer darf Angebote sehen.")
|
||||||
|
invoices_offers_info = models.BooleanField(default=False, verbose_name="Angebotsstatistiken ansehen", help_text="Benutzer darf Angebotsstatistiken sehen.")
|
||||||
invoices_offers_add = models.BooleanField(default=False, verbose_name="Angebot erstellen", help_text="Benutzer darf Angebote für Kunden erstellen.")
|
invoices_offers_add = models.BooleanField(default=False, verbose_name="Angebot erstellen", help_text="Benutzer darf Angebote für Kunden erstellen.")
|
||||||
invoices_offers_offers_view = models.BooleanField(default=False, verbose_name="Angebotsdetails anzeigen", help_text="Benutzer darf Angebotsdetails sehen.")
|
invoices_offers_offers_view = models.BooleanField(default=False, verbose_name="Angebotsdetails anzeigen", help_text="Benutzer darf Angebotsdetails sehen.")
|
||||||
invoices_offers_offers_item_add = models.BooleanField(default=False, verbose_name="Angebotsposition hinzufügen", help_text="Benutzer darf Positionen hinzufügen.")
|
invoices_offers_offers_item_add = models.BooleanField(default=False, verbose_name="Angebotsposition hinzufügen", help_text="Benutzer darf Positionen hinzufügen.")
|
||||||
invoices_offers_offers_item_delete = models.BooleanField(default=False, verbose_name="Angebotsposition löschen", help_text="Benutzer darf Positionen löschen.")
|
invoices_offers_offers_item_delete = models.BooleanField(default=False, verbose_name="Angebotsposition löschen", help_text="Benutzer darf Positionen löschen.")
|
||||||
invoices_offers_offers_preview = models.BooleanField(default=False, verbose_name="Angebotsvorschau anzeigen", help_text="Benutzer darf Vorschauen öffnen.")
|
invoices_offers_offers_preview = models.BooleanField(default=False, verbose_name="Angebotsvorschau anzeigen", help_text="Benutzer darf Vorschauen öffnen.")
|
||||||
invoices_offers_offers_download = models.BooleanField(default=False, verbose_name="Angebot herunterladen", help_text="Benutzer darf Angebote herunterladen.")
|
invoices_offers_offers_download = models.BooleanField(default=False, verbose_name="Angebot herunterladen", help_text="Benutzer darf Angebote herunterladen.")
|
||||||
invoices_offers_offers_status = models.BooleanField(default=False, verbose_name="Angebotsstatus bearbeiten", help_text="Benutzer darf Angebotsstatus ändern.")
|
|
||||||
invoices_offers_offers_delete = models.BooleanField(default=False, verbose_name="Angebote löschen", help_text="Benutzer darf Angebote löschen.")
|
invoices_offers_offers_delete = models.BooleanField(default=False, verbose_name="Angebote löschen", help_text="Benutzer darf Angebote löschen.")
|
||||||
|
|
||||||
invoices_reminders_view = models.BooleanField(default=False, verbose_name="Mahnungen anzeigen", help_text="Benutzer darf Mahnungen einsehen.")
|
invoices_reminders_view = models.BooleanField(default=False, verbose_name="Mahnungen anzeigen", help_text="Benutzer darf Mahnungen einsehen.")
|
||||||
|
invoices_reminders_info = models.BooleanField(default=False, verbose_name="Mahnungsstatistiken ansehen", help_text="Benutzer darf Mahnungsstatistiken sehen.")
|
||||||
invoices_reminders_add = models.BooleanField(default=False, verbose_name="Mahnungen hinzufügen", help_text="Benutzer darf Mahnungen erstellen.")
|
invoices_reminders_add = models.BooleanField(default=False, verbose_name="Mahnungen hinzufügen", help_text="Benutzer darf Mahnungen erstellen.")
|
||||||
invoices_reminders_reminders_view = models.BooleanField(default=False, verbose_name="Erinnerungen anzeigen", help_text="Benutzer darf Zahlungserinnerungen einsehen.")
|
invoices_reminders_reminders_view = models.BooleanField(default=False, verbose_name="Erinnerungen anzeigen", help_text="Benutzer darf Zahlungserinnerungen einsehen.")
|
||||||
invoices_reminders_reminders_cancel = models.BooleanField(default=False, verbose_name="Erinnerungen stornieren", help_text="Benutzer darf Zahlungserinnerungen stornieren.")
|
invoices_reminders_reminders_cancel = models.BooleanField(default=False, verbose_name="Erinnerungen stornieren", help_text="Benutzer darf Zahlungserinnerungen stornieren.")
|
||||||
invoices_reminders_reminders_delete = models.BooleanField(default=False, verbose_name="Erinnerungen löschen", help_text="Benutzer darf Zahlungserinnerungen löschen.")
|
invoices_reminders_reminders_delete = models.BooleanField(default=False, verbose_name="Erinnerungen löschen", help_text="Benutzer darf Zahlungserinnerungen löschen.")
|
||||||
invoices_reminders_reminders_download = models.BooleanField(default=False, verbose_name="Erinnerungen herunterladen", help_text="Benutzer darf Zahlungserinnerungen herunterladen.")
|
invoices_reminders_reminders_download = models.BooleanField(default=False, verbose_name="Erinnerungen herunterladen", help_text="Benutzer darf Zahlungserinnerungen herunterladen.")
|
||||||
invoices_reminders_reminders_paid = models.BooleanField(default=False, verbose_name="Erinnerungen als bezahlt markieren", help_text="Benutzer darf Zahlungserinnerungen als bezahlt markieren.")
|
|
||||||
invoices_reminders_reminders_expire = models.BooleanField(default=False, verbose_name="Erinnerungen ablaufen lassen", help_text="Benutzer darf Zahlungserinnerungen als abgelaufen markieren.")
|
|
||||||
|
|
||||||
|
invoices_payments_view = models.BooleanField(default=False, verbose_name="Zahlungen anzeigen", help_text="Benutzer darf Zahlungen einsehen.")
|
||||||
# =====================
|
# =====================
|
||||||
# Benutzerverwaltung
|
# Benutzerverwaltung
|
||||||
# =====================
|
# =====================
|
||||||
|
|||||||
@@ -1,41 +1,36 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}Login{% endblock %}
|
||||||
Login
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block header_title %}
|
{% block header %}{% endblock %}
|
||||||
<div class="flex justify-center">
|
|
||||||
{% if company_settings.logo %}
|
|
||||||
<img src="{{ company_settings.logo.url }}" alt="" class="h-32">
|
|
||||||
|
|
||||||
{% elif company_settings.companyname %}
|
|
||||||
{{ company_settings.companyname }}
|
|
||||||
{% else %}
|
|
||||||
Projektverwaltung
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block header_description %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<div class="fixed inset-0 flex justify-center items-center">
|
||||||
|
<div class="p-8 rounded-2xl shadow-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors w-full max-w-md">
|
||||||
|
|
||||||
<div class="flex justify-center items-center min-h-[80vh]">
|
<!-- Logo / Firmenname -->
|
||||||
|
<div class="flex justify-center mb-6">
|
||||||
|
{% if company_settings.logo %}
|
||||||
|
<img src="{{ company_settings.logo.url }}" alt="" class="h-30 object-contain">
|
||||||
|
{% elif company_settings.companyname %}
|
||||||
|
<span class="text-2xl font-bold tracking-tight">{{ company_settings.companyname }}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-2xl font-bold tracking-tight">Projektverwaltung</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors w-full max-w-md">
|
<!-- Trennlinie mit Company Color -->
|
||||||
|
<div class="h-0.5 rounded-full mb-6" style="background: var(--company-color, #6366f1);"></div>
|
||||||
|
|
||||||
<h1 class="text-3xl font-bold text-center mb-6">
|
<h1 class="text-xl font-semibold text-center mb-6">Anmelden</h1>
|
||||||
Anmelden
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<form method="post" class="space-y-4">
|
<form method="post" class="space-y-4">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
{% for field in form %}
|
{% for field in form %}
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300">
|
<label for="{{ field.id_for_label }}"
|
||||||
|
class="block text-sm font-medium text-slate-700 dark:text-slate-300">
|
||||||
{{ field.label }}
|
{{ field.label }}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -43,8 +38,12 @@ Login
|
|||||||
name="{{ field.html_name }}"
|
name="{{ field.html_name }}"
|
||||||
id="{{ field.id_for_label }}"
|
id="{{ field.id_for_label }}"
|
||||||
{% if field.value %}value="{{ field.value }}"{% endif %}
|
{% if field.value %}value="{{ field.value }}"{% endif %}
|
||||||
class="w-full px-4 py-3 rounded-lg border border-slate-300 dark:border-slate-600 bg-slate-50 dark:bg-slate-700 text-slate-900 dark:text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition"
|
|
||||||
placeholder="{{ field.label }}"
|
placeholder="{{ field.label }}"
|
||||||
|
class="mt-1 w-full px-4 py-2 rounded-lg border border-slate-300 dark:border-slate-600
|
||||||
|
bg-slate-50 dark:bg-slate-700 text-slate-900 dark:text-white
|
||||||
|
placeholder-slate-400 focus:outline-none focus:ring-2
|
||||||
|
focus:border-transparent transition"
|
||||||
|
style="--tw-ring-color: var(--company-color, #6366f1);"
|
||||||
>
|
>
|
||||||
{% if field.errors %}
|
{% if field.errors %}
|
||||||
<p class="text-sm text-red-500">{{ field.errors|join:", " }}</p>
|
<p class="text-sm text-red-500">{{ field.errors|join:", " }}</p>
|
||||||
@@ -54,7 +53,8 @@ Login
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
class="btn w-full btn-action"
|
class="w-full py-3 rounded-lg font-semibold text-white transition hover:opacity-90 active:scale-95 mt-2"
|
||||||
|
style="background: var(--company-color, #6366f1);"
|
||||||
>
|
>
|
||||||
Anmelden
|
Anmelden
|
||||||
</button>
|
</button>
|
||||||
@@ -62,7 +62,6 @@ Login
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -18,7 +18,7 @@ Mitarbeiter, Rollen und Berechtigungen verwalten
|
|||||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
|
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-xl font-bold">Aktionen</h2>
|
<h2 class="text-lg font-bold">Aktionen</h2>
|
||||||
<p class="text-slate-400 text-sm mt-1">Mitarbeiter oder Rollen hinzufügen</p>
|
<p class="text-slate-400 text-sm mt-1">Mitarbeiter oder Rollen hinzufügen</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ Mitarbeiter, Rollen und Berechtigungen verwalten
|
|||||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors mb-8">
|
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors mb-8">
|
||||||
|
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<h2 class="text-xl font-bold">
|
<h2 class="text-lg font-bold">
|
||||||
Benutzer
|
Benutzer
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ Mitarbeiter, Rollen und Berechtigungen verwalten
|
|||||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors mb-8">
|
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors mb-8">
|
||||||
|
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<h2 class="text-xl font-bold">
|
<h2 class="text-lg font-bold">
|
||||||
Archivierte Benutzer
|
Archivierte Benutzer
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ Mitarbeiter, Rollen und Berechtigungen verwalten
|
|||||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors mt-8">
|
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors mt-8">
|
||||||
|
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<h2 class="text-xl font-bold">
|
<h2 class="text-lg font-bold">
|
||||||
Rollen
|
Rollen
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
|
|||||||
@@ -91,7 +91,7 @@
|
|||||||
type="text"
|
type="text"
|
||||||
disabled
|
disabled
|
||||||
value="{{ item.last_login|date:'d.m.Y H:i' }}"
|
value="{{ item.last_login|date:'d.m.Y H:i' }}"
|
||||||
class="border rounded px-3 py-2 w-full bg-slate-100">
|
class="border rounded px-3 py-2 w-full bg-slate-100 dark:bg-slate-700">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -86,12 +86,11 @@
|
|||||||
<label class="block text-sm mb-1">
|
<label class="block text-sm mb-1">
|
||||||
Letzter Login
|
Letzter Login
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
disabled
|
disabled
|
||||||
value="{{ item.last_login|date:'d.m.Y H:i' }}"
|
value="{{ item.last_login|date:'d.m.Y H:i' }}"
|
||||||
class="border rounded px-3 py-2 w-full bg-slate-100">
|
class="border rounded px-3 py-2 w-full bg-slate-100 dark:bg-slate-700">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ INSTALLED_APPS = [
|
|||||||
'time_tracking',
|
'time_tracking',
|
||||||
'tailwind',
|
'tailwind',
|
||||||
'theme',
|
'theme',
|
||||||
|
'django_apscheduler',
|
||||||
"django_tables2",
|
"django_tables2",
|
||||||
"django_filters",
|
"django_filters",
|
||||||
'django.contrib.admin',
|
'django.contrib.admin',
|
||||||
@@ -83,6 +84,9 @@ USE_I18N = True
|
|||||||
USE_TZ = True
|
USE_TZ = True
|
||||||
|
|
||||||
STATIC_URL = 'static/'
|
STATIC_URL = 'static/'
|
||||||
|
STATICFILES_DIRS = [
|
||||||
|
BASE_DIR / "static",
|
||||||
|
]
|
||||||
STATIC_ROOT = BASE_DIR / 'staticfiles'
|
STATIC_ROOT = BASE_DIR / 'staticfiles'
|
||||||
|
|
||||||
TAILWIND_APP_NAME = 'theme'
|
TAILWIND_APP_NAME = 'theme'
|
||||||
|
|||||||
@@ -1,17 +1,23 @@
|
|||||||
from projects.models import ProjectMembership
|
from projects.models import ProjectMembership, Project
|
||||||
from .models import CompanySetting
|
from .models import CompanySetting
|
||||||
|
|
||||||
def project_switcher(request):
|
def project_switcher(request):
|
||||||
if not request.user.is_authenticated:
|
if not request.user.is_authenticated:
|
||||||
return {"user_projects": []}
|
return {"user_projects": [], "archived_count": 0}
|
||||||
|
|
||||||
memberships = ProjectMembership.objects.select_related("project").filter(
|
memberships = ProjectMembership.objects.select_related("project").filter(
|
||||||
user=request.user,
|
user=request.user,
|
||||||
project__active=True
|
project__active=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
archived_count = ProjectMembership.objects.filter(
|
||||||
|
user=request.user,
|
||||||
|
project__active=False
|
||||||
|
).count()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"user_projects": [m.project for m in memberships]
|
"user_projects": [m.project for m in memberships],
|
||||||
|
"archived_count": archived_count,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+7
-4
@@ -4,10 +4,13 @@ from django import forms
|
|||||||
class BaseForm(forms.ModelForm):
|
class BaseForm(forms.ModelForm):
|
||||||
|
|
||||||
default_class = (
|
default_class = (
|
||||||
"w-full px-4 py-3 rounded-lg border border-slate-300 dark:border-slate-600 "
|
"w-full px-4 py-2 rounded-lg border mt-1 "
|
||||||
"bg-slate-50 dark:bg-slate-700 text-slate-900 dark:text-white "
|
"border-slate-300 dark:border-slate-600 "
|
||||||
"placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 "
|
"bg-slate-50 dark:bg-slate-700 "
|
||||||
"focus:border-transparent transition"
|
"text-slate-900 dark:text-white "
|
||||||
|
"placeholder-slate-400 focus:outline-none "
|
||||||
|
"focus:border-transparent transition "
|
||||||
|
"[--tw-ring-color:var(--company-color)] focus:ring-2 "
|
||||||
)
|
)
|
||||||
|
|
||||||
def apply_style(self):
|
def apply_style(self):
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 6.0.6 on 2026-06-27 17:19
|
# Generated by Django 6.0.6 on 2026-06-28 10:19
|
||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
@@ -18,6 +18,7 @@ class Migration(migrations.Migration):
|
|||||||
('companyname', models.CharField(max_length=255, verbose_name='Firmenname')),
|
('companyname', models.CharField(max_length=255, verbose_name='Firmenname')),
|
||||||
('name', models.CharField(max_length=255, verbose_name='Geschäftsführer')),
|
('name', models.CharField(max_length=255, verbose_name='Geschäftsführer')),
|
||||||
('logo', models.ImageField(blank=True, null=True, upload_to='company/', verbose_name='Logo')),
|
('logo', models.ImageField(blank=True, null=True, upload_to='company/', verbose_name='Logo')),
|
||||||
|
('favicon', models.ImageField(blank=True, null=True, upload_to='company/', verbose_name='Favicon')),
|
||||||
('slogan', models.CharField(blank=True, max_length=255, verbose_name='Slogan')),
|
('slogan', models.CharField(blank=True, max_length=255, verbose_name='Slogan')),
|
||||||
('color', models.CharField(default='#0f172a', max_length=20, verbose_name='Farbe')),
|
('color', models.CharField(default='#0f172a', max_length=20, verbose_name='Farbe')),
|
||||||
('street', models.CharField(max_length=255, verbose_name='Straße')),
|
('street', models.CharField(max_length=255, verbose_name='Straße')),
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ class CompanySetting(models.Model):
|
|||||||
companyname = models.CharField(max_length=255,verbose_name="Firmenname")
|
companyname = models.CharField(max_length=255,verbose_name="Firmenname")
|
||||||
name = models.CharField(max_length=255,verbose_name="Geschäftsführer")
|
name = models.CharField(max_length=255,verbose_name="Geschäftsführer")
|
||||||
logo = models.ImageField(upload_to="company/",blank=True,null=True,verbose_name="Logo")
|
logo = models.ImageField(upload_to="company/",blank=True,null=True,verbose_name="Logo")
|
||||||
|
favicon = models.ImageField(upload_to="company/",blank=True,null=True,verbose_name="Favicon")
|
||||||
slogan = models.CharField(max_length=255,blank=True,verbose_name="Slogan")
|
slogan = models.CharField(max_length=255,blank=True,verbose_name="Slogan")
|
||||||
color = models.CharField(max_length=20,default="#0f172a",verbose_name="Farbe")
|
color = models.CharField(max_length=20,default="#0f172a",verbose_name="Farbe")
|
||||||
street = models.CharField(max_length=255,verbose_name="Straße")
|
street = models.CharField(max_length=255,verbose_name="Straße")
|
||||||
|
|||||||
+47
-18
@@ -18,6 +18,11 @@
|
|||||||
</script>
|
</script>
|
||||||
{% tailwind_css %}
|
{% tailwind_css %}
|
||||||
|
|
||||||
|
{% if company_settings.favicon %}
|
||||||
|
<link rel="icon" type="image/png" href="{{ company_settings.favicon.url }}">
|
||||||
|
{% else %}
|
||||||
|
<link rel="icon" type="image/png" href="{% static 'favicon.png' %}">
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
@@ -34,14 +39,18 @@
|
|||||||
onclick="toggleSidebar()"></div>
|
onclick="toggleSidebar()"></div>
|
||||||
|
|
||||||
<!-- Sidebar -->
|
<!-- Sidebar -->
|
||||||
<aside id="sidebar" class="fixed top-0 z-40 w-72 h-screen bg-slate-800 text-white transform -translate-x-full lg:translate-x-0 transition-transform duration-300 flex flex-col border-r border-white/20">
|
<aside id="sidebar" class="fixed top-0 z-40 w-72 h-screen bg-slate-800 text-white transform -translate-x-full lg:translate-x-0 transition-transform duration-300 flex flex-col border-r border-white/10">
|
||||||
|
|
||||||
<div class="p-6 border-b border-white/20 " style="background-color: {{ company_settings.color }};">
|
<div class="px-5 py-5 border-b border-white/10">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
<h2 class="text-xl font-bold">
|
<div class="w-2 h-2 rounded-full flex-shrink-0" style="background:var(--company-color);"></div>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-semibold text-white leading-none">
|
||||||
{{ company_settings.companyname|default:"Projektverwaltung" }}
|
{{ company_settings.companyname|default:"Projektverwaltung" }}
|
||||||
</h2>
|
</p>
|
||||||
|
<p class="text-xs mt-1" style="color:rgba(255,255,255,.3);">Projektverwaltung</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav class="flex-1 p-4 space-y-2">
|
<nav class="flex-1 p-4 space-y-2">
|
||||||
@@ -60,7 +69,7 @@
|
|||||||
onclick="toggleProjects()"
|
onclick="toggleProjects()"
|
||||||
class="w-full flex items-center justify-between px-4 py-3 hover:bg-slate-700">
|
class="w-full flex items-center justify-between px-4 py-3 hover:bg-slate-700">
|
||||||
|
|
||||||
<span>{{ current_project.name|default:"Projekt wählen" }}</span>
|
<span>Projekte</span>
|
||||||
|
|
||||||
<svg id="projectArrow"
|
<svg id="projectArrow"
|
||||||
class="w-4 h-4 transition-transform"
|
class="w-4 h-4 transition-transform"
|
||||||
@@ -84,6 +93,21 @@
|
|||||||
</a>
|
</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
|
{# Trennlinie #}
|
||||||
|
<div class="mx-4 my-1 border-t border-slate-700/60"></div>
|
||||||
|
|
||||||
|
<a href="{% url 'projects:add' %}"
|
||||||
|
class="block px-8 py-3 hover:bg-slate-700 text-slate-400 hover:text-white transition-colors">
|
||||||
|
Hinzufügen
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{% if archived_count %}
|
||||||
|
<a href="{% url 'projects:archived' %}"
|
||||||
|
class="block px-8 py-3 hover:bg-slate-700 text-slate-400 hover:text-white transition-colors">
|
||||||
|
Archiviert ({{ archived_count }})
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -145,6 +169,11 @@
|
|||||||
Posten
|
Posten
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<a href="{% url 'invoices:invoices_payments_overview' %}"
|
||||||
|
class="block px-8 py-3 hover:bg-slate-700 transition">
|
||||||
|
Zahlungen
|
||||||
|
</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -188,17 +217,17 @@
|
|||||||
|
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="p-4 border-t border-white/20">
|
<div class="p-4 border-t border-white/10">
|
||||||
{% if user.is_superuser %}
|
{% if user.is_superuser %}
|
||||||
<div class="w-full rounded-xl flex items-center justify-between px-4 py-3 hover:bg-slate-700 mb-4 border-white/20 border">
|
<div class="w-full rounded-xl flex items-center justify-between px-4 py-3 hover:bg-slate-700 mb-4 border-white/10 border">
|
||||||
<a href="{% url 'admin:index' %}">Admin</a>
|
<a href="{% url 'admin:index' %}">Admin</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="flex gap-2 w-full">
|
<div class="flex gap-2 w-full">
|
||||||
<button id="theme-toggle" class="rounded-xl px-4 py-3 hover:bg-slate-700 border-white/20 border">🌙</button>
|
<button id="theme-toggle" class="rounded-xl px-4 py-3 hover:bg-slate-700 border-white/10 border">🌙</button>
|
||||||
<form method="post" action="{% url 'accounts:logout' %}" class="w-full">
|
<form method="post" action="{% url 'accounts:logout' %}" class="w-full">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<button class="w-full rounded-xl px-4 py-3 border-white/20 border" style="background-color: {{ company_settings.color }};">Abmelden</button>
|
<button class="w-full rounded-xl px-4 py-3 border-white/10 border" style="background-color: {{ company_settings.color }};">Abmelden</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -217,16 +246,16 @@
|
|||||||
<span class="ml-4 font-bold">{{ company_settings.companyname|default:"Projektverwaltung" }}</span>
|
<span class="ml-4 font-bold">{{ company_settings.companyname|default:"Projektverwaltung" }}</span>
|
||||||
</header>
|
</header>
|
||||||
<main class="flex-1 p-6 overflow-x-auto w-screen lg:w-auto">
|
<main class="flex-1 p-6 overflow-x-auto w-screen lg:w-auto">
|
||||||
<div class="p-6 rounded-xl shadow mb-8 bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
{% block header %}
|
||||||
<h1 class="text-3xl font-bold">
|
<div class="border border-slate-100 dark:border-slate-700 p-6 rounded-xl shadow mb-8 bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||||
{% block header_title %}
|
<h1 class="text-xl font-bold">
|
||||||
{% endblock %}
|
{% block header_title %}{% endblock %}
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-slate-400 mt-1">
|
<p class="text-slate-400 mt-1 text-md">
|
||||||
{% block header_description %}
|
{% block header_description %}{% endblock %}
|
||||||
{% endblock %}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -2,9 +2,7 @@
|
|||||||
|
|
||||||
{% block title %}Dashboard{% endblock %}
|
{% block title %}Dashboard{% endblock %}
|
||||||
|
|
||||||
{% block header_title %}
|
{% block header_title %}Dashboard{% endblock %}
|
||||||
Dashboard
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block header_description %}
|
{% block header_description %}
|
||||||
Willkommen {{ user.username }}
|
Willkommen {{ user.username }}
|
||||||
@@ -12,103 +10,245 @@ Willkommen {{ user.username }}
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
|
{# Navigation #}
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4 gap-4 mb-8">
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-2 mb-6">
|
||||||
<a href="{% url 'invoices:overview' %}" class="border border-white/20 p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
<a href="{% url 'invoices:overview' %}" class="group p-4 rounded-xl bg-white dark:bg-slate-800 border border-slate-100 dark:border-slate-700 hover:border-[var(--company-color)]/40 hover:shadow-sm transition-all duration-200">
|
||||||
<div class="flex items-center gap-3 justify-center">
|
<p class="text-[11px] text-slate-400 mb-0.5">Modul</p>
|
||||||
<h2 class="text-xl font-bold">Rechnungswesen</h2>
|
<p class="text-sm font-semibold text-slate-800 dark:text-white">Rechnungswesen</p>
|
||||||
</div>
|
|
||||||
</a>
|
</a>
|
||||||
<!--
|
<a href="{% url 'inventory_global:overview' %}" class="group p-4 rounded-xl bg-white dark:bg-slate-800 border border-slate-100 dark:border-slate-700 hover:border-[var(--company-color)]/40 hover:shadow-sm transition-all duration-200">
|
||||||
<a href="" class="border border-white/20 p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
<p class="text-[11px] text-slate-400 mb-0.5">Modul</p>
|
||||||
<div class="flex items-center gap-3 justify-center">
|
<p class="text-sm font-semibold text-slate-800 dark:text-white">Inventar</p>
|
||||||
<h2 class="text-xl font-bold">Finanzübersicht</h2>
|
|
||||||
</div>
|
|
||||||
</a>
|
</a>
|
||||||
<a href="" class="border border-white/20 p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
<a href="{% url 'accounts:overview' %}" class="group p-4 rounded-xl bg-white dark:bg-slate-800 border border-slate-100 dark:border-slate-700 hover:border-[var(--company-color)]/40 hover:shadow-sm transition-all duration-200">
|
||||||
<div class="flex items-center gap-3 justify-center">
|
<p class="text-[11px] text-slate-400 mb-0.5">Modul</p>
|
||||||
<h2 class="text-xl font-bold">Arbeitszeiten</h2>
|
<p class="text-sm font-semibold text-slate-800 dark:text-white">Mitarbeiter</p>
|
||||||
</div>
|
|
||||||
</a> -->
|
|
||||||
<a href="{% url 'inventory_global:overview' %}" class="border border-white/20 p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
|
||||||
<div class="flex items-center gap-3 justify-center">
|
|
||||||
<h2 class="text-xl font-bold">Inventar</h2>
|
|
||||||
</div>
|
|
||||||
</a>
|
</a>
|
||||||
<!--
|
<a href="{% url 'masterdata:overview' %}" class="group p-4 rounded-xl bg-white dark:bg-slate-800 border border-slate-100 dark:border-slate-700 hover:border-[var(--company-color)]/40 hover:shadow-sm transition-all duration-200">
|
||||||
<a href="" class="border border-white/20 p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
<p class="text-[11px] text-slate-400 mb-0.5">Modul</p>
|
||||||
<div class="flex items-center gap-3 justify-center">
|
<p class="text-sm font-semibold text-slate-800 dark:text-white">Einstellungen</p>
|
||||||
<h2 class="text-xl font-bold">Investitionen</h2>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
-->
|
|
||||||
<a href="{% url 'accounts:overview' %}" class="border border-white/20 p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
|
||||||
<div class="flex items-center gap-3 justify-center">
|
|
||||||
<h2 class="text-xl font-bold">Mitarbeiter</h2>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a href="{% url 'masterdata:overview' %}" class="border border-white/20 p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
|
||||||
<div class="flex items-center gap-3 justify-center">
|
|
||||||
<h2 class="text-xl font-bold">Einstellungen</h2>
|
|
||||||
</div>
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{# Haupt-Dashboard #}
|
||||||
|
<div class="bg-white dark:bg-slate-800 rounded-2xl border border-slate-100 dark:border-slate-700 overflow-hidden mb-6">
|
||||||
|
|
||||||
|
{# KPIs #}
|
||||||
|
<div class="px-5 pt-5 pb-4 border-b border-slate-100 dark:border-slate-700">
|
||||||
|
<p class="text-[11px] font-semibold uppercase tracking-widest text-slate-400 mb-3">Handlungsbedarf</p>
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-2" id="kpiMetrics">
|
||||||
|
<div class="rounded-xl bg-slate-100 dark:bg-slate-700 p-4 animate-pulse h-16"></div>
|
||||||
|
<div class="rounded-xl bg-slate-100 dark:bg-slate-700 p-4 animate-pulse h-16"></div>
|
||||||
|
<div class="rounded-xl bg-slate-100 dark:bg-slate-700 p-4 animate-pulse h-16"></div>
|
||||||
<div class="mb-8">
|
<div class="rounded-xl bg-slate-100 dark:bg-slate-700 p-4 animate-pulse h-16"></div>
|
||||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
</div>
|
||||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
|
|
||||||
<div>
|
|
||||||
<h2 class="text-xl font-bold">Aktionen</h2>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col sm:flex-row gap-2">
|
{# Charts #}
|
||||||
<a href="{% url 'projects:add' %}" class="btn btn-add">+ Projekt</a>
|
<div class="px-5 py-4 border-b border-slate-100 dark:border-slate-700">
|
||||||
|
<p class="text-[11px] font-semibold uppercase tracking-widest text-slate-400 mb-3">Aktueller Monat</p>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-2">
|
||||||
|
|
||||||
|
<div class="rounded-xl bg-slate-100 dark:bg-slate-700 p-4">
|
||||||
|
<div class="flex items-center justify-between mb-3">
|
||||||
|
<p class="text-xs font-semibold text-slate-600 dark:text-slate-300">Rechnungen</p>
|
||||||
|
<a href="{% url 'invoices:invoices_invoices_overview' %}" class="text-[11px] text-[var(--company-color)] hover:underline">Alle →</a>
|
||||||
|
</div>
|
||||||
|
<div style="position:relative;height:120px;">
|
||||||
|
<canvas id="invoiceChart" role="img" aria-label="Rechnungen nach Status"></canvas>
|
||||||
|
</div>
|
||||||
|
<div id="invoiceLegend" class="flex flex-wrap gap-x-2 gap-y-1 mt-3"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-xl bg-slate-100 dark:bg-slate-700 p-4">
|
||||||
|
<div class="flex items-center justify-between mb-3">
|
||||||
|
<p class="text-xs font-semibold text-slate-600 dark:text-slate-300">Angebote</p>
|
||||||
|
<a href="{% url 'invoices:invoices_offers_overview' %}" class="text-[11px] text-[var(--company-color)] hover:underline">Alle →</a>
|
||||||
|
</div>
|
||||||
|
<div style="position:relative;height:120px;">
|
||||||
|
<canvas id="offerChart" role="img" aria-label="Angebote nach Status"></canvas>
|
||||||
|
</div>
|
||||||
|
<div id="offerLegend" class="flex flex-wrap gap-x-2 gap-y-1 mt-3"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-xl bg-slate-100 dark:bg-slate-700 p-4">
|
||||||
|
<div class="flex items-center justify-between mb-3">
|
||||||
|
<p class="text-xs font-semibold text-slate-600 dark:text-slate-300">Mahnungen</p>
|
||||||
|
<a href="{% url 'invoices:invoices_reminders_overview' %}" class="text-[11px] text-[var(--company-color)] hover:underline">Alle →</a>
|
||||||
|
</div>
|
||||||
|
<div style="position:relative;height:120px;">
|
||||||
|
<canvas id="reminderChart" role="img" aria-label="Mahnungen nach Status"></canvas>
|
||||||
|
</div>
|
||||||
|
<div id="reminderLegend" class="flex flex-wrap gap-x-2 gap-y-1 mt-3"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{# Kunden #}
|
||||||
|
<div class="px-5 py-4">
|
||||||
|
<p class="text-[11px] font-semibold uppercase tracking-widest text-slate-400 mb-3">Kunden</p>
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-2" id="customerMetrics">
|
||||||
|
<div class="rounded-xl bg-slate-100 dark:bg-slate-700 p-4 animate-pulse h-16"></div>
|
||||||
|
<div class="rounded-xl bg-slate-100 dark:bg-slate-700 p-4 animate-pulse h-16"></div>
|
||||||
|
<div class="rounded-xl bg-slate-100 dark:bg-slate-700 p-4 animate-pulse h-16"></div>
|
||||||
|
<div class="rounded-xl bg-slate-100 dark:bg-slate-700 p-4 animate-pulse h-16"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
<script>
|
||||||
|
window.URLS = {
|
||||||
|
invoiceInfo: "{% url 'invoices:ajax_invoice_info' %}",
|
||||||
|
offerInfo: "{% url 'invoices:ajax_offer_info' %}",
|
||||||
|
reminderInfo: "{% url 'invoices:ajax_reminder_info' %}",
|
||||||
|
customerInfo: "{% url 'invoices:ajax_customer_info' %}",
|
||||||
|
kpiInfo: "{% url 'invoices:ajax_kpi_info' %}",
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
{% if memberships %}
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.js"></script>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<script>
|
||||||
{% for m in memberships %}
|
const STATUS_META = {
|
||||||
<a href="{% url 'projects:overview' m.project.id %}"
|
draft: { label: "Entwurf", color: "#94a3b8" },
|
||||||
class="p-6 rounded-xl shadow hover:shadow-lg transition block text-white"
|
sent: { label: "Versendet", color: "#2a78d6" },
|
||||||
style="background: linear-gradient(135deg, {{ m.project.color }}, {{ m.project.color }}ff);">
|
paid: { label: "Bezahlt", color: "#1baf7a" },
|
||||||
<h2 class="text-xl font-bold">
|
overdue: { label: "Überfällig", color: "#e34948" },
|
||||||
{{ m.project.name }}
|
canceled: { label: "Storniert", color: "#eb6834" },
|
||||||
</h2>
|
accepted: { label: "Akzeptiert", color: "#1baf7a" },
|
||||||
<p class="text-sm mt-2">
|
rejected: { label: "Abgelehnt", color: "#e34948" },
|
||||||
Rolle: {{ m.get_role_display }}
|
expired: { label: "Frist abgelaufen", color: "#eda100" },
|
||||||
</p>
|
};
|
||||||
|
|
||||||
|
function buildDonut(canvasId, legendId, data) {
|
||||||
|
const entries = Object.entries(data).filter(([, v]) => v > 0);
|
||||||
|
if (!entries.length) {
|
||||||
|
document.getElementById(canvasId).parentElement.innerHTML =
|
||||||
|
'<p class="text-[11px] text-slate-400 text-center py-8">Keine Einträge diesen Monat</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const keys = entries.map(([k]) => k);
|
||||||
|
const values = entries.map(([, v]) => v);
|
||||||
|
const colors = keys.map(k => (STATUS_META[k] || { color: "#888" }).color);
|
||||||
|
const labels = keys.map(k => (STATUS_META[k] || { label: k }).label);
|
||||||
|
const total = values.reduce((a, b) => a + b, 0);
|
||||||
|
|
||||||
|
new Chart(document.getElementById(canvasId), {
|
||||||
|
type: 'doughnut',
|
||||||
|
data: {
|
||||||
|
labels,
|
||||||
|
datasets: [{
|
||||||
|
data: values,
|
||||||
|
backgroundColor: colors,
|
||||||
|
borderWidth: 0,
|
||||||
|
hoverOffset: 3,
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
cutout: '72%',
|
||||||
|
plugins: {
|
||||||
|
legend: { display: false },
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
label: ctx => ` ${ctx.label}: ${ctx.parsed} (${Math.round(ctx.parsed / total * 100)}%)`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const legend = document.getElementById(legendId);
|
||||||
|
keys.forEach((k, i) => {
|
||||||
|
const item = document.createElement('span');
|
||||||
|
item.className = 'flex items-center gap-1 text-[11px] text-slate-400 dark:text-slate-400';
|
||||||
|
item.innerHTML = `
|
||||||
|
<span style="width:6px;height:6px;border-radius:50%;background:${colors[i]};display:inline-block;flex-shrink:0;"></span>
|
||||||
|
${labels[i]}: <span class="font-medium text-slate-700 dark:text-slate-200">${values[i]}</span>
|
||||||
|
`;
|
||||||
|
legend.appendChild(item);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchAndRender(url, canvasId, legendId) {
|
||||||
|
try {
|
||||||
|
const r = await fetch(url);
|
||||||
|
if (!r.ok) throw new Error();
|
||||||
|
buildDonut(canvasId, legendId, await r.json());
|
||||||
|
} catch {
|
||||||
|
document.getElementById(canvasId).parentElement.innerHTML =
|
||||||
|
'<p class="text-[11px] text-slate-400 text-center py-8">Keine Daten</p>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchKpis(url) {
|
||||||
|
try {
|
||||||
|
const r = await fetch(url);
|
||||||
|
if (!r.ok) throw new Error();
|
||||||
|
const d = await r.json();
|
||||||
|
document.getElementById('kpiMetrics').innerHTML = `
|
||||||
|
<a href="{% url 'invoices:invoices_invoices_overview' %}?status=overdue"
|
||||||
|
class="rounded-xl bg-slate-100 dark:bg-slate-700 p-4 transition-colors block">
|
||||||
|
<p class="text-[11px] text-slate-400 mb-1">Überfällige Rechnungen</p>
|
||||||
|
<p class="text-2xl font-semibold ${d.overdue_invoices > 0 ? 'text-red-500' : 'text-slate-800 dark:text-white'}">${d.overdue_invoices}</p>
|
||||||
</a>
|
</a>
|
||||||
{% endfor %}
|
<a href="{% url 'invoices:invoices_invoices_overview' %}?status=sent"
|
||||||
</div>
|
class="rounded-xl bg-slate-100 dark:bg-slate-700 p-4 transition-colors block">
|
||||||
{% else %}
|
<p class="text-[11px] text-slate-400 mb-1">Offene Rechnungen</p>
|
||||||
<div class="bg-white dark:bg-slate-800 border border-orange-500 rounded-xl p-6 text-orange-500">
|
<p class="text-2xl font-semibold text-slate-800 dark:text-white">${d.open_invoices}</p>
|
||||||
Du bist noch keinem aktiven Projekt zugewiesen.
|
</a>
|
||||||
</div>
|
<a href="{% url 'invoices:invoices_offers_overview' %}?status=sent"
|
||||||
{% endif %}
|
class="rounded-xl bg-slate-100 dark:bg-slate-700 p-4 transition-colors block">
|
||||||
|
<p class="text-[11px] text-slate-400 mb-1">Offene Angebote</p>
|
||||||
|
<p class="text-2xl font-semibold text-slate-800 dark:text-white">${d.open_offers}</p>
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'invoices:invoices_reminders_overview' %}?status=sent"
|
||||||
|
class="rounded-xl bg-slate-100 dark:bg-slate-700 p-4 transition-colors block">
|
||||||
|
<p class="text-[11px] text-slate-400 mb-1">Offene Mahnungen</p>
|
||||||
|
<p class="text-2xl font-semibold ${d.open_reminders > 0 ? 'text-amber-500' : 'text-slate-800 dark:text-white'}">${d.open_reminders}</p>
|
||||||
|
</a>
|
||||||
|
`;
|
||||||
|
} catch {
|
||||||
|
document.getElementById('kpiMetrics').innerHTML =
|
||||||
|
'<p class="text-[11px] text-slate-400">Keine Daten</p>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
{% if archived_count %}
|
async function fetchCustomers(url) {
|
||||||
<a href="{% url 'projects:archived' %}" class="text-sm text-slate-400 hover:text-slate-500">
|
try {
|
||||||
{{ archived_count }} archivierte
|
const r = await fetch(url);
|
||||||
{% if archived_count == 1 %}
|
if (!r.ok) throw new Error();
|
||||||
Projekt anzeigen
|
const d = await r.json();
|
||||||
{% else %}
|
document.getElementById('customerMetrics').innerHTML = `
|
||||||
Projekte anzeigen
|
<div class="rounded-xl bg-slate-100 dark:bg-slate-700 p-4">
|
||||||
{% endif %}
|
<p class="text-[11px] text-slate-400 mb-1">Gesamt</p>
|
||||||
</a>
|
<p class="text-2xl font-semibold text-slate-800 dark:text-white">${d.total}</p>
|
||||||
{% endif %}
|
</div>
|
||||||
|
<div class="rounded-xl bg-slate-100 dark:bg-slate-700 p-4">
|
||||||
|
<p class="text-[11px] text-slate-400 mb-1">Neue diese Woche</p>
|
||||||
|
<p class="text-2xl font-semibold text-slate-800 dark:text-white">${d.new_this_week}</p>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-xl bg-slate-100 dark:bg-slate-700 p-4">
|
||||||
|
<p class="text-[11px] text-slate-400 mb-1">Privatkunden</p>
|
||||||
|
<p class="text-2xl font-semibold text-slate-800 dark:text-white">${d.private}</p>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-xl bg-slate-100 dark:bg-slate-700 p-4">
|
||||||
|
<p class="text-[11px] text-slate-400 mb-1">Unternehmen</p>
|
||||||
|
<p class="text-2xl font-semibold text-slate-800 dark:text-white">${d.companies}</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} catch {
|
||||||
|
document.getElementById('customerMetrics').innerHTML =
|
||||||
|
'<p class="text-[11px] text-slate-400">Keine Daten</p>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchKpis(window.URLS.kpiInfo);
|
||||||
|
fetchAndRender(window.URLS.invoiceInfo, 'invoiceChart', 'invoiceLegend');
|
||||||
|
fetchAndRender(window.URLS.offerInfo, 'offerChart', 'offerLegend');
|
||||||
|
fetchAndRender(window.URLS.reminderInfo, 'reminderChart', 'reminderLegend');
|
||||||
|
fetchCustomers(window.URLS.customerInfo);
|
||||||
|
</script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 6.0.6 on 2026-06-27 17:19
|
# Generated by Django 6.0.6 on 2026-06-28 10:19
|
||||||
|
|
||||||
import documents.models
|
import documents.models
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 6.0.6 on 2026-06-27 17:19
|
# Generated by Django 6.0.6 on 2026-06-28 10:19
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ Dateien & Bilder
|
|||||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
|
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-xl font-bold">Aktionen</h2>
|
<h2 class="text-lg font-bold">Aktionen</h2>
|
||||||
<p class="text-slate-400 text-sm mt-1">Dokumente hochladen</p>
|
<p class="text-slate-400 text-sm mt-1">Dokumente hochladen</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ Dateien & Bilder
|
|||||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors mb-2" >
|
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors mb-2" >
|
||||||
|
|
||||||
<div class="flex items-center justify-between mb-2">
|
<div class="flex items-center justify-between mb-2">
|
||||||
<h2 class="text-xl font-bold">
|
<h2 class="text-lg font-bold">
|
||||||
{{ cat.name }}
|
{{ cat.name }}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# Generated by Django 6.0.6 on 2026-06-27 17:19
|
# Generated by Django 6.0.6 on 2026-06-28 10:19
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
import finance.models
|
import finance.models
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@@ -31,7 +32,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('name', models.CharField(max_length=255, null=True, verbose_name='Name')),
|
('name', models.CharField(max_length=255, null=True, verbose_name='Name')),
|
||||||
('amount', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Betrag (€)')),
|
('amount', models.DecimalField(decimal_places=2, max_digits=10, validators=[django.core.validators.MinValueValidator(0, message='Der Betrag darf nicht negativ sein.')], verbose_name='Betrag (€)')),
|
||||||
('date', models.DateTimeField(verbose_name='Datum')),
|
('date', models.DateTimeField(verbose_name='Datum')),
|
||||||
('note', models.TextField(blank=True, verbose_name='Notiz')),
|
('note', models.TextField(blank=True, verbose_name='Notiz')),
|
||||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Erstellt am')),
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Erstellt am')),
|
||||||
@@ -64,7 +65,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('name', models.CharField(max_length=255, verbose_name='Name')),
|
('name', models.CharField(max_length=255, verbose_name='Name')),
|
||||||
('amount', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Betrag (€)')),
|
('amount', models.DecimalField(decimal_places=2, max_digits=10, validators=[django.core.validators.MinValueValidator(0, message='Der Betrag darf nicht negativ sein.')], verbose_name='Betrag (€)')),
|
||||||
('date', models.DateTimeField(verbose_name='Datum')),
|
('date', models.DateTimeField(verbose_name='Datum')),
|
||||||
('receipt', models.FileField(blank=True, null=True, upload_to=finance.models.receipt_upload_path, verbose_name='Beleg')),
|
('receipt', models.FileField(blank=True, null=True, upload_to=finance.models.receipt_upload_path, verbose_name='Beleg')),
|
||||||
('note', models.TextField(blank=True, verbose_name='Notiz')),
|
('note', models.TextField(blank=True, verbose_name='Notiz')),
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 6.0.6 on 2026-06-27 17:19
|
# Generated by Django 6.0.6 on 2026-06-28 10:19
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@@ -11,6 +11,7 @@ class Migration(migrations.Migration):
|
|||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('finance', '0001_initial'),
|
('finance', '0001_initial'),
|
||||||
|
('invoices', '0001_initial'),
|
||||||
('projects', '0001_initial'),
|
('projects', '0001_initial'),
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
]
|
]
|
||||||
@@ -31,6 +32,11 @@ class Migration(migrations.Migration):
|
|||||||
name='created_by',
|
name='created_by',
|
||||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Erstellt von'),
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Erstellt von'),
|
||||||
),
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='income',
|
||||||
|
name='invoice',
|
||||||
|
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='income', to='invoices.invoice'),
|
||||||
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='income',
|
model_name='income',
|
||||||
name='project',
|
name='project',
|
||||||
|
|||||||
+13
-2
@@ -2,6 +2,9 @@ from django.db import models
|
|||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from projects.models import Project
|
from projects.models import Project
|
||||||
import os
|
import os
|
||||||
|
from django.core.validators import MinValueValidator
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
|
||||||
@@ -42,7 +45,7 @@ class Expense(models.Model):
|
|||||||
supplier = models.ForeignKey(Supplier, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Lieferant")
|
supplier = models.ForeignKey(Supplier, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Lieferant")
|
||||||
created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, verbose_name="Erstellt von")
|
created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, verbose_name="Erstellt von")
|
||||||
payment_method = models.ForeignKey(PaymentMethod, on_delete=models.SET_NULL, null=True, verbose_name="Zahlungsmethode")
|
payment_method = models.ForeignKey(PaymentMethod, on_delete=models.SET_NULL, null=True, verbose_name="Zahlungsmethode")
|
||||||
amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="Betrag (€)")
|
amount = models.DecimalField(max_digits=10,decimal_places=2,verbose_name="Betrag (€)",validators=[MinValueValidator(0,message="Der Betrag darf nicht negativ sein.")])
|
||||||
date = models.DateTimeField(verbose_name="Datum")
|
date = models.DateTimeField(verbose_name="Datum")
|
||||||
receipt = models.FileField(upload_to=receipt_upload_path, null=True, blank=True, verbose_name="Beleg")
|
receipt = models.FileField(upload_to=receipt_upload_path, null=True, blank=True, verbose_name="Beleg")
|
||||||
note = models.TextField(blank=True, verbose_name="Notiz")
|
note = models.TextField(blank=True, verbose_name="Notiz")
|
||||||
@@ -54,8 +57,16 @@ class Expense(models.Model):
|
|||||||
class Income(models.Model):
|
class Income(models.Model):
|
||||||
name = models.CharField(max_length=255, verbose_name="Name", null=True)
|
name = models.CharField(max_length=255, verbose_name="Name", null=True)
|
||||||
project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name="incomes", verbose_name="Projekt")
|
project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name="incomes", verbose_name="Projekt")
|
||||||
|
invoice = models.OneToOneField(
|
||||||
|
"invoices.Invoice",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name="income"
|
||||||
|
)
|
||||||
|
|
||||||
category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, verbose_name="Kategorie")
|
category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, verbose_name="Kategorie")
|
||||||
amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="Betrag (€)")
|
amount = models.DecimalField(max_digits=10,decimal_places=2,verbose_name="Betrag (€)",validators=[MinValueValidator(0,message="Der Betrag darf nicht negativ sein.")])
|
||||||
date = models.DateTimeField(verbose_name="Datum")
|
date = models.DateTimeField(verbose_name="Datum")
|
||||||
created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, verbose_name="Erstellt von")
|
created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, verbose_name="Erstellt von")
|
||||||
payment_method = models.ForeignKey(PaymentMethod, on_delete=models.SET_NULL, null=True, verbose_name="Zahlungsmethode")
|
payment_method = models.ForeignKey(PaymentMethod, on_delete=models.SET_NULL, null=True, verbose_name="Zahlungsmethode")
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ Einnahmen & Ausgaben
|
|||||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
|
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-xl font-bold">Aktionen</h2>
|
<h2 class="text-lg font-bold">Aktionen</h2>
|
||||||
<p class="text-slate-400 text-sm mt-1">Einnahmen und Ausgaben tracken</p>
|
<p class="text-slate-400 text-sm mt-1">Einnahmen und Ausgaben tracken</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ Einnahmen & Ausgaben
|
|||||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||||
|
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<h2 class="text-xl font-bold">Filter</h2>
|
<h2 class="text-lg font-bold">Filter</h2>
|
||||||
<p class="text-slate-400 text-sm mt-1">Zeitraum für aktuelle Auswertung</p>
|
<p class="text-slate-400 text-sm mt-1">Zeitraum für aktuelle Auswertung</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -72,30 +72,80 @@ Einnahmen & Ausgaben
|
|||||||
<!-- ===================== -->
|
<!-- ===================== -->
|
||||||
<!-- KPI FLOATING CARDS -->
|
<!-- KPI FLOATING CARDS -->
|
||||||
<!-- ===================== -->
|
<!-- ===================== -->
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-2">
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
|
||||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
<!-- Einnahmen -->
|
||||||
<p class="text-slate-400 text-sm mt-1">Einnahmen</p>
|
<div class="p-6 rounded-xl border border-green-200 dark:border-green-900 bg-white dark:bg-slate-800 transition">
|
||||||
<p class="text-3xl font-bold text-green-600 mt-1">
|
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm text-slate-500 dark:text-slate-400">
|
||||||
|
Einnahmen
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="text-3xl font-bold text-green-600 mt-2">
|
||||||
{{ total_income }} €
|
{{ total_income }} €
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
<div class="flex items-center justify-center w-12 h-12 rounded-xl bg-green-100 dark:bg-green-900/40">
|
||||||
<p class="text-slate-400 text-sm mt-1">Ausgaben</p>
|
<span class="text-2xl text-green-600">
|
||||||
<p class="text-3xl font-bold text-red-600 mt-1">
|
↑
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Ausgaben -->
|
||||||
|
<div class="p-6 rounded-xl border border-red-200 dark:border-red-900 bg-white dark:bg-slate-800 transition">
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm text-slate-500 dark:text-slate-400">
|
||||||
|
Ausgaben
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="text-3xl font-bold text-red-600 mt-2">
|
||||||
{{ total_expense }} €
|
{{ total_expense }} €
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
<div class="flex items-center justify-center w-12 h-12 rounded-xl bg-red-100 dark:bg-red-900/40">
|
||||||
<p class="text-slate-400 text-sm mt-1">Gewinn</p>
|
<span class="text-2xl text-red-600">
|
||||||
<p class="text-3xl font-bold text-blue-600 mt-1">
|
↓
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Gewinn -->
|
||||||
|
<div class="p-6 rounded-xl border border-blue-200 dark:border-blue-900 bg-white dark:bg-slate-800 transition">
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm text-slate-500 dark:text-slate-400">
|
||||||
|
Gewinn
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="text-3xl font-bold text-blue-600 mt-2">
|
||||||
{{ total_profit }} €
|
{{ total_profit }} €
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-center w-12 h-12 rounded-xl bg-blue-100 dark:bg-blue-900/40">
|
||||||
|
<span class="text-2xl text-blue-600">
|
||||||
|
€
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- ===================== -->
|
<!-- ===================== -->
|
||||||
@@ -106,21 +156,21 @@ Einnahmen & Ausgaben
|
|||||||
<!-- INCOME -->
|
<!-- INCOME -->
|
||||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||||
|
|
||||||
<div class="p-4">
|
<div class="pb-4">
|
||||||
<h3 class="font-semibold text-green-600">Einnahmen</h3>
|
<h3 class="font-semibold text-green-600">Einnahmen</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="divide-y">
|
<div class="flex flex-col gap-2 max-h-80 overflow-y-auto hide-scrollbar">
|
||||||
|
|
||||||
{% for i in incomes %}
|
{% for i in incomes %}
|
||||||
<div class="p-4 flex justify-between">
|
<div class="p-4 flex justify-between bg-slate-100 dark:bg-slate-700 rounded-xl">
|
||||||
<div>
|
<div>
|
||||||
<p class="font-medium text-green-600">+{{ i.amount }} € {{ i.name }}</p>
|
<p class="font-medium text-green-600">+{{ i.amount }} € {{ i.name }}</p>
|
||||||
<p class="text-slate-400 text-sm mt-1">{{ i.date|date:"d.m.Y" }} · {{ i.category }}</p>
|
<p class="text-slate-400 text-sm mt-1">{{ i.date|date:"d.m.Y" }} · {{ i.category }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<p class="p-4 text-slate-400">Keine Einnahmen</p>
|
<div class="bg-white dark:bg-slate-800 border border-slate-500 rounded-xl p-6 text-slate-500">Keine Einnahmen</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -131,21 +181,21 @@ Einnahmen & Ausgaben
|
|||||||
<!-- EXPENSE -->
|
<!-- EXPENSE -->
|
||||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||||
|
|
||||||
<div class="p-4">
|
<div class="pb-4">
|
||||||
<h3 class="font-semibold text-red-600">Ausgaben</h3>
|
<h3 class="font-semibold text-red-600">Ausgaben</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="divide-y">
|
<div class="flex flex-col gap-2 max-h-80 overflow-y-auto hide-scrollbar">
|
||||||
|
|
||||||
{% for e in expenses %}
|
{% for e in expenses %}
|
||||||
<div class="p-4 flex justify-between">
|
<div class="p-4 flex justify-between bg-slate-100 dark:bg-slate-700 rounded-xl">
|
||||||
<div>
|
<div>
|
||||||
<p class="font-medium text-red-600">-{{ e.amount }} € {{ e.name }}</p>
|
<p class="font-medium text-red-600">-{{ e.amount }} € {{ e.name }}</p>
|
||||||
<p class="text-slate-400 text-sm mt-1">{{ e.date|date:"d.m.Y" }} · {{ e.category }}</p>
|
<p class="text-slate-400 text-sm mt-1">{{ e.date|date:"d.m.Y" }} · {{ e.category }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<p class="p-4 text-slate-400">Keine Ausgaben</p>
|
<div class="bg-white dark:bg-slate-800 border border-slate-500 rounded-xl p-6 text-slate-500">Keine Ausgaben</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -160,7 +210,7 @@ Einnahmen & Ausgaben
|
|||||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||||
|
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<h2 class="text-xl font-bold">
|
<h2 class="text-lg font-bold">
|
||||||
Zeitraum-Analyse
|
Zeitraum-Analyse
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
|
|||||||
+44
-10
@@ -11,6 +11,8 @@ from django.http import HttpResponse
|
|||||||
from calendar import monthrange
|
from calendar import monthrange
|
||||||
from .forms import IncomeForm, ExpenseForm
|
from .forms import IncomeForm, ExpenseForm
|
||||||
from .tables import PeriodAnalyticsTable, FinanceTransactionTable
|
from .tables import PeriodAnalyticsTable, FinanceTransactionTable
|
||||||
|
import json
|
||||||
|
from inventory.models import ShoppingItem
|
||||||
|
|
||||||
# =========================================================
|
# =========================================================
|
||||||
# OVERVIEW
|
# OVERVIEW
|
||||||
@@ -102,23 +104,35 @@ def finance_overview(request, project_id):
|
|||||||
}
|
}
|
||||||
|
|
||||||
period_data = []
|
period_data = []
|
||||||
|
period_keys = set()
|
||||||
for i in income_stats:
|
for i in income_stats:
|
||||||
period_start = i["period_key"]
|
period_keys.add(i["period_key"])
|
||||||
income_total = i["total"] or 0
|
for e in expense_stats:
|
||||||
expense_total = expense_map.get(str(period_start), 0)
|
period_keys.add(e["period_key"])
|
||||||
|
|
||||||
|
income_map = {
|
||||||
|
str(i["period_key"]): i["total"] or 0
|
||||||
|
for i in income_stats
|
||||||
|
}
|
||||||
|
expense_map = {
|
||||||
|
str(e["period_key"]): e["total"] or 0
|
||||||
|
for e in expense_stats
|
||||||
|
}
|
||||||
|
|
||||||
|
for period_start in sorted(period_keys, reverse=True):
|
||||||
|
income_total = income_map.get(str(period_start), 0)
|
||||||
|
expense_total = expense_map.get(str(period_start), 0)
|
||||||
if period == "year":
|
if period == "year":
|
||||||
period_end = period_start.replace(month=12, day=31)
|
period_end = period_start.replace(month=12, day=31)
|
||||||
|
|
||||||
elif period == "week":
|
elif period == "week":
|
||||||
period_end = period_start + timedelta(days=6)
|
period_end = period_start + timedelta(days=6)
|
||||||
|
|
||||||
elif period == "day":
|
elif period == "day":
|
||||||
period_end = period_start
|
period_end = period_start
|
||||||
|
|
||||||
else:
|
else:
|
||||||
last_day = monthrange(period_start.year, period_start.month)[1]
|
last_day = monthrange(
|
||||||
|
period_start.year,
|
||||||
|
period_start.month
|
||||||
|
)[1]
|
||||||
period_end = period_start.replace(day=last_day)
|
period_end = period_start.replace(day=last_day)
|
||||||
|
|
||||||
period_data.append({
|
period_data.append({
|
||||||
@@ -319,10 +333,30 @@ def add_expense(request, project_id):
|
|||||||
expense.project = project
|
expense.project = project
|
||||||
expense.created_by = request.user
|
expense.created_by = request.user
|
||||||
expense.save()
|
expense.save()
|
||||||
return redirect("finance:overview", project_id=project.id)
|
|
||||||
|
shopping_ids = request.session.pop(
|
||||||
|
"shopping_item_ids",
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
if shopping_ids:
|
||||||
|
ShoppingItem.objects.filter(
|
||||||
|
id__in=shopping_ids,
|
||||||
|
project=project
|
||||||
|
).delete()
|
||||||
|
|
||||||
|
return redirect(
|
||||||
|
"finance:overview",
|
||||||
|
project_id=project.id
|
||||||
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
form = ExpenseForm()
|
initial = request.session.pop(
|
||||||
|
"expense_initial",
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
|
||||||
|
form = ExpenseForm(initial=initial)
|
||||||
|
|
||||||
return render(request, "finance/form.html", {
|
return render(request, "finance/form.html", {
|
||||||
"form": form,
|
"form": form,
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ from core.forms.base import BaseForm
|
|||||||
|
|
||||||
|
|
||||||
class InventoryItemForm(BaseForm):
|
class InventoryItemForm(BaseForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = InventoryItem
|
model = InventoryItem
|
||||||
fields = [
|
fields = [
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# Generated by Django 6.0.6 on 2026-06-27 17:19
|
# Generated by Django 6.0.6 on 2026-06-28 10:19
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
@@ -44,8 +45,8 @@ class Migration(migrations.Migration):
|
|||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('name', models.CharField(max_length=255, verbose_name='Name')),
|
('name', models.CharField(max_length=255, verbose_name='Name')),
|
||||||
('description', models.TextField(blank=True, verbose_name='Beschreibung')),
|
('description', models.TextField(blank=True, verbose_name='Beschreibung')),
|
||||||
('quantity', models.PositiveIntegerField(default=1, verbose_name='Anzahl')),
|
('quantity', models.PositiveIntegerField(default=1, validators=[django.core.validators.MinValueValidator(0, message='Die Anzahl darf nicht negativ sein.')], verbose_name='Anzahl')),
|
||||||
('price', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='Preis')),
|
('price', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, validators=[django.core.validators.MinValueValidator(0, message='Der Betrag darf nicht negativ sein.')], verbose_name='Preis')),
|
||||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
@@ -60,7 +61,7 @@ class Migration(migrations.Migration):
|
|||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('name', models.CharField(max_length=255, verbose_name='Name')),
|
('name', models.CharField(max_length=255, verbose_name='Name')),
|
||||||
('description', models.TextField(blank=True, verbose_name='Beschreibung')),
|
('description', models.TextField(blank=True, verbose_name='Beschreibung')),
|
||||||
('quantity', models.PositiveIntegerField(default=0, verbose_name='Anzahl')),
|
('quantity', models.PositiveIntegerField(default=0, validators=[django.core.validators.MinValueValidator(0, message='Die Anzahl darf nicht negativ sein.')], verbose_name='Anzahl')),
|
||||||
('article_number', models.CharField(blank=True, max_length=255, verbose_name='Artikelnummer')),
|
('article_number', models.CharField(blank=True, max_length=255, verbose_name='Artikelnummer')),
|
||||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='inventory.inventorycategory', verbose_name='Kategorie')),
|
('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='inventory.inventorycategory', verbose_name='Kategorie')),
|
||||||
@@ -77,7 +78,7 @@ class Migration(migrations.Migration):
|
|||||||
name='ProjectInventoryItem',
|
name='ProjectInventoryItem',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('quantity', models.PositiveIntegerField(default=1, verbose_name='Anzahl')),
|
('quantity', models.PositiveIntegerField(default=1, validators=[django.core.validators.MinValueValidator(0, message='Die Anzahl darf nicht negativ sein.')], verbose_name='Anzahl')),
|
||||||
('status', models.CharField(choices=[('open', 'Offen'), ('packed', 'Eingepackt'), ('on_site', 'Vor Ort')], default='open', max_length=20, verbose_name='Status')),
|
('status', models.CharField(choices=[('open', 'Offen'), ('packed', 'Eingepackt'), ('on_site', 'Vor Ort')], default='open', max_length=20, verbose_name='Status')),
|
||||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.inventoryitem', verbose_name='Artikel')),
|
('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.inventoryitem', verbose_name='Artikel')),
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 6.0.6 on 2026-06-27 17:19
|
# Generated by Django 6.0.6 on 2026-06-28 10:19
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|||||||
+5
-4
@@ -3,6 +3,7 @@ from django.contrib.auth.models import User
|
|||||||
from projects.models import Project
|
from projects.models import Project
|
||||||
from finance.models import Category, Supplier
|
from finance.models import Category, Supplier
|
||||||
from django.db.models import Sum
|
from django.db.models import Sum
|
||||||
|
from django.core.validators import MinValueValidator
|
||||||
|
|
||||||
class InventoryCategory(models.Model):
|
class InventoryCategory(models.Model):
|
||||||
name = models.CharField(max_length=255, unique=True, verbose_name="Name")
|
name = models.CharField(max_length=255, unique=True, verbose_name="Name")
|
||||||
@@ -31,7 +32,7 @@ class InventoryLocation(models.Model):
|
|||||||
class InventoryItem(models.Model):
|
class InventoryItem(models.Model):
|
||||||
name = models.CharField(max_length=255, verbose_name="Name")
|
name = models.CharField(max_length=255, verbose_name="Name")
|
||||||
description = models.TextField(blank=True, verbose_name="Beschreibung")
|
description = models.TextField(blank=True, verbose_name="Beschreibung")
|
||||||
quantity = models.PositiveIntegerField(default=0, verbose_name="Anzahl")
|
quantity = models.PositiveIntegerField(default=0, verbose_name="Anzahl", validators=[MinValueValidator(0,message="Die Anzahl darf nicht negativ sein.")])
|
||||||
location = models.ForeignKey(InventoryLocation, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Standort")
|
location = models.ForeignKey(InventoryLocation, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Standort")
|
||||||
article_number = models.CharField(max_length=255, blank=True, verbose_name="Artikelnummer")
|
article_number = models.CharField(max_length=255, blank=True, verbose_name="Artikelnummer")
|
||||||
responsible = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Verantwortlicher")
|
responsible = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Verantwortlicher")
|
||||||
@@ -55,7 +56,7 @@ class ProjectInventoryItem(models.Model):
|
|||||||
|
|
||||||
project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name="inventory_items", verbose_name="Projekt")
|
project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name="inventory_items", verbose_name="Projekt")
|
||||||
item = models.ForeignKey(InventoryItem, on_delete=models.CASCADE, verbose_name="Artikel")
|
item = models.ForeignKey(InventoryItem, on_delete=models.CASCADE, verbose_name="Artikel")
|
||||||
quantity = models.PositiveIntegerField(default=1, verbose_name="Anzahl")
|
quantity = models.PositiveIntegerField(default=1, verbose_name="Anzahl", validators=[MinValueValidator(0,message="Die Anzahl darf nicht negativ sein.")])
|
||||||
status = models.CharField(max_length=20, choices=Status.choices, default=Status.OPEN, verbose_name="Status")
|
status = models.CharField(max_length=20, choices=Status.choices, default=Status.OPEN, verbose_name="Status")
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
@@ -72,9 +73,9 @@ class ShoppingItem(models.Model):
|
|||||||
project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name="shopping_items", verbose_name="Projekt")
|
project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name="shopping_items", verbose_name="Projekt")
|
||||||
name = models.CharField(max_length=255, verbose_name="Name")
|
name = models.CharField(max_length=255, verbose_name="Name")
|
||||||
description = models.TextField(blank=True, verbose_name="Beschreibung")
|
description = models.TextField(blank=True, verbose_name="Beschreibung")
|
||||||
quantity = models.PositiveIntegerField(default=1, verbose_name="Anzahl")
|
quantity = models.PositiveIntegerField(default=1, verbose_name="Anzahl", validators=[MinValueValidator(0,message="Die Anzahl darf nicht negativ sein.")])
|
||||||
supplier = models.ForeignKey(Supplier, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Lieferant")
|
supplier = models.ForeignKey(Supplier, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Lieferant")
|
||||||
price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name="Preis")
|
price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name="Preis",validators=[MinValueValidator(0,message="Der Betrag darf nicht negativ sein.")])
|
||||||
category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Kategorie")
|
category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Kategorie")
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
|||||||
+8
-1
@@ -32,7 +32,6 @@ class InventoryGlobalTable(tables.Table):
|
|||||||
model = InventoryItem
|
model = InventoryItem
|
||||||
template_name = "django_tables2/table_toggle.html"
|
template_name = "django_tables2/table_toggle.html"
|
||||||
fields = (
|
fields = (
|
||||||
"expand",
|
|
||||||
"name",
|
"name",
|
||||||
"category",
|
"category",
|
||||||
"available",
|
"available",
|
||||||
@@ -85,6 +84,11 @@ class InventoryAddTable(tables.Table):
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ShoppingItemTable(tables.Table):
|
class ShoppingItemTable(tables.Table):
|
||||||
|
selection = tables.TemplateColumn(
|
||||||
|
template_name="inventory/tables/shopping/selection.html",
|
||||||
|
orderable=False,
|
||||||
|
verbose_name="",
|
||||||
|
)
|
||||||
|
|
||||||
name = tables.TemplateColumn(
|
name = tables.TemplateColumn(
|
||||||
template_name="inventory/tables/shopping/name.html",
|
template_name="inventory/tables/shopping/name.html",
|
||||||
@@ -125,6 +129,7 @@ class ShoppingItemTable(tables.Table):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = ShoppingItem
|
model = ShoppingItem
|
||||||
fields = (
|
fields = (
|
||||||
|
"selection",
|
||||||
"name",
|
"name",
|
||||||
"category",
|
"category",
|
||||||
"quantity",
|
"quantity",
|
||||||
@@ -137,6 +142,8 @@ class ShoppingItemTable(tables.Table):
|
|||||||
"class": "w-full text-sm"
|
"class": "w-full text-sm"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ProjectInventoryItemTable(tables.Table):
|
class ProjectInventoryItemTable(tables.Table):
|
||||||
|
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ Globale Inventarverwaltung
|
|||||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
|
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-xl font-bold">Aktionen</h2>
|
<h2 class="text-lg font-bold">Aktionen</h2>
|
||||||
<p class="text-slate-400 text-sm mt-1">Inventarartikel hinzufügen</p>
|
<p class="text-slate-400 text-sm mt-1">Inventarartikel hinzufügen</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ Globale Inventarverwaltung
|
|||||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||||
|
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<h2 class="text-xl font-bold">Inventarliste</h2>
|
<h2 class="text-lg font-bold">Inventarliste</h2>
|
||||||
<p class="text-slate-400 text-sm mt-1">Gelagerte und verfügbare Artikel</p>
|
<p class="text-slate-400 text-sm mt-1">Gelagerte und verfügbare Artikel</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ Vorhandenen & benötigtes Material
|
|||||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
|
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-xl font-bold">Aktionen</h2>
|
<h2 class="text-lg font-bold">Aktionen</h2>
|
||||||
<p class="text-slate-400 text-sm mt-1">Artikel zur Packliste oder Einkaufsliste hinzufügen</p>
|
<p class="text-slate-400 text-sm mt-1">Artikel zur Packliste oder Einkaufsliste hinzufügen</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ Vorhandenen & benötigtes Material
|
|||||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||||
|
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<h2 class="text-xl font-bold">Packliste</h2>
|
<h2 class="text-lg font-bold">Packliste</h2>
|
||||||
<p class="text-slate-400 text-sm mt-1">Vorhandene und benötigte Artikel</p>
|
<p class="text-slate-400 text-sm mt-1">Vorhandene und benötigte Artikel</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ Vorhandenen & benötigtes Material
|
|||||||
<div class="flex flex-wrap justify-between items-center mb-6">
|
<div class="flex flex-wrap justify-between items-center mb-6">
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-xl font-bold">
|
<h2 class="text-lg font-bold">
|
||||||
Einkaufsliste
|
Einkaufsliste
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
@@ -64,9 +64,13 @@ Vorhandenen & benötigtes Material
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="overflow-x-auto rounded-xl">
|
<div class="overflow-x-auto rounded-xl">
|
||||||
|
<form id="bulk-shopping-form" method="post" action="{% url 'inventory:shopping_to_expense' project.id %}" class="rounded-xl">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" class="btn btn-action mb-2">
|
||||||
|
Zu Ausgabe konvertieren
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
{% render_table shopping_table %}
|
{% render_table shopping_table %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
name="item_id"
|
name="item_id"
|
||||||
value="{{ record.id }}"
|
value="{{ record.id }}"
|
||||||
form="save-form-{{ record.id }}">
|
form="save-form-{{ record.id }}">
|
||||||
|
<div class="flex gap-2">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
form="save-form-{{ record.id }}"
|
form="save-form-{{ record.id }}"
|
||||||
@@ -25,5 +25,5 @@
|
|||||||
class="btn btn-delete">
|
class="btn btn-delete">
|
||||||
Löschen
|
Löschen
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -3,8 +3,12 @@
|
|||||||
{{ record.available_quantity }}
|
{{ record.available_quantity }}
|
||||||
</span>
|
</span>
|
||||||
{% elif record.available_quantity == 0 %}
|
{% elif record.available_quantity == 0 %}
|
||||||
<span class="text-orange-600 font-semibold">
|
<span class="text-yellow-600 font-semibold">
|
||||||
0
|
0
|
||||||
</span>
|
</span>
|
||||||
|
{% elif record.available_quantity < 0 %}
|
||||||
|
<span class="text-orange-600 font-semibold">
|
||||||
|
{{ record.available_quantity }}
|
||||||
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
/ {{ record.quantity }}
|
/ {{ record.quantity }}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<form
|
<form
|
||||||
id="shopping-save-form-{{ record.id }}"
|
id="shopping-save-form-{{ record.id }}"
|
||||||
method="post"
|
method="post"
|
||||||
action="{% url 'inventory:shopping_edit' table.project.id record.id %}">
|
action="{% url 'inventory:shopping_edit' shopping_table.project.id record.id %}">
|
||||||
|
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
form="shopping-save-form-{{ record.id }}"
|
form="shopping-save-form-{{ record.id }}"
|
||||||
formaction="{% url 'inventory:shopping_delete' table.project.id record.id %}"
|
formaction="{% url 'inventory:shopping_delete' shopping_table.project.id record.id %}"
|
||||||
class="btn btn-delete">
|
class="btn btn-delete">
|
||||||
Löschen
|
Löschen
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
<option value="">---</option>
|
<option value="">---</option>
|
||||||
|
|
||||||
{% for c in table.categories %}
|
{% for c in shopping_table.categories %}
|
||||||
<option value="{{ c.id }}"
|
<option value="{{ c.id }}"
|
||||||
{% if record.category_id == c.id %}selected{% endif %}>
|
{% if record.category_id == c.id %}selected{% endif %}>
|
||||||
{{ c.name }}
|
{{ c.name }}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="selection"
|
||||||
|
value="{{ record.id }}"
|
||||||
|
form="bulk-shopping-form">
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
<option value="">---</option>
|
<option value="">---</option>
|
||||||
|
|
||||||
{% for s in table.suppliers %}
|
{% for s in shopping_table.suppliers %}
|
||||||
<option value="{{ s.id }}"
|
<option value="{{ s.id }}"
|
||||||
{% if record.supplier_id == s.id %}selected{% endif %}>
|
{% if record.supplier_id == s.id %}selected{% endif %}>
|
||||||
{{ s.name }}
|
{{ s.name }}
|
||||||
|
|||||||
@@ -13,4 +13,6 @@ urlpatterns = [
|
|||||||
|
|
||||||
path("<int:project_id>/inventory/delete/<int:item_id>/", views.inventory_delete, name="delete"),
|
path("<int:project_id>/inventory/delete/<int:item_id>/", views.inventory_delete, name="delete"),
|
||||||
path("<int:project_id>/inventory/shopping-delete/<int:item_id>/", views.shopping_delete, name="shopping_delete"),
|
path("<int:project_id>/inventory/shopping-delete/<int:item_id>/", views.shopping_delete, name="shopping_delete"),
|
||||||
|
|
||||||
|
path("<int:project_id>/inventory/shopping-to-expense/", views.shopping_to_expense, name="shopping_to_expense"),
|
||||||
]
|
]
|
||||||
@@ -9,6 +9,9 @@ from django.db.models import Sum, Value, F
|
|||||||
from django_tables2 import RequestConfig
|
from django_tables2 import RequestConfig
|
||||||
from .tables import ProjectInventoryItemTable, ShoppingItemTable, InventoryAddTable, InventoryGlobalTable
|
from .tables import ProjectInventoryItemTable, ShoppingItemTable, InventoryAddTable, InventoryGlobalTable
|
||||||
from django.db.models.functions import Coalesce
|
from django.db.models.functions import Coalesce
|
||||||
|
import json
|
||||||
|
from collections import Counter
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@permission_required("projects_inventory_inventory_view")
|
@permission_required("projects_inventory_inventory_view")
|
||||||
@@ -429,3 +432,93 @@ def inventory_global_edit(request, item_id):
|
|||||||
"users": User.objects.order_by("username"),
|
"users": User.objects.order_by("username"),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@permission_required("projects_inventory_shopping_to_expense")
|
||||||
|
def shopping_to_expense(request, project_id):
|
||||||
|
|
||||||
|
ids = request.POST.getlist("selection")
|
||||||
|
|
||||||
|
items = ShoppingItem.objects.filter(pk__in=ids)
|
||||||
|
|
||||||
|
data = []
|
||||||
|
total_amount = 0
|
||||||
|
categories = []
|
||||||
|
suppliers = []
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
|
||||||
|
total_amount += (item.quantity * item.price)
|
||||||
|
|
||||||
|
if item.category:
|
||||||
|
categories.append(item.category.id)
|
||||||
|
|
||||||
|
if item.supplier:
|
||||||
|
suppliers.append(item.supplier.id)
|
||||||
|
|
||||||
|
data.append({
|
||||||
|
"name": item.name,
|
||||||
|
"quantity": item.quantity,
|
||||||
|
"price": str(item.price),
|
||||||
|
"supplier": str(item.supplier) if item.supplier else None,
|
||||||
|
"category": str(item.category) if item.category else None,
|
||||||
|
"description": item.description,
|
||||||
|
})
|
||||||
|
|
||||||
|
most_category = (
|
||||||
|
Counter(categories).most_common(1)[0][0]
|
||||||
|
if categories else None
|
||||||
|
)
|
||||||
|
|
||||||
|
most_supplier = (
|
||||||
|
Counter(suppliers).most_common(1)[0][0]
|
||||||
|
if suppliers else None
|
||||||
|
)
|
||||||
|
|
||||||
|
request.session["expense_initial"] = {
|
||||||
|
"name": "Einkauf vom " + timezone.now().strftime("%d.%m.%Y"),
|
||||||
|
"amount": str(total_amount),
|
||||||
|
"date": timezone.now().strftime("%Y-%m-%dT%H:%M"),
|
||||||
|
"category": most_category,
|
||||||
|
"supplier": most_supplier,
|
||||||
|
"note": json.dumps(
|
||||||
|
data,
|
||||||
|
ensure_ascii=False,
|
||||||
|
indent=2
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
request.session["shopping_item_ids"] = list(
|
||||||
|
items.values_list("id", flat=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
return redirect(
|
||||||
|
"finance:add_expense",
|
||||||
|
project_id=project_id
|
||||||
|
)
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# invoices/__init__.py
|
||||||
|
default_app_config = "invoices.apps.InvoicesConfig"
|
||||||
+67
-1
@@ -7,7 +7,9 @@ from .models import (
|
|||||||
Invoice,
|
Invoice,
|
||||||
InvoiceItem,
|
InvoiceItem,
|
||||||
Unit,
|
Unit,
|
||||||
Reminder
|
Reminder,
|
||||||
|
InvoicePayment,
|
||||||
|
InvoicePaymentAllocation,
|
||||||
)
|
)
|
||||||
|
|
||||||
@admin.register(Unit)
|
@admin.register(Unit)
|
||||||
@@ -273,3 +275,67 @@ class ReminderAdmin(admin.ModelAdmin):
|
|||||||
)
|
)
|
||||||
|
|
||||||
ordering = ("-created_at",)
|
ordering = ("-created_at",)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(InvoicePayment)
|
||||||
|
class InvoicePaymentAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
|
list_display = (
|
||||||
|
"invoice",
|
||||||
|
"amount",
|
||||||
|
"date",
|
||||||
|
"payment_method",
|
||||||
|
"created_by",
|
||||||
|
"created_at",
|
||||||
|
)
|
||||||
|
|
||||||
|
list_filter = (
|
||||||
|
"payment_method",
|
||||||
|
"date",
|
||||||
|
"created_at",
|
||||||
|
)
|
||||||
|
|
||||||
|
search_fields = (
|
||||||
|
"invoice__invoice_number",
|
||||||
|
"invoice__customer__company_name",
|
||||||
|
"note",
|
||||||
|
)
|
||||||
|
|
||||||
|
readonly_fields = (
|
||||||
|
"created_at",
|
||||||
|
"created_by",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(InvoicePaymentAllocation)
|
||||||
|
class InvoicePaymentAllocationAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
|
list_display = (
|
||||||
|
"payment",
|
||||||
|
"amount",
|
||||||
|
"reminder",
|
||||||
|
)
|
||||||
+20
-2
@@ -1,5 +1,23 @@
|
|||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
class InvoicesConfig(AppConfig):
|
class InvoicesConfig(AppConfig):
|
||||||
name = 'invoices'
|
name = "invoices"
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
from django_apscheduler.jobstores import DjangoJobStore
|
||||||
|
from apscheduler.schedulers.background import BackgroundScheduler
|
||||||
|
from . import scheduler
|
||||||
|
|
||||||
|
sched = BackgroundScheduler()
|
||||||
|
sched.add_jobstore(DjangoJobStore(), "default")
|
||||||
|
|
||||||
|
sched.add_job(
|
||||||
|
scheduler.mark_overdue,
|
||||||
|
trigger="cron",
|
||||||
|
hour=1,
|
||||||
|
minute=0,
|
||||||
|
id="mark_overdue",
|
||||||
|
replace_existing=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
sched.start()
|
||||||
+33
-1
@@ -1,12 +1,41 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from core.forms.base import BaseForm
|
from core.forms.base import BaseForm
|
||||||
from .models import Customer, Item, Invoice, InvoiceItem, CustomerContact, CustomerLocation, Offer, OfferItem, Reminder
|
from .models import Customer, Item, Invoice, InvoiceItem, CustomerContact, CustomerLocation, Offer, OfferItem, Reminder, InvoicePayment
|
||||||
from projects.models import Project
|
from projects.models import Project
|
||||||
from time_tracking.models import TimeEntry
|
from time_tracking.models import TimeEntry
|
||||||
from finance.models import Expense
|
from finance.models import Expense
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.db.models import Q, Max
|
from django.db.models import Q, Max
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class InvoicePaymentForm(BaseForm):
|
||||||
|
class Meta:
|
||||||
|
model = InvoicePayment
|
||||||
|
fields = [
|
||||||
|
"amount",
|
||||||
|
"date",
|
||||||
|
"payment_method",
|
||||||
|
"note",
|
||||||
|
]
|
||||||
|
|
||||||
|
widgets = {
|
||||||
|
"date": forms.DateInput(
|
||||||
|
attrs={
|
||||||
|
"type": "date"
|
||||||
|
}
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields["date"].initial = timezone.now().date()
|
||||||
|
self.apply_style()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class CustomerForm(BaseForm):
|
class CustomerForm(BaseForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Customer
|
model = Customer
|
||||||
@@ -32,9 +61,12 @@ class ItemForm(BaseForm):
|
|||||||
fields = [
|
fields = [
|
||||||
"item_type",
|
"item_type",
|
||||||
"description",
|
"description",
|
||||||
|
"category",
|
||||||
"unit",
|
"unit",
|
||||||
"price_net",
|
"price_net",
|
||||||
"tax_rate",
|
"tax_rate",
|
||||||
|
"price_buy",
|
||||||
|
"supplier",
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# Generated by Django 6.0.6 on 2026-06-27 17:19
|
# Generated by Django 6.0.6 on 2026-06-28 10:19
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
@@ -40,10 +41,10 @@ class Migration(migrations.Migration):
|
|||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('source_type', models.CharField(choices=[('item', 'Artikel'), ('time', 'Zeiterfassung'), ('expense', 'Ausgabe')], default='item', max_length=20, verbose_name='Quelle')),
|
('source_type', models.CharField(choices=[('item', 'Artikel'), ('time', 'Zeiterfassung'), ('expense', 'Ausgabe')], default='item', max_length=20, verbose_name='Quelle')),
|
||||||
('description', models.CharField(max_length=255, verbose_name='Beschreibung')),
|
('description', models.CharField(max_length=255, verbose_name='Beschreibung')),
|
||||||
('quantity', models.DecimalField(decimal_places=2, default=1, max_digits=10, verbose_name='Menge')),
|
('quantity', models.DecimalField(decimal_places=2, default=1, max_digits=10, validators=[django.core.validators.MinValueValidator(0, message='Die Anzahl darf nicht negativ sein.')], verbose_name='Menge')),
|
||||||
('unit', models.CharField(max_length=50, verbose_name='Einheit')),
|
('unit', models.CharField(max_length=50, verbose_name='Einheit')),
|
||||||
('unit_price_net', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Einzelpreis Netto')),
|
('unit_price_net', models.DecimalField(decimal_places=2, max_digits=10, validators=[django.core.validators.MinValueValidator(0, message='Der Betrag darf nicht negativ sein.')], verbose_name='Einzelpreis Netto')),
|
||||||
('unit_tax_rate', models.DecimalField(decimal_places=2, default=19.0, max_digits=5, verbose_name='MwSt. Satz (%)')),
|
('unit_tax_rate', models.DecimalField(decimal_places=2, default=19.0, max_digits=5, validators=[django.core.validators.MinValueValidator(0, message='Der Satz darf nicht negativ sein.')], verbose_name='MwSt. Satz (%)')),
|
||||||
('service_period_from', models.DateField(blank=True, null=True, verbose_name='Leistungszeitraum von')),
|
('service_period_from', models.DateField(blank=True, null=True, verbose_name='Leistungszeitraum von')),
|
||||||
('service_period_to', models.DateField(blank=True, null=True, verbose_name='Leistungszeitraum bis')),
|
('service_period_to', models.DateField(blank=True, null=True, verbose_name='Leistungszeitraum bis')),
|
||||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
@@ -53,14 +54,32 @@ class Migration(migrations.Migration):
|
|||||||
'verbose_name_plural': 'Rechnungspositionen',
|
'verbose_name_plural': 'Rechnungspositionen',
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='InvoicePayment',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('amount', models.DecimalField(decimal_places=2, max_digits=10, validators=[django.core.validators.MinValueValidator(0, message='Der Betrag darf nicht negativ sein.')], verbose_name='Betrag (€)')),
|
||||||
|
('date', models.DateTimeField(verbose_name='Datum')),
|
||||||
|
('note', models.TextField(blank=True, verbose_name='Notiz')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='InvoicePaymentAllocation',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('amount', models.DecimalField(decimal_places=2, max_digits=10)),
|
||||||
|
],
|
||||||
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='Item',
|
name='Item',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('item_type', models.CharField(choices=[('service', 'Dienstleistung'), ('product', 'Ware')], default='product', max_length=20, verbose_name='Art')),
|
('item_type', models.CharField(choices=[('service', 'Dienstleistung'), ('product', 'Ware')], default='product', max_length=20, verbose_name='Art')),
|
||||||
('description', models.CharField(max_length=255, verbose_name='Beschreibung')),
|
('description', models.CharField(max_length=255, verbose_name='Beschreibung')),
|
||||||
('price_net', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Preis Netto')),
|
('price_net', models.DecimalField(decimal_places=2, max_digits=10, validators=[django.core.validators.MinValueValidator(0, message='Der Betrag darf nicht negativ sein.')], verbose_name='Preis Netto')),
|
||||||
('tax_rate', models.DecimalField(decimal_places=2, default=19.0, max_digits=5, verbose_name='MwSt. Satz (%)')),
|
('tax_rate', models.DecimalField(decimal_places=2, default=19.0, max_digits=5, validators=[django.core.validators.MinValueValidator(0, message='Der Satz darf nicht negativ sein.')], verbose_name='MwSt. Satz (%)')),
|
||||||
|
('price_buy', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, validators=[django.core.validators.MinValueValidator(0, message='Der Betrag darf nicht negativ sein.')], verbose_name='Einkaufspreis')),
|
||||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
@@ -95,7 +114,7 @@ class Migration(migrations.Migration):
|
|||||||
options={
|
options={
|
||||||
'verbose_name': 'Angebot',
|
'verbose_name': 'Angebot',
|
||||||
'verbose_name_plural': 'Angebote',
|
'verbose_name_plural': 'Angebote',
|
||||||
'ordering': ['-issue_date'],
|
'ordering': ['-offer_number'],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
@@ -103,10 +122,10 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('description', models.CharField(max_length=255, verbose_name='Beschreibung')),
|
('description', models.CharField(max_length=255, verbose_name='Beschreibung')),
|
||||||
('quantity', models.DecimalField(decimal_places=2, default=1, max_digits=10, verbose_name='Menge')),
|
('quantity', models.DecimalField(decimal_places=2, default=1, max_digits=10, validators=[django.core.validators.MinValueValidator(0, message='Die Anzahl darf nicht negativ sein.')], verbose_name='Menge')),
|
||||||
('unit', models.CharField(max_length=50, verbose_name='Einheit')),
|
('unit', models.CharField(max_length=50, verbose_name='Einheit')),
|
||||||
('unit_price_net', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Einzelpreis Netto')),
|
('unit_price_net', models.DecimalField(decimal_places=2, max_digits=10, validators=[django.core.validators.MinValueValidator(0, message='Der Betrag darf nicht negativ sein.')], verbose_name='Einzelpreis Netto')),
|
||||||
('unit_tax_rate', models.DecimalField(decimal_places=2, default=19.0, max_digits=5, verbose_name='MwSt. Satz (%)')),
|
('unit_tax_rate', models.DecimalField(decimal_places=2, default=19.0, max_digits=5, validators=[django.core.validators.MinValueValidator(0, message='Der Satz darf nicht negativ sein.')], verbose_name='MwSt. Satz (%)')),
|
||||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
@@ -123,7 +142,7 @@ class Migration(migrations.Migration):
|
|||||||
('level', models.PositiveSmallIntegerField(default=1, help_text='1 = Erste Mahnung, 2 = Zweite Mahnung, 3 = Letzte Mahnung', verbose_name='Mahnstufe')),
|
('level', models.PositiveSmallIntegerField(default=1, help_text='1 = Erste Mahnung, 2 = Zweite Mahnung, 3 = Letzte Mahnung', verbose_name='Mahnstufe')),
|
||||||
('issue_date', models.DateField(verbose_name='Mahndatum')),
|
('issue_date', models.DateField(verbose_name='Mahndatum')),
|
||||||
('due_date', models.DateField(blank=True, null=True, verbose_name='Neue Zahlungsfrist')),
|
('due_date', models.DateField(blank=True, null=True, verbose_name='Neue Zahlungsfrist')),
|
||||||
('fee', models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Mahngebühr')),
|
('fee', models.DecimalField(decimal_places=2, default=0, max_digits=10, validators=[django.core.validators.MinValueValidator(0, message='Der Betrag darf nicht negativ sein.')], verbose_name='Mahngebühr')),
|
||||||
('note', models.TextField(blank=True, verbose_name='Hinweis')),
|
('note', models.TextField(blank=True, verbose_name='Hinweis')),
|
||||||
('pdf_file', models.FileField(blank=True, null=True, upload_to='reminder_pdfs/', verbose_name='PDF')),
|
('pdf_file', models.FileField(blank=True, null=True, upload_to='reminder_pdfs/', verbose_name='PDF')),
|
||||||
('pdf_generated_at', models.DateTimeField(blank=True, null=True, verbose_name='PDF erstellt am')),
|
('pdf_generated_at', models.DateTimeField(blank=True, null=True, verbose_name='PDF erstellt am')),
|
||||||
@@ -210,6 +229,7 @@ class Migration(migrations.Migration):
|
|||||||
('due_date', models.DateField(blank=True, null=True, verbose_name='Fälligkeitsdatum')),
|
('due_date', models.DateField(blank=True, null=True, verbose_name='Fälligkeitsdatum')),
|
||||||
('note', models.TextField(blank=True, verbose_name='Notiz')),
|
('note', models.TextField(blank=True, verbose_name='Notiz')),
|
||||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('is_small_business', models.BooleanField(default=False, verbose_name='Kleinunternehmer (§19 UStG)')),
|
||||||
('pdf_file', models.FileField(blank=True, null=True, upload_to='invoice_pdfs/', verbose_name='PDF')),
|
('pdf_file', models.FileField(blank=True, null=True, upload_to='invoice_pdfs/', verbose_name='PDF')),
|
||||||
('pdf_generated_at', models.DateTimeField(blank=True, null=True, verbose_name='PDF erstellt am')),
|
('pdf_generated_at', models.DateTimeField(blank=True, null=True, verbose_name='PDF erstellt am')),
|
||||||
('cancel_number', models.CharField(blank=True, max_length=50, null=True, unique=True, verbose_name='Stornorechnungsnummer')),
|
('cancel_number', models.CharField(blank=True, max_length=50, null=True, unique=True, verbose_name='Stornorechnungsnummer')),
|
||||||
@@ -230,7 +250,7 @@ class Migration(migrations.Migration):
|
|||||||
options={
|
options={
|
||||||
'verbose_name': 'Rechnung',
|
'verbose_name': 'Rechnung',
|
||||||
'verbose_name_plural': 'Rechnungen',
|
'verbose_name_plural': 'Rechnungen',
|
||||||
'ordering': ['-issue_date'],
|
'ordering': ['-invoice_number'],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# Generated by Django 6.0.6 on 2026-06-27 17:19
|
# Generated by Django 6.0.6 on 2026-06-28 10:19
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
@@ -13,6 +14,7 @@ class Migration(migrations.Migration):
|
|||||||
('invoices', '0001_initial'),
|
('invoices', '0001_initial'),
|
||||||
('projects', '0001_initial'),
|
('projects', '0001_initial'),
|
||||||
('time_tracking', '0001_initial'),
|
('time_tracking', '0001_initial'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
@@ -36,6 +38,36 @@ class Migration(migrations.Migration):
|
|||||||
name='time_entry',
|
name='time_entry',
|
||||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='time_tracking.timeentry', verbose_name='Zeiterfassung'),
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='time_tracking.timeentry', verbose_name='Zeiterfassung'),
|
||||||
),
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='invoicepayment',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='invoicepayment',
|
||||||
|
name='invoice',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payments', to='invoices.invoice'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='invoicepayment',
|
||||||
|
name='payment_method',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='finance.paymentmethod', verbose_name='Zahlungsmethode'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='invoicepaymentallocation',
|
||||||
|
name='payment',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='allocations', to='invoices.invoicepayment'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='item',
|
||||||
|
name='category',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='finance.category', verbose_name='Kategorie'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='item',
|
||||||
|
name='supplier',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='finance.supplier', verbose_name='Lieferant'),
|
||||||
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='invoiceitem',
|
model_name='invoiceitem',
|
||||||
name='item',
|
name='item',
|
||||||
@@ -61,6 +93,11 @@ class Migration(migrations.Migration):
|
|||||||
name='invoice',
|
name='invoice',
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='reminders', to='invoices.invoice', verbose_name='Rechnung'),
|
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='reminders', to='invoices.invoice', verbose_name='Rechnung'),
|
||||||
),
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='invoicepaymentallocation',
|
||||||
|
name='reminder',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='payment_allocations', to='invoices.reminder'),
|
||||||
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='item',
|
model_name='item',
|
||||||
name='unit',
|
name='unit',
|
||||||
|
|||||||
+261
-14
@@ -1,6 +1,12 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from core.models import CompanySetting
|
from core.models import CompanySetting
|
||||||
|
from django.db.models import Sum, F, DecimalField, ExpressionWrapper
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.utils import timezone
|
||||||
|
from decimal import Decimal
|
||||||
|
from django.core.validators import MinValueValidator
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
class Unit(models.Model):
|
class Unit(models.Model):
|
||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
@@ -92,15 +98,21 @@ class Item(models.Model):
|
|||||||
|
|
||||||
item_type = models.CharField(max_length=20, choices=ItemType.choices, default=ItemType.PRODUCT, verbose_name="Art")
|
item_type = models.CharField(max_length=20, choices=ItemType.choices, default=ItemType.PRODUCT, verbose_name="Art")
|
||||||
description = models.CharField(max_length=255, verbose_name="Beschreibung")
|
description = models.CharField(max_length=255, verbose_name="Beschreibung")
|
||||||
|
category = models.ForeignKey("finance.Category", on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Kategorie")
|
||||||
|
supplier = models.ForeignKey("finance.Supplier", on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Lieferant")
|
||||||
|
|
||||||
unit = models.ForeignKey(Unit,on_delete=models.PROTECT,related_name="item_items",verbose_name="Einheit")
|
unit = models.ForeignKey(Unit,on_delete=models.PROTECT,related_name="item_items",verbose_name="Einheit")
|
||||||
|
|
||||||
price_net = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="Preis Netto")
|
price_net = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="Preis Netto",validators=[MinValueValidator(0,message="Der Betrag darf nicht negativ sein.")])
|
||||||
tax_rate = models.DecimalField(
|
tax_rate = models.DecimalField(
|
||||||
max_digits=5,
|
max_digits=5,
|
||||||
decimal_places=2,
|
decimal_places=2,
|
||||||
default=19.00,
|
default=19.00,
|
||||||
verbose_name="MwSt. Satz (%)"
|
verbose_name="MwSt. Satz (%)",
|
||||||
|
validators=[MinValueValidator(0,message="Der Satz darf nicht negativ sein.")]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
price_buy = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="Einkaufspreis", null=True, blank=True,validators=[MinValueValidator(0,message="Der Betrag darf nicht negativ sein.")])
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -142,6 +154,8 @@ class Invoice(models.Model):
|
|||||||
note = models.TextField(blank=True, verbose_name="Notiz")
|
note = models.TextField(blank=True, verbose_name="Notiz")
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
is_small_business = models.BooleanField(default=False,verbose_name="Kleinunternehmer (§19 UStG)")
|
||||||
|
|
||||||
pdf_file = models.FileField(upload_to="invoice_pdfs/",null=True,blank=True,verbose_name="PDF")
|
pdf_file = models.FileField(upload_to="invoice_pdfs/",null=True,blank=True,verbose_name="PDF")
|
||||||
pdf_generated_at = models.DateTimeField(null=True,blank=True,verbose_name="PDF erstellt am")
|
pdf_generated_at = models.DateTimeField(null=True,blank=True,verbose_name="PDF erstellt am")
|
||||||
|
|
||||||
@@ -163,7 +177,7 @@ class Invoice(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Rechnung"
|
verbose_name = "Rechnung"
|
||||||
verbose_name_plural = "Rechnungen"
|
verbose_name_plural = "Rechnungen"
|
||||||
ordering = ["-issue_date"]
|
ordering = ["-invoice_number"]
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
|
|
||||||
@@ -201,9 +215,106 @@ class Invoice(models.Model):
|
|||||||
|
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
|
||||||
|
if self.issue_date and self.due_date:
|
||||||
|
if self.issue_date > self.due_date:
|
||||||
|
raise ValidationError(
|
||||||
|
{
|
||||||
|
"due_date": "Das Fälligkeitsdatum muss nach dem Rechnungsdatum liegen."
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.invoice_number
|
return self.invoice_number
|
||||||
|
|
||||||
|
@property
|
||||||
|
def total_net(self):
|
||||||
|
result = self.items.aggregate(
|
||||||
|
total=Sum(
|
||||||
|
ExpressionWrapper(
|
||||||
|
F("quantity") * F("unit_price_net"),
|
||||||
|
output_field=DecimalField()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return result["total"] or Decimal("0.00")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def total_tax(self):
|
||||||
|
total = Decimal("0.00")
|
||||||
|
|
||||||
|
for item in self.items.all():
|
||||||
|
|
||||||
|
item_net = item.calculated_total_net
|
||||||
|
|
||||||
|
total += (
|
||||||
|
item_net *
|
||||||
|
item.unit_tax_rate /
|
||||||
|
Decimal("100")
|
||||||
|
)
|
||||||
|
|
||||||
|
return total
|
||||||
|
|
||||||
|
@property
|
||||||
|
def total_amount(self):
|
||||||
|
if self.is_small_business:
|
||||||
|
return self.total_net
|
||||||
|
return self.total_net + self.total_tax
|
||||||
|
|
||||||
|
@property
|
||||||
|
def paid_amount(self):
|
||||||
|
return self.payments.aggregate(
|
||||||
|
total=Sum("amount")
|
||||||
|
)["total"] or Decimal("0.00")
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def paid_reminder_amount(self):
|
||||||
|
return self.reminders.aggregate(
|
||||||
|
total=Sum(
|
||||||
|
"payment_allocations__amount"
|
||||||
|
)
|
||||||
|
)["total"] or Decimal("0.00")
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def paid_invoice_amount(self):
|
||||||
|
return (
|
||||||
|
self.paid_amount
|
||||||
|
- self.paid_reminder_amount
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def reminder_amount(self):
|
||||||
|
return self.reminders.filter(
|
||||||
|
status__in=[
|
||||||
|
Reminder.Status.SENT,
|
||||||
|
Reminder.Status.EXPIRED,
|
||||||
|
Reminder.Status.PAID,
|
||||||
|
]
|
||||||
|
).aggregate(
|
||||||
|
total=Sum("fee")
|
||||||
|
)["total"] or Decimal("0.00")
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def total_with_reminders(self):
|
||||||
|
return (
|
||||||
|
self.total_amount
|
||||||
|
+ self.reminder_amount
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def open_amount(self):
|
||||||
|
return (
|
||||||
|
self.total_with_reminders
|
||||||
|
- self.paid_amount
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class InvoiceItem(models.Model):
|
class InvoiceItem(models.Model):
|
||||||
invoice = models.ForeignKey(Invoice,on_delete=models.CASCADE,related_name="items",verbose_name="Rechnung")
|
invoice = models.ForeignKey(Invoice,on_delete=models.CASCADE,related_name="items",verbose_name="Rechnung")
|
||||||
@@ -212,10 +323,10 @@ class InvoiceItem(models.Model):
|
|||||||
time_entry = models.ForeignKey("time_tracking.TimeEntry",on_delete=models.SET_NULL,null=True,blank=True,verbose_name="Zeiterfassung") # Quelle 2
|
time_entry = models.ForeignKey("time_tracking.TimeEntry",on_delete=models.SET_NULL,null=True,blank=True,verbose_name="Zeiterfassung") # Quelle 2
|
||||||
expense = models.ForeignKey("finance.Expense",on_delete=models.SET_NULL,null=True,blank=True,verbose_name="Ausgabe") # Quelle 3
|
expense = models.ForeignKey("finance.Expense",on_delete=models.SET_NULL,null=True,blank=True,verbose_name="Ausgabe") # Quelle 3
|
||||||
description = models.CharField(max_length=255,verbose_name="Beschreibung")
|
description = models.CharField(max_length=255,verbose_name="Beschreibung")
|
||||||
quantity = models.DecimalField(max_digits=10,decimal_places=2,default=1,verbose_name="Menge")
|
quantity = models.DecimalField(max_digits=10,decimal_places=2,default=1,verbose_name="Menge",validators=[MinValueValidator(0,message="Die Anzahl darf nicht negativ sein.")])
|
||||||
unit = models.CharField(max_length=50,verbose_name="Einheit")
|
unit = models.CharField(max_length=50,verbose_name="Einheit")
|
||||||
unit_price_net = models.DecimalField(max_digits=10,decimal_places=2,verbose_name="Einzelpreis Netto")
|
unit_price_net = models.DecimalField(max_digits=10,decimal_places=2,verbose_name="Einzelpreis Netto",validators=[MinValueValidator(0,message="Der Betrag darf nicht negativ sein.")])
|
||||||
unit_tax_rate = models.DecimalField(max_digits=5,decimal_places=2,default=19.00,verbose_name="MwSt. Satz (%)")
|
unit_tax_rate = models.DecimalField(max_digits=5,decimal_places=2,default=19.00,verbose_name="MwSt. Satz (%)",validators=[MinValueValidator(0,message="Der Satz darf nicht negativ sein.")])
|
||||||
service_period_from = models.DateField(null=True,blank=True,verbose_name="Leistungszeitraum von",)
|
service_period_from = models.DateField(null=True,blank=True,verbose_name="Leistungszeitraum von",)
|
||||||
service_period_to = models.DateField(null=True,blank=True,verbose_name="Leistungszeitraum bis",)
|
service_period_to = models.DateField(null=True,blank=True,verbose_name="Leistungszeitraum bis",)
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
@@ -229,7 +340,6 @@ class InvoiceItem(models.Model):
|
|||||||
verbose_name_plural = "Rechnungspositionen"
|
verbose_name_plural = "Rechnungspositionen"
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
|
||||||
sources = [
|
sources = [
|
||||||
self.item,
|
self.item,
|
||||||
self.time_entry,
|
self.time_entry,
|
||||||
@@ -239,9 +349,13 @@ class InvoiceItem(models.Model):
|
|||||||
selected = sum(1 for s in sources if s)
|
selected = sum(1 for s in sources if s)
|
||||||
|
|
||||||
if selected != 1:
|
if selected != 1:
|
||||||
raise ValidationError(
|
raise ValidationError("Genau eine Quelle muss ausgewählt werden.")
|
||||||
"Genau eine Quelle muss ausgewählt werden."
|
|
||||||
)
|
if self.service_period_from and self.service_period_to:
|
||||||
|
if self.service_period_from >= self.service_period_to:
|
||||||
|
raise ValidationError({
|
||||||
|
'service_period_to': 'Das Ende des Leistungszeitraums muss nach dem Beginn liegen.'
|
||||||
|
})
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.description
|
return self.description
|
||||||
@@ -272,7 +386,6 @@ class Reminder(models.Model):
|
|||||||
DRAFT = "draft", "Entwurf"
|
DRAFT = "draft", "Entwurf"
|
||||||
SENT = "sent", "Versendet"
|
SENT = "sent", "Versendet"
|
||||||
PAID = "paid", "Bezahlt"
|
PAID = "paid", "Bezahlt"
|
||||||
#PARTIALLY_PAID = "partial", "Teilweise bezahlt"
|
|
||||||
CANCELED = "canceled", "Storniert"
|
CANCELED = "canceled", "Storniert"
|
||||||
EXPIRED = "expired", "Frist abgelaufen"
|
EXPIRED = "expired", "Frist abgelaufen"
|
||||||
|
|
||||||
@@ -288,7 +401,7 @@ class Reminder(models.Model):
|
|||||||
level = models.PositiveSmallIntegerField(default=1,verbose_name="Mahnstufe",help_text="1 = Erste Mahnung, 2 = Zweite Mahnung, 3 = Letzte Mahnung",)
|
level = models.PositiveSmallIntegerField(default=1,verbose_name="Mahnstufe",help_text="1 = Erste Mahnung, 2 = Zweite Mahnung, 3 = Letzte Mahnung",)
|
||||||
issue_date = models.DateField(verbose_name="Mahndatum",)
|
issue_date = models.DateField(verbose_name="Mahndatum",)
|
||||||
due_date = models.DateField(null=True,blank=True,verbose_name="Neue Zahlungsfrist",)
|
due_date = models.DateField(null=True,blank=True,verbose_name="Neue Zahlungsfrist",)
|
||||||
fee = models.DecimalField(max_digits=10,decimal_places=2,default=0,verbose_name="Mahngebühr",)
|
fee = models.DecimalField(max_digits=10,decimal_places=2,default=0,verbose_name="Mahngebühr",validators=[MinValueValidator(0,message="Der Betrag darf nicht negativ sein.")])
|
||||||
note = models.TextField(blank=True,verbose_name="Hinweis",)
|
note = models.TextField(blank=True,verbose_name="Hinweis",)
|
||||||
pdf_file = models.FileField(upload_to="reminder_pdfs/",null=True,blank=True,verbose_name="PDF",)
|
pdf_file = models.FileField(upload_to="reminder_pdfs/",null=True,blank=True,verbose_name="PDF",)
|
||||||
pdf_generated_at = models.DateTimeField(null=True,blank=True,verbose_name="PDF erstellt am",)
|
pdf_generated_at = models.DateTimeField(null=True,blank=True,verbose_name="PDF erstellt am",)
|
||||||
@@ -302,6 +415,27 @@ class Reminder(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.reminder_number
|
return self.reminder_number
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
|
||||||
|
if self.issue_date and self.due_date:
|
||||||
|
if self.issue_date > self.due_date:
|
||||||
|
raise ValidationError(
|
||||||
|
{
|
||||||
|
"due_date": "Das Fälligkeitsdatum muss nach dem Mahnungsdatum liegen."
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def paid_amount(self):
|
||||||
|
return self.payment_allocations.aggregate(
|
||||||
|
total=Sum("amount")
|
||||||
|
)["total"] or Decimal("0.00")
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_paid(self):
|
||||||
|
return self.paid_amount >= self.fee
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -451,10 +585,9 @@ class Offer(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Angebot"
|
verbose_name = "Angebot"
|
||||||
verbose_name_plural = "Angebote"
|
verbose_name_plural = "Angebote"
|
||||||
ordering = ["-issue_date"]
|
ordering = ["-offer_number"]
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
|
|
||||||
if self.pk:
|
if self.pk:
|
||||||
|
|
||||||
old = Offer.objects.get(pk=self.pk)
|
old = Offer.objects.get(pk=self.pk)
|
||||||
@@ -485,6 +618,17 @@ class Offer(models.Model):
|
|||||||
|
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
|
||||||
|
if self.issue_date and self.due_date:
|
||||||
|
if self.issue_date > self.due_date:
|
||||||
|
raise ValidationError(
|
||||||
|
{
|
||||||
|
"due_date": "Das Gültigkeitsdatum muss nach dem Angebotsdatum liegen."
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.offer_number
|
return self.offer_number
|
||||||
|
|
||||||
@@ -515,6 +659,7 @@ class OfferItem(models.Model):
|
|||||||
decimal_places=2,
|
decimal_places=2,
|
||||||
default=1,
|
default=1,
|
||||||
verbose_name="Menge",
|
verbose_name="Menge",
|
||||||
|
validators=[MinValueValidator(0,message="Die Anzahl darf nicht negativ sein.")]
|
||||||
)
|
)
|
||||||
|
|
||||||
unit = models.CharField(
|
unit = models.CharField(
|
||||||
@@ -526,6 +671,7 @@ class OfferItem(models.Model):
|
|||||||
max_digits=10,
|
max_digits=10,
|
||||||
decimal_places=2,
|
decimal_places=2,
|
||||||
verbose_name="Einzelpreis Netto",
|
verbose_name="Einzelpreis Netto",
|
||||||
|
validators=[MinValueValidator(0,message="Der Betrag darf nicht negativ sein.")]
|
||||||
)
|
)
|
||||||
|
|
||||||
unit_tax_rate = models.DecimalField(
|
unit_tax_rate = models.DecimalField(
|
||||||
@@ -533,6 +679,7 @@ class OfferItem(models.Model):
|
|||||||
decimal_places=2,
|
decimal_places=2,
|
||||||
default=19.00,
|
default=19.00,
|
||||||
verbose_name="MwSt. Satz (%)",
|
verbose_name="MwSt. Satz (%)",
|
||||||
|
validators=[MinValueValidator(0,message="Der Satz darf nicht negativ sein.")]
|
||||||
)
|
)
|
||||||
|
|
||||||
created_at = models.DateTimeField(
|
created_at = models.DateTimeField(
|
||||||
@@ -593,3 +740,103 @@ class DocumentNumberSettings(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Dokumentnummer"
|
verbose_name = "Dokumentnummer"
|
||||||
verbose_name_plural = "Dokumentnummern"
|
verbose_name_plural = "Dokumentnummern"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class InvoicePayment(models.Model):
|
||||||
|
invoice = models.ForeignKey(
|
||||||
|
Invoice,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="payments"
|
||||||
|
)
|
||||||
|
|
||||||
|
amount = models.DecimalField(
|
||||||
|
max_digits=10,
|
||||||
|
decimal_places=2,
|
||||||
|
verbose_name="Betrag (€)",
|
||||||
|
validators=[MinValueValidator(0,message="Der Betrag darf nicht negativ sein.")]
|
||||||
|
)
|
||||||
|
|
||||||
|
date = models.DateTimeField(verbose_name="Datum")
|
||||||
|
|
||||||
|
payment_method = models.ForeignKey(
|
||||||
|
"finance.PaymentMethod",
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="Zahlungsmethode",
|
||||||
|
)
|
||||||
|
|
||||||
|
note = models.TextField(blank=True, verbose_name="Notiz")
|
||||||
|
|
||||||
|
created_by = models.ForeignKey(
|
||||||
|
User,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
created_at = models.DateTimeField(
|
||||||
|
auto_now_add=True
|
||||||
|
)
|
||||||
|
def clean(self):
|
||||||
|
if self.date and self.invoice_id:
|
||||||
|
if self.date.date() < self.invoice.issue_date:
|
||||||
|
raise ValidationError({
|
||||||
|
'date': 'Das Zahlungsdatum darf nicht vor dem Rechnungsdatum liegen.'
|
||||||
|
})
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
self.full_clean()
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def allocated_amount(self):
|
||||||
|
return self.allocations.aggregate(
|
||||||
|
total=Sum("amount")
|
||||||
|
)["total"] or 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def remaining_amount(self):
|
||||||
|
return self.amount - self.allocated_amount
|
||||||
|
|
||||||
|
|
||||||
|
class InvoicePaymentAllocation(models.Model):
|
||||||
|
|
||||||
|
payment = models.ForeignKey(
|
||||||
|
InvoicePayment,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="allocations"
|
||||||
|
)
|
||||||
|
|
||||||
|
reminder = models.ForeignKey(
|
||||||
|
Reminder,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="payment_allocations"
|
||||||
|
)
|
||||||
|
|
||||||
|
amount = models.DecimalField(
|
||||||
|
max_digits=10,
|
||||||
|
decimal_places=2
|
||||||
|
)
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
from django.utils import timezone
|
||||||
|
from .models import Invoice, Offer, Reminder
|
||||||
|
|
||||||
|
def mark_overdue():
|
||||||
|
today = timezone.now().date()
|
||||||
|
|
||||||
|
Invoice.objects.filter(
|
||||||
|
status="sent",
|
||||||
|
due_date__lt=today,
|
||||||
|
).update(status="overdue")
|
||||||
|
|
||||||
|
Offer.objects.filter(
|
||||||
|
status="sent",
|
||||||
|
due_date__lt=today,
|
||||||
|
).update(status="expired")
|
||||||
|
|
||||||
|
Reminder.objects.filter(
|
||||||
|
status="sent",
|
||||||
|
due_date__lt=today,
|
||||||
|
).update(status="expired")
|
||||||
+47
-38
@@ -1,8 +1,42 @@
|
|||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
from .models import Item, Customer, Reminder, Invoice, InvoiceItem, Offer, OfferItem
|
from .models import Item, Customer, Reminder, Invoice, InvoiceItem, Offer, OfferItem, InvoicePayment
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class InvoicePaymentTable(tables.Table):
|
||||||
|
invoice = tables.Column(
|
||||||
|
verbose_name="Rechnung"
|
||||||
|
)
|
||||||
|
amount = tables.Column(
|
||||||
|
verbose_name="Betrag"
|
||||||
|
)
|
||||||
|
date = tables.DateColumn(
|
||||||
|
format="d.m.Y",
|
||||||
|
verbose_name="Datum"
|
||||||
|
)
|
||||||
|
payment_method = tables.Column(
|
||||||
|
verbose_name="Zahlungsmethode"
|
||||||
|
)
|
||||||
|
created_by = tables.Column(
|
||||||
|
verbose_name="Erfasst von"
|
||||||
|
)
|
||||||
|
class Meta:
|
||||||
|
model = InvoicePayment
|
||||||
|
fields = [
|
||||||
|
"invoice",
|
||||||
|
"amount",
|
||||||
|
"date",
|
||||||
|
"payment_method",
|
||||||
|
"created_by",
|
||||||
|
]
|
||||||
|
attrs = {
|
||||||
|
"class": "w-full text-sm"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class OfferItemTable(tables.Table):
|
class OfferItemTable(tables.Table):
|
||||||
|
|
||||||
unit_price_net = tables.TemplateColumn(
|
unit_price_net = tables.TemplateColumn(
|
||||||
template_name="invoices/tables/offer_items/unit_price.html",
|
template_name="invoices/tables/offer_items/unit_price.html",
|
||||||
verbose_name="Einzelpreis",
|
verbose_name="Einzelpreis",
|
||||||
@@ -246,55 +280,30 @@ class ReminderTable(tables.Table):
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ItemTable(tables.Table):
|
class ItemTable(tables.Table):
|
||||||
item_type = tables.TemplateColumn(
|
toggle_prefix = "item"
|
||||||
template_name="invoices/tables/items/item_type.html",
|
|
||||||
verbose_name="Typ",
|
|
||||||
orderable=False
|
|
||||||
)
|
|
||||||
|
|
||||||
description = tables.TemplateColumn(
|
detail_template = (
|
||||||
template_name="invoices/tables/items/description.html",
|
"invoices/tables/items/detail.html"
|
||||||
verbose_name="Beschreibung",
|
|
||||||
orderable=False
|
|
||||||
)
|
)
|
||||||
|
|
||||||
unit = tables.TemplateColumn(
|
|
||||||
template_name="invoices/tables/items/unit.html",
|
|
||||||
verbose_name="Einheit",
|
|
||||||
orderable=False
|
|
||||||
)
|
|
||||||
|
|
||||||
price_net = tables.TemplateColumn(
|
|
||||||
template_name="invoices/tables/items/price_net.html",
|
|
||||||
verbose_name="Preis Netto",
|
|
||||||
orderable=False
|
|
||||||
)
|
|
||||||
|
|
||||||
tax_rate = tables.TemplateColumn(
|
|
||||||
template_name="invoices/tables/items/tax_rate.html",
|
|
||||||
verbose_name="MwSt. Satz (%)",
|
|
||||||
orderable=False
|
|
||||||
)
|
|
||||||
|
|
||||||
actions = tables.TemplateColumn(
|
|
||||||
template_name="invoices/tables/items/actions.html",
|
|
||||||
verbose_name="Aktion",
|
|
||||||
orderable=False
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Item
|
model = Item
|
||||||
|
|
||||||
|
template_name = (
|
||||||
|
"django_tables2/table_toggle.html"
|
||||||
|
)
|
||||||
|
|
||||||
fields = (
|
fields = (
|
||||||
"item_type",
|
"item_type",
|
||||||
"description",
|
"description",
|
||||||
|
"category",
|
||||||
"unit",
|
"unit",
|
||||||
"price_net",
|
"price_net",
|
||||||
"tax_rate",
|
"tax_rate",
|
||||||
"actions",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
attrs = {
|
attrs = {
|
||||||
"class": "w-full text-sm min-w-[700px]"
|
"class": "w-full text-sm min-w-[900px]",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ Kunde bearbeiten
|
|||||||
|
|
||||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||||
|
|
||||||
<h2 class="text-xl font-bold mb-2">Kundendaten</h2>
|
<h2 class="text-lg font-bold mb-2">Kundendaten</h2>
|
||||||
|
|
||||||
<div class="grid md:grid-cols-2 gap-4 mb-8">
|
<div class="grid md:grid-cols-2 gap-4 mb-8">
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ Kundenstammdaten verwalten
|
|||||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
|
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-xl font-bold">Aktionen</h2>
|
<h2 class="text-lg font-bold">Aktionen</h2>
|
||||||
<p class="text-slate-400 text-sm mt-1">
|
<p class="text-slate-400 text-sm mt-1">
|
||||||
Kunden anlegen
|
Kunden anlegen
|
||||||
</p>
|
</p>
|
||||||
@@ -36,7 +36,7 @@ Kundenstammdaten verwalten
|
|||||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors mb-8">
|
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors mb-8">
|
||||||
|
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<h2 class="text-xl font-bold">Kundenliste</h2>
|
<h2 class="text-lg font-bold">Kundenliste</h2>
|
||||||
<p class="text-slate-400 text-sm mt-1">Alle Kunden, inkl. Standorte und Ansprechpartner</p>
|
<p class="text-slate-400 text-sm mt-1">Alle Kunden, inkl. Standorte und Ansprechpartner</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ Kundenstammdaten verwalten
|
|||||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||||
|
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<h2 class="text-xl font-bold">Archivierte Kunden</h2>
|
<h2 class="text-lg font-bold">Archivierte Kunden</h2>
|
||||||
<p class="text-slate-400 text-sm mt-1">Alle inaktiven Kunden, inkl. Standorte und Ansprechpartner</p>
|
<p class="text-slate-400 text-sm mt-1">Alle inaktiven Kunden, inkl. Standorte und Ansprechpartner</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,249 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Rechnungswesen - Rechnungen{% endblock %}
|
||||||
|
|
||||||
|
{% block header_title %}
|
||||||
|
Zahlung erfassen
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block header_description %}
|
||||||
|
Zahlung für Rechnung {{ invoice.invoice_number }} hinzufügen
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
|
<!-- PAYMENT FORM -->
|
||||||
|
<div class="lg:col-span-2">
|
||||||
|
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<form method="post" enctype="multipart/form-data" class="space-y-4">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
{{ form.as_p }}
|
||||||
|
|
||||||
|
<button class="btn btn-save">
|
||||||
|
Speichern
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- PAYMENT SUMMARY -->
|
||||||
|
<div class="lg:col-span-1">
|
||||||
|
|
||||||
|
<div class="bg-white dark:bg-slate-800 rounded-xl shadow p-6 font-mono text-sm">
|
||||||
|
|
||||||
|
<!-- HEADER -->
|
||||||
|
<div class="text-center">
|
||||||
|
|
||||||
|
<h2 class="text-lg font-bold">
|
||||||
|
Zahlungsübersicht
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p class="text-xs text-slate-400 mt-1">
|
||||||
|
Rechnung {{ invoice.invoice_number }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="border-t border-dashed border-slate-300 dark:border-slate-600 my-5"></div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- RECHNUNG -->
|
||||||
|
<div class="space-y-2">
|
||||||
|
|
||||||
|
|
||||||
|
{% if not invoice.is_small_business %}
|
||||||
|
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-slate-400">
|
||||||
|
Netto
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
{{ invoice.total_net|floatformat:2 }} €
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-slate-400">
|
||||||
|
Umsatzsteuer
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
{{ invoice.total_tax|floatformat:2 }} €
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
<div class="flex justify-between font-semibold">
|
||||||
|
<span>
|
||||||
|
Rechnungsbetrag
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
{{ invoice.total_amount|floatformat:2 }} €
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- MAHNUNGEN -->
|
||||||
|
{% if invoice.reminder_amount > 0 %}
|
||||||
|
|
||||||
|
<div class="border-t border-dashed border-slate-300 dark:border-slate-600 my-5"></div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
|
||||||
|
<div class="flex justify-between text-red-600">
|
||||||
|
|
||||||
|
<span>
|
||||||
|
Mahngebühren
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
+ {{ invoice.reminder_amount|floatformat:2 }} €
|
||||||
|
</span>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{% for reminder in invoice.reminders.all %}
|
||||||
|
|
||||||
|
{% if reminder.status != "canceled" %}
|
||||||
|
|
||||||
|
<div class="flex justify-between text-xs text-slate-400">
|
||||||
|
|
||||||
|
<span>
|
||||||
|
{{ reminder.level }}. Mahnung
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
{{ reminder.fee|floatformat:2 }} €
|
||||||
|
</span>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- GESAMT -->
|
||||||
|
<div class="border-t border-dashed border-slate-300 dark:border-slate-600 my-5"></div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="flex justify-between text-lg font-bold">
|
||||||
|
|
||||||
|
<span>
|
||||||
|
Gesamtforderung
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
{{ invoice.total_with_reminders|floatformat:2 }} €
|
||||||
|
</span>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- ZAHLUNGEN -->
|
||||||
|
<div class="mt-4 space-y-2">
|
||||||
|
|
||||||
|
|
||||||
|
<div class="flex justify-between text-green-600">
|
||||||
|
|
||||||
|
<span>
|
||||||
|
Bereits bezahlt
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
- {{ invoice.paid_amount|floatformat:2 }} €
|
||||||
|
</span>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{% for payment in invoice.payments.all %}
|
||||||
|
|
||||||
|
<div class="flex justify-between text-xs text-slate-400">
|
||||||
|
|
||||||
|
<span>
|
||||||
|
Zahlung {{ payment.date|date:"d.m.Y" }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
{{ payment.amount|floatformat:2 }} €
|
||||||
|
</span>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- OFFEN -->
|
||||||
|
<div class="border-t border-dashed border-slate-300 dark:border-slate-600 my-5"></div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="bg-slate-100 dark:bg-slate-700 rounded-xl p-4 text-center">
|
||||||
|
|
||||||
|
<p class="text-xs uppercase text-slate-400">
|
||||||
|
Offener Betrag
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
<p class="text-3xl font-bold text-red-600 mt-1">
|
||||||
|
|
||||||
|
{{ invoice.open_amount|floatformat:2 }} €
|
||||||
|
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- FOOTER -->
|
||||||
|
<div class="border-t border-dashed border-slate-300 dark:border-slate-600 my-5"></div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="text-center text-xs text-slate-400">
|
||||||
|
|
||||||
|
Zahlungserfassung
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
@@ -16,7 +16,7 @@ Details & Positionen
|
|||||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
|
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-xl font-bold">Aktionen</h2>
|
<h2 class="text-lg font-bold">Aktionen</h2>
|
||||||
<p class="text-slate-400 text-sm mt-1">
|
<p class="text-slate-400 text-sm mt-1">
|
||||||
Positionen zur Rechnung hinzufügen
|
Positionen zur Rechnung hinzufügen
|
||||||
</p>
|
</p>
|
||||||
@@ -32,7 +32,6 @@ Details & Positionen
|
|||||||
<a href="{% url 'invoices:invoices_download' invoice.id %}" class="btn btn-action">Download</a>
|
<a href="{% url 'invoices:invoices_download' invoice.id %}" class="btn btn-action">Download</a>
|
||||||
<a href="{% url 'invoices:invoices_cancel' invoice.id %}" onclick="startDownloadAndRedirect()" class="btn btn-delete">Stornieren</a>
|
<a href="{% url 'invoices:invoices_cancel' invoice.id %}" onclick="startDownloadAndRedirect()" class="btn btn-delete">Stornieren</a>
|
||||||
<a href="{% url 'invoices:invoices_paid' invoice.id %}" class="btn btn-action">Zahlung verbuchen</a>
|
<a href="{% url 'invoices:invoices_paid' invoice.id %}" class="btn btn-action">Zahlung verbuchen</a>
|
||||||
<a href="{% url 'invoices:invoices_overdue' invoice.id %}" class="btn btn-action">Überfällig</a>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if invoice.status == "paid" %}
|
{% if invoice.status == "paid" %}
|
||||||
<a href="{% url 'invoices:invoices_download' invoice.id %}" class="btn btn-action">Download</a>
|
<a href="{% url 'invoices:invoices_download' invoice.id %}" class="btn btn-action">Download</a>
|
||||||
@@ -44,6 +43,8 @@ Details & Positionen
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% if invoice.status == "overdue" %}
|
{% if invoice.status == "overdue" %}
|
||||||
<a href="{% url 'invoices:invoices_download' invoice.id %}" class="btn btn-action">Download</a>
|
<a href="{% url 'invoices:invoices_download' invoice.id %}" class="btn btn-action">Download</a>
|
||||||
|
<a href="{% url 'invoices:invoices_cancel' invoice.id %}" onclick="startDownloadAndRedirect()" class="btn btn-delete">Stornieren</a>
|
||||||
|
<a href="{% url 'invoices:invoices_paid' invoice.id %}" class="btn btn-action">Zahlung verbuchen</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
|
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-xl font-bold">Aktionen</h2>
|
<h2 class="text-lg font-bold">Aktionen</h2>
|
||||||
<p class="text-slate-400 text-sm mt-1">Neue Rechnung erstellen</p>
|
<p class="text-slate-400 text-sm mt-1">Neue Rechnung erstellen</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<h2 class="text-xl font-bold">Rechnungen</h2>
|
<h2 class="text-lg font-bold">Rechnungen</h2>
|
||||||
<p class="text-slate-400 text-sm mt-1">Alle Rechnungen mit aktuellem Status</p>
|
<p class="text-slate-400 text-sm mt-1">Alle Rechnungen mit aktuellem Status</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="overflow-x-auto rounded-xl">
|
<div class="overflow-x-auto rounded-xl">
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ table {
|
|||||||
|
|
||||||
margin-top:30px;
|
margin-top:30px;
|
||||||
margin-left:auto;
|
margin-left:auto;
|
||||||
width:250px;
|
width: 300px;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,7 +178,7 @@ table {
|
|||||||
border-top:1px solid #cbd5e1;
|
border-top:1px solid #cbd5e1;
|
||||||
margin-top:10px;
|
margin-top:10px;
|
||||||
padding-top:10px;
|
padding-top:10px;
|
||||||
font-size:16px;
|
font-size:14px;
|
||||||
font-weight:bold;
|
font-weight:bold;
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -454,7 +454,7 @@ Preis
|
|||||||
</th>
|
</th>
|
||||||
|
|
||||||
|
|
||||||
{% if not company.is_small_business %}
|
{% if not invoice.is_small_business %}
|
||||||
|
|
||||||
<th class="left">
|
<th class="left">
|
||||||
Steuer
|
Steuer
|
||||||
@@ -518,7 +518,7 @@ Gesamt
|
|||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
|
||||||
{% if not company.is_small_business %}
|
{% if not invoice.is_small_business %}
|
||||||
|
|
||||||
<td class="left">
|
<td class="left">
|
||||||
{{ item.unit_tax_rate|floatformat:0 }} %
|
{{ item.unit_tax_rate|floatformat:0 }} %
|
||||||
@@ -569,7 +569,7 @@ Netto
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
{% if not company.is_small_business %}
|
{% if not invoice.is_small_business %}
|
||||||
{% for tax_rate,tax_amount in tax_summary.items %}
|
{% for tax_rate,tax_amount in tax_summary.items %}
|
||||||
|
|
||||||
<div class="total-row">
|
<div class="total-row">
|
||||||
@@ -603,7 +603,7 @@ Gesamt
|
|||||||
|
|
||||||
<span>
|
<span>
|
||||||
|
|
||||||
{% if company.is_small_business %}
|
{% if invoice.is_small_business %}
|
||||||
{{ total_net|floatformat:2 }}
|
{{ total_net|floatformat:2 }}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ total_gross|floatformat:2 }}
|
{{ total_gross|floatformat:2 }}
|
||||||
@@ -644,7 +644,7 @@ Rechnungshinweis
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
{% if company.is_small_business %}
|
{% if invoice.is_small_business %}
|
||||||
|
|
||||||
<div class="note">
|
<div class="note">
|
||||||
|
|
||||||
|
|||||||
@@ -155,10 +155,9 @@ table {
|
|||||||
|
|
||||||
|
|
||||||
.total-box {
|
.total-box {
|
||||||
|
|
||||||
margin-top:30px;
|
margin-top:30px;
|
||||||
margin-left:auto;
|
margin-left:auto;
|
||||||
width:250px;
|
width:300px;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,11 +173,10 @@ table {
|
|||||||
|
|
||||||
|
|
||||||
.total-final {
|
.total-final {
|
||||||
|
|
||||||
border-top:1px solid #cbd5e1;
|
border-top:1px solid #cbd5e1;
|
||||||
margin-top:10px;
|
margin-top:10px;
|
||||||
padding-top:10px;
|
padding-top:10px;
|
||||||
font-size:16px;
|
font-size:14px;
|
||||||
font-weight:bold;
|
font-weight:bold;
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -414,12 +412,11 @@ Stornodatum: {{ cancel_date|date:"d.m.Y" }}
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- POSITIONEN -->
|
<!-- POSITIONEN -->
|
||||||
|
|
||||||
<div class="items-wrapper">
|
<div class="items-wrapper">
|
||||||
<table class="items">
|
|
||||||
|
|
||||||
|
<table class="items">
|
||||||
|
|
||||||
<thead>
|
<thead>
|
||||||
|
|
||||||
@@ -429,37 +426,46 @@ Stornodatum: {{ cancel_date|date:"d.m.Y" }}
|
|||||||
Nr.
|
Nr.
|
||||||
</th>
|
</th>
|
||||||
|
|
||||||
|
|
||||||
<th>
|
<th>
|
||||||
Beschreibung
|
Beschreibung
|
||||||
</th>
|
</th>
|
||||||
|
|
||||||
{% if show_service_date %}
|
|
||||||
<th class="left">Leistungsdatum</th>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<th class="left">
|
{% if show_service_date %}
|
||||||
|
|
||||||
|
<th>
|
||||||
|
Leistungsdatum
|
||||||
|
</th>
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
<th class="right">
|
||||||
Menge
|
Menge
|
||||||
</th>
|
</th>
|
||||||
|
|
||||||
<th class="left">
|
|
||||||
|
<th>
|
||||||
Einheit
|
Einheit
|
||||||
</th>
|
</th>
|
||||||
|
|
||||||
<th class="left">
|
|
||||||
Preis
|
<th class="right">
|
||||||
|
Einzelpreis
|
||||||
</th>
|
</th>
|
||||||
|
|
||||||
|
|
||||||
{% if not company.is_small_business %}
|
{% if not invoice.is_small_business %}
|
||||||
|
|
||||||
<th class="left">
|
<th class="right">
|
||||||
Steuer
|
Steuer
|
||||||
</th>
|
</th>
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
<th class="left">
|
<th class="right">
|
||||||
Gesamt
|
Gesamt
|
||||||
</th>
|
</th>
|
||||||
|
|
||||||
@@ -469,7 +475,6 @@ Gesamt
|
|||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|
||||||
|
|
||||||
@@ -483,40 +488,52 @@ Gesamt
|
|||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
{{ item.description }}
|
Storno: {{ item.description }}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
|
||||||
{% if show_service_date %}
|
|
||||||
<td class="left">
|
|
||||||
{% if item.service_period_from %}
|
|
||||||
{{ item.service_period_from|date:"d.m.Y" }}
|
|
||||||
{% endif %}
|
|
||||||
{% if item.service_period_to %}
|
|
||||||
- {{ item.service_period_to|date:"d.m.Y" }}
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<td class="left">
|
{% if show_service_date %}
|
||||||
|
|
||||||
|
<td>
|
||||||
|
|
||||||
|
{% if item.service_period_from %}
|
||||||
|
{{ item.service_period_from|date:"d.m.Y" }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if item.service_period_to %}
|
||||||
|
- {{ item.service_period_to|date:"d.m.Y" }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<td class="right">
|
||||||
{{ item.quantity }}
|
{{ item.quantity }}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
|
||||||
<td class="left">
|
|
||||||
|
<td>
|
||||||
{{ item.unit }}
|
{{ item.unit }}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
|
||||||
<td class="left">
|
|
||||||
|
<td class="right">
|
||||||
-{{ item.unit_price_net|floatformat:2 }} €
|
-{{ item.unit_price_net|floatformat:2 }} €
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
|
||||||
{% if not company.is_small_business %}
|
|
||||||
|
|
||||||
<td class="left">
|
{% if not invoice.is_small_business %}
|
||||||
|
|
||||||
|
<td class="right">
|
||||||
{{ item.unit_tax_rate|floatformat:0 }} %
|
{{ item.unit_tax_rate|floatformat:0 }} %
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
@@ -524,7 +541,7 @@ Gesamt
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<td class="left">
|
<td class="right">
|
||||||
-{{ item.calculated_total_net|floatformat:2 }} €
|
-{{ item.calculated_total_net|floatformat:2 }} €
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
@@ -540,14 +557,28 @@ Gesamt
|
|||||||
|
|
||||||
|
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- SUMMEN -->
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- SUMMEN -->
|
||||||
|
|
||||||
<div class="total-box">
|
<div class="total-box">
|
||||||
|
|
||||||
|
|
||||||
@@ -565,7 +596,9 @@ Netto
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
{% if not company.is_small_business %}
|
|
||||||
|
{% if not invoice.is_small_business %}
|
||||||
|
|
||||||
{% for tax_rate,tax_amount in tax_summary.items %}
|
{% for tax_rate,tax_amount in tax_summary.items %}
|
||||||
|
|
||||||
<div class="total-row">
|
<div class="total-row">
|
||||||
@@ -574,32 +607,27 @@ Netto
|
|||||||
Umsatzsteuer {{ tax_rate|floatformat:0 }} %
|
Umsatzsteuer {{ tax_rate|floatformat:0 }} %
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
|
||||||
<span>
|
<span>
|
||||||
-{{ tax_amount|floatformat:2 }} €
|
-{{ tax_amount|floatformat:2 }} €
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="total-row total-final">
|
<div class="total-row total-final">
|
||||||
|
|
||||||
<span>
|
<span>
|
||||||
Gesamt
|
Rechnungsbetrag
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
|
||||||
<span>
|
<span>
|
||||||
|
|
||||||
{% if company.is_small_business %}
|
{% if invoice.is_small_business %}
|
||||||
-{{ total_net|floatformat:2 }}
|
-{{ total_net|floatformat:2 }}
|
||||||
{% else %}
|
{% else %}
|
||||||
-{{ total_gross|floatformat:2 }}
|
-{{ total_gross|floatformat:2 }}
|
||||||
@@ -609,12 +637,41 @@ Gesamt
|
|||||||
|
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="total-row">
|
||||||
|
|
||||||
|
<span>
|
||||||
|
Bereits bezahlt
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
-{{ paid_amount|floatformat:2 }} €
|
||||||
|
</span>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="total-row total-final">
|
||||||
|
|
||||||
|
<span>
|
||||||
|
Erstattungsbetrag
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
{{ refund_amount|floatformat:2 }} €
|
||||||
|
</span>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="note">
|
<div class="note">
|
||||||
|
|
||||||
<div class="receiver-title">
|
<div class="receiver-title">
|
||||||
@@ -623,37 +680,45 @@ Gesamt
|
|||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
Die Rechnung {{ invoice.invoice_number }}
|
|
||||||
wird durch diese Stornorechnung vollständig aufgehoben.
|
|
||||||
|
|
||||||
{% if original_status == "paid" %}
|
Die Rechnung
|
||||||
|
<strong>{{ invoice.invoice_number }}</strong>
|
||||||
|
vom {{ invoice.issue_date|date:"d.m.Y" }}
|
||||||
|
wird durch diese Stornorechnung aufgehoben.
|
||||||
|
|
||||||
|
|
||||||
|
{% if paid_amount > 0 %}
|
||||||
|
|
||||||
<br><br>
|
<br><br>
|
||||||
|
|
||||||
|
|
||||||
Die ursprüngliche Rechnung wurde bereits bezahlt.
|
Die ursprüngliche Rechnung wurde bereits bezahlt.
|
||||||
|
|
||||||
Es entsteht ein Guthaben in Höhe von
|
|
||||||
|
|
||||||
{% if company.is_small_business %}
|
Der zu erstattende Betrag beträgt:
|
||||||
{{ total_net|floatformat:2 }}
|
|
||||||
{% else %}
|
|
||||||
{{ total_gross|floatformat:2 }}
|
<strong>
|
||||||
{% endif %}
|
{{ refund_amount|floatformat:2 }} €
|
||||||
|
</strong>
|
||||||
|
|
||||||
|
|
||||||
|
<br><br>
|
||||||
|
|
||||||
€.
|
|
||||||
|
|
||||||
Die Rückerstattung erfolgt separat.
|
Die Rückerstattung erfolgt separat.
|
||||||
|
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{% if invoice.is_small_business %}
|
||||||
{% if company.is_small_business %}
|
|
||||||
|
|
||||||
<div class="note">
|
<div class="note">
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ Leistungen und Artikel verwalten
|
|||||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
|
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-xl font-bold">Aktionen</h2>
|
<h2 class="text-lg font-bold">Aktionen</h2>
|
||||||
<p class="text-slate-400 text-sm mt-1">
|
<p class="text-slate-400 text-sm mt-1">
|
||||||
Leistungen und Artikel anlegen
|
Leistungen und Artikel anlegen
|
||||||
</p>
|
</p>
|
||||||
@@ -38,7 +38,7 @@ Leistungen und Artikel verwalten
|
|||||||
<div class="flex flex-wrap justify-between items-center mb-6">
|
<div class="flex flex-wrap justify-between items-center mb-6">
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-xl font-bold">
|
<h2 class="text-lg font-bold">
|
||||||
Artikel und Leistungen
|
Artikel und Leistungen
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ Details & Positionen
|
|||||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
|
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-xl font-bold">Aktionen</h2>
|
<h2 class="text-lg font-bold">Aktionen</h2>
|
||||||
<p class="text-slate-400 text-sm mt-1">
|
<p class="text-slate-400 text-sm mt-1">
|
||||||
Positionen zum Angebot hinzufügen
|
Positionen zum Angebot hinzufügen
|
||||||
</p>
|
</p>
|
||||||
@@ -32,7 +32,6 @@ Details & Positionen
|
|||||||
<a href="{% url 'invoices:offers_download' offer.id %}" class="btn btn-action">Download</a>
|
<a href="{% url 'invoices:offers_download' offer.id %}" class="btn btn-action">Download</a>
|
||||||
<a href="{% url 'invoices:offers_accept' offer.id %}" class="btn btn-action">Akzeptiert</a>
|
<a href="{% url 'invoices:offers_accept' offer.id %}" class="btn btn-action">Akzeptiert</a>
|
||||||
<a href="{% url 'invoices:offers_reject' offer.id %}" class="btn btn-action">Abgelehnt</a>
|
<a href="{% url 'invoices:offers_reject' offer.id %}" class="btn btn-action">Abgelehnt</a>
|
||||||
<a href="{% url 'invoices:offers_expire' offer.id %}" class="btn btn-action">Ausgelaufen</a>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if offer.status == "accepted" or offer.status == "rejected" or offer.status == "expired" %}
|
{% if offer.status == "accepted" or offer.status == "rejected" or offer.status == "expired" %}
|
||||||
<a href="{% url 'invoices:offers_download' offer.id %}" class="btn btn-action">Download</a>
|
<a href="{% url 'invoices:offers_download' offer.id %}" class="btn btn-action">Download</a>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
|
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-xl font-bold">Aktionen</h2>
|
<h2 class="text-lg font-bold">Aktionen</h2>
|
||||||
<p class="text-slate-400 text-sm mt-1">Neues Angebot erstellen</p>
|
<p class="text-slate-400 text-sm mt-1">Neues Angebot erstellen</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<h2 class="text-xl font-bold">Angebote</h2>
|
<h2 class="text-lg font-bold">Angebote</h2>
|
||||||
<p class="text-slate-400 text-sm mt-1">Alle Angebote mit aktuellem Status</p>
|
<p class="text-slate-400 text-sm mt-1">Alle Angebote mit aktuellem Status</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="overflow-x-auto rounded-xl">
|
<div class="overflow-x-auto rounded-xl">
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ table {
|
|||||||
|
|
||||||
margin-top:30px;
|
margin-top:30px;
|
||||||
margin-left:auto;
|
margin-left:auto;
|
||||||
width:250px;
|
width: 300px;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,7 +178,7 @@ table {
|
|||||||
border-top:1px solid #cbd5e1;
|
border-top:1px solid #cbd5e1;
|
||||||
margin-top:10px;
|
margin-top:10px;
|
||||||
padding-top:10px;
|
padding-top:10px;
|
||||||
font-size:16px;
|
font-size:14px;
|
||||||
font-weight:bold;
|
font-weight:bold;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,44 +18,37 @@ Angebote, Rechnungen, Mahnungen verwalten
|
|||||||
<!-- MODULE GRID -->
|
<!-- MODULE GRID -->
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
|
||||||
<a href="{% url 'invoices:invoices_offers_overview' %}" class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
<a href="{% url 'invoices:invoices_offers_overview' %}" class="border border-slate-100 dark:border-slate-700 hover:border-[var(--company-color)]/40 hover:shadow-sm transition-all duration-200 p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||||
<div class="text-2xl mb-2">📄</div>
|
<div class="text-2xl mb-2">📄</div>
|
||||||
<h2 class="text-xl font-bold">Angebote</h2>
|
<h2 class="text-lg font-bold">Angebote</h2>
|
||||||
<p class="text-slate-400 text-sm mt-1">Angebote erstellen und verwalten</p>
|
<p class="text-slate-400 text-sm mt-1">Angebote erstellen und verwalten</p>
|
||||||
</a>
|
</a>
|
||||||
<a href="{% url 'invoices:invoices_invoices_overview' %}" class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
<a href="{% url 'invoices:invoices_invoices_overview' %}" class="border border-slate-100 dark:border-slate-700 hover:border-[var(--company-color)]/40 hover:shadow-sm transition-all duration-200 p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||||
<div class="text-2xl mb-2">🧾</div>
|
<div class="text-2xl mb-2">🧾</div>
|
||||||
<h2 class="text-xl font-bold">Rechnungen</h2>
|
<h2 class="text-lg font-bold">Rechnungen</h2>
|
||||||
<p class="text-slate-400 text-sm mt-1">Rechnungen erstellen und verwalten</p>
|
<p class="text-slate-400 text-sm mt-1">Rechnungen erstellen und verwalten</p>
|
||||||
</a>
|
</a>
|
||||||
<a href="{% url 'invoices:invoices_reminders_overview' %}" class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
<a href="{% url 'invoices:invoices_reminders_overview' %}" class="border border-slate-100 dark:border-slate-700 hover:border-[var(--company-color)]/40 hover:shadow-sm transition-all duration-200 p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||||
<div class="text-2xl mb-2">⚠️</div>
|
<div class="text-2xl mb-2">⚠️</div>
|
||||||
<h2 class="text-xl font-bold">Mahnungen</h2>
|
<h2 class="text-lg font-bold">Mahnungen</h2>
|
||||||
<p class="text-slate-400 text-sm mt-1">Offene Forderungen verfolgen</p>
|
<p class="text-slate-400 text-sm mt-1">Offene Forderungen verfolgen</p>
|
||||||
</a>
|
</a>
|
||||||
<a href="{% url 'invoices:customers_overview' %}" class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
<a href="{% url 'invoices:customers_overview' %}" class="border border-slate-100 dark:border-slate-700 hover:border-[var(--company-color)]/40 hover:shadow-sm transition-all duration-200 p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||||
<div class="text-2xl mb-2">👥</div>
|
<div class="text-2xl mb-2">👥</div>
|
||||||
<h2 class="text-xl font-bold">Kunden</h2>
|
<h2 class="text-lg font-bold">Kunden</h2>
|
||||||
<p class="text-slate-400 text-sm mt-1">Kundenstammdaten verwalten</p>
|
<p class="text-slate-400 text-sm mt-1">Kundenstammdaten verwalten</p>
|
||||||
</a>
|
</a>
|
||||||
<a href="{% url 'invoices:items_overview' %}" class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
<a href="{% url 'invoices:items_overview' %}" class="border border-slate-100 dark:border-slate-700 hover:border-[var(--company-color)]/40 hover:shadow-sm transition-all duration-200 p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||||
<div class="text-2xl mb-2">📦</div>
|
<div class="text-2xl mb-2">📦</div>
|
||||||
<h2 class="text-xl font-bold">Posten</h2>
|
<h2 class="text-lg font-bold">Posten</h2>
|
||||||
<p class="text-slate-400 text-sm mt-1">Leistungen und Artikel verwalten</p>
|
<p class="text-slate-400 text-sm mt-1">Leistungen und Artikel verwalten</p>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- OPTIONAL INFO BOX -->
|
|
||||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors mt-8">
|
|
||||||
|
|
||||||
<h2 class="text-xl font-bold">
|
|
||||||
Zusammenfassung
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<p class="text-slate-400 text-sm mt-1">
|
|
||||||
folgt
|
|
||||||
</p>
|
|
||||||
|
|
||||||
|
<a href="{% url 'invoices:invoices_payments_overview' %}" class="border border-slate-100 dark:border-slate-700 hover:border-[var(--company-color)]/40 hover:shadow-sm transition-all duration-200 p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||||
|
<div class="text-2xl mb-2">🏦</div>
|
||||||
|
<h2 class="text-lg font-bold">Zahlungen</h2>
|
||||||
|
<p class="text-slate-400 text-sm mt-1">Zahlungsvorgänge verfolgen</p>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Rechnungswesen - Zahlungen{% endblock %}
|
||||||
|
|
||||||
|
{% block header_title %}Zahlungen{% endblock %}
|
||||||
|
{% block header_description %}Zahlungsvorgänge verfolgen{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% load django_tables2 %}
|
||||||
|
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||||
|
<div class="mb-6">
|
||||||
|
<h2 class="text-lg font-bold">Zahlungen</h2>
|
||||||
|
<p class="text-slate-400 text-sm mt-1">Alle Zahlungsvorgänge</p>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-x-auto rounded-xl">
|
||||||
|
{% render_table table %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
@@ -15,7 +15,7 @@ Details
|
|||||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
|
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-xl font-bold">Aktionen</h2>
|
<h2 class="text-lg font-bold">Aktionen</h2>
|
||||||
<p class="text-slate-400 text-sm mt-1">
|
<p class="text-slate-400 text-sm mt-1">
|
||||||
Mahnung verwalten
|
Mahnung verwalten
|
||||||
</p>
|
</p>
|
||||||
@@ -28,8 +28,6 @@ Details
|
|||||||
{% if reminder.status == "sent" %}
|
{% if reminder.status == "sent" %}
|
||||||
<a href="{% url 'invoices:reminders_download' reminder.id %}" class="btn btn-action">Download</a>
|
<a href="{% url 'invoices:reminders_download' reminder.id %}" class="btn btn-action">Download</a>
|
||||||
<a href="{% url 'invoices:reminders_cancel' reminder.id %}" class="btn btn-delete">Stornieren</a>
|
<a href="{% url 'invoices:reminders_cancel' reminder.id %}" class="btn btn-delete">Stornieren</a>
|
||||||
<a href="{% url 'invoices:reminders_paid' reminder.id %}" class="btn btn-action">Zahlung verbuchen</a>
|
|
||||||
<a href="{% url 'invoices:reminders_expire' reminder.id %}" class="btn btn-action">Überfällig</a>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if reminder.status == "paid" or reminder.status == "canceled" or reminder.status == "expired" %}
|
{% if reminder.status == "paid" or reminder.status == "canceled" or reminder.status == "expired" %}
|
||||||
<a href="{% url 'invoices:reminders_download' reminder.id %}" class="btn btn-action">Download</a>
|
<a href="{% url 'invoices:reminders_download' reminder.id %}" class="btn btn-action">Download</a>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
|
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-xl font-bold">Aktionen</h2>
|
<h2 class="text-lg font-bold">Aktionen</h2>
|
||||||
<p class="text-slate-400 text-sm mt-1">Neue Mahnung erstellen</p>
|
<p class="text-slate-400 text-sm mt-1">Neue Mahnung erstellen</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<h2 class="text-xl font-bold">Mahnungen</h2>
|
<h2 class="text-lg font-bold">Mahnungen</h2>
|
||||||
<p class="text-slate-400 text-sm mt-1">Alle Mahnungen mit aktuellem Status</p>
|
<p class="text-slate-400 text-sm mt-1">Alle Mahnungen mit aktuellem Status</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="overflow-x-auto rounded-xl">
|
<div class="overflow-x-auto rounded-xl">
|
||||||
|
|||||||
@@ -149,16 +149,15 @@ table {
|
|||||||
|
|
||||||
|
|
||||||
.right {
|
.right {
|
||||||
text-align:right;
|
text-align: right !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.total-box {
|
.total-box {
|
||||||
|
|
||||||
margin-top:30px;
|
margin-top:30px;
|
||||||
margin-left:auto;
|
margin-left:auto;
|
||||||
width:250px;
|
width:300px;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,7 +177,7 @@ table {
|
|||||||
border-top:1px solid #cbd5e1;
|
border-top:1px solid #cbd5e1;
|
||||||
margin-top:10px;
|
margin-top:10px;
|
||||||
padding-top:10px;
|
padding-top:10px;
|
||||||
font-size:16px;
|
font-size:14px;
|
||||||
font-weight:bold;
|
font-weight:bold;
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -440,7 +439,7 @@ Betrag
|
|||||||
Rechnung {{ reminder.invoice.invoice_number }}
|
Rechnung {{ reminder.invoice.invoice_number }}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
<td class="right">
|
||||||
{{ invoice_amount|floatformat:2 }} €
|
{{ invoice_amount|floatformat:2 }} €
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
@@ -459,7 +458,7 @@ Rechnung {{ reminder.invoice.invoice_number }}
|
|||||||
({{ old_reminder.issue_date|date:"d.m.Y" }})
|
({{ old_reminder.issue_date|date:"d.m.Y" }})
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
<td class="right">
|
||||||
{{ old_reminder.fee|floatformat:2 }} €
|
{{ old_reminder.fee|floatformat:2 }} €
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
@@ -474,7 +473,7 @@ Rechnung {{ reminder.invoice.invoice_number }}
|
|||||||
Aktuelle Mahngebühr
|
Aktuelle Mahngebühr
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
<td class="right">
|
||||||
{{ reminder.fee|floatformat:2 }} €
|
{{ reminder.fee|floatformat:2 }} €
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
@@ -484,6 +483,32 @@ Rechnung {{ reminder.invoice.invoice_number }}
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{% if payments %}
|
||||||
|
|
||||||
|
{% for payment in payments %}
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
Zahlung vom {{ payment.date|date:"d.m.Y" }}
|
||||||
|
{% if payment.payment_method %}
|
||||||
|
({{ payment.payment_method }})
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="right">
|
||||||
|
- {{ payment.amount|floatformat:2 }} €
|
||||||
|
</td>
|
||||||
|
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
||||||
|
|
||||||
@@ -494,12 +519,20 @@ Rechnung {{ reminder.invoice.invoice_number }}
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- GESAMTFORDERUNG -->
|
<!-- GESAMTFORDERUNG -->
|
||||||
|
|
||||||
|
|
||||||
<div class="total-box">
|
<div class="total-box">
|
||||||
|
|
||||||
|
|
||||||
<div class="total-row">
|
<div class="total-row">
|
||||||
|
|
||||||
<span>
|
<span>
|
||||||
@@ -513,7 +546,6 @@ Offener Rechnungsbetrag
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{% if previous_reminders or reminder.fee > 0 %}
|
{% if previous_reminders or reminder.fee > 0 %}
|
||||||
|
|
||||||
<div class="total-row">
|
<div class="total-row">
|
||||||
@@ -531,23 +563,36 @@ Mahngebühren
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
<div class="total-row">
|
||||||
|
|
||||||
|
<span>
|
||||||
|
Bereits bezahlt
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
- {{ total_paid|floatformat:2 }} €
|
||||||
|
</span>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="total-row total-final">
|
<div class="total-row total-final">
|
||||||
|
|
||||||
<span>
|
<span>
|
||||||
Gesamtforderung
|
Gesamt
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
|
||||||
<span>
|
<span>
|
||||||
{{ total_due|floatformat:2 }} €
|
{{ open_amount|floatformat:2 }} €
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
<form
|
|
||||||
id="save-form-{{ record.id }}"
|
|
||||||
method="post"
|
|
||||||
action="{% url 'invoices:items_edit' record.id %}">
|
|
||||||
|
|
||||||
{% csrf_token %}
|
|
||||||
|
|
||||||
</form>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="flex gap-2">
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
form="save-form-{{ record.id }}"
|
|
||||||
class="btn btn-save">
|
|
||||||
Speichern
|
|
||||||
</button>
|
|
||||||
|
|
||||||
|
|
||||||
<form
|
|
||||||
method="post"
|
|
||||||
action="{% url 'invoices:items_delete' record.id %}">
|
|
||||||
|
|
||||||
{% csrf_token %}
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="btn btn-delete">
|
|
||||||
Löschen
|
|
||||||
</button>
|
|
||||||
|
|
||||||
</form>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="description"
|
|
||||||
form="save-form-{{ record.id }}"
|
|
||||||
value="{{ record.description }}"
|
|
||||||
class="border rounded px-2 py-1 w-full">
|
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
<form method="post" action="{% url 'invoices:items_edit' item.id %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<div class="grid md:grid-cols-2 xl:grid-cols-3 gap-4">
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm mb-1">
|
||||||
|
Typ
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<select
|
||||||
|
name="item_type"
|
||||||
|
class="border rounded px-3 py-2 w-full">
|
||||||
|
|
||||||
|
{% for value,label in item.ItemType.choices %}
|
||||||
|
<option
|
||||||
|
value="{{ value }}"
|
||||||
|
{% if item.item_type == value %}selected{% endif %}>
|
||||||
|
{{ label }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm mb-1">
|
||||||
|
Beschreibung
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="description"
|
||||||
|
value="{{ item.description }}"
|
||||||
|
class="border rounded px-3 py-2 w-full">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm mb-1">
|
||||||
|
Kategorie
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<select
|
||||||
|
name="category"
|
||||||
|
class="border rounded px-3 py-2 w-full">
|
||||||
|
|
||||||
|
<option value="">
|
||||||
|
---
|
||||||
|
</option>
|
||||||
|
|
||||||
|
{% for c in table.categories %}
|
||||||
|
<option
|
||||||
|
value="{{ c.id }}"
|
||||||
|
{% if item.category_id == c.id %}selected{% endif %}>
|
||||||
|
{{ c.name }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm mb-1">
|
||||||
|
Beschreibung
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="description"
|
||||||
|
value="{{ item.description }}"
|
||||||
|
class="border rounded px-3 py-2 w-full">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm mb-1">
|
||||||
|
Einheit
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<select
|
||||||
|
name="unit"
|
||||||
|
class="border rounded px-3 py-2 w-full">
|
||||||
|
|
||||||
|
<option value="">
|
||||||
|
---
|
||||||
|
</option>
|
||||||
|
|
||||||
|
{% for u in table.units %}
|
||||||
|
<option
|
||||||
|
value="{{ u.id }}"
|
||||||
|
{% if item.unit_id == u.id %}selected{% endif %}>
|
||||||
|
{{ u.name }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm mb-1">
|
||||||
|
Einzelpreis Netto
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
name="price_net"
|
||||||
|
value="{{ item.price_net }}"
|
||||||
|
class="border rounded px-3 py-2 w-full">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm mb-1">
|
||||||
|
MwSt. (%)
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
name="tax_rate"
|
||||||
|
value="{{ item.tax_rate }}"
|
||||||
|
class="border rounded px-3 py-2 w-full">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm mb-1">
|
||||||
|
Einkaufspreis
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
name="price_buy"
|
||||||
|
value="{{ item.price_buy }}"
|
||||||
|
class="border rounded px-3 py-2 w-full">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm mb-1">
|
||||||
|
Lieferant
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<select
|
||||||
|
name="supplier"
|
||||||
|
class="border rounded px-3 py-2 w-full">
|
||||||
|
|
||||||
|
<option value="">
|
||||||
|
---
|
||||||
|
</option>
|
||||||
|
|
||||||
|
{% for s in table.suppliers %}
|
||||||
|
<option
|
||||||
|
value="{{ s.id }}"
|
||||||
|
{% if item.supplier_id == s.id %}selected{% endif %}>
|
||||||
|
{{ s.name }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-5 flex gap-2">
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-save">
|
||||||
|
Speichern
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
formaction="{% url 'invoices:items_delete' item.id %}"
|
||||||
|
class="btn btn-delete">
|
||||||
|
Löschen
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<select
|
|
||||||
name="item_type"
|
|
||||||
form="save-form-{{ record.id }}"
|
|
||||||
class="border rounded px-2 py-1 w-full">
|
|
||||||
|
|
||||||
{% for v,l in table.types %}
|
|
||||||
|
|
||||||
<option
|
|
||||||
value="{{ v }}"
|
|
||||||
{% if record.item_type == v %}selected{% endif %}>
|
|
||||||
{{ l }}
|
|
||||||
</option>
|
|
||||||
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
</select>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<input
|
|
||||||
type="number"
|
|
||||||
step="0.01"
|
|
||||||
name="price_net"
|
|
||||||
form="save-form-{{ record.id }}"
|
|
||||||
value="{{ record.price_net }}"
|
|
||||||
class="border rounded px-2 py-1 w-full">
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<input
|
|
||||||
type="number"
|
|
||||||
step="0.01"
|
|
||||||
name="tax_rate"
|
|
||||||
form="save-form-{{ record.id }}"
|
|
||||||
value="{{ record.tax_rate }}"
|
|
||||||
class="border rounded px-2 py-1 w-full">
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
<select
|
|
||||||
name="unit"
|
|
||||||
form="save-form-{{ record.id }}"
|
|
||||||
class="border rounded px-2 py-1 w-full">
|
|
||||||
|
|
||||||
|
|
||||||
{% for unit in table.units %}
|
|
||||||
|
|
||||||
<option
|
|
||||||
value="{{ unit.id }}"
|
|
||||||
{% if record.unit_id == unit.id %}selected{% endif %}>
|
|
||||||
|
|
||||||
{{ unit.name }}
|
|
||||||
|
|
||||||
</option>
|
|
||||||
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
|
|
||||||
</select>
|
|
||||||
+9
-5
@@ -1,5 +1,5 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
from . import views
|
from . import views, views_info
|
||||||
|
|
||||||
app_name = "invoices"
|
app_name = "invoices"
|
||||||
|
|
||||||
@@ -37,7 +37,6 @@ urlpatterns = [
|
|||||||
path("invoices/<int:invoice_id>/download/", views.invoices_download, name="invoices_download"),
|
path("invoices/<int:invoice_id>/download/", views.invoices_download, name="invoices_download"),
|
||||||
path("invoices/<int:invoice_id>/cancel/", views.invoices_cancel, name="invoices_cancel"),
|
path("invoices/<int:invoice_id>/cancel/", views.invoices_cancel, name="invoices_cancel"),
|
||||||
path("invoices/<int:invoice_id>/paid/", views.invoices_paid, name="invoices_paid"),
|
path("invoices/<int:invoice_id>/paid/", views.invoices_paid, name="invoices_paid"),
|
||||||
path("invoices/<int:invoice_id>/overdue/", views.invoices_overdue, name="invoices_overdue"),
|
|
||||||
path("invoices/<int:invoice_id>/delete/", views.invoices_delete, name="invoices_delete"),
|
path("invoices/<int:invoice_id>/delete/", views.invoices_delete, name="invoices_delete"),
|
||||||
|
|
||||||
|
|
||||||
@@ -54,7 +53,6 @@ urlpatterns = [
|
|||||||
path("offers/<int:offer_id>/download/", views.offers_download, name="offers_download"),
|
path("offers/<int:offer_id>/download/", views.offers_download, name="offers_download"),
|
||||||
path("offers/<int:offer_id>/accept/", views.offers_accept, name="offers_accept"),
|
path("offers/<int:offer_id>/accept/", views.offers_accept, name="offers_accept"),
|
||||||
path("offers/<int:offer_id>/reject/", views.offers_reject, name="offers_reject"),
|
path("offers/<int:offer_id>/reject/", views.offers_reject, name="offers_reject"),
|
||||||
path("offers/<int:offer_id>/expire/", views.offers_expire, name="offers_expire"),
|
|
||||||
path("offers/<int:offer_id>/delete/", views.offers_delete, name="offers_delete"),
|
path("offers/<int:offer_id>/delete/", views.offers_delete, name="offers_delete"),
|
||||||
|
|
||||||
# Mahnungen
|
# Mahnungen
|
||||||
@@ -64,9 +62,9 @@ urlpatterns = [
|
|||||||
path("reminders/<int:reminder_id>/download/", views.reminders_download, name="reminders_download"),
|
path("reminders/<int:reminder_id>/download/", views.reminders_download, name="reminders_download"),
|
||||||
path("reminders/<int:reminder_id>/cancel/", views.reminders_cancel, name="reminders_cancel"),
|
path("reminders/<int:reminder_id>/cancel/", views.reminders_cancel, name="reminders_cancel"),
|
||||||
path("reminders/<int:reminder_id>/delete/", views.reminders_delete, name="reminders_delete"),
|
path("reminders/<int:reminder_id>/delete/", views.reminders_delete, name="reminders_delete"),
|
||||||
path("reminders/<int:reminder_id>/expire/", views.reminders_expire, name="reminders_expire"),
|
|
||||||
path("reminders/<int:reminder_id>/paid/", views.reminders_paid, name="reminders_paid"),
|
|
||||||
|
|
||||||
|
# Zahlungen
|
||||||
|
path("payments/", views.invoices_payments_overview, name="invoices_payments_overview"),
|
||||||
|
|
||||||
# AJAX
|
# AJAX
|
||||||
path("ajax/customer/<int:customer_id>/contacts/", views.ajax_contacts, name="ajax_contacts"),
|
path("ajax/customer/<int:customer_id>/contacts/", views.ajax_contacts, name="ajax_contacts"),
|
||||||
@@ -74,4 +72,10 @@ urlpatterns = [
|
|||||||
path("ajax/project/<int:project_id>/invoice-data/", views.ajax_invoicedata, name="ajax_invoicedata"),
|
path("ajax/project/<int:project_id>/invoice-data/", views.ajax_invoicedata, name="ajax_invoicedata"),
|
||||||
path("ajax/invoice/<int:invoice_id>/sources/", views.ajax_invoiceitemsources, name="ajax_invoice_item_sources"),
|
path("ajax/invoice/<int:invoice_id>/sources/", views.ajax_invoiceitemsources, name="ajax_invoice_item_sources"),
|
||||||
path("ajax/invoice-item-data/", views.ajax_invoice_item_data, name="ajax_invoice_item_data"),
|
path("ajax/invoice-item-data/", views.ajax_invoice_item_data, name="ajax_invoice_item_data"),
|
||||||
|
|
||||||
|
path("ajax/invoice-info/", views_info.ajax_invoice_info, name="ajax_invoice_info"),
|
||||||
|
path("ajax/offer-info/", views_info.ajax_offer_info, name="ajax_offer_info"),
|
||||||
|
path("ajax/reminder-info/",views_info.ajax_reminder_info,name="ajax_reminder_info"),
|
||||||
|
path("ajax/customer-info/",views_info.ajax_customer_info, name="ajax_customer_info"),
|
||||||
|
path("ajax/kpi-info/", views_info.ajax_kpi_info, name="ajax_kpi_info"),
|
||||||
]
|
]
|
||||||
+302
-155
@@ -1,8 +1,8 @@
|
|||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from accounts.decorators import permission_required
|
from accounts.decorators import permission_required
|
||||||
from django.shortcuts import render, redirect, get_object_or_404
|
from django.shortcuts import render, redirect, get_object_or_404
|
||||||
from .models import Customer, CustomerContact, CustomerLocation, Item, Invoice, InvoiceItem, Unit, Offer, OfferItem, Reminder
|
from .models import Customer, CustomerContact, CustomerLocation, Item, Invoice, InvoiceItem, Unit, Offer, OfferItem, Reminder, InvoicePaymentAllocation, InvoicePayment
|
||||||
from .forms import CustomerForm, ItemForm, CustomerInvoiceForm, ProjectInvoiceForm, InvoiceItemForm, OfferForm, OfferItemForm, ReminderForm
|
from .forms import CustomerForm, ItemForm, CustomerInvoiceForm, ProjectInvoiceForm, InvoiceItemForm, OfferForm, OfferItemForm, ReminderForm, InvoicePaymentForm
|
||||||
from datetime import date
|
from datetime import date
|
||||||
from projects.models import Project
|
from projects.models import Project
|
||||||
from django.http import JsonResponse, FileResponse
|
from django.http import JsonResponse, FileResponse
|
||||||
@@ -18,8 +18,9 @@ from django.utils import timezone
|
|||||||
from weasyprint import HTML
|
from weasyprint import HTML
|
||||||
import io
|
import io
|
||||||
from django.db.models import Prefetch, Sum
|
from django.db.models import Prefetch, Sum
|
||||||
from .tables import ItemTable, CustomerTable, CustomerArchivedTable, ReminderTable, InvoiceTable, InvoiceItemTable, OfferTable, OfferItemTable
|
from .tables import ItemTable, CustomerTable, CustomerArchivedTable, ReminderTable, InvoiceTable, InvoiceItemTable, OfferTable, OfferItemTable, InvoicePaymentTable
|
||||||
from django_tables2 import RequestConfig
|
from django_tables2 import RequestConfig
|
||||||
|
from finance.models import Category, Supplier
|
||||||
|
|
||||||
def generate_document_number(template, number):
|
def generate_document_number(template, number):
|
||||||
today = date.today()
|
today = date.today()
|
||||||
@@ -416,6 +417,8 @@ def items_overview(request):
|
|||||||
table = ItemTable(items)
|
table = ItemTable(items)
|
||||||
table.types = Item.ItemType.choices
|
table.types = Item.ItemType.choices
|
||||||
table.units = Unit.objects.order_by("name")
|
table.units = Unit.objects.order_by("name")
|
||||||
|
table.categories = Category.objects.order_by("name")
|
||||||
|
table.suppliers = Supplier.objects.order_by("name")
|
||||||
|
|
||||||
RequestConfig(
|
RequestConfig(
|
||||||
request,
|
request,
|
||||||
@@ -450,13 +453,14 @@ def items_add(request):
|
|||||||
@login_required
|
@login_required
|
||||||
@permission_required("invoices_items_edit")
|
@permission_required("invoices_items_edit")
|
||||||
def items_edit(request, item_id):
|
def items_edit(request, item_id):
|
||||||
|
|
||||||
if request.method != "POST":
|
if request.method != "POST":
|
||||||
|
print("Invalid request method for items_edit")
|
||||||
return redirect(
|
return redirect(
|
||||||
"invoices:items_overview"
|
"invoices:items_overview"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
item = get_object_or_404(
|
item = get_object_or_404(
|
||||||
Item,
|
Item,
|
||||||
id=item_id
|
id=item_id
|
||||||
@@ -467,7 +471,8 @@ def items_edit(request, item_id):
|
|||||||
request.POST,
|
request.POST,
|
||||||
instance=item
|
instance=item
|
||||||
)
|
)
|
||||||
|
if not form.is_valid():
|
||||||
|
print(form.errors.as_json())
|
||||||
|
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
form.save()
|
form.save()
|
||||||
@@ -751,11 +756,12 @@ def ajax_invoice_item_data(request):
|
|||||||
@login_required
|
@login_required
|
||||||
@permission_required("invoices_invoices_view")
|
@permission_required("invoices_invoices_view")
|
||||||
def invoices_invoices_overview(request):
|
def invoices_invoices_overview(request):
|
||||||
|
status = request.GET.get("status")
|
||||||
|
|
||||||
invoices = Invoice.objects.select_related(
|
invoices = Invoice.objects.select_related("customer", "project").all()
|
||||||
"customer",
|
|
||||||
"project"
|
if status:
|
||||||
).all()
|
invoices = invoices.filter(status=status)
|
||||||
|
|
||||||
table = InvoiceTable(invoices)
|
table = InvoiceTable(invoices)
|
||||||
|
|
||||||
@@ -1174,7 +1180,11 @@ def invoices_download(request, invoice_id):
|
|||||||
)
|
)
|
||||||
number_settings.invoice_next_number += 1
|
number_settings.invoice_next_number += 1
|
||||||
number_settings.save(update_fields=["invoice_next_number"])
|
number_settings.save(update_fields=["invoice_next_number"])
|
||||||
invoice.save(update_fields=["invoice_number"])
|
|
||||||
|
company = CompanySetting.objects.first()
|
||||||
|
invoice.is_small_business = company.is_small_business
|
||||||
|
|
||||||
|
invoice.save(update_fields=["invoice_number", "is_small_business"])
|
||||||
|
|
||||||
# ==========================
|
# ==========================
|
||||||
# PDF HTML
|
# PDF HTML
|
||||||
@@ -1290,40 +1300,87 @@ def invoices_cancel(request, invoice_id):
|
|||||||
id=invoice_id,
|
id=invoice_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
if invoice.status == "canceled" and invoice.pdf_canceled_file:
|
|
||||||
|
# ==========================
|
||||||
|
# Existierendes Storno zurückgeben
|
||||||
|
# ==========================
|
||||||
|
|
||||||
|
if invoice.status == Invoice.Status.CANCELED and invoice.pdf_canceled_file:
|
||||||
|
|
||||||
return FileResponse(
|
return FileResponse(
|
||||||
invoice.pdf_canceled_file.open("rb"),
|
invoice.pdf_canceled_file.open("rb"),
|
||||||
as_attachment=True,
|
as_attachment=True,
|
||||||
filename=invoice.pdf_canceled_file.name.split("/")[-1],
|
filename=invoice.pdf_canceled_file.name.split("/")[-1],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ==========================
|
||||||
|
# Nur finale Rechnungen stornierbar
|
||||||
|
# ==========================
|
||||||
|
|
||||||
|
if invoice.status not in [
|
||||||
|
Invoice.Status.SENT,
|
||||||
|
Invoice.Status.PAID,
|
||||||
|
Invoice.Status.OVERDUE,
|
||||||
|
]:
|
||||||
|
return redirect(
|
||||||
|
"invoices:invoices_detail",
|
||||||
|
invoice.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
company = CompanySetting.objects.first()
|
company = CompanySetting.objects.first()
|
||||||
number_settings = company.number_settings
|
number_settings = company.number_settings
|
||||||
|
|
||||||
|
|
||||||
|
# ==========================
|
||||||
|
# Stornonummer
|
||||||
|
# ==========================
|
||||||
|
|
||||||
invoice.cancel_number = generate_document_number(
|
invoice.cancel_number = generate_document_number(
|
||||||
number_settings.cancel_format,
|
number_settings.cancel_format,
|
||||||
number_settings.cancel_next_number,
|
number_settings.cancel_next_number,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
number_settings.cancel_next_number += 1
|
number_settings.cancel_next_number += 1
|
||||||
|
|
||||||
number_settings.save(
|
number_settings.save(
|
||||||
update_fields=[
|
update_fields=[
|
||||||
"cancel_next_number"
|
"cancel_next_number"
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
if invoice.status not in ["sent", "paid"]:
|
|
||||||
return redirect(
|
# ==========================
|
||||||
"invoices:invoices_detail",
|
# Positionen
|
||||||
invoice.id,
|
# ==========================
|
||||||
)
|
|
||||||
|
|
||||||
items = invoice.items.all()
|
items = invoice.items.all()
|
||||||
|
|
||||||
|
|
||||||
# ==========================
|
# ==========================
|
||||||
# Summen
|
# Bereits bezahlt
|
||||||
# ==========================
|
# ==========================
|
||||||
|
|
||||||
total_net = Decimal("0.00")
|
paid_amount = invoice.paid_amount
|
||||||
|
|
||||||
|
|
||||||
|
# ==========================
|
||||||
|
# Rückerstattung berechnen
|
||||||
|
# ==========================
|
||||||
|
|
||||||
|
refund_amount = min(
|
||||||
|
paid_amount,
|
||||||
|
invoice.total_amount
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
show_service_date = any(
|
||||||
|
item.service_period_from
|
||||||
|
for item in items
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
tax_summary = defaultdict(
|
tax_summary = defaultdict(
|
||||||
lambda: Decimal("0.00")
|
lambda: Decimal("0.00")
|
||||||
@@ -1331,42 +1388,19 @@ def invoices_cancel(request, invoice_id):
|
|||||||
|
|
||||||
for item in items:
|
for item in items:
|
||||||
|
|
||||||
item_net = Decimal(
|
item_net = item.calculated_total_net
|
||||||
str(item.calculated_total_net)
|
|
||||||
)
|
|
||||||
|
|
||||||
total_net += item_net
|
|
||||||
|
|
||||||
tax_rate = Decimal(
|
|
||||||
str(item.unit_tax_rate)
|
|
||||||
)
|
|
||||||
|
|
||||||
tax_amount = (
|
tax_amount = (
|
||||||
item_net *
|
item_net *
|
||||||
tax_rate /
|
item.unit_tax_rate /
|
||||||
Decimal("100")
|
Decimal("100")
|
||||||
)
|
)
|
||||||
|
|
||||||
tax_summary[tax_rate] += tax_amount
|
tax_summary[item.unit_tax_rate] += tax_amount
|
||||||
|
|
||||||
total_tax = sum(
|
# ==========================
|
||||||
tax_summary.values(),
|
# PDF HTML
|
||||||
Decimal("0.00")
|
# ==========================
|
||||||
)
|
|
||||||
|
|
||||||
total_gross = (
|
|
||||||
total_net +
|
|
||||||
total_tax
|
|
||||||
)
|
|
||||||
|
|
||||||
company = CompanySetting.objects.first()
|
|
||||||
|
|
||||||
original_status = invoice.status
|
|
||||||
|
|
||||||
show_service_date = any(
|
|
||||||
item.service_period_from
|
|
||||||
for item in items
|
|
||||||
)
|
|
||||||
|
|
||||||
html_string = render_to_string(
|
html_string = render_to_string(
|
||||||
"invoices/invoices/pdf_cancel.html",
|
"invoices/invoices/pdf_cancel.html",
|
||||||
@@ -1376,42 +1410,54 @@ def invoices_cancel(request, invoice_id):
|
|||||||
"company": company,
|
"company": company,
|
||||||
"cancel_date": timezone.now(),
|
"cancel_date": timezone.now(),
|
||||||
"cancel_number": invoice.cancel_number,
|
"cancel_number": invoice.cancel_number,
|
||||||
"original_status": original_status,
|
"paid_amount": paid_amount,
|
||||||
"total_net": total_net,
|
"refund_amount": refund_amount,
|
||||||
"tax_summary": dict(tax_summary),
|
"total_net": invoice.total_net,
|
||||||
"total_tax": total_tax,
|
"total_tax": invoice.total_tax,
|
||||||
"total_gross": total_gross,
|
"total_gross": invoice.total_amount,
|
||||||
"show_service_date": show_service_date,
|
"show_service_date": show_service_date,
|
||||||
|
"tax_summary": dict(tax_summary),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ==========================
|
||||||
|
# PDF erstellen
|
||||||
|
# ==========================
|
||||||
|
|
||||||
pdf_file = HTML(
|
pdf_file = HTML(
|
||||||
string=html_string,
|
string=html_string,
|
||||||
base_url=request.build_absolute_uri("/"),
|
base_url=request.build_absolute_uri("/"),
|
||||||
).write_pdf()
|
).write_pdf()
|
||||||
|
|
||||||
|
|
||||||
filename = (
|
filename = (
|
||||||
f"Stornorechnung-{invoice.cancel_number}.pdf"
|
f"Stornorechnung-{invoice.cancel_number}.pdf"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
invoice.pdf_canceled_file.save(
|
invoice.pdf_canceled_file.save(
|
||||||
filename,
|
filename,
|
||||||
ContentFile(pdf_file),
|
ContentFile(pdf_file),
|
||||||
save=False,
|
save=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
invoice.pdf_canceled_generated_at = timezone.now()
|
invoice.pdf_canceled_generated_at = timezone.now()
|
||||||
invoice.status = "canceled"
|
|
||||||
|
invoice.status = Invoice.Status.CANCELED
|
||||||
|
|
||||||
|
|
||||||
invoice.save(
|
invoice.save(
|
||||||
update_fields=[
|
update_fields=[
|
||||||
"pdf_canceled_file",
|
"pdf_canceled_file",
|
||||||
"pdf_canceled_generated_at",
|
"pdf_canceled_generated_at",
|
||||||
"status",
|
"status",
|
||||||
'cancel_number',
|
"cancel_number",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
return FileResponse(
|
return FileResponse(
|
||||||
io.BytesIO(pdf_file),
|
io.BytesIO(pdf_file),
|
||||||
as_attachment=True,
|
as_attachment=True,
|
||||||
@@ -1438,64 +1484,179 @@ def invoices_delete(request, invoice_id):
|
|||||||
return redirect("invoices:invoices_invoices_overview",)
|
return redirect("invoices:invoices_invoices_overview",)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def create_invoice_income(invoice):
|
||||||
|
from finance.models import Income
|
||||||
|
|
||||||
|
if Income.objects.filter(
|
||||||
|
invoice=invoice
|
||||||
|
).exists():
|
||||||
|
return
|
||||||
|
|
||||||
|
last_payment = invoice.payments.order_by(
|
||||||
|
"-date"
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not last_payment:
|
||||||
|
return
|
||||||
|
|
||||||
|
Income.objects.create(
|
||||||
|
invoice=invoice,
|
||||||
|
amount=invoice.paid_invoice_amount,
|
||||||
|
date=last_payment.date,
|
||||||
|
name=f"Zahlung Rechnung {invoice.invoice_number}",
|
||||||
|
project=invoice.project,
|
||||||
|
payment_method=last_payment.payment_method,
|
||||||
|
created_by=last_payment.created_by,
|
||||||
|
)
|
||||||
|
|
||||||
|
def allocate_invoice_payment(payment):
|
||||||
|
remaining = payment.amount
|
||||||
|
invoice = payment.invoice
|
||||||
|
reminders = invoice.reminders.filter(
|
||||||
|
status__in=[
|
||||||
|
Reminder.Status.SENT,
|
||||||
|
Reminder.Status.EXPIRED,
|
||||||
|
]
|
||||||
|
).order_by("level")
|
||||||
|
|
||||||
|
for reminder in reminders:
|
||||||
|
already_paid = reminder.payment_allocations.aggregate(
|
||||||
|
total=Sum("amount")
|
||||||
|
)["total"] or Decimal("0.00")
|
||||||
|
open_fee = reminder.fee - already_paid
|
||||||
|
if open_fee <= 0:
|
||||||
|
continue
|
||||||
|
allocation_amount = min(
|
||||||
|
remaining,
|
||||||
|
open_fee
|
||||||
|
)
|
||||||
|
if allocation_amount > 0:
|
||||||
|
InvoicePaymentAllocation.objects.create(
|
||||||
|
payment=payment,
|
||||||
|
reminder=reminder,
|
||||||
|
amount=allocation_amount
|
||||||
|
)
|
||||||
|
remaining -= allocation_amount
|
||||||
|
if remaining <= 0:
|
||||||
|
break
|
||||||
|
if remaining > 0:
|
||||||
|
InvoicePaymentAllocation.objects.create(
|
||||||
|
payment=payment,
|
||||||
|
reminder=None,
|
||||||
|
amount=remaining
|
||||||
|
)
|
||||||
|
|
||||||
|
def update_invoice_payment_status(invoice):
|
||||||
|
# Mahnungen prüfen
|
||||||
|
reminders = invoice.reminders.filter(
|
||||||
|
status__in=[
|
||||||
|
Reminder.Status.SENT,
|
||||||
|
Reminder.Status.EXPIRED,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
for reminder in reminders:
|
||||||
|
if reminder.paid_amount >= reminder.fee:
|
||||||
|
reminder.status = Reminder.Status.PAID
|
||||||
|
reminder.save(
|
||||||
|
update_fields=[
|
||||||
|
"status"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
if invoice.paid_invoice_amount >= invoice.total_amount:
|
||||||
|
if invoice.status != Invoice.Status.PAID:
|
||||||
|
invoice.status = Invoice.Status.PAID
|
||||||
|
invoice.save(
|
||||||
|
update_fields=[
|
||||||
|
"status"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
if invoice.project:
|
||||||
|
create_invoice_income(invoice)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@permission_required("invoices_invoices_invoices_paid")
|
@permission_required("invoices_invoices_invoices_paid")
|
||||||
def invoices_paid(request, invoice_id):
|
def invoices_paid(request, invoice_id):
|
||||||
|
|
||||||
invoice = get_object_or_404(
|
invoice = get_object_or_404(
|
||||||
Invoice,
|
Invoice,
|
||||||
id=invoice_id,
|
id=invoice_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
if invoice.status != "sent":
|
if invoice.status not in [
|
||||||
|
Invoice.Status.SENT,
|
||||||
|
Invoice.Status.OVERDUE,
|
||||||
|
]:
|
||||||
return redirect(
|
return redirect(
|
||||||
"invoices:invoices_detail",
|
"invoices:invoices_detail",
|
||||||
invoice_id=invoice.id,
|
invoice_id=invoice.id,
|
||||||
)
|
)
|
||||||
|
|
||||||
invoice.status = "paid"
|
if request.method == "POST":
|
||||||
|
form = InvoicePaymentForm(request.POST)
|
||||||
invoice.save(
|
if form.is_valid():
|
||||||
update_fields=[
|
payment = form.save(commit=False)
|
||||||
"status",
|
payment.invoice = invoice
|
||||||
]
|
payment.created_by = request.user
|
||||||
)
|
payment.save()
|
||||||
|
allocate_invoice_payment(payment)
|
||||||
|
update_invoice_payment_status(invoice)
|
||||||
return redirect(
|
return redirect(
|
||||||
"invoices:invoices_detail",
|
"invoices:invoices_detail",
|
||||||
invoice_id=invoice.id,
|
invoice_id=invoice.id,
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
print(form.errors)
|
||||||
@login_required
|
else:
|
||||||
@permission_required("invoices_invoices_invoices_overdue")
|
form = InvoicePaymentForm()
|
||||||
def invoices_overdue(request, invoice_id):
|
return render(
|
||||||
|
request,
|
||||||
invoice = get_object_or_404(
|
"invoices/invoices/add_payment.html",
|
||||||
Invoice,
|
{
|
||||||
id=invoice_id,
|
"form": form,
|
||||||
|
"invoice": invoice,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
today = timezone.now().date()
|
|
||||||
|
|
||||||
if invoice.status != "sent":
|
|
||||||
return redirect(
|
|
||||||
"invoices:invoices_detail",
|
|
||||||
invoice_id=invoice.id,
|
|
||||||
)
|
|
||||||
|
|
||||||
if invoice.due_date and invoice.due_date < today:
|
|
||||||
invoice.status = "overdue"
|
|
||||||
invoice.save(
|
|
||||||
update_fields=[
|
|
||||||
"status",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
return redirect(
|
|
||||||
"invoices:invoices_detail",
|
|
||||||
invoice_id=invoice.id,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1561,8 +1722,12 @@ def invoices_overdue(request, invoice_id):
|
|||||||
@login_required
|
@login_required
|
||||||
@permission_required("invoices_offers_view")
|
@permission_required("invoices_offers_view")
|
||||||
def invoices_offers_overview(request):
|
def invoices_offers_overview(request):
|
||||||
|
status = request.GET.get("status")
|
||||||
|
|
||||||
offers = Offer.objects.select_related("customer",).all()
|
offers = Offer.objects.select_related("customer",).all()
|
||||||
|
|
||||||
|
if status:
|
||||||
|
offers = offers.filter(status=status)
|
||||||
table = OfferTable(offers)
|
table = OfferTable(offers)
|
||||||
|
|
||||||
RequestConfig(
|
RequestConfig(
|
||||||
@@ -2059,33 +2224,6 @@ def offers_reject(request, offer_id):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@permission_required("invoices_offers_offers_status")
|
|
||||||
def offers_expire(request, offer_id):
|
|
||||||
|
|
||||||
offer = get_object_or_404(
|
|
||||||
Offer,
|
|
||||||
id=offer_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
if offer.status != "sent":
|
|
||||||
return redirect(
|
|
||||||
"invoices:offers_detail",
|
|
||||||
offer_id=offer.id,
|
|
||||||
)
|
|
||||||
|
|
||||||
offer.status = "expired"
|
|
||||||
|
|
||||||
offer.save(
|
|
||||||
update_fields=[
|
|
||||||
"status",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
return redirect(
|
|
||||||
"invoices:offers_detail",
|
|
||||||
offer_id=offer.id,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -2163,9 +2301,10 @@ def offers_expire(request, offer_id):
|
|||||||
@login_required
|
@login_required
|
||||||
@permission_required("invoices_reminders_view")
|
@permission_required("invoices_reminders_view")
|
||||||
def invoices_reminders_overview(request):
|
def invoices_reminders_overview(request):
|
||||||
|
status = request.GET.get("status")
|
||||||
reminders = Reminder.objects.select_related("invoice").all()
|
reminders = Reminder.objects.select_related("invoice").all()
|
||||||
|
if status:
|
||||||
|
reminders = reminders.filter(status=status)
|
||||||
table = ReminderTable(reminders)
|
table = ReminderTable(reminders)
|
||||||
|
|
||||||
RequestConfig(
|
RequestConfig(
|
||||||
@@ -2330,6 +2469,12 @@ def reminders_download(request, reminder_id):
|
|||||||
|
|
||||||
invoice = reminder.invoice
|
invoice = reminder.invoice
|
||||||
|
|
||||||
|
payments = invoice.payments.order_by("date", "created_at")
|
||||||
|
|
||||||
|
total_paid = sum(
|
||||||
|
payment.amount
|
||||||
|
for payment in payments
|
||||||
|
)
|
||||||
|
|
||||||
# ==========================
|
# ==========================
|
||||||
# Rechnungsbetrag berechnen
|
# Rechnungsbetrag berechnen
|
||||||
@@ -2382,6 +2527,7 @@ def reminders_download(request, reminder_id):
|
|||||||
+ reminder.fee
|
+ reminder.fee
|
||||||
)
|
)
|
||||||
|
|
||||||
|
open_amount = total_due - total_paid
|
||||||
total_fees = (total_previous_fees + reminder.fee)
|
total_fees = (total_previous_fees + reminder.fee)
|
||||||
|
|
||||||
company = CompanySetting.objects.first()
|
company = CompanySetting.objects.first()
|
||||||
@@ -2429,11 +2575,13 @@ def reminders_download(request, reminder_id):
|
|||||||
"invoice_amount": invoice_amount,
|
"invoice_amount": invoice_amount,
|
||||||
"previous_reminders": previous_reminders,
|
"previous_reminders": previous_reminders,
|
||||||
"total_previous_fees": total_previous_fees,
|
"total_previous_fees": total_previous_fees,
|
||||||
|
"total_fees": total_fees,
|
||||||
|
"payments": payments,
|
||||||
|
"total_paid": total_paid,
|
||||||
"total_due": total_due,
|
"total_due": total_due,
|
||||||
"total_fees": total_due,
|
"open_amount": open_amount,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# ==========================
|
# ==========================
|
||||||
# PDF erstellen
|
# PDF erstellen
|
||||||
# ==========================
|
# ==========================
|
||||||
@@ -2475,50 +2623,49 @@ def reminders_download(request, reminder_id):
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@permission_required("invoices_reminders_reminders_paid")
|
|
||||||
def reminders_paid(request, reminder_id):
|
|
||||||
reminder = get_object_or_404(
|
|
||||||
Reminder,
|
|
||||||
id=reminder_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
if reminder.status != "sent":
|
|
||||||
return redirect(
|
|
||||||
"invoices:reminders_detail",
|
|
||||||
reminder_id=reminder.id,
|
|
||||||
)
|
|
||||||
|
|
||||||
reminder.status = "paid"
|
|
||||||
reminder.save(update_fields=["status",])
|
|
||||||
|
|
||||||
reminder.invoice.status = "paid"
|
|
||||||
reminder.invoice.save(update_fields=["status",])
|
|
||||||
|
|
||||||
return redirect(
|
|
||||||
"invoices:reminders_detail",
|
|
||||||
reminder_id=reminder.id,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@permission_required("invoices_reminders_reminders_expire")
|
@permission_required("invoices_payments_view")
|
||||||
def reminders_expire(request, reminder_id):
|
def invoices_payments_overview(request):
|
||||||
reminder = get_object_or_404(
|
|
||||||
Reminder,
|
payments = InvoicePayment.objects.select_related(
|
||||||
id=reminder_id,
|
"invoice",
|
||||||
|
"invoice__customer",
|
||||||
|
"payment_method",
|
||||||
|
"created_by",
|
||||||
|
).order_by(
|
||||||
|
"-date"
|
||||||
)
|
)
|
||||||
|
|
||||||
if reminder.status != "sent":
|
|
||||||
return redirect(
|
table = InvoicePaymentTable(
|
||||||
"invoices:reminders_detail",
|
payments
|
||||||
reminder_id=reminder.id,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
reminder.status = "expired"
|
|
||||||
reminder.save(update_fields=["status",])
|
|
||||||
|
|
||||||
return redirect(
|
RequestConfig(
|
||||||
"invoices:reminders_detail",
|
request,
|
||||||
reminder_id=reminder.id,
|
paginate={
|
||||||
|
"per_page": 25
|
||||||
|
}
|
||||||
|
).configure(
|
||||||
|
table
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"invoices/payments/overview.html",
|
||||||
|
{
|
||||||
|
"table": table,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
from django.contrib.auth.decorators import login_required, permission_required
|
||||||
|
from django.db.models import Count, Q, Sum
|
||||||
|
from django.http import JsonResponse
|
||||||
|
from django.utils import timezone
|
||||||
|
from datetime import timedelta
|
||||||
|
from .models import Invoice, Offer, Reminder, Customer
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@permission_required("invoices_invoices_info")
|
||||||
|
def ajax_invoice_info(request):
|
||||||
|
now = timezone.now()
|
||||||
|
data = (
|
||||||
|
Invoice.objects
|
||||||
|
.filter(issue_date__year=now.year, issue_date__month=now.month)
|
||||||
|
.values("status")
|
||||||
|
.annotate(count=Count("id"))
|
||||||
|
)
|
||||||
|
return JsonResponse({item["status"]: item["count"] for item in data})
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@permission_required("invoices_offers_info")
|
||||||
|
def ajax_offer_info(request):
|
||||||
|
now = timezone.now()
|
||||||
|
data = (
|
||||||
|
Offer.objects
|
||||||
|
.filter(issue_date__year=now.year, issue_date__month=now.month)
|
||||||
|
.values("status")
|
||||||
|
.annotate(count=Count("id"))
|
||||||
|
)
|
||||||
|
return JsonResponse({item["status"]: item["count"] for item in data})
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@permission_required("invoices_reminders_info")
|
||||||
|
def ajax_reminder_info(request):
|
||||||
|
now = timezone.now()
|
||||||
|
data = (
|
||||||
|
Reminder.objects
|
||||||
|
.filter(issue_date__year=now.year, issue_date__month=now.month)
|
||||||
|
.values("status")
|
||||||
|
.annotate(count=Count("id"))
|
||||||
|
)
|
||||||
|
return JsonResponse({item["status"]: item["count"] for item in data})
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@permission_required("invoices_info")
|
||||||
|
def ajax_kpi_info(request):
|
||||||
|
today = timezone.now().date()
|
||||||
|
|
||||||
|
overdue_invoices = Invoice.objects.filter(status="overdue").count()
|
||||||
|
open_invoices = Invoice.objects.filter(status="sent").count()
|
||||||
|
open_offers = Offer.objects.filter(status="sent").count()
|
||||||
|
open_reminders = Reminder.objects.filter(status="sent").count()
|
||||||
|
|
||||||
|
overdue_amount = (
|
||||||
|
Invoice.objects
|
||||||
|
.filter(status="overdue")
|
||||||
|
.aggregate(total=Sum("items__unit_price_net"))["total"] or 0
|
||||||
|
)
|
||||||
|
|
||||||
|
return JsonResponse({
|
||||||
|
"overdue_invoices": overdue_invoices,
|
||||||
|
"open_invoices": open_invoices,
|
||||||
|
"open_offers": open_offers,
|
||||||
|
"open_reminders": open_reminders,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@permission_required("invoices_customers_info")
|
||||||
|
def ajax_customer_info(request):
|
||||||
|
start_of_week = timezone.now().date() - timedelta(days=timezone.now().weekday())
|
||||||
|
return JsonResponse({
|
||||||
|
"total": Customer.objects.count(),
|
||||||
|
"new_this_week": Customer.objects.filter(created_at__date__gte=start_of_week).count(),
|
||||||
|
"companies": Customer.objects.filter(customer_type="company").count(),
|
||||||
|
"private": Customer.objects.filter(customer_type="private").count(),
|
||||||
|
})
|
||||||
@@ -19,7 +19,7 @@ Einstellungen der App und Module bearbeiten
|
|||||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
|
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-xl font-bold">Aktionen</h2>
|
<h2 class="text-lg font-bold">Aktionen</h2>
|
||||||
<p class="text-slate-400 text-sm mt-1">Einträge hinzufügen</p>
|
<p class="text-slate-400 text-sm mt-1">Einträge hinzufügen</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ Einstellungen der App und Module bearbeiten
|
|||||||
|
|
||||||
|
|
||||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||||
<h2 class="text-xl font-bold">Einträge</h2>
|
<h2 class="text-lg font-bold">Einträge</h2>
|
||||||
{% for obj in objects %}
|
{% for obj in objects %}
|
||||||
<div class="flex justify-between border-b py-3">
|
<div class="flex justify-between border-b py-3">
|
||||||
<span>{{ obj }}</span>
|
<span>{{ obj }}</span>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ Einstellungen der App und Module bearbeiten
|
|||||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors mb-8">
|
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors mb-8">
|
||||||
|
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<h2 class="text-xl font-bold">
|
<h2 class="text-lg font-bold">
|
||||||
{{ group }}
|
{{ group }}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
@@ -34,9 +34,9 @@ Einstellungen der App und Module bearbeiten
|
|||||||
|
|
||||||
<a
|
<a
|
||||||
href="{% url 'masterdata:detail' entry.model_name %}"
|
href="{% url 'masterdata:detail' entry.model_name %}"
|
||||||
class="bg-slate-100 dark:bg-slate-700 rounded-xl p-6"
|
class="bg-slate-100 dark:bg-slate-700 rounded-xl p-6 border border-slate-100 dark:border-slate-700 hover:border-[var(--company-color)]/40 hover:shadow-sm transition-all duration-200"
|
||||||
>
|
>
|
||||||
<h2 class="text-xl font-bold">
|
<h2 class="text-lg font-bold">
|
||||||
{{ entry.name }}
|
{{ entry.name }}
|
||||||
</h2>
|
</h2>
|
||||||
<p class="text-sm mt-2">
|
<p class="text-sm mt-2">
|
||||||
@@ -56,4 +56,134 @@ Einstellungen der App und Module bearbeiten
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- System / Developer Panel -->
|
||||||
|
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors mb-8">
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between flex-wrap gap-3 mb-6">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="w-8 h-8 rounded-lg bg-indigo-100 dark:bg-indigo-900 flex items-center justify-center text-indigo-600 dark:text-indigo-300">
|
||||||
|
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path d="M8 9l3 3-3 3m5 0h3M3 12a9 9 0 1 0 18 0A9 9 0 0 0 3 12z"/></svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 class="text-lg font-bold">System</h2>
|
||||||
|
<p class="text-xs text-slate-400 mt-1">ProjectApp · Entwicklerinfo</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span id="status-badge" class="hidden items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Meta-Grid -->
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-2 mb-3" id="meta-grid" style="display:none">
|
||||||
|
<div class="bg-slate-100 dark:bg-slate-700 rounded-lg px-3 py-2">
|
||||||
|
<p class="text-xs text-slate-400 mb-1">Branch</p>
|
||||||
|
<p class="text-sm font-mono font-medium" id="meta-branch">—</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-slate-100 dark:bg-slate-700 rounded-lg px-3 py-2">
|
||||||
|
<p class="text-xs text-slate-400 mb-1">Lokaler commit</p>
|
||||||
|
<p class="text-sm font-mono font-medium" id="meta-local">—</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-slate-100 dark:bg-slate-700 rounded-lg px-3 py-2">
|
||||||
|
<p class="text-xs text-slate-400 mb-1">Letzter commit</p>
|
||||||
|
<p class="text-sm font-medium" id="meta-date">—</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-slate-100 dark:bg-slate-700 rounded-lg px-3 py-2">
|
||||||
|
<p class="text-xs text-slate-400 mb-1">Autor</p>
|
||||||
|
<p class="text-sm font-medium" id="meta-author">—</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Letzte Commit-Message -->
|
||||||
|
<div class="bg-slate-100 dark:bg-slate-700 rounded-lg px-3 py-2 mb-4 hidden" id="meta-msg-box">
|
||||||
|
<p class="text-xs text-slate-400 mb-1">Letzte Änderung</p>
|
||||||
|
<p class="text-xs font-medium" id="meta-msg">—</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-t border-slate-200 dark:border-slate-700 my-4"></div>
|
||||||
|
|
||||||
|
<!-- Aktionen -->
|
||||||
|
<div class="flex items-center gap-3 flex-wrap">
|
||||||
|
<button onclick="checkUpdate()" id="check-btn" class="btn btn-action flex items-center gap-2 text-sm">
|
||||||
|
<svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M4 4v6h6M20 20v-6h-6M4.93 15a9 9 0 1 0 .67-6.27"/></svg>
|
||||||
|
Auf Updates prüfen
|
||||||
|
</button>
|
||||||
|
<span class="text-xs text-slate-400">Vergleicht mit origin/main</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Changelog -->
|
||||||
|
<div id="changelog-box" class="hidden mt-4 pl-3 border-l-2 border-indigo-400">
|
||||||
|
<p class="text-xs text-slate-400 uppercase tracking-wider mb-2">Neue commits</p>
|
||||||
|
<div id="changelog-items" class="space-y-1"></div>
|
||||||
|
<button onclick="applyUpdate()" id="apply-btn" class="hidden btn btn-add mt-3 text-sm">
|
||||||
|
Update installieren
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
async function checkUpdate() {
|
||||||
|
const btn = document.getElementById('check-btn');
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.innerHTML = '<svg class="animate-spin" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M4 4v6h6M20 20v-6h-6M4.93 15a9 9 0 1 0 .67-6.27"/></svg> Prüfe…';
|
||||||
|
|
||||||
|
const res = await fetch("{% url 'masterdata:update_check' %}");
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
// Meta-Grid befüllen
|
||||||
|
document.getElementById('meta-branch').textContent = data.branch || '—';
|
||||||
|
document.getElementById('meta-local').textContent = data.local || '—';
|
||||||
|
document.getElementById('meta-date').textContent = data.last_commit_date || '—';
|
||||||
|
document.getElementById('meta-author').textContent = data.last_commit_author || '—';
|
||||||
|
document.getElementById('meta-msg').textContent = data.last_commit_msg || '—';
|
||||||
|
document.getElementById('meta-grid').style.display = 'grid';
|
||||||
|
document.getElementById('meta-msg-box').classList.remove('hidden');
|
||||||
|
|
||||||
|
// Badge
|
||||||
|
const badge = document.getElementById('status-badge');
|
||||||
|
badge.classList.remove('hidden');
|
||||||
|
badge.classList.add('inline-flex');
|
||||||
|
if (data.up_to_date) {
|
||||||
|
badge.className = 'inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300';
|
||||||
|
badge.innerHTML = '<svg width="10" height="10" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414L8.414 15 3.293 9.879a1 1 0 011.414-1.414L8.414 12.172l6.879-6.879a1 1 0 011.414 0z"/></svg> Aktuell';
|
||||||
|
document.getElementById('changelog-box').classList.add('hidden');
|
||||||
|
} else {
|
||||||
|
badge.className = 'inline-flex items-center gap-1 text-xs font-medium px-2.5 py-1 rounded-full bg-amber-100 text-amber-700 dark:bg-amber-900 dark:text-amber-300';
|
||||||
|
badge.innerHTML = `<svg width="10" height="10" fill="currentColor" viewBox="0 0 20 20"><path d="M10 2a8 8 0 100 16A8 8 0 0010 2zm0 4a1 1 0 011 1v3a1 1 0 01-2 0V7a1 1 0 011-1zm0 7a1 1 0 100 2 1 1 0 000-2z"/></svg> Update verfügbar → ${data.remote}`;
|
||||||
|
|
||||||
|
const items = document.getElementById('changelog-items');
|
||||||
|
items.innerHTML = data.changes.map(c => {
|
||||||
|
const [hash, ...rest] = c.split(' ');
|
||||||
|
return `<p class="text-xs font-mono"><span class="text-indigo-400">${hash}</span> <span class="text-slate-400">${rest.join(' ')}</span></p>`;
|
||||||
|
}).join('');
|
||||||
|
document.getElementById('apply-btn').classList.remove('hidden');
|
||||||
|
document.getElementById('changelog-box').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.innerHTML = '<svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M4 4v6h6M20 20v-6h-6M4.93 15a9 9 0 1 0 .67-6.27"/></svg> Erneut prüfen';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyUpdate() {
|
||||||
|
if (!confirm('Update jetzt installieren?')) return;
|
||||||
|
const res = await fetch("{% url 'masterdata:update_apply' %}", {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'X-CSRFToken': '{{ csrf_token }}' }
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.success) { location.reload(); }
|
||||||
|
else { alert('Fehler: ' + data.error); }
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
+28
-2
@@ -9,6 +9,8 @@ from core.forms.base import BaseForm
|
|||||||
import subprocess
|
import subprocess
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
def get_registry_entry(model_name):
|
def get_registry_entry(model_name):
|
||||||
entry = next(
|
entry = next(
|
||||||
@@ -121,6 +123,8 @@ def setting_delete(request, model_name, pk):
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -130,18 +134,32 @@ def _git(cmd):
|
|||||||
r = subprocess.run(cmd, capture_output=True, text=True, cwd=BASE_DIR)
|
r = subprocess.run(cmd, capture_output=True, text=True, cwd=BASE_DIR)
|
||||||
return r.stdout.strip(), r.returncode
|
return r.stdout.strip(), r.returncode
|
||||||
|
|
||||||
|
def _run(cmd):
|
||||||
|
r = subprocess.run(cmd, capture_output=True, text=True, cwd=BASE_DIR)
|
||||||
|
return r.stdout.strip(), r.returncode
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@permission_required("settings_update_check")
|
@permission_required("settings_update_check")
|
||||||
def update_check(request):
|
def update_check(request):
|
||||||
_git(["git", "fetch", "origin"])
|
_git(["git", "fetch", "origin"])
|
||||||
local, _ = _git(["git", "rev-parse", "HEAD"])
|
local, _ = _git(["git", "rev-parse", "HEAD"])
|
||||||
remote, _ = _git(["git", "rev-parse", "origin/main"])
|
remote, _ = _git(["git", "rev-parse", "origin/main"])
|
||||||
|
branch, _ = _git(["git", "rev-parse", "--abbrev-ref", "HEAD"])
|
||||||
log, _ = _git(["git", "log", f"{local}..{remote}", "--oneline"])
|
log, _ = _git(["git", "log", f"{local}..{remote}", "--oneline"])
|
||||||
|
date, _ = _git(["git", "log", "-1", "--format=%cd", "--date=format:%d.%m.%Y %H:%M"])
|
||||||
|
author, _ = _git(["git", "log", "-1", "--format=%an"])
|
||||||
|
msg, _ = _git(["git", "log", "-1", "--format=%s"])
|
||||||
|
|
||||||
return JsonResponse({
|
return JsonResponse({
|
||||||
"up_to_date": local == remote,
|
"up_to_date": local == remote,
|
||||||
"local": local[:7],
|
"local": local[:7],
|
||||||
"remote": remote[:7],
|
"remote": remote[:7],
|
||||||
|
"branch": branch,
|
||||||
"changes": log.splitlines() if log else [],
|
"changes": log.splitlines() if log else [],
|
||||||
|
"last_commit_date": date,
|
||||||
|
"last_commit_author": author,
|
||||||
|
"last_commit_msg": msg,
|
||||||
})
|
})
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@@ -149,9 +167,17 @@ def update_check(request):
|
|||||||
def update_apply(request):
|
def update_apply(request):
|
||||||
if request.method != "POST":
|
if request.method != "POST":
|
||||||
return JsonResponse({"error": "POST required"}, status=405)
|
return JsonResponse({"error": "POST required"}, status=405)
|
||||||
|
|
||||||
_, code = _git(["git", "pull", "origin", "main"])
|
_, code = _git(["git", "pull", "origin", "main"])
|
||||||
if code != 0:
|
if code != 0:
|
||||||
return JsonResponse({"error": "git pull fehlgeschlagen"}, status=500)
|
return JsonResponse({"error": "git pull fehlgeschlagen"}, status=500)
|
||||||
_git(["pip", "install", "-r", f"{BASE_DIR}/requirements.txt", "-q"])
|
|
||||||
_git(["python", "manage.py", "migrate", "--no-input"])
|
_, pip_code = _run([sys.executable, "-m", "pip", "install", "-r", str(BASE_DIR / "requirements.txt"), "-q"])
|
||||||
|
if pip_code != 0:
|
||||||
|
return JsonResponse({"error": "pip install fehlgeschlagen"}, status=500)
|
||||||
|
|
||||||
|
_, migrate_code = _run([sys.executable, "manage.py", "migrate", "--no-input"])
|
||||||
|
if migrate_code != 0:
|
||||||
|
return JsonResponse({"error": "Migration fehlgeschlagen"}, status=500)
|
||||||
|
|
||||||
return JsonResponse({"success": True})
|
return JsonResponse({"success": True})
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 6.0.6 on 2026-06-27 17:19
|
# Generated by Django 6.0.6 on 2026-06-28 10:19
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
import projects.models
|
import projects.models
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ Abgeschlossene und deaktivierte Projekte
|
|||||||
<a href="{% url 'projects:overview' m.project.id %}"
|
<a href="{% url 'projects:overview' m.project.id %}"
|
||||||
class="p-6 rounded-xl shadow hover:shadow-lg transition block text-white"
|
class="p-6 rounded-xl shadow hover:shadow-lg transition block text-white"
|
||||||
style="background: linear-gradient(135deg, {{ m.project.color }}, {{ m.project.color }}cc);">
|
style="background: linear-gradient(135deg, {{ m.project.color }}, {{ m.project.color }}cc);">
|
||||||
<h2 class="text-xl font-bold">
|
<h2 class="text-lg font-bold">
|
||||||
{{ m.project.name }}
|
{{ m.project.name }}
|
||||||
</h2>
|
</h2>
|
||||||
<p class="text-sm mt-2">
|
<p class="text-sm mt-2">
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ Standorte & Ansprechpartner
|
|||||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
|
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-xl font-bold">Aktionen</h2>
|
<h2 class="text-lg font-bold">Aktionen</h2>
|
||||||
<p class="text-slate-400 text-sm mt-1">Standorte und Ansprechpartner hinzufügen</p>
|
<p class="text-slate-400 text-sm mt-1">Standorte und Ansprechpartner hinzufügen</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ Standorte & Ansprechpartner
|
|||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||||
|
|
||||||
<h2 class="text-xl font-bold mb-2">Allgemein</h2>
|
<h2 class="text-lg font-bold mb-2">Allgemein</h2>
|
||||||
|
|
||||||
<form method="post" enctype="multipart/form-data" action="{% url 'projects:edit' project.id %}">
|
<form method="post" enctype="multipart/form-data" action="{% url 'projects:edit' project.id %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
@@ -78,7 +78,7 @@ Standorte & Ansprechpartner
|
|||||||
<!-- Mitarbeiter -->
|
<!-- Mitarbeiter -->
|
||||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||||
|
|
||||||
<h2 class="text-xl font-bold mb-3">
|
<h2 class="text-lg font-bold mb-2">
|
||||||
Mitarbeiter
|
Mitarbeiter
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
@@ -122,7 +122,7 @@ Standorte & Ansprechpartner
|
|||||||
<!-- Standorte -->
|
<!-- Standorte -->
|
||||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||||
|
|
||||||
<h2 class="text-xl font-bold mb-3">
|
<h2 class="text-lg font-bold mb-2">
|
||||||
Standorte
|
Standorte
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
@@ -181,7 +181,7 @@ Standorte & Ansprechpartner
|
|||||||
<!-- Ansprechpartner -->
|
<!-- Ansprechpartner -->
|
||||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||||
|
|
||||||
<h2 class="text-xl font-bold mb-3">
|
<h2 class="text-lg font-bold mb-2">
|
||||||
Ansprechpartner
|
Ansprechpartner
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
|
|||||||
@@ -19,62 +19,49 @@ Projektübersicht und Module
|
|||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
|
||||||
|
|
||||||
<!-- FINANZEN -->
|
<!-- FINANZEN -->
|
||||||
<a href="{% url 'finance:overview' project.id %}" class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
<a href="{% url 'finance:overview' project.id %}" class="border border-slate-100 dark:border-slate-700 hover:border-[var(--company-color)]/40 hover:shadow-sm transition-all duration-200 p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||||
<div class="text-2xl mb-2">💰</div>
|
<div class="text-2xl mb-2">💰</div>
|
||||||
<h2 class="text-xl font-bold">Finanzen</h2>
|
<h2 class="text-lg font-bold">Finanzen</h2>
|
||||||
<p class="text-slate-400 text-sm mt-1">Einnahmen & Ausgaben</p>
|
<p class="text-slate-400 text-sm mt-1">Einnahmen & Ausgaben</p>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<!-- KASSE -->
|
<!-- KASSE -->
|
||||||
<a href="" class="relative p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
<a href="" class="border border-slate-100 dark:border-slate-700 hover:border-[var(--company-color)]/40 hover:shadow-sm transition-all duration-200 relative p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||||
<div class="absolute top-3 right-3 text-xs font-semibold px-2 py-1 rounded-full text-white" style="background-color: {{ company_settings.color }};">
|
<div class="absolute top-3 right-3 text-xs font-semibold px-2 py-1 rounded-full text-white" style="background-color: {{ company_settings.color }};">
|
||||||
In Arbeit
|
In Arbeit
|
||||||
</div>
|
</div>
|
||||||
<div class="text-2xl mb-2">🏦</div>
|
<div class="text-2xl mb-2">🏦</div>
|
||||||
<h2 class="text-xl font-bold">Kasse</h2>
|
<h2 class="text-lg font-bold">Kasse</h2>
|
||||||
<p class="text-slate-400 text-sm mt-1">Buchungen vornehmen</p>
|
<p class="text-slate-400 text-sm mt-1">Buchungen vornehmen</p>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<!-- ZEITERFASSUNG -->
|
<!-- ZEITERFASSUNG -->
|
||||||
<a href="{% url 'time_tracking:overview' project.id %}" class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
<a href="{% url 'time_tracking:overview' project.id %}" class="border border-slate-100 dark:border-slate-700 hover:border-[var(--company-color)]/40 hover:shadow-sm transition-all duration-200 p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||||
<div class="text-2xl mb-2">⏱</div>
|
<div class="text-2xl mb-2">⏱</div>
|
||||||
<h2 class="text-xl font-bold">Zeiterfassung</h2>
|
<h2 class="text-lg font-bold">Zeiterfassung</h2>
|
||||||
<p class="text-slate-400 text-sm mt-1">Arbeitszeiten der Helfer</p>
|
<p class="text-slate-400 text-sm mt-1">Arbeitszeiten der Helfer</p>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<!-- DOKUMENTE -->
|
<!-- DOKUMENTE -->
|
||||||
<a href="{% url 'documents:overview' project.id %}" class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
<a href="{% url 'documents:overview' project.id %}" class="border border-slate-100 dark:border-slate-700 hover:border-[var(--company-color)]/40 hover:shadow-sm transition-all duration-200 p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||||
<div class="text-2xl mb-2">📁</div>
|
<div class="text-2xl mb-2">📁</div>
|
||||||
<h2 class="text-xl font-bold">Dokumente</h2>
|
<h2 class="text-lg font-bold">Dokumente</h2>
|
||||||
<p class="text-slate-400 text-sm mt-1">Dateien & Bilder</p>
|
<p class="text-slate-400 text-sm mt-1">Dateien & Bilder</p>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<!-- LISTE -->
|
<!-- LISTE -->
|
||||||
<a href="{% url 'inventory:overview' project.id %}" class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
<a href="{% url 'inventory:overview' project.id %}" class="border border-slate-100 dark:border-slate-700 hover:border-[var(--company-color)]/40 hover:shadow-sm transition-all duration-200 p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||||
<div class="text-2xl mb-2">🛒</div>
|
<div class="text-2xl mb-2">🛒</div>
|
||||||
<h2 class="text-xl font-bold">Pack-/Einkaufsliste</h2>
|
<h2 class="text-lg font-bold">Pack-/Einkaufsliste</h2>
|
||||||
<p class="text-slate-400 text-sm mt-1">Vorhandenen & benötigtes Material</p>
|
<p class="text-slate-400 text-sm mt-1">Vorhandenen & benötigtes Material</p>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<!-- Projektinformationen -->
|
<!-- Projektinformationen -->
|
||||||
<a href="{% url 'projects:info' project.id %}" class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
<a href="{% url 'projects:info' project.id %}" class="border border-slate-100 dark:border-slate-700 hover:border-[var(--company-color)]/40 hover:shadow-sm transition-all duration-200 p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||||
<div class="text-2xl mb-2">ℹ️</div>
|
<div class="text-2xl mb-2">ℹ️</div>
|
||||||
<h2 class="text-xl font-bold">Projektinformationen</h2>
|
<h2 class="text-lg font-bold">Projektinformationen</h2>
|
||||||
<p class="text-slate-400 text-sm mt-1">Standorte & Ansprechpartner</p>
|
<p class="text-slate-400 text-sm mt-1">Standorte & Ansprechpartner</p>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- OPTIONAL INFO BOX -->
|
|
||||||
<div class="p-6 rounded-xl shadow mb-8 bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
|
||||||
|
|
||||||
<h2 class="text-xl font-bold">
|
|
||||||
Zusammenfassung
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<p class="text-slate-400 text-sm mt-1">
|
|
||||||
folgt
|
|
||||||
</p>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
APScheduler==3.11.2
|
||||||
arrow==1.4.0
|
arrow==1.4.0
|
||||||
asgiref==3.11.1
|
asgiref==3.11.1
|
||||||
binaryornot==0.6.0
|
binaryornot==0.6.0
|
||||||
@@ -9,6 +10,7 @@ click==8.4.2
|
|||||||
cookiecutter==2.7.1
|
cookiecutter==2.7.1
|
||||||
cssselect2==0.9.0
|
cssselect2==0.9.0
|
||||||
Django==6.0.6
|
Django==6.0.6
|
||||||
|
django-apscheduler==0.7.0
|
||||||
django-browser-reload==1.21.0
|
django-browser-reload==1.21.0
|
||||||
django-filter==25.2
|
django-filter==25.2
|
||||||
django-tables2==3.0.0
|
django-tables2==3.0.0
|
||||||
@@ -40,6 +42,7 @@ text-unidecode==1.3
|
|||||||
tinycss2==1.5.1
|
tinycss2==1.5.1
|
||||||
tinyhtml5==2.1.0
|
tinyhtml5==2.1.0
|
||||||
tzdata==2026.2
|
tzdata==2026.2
|
||||||
|
tzlocal==5.4.3
|
||||||
urllib3==2.7.0
|
urllib3==2.7.0
|
||||||
weasyprint==69.0
|
weasyprint==69.0
|
||||||
webencodings==0.5.1
|
webencodings==0.5.1
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 262 KiB |
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -75,4 +75,17 @@
|
|||||||
@apply text-white;
|
@apply text-white;
|
||||||
background-color: #cc4d49;
|
background-color: #cc4d49;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
@apply text-black dark:text-white dark:bg-slate-800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hide-scrollbar {
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
-ms-overflow-style: none; /* IE/Edge */
|
||||||
|
}
|
||||||
|
|
||||||
|
.hide-scrollbar::-webkit-scrollbar {
|
||||||
|
display: none; /* Chrome/Safari */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 6.0.6 on 2026-06-27 17:19
|
# Generated by Django 6.0.6 on 2026-06-28 10:19
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|||||||
+11
-1
@@ -1,6 +1,6 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
@@ -71,6 +71,16 @@ class TimeEntry(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.user} - {self.start:%d.%m.%Y} - {self.duration_hours} Stunden"
|
return f"{self.user} - {self.start:%d.%m.%Y} - {self.duration_hours} Stunden"
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
if self.start and self.end and self.start >= self.end:
|
||||||
|
raise ValidationError({
|
||||||
|
'end': 'Das Ende muss nach dem Start liegen.'
|
||||||
|
})
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
self.full_clean()
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def duration_minutes(self):
|
def duration_minutes(self):
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ Zeiten erfassen und verwalten
|
|||||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
|
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-xl font-bold">
|
<h2 class="text-lg font-bold">
|
||||||
Aktionen
|
Aktionen
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ Zeiten erfassen und verwalten
|
|||||||
|
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
|
|
||||||
<h2 class="text-xl font-bold">
|
<h2 class="text-lg font-bold">
|
||||||
Zeiterfassungen
|
Zeiterfassungen
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user