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, )