Initial commit
This commit is contained in:
@@ -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",)
|
||||
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class InvoicesConfig(AppConfig):
|
||||
name = 'invoices'
|
||||
@@ -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()
|
||||
@@ -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'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
+28
@@ -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',
|
||||
),
|
||||
]
|
||||
@@ -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"
|
||||
@@ -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",
|
||||
)
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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>
|
||||
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user