Files
ProjectApp/invoices/views.py
T
Felix Baumann fb47c53d52 Initial commit
2026-06-27 17:07:34 +00:00

2524 lines
48 KiB
Python

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 datetime import date
from projects.models import Project
from django.http import JsonResponse, FileResponse
from decimal import Decimal
from time_tracking.models import TimeEntry
from finance.models import Expense
from core.models import CompanySetting
from django.views.decorators.http import require_POST
from collections import defaultdict
from django.template.loader import render_to_string
from django.core.files.base import ContentFile
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 django_tables2 import RequestConfig
def generate_document_number(template, number):
today = date.today()
return template.format(
year=today.strftime("%Y"),
month=today.strftime("%m"),
day=today.strftime("%d"),
number=str(number).zfill(5),
)
@login_required
@permission_required("invoices_view")
def invoices_overview(request):
return render(request, "invoices/overview.html")
@login_required
@permission_required("invoices_customers_view")
def customers_overview(request):
customers = Customer.objects.filter(active=True).order_by("company_name")
archived_customers = Customer.objects.filter(active=False).order_by("company_name")
table = CustomerTable(customers)
RequestConfig(
request,
paginate=False
).configure(table)
table_archived = CustomerArchivedTable(archived_customers)
RequestConfig(
request,
paginate=False
).configure(table_archived)
return render(request, "invoices/customers/overview.html", {
"table": table, "table_archived": table_archived,
})
@login_required
@permission_required("invoices_customers_add")
def customers_add(request):
if request.method == "POST":
form = CustomerForm(request.POST)
if form.is_valid():
form.save()
return redirect("invoices:customers_overview")
else:
form = CustomerForm()
return render(request, "invoices/customers/add.html", {
"form": form,
})
@login_required
@permission_required("invoices_customers_edit")
def customers_edit(request, customer_id):
customer = get_object_or_404(
Customer.objects.prefetch_related(
"contacts",
"locations",
),
id=customer_id
)
if request.method != "POST":
return redirect(
"invoices:customers_detail",
customer.id
)
# =========================
# CUSTOMER SAVE
# =========================
form = CustomerForm(
request.POST,
instance=customer
)
if form.is_valid():
form.save()
# =========================
# CONTACT DELETE
# =========================
delete_contact_id = request.POST.get(
"delete_contact_id"
)
if delete_contact_id:
CustomerContact.objects.filter(
id=delete_contact_id,
customer=customer
).delete()
return redirect(
"invoices:customers_detail",
customer.id
)
# =========================
# CONTACT UPDATE
# =========================
for contact in customer.contacts.all():
contact.first_name = request.POST.get(
f"contact_first_name_{contact.id}",
contact.first_name
)
contact.last_name = request.POST.get(
f"contact_last_name_{contact.id}",
contact.last_name
)
contact.email = request.POST.get(
f"contact_email_{contact.id}",
contact.email
)
contact.phone = request.POST.get(
f"contact_phone_{contact.id}",
contact.phone
)
contact.save()
# =========================
# NEW CONTACTS
# =========================
first_names = request.POST.getlist(
"new_contact_first_name"
)
last_names = request.POST.getlist(
"new_contact_last_name"
)
emails = request.POST.getlist(
"new_contact_email"
)
phones = request.POST.getlist(
"new_contact_phone"
)
for i in range(len(first_names)):
if not (
first_names[i].strip()
or last_names[i].strip()
):
continue
CustomerContact.objects.create(
customer=customer,
first_name=first_names[i],
last_name=last_names[i],
email=emails[i],
phone=phones[i],
)
# =========================
# LOCATION DELETE
# =========================
delete_location_id = request.POST.get(
"delete_location_id"
)
if delete_location_id:
CustomerLocation.objects.filter(
id=delete_location_id,
customer=customer
).delete()
return redirect(
"invoices:customers_detail",
customer.id
)
# =========================
# LOCATION UPDATE
# =========================
for location in customer.locations.all():
location.name = request.POST.get(
f"location_name_{location.id}",
location.name
)
location.street = request.POST.get(
f"location_street_{location.id}",
location.street
)
location.zip_code = request.POST.get(
f"location_zip_{location.id}",
location.zip_code
)
location.city = request.POST.get(
f"location_city_{location.id}",
location.city
)
location.country = request.POST.get(
f"location_country_{location.id}",
location.country
)
location.save()
# =========================
# NEW LOCATIONS
# =========================
names = request.POST.getlist(
"new_location_name"
)
streets = request.POST.getlist(
"new_location_street"
)
zips = request.POST.getlist(
"new_location_zip"
)
cities = request.POST.getlist(
"new_location_city"
)
countries = request.POST.getlist(
"new_location_country"
)
for i in range(len(names)):
if not names[i].strip():
continue
CustomerLocation.objects.create(
customer=customer,
name=names[i],
street=streets[i],
zip_code=zips[i],
city=cities[i],
country=countries[i],
)
return redirect(
"invoices:customers_detail",
customer.id
)
@login_required
@permission_required("invoices_customers_archive")
def customers_archive(request, customer_id):
customer = get_object_or_404(
Customer,
id=customer_id
)
if request.method == "POST":
customer.active = False
customer.save()
return redirect(
"invoices:customers_overview"
)
@login_required
@permission_required("invoices_customers_activate")
def customers_reactivate(request, customer_id):
customer = get_object_or_404(
Customer,
id=customer_id
)
if request.method == "POST":
customer.active = True
customer.save()
return redirect(
"invoices:customers_overview"
)
@login_required
@permission_required("invoices_customers_detail_view")
def customers_detail(request, customer_id):
customer = get_object_or_404(
Customer.objects.prefetch_related(
"contacts",
"locations",
"projects",
),
id=customer_id
)
form = CustomerForm(
instance=customer
)
return render(
request,
"invoices/customers/detail.html",
{
"customer": customer,
"form": form,
}
)
@login_required
@permission_required("invoices_items_view")
def items_overview(request):
units = Unit.objects.all()
items = Item.objects.select_related(
"unit"
).all()
table = ItemTable(items)
table.types = Item.ItemType.choices
table.units = Unit.objects.order_by("name")
RequestConfig(
request,
paginate=False
).configure(table)
return render(
request,
"invoices/items/overview.html",
{
"table": table,
}
)
@login_required
@permission_required("invoices_items_add")
def items_add(request):
if request.method == "POST":
form = ItemForm(request.POST)
if form.is_valid():
form.save()
return redirect("invoices:items_overview")
else:
form = ItemForm()
return render(request, "invoices/items/add.html", {
"form": form
})
@login_required
@permission_required("invoices_items_edit")
def items_edit(request, item_id):
if request.method != "POST":
return redirect(
"invoices:items_overview"
)
item = get_object_or_404(
Item,
id=item_id
)
form = ItemForm(
request.POST,
instance=item
)
if form.is_valid():
form.save()
return redirect(
"invoices:items_overview"
)
@login_required
@permission_required("invoices_items_delete")
def items_delete(request,item_id):
item = get_object_or_404(
Item,
id=item_id
)
if request.method == "POST":
item.delete()
return redirect(
"invoices:items_overview"
)
@login_required
def ajax_contacts(request, customer_id):
contacts = CustomerContact.objects.filter(customer_id=customer_id)
data = [
{"id": c.id, "text": str(c)}
for c in contacts
]
return JsonResponse(data, safe=False)
@login_required
def ajax_locations(request, customer_id):
locations = CustomerLocation.objects.filter(customer_id=customer_id)
data = [
{"id": l.id, "text": str(l)}
for l in locations
]
return JsonResponse(data, safe=False)
@login_required
def ajax_invoicedata(request, project_id):
project = get_object_or_404(
Project,
id=project_id
)
if project.customer:
contacts = [
{
"id": c.id,
"text": str(c)
}
for c in project.customer.contacts.all()
]
locations = [
{
"id": l.id,
"text": str(l)
}
for l in project.customer.locations.all()
]
return JsonResponse({
"has_customer": True,
"contacts": contacts,
"locations": locations,
})
return JsonResponse({
"has_customer": False
})
@login_required
def ajax_invoiceitemsources(request, invoice_id):
invoice = get_object_or_404(
Invoice,
id=invoice_id
)
data = {
"has_project": False,
"time_entries": [],
"expenses": [],
}
if invoice.project:
data["has_project"] = True
data["time_entries"] = [
{
"id": t.id,
"text": str(t)
}
for t in invoice.project.time_entries.all()
]
data["expenses"] = [
{
"id": e.id,
"text": str(e)
}
for e in invoice.project.expenses.all()
]
return JsonResponse(data)
@login_required
def ajax_invoice_item_data(request):
source = request.GET.get("source")
source_id = request.GET.get("id")
data = {}
if source == "item":
item = get_object_or_404(
Item,
id=source_id
)
data = {
"description": item.description,
"quantity": "1.00",
"unit": item.unit.name,
"unit_price_net": str(item.price_net),
"unit_tax_rate": str(item.tax_rate),
}
elif source == "time":
entry = get_object_or_404(
TimeEntry,
id=source_id,
)
data = {
"description": (
f"Arbeitszeit "
f"{entry.start.strftime('%d.%m.%Y')} - "
f"{entry.end.strftime('%d.%m.%Y')}"
),
"quantity": str(
entry.duration_hours
),
"unit": "Stunden",
"unit_price_net": str(
entry.project.hourly_rate
),
}
elif source == "expense":
expense = get_object_or_404(
Expense,
id=source_id
)
data = {
"description": str(expense.name),
"quantity": "1.00",
"unit": "Stück",
"unit_price_net": str(expense.amount),
}
return JsonResponse(data)
@login_required
@permission_required("invoices_invoices_view")
def invoices_invoices_overview(request):
invoices = Invoice.objects.select_related(
"customer",
"project"
).all()
table = InvoiceTable(invoices)
RequestConfig(
request,
paginate=False
).configure(table)
return render(
request,
"invoices/invoices/overview.html",
{
"table": table
}
)
@login_required
@permission_required("invoices_invoices_customer_add")
def invoice_add_customer(request):
form = CustomerInvoiceForm(request.POST or None)
if form.is_valid():
invoice = form.save(commit=False)
customer = invoice.customer
contact = form.cleaned_data.get("contact")
location = form.cleaned_data.get("location")
invoice.tax_number = customer.tax_number
invoice.vat_number = customer.vat_number
# SNAPSHOT CONTACT
if contact:
invoice.contact_name = f"{contact.first_name} {contact.last_name}"
invoice.contact_email = contact.email
invoice.contact_phone = contact.phone
# SNAPSHOT LOCATION
if location:
invoice.location_name = location.name
invoice.location_street = location.street
invoice.location_city = location.city
invoice.location_zip = location.zip_code
invoice.location_country = location.country
invoice.save()
return redirect("invoices:invoices_invoices_overview")
return render(request, "invoices/invoices/add_customer.html", {
"form": form
})
@login_required
@permission_required("invoices_invoices_project_add")
def invoice_add_project(request):
form = ProjectInvoiceForm(request.POST or None)
if form.is_valid():
invoice = form.save(commit=False)
project = form.cleaned_data.get("project")
invoice.project = project
if project and project.customer:
customer = project.customer
invoice.customer = customer
invoice.tax_number = customer.tax_number
invoice.vat_number = customer.vat_number
contact = form.cleaned_data.get("contact")
if contact:
invoice.contact_name = (
f"{contact.first_name} {contact.last_name}"
)
invoice.contact_email = contact.email
invoice.contact_phone = contact.phone
location = form.cleaned_data.get("location")
if location:
invoice.location_name = location.name
invoice.location_street = location.street
invoice.location_zip = location.zip_code
invoice.location_city = location.city
invoice.location_country = location.country
else:
invoice.customer = None
invoice.contact_name = form.cleaned_data.get(
"contact_name_manual",
""
)
invoice.contact_email = form.cleaned_data.get(
"contact_email_manual",
""
)
invoice.contact_phone = form.cleaned_data.get(
"contact_phone_manual",
""
)
invoice.location_name = form.cleaned_data.get(
"location_name_manual",
""
)
invoice.location_street = form.cleaned_data.get(
"location_street_manual",
""
)
invoice.location_zip = form.cleaned_data.get(
"location_zip_manual",
""
)
invoice.location_city = form.cleaned_data.get(
"location_city_manual",
""
)
invoice.location_country = form.cleaned_data.get(
"location_country_manual",
""
)
invoice.save()
return redirect(
"invoices:invoices_invoices_overview"
)
return render(
request,
"invoices/invoices/add_project.html",
{
"form": form
}
)
@login_required
@permission_required("invoices_invoices_invoices_view")
def invoices_detail(request, invoice_id):
invoice = get_object_or_404(
Invoice.objects.select_related(
"customer",
"project",
).prefetch_related(
"items__item",
"items__time_entry",
"items__expense",
"reminders",
),
id=invoice_id
)
items = invoice.items.all()
reminders = invoice.reminders.all()
total_net = sum(
(i.unit_price_net or Decimal("0.00")) *
(i.quantity or Decimal("0.00"))
for i in items
)
show_service_date = any(
item.service_period_from
for item in items
)
table = InvoiceItemTable(items)
if not show_service_date:
table.columns.hide("service_period")
if invoice.status != "draft":
table.columns.hide("actions")
RequestConfig(
request,
paginate=False
).configure(table)
return render(
request,
"invoices/invoices/detail.html",
{
"invoice": invoice,
"table": table,
"reminders": reminders,
"total_net": total_net,
"show_service_date": show_service_date,
}
)
@login_required
@permission_required("invoices_invoices_invoices_item_add")
def invoice_item_add(request, invoice_id):
invoice = get_object_or_404(
Invoice,
id=invoice_id
)
if invoice.status != "draft":
return redirect("invoices:invoices_detail",invoice.id)
if request.method == "POST":
form = InvoiceItemForm(
request.POST,
invoice=invoice
)
if form.is_valid():
item = form.save(
commit=False
)
item.invoice = invoice
item.save()
return redirect(
"invoices:invoices_detail",
invoice.id
)
else:
form = InvoiceItemForm(
invoice=invoice
)
return render(
request,
"invoices/invoices/item_add.html",
{
"form": form,
"invoice": invoice
}
)
@login_required
@permission_required("invoices_invoices_invoices_preview")
def invoices_preview(request, invoice_id):
invoice = get_object_or_404(
Invoice.objects.select_related(
"customer",
"project",
),
id=invoice_id
)
if invoice.status != "draft":
return redirect("invoices:invoices_detail",invoice.id)
items = invoice.items.all()
# ==========================
# Summen berechnen
# ==========================
total_net = Decimal("0.00")
tax_summary = defaultdict(
lambda: Decimal("0.00")
)
for item in items:
item_net = Decimal(str(item.calculated_total_net))
total_net += item_net
tax_rate = Decimal(str(item.unit_tax_rate))
tax_amount = (
item_net *
tax_rate /
Decimal("100")
)
tax_summary[tax_rate] += tax_amount
total_tax = sum(
tax_summary.values(),
Decimal("0.00")
)
total_gross = (
total_net +
total_tax
)
show_service_date = any(
item.service_period_from
for item in items
)
# ==========================
# Firma
# ==========================
company = CompanySetting.objects.first()
return render(
request,
"invoices/invoices/preview.html",
{
"invoice": invoice,
"items": items,
"company": company,
# Summen
"total_net": total_net,
"tax_summary": dict(tax_summary),
"total_tax": total_tax,
"total_gross": total_gross,
"show_service_date": show_service_date,
}
)
@login_required
@permission_required("invoices_invoices_invoices_download")
def invoices_download(request, invoice_id):
invoice = get_object_or_404(
Invoice.objects.select_related(
"customer",
"project",
),
id=invoice_id
)
# ==========================
# Bestehendes PDF zurückgeben
# ==========================
if invoice.status != "draft" and invoice.pdf_file:
return FileResponse(
invoice.pdf_file.open("rb"),
as_attachment=True,
filename=invoice.pdf_file.name.split("/")[-1],
)
items = invoice.items.all()
# ==========================
# Summen
# ==========================
total_net = Decimal("0.00")
tax_summary = defaultdict(
lambda: Decimal("0.00")
)
for item in items:
item_net = Decimal(
str(item.calculated_total_net)
)
total_net += item_net
tax_rate = Decimal(
str(item.unit_tax_rate)
)
tax_amount = (
item_net *
tax_rate /
Decimal("100")
)
tax_summary[tax_rate] += tax_amount
total_tax = sum(
tax_summary.values(),
Decimal("0.00")
)
total_gross = total_net + total_tax
company = CompanySetting.objects.first()
number_settings = company.number_settings
invoice.invoice_number = generate_document_number(
number_settings.invoice_format,
number_settings.invoice_next_number,
)
number_settings.invoice_next_number += 1
number_settings.save(update_fields=["invoice_next_number"])
invoice.save(update_fields=["invoice_number"])
# ==========================
# PDF HTML
# ==========================
show_service_date = any(
item.service_period_from
for item in items
)
html_string = render_to_string(
"invoices/invoices/pdf.html",
{
"invoice": invoice,
"items": items,
"company": company,
"total_net": total_net,
"tax_summary": dict(tax_summary),
"total_tax": total_tax,
"total_gross": total_gross,
"show_service_date": show_service_date,
}
)
# ==========================
# PDF erstellen
# ==========================
pdf_file = HTML(
string=html_string,
base_url=request.build_absolute_uri("/")
).write_pdf()
filename = f"Rechnung-{invoice.invoice_number}.pdf"
# ==========================
# PDF speichern
# ==========================
invoice.pdf_file.save(
filename,
ContentFile(pdf_file),
save=False
)
invoice.pdf_generated_at = timezone.now()
invoice.status = "sent"
invoice.save(
update_fields=[
"pdf_file",
"pdf_generated_at",
"status",
]
)
return FileResponse(
io.BytesIO(pdf_file),
as_attachment=True,
filename=filename
)
@login_required
@permission_required("invoices_invoices_invoices_item_delete")
def invoice_item_delete(request,invoice_id,item_id):
invoice = get_object_or_404(
Invoice.objects.select_related(
"customer",
"project",
),
id=invoice_id
)
item = get_object_or_404(
InvoiceItem,
id=item_id,
invoice_id=invoice_id
)
if invoice.status != "draft":
return redirect("invoices:invoices_detail",invoice.id)
if request.method == "POST":
item.delete()
return redirect(
"invoices:invoices_detail",
invoice_id=invoice_id
)
@login_required
@permission_required("invoices_invoices_invoices_cancel")
def invoices_cancel(request, invoice_id):
invoice = get_object_or_404(
Invoice.objects.select_related(
"customer",
"project",
),
id=invoice_id,
)
if 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],
)
company = CompanySetting.objects.first()
number_settings = company.number_settings
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,
)
items = invoice.items.all()
# ==========================
# Summen
# ==========================
total_net = Decimal("0.00")
tax_summary = defaultdict(
lambda: Decimal("0.00")
)
for item in items:
item_net = Decimal(
str(item.calculated_total_net)
)
total_net += item_net
tax_rate = Decimal(
str(item.unit_tax_rate)
)
tax_amount = (
item_net *
tax_rate /
Decimal("100")
)
tax_summary[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
)
html_string = render_to_string(
"invoices/invoices/pdf_cancel.html",
{
"invoice": invoice,
"items": items,
"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,
"show_service_date": show_service_date,
},
)
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.save(
update_fields=[
"pdf_canceled_file",
"pdf_canceled_generated_at",
"status",
'cancel_number',
]
)
return FileResponse(
io.BytesIO(pdf_file),
as_attachment=True,
filename=filename,
)
@login_required
@permission_required("invoices_invoices_invoices_delete")
def invoices_delete(request, invoice_id):
invoice = get_object_or_404(
Invoice,
id=invoice_id,
)
if invoice.status != Invoice.Status.DRAFT:
return redirect(
"invoices:invoices_detail",
invoice_id=invoice.id,
)
invoice.delete()
return redirect("invoices:invoices_invoices_overview",)
@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":
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,
)
@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,
)
@login_required
@permission_required("invoices_offers_view")
def invoices_offers_overview(request):
offers = Offer.objects.select_related("customer",).all()
table = OfferTable(offers)
RequestConfig(
request,
paginate=False
).configure(table)
return render(
request,
"invoices/offers/overview.html",
{
"table": table
}
)
@login_required
@permission_required("invoices_offers_add")
def invoices_offers_add(request):
form = OfferForm(request.POST or None)
if form.is_valid():
offer = form.save(commit=False)
customer = offer.customer
contact = form.cleaned_data.get("contact")
location = form.cleaned_data.get("location")
offer.tax_number = customer.tax_number
offer.vat_number = customer.vat_number
# SNAPSHOT CONTACT
if contact:
offer.contact_name = f"{contact.first_name} {contact.last_name}"
offer.contact_email = contact.email
offer.contact_phone = contact.phone
# SNAPSHOT LOCATION
if location:
offer.location_name = location.name
offer.location_street = location.street
offer.location_city = location.city
offer.location_zip = location.zip_code
offer.location_country = location.country
offer.save()
return redirect("invoices:invoices_offers_overview")
return render(request, "invoices/offers/add.html", {
"form": form
})
@login_required
@permission_required("invoices_offers_offers_delete")
def offers_delete(request, offer_id):
offer = get_object_or_404(
Offer,
id=offer_id,
)
if offer.status != Offer.Status.DRAFT:
return redirect(
"invoices:offers_detail",
offer_id=offer.id,
)
offer.delete()
return redirect("invoices:invoices_offers_overview",)
@login_required
@permission_required("invoices_offers_offers_view")
def offers_detail(request, offer_id):
offer = get_object_or_404(
Offer.objects.select_related(
"customer",
),
id=offer_id
)
items = offer.items.select_related(
"item",
)
total_net = sum(
(i.unit_price_net or Decimal("0.00")) *
(i.quantity or Decimal("0.00"))
for i in items
)
table = OfferItemTable(items)
table.offer = offer
if offer.status != "draft":
table.columns.hide("actions")
RequestConfig(
request,
paginate=False
).configure(table)
return render(
request,
"invoices/offers/detail.html",
{
"offer": offer,
"table": table,
"total_net": total_net
}
)
@login_required
@permission_required("invoices_offers_offers_item_add")
def offers_item_add(request, offer_id):
offer = get_object_or_404(
Offer,
id=offer_id
)
if offer.status != "draft":
return redirect("invoices:offers_detail",offer.id)
if request.method == "POST":
form = OfferItemForm(
request.POST,
offer=offer
)
if form.is_valid():
item = form.save(
commit=False
)
item.offer = offer
item.save()
return redirect(
"invoices:offers_detail",
offer.id
)
else:
form = OfferItemForm(
offer=offer
)
return render(
request,
"invoices/offers/item_add.html",
{
"form": form,
"offer": offer
}
)
@login_required
@permission_required("invoices_offers_offers_item_delete")
def offers_item_delete(request,offer_id,item_id):
offer = get_object_or_404(
Offer.objects.select_related(
"customer",
),
id=offer_id
)
item = get_object_or_404(
OfferItem,
id=item_id,
offer_id=offer_id
)
if offer.status != "draft":
return redirect("invoices:offers_detail",offer.id)
if request.method == "POST":
item.delete()
return redirect(
"invoices:offers_detail",
offer_id=offer_id
)
@login_required
@permission_required("invoices_offers_offers_preview")
def offers_preview(request, offer_id):
offer = get_object_or_404(
Offer.objects.select_related(
"customer",
),
id=offer_id
)
if offer.status != "draft":
return redirect("invoices:offers_detail",offer.id)
items = offer.items.all()
# ==========================
# Summen berechnen
# ==========================
total_net = Decimal("0.00")
tax_summary = defaultdict(
lambda: Decimal("0.00")
)
for item in items:
item_net = Decimal(str(item.calculated_total_net))
total_net += item_net
tax_rate = Decimal(str(item.unit_tax_rate))
tax_amount = (
item_net *
tax_rate /
Decimal("100")
)
tax_summary[tax_rate] += tax_amount
total_tax = sum(
tax_summary.values(),
Decimal("0.00")
)
total_gross = (
total_net +
total_tax
)
# ==========================
# Firma
# ==========================
company = CompanySetting.objects.first()
return render(
request,
"invoices/offers/preview.html",
{
"offer": offer,
"items": items,
"company": company,
# Summen
"total_net": total_net,
"tax_summary": dict(tax_summary),
"total_tax": total_tax,
"total_gross": total_gross,
}
)
@login_required
@permission_required("invoices_offers_offers_download")
def offers_download(request, offer_id):
offer = get_object_or_404(
Offer.objects.select_related(
"customer",
),
id=offer_id
)
# ==========================
# Bestehendes PDF zurückgeben
# ==========================
if offer.status != "draft" and offer.pdf_file:
return FileResponse(
offer.pdf_file.open("rb"),
as_attachment=True,
filename=offer.pdf_file.name.split("/")[-1],
)
items = offer.items.all()
# ==========================
# Summen
# ==========================
total_net = Decimal("0.00")
tax_summary = defaultdict(
lambda: Decimal("0.00")
)
for item in items:
item_net = Decimal(
str(item.calculated_total_net)
)
total_net += item_net
tax_rate = Decimal(
str(item.unit_tax_rate)
)
tax_amount = (
item_net *
tax_rate /
Decimal("100")
)
tax_summary[tax_rate] += tax_amount
total_tax = sum(
tax_summary.values(),
Decimal("0.00")
)
total_gross = total_net + total_tax
company = CompanySetting.objects.first()
number_settings = company.number_settings
offer.offer_number = generate_document_number(
number_settings.offer_format,
number_settings.offer_next_number,
)
number_settings.offer_next_number += 1
number_settings.save(update_fields=["offer_next_number"])
offer.save(update_fields=["offer_number"])
# ==========================
# PDF HTML
# ==========================
html_string = render_to_string(
"invoices/offers/pdf.html",
{
"offer": offer,
"items": items,
"company": company,
"total_net": total_net,
"tax_summary": dict(tax_summary),
"total_tax": total_tax,
"total_gross": total_gross,
}
)
# ==========================
# PDF erstellen
# ==========================
pdf_file = HTML(
string=html_string,
base_url=request.build_absolute_uri("/")
).write_pdf()
filename = f"Angebot-{offer.offer_number}.pdf"
# ==========================
# PDF speichern
# ==========================
offer.pdf_file.save(
filename,
ContentFile(pdf_file),
save=False
)
offer.pdf_generated_at = timezone.now()
offer.status = "sent"
offer.save(
update_fields=[
"pdf_file",
"pdf_generated_at",
"status",
]
)
return FileResponse(
io.BytesIO(pdf_file),
as_attachment=True,
filename=filename
)
@login_required
@permission_required("invoices_offers_offers_status")
def offers_accept(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 = "accepted"
offer.save(
update_fields=[
"status",
]
)
return redirect(
"invoices:offers_detail",
offer_id=offer.id,
)
@login_required
@permission_required("invoices_offers_offers_status")
def offers_reject(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 = "rejected"
offer.save(
update_fields=[
"status",
]
)
return redirect(
"invoices:offers_detail",
offer_id=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,
)
@login_required
@permission_required("invoices_reminders_view")
def invoices_reminders_overview(request):
reminders = Reminder.objects.select_related("invoice").all()
table = ReminderTable(reminders)
RequestConfig(
request,
paginate=False
).configure(table)
return render(
request,
"invoices/reminders/overview.html",
{
"table": table,
}
)
@login_required
@permission_required("invoices_reminders_add")
def invoices_reminders_add(request):
if request.method == "POST":
form = ReminderForm(request.POST)
if form.is_valid():
reminder = form.save(commit=False)
invoice = reminder.invoice
reminders = invoice.reminders.exclude(
status=Reminder.Status.CANCELED
).order_by("-level")
# ==========================
# Vorhandene Mahnung prüfen
# ==========================
if reminders.exists():
last_reminder = reminders.first()
if last_reminder.status not in [
Reminder.Status.EXPIRED,
]:
return redirect("invoices:invoices_reminders_overview",)
next_level = last_reminder.level + 1
else:
next_level = 1
if next_level > 3:
return redirect(
"invoices:invoices_reminders_overview",)
reminder.level = next_level
reminder.status = Reminder.Status.DRAFT
reminder.save()
return redirect(
"invoices:reminders_detail",
reminder_id=reminder.pk,
)
else:
form = ReminderForm()
return render(
request,
"invoices/reminders/add.html",
{
"form": form,
},
)
@login_required
@permission_required("invoices_reminders_reminders_view")
def reminders_detail(request, reminder_id):
reminder = get_object_or_404(
Reminder.objects.select_related("invoice",),
id=reminder_id
)
return render(
request,
"invoices/reminders/detail.html",
{
"reminder": reminder
}
)
@login_required
@permission_required("invoices_reminders_reminders_cancel")
def reminders_cancel(request, reminder_id):
reminder = get_object_or_404(
Reminder,
id=reminder_id,
)
if reminder.status != "sent" and reminder.status != "draft":
return redirect(
"invoices:reminders_detail",
reminder_id=reminder.id,
)
reminder.status = "canceled"
reminder.save(
update_fields=[
"status",
]
)
return redirect(
"invoices:reminders_detail",
reminder_id=reminder.id,
)
@login_required
@permission_required("invoices_reminders_reminders_delete")
def reminders_delete(request, reminder_id):
reminder = get_object_or_404(
Reminder,
id=reminder_id,
)
if reminder.status != Reminder.Status.DRAFT:
return redirect(
"invoices:reminders_detail",
reminder_id=reminder.id,
)
reminder.delete()
return redirect("invoices:invoices_reminders_overview",)
@login_required
@permission_required("invoices_reminders_reminders_download")
def reminders_download(request, reminder_id):
reminder = get_object_or_404(
Reminder.objects.select_related(
"invoice",
"invoice__customer",
"invoice__project",
),
id=reminder_id
)
# ==========================
# Bestehendes PDF zurückgeben
# ==========================
if reminder.pdf_file:
return FileResponse(
reminder.pdf_file.open("rb"),
as_attachment=True,
filename=reminder.pdf_file.name.split("/")[-1],
)
invoice = reminder.invoice
# ==========================
# Rechnungsbetrag berechnen
# ==========================
items = invoice.items.all()
total_net = Decimal("0.00")
total_tax = Decimal("0.00")
for item in items:
item_net = Decimal(
str(item.calculated_total_net)
)
total_net += item_net
tax_rate = Decimal(
str(item.unit_tax_rate)
)
total_tax += (
item_net *
tax_rate /
Decimal("100")
)
invoice_amount = total_net + total_tax
# ==========================
# Vorherige Mahnungen
# ==========================
previous_reminders = (
invoice.reminders
.exclude(id=reminder.id)
.exclude(status=Reminder.Status.CANCELED)
.order_by("level")
)
total_previous_fees = sum(
r.fee for r in previous_reminders
)
total_due = (
invoice_amount
+ total_previous_fees
+ reminder.fee
)
total_fees = (total_previous_fees + reminder.fee)
company = CompanySetting.objects.first()
# ==========================
# Mahnungsnummer vergeben
# ==========================
if not reminder.reminder_number:
number_settings = company.number_settings
reminder.reminder_number = generate_document_number(
number_settings.reminder_format,
number_settings.reminder_next_number,
)
number_settings.reminder_next_number += 1
number_settings.save(
update_fields=[
"reminder_next_number"
]
)
reminder.save(
update_fields=[
"reminder_number",
]
)
# ==========================
# PDF HTML
# ==========================
html_string = render_to_string(
"invoices/reminders/pdf.html",
{
"reminder": reminder,
"invoice": invoice,
"company": company,
"invoice_amount": invoice_amount,
"previous_reminders": previous_reminders,
"total_previous_fees": total_previous_fees,
"total_due": total_due,
"total_fees": total_due,
}
)
# ==========================
# PDF erstellen
# ==========================
pdf_file = HTML(
string=html_string,
base_url=request.build_absolute_uri("/")
).write_pdf()
filename = f"Mahnung-{reminder.reminder_number}.pdf"
reminder.pdf_file.save(
filename,
ContentFile(pdf_file),
save=False
)
reminder.pdf_generated_at = timezone.now()
reminder.status = Reminder.Status.SENT
reminder.save(
update_fields=[
"pdf_file",
"pdf_generated_at",
"status",
]
)
return FileResponse(
io.BytesIO(pdf_file),
as_attachment=True,
filename=filename
)
@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,
)
if reminder.status != "sent":
return redirect(
"invoices:reminders_detail",
reminder_id=reminder.id,
)
reminder.status = "expired"
reminder.save(update_fields=["status",])
return redirect(
"invoices:reminders_detail",
reminder_id=reminder.id,
)