From b304679e59704cabe8048fd15f7952534b56841e Mon Sep 17 00:00:00 2001 From: Felix Baumann Date: Sat, 27 Jun 2026 17:33:12 +0000 Subject: [PATCH] Deploy-Script, requirements, settings cleanup --- .env.example | 3 + accounts/migrations/0001_initial.py | 26 +- ...ole_invoices_invoices_invoices_download.py | 18 - ..._role_invoices_invoices_invoices_cancel.py | 18 - ...oices_invoices_invoices_cancel_download.py | 18 - ...oices_invoices_cancel_download_and_more.py | 22 - .../0006_role_invoices_offers_add_and_more.py | 53 - accounts/models.py | 18 +- change.py | 65 - config/settings.py | 61 +- core/migrations/0001_initial.py | 4 +- ...g_invoice_number_documentnumbersettings.py | 32 - .../0003_delete_documentnumbersettings.py | 16 - .../0004_companysetting_offer_header_text.py | 18 - ...05_remove_companysetting_invoice_number.py | 17 - ...006_companysetting_reminder_header_text.py | 18 - deploy.sh | 149 +++ documents/migrations/0001_initial.py | 2 +- documents/migrations/0002_initial.py | 2 +- finance/migrations/0001_initial.py | 2 +- finance/migrations/0002_initial.py | 2 +- inventory/migrations/0001_initial.py | 2 +- inventory/migrations/0002_initial.py | 2 +- invoices/migrations/0001_initial.py | 97 +- invoices/migrations/0002_initial.py | 22 +- ...0003_invoice_pdf_canceled_file_and_more.py | 23 - .../migrations/0004_documentnumbersettings.py | 28 - ...ntnumbersettings_cancel_prefix_and_more.py | 40 - ...documentnumbersettings_options_and_more.py | 54 - .../migrations/0007_invoice_cancel_number.py | 18 - invoices/migrations/0008_reminder.py | 36 - invoices/migrations/0009_offer_offeritem.py | 64 - ...vice_period_from_item_service_period_to.py | 23 - ...emove_item_service_period_from_and_more.py | 31 - .../migrations/0012_remove_offer_project.py | 17 - invoices/migrations/0013_reminder_status.py | 18 - .../migrations/0014_alter_invoice_status.py | 18 - ...entnumbersettings_offer_format_and_more.py | 23 - ...numbersettings_reminder_format_and_more.py | 33 - .../migrations/0017_alter_reminder_status.py | 18 - .../migrations/0018_alter_reminder_status.py | 18 - .../0019_alter_invoice_invoice_number.py | 18 - .../0020_alter_offer_offer_number.py | 18 - .../0021_alter_reminder_reminder_number.py | 18 - ...r_interest_rate_alter_reminder_interest.py | 23 - .../migrations/0023_reminder_interest_days.py | 18 - ...t_alter_reminder_interest_days_and_more.py | 28 - .../0025_remove_reminder_interest_and_more.py | 25 - projects/migrations/0001_initial.py | 2 +- requirements.txt | 46 + search.py | 38 - theme/static/css/dist/styles.css | 1188 +---------------- time_tracking/migrations/0001_initial.py | 2 +- 53 files changed, 377 insertions(+), 2196 deletions(-) create mode 100644 .env.example delete mode 100644 accounts/migrations/0002_role_invoices_invoices_invoices_download.py delete mode 100644 accounts/migrations/0003_role_invoices_invoices_invoices_cancel.py delete mode 100644 accounts/migrations/0004_role_invoices_invoices_invoices_cancel_download.py delete mode 100644 accounts/migrations/0005_remove_role_invoices_invoices_invoices_cancel_download_and_more.py delete mode 100644 accounts/migrations/0006_role_invoices_offers_add_and_more.py delete mode 100644 change.py delete mode 100644 core/migrations/0002_companysetting_invoice_number_documentnumbersettings.py delete mode 100644 core/migrations/0003_delete_documentnumbersettings.py delete mode 100644 core/migrations/0004_companysetting_offer_header_text.py delete mode 100644 core/migrations/0005_remove_companysetting_invoice_number.py delete mode 100644 core/migrations/0006_companysetting_reminder_header_text.py create mode 100755 deploy.sh delete mode 100644 invoices/migrations/0003_invoice_pdf_canceled_file_and_more.py delete mode 100644 invoices/migrations/0004_documentnumbersettings.py delete mode 100644 invoices/migrations/0005_remove_documentnumbersettings_cancel_prefix_and_more.py delete mode 100644 invoices/migrations/0006_alter_documentnumbersettings_options_and_more.py delete mode 100644 invoices/migrations/0007_invoice_cancel_number.py delete mode 100644 invoices/migrations/0008_reminder.py delete mode 100644 invoices/migrations/0009_offer_offeritem.py delete mode 100644 invoices/migrations/0010_item_service_period_from_item_service_period_to.py delete mode 100644 invoices/migrations/0011_remove_item_service_period_from_and_more.py delete mode 100644 invoices/migrations/0012_remove_offer_project.py delete mode 100644 invoices/migrations/0013_reminder_status.py delete mode 100644 invoices/migrations/0014_alter_invoice_status.py delete mode 100644 invoices/migrations/0015_alter_documentnumbersettings_offer_format_and_more.py delete mode 100644 invoices/migrations/0016_documentnumbersettings_reminder_format_and_more.py delete mode 100644 invoices/migrations/0017_alter_reminder_status.py delete mode 100644 invoices/migrations/0018_alter_reminder_status.py delete mode 100644 invoices/migrations/0019_alter_invoice_invoice_number.py delete mode 100644 invoices/migrations/0020_alter_offer_offer_number.py delete mode 100644 invoices/migrations/0021_alter_reminder_reminder_number.py delete mode 100644 invoices/migrations/0022_reminder_interest_rate_alter_reminder_interest.py delete mode 100644 invoices/migrations/0023_reminder_interest_days.py delete mode 100644 invoices/migrations/0024_alter_reminder_interest_alter_reminder_interest_days_and_more.py delete mode 100644 invoices/migrations/0025_remove_reminder_interest_and_more.py create mode 100644 requirements.txt delete mode 100644 search.py diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..bd9169e --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +SECRET_KEY= +DEBUG=False +ALLOWED_HOSTS=localhost diff --git a/accounts/migrations/0001_initial.py b/accounts/migrations/0001_initial.py index 2c3ef45..f1d119a 100644 --- a/accounts/migrations/0001_initial.py +++ b/accounts/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 6.0.6 on 2026-06-23 22:12 +# Generated by Django 6.0.6 on 2026-06-27 17:19 import django.db.models.deletion from django.conf import settings @@ -68,6 +68,28 @@ class Migration(migrations.Migration): ('invoices_invoices_invoices_item_add', models.BooleanField(default=False, help_text='Benutzer darf Positionen hinzufügen.', verbose_name='Rechnungsposition hinzufügen')), ('invoices_invoices_invoices_item_delete', models.BooleanField(default=False, help_text='Benutzer darf Positionen löschen.', verbose_name='Rechnungsposition löschen')), ('invoices_invoices_invoices_preview', models.BooleanField(default=False, help_text='Benutzer darf Vorschauen öffnen.', verbose_name='Rechnungsvorschau anzeigen')), + ('invoices_invoices_invoices_download', models.BooleanField(default=False, help_text='Benutzer darf Rechnungen herunterladen.', verbose_name='Rechnung herunterladen')), + ('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_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_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_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_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_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_reminders_view', models.BooleanField(default=False, help_text='Benutzer darf Mahnungen einsehen.', verbose_name='Mahnungen anzeigen')), + ('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_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_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_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_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')), @@ -80,6 +102,8 @@ class Migration(migrations.Migration): ('settings_detail_view', models.BooleanField(default=False, help_text='Benutzer darf Systemeinstellungen sehen.', verbose_name='Einstellungen anzeigen')), ('settings_add', models.BooleanField(default=False, help_text='Benutzer darf neue Einstellungen hinzufügen.', verbose_name='Einstellungen erstellen')), ('settings_delete', models.BooleanField(default=False, help_text='Benutzer darf Einstellungen entfernen.', verbose_name='Einstellungen löschen')), + ('settings_update_check', models.BooleanField(default=False, help_text='Benutzer darf nach Updates suchen.', verbose_name='Updates prüfen')), + ('settings_update_apply', models.BooleanField(default=False, help_text='Benutzer darf Updates installieren.', verbose_name='Updates installieren')), ], options={ 'verbose_name': 'Rolle', diff --git a/accounts/migrations/0002_role_invoices_invoices_invoices_download.py b/accounts/migrations/0002_role_invoices_invoices_invoices_download.py deleted file mode 100644 index d7edc16..0000000 --- a/accounts/migrations/0002_role_invoices_invoices_invoices_download.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 6.0.6 on 2026-06-23 23:44 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('accounts', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='role', - name='invoices_invoices_invoices_download', - field=models.BooleanField(default=False, help_text='Benutzer darf Rechnungen herunterladen.', verbose_name='Rechnung herunterladen'), - ), - ] diff --git a/accounts/migrations/0003_role_invoices_invoices_invoices_cancel.py b/accounts/migrations/0003_role_invoices_invoices_invoices_cancel.py deleted file mode 100644 index c90dee8..0000000 --- a/accounts/migrations/0003_role_invoices_invoices_invoices_cancel.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 6.0.6 on 2026-06-24 08:59 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('accounts', '0002_role_invoices_invoices_invoices_download'), - ] - - operations = [ - migrations.AddField( - model_name='role', - name='invoices_invoices_invoices_cancel', - field=models.BooleanField(default=False, help_text='Benutzer darf Rechnungen stornieren.', verbose_name='Rechnung stornieren'), - ), - ] diff --git a/accounts/migrations/0004_role_invoices_invoices_invoices_cancel_download.py b/accounts/migrations/0004_role_invoices_invoices_invoices_cancel_download.py deleted file mode 100644 index aadb661..0000000 --- a/accounts/migrations/0004_role_invoices_invoices_invoices_cancel_download.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 6.0.6 on 2026-06-24 09:13 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('accounts', '0003_role_invoices_invoices_invoices_cancel'), - ] - - operations = [ - migrations.AddField( - model_name='role', - name='invoices_invoices_invoices_cancel_download', - field=models.BooleanField(default=False, help_text='Benutzer darf Stornorechnungen herunterladen.', verbose_name='Stornorechnung Download'), - ), - ] diff --git a/accounts/migrations/0005_remove_role_invoices_invoices_invoices_cancel_download_and_more.py b/accounts/migrations/0005_remove_role_invoices_invoices_invoices_cancel_download_and_more.py deleted file mode 100644 index af5552b..0000000 --- a/accounts/migrations/0005_remove_role_invoices_invoices_invoices_cancel_download_and_more.py +++ /dev/null @@ -1,22 +0,0 @@ -# Generated by Django 6.0.6 on 2026-06-24 13:00 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('accounts', '0004_role_invoices_invoices_invoices_cancel_download'), - ] - - operations = [ - migrations.RemoveField( - model_name='role', - name='invoices_invoices_invoices_cancel_download', - ), - migrations.AddField( - model_name='role', - name='invoices_invoices_invoices_paid', - field=models.BooleanField(default=False, help_text='Benutzer darf Rechnungsstatus ändern.', verbose_name='Rechnung auf bezahlt setzen'), - ), - ] diff --git a/accounts/migrations/0006_role_invoices_offers_add_and_more.py b/accounts/migrations/0006_role_invoices_offers_add_and_more.py deleted file mode 100644 index 27e67d6..0000000 --- a/accounts/migrations/0006_role_invoices_offers_add_and_more.py +++ /dev/null @@ -1,53 +0,0 @@ -# Generated by Django 6.0.6 on 2026-06-25 16:29 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('accounts', '0005_remove_role_invoices_invoices_invoices_cancel_download_and_more'), - ] - - operations = [ - migrations.AddField( - model_name='role', - name='invoices_offers_add', - field=models.BooleanField(default=False, help_text='Benutzer darf Angebote für Kunden erstellen.', verbose_name='Angebot erstellen'), - ), - migrations.AddField( - model_name='role', - name='invoices_offers_offers_download', - field=models.BooleanField(default=False, help_text='Benutzer darf Angebote herunterladen.', verbose_name='Angebot herunterladen'), - ), - migrations.AddField( - model_name='role', - name='invoices_offers_offers_item_add', - field=models.BooleanField(default=False, help_text='Benutzer darf Positionen hinzufügen.', verbose_name='Angebotsposition hinzufügen'), - ), - migrations.AddField( - model_name='role', - name='invoices_offers_offers_item_delete', - field=models.BooleanField(default=False, help_text='Benutzer darf Positionen löschen.', verbose_name='Angebotsposition löschen'), - ), - migrations.AddField( - model_name='role', - name='invoices_offers_offers_preview', - field=models.BooleanField(default=False, help_text='Benutzer darf Vorschauen öffnen.', verbose_name='Angebotsvorschau anzeigen'), - ), - migrations.AddField( - model_name='role', - name='invoices_offers_offers_status', - field=models.BooleanField(default=False, help_text='Benutzer darf Angebotsstatus ändern.', verbose_name='Angebotsstatus bearbeiten'), - ), - migrations.AddField( - model_name='role', - name='invoices_offers_offers_view', - field=models.BooleanField(default=False, help_text='Benutzer darf Angebotsdetails sehen.', verbose_name='Angebotsdetails anzeigen'), - ), - migrations.AddField( - model_name='role', - name='invoices_offers_view', - field=models.BooleanField(default=False, help_text='Benutzer darf Angebote sehen.', verbose_name='Angebote anzeigen'), - ), - ] diff --git a/accounts/models.py b/accounts/models.py index 095f31c..20e42e9 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -81,7 +81,8 @@ class Role(models.Model): invoices_invoices_invoices_download = models.BooleanField(default=False, verbose_name="Rechnung herunterladen", help_text="Benutzer darf Rechnungen herunterladen.") 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_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_add = models.BooleanField(default=False, verbose_name="Angebot erstellen", help_text="Benutzer darf Angebote für Kunden erstellen.") @@ -91,9 +92,16 @@ class Role(models.Model): 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_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_reminders_view = models.BooleanField(default=False, verbose_name="Mahnungen anzeigen", help_text="Benutzer darf Mahnungen einsehen.") + 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_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_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.") # ===================== # Benutzerverwaltung @@ -115,9 +123,9 @@ class Role(models.Model): settings_view = models.BooleanField(default=False, verbose_name="Einstellungsübersicht anzeigen", help_text="Benutzer darf Einstellungskategorien sehen.") settings_detail_view = models.BooleanField(default=False, verbose_name="Einstellungen anzeigen", help_text="Benutzer darf Systemeinstellungen sehen.") settings_add = models.BooleanField(default=False, verbose_name="Einstellungen erstellen", help_text="Benutzer darf neue Einstellungen hinzufügen.") - #settings_edit = models.BooleanField(default=False, verbose_name="Einstellungen bearbeiten", help_text="Benutzer darf Einstellungen ändern.") settings_delete = models.BooleanField(default=False, verbose_name="Einstellungen löschen", help_text="Benutzer darf Einstellungen entfernen.") - + settings_update_check = models.BooleanField(default=False, verbose_name="Updates prüfen", help_text="Benutzer darf nach Updates suchen.") + settings_update_apply = models.BooleanField(default=False, verbose_name="Updates installieren", help_text="Benutzer darf Updates installieren.") # ===================== # Zeiterfassung diff --git a/change.py b/change.py deleted file mode 100644 index c1b89bc..0000000 --- a/change.py +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env python3 - -import re -from pathlib import Path - -ROOT = Path("/opt/projects") - -EXCLUDE_DIRS = { - "venv", - "__pycache__", - ".git", - "node_modules", - "staticfiles", - "media", -} - -CLASS_RE = re.compile(r'class="([^"]*)"') - - -for path in ROOT.rglob("*"): - - if not path.is_file(): - continue - - if any(part in EXCLUDE_DIRS for part in path.parts): - continue - - if path.suffix not in {".html", ".htm"}: - continue - - try: - text = path.read_text(encoding="utf-8") - except UnicodeDecodeError: - continue - - - changed = False - - - def replace(match): - classes = match.group(1).split() - - has_btn = "btn" in classes - has_btn_variant = any( - c.startswith("btn-") - for c in classes - ) - - if has_btn_variant and not has_btn: - classes.insert(0, "btn") - return f'class="{" ".join(classes)}"' - - return match.group(0) - - - new_text = CLASS_RE.sub(replace, text) - - - if new_text != text: - path.write_text( - new_text, - encoding="utf-8" - ) - - print(f"✔ geändert: {path}") \ No newline at end of file diff --git a/config/settings.py b/config/settings.py index 1bd0ec6..2a721be 100644 --- a/config/settings.py +++ b/config/settings.py @@ -1,18 +1,15 @@ from pathlib import Path +from dotenv import load_dotenv +import os + +load_dotenv() -# Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent +SECRET_KEY = os.environ.get('SECRET_KEY') +DEBUG = os.environ.get('DEBUG', 'False') == 'True' +ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', 'localhost').split(',') -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/ -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'django-insecure-a*^y8tl19j6+i9029ye&n1nhyaze(sd&!drt1++kfq_lok)5o@' -DEBUG = True -ALLOWED_HOSTS = ["91.99.235.6", "localhost"] - - -# Application definition INSTALLED_APPS = [ 'accounts', 'core', @@ -66,9 +63,6 @@ TEMPLATES = [ WSGI_APPLICATION = 'config.wsgi.application' - -# Database -# https://docs.djangoproject.com/en/6.0/ref/settings/#databases DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', @@ -76,57 +70,30 @@ DATABASES = { } } - -# Password validation -# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ - { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', - }, + {'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'}, + {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'}, + {'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'}, + {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'}, ] - -# Internationalization -# https://docs.djangoproject.com/en/6.0/topics/i18n/ LANGUAGE_CODE = 'en-us' TIME_ZONE = 'UTC' USE_I18N = True USE_TZ = True - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/6.0/howto/static-files/ STATIC_URL = 'static/' +STATIC_ROOT = BASE_DIR / 'staticfiles' +TAILWIND_APP_NAME = 'theme' - -# TAILWIND -TAILWIND_APP_NAME = 'theme' - - -# ACCOUNTS LOGIN_URL = "accounts:login" LOGIN_REDIRECT_URL = "core:dashboard" LOGOUT_REDIRECT_URL = "accounts:login" -#AUTH_USER_MODEL = "accounts.User" -# MEDIEN MEDIA_URL = "/media/" MEDIA_ROOT = BASE_DIR / "media" - - if DEBUG: INSTALLED_APPS += ["django_browser_reload"] - MIDDLEWARE += [ - "django_browser_reload.middleware.BrowserReloadMiddleware", - ] \ No newline at end of file + MIDDLEWARE += ["django_browser_reload.middleware.BrowserReloadMiddleware"] \ No newline at end of file diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py index 3a0fd31..967f750 100644 --- a/core/migrations/0001_initial.py +++ b/core/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 6.0.6 on 2026-06-23 22:12 +# Generated by Django 6.0.6 on 2026-06-27 17:19 from django.db import migrations, models @@ -32,6 +32,8 @@ class Migration(migrations.Migration): ('is_small_business', models.BooleanField(default=False, verbose_name='Kleinunternehmer (§19 UStG)')), ('small_business_text', models.TextField(blank=True, verbose_name='Kleinunternehmer Hinweis')), ('invoice_header_text', models.TextField(blank=True, verbose_name='Rechnungszusatztext')), + ('offer_header_text', models.TextField(blank=True, verbose_name='Angebotszusatztext')), + ('reminder_header_text', models.TextField(blank=True, verbose_name='Mahnungszusatztext')), ('bank_name', models.CharField(blank=True, max_length=255, verbose_name='Bank')), ('iban', models.CharField(blank=True, max_length=50, verbose_name='IBAN')), ('bic', models.CharField(blank=True, max_length=50, verbose_name='BIC')), diff --git a/core/migrations/0002_companysetting_invoice_number_documentnumbersettings.py b/core/migrations/0002_companysetting_invoice_number_documentnumbersettings.py deleted file mode 100644 index a0a8ff8..0000000 --- a/core/migrations/0002_companysetting_invoice_number_documentnumbersettings.py +++ /dev/null @@ -1,32 +0,0 @@ -# Generated by Django 6.0.6 on 2026-06-24 09:51 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='companysetting', - name='invoice_number', - field=models.CharField(blank=True, max_length=50, verbose_name='BIC'), - ), - migrations.CreateModel( - name='DocumentNumberSettings', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('invoice_prefix', models.CharField(default='RE', max_length=20)), - ('invoice_next_number', models.PositiveIntegerField(default=1)), - ('cancel_prefix', models.CharField(default='ST', max_length=20)), - ('cancel_next_number', models.PositiveIntegerField(default=1)), - ('offer_prefix', models.CharField(default='ANG', max_length=20)), - ('offer_next_number', models.PositiveIntegerField(default=1)), - ('company', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='number_settings', to='core.companysetting')), - ], - ), - ] diff --git a/core/migrations/0003_delete_documentnumbersettings.py b/core/migrations/0003_delete_documentnumbersettings.py deleted file mode 100644 index e1c0e10..0000000 --- a/core/migrations/0003_delete_documentnumbersettings.py +++ /dev/null @@ -1,16 +0,0 @@ -# Generated by Django 6.0.6 on 2026-06-24 09:53 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0002_companysetting_invoice_number_documentnumbersettings'), - ] - - operations = [ - migrations.DeleteModel( - name='DocumentNumberSettings', - ), - ] diff --git a/core/migrations/0004_companysetting_offer_header_text.py b/core/migrations/0004_companysetting_offer_header_text.py deleted file mode 100644 index af87337..0000000 --- a/core/migrations/0004_companysetting_offer_header_text.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 6.0.6 on 2026-06-25 14:57 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0003_delete_documentnumbersettings'), - ] - - operations = [ - migrations.AddField( - model_name='companysetting', - name='offer_header_text', - field=models.TextField(blank=True, verbose_name='Angebotszusatztext'), - ), - ] diff --git a/core/migrations/0005_remove_companysetting_invoice_number.py b/core/migrations/0005_remove_companysetting_invoice_number.py deleted file mode 100644 index f12d3f7..0000000 --- a/core/migrations/0005_remove_companysetting_invoice_number.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 6.0.6 on 2026-06-26 06:13 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0004_companysetting_offer_header_text'), - ] - - operations = [ - migrations.RemoveField( - model_name='companysetting', - name='invoice_number', - ), - ] diff --git a/core/migrations/0006_companysetting_reminder_header_text.py b/core/migrations/0006_companysetting_reminder_header_text.py deleted file mode 100644 index f30fb5e..0000000 --- a/core/migrations/0006_companysetting_reminder_header_text.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 6.0.6 on 2026-06-27 08:53 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0005_remove_companysetting_invoice_number'), - ] - - operations = [ - migrations.AddField( - model_name='companysetting', - name='reminder_header_text', - field=models.TextField(blank=True, verbose_name='Mahnungszusatztext'), - ), - ] diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..0091755 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,149 @@ +#!/bin/bash +set -e + +# ── Farben ────────────────────────────────────────────── +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +log() { echo -e "${GREEN}[✓]${NC} $1"; } +warn() { echo -e "${YELLOW}[!]${NC} $1"; } +error() { echo -e "${RED}[✗]${NC} $1"; exit 1; } + +# ── Konfiguration ──────────────────────────────────────── +APP_DIR="/opt/projects" +REPO_URL="https://github.com/Fixelx/ProjectApp.git" +DOMAIN=${1:-"localhost"} + +echo "" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo " ProjectApp Deployment" +echo " Domain: $DOMAIN" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "" + +# ── Root check ─────────────────────────────────────────── +[ "$EUID" -ne 0 ] && error "Bitte als root ausführen (sudo ./deploy.sh)" + +# ── System-Pakete ──────────────────────────────────────── +log "System-Pakete installieren..." +apt update -q +apt install -y python3 python3-pip python3-venv git nginx curl -q + +# ── Repo ───────────────────────────────────────────────── +if [ -d "$APP_DIR/.git" ]; then + log "Repo aktualisieren..." + cd $APP_DIR + git pull origin main +else + log "Repo klonen..." + git clone $REPO_URL $APP_DIR + cd $APP_DIR +fi + +# ── .env prüfen ────────────────────────────────────────── +if [ ! -f "$APP_DIR/.env" ]; then + warn ".env fehlt – wird aus .env.example erstellt..." + cp $APP_DIR/.env.example $APP_DIR/.env + + SECRET=$(python3 -c "import secrets; print(secrets.token_urlsafe(50))") + sed -i "s/^SECRET_KEY=.*/SECRET_KEY=$SECRET/" $APP_DIR/.env + sed -i "s/^ALLOWED_HOSTS=.*/ALLOWED_HOSTS=$DOMAIN,localhost/" $APP_DIR/.env + sed -i "s/^CSRF_TRUSTED_ORIGINS=.*/CSRF_TRUSTED_ORIGINS=https:\/\/$DOMAIN/" $APP_DIR/.env + + warn ".env wurde angelegt – bitte prüfen: nano $APP_DIR/.env" +fi + +# ── Python venv ────────────────────────────────────────── +log "Virtualenv einrichten..." +python3 -m venv $APP_DIR/venv +source $APP_DIR/venv/bin/activate + +# ── Abhängigkeiten ─────────────────────────────────────── +log "Python-Pakete installieren..." +pip install -r $APP_DIR/requirements.txt -q +pip install gunicorn -q + +# ── Django setup ───────────────────────────────────────── +log "Migrationen ausführen..." +python manage.py migrate --no-input + +log "Static files sammeln..." +python manage.py collectstatic --no-input + +# ── Gunicorn Service ───────────────────────────────────── +log "Gunicorn Service einrichten..." +cat > /etc/systemd/system/gunicorn.service << EOF +[Unit] +Description=Gunicorn für ProjectApp +After=network.target + +[Service] +User=root +Group=www-data +WorkingDirectory=$APP_DIR +EnvironmentFile=$APP_DIR/.env +ExecStart=$APP_DIR/venv/bin/gunicorn \\ + --workers 3 \\ + --bind unix:$APP_DIR/gunicorn.sock \\ + config.wsgi:application +Restart=always + +[Install] +WantedBy=multi-user.target +EOF + +systemctl daemon-reload +systemctl enable gunicorn -q +systemctl restart gunicorn + +# ── Nginx config ───────────────────────────────────────── +log "Nginx konfigurieren..." +cat > /etc/nginx/sites-available/projectapp << EOF +server { + listen 80; + server_name $DOMAIN; + + location /static/ { + alias $APP_DIR/staticfiles/; + } + + location /media/ { + alias $APP_DIR/media/; + } + + location / { + proxy_pass http://unix:$APP_DIR/gunicorn.sock; + proxy_set_header Host \$host; + proxy_set_header X-Real-IP \$remote_addr; + proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto \$scheme; + } +} +EOF + +# Default-Site deaktivieren falls vorhanden +[ -f /etc/nginx/sites-enabled/default ] && rm /etc/nginx/sites-enabled/default + +ln -sf /etc/nginx/sites-available/projectapp /etc/nginx/sites-enabled/ +nginx -t && systemctl restart nginx + + +# ── Superuser ──────────────────────────────────────────── +warn "Superuser anlegen? [j/N]" +read -r SU +if [ "$SU" = "j" ] || [ "$SU" = "J" ]; then + python manage.py createsuperuser +fi + +# ── Fertig ─────────────────────────────────────────────── +echo "" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +log "Deployment abgeschlossen!" +echo "" +echo " App erreichbar unter: http://$DOMAIN" +echo " .env prüfen: nano $APP_DIR/.env" +echo " Logs: journalctl -u gunicorn -f" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "" \ No newline at end of file diff --git a/documents/migrations/0001_initial.py b/documents/migrations/0001_initial.py index 0a6bc1e..a8f18a2 100644 --- a/documents/migrations/0001_initial.py +++ b/documents/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 6.0.6 on 2026-06-23 22:12 +# Generated by Django 6.0.6 on 2026-06-27 17:19 import documents.models from django.db import migrations, models diff --git a/documents/migrations/0002_initial.py b/documents/migrations/0002_initial.py index da3d76e..a4a3a9e 100644 --- a/documents/migrations/0002_initial.py +++ b/documents/migrations/0002_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 6.0.6 on 2026-06-23 22:12 +# Generated by Django 6.0.6 on 2026-06-27 17:19 import django.db.models.deletion from django.conf import settings diff --git a/finance/migrations/0001_initial.py b/finance/migrations/0001_initial.py index 1a55e95..a2ac6be 100644 --- a/finance/migrations/0001_initial.py +++ b/finance/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 6.0.6 on 2026-06-23 22:12 +# Generated by Django 6.0.6 on 2026-06-27 17:19 import django.db.models.deletion import finance.models diff --git a/finance/migrations/0002_initial.py b/finance/migrations/0002_initial.py index f5a284d..6270cd4 100644 --- a/finance/migrations/0002_initial.py +++ b/finance/migrations/0002_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 6.0.6 on 2026-06-23 22:12 +# Generated by Django 6.0.6 on 2026-06-27 17:19 import django.db.models.deletion from django.conf import settings diff --git a/inventory/migrations/0001_initial.py b/inventory/migrations/0001_initial.py index a425efc..8651fd5 100644 --- a/inventory/migrations/0001_initial.py +++ b/inventory/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 6.0.6 on 2026-06-23 22:12 +# Generated by Django 6.0.6 on 2026-06-27 17:19 import django.db.models.deletion from django.conf import settings diff --git a/inventory/migrations/0002_initial.py b/inventory/migrations/0002_initial.py index bb89df3..e14b440 100644 --- a/inventory/migrations/0002_initial.py +++ b/inventory/migrations/0002_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 6.0.6 on 2026-06-23 22:12 +# Generated by Django 6.0.6 on 2026-06-27 17:19 import django.db.models.deletion from django.db import migrations, models diff --git a/invoices/migrations/0001_initial.py b/invoices/migrations/0001_initial.py index cccb19e..260022c 100644 --- a/invoices/migrations/0001_initial.py +++ b/invoices/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 6.0.6 on 2026-06-23 22:12 +# Generated by Django 6.0.6 on 2026-06-27 17:19 import django.db.models.deletion from django.db import migrations, models @@ -9,6 +9,7 @@ class Migration(migrations.Migration): initial = True dependencies = [ + ('core', '0001_initial'), ] operations = [ @@ -43,6 +44,8 @@ class Migration(migrations.Migration): ('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_tax_rate', models.DecimalField(decimal_places=2, default=19.0, max_digits=5, verbose_name='MwSt. Satz (%)')), + ('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')), ('created_at', models.DateTimeField(auto_now_add=True)), ], options={ @@ -66,6 +69,72 @@ class Migration(migrations.Migration): 'ordering': ['description'], }, ), + migrations.CreateModel( + name='Offer', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('offer_number', models.CharField(blank=True, max_length=50, verbose_name='Angebotsnummer')), + ('status', models.CharField(choices=[('draft', 'Entwurf'), ('sent', 'Versendet'), ('accepted', 'Akzeptiert'), ('rejected', 'Abgelehnt'), ('expired', 'Ausgelaufen')], default='draft', max_length=20, verbose_name='Status')), + ('issue_date', models.DateField(verbose_name='Angebotsdatum')), + ('due_date', models.DateField(blank=True, null=True, verbose_name='Gültig bis')), + ('note', models.TextField(blank=True, verbose_name='Notiz')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('pdf_file', models.FileField(blank=True, null=True, upload_to='offer_pdfs/', verbose_name='PDF')), + ('pdf_generated_at', models.DateTimeField(blank=True, null=True, verbose_name='PDF erstellt am')), + ('tax_number', models.CharField(blank=True, max_length=100, verbose_name='Steuernummer Kunde')), + ('vat_number', models.CharField(blank=True, max_length=100, verbose_name='USt-IdNr Kunde')), + ('contact_name', models.CharField(max_length=255, verbose_name='Ansprechpartner')), + ('contact_email', models.EmailField(blank=True, max_length=254, verbose_name='E-Mail Ansprechpartner')), + ('contact_phone', models.CharField(blank=True, max_length=100, verbose_name='Telefon Ansprechpartner')), + ('location_name', models.CharField(max_length=255, verbose_name='Standort')), + ('location_street', models.CharField(blank=True, max_length=255, verbose_name='Straße')), + ('location_city', models.CharField(blank=True, max_length=255, verbose_name='Ort')), + ('location_zip', models.CharField(blank=True, max_length=20, verbose_name='PLZ')), + ('location_country', models.CharField(blank=True, max_length=100, verbose_name='Land')), + ], + options={ + 'verbose_name': 'Angebot', + 'verbose_name_plural': 'Angebote', + 'ordering': ['-issue_date'], + }, + ), + migrations.CreateModel( + name='OfferItem', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('description', models.CharField(max_length=255, verbose_name='Beschreibung')), + ('quantity', models.DecimalField(decimal_places=2, default=1, max_digits=10, verbose_name='Menge')), + ('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_tax_rate', models.DecimalField(decimal_places=2, default=19.0, max_digits=5, verbose_name='MwSt. Satz (%)')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ], + options={ + 'verbose_name': 'Angebotsposition', + 'verbose_name_plural': 'Angebotspositionen', + }, + ), + migrations.CreateModel( + name='Reminder', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('reminder_number', models.CharField(blank=True, max_length=50, verbose_name='Mahnungsnummer')), + ('status', models.CharField(choices=[('draft', 'Entwurf'), ('sent', 'Versendet'), ('paid', 'Bezahlt'), ('canceled', 'Storniert'), ('expired', 'Frist abgelaufen')], default='draft', max_length=20, verbose_name='Status')), + ('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')), + ('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')), + ('note', models.TextField(blank=True, verbose_name='Hinweis')), + ('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')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ], + options={ + 'verbose_name': 'Mahnung', + 'verbose_name_plural': 'Mahnungen', + 'ordering': ['-created_at'], + }, + ), migrations.CreateModel( name='Unit', fields=[ @@ -112,18 +181,40 @@ class Migration(migrations.Migration): 'ordering': ['name'], }, ), + migrations.CreateModel( + name='DocumentNumberSettings', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('invoice_format', models.CharField(default='RE-{year}-{number}', max_length=100, verbose_name='Rechnungsformat')), + ('invoice_next_number', models.PositiveIntegerField(default=1, verbose_name='Rechnungs ID (nächste)')), + ('cancel_format', models.CharField(default='ST-{year}-{number}', max_length=100, verbose_name='Stornoformat')), + ('cancel_next_number', models.PositiveIntegerField(default=1, verbose_name='Storno ID (nächste)')), + ('offer_format', models.CharField(default='ANG-{year}-{number}', max_length=100, verbose_name='Angebotsformat')), + ('offer_next_number', models.PositiveIntegerField(default=1, verbose_name='Angebots ID (nächste)')), + ('reminder_format', models.CharField(default='M-{year}-{number}', max_length=100, verbose_name='Mahnungsformat')), + ('reminder_next_number', models.PositiveIntegerField(default=1, verbose_name='Mahnungs ID (nächste)')), + ('company', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='number_settings', to='core.companysetting', verbose_name='Unternehmen')), + ], + options={ + 'verbose_name': 'Dokumentnummer', + 'verbose_name_plural': 'Dokumentnummern', + }, + ), migrations.CreateModel( name='Invoice', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('invoice_number', models.CharField(max_length=50, unique=True, verbose_name='Rechnungsnummer')), - ('status', models.CharField(choices=[('draft', 'Entwurf'), ('sent', 'Versendet'), ('paid', 'Bezahlt'), ('canceled', 'Storniert')], default='draft', max_length=20, verbose_name='Status')), + ('invoice_number', models.CharField(blank=True, max_length=50, verbose_name='Rechnungsnummer')), + ('status', models.CharField(choices=[('draft', 'Entwurf'), ('sent', 'Versendet'), ('paid', 'Bezahlt'), ('overdue', 'Überfällig'), ('canceled', 'Storniert')], default='draft', max_length=20, verbose_name='Status')), ('issue_date', models.DateField(verbose_name='Rechnungsdatum')), ('due_date', models.DateField(blank=True, null=True, verbose_name='Fälligkeitsdatum')), ('note', models.TextField(blank=True, verbose_name='Notiz')), ('created_at', models.DateTimeField(auto_now_add=True)), ('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')), + ('cancel_number', models.CharField(blank=True, max_length=50, null=True, unique=True, verbose_name='Stornorechnungsnummer')), + ('pdf_canceled_file', models.FileField(blank=True, null=True, upload_to='invoice_pdfs/', verbose_name='Stornorechnung')), + ('pdf_canceled_generated_at', models.DateTimeField(blank=True, null=True, verbose_name='Stornorechnung erstellt am')), ('tax_number', models.CharField(blank=True, max_length=100, verbose_name='Steuernummer Kunde')), ('vat_number', models.CharField(blank=True, max_length=100, verbose_name='USt-IdNr Kunde')), ('contact_name', models.CharField(max_length=255, verbose_name='Ansprechpartner')), diff --git a/invoices/migrations/0002_initial.py b/invoices/migrations/0002_initial.py index 2db02a0..30e4734 100644 --- a/invoices/migrations/0002_initial.py +++ b/invoices/migrations/0002_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 6.0.6 on 2026-06-23 22:12 +# Generated by Django 6.0.6 on 2026-06-27 17:19 import django.db.models.deletion from django.db import migrations, models @@ -41,6 +41,26 @@ class Migration(migrations.Migration): name='item', field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='invoices.item', verbose_name='Artikel'), ), + migrations.AddField( + model_name='offer', + name='customer', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='offers', to='invoices.customer', verbose_name='Kunde'), + ), + migrations.AddField( + model_name='offeritem', + name='item', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='invoices.item', verbose_name='Artikel'), + ), + migrations.AddField( + model_name='offeritem', + name='offer', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='invoices.offer', verbose_name='Angebot'), + ), + migrations.AddField( + model_name='reminder', + name='invoice', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='reminders', to='invoices.invoice', verbose_name='Rechnung'), + ), migrations.AddField( model_name='item', name='unit', diff --git a/invoices/migrations/0003_invoice_pdf_canceled_file_and_more.py b/invoices/migrations/0003_invoice_pdf_canceled_file_and_more.py deleted file mode 100644 index 68a331b..0000000 --- a/invoices/migrations/0003_invoice_pdf_canceled_file_and_more.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 6.0.6 on 2026-06-24 08:38 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('invoices', '0002_initial'), - ] - - operations = [ - migrations.AddField( - model_name='invoice', - name='pdf_canceled_file', - field=models.FileField(blank=True, null=True, upload_to='invoice_pdfs/', verbose_name='Stornorechnung'), - ), - migrations.AddField( - model_name='invoice', - name='pdf_canceled_generated_at', - field=models.DateTimeField(blank=True, null=True, verbose_name='Stornorechnung erstellt am'), - ), - ] diff --git a/invoices/migrations/0004_documentnumbersettings.py b/invoices/migrations/0004_documentnumbersettings.py deleted file mode 100644 index 467e37b..0000000 --- a/invoices/migrations/0004_documentnumbersettings.py +++ /dev/null @@ -1,28 +0,0 @@ -# Generated by Django 6.0.6 on 2026-06-24 09:53 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0003_delete_documentnumbersettings'), - ('invoices', '0003_invoice_pdf_canceled_file_and_more'), - ] - - operations = [ - migrations.CreateModel( - name='DocumentNumberSettings', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('invoice_prefix', models.CharField(default='RE', max_length=20)), - ('invoice_next_number', models.PositiveIntegerField(default=1)), - ('cancel_prefix', models.CharField(default='ST', max_length=20)), - ('cancel_next_number', models.PositiveIntegerField(default=1)), - ('offer_prefix', models.CharField(default='ANG', max_length=20)), - ('offer_next_number', models.PositiveIntegerField(default=1)), - ('company', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='number_settings', to='core.companysetting')), - ], - ), - ] diff --git a/invoices/migrations/0005_remove_documentnumbersettings_cancel_prefix_and_more.py b/invoices/migrations/0005_remove_documentnumbersettings_cancel_prefix_and_more.py deleted file mode 100644 index a78f87e..0000000 --- a/invoices/migrations/0005_remove_documentnumbersettings_cancel_prefix_and_more.py +++ /dev/null @@ -1,40 +0,0 @@ -# Generated by Django 6.0.6 on 2026-06-24 09:56 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('invoices', '0004_documentnumbersettings'), - ] - - operations = [ - migrations.RemoveField( - model_name='documentnumbersettings', - name='cancel_prefix', - ), - migrations.RemoveField( - model_name='documentnumbersettings', - name='invoice_prefix', - ), - migrations.RemoveField( - model_name='documentnumbersettings', - name='offer_prefix', - ), - migrations.AddField( - model_name='documentnumbersettings', - name='cancel_format', - field=models.CharField(default='ST-{year}-{number}', max_length=100), - ), - migrations.AddField( - model_name='documentnumbersettings', - name='invoice_format', - field=models.CharField(default='RE-{year}-{number}', max_length=100), - ), - migrations.AddField( - model_name='documentnumbersettings', - name='offer_format', - field=models.CharField(default='ANG-{year}-{number}', max_length=100), - ), - ] diff --git a/invoices/migrations/0006_alter_documentnumbersettings_options_and_more.py b/invoices/migrations/0006_alter_documentnumbersettings_options_and_more.py deleted file mode 100644 index 6d5f508..0000000 --- a/invoices/migrations/0006_alter_documentnumbersettings_options_and_more.py +++ /dev/null @@ -1,54 +0,0 @@ -# Generated by Django 6.0.6 on 2026-06-24 09:59 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0003_delete_documentnumbersettings'), - ('invoices', '0005_remove_documentnumbersettings_cancel_prefix_and_more'), - ] - - operations = [ - migrations.AlterModelOptions( - name='documentnumbersettings', - options={'verbose_name': 'Dokumentnummer', 'verbose_name_plural': 'Dokumentnummern'}, - ), - migrations.AlterField( - model_name='documentnumbersettings', - name='cancel_format', - field=models.CharField(default='ST-{year}-{number}', max_length=100, verbose_name='Stornoformat'), - ), - migrations.AlterField( - model_name='documentnumbersettings', - name='cancel_next_number', - field=models.PositiveIntegerField(default=1, verbose_name='Storno ID (nächste)'), - ), - migrations.AlterField( - model_name='documentnumbersettings', - name='company', - field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='number_settings', to='core.companysetting', verbose_name='Unternehmen'), - ), - migrations.AlterField( - model_name='documentnumbersettings', - name='invoice_format', - field=models.CharField(default='RE-{year}-{number}', max_length=100, verbose_name='Rechnungsformat'), - ), - migrations.AlterField( - model_name='documentnumbersettings', - name='invoice_next_number', - field=models.PositiveIntegerField(default=1, verbose_name='Rechnungs ID (nächste)'), - ), - migrations.AlterField( - model_name='documentnumbersettings', - name='offer_format', - field=models.CharField(default='ANG-{year}-{number}', max_length=100, verbose_name='Angebotsformat'), - ), - migrations.AlterField( - model_name='documentnumbersettings', - name='offer_next_number', - field=models.PositiveIntegerField(default=1, verbose_name='Angebots ID (nächste)'), - ), - ] diff --git a/invoices/migrations/0007_invoice_cancel_number.py b/invoices/migrations/0007_invoice_cancel_number.py deleted file mode 100644 index 4543292..0000000 --- a/invoices/migrations/0007_invoice_cancel_number.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 6.0.6 on 2026-06-24 11:30 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('invoices', '0006_alter_documentnumbersettings_options_and_more'), - ] - - operations = [ - migrations.AddField( - model_name='invoice', - name='cancel_number', - field=models.CharField(blank=True, max_length=50, null=True, unique=True, verbose_name='Stornorechnungsnummer'), - ), - ] diff --git a/invoices/migrations/0008_reminder.py b/invoices/migrations/0008_reminder.py deleted file mode 100644 index 6744a9b..0000000 --- a/invoices/migrations/0008_reminder.py +++ /dev/null @@ -1,36 +0,0 @@ -# Generated by Django 6.0.6 on 2026-06-24 18:29 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('invoices', '0007_invoice_cancel_number'), - ] - - operations = [ - migrations.CreateModel( - name='Reminder', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('reminder_number', models.CharField(max_length=50, unique=True, verbose_name='Mahnungsnummer')), - ('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')), - ('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')), - ('interest', models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Verzugszinsen')), - ('note', models.TextField(blank=True, verbose_name='Hinweis')), - ('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')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('invoice', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='reminders', to='invoices.invoice', verbose_name='Rechnung')), - ], - options={ - 'verbose_name': 'Mahnung', - 'verbose_name_plural': 'Mahnungen', - 'ordering': ['-created_at'], - }, - ), - ] diff --git a/invoices/migrations/0009_offer_offeritem.py b/invoices/migrations/0009_offer_offeritem.py deleted file mode 100644 index 9d6d493..0000000 --- a/invoices/migrations/0009_offer_offeritem.py +++ /dev/null @@ -1,64 +0,0 @@ -# Generated by Django 6.0.6 on 2026-06-24 19:17 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('invoices', '0008_reminder'), - ('projects', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='Offer', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('offer_number', models.CharField(max_length=50, unique=True, verbose_name='Angebotsnummer')), - ('status', models.CharField(choices=[('draft', 'Entwurf'), ('sent', 'Versendet'), ('accepted', 'Akzeptiert'), ('rejected', 'Abgelehnt'), ('expired', 'Ausgelaufen')], default='draft', max_length=20, verbose_name='Status')), - ('issue_date', models.DateField(verbose_name='Angebotsdatum')), - ('due_date', models.DateField(blank=True, null=True, verbose_name='Gültig bis')), - ('note', models.TextField(blank=True, verbose_name='Notiz')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('pdf_file', models.FileField(blank=True, null=True, upload_to='offer_pdfs/', verbose_name='PDF')), - ('pdf_generated_at', models.DateTimeField(blank=True, null=True, verbose_name='PDF erstellt am')), - ('tax_number', models.CharField(blank=True, max_length=100, verbose_name='Steuernummer Kunde')), - ('vat_number', models.CharField(blank=True, max_length=100, verbose_name='USt-IdNr Kunde')), - ('contact_name', models.CharField(max_length=255, verbose_name='Ansprechpartner')), - ('contact_email', models.EmailField(blank=True, max_length=254, verbose_name='E-Mail Ansprechpartner')), - ('contact_phone', models.CharField(blank=True, max_length=100, verbose_name='Telefon Ansprechpartner')), - ('location_name', models.CharField(max_length=255, verbose_name='Standort')), - ('location_street', models.CharField(blank=True, max_length=255, verbose_name='Straße')), - ('location_city', models.CharField(blank=True, max_length=255, verbose_name='Ort')), - ('location_zip', models.CharField(blank=True, max_length=20, verbose_name='PLZ')), - ('location_country', models.CharField(blank=True, max_length=100, verbose_name='Land')), - ('customer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='offers', to='invoices.customer', verbose_name='Kunde')), - ('project', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='offers', to='projects.project', verbose_name='Projekt')), - ], - options={ - 'verbose_name': 'Angebot', - 'verbose_name_plural': 'Angebote', - 'ordering': ['-issue_date'], - }, - ), - migrations.CreateModel( - name='OfferItem', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('description', models.CharField(max_length=255, verbose_name='Beschreibung')), - ('quantity', models.DecimalField(decimal_places=2, default=1, max_digits=10, verbose_name='Menge')), - ('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_tax_rate', models.DecimalField(decimal_places=2, default=19.0, max_digits=5, verbose_name='MwSt. Satz (%)')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('item', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='invoices.item', verbose_name='Artikel')), - ('offer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='invoices.offer', verbose_name='Angebot')), - ], - options={ - 'verbose_name': 'Angebotsposition', - 'verbose_name_plural': 'Angebotspositionen', - }, - ), - ] diff --git a/invoices/migrations/0010_item_service_period_from_item_service_period_to.py b/invoices/migrations/0010_item_service_period_from_item_service_period_to.py deleted file mode 100644 index 146a4b8..0000000 --- a/invoices/migrations/0010_item_service_period_from_item_service_period_to.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 6.0.6 on 2026-06-25 15:17 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('invoices', '0009_offer_offeritem'), - ] - - operations = [ - migrations.AddField( - model_name='item', - name='service_period_from', - field=models.DateField(blank=True, null=True, verbose_name='Leistungszeitraum von'), - ), - migrations.AddField( - model_name='item', - name='service_period_to', - field=models.DateField(blank=True, null=True, verbose_name='Leistungszeitraum bis'), - ), - ] diff --git a/invoices/migrations/0011_remove_item_service_period_from_and_more.py b/invoices/migrations/0011_remove_item_service_period_from_and_more.py deleted file mode 100644 index b8523f3..0000000 --- a/invoices/migrations/0011_remove_item_service_period_from_and_more.py +++ /dev/null @@ -1,31 +0,0 @@ -# Generated by Django 6.0.6 on 2026-06-25 15:19 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('invoices', '0010_item_service_period_from_item_service_period_to'), - ] - - operations = [ - migrations.RemoveField( - model_name='item', - name='service_period_from', - ), - migrations.RemoveField( - model_name='item', - name='service_period_to', - ), - migrations.AddField( - model_name='invoiceitem', - name='service_period_from', - field=models.DateField(blank=True, null=True, verbose_name='Leistungszeitraum von'), - ), - migrations.AddField( - model_name='invoiceitem', - name='service_period_to', - field=models.DateField(blank=True, null=True, verbose_name='Leistungszeitraum bis'), - ), - ] diff --git a/invoices/migrations/0012_remove_offer_project.py b/invoices/migrations/0012_remove_offer_project.py deleted file mode 100644 index f49d9a1..0000000 --- a/invoices/migrations/0012_remove_offer_project.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 6.0.6 on 2026-06-25 16:13 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('invoices', '0011_remove_item_service_period_from_and_more'), - ] - - operations = [ - migrations.RemoveField( - model_name='offer', - name='project', - ), - ] diff --git a/invoices/migrations/0013_reminder_status.py b/invoices/migrations/0013_reminder_status.py deleted file mode 100644 index 8137a9c..0000000 --- a/invoices/migrations/0013_reminder_status.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 6.0.6 on 2026-06-27 05:50 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('invoices', '0012_remove_offer_project'), - ] - - operations = [ - migrations.AddField( - model_name='reminder', - name='status', - field=models.CharField(choices=[('draft', 'Entwurf'), ('sent', 'Versendet'), ('paid', 'Bezahlt'), ('partial', 'Teilweise bezahlt'), ('cancelled', 'Storniert'), ('expired', 'Frist abgelaufen')], default='draft', max_length=20, verbose_name='Status'), - ), - ] diff --git a/invoices/migrations/0014_alter_invoice_status.py b/invoices/migrations/0014_alter_invoice_status.py deleted file mode 100644 index 2dce8a8..0000000 --- a/invoices/migrations/0014_alter_invoice_status.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 6.0.6 on 2026-06-27 06:00 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('invoices', '0013_reminder_status'), - ] - - operations = [ - migrations.AlterField( - model_name='invoice', - name='status', - field=models.CharField(choices=[('draft', 'Entwurf'), ('sent', 'Versendet'), ('paid', 'Bezahlt'), ('overdue', 'Überfällig'), ('canceled', 'Storniert')], default='draft', max_length=20, verbose_name='Status'), - ), - ] diff --git a/invoices/migrations/0015_alter_documentnumbersettings_offer_format_and_more.py b/invoices/migrations/0015_alter_documentnumbersettings_offer_format_and_more.py deleted file mode 100644 index 9e61b50..0000000 --- a/invoices/migrations/0015_alter_documentnumbersettings_offer_format_and_more.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 6.0.6 on 2026-06-27 06:14 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('invoices', '0014_alter_invoice_status'), - ] - - operations = [ - migrations.AlterField( - model_name='documentnumbersettings', - name='offer_format', - field=models.CharField(default='M-{year}-{number}', max_length=100, verbose_name='Mahnungsformat'), - ), - migrations.AlterField( - model_name='documentnumbersettings', - name='offer_next_number', - field=models.PositiveIntegerField(default=1, verbose_name='Mahnungs ID (nächste)'), - ), - ] diff --git a/invoices/migrations/0016_documentnumbersettings_reminder_format_and_more.py b/invoices/migrations/0016_documentnumbersettings_reminder_format_and_more.py deleted file mode 100644 index 8e0ee54..0000000 --- a/invoices/migrations/0016_documentnumbersettings_reminder_format_and_more.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 6.0.6 on 2026-06-27 06:21 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('invoices', '0015_alter_documentnumbersettings_offer_format_and_more'), - ] - - operations = [ - migrations.AddField( - model_name='documentnumbersettings', - name='reminder_format', - field=models.CharField(default='M-{year}-{number}', max_length=100, verbose_name='Mahnungsformat'), - ), - migrations.AddField( - model_name='documentnumbersettings', - name='reminder_next_number', - field=models.PositiveIntegerField(default=1, verbose_name='Mahnungs ID (nächste)'), - ), - migrations.AlterField( - model_name='documentnumbersettings', - name='offer_format', - field=models.CharField(default='ANG-{year}-{number}', max_length=100, verbose_name='Angebotsformat'), - ), - migrations.AlterField( - model_name='documentnumbersettings', - name='offer_next_number', - field=models.PositiveIntegerField(default=1, verbose_name='Angebots ID (nächste)'), - ), - ] diff --git a/invoices/migrations/0017_alter_reminder_status.py b/invoices/migrations/0017_alter_reminder_status.py deleted file mode 100644 index 91e4267..0000000 --- a/invoices/migrations/0017_alter_reminder_status.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 6.0.6 on 2026-06-27 06:59 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('invoices', '0016_documentnumbersettings_reminder_format_and_more'), - ] - - operations = [ - migrations.AlterField( - model_name='reminder', - name='status', - field=models.CharField(choices=[('draft', 'Entwurf'), ('sent', 'Versendet'), ('paid', 'Bezahlt'), ('partial', 'Teilweise bezahlt'), ('canceled', 'Storniert'), ('expired', 'Frist abgelaufen')], default='draft', max_length=20, verbose_name='Status'), - ), - ] diff --git a/invoices/migrations/0018_alter_reminder_status.py b/invoices/migrations/0018_alter_reminder_status.py deleted file mode 100644 index 5cf8b12..0000000 --- a/invoices/migrations/0018_alter_reminder_status.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 6.0.6 on 2026-06-27 07:53 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('invoices', '0017_alter_reminder_status'), - ] - - operations = [ - migrations.AlterField( - model_name='reminder', - name='status', - field=models.CharField(choices=[('draft', 'Entwurf'), ('sent', 'Versendet'), ('paid', 'Bezahlt'), ('canceled', 'Storniert'), ('expired', 'Frist abgelaufen')], default='draft', max_length=20, verbose_name='Status'), - ), - ] diff --git a/invoices/migrations/0019_alter_invoice_invoice_number.py b/invoices/migrations/0019_alter_invoice_invoice_number.py deleted file mode 100644 index 67acdde..0000000 --- a/invoices/migrations/0019_alter_invoice_invoice_number.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 6.0.6 on 2026-06-27 08:13 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('invoices', '0018_alter_reminder_status'), - ] - - operations = [ - migrations.AlterField( - model_name='invoice', - name='invoice_number', - field=models.CharField(blank=True, max_length=50, verbose_name='Rechnungsnummer'), - ), - ] diff --git a/invoices/migrations/0020_alter_offer_offer_number.py b/invoices/migrations/0020_alter_offer_offer_number.py deleted file mode 100644 index dedbe6f..0000000 --- a/invoices/migrations/0020_alter_offer_offer_number.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 6.0.6 on 2026-06-27 08:26 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('invoices', '0019_alter_invoice_invoice_number'), - ] - - operations = [ - migrations.AlterField( - model_name='offer', - name='offer_number', - field=models.CharField(blank=True, max_length=50, verbose_name='Angebotsnummer'), - ), - ] diff --git a/invoices/migrations/0021_alter_reminder_reminder_number.py b/invoices/migrations/0021_alter_reminder_reminder_number.py deleted file mode 100644 index 38e62f2..0000000 --- a/invoices/migrations/0021_alter_reminder_reminder_number.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 6.0.6 on 2026-06-27 08:27 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('invoices', '0020_alter_offer_offer_number'), - ] - - operations = [ - migrations.AlterField( - model_name='reminder', - name='reminder_number', - field=models.CharField(blank=True, max_length=50, verbose_name='Mahnungsnummer'), - ), - ] diff --git a/invoices/migrations/0022_reminder_interest_rate_alter_reminder_interest.py b/invoices/migrations/0022_reminder_interest_rate_alter_reminder_interest.py deleted file mode 100644 index ce551e7..0000000 --- a/invoices/migrations/0022_reminder_interest_rate_alter_reminder_interest.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 6.0.6 on 2026-06-27 08:58 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('invoices', '0021_alter_reminder_reminder_number'), - ] - - operations = [ - migrations.AddField( - model_name='reminder', - name='interest_rate', - field=models.DecimalField(decimal_places=2, default=0, max_digits=5, verbose_name='Verzugszinssatz (%)'), - ), - migrations.AlterField( - model_name='reminder', - name='interest', - field=models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Verzugszinsen Betrag'), - ), - ] diff --git a/invoices/migrations/0023_reminder_interest_days.py b/invoices/migrations/0023_reminder_interest_days.py deleted file mode 100644 index 41cc617..0000000 --- a/invoices/migrations/0023_reminder_interest_days.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 6.0.6 on 2026-06-27 09:00 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('invoices', '0022_reminder_interest_rate_alter_reminder_interest'), - ] - - operations = [ - migrations.AddField( - model_name='reminder', - name='interest_days', - field=models.PositiveIntegerField(default=0, verbose_name='Verzugstage'), - ), - ] diff --git a/invoices/migrations/0024_alter_reminder_interest_alter_reminder_interest_days_and_more.py b/invoices/migrations/0024_alter_reminder_interest_alter_reminder_interest_days_and_more.py deleted file mode 100644 index 189c205..0000000 --- a/invoices/migrations/0024_alter_reminder_interest_alter_reminder_interest_days_and_more.py +++ /dev/null @@ -1,28 +0,0 @@ -# Generated by Django 6.0.6 on 2026-06-27 09:03 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('invoices', '0023_reminder_interest_days'), - ] - - operations = [ - migrations.AlterField( - model_name='reminder', - name='interest', - field=models.DecimalField(blank=True, decimal_places=2, default=0, max_digits=10, verbose_name='Verzugszinsen Betrag'), - ), - migrations.AlterField( - model_name='reminder', - name='interest_days', - field=models.PositiveIntegerField(blank=True, default=0, verbose_name='Verzugstage'), - ), - migrations.AlterField( - model_name='reminder', - name='interest_rate', - field=models.DecimalField(blank=True, decimal_places=2, default=0, max_digits=5, verbose_name='Verzugszinssatz (%)'), - ), - ] diff --git a/invoices/migrations/0025_remove_reminder_interest_and_more.py b/invoices/migrations/0025_remove_reminder_interest_and_more.py deleted file mode 100644 index 2412b6f..0000000 --- a/invoices/migrations/0025_remove_reminder_interest_and_more.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 6.0.6 on 2026-06-27 09:33 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('invoices', '0024_alter_reminder_interest_alter_reminder_interest_days_and_more'), - ] - - operations = [ - migrations.RemoveField( - model_name='reminder', - name='interest', - ), - migrations.RemoveField( - model_name='reminder', - name='interest_days', - ), - migrations.RemoveField( - model_name='reminder', - name='interest_rate', - ), - ] diff --git a/projects/migrations/0001_initial.py b/projects/migrations/0001_initial.py index ab368e7..8b2ae4c 100644 --- a/projects/migrations/0001_initial.py +++ b/projects/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 6.0.6 on 2026-06-23 22:12 +# Generated by Django 6.0.6 on 2026-06-27 17:19 import django.db.models.deletion import projects.models diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..cb5a59d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,46 @@ +arrow==1.4.0 +asgiref==3.11.1 +binaryornot==0.6.0 +brotli==1.2.0 +certifi==2026.6.17 +cffi==2.0.0 +charset-normalizer==3.4.7 +click==8.4.2 +cookiecutter==2.7.1 +cssselect2==0.9.0 +Django==6.0.6 +django-browser-reload==1.21.0 +django-filter==25.2 +django-tables2==3.0.0 +django-tailwind==4.5.0 +fonttools==4.63.0 +gunicorn==26.0.0 +honcho==2.0.0 +idna==3.18 +Jinja2==3.1.6 +markdown-it-py==4.2.0 +MarkupSafe==3.0.3 +mdurl==0.1.2 +packaging==26.2 +pillow==12.2.0 +pycparser==3.0 +pydyf==0.12.1 +Pygments==2.20.0 +pyphen==0.17.2 +pytailwindcss==0.3.1 +python-dateutil==2.9.0.post0 +python-dotenv==1.2.2 +python-slugify==8.0.4 +PyYAML==6.0.3 +requests==2.34.2 +rich==15.0.0 +six==1.17.0 +sqlparse==0.5.5 +text-unidecode==1.3 +tinycss2==1.5.1 +tinyhtml5==2.1.0 +tzdata==2026.2 +urllib3==2.7.0 +weasyprint==69.0 +webencodings==0.5.1 +zopfli==0.4.3 diff --git a/search.py b/search.py deleted file mode 100644 index 8492599..0000000 --- a/search.py +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env python3 - -from pathlib import Path - -ROOT = Path("/opt/projects") -SEARCH = " :not(:last-child)) { - --tw-space-y-reverse: 0; - margin-block-start: calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse)); - margin-block-end: calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse))); - } - } - .space-y-2 { - :where(& > :not(:last-child)) { - --tw-space-y-reverse: 0; - margin-block-start: calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse)); - margin-block-end: calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse))); - } - } - .space-y-3 { - :where(& > :not(:last-child)) { - --tw-space-y-reverse: 0; - margin-block-start: calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse)); - margin-block-end: calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse))); - } - } - .space-y-4 { - :where(& > :not(:last-child)) { - --tw-space-y-reverse: 0; - margin-block-start: calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse)); - margin-block-end: calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse))); - } - } - .divide-y { - :where(& > :not(:last-child)) { - --tw-divide-y-reverse: 0; - border-bottom-style: var(--tw-border-style); - border-top-style: var(--tw-border-style); - border-top-width: calc(1px * var(--tw-divide-y-reverse)); - border-bottom-width: calc(1px * calc(1 - var(--tw-divide-y-reverse))); - } - } - .truncate { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - .overflow-hidden { - overflow: hidden; - } - .overflow-x-auto { - overflow-x: auto; - } - .rounded { - border-radius: 0.25rem; - } - .rounded-2xl { - border-radius: var(--radius-2xl); - } - .rounded-full { - border-radius: calc(infinity * 1px); - } - .rounded-lg { - border-radius: var(--radius-lg); - } - .rounded-xl { - border-radius: var(--radius-xl); - } - .border { - border-style: var(--tw-border-style); - border-width: 1px; - } - .border-t { - border-top-style: var(--tw-border-style); - border-top-width: 1px; - } - .border-r { - border-right-style: var(--tw-border-style); - border-right-width: 1px; - } - .border-b { - border-bottom-style: var(--tw-border-style); - border-bottom-width: 1px; - } - .border-blue-300 { - border-color: var(--color-blue-300); - } - .border-green-300 { - border-color: var(--color-green-300); - } - .border-orange-500 { - border-color: var(--color-orange-500); - } - .border-red-300 { - border-color: var(--color-red-300); - } - .border-slate-300 { - border-color: var(--color-slate-300); - } - .border-slate-700 { - border-color: var(--color-slate-700); - } - .border-white\/20 { - border-color: color-mix(in srgb, #fff 20%, transparent); - @supports (color: color-mix(in lab, red, red)) { - border-color: color-mix(in oklab, var(--color-white) 20%, transparent); - } - } - .border-yellow-300 { - border-color: var(--color-yellow-300); - } - .bg-black\/40 { - background-color: color-mix(in srgb, #000 40%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-black) 40%, transparent); - } - } - .bg-blue-100 { - background-color: var(--color-blue-100); - } - .bg-green-100 { - background-color: var(--color-green-100); - } - .bg-red-100 { - background-color: var(--color-red-100); - } - .bg-slate-50 { - background-color: var(--color-slate-50); - } - .bg-slate-100 { - background-color: var(--color-slate-100); - } - .bg-slate-200 { - background-color: var(--color-slate-200); - } - .bg-slate-800 { - background-color: var(--color-slate-800); - } - .bg-slate-900 { - background-color: var(--color-slate-900); - } - .bg-white { - background-color: var(--color-white); - } - .bg-yellow-100 { - background-color: var(--color-yellow-100); - } - .object-cover { - object-fit: cover; - } - .p-3 { - padding: calc(var(--spacing) * 3); - } - .p-4 { - padding: calc(var(--spacing) * 4); - } - .p-6 { - padding: calc(var(--spacing) * 6); - } - .p-10 { - padding: calc(var(--spacing) * 10); - } - .px-2 { - padding-inline: calc(var(--spacing) * 2); - } - .px-3 { - padding-inline: calc(var(--spacing) * 3); - } - .px-4 { - padding-inline: calc(var(--spacing) * 4); - } - .px-5 { - padding-inline: calc(var(--spacing) * 5); - } - .px-8 { - padding-inline: calc(var(--spacing) * 8); - } - .py-1 { - padding-block: calc(var(--spacing) * 1); - } - .py-2 { - padding-block: calc(var(--spacing) * 2); - } - .py-3 { - padding-block: calc(var(--spacing) * 3); - } - .py-4 { - padding-block: calc(var(--spacing) * 4); - } - .pt-4 { - padding-top: calc(var(--spacing) * 4); - } - .pt-6 { - padding-top: calc(var(--spacing) * 6); - } - .pr-8 { - padding-right: calc(var(--spacing) * 8); - } - .text-center { - text-align: center; - } - .text-left { - text-align: left; - } - .text-right { - text-align: right; - } - .text-2xl { - font-size: var(--text-2xl); - line-height: var(--tw-leading, var(--text-2xl--line-height)); - } - .text-3xl { - font-size: var(--text-3xl); - line-height: var(--tw-leading, var(--text-3xl--line-height)); - } - .text-5xl { - font-size: var(--text-5xl); - line-height: var(--tw-leading, var(--text-5xl--line-height)); - } - .text-lg { - font-size: var(--text-lg); - line-height: var(--tw-leading, var(--text-lg--line-height)); - } - .text-sm { - font-size: var(--text-sm); - line-height: var(--tw-leading, var(--text-sm--line-height)); - } - .text-xl { - font-size: var(--text-xl); - line-height: var(--tw-leading, var(--text-xl--line-height)); - } - .text-xs { - font-size: var(--text-xs); - line-height: var(--tw-leading, var(--text-xs--line-height)); - } - .font-bold { - --tw-font-weight: var(--font-weight-bold); - font-weight: var(--font-weight-bold); - } - .font-medium { - --tw-font-weight: var(--font-weight-medium); - font-weight: var(--font-weight-medium); - } - .font-semibold { - --tw-font-weight: var(--font-weight-semibold); - font-weight: var(--font-weight-semibold); - } - .tracking-wide { - --tw-tracking: var(--tracking-wide); - letter-spacing: var(--tracking-wide); - } - .text-blue-600 { - color: var(--color-blue-600); - } - .text-blue-800 { - color: var(--color-blue-800); - } - .text-green-600 { - color: var(--color-green-600); - } - .text-green-800 { - color: var(--color-green-800); - } - .text-orange-500 { - color: var(--color-orange-500); - } - .text-orange-600 { - color: var(--color-orange-600); - } - .text-red-500 { - color: var(--color-red-500); - } - .text-red-600 { - color: var(--color-red-600); - } - .text-red-800 { - color: var(--color-red-800); - } - .text-slate-400 { - color: var(--color-slate-400); - } - .text-slate-500 { - color: var(--color-slate-500); - } - .text-slate-600 { - color: var(--color-slate-600); - } - .text-slate-700 { - color: var(--color-slate-700); - } - .text-slate-900 { - color: var(--color-slate-900); - } - .text-white { - color: var(--color-white); - } - .text-yellow-800 { - color: var(--color-yellow-800); - } - .uppercase { - text-transform: uppercase; - } - .placeholder-slate-400 { - &::placeholder { - color: var(--color-slate-400); - } - } - .shadow { - --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); - box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); - } - .shadow-lg { - --tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); - box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); - } - .transition { - transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, content-visibility, overlay, pointer-events; - transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); - transition-duration: var(--tw-duration, var(--default-transition-duration)); - } - .transition-colors { - transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to; - transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); - transition-duration: var(--tw-duration, var(--default-transition-duration)); - } - .transition-transform { - transition-property: transform, translate, scale, rotate; - transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); - transition-duration: var(--tw-duration, var(--default-transition-duration)); - } - .duration-200 { - --tw-duration: 200ms; - transition-duration: 200ms; - } - .duration-300 { - --tw-duration: 300ms; - transition-duration: 300ms; - } - .hover\:bg-slate-700 { - &:hover { - @media (hover: hover) { - background-color: var(--color-slate-700); - } - } - } - .hover\:text-slate-500 { - &:hover { - @media (hover: hover) { - color: var(--color-slate-500); - } - } - } - .hover\:shadow-lg { - &:hover { - @media (hover: hover) { - --tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); - box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); - } - } - } - .focus\:border-transparent { - &:focus { - border-color: transparent; - } - } - .focus\:ring-2 { - &:focus { - --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); - box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); - } - } - .focus\:ring-blue-500 { - &:focus { - --tw-ring-color: var(--color-blue-500); - } - } - .focus\:outline-none { - &:focus { - --tw-outline-style: none; - outline-style: none; - } - } - .sm\:grid-cols-2 { - @media (width >= 40rem) { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - } - .sm\:flex-row { - @media (width >= 40rem) { - flex-direction: row; - } - } - .sm\:items-center { - @media (width >= 40rem) { - align-items: center; - } - } - .sm\:justify-between { - @media (width >= 40rem) { - justify-content: space-between; - } - } - .md\:col-span-2 { - @media (width >= 48rem) { - grid-column: span 2 / span 2; - } - } - .md\:grid-cols-2 { - @media (width >= 48rem) { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - } - .md\:grid-cols-3 { - @media (width >= 48rem) { - grid-template-columns: repeat(3, minmax(0, 1fr)); - } - } - .lg\:ml-72 { - @media (width >= 64rem) { - margin-left: calc(var(--spacing) * 72); - } - } - .lg\:hidden { - @media (width >= 64rem) { - display: none; - } - } - .lg\:w-auto { - @media (width >= 64rem) { - width: auto; - } - } - .lg\:translate-x-0 { - @media (width >= 64rem) { - --tw-translate-x: calc(var(--spacing) * 0); - translate: var(--tw-translate-x) var(--tw-translate-y); - } - } - .lg\:grid-cols-2 { - @media (width >= 64rem) { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - } - .lg\:grid-cols-3 { - @media (width >= 64rem) { - grid-template-columns: repeat(3, minmax(0, 1fr)); - } - } - .xl\:grid-cols-3 { - @media (width >= 80rem) { - grid-template-columns: repeat(3, minmax(0, 1fr)); - } - } - .xl\:grid-cols-4 { - @media (width >= 80rem) { - grid-template-columns: repeat(4, minmax(0, 1fr)); - } - } - .dark\:border-slate-600 { - &:where(.dark, .dark *) { - border-color: var(--color-slate-600); - } - } - .dark\:bg-slate-600 { - &:where(.dark, .dark *) { - background-color: var(--color-slate-600); - } - } - .dark\:bg-slate-700 { - &:where(.dark, .dark *) { - background-color: var(--color-slate-700); - } - } - .dark\:bg-slate-800 { - &:where(.dark, .dark *) { - background-color: var(--color-slate-800); - } - } - .dark\:bg-slate-900 { - &:where(.dark, .dark *) { - background-color: var(--color-slate-900); - } - } - .dark\:text-slate-200 { - &:where(.dark, .dark *) { - color: var(--color-slate-200); - } - } - .dark\:text-slate-300 { - &:where(.dark, .dark *) { - color: var(--color-slate-300); - } - } - .dark\:text-white { - &:where(.dark, .dark *) { - color: var(--color-white); - } - } -} -@layer components { - .company_link { - --tw-font-weight: var(--font-weight-semibold); - font-weight: var(--font-weight-semibold); - transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to; - transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); - transition-duration: var(--tw-duration, var(--default-transition-duration)); - color: var(--company-color); - } - .company_link:hover { - opacity: 0.8; - } - .btn { - display: inline-flex; - align-items: center; - justify-content: center; - border-radius: var(--radius-lg); - padding-inline: calc(var(--spacing) * 5); - padding-block: calc(var(--spacing) * 2); - --tw-font-weight: var(--font-weight-medium); - font-weight: var(--font-weight-medium); - --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); - box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); - transition-property: all; - transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); - transition-duration: var(--tw-duration, var(--default-transition-duration)); - --tw-duration: 200ms; - transition-duration: 200ms; - &:hover { - @media (hover: hover) { - --tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); - box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); - } - } - &:active { - scale: 0.98; - } - &:disabled { - pointer-events: none; - } - &:disabled { - opacity: 50%; - } - } - .btn:hover { - opacity: 0.8; - } - .btn-save { - color: var(--color-white); - background-color: var(--company-color); - } - .btn-delete { - color: var(--color-white); - background-color: #cc4d49; - } - .btn-edit { - color: var(--color-white); - background-color: var(--company-color); - } - .btn-add { - color: var(--color-white); - background-color: var(--company-color); - } - .btn-action { - color: var(--color-white); - background: var(--company-color); - } - .btn-action-start { - color: var(--color-white); - background-color: #059669; - } - .btn-action-stop { - color: var(--color-white); - background-color: #cc4d49; - } -} -@property --tw-translate-x { - syntax: "*"; - inherits: false; - initial-value: 0; -} -@property --tw-translate-y { - syntax: "*"; - inherits: false; - initial-value: 0; -} -@property --tw-translate-z { - syntax: "*"; - inherits: false; - initial-value: 0; -} -@property --tw-rotate-x { - syntax: "*"; - inherits: false; -} -@property --tw-rotate-y { - syntax: "*"; - inherits: false; -} -@property --tw-rotate-z { - syntax: "*"; - inherits: false; -} -@property --tw-skew-x { - syntax: "*"; - inherits: false; -} -@property --tw-skew-y { - syntax: "*"; - inherits: false; -} -@property --tw-space-y-reverse { - syntax: "*"; - inherits: false; - initial-value: 0; -} -@property --tw-divide-y-reverse { - syntax: "*"; - inherits: false; - initial-value: 0; -} -@property --tw-border-style { - syntax: "*"; - inherits: false; - initial-value: solid; -} -@property --tw-font-weight { - syntax: "*"; - inherits: false; -} -@property --tw-tracking { - syntax: "*"; - inherits: false; -} -@property --tw-shadow { - syntax: "*"; - inherits: false; - initial-value: 0 0 #0000; -} -@property --tw-shadow-color { - syntax: "*"; - inherits: false; -} -@property --tw-shadow-alpha { - syntax: ""; - inherits: false; - initial-value: 100%; -} -@property --tw-inset-shadow { - syntax: "*"; - inherits: false; - initial-value: 0 0 #0000; -} -@property --tw-inset-shadow-color { - syntax: "*"; - inherits: false; -} -@property --tw-inset-shadow-alpha { - syntax: ""; - inherits: false; - initial-value: 100%; -} -@property --tw-ring-color { - syntax: "*"; - inherits: false; -} -@property --tw-ring-shadow { - syntax: "*"; - inherits: false; - initial-value: 0 0 #0000; -} -@property --tw-inset-ring-color { - syntax: "*"; - inherits: false; -} -@property --tw-inset-ring-shadow { - syntax: "*"; - inherits: false; - initial-value: 0 0 #0000; -} -@property --tw-ring-inset { - syntax: "*"; - inherits: false; -} -@property --tw-ring-offset-width { - syntax: ""; - inherits: false; - initial-value: 0px; -} -@property --tw-ring-offset-color { - syntax: "*"; - inherits: false; - initial-value: #fff; -} -@property --tw-ring-offset-shadow { - syntax: "*"; - inherits: false; - initial-value: 0 0 #0000; -} -@property --tw-duration { - syntax: "*"; - inherits: false; -} -@layer properties { - @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) { - *, ::before, ::after, ::backdrop { - --tw-translate-x: 0; - --tw-translate-y: 0; - --tw-translate-z: 0; - --tw-rotate-x: initial; - --tw-rotate-y: initial; - --tw-rotate-z: initial; - --tw-skew-x: initial; - --tw-skew-y: initial; - --tw-space-y-reverse: 0; - --tw-divide-y-reverse: 0; - --tw-border-style: solid; - --tw-font-weight: initial; - --tw-tracking: initial; - --tw-shadow: 0 0 #0000; - --tw-shadow-color: initial; - --tw-shadow-alpha: 100%; - --tw-inset-shadow: 0 0 #0000; - --tw-inset-shadow-color: initial; - --tw-inset-shadow-alpha: 100%; - --tw-ring-color: initial; - --tw-ring-shadow: 0 0 #0000; - --tw-inset-ring-color: initial; - --tw-inset-ring-shadow: 0 0 #0000; - --tw-ring-inset: initial; - --tw-ring-offset-width: 0px; - --tw-ring-offset-color: #fff; - --tw-ring-offset-shadow: 0 0 #0000; - --tw-duration: initial; - } - } -} +@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-duration:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-100:oklch(93.6% .032 17.717);--color-red-300:oklch(80.8% .114 19.571);--color-red-500:oklch(63.7% .237 25.331);--color-red-600:oklch(57.7% .245 27.325);--color-red-800:oklch(44.4% .177 26.899);--color-orange-500:oklch(70.5% .213 47.604);--color-orange-600:oklch(64.6% .222 41.116);--color-yellow-100:oklch(97.3% .071 103.193);--color-yellow-300:oklch(90.5% .182 98.111);--color-yellow-800:oklch(47.6% .114 61.907);--color-green-100:oklch(96.2% .044 156.743);--color-green-300:oklch(87.1% .15 154.449);--color-green-600:oklch(62.7% .194 149.214);--color-green-800:oklch(44.8% .119 151.328);--color-blue-100:oklch(93.2% .032 255.585);--color-blue-300:oklch(80.9% .105 251.813);--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-blue-800:oklch(42.4% .199 265.638);--color-slate-50:oklch(98.4% .003 247.858);--color-slate-100:oklch(96.8% .007 247.896);--color-slate-200:oklch(92.9% .013 255.508);--color-slate-300:oklch(86.9% .022 252.894);--color-slate-400:oklch(70.4% .04 256.788);--color-slate-500:oklch(55.4% .046 257.417);--color-slate-600:oklch(44.6% .043 257.281);--color-slate-700:oklch(37.2% .044 257.287);--color-slate-800:oklch(27.9% .041 260.031);--color-slate-900:oklch(20.8% .042 265.755);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-md:28rem;--container-5xl:64rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--text-5xl:3rem;--text-5xl--line-height:1;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-wide:.025em;--radius-lg:.5rem;--radius-xl:.75rem;--radius-2xl:1rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components{.company_link{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));color:var(--company-color)}.company_link:hover{opacity:.8}.btn{border-radius:var(--radius-lg);padding-inline:calc(var(--spacing) * 5);padding-block:calc(var(--spacing) * 2);--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.2s;justify-content:center;align-items:center;transition-duration:.2s;display:inline-flex}@media (hover:hover){.btn:hover{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}}.btn:active{scale:.98}.btn:disabled{pointer-events:none;opacity:.5}.btn:hover{opacity:.8}.btn-save{color:var(--color-white);background-color:var(--company-color)}.btn-delete{color:var(--color-white);background-color:#cc4d49}.btn-edit,.btn-add{color:var(--color-white);background-color:var(--company-color)}.btn-action{color:var(--color-white);background:var(--company-color)}.btn-action-start{color:var(--color-white);background-color:#059669}.btn-action-stop{color:var(--color-white);background-color:#cc4d49}}@layer utilities{.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.inset-0{inset:calc(var(--spacing) * 0)}.top-0{top:calc(var(--spacing) * 0)}.top-3{top:calc(var(--spacing) * 3)}.top-5{top:calc(var(--spacing) * 5)}.right-3{right:calc(var(--spacing) * 3)}.right-5{right:calc(var(--spacing) * 5)}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.mx-auto{margin-inline:auto}.my-8{margin-block:calc(var(--spacing) * 8)}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-5{margin-top:calc(var(--spacing) * 5)}.mt-6{margin-top:calc(var(--spacing) * 6)}.mt-8{margin-top:calc(var(--spacing) * 8)}.mt-10{margin-top:calc(var(--spacing) * 10)}.mt-14{margin-top:calc(var(--spacing) * 14)}.mb-1{margin-bottom:calc(var(--spacing) * 1)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.mb-8{margin-bottom:calc(var(--spacing) * 8)}.mb-10{margin-bottom:calc(var(--spacing) * 10)}.ml-4{margin-left:calc(var(--spacing) * 4)}.ml-auto{margin-left:auto}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.table{display:table}.h-4{height:calc(var(--spacing) * 4)}.h-16{height:calc(var(--spacing) * 16)}.h-32{height:calc(var(--spacing) * 32)}.h-64{height:calc(var(--spacing) * 64)}.h-full{height:100%}.h-screen{height:100vh}.min-h-\[80vh\]{min-height:80vh}.min-h-screen{min-height:100vh}.w-4{width:calc(var(--spacing) * 4)}.w-5xl{width:var(--container-5xl)}.w-24{width:calc(var(--spacing) * 24)}.w-28{width:calc(var(--spacing) * 28)}.w-72{width:calc(var(--spacing) * 72)}.w-80{width:calc(var(--spacing) * 80)}.w-full{width:100%}.w-screen{width:100vw}.max-w-5xl{max-width:var(--container-5xl)}.max-w-md{max-width:var(--container-md)}.min-w-\[700px\]{min-width:700px}.min-w-\[1200px\]{min-width:1200px}.flex-1{flex:1}.-translate-x-full{--tw-translate-x:-100%;translate:var(--tw-translate-x) var(--tw-translate-y)}.rotate-180{rotate:180deg}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-end{align-items:flex-end}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-5{gap:calc(var(--spacing) * 5)}.gap-6{gap:calc(var(--spacing) * 6)}.gap-8{gap:calc(var(--spacing) * 8)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:var(--radius-2xl)}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-blue-300{border-color:var(--color-blue-300)}.border-green-300{border-color:var(--color-green-300)}.border-orange-500{border-color:var(--color-orange-500)}.border-red-300{border-color:var(--color-red-300)}.border-slate-300{border-color:var(--color-slate-300)}.border-slate-700{border-color:var(--color-slate-700)}.border-white\/20{border-color:#fff3}@supports (color:color-mix(in lab, red, red)){.border-white\/20{border-color:color-mix(in oklab, var(--color-white) 20%, transparent)}}.border-yellow-300{border-color:var(--color-yellow-300)}.bg-black\/40{background-color:#0006}@supports (color:color-mix(in lab, red, red)){.bg-black\/40{background-color:color-mix(in oklab, var(--color-black) 40%, transparent)}}.bg-blue-100{background-color:var(--color-blue-100)}.bg-green-100{background-color:var(--color-green-100)}.bg-red-100{background-color:var(--color-red-100)}.bg-slate-50{background-color:var(--color-slate-50)}.bg-slate-100{background-color:var(--color-slate-100)}.bg-slate-200{background-color:var(--color-slate-200)}.bg-slate-800{background-color:var(--color-slate-800)}.bg-slate-900{background-color:var(--color-slate-900)}.bg-white{background-color:var(--color-white)}.bg-yellow-100{background-color:var(--color-yellow-100)}.object-cover{object-fit:cover}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-6{padding:calc(var(--spacing) * 6)}.p-10{padding:calc(var(--spacing) * 10)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.px-8{padding-inline:calc(var(--spacing) * 8)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-4{padding-block:calc(var(--spacing) * 4)}.pt-4{padding-top:calc(var(--spacing) * 4)}.pt-6{padding-top:calc(var(--spacing) * 6)}.pr-8{padding-right:calc(var(--spacing) * 8)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.text-blue-600{color:var(--color-blue-600)}.text-blue-800{color:var(--color-blue-800)}.text-green-600{color:var(--color-green-600)}.text-green-800{color:var(--color-green-800)}.text-orange-500{color:var(--color-orange-500)}.text-orange-600{color:var(--color-orange-600)}.text-red-500{color:var(--color-red-500)}.text-red-600{color:var(--color-red-600)}.text-red-800{color:var(--color-red-800)}.text-slate-400{color:var(--color-slate-400)}.text-slate-500{color:var(--color-slate-500)}.text-slate-600{color:var(--color-slate-600)}.text-slate-700{color:var(--color-slate-700)}.text-slate-900{color:var(--color-slate-900)}.text-white{color:var(--color-white)}.text-yellow-800{color:var(--color-yellow-800)}.uppercase{text-transform:uppercase}.placeholder-slate-400::placeholder{color:var(--color-slate-400)}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-200{--tw-duration:.2s;transition-duration:.2s}.duration-300{--tw-duration:.3s;transition-duration:.3s}@media (hover:hover){.hover\:bg-slate-700:hover{background-color:var(--color-slate-700)}.hover\:text-slate-500:hover{color:var(--color-slate-500)}.hover\:shadow-lg:hover{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}}.focus\:border-transparent:focus{border-color:#0000}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus\:ring-blue-500:focus{--tw-ring-color:var(--color-blue-500)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}@media (min-width:40rem){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:flex-row{flex-direction:row}.sm\:items-center{align-items:center}.sm\:justify-between{justify-content:space-between}}@media (min-width:48rem){.md\:col-span-2{grid-column:span 2/span 2}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}@media (min-width:64rem){.lg\:ml-72{margin-left:calc(var(--spacing) * 72)}.lg\:hidden{display:none}.lg\:w-auto{width:auto}.lg\:translate-x-0{--tw-translate-x:calc(var(--spacing) * 0);translate:var(--tw-translate-x) var(--tw-translate-y)}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}@media (min-width:80rem){.xl\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.xl\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}.dark\:border-slate-600:where(.dark,.dark *){border-color:var(--color-slate-600)}.dark\:bg-slate-600:where(.dark,.dark *){background-color:var(--color-slate-600)}.dark\:bg-slate-700:where(.dark,.dark *){background-color:var(--color-slate-700)}.dark\:bg-slate-800:where(.dark,.dark *){background-color:var(--color-slate-800)}.dark\:bg-slate-900:where(.dark,.dark *){background-color:var(--color-slate-900)}.dark\:text-slate-200:where(.dark,.dark *){color:var(--color-slate-200)}.dark\:text-slate-300:where(.dark,.dark *){color:var(--color-slate-300)}.dark\:text-white:where(.dark,.dark *){color:var(--color-white)}}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-duration{syntax:"*";inherits:false} \ No newline at end of file diff --git a/time_tracking/migrations/0001_initial.py b/time_tracking/migrations/0001_initial.py index efd72b6..73f1cbe 100644 --- a/time_tracking/migrations/0001_initial.py +++ b/time_tracking/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 6.0.6 on 2026-06-23 22:12 +# Generated by Django 6.0.6 on 2026-06-27 17:19 import django.db.models.deletion from django.conf import settings