From 0a5c93a3c4eca52987f407b52ae5afd7bbf3bbf0 Mon Sep 17 00:00:00 2001 From: Peter Date: Tue, 3 Oct 2023 21:59:00 +0800 Subject: [PATCH] Redirect / -> /admin/, better random color function --- .gitignore | 5 ++ budget/urls.py | 5 +- expenses/admin.py | 67 +++++++++++------- ...vendor_remove_expense_merchant_and_more.py | 68 +++++++++++++++++++ expenses/models.py | 16 ++++- requirements.txt | 4 +- util.py | 8 +++ 7 files changed, 143 insertions(+), 30 deletions(-) create mode 100644 expenses/migrations/0004_vendor_remove_expense_merchant_and_more.py diff --git a/.gitignore b/.gitignore index 46e6f80..f1bfebd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,8 @@ +# nah i don't think i want you to have my receipts thanks... +receipts/ +receipts/* + + etc/ test.py localhost_config.py diff --git a/budget/urls.py b/budget/urls.py index 62bfab1..d0e721a 100644 --- a/budget/urls.py +++ b/budget/urls.py @@ -17,11 +17,12 @@ Including another URLconf from django.conf import settings from django.conf.urls.static import static from django.contrib import admin -from django.urls import include, path +from django.urls import include, path, reverse_lazy from expenses.models import Expense, Timesheet - +from django.views.generic.base import RedirectView urlpatterns = [ + path("", RedirectView.as_view(url=reverse_lazy("admin:index"))), path("admin/expenses/", include("expenses.admin_urls")), path("admin/summary/", include("summary.urls")), path("admin/", admin.site.urls), diff --git a/expenses/admin.py b/expenses/admin.py index bbf5ff9..690311e 100644 --- a/expenses/admin.py +++ b/expenses/admin.py @@ -1,21 +1,13 @@ -from datetime import datetime, timedelta -import random -from typing import Any, Dict +from datetime import datetime +import distinctipy 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 util import next_payday, rgb_tuple_to_hex from .admin_base import AdminBase, DeletedListFilter, DeletableAdminForm -from .models import Expense, ExpenseCategory, Timesheet, TimesheetRate +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 @@ -23,6 +15,22 @@ 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", + ) + search_fields = ("name",) + form = VendorAdminForm + + class TimesheetRateAdminForm(DeletableAdminForm): class Meta: model = TimesheetRate @@ -169,8 +177,9 @@ class ExpenseAdminForm(DeletableAdminForm): class ExpenseAdmin(AdminBase, AdminChartMixin, ImportExportModelAdmin): form = ExpenseAdminForm - list_display = ("date", "price", "description", "category") - list_filter = ("date", ("deleted", DeletedListFilter), "category") + autocomplete_fields = ("vendor", "category") + list_display = ("date", "price", "description", "category", "vendor") + list_filter = ("date", ("deleted", DeletedListFilter), "category", "vendor") ordering = ("-date", "-deleted") readonly_fields = ("deleted",) @@ -197,14 +206,22 @@ class ExpenseAdmin(AdminBase, AdminChartMixin, ImportExportModelAdmin): 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]) - } + 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( @@ -213,7 +230,7 @@ class ExpenseAdmin(AdminBase, AdminChartMixin, ImportExportModelAdmin): convert_money(x.total, "AUD").amount for x in timesheets if x.shift_start.year == b.year - and x.shift_start.month == b.month + and x.shift_start.month == b.month # noqa ] ) ) @@ -241,10 +258,10 @@ class ExpenseAdmin(AdminBase, AdminChartMixin, ImportExportModelAdmin): "backgroundColor": "#865137", }, ] - + list(expenses.values()), + + list(expenses.values()), # noqa } @admin.register(ExpenseCategory) class ExpenseCategoryAdmin(AdminBase): - pass + search_fields = ("name",) diff --git a/expenses/migrations/0004_vendor_remove_expense_merchant_and_more.py b/expenses/migrations/0004_vendor_remove_expense_merchant_and_more.py new file mode 100644 index 0000000..c97c07e --- /dev/null +++ b/expenses/migrations/0004_vendor_remove_expense_merchant_and_more.py @@ -0,0 +1,68 @@ +# Generated by Django 4.2.5 on 2023-09-21 04:31 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import simple_history.models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('expenses', '0003_remove_expensecategory_default_rate_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='Vendor', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('deleted', models.BooleanField(default=False)), + ('name', models.TextField(blank=True)), + ('description', models.TextField(blank=True)), + ], + options={ + 'abstract': False, + }, + ), + migrations.RemoveField( + model_name='expense', + name='merchant', + ), + migrations.RemoveField( + model_name='historicalexpense', + name='merchant', + ), + migrations.CreateModel( + name='HistoricalVendor', + fields=[ + ('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')), + ('deleted', models.BooleanField(default=False)), + ('name', models.TextField(blank=True)), + ('description', models.TextField(blank=True)), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField(db_index=True)), + ('history_change_reason', models.CharField(max_length=100, null=True)), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'historical vendor', + 'verbose_name_plural': 'historical vendors', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': ('history_date', 'history_id'), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.AddField( + model_name='expense', + name='vendor', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='expenses', to='expenses.vendor'), + ), + migrations.AddField( + model_name='historicalexpense', + name='vendor', + field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='expenses.vendor'), + ), + ] diff --git a/expenses/models.py b/expenses/models.py index 65acb4a..00ae59e 100644 --- a/expenses/models.py +++ b/expenses/models.py @@ -108,6 +108,14 @@ class ExpenseCategory(DeletableModel): return truncate_string(self.name, 48) +class Vendor(DeletableModel): + name = models.TextField(blank=True) + description = models.TextField(blank=True) + + def __str__(self): + return truncate_string(self.name, 48) + + class Expense(DeletableModel): price = MoneyField( decimal_places=3, @@ -117,7 +125,13 @@ class Expense(DeletableModel): ) date = models.DateField() description = models.TextField(blank=True) - merchant = models.CharField(max_length=255, blank=True) + vendor = models.ForeignKey( + Vendor, + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="expenses", + ) receipt = models.FileField(upload_to="receipts/", blank=True) link = models.URLField(blank=True) category = models.ForeignKey( diff --git a/requirements.txt b/requirements.txt index 13379a9..89d17d3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,15 +3,15 @@ Babel==2.12.1 backports.zoneinfo==0.2.1 defusedxml==0.7.1 diff-match-patch==20230430 +distinctipy==1.2.3 Django==4.2.5 django-admincharts==0.4.1 -django-computedfields==0.2.3 -django-fast-update==0.2.3 django-import-export==3.3.1 django-money==3.2.0 django-simple-history==3.4.0 et-xmlfile==1.1.0 MarkupPy==1.14 +numpy==1.24.4 odfpy==1.4.1 openpyxl==3.1.2 pip-autoremove==0.10.0 diff --git a/util.py b/util.py index 43adcdb..93e8760 100644 --- a/util.py +++ b/util.py @@ -33,3 +33,11 @@ def next_payday(day=datetime.now()) -> datetime: next_payday -= timedelta(days=1) return next_payday + + +def rgb_tuple_to_hex(rgb_tuple): + # Ensure the values are within the valid range + color = [min(max(0, x), 1) for x in rgb_tuple] + color = [format(int(x * 255), "02x") for x in color] + rgb_hex = f"#{color[0]}{color[1]}{color[2]}" + return rgb_hex