2524 lines
48 KiB
Python
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,
|
|
) |