money-manager/expenses/admin.py

379 lines
10 KiB
Python

from datetime import datetime
import distinctipy
from django.contrib import admin
from util import next_payday, rgb_tuple_to_hex
from .admin_base import AdminBase, DeletedListFilter, DeletableAdminForm
from .models import (
Log,
Expense,
ExpenseCategory,
LogCategory,
Timesheet,
TimesheetRate,
Todo,
Vendor,
)
from django import forms
from django.utils import timezone
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
from django.utils.html import format_html
from django.db.models import F
class TodosAdminForm(DeletableAdminForm):
class Meta:
model = Todo
fields = "__all__"
@admin.register(Todo)
class TodosAdmin(AdminBase):
class Media:
js = ("js/todo.js",)
# TODO: add stats bars for resolved/completed reports
list_display = (
"title",
"due_date",
"priority",
# "bump_priority",
)
search_fields = (
"title",
"due_date",
"priority",
)
ordering = (
F("due_date").asc(nulls_last=True),
"-priority",
)
list_filter = (
"priority",
"due_date",
("deleted", DeletedListFilter),
)
form = TodosAdminForm
# def bump_priority(self, obj):
# return format_html(
# "<button class='button' onclick=`doSomething({})`>+</button>",
# obj.id,
# obj.id,
# )
def changelist_view(self, request, extra_context=None):
extra_context = extra_context or {}
extra_context = {"title": "SHIT TODO"}
return super().changelist_view(request, extra_context=extra_context)
class LogsAdminForm(DeletableAdminForm):
class Meta:
model = Log
fields = "__all__"
@admin.register(Log)
class LogsAdmin(AdminBase):
class Media:
css = {"all": ("/static/logs.css",)}
def truncated_title(self, instance):
return f"{instance.title[:2]}...{instance.title[-1:]}"
list_display = (
"title",
"truncated_title",
"date",
"goodness_value",
"category",
)
list_display_links = (
"title",
"truncated_title",
)
search_fields = (
"title",
"date",
"category",
)
list_filter = (
"date",
# "category",
)
form = LogsAdminForm
class VendorAdminForm(DeletableAdminForm):
class Meta:
model = Vendor
fields = "__all__"
@admin.register(Vendor)
class VendorAdmin(AdminBase):
list_display = (
"name",
"description",
)
search_fields = ("name",)
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
autocomplete_fields = ("vendor", "category")
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 = []
expense_types = set([x.category.name for x in queryset])
colors = distinctipy.get_colors(len(expense_types), pastel_factor=0.2, rng=69)
expenses = {}
i = 0
for k in expense_types:
expenses.update(
{
k: {
"label": k,
"data": [],
"backgroundColor": rgb_tuple_to_hex(colors[i]),
}
}
)
i += 1
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 # noqa
]
)
)
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()), # noqa
}
@admin.register(ExpenseCategory)
class ExpenseCategoryAdmin(AdminBase):
list_display = (
"name",
"description",
)
search_fields = list_display
@admin.register(LogCategory)
class LogCategoryAdmin(AdminBase):
list_display = (
"name",
"description",
)
search_fields = list_display
class Media:
css = {"all": ("/static/logs.css",)}