Initial commit

This commit is contained in:
Felix Baumann
2026-06-27 17:07:34 +00:00
commit fb47c53d52
256 changed files with 21270 additions and 0 deletions
View File
+275
View File
@@ -0,0 +1,275 @@
from django.contrib import admin
from .models import (
Customer,
CustomerContact,
CustomerLocation,
Item,
Invoice,
InvoiceItem,
Unit,
Reminder
)
@admin.register(Unit)
class UnitAdmin(admin.ModelAdmin):
list_display = (
"name",
"short_name",
)
search_fields = (
"name",
"short_name",
)
class CustomerContactInline(admin.TabularInline):
model = CustomerContact
extra = 0
class CustomerLocationInline(admin.TabularInline):
model = CustomerLocation
extra = 0
@admin.register(Customer)
class CustomerAdmin(admin.ModelAdmin):
list_display = (
"customer_number",
"company_name",
"customer_type",
"phone",
"email",
)
search_fields = (
"customer_number",
"company_name",
"email",
"phone",
"tax_number",
"vat_number",
)
list_filter = (
"customer_type",
)
inlines = (
CustomerContactInline,
CustomerLocationInline,
)
@admin.register(CustomerContact)
class CustomerContactAdmin(admin.ModelAdmin):
list_display = (
"first_name",
"last_name",
"customer",
"phone",
"email",
)
search_fields = (
"first_name",
"last_name",
"email",
"phone",
"customer__company_name",
)
@admin.register(CustomerLocation)
class CustomerLocationAdmin(admin.ModelAdmin):
list_display = (
"name",
"customer",
"city",
"country",
)
search_fields = (
"name",
"city",
"street",
"customer__company_name",
)
@admin.register(Item)
class ItemAdmin(admin.ModelAdmin):
list_display = (
"description",
"item_type",
"unit",
"price_net",
)
search_fields = (
"description",
)
list_filter = (
"item_type",
"unit",
)
class InvoiceItemInline(admin.TabularInline):
model = InvoiceItem
extra = 0
@admin.register(Invoice)
class InvoiceAdmin(admin.ModelAdmin):
list_display = (
"invoice_number",
"customer",
"project",
"status",
"issue_date",
"due_date",
)
list_filter = (
"status",
"issue_date",
"customer",
)
search_fields = (
"invoice_number",
"customer__company_name",
)
autocomplete_fields = (
"customer",
"project",
)
@admin.register(InvoiceItem)
class InvoiceItemAdmin(admin.ModelAdmin):
list_display = (
"description",
"invoice",
"quantity",
"unit_price_net",
)
search_fields = (
"description",
"invoice__invoice_number",
)
autocomplete_fields = (
"invoice",
"item",
"time_entry",
"expense",
)
from .models import (
Offer,
OfferItem,
)
class OfferItemInline(admin.TabularInline):
model = OfferItem
extra = 0
@admin.register(Offer)
class OfferAdmin(admin.ModelAdmin):
list_display = (
"offer_number",
"customer",
"status",
"issue_date",
"due_date",
"created_at",
)
list_filter = (
"status",
"issue_date",
)
search_fields = (
"offer_number",
"contact_name",
"customer__company_name",
)
readonly_fields = (
"created_at",
"pdf_generated_at",
)
inlines = [
OfferItemInline,
]
@admin.register(OfferItem)
class OfferItemAdmin(admin.ModelAdmin):
list_display = (
"offer",
"description",
"quantity",
"unit",
"unit_price_net",
"unit_tax_rate",
"created_at",
)
search_fields = (
"description",
"offer__offer_number",
)
autocomplete_fields = (
"offer",
"item",
)
@admin.register(Reminder)
class ReminderAdmin(admin.ModelAdmin):
list_filter = (
"level",
"issue_date",
"due_date",
"pdf_generated_at",
)
search_fields = (
"reminder_number",
)
readonly_fields = (
"created_at",
"pdf_generated_at",
)
ordering = ("-created_at",)
+5
View File
@@ -0,0 +1,5 @@
from django.apps import AppConfig
class InvoicesConfig(AppConfig):
name = 'invoices'
+641
View File
@@ -0,0 +1,641 @@
from django import forms
from core.forms.base import BaseForm
from .models import Customer, Item, Invoice, InvoiceItem, CustomerContact, CustomerLocation, Offer, OfferItem, Reminder
from projects.models import Project
from time_tracking.models import TimeEntry
from finance.models import Expense
from django.utils import timezone
from django.db.models import Q, Max
class CustomerForm(BaseForm):
class Meta:
model = Customer
fields = [
"customer_number",
"customer_type",
"company_name",
"tax_number",
"vat_number",
"phone",
"email",
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.apply_style()
class ItemForm(BaseForm):
class Meta:
model = Item
fields = [
"item_type",
"description",
"unit",
"price_net",
"tax_rate",
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.apply_style()
class CustomerInvoiceForm(BaseForm):
contact = forms.ModelChoiceField(
queryset=CustomerContact.objects.none(),
label="Ansprechpartner",
required=True
)
location = forms.ModelChoiceField(
queryset=CustomerLocation.objects.none(),
label="Standort",
required=True
)
class Meta:
model = Invoice
fields = [
"customer",
"contact",
"location",
"issue_date",
"due_date",
"note",
]
widgets = {
"issue_date": forms.DateInput(attrs={"type": "date"}),
"due_date": forms.DateInput(attrs={"type": "date"}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.apply_style()
customer_id = (
self.data.get("customer")
or getattr(self.instance, "customer_id", None)
)
if customer_id:
self.fields["contact"].queryset = CustomerContact.objects.filter(customer_id=customer_id)
self.fields["location"].queryset = CustomerLocation.objects.filter(customer_id=customer_id)
else:
self.fields["contact"].queryset = CustomerContact.objects.none()
self.fields["location"].queryset = CustomerLocation.objects.none()
class ProjectInvoiceForm(BaseForm):
contact = forms.ModelChoiceField(
queryset=CustomerContact.objects.none(),
required=False,
label="Ansprechpartner",
widget=forms.Select(attrs={
"class": "customer-field"
})
)
location = forms.ModelChoiceField(
queryset=CustomerLocation.objects.none(),
required=False,
label="Standort",
widget=forms.Select(attrs={
"class": "customer-field"
})
)
contact_name_manual = forms.CharField(
required=False,
label="Ansprechpartner",
widget=forms.TextInput(attrs={
"class": "manual-field"
})
)
contact_email_manual = forms.EmailField(
required=False,
label="E-Mail",
widget=forms.EmailInput(attrs={
"class": "manual-field"
})
)
contact_phone_manual = forms.CharField(
required=False,
label="Telefon",
widget=forms.TextInput(attrs={
"class": "manual-field"
})
)
location_name_manual = forms.CharField(
required=False,
label="Standort",
widget=forms.TextInput(attrs={
"class": "manual-field"
})
)
location_street_manual = forms.CharField(
required=False,
label="Straße",
widget=forms.TextInput(attrs={
"class": "manual-field"
})
)
location_zip_manual = forms.CharField(
required=False,
label="PLZ",
widget=forms.TextInput(attrs={
"class": "manual-field"
})
)
location_city_manual = forms.CharField(
required=False,
label="Ort",
widget=forms.TextInput(attrs={
"class": "manual-field"
})
)
location_country_manual = forms.CharField(
required=False,
label="Land",
widget=forms.TextInput(attrs={
"class": "manual-field"
})
)
class Meta:
model = Invoice
fields = [
"project",
"contact",
"location",
"contact_name_manual",
"contact_email_manual",
"contact_phone_manual",
"location_name_manual",
"location_street_manual",
"location_zip_manual",
"location_city_manual",
"location_country_manual",
"issue_date",
"due_date",
"note",
]
widgets = {
"issue_date": forms.DateInput(
attrs={
"type": "date"
}
),
"due_date": forms.DateInput(
attrs={
"type": "date"
}
),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.apply_style()
project_id = self.data.get("project") or getattr(
self.instance,
"project_id",
None
)
if project_id:
project = Project.objects.filter(
id=project_id
).first()
if project and project.customer:
self.fields["contact"].queryset = CustomerContact.objects.filter(
customer=project.customer
)
self.fields["location"].queryset = CustomerLocation.objects.filter(
customer=project.customer
)
class InvoiceItemForm(BaseForm):
class Meta:
model = InvoiceItem
fields = [
"source_type",
"item",
"time_entry",
"expense",
"description",
"quantity",
"unit",
"unit_price_net",
"unit_tax_rate",
'service_period_from',
'service_period_to',
]
widgets = {
"quantity": forms.NumberInput(attrs={
"step": "1.00"
}),
"unit_price_net": forms.NumberInput(attrs={
"step": "1.00"
}),
"unit_tax_rate": forms.NumberInput(attrs={
"step": "1.00"
}),
"issue_date": forms.DateInput(
attrs={
"type": "date"
}
),
"service_period_from": forms.DateInput(
attrs={
"type": "date"
}
),
"service_period_to": forms.DateInput(
attrs={
"type": "date"
}
),
}
def __init__(
self,
*args,
invoice=None,
**kwargs
):
super().__init__(*args, **kwargs)
self.apply_style()
self.invoice = invoice
self.fields["item"].queryset = Item.objects.all()
self.fields["time_entry"].queryset = TimeEntry.objects.none()
self.fields["expense"].queryset = Expense.objects.none()
if invoice and invoice.project:
self.fields["source_type"].choices = [
("item", "Artikel"),
("time", "Zeiterfassung"),
("expense", "Ausgabe"),
]
self.fields["time_entry"].queryset = TimeEntry.objects.filter(
project=invoice.project,end__isnull=False
)
self.fields["expense"].queryset = Expense.objects.filter(
project=invoice.project
)
else:
self.fields["source_type"].choices = [
("item", "Artikel"),
]
self.fields["item"].widget.attrs["class"] += " source-item"
self.fields["time_entry"].widget.attrs["class"] += " source-time"
self.fields["expense"].widget.attrs["class"] += " source-expense"
def save(self, commit=True):
obj = super().save(commit=False)
source = self.cleaned_data.get(
"source_type"
)
# -------------------------
# ARTIKEL
# -------------------------
if source == "item":
item = self.cleaned_data["item"]
obj.item = item
obj.time_entry = None
obj.expense = None
obj.description = self.cleaned_data["description"]
obj.unit = self.cleaned_data["unit"]
obj.unit_price_net = self.cleaned_data["unit_price_net"]
obj.unit_tax_rate = self.cleaned_data["unit_tax_rate"]
# -------------------------
# ZEITERFASSUNG
# -------------------------
elif source == "time":
entry = self.cleaned_data["time_entry"]
obj.item = None
obj.time_entry = entry
obj.expense = None
obj.description = self.cleaned_data["description"]
obj.unit = self.cleaned_data["unit"]
obj.unit_price_net = self.cleaned_data["unit_price_net"]
obj.unit_tax_rate = self.cleaned_data["unit_tax_rate"]
# -------------------------
# AUSGABE
# -------------------------
elif source == "expense":
expense = self.cleaned_data["expense"]
obj.item = None
obj.time_entry = None
obj.expense = expense
obj.description = self.cleaned_data["description"]
obj.unit = self.cleaned_data["unit"]
obj.unit_price_net = self.cleaned_data["unit_price_net"]
obj.unit_tax_rate = self.cleaned_data["unit_tax_rate"]
if commit:
obj.save()
return obj
class OfferForm(BaseForm):
contact = forms.ModelChoiceField(
queryset=CustomerContact.objects.none(),
label="Ansprechpartner",
required=True
)
location = forms.ModelChoiceField(
queryset=CustomerLocation.objects.none(),
label="Standort",
required=True
)
class Meta:
model = Offer
fields = [
"customer",
"contact",
"location",
"issue_date",
"due_date",
"note",
]
widgets = {
"issue_date": forms.DateInput(attrs={"type": "date"}),
"due_date": forms.DateInput(attrs={"type": "date"}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.apply_style()
customer_id = (
self.data.get("customer")
or getattr(self.instance, "customer_id", None)
)
if customer_id:
self.fields["contact"].queryset = CustomerContact.objects.filter(customer_id=customer_id)
self.fields["location"].queryset = CustomerLocation.objects.filter(customer_id=customer_id)
else:
self.fields["contact"].queryset = CustomerContact.objects.none()
self.fields["location"].queryset = CustomerLocation.objects.none()
class OfferItemForm(BaseForm):
class Meta:
model = OfferItem
fields = [
"item",
"description",
"quantity",
"unit",
"unit_price_net",
"unit_tax_rate",
]
widgets = {
"quantity": forms.NumberInput(
attrs={"step": "1.00"}
),
"unit_price_net": forms.NumberInput(
attrs={"step": "1.00"}
),
"unit_tax_rate": forms.NumberInput(
attrs={"step": "1.00"}
),
}
def __init__(
self,
*args,
offer=None,
**kwargs
):
super().__init__(*args, **kwargs)
self.apply_style()
self.offer = offer
self.fields["item"].queryset = (
Item.objects.all()
)
def save(self, commit=True):
obj = super().save(commit=False)
item = self.cleaned_data.get("item")
if item:
obj.item = item
if commit:
obj.save()
return obj
class ReminderForm(BaseForm):
class Meta:
model = Reminder
fields = [
"invoice",
"issue_date",
"due_date",
"fee",
"note",
]
widgets = {
"issue_date": forms.DateInput(attrs={"type": "date"}),
"due_date": forms.DateInput(attrs={"type": "date"}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
possible_invoices = []
invoices = Invoice.objects.filter(
status=Invoice.Status.OVERDUE,
due_date__lt=timezone.now().date(),
).prefetch_related(
"reminders"
)
for invoice in invoices:
reminders = invoice.reminders.exclude(
status=Reminder.Status.CANCELED
).order_by("-level")
# Keine Mahnung vorhanden
if not reminders.exists():
possible_invoices.append(invoice)
continue
last_reminder = reminders.first()
# Nur nach expired möglich
if last_reminder.status != Reminder.Status.EXPIRED:
continue
# Maximal 3 Mahnungen
if last_reminder.level >= 3:
continue
possible_invoices.append(invoice)
self.fields["invoice"].queryset = Invoice.objects.filter(
id__in=[i.id for i in possible_invoices]
).order_by("due_date")
self.apply_style()
+145
View File
@@ -0,0 +1,145 @@
# Generated by Django 6.0.6 on 2026-06-23 22:12
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Customer',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('customer_number', models.CharField(max_length=50, unique=True, verbose_name='Kunden-ID')),
('customer_type', models.CharField(choices=[('private', 'Privat'), ('company', 'Unternehmen')], default='company', max_length=20, verbose_name='Kundentyp')),
('company_name', models.CharField(max_length=255, verbose_name='Firmenname / Kundenname')),
('tax_number', models.CharField(blank=True, max_length=100, verbose_name='Steuernummer')),
('vat_number', models.CharField(blank=True, max_length=100, verbose_name='UST-IdNr')),
('phone', models.CharField(blank=True, max_length=100, verbose_name='Telefon')),
('email', models.EmailField(blank=True, max_length=254, verbose_name='E-Mail')),
('active', models.BooleanField(default=True, verbose_name='Status')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'verbose_name': 'Kunde',
'verbose_name_plural': 'Kunden',
'ordering': ['company_name'],
},
),
migrations.CreateModel(
name='InvoiceItem',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('source_type', models.CharField(choices=[('item', 'Artikel'), ('time', 'Zeiterfassung'), ('expense', 'Ausgabe')], default='item', max_length=20, verbose_name='Quelle')),
('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': 'Rechnungsposition',
'verbose_name_plural': 'Rechnungspositionen',
},
),
migrations.CreateModel(
name='Item',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('item_type', models.CharField(choices=[('service', 'Dienstleistung'), ('product', 'Ware')], default='product', max_length=20, verbose_name='Art')),
('description', models.CharField(max_length=255, verbose_name='Beschreibung')),
('price_net', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Preis Netto')),
('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': 'Posten',
'verbose_name_plural': 'Posten',
'ordering': ['description'],
},
),
migrations.CreateModel(
name='Unit',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50, verbose_name='Bezeichnung')),
('short_name', models.CharField(max_length=20, verbose_name='Kürzel')),
],
options={
'verbose_name': 'Einheit',
'verbose_name_plural': 'Einheiten',
'ordering': ['name'],
},
),
migrations.CreateModel(
name='CustomerContact',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('first_name', models.CharField(max_length=150, verbose_name='Vorname')),
('last_name', models.CharField(max_length=150, verbose_name='Nachname')),
('email', models.EmailField(blank=True, max_length=254, verbose_name='E-Mail')),
('phone', models.CharField(blank=True, max_length=100, verbose_name='Telefon')),
('customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contacts', to='invoices.customer', verbose_name='Kunde')),
],
options={
'verbose_name': 'Ansprechpartner',
'verbose_name_plural': 'Ansprechpartner',
'ordering': ['last_name', 'first_name'],
},
),
migrations.CreateModel(
name='CustomerLocation',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='Bezeichnung')),
('zip_code', models.CharField(blank=True, max_length=20, verbose_name='PLZ')),
('city', models.CharField(blank=True, max_length=255, verbose_name='Ort')),
('street', models.CharField(blank=True, max_length=255, verbose_name='Adresse')),
('country', models.CharField(blank=True, max_length=100, verbose_name='Land')),
('customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='locations', to='invoices.customer', verbose_name='Kunde')),
],
options={
'verbose_name': 'Standort',
'verbose_name_plural': 'Standorte',
'ordering': ['name'],
},
),
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')),
('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')),
('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='invoices', to='invoices.customer', verbose_name='Kunde')),
],
options={
'verbose_name': 'Rechnung',
'verbose_name_plural': 'Rechnungen',
'ordering': ['-issue_date'],
},
),
]
+49
View File
@@ -0,0 +1,49 @@
# Generated by Django 6.0.6 on 2026-06-23 22:12
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('finance', '0002_initial'),
('invoices', '0001_initial'),
('projects', '0001_initial'),
('time_tracking', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='invoice',
name='project',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='invoices', to='projects.project', verbose_name='Projekt'),
),
migrations.AddField(
model_name='invoiceitem',
name='expense',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='finance.expense', verbose_name='Ausgabe'),
),
migrations.AddField(
model_name='invoiceitem',
name='invoice',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='invoices.invoice', verbose_name='Rechnung'),
),
migrations.AddField(
model_name='invoiceitem',
name='time_entry',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='time_tracking.timeentry', verbose_name='Zeiterfassung'),
),
migrations.AddField(
model_name='invoiceitem',
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='item',
name='unit',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='item_items', to='invoices.unit', verbose_name='Einheit'),
),
]
@@ -0,0 +1,23 @@
# 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'),
),
]
@@ -0,0 +1,28 @@
# 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')),
],
),
]
@@ -0,0 +1,40 @@
# 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),
),
]
@@ -0,0 +1,54 @@
# 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)'),
),
]
@@ -0,0 +1,18 @@
# 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'),
),
]
+36
View File
@@ -0,0 +1,36 @@
# 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'],
},
),
]
@@ -0,0 +1,64 @@
# 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',
},
),
]
@@ -0,0 +1,23 @@
# 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'),
),
]
@@ -0,0 +1,31 @@
# 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'),
),
]
@@ -0,0 +1,17 @@
# 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',
),
]
@@ -0,0 +1,18 @@
# 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'),
),
]
@@ -0,0 +1,18 @@
# 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'),
),
]
@@ -0,0 +1,23 @@
# 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)'),
),
]
@@ -0,0 +1,33 @@
# 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)'),
),
]
@@ -0,0 +1,18 @@
# 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'),
),
]
@@ -0,0 +1,18 @@
# 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'),
),
]
@@ -0,0 +1,18 @@
# 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'),
),
]
@@ -0,0 +1,18 @@
# 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'),
),
]
@@ -0,0 +1,18 @@
# 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'),
),
]
@@ -0,0 +1,23 @@
# 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'),
),
]
@@ -0,0 +1,18 @@
# 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'),
),
]
@@ -0,0 +1,28 @@
# 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 (%)'),
),
]
@@ -0,0 +1,25 @@
# 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',
),
]
View File
+595
View File
@@ -0,0 +1,595 @@
from django.db import models
from django.core.exceptions import ValidationError
from core.models import CompanySetting
class Unit(models.Model):
name = models.CharField(
max_length=50,
verbose_name="Bezeichnung"
)
short_name = models.CharField(
max_length=20,
verbose_name="Kürzel"
)
class Meta:
verbose_name = "Einheit"
verbose_name_plural = "Einheiten"
ordering = ["name"]
def __str__(self):
return self.name
class Customer(models.Model):
class CustomerType(models.TextChoices):
PRIVATE = "private", "Privat"
COMPANY = "company", "Unternehmen"
customer_number = models.CharField(max_length=50, unique=True, verbose_name="Kunden-ID")
customer_type = models.CharField(max_length=20, choices=CustomerType.choices, default=CustomerType.COMPANY, verbose_name="Kundentyp")
company_name = models.CharField(max_length=255, verbose_name="Firmenname / Kundenname")
tax_number = models.CharField(max_length=100, blank=True, verbose_name="Steuernummer")
vat_number = models.CharField(max_length=100, blank=True, verbose_name="UST-IdNr")
phone = models.CharField(max_length=100, blank=True, verbose_name="Telefon")
email = models.EmailField(blank=True, verbose_name="E-Mail")
active = models.BooleanField(default=True, verbose_name="Status")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = "Kunde"
verbose_name_plural = "Kunden"
ordering = ["company_name"]
def __str__(self):
return f"{self.customer_number} - {self.company_name}"
class CustomerContact(models.Model):
customer = models.ForeignKey(Customer, on_delete=models.CASCADE, related_name="contacts", verbose_name="Kunde")
first_name = models.CharField(max_length=150, verbose_name="Vorname")
last_name = models.CharField(max_length=150, verbose_name="Nachname")
email = models.EmailField(blank=True, verbose_name="E-Mail")
phone = models.CharField(max_length=100, blank=True, verbose_name="Telefon")
class Meta:
verbose_name = "Ansprechpartner"
verbose_name_plural = "Ansprechpartner"
ordering = ["last_name", "first_name"]
def __str__(self):
return f"{self.first_name} {self.last_name}"
class CustomerLocation(models.Model):
customer = models.ForeignKey(Customer, on_delete=models.CASCADE, related_name="locations", verbose_name="Kunde")
name = models.CharField(max_length=255, verbose_name="Bezeichnung")
zip_code = models.CharField(max_length=20, blank=True, verbose_name="PLZ")
city = models.CharField(max_length=255, blank=True, verbose_name="Ort")
street = models.CharField(max_length=255, blank=True, verbose_name="Adresse")
country = models.CharField(max_length=100, blank=True, verbose_name="Land")
class Meta:
verbose_name = "Standort"
verbose_name_plural = "Standorte"
ordering = ["name"]
def __str__(self):
return self.name
class Item(models.Model):
class ItemType(models.TextChoices):
SERVICE = "service", "Dienstleistung"
PRODUCT = "product", "Ware"
item_type = models.CharField(max_length=20, choices=ItemType.choices, default=ItemType.PRODUCT, verbose_name="Art")
description = models.CharField(max_length=255, verbose_name="Beschreibung")
unit = models.ForeignKey(Unit,on_delete=models.PROTECT,related_name="item_items",verbose_name="Einheit")
price_net = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="Preis Netto")
tax_rate = models.DecimalField(
max_digits=5,
decimal_places=2,
default=19.00,
verbose_name="MwSt. Satz (%)"
)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name = "Posten"
verbose_name_plural = "Posten"
ordering = ["description"]
def __str__(self):
return self.description
class Invoice(models.Model):
class Status(models.TextChoices):
DRAFT = "draft", "Entwurf"
SENT = "sent", "Versendet"
PAID = "paid", "Bezahlt"
OVERDUE = "overdue", "Überfällig"
CANCELED = "canceled", "Storniert"
invoice_number = models.CharField(max_length=50,verbose_name="Rechnungsnummer", blank=True)
customer = models.ForeignKey(Customer, on_delete=models.PROTECT, related_name="invoices", null=True, blank=True, verbose_name="Kunde")
project = models.ForeignKey("projects.Project", on_delete=models.SET_NULL, null=True, blank=True, related_name="invoices", verbose_name="Projekt")
status = models.CharField(max_length=20, choices=Status.choices, default=Status.DRAFT, verbose_name="Status")
issue_date = models.DateField(verbose_name="Rechnungsdatum")
due_date = models.DateField(null=True, blank=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(upload_to="invoice_pdfs/",null=True,blank=True,verbose_name="PDF")
pdf_generated_at = models.DateTimeField(null=True,blank=True,verbose_name="PDF erstellt am")
cancel_number = models.CharField(max_length=50,unique=True,null=True,blank=True,verbose_name="Stornorechnungsnummer")
pdf_canceled_file = models.FileField(upload_to="invoice_pdfs/",null=True,blank=True,verbose_name="Stornorechnung")
pdf_canceled_generated_at = models.DateTimeField(null=True,blank=True,verbose_name="Stornorechnung erstellt am")
tax_number = models.CharField(max_length=100, blank=True, verbose_name="Steuernummer Kunde")
vat_number = models.CharField(max_length=100, blank=True, verbose_name="USt-IdNr Kunde")
contact_name = models.CharField(max_length=255, verbose_name="Ansprechpartner")
contact_email = models.EmailField(blank=True, verbose_name="E-Mail Ansprechpartner")
contact_phone = models.CharField(max_length=100, blank=True, verbose_name="Telefon Ansprechpartner")
location_name = models.CharField(max_length=255, verbose_name="Standort")
location_street = models.CharField(max_length=255, blank=True, verbose_name="Straße")
location_city = models.CharField(max_length=255, blank=True, verbose_name="Ort")
location_zip = models.CharField(max_length=20, blank=True, verbose_name="PLZ")
location_country = models.CharField(max_length=100, blank=True, verbose_name="Land")
class Meta:
verbose_name = "Rechnung"
verbose_name_plural = "Rechnungen"
ordering = ["-issue_date"]
def save(self, *args, **kwargs):
if self.pk:
old = Invoice.objects.get(pk=self.pk)
if old.status in [
self.Status.SENT,
self.Status.PAID,
self.Status.CANCELED,
]:
protected_fields = [
field.name
for field in self._meta.fields
if field.name not in [
"status",
"pdf_file",
"pdf_generated_at",
"created_at",
"pdf_canceled_file",
"pdf_canceled_generated_at",
"cancel_number",
]
]
for field in protected_fields:
if getattr(old, field) != getattr(self, field):
raise ValidationError(
"Eine finale Rechnung kann nicht mehr bearbeitet werden."
)
super().save(*args, **kwargs)
def __str__(self):
return self.invoice_number
class InvoiceItem(models.Model):
invoice = models.ForeignKey(Invoice,on_delete=models.CASCADE,related_name="items",verbose_name="Rechnung")
source_type = models.CharField(max_length=20,choices=[("item","Artikel"),("time","Zeiterfassung"),("expense","Ausgabe"),],default="item",verbose_name="Quelle",)
item = models.ForeignKey(Item,on_delete=models.SET_NULL,null=True,blank=True,verbose_name="Artikel") # Quelle 1
time_entry = models.ForeignKey("time_tracking.TimeEntry",on_delete=models.SET_NULL,null=True,blank=True,verbose_name="Zeiterfassung") # Quelle 2
expense = models.ForeignKey("finance.Expense",on_delete=models.SET_NULL,null=True,blank=True,verbose_name="Ausgabe") # Quelle 3
description = models.CharField(max_length=255,verbose_name="Beschreibung")
quantity = models.DecimalField(max_digits=10,decimal_places=2,default=1,verbose_name="Menge")
unit = models.CharField(max_length=50,verbose_name="Einheit")
unit_price_net = models.DecimalField(max_digits=10,decimal_places=2,verbose_name="Einzelpreis Netto")
unit_tax_rate = models.DecimalField(max_digits=5,decimal_places=2,default=19.00,verbose_name="MwSt. Satz (%)")
service_period_from = models.DateField(null=True,blank=True,verbose_name="Leistungszeitraum von",)
service_period_to = models.DateField(null=True,blank=True,verbose_name="Leistungszeitraum bis",)
created_at = models.DateTimeField(auto_now_add=True)
@property
def calculated_total_net(self):
return self.quantity * self.unit_price_net
class Meta:
verbose_name = "Rechnungsposition"
verbose_name_plural = "Rechnungspositionen"
def clean(self):
sources = [
self.item,
self.time_entry,
self.expense,
]
selected = sum(1 for s in sources if s)
if selected != 1:
raise ValidationError(
"Genau eine Quelle muss ausgewählt werden."
)
def __str__(self):
return self.description
class Reminder(models.Model):
class Status(models.TextChoices):
DRAFT = "draft", "Entwurf"
SENT = "sent", "Versendet"
PAID = "paid", "Bezahlt"
#PARTIALLY_PAID = "partial", "Teilweise bezahlt"
CANCELED = "canceled", "Storniert"
EXPIRED = "expired", "Frist abgelaufen"
invoice = models.ForeignKey("Invoice",on_delete=models.PROTECT,related_name="reminders",verbose_name="Rechnung")
reminder_number = models.CharField(max_length=50,verbose_name="Mahnungsnummer", blank=True)
status = models.CharField(
max_length=20,
choices=Status.choices,
default=Status.DRAFT,
verbose_name="Status",
)
level = models.PositiveSmallIntegerField(default=1,verbose_name="Mahnstufe",help_text="1 = Erste Mahnung, 2 = Zweite Mahnung, 3 = Letzte Mahnung",)
issue_date = models.DateField(verbose_name="Mahndatum",)
due_date = models.DateField(null=True,blank=True,verbose_name="Neue Zahlungsfrist",)
fee = models.DecimalField(max_digits=10,decimal_places=2,default=0,verbose_name="Mahngebühr",)
note = models.TextField(blank=True,verbose_name="Hinweis",)
pdf_file = models.FileField(upload_to="reminder_pdfs/",null=True,blank=True,verbose_name="PDF",)
pdf_generated_at = models.DateTimeField(null=True,blank=True,verbose_name="PDF erstellt am",)
created_at = models.DateTimeField(auto_now_add=True,)
class Meta:
ordering = ["-created_at"]
verbose_name = "Mahnung"
verbose_name_plural = "Mahnungen"
def __str__(self):
return self.reminder_number
class Offer(models.Model):
class Status(models.TextChoices):
DRAFT = "draft", "Entwurf"
SENT = "sent", "Versendet"
ACCEPTED = "accepted", "Akzeptiert"
REJECTED = "rejected", "Abgelehnt"
EXPIRED = "expired", "Ausgelaufen"
offer_number = models.CharField(max_length=50,verbose_name="Angebotsnummer", blank=True)
customer = models.ForeignKey(
Customer,
on_delete=models.PROTECT,
related_name="offers",
null=True,
blank=True,
verbose_name="Kunde",
)
status = models.CharField(
max_length=20,
choices=Status.choices,
default=Status.DRAFT,
verbose_name="Status",
)
issue_date = models.DateField(
verbose_name="Angebotsdatum",
)
due_date = models.DateField(
null=True,
blank=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(
upload_to="offer_pdfs/",
null=True,
blank=True,
verbose_name="PDF",
)
pdf_generated_at = models.DateTimeField(
null=True,
blank=True,
verbose_name="PDF erstellt am",
)
tax_number = models.CharField(
max_length=100,
blank=True,
verbose_name="Steuernummer Kunde",
)
vat_number = models.CharField(
max_length=100,
blank=True,
verbose_name="USt-IdNr Kunde",
)
contact_name = models.CharField(
max_length=255,
verbose_name="Ansprechpartner",
)
contact_email = models.EmailField(
blank=True,
verbose_name="E-Mail Ansprechpartner",
)
contact_phone = models.CharField(
max_length=100,
blank=True,
verbose_name="Telefon Ansprechpartner",
)
location_name = models.CharField(
max_length=255,
verbose_name="Standort",
)
location_street = models.CharField(
max_length=255,
blank=True,
verbose_name="Straße",
)
location_city = models.CharField(
max_length=255,
blank=True,
verbose_name="Ort",
)
location_zip = models.CharField(
max_length=20,
blank=True,
verbose_name="PLZ",
)
location_country = models.CharField(
max_length=100,
blank=True,
verbose_name="Land",
)
class Meta:
verbose_name = "Angebot"
verbose_name_plural = "Angebote"
ordering = ["-issue_date"]
def save(self, *args, **kwargs):
if self.pk:
old = Offer.objects.get(pk=self.pk)
if old.status in [
self.Status.ACCEPTED,
self.Status.REJECTED,
self.Status.EXPIRED,
]:
protected_fields = [
field.name
for field in self._meta.fields
if field.name not in [
"status",
"pdf_file",
"pdf_generated_at",
]
]
for field in protected_fields:
if getattr(old, field) != getattr(self, field):
raise ValidationError(
"Ein finales Angebot kann nicht mehr bearbeitet werden."
)
super().save(*args, **kwargs)
def __str__(self):
return self.offer_number
class OfferItem(models.Model):
offer = models.ForeignKey(
Offer,
on_delete=models.CASCADE,
related_name="items",
verbose_name="Angebot",
)
item = models.ForeignKey(
Item,
on_delete=models.SET_NULL,
null=True,
blank=True,
verbose_name="Artikel",
)
description = models.CharField(
max_length=255,
verbose_name="Beschreibung",
)
quantity = models.DecimalField(
max_digits=10,
decimal_places=2,
default=1,
verbose_name="Menge",
)
unit = models.CharField(
max_length=50,
verbose_name="Einheit",
)
unit_price_net = models.DecimalField(
max_digits=10,
decimal_places=2,
verbose_name="Einzelpreis Netto",
)
unit_tax_rate = models.DecimalField(
max_digits=5,
decimal_places=2,
default=19.00,
verbose_name="MwSt. Satz (%)",
)
created_at = models.DateTimeField(
auto_now_add=True,
)
@property
def calculated_total_net(self):
return self.quantity * self.unit_price_net
class Meta:
verbose_name = "Angebotsposition"
verbose_name_plural = "Angebotspositionen"
def __str__(self):
return self.description
class DocumentNumberSettings(models.Model):
company = models.OneToOneField(CompanySetting,on_delete=models.CASCADE,related_name="number_settings",verbose_name="Unternehmen")
invoice_format = models.CharField(max_length=100,default="RE-{year}-{number}",verbose_name="Rechnungsformat")
invoice_next_number = models.PositiveIntegerField(default=1,verbose_name="Rechnungs ID (nächste)")
cancel_format = models.CharField(max_length=100,default="ST-{year}-{number}",verbose_name="Stornoformat")
cancel_next_number = models.PositiveIntegerField(default=1,verbose_name="Storno ID (nächste)")
offer_format = models.CharField(max_length=100,default="ANG-{year}-{number}",verbose_name="Angebotsformat")
offer_next_number = models.PositiveIntegerField(default=1,verbose_name="Angebots ID (nächste)")
reminder_format = models.CharField(max_length=100,default="M-{year}-{number}",verbose_name="Mahnungsformat")
reminder_next_number = models.PositiveIntegerField(default=1,verbose_name="Mahnungs ID (nächste)")
class Meta:
verbose_name = "Dokumentnummer"
verbose_name_plural = "Dokumentnummern"
+17
View File
@@ -0,0 +1,17 @@
from masterdata.registry import register_setting
from .models import (
Unit, DocumentNumberSettings
)
register_setting(
group="Rechnungswesen",
name="Einheiten",
model=Unit,
)
register_setting(
group="Rechnungswesen",
name="Dokumentnummern",
model=DocumentNumberSettings,
setting_type="single",
)
+426
View File
@@ -0,0 +1,426 @@
import django_tables2 as tables
from .models import Item, Customer, Reminder, Invoice, InvoiceItem, Offer, OfferItem
class OfferItemTable(tables.Table):
unit_price_net = tables.TemplateColumn(
template_name="invoices/tables/offer_items/unit_price.html",
verbose_name="Einzelpreis",
orderable=False,
)
unit_tax_rate = tables.TemplateColumn(
template_name="invoices/tables/offer_items/tax_rate.html",
verbose_name="Steuerart",
orderable=False,
)
total = tables.TemplateColumn(
template_name="invoices/tables/offer_items/total.html",
verbose_name="Gesamt",
orderable=False,
)
actions = tables.TemplateColumn(
template_name="invoices/tables/offer_items/actions.html",
verbose_name="Aktionen",
orderable=False,
)
class Meta:
model = OfferItem
fields = (
"description",
"quantity",
"unit",
"unit_price_net",
"unit_tax_rate",
"total",
"actions",
)
attrs = {
"class": "w-full text-sm min-w-[700px]"
}
class OfferTable(tables.Table):
customer = tables.Column(
accessor="customer.company_name",
verbose_name="Kunde"
)
status = tables.Column(
accessor="get_status_display",
verbose_name="Status"
)
issue_date = tables.DateColumn(
format="d.m.Y",
verbose_name="Datum"
)
due_date = tables.DateColumn(
format="d.m.Y",
verbose_name="Gültig bis"
)
actions = tables.TemplateColumn(
template_name="invoices/tables/offers/actions.html",
verbose_name="Aktion",
orderable=False
)
class Meta:
model = Offer
fields = (
"offer_number",
"customer",
"status",
"issue_date",
"due_date",
"actions",
)
attrs = {
"class": "w-full text-sm min-w-[700px]"
}
class InvoiceItemTable(tables.Table):
service_period = tables.TemplateColumn(
template_name="invoices/tables/invoice_items/service_period.html",
verbose_name="Leistungsdatum",
orderable=False,
)
unit_price_net = tables.TemplateColumn(
template_name="invoices/tables/invoice_items/unit_price.html",
verbose_name="Einzelpreis",
orderable=False,
)
unit_tax_rate = tables.TemplateColumn(
template_name="invoices/tables/invoice_items/tax_rate.html",
verbose_name="Steuerart",
orderable=False,
)
total = tables.TemplateColumn(
template_name="invoices/tables/invoice_items/total.html",
verbose_name="Gesamt",
orderable=False,
)
actions = tables.TemplateColumn(
template_name="invoices/tables/invoice_items/actions.html",
verbose_name="Aktionen",
orderable=False,
)
class Meta:
model = InvoiceItem
fields = (
"description",
"service_period",
"quantity",
"unit",
"unit_price_net",
"unit_tax_rate",
"total",
"actions",
)
attrs = {
"class": "w-full text-sm min-w-[700px]"
}
class InvoiceTable(tables.Table):
customer = tables.Column(
accessor="customer.company_name",
verbose_name="Kunde"
)
project = tables.Column(
verbose_name="Projekt",
)
status = tables.Column(
accessor="get_status_display",
verbose_name="Status"
)
issue_date = tables.DateColumn(
format="d.m.Y",
verbose_name="Datum"
)
due_date = tables.DateColumn(
format="d.m.Y",
verbose_name="Fälligkeitsdatum"
)
actions = tables.TemplateColumn(
template_name="invoices/tables/invoices/actions.html",
verbose_name="Aktion",
orderable=False
)
class Meta:
model = Invoice
fields = (
"invoice_number",
"customer",
"project",
"status",
"issue_date",
"due_date",
"actions",
)
attrs = {
"class": "w-full text-sm min-w-[700px]"
}
class ReminderTable(tables.Table):
invoice = tables.TemplateColumn(
template_name="invoices/tables/reminders/invoice.html",
verbose_name="Rechnung",
orderable=False
)
level = tables.Column(
verbose_name="Mahnstufe"
)
status = tables.Column(
accessor="get_status_display",
verbose_name="Status"
)
issue_date = tables.DateColumn(
format="d.m.Y",
verbose_name="Datum"
)
due_date = tables.DateColumn(
format="d.m.Y",
verbose_name="Neue Zahlungsfrist"
)
actions = tables.TemplateColumn(
template_name="invoices/tables/reminders/actions.html",
verbose_name="Aktion",
orderable=False
)
class Meta:
model = Reminder
fields = (
"reminder_number",
"invoice",
"level",
"status",
"issue_date",
"due_date",
"actions",
)
attrs = {
"class": "w-full text-sm min-w-[700px]"
}
class ItemTable(tables.Table):
item_type = tables.TemplateColumn(
template_name="invoices/tables/items/item_type.html",
verbose_name="Typ",
orderable=False
)
description = tables.TemplateColumn(
template_name="invoices/tables/items/description.html",
verbose_name="Beschreibung",
orderable=False
)
unit = tables.TemplateColumn(
template_name="invoices/tables/items/unit.html",
verbose_name="Einheit",
orderable=False
)
price_net = tables.TemplateColumn(
template_name="invoices/tables/items/price_net.html",
verbose_name="Preis Netto",
orderable=False
)
tax_rate = tables.TemplateColumn(
template_name="invoices/tables/items/tax_rate.html",
verbose_name="MwSt. Satz (%)",
orderable=False
)
actions = tables.TemplateColumn(
template_name="invoices/tables/items/actions.html",
verbose_name="Aktion",
orderable=False
)
class Meta:
model = Item
fields = (
"item_type",
"description",
"unit",
"price_net",
"tax_rate",
"actions",
)
attrs = {
"class": "w-full text-sm min-w-[700px]"
}
class CustomerTable(tables.Table):
toggle_prefix = "customer"
detail_template = (
"invoices/tables/customer/detail.html"
)
customer_number = tables.Column(
verbose_name="Kunden-ID"
)
company_name = tables.Column(
verbose_name="Name"
)
customer_type = tables.Column(
accessor="get_customer_type_display",
verbose_name="Typ"
)
phone = tables.Column(
verbose_name="Telefon"
)
email = tables.Column(
verbose_name="E-Mail"
)
tax_number = tables.Column(
verbose_name="Steuernummer"
)
vat_number = tables.Column(
verbose_name="UST-IdNr"
)
class Meta:
model = Customer
template_name = (
"django_tables2/table_toggle.html"
)
fields = (
"customer_number",
"company_name",
"customer_type",
"phone",
"email",
"tax_number",
"vat_number",
)
attrs = {
"class": "w-full text-sm min-w-[700px]"
}
class CustomerArchivedTable(tables.Table):
toggle_prefix = "customer"
detail_template = (
"invoices/tables/customer/detail_archived.html"
)
customer_number = tables.Column(
verbose_name="Kunden-ID"
)
company_name = tables.Column(
verbose_name="Name"
)
customer_type = tables.Column(
accessor="get_customer_type_display",
verbose_name="Typ"
)
phone = tables.Column(
verbose_name="Telefon"
)
email = tables.Column(
verbose_name="E-Mail"
)
tax_number = tables.Column(
verbose_name="Steuernummer"
)
vat_number = tables.Column(
verbose_name="UST-IdNr"
)
class Meta:
model = Customer
template_name = (
"django_tables2/table_toggle.html"
)
fields = (
"customer_number",
"company_name",
"customer_type",
"phone",
"email",
"tax_number",
"vat_number",
)
attrs = {
"class": "w-full text-sm min-w-[700px]"
}
@@ -0,0 +1,36 @@
{% extends "base.html" %}
{% block title %}
Rechnungswesen - Kunden
{% endblock %}
{% block header_title %}
Kunde hinzufügen
{% endblock %}
{% block header_description %}
Neuen Kunden anlegen
{% endblock %}
{% block content %}
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors mb-8">
<form method="post" enctype="multipart/form-data" class="space-y-4">
{% csrf_token %}
<div class="grid grid-cols-1 gap-4">
{{ form.as_p }}
</div>
<button
type="submit"
class="btn mt-4 btn-save">
Speichern
</button>
</form>
</div>
{% endblock %}
@@ -0,0 +1,230 @@
{% extends "base.html" %}
{% block title %}
Rechnungswesen - Kunden
{% endblock %}
{% block header_title %}
{{ customer.customer_number }} - {{ customer.company_name }}
{% endblock %}
{% block header_description %}
Kunde bearbeiten
{% endblock %}
{% block content %}
<form method="post" action="{% url 'invoices:customers_edit' customer.id %}">
{% csrf_token %}
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<h2 class="text-xl font-bold mb-2">Kundendaten</h2>
<div class="grid md:grid-cols-2 gap-4 mb-8">
{% for field in form %}
<div>
<label class="block text-sm font-medium mb-1">
{{ field.label }}
</label>
{{ field }}
{% if field.errors %}
<div class="text-red-600 text-sm mt-1">
{{ field.errors }}
</div>
{% endif %}
</div>
{% endfor %}
</div>
<div class="grid lg:grid-cols-2 gap-8">
<!-- Ansprechpartner -->
<div>
<h2 class="font-bold text-lg mb-2">
Ansprechpartner
</h2>
{% for contact in customer.contacts.all %}
<div class="bg-slate-100 dark:bg-slate-700 rounded-xl p-4 mb-3">
<div class="grid md:grid-cols-2 gap-3">
<input
type="text"
name="contact_first_name_{{ contact.id }}"
value="{{ contact.first_name }}"
placeholder="Vorname"
class="border rounded px-3 py-2">
<input
type="text"
name="contact_last_name_{{ contact.id }}"
value="{{ contact.last_name }}"
placeholder="Nachname"
class="border rounded px-3 py-2">
<input
type="email"
name="contact_email_{{ contact.id }}"
value="{{ contact.email }}"
placeholder="E-Mail"
class="border rounded px-3 py-2">
<input
type="text"
name="contact_phone_{{ contact.id }}"
value="{{ contact.phone }}"
placeholder="Telefon"
class="border rounded px-3 py-2">
</div>
<button type="submit"
name="delete_contact_id"
value="{{ contact.id }}"
class="btn btn-delete">
Löschen
</button>
</div>
{% endfor %}
<div id="contacts-container"></div>
<button
type="button"
onclick="addContact()"
class="btn btn-add">
+ Ansprechpartner hinzufügen
</button>
</div>
<!-- Standorte -->
<div>
<h2 class="font-bold text-lg mb-2">
Standorte
</h2>
{% for location in customer.locations.all %}
<div class="bg-slate-100 dark:bg-slate-700 rounded-xl p-4 mb-3">
<div class="grid md:grid-cols-2 gap-3">
<input
type="text"
name="location_name_{{ location.id }}"
value="{{ location.name }}"
placeholder="Name"
class="border rounded px-3 py-2">
<input
type="text"
name="location_street_{{ location.id }}"
value="{{ location.street }}"
placeholder="Straße"
class="border rounded px-3 py-2">
<input
type="text"
name="location_zip_{{ location.id }}"
value="{{ location.zip_code }}"
placeholder="PLZ"
class="border rounded px-3 py-2">
<input
type="text"
name="location_city_{{ location.id }}"
value="{{ location.city }}"
placeholder="Ort"
class="border rounded px-3 py-2">
<input
type="text"
name="location_country_{{ location.id }}"
value="{{ location.country }}"
placeholder="Land"
class="border rounded px-3 py-2 md:col-span-2">
</div>
<button type="submit"
name="delete_location_id"
value="{{ location.id }}"
class="btn btn-delete">
Löschen
</button>
</div>
{% endfor %}
<div id="locations-container"></div>
<button
type="button"
onclick="addLocation()"
class="btn btn-add">
+ Standort hinzufügen
</button>
</div>
</div>
<button
type="submit"
class="btn mt-8 btn-save">
Speichern
</button>
</div>
</form>
<script>
function addContact() {
document.getElementById("contacts-container").insertAdjacentHTML("beforeend", `
<div class="contact-card bg-slate-100 dark:bg-slate-700 rounded-xl p-4 mb-3">
<div class="grid md:grid-cols-2 gap-3">
<input type="text" name="new_contact_first_name" placeholder="Vorname" class="border rounded px-3 py-2">
<input type="text" name="new_contact_last_name" placeholder="Nachname" class="border rounded px-3 py-2">
<input type="email" name="new_contact_email" placeholder="E-Mail" class="border rounded px-3 py-2">
<input type="text" name="new_contact_phone" placeholder="Telefon" class="border rounded px-3 py-2">
</div>
</div>
`);
}
function addLocation() {
document.getElementById("locations-container").insertAdjacentHTML("beforeend", `
<div class="location-card bg-slate-100 dark:bg-slate-700 rounded-xl p-4 mb-3">
<div class="grid md:grid-cols-2 gap-3">
<input type="text" name="new_location_name" placeholder="Name" class="border rounded px-3 py-2">
<input type="text" name="new_location_street" placeholder="Straße" class="border rounded px-3 py-2">
<input type="text" name="new_location_zip" placeholder="PLZ" class="border rounded px-3 py-2">
<input type="text" name="new_location_city" placeholder="Ort" class="border rounded px-3 py-2">
<input type="text" name="new_location_country" placeholder="Land" class="border rounded px-3 py-2 md:col-span-2">
</div>
</div>
`);
}
</script>
{% endblock %}
@@ -0,0 +1,62 @@
{% extends "base.html" %}
{% block title %}
Rechnungswesen - Kunden
{% endblock %}
{% block header_title %}
Kunden
{% endblock %}
{% block header_description %}
Kundenstammdaten verwalten
{% endblock %}
{% block content %}
{% load django_tables2 %}
<div class="mb-8">
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
<div>
<h2 class="text-xl font-bold">Aktionen</h2>
<p class="text-slate-400 text-sm mt-1">
Kunden anlegen
</p>
</div>
<div class="flex flex-col sm:flex-row gap-2">
<a href="{% url 'invoices:customers_add' %}"
class="btn btn-add">
+ Kunde
</a>
</div>
</div>
</div>
</div>
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors mb-8">
<div class="mb-6">
<h2 class="text-xl font-bold">Kundenliste</h2>
<p class="text-slate-400 text-sm mt-1">Alle Kunden, inkl. Standorte und Ansprechpartner</p>
</div>
<div class="overflow-x-auto rounded-xl">
{% render_table table %}
</div>
</div>
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<div class="mb-6">
<h2 class="text-xl font-bold">Archivierte Kunden</h2>
<p class="text-slate-400 text-sm mt-1">Alle inaktiven Kunden, inkl. Standorte und Ansprechpartner</p>
</div>
<div class="overflow-x-auto rounded-xl">
{% render_table table_archived %}
</div>
</div>
{% endblock %}
@@ -0,0 +1,69 @@
{% extends "base.html" %}
{% block title %}Rechnungswesen - Rechnungen{% endblock %}
{% block header_title %}
Kundenrechnung
{% endblock %}
{% block header_description %}
Rechnung auf Basis eines Kunden erstellen
{% endblock %}
{% block content %}
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<form method="post" enctype="multipart/form-data" class="space-y-4">
{% csrf_token %}
{{ form.as_p }}
<button class="btn btn-save">
Speichern
</button>
</form>
</div>
<script>
document.addEventListener("DOMContentLoaded", function () {
const customer = document.getElementById("id_customer");
const contact = document.getElementById("id_contact");
const location = document.getElementById("id_location");
function fill(select, data) {
select.innerHTML = "<option value=''>---------</option>";
data.forEach(item => {
select.innerHTML += `<option value="${item.id}">${item.text}</option>`;
});
}
if (customer) {
customer.addEventListener("change", function () {
const id = this.value;
if (!id) {
contact.innerHTML = "";
location.innerHTML = "";
return;
}
fetch(`/invoices/ajax/customer/${id}/contacts/`)
.then(r => r.json())
.then(data => fill(contact, data));
fetch(`/invoices/ajax/customer/${id}/locations/`)
.then(r => r.json())
.then(data => fill(location, data));
});
}
});
</script>
{% endblock %}
@@ -0,0 +1,221 @@
{% extends "base.html" %}
{% block title %}Rechnungswesen - Rechnungen{% endblock %}
{% block header_title %}
Projektrechnung
{% endblock %}
{% block header_description %}
Rechnung auf Basis eines Projekts erstellen
{% endblock %}
{% block content %}
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<form method="post" enctype="multipart/form-data" class="space-y-4">
{% csrf_token %}
{{ form.as_p }}
<button class="btn btn-save">
Speichern
</button>
</form>
</div>
<script>
document.addEventListener("DOMContentLoaded", function () {
const project = document.getElementById("id_project");
const contact = document.getElementById("id_contact");
const location = document.getElementById("id_location");
const customerFields = [
"id_contact",
"id_location"
];
const manualFields = [
"id_contact_name_manual",
"id_contact_email_manual",
"id_contact_phone_manual",
"id_location_name_manual",
"id_location_street_manual",
"id_location_zip_manual",
"id_location_city_manual",
"id_location_country_manual"
];
function hideField(id){
const field = document.getElementById(id);
if(field){
const row = field.closest("p");
row.style.display = "none";
}
}
function showField(id){
const field = document.getElementById(id);
if(field){
const row = field.closest("p");
row.style.display = "";
}
}
function hideCustomerFields(){
customerFields.forEach(hideField);
}
function showCustomerFields(){
customerFields.forEach(showField);
}
function hideManualFields(){
manualFields.forEach(hideField);
}
function showManualFields(){
manualFields.forEach(showField);
}
function fill(select, data){
if(!select){
return;
}
select.innerHTML = "<option value=''>---------</option>";
data.forEach(item => {
select.innerHTML += `
<option value="${item.id}">
${item.text}
</option>
`;
});
}
function checkProject(projectId){
if(!projectId){
hideCustomerFields();
hideManualFields();
return;
}
fetch(`/invoices/ajax/project/${projectId}/invoice-data/`)
.then(response => response.json())
.then(data => {
if(data.has_customer){
showCustomerFields();
hideManualFields();
fill(
contact,
data.contacts
);
fill(
location,
data.locations
);
} else {
hideCustomerFields();
showManualFields();
}
});
}
// Anfang prüfen
if(project.value){
checkProject(project.value);
} else {
hideCustomerFields();
hideManualFields();
}
project.addEventListener("change", function(){
checkProject(this.value);
});
});
</script>
{% endblock %}
@@ -0,0 +1,239 @@
{% extends "base.html" %}
{% block title %}Rechnungswesen - Rechnungen{% endblock %}
{% block header_title %}
Rechnung {{ invoice.invoice_number }}
{% endblock %}
{% block header_description %}
Details & Positionen
{% endblock %}
{% block content %}
{% load django_tables2 %}
<div class="mb-8">
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
<div>
<h2 class="text-xl font-bold">Aktionen</h2>
<p class="text-slate-400 text-sm mt-1">
Positionen zur Rechnung hinzufügen
</p>
</div>
<div class="flex flex-col sm:flex-row gap-2">
{% if invoice.status == "draft" %}
<a href="{% url 'invoices:invoice_item_add' invoice.id %}" class="btn btn-add">+ Position</a>
<a href="{% url 'invoices:invoices_preview' invoice.id %}" class="btn btn-action">Vorschau</a>
<a href="{% url 'invoices:invoices_download' invoice.id %}" onclick="startDownloadAndRedirect()" class="btn btn-action">Download</a>
<a href="{% url 'invoices:invoices_delete' invoice.id %}" class="btn btn-delete">Löschen</a>
{% endif %}
{% if invoice.status == "sent" %}
<a href="{% url 'invoices:invoices_download' invoice.id %}" class="btn btn-action">Download</a>
<a href="{% url 'invoices:invoices_cancel' invoice.id %}" onclick="startDownloadAndRedirect()" class="btn btn-delete">Stornieren</a>
<a href="{% url 'invoices:invoices_paid' invoice.id %}" class="btn btn-action">Zahlung verbuchen</a>
<a href="{% url 'invoices:invoices_overdue' invoice.id %}" class="btn btn-action">Überfällig</a>
{% endif %}
{% if invoice.status == "paid" %}
<a href="{% url 'invoices:invoices_download' invoice.id %}" class="btn btn-action">Download</a>
<a href="{% url 'invoices:invoices_cancel' invoice.id %}" onclick="startDownloadAndRedirect()" class="btn btn-delete">Stornieren</a>
{% endif %}
{% if invoice.status == "canceled" %}
<a href="{% url 'invoices:invoices_download' invoice.id %}" class="btn btn-action">Download</a>
<a href="{% url 'invoices:invoices_cancel' invoice.id %}" class="btn btn-action">Stornorechnung</a>
{% endif %}
{% if invoice.status == "overdue" %}
<a href="{% url 'invoices:invoices_download' invoice.id %}" class="btn btn-action">Download</a>
{% endif %}
</div>
</div>
</div>
</div>
{% if reminders %}
<div class="mb-8 p-6 border border-orange-500 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<h2 class="text-xl font-bold mb-6 text-orange-500">Mahnungen</h2>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
{% for reminder in reminders %}
{% if reminder.status != "canceled" %}
<div class="bg-slate-100 dark:bg-slate-700 rounded-xl p-6">
<div class="flex justify-between items-start">
<div>
<a href="{% url 'invoices:reminders_detail' reminder.id %}" class="company_link">
{{ reminder.reminder_number }}
</a>
<p class="text-sm text-slate-500 dark:text-slate-300 mt-1">
Mahnstufe {{ reminder.level }}
· {{ reminder.issue_date|date:"d.m.Y" }}
</p>
</div>
<span class="px-3 py-1 rounded-full text-xs font-medium bg-slate-200 text-slate-700 dark:bg-slate-600 dark:text-slate-200">
{{ reminder.get_status_display }}
</span>
</div>
<div class="mt-4 text-sm text-slate-600 dark:text-slate-300">
{% if reminder.due_date %}
<p>
Neue Zahlungsfrist:
<span class="font-medium">
{{ reminder.due_date|date:"d.m.Y" }}
</span>
</p>
{% endif %}
{% if reminder.fee %}
<p>
Mahngebühr:
<span class="font-medium">
{{ reminder.fee|floatformat:2 }} €
</span>
</p>
{% endif %}
</div>
</div>
{% endif %}
{% endfor %}
</div>
</div>
{% endif %}
<!-- HEADER INFO -->
<div class="mb-8 p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<h2 class="text-xl font-bold mb-6">Allgemein</h2>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
<!-- Kunde -->
<div>
<div class="text-slate-400 text-sm">Kunde</div>
<div class="font-semibold">{{ invoice.customer.company_name }}</div>
</div>
<!-- Projekt -->
<div>
<div class="text-slate-400 text-sm">Projekt</div>
<div class="font-semibold">
{% if invoice.project %}
{{ invoice.project.name }}
{% else %}
-
{% endif %}
</div>
</div>
<!-- Status -->
<div>
<div class="text-slate-400 text-sm">Status</div>
<div class="font-semibold">{{ invoice.get_status_display }}</div>
</div>
<!-- Datum -->
<div>
<div class="text-slate-400 text-sm">Rechnungsdatum</div>
<div class="font-semibold">{{ invoice.issue_date|date:"d.m.Y" }}</div>
</div>
<div>
<div class="text-slate-400 text-sm">Fälligkeitsdatum</div>
<div class="font-semibold">{{ invoice.due_date|date:"d.m.Y" }}</div>
</div>
</div>
</div>
<!-- SNAPSHOT: KONTAKT & ADRESSE -->
<div class="mb-8 p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<h2 class="text-xl font-bold mb-6">Rechnungsadresse</h2>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
<!-- Kontakt -->
<div>
<div class="text-slate-400 text-sm">Ansprechpartner</div>
<div class="font-semibold">{{ invoice.contact_name|default:"-" }}</div>
{% if invoice.contact_email %}
<div class="text-slate-400 text-sm">{{ invoice.contact_email }}</div>
{% endif %}
{% if invoice.contact_phone %}
<div class="text-slate-400 text-sm">{{ invoice.contact_phone }}</div>
{% endif %}
</div>
<!-- Adresse -->
<div>
<div class="text-slate-400 text-sm">Standort</div>
<div class="font-semibold">
{{ invoice.location_name|default:"-" }}
</div>
<div class="text-slate-400 text-sm">
{{ invoice.location_street }}
</div>
<div class="text-slate-400 text-sm">
{{ invoice.location_zip }} {{ invoice.location_city }}
</div>
<div class="text-slate-400 text-sm">
{{ invoice.location_country }}
</div>
</div>
</div>
</div>
<div class="mb-8 p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<h2 class="text-xl font-bold mb-6">Rechnungshinweis</h2>
<div class="grid grid-cols-1 gap-6">
<div>
<div class="font-semibold">{{ invoice.note|default:"-" }}</div>
</div>
</div>
</div>
<!-- POSITIONEN -->
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<h2 class="text-xl font-bold mb-6">Positionen</h2>
<div class="overflow-x-auto rounded-xl">
{% render_table table %}
</div>
<div class="mt-4 text-right font-bold">
Gesamt: {{ total_net|floatformat:2 }} €
</div>
</div>
<script>
function startDownloadAndRedirect() {
setTimeout(function() {
window.location.href = "{% url 'invoices:invoices_detail' invoice.id %}";
}, 1000);
}
</script>
{% endblock %}
@@ -0,0 +1,197 @@
{% extends "base.html" %}
{% block title %}
Rechnungswesen - Rechnungen
{% endblock %}
{% block header_title %}
Rechnungsposition hinzufügen
{% endblock %}
{% block header_description %}
Neue Rechnungsposition anlegen
{% endblock %}
{% block content %}
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<form method="post" enctype="multipart/form-data" class="space-y-4">
{% csrf_token %}
{{ form.as_p }}
<button class="btn btn-save">
Speichern
</button>
</form>
</div>
<script>
document.addEventListener("DOMContentLoaded", function () {
const source = document.getElementById("id_source_type");
const fields = {
item: document.getElementById("id_item"),
time: document.getElementById("id_time_entry"),
expense: document.getElementById("id_expense"),
};
function toggleFields() {
const value = source.value;
Object.keys(fields).forEach(key => {
const field = fields[key];
if (!field) {
return;
}
const row = field.closest("p");
if (key === value) {
row.style.display = "";
} else {
row.style.display = "none";
field.value = "";
}
});
}
source.addEventListener(
"change",
toggleFields
);
// Initial ausführen
toggleFields();
});
</script>
<script>
document.addEventListener("DOMContentLoaded", function () {
const source = document.getElementById("id_source_type");
const selects = {
item: document.getElementById("id_item"),
time: document.getElementById("id_time_entry"),
expense: document.getElementById("id_expense"),
};
const description = document.getElementById(
"id_description"
);
const quantity = document.getElementById(
"id_quantity"
);
const unit = document.getElementById(
"id_unit"
);
const price = document.getElementById(
"id_unit_price_net"
);
const tax = document.getElementById(
"id_unit_tax_rate"
);
function loadData(type, id){
if(!id){
return;
}
fetch(
`/invoices/ajax/invoice-item-data/?source=${type}&id=${id}`
)
.then(response => response.json())
.then(data => {
description.value = data.description;
quantity.value = data.quantity;
unit.value = data.unit;
price.value = data.unit_price_net;
tax.value = data.unit_tax_rate;
});
}
Object.keys(selects).forEach(type => {
if(selects[type]){
selects[type].addEventListener(
"change",
function(){
loadData(
type,
this.value
);
}
);
}
});
});
</script>
{% endblock %}
@@ -0,0 +1,35 @@
{% extends "base.html" %}
{% block title %}Rechnungswesen - Rechnungen{% endblock %}
{% block header_title %}Rechnungen{% endblock %}
{% block header_description %}Rechnungen erstellen und verwalten{% endblock %}
{% block content %}
{% load django_tables2 %}
<div class="mb-8">
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
<div>
<h2 class="text-xl font-bold">Aktionen</h2>
<p class="text-slate-400 text-sm mt-1">Neue Rechnung erstellen</p>
</div>
<div class="flex flex-col sm:flex-row gap-2">
<a href="{% url 'invoices:invoice_add_customer' %}" class="btn btn-add">+ Kundenrechnung</a>
<a href="{% url 'invoices:invoice_add_project' %}" class="btn btn-add">+ Projektrechnung</a>
</div>
</div>
</div>
</div>
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<div class="mb-6">
<h2 class="text-xl font-bold">Rechnungen</h2>
<p class="text-slate-400 text-sm mt-1">Alle Rechnungen mit aktuellem Status</p>
</div>
<div class="overflow-x-auto rounded-xl">
{% render_table table %}
</div>
</div>
{% endblock %}
@@ -0,0 +1,747 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<style>
@page {
size: A4;
margin: 18mm;
}
body {
font-family: Arial, sans-serif;
font-size: 11px;
color: #334155;
}
h1,h2,h3,p {
margin:0;
}
table {
width:100%;
border-collapse:collapse;
}
.header {
display:flex;
justify-content:space-between;
}
.logo {
height:128px;
margin-bottom:15px;
}
.company-name {
font-size:24px;
font-weight:bold;
}
.invoice-title {
font-size:32px;
font-weight:bold;
}
.info-table td {
padding:4px 0;
}
.label {
color:#64748b;
padding-right:30px;
}
.divider {
border-top:1px solid #e2e8f0;
margin:30px 0;
}
.receiver-title {
font-size:10px;
text-transform:uppercase;
color:#64748b;
letter-spacing:1px;
}
.receiver-name {
font-size:16px;
font-weight:bold;
margin-top:5px;
}
.invoice-heading {
font-size:22px;
font-weight:bold;
margin-bottom:10px;
}
.items-wrapper {
border:1px solid #e2e8f0;
border-radius:10px;
overflow:hidden;
margin-top:20px;
}
.items {
width:100%;
border-collapse:collapse;
}
.items thead th:first-child {
border-top-left-radius:10px;
}
.items thead th:last-child {
border-top-right-radius:10px;
}
.items th {
background:{{ company.color }};
color:white;
padding:10px;
text-align:left;
}
.items td {
padding:10px;
border-bottom:1px solid #e2e8f0;
}
.items tbody tr:last-child td {
border-bottom:none;
}
.right {
text-align:right;
}
.total-box {
margin-top:30px;
margin-left:auto;
width:250px;
}
.total-row {
display:flex;
justify-content:space-between;
padding:6px 0;
}
.total-final {
border-top:1px solid #cbd5e1;
margin-top:10px;
padding-top:10px;
font-size:16px;
font-weight:bold;
}
.note {
margin-top:30px;
padding:12px;
border:1px solid #e2e8f0;
border-radius: 10px;
}
.small-business {
margin-top:25px;
color:#64748b;
}
@page {
size: A4;
margin: 18mm 18mm 35mm 18mm;
}
.footer {
position: fixed;
bottom: -25mm;
left: 0;
right: 0;
padding-top:15px;
border-top:1px solid #e2e8f0;
display:flex;
justify-content:space-between;
font-size:10px;
color:#64748b;
}
.footer-title {
font-weight:bold;
color:#334155;
margin-bottom:5px;
}
</style>
</head>
<body>
<!-- HEADER -->
<div class="header">
<div>
{% if company.logo %}
<img src="{{ company.logo.url }}" class="logo">
{% else %}
<h1 class="company-name" style="color: {{ company.color }}">
{{ company.companyname }}
</h1>
{% if company.slogan %}
<p>
{{ company.slogan }}
</p>
{% endif %}
{% endif %}
<p style="margin-top:15px">
{{ company.name }}<br>
{{ company.street }} · {{ company.zip }} {{ company.city }}
</p>
</div>
<div style="text-align:right">
<div class="invoice-title">
RECHNUNG
</div>
<table class="info-table">
<tr>
<td class="label">
Rechnungsnummer
</td>
<td>
{{ invoice.invoice_number }}
</td>
</tr>
<tr>
<td class="label">
Rechnungsdatum
</td>
<td>
{{ invoice.issue_date|date:"d.m.Y" }}
</td>
</tr>
{% if invoice.due_date %}
<tr>
<td class="label">
Fälligkeitsdatum
</td>
<td>
{{ invoice.due_date|date:"d.m.Y" }}
</td>
</tr>
{% endif %}
</table>
</div>
</div>
<div class="divider"></div>
<!-- EMPFÄNGER -->
<div>
<div class="receiver-title">
Rechnungsempfänger
</div>
<div class="receiver-name">
{% if invoice.customer.type == "company" %}
{{ invoice.customer.company_name }}
{% else %}
{{ invoice.contact_name }}
{% endif %}
</div>
{% if invoice.customer.type == "company" %}
<p>
z.Hd. {{ invoice.contact_name }}
</p>
{% endif %}
<p style="margin-top:10px">
{{ invoice.location_street }}<br>
{{ invoice.location_zip }}
{{ invoice.location_city }}
</p>
</div>
<!-- TITEL -->
<div style="margin-top:40px">
<div class="invoice-heading"
style="color:{{ company.color }}">
Rechnung
</div>
{% if company.invoice_header_text %}
<p>
{{ company.invoice_header_text|linebreaksbr }}
</p>
{% endif %}
</div>
<!-- POSITIONEN -->
<div class="items-wrapper">
<table class="items">
<thead>
<tr>
<th>
Nr.
</th>
<th>
Beschreibung
</th>
{% if show_service_date %}
<th class="left">Leistungsdatum</th>
{% endif %}
<th class="left">
Menge
</th>
<th class="left">
Einheit
</th>
<th class="left">
Preis
</th>
{% if not company.is_small_business %}
<th class="left">
Steuer
</th>
{% endif %}
<th class="left">
Gesamt
</th>
</tr>
</thead>
<tbody>
{% for item in items %}
<tr>
<td>
{{ forloop.counter }}
</td>
<td>
{{ item.description }}
</td>
{% if show_service_date %}
<td class="left">
{% if item.service_period_from %}
{{ item.service_period_from|date:"d.m.Y" }}
{% endif %}
{% if item.service_period_to %}
- {{ item.service_period_to|date:"d.m.Y" }}
{% endif %}
</td>
{% endif %}
<td class="left">
{{ item.quantity }}
</td>
<td class="left">
{{ item.unit }}
</td>
<td class="left">
{{ item.unit_price_net|floatformat:2 }} €
</td>
{% if not company.is_small_business %}
<td class="left">
{{ item.unit_tax_rate|floatformat:0 }} %
</td>
{% endif %}
<td class="left">
{{ item.calculated_total_net|floatformat:2 }} €
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- SUMMEN -->
<div class="total-box">
<div class="total-row">
<span>
Netto
</span>
<span>
{{ total_net|floatformat:2 }} €
</span>
</div>
{% if not company.is_small_business %}
{% for tax_rate,tax_amount in tax_summary.items %}
<div class="total-row">
<span>
Umsatzsteuer {{ tax_rate|floatformat:0 }} %
</span>
<span>
{{ tax_amount|floatformat:2 }} €
</span>
</div>
{% endfor %}
{% endif %}
<div class="total-row total-final">
<span>
Gesamt
</span>
<span>
{% if company.is_small_business %}
{{ total_net|floatformat:2 }}
{% else %}
{{ total_gross|floatformat:2 }}
{% endif %}
€
</span>
</div>
</div>
{% if invoice.note %}
<div class="note">
<div class="receiver-title">
Rechnungshinweis
</div>
<br>
{{ invoice.note|linebreaksbr }}
</div>
{% endif %}
{% if company.is_small_business %}
<div class="note">
<div class="receiver-title">
Kleinunternehmerhinweis
</div>
<br>
{{ company.small_business_text }}
</div>
{% endif %}
<!-- FOOTER -->
<div class="footer">
<div>
<div class="footer-title">
Kontakt
</div>
{% if company.phone %}
{{ company.phone }}<br>
{% endif %}
{% if company.email %}
{{ company.email }}<br>
{% endif %}
{% if company.website %}
{{ company.website }}
{% endif %}
</div>
<div>
<div class="footer-title">
Steuern
</div>
{% if company.tax_number %}
Steuernummer: {{ company.tax_number }}<br>
{% endif %}
{% if company.vat_number %}
USt-IdNr.: {{ company.vat_number }}
{% endif %}
</div>
<div>
<div class="footer-title">
Bank
</div>
{% if company.bank_name %}
{{ company.bank_name }}<br>
{% endif %}
{% if company.iban %}
IBAN: {{ company.iban }}<br>
{% endif %}
{% if company.bic %}
BIC: {{ company.bic }}
{% endif %}
</div>
</div>
</body>
</html>
@@ -0,0 +1,756 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<style>
@page {
size: A4;
margin: 18mm;
}
body {
font-family: Arial, sans-serif;
font-size: 11px;
color: #334155;
}
h1,h2,h3,p {
margin:0;
}
table {
width:100%;
border-collapse:collapse;
}
.header {
display:flex;
justify-content:space-between;
}
.logo {
height:128px;
margin-bottom:15px;
}
.company-name {
font-size:24px;
font-weight:bold;
}
.invoice-title {
font-size:32px;
font-weight:bold;
}
.info-table td {
padding:4px 0;
}
.label {
color:#64748b;
padding-right:30px;
}
.divider {
border-top:1px solid #e2e8f0;
margin:30px 0;
}
.receiver-title {
font-size:10px;
text-transform:uppercase;
color:#64748b;
letter-spacing:1px;
}
.receiver-name {
font-size:16px;
font-weight:bold;
margin-top:5px;
}
.invoice-heading {
font-size:22px;
font-weight:bold;
margin-bottom:10px;
}
.items-wrapper {
border:1px solid #e2e8f0;
border-radius:10px;
overflow:hidden;
margin-top:20px;
}
.items {
width:100%;
border-collapse:collapse;
}
.items thead th:first-child {
border-top-left-radius:10px;
}
.items thead th:last-child {
border-top-right-radius:10px;
}
.items th {
background:{{ company.color }};
color:white;
padding:10px;
text-align:left;
}
.items td {
padding:10px;
border-bottom:1px solid #e2e8f0;
}
.items tbody tr:last-child td {
border-bottom:none;
}
.right {
text-align:right;
}
.total-box {
margin-top:30px;
margin-left:auto;
width:250px;
}
.total-row {
display:flex;
justify-content:space-between;
padding:6px 0;
}
.total-final {
border-top:1px solid #cbd5e1;
margin-top:10px;
padding-top:10px;
font-size:16px;
font-weight:bold;
}
.note {
margin-top:30px;
padding:12px;
border:1px solid #e2e8f0;
border-radius: 10px;
}
.small-business {
margin-top:25px;
color:#64748b;
}
@page {
size: A4;
margin: 18mm 18mm 35mm 18mm;
}
.footer {
position: fixed;
bottom: -25mm;
left: 0;
right: 0;
padding-top:15px;
border-top:1px solid #e2e8f0;
display:flex;
justify-content:space-between;
font-size:10px;
color:#64748b;
}
.footer-title {
font-weight:bold;
color:#334155;
margin-bottom:5px;
}
</style>
</head>
<body>
<!-- HEADER -->
<div class="header">
<div>
{% if company.logo %}
<img src="{{ company.logo.url }}" class="logo">
{% else %}
<h1 class="company-name" style="color: {{ company.color }}">
{{ company.companyname }}
</h1>
{% if company.slogan %}
<p>
{{ company.slogan }}
</p>
{% endif %}
{% endif %}
<p style="margin-top:15px">
{{ company.name }}<br>
{{ company.street }} · {{ company.zip }} {{ company.city }}
</p>
</div>
<div style="text-align:right">
<div class="invoice-title">
STORNO
</div>
<table class="info-table">
<tr>
<td class="label">
Stornonummer
</td>
<td>
{{ cancel_number }}
</td>
</tr>
<tr>
<td class="label">
Stornodatum
</td>
<td>
{{ cancel_date|date:"d.m.Y" }}
</td>
</tr>
<tr>
<td class="label">
Ursprüngliche Rechnung
</td>
<td>
{{ invoice.invoice_number }}
</td>
</tr>
<tr>
<td class="label">
Rechnungsdatum
</td>
<td>
{{ invoice.issue_date|date:"d.m.Y" }}
</td>
</tr>
</table>
</div>
</div>
<div class="divider"></div>
<!-- EMPFÄNGER -->
<div>
<div class="receiver-title">
Rechnungsempfänger
</div>
<div class="receiver-name">
{% if invoice.customer.type == "company" %}
{{ invoice.customer.company_name }}
{% else %}
{{ invoice.contact_name }}
{% endif %}
</div>
{% if invoice.customer.type == "company" %}
<p>
z.Hd. {{ invoice.contact_name }}
</p>
{% endif %}
<p style="margin-top:10px">
{{ invoice.location_street }}<br>
{{ invoice.location_zip }}
{{ invoice.location_city }}
</p>
</div>
<!-- TITEL -->
<div style="margin-top:40px">
<div class="invoice-heading"
style="color:{{ company.color }}">
Stornorechnung
</div>
<p style="margin-top:10px;">
Diese Stornorechnung storniert die Rechnung
<strong>{{ invoice.invoice_number }}</strong>
vom {{ invoice.issue_date|date:"d.m.Y" }} vollständig.
</p>
<p style="margin-top:5px;">
Stornodatum: {{ cancel_date|date:"d.m.Y" }}
</p>
</div>
<!-- POSITIONEN -->
<div class="items-wrapper">
<table class="items">
<thead>
<tr>
<th>
Nr.
</th>
<th>
Beschreibung
</th>
{% if show_service_date %}
<th class="left">Leistungsdatum</th>
{% endif %}
<th class="left">
Menge
</th>
<th class="left">
Einheit
</th>
<th class="left">
Preis
</th>
{% if not company.is_small_business %}
<th class="left">
Steuer
</th>
{% endif %}
<th class="left">
Gesamt
</th>
</tr>
</thead>
<tbody>
{% for item in items %}
<tr>
<td>
{{ forloop.counter }}
</td>
<td>
{{ item.description }}
</td>
{% if show_service_date %}
<td class="left">
{% if item.service_period_from %}
{{ item.service_period_from|date:"d.m.Y" }}
{% endif %}
{% if item.service_period_to %}
- {{ item.service_period_to|date:"d.m.Y" }}
{% endif %}
</td>
{% endif %}
<td class="left">
{{ item.quantity }}
</td>
<td class="left">
{{ item.unit }}
</td>
<td class="left">
-{{ item.unit_price_net|floatformat:2 }} €
</td>
{% if not company.is_small_business %}
<td class="left">
{{ item.unit_tax_rate|floatformat:0 }} %
</td>
{% endif %}
<td class="left">
-{{ item.calculated_total_net|floatformat:2 }} €
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- SUMMEN -->
<div class="total-box">
<div class="total-row">
<span>
Netto
</span>
<span>
-{{ total_net|floatformat:2 }} €
</span>
</div>
{% if not company.is_small_business %}
{% for tax_rate,tax_amount in tax_summary.items %}
<div class="total-row">
<span>
Umsatzsteuer {{ tax_rate|floatformat:0 }} %
</span>
<span>
-{{ tax_amount|floatformat:2 }} €
</span>
</div>
{% endfor %}
{% endif %}
<div class="total-row total-final">
<span>
Gesamt
</span>
<span>
{% if company.is_small_business %}
-{{ total_net|floatformat:2 }}
{% else %}
-{{ total_gross|floatformat:2 }}
{% endif %}
€
</span>
</div>
</div>
<div class="note">
<div class="receiver-title">
Stornohinweis
</div>
<br>
Die Rechnung {{ invoice.invoice_number }}
wird durch diese Stornorechnung vollständig aufgehoben.
{% if original_status == "paid" %}
<br><br>
Die ursprüngliche Rechnung wurde bereits bezahlt.
Es entsteht ein Guthaben in Höhe von
{% if company.is_small_business %}
{{ total_net|floatformat:2 }}
{% else %}
{{ total_gross|floatformat:2 }}
{% endif %}
€.
Die Rückerstattung erfolgt separat.
{% endif %}
</div>
{% if company.is_small_business %}
<div class="note">
<div class="receiver-title">
Kleinunternehmerhinweis
</div>
<br>
{{ company.small_business_text }}
</div>
{% endif %}
<!-- FOOTER -->
<div class="footer">
<div>
<div class="footer-title">
Kontakt
</div>
{% if company.phone %}
{{ company.phone }}<br>
{% endif %}
{% if company.email %}
{{ company.email }}<br>
{% endif %}
{% if company.website %}
{{ company.website }}
{% endif %}
</div>
<div>
<div class="footer-title">
Steuern
</div>
{% if company.tax_number %}
Steuernummer: {{ company.tax_number }}<br>
{% endif %}
{% if company.vat_number %}
USt-IdNr.: {{ company.vat_number }}
{% endif %}
</div>
<div>
<div class="footer-title">
Bank
</div>
{% if company.bank_name %}
{{ company.bank_name }}<br>
{% endif %}
{% if company.iban %}
IBAN: {{ company.iban }}<br>
{% endif %}
{% if company.bic %}
BIC: {{ company.bic }}
{% endif %}
</div>
</div>
</body>
</html>
@@ -0,0 +1,465 @@
{% extends "base.html" %}
{% block title %}Rechnungswesen - Rechnungen{% endblock %}
{% block header_title %}Rechnung {{ invoice.invoice_number }}{% endblock %}
{% block header_description %}Vorschau{% endblock %}
{% block content %}
<div class="max-w-5xl w-5xl mx-auto bg-white rounded-2xl shadow p-10 text-slate-900">
<!-- HEADER -->
<div class="flex justify-between items-start">
<div>
{% if company.logo %}
<img src="{{ company.logo.url }}" class="h-32 mb-4">
{% else %}
<h1 class="text-2xl font-bold" style="color: {{ company.color }}">
{{ company.companyname }}
</h1>
{% if company.slogan %}
<p class="text-sm text-slate-600 mt-1">
{{ company.slogan }}
</p>
{% endif %}
{% endif %}
<div class="mt-4 text-sm">
<p>{{ company.name }}</p>
<p>
{{ company.street }}
·
{{ company.zip }}
{{ company.city }}
</p>
</div>
</div>
<div class="text-right">
<h2 class="text-3xl font-bold ">
RECHNUNG
</h2>
<table class="mt-4 text-sm ml-auto">
<tr>
<td class="text-slate-600 pr-8 py-1">
Rechnungsnummer
</td>
<td class="font-medium">
{{ invoice.invoice_number }}
</td>
</tr>
<tr>
<td class="text-slate-600 pr-8 py-1">
Rechnungsdatum
</td>
<td class="font-medium">
{{ invoice.issue_date|date:"d.m.Y" }}
</td>
</tr>
{% if invoice.due_date %}
<tr>
<td class="text-slate-600 pr-8 py-1">
Fälligkeitsdatum
</td>
<td class="font-medium">
{{ invoice.due_date|date:"d.m.Y" }}
</td>
</tr>
{% endif %}
</table>
</div>
</div>
<div class="my-8 border-t"></div>
<!-- EMPFÄNGER -->
<div class="mb-10">
<p class="text-xs uppercase tracking-wide text-slate-600 mb-2">
Rechnungsempfänger
</p>
<p class="font-semibold text-lg ">
{% if invoice.customer.type == "company" %}
{{ invoice.customer.company_name }}
{% else %}
{{ invoice.contact_name }}
{% endif %}
</p>
{% if invoice.customer.type == "company" %}
<p class="text-sm">
z.Hd. {{ invoice.contact_name }}
</p>
{% endif %}
<p class="mt-2 text-sm">
{{ invoice.location_street }}<br>
{{ invoice.location_zip }}
{{ invoice.location_city }}
</p>
</div>
<!-- TITEL -->
<div class="mb-8">
<h2 class="text-2xl font-bold" style="color: {{ company.color }}">
Rechnung
</h2>
{% if company.invoice_header_text %}
<div class="mt-3 text-sm text-slate-600">
{{ company.invoice_header_text|linebreaks }}
</div>
{% endif %}
</div>
<!-- POSITIONEN -->
<div class="border rounded-xl overflow-hidden">
<table class="w-full text-sm">
<thead>
<tr style="background-color: {{ company.color }}" class="text-white">
<th class="p-3 text-left">
Nr.
</th>
<th class="p-3 text-left">
Beschreibung
</th>
{% if show_service_date %}
<th class="p-3 text-left">Leistungsdatum</th>
{% endif %}
<th class="p-3 text-right">
Menge
</th>
<th class="p-3 text-right">
Einheit
</th>
<th class="p-3 text-right">
Preis
</th>
{% if not company.is_small_business %}
<th class="p-3 text-right">
Steuer
</th>
{% endif %}
<th class="p-3 text-right">
Gesamt
</th>
</tr>
</thead>
<tbody>
{% for item in items %}
<tr class="border-t">
<td class="p-3 text-left">
{{ forloop.counter }}
</td>
<td class="p-3 text-left">
{{ item.description }}
</td>
{% if show_service_date %}
<td class="p-3 text-left">
{% if item.service_period_from %}
{{ item.service_period_from|date:"d.m.Y" }}
{% endif %}
{% if item.service_period_to %}
- {{ item.service_period_to|date:"d.m.Y" }}
{% endif %}
</td>
{% endif %}
<td class="p-3 text-right">
{{ item.quantity }}
</td>
<td class="p-3 text-right">
{{ item.unit }}
</td>
<td class="p-3 text-right">
{{ item.unit_price_net|floatformat:2 }} €
</td>
{% if not company.is_small_business %}
<td class="p-3 text-right">
{{ item.unit_tax_rate|floatformat:0 }} %
</td>
{% endif %}
<td class="p-3 text-right font-medium">
{{ item.calculated_total_net|floatformat:2 }} €
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- SUMMEN -->
<div class="flex justify-end mt-8">
<div class="w-80 text-sm">
<div class="flex justify-between py-2">
<span class="text-slate-600">
Netto
</span>
<span>
{{ total_net|floatformat:2 }} €
</span>
</div>
{% if not company.is_small_business %}
{% for tax_rate, tax_amount in tax_summary.items %}
<div class="flex justify-between py-2">
<span class="text-slate-600">
Umsatzsteuer {{ tax_rate|floatformat:0 }} %
</span>
<span>
{{ tax_amount|floatformat:2 }} €
</span>
</div>
{% endfor %}
{% endif %}
<div class="border-t mt-3 pt-4 flex justify-between text-lg font-bold ">
<span>
Gesamt
</span>
<span>
{% if company.is_small_business %}
{{ total_net|floatformat:2 }} €
{% else %}
{{ total_gross|floatformat:2 }} €
{% endif %}
</span>
</div>
</div>
</div>
<!-- NOTIZ -->
{% if invoice.note %}
<div class="mt-10 border rounded-xl p-4">
<p class="text-xs uppercase tracking-wide text-slate-600 mb-2">
Rechnungshinweis
</p>
<div class="text-sm">
{{ invoice.note|linebreaks }}
</div>
</div>
{% endif %}
<!-- KLEINUNTERNEHMER -->
{% if company.is_small_business %}
<div class="mt-10 border rounded-xl p-4">
<p class="text-xs uppercase tracking-wide text-slate-600 mb-2">
Kleinunternehmerhinweis
</p>
<div class="text-sm">
{{ company.small_business_text }}
</div>
</div>
{% endif %}
<!-- FOOTER -->
<div class="mt-14 pt-6 border-t grid grid-cols-3 gap-6 text-xs text-slate-600">
<!-- KONTAKT -->
<div>
<p class="font-semibold mb-2">
Kontakt
</p>
{% if company.phone %}
<p>{{ company.phone }}</p>
{% endif %}
{% if company.email %}
<p>{{ company.email }}</p>
{% endif %}
{% if company.website %}
<p>{{ company.website }}</p>
{% endif %}
</div>
<!-- STEUERN -->
<div>
<p class="font-semibold mb-2">
Steuern
</p>
{% if company.tax_number %}
<p>
Steuernummer:
{{ company.tax_number }}
</p>
{% endif %}
{% if company.vat_number %}
<p>
USt-IdNr.:
{{ company.vat_number }}
</p>
{% endif %}
</div>
<!-- BANK -->
<div>
<p class="font-semibold mb-2">
Bankverbindung
</p>
{% if company.bank_name %}
<p>
{{ company.bank_name }}
</p>
{% endif %}
{% if company.iban %}
<p>
IBAN:
{{ company.iban }}
</p>
{% endif %}
{% if company.bic %}
<p>
BIC:
{{ company.bic }}
</p>
{% endif %}
</div>
</div>
</div>
{% endblock %}
@@ -0,0 +1,36 @@
{% extends "base.html" %}
{% block title %}
Rechnungswesen - Posten
{% endblock %}
{% block header_title %}
Posten hinzufügen
{% endblock %}
{% block header_description %}
Neue Ware oder Leistung hinzufügen
{% endblock %}
{% block content %}
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<form method="post" enctype="multipart/form-data" class="space-y-4">
{% csrf_token %}
<div class="grid grid-cols-1 gap-4">
{{ form.as_p }}
</div>
<button
type="submit"
class="btn mt-4 btn-save">
Speichern
</button>
</form>
</div>
{% endblock %}
@@ -0,0 +1,56 @@
{% extends "base.html" %}
{% block title %}
Rechnungswesen - Posten
{% endblock %}
{% block header_title %}
Posten
{% endblock %}
{% block header_description %}
Leistungen und Artikel verwalten
{% endblock %}
{% block content %}
{% load django_tables2 %}
<div class="mb-8">
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
<div>
<h2 class="text-xl font-bold">Aktionen</h2>
<p class="text-slate-400 text-sm mt-1">
Leistungen und Artikel anlegen
</p>
</div>
<div class="flex flex-col sm:flex-row gap-2">
<a href="{% url 'invoices:items_add' %}"
class="btn btn-add">
+ Posten
</a>
</div>
</div>
</div>
</div>
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors mt-8">
<div class="flex flex-wrap justify-between items-center mb-6">
<div>
<h2 class="text-xl font-bold">
Artikel und Leistungen
</h2>
<p class="text-slate-400 text-sm mt-1">
Übersicht über alle Artikel und Leistungen
</p>
</div>
</div>
<div class="overflow-x-auto rounded-xl">
{% render_table table %}
</div>
</div>
{% endblock %}
@@ -0,0 +1,69 @@
{% extends "base.html" %}
{% block title %}Rechnungswesen - Angebote{% endblock %}
{% block header_title %}
Angebot erstellen
{% endblock %}
{% block header_description %}
Angebot für einen Kunden erstellen
{% endblock %}
{% block content %}
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<form method="post" enctype="multipart/form-data" class="space-y-4">
{% csrf_token %}
{{ form.as_p }}
<button class="btn btn-save">
Speichern
</button>
</form>
</div>
<script>
document.addEventListener("DOMContentLoaded", function () {
const customer = document.getElementById("id_customer");
const contact = document.getElementById("id_contact");
const location = document.getElementById("id_location");
function fill(select, data) {
select.innerHTML = "<option value=''>---------</option>";
data.forEach(item => {
select.innerHTML += `<option value="${item.id}">${item.text}</option>`;
});
}
if (customer) {
customer.addEventListener("change", function () {
const id = this.value;
if (!id) {
contact.innerHTML = "";
location.innerHTML = "";
return;
}
fetch(`/invoices/ajax/customer/${id}/contacts/`)
.then(r => r.json())
.then(data => fill(contact, data));
fetch(`/invoices/ajax/customer/${id}/locations/`)
.then(r => r.json())
.then(data => fill(location, data));
});
}
});
</script>
{% endblock %}
@@ -0,0 +1,156 @@
{% extends "base.html" %}
{% block title %}Rechnungswesen - Angebote{% endblock %}
{% block header_title %}
Angebot {{ offer.offer_number }}
{% endblock %}
{% block header_description %}
Details & Positionen
{% endblock %}
{% block content %}
{% load django_tables2 %}
<div class="mb-8">
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
<div>
<h2 class="text-xl font-bold">Aktionen</h2>
<p class="text-slate-400 text-sm mt-1">
Positionen zum Angebot hinzufügen
</p>
</div>
<div class="flex flex-col sm:flex-row gap-2">
{% if offer.status == "draft" %}
<a href="{% url 'invoices:offers_item_add' offer.id %}" class="btn btn-add">+ Position</a>
<a href="{% url 'invoices:offers_preview' offer.id %}" class="btn btn-action">Vorschau</a>
<a href="{% url 'invoices:offers_download' offer.id %}" onclick="startDownloadAndRedirect()" class="btn btn-action">Download</a>
<a href="{% url 'invoices:offers_delete' offer.id %}" class="btn btn-delete">Löschen</a>
{% endif %}
{% if offer.status == "sent" %}
<a href="{% url 'invoices:offers_download' offer.id %}" class="btn btn-action">Download</a>
<a href="{% url 'invoices:offers_accept' offer.id %}" class="btn btn-action">Akzeptiert</a>
<a href="{% url 'invoices:offers_reject' offer.id %}" class="btn btn-action">Abgelehnt</a>
<a href="{% url 'invoices:offers_expire' offer.id %}" class="btn btn-action">Ausgelaufen</a>
{% endif %}
{% if offer.status == "accepted" or offer.status == "rejected" or offer.status == "expired" %}
<a href="{% url 'invoices:offers_download' offer.id %}" class="btn btn-action">Download</a>
{% endif %}
</div>
</div>
</div>
</div>
<!-- HEADER INFO -->
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors mb-8">
<h2 class="text-xl font-bold mb-6">Allgemein</h2>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
<!-- Kunde -->
<div>
<div class="text-slate-400 text-sm">Kunde</div>
<div class="font-semibold">{{ offer.customer.company_name }}</div>
</div>
<!-- Status -->
<div>
<div class="text-slate-400 text-sm">Status</div>
<div class="font-semibold">{{ offer.get_status_display }}</div>
</div>
<!-- Datum -->
<div>
<div class="text-slate-400 text-sm">Angebotsdatum</div>
<div class="font-semibold">{{ offer.issue_date|date:"d.m.Y" }}</div>
</div>
<div>
<div class="text-slate-400 text-sm">Gültig bis</div>
<div class="font-semibold">{{ offer.due_date|date:"d.m.Y" }}</div>
</div>
</div>
</div>
<!-- SNAPSHOT: KONTAKT & ADRESSE -->
<div class="mb-8 p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<h2 class="text-xl font-bold mb-6">Angebotsadresse</h2>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
<!-- Kontakt -->
<div>
<div class="text-slate-400 text-sm">Ansprechpartner</div>
<div class="font-semibold">{{ offer.contact_name|default:"-" }}</div>
{% if offer.contact_email %}
<div class="text-slate-400 text-sm">{{ offer.contact_email }}</div>
{% endif %}
{% if offer.contact_phone %}
<div class="text-slate-400 text-sm">{{ offer.contact_phone }}</div>
{% endif %}
</div>
<!-- Adresse -->
<div>
<div class="text-slate-400 text-sm">Standort</div>
<div class="font-semibold">
{{ offer.location_name|default:"-" }}
</div>
<div class="text-slate-400 text-sm">
{{ offer.location_street }}
</div>
<div class="text-slate-400 text-sm">
{{ offer.location_zip }} {{ offer.location_city }}
</div>
<div class="text-slate-400 text-sm">
{{ offer.location_country }}
</div>
</div>
</div>
</div>
<div class="mb-8 p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<h2 class="text-xl font-bold mb-6">Angebotshinweis</h2>
<div class="grid grid-cols-1 gap-6">
<div>
<div class="font-semibold">{{ offer.note|default:"-" }}</div>
</div>
</div>
</div>
<!-- POSITIONEN -->
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<h2 class="text-xl font-bold mb-6">Positionen</h2>
<div class="overflow-x-auto rounded-xl">
{% render_table table %}
</div>
<div class="mt-4 text-right font-bold">
Gesamt: {{ total_net|floatformat:2 }} €
</div>
</div>
<script>
function startDownloadAndRedirect() {
setTimeout(function() {
window.location.href = "{% url 'invoices:offers_detail' offer.id %}";
}, 1000);
}
</script>
{% endblock %}
@@ -0,0 +1,197 @@
{% extends "base.html" %}
{% block title %}
Rechnungswesen - Rechnungen
{% endblock %}
{% block header_title %}
Rechnungsposition hinzufügen
{% endblock %}
{% block header_description %}
Neue Rechnungsposition anlegen
{% endblock %}
{% block content %}
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<form method="post" enctype="multipart/form-data" class="space-y-4">
{% csrf_token %}
{{ form.as_p }}
<button class="btn btn-save">
Speichern
</button>
</form>
</div>
<script>
document.addEventListener("DOMContentLoaded", function () {
const source = document.getElementById("id_source_type");
const fields = {
item: document.getElementById("id_item"),
time: document.getElementById("id_time_entry"),
expense: document.getElementById("id_expense"),
};
function toggleFields() {
const value = source.value;
Object.keys(fields).forEach(key => {
const field = fields[key];
if (!field) {
return;
}
const row = field.closest("p");
if (key === value) {
row.style.display = "";
} else {
row.style.display = "none";
field.value = "";
}
});
}
source.addEventListener(
"change",
toggleFields
);
// Initial ausführen
toggleFields();
});
</script>
<script>
document.addEventListener("DOMContentLoaded", function () {
const source = document.getElementById("id_source_type");
const selects = {
item: document.getElementById("id_item"),
time: document.getElementById("id_time_entry"),
expense: document.getElementById("id_expense"),
};
const description = document.getElementById(
"id_description"
);
const quantity = document.getElementById(
"id_quantity"
);
const unit = document.getElementById(
"id_unit"
);
const price = document.getElementById(
"id_unit_price_net"
);
const tax = document.getElementById(
"id_unit_tax_rate"
);
function loadData(type, id){
if(!id){
return;
}
fetch(
`/invoices/ajax/invoice-item-data/?source=${type}&id=${id}`
)
.then(response => response.json())
.then(data => {
description.value = data.description;
quantity.value = data.quantity;
unit.value = data.unit;
price.value = data.unit_price_net;
tax.value = data.unit_tax_rate;
});
}
Object.keys(selects).forEach(type => {
if(selects[type]){
selects[type].addEventListener(
"change",
function(){
loadData(
type,
this.value
);
}
);
}
});
});
</script>
{% endblock %}
@@ -0,0 +1,35 @@
{% extends "base.html" %}
{% block title %}Rechnungswesen - Angebote{% endblock %}
{% block header_title %}Angebote{% endblock %}
{% block header_description %}Angebote erstellen und verwalten{% endblock %}
{% block content %}
{% load django_tables2 %}
<div class="mb-8">
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
<div>
<h2 class="text-xl font-bold">Aktionen</h2>
<p class="text-slate-400 text-sm mt-1">Neues Angebot erstellen</p>
</div>
<div class="flex flex-col sm:flex-row gap-2">
<a href="{% url 'invoices:invoices_offers_add' %}" class="btn btn-add">+ Angebot</a>
</div>
</div>
</div>
</div>
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<div class="mb-6">
<h2 class="text-xl font-bold">Angebote</h2>
<p class="text-slate-400 text-sm mt-1">Alle Angebote mit aktuellem Status</p>
</div>
<div class="overflow-x-auto rounded-xl">
{% render_table table %}
</div>
</div>
{% endblock %}
+732
View File
@@ -0,0 +1,732 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<style>
@page {
size: A4;
margin: 18mm;
}
body {
font-family: Arial, sans-serif;
font-size: 11px;
color: #334155;
}
h1,h2,h3,p {
margin:0;
}
table {
width:100%;
border-collapse:collapse;
}
.header {
display:flex;
justify-content:space-between;
}
.logo {
height:128px;
margin-bottom:15px;
}
.company-name {
font-size:24px;
font-weight:bold;
}
.invoice-title {
font-size:32px;
font-weight:bold;
}
.info-table td {
padding:4px 0;
}
.label {
color:#64748b;
padding-right:30px;
}
.divider {
border-top:1px solid #e2e8f0;
margin:30px 0;
}
.receiver-title {
font-size:10px;
text-transform:uppercase;
color:#64748b;
letter-spacing:1px;
}
.receiver-name {
font-size:16px;
font-weight:bold;
margin-top:5px;
}
.invoice-heading {
font-size:22px;
font-weight:bold;
margin-bottom:10px;
}
.items-wrapper {
border:1px solid #e2e8f0;
border-radius:10px;
overflow:hidden;
margin-top:20px;
}
.items {
width:100%;
border-collapse:collapse;
}
.items thead th:first-child {
border-top-left-radius:10px;
}
.items thead th:last-child {
border-top-right-radius:10px;
}
.items th {
background:{{ company.color }};
color:white;
padding:10px;
text-align:left;
}
.items td {
padding:10px;
border-bottom:1px solid #e2e8f0;
}
.items tbody tr:last-child td {
border-bottom:none;
}
.right {
text-align:right;
}
.total-box {
margin-top:30px;
margin-left:auto;
width:250px;
}
.total-row {
display:flex;
justify-content:space-between;
padding:6px 0;
}
.total-final {
border-top:1px solid #cbd5e1;
margin-top:10px;
padding-top:10px;
font-size:16px;
font-weight:bold;
}
.note {
margin-top:30px;
padding:12px;
border:1px solid #e2e8f0;
border-radius: 10px;
}
.small-business {
margin-top:25px;
color:#64748b;
}
@page {
size: A4;
margin: 18mm 18mm 35mm 18mm;
}
.footer {
position: fixed;
bottom: -25mm;
left: 0;
right: 0;
padding-top:15px;
border-top:1px solid #e2e8f0;
display:flex;
justify-content:space-between;
font-size:10px;
color:#64748b;
}
.footer-title {
font-weight:bold;
color:#334155;
margin-bottom:5px;
}
</style>
</head>
<body>
<!-- HEADER -->
<div class="header">
<div>
{% if company.logo %}
<img src="{{ company.logo.url }}" class="logo">
{% else %}
<h1 class="company-name" style="color: {{ company.color }}">
{{ company.companyname }}
</h1>
{% if company.slogan %}
<p>
{{ company.slogan }}
</p>
{% endif %}
{% endif %}
<p style="margin-top:15px">
{{ company.name }}<br>
{{ company.street }} · {{ company.zip }} {{ company.city }}
</p>
</div>
<div style="text-align:right">
<div class="invoice-title">
ANGEBOT
</div>
<table class="info-table">
<tr>
<td class="label">
Angebotsnummer
</td>
<td>
{{ offer.offer_number }}
</td>
</tr>
<tr>
<td class="label">
Angebotsdatum
</td>
<td>
{{ offer.issue_date|date:"d.m.Y" }}
</td>
</tr>
{% if offer.due_date %}
<tr>
<td class="label">
Gültig bis
</td>
<td>
{{ offer.due_date|date:"d.m.Y" }}
</td>
</tr>
{% endif %}
</table>
</div>
</div>
<div class="divider"></div>
<!-- EMPFÄNGER -->
<div>
<div class="receiver-title">
Angebotsempfänger
</div>
<div class="receiver-name">
{% if offer.customer.type == "company" %}
{{ offer.customer.company_name }}
{% else %}
{{ offer.contact_name }}
{% endif %}
</div>
{% if offer.customer.type == "company" %}
<p>
z.Hd. {{ offer.contact_name }}
</p>
{% endif %}
<p style="margin-top:10px">
{{ offer.location_street }}<br>
{{ offer.location_zip }}
{{ offer.location_city }}
</p>
</div>
<!-- TITEL -->
<div style="margin-top:40px">
<div class="invoice-heading"
style="color:{{ company.color }}">
Angebot
</div>
{% if company.offer_header_text %}
<p>
{{ company.offer_header_text|linebreaksbr }}
</p>
{% endif %}
</div>
<!-- POSITIONEN -->
<div class="items-wrapper">
<table class="items">
<thead>
<tr>
<th>
Nr.
</th>
<th>
Beschreibung
</th>
<th class="left">
Menge
</th>
<th class="left">
Einheit
</th>
<th class="left">
Preis
</th>
{% if not company.is_small_business %}
<th class="left">
Steuer
</th>
{% endif %}
<th class="left">
Gesamt
</th>
</tr>
</thead>
<tbody>
{% for item in items %}
<tr>
<td>
{{ forloop.counter }}
</td>
<td>
{{ item.description }}
</td>
<td class="left">
{{ item.quantity }}
</td>
<td class="left">
{{ item.unit }}
</td>
<td class="left">
{{ item.unit_price_net|floatformat:2 }} €
</td>
{% if not company.is_small_business %}
<td class="left">
{{ item.unit_tax_rate|floatformat:0 }} %
</td>
{% endif %}
<td class="left">
{{ item.calculated_total_net|floatformat:2 }} €
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- SUMMEN -->
<div class="total-box">
<div class="total-row">
<span>
Netto
</span>
<span>
{{ total_net|floatformat:2 }} €
</span>
</div>
{% if not company.is_small_business %}
{% for tax_rate,tax_amount in tax_summary.items %}
<div class="total-row">
<span>
Umsatzsteuer {{ tax_rate|floatformat:0 }} %
</span>
<span>
{{ tax_amount|floatformat:2 }} €
</span>
</div>
{% endfor %}
{% endif %}
<div class="total-row total-final">
<span>
Gesamt
</span>
<span>
{% if company.is_small_business %}
{{ total_net|floatformat:2 }}
{% else %}
{{ total_gross|floatformat:2 }}
{% endif %}
€
</span>
</div>
</div>
{% if offer.note %}
<div class="note">
<div class="receiver-title">
Angebotshinweis
</div>
<br>
{{ offer.note|linebreaksbr }}
</div>
{% endif %}
{% if company.is_small_business %}
<div class="note">
<div class="receiver-title">
Kleinunternehmerhinweis
</div>
<br>
{{ company.small_business_text }}
</div>
{% endif %}
<!-- FOOTER -->
<div class="footer">
<div>
<div class="footer-title">
Kontakt
</div>
{% if company.phone %}
{{ company.phone }}<br>
{% endif %}
{% if company.email %}
{{ company.email }}<br>
{% endif %}
{% if company.website %}
{{ company.website }}
{% endif %}
</div>
<div>
<div class="footer-title">
Steuern
</div>
{% if company.tax_number %}
Steuernummer: {{ company.tax_number }}<br>
{% endif %}
{% if company.vat_number %}
USt-IdNr.: {{ company.vat_number }}
{% endif %}
</div>
<div>
<div class="footer-title">
Bank
</div>
{% if company.bank_name %}
{{ company.bank_name }}<br>
{% endif %}
{% if company.iban %}
IBAN: {{ company.iban }}<br>
{% endif %}
{% if company.bic %}
BIC: {{ company.bic }}
{% endif %}
</div>
</div>
</body>
</html>
@@ -0,0 +1,450 @@
{% extends "base.html" %}
{% block title %}Rechnungswesen - Angebote{% endblock %}
{% block header_title %}Angebot {{ offer.offer_number }}{% endblock %}
{% block header_description %}Vorschau{% endblock %}
{% block content %}
<div class="w-5xl max-w-5xl mx-auto bg-white rounded-2xl shadow p-10 text-slate-900">
<!-- HEADER -->
<div class="flex justify-between items-start">
<div>
{% if company.logo %}
<img src="{{ company.logo.url }}" class="h-32 mb-4">
{% else %}
<h1 class="text-2xl font-bold" style="color: {{ company.color }}">
{{ company.companyname }}
</h1>
{% if company.slogan %}
<p class="text-sm text-slate-600 mt-1">
{{ company.slogan }}
</p>
{% endif %}
{% endif %}
<div class="mt-4 text-sm">
<p>{{ company.name }}</p>
<p>
{{ company.street }}
·
{{ company.zip }}
{{ company.city }}
</p>
</div>
</div>
<div class="text-right">
<h2 class="text-3xl font-bold ">
ANGEBOT
</h2>
<table class="mt-4 text-sm ml-auto">
<tr>
<td class="text-slate-600 pr-8 py-1">
Angebotsnummer
</td>
<td class="font-medium">
{{ offer.offer_number }}
</td>
</tr>
<tr>
<td class="text-slate-600 pr-8 py-1">
Angebotsdatum
</td>
<td class="font-medium">
{{ offer.issue_date|date:"d.m.Y" }}
</td>
</tr>
{% if offer.due_date %}
<tr>
<td class="text-slate-600 pr-8 py-1">
Gültig bis
</td>
<td class="font-medium">
{{ offer.due_date|date:"d.m.Y" }}
</td>
</tr>
{% endif %}
</table>
</div>
</div>
<div class="my-8 border-t"></div>
<!-- EMPFÄNGER -->
<div class="mb-10">
<p class="text-xs uppercase tracking-wide text-slate-600 mb-2">
Angebotsempfänger
</p>
<p class="font-semibold text-lg ">
{% if offer.customer.type == "company" %}
{{ offer.customer.company_name }}
{% else %}
{{ offer.contact_name }}
{% endif %}
</p>
{% if offer.customer.type == "company" %}
<p class="text-sm">
z.Hd. {{ offer.contact_name }}
</p>
{% endif %}
<p class="mt-2 text-sm">
{{ offer.location_street }}<br>
{{ offer.location_zip }}
{{ offer.location_city }}
</p>
</div>
<!-- TITEL -->
<div class="mb-8">
<h2 class="text-2xl font-bold" style="color: {{ company.color }}">
Angebot
</h2>
{% if company.offer_header_text %}
<div class="mt-3 text-sm text-slate-600">
{{ company.offer_header_text|linebreaks }}
</div>
{% endif %}
</div>
<!-- POSITIONEN -->
<div class="border rounded-xl overflow-hidden">
<table class="w-full text-sm">
<thead>
<tr style="background-color: {{ company.color }}" class="text-white">
<th class="p-3 text-left">
Nr.
</th>
<th class="p-3 text-left">
Beschreibung
</th>
<th class="p-3 text-right">
Menge
</th>
<th class="p-3 text-right">
Einheit
</th>
<th class="p-3 text-right">
Preis
</th>
{% if not company.is_small_business %}
<th class="p-3 text-right">
Steuer
</th>
{% endif %}
<th class="p-3 text-right">
Gesamt
</th>
</tr>
</thead>
<tbody>
{% for item in items %}
<tr class="border-t">
<td class="p-3 text-left">
{{ forloop.counter }}
</td>
<td class="p-3 text-left">
{{ item.description }}
</td>
<td class="p-3 text-right">
{{ item.quantity }}
</td>
<td class="p-3 text-right">
{{ item.unit }}
</td>
<td class="p-3 text-right">
{{ item.unit_price_net|floatformat:2 }} €
</td>
{% if not company.is_small_business %}
<td class="p-3 text-right">
{{ item.unit_tax_rate|floatformat:0 }} %
</td>
{% endif %}
<td class="p-3 text-right font-medium">
{{ item.calculated_total_net|floatformat:2 }} €
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- SUMMEN -->
<div class="flex justify-end mt-8">
<div class="w-80 text-sm">
<div class="flex justify-between py-2">
<span class="text-slate-600">
Netto
</span>
<span>
{{ total_net|floatformat:2 }} €
</span>
</div>
{% if not company.is_small_business %}
{% for tax_rate, tax_amount in tax_summary.items %}
<div class="flex justify-between py-2">
<span class="text-slate-600">
Umsatzsteuer {{ tax_rate|floatformat:0 }} %
</span>
<span>
{{ tax_amount|floatformat:2 }} €
</span>
</div>
{% endfor %}
{% endif %}
<div class="border-t mt-3 pt-4 flex justify-between text-lg font-bold ">
<span>
Gesamt
</span>
<span>
{% if company.is_small_business %}
{{ total_net|floatformat:2 }} €
{% else %}
{{ total_gross|floatformat:2 }} €
{% endif %}
</span>
</div>
</div>
</div>
<!-- NOTIZ -->
{% if offer.note %}
<div class="mt-10 border rounded-xl p-4">
<p class="text-xs uppercase tracking-wide text-slate-600 mb-2">
Angebotshinweis
</p>
<div class="text-sm">
{{ offer.note|linebreaks }}
</div>
</div>
{% endif %}
<!-- KLEINUNTERNEHMER -->
{% if company.is_small_business %}
<div class="mt-10 border rounded-xl p-4">
<p class="text-xs uppercase tracking-wide text-slate-600 mb-2">
Kleinunternehmerhinweis
</p>
<div class="text-sm">
{{ company.small_business_text }}
</div>
</div>
{% endif %}
<!-- FOOTER -->
<div class="mt-14 pt-6 border-t grid grid-cols-3 gap-6 text-xs text-slate-600">
<!-- KONTAKT -->
<div>
<p class="font-semibold mb-2">
Kontakt
</p>
{% if company.phone %}
<p>{{ company.phone }}</p>
{% endif %}
{% if company.email %}
<p>{{ company.email }}</p>
{% endif %}
{% if company.website %}
<p>{{ company.website }}</p>
{% endif %}
</div>
<!-- STEUERN -->
<div>
<p class="font-semibold mb-2">
Steuern
</p>
{% if company.tax_number %}
<p>
Steuernummer:
{{ company.tax_number }}
</p>
{% endif %}
{% if company.vat_number %}
<p>
USt-IdNr.:
{{ company.vat_number }}
</p>
{% endif %}
</div>
<!-- BANK -->
<div>
<p class="font-semibold mb-2">
Bankverbindung
</p>
{% if company.bank_name %}
<p>
{{ company.bank_name }}
</p>
{% endif %}
{% if company.iban %}
<p>
IBAN:
{{ company.iban }}
</p>
{% endif %}
{% if company.bic %}
<p>
BIC:
{{ company.bic }}
</p>
{% endif %}
</div>
</div>
</div>
{% endblock %}
+61
View File
@@ -0,0 +1,61 @@
{% extends "base.html" %}
{% block title %}
Rechnungswesen
{% endblock %}
{% block header_title %}
Rechnungswesen
{% endblock %}
{% block header_description %}
Angebote, Rechnungen, Mahnungen verwalten
{% endblock %}
{% block content %}
<!-- MODULE GRID -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<a href="{% url 'invoices:invoices_offers_overview' %}" class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<div class="text-2xl mb-2">📄</div>
<h2 class="text-xl font-bold">Angebote</h2>
<p class="text-slate-400 text-sm mt-1">Angebote erstellen und verwalten</p>
</a>
<a href="{% url 'invoices:invoices_invoices_overview' %}" class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<div class="text-2xl mb-2">🧾</div>
<h2 class="text-xl font-bold">Rechnungen</h2>
<p class="text-slate-400 text-sm mt-1">Rechnungen erstellen und verwalten</p>
</a>
<a href="{% url 'invoices:invoices_reminders_overview' %}" class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<div class="text-2xl mb-2">⚠️</div>
<h2 class="text-xl font-bold">Mahnungen</h2>
<p class="text-slate-400 text-sm mt-1">Offene Forderungen verfolgen</p>
</a>
<a href="{% url 'invoices:customers_overview' %}" class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<div class="text-2xl mb-2">👥</div>
<h2 class="text-xl font-bold">Kunden</h2>
<p class="text-slate-400 text-sm mt-1">Kundenstammdaten verwalten</p>
</a>
<a href="{% url 'invoices:items_overview' %}" class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<div class="text-2xl mb-2">📦</div>
<h2 class="text-xl font-bold">Posten</h2>
<p class="text-slate-400 text-sm mt-1">Leistungen und Artikel verwalten</p>
</a>
</div>
<!-- OPTIONAL INFO BOX -->
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors mt-8">
<h2 class="text-xl font-bold">
Zusammenfassung
</h2>
<p class="text-slate-400 text-sm mt-1">
folgt
</p>
</div>
{% endblock %}
@@ -0,0 +1,31 @@
{% extends "base.html" %}
{% block title %}Rechnungswesen - Mahnungen{% endblock %}
{% block header_title %}
Mahnung erstellen
{% endblock %}
{% block header_description %}
Mahnung für einen Kunden erstellen
{% endblock %}
{% block content %}
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<form method="post" enctype="multipart/form-data" class="space-y-4">
{% csrf_token %}
{{ form.as_p }}
<button class="btn btn-save">
Speichern
</button>
</form>
</div>
{% endblock %}
@@ -0,0 +1,184 @@
{% extends "base.html" %}
{% block title %}Rechnungswesen - Mahnungen{% endblock %}
{% block header_title %}
Mahnung {{ reminder.reminder_number }}
{% endblock %}
{% block header_description %}
Details
{% endblock %}
{% block content %}
<div class="mb-8">
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
<div>
<h2 class="text-xl font-bold">Aktionen</h2>
<p class="text-slate-400 text-sm mt-1">
Mahnung verwalten
</p>
</div>
<div class="flex flex-col sm:flex-row gap-2">
{% if reminder.status == "draft" %}
<a href="{% url 'invoices:reminders_download' reminder.id %}" onclick="startDownloadAndRedirect()" class="btn btn-action">Download</a>
<a href="{% url 'invoices:reminders_delete' reminder.id %}" class="btn btn-delete">Löschen</a>
{% endif %}
{% if reminder.status == "sent" %}
<a href="{% url 'invoices:reminders_download' reminder.id %}" class="btn btn-action">Download</a>
<a href="{% url 'invoices:reminders_cancel' reminder.id %}" class="btn btn-delete">Stornieren</a>
<a href="{% url 'invoices:reminders_paid' reminder.id %}" class="btn btn-action">Zahlung verbuchen</a>
<a href="{% url 'invoices:reminders_expire' reminder.id %}" class="btn btn-action">Überfällig</a>
{% endif %}
{% if reminder.status == "paid" or reminder.status == "canceled" or reminder.status == "expired" %}
<a href="{% url 'invoices:reminders_download' reminder.id %}" class="btn btn-action">Download</a>
{% endif %}
</div>
</div>
</div>
</div>
<!-- HEADER INFO -->
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors mb-8">
<h2 class="text-xl font-bold mb-6">Allgemein</h2>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
<!-- Status -->
<div>
<div class="text-slate-400 text-sm">Status</div>
<div class="font-semibold">{{ reminder.get_status_display }}</div>
</div>
<div>
<div class="text-slate-400 text-sm">Level</div>
<div class="font-semibold">{{ reminder.level }}/3</div>
</div>
<!-- Datum -->
<div>
<div class="text-slate-400 text-sm">Mahnungsdatum</div>
<div class="font-semibold">{{ reminder.issue_date|date:"d.m.Y" }}</div>
</div>
<div>
<div class="text-slate-400 text-sm">Neues Zahlungsziel</div>
<div class="font-semibold">{{ reminder.due_date|date:"d.m.Y" }}</div>
</div>
<div>
<div class="text-slate-400 text-sm">Mahngebühr</div>
<div class="font-semibold">{{ reminder.fee }} €</div>
</div>
</div>
</div>
<!-- HEADER INFO -->
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors mb-8">
<h2 class="text-xl font-bold mb-6">Rechnungsinformationen</h2>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
<div>
<div class="text-slate-400 text-sm">Rechnungsnummer</div>
<div class="font-semibold"><a class="company_link" href="{% url 'invoices:invoices_detail' reminder.invoice.id %}">{{ reminder.invoice.invoice_number }}</a></div>
</div>
<!-- Kunde -->
<div>
<div class="text-slate-400 text-sm">Kunde</div>
<div class="font-semibold">{{ reminder.invoice.customer.company_name }}</div>
</div>
<div>
<div class="text-slate-400 text-sm">Projekt</div>
<div class="font-semibold">{{ reminder.invoice.project.name|default:"-" }}</div>
</div>
<!-- Status -->
<div>
<div class="text-slate-400 text-sm">Rechnungsstatus</div>
<div class="font-semibold">{{ reminder.invoice.get_status_display }}</div>
</div>
<!-- Datum -->
<div>
<div class="text-slate-400 text-sm">Rechnungsdatum</div>
<div class="font-semibold">{{ reminder.invoice.issue_date|date:"d.m.Y" }}</div>
</div>
<div>
<div class="text-slate-400 text-sm">Ursprüngliches Zahlungsziel</div>
<div class="font-semibold">{{ reminder.invoice.due_date|date:"d.m.Y" }}</div>
</div>
</div>
</div>
<!-- SNAPSHOT: KONTAKT & ADRESSE -->
<div class="mb-8 p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<h2 class="text-xl font-bold mb-6">Mahnungsadresse</h2>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
<!-- Kontakt -->
<div>
<div class="text-slate-400 text-sm">Ansprechpartner</div>
<div class="font-semibold">{{ reminder.invoice.contact_name|default:"-" }}</div>
{% if reminder.invoice.contact_email %}
<div class="text-slate-400 text-sm">{{ reminder.invoice.contact_email }}</div>
{% endif %}
{% if reminder.invoice.contact_phone %}
<div class="text-slate-400 text-sm">{{ reminder.invoice.contact_phone }}</div>
{% endif %}
</div>
<!-- Adresse -->
<div>
<div class="text-slate-400 text-sm">Standort</div>
<div class="font-semibold">
{{ reminder.invoice.location_name|default:"-" }}
</div>
<div class="text-slate-400 text-sm">
{{ reminder.invoice.location_street }}
</div>
<div class="text-slate-400 text-sm">
{{ reminder.invoice.location_zip }} {{ reminder.invoice.location_city }}
</div>
<div class="text-slate-400 text-sm">
{{ reminder.invoice.location_country }}
</div>
</div>
</div>
</div>
<div class="mb-8 p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<h2 class="text-xl font-bold mb-6">Mahnungshinweis</h2>
<div class="grid grid-cols-1 gap-6">
<div>
<div class="font-semibold">{{ reminder.note|default:"-" }}</div>
</div>
</div>
</div>
<script>
function startDownloadAndRedirect() {
setTimeout(function() {
window.location.href = "{% url 'invoices:reminders_detail' reminder.id %}";
}, 1000);
}
</script>
{% endblock %}
@@ -0,0 +1,35 @@
{% extends "base.html" %}
{% block title %}Rechnungswesen - Mahnungen{% endblock %}
{% block header_title %}Mahnungen{% endblock %}
{% block header_description %}Offene Forderungen verfolgen{% endblock %}
{% block content %}
{% load django_tables2 %}
<div class="mb-8">
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
<div>
<h2 class="text-xl font-bold">Aktionen</h2>
<p class="text-slate-400 text-sm mt-1">Neue Mahnung erstellen</p>
</div>
<div class="flex flex-col sm:flex-row gap-2">
<a href="{% url 'invoices:invoices_reminders_add' %}" class="btn btn-add">+ Mahnung</a>
</div>
</div>
</div>
</div>
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<div class="mb-6">
<h2 class="text-xl font-bold">Mahnungen</h2>
<p class="text-slate-400 text-sm mt-1">Alle Mahnungen mit aktuellem Status</p>
</div>
<div class="overflow-x-auto rounded-xl">
{% render_table table %}
</div>
</div>
{% endblock %}
@@ -0,0 +1,663 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<style>
@page {
size: A4;
margin: 18mm;
}
body {
font-family: Arial, sans-serif;
font-size: 11px;
color: #334155;
}
h1,h2,h3,p {
margin:0;
}
table {
width:100%;
border-collapse:collapse;
}
.header {
display:flex;
justify-content:space-between;
}
.logo {
height:128px;
margin-bottom:15px;
}
.company-name {
font-size:24px;
font-weight:bold;
}
.invoice-title {
font-size:32px;
font-weight:bold;
}
.info-table td {
padding:4px 0;
}
.label {
color:#64748b;
padding-right:30px;
}
.divider {
border-top:1px solid #e2e8f0;
margin:30px 0;
}
.receiver-title {
font-size:10px;
text-transform:uppercase;
color:#64748b;
letter-spacing:1px;
}
.receiver-name {
font-size:16px;
font-weight:bold;
margin-top:5px;
}
.invoice-heading {
font-size:22px;
font-weight:bold;
margin-bottom:10px;
}
.items-wrapper {
border:1px solid #e2e8f0;
border-radius:10px;
overflow:hidden;
margin-top:20px;
}
.items {
width:100%;
border-collapse:collapse;
}
.items thead th:first-child {
border-top-left-radius:10px;
}
.items thead th:last-child {
border-top-right-radius:10px;
}
.items th {
background:{{ company.color }};
color:white;
padding:10px;
text-align:left;
}
.items td {
padding:10px;
border-bottom:1px solid #e2e8f0;
}
.items tbody tr:last-child td {
border-bottom:none;
}
.right {
text-align:right;
}
.total-box {
margin-top:30px;
margin-left:auto;
width:250px;
}
.total-row {
display:flex;
justify-content:space-between;
padding:6px 0;
}
.total-final {
border-top:1px solid #cbd5e1;
margin-top:10px;
padding-top:10px;
font-size:16px;
font-weight:bold;
}
.note {
margin-top:30px;
padding:12px;
border:1px solid #e2e8f0;
border-radius: 10px;
}
.small-business {
margin-top:25px;
color:#64748b;
}
@page {
size: A4;
margin: 18mm 18mm 35mm 18mm;
}
.footer {
position: fixed;
bottom: -25mm;
left: 0;
right: 0;
padding-top:15px;
border-top:1px solid #e2e8f0;
display:flex;
justify-content:space-between;
font-size:10px;
color:#64748b;
}
.footer-title {
font-weight:bold;
color:#334155;
margin-bottom:5px;
}
</style>
</head>
<body>
<!-- HEADER -->
<div class="header">
<div>
{% if company.logo %}
<img src="{{ company.logo.url }}" class="logo">
{% else %}
<h1 class="company-name" style="color: {{ company.color }}">
{{ company.companyname }}
</h1>
{% if company.slogan %}
<p>
{{ company.slogan }}
</p>
{% endif %}
{% endif %}
<p style="margin-top:15px">
{{ company.name }}<br>
{{ company.street }} · {{ company.zip }} {{ company.city }}
</p>
</div>
<div style="text-align:right">
<div class="invoice-title">
MAHNUNG
</div>
<table class="info-table">
<tr>
<td class="label">Mahnungsnummer</td>
<td>{{ reminder.reminder_number }}</td>
</tr>
<tr>
<td class="label">Mahnungsdatum</td>
<td>{{ reminder.issue_date|date:"d.m.Y" }}</td>
</tr>
<tr>
<td class="label">Mahnungsstufe</td>
<td>{{ reminder.level }}. Mahnung</td>
</tr>
<tr>
<td class="label">Ursprüngliches Zahlungsziel</td>
<td>{{ reminder.invoice.due_date|date:"d.m.Y" }}</td>
</tr>
<tr>
<td class="label">Neues Zahlungsziel</td>
<td>{{ reminder.due_date|date:"d.m.Y" }}</td>
</tr>
<tr>
<td class="label">Rechnungsnummer</td>
<td>{{ reminder.invoice.invoice_number }}</td>
</tr>
</table>
</div>
</div>
<div class="divider"></div>
<!-- EMPFÄNGER -->
<div>
<div class="receiver-title">
Empfänger
</div>
<div class="receiver-name">
{% if reminder.invoice.customer.type == "company" %}
{{ reminder.invoice.customer.company_name }}
{% else %}
{{ reminder.invoice.contact_name }}
{% endif %}
</div>
{% if reminder.invoice.customer.type == "company" %}
<p>
z.Hd. {{ reminder.invoice.contact_name }}
</p>
{% endif %}
<p style="margin-top:10px">
{{ reminder.invoice.location_street }}<br>
{{ reminder.invoice.location_zip }}
{{ reminder.invoice.location_city }}
</p>
</div>
<!-- TITEL -->
<div style="margin-top:40px">
<div class="invoice-heading"
style="color:{{ company.color }}">
Mahnung
</div>
{% if company.reminder_header_text %}
<p>
{{ company.reminder_header_text|linebreaksbr }}
</p>
{% endif %}
</div>
<!-- FORDERUNGSÜBERSICHT -->
<div class="items-wrapper">
<table class="items">
<thead>
<tr>
<th>
Beschreibung
</th>
<th class="right">
Betrag
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
Rechnung {{ reminder.invoice.invoice_number }}
</td>
<td>
{{ invoice_amount|floatformat:2 }} €
</td>
</tr>
{% if previous_reminders or reminder.fee > 0 %}
{% for old_reminder in previous_reminders %}
<tr>
<td>
{{ old_reminder.level }}. Mahnung
({{ old_reminder.issue_date|date:"d.m.Y" }})
</td>
<td>
{{ old_reminder.fee|floatformat:2 }} €
</td>
</tr>
{% endfor %}
<tr>
<td>
Aktuelle Mahngebühr
</td>
<td>
{{ reminder.fee|floatformat:2 }} €
</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
<!-- GESAMTFORDERUNG -->
<div class="total-box">
<div class="total-row">
<span>
Offener Rechnungsbetrag
</span>
<span>
{{ invoice_amount|floatformat:2 }} €
</span>
</div>
{% if previous_reminders or reminder.fee > 0 %}
<div class="total-row">
<span>
Mahngebühren
</span>
<span>
{{ total_fees|floatformat:2 }} €
</span>
</div>
{% endif %}
<div class="total-row total-final">
<span>
Gesamtforderung
</span>
<span>
{{ total_due|floatformat:2 }} €
</span>
</div>
</div>
{% if reminder.note %}
<div class="note">
<div class="receiver-title">
Mahnungshinweis
</div>
<br>
{{ reminder.note|linebreaksbr }}
</div>
{% endif %}
<!-- FOOTER -->
<div class="footer">
<div>
<div class="footer-title">
Kontakt
</div>
{% if company.phone %}
{{ company.phone }}<br>
{% endif %}
{% if company.email %}
{{ company.email }}<br>
{% endif %}
{% if company.website %}
{{ company.website }}
{% endif %}
</div>
<div>
<div class="footer-title">
Steuern
</div>
{% if company.tax_number %}
Steuernummer: {{ company.tax_number }}<br>
{% endif %}
{% if company.vat_number %}
USt-IdNr.: {{ company.vat_number }}
{% endif %}
</div>
<div>
<div class="footer-title">
Bank
</div>
{% if company.bank_name %}
{{ company.bank_name }}<br>
{% endif %}
{% if company.iban %}
IBAN: {{ company.iban }}<br>
{% endif %}
{% if company.bic %}
BIC: {{ company.bic }}
{% endif %}
</div>
</div>
</body>
</html>
@@ -0,0 +1,218 @@
<div>
<div class="grid md:grid-cols-2 xl:grid-cols-3 gap-6">
<!-- Ansprechpartner -->
<div>
<h3 class="font-bold mb-3">
Ansprechpartner
</h3>
{% for contact in item.contacts.all %}
<div class="
bg-slate-100
dark:bg-slate-700
rounded-xl
p-3
mb-2
">
<div class="font-medium">
{{ contact.first_name }}
{{ contact.last_name }}
</div>
{% if contact.phone %}
<div class="text-sm text-slate-400">
{{ contact.phone }}
</div>
{% endif %}
{% if contact.email %}
<div class="text-sm text-slate-400">
{{ contact.email }}
</div>
{% endif %}
</div>
{% empty %}
<div class="
bg-white
dark:bg-slate-800
border
border-orange-500
rounded-xl
p-6
text-orange-500
">
Keine Ansprechpartner vorhanden
</div>
{% endfor %}
</div>
<!-- Standorte -->
<div>
<h3 class="font-bold mb-3">
Standorte
</h3>
{% for location in item.locations.all %}
<div class="
bg-slate-100
dark:bg-slate-700
rounded-xl
p-3
mb-2
">
<div class="font-medium">
{{ location.name }}
</div>
<div class="text-sm text-slate-400">
{{ location.street }}
</div>
<div class="text-sm text-slate-400">
{{ location.zip_code }}
{{ location.city }}
</div>
<div class="text-sm text-slate-400">
{{ location.country }}
</div>
</div>
{% empty %}
<div class="
bg-white
dark:bg-slate-800
border
border-orange-500
rounded-xl
p-6
text-orange-500
">
Keine Standorte vorhanden
</div>
{% endfor %}
</div>
<!-- Projekte -->
<div>
<h3 class="font-bold mb-3">
Projekte
</h3>
{% for project in item.projects.all %}
<div class="
bg-slate-100
dark:bg-slate-700
rounded-xl
p-3
mb-2
">
{{ project.name }}
</div>
{% empty %}
<div class="
bg-white
dark:bg-slate-800
border
border-orange-500
rounded-xl
p-6
text-orange-500
">
Keine Projekte zugeordnet
</div>
{% endfor %}
</div>
</div>
<div class="mt-5 flex gap-2">
<a
href="{% url 'invoices:customers_detail' item.id %}"
class="btn btn-edit">
Bearbeiten
</a>
<form
method="post"
action="{% url 'invoices:customers_archive' item.id %}">
{% csrf_token %}
<button
type="submit"
name="archiv"
value="1"
class="btn btn-action">
Archivieren
</button>
</form>
</div>
</div>
@@ -0,0 +1,218 @@
<div>
<div class="grid md:grid-cols-2 xl:grid-cols-3 gap-6">
<!-- Ansprechpartner -->
<div>
<h3 class="font-bold mb-3">
Ansprechpartner
</h3>
{% for contact in item.contacts.all %}
<div class="
bg-slate-100
dark:bg-slate-700
rounded-xl
p-3
mb-2
">
<div class="font-medium">
{{ contact.first_name }}
{{ contact.last_name }}
</div>
{% if contact.phone %}
<div class="text-sm text-slate-400">
{{ contact.phone }}
</div>
{% endif %}
{% if contact.email %}
<div class="text-sm text-slate-400">
{{ contact.email }}
</div>
{% endif %}
</div>
{% empty %}
<div class="
bg-white
dark:bg-slate-800
border
border-orange-500
rounded-xl
p-6
text-orange-500
">
Keine Ansprechpartner vorhanden
</div>
{% endfor %}
</div>
<!-- Standorte -->
<div>
<h3 class="font-bold mb-3">
Standorte
</h3>
{% for location in item.locations.all %}
<div class="
bg-slate-100
dark:bg-slate-700
rounded-xl
p-3
mb-2
">
<div class="font-medium">
{{ location.name }}
</div>
<div class="text-sm text-slate-400">
{{ location.street }}
</div>
<div class="text-sm text-slate-400">
{{ location.zip_code }}
{{ location.city }}
</div>
<div class="text-sm text-slate-400">
{{ location.country }}
</div>
</div>
{% empty %}
<div class="
bg-white
dark:bg-slate-800
border
border-orange-500
rounded-xl
p-6
text-orange-500
">
Keine Standorte vorhanden
</div>
{% endfor %}
</div>
<!-- Projekte -->
<div>
<h3 class="font-bold mb-3">
Projekte
</h3>
{% for project in item.projects.all %}
<div class="
bg-slate-100
dark:bg-slate-700
rounded-xl
p-3
mb-2
">
{{ project.name }}
</div>
{% empty %}
<div class="
bg-white
dark:bg-slate-800
border
border-orange-500
rounded-xl
p-6
text-orange-500
">
Keine Projekte zugeordnet
</div>
{% endfor %}
</div>
</div>
<div class="mt-5 flex gap-2">
<a
href="{% url 'invoices:customers_detail' item.id %}"
class="btn btn-edit">
Bearbeiten
</a>
<form
method="post"
action="{% url 'invoices:customers_reactivate' item.id %}">
{% csrf_token %}
<button
type="submit"
name="archiv"
value="1"
class="btn btn-action">
Aktivieren
</button>
</form>
</div>
</div>
@@ -0,0 +1,20 @@
<form
method="post"
action="{% url 'invoices:invoice_item_delete' invoice.id record.id %}">
{% csrf_token %}
<input
type="hidden"
name="delete_item"
value="{{ record.id }}">
<button
type="submit"
class="btn btn-delete">
Löschen
</button>
</form>
@@ -0,0 +1,7 @@
{% if record.service_period_from %}
{{ record.service_period_from|date:"d.m.Y" }}
{% endif %}
{% if record.service_period_to %}
- {{ record.service_period_to|date:"d.m.Y" }}
{% endif %}
@@ -0,0 +1 @@
USt. {{ record.unit_tax_rate|floatformat:0 }}%
@@ -0,0 +1 @@
{{ record.calculated_total_net|floatformat:2 }} €
@@ -0,0 +1 @@
{{ record.unit_price_net }} €
@@ -0,0 +1,7 @@
<a
href="{% url 'invoices:invoices_detail' record.id %}"
class="btn btn-edit">
Bearbeiten
</a>
@@ -0,0 +1,35 @@
<form
id="save-form-{{ record.id }}"
method="post"
action="{% url 'invoices:items_edit' record.id %}">
{% csrf_token %}
</form>
<div class="flex gap-2">
<button
type="submit"
form="save-form-{{ record.id }}"
class="btn btn-save">
Speichern
</button>
<form
method="post"
action="{% url 'invoices:items_delete' record.id %}">
{% csrf_token %}
<button
type="submit"
class="btn btn-delete">
Löschen
</button>
</form>
</div>
@@ -0,0 +1,6 @@
<input
type="text"
name="description"
form="save-form-{{ record.id }}"
value="{{ record.description }}"
class="border rounded px-2 py-1 w-full">
@@ -0,0 +1,16 @@
<select
name="item_type"
form="save-form-{{ record.id }}"
class="border rounded px-2 py-1 w-full">
{% for v,l in table.types %}
<option
value="{{ v }}"
{% if record.item_type == v %}selected{% endif %}>
{{ l }}
</option>
{% endfor %}
</select>
@@ -0,0 +1,7 @@
<input
type="number"
step="0.01"
name="price_net"
form="save-form-{{ record.id }}"
value="{{ record.price_net }}"
class="border rounded px-2 py-1 w-full">
@@ -0,0 +1,7 @@
<input
type="number"
step="0.01"
name="tax_rate"
form="save-form-{{ record.id }}"
value="{{ record.tax_rate }}"
class="border rounded px-2 py-1 w-full">
@@ -0,0 +1,20 @@
<select
name="unit"
form="save-form-{{ record.id }}"
class="border rounded px-2 py-1 w-full">
{% for unit in table.units %}
<option
value="{{ unit.id }}"
{% if record.unit_id == unit.id %}selected{% endif %}>
{{ unit.name }}
</option>
{% endfor %}
</select>
@@ -0,0 +1,20 @@
<form
method="post"
action="{% url 'invoices:offers_item_delete' table.offer.id record.id %}">
{% csrf_token %}
<input
type="hidden"
name="delete_item"
value="{{ record.id }}">
<button
type="submit"
class="btn btn-delete">
Löschen
</button>
</form>
@@ -0,0 +1 @@
USt. {{ record.unit_tax_rate|floatformat:0 }}%
@@ -0,0 +1 @@
{{ record.calculated_total_net|floatformat:2 }} €
@@ -0,0 +1 @@
{{ record.unit_price_net }} €
@@ -0,0 +1,7 @@
<a
href="{% url 'invoices:offers_detail' record.id %}"
class="btn btn-edit">
Bearbeiten
</a>
@@ -0,0 +1,7 @@
<a
href="{% url 'invoices:reminders_detail' record.id %}"
class="btn btn-edit">
Bearbeiten
</a>
@@ -0,0 +1,7 @@
<a
class="company_link"
href="{% url 'invoices:invoices_detail' record.invoice.id %}">
{{ record.invoice.invoice_number }}
</a>
+3
View File
@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.
+77
View File
@@ -0,0 +1,77 @@
from django.urls import path
from . import views
app_name = "invoices"
urlpatterns = [
path("", views.invoices_overview, name="overview"),
# Kunden
path("customers/", views.customers_overview, name="customers_overview"),
path("customers/add/", views.customers_add, name="customers_add"),
path("customers/<int:customer_id>/", views.customers_detail, name="customers_detail"),
path("customers/<int:customer_id>/edit/", views.customers_edit, name="customers_edit"),
path("customers/<int:customer_id>/archive/", views.customers_archive, name="customers_archive"),
path("customers/<int:customer_id>/reactivate/", views.customers_reactivate, name="customers_reactivate"),
# Items
path("items/", views.items_overview, name="items_overview"),
path("items/add/", views.items_add, name="items_add"),
path("items/<int:item_id>/edit/", views.items_edit, name="items_edit"),
path("items/<int:item_id>/delete/", views.items_delete, name="items_delete"),
# Rechnungen
path("invoices/", views.invoices_invoices_overview, name="invoices_invoices_overview"),
path("invoices/<int:invoice_id>/", views.invoices_detail, name="invoices_detail"),
path("invoices/add/customer/", views.invoice_add_customer, name="invoice_add_customer"),
path("invoices/add/project/", views.invoice_add_project, name="invoice_add_project"),
path("invoices/<int:invoice_id>/items/add/", views.invoice_item_add, name="invoice_item_add"),
path("invoices/<int:invoice_id>/items/<int:item_id>/delete/", views.invoice_item_delete, name="invoice_item_delete"),
path("invoices/<int:invoice_id>/preview/", views.invoices_preview, name="invoices_preview"),
path("invoices/<int:invoice_id>/download/", views.invoices_download, name="invoices_download"),
path("invoices/<int:invoice_id>/cancel/", views.invoices_cancel, name="invoices_cancel"),
path("invoices/<int:invoice_id>/paid/", views.invoices_paid, name="invoices_paid"),
path("invoices/<int:invoice_id>/overdue/", views.invoices_overdue, name="invoices_overdue"),
path("invoices/<int:invoice_id>/delete/", views.invoices_delete, name="invoices_delete"),
# Angebote
path("offers/", views.invoices_offers_overview, name="invoices_offers_overview"),
path("offers/<int:offer_id>/", views.offers_detail, name="offers_detail"),
path("offers/add/", views.invoices_offers_add, name="invoices_offers_add"),
path("offers/<int:offer_id>/items/add/", views.offers_item_add, name="offers_item_add"),
path("offers/<int:offer_id>/items/<int:item_id>/delete/", views.offers_item_delete, name="offers_item_delete"),
path("offers/<int:offer_id>/preview/", views.offers_preview, name="offers_preview"),
path("offers/<int:offer_id>/download/", views.offers_download, name="offers_download"),
path("offers/<int:offer_id>/accept/", views.offers_accept, name="offers_accept"),
path("offers/<int:offer_id>/reject/", views.offers_reject, name="offers_reject"),
path("offers/<int:offer_id>/expire/", views.offers_expire, name="offers_expire"),
path("offers/<int:offer_id>/delete/", views.offers_delete, name="offers_delete"),
# Mahnungen
path("reminders/", views.invoices_reminders_overview, name="invoices_reminders_overview"),
path("reminders/<int:reminder_id>/", views.reminders_detail, name="reminders_detail"),
path("reminders/add/", views.invoices_reminders_add, name="invoices_reminders_add"),
path("reminders/<int:reminder_id>/download/", views.reminders_download, name="reminders_download"),
path("reminders/<int:reminder_id>/cancel/", views.reminders_cancel, name="reminders_cancel"),
path("reminders/<int:reminder_id>/delete/", views.reminders_delete, name="reminders_delete"),
path("reminders/<int:reminder_id>/expire/", views.reminders_expire, name="reminders_expire"),
path("reminders/<int:reminder_id>/paid/", views.reminders_paid, name="reminders_paid"),
# AJAX
path("ajax/customer/<int:customer_id>/contacts/", views.ajax_contacts, name="ajax_contacts"),
path("ajax/customer/<int:customer_id>/locations/", views.ajax_locations, name="ajax_locations"),
path("ajax/project/<int:project_id>/invoice-data/", views.ajax_invoicedata, name="ajax_invoicedata"),
path("ajax/invoice/<int:invoice_id>/sources/", views.ajax_invoiceitemsources, name="ajax_invoice_item_sources"),
path("ajax/invoice-item-data/", views.ajax_invoice_item_data, name="ajax_invoice_item_data"),
]
+2524
View File
File diff suppressed because it is too large Load Diff