v1.0
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
# invoices/__init__.py
|
||||
default_app_config = "invoices.apps.InvoicesConfig"
|
||||
+67
-1
@@ -7,7 +7,9 @@ from .models import (
|
||||
Invoice,
|
||||
InvoiceItem,
|
||||
Unit,
|
||||
Reminder
|
||||
Reminder,
|
||||
InvoicePayment,
|
||||
InvoicePaymentAllocation,
|
||||
)
|
||||
|
||||
@admin.register(Unit)
|
||||
@@ -273,3 +275,67 @@ class ReminderAdmin(admin.ModelAdmin):
|
||||
)
|
||||
|
||||
ordering = ("-created_at",)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@admin.register(InvoicePayment)
|
||||
class InvoicePaymentAdmin(admin.ModelAdmin):
|
||||
|
||||
list_display = (
|
||||
"invoice",
|
||||
"amount",
|
||||
"date",
|
||||
"payment_method",
|
||||
"created_by",
|
||||
"created_at",
|
||||
)
|
||||
|
||||
list_filter = (
|
||||
"payment_method",
|
||||
"date",
|
||||
"created_at",
|
||||
)
|
||||
|
||||
search_fields = (
|
||||
"invoice__invoice_number",
|
||||
"invoice__customer__company_name",
|
||||
"note",
|
||||
)
|
||||
|
||||
readonly_fields = (
|
||||
"created_at",
|
||||
"created_by",
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@admin.register(InvoicePaymentAllocation)
|
||||
class InvoicePaymentAllocationAdmin(admin.ModelAdmin):
|
||||
|
||||
list_display = (
|
||||
"payment",
|
||||
"amount",
|
||||
"reminder",
|
||||
)
|
||||
+20
-2
@@ -1,5 +1,23 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class InvoicesConfig(AppConfig):
|
||||
name = 'invoices'
|
||||
name = "invoices"
|
||||
|
||||
def ready(self):
|
||||
from django_apscheduler.jobstores import DjangoJobStore
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from . import scheduler
|
||||
|
||||
sched = BackgroundScheduler()
|
||||
sched.add_jobstore(DjangoJobStore(), "default")
|
||||
|
||||
sched.add_job(
|
||||
scheduler.mark_overdue,
|
||||
trigger="cron",
|
||||
hour=1,
|
||||
minute=0,
|
||||
id="mark_overdue",
|
||||
replace_existing=True,
|
||||
)
|
||||
|
||||
sched.start()
|
||||
+33
-1
@@ -1,12 +1,41 @@
|
||||
from django import forms
|
||||
from core.forms.base import BaseForm
|
||||
from .models import Customer, Item, Invoice, InvoiceItem, CustomerContact, CustomerLocation, Offer, OfferItem, Reminder
|
||||
from .models import Customer, Item, Invoice, InvoiceItem, CustomerContact, CustomerLocation, Offer, OfferItem, Reminder, InvoicePayment
|
||||
from projects.models import Project
|
||||
from time_tracking.models import TimeEntry
|
||||
from finance.models import Expense
|
||||
from django.utils import timezone
|
||||
from django.db.models import Q, Max
|
||||
|
||||
|
||||
|
||||
class InvoicePaymentForm(BaseForm):
|
||||
class Meta:
|
||||
model = InvoicePayment
|
||||
fields = [
|
||||
"amount",
|
||||
"date",
|
||||
"payment_method",
|
||||
"note",
|
||||
]
|
||||
|
||||
widgets = {
|
||||
"date": forms.DateInput(
|
||||
attrs={
|
||||
"type": "date"
|
||||
}
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["date"].initial = timezone.now().date()
|
||||
self.apply_style()
|
||||
|
||||
|
||||
|
||||
|
||||
class CustomerForm(BaseForm):
|
||||
class Meta:
|
||||
model = Customer
|
||||
@@ -32,9 +61,12 @@ class ItemForm(BaseForm):
|
||||
fields = [
|
||||
"item_type",
|
||||
"description",
|
||||
"category",
|
||||
"unit",
|
||||
"price_net",
|
||||
"tax_rate",
|
||||
"price_buy",
|
||||
"supplier",
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# Generated by Django 6.0.6 on 2026-06-27 17:19
|
||||
# Generated by Django 6.0.6 on 2026-06-28 10:19
|
||||
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
@@ -40,10 +41,10 @@ class Migration(migrations.Migration):
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('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')),
|
||||
('quantity', models.DecimalField(decimal_places=2, default=1, max_digits=10, validators=[django.core.validators.MinValueValidator(0, message='Die Anzahl darf nicht negativ sein.')], verbose_name='Menge')),
|
||||
('unit', models.CharField(max_length=50, verbose_name='Einheit')),
|
||||
('unit_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 (%)')),
|
||||
('unit_price_net', models.DecimalField(decimal_places=2, max_digits=10, validators=[django.core.validators.MinValueValidator(0, message='Der Betrag darf nicht negativ sein.')], verbose_name='Einzelpreis Netto')),
|
||||
('unit_tax_rate', models.DecimalField(decimal_places=2, default=19.0, max_digits=5, validators=[django.core.validators.MinValueValidator(0, message='Der Satz darf nicht negativ sein.')], verbose_name='MwSt. Satz (%)')),
|
||||
('service_period_from', models.DateField(blank=True, null=True, verbose_name='Leistungszeitraum von')),
|
||||
('service_period_to', models.DateField(blank=True, null=True, verbose_name='Leistungszeitraum bis')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
@@ -53,14 +54,32 @@ class Migration(migrations.Migration):
|
||||
'verbose_name_plural': 'Rechnungspositionen',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='InvoicePayment',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('amount', models.DecimalField(decimal_places=2, max_digits=10, validators=[django.core.validators.MinValueValidator(0, message='Der Betrag darf nicht negativ sein.')], verbose_name='Betrag (€)')),
|
||||
('date', models.DateTimeField(verbose_name='Datum')),
|
||||
('note', models.TextField(blank=True, verbose_name='Notiz')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='InvoicePaymentAllocation',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('amount', models.DecimalField(decimal_places=2, max_digits=10)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
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 (%)')),
|
||||
('price_net', models.DecimalField(decimal_places=2, max_digits=10, validators=[django.core.validators.MinValueValidator(0, message='Der Betrag darf nicht negativ sein.')], verbose_name='Preis Netto')),
|
||||
('tax_rate', models.DecimalField(decimal_places=2, default=19.0, max_digits=5, validators=[django.core.validators.MinValueValidator(0, message='Der Satz darf nicht negativ sein.')], verbose_name='MwSt. Satz (%)')),
|
||||
('price_buy', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, validators=[django.core.validators.MinValueValidator(0, message='Der Betrag darf nicht negativ sein.')], verbose_name='Einkaufspreis')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
options={
|
||||
@@ -95,7 +114,7 @@ class Migration(migrations.Migration):
|
||||
options={
|
||||
'verbose_name': 'Angebot',
|
||||
'verbose_name_plural': 'Angebote',
|
||||
'ordering': ['-issue_date'],
|
||||
'ordering': ['-offer_number'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
@@ -103,10 +122,10 @@ class Migration(migrations.Migration):
|
||||
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')),
|
||||
('quantity', models.DecimalField(decimal_places=2, default=1, max_digits=10, validators=[django.core.validators.MinValueValidator(0, message='Die Anzahl darf nicht negativ sein.')], verbose_name='Menge')),
|
||||
('unit', models.CharField(max_length=50, verbose_name='Einheit')),
|
||||
('unit_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 (%)')),
|
||||
('unit_price_net', models.DecimalField(decimal_places=2, max_digits=10, validators=[django.core.validators.MinValueValidator(0, message='Der Betrag darf nicht negativ sein.')], verbose_name='Einzelpreis Netto')),
|
||||
('unit_tax_rate', models.DecimalField(decimal_places=2, default=19.0, max_digits=5, validators=[django.core.validators.MinValueValidator(0, message='Der Satz darf nicht negativ sein.')], verbose_name='MwSt. Satz (%)')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
options={
|
||||
@@ -123,7 +142,7 @@ class Migration(migrations.Migration):
|
||||
('level', models.PositiveSmallIntegerField(default=1, help_text='1 = Erste Mahnung, 2 = Zweite Mahnung, 3 = Letzte Mahnung', verbose_name='Mahnstufe')),
|
||||
('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')),
|
||||
('fee', models.DecimalField(decimal_places=2, default=0, max_digits=10, validators=[django.core.validators.MinValueValidator(0, message='Der Betrag darf nicht negativ sein.')], verbose_name='Mahngebühr')),
|
||||
('note', models.TextField(blank=True, verbose_name='Hinweis')),
|
||||
('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')),
|
||||
@@ -210,6 +229,7 @@ class Migration(migrations.Migration):
|
||||
('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)),
|
||||
('is_small_business', models.BooleanField(default=False, verbose_name='Kleinunternehmer (§19 UStG)')),
|
||||
('pdf_file', models.FileField(blank=True, null=True, upload_to='invoice_pdfs/', verbose_name='PDF')),
|
||||
('pdf_generated_at', models.DateTimeField(blank=True, null=True, verbose_name='PDF erstellt am')),
|
||||
('cancel_number', models.CharField(blank=True, max_length=50, null=True, unique=True, verbose_name='Stornorechnungsnummer')),
|
||||
@@ -230,7 +250,7 @@ class Migration(migrations.Migration):
|
||||
options={
|
||||
'verbose_name': 'Rechnung',
|
||||
'verbose_name_plural': 'Rechnungen',
|
||||
'ordering': ['-issue_date'],
|
||||
'ordering': ['-invoice_number'],
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Generated by Django 6.0.6 on 2026-06-27 17:19
|
||||
# Generated by Django 6.0.6 on 2026-06-28 10:19
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
@@ -13,6 +14,7 @@ class Migration(migrations.Migration):
|
||||
('invoices', '0001_initial'),
|
||||
('projects', '0001_initial'),
|
||||
('time_tracking', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
@@ -36,6 +38,36 @@ class Migration(migrations.Migration):
|
||||
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='invoicepayment',
|
||||
name='created_by',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='invoicepayment',
|
||||
name='invoice',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payments', to='invoices.invoice'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='invoicepayment',
|
||||
name='payment_method',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='finance.paymentmethod', verbose_name='Zahlungsmethode'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='invoicepaymentallocation',
|
||||
name='payment',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='allocations', to='invoices.invoicepayment'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='item',
|
||||
name='category',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='finance.category', verbose_name='Kategorie'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='item',
|
||||
name='supplier',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='finance.supplier', verbose_name='Lieferant'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='invoiceitem',
|
||||
name='item',
|
||||
@@ -61,6 +93,11 @@ class Migration(migrations.Migration):
|
||||
name='invoice',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='reminders', to='invoices.invoice', verbose_name='Rechnung'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='invoicepaymentallocation',
|
||||
name='reminder',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='payment_allocations', to='invoices.reminder'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='item',
|
||||
name='unit',
|
||||
|
||||
+261
-14
@@ -1,6 +1,12 @@
|
||||
from django.db import models
|
||||
from django.core.exceptions import ValidationError
|
||||
from core.models import CompanySetting
|
||||
from django.db.models import Sum, F, DecimalField, ExpressionWrapper
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.utils import timezone
|
||||
from decimal import Decimal
|
||||
from django.core.validators import MinValueValidator
|
||||
User = get_user_model()
|
||||
|
||||
class Unit(models.Model):
|
||||
name = models.CharField(
|
||||
@@ -92,15 +98,21 @@ class Item(models.Model):
|
||||
|
||||
item_type = models.CharField(max_length=20, choices=ItemType.choices, default=ItemType.PRODUCT, verbose_name="Art")
|
||||
description = models.CharField(max_length=255, verbose_name="Beschreibung")
|
||||
category = models.ForeignKey("finance.Category", on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Kategorie")
|
||||
supplier = models.ForeignKey("finance.Supplier", on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Lieferant")
|
||||
|
||||
unit = models.ForeignKey(Unit,on_delete=models.PROTECT,related_name="item_items",verbose_name="Einheit")
|
||||
|
||||
price_net = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="Preis Netto")
|
||||
price_net = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="Preis Netto",validators=[MinValueValidator(0,message="Der Betrag darf nicht negativ sein.")])
|
||||
tax_rate = models.DecimalField(
|
||||
max_digits=5,
|
||||
decimal_places=2,
|
||||
default=19.00,
|
||||
verbose_name="MwSt. Satz (%)"
|
||||
verbose_name="MwSt. Satz (%)",
|
||||
validators=[MinValueValidator(0,message="Der Satz darf nicht negativ sein.")]
|
||||
)
|
||||
|
||||
price_buy = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="Einkaufspreis", null=True, blank=True,validators=[MinValueValidator(0,message="Der Betrag darf nicht negativ sein.")])
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
@@ -142,6 +154,8 @@ class Invoice(models.Model):
|
||||
note = models.TextField(blank=True, verbose_name="Notiz")
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
is_small_business = models.BooleanField(default=False,verbose_name="Kleinunternehmer (§19 UStG)")
|
||||
|
||||
pdf_file = models.FileField(upload_to="invoice_pdfs/",null=True,blank=True,verbose_name="PDF")
|
||||
pdf_generated_at = models.DateTimeField(null=True,blank=True,verbose_name="PDF erstellt am")
|
||||
|
||||
@@ -163,7 +177,7 @@ class Invoice(models.Model):
|
||||
class Meta:
|
||||
verbose_name = "Rechnung"
|
||||
verbose_name_plural = "Rechnungen"
|
||||
ordering = ["-issue_date"]
|
||||
ordering = ["-invoice_number"]
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
@@ -201,9 +215,106 @@ class Invoice(models.Model):
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
if self.issue_date and self.due_date:
|
||||
if self.issue_date > self.due_date:
|
||||
raise ValidationError(
|
||||
{
|
||||
"due_date": "Das Fälligkeitsdatum muss nach dem Rechnungsdatum liegen."
|
||||
}
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.invoice_number
|
||||
|
||||
@property
|
||||
def total_net(self):
|
||||
result = self.items.aggregate(
|
||||
total=Sum(
|
||||
ExpressionWrapper(
|
||||
F("quantity") * F("unit_price_net"),
|
||||
output_field=DecimalField()
|
||||
)
|
||||
)
|
||||
)
|
||||
return result["total"] or Decimal("0.00")
|
||||
|
||||
@property
|
||||
def total_tax(self):
|
||||
total = Decimal("0.00")
|
||||
|
||||
for item in self.items.all():
|
||||
|
||||
item_net = item.calculated_total_net
|
||||
|
||||
total += (
|
||||
item_net *
|
||||
item.unit_tax_rate /
|
||||
Decimal("100")
|
||||
)
|
||||
|
||||
return total
|
||||
|
||||
@property
|
||||
def total_amount(self):
|
||||
if self.is_small_business:
|
||||
return self.total_net
|
||||
return self.total_net + self.total_tax
|
||||
|
||||
@property
|
||||
def paid_amount(self):
|
||||
return self.payments.aggregate(
|
||||
total=Sum("amount")
|
||||
)["total"] or Decimal("0.00")
|
||||
|
||||
|
||||
@property
|
||||
def paid_reminder_amount(self):
|
||||
return self.reminders.aggregate(
|
||||
total=Sum(
|
||||
"payment_allocations__amount"
|
||||
)
|
||||
)["total"] or Decimal("0.00")
|
||||
|
||||
|
||||
@property
|
||||
def paid_invoice_amount(self):
|
||||
return (
|
||||
self.paid_amount
|
||||
- self.paid_reminder_amount
|
||||
)
|
||||
|
||||
|
||||
@property
|
||||
def reminder_amount(self):
|
||||
return self.reminders.filter(
|
||||
status__in=[
|
||||
Reminder.Status.SENT,
|
||||
Reminder.Status.EXPIRED,
|
||||
Reminder.Status.PAID,
|
||||
]
|
||||
).aggregate(
|
||||
total=Sum("fee")
|
||||
)["total"] or Decimal("0.00")
|
||||
|
||||
|
||||
@property
|
||||
def total_with_reminders(self):
|
||||
return (
|
||||
self.total_amount
|
||||
+ self.reminder_amount
|
||||
)
|
||||
|
||||
|
||||
@property
|
||||
def open_amount(self):
|
||||
return (
|
||||
self.total_with_reminders
|
||||
- self.paid_amount
|
||||
)
|
||||
|
||||
|
||||
class InvoiceItem(models.Model):
|
||||
invoice = models.ForeignKey(Invoice,on_delete=models.CASCADE,related_name="items",verbose_name="Rechnung")
|
||||
@@ -212,10 +323,10 @@ class InvoiceItem(models.Model):
|
||||
time_entry = models.ForeignKey("time_tracking.TimeEntry",on_delete=models.SET_NULL,null=True,blank=True,verbose_name="Zeiterfassung") # Quelle 2
|
||||
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")
|
||||
quantity = models.DecimalField(max_digits=10,decimal_places=2,default=1,verbose_name="Menge",validators=[MinValueValidator(0,message="Die Anzahl darf nicht negativ sein.")])
|
||||
unit = models.CharField(max_length=50,verbose_name="Einheit")
|
||||
unit_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 (%)")
|
||||
unit_price_net = models.DecimalField(max_digits=10,decimal_places=2,verbose_name="Einzelpreis Netto",validators=[MinValueValidator(0,message="Der Betrag darf nicht negativ sein.")])
|
||||
unit_tax_rate = models.DecimalField(max_digits=5,decimal_places=2,default=19.00,verbose_name="MwSt. Satz (%)",validators=[MinValueValidator(0,message="Der Satz darf nicht negativ sein.")])
|
||||
service_period_from = models.DateField(null=True,blank=True,verbose_name="Leistungszeitraum von",)
|
||||
service_period_to = models.DateField(null=True,blank=True,verbose_name="Leistungszeitraum bis",)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
@@ -229,7 +340,6 @@ class InvoiceItem(models.Model):
|
||||
verbose_name_plural = "Rechnungspositionen"
|
||||
|
||||
def clean(self):
|
||||
|
||||
sources = [
|
||||
self.item,
|
||||
self.time_entry,
|
||||
@@ -239,9 +349,13 @@ class InvoiceItem(models.Model):
|
||||
selected = sum(1 for s in sources if s)
|
||||
|
||||
if selected != 1:
|
||||
raise ValidationError(
|
||||
"Genau eine Quelle muss ausgewählt werden."
|
||||
)
|
||||
raise ValidationError("Genau eine Quelle muss ausgewählt werden.")
|
||||
|
||||
if self.service_period_from and self.service_period_to:
|
||||
if self.service_period_from >= self.service_period_to:
|
||||
raise ValidationError({
|
||||
'service_period_to': 'Das Ende des Leistungszeitraums muss nach dem Beginn liegen.'
|
||||
})
|
||||
|
||||
def __str__(self):
|
||||
return self.description
|
||||
@@ -272,7 +386,6 @@ class Reminder(models.Model):
|
||||
DRAFT = "draft", "Entwurf"
|
||||
SENT = "sent", "Versendet"
|
||||
PAID = "paid", "Bezahlt"
|
||||
#PARTIALLY_PAID = "partial", "Teilweise bezahlt"
|
||||
CANCELED = "canceled", "Storniert"
|
||||
EXPIRED = "expired", "Frist abgelaufen"
|
||||
|
||||
@@ -288,7 +401,7 @@ class Reminder(models.Model):
|
||||
level = models.PositiveSmallIntegerField(default=1,verbose_name="Mahnstufe",help_text="1 = Erste Mahnung, 2 = Zweite Mahnung, 3 = Letzte Mahnung",)
|
||||
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",)
|
||||
fee = models.DecimalField(max_digits=10,decimal_places=2,default=0,verbose_name="Mahngebühr",validators=[MinValueValidator(0,message="Der Betrag darf nicht negativ sein.")])
|
||||
note = models.TextField(blank=True,verbose_name="Hinweis",)
|
||||
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",)
|
||||
@@ -302,6 +415,27 @@ class Reminder(models.Model):
|
||||
def __str__(self):
|
||||
return self.reminder_number
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
if self.issue_date and self.due_date:
|
||||
if self.issue_date > self.due_date:
|
||||
raise ValidationError(
|
||||
{
|
||||
"due_date": "Das Fälligkeitsdatum muss nach dem Mahnungsdatum liegen."
|
||||
}
|
||||
)
|
||||
|
||||
@property
|
||||
def paid_amount(self):
|
||||
return self.payment_allocations.aggregate(
|
||||
total=Sum("amount")
|
||||
)["total"] or Decimal("0.00")
|
||||
|
||||
|
||||
@property
|
||||
def is_paid(self):
|
||||
return self.paid_amount >= self.fee
|
||||
|
||||
|
||||
|
||||
@@ -451,10 +585,9 @@ class Offer(models.Model):
|
||||
class Meta:
|
||||
verbose_name = "Angebot"
|
||||
verbose_name_plural = "Angebote"
|
||||
ordering = ["-issue_date"]
|
||||
ordering = ["-offer_number"]
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
if self.pk:
|
||||
|
||||
old = Offer.objects.get(pk=self.pk)
|
||||
@@ -485,6 +618,17 @@ class Offer(models.Model):
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
if self.issue_date and self.due_date:
|
||||
if self.issue_date > self.due_date:
|
||||
raise ValidationError(
|
||||
{
|
||||
"due_date": "Das Gültigkeitsdatum muss nach dem Angebotsdatum liegen."
|
||||
}
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.offer_number
|
||||
|
||||
@@ -515,6 +659,7 @@ class OfferItem(models.Model):
|
||||
decimal_places=2,
|
||||
default=1,
|
||||
verbose_name="Menge",
|
||||
validators=[MinValueValidator(0,message="Die Anzahl darf nicht negativ sein.")]
|
||||
)
|
||||
|
||||
unit = models.CharField(
|
||||
@@ -526,6 +671,7 @@ class OfferItem(models.Model):
|
||||
max_digits=10,
|
||||
decimal_places=2,
|
||||
verbose_name="Einzelpreis Netto",
|
||||
validators=[MinValueValidator(0,message="Der Betrag darf nicht negativ sein.")]
|
||||
)
|
||||
|
||||
unit_tax_rate = models.DecimalField(
|
||||
@@ -533,6 +679,7 @@ class OfferItem(models.Model):
|
||||
decimal_places=2,
|
||||
default=19.00,
|
||||
verbose_name="MwSt. Satz (%)",
|
||||
validators=[MinValueValidator(0,message="Der Satz darf nicht negativ sein.")]
|
||||
)
|
||||
|
||||
created_at = models.DateTimeField(
|
||||
@@ -593,3 +740,103 @@ class DocumentNumberSettings(models.Model):
|
||||
class Meta:
|
||||
verbose_name = "Dokumentnummer"
|
||||
verbose_name_plural = "Dokumentnummern"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class InvoicePayment(models.Model):
|
||||
invoice = models.ForeignKey(
|
||||
Invoice,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="payments"
|
||||
)
|
||||
|
||||
amount = models.DecimalField(
|
||||
max_digits=10,
|
||||
decimal_places=2,
|
||||
verbose_name="Betrag (€)",
|
||||
validators=[MinValueValidator(0,message="Der Betrag darf nicht negativ sein.")]
|
||||
)
|
||||
|
||||
date = models.DateTimeField(verbose_name="Datum")
|
||||
|
||||
payment_method = models.ForeignKey(
|
||||
"finance.PaymentMethod",
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="Zahlungsmethode",
|
||||
)
|
||||
|
||||
note = models.TextField(blank=True, verbose_name="Notiz")
|
||||
|
||||
created_by = models.ForeignKey(
|
||||
User,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
)
|
||||
|
||||
created_at = models.DateTimeField(
|
||||
auto_now_add=True
|
||||
)
|
||||
def clean(self):
|
||||
if self.date and self.invoice_id:
|
||||
if self.date.date() < self.invoice.issue_date:
|
||||
raise ValidationError({
|
||||
'date': 'Das Zahlungsdatum darf nicht vor dem Rechnungsdatum liegen.'
|
||||
})
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.full_clean()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def allocated_amount(self):
|
||||
return self.allocations.aggregate(
|
||||
total=Sum("amount")
|
||||
)["total"] or 0
|
||||
|
||||
@property
|
||||
def remaining_amount(self):
|
||||
return self.amount - self.allocated_amount
|
||||
|
||||
|
||||
class InvoicePaymentAllocation(models.Model):
|
||||
|
||||
payment = models.ForeignKey(
|
||||
InvoicePayment,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="allocations"
|
||||
)
|
||||
|
||||
reminder = models.ForeignKey(
|
||||
Reminder,
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="payment_allocations"
|
||||
)
|
||||
|
||||
amount = models.DecimalField(
|
||||
max_digits=10,
|
||||
decimal_places=2
|
||||
)
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
from django.utils import timezone
|
||||
from .models import Invoice, Offer, Reminder
|
||||
|
||||
def mark_overdue():
|
||||
today = timezone.now().date()
|
||||
|
||||
Invoice.objects.filter(
|
||||
status="sent",
|
||||
due_date__lt=today,
|
||||
).update(status="overdue")
|
||||
|
||||
Offer.objects.filter(
|
||||
status="sent",
|
||||
due_date__lt=today,
|
||||
).update(status="expired")
|
||||
|
||||
Reminder.objects.filter(
|
||||
status="sent",
|
||||
due_date__lt=today,
|
||||
).update(status="expired")
|
||||
+47
-38
@@ -1,8 +1,42 @@
|
||||
import django_tables2 as tables
|
||||
from .models import Item, Customer, Reminder, Invoice, InvoiceItem, Offer, OfferItem
|
||||
from .models import Item, Customer, Reminder, Invoice, InvoiceItem, Offer, OfferItem, InvoicePayment
|
||||
|
||||
|
||||
|
||||
class InvoicePaymentTable(tables.Table):
|
||||
invoice = tables.Column(
|
||||
verbose_name="Rechnung"
|
||||
)
|
||||
amount = tables.Column(
|
||||
verbose_name="Betrag"
|
||||
)
|
||||
date = tables.DateColumn(
|
||||
format="d.m.Y",
|
||||
verbose_name="Datum"
|
||||
)
|
||||
payment_method = tables.Column(
|
||||
verbose_name="Zahlungsmethode"
|
||||
)
|
||||
created_by = tables.Column(
|
||||
verbose_name="Erfasst von"
|
||||
)
|
||||
class Meta:
|
||||
model = InvoicePayment
|
||||
fields = [
|
||||
"invoice",
|
||||
"amount",
|
||||
"date",
|
||||
"payment_method",
|
||||
"created_by",
|
||||
]
|
||||
attrs = {
|
||||
"class": "w-full text-sm"
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
class OfferItemTable(tables.Table):
|
||||
|
||||
unit_price_net = tables.TemplateColumn(
|
||||
template_name="invoices/tables/offer_items/unit_price.html",
|
||||
verbose_name="Einzelpreis",
|
||||
@@ -246,55 +280,30 @@ class ReminderTable(tables.Table):
|
||||
}
|
||||
|
||||
class ItemTable(tables.Table):
|
||||
item_type = tables.TemplateColumn(
|
||||
template_name="invoices/tables/items/item_type.html",
|
||||
verbose_name="Typ",
|
||||
orderable=False
|
||||
)
|
||||
toggle_prefix = "item"
|
||||
|
||||
description = tables.TemplateColumn(
|
||||
template_name="invoices/tables/items/description.html",
|
||||
verbose_name="Beschreibung",
|
||||
orderable=False
|
||||
detail_template = (
|
||||
"invoices/tables/items/detail.html"
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
template_name = (
|
||||
"django_tables2/table_toggle.html"
|
||||
)
|
||||
|
||||
fields = (
|
||||
"item_type",
|
||||
"description",
|
||||
"category",
|
||||
"unit",
|
||||
"price_net",
|
||||
"tax_rate",
|
||||
"actions",
|
||||
)
|
||||
|
||||
attrs = {
|
||||
"class": "w-full text-sm min-w-[700px]"
|
||||
"class": "w-full text-sm min-w-[900px]",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ Kunde bearbeiten
|
||||
|
||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||
|
||||
<h2 class="text-xl font-bold mb-2">Kundendaten</h2>
|
||||
<h2 class="text-lg font-bold mb-2">Kundendaten</h2>
|
||||
|
||||
<div class="grid md:grid-cols-2 gap-4 mb-8">
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ Kundenstammdaten verwalten
|
||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
|
||||
<div>
|
||||
<h2 class="text-xl font-bold">Aktionen</h2>
|
||||
<h2 class="text-lg font-bold">Aktionen</h2>
|
||||
<p class="text-slate-400 text-sm mt-1">
|
||||
Kunden anlegen
|
||||
</p>
|
||||
@@ -36,7 +36,7 @@ Kundenstammdaten verwalten
|
||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors mb-8">
|
||||
|
||||
<div class="mb-6">
|
||||
<h2 class="text-xl font-bold">Kundenliste</h2>
|
||||
<h2 class="text-lg font-bold">Kundenliste</h2>
|
||||
<p class="text-slate-400 text-sm mt-1">Alle Kunden, inkl. Standorte und Ansprechpartner</p>
|
||||
</div>
|
||||
|
||||
@@ -49,7 +49,7 @@ Kundenstammdaten verwalten
|
||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||
|
||||
<div class="mb-6">
|
||||
<h2 class="text-xl font-bold">Archivierte Kunden</h2>
|
||||
<h2 class="text-lg font-bold">Archivierte Kunden</h2>
|
||||
<p class="text-slate-400 text-sm mt-1">Alle inaktiven Kunden, inkl. Standorte und Ansprechpartner</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Rechnungswesen - Rechnungen{% endblock %}
|
||||
|
||||
{% block header_title %}
|
||||
Zahlung erfassen
|
||||
{% endblock %}
|
||||
|
||||
{% block header_description %}
|
||||
Zahlung für Rechnung {{ invoice.invoice_number }} hinzufügen
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<!-- PAYMENT FORM -->
|
||||
<div class="lg:col-span-2">
|
||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||
|
||||
|
||||
|
||||
<form method="post" enctype="multipart/form-data" class="space-y-4">
|
||||
{% csrf_token %}
|
||||
|
||||
{{ form.as_p }}
|
||||
|
||||
<button class="btn btn-save">
|
||||
Speichern
|
||||
</button>
|
||||
</form>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- PAYMENT SUMMARY -->
|
||||
<div class="lg:col-span-1">
|
||||
|
||||
<div class="bg-white dark:bg-slate-800 rounded-xl shadow p-6 font-mono text-sm">
|
||||
|
||||
<!-- HEADER -->
|
||||
<div class="text-center">
|
||||
|
||||
<h2 class="text-lg font-bold">
|
||||
Zahlungsübersicht
|
||||
</h2>
|
||||
|
||||
<p class="text-xs text-slate-400 mt-1">
|
||||
Rechnung {{ invoice.invoice_number }}
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class="border-t border-dashed border-slate-300 dark:border-slate-600 my-5"></div>
|
||||
|
||||
|
||||
<!-- RECHNUNG -->
|
||||
<div class="space-y-2">
|
||||
|
||||
|
||||
{% if not invoice.is_small_business %}
|
||||
|
||||
<div class="flex justify-between">
|
||||
<span class="text-slate-400">
|
||||
Netto
|
||||
</span>
|
||||
|
||||
<span>
|
||||
{{ invoice.total_net|floatformat:2 }} €
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex justify-between">
|
||||
<span class="text-slate-400">
|
||||
Umsatzsteuer
|
||||
</span>
|
||||
|
||||
<span>
|
||||
{{ invoice.total_tax|floatformat:2 }} €
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
{% endif %}
|
||||
|
||||
|
||||
<div class="flex justify-between font-semibold">
|
||||
<span>
|
||||
Rechnungsbetrag
|
||||
</span>
|
||||
|
||||
<span>
|
||||
{{ invoice.total_amount|floatformat:2 }} €
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- MAHNUNGEN -->
|
||||
{% if invoice.reminder_amount > 0 %}
|
||||
|
||||
<div class="border-t border-dashed border-slate-300 dark:border-slate-600 my-5"></div>
|
||||
|
||||
|
||||
<div class="space-y-2">
|
||||
|
||||
<div class="flex justify-between text-red-600">
|
||||
|
||||
<span>
|
||||
Mahngebühren
|
||||
</span>
|
||||
|
||||
<span>
|
||||
+ {{ invoice.reminder_amount|floatformat:2 }} €
|
||||
</span>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
{% for reminder in invoice.reminders.all %}
|
||||
|
||||
{% if reminder.status != "canceled" %}
|
||||
|
||||
<div class="flex justify-between text-xs text-slate-400">
|
||||
|
||||
<span>
|
||||
{{ reminder.level }}. Mahnung
|
||||
</span>
|
||||
|
||||
<span>
|
||||
{{ reminder.fee|floatformat:2 }} €
|
||||
</span>
|
||||
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% endfor %}
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
<!-- GESAMT -->
|
||||
<div class="border-t border-dashed border-slate-300 dark:border-slate-600 my-5"></div>
|
||||
|
||||
|
||||
<div class="flex justify-between text-lg font-bold">
|
||||
|
||||
<span>
|
||||
Gesamtforderung
|
||||
</span>
|
||||
|
||||
<span>
|
||||
{{ invoice.total_with_reminders|floatformat:2 }} €
|
||||
</span>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- ZAHLUNGEN -->
|
||||
<div class="mt-4 space-y-2">
|
||||
|
||||
|
||||
<div class="flex justify-between text-green-600">
|
||||
|
||||
<span>
|
||||
Bereits bezahlt
|
||||
</span>
|
||||
|
||||
<span>
|
||||
- {{ invoice.paid_amount|floatformat:2 }} €
|
||||
</span>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{% for payment in invoice.payments.all %}
|
||||
|
||||
<div class="flex justify-between text-xs text-slate-400">
|
||||
|
||||
<span>
|
||||
Zahlung {{ payment.date|date:"d.m.Y" }}
|
||||
</span>
|
||||
|
||||
<span>
|
||||
{{ payment.amount|floatformat:2 }} €
|
||||
</span>
|
||||
|
||||
</div>
|
||||
|
||||
{% endfor %}
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- OFFEN -->
|
||||
<div class="border-t border-dashed border-slate-300 dark:border-slate-600 my-5"></div>
|
||||
|
||||
|
||||
<div class="bg-slate-100 dark:bg-slate-700 rounded-xl p-4 text-center">
|
||||
|
||||
<p class="text-xs uppercase text-slate-400">
|
||||
Offener Betrag
|
||||
</p>
|
||||
|
||||
|
||||
<p class="text-3xl font-bold text-red-600 mt-1">
|
||||
|
||||
{{ invoice.open_amount|floatformat:2 }} €
|
||||
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- FOOTER -->
|
||||
<div class="border-t border-dashed border-slate-300 dark:border-slate-600 my-5"></div>
|
||||
|
||||
|
||||
<div class="text-center text-xs text-slate-400">
|
||||
|
||||
Zahlungserfassung
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -16,7 +16,7 @@ Details & Positionen
|
||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
|
||||
<div>
|
||||
<h2 class="text-xl font-bold">Aktionen</h2>
|
||||
<h2 class="text-lg font-bold">Aktionen</h2>
|
||||
<p class="text-slate-400 text-sm mt-1">
|
||||
Positionen zur Rechnung hinzufügen
|
||||
</p>
|
||||
@@ -32,7 +32,6 @@ Details & Positionen
|
||||
<a href="{% url 'invoices:invoices_download' invoice.id %}" class="btn btn-action">Download</a>
|
||||
<a href="{% url 'invoices:invoices_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>
|
||||
@@ -44,6 +43,8 @@ Details & Positionen
|
||||
{% endif %}
|
||||
{% if invoice.status == "overdue" %}
|
||||
<a href="{% url 'invoices:invoices_download' invoice.id %}" class="btn btn-action">Download</a>
|
||||
<a href="{% url 'invoices:invoices_cancel' invoice.id %}" onclick="startDownloadAndRedirect()" class="btn btn-delete">Stornieren</a>
|
||||
<a href="{% url 'invoices:invoices_paid' invoice.id %}" class="btn btn-action">Zahlung verbuchen</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
|
||||
<div>
|
||||
<h2 class="text-xl font-bold">Aktionen</h2>
|
||||
<h2 class="text-lg font-bold">Aktionen</h2>
|
||||
<p class="text-slate-400 text-sm mt-1">Neue Rechnung erstellen</p>
|
||||
</div>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
</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>
|
||||
<h2 class="text-lg 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">
|
||||
|
||||
@@ -158,7 +158,7 @@ table {
|
||||
|
||||
margin-top:30px;
|
||||
margin-left:auto;
|
||||
width:250px;
|
||||
width: 300px;
|
||||
|
||||
}
|
||||
|
||||
@@ -178,7 +178,7 @@ table {
|
||||
border-top:1px solid #cbd5e1;
|
||||
margin-top:10px;
|
||||
padding-top:10px;
|
||||
font-size:16px;
|
||||
font-size:14px;
|
||||
font-weight:bold;
|
||||
|
||||
}
|
||||
@@ -454,7 +454,7 @@ Preis
|
||||
</th>
|
||||
|
||||
|
||||
{% if not company.is_small_business %}
|
||||
{% if not invoice.is_small_business %}
|
||||
|
||||
<th class="left">
|
||||
Steuer
|
||||
@@ -518,7 +518,7 @@ Gesamt
|
||||
</td>
|
||||
|
||||
|
||||
{% if not company.is_small_business %}
|
||||
{% if not invoice.is_small_business %}
|
||||
|
||||
<td class="left">
|
||||
{{ item.unit_tax_rate|floatformat:0 }} %
|
||||
@@ -569,7 +569,7 @@ Netto
|
||||
|
||||
|
||||
|
||||
{% if not company.is_small_business %}
|
||||
{% if not invoice.is_small_business %}
|
||||
{% for tax_rate,tax_amount in tax_summary.items %}
|
||||
|
||||
<div class="total-row">
|
||||
@@ -603,7 +603,7 @@ Gesamt
|
||||
|
||||
<span>
|
||||
|
||||
{% if company.is_small_business %}
|
||||
{% if invoice.is_small_business %}
|
||||
{{ total_net|floatformat:2 }}
|
||||
{% else %}
|
||||
{{ total_gross|floatformat:2 }}
|
||||
@@ -644,7 +644,7 @@ Rechnungshinweis
|
||||
|
||||
|
||||
|
||||
{% if company.is_small_business %}
|
||||
{% if invoice.is_small_business %}
|
||||
|
||||
<div class="note">
|
||||
|
||||
|
||||
@@ -155,10 +155,9 @@ table {
|
||||
|
||||
|
||||
.total-box {
|
||||
|
||||
margin-top:30px;
|
||||
margin-left:auto;
|
||||
width:250px;
|
||||
width:300px;
|
||||
|
||||
}
|
||||
|
||||
@@ -174,11 +173,10 @@ table {
|
||||
|
||||
|
||||
.total-final {
|
||||
|
||||
border-top:1px solid #cbd5e1;
|
||||
margin-top:10px;
|
||||
padding-top:10px;
|
||||
font-size:16px;
|
||||
font-size:14px;
|
||||
font-weight:bold;
|
||||
|
||||
}
|
||||
@@ -414,12 +412,11 @@ Stornodatum: {{ cancel_date|date:"d.m.Y" }}
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- POSITIONEN -->
|
||||
|
||||
<div class="items-wrapper">
|
||||
<table class="items">
|
||||
|
||||
<table class="items">
|
||||
|
||||
<thead>
|
||||
|
||||
@@ -429,37 +426,46 @@ Stornodatum: {{ cancel_date|date:"d.m.Y" }}
|
||||
Nr.
|
||||
</th>
|
||||
|
||||
|
||||
<th>
|
||||
Beschreibung
|
||||
</th>
|
||||
|
||||
{% if show_service_date %}
|
||||
<th class="left">Leistungsdatum</th>
|
||||
{% endif %}
|
||||
|
||||
<th class="left">
|
||||
{% if show_service_date %}
|
||||
|
||||
<th>
|
||||
Leistungsdatum
|
||||
</th>
|
||||
|
||||
{% endif %}
|
||||
|
||||
|
||||
<th class="right">
|
||||
Menge
|
||||
</th>
|
||||
|
||||
<th class="left">
|
||||
|
||||
<th>
|
||||
Einheit
|
||||
</th>
|
||||
|
||||
<th class="left">
|
||||
Preis
|
||||
|
||||
<th class="right">
|
||||
Einzelpreis
|
||||
</th>
|
||||
|
||||
|
||||
{% if not company.is_small_business %}
|
||||
{% if not invoice.is_small_business %}
|
||||
|
||||
<th class="left">
|
||||
<th class="right">
|
||||
Steuer
|
||||
</th>
|
||||
|
||||
{% endif %}
|
||||
|
||||
|
||||
<th class="left">
|
||||
<th class="right">
|
||||
Gesamt
|
||||
</th>
|
||||
|
||||
@@ -469,7 +475,6 @@ Gesamt
|
||||
</thead>
|
||||
|
||||
|
||||
|
||||
<tbody>
|
||||
|
||||
|
||||
@@ -483,40 +488,52 @@ Gesamt
|
||||
</td>
|
||||
|
||||
|
||||
|
||||
<td>
|
||||
{{ item.description }}
|
||||
Storno: {{ 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">
|
||||
{% if show_service_date %}
|
||||
|
||||
<td>
|
||||
|
||||
{% if item.service_period_from %}
|
||||
{{ item.service_period_from|date:"d.m.Y" }}
|
||||
{% endif %}
|
||||
|
||||
{% if item.service_period_to %}
|
||||
- {{ item.service_period_to|date:"d.m.Y" }}
|
||||
{% endif %}
|
||||
|
||||
</td>
|
||||
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
<td class="right">
|
||||
{{ item.quantity }}
|
||||
</td>
|
||||
|
||||
|
||||
<td class="left">
|
||||
|
||||
<td>
|
||||
{{ item.unit }}
|
||||
</td>
|
||||
|
||||
|
||||
<td class="left">
|
||||
|
||||
<td class="right">
|
||||
-{{ item.unit_price_net|floatformat:2 }} €
|
||||
</td>
|
||||
|
||||
|
||||
{% if not company.is_small_business %}
|
||||
|
||||
<td class="left">
|
||||
{% if not invoice.is_small_business %}
|
||||
|
||||
<td class="right">
|
||||
{{ item.unit_tax_rate|floatformat:0 }} %
|
||||
</td>
|
||||
|
||||
@@ -524,7 +541,7 @@ Gesamt
|
||||
|
||||
|
||||
|
||||
<td class="left">
|
||||
<td class="right">
|
||||
-{{ item.calculated_total_net|floatformat:2 }} €
|
||||
</td>
|
||||
|
||||
@@ -540,14 +557,28 @@ Gesamt
|
||||
|
||||
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- SUMMEN -->
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- SUMMEN -->
|
||||
|
||||
<div class="total-box">
|
||||
|
||||
|
||||
@@ -565,7 +596,9 @@ Netto
|
||||
|
||||
|
||||
|
||||
{% if not company.is_small_business %}
|
||||
|
||||
{% if not invoice.is_small_business %}
|
||||
|
||||
{% for tax_rate,tax_amount in tax_summary.items %}
|
||||
|
||||
<div class="total-row">
|
||||
@@ -574,32 +607,27 @@ Netto
|
||||
Umsatzsteuer {{ tax_rate|floatformat:0 }} %
|
||||
</span>
|
||||
|
||||
|
||||
<span>
|
||||
-{{ tax_amount|floatformat:2 }} €
|
||||
</span>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
{% endfor %}
|
||||
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="total-row total-final">
|
||||
|
||||
<span>
|
||||
Gesamt
|
||||
Rechnungsbetrag
|
||||
</span>
|
||||
|
||||
|
||||
<span>
|
||||
|
||||
{% if company.is_small_business %}
|
||||
{% if invoice.is_small_business %}
|
||||
-{{ total_net|floatformat:2 }}
|
||||
{% else %}
|
||||
-{{ total_gross|floatformat:2 }}
|
||||
@@ -609,12 +637,41 @@ Gesamt
|
||||
|
||||
</span>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="total-row">
|
||||
|
||||
<span>
|
||||
Bereits bezahlt
|
||||
</span>
|
||||
|
||||
<span>
|
||||
-{{ paid_amount|floatformat:2 }} €
|
||||
</span>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="total-row total-final">
|
||||
|
||||
<span>
|
||||
Erstattungsbetrag
|
||||
</span>
|
||||
|
||||
<span>
|
||||
{{ refund_amount|floatformat:2 }} €
|
||||
</span>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="note">
|
||||
|
||||
<div class="receiver-title">
|
||||
@@ -623,37 +680,45 @@ Gesamt
|
||||
|
||||
<br>
|
||||
|
||||
Die Rechnung {{ invoice.invoice_number }}
|
||||
wird durch diese Stornorechnung vollständig aufgehoben.
|
||||
|
||||
{% if original_status == "paid" %}
|
||||
Die Rechnung
|
||||
<strong>{{ invoice.invoice_number }}</strong>
|
||||
vom {{ invoice.issue_date|date:"d.m.Y" }}
|
||||
wird durch diese Stornorechnung aufgehoben.
|
||||
|
||||
<br><br>
|
||||
|
||||
Die ursprüngliche Rechnung wurde bereits bezahlt.
|
||||
{% if paid_amount > 0 %}
|
||||
|
||||
Es entsteht ein Guthaben in Höhe von
|
||||
<br><br>
|
||||
|
||||
{% if company.is_small_business %}
|
||||
{{ total_net|floatformat:2 }}
|
||||
{% else %}
|
||||
{{ total_gross|floatformat:2 }}
|
||||
{% endif %}
|
||||
|
||||
€.
|
||||
Die ursprüngliche Rechnung wurde bereits bezahlt.
|
||||
|
||||
|
||||
Der zu erstattende Betrag beträgt:
|
||||
|
||||
|
||||
<strong>
|
||||
{{ refund_amount|floatformat:2 }} €
|
||||
</strong>
|
||||
|
||||
|
||||
<br><br>
|
||||
|
||||
|
||||
Die Rückerstattung erfolgt separat.
|
||||
|
||||
Die Rückerstattung erfolgt separat.
|
||||
|
||||
{% endif %}
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
{% if company.is_small_business %}
|
||||
{% if invoice.is_small_business %}
|
||||
|
||||
<div class="note">
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ Leistungen und Artikel verwalten
|
||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
|
||||
<div>
|
||||
<h2 class="text-xl font-bold">Aktionen</h2>
|
||||
<h2 class="text-lg font-bold">Aktionen</h2>
|
||||
<p class="text-slate-400 text-sm mt-1">
|
||||
Leistungen und Artikel anlegen
|
||||
</p>
|
||||
@@ -38,7 +38,7 @@ Leistungen und Artikel verwalten
|
||||
<div class="flex flex-wrap justify-between items-center mb-6">
|
||||
|
||||
<div>
|
||||
<h2 class="text-xl font-bold">
|
||||
<h2 class="text-lg font-bold">
|
||||
Artikel und Leistungen
|
||||
</h2>
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ Details & Positionen
|
||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
|
||||
<div>
|
||||
<h2 class="text-xl font-bold">Aktionen</h2>
|
||||
<h2 class="text-lg font-bold">Aktionen</h2>
|
||||
<p class="text-slate-400 text-sm mt-1">
|
||||
Positionen zum Angebot hinzufügen
|
||||
</p>
|
||||
@@ -32,7 +32,6 @@ Details & Positionen
|
||||
<a href="{% url 'invoices:offers_download' offer.id %}" class="btn btn-action">Download</a>
|
||||
<a href="{% url 'invoices:offers_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>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
|
||||
<div>
|
||||
<h2 class="text-xl font-bold">Aktionen</h2>
|
||||
<h2 class="text-lg font-bold">Aktionen</h2>
|
||||
<p class="text-slate-400 text-sm mt-1">Neues Angebot erstellen</p>
|
||||
</div>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
</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>
|
||||
<h2 class="text-lg 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">
|
||||
|
||||
@@ -158,7 +158,7 @@ table {
|
||||
|
||||
margin-top:30px;
|
||||
margin-left:auto;
|
||||
width:250px;
|
||||
width: 300px;
|
||||
|
||||
}
|
||||
|
||||
@@ -178,7 +178,7 @@ table {
|
||||
border-top:1px solid #cbd5e1;
|
||||
margin-top:10px;
|
||||
padding-top:10px;
|
||||
font-size:16px;
|
||||
font-size:14px;
|
||||
font-weight:bold;
|
||||
|
||||
}
|
||||
|
||||
@@ -18,44 +18,37 @@ Angebote, Rechnungen, Mahnungen verwalten
|
||||
<!-- 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">
|
||||
<a href="{% url 'invoices:invoices_offers_overview' %}" class="border border-slate-100 dark:border-slate-700 hover:border-[var(--company-color)]/40 hover:shadow-sm transition-all duration-200 p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||
<div class="text-2xl mb-2">📄</div>
|
||||
<h2 class="text-xl font-bold">Angebote</h2>
|
||||
<h2 class="text-lg font-bold">Angebote</h2>
|
||||
<p class="text-slate-400 text-sm mt-1">Angebote erstellen und verwalten</p>
|
||||
</a>
|
||||
<a href="{% url 'invoices:invoices_invoices_overview' %}" class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||
<a href="{% url 'invoices:invoices_invoices_overview' %}" class="border border-slate-100 dark:border-slate-700 hover:border-[var(--company-color)]/40 hover:shadow-sm transition-all duration-200 p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||
<div class="text-2xl mb-2">🧾</div>
|
||||
<h2 class="text-xl font-bold">Rechnungen</h2>
|
||||
<h2 class="text-lg font-bold">Rechnungen</h2>
|
||||
<p class="text-slate-400 text-sm mt-1">Rechnungen erstellen und verwalten</p>
|
||||
</a>
|
||||
<a href="{% url 'invoices:invoices_reminders_overview' %}" class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||
<a href="{% url 'invoices:invoices_reminders_overview' %}" class="border border-slate-100 dark:border-slate-700 hover:border-[var(--company-color)]/40 hover:shadow-sm transition-all duration-200 p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||
<div class="text-2xl mb-2">⚠️</div>
|
||||
<h2 class="text-xl font-bold">Mahnungen</h2>
|
||||
<h2 class="text-lg font-bold">Mahnungen</h2>
|
||||
<p class="text-slate-400 text-sm mt-1">Offene Forderungen verfolgen</p>
|
||||
</a>
|
||||
<a href="{% url 'invoices:customers_overview' %}" class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||
<a href="{% url 'invoices:customers_overview' %}" class="border border-slate-100 dark:border-slate-700 hover:border-[var(--company-color)]/40 hover:shadow-sm transition-all duration-200 p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||
<div class="text-2xl mb-2">👥</div>
|
||||
<h2 class="text-xl font-bold">Kunden</h2>
|
||||
<h2 class="text-lg font-bold">Kunden</h2>
|
||||
<p class="text-slate-400 text-sm mt-1">Kundenstammdaten verwalten</p>
|
||||
</a>
|
||||
<a href="{% url 'invoices:items_overview' %}" class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||
<a href="{% url 'invoices:items_overview' %}" class="border border-slate-100 dark:border-slate-700 hover:border-[var(--company-color)]/40 hover:shadow-sm transition-all duration-200 p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||
<div class="text-2xl mb-2">📦</div>
|
||||
<h2 class="text-xl font-bold">Posten</h2>
|
||||
<h2 class="text-lg font-bold">Posten</h2>
|
||||
<p class="text-slate-400 text-sm mt-1">Leistungen und Artikel verwalten</p>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- OPTIONAL INFO BOX -->
|
||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors mt-8">
|
||||
|
||||
<h2 class="text-xl font-bold">
|
||||
Zusammenfassung
|
||||
</h2>
|
||||
|
||||
<p class="text-slate-400 text-sm mt-1">
|
||||
folgt
|
||||
</p>
|
||||
|
||||
<a href="{% url 'invoices:invoices_payments_overview' %}" class="border border-slate-100 dark:border-slate-700 hover:border-[var(--company-color)]/40 hover:shadow-sm transition-all duration-200 p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||
<div class="text-2xl mb-2">🏦</div>
|
||||
<h2 class="text-lg font-bold">Zahlungen</h2>
|
||||
<p class="text-slate-400 text-sm mt-1">Zahlungsvorgänge verfolgen</p>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,20 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Rechnungswesen - Zahlungen{% endblock %}
|
||||
|
||||
{% block header_title %}Zahlungen{% endblock %}
|
||||
{% block header_description %}Zahlungsvorgänge verfolgen{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% load django_tables2 %}
|
||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||
<div class="mb-6">
|
||||
<h2 class="text-lg font-bold">Zahlungen</h2>
|
||||
<p class="text-slate-400 text-sm mt-1">Alle Zahlungsvorgänge</p>
|
||||
</div>
|
||||
<div class="overflow-x-auto rounded-xl">
|
||||
{% render_table table %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -15,7 +15,7 @@ Details
|
||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
|
||||
<div>
|
||||
<h2 class="text-xl font-bold">Aktionen</h2>
|
||||
<h2 class="text-lg font-bold">Aktionen</h2>
|
||||
<p class="text-slate-400 text-sm mt-1">
|
||||
Mahnung verwalten
|
||||
</p>
|
||||
@@ -28,8 +28,6 @@ Details
|
||||
{% 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>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
|
||||
<div>
|
||||
<h2 class="text-xl font-bold">Aktionen</h2>
|
||||
<h2 class="text-lg font-bold">Aktionen</h2>
|
||||
<p class="text-slate-400 text-sm mt-1">Neue Mahnung erstellen</p>
|
||||
</div>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
</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>
|
||||
<h2 class="text-lg 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">
|
||||
|
||||
@@ -149,16 +149,15 @@ table {
|
||||
|
||||
|
||||
.right {
|
||||
text-align:right;
|
||||
text-align: right !important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.total-box {
|
||||
|
||||
margin-top:30px;
|
||||
margin-left:auto;
|
||||
width:250px;
|
||||
width:300px;
|
||||
|
||||
}
|
||||
|
||||
@@ -178,7 +177,7 @@ table {
|
||||
border-top:1px solid #cbd5e1;
|
||||
margin-top:10px;
|
||||
padding-top:10px;
|
||||
font-size:16px;
|
||||
font-size:14px;
|
||||
font-weight:bold;
|
||||
|
||||
}
|
||||
@@ -440,7 +439,7 @@ Betrag
|
||||
Rechnung {{ reminder.invoice.invoice_number }}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<td class="right">
|
||||
{{ invoice_amount|floatformat:2 }} €
|
||||
</td>
|
||||
|
||||
@@ -459,7 +458,7 @@ Rechnung {{ reminder.invoice.invoice_number }}
|
||||
({{ old_reminder.issue_date|date:"d.m.Y" }})
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<td class="right">
|
||||
{{ old_reminder.fee|floatformat:2 }} €
|
||||
</td>
|
||||
|
||||
@@ -474,7 +473,7 @@ Rechnung {{ reminder.invoice.invoice_number }}
|
||||
Aktuelle Mahngebühr
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<td class="right">
|
||||
{{ reminder.fee|floatformat:2 }} €
|
||||
</td>
|
||||
|
||||
@@ -484,6 +483,32 @@ Rechnung {{ reminder.invoice.invoice_number }}
|
||||
|
||||
|
||||
|
||||
{% if payments %}
|
||||
|
||||
{% for payment in payments %}
|
||||
|
||||
<tr>
|
||||
|
||||
<td>
|
||||
Zahlung vom {{ payment.date|date:"d.m.Y" }}
|
||||
{% if payment.payment_method %}
|
||||
({{ payment.payment_method }})
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<td class="right">
|
||||
- {{ payment.amount|floatformat:2 }} €
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
{% endfor %}
|
||||
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
|
||||
</tbody>
|
||||
|
||||
|
||||
@@ -494,12 +519,20 @@ Rechnung {{ reminder.invoice.invoice_number }}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- GESAMTFORDERUNG -->
|
||||
|
||||
|
||||
<div class="total-box">
|
||||
|
||||
|
||||
<div class="total-row">
|
||||
|
||||
<span>
|
||||
@@ -513,7 +546,6 @@ Offener Rechnungsbetrag
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{% if previous_reminders or reminder.fee > 0 %}
|
||||
|
||||
<div class="total-row">
|
||||
@@ -531,23 +563,36 @@ Mahngebühren
|
||||
{% endif %}
|
||||
|
||||
|
||||
<div class="total-row">
|
||||
|
||||
<span>
|
||||
Bereits bezahlt
|
||||
</span>
|
||||
|
||||
<span>
|
||||
- {{ total_paid|floatformat:2 }} €
|
||||
</span>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class="total-row total-final">
|
||||
|
||||
<span>
|
||||
Gesamtforderung
|
||||
Gesamt
|
||||
</span>
|
||||
|
||||
|
||||
<span>
|
||||
{{ total_due|floatformat:2 }} €
|
||||
{{ open_amount|floatformat:2 }} €
|
||||
</span>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
<form
|
||||
id="save-form-{{ record.id }}"
|
||||
method="post"
|
||||
action="{% url 'invoices:items_edit' record.id %}">
|
||||
|
||||
{% csrf_token %}
|
||||
|
||||
</form>
|
||||
|
||||
|
||||
<div class="flex gap-2">
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
form="save-form-{{ record.id }}"
|
||||
class="btn btn-save">
|
||||
Speichern
|
||||
</button>
|
||||
|
||||
|
||||
<form
|
||||
method="post"
|
||||
action="{% url 'invoices:items_delete' record.id %}">
|
||||
|
||||
{% csrf_token %}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-delete">
|
||||
Löschen
|
||||
</button>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
@@ -1,6 +0,0 @@
|
||||
<input
|
||||
type="text"
|
||||
name="description"
|
||||
form="save-form-{{ record.id }}"
|
||||
value="{{ record.description }}"
|
||||
class="border rounded px-2 py-1 w-full">
|
||||
@@ -0,0 +1,176 @@
|
||||
<form method="post" action="{% url 'invoices:items_edit' item.id %}">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="grid md:grid-cols-2 xl:grid-cols-3 gap-4">
|
||||
|
||||
<div>
|
||||
<label class="block text-sm mb-1">
|
||||
Typ
|
||||
</label>
|
||||
|
||||
<select
|
||||
name="item_type"
|
||||
class="border rounded px-3 py-2 w-full">
|
||||
|
||||
{% for value,label in item.ItemType.choices %}
|
||||
<option
|
||||
value="{{ value }}"
|
||||
{% if item.item_type == value %}selected{% endif %}>
|
||||
{{ label }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm mb-1">
|
||||
Beschreibung
|
||||
</label>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
name="description"
|
||||
value="{{ item.description }}"
|
||||
class="border rounded px-3 py-2 w-full">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm mb-1">
|
||||
Kategorie
|
||||
</label>
|
||||
|
||||
<select
|
||||
name="category"
|
||||
class="border rounded px-3 py-2 w-full">
|
||||
|
||||
<option value="">
|
||||
---
|
||||
</option>
|
||||
|
||||
{% for c in table.categories %}
|
||||
<option
|
||||
value="{{ c.id }}"
|
||||
{% if item.category_id == c.id %}selected{% endif %}>
|
||||
{{ c.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm mb-1">
|
||||
Beschreibung
|
||||
</label>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
name="description"
|
||||
value="{{ item.description }}"
|
||||
class="border rounded px-3 py-2 w-full">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm mb-1">
|
||||
Einheit
|
||||
</label>
|
||||
|
||||
<select
|
||||
name="unit"
|
||||
class="border rounded px-3 py-2 w-full">
|
||||
|
||||
<option value="">
|
||||
---
|
||||
</option>
|
||||
|
||||
{% for u in table.units %}
|
||||
<option
|
||||
value="{{ u.id }}"
|
||||
{% if item.unit_id == u.id %}selected{% endif %}>
|
||||
{{ u.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm mb-1">
|
||||
Einzelpreis Netto
|
||||
</label>
|
||||
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
name="price_net"
|
||||
value="{{ item.price_net }}"
|
||||
class="border rounded px-3 py-2 w-full">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm mb-1">
|
||||
MwSt. (%)
|
||||
</label>
|
||||
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
name="tax_rate"
|
||||
value="{{ item.tax_rate }}"
|
||||
class="border rounded px-3 py-2 w-full">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm mb-1">
|
||||
Einkaufspreis
|
||||
</label>
|
||||
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
name="price_buy"
|
||||
value="{{ item.price_buy }}"
|
||||
class="border rounded px-3 py-2 w-full">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm mb-1">
|
||||
Lieferant
|
||||
</label>
|
||||
|
||||
<select
|
||||
name="supplier"
|
||||
class="border rounded px-3 py-2 w-full">
|
||||
|
||||
<option value="">
|
||||
---
|
||||
</option>
|
||||
|
||||
{% for s in table.suppliers %}
|
||||
<option
|
||||
value="{{ s.id }}"
|
||||
{% if item.supplier_id == s.id %}selected{% endif %}>
|
||||
{{ s.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 flex gap-2">
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-save">
|
||||
Speichern
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
formaction="{% url 'invoices:items_delete' item.id %}"
|
||||
class="btn btn-delete">
|
||||
Löschen
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
</form>
|
||||
@@ -1,16 +0,0 @@
|
||||
<select
|
||||
name="item_type"
|
||||
form="save-form-{{ record.id }}"
|
||||
class="border rounded px-2 py-1 w-full">
|
||||
|
||||
{% for v,l in table.types %}
|
||||
|
||||
<option
|
||||
value="{{ v }}"
|
||||
{% if record.item_type == v %}selected{% endif %}>
|
||||
{{ l }}
|
||||
</option>
|
||||
|
||||
{% endfor %}
|
||||
|
||||
</select>
|
||||
@@ -1,7 +0,0 @@
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
name="price_net"
|
||||
form="save-form-{{ record.id }}"
|
||||
value="{{ record.price_net }}"
|
||||
class="border rounded px-2 py-1 w-full">
|
||||
@@ -1,7 +0,0 @@
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
name="tax_rate"
|
||||
form="save-form-{{ record.id }}"
|
||||
value="{{ record.tax_rate }}"
|
||||
class="border rounded px-2 py-1 w-full">
|
||||
@@ -1,20 +0,0 @@
|
||||
<select
|
||||
name="unit"
|
||||
form="save-form-{{ record.id }}"
|
||||
class="border rounded px-2 py-1 w-full">
|
||||
|
||||
|
||||
{% for unit in table.units %}
|
||||
|
||||
<option
|
||||
value="{{ unit.id }}"
|
||||
{% if record.unit_id == unit.id %}selected{% endif %}>
|
||||
|
||||
{{ unit.name }}
|
||||
|
||||
</option>
|
||||
|
||||
{% endfor %}
|
||||
|
||||
|
||||
</select>
|
||||
+9
-5
@@ -1,5 +1,5 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
from . import views, views_info
|
||||
|
||||
app_name = "invoices"
|
||||
|
||||
@@ -37,7 +37,6 @@ urlpatterns = [
|
||||
path("invoices/<int:invoice_id>/download/", views.invoices_download, name="invoices_download"),
|
||||
path("invoices/<int:invoice_id>/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"),
|
||||
|
||||
|
||||
@@ -54,7 +53,6 @@ urlpatterns = [
|
||||
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
|
||||
@@ -64,9 +62,9 @@ urlpatterns = [
|
||||
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"),
|
||||
|
||||
# Zahlungen
|
||||
path("payments/", views.invoices_payments_overview, name="invoices_payments_overview"),
|
||||
|
||||
# AJAX
|
||||
path("ajax/customer/<int:customer_id>/contacts/", views.ajax_contacts, name="ajax_contacts"),
|
||||
@@ -74,4 +72,10 @@ urlpatterns = [
|
||||
path("ajax/project/<int:project_id>/invoice-data/", views.ajax_invoicedata, name="ajax_invoicedata"),
|
||||
path("ajax/invoice/<int:invoice_id>/sources/", views.ajax_invoiceitemsources, name="ajax_invoice_item_sources"),
|
||||
path("ajax/invoice-item-data/", views.ajax_invoice_item_data, name="ajax_invoice_item_data"),
|
||||
|
||||
path("ajax/invoice-info/", views_info.ajax_invoice_info, name="ajax_invoice_info"),
|
||||
path("ajax/offer-info/", views_info.ajax_offer_info, name="ajax_offer_info"),
|
||||
path("ajax/reminder-info/",views_info.ajax_reminder_info,name="ajax_reminder_info"),
|
||||
path("ajax/customer-info/",views_info.ajax_customer_info, name="ajax_customer_info"),
|
||||
path("ajax/kpi-info/", views_info.ajax_kpi_info, name="ajax_kpi_info"),
|
||||
]
|
||||
+305
-158
@@ -1,8 +1,8 @@
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from accounts.decorators import permission_required
|
||||
from django.shortcuts import render, redirect, get_object_or_404
|
||||
from .models import Customer, CustomerContact, CustomerLocation, Item, Invoice, InvoiceItem, Unit, Offer, OfferItem, Reminder
|
||||
from .forms import CustomerForm, ItemForm, CustomerInvoiceForm, ProjectInvoiceForm, InvoiceItemForm, OfferForm, OfferItemForm, ReminderForm
|
||||
from .models import Customer, CustomerContact, CustomerLocation, Item, Invoice, InvoiceItem, Unit, Offer, OfferItem, Reminder, InvoicePaymentAllocation, InvoicePayment
|
||||
from .forms import CustomerForm, ItemForm, CustomerInvoiceForm, ProjectInvoiceForm, InvoiceItemForm, OfferForm, OfferItemForm, ReminderForm, InvoicePaymentForm
|
||||
from datetime import date
|
||||
from projects.models import Project
|
||||
from django.http import JsonResponse, FileResponse
|
||||
@@ -18,8 +18,9 @@ from django.utils import timezone
|
||||
from weasyprint import HTML
|
||||
import io
|
||||
from django.db.models import Prefetch, Sum
|
||||
from .tables import ItemTable, CustomerTable, CustomerArchivedTable, ReminderTable, InvoiceTable, InvoiceItemTable, OfferTable, OfferItemTable
|
||||
from .tables import ItemTable, CustomerTable, CustomerArchivedTable, ReminderTable, InvoiceTable, InvoiceItemTable, OfferTable, OfferItemTable, InvoicePaymentTable
|
||||
from django_tables2 import RequestConfig
|
||||
from finance.models import Category, Supplier
|
||||
|
||||
def generate_document_number(template, number):
|
||||
today = date.today()
|
||||
@@ -416,6 +417,8 @@ def items_overview(request):
|
||||
table = ItemTable(items)
|
||||
table.types = Item.ItemType.choices
|
||||
table.units = Unit.objects.order_by("name")
|
||||
table.categories = Category.objects.order_by("name")
|
||||
table.suppliers = Supplier.objects.order_by("name")
|
||||
|
||||
RequestConfig(
|
||||
request,
|
||||
@@ -450,13 +453,14 @@ def items_add(request):
|
||||
@login_required
|
||||
@permission_required("invoices_items_edit")
|
||||
def items_edit(request, item_id):
|
||||
|
||||
if request.method != "POST":
|
||||
print("Invalid request method for items_edit")
|
||||
return redirect(
|
||||
"invoices:items_overview"
|
||||
)
|
||||
|
||||
|
||||
|
||||
item = get_object_or_404(
|
||||
Item,
|
||||
id=item_id
|
||||
@@ -467,7 +471,8 @@ def items_edit(request, item_id):
|
||||
request.POST,
|
||||
instance=item
|
||||
)
|
||||
|
||||
if not form.is_valid():
|
||||
print(form.errors.as_json())
|
||||
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
@@ -751,11 +756,12 @@ def ajax_invoice_item_data(request):
|
||||
@login_required
|
||||
@permission_required("invoices_invoices_view")
|
||||
def invoices_invoices_overview(request):
|
||||
status = request.GET.get("status")
|
||||
|
||||
invoices = Invoice.objects.select_related(
|
||||
"customer",
|
||||
"project"
|
||||
).all()
|
||||
invoices = Invoice.objects.select_related("customer", "project").all()
|
||||
|
||||
if status:
|
||||
invoices = invoices.filter(status=status)
|
||||
|
||||
table = InvoiceTable(invoices)
|
||||
|
||||
@@ -1174,7 +1180,11 @@ def invoices_download(request, invoice_id):
|
||||
)
|
||||
number_settings.invoice_next_number += 1
|
||||
number_settings.save(update_fields=["invoice_next_number"])
|
||||
invoice.save(update_fields=["invoice_number"])
|
||||
|
||||
company = CompanySetting.objects.first()
|
||||
invoice.is_small_business = company.is_small_business
|
||||
|
||||
invoice.save(update_fields=["invoice_number", "is_small_business"])
|
||||
|
||||
# ==========================
|
||||
# PDF HTML
|
||||
@@ -1290,40 +1300,87 @@ def invoices_cancel(request, invoice_id):
|
||||
id=invoice_id,
|
||||
)
|
||||
|
||||
if invoice.status == "canceled" and invoice.pdf_canceled_file:
|
||||
|
||||
# ==========================
|
||||
# Existierendes Storno zurückgeben
|
||||
# ==========================
|
||||
|
||||
if invoice.status == Invoice.Status.CANCELED and invoice.pdf_canceled_file:
|
||||
|
||||
return FileResponse(
|
||||
invoice.pdf_canceled_file.open("rb"),
|
||||
as_attachment=True,
|
||||
filename=invoice.pdf_canceled_file.name.split("/")[-1],
|
||||
)
|
||||
|
||||
|
||||
|
||||
# ==========================
|
||||
# Nur finale Rechnungen stornierbar
|
||||
# ==========================
|
||||
|
||||
if invoice.status not in [
|
||||
Invoice.Status.SENT,
|
||||
Invoice.Status.PAID,
|
||||
Invoice.Status.OVERDUE,
|
||||
]:
|
||||
return redirect(
|
||||
"invoices:invoices_detail",
|
||||
invoice.id,
|
||||
)
|
||||
|
||||
|
||||
company = CompanySetting.objects.first()
|
||||
number_settings = company.number_settings
|
||||
|
||||
|
||||
# ==========================
|
||||
# Stornonummer
|
||||
# ==========================
|
||||
|
||||
invoice.cancel_number = generate_document_number(
|
||||
number_settings.cancel_format,
|
||||
number_settings.cancel_next_number,
|
||||
)
|
||||
|
||||
|
||||
number_settings.cancel_next_number += 1
|
||||
|
||||
number_settings.save(
|
||||
update_fields=[
|
||||
"cancel_next_number"
|
||||
]
|
||||
)
|
||||
|
||||
if invoice.status not in ["sent", "paid"]:
|
||||
return redirect(
|
||||
"invoices:invoices_detail",
|
||||
invoice.id,
|
||||
)
|
||||
|
||||
# ==========================
|
||||
# Positionen
|
||||
# ==========================
|
||||
|
||||
items = invoice.items.all()
|
||||
|
||||
|
||||
# ==========================
|
||||
# Summen
|
||||
# Bereits bezahlt
|
||||
# ==========================
|
||||
|
||||
total_net = Decimal("0.00")
|
||||
paid_amount = invoice.paid_amount
|
||||
|
||||
|
||||
# ==========================
|
||||
# Rückerstattung berechnen
|
||||
# ==========================
|
||||
|
||||
refund_amount = min(
|
||||
paid_amount,
|
||||
invoice.total_amount
|
||||
)
|
||||
|
||||
|
||||
show_service_date = any(
|
||||
item.service_period_from
|
||||
for item in items
|
||||
)
|
||||
|
||||
|
||||
tax_summary = defaultdict(
|
||||
lambda: Decimal("0.00")
|
||||
@@ -1331,42 +1388,19 @@ def invoices_cancel(request, invoice_id):
|
||||
|
||||
for item in items:
|
||||
|
||||
item_net = Decimal(
|
||||
str(item.calculated_total_net)
|
||||
)
|
||||
|
||||
total_net += item_net
|
||||
|
||||
tax_rate = Decimal(
|
||||
str(item.unit_tax_rate)
|
||||
)
|
||||
item_net = item.calculated_total_net
|
||||
|
||||
tax_amount = (
|
||||
item_net *
|
||||
tax_rate /
|
||||
item.unit_tax_rate /
|
||||
Decimal("100")
|
||||
)
|
||||
|
||||
tax_summary[tax_rate] += tax_amount
|
||||
tax_summary[item.unit_tax_rate] += tax_amount
|
||||
|
||||
total_tax = sum(
|
||||
tax_summary.values(),
|
||||
Decimal("0.00")
|
||||
)
|
||||
|
||||
total_gross = (
|
||||
total_net +
|
||||
total_tax
|
||||
)
|
||||
|
||||
company = CompanySetting.objects.first()
|
||||
|
||||
original_status = invoice.status
|
||||
|
||||
show_service_date = any(
|
||||
item.service_period_from
|
||||
for item in items
|
||||
)
|
||||
# ==========================
|
||||
# PDF HTML
|
||||
# ==========================
|
||||
|
||||
html_string = render_to_string(
|
||||
"invoices/invoices/pdf_cancel.html",
|
||||
@@ -1376,42 +1410,54 @@ def invoices_cancel(request, invoice_id):
|
||||
"company": company,
|
||||
"cancel_date": timezone.now(),
|
||||
"cancel_number": invoice.cancel_number,
|
||||
"original_status": original_status,
|
||||
"total_net": total_net,
|
||||
"tax_summary": dict(tax_summary),
|
||||
"total_tax": total_tax,
|
||||
"total_gross": total_gross,
|
||||
"paid_amount": paid_amount,
|
||||
"refund_amount": refund_amount,
|
||||
"total_net": invoice.total_net,
|
||||
"total_tax": invoice.total_tax,
|
||||
"total_gross": invoice.total_amount,
|
||||
"show_service_date": show_service_date,
|
||||
"tax_summary": dict(tax_summary),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
# ==========================
|
||||
# PDF erstellen
|
||||
# ==========================
|
||||
|
||||
pdf_file = HTML(
|
||||
string=html_string,
|
||||
base_url=request.build_absolute_uri("/"),
|
||||
).write_pdf()
|
||||
|
||||
|
||||
filename = (
|
||||
f"Stornorechnung-{invoice.cancel_number}.pdf"
|
||||
)
|
||||
|
||||
|
||||
invoice.pdf_canceled_file.save(
|
||||
filename,
|
||||
ContentFile(pdf_file),
|
||||
save=False,
|
||||
)
|
||||
|
||||
|
||||
invoice.pdf_canceled_generated_at = timezone.now()
|
||||
invoice.status = "canceled"
|
||||
|
||||
invoice.status = Invoice.Status.CANCELED
|
||||
|
||||
|
||||
invoice.save(
|
||||
update_fields=[
|
||||
"pdf_canceled_file",
|
||||
"pdf_canceled_generated_at",
|
||||
"status",
|
||||
'cancel_number',
|
||||
"cancel_number",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
return FileResponse(
|
||||
io.BytesIO(pdf_file),
|
||||
as_attachment=True,
|
||||
@@ -1438,64 +1484,179 @@ def invoices_delete(request, invoice_id):
|
||||
return redirect("invoices:invoices_invoices_overview",)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def create_invoice_income(invoice):
|
||||
from finance.models import Income
|
||||
|
||||
if Income.objects.filter(
|
||||
invoice=invoice
|
||||
).exists():
|
||||
return
|
||||
|
||||
last_payment = invoice.payments.order_by(
|
||||
"-date"
|
||||
).first()
|
||||
|
||||
if not last_payment:
|
||||
return
|
||||
|
||||
Income.objects.create(
|
||||
invoice=invoice,
|
||||
amount=invoice.paid_invoice_amount,
|
||||
date=last_payment.date,
|
||||
name=f"Zahlung Rechnung {invoice.invoice_number}",
|
||||
project=invoice.project,
|
||||
payment_method=last_payment.payment_method,
|
||||
created_by=last_payment.created_by,
|
||||
)
|
||||
|
||||
def allocate_invoice_payment(payment):
|
||||
remaining = payment.amount
|
||||
invoice = payment.invoice
|
||||
reminders = invoice.reminders.filter(
|
||||
status__in=[
|
||||
Reminder.Status.SENT,
|
||||
Reminder.Status.EXPIRED,
|
||||
]
|
||||
).order_by("level")
|
||||
|
||||
for reminder in reminders:
|
||||
already_paid = reminder.payment_allocations.aggregate(
|
||||
total=Sum("amount")
|
||||
)["total"] or Decimal("0.00")
|
||||
open_fee = reminder.fee - already_paid
|
||||
if open_fee <= 0:
|
||||
continue
|
||||
allocation_amount = min(
|
||||
remaining,
|
||||
open_fee
|
||||
)
|
||||
if allocation_amount > 0:
|
||||
InvoicePaymentAllocation.objects.create(
|
||||
payment=payment,
|
||||
reminder=reminder,
|
||||
amount=allocation_amount
|
||||
)
|
||||
remaining -= allocation_amount
|
||||
if remaining <= 0:
|
||||
break
|
||||
if remaining > 0:
|
||||
InvoicePaymentAllocation.objects.create(
|
||||
payment=payment,
|
||||
reminder=None,
|
||||
amount=remaining
|
||||
)
|
||||
|
||||
def update_invoice_payment_status(invoice):
|
||||
# Mahnungen prüfen
|
||||
reminders = invoice.reminders.filter(
|
||||
status__in=[
|
||||
Reminder.Status.SENT,
|
||||
Reminder.Status.EXPIRED,
|
||||
]
|
||||
)
|
||||
for reminder in reminders:
|
||||
if reminder.paid_amount >= reminder.fee:
|
||||
reminder.status = Reminder.Status.PAID
|
||||
reminder.save(
|
||||
update_fields=[
|
||||
"status"
|
||||
]
|
||||
)
|
||||
if invoice.paid_invoice_amount >= invoice.total_amount:
|
||||
if invoice.status != Invoice.Status.PAID:
|
||||
invoice.status = Invoice.Status.PAID
|
||||
invoice.save(
|
||||
update_fields=[
|
||||
"status"
|
||||
]
|
||||
)
|
||||
|
||||
if invoice.project:
|
||||
create_invoice_income(invoice)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required("invoices_invoices_invoices_paid")
|
||||
def invoices_paid(request, invoice_id):
|
||||
|
||||
invoice = get_object_or_404(
|
||||
Invoice,
|
||||
id=invoice_id,
|
||||
)
|
||||
|
||||
if invoice.status != "sent":
|
||||
if invoice.status not in [
|
||||
Invoice.Status.SENT,
|
||||
Invoice.Status.OVERDUE,
|
||||
]:
|
||||
return redirect(
|
||||
"invoices:invoices_detail",
|
||||
invoice_id=invoice.id,
|
||||
)
|
||||
|
||||
invoice.status = "paid"
|
||||
|
||||
invoice.save(
|
||||
update_fields=[
|
||||
"status",
|
||||
]
|
||||
)
|
||||
|
||||
return redirect(
|
||||
"invoices:invoices_detail",
|
||||
invoice_id=invoice.id,
|
||||
if request.method == "POST":
|
||||
form = InvoicePaymentForm(request.POST)
|
||||
if form.is_valid():
|
||||
payment = form.save(commit=False)
|
||||
payment.invoice = invoice
|
||||
payment.created_by = request.user
|
||||
payment.save()
|
||||
allocate_invoice_payment(payment)
|
||||
update_invoice_payment_status(invoice)
|
||||
return redirect(
|
||||
"invoices:invoices_detail",
|
||||
invoice_id=invoice.id,
|
||||
)
|
||||
else:
|
||||
print(form.errors)
|
||||
else:
|
||||
form = InvoicePaymentForm()
|
||||
return render(
|
||||
request,
|
||||
"invoices/invoices/add_payment.html",
|
||||
{
|
||||
"form": form,
|
||||
"invoice": invoice,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required("invoices_invoices_invoices_overdue")
|
||||
def invoices_overdue(request, invoice_id):
|
||||
|
||||
invoice = get_object_or_404(
|
||||
Invoice,
|
||||
id=invoice_id,
|
||||
)
|
||||
|
||||
today = timezone.now().date()
|
||||
|
||||
if invoice.status != "sent":
|
||||
return redirect(
|
||||
"invoices:invoices_detail",
|
||||
invoice_id=invoice.id,
|
||||
)
|
||||
|
||||
if invoice.due_date and invoice.due_date < today:
|
||||
invoice.status = "overdue"
|
||||
invoice.save(
|
||||
update_fields=[
|
||||
"status",
|
||||
]
|
||||
)
|
||||
|
||||
return redirect(
|
||||
"invoices:invoices_detail",
|
||||
invoice_id=invoice.id,
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1561,8 +1722,12 @@ def invoices_overdue(request, invoice_id):
|
||||
@login_required
|
||||
@permission_required("invoices_offers_view")
|
||||
def invoices_offers_overview(request):
|
||||
status = request.GET.get("status")
|
||||
|
||||
offers = Offer.objects.select_related("customer",).all()
|
||||
|
||||
if status:
|
||||
offers = offers.filter(status=status)
|
||||
table = OfferTable(offers)
|
||||
|
||||
RequestConfig(
|
||||
@@ -2059,33 +2224,6 @@ def offers_reject(request, offer_id):
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required("invoices_offers_offers_status")
|
||||
def offers_expire(request, offer_id):
|
||||
|
||||
offer = get_object_or_404(
|
||||
Offer,
|
||||
id=offer_id,
|
||||
)
|
||||
|
||||
if offer.status != "sent":
|
||||
return redirect(
|
||||
"invoices:offers_detail",
|
||||
offer_id=offer.id,
|
||||
)
|
||||
|
||||
offer.status = "expired"
|
||||
|
||||
offer.save(
|
||||
update_fields=[
|
||||
"status",
|
||||
]
|
||||
)
|
||||
|
||||
return redirect(
|
||||
"invoices:offers_detail",
|
||||
offer_id=offer.id,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -2163,9 +2301,10 @@ def offers_expire(request, offer_id):
|
||||
@login_required
|
||||
@permission_required("invoices_reminders_view")
|
||||
def invoices_reminders_overview(request):
|
||||
|
||||
status = request.GET.get("status")
|
||||
reminders = Reminder.objects.select_related("invoice").all()
|
||||
|
||||
if status:
|
||||
reminders = reminders.filter(status=status)
|
||||
table = ReminderTable(reminders)
|
||||
|
||||
RequestConfig(
|
||||
@@ -2330,6 +2469,12 @@ def reminders_download(request, reminder_id):
|
||||
|
||||
invoice = reminder.invoice
|
||||
|
||||
payments = invoice.payments.order_by("date", "created_at")
|
||||
|
||||
total_paid = sum(
|
||||
payment.amount
|
||||
for payment in payments
|
||||
)
|
||||
|
||||
# ==========================
|
||||
# Rechnungsbetrag berechnen
|
||||
@@ -2382,6 +2527,7 @@ def reminders_download(request, reminder_id):
|
||||
+ reminder.fee
|
||||
)
|
||||
|
||||
open_amount = total_due - total_paid
|
||||
total_fees = (total_previous_fees + reminder.fee)
|
||||
|
||||
company = CompanySetting.objects.first()
|
||||
@@ -2429,11 +2575,13 @@ def reminders_download(request, reminder_id):
|
||||
"invoice_amount": invoice_amount,
|
||||
"previous_reminders": previous_reminders,
|
||||
"total_previous_fees": total_previous_fees,
|
||||
"total_fees": total_fees,
|
||||
"payments": payments,
|
||||
"total_paid": total_paid,
|
||||
"total_due": total_due,
|
||||
"total_fees": total_due,
|
||||
"open_amount": open_amount,
|
||||
}
|
||||
)
|
||||
|
||||
# ==========================
|
||||
# PDF erstellen
|
||||
# ==========================
|
||||
@@ -2475,50 +2623,49 @@ def reminders_download(request, reminder_id):
|
||||
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required("invoices_reminders_reminders_paid")
|
||||
def reminders_paid(request, reminder_id):
|
||||
reminder = get_object_or_404(
|
||||
Reminder,
|
||||
id=reminder_id,
|
||||
)
|
||||
|
||||
if reminder.status != "sent":
|
||||
return redirect(
|
||||
"invoices:reminders_detail",
|
||||
reminder_id=reminder.id,
|
||||
)
|
||||
|
||||
reminder.status = "paid"
|
||||
reminder.save(update_fields=["status",])
|
||||
|
||||
reminder.invoice.status = "paid"
|
||||
reminder.invoice.save(update_fields=["status",])
|
||||
|
||||
return redirect(
|
||||
"invoices:reminders_detail",
|
||||
reminder_id=reminder.id,
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required("invoices_reminders_reminders_expire")
|
||||
def reminders_expire(request, reminder_id):
|
||||
reminder = get_object_or_404(
|
||||
Reminder,
|
||||
id=reminder_id,
|
||||
@permission_required("invoices_payments_view")
|
||||
def invoices_payments_overview(request):
|
||||
|
||||
payments = InvoicePayment.objects.select_related(
|
||||
"invoice",
|
||||
"invoice__customer",
|
||||
"payment_method",
|
||||
"created_by",
|
||||
).order_by(
|
||||
"-date"
|
||||
)
|
||||
|
||||
if reminder.status != "sent":
|
||||
return redirect(
|
||||
"invoices:reminders_detail",
|
||||
reminder_id=reminder.id,
|
||||
)
|
||||
|
||||
reminder.status = "expired"
|
||||
reminder.save(update_fields=["status",])
|
||||
table = InvoicePaymentTable(
|
||||
payments
|
||||
)
|
||||
|
||||
return redirect(
|
||||
"invoices:reminders_detail",
|
||||
reminder_id=reminder.id,
|
||||
|
||||
RequestConfig(
|
||||
request,
|
||||
paginate={
|
||||
"per_page": 25
|
||||
}
|
||||
).configure(
|
||||
table
|
||||
)
|
||||
|
||||
|
||||
return render(
|
||||
request,
|
||||
"invoices/payments/overview.html",
|
||||
{
|
||||
"table": table,
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,81 @@
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
from django.db.models import Count, Q, Sum
|
||||
from django.http import JsonResponse
|
||||
from django.utils import timezone
|
||||
from datetime import timedelta
|
||||
from .models import Invoice, Offer, Reminder, Customer
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required("invoices_invoices_info")
|
||||
def ajax_invoice_info(request):
|
||||
now = timezone.now()
|
||||
data = (
|
||||
Invoice.objects
|
||||
.filter(issue_date__year=now.year, issue_date__month=now.month)
|
||||
.values("status")
|
||||
.annotate(count=Count("id"))
|
||||
)
|
||||
return JsonResponse({item["status"]: item["count"] for item in data})
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required("invoices_offers_info")
|
||||
def ajax_offer_info(request):
|
||||
now = timezone.now()
|
||||
data = (
|
||||
Offer.objects
|
||||
.filter(issue_date__year=now.year, issue_date__month=now.month)
|
||||
.values("status")
|
||||
.annotate(count=Count("id"))
|
||||
)
|
||||
return JsonResponse({item["status"]: item["count"] for item in data})
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required("invoices_reminders_info")
|
||||
def ajax_reminder_info(request):
|
||||
now = timezone.now()
|
||||
data = (
|
||||
Reminder.objects
|
||||
.filter(issue_date__year=now.year, issue_date__month=now.month)
|
||||
.values("status")
|
||||
.annotate(count=Count("id"))
|
||||
)
|
||||
return JsonResponse({item["status"]: item["count"] for item in data})
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required("invoices_info")
|
||||
def ajax_kpi_info(request):
|
||||
today = timezone.now().date()
|
||||
|
||||
overdue_invoices = Invoice.objects.filter(status="overdue").count()
|
||||
open_invoices = Invoice.objects.filter(status="sent").count()
|
||||
open_offers = Offer.objects.filter(status="sent").count()
|
||||
open_reminders = Reminder.objects.filter(status="sent").count()
|
||||
|
||||
overdue_amount = (
|
||||
Invoice.objects
|
||||
.filter(status="overdue")
|
||||
.aggregate(total=Sum("items__unit_price_net"))["total"] or 0
|
||||
)
|
||||
|
||||
return JsonResponse({
|
||||
"overdue_invoices": overdue_invoices,
|
||||
"open_invoices": open_invoices,
|
||||
"open_offers": open_offers,
|
||||
"open_reminders": open_reminders,
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required("invoices_customers_info")
|
||||
def ajax_customer_info(request):
|
||||
start_of_week = timezone.now().date() - timedelta(days=timezone.now().weekday())
|
||||
return JsonResponse({
|
||||
"total": Customer.objects.count(),
|
||||
"new_this_week": Customer.objects.filter(created_at__date__gte=start_of_week).count(),
|
||||
"companies": Customer.objects.filter(customer_type="company").count(),
|
||||
"private": Customer.objects.filter(customer_type="private").count(),
|
||||
})
|
||||
Reference in New Issue
Block a user