money-manager/expenses/admin.py

266 lines
8.0 KiB
Python

from datetime import datetime, timedelta
import random
from typing import Any, Dict
from django.contrib import admin
from django.contrib.admin.options import ModelAdmin
from django.core.handlers.wsgi import WSGIRequest
from django.db.models.base import Model
from django.db.models.fields import Field
from django.forms import DecimalField, TextInput, NumberInput
from django.db import models
from util import next_payday
from .admin_base import AdminBase, DeletedListFilter, DeletableAdminForm
from .models import Expense, ExpenseCategory, Timesheet, TimesheetRate, Vendor
from django import forms
from django.utils import timezone
from django.contrib.admin.widgets import AdminDateWidget, AdminSplitDateTime
from import_export import resources
from import_export.admin import ImportExportModelAdmin
from admincharts.admin import AdminChartMixin
from admincharts.utils import months_between_dates
from djmoney.contrib.exchange.models import convert_money
class VendorAdminForm(DeletableAdminForm):
class Meta:
model = Vendor
fields = "__all__"
@admin.register(Vendor)
class VendorAdmin(AdminBase):
list_display = (
"name",
"description",
)
form = VendorAdminForm
class TimesheetRateAdminForm(DeletableAdminForm):
class Meta:
model = TimesheetRate
fields = "__all__"
@admin.register(TimesheetRate)
class TimesheetRateAdmin(AdminBase):
list_display = (
"rate",
"description",
"default_rate",
)
form = TimesheetRateAdminForm
class TimesheetResource(resources.ModelResource):
class Meta:
model = Timesheet
class TimesheetAdminForm(DeletableAdminForm):
class Meta:
model = Timesheet
fields = "__all__"
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if not self.instance.shift_start:
self.initial["shift_start"] = timezone.now()
self.fields["rate"].initial = forms.ModelChoiceField(
queryset=TimesheetRate.objects.all(),
required=True,
widget=forms.Select,
label="Select Another Table",
)
self.initial["rate"] = TimesheetRate.objects.filter(default_rate=True).first()
@admin.register(Timesheet)
class TimesheetAdmin(AdminBase, AdminChartMixin, ImportExportModelAdmin):
resource_classes = [TimesheetResource]
form = TimesheetAdminForm
fields = (
("shift_start", "shift_end"),
("break_start", "break_end"),
("rate"),
("submitted"),
)
list_display = (
"shift_start",
"worked_hours",
"worked_decimal",
"total",
"_rules",
"submitted",
"rate",
# "shift_hours",
# "break_hours",
)
list_filter = ("shift_start", ("deleted", DeletedListFilter))
ordering = ("-shift_start", "-deleted")
readonly_fields = ("deleted",)
def submit(self, request, queryset):
if queryset.exists():
for item in queryset:
if not item.submitted:
item.submitted = timezone.now()
item.save()
def changelist_view(self, request, extra_context=None):
payday = next_payday()
extra_context = {
"title": f"Next timesheet due {payday.strftime('%B %-d')} (in {(payday - datetime.now()).days + 1} days)."
}
return super().changelist_view(request, extra_context=extra_context)
submit.short_description = "Mark as submitted"
actions = [submit, AdminBase.delete_selected, AdminBase.restore_deleted]
def get_list_chart_data(self, queryset):
if not queryset:
return {}
# Cannot reorder the queryset at this point
earliest = min([x.shift_start for x in queryset]).replace(day=1)
expenses_in_range = Expense.objects.filter(
date__range=[earliest, timezone.now()], deleted=False
)
labels = []
totals = []
expenses_total = []
for b in months_between_dates(earliest, timezone.now()):
labels.append(b.strftime("%b %Y"))
totals.append(
sum(
[
convert_money(x.total, "AUD").amount
for x in queryset
if x.shift_start.year == b.year
and x.shift_start.month == b.month
]
)
)
expenses_total.append(
sum(
[
convert_money(x.price, "AUD").amount
for x in expenses_in_range
if x.date.year == b.year and x.date.month == b.month
]
)
)
return {
"labels": labels,
"datasets": [
{
"label": "Income (Pre-tax)",
"data": totals,
"backgroundColor": "#79aec8",
},
{
"label": "Expenditure",
"data": expenses_total,
"backgroundColor": "#865137",
},
],
}
class ExpenseAdminForm(DeletableAdminForm):
class Meta:
model = Expense
fields = "__all__"
@admin.register(Expense)
class ExpenseAdmin(AdminBase, AdminChartMixin, ImportExportModelAdmin):
form = ExpenseAdminForm
list_display = ("date", "price", "description", "category", "vendor")
list_filter = ("date", ("deleted", DeletedListFilter), "category", "vendor")
ordering = ("-date", "-deleted")
readonly_fields = ("deleted",)
def get_changeform_initial_data(self, request):
initial = super().get_changeform_initial_data(request)
initial["date"] = timezone.now().date()
return initial
list_chart_type = "bar"
list_chart_data = {}
list_chart_options = {"aspectRatio": 6}
list_chart_config = None # Override the combined settings
def get_list_chart_data(self, queryset):
if not queryset:
return {}
# Cannot reorder the queryset at this point
earliest = min([x.date for x in queryset]).replace(day=1)
timesheets = Timesheet.objects.filter(
shift_start__range=[earliest, timezone.now()]
)
labels = []
totals = []
expenses_total = []
expenses = {
k: {
"label": k,
"data": [],
"backgroundColor": f"#{random.Random(x=1).randrange(0x1000000):06x}",
}
for k in set([x.category.name for x in queryset])
}
for b in months_between_dates(earliest, timezone.now().date()):
labels.append(b.strftime("%b %Y"))
totals.append(
sum(
[
convert_money(x.total, "AUD").amount
for x in timesheets
if x.shift_start.year == b.year
and x.shift_start.month == b.month
]
)
)
expenses_total.append(0)
for k in expenses.keys():
expenses[k]["data"].append(0)
for x in queryset:
if x.date.year == b.year and x.date.month == b.month:
expenses_total[-1] += convert_money(x.price, "AUD").amount
expenses[x.category.name]["data"][-1] += convert_money(
x.price, "AUD"
).amount
return {
"labels": labels,
"datasets": [
{
"label": "Income (Pre-tax)",
"data": totals,
"backgroundColor": "#79aec8",
},
{
"label": "Expenditure",
"data": expenses_total,
"backgroundColor": "#865137",
},
]
+ list(expenses.values()),
}
@admin.register(ExpenseCategory)
class ExpenseCategoryAdmin(AdminBase):
pass