From a2a387036957c7c60dca6ac58b669dd1bdbbf020 Mon Sep 17 00:00:00 2001 From: Peter Date: Sun, 17 Sep 2023 16:54:34 +0800 Subject: [PATCH] basic --- .gitignore | 164 +++++++++++++++++ admin.py | 3 + budget/__init__.py | 0 budget/asgi.py | 16 ++ budget/settings.py | 141 +++++++++++++++ budget/urls.py | 29 +++ budget/wsgi.py | 16 ++ expenses/__init__.py | 0 expenses/admin.py | 165 ++++++++++++++++++ expenses/admin_base.py | 103 +++++++++++ expenses/admin_urls.py | 12 ++ expenses/apps.py | 6 + expenses/deletable_model.py | 13 ++ expenses/migrations/0001_initial.py | 141 +++++++++++++++ ...gory_historicalexpensecategory_and_more.py | 62 +++++++ expenses/migrations/__init__.py | 0 expenses/models.py | 133 ++++++++++++++ .../templates/admin/expense_changelist.html | 9 + .../templates/admin/expenses/change_form.html | 27 +++ .../templates/admin/expenses/submit_line.html | 23 +++ expenses/tests.py | 3 + expenses/views.py | 17 ++ manage.py | 22 +++ requirements.txt | 21 +++ summary/__init__.py | 0 summary/admin.py | 54 ++++++ summary/apps.py | 6 + summary/migrations/0001_initial.py | 57 ++++++ summary/migrations/__init__.py | 0 summary/models.py | 9 + .../admin/sale_summary_change_list.html | 62 +++++++ summary/templatetags/__init__.py | 0 summary/templatetags/month_name.py | 13 ++ summary/tests.py | 3 + summary/urls.py | 6 + summary/views.py | 32 ++++ util.py | 35 ++++ 37 files changed, 1403 insertions(+) create mode 100644 .gitignore create mode 100644 admin.py create mode 100644 budget/__init__.py create mode 100644 budget/asgi.py create mode 100644 budget/settings.py create mode 100644 budget/urls.py create mode 100644 budget/wsgi.py create mode 100644 expenses/__init__.py create mode 100644 expenses/admin.py create mode 100644 expenses/admin_base.py create mode 100644 expenses/admin_urls.py create mode 100644 expenses/apps.py create mode 100644 expenses/deletable_model.py create mode 100644 expenses/migrations/0001_initial.py create mode 100644 expenses/migrations/0002_expensecategory_historicalexpensecategory_and_more.py create mode 100644 expenses/migrations/__init__.py create mode 100644 expenses/models.py create mode 100644 expenses/templates/admin/expense_changelist.html create mode 100644 expenses/templates/admin/expenses/change_form.html create mode 100644 expenses/templates/admin/expenses/submit_line.html create mode 100644 expenses/tests.py create mode 100644 expenses/views.py create mode 100755 manage.py create mode 100644 requirements.txt create mode 100644 summary/__init__.py create mode 100644 summary/admin.py create mode 100644 summary/apps.py create mode 100644 summary/migrations/0001_initial.py create mode 100644 summary/migrations/__init__.py create mode 100644 summary/models.py create mode 100644 summary/templates/admin/sale_summary_change_list.html create mode 100644 summary/templatetags/__init__.py create mode 100644 summary/templatetags/month_name.py create mode 100644 summary/tests.py create mode 100644 summary/urls.py create mode 100644 summary/views.py create mode 100644 util.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6178c5f --- /dev/null +++ b/.gitignore @@ -0,0 +1,164 @@ +etc/ +test.py + + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/admin.py b/admin.py new file mode 100644 index 0000000..70183fe --- /dev/null +++ b/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +admin.site.site_header = "Your Custom Admin Title" diff --git a/budget/__init__.py b/budget/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/budget/asgi.py b/budget/asgi.py new file mode 100644 index 0000000..fb90256 --- /dev/null +++ b/budget/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for budget project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'budget.settings') + +application = get_asgi_application() diff --git a/budget/settings.py b/budget/settings.py new file mode 100644 index 0000000..fcd60b3 --- /dev/null +++ b/budget/settings.py @@ -0,0 +1,141 @@ +""" +Django settings for budget project. + +Generated by 'django-admin startproject' using Django 4.2.5. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/4.2/ref/settings/ +""" + +from import_export.formats.base_formats import CSV, XLSX +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = "django-insecure-=-nu_j+*8_eem=++@3@jw^5nz4!s0%-$y_#p)!wwojce4^*b@!" + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + "admin_tools", + "admin_tools.theming", + "admin_tools.menu", + "admin_tools.dashboard", + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "django.contrib.humanize", + "djmoney", + "expenses", + "summary", + "simple_history", + "import_export", + "admincharts", + "djmoney.contrib.exchange", +] + +IMPORT_FORMATS = [CSV, XLSX] +EXPORT_FORMATS = [CSV, XLSX] + +MIDDLEWARE = [ + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", + "simple_history.middleware.HistoryRequestMiddleware", +] + +ROOT_URLCONF = "budget.urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] + +WSGI_APPLICATION = "budget.wsgi.application" + + +# Database +# https://docs.djangoproject.com/en/4.2/ref/settings/#databases + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", + } +} + + +# Password validation +# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/4.2/topics/i18n/ + +LANGUAGE_CODE = "en-us" + +TIME_ZONE = "Australia/West" +USE_TZ = True + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/4.2/howto/static-files/ + +STATIC_URL = "static/" + +# Default primary key field type +# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" diff --git a/budget/urls.py b/budget/urls.py new file mode 100644 index 0000000..45e76cd --- /dev/null +++ b/budget/urls.py @@ -0,0 +1,29 @@ +""" +URL configuration for budget project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/4.2/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import include, path + + +urlpatterns = [ + path("admin/expenses/", include("expenses.admin_urls")), + path("admin/summary/", include("summary.urls")), + path("admin/", admin.site.urls), +] + +admin.site.site_header = "Better Admin" +admin.site.index_title = "Better Admin" +admin.site.site_title = "" diff --git a/budget/wsgi.py b/budget/wsgi.py new file mode 100644 index 0000000..0627587 --- /dev/null +++ b/budget/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for budget project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'budget.settings') + +application = get_wsgi_application() diff --git a/expenses/__init__.py b/expenses/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/expenses/admin.py b/expenses/admin.py new file mode 100644 index 0000000..02940a2 --- /dev/null +++ b/expenses/admin.py @@ -0,0 +1,165 @@ +from datetime import datetime +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, Timesheet, TimesheetRate +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 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]) + + labels = [] + totals = [] + 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 + ] + ) + ) + + return { + "labels": labels, + "datasets": [ + { + "label": "Income (Pre-tax)", + "data": totals, + "backgroundColor": "#79aec8", + }, + ], + } + + +class ExpenseAdminForm(DeletableAdminForm): + class Meta: + model = Expense + fields = "__all__" + + +@admin.register(Expense) +class ExpenseAdmin(AdminBase, AdminChartMixin, ImportExportModelAdmin): + form = ExpenseAdminForm + + list_display = ("date", "price", "description") + list_filter = ("date", ("deleted", DeletedListFilter)) + 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 diff --git a/expenses/admin_base.py b/expenses/admin_base.py new file mode 100644 index 0000000..0a5381d --- /dev/null +++ b/expenses/admin_base.py @@ -0,0 +1,103 @@ +from simple_history.admin import SimpleHistoryAdmin +from .deletable_model import DeletableModel +from django.utils.html import format_html +from django.contrib import admin +from django import forms + + +class DeletedListFilter(admin.BooleanFieldListFilter): + default = "0" + + def __init__(self, field, request, params, model, model_admin, field_path): + super().__init__(field, request, params, model, model_admin, field_path) + if not self.lookup_val: + self.lookup_val = self.default + self.used_parameters[self.lookup_kwarg] = self.default + + def choices(self, changelist): + choices = super().choices(changelist) + choices.__next__() + for choice in choices: + yield choice + + +class DeletableAdminForm(forms.ModelForm): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if self.instance and self.instance.deleted: + # If the object is marked as deleted, add your custom action button + self.fields["restore_deleted"] = forms.BooleanField( + widget=forms.CheckboxInput(attrs={"class": "hidden"}), + required=False, + initial=True, + label="Restore This Item", + ) + + +class AdminBase(SimpleHistoryAdmin): + readonly_fields = ("deleted",) + + # Override the delete_view method to suppress the confirmation page + def delete_view(self, request, object_id, extra_context=None): + obj: DeletableModel = self.get_object(request, object_id) + if obj: + obj.mark_deleted() + obj.save() + return self.response_delete(request, obj_display=str(obj), obj_id=obj.pk) + + # Override delete button + def delete_model(self, request, obj: DeletableModel): + if obj: + obj.mark_deleted() + obj.save() + + def delete_selected(modeladmin, request, queryset): + # Show confirmation page. + for obj in queryset: + if obj: + obj.mark_deleted() + obj.save() + + def restore_deleted(self, request, queryset): + # Check if the selected items are deleted + deleted_items = queryset.filter(deleted=True) + + # Perform the action only if there are deleted items + if deleted_items.exists(): + # Your custom logic here + for item in deleted_items: + item.deleted = False + item.save() + + restore_deleted.short_description = "Restore Selected Items" # Action button text + + # Add your custom action to the actions list + actions = [delete_selected, restore_deleted] + + """ + HISTORY + https://stackoverflow.com/a/72187314 + """ + history_list_display = ["list_changes"] + + def changed_fields(self, obj): + if obj.prev_record: + delta = obj.diff_against(obj.prev_record) + return delta.changed_fields + return None + + def list_changes(self, obj): + fields = "" + if obj.prev_record: + delta = obj.diff_against(obj.prev_record) + + for change in delta.changes: + fields += str( + ( + f"{change.field} " + f"changed from {change.old} " + f"to {change.new} .
" + ) + ) + return format_html(fields) + return None diff --git a/expenses/admin_urls.py b/expenses/admin_urls.py new file mode 100644 index 0000000..c46e86d --- /dev/null +++ b/expenses/admin_urls.py @@ -0,0 +1,12 @@ +from django.urls import path +from . import views + +app_name = "expenses" + +urlpatterns = [ + path( + "expense//restore/", + views.restore_item, + name="expenses_restore_item", + ), +] diff --git a/expenses/apps.py b/expenses/apps.py new file mode 100644 index 0000000..3de36fa --- /dev/null +++ b/expenses/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ExpensesConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "expenses" diff --git a/expenses/deletable_model.py b/expenses/deletable_model.py new file mode 100644 index 0000000..3e22ff0 --- /dev/null +++ b/expenses/deletable_model.py @@ -0,0 +1,13 @@ +from django.db import models +from simple_history.models import HistoricalRecords + + +class DeletableModel(models.Model): + class Meta: + abstract = True + + history = HistoricalRecords(inherit=True) + deleted = models.BooleanField(default=False) + + def mark_deleted(self): + self.deleted = True diff --git a/expenses/migrations/0001_initial.py b/expenses/migrations/0001_initial.py new file mode 100644 index 0000000..a3a6189 --- /dev/null +++ b/expenses/migrations/0001_initial.py @@ -0,0 +1,141 @@ +# Generated by Django 4.2.5 on 2023-09-16 17:11 + +from decimal import Decimal +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import djmoney.models.fields +import simple_history.models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Expense', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('deleted', models.BooleanField(default=False)), + ('price_currency', djmoney.models.fields.CurrencyField(choices=[('XUA', 'ADB Unit of Account'), ('AFN', 'Afghan Afghani'), ('AFA', 'Afghan Afghani (1927–2002)'), ('ALL', 'Albanian Lek'), ('ALK', 'Albanian Lek (1946–1965)'), ('DZD', 'Algerian Dinar'), ('ADP', 'Andorran Peseta'), ('AOA', 'Angolan Kwanza'), ('AOK', 'Angolan Kwanza (1977–1991)'), ('AON', 'Angolan New Kwanza (1990–2000)'), ('AOR', 'Angolan Readjusted Kwanza (1995–1999)'), ('ARA', 'Argentine Austral'), ('ARS', 'Argentine Peso'), ('ARM', 'Argentine Peso (1881–1970)'), ('ARP', 'Argentine Peso (1983–1985)'), ('ARL', 'Argentine Peso Ley (1970–1983)'), ('AMD', 'Armenian Dram'), ('AWG', 'Aruban Florin'), ('AUD', 'Australian Dollar'), ('ATS', 'Austrian Schilling'), ('AZN', 'Azerbaijani Manat'), ('AZM', 'Azerbaijani Manat (1993–2006)'), ('BSD', 'Bahamian Dollar'), ('BHD', 'Bahraini Dinar'), ('BDT', 'Bangladeshi Taka'), ('BBD', 'Barbadian Dollar'), ('BYN', 'Belarusian Ruble'), ('BYB', 'Belarusian Ruble (1994–1999)'), ('BYR', 'Belarusian Ruble (2000–2016)'), ('BEF', 'Belgian Franc'), ('BEC', 'Belgian Franc (convertible)'), ('BEL', 'Belgian Franc (financial)'), ('BZD', 'Belize Dollar'), ('BMD', 'Bermudan Dollar'), ('BTN', 'Bhutanese Ngultrum'), ('BOB', 'Bolivian Boliviano'), ('BOL', 'Bolivian Boliviano (1863–1963)'), ('BOV', 'Bolivian Mvdol'), ('BOP', 'Bolivian Peso'), ('VED', 'Bolívar Soberano'), ('BAM', 'Bosnia-Herzegovina Convertible Mark'), ('BAD', 'Bosnia-Herzegovina Dinar (1992–1994)'), ('BAN', 'Bosnia-Herzegovina New Dinar (1994–1997)'), ('BWP', 'Botswanan Pula'), ('BRC', 'Brazilian Cruzado (1986–1989)'), ('BRZ', 'Brazilian Cruzeiro (1942–1967)'), ('BRE', 'Brazilian Cruzeiro (1990–1993)'), ('BRR', 'Brazilian Cruzeiro (1993–1994)'), ('BRN', 'Brazilian New Cruzado (1989–1990)'), ('BRB', 'Brazilian New Cruzeiro (1967–1986)'), ('BRL', 'Brazilian Real'), ('GBP', 'British Pound'), ('BND', 'Brunei Dollar'), ('BGL', 'Bulgarian Hard Lev'), ('BGN', 'Bulgarian Lev'), ('BGO', 'Bulgarian Lev (1879–1952)'), ('BGM', 'Bulgarian Socialist Lev'), ('BUK', 'Burmese Kyat'), ('BIF', 'Burundian Franc'), ('XPF', 'CFP Franc'), ('KHR', 'Cambodian Riel'), ('CAD', 'Canadian Dollar'), ('CVE', 'Cape Verdean Escudo'), ('KYD', 'Cayman Islands Dollar'), ('XAF', 'Central African CFA Franc'), ('CLE', 'Chilean Escudo'), ('CLP', 'Chilean Peso'), ('CLF', 'Chilean Unit of Account (UF)'), ('CNX', 'Chinese People’s Bank Dollar'), ('CNY', 'Chinese Yuan'), ('CNH', 'Chinese Yuan (offshore)'), ('COP', 'Colombian Peso'), ('COU', 'Colombian Real Value Unit'), ('KMF', 'Comorian Franc'), ('CDF', 'Congolese Franc'), ('CRC', 'Costa Rican Colón'), ('HRD', 'Croatian Dinar'), ('HRK', 'Croatian Kuna'), ('CUC', 'Cuban Convertible Peso'), ('CUP', 'Cuban Peso'), ('CYP', 'Cypriot Pound'), ('CZK', 'Czech Koruna'), ('CSK', 'Czechoslovak Hard Koruna'), ('DKK', 'Danish Krone'), ('DJF', 'Djiboutian Franc'), ('DOP', 'Dominican Peso'), ('NLG', 'Dutch Guilder'), ('XCD', 'East Caribbean Dollar'), ('DDM', 'East German Mark'), ('ECS', 'Ecuadorian Sucre'), ('ECV', 'Ecuadorian Unit of Constant Value'), ('EGP', 'Egyptian Pound'), ('GQE', 'Equatorial Guinean Ekwele'), ('ERN', 'Eritrean Nakfa'), ('EEK', 'Estonian Kroon'), ('ETB', 'Ethiopian Birr'), ('EUR', 'Euro'), ('XBA', 'European Composite Unit'), ('XEU', 'European Currency Unit'), ('XBB', 'European Monetary Unit'), ('XBC', 'European Unit of Account (XBC)'), ('XBD', 'European Unit of Account (XBD)'), ('FKP', 'Falkland Islands Pound'), ('FJD', 'Fijian Dollar'), ('FIM', 'Finnish Markka'), ('FRF', 'French Franc'), ('XFO', 'French Gold Franc'), ('XFU', 'French UIC-Franc'), ('GMD', 'Gambian Dalasi'), ('GEK', 'Georgian Kupon Larit'), ('GEL', 'Georgian Lari'), ('DEM', 'German Mark'), ('GHS', 'Ghanaian Cedi'), ('GHC', 'Ghanaian Cedi (1979–2007)'), ('GIP', 'Gibraltar Pound'), ('XAU', 'Gold'), ('GRD', 'Greek Drachma'), ('GTQ', 'Guatemalan Quetzal'), ('GWP', 'Guinea-Bissau Peso'), ('GNF', 'Guinean Franc'), ('GNS', 'Guinean Syli'), ('GYD', 'Guyanaese Dollar'), ('HTG', 'Haitian Gourde'), ('HNL', 'Honduran Lempira'), ('HKD', 'Hong Kong Dollar'), ('HUF', 'Hungarian Forint'), ('IMP', 'IMP'), ('ISK', 'Icelandic Króna'), ('ISJ', 'Icelandic Króna (1918–1981)'), ('INR', 'Indian Rupee'), ('IDR', 'Indonesian Rupiah'), ('IRR', 'Iranian Rial'), ('IQD', 'Iraqi Dinar'), ('IEP', 'Irish Pound'), ('ILS', 'Israeli New Shekel'), ('ILP', 'Israeli Pound'), ('ILR', 'Israeli Shekel (1980–1985)'), ('ITL', 'Italian Lira'), ('JMD', 'Jamaican Dollar'), ('JPY', 'Japanese Yen'), ('JOD', 'Jordanian Dinar'), ('KZT', 'Kazakhstani Tenge'), ('KES', 'Kenyan Shilling'), ('KWD', 'Kuwaiti Dinar'), ('KGS', 'Kyrgystani Som'), ('LAK', 'Laotian Kip'), ('LVL', 'Latvian Lats'), ('LVR', 'Latvian Ruble'), ('LBP', 'Lebanese Pound'), ('LSL', 'Lesotho Loti'), ('LRD', 'Liberian Dollar'), ('LYD', 'Libyan Dinar'), ('LTL', 'Lithuanian Litas'), ('LTT', 'Lithuanian Talonas'), ('LUL', 'Luxembourg Financial Franc'), ('LUC', 'Luxembourgian Convertible Franc'), ('LUF', 'Luxembourgian Franc'), ('MOP', 'Macanese Pataca'), ('MKD', 'Macedonian Denar'), ('MKN', 'Macedonian Denar (1992–1993)'), ('MGA', 'Malagasy Ariary'), ('MGF', 'Malagasy Franc'), ('MWK', 'Malawian Kwacha'), ('MYR', 'Malaysian Ringgit'), ('MVR', 'Maldivian Rufiyaa'), ('MVP', 'Maldivian Rupee (1947–1981)'), ('MLF', 'Malian Franc'), ('MTL', 'Maltese Lira'), ('MTP', 'Maltese Pound'), ('MRU', 'Mauritanian Ouguiya'), ('MRO', 'Mauritanian Ouguiya (1973–2017)'), ('MUR', 'Mauritian Rupee'), ('MXV', 'Mexican Investment Unit'), ('MXN', 'Mexican Peso'), ('MXP', 'Mexican Silver Peso (1861–1992)'), ('MDC', 'Moldovan Cupon'), ('MDL', 'Moldovan Leu'), ('MCF', 'Monegasque Franc'), ('MNT', 'Mongolian Tugrik'), ('MAD', 'Moroccan Dirham'), ('MAF', 'Moroccan Franc'), ('MZE', 'Mozambican Escudo'), ('MZN', 'Mozambican Metical'), ('MZM', 'Mozambican Metical (1980–2006)'), ('MMK', 'Myanmar Kyat'), ('NAD', 'Namibian Dollar'), ('NPR', 'Nepalese Rupee'), ('ANG', 'Netherlands Antillean Guilder'), ('TWD', 'New Taiwan Dollar'), ('NZD', 'New Zealand Dollar'), ('NIO', 'Nicaraguan Córdoba'), ('NIC', 'Nicaraguan Córdoba (1988–1991)'), ('NGN', 'Nigerian Naira'), ('KPW', 'North Korean Won'), ('NOK', 'Norwegian Krone'), ('OMR', 'Omani Rial'), ('PKR', 'Pakistani Rupee'), ('XPD', 'Palladium'), ('PAB', 'Panamanian Balboa'), ('PGK', 'Papua New Guinean Kina'), ('PYG', 'Paraguayan Guarani'), ('PEI', 'Peruvian Inti'), ('PEN', 'Peruvian Sol'), ('PES', 'Peruvian Sol (1863–1965)'), ('PHP', 'Philippine Peso'), ('XPT', 'Platinum'), ('PLN', 'Polish Zloty'), ('PLZ', 'Polish Zloty (1950–1995)'), ('PTE', 'Portuguese Escudo'), ('GWE', 'Portuguese Guinea Escudo'), ('QAR', 'Qatari Riyal'), ('XRE', 'RINET Funds'), ('RHD', 'Rhodesian Dollar'), ('RON', 'Romanian Leu'), ('ROL', 'Romanian Leu (1952–2006)'), ('RUB', 'Russian Ruble'), ('RUR', 'Russian Ruble (1991–1998)'), ('RWF', 'Rwandan Franc'), ('SVC', 'Salvadoran Colón'), ('WST', 'Samoan Tala'), ('SAR', 'Saudi Riyal'), ('RSD', 'Serbian Dinar'), ('CSD', 'Serbian Dinar (2002–2006)'), ('SCR', 'Seychellois Rupee'), ('SLE', 'Sierra Leonean Leone'), ('SLL', 'Sierra Leonean Leone (1964—2022)'), ('XAG', 'Silver'), ('SGD', 'Singapore Dollar'), ('SKK', 'Slovak Koruna'), ('SIT', 'Slovenian Tolar'), ('SBD', 'Solomon Islands Dollar'), ('SOS', 'Somali Shilling'), ('ZAR', 'South African Rand'), ('ZAL', 'South African Rand (financial)'), ('KRH', 'South Korean Hwan (1953–1962)'), ('KRW', 'South Korean Won'), ('KRO', 'South Korean Won (1945–1953)'), ('SSP', 'South Sudanese Pound'), ('SUR', 'Soviet Rouble'), ('ESP', 'Spanish Peseta'), ('ESA', 'Spanish Peseta (A account)'), ('ESB', 'Spanish Peseta (convertible account)'), ('XDR', 'Special Drawing Rights'), ('LKR', 'Sri Lankan Rupee'), ('SHP', 'St. Helena Pound'), ('XSU', 'Sucre'), ('SDD', 'Sudanese Dinar (1992–2007)'), ('SDG', 'Sudanese Pound'), ('SDP', 'Sudanese Pound (1957–1998)'), ('SRD', 'Surinamese Dollar'), ('SRG', 'Surinamese Guilder'), ('SZL', 'Swazi Lilangeni'), ('SEK', 'Swedish Krona'), ('CHF', 'Swiss Franc'), ('SYP', 'Syrian Pound'), ('STN', 'São Tomé & Príncipe Dobra'), ('STD', 'São Tomé & Príncipe Dobra (1977–2017)'), ('TVD', 'TVD'), ('TJR', 'Tajikistani Ruble'), ('TJS', 'Tajikistani Somoni'), ('TZS', 'Tanzanian Shilling'), ('XTS', 'Testing Currency Code'), ('THB', 'Thai Baht'), ('XXX', 'The codes assigned for transactions where no currency is involved'), ('TPE', 'Timorese Escudo'), ('TOP', 'Tongan Paʻanga'), ('TTD', 'Trinidad & Tobago Dollar'), ('TND', 'Tunisian Dinar'), ('TRY', 'Turkish Lira'), ('TRL', 'Turkish Lira (1922–2005)'), ('TMT', 'Turkmenistani Manat'), ('TMM', 'Turkmenistani Manat (1993–2009)'), ('USD', 'US Dollar'), ('USN', 'US Dollar (Next day)'), ('USS', 'US Dollar (Same day)'), ('UGX', 'Ugandan Shilling'), ('UGS', 'Ugandan Shilling (1966–1987)'), ('UAH', 'Ukrainian Hryvnia'), ('UAK', 'Ukrainian Karbovanets'), ('AED', 'United Arab Emirates Dirham'), ('UYW', 'Uruguayan Nominal Wage Index Unit'), ('UYU', 'Uruguayan Peso'), ('UYP', 'Uruguayan Peso (1975–1993)'), ('UYI', 'Uruguayan Peso (Indexed Units)'), ('UZS', 'Uzbekistani Som'), ('VUV', 'Vanuatu Vatu'), ('VES', 'Venezuelan Bolívar'), ('VEB', 'Venezuelan Bolívar (1871–2008)'), ('VEF', 'Venezuelan Bolívar (2008–2018)'), ('VND', 'Vietnamese Dong'), ('VNN', 'Vietnamese Dong (1978–1985)'), ('CHE', 'WIR Euro'), ('CHW', 'WIR Franc'), ('XOF', 'West African CFA Franc'), ('YDD', 'Yemeni Dinar'), ('YER', 'Yemeni Rial'), ('YUN', 'Yugoslavian Convertible Dinar (1990–1992)'), ('YUD', 'Yugoslavian Hard Dinar (1966–1990)'), ('YUM', 'Yugoslavian New Dinar (1994–2002)'), ('YUR', 'Yugoslavian Reformed Dinar (1992–1993)'), ('ZWN', 'ZWN'), ('ZRN', 'Zairean New Zaire (1993–1998)'), ('ZRZ', 'Zairean Zaire (1971–1993)'), ('ZMW', 'Zambian Kwacha'), ('ZMK', 'Zambian Kwacha (1968–2012)'), ('ZWD', 'Zimbabwean Dollar (1980–2008)'), ('ZWR', 'Zimbabwean Dollar (2008)'), ('ZWL', 'Zimbabwean Dollar (2009)')], default='AUD', editable=False, max_length=3)), + ('price', djmoney.models.fields.MoneyField(decimal_places=3, default=Decimal('0.0'), default_currency='AUD', max_digits=11)), + ('date', models.DateField()), + ('description', models.TextField(blank=True)), + ('merchant', models.CharField(blank=True, max_length=255)), + ('receipt', models.FileField(blank=True, upload_to='receipts/')), + ('link', models.URLField(blank=True)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='TimesheetRate', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('deleted', models.BooleanField(default=False)), + ('rate_currency', djmoney.models.fields.CurrencyField(choices=[('XUA', 'ADB Unit of Account'), ('AFN', 'Afghan Afghani'), ('AFA', 'Afghan Afghani (1927–2002)'), ('ALL', 'Albanian Lek'), ('ALK', 'Albanian Lek (1946–1965)'), ('DZD', 'Algerian Dinar'), ('ADP', 'Andorran Peseta'), ('AOA', 'Angolan Kwanza'), ('AOK', 'Angolan Kwanza (1977–1991)'), ('AON', 'Angolan New Kwanza (1990–2000)'), ('AOR', 'Angolan Readjusted Kwanza (1995–1999)'), ('ARA', 'Argentine Austral'), ('ARS', 'Argentine Peso'), ('ARM', 'Argentine Peso (1881–1970)'), ('ARP', 'Argentine Peso (1983–1985)'), ('ARL', 'Argentine Peso Ley (1970–1983)'), ('AMD', 'Armenian Dram'), ('AWG', 'Aruban Florin'), ('AUD', 'Australian Dollar'), ('ATS', 'Austrian Schilling'), ('AZN', 'Azerbaijani Manat'), ('AZM', 'Azerbaijani Manat (1993–2006)'), ('BSD', 'Bahamian Dollar'), ('BHD', 'Bahraini Dinar'), ('BDT', 'Bangladeshi Taka'), ('BBD', 'Barbadian Dollar'), ('BYN', 'Belarusian Ruble'), ('BYB', 'Belarusian Ruble (1994–1999)'), ('BYR', 'Belarusian Ruble (2000–2016)'), ('BEF', 'Belgian Franc'), ('BEC', 'Belgian Franc (convertible)'), ('BEL', 'Belgian Franc (financial)'), ('BZD', 'Belize Dollar'), ('BMD', 'Bermudan Dollar'), ('BTN', 'Bhutanese Ngultrum'), ('BOB', 'Bolivian Boliviano'), ('BOL', 'Bolivian Boliviano (1863–1963)'), ('BOV', 'Bolivian Mvdol'), ('BOP', 'Bolivian Peso'), ('VED', 'Bolívar Soberano'), ('BAM', 'Bosnia-Herzegovina Convertible Mark'), ('BAD', 'Bosnia-Herzegovina Dinar (1992–1994)'), ('BAN', 'Bosnia-Herzegovina New Dinar (1994–1997)'), ('BWP', 'Botswanan Pula'), ('BRC', 'Brazilian Cruzado (1986–1989)'), ('BRZ', 'Brazilian Cruzeiro (1942–1967)'), ('BRE', 'Brazilian Cruzeiro (1990–1993)'), ('BRR', 'Brazilian Cruzeiro (1993–1994)'), ('BRN', 'Brazilian New Cruzado (1989–1990)'), ('BRB', 'Brazilian New Cruzeiro (1967–1986)'), ('BRL', 'Brazilian Real'), ('GBP', 'British Pound'), ('BND', 'Brunei Dollar'), ('BGL', 'Bulgarian Hard Lev'), ('BGN', 'Bulgarian Lev'), ('BGO', 'Bulgarian Lev (1879–1952)'), ('BGM', 'Bulgarian Socialist Lev'), ('BUK', 'Burmese Kyat'), ('BIF', 'Burundian Franc'), ('XPF', 'CFP Franc'), ('KHR', 'Cambodian Riel'), ('CAD', 'Canadian Dollar'), ('CVE', 'Cape Verdean Escudo'), ('KYD', 'Cayman Islands Dollar'), ('XAF', 'Central African CFA Franc'), ('CLE', 'Chilean Escudo'), ('CLP', 'Chilean Peso'), ('CLF', 'Chilean Unit of Account (UF)'), ('CNX', 'Chinese People’s Bank Dollar'), ('CNY', 'Chinese Yuan'), ('CNH', 'Chinese Yuan (offshore)'), ('COP', 'Colombian Peso'), ('COU', 'Colombian Real Value Unit'), ('KMF', 'Comorian Franc'), ('CDF', 'Congolese Franc'), ('CRC', 'Costa Rican Colón'), ('HRD', 'Croatian Dinar'), ('HRK', 'Croatian Kuna'), ('CUC', 'Cuban Convertible Peso'), ('CUP', 'Cuban Peso'), ('CYP', 'Cypriot Pound'), ('CZK', 'Czech Koruna'), ('CSK', 'Czechoslovak Hard Koruna'), ('DKK', 'Danish Krone'), ('DJF', 'Djiboutian Franc'), ('DOP', 'Dominican Peso'), ('NLG', 'Dutch Guilder'), ('XCD', 'East Caribbean Dollar'), ('DDM', 'East German Mark'), ('ECS', 'Ecuadorian Sucre'), ('ECV', 'Ecuadorian Unit of Constant Value'), ('EGP', 'Egyptian Pound'), ('GQE', 'Equatorial Guinean Ekwele'), ('ERN', 'Eritrean Nakfa'), ('EEK', 'Estonian Kroon'), ('ETB', 'Ethiopian Birr'), ('EUR', 'Euro'), ('XBA', 'European Composite Unit'), ('XEU', 'European Currency Unit'), ('XBB', 'European Monetary Unit'), ('XBC', 'European Unit of Account (XBC)'), ('XBD', 'European Unit of Account (XBD)'), ('FKP', 'Falkland Islands Pound'), ('FJD', 'Fijian Dollar'), ('FIM', 'Finnish Markka'), ('FRF', 'French Franc'), ('XFO', 'French Gold Franc'), ('XFU', 'French UIC-Franc'), ('GMD', 'Gambian Dalasi'), ('GEK', 'Georgian Kupon Larit'), ('GEL', 'Georgian Lari'), ('DEM', 'German Mark'), ('GHS', 'Ghanaian Cedi'), ('GHC', 'Ghanaian Cedi (1979–2007)'), ('GIP', 'Gibraltar Pound'), ('XAU', 'Gold'), ('GRD', 'Greek Drachma'), ('GTQ', 'Guatemalan Quetzal'), ('GWP', 'Guinea-Bissau Peso'), ('GNF', 'Guinean Franc'), ('GNS', 'Guinean Syli'), ('GYD', 'Guyanaese Dollar'), ('HTG', 'Haitian Gourde'), ('HNL', 'Honduran Lempira'), ('HKD', 'Hong Kong Dollar'), ('HUF', 'Hungarian Forint'), ('IMP', 'IMP'), ('ISK', 'Icelandic Króna'), ('ISJ', 'Icelandic Króna (1918–1981)'), ('INR', 'Indian Rupee'), ('IDR', 'Indonesian Rupiah'), ('IRR', 'Iranian Rial'), ('IQD', 'Iraqi Dinar'), ('IEP', 'Irish Pound'), ('ILS', 'Israeli New Shekel'), ('ILP', 'Israeli Pound'), ('ILR', 'Israeli Shekel (1980–1985)'), ('ITL', 'Italian Lira'), ('JMD', 'Jamaican Dollar'), ('JPY', 'Japanese Yen'), ('JOD', 'Jordanian Dinar'), ('KZT', 'Kazakhstani Tenge'), ('KES', 'Kenyan Shilling'), ('KWD', 'Kuwaiti Dinar'), ('KGS', 'Kyrgystani Som'), ('LAK', 'Laotian Kip'), ('LVL', 'Latvian Lats'), ('LVR', 'Latvian Ruble'), ('LBP', 'Lebanese Pound'), ('LSL', 'Lesotho Loti'), ('LRD', 'Liberian Dollar'), ('LYD', 'Libyan Dinar'), ('LTL', 'Lithuanian Litas'), ('LTT', 'Lithuanian Talonas'), ('LUL', 'Luxembourg Financial Franc'), ('LUC', 'Luxembourgian Convertible Franc'), ('LUF', 'Luxembourgian Franc'), ('MOP', 'Macanese Pataca'), ('MKD', 'Macedonian Denar'), ('MKN', 'Macedonian Denar (1992–1993)'), ('MGA', 'Malagasy Ariary'), ('MGF', 'Malagasy Franc'), ('MWK', 'Malawian Kwacha'), ('MYR', 'Malaysian Ringgit'), ('MVR', 'Maldivian Rufiyaa'), ('MVP', 'Maldivian Rupee (1947–1981)'), ('MLF', 'Malian Franc'), ('MTL', 'Maltese Lira'), ('MTP', 'Maltese Pound'), ('MRU', 'Mauritanian Ouguiya'), ('MRO', 'Mauritanian Ouguiya (1973–2017)'), ('MUR', 'Mauritian Rupee'), ('MXV', 'Mexican Investment Unit'), ('MXN', 'Mexican Peso'), ('MXP', 'Mexican Silver Peso (1861–1992)'), ('MDC', 'Moldovan Cupon'), ('MDL', 'Moldovan Leu'), ('MCF', 'Monegasque Franc'), ('MNT', 'Mongolian Tugrik'), ('MAD', 'Moroccan Dirham'), ('MAF', 'Moroccan Franc'), ('MZE', 'Mozambican Escudo'), ('MZN', 'Mozambican Metical'), ('MZM', 'Mozambican Metical (1980–2006)'), ('MMK', 'Myanmar Kyat'), ('NAD', 'Namibian Dollar'), ('NPR', 'Nepalese Rupee'), ('ANG', 'Netherlands Antillean Guilder'), ('TWD', 'New Taiwan Dollar'), ('NZD', 'New Zealand Dollar'), ('NIO', 'Nicaraguan Córdoba'), ('NIC', 'Nicaraguan Córdoba (1988–1991)'), ('NGN', 'Nigerian Naira'), ('KPW', 'North Korean Won'), ('NOK', 'Norwegian Krone'), ('OMR', 'Omani Rial'), ('PKR', 'Pakistani Rupee'), ('XPD', 'Palladium'), ('PAB', 'Panamanian Balboa'), ('PGK', 'Papua New Guinean Kina'), ('PYG', 'Paraguayan Guarani'), ('PEI', 'Peruvian Inti'), ('PEN', 'Peruvian Sol'), ('PES', 'Peruvian Sol (1863–1965)'), ('PHP', 'Philippine Peso'), ('XPT', 'Platinum'), ('PLN', 'Polish Zloty'), ('PLZ', 'Polish Zloty (1950–1995)'), ('PTE', 'Portuguese Escudo'), ('GWE', 'Portuguese Guinea Escudo'), ('QAR', 'Qatari Riyal'), ('XRE', 'RINET Funds'), ('RHD', 'Rhodesian Dollar'), ('RON', 'Romanian Leu'), ('ROL', 'Romanian Leu (1952–2006)'), ('RUB', 'Russian Ruble'), ('RUR', 'Russian Ruble (1991–1998)'), ('RWF', 'Rwandan Franc'), ('SVC', 'Salvadoran Colón'), ('WST', 'Samoan Tala'), ('SAR', 'Saudi Riyal'), ('RSD', 'Serbian Dinar'), ('CSD', 'Serbian Dinar (2002–2006)'), ('SCR', 'Seychellois Rupee'), ('SLE', 'Sierra Leonean Leone'), ('SLL', 'Sierra Leonean Leone (1964—2022)'), ('XAG', 'Silver'), ('SGD', 'Singapore Dollar'), ('SKK', 'Slovak Koruna'), ('SIT', 'Slovenian Tolar'), ('SBD', 'Solomon Islands Dollar'), ('SOS', 'Somali Shilling'), ('ZAR', 'South African Rand'), ('ZAL', 'South African Rand (financial)'), ('KRH', 'South Korean Hwan (1953–1962)'), ('KRW', 'South Korean Won'), ('KRO', 'South Korean Won (1945–1953)'), ('SSP', 'South Sudanese Pound'), ('SUR', 'Soviet Rouble'), ('ESP', 'Spanish Peseta'), ('ESA', 'Spanish Peseta (A account)'), ('ESB', 'Spanish Peseta (convertible account)'), ('XDR', 'Special Drawing Rights'), ('LKR', 'Sri Lankan Rupee'), ('SHP', 'St. Helena Pound'), ('XSU', 'Sucre'), ('SDD', 'Sudanese Dinar (1992–2007)'), ('SDG', 'Sudanese Pound'), ('SDP', 'Sudanese Pound (1957–1998)'), ('SRD', 'Surinamese Dollar'), ('SRG', 'Surinamese Guilder'), ('SZL', 'Swazi Lilangeni'), ('SEK', 'Swedish Krona'), ('CHF', 'Swiss Franc'), ('SYP', 'Syrian Pound'), ('STN', 'São Tomé & Príncipe Dobra'), ('STD', 'São Tomé & Príncipe Dobra (1977–2017)'), ('TVD', 'TVD'), ('TJR', 'Tajikistani Ruble'), ('TJS', 'Tajikistani Somoni'), ('TZS', 'Tanzanian Shilling'), ('XTS', 'Testing Currency Code'), ('THB', 'Thai Baht'), ('XXX', 'The codes assigned for transactions where no currency is involved'), ('TPE', 'Timorese Escudo'), ('TOP', 'Tongan Paʻanga'), ('TTD', 'Trinidad & Tobago Dollar'), ('TND', 'Tunisian Dinar'), ('TRY', 'Turkish Lira'), ('TRL', 'Turkish Lira (1922–2005)'), ('TMT', 'Turkmenistani Manat'), ('TMM', 'Turkmenistani Manat (1993–2009)'), ('USD', 'US Dollar'), ('USN', 'US Dollar (Next day)'), ('USS', 'US Dollar (Same day)'), ('UGX', 'Ugandan Shilling'), ('UGS', 'Ugandan Shilling (1966–1987)'), ('UAH', 'Ukrainian Hryvnia'), ('UAK', 'Ukrainian Karbovanets'), ('AED', 'United Arab Emirates Dirham'), ('UYW', 'Uruguayan Nominal Wage Index Unit'), ('UYU', 'Uruguayan Peso'), ('UYP', 'Uruguayan Peso (1975–1993)'), ('UYI', 'Uruguayan Peso (Indexed Units)'), ('UZS', 'Uzbekistani Som'), ('VUV', 'Vanuatu Vatu'), ('VES', 'Venezuelan Bolívar'), ('VEB', 'Venezuelan Bolívar (1871–2008)'), ('VEF', 'Venezuelan Bolívar (2008–2018)'), ('VND', 'Vietnamese Dong'), ('VNN', 'Vietnamese Dong (1978–1985)'), ('CHE', 'WIR Euro'), ('CHW', 'WIR Franc'), ('XOF', 'West African CFA Franc'), ('YDD', 'Yemeni Dinar'), ('YER', 'Yemeni Rial'), ('YUN', 'Yugoslavian Convertible Dinar (1990–1992)'), ('YUD', 'Yugoslavian Hard Dinar (1966–1990)'), ('YUM', 'Yugoslavian New Dinar (1994–2002)'), ('YUR', 'Yugoslavian Reformed Dinar (1992–1993)'), ('ZWN', 'ZWN'), ('ZRN', 'Zairean New Zaire (1993–1998)'), ('ZRZ', 'Zairean Zaire (1971–1993)'), ('ZMW', 'Zambian Kwacha'), ('ZMK', 'Zambian Kwacha (1968–2012)'), ('ZWD', 'Zimbabwean Dollar (1980–2008)'), ('ZWR', 'Zimbabwean Dollar (2008)'), ('ZWL', 'Zimbabwean Dollar (2009)')], default='AUD', editable=False, max_length=3)), + ('rate', djmoney.models.fields.MoneyField(decimal_places=3, default=Decimal('0.0'), default_currency='AUD', max_digits=11)), + ('description', models.TextField(blank=True)), + ('default_rate', models.BooleanField(default=False)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Timesheet', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('deleted', models.BooleanField(default=False)), + ('shift_start', models.DateTimeField(blank=True, null=True)), + ('shift_end', models.DateTimeField(blank=True, null=True)), + ('break_start', models.DateTimeField(blank=True, null=True)), + ('break_end', models.DateTimeField(blank=True, null=True)), + ('submitted', models.DateTimeField(blank=True, null=True)), + ('rate', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='timesheets', to='expenses.timesheetrate')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='HistoricalTimesheetRate', + fields=[ + ('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')), + ('deleted', models.BooleanField(default=False)), + ('rate_currency', djmoney.models.fields.CurrencyField(choices=[('XUA', 'ADB Unit of Account'), ('AFN', 'Afghan Afghani'), ('AFA', 'Afghan Afghani (1927–2002)'), ('ALL', 'Albanian Lek'), ('ALK', 'Albanian Lek (1946–1965)'), ('DZD', 'Algerian Dinar'), ('ADP', 'Andorran Peseta'), ('AOA', 'Angolan Kwanza'), ('AOK', 'Angolan Kwanza (1977–1991)'), ('AON', 'Angolan New Kwanza (1990–2000)'), ('AOR', 'Angolan Readjusted Kwanza (1995–1999)'), ('ARA', 'Argentine Austral'), ('ARS', 'Argentine Peso'), ('ARM', 'Argentine Peso (1881–1970)'), ('ARP', 'Argentine Peso (1983–1985)'), ('ARL', 'Argentine Peso Ley (1970–1983)'), ('AMD', 'Armenian Dram'), ('AWG', 'Aruban Florin'), ('AUD', 'Australian Dollar'), ('ATS', 'Austrian Schilling'), ('AZN', 'Azerbaijani Manat'), ('AZM', 'Azerbaijani Manat (1993–2006)'), ('BSD', 'Bahamian Dollar'), ('BHD', 'Bahraini Dinar'), ('BDT', 'Bangladeshi Taka'), ('BBD', 'Barbadian Dollar'), ('BYN', 'Belarusian Ruble'), ('BYB', 'Belarusian Ruble (1994–1999)'), ('BYR', 'Belarusian Ruble (2000–2016)'), ('BEF', 'Belgian Franc'), ('BEC', 'Belgian Franc (convertible)'), ('BEL', 'Belgian Franc (financial)'), ('BZD', 'Belize Dollar'), ('BMD', 'Bermudan Dollar'), ('BTN', 'Bhutanese Ngultrum'), ('BOB', 'Bolivian Boliviano'), ('BOL', 'Bolivian Boliviano (1863–1963)'), ('BOV', 'Bolivian Mvdol'), ('BOP', 'Bolivian Peso'), ('VED', 'Bolívar Soberano'), ('BAM', 'Bosnia-Herzegovina Convertible Mark'), ('BAD', 'Bosnia-Herzegovina Dinar (1992–1994)'), ('BAN', 'Bosnia-Herzegovina New Dinar (1994–1997)'), ('BWP', 'Botswanan Pula'), ('BRC', 'Brazilian Cruzado (1986–1989)'), ('BRZ', 'Brazilian Cruzeiro (1942–1967)'), ('BRE', 'Brazilian Cruzeiro (1990–1993)'), ('BRR', 'Brazilian Cruzeiro (1993–1994)'), ('BRN', 'Brazilian New Cruzado (1989–1990)'), ('BRB', 'Brazilian New Cruzeiro (1967–1986)'), ('BRL', 'Brazilian Real'), ('GBP', 'British Pound'), ('BND', 'Brunei Dollar'), ('BGL', 'Bulgarian Hard Lev'), ('BGN', 'Bulgarian Lev'), ('BGO', 'Bulgarian Lev (1879–1952)'), ('BGM', 'Bulgarian Socialist Lev'), ('BUK', 'Burmese Kyat'), ('BIF', 'Burundian Franc'), ('XPF', 'CFP Franc'), ('KHR', 'Cambodian Riel'), ('CAD', 'Canadian Dollar'), ('CVE', 'Cape Verdean Escudo'), ('KYD', 'Cayman Islands Dollar'), ('XAF', 'Central African CFA Franc'), ('CLE', 'Chilean Escudo'), ('CLP', 'Chilean Peso'), ('CLF', 'Chilean Unit of Account (UF)'), ('CNX', 'Chinese People’s Bank Dollar'), ('CNY', 'Chinese Yuan'), ('CNH', 'Chinese Yuan (offshore)'), ('COP', 'Colombian Peso'), ('COU', 'Colombian Real Value Unit'), ('KMF', 'Comorian Franc'), ('CDF', 'Congolese Franc'), ('CRC', 'Costa Rican Colón'), ('HRD', 'Croatian Dinar'), ('HRK', 'Croatian Kuna'), ('CUC', 'Cuban Convertible Peso'), ('CUP', 'Cuban Peso'), ('CYP', 'Cypriot Pound'), ('CZK', 'Czech Koruna'), ('CSK', 'Czechoslovak Hard Koruna'), ('DKK', 'Danish Krone'), ('DJF', 'Djiboutian Franc'), ('DOP', 'Dominican Peso'), ('NLG', 'Dutch Guilder'), ('XCD', 'East Caribbean Dollar'), ('DDM', 'East German Mark'), ('ECS', 'Ecuadorian Sucre'), ('ECV', 'Ecuadorian Unit of Constant Value'), ('EGP', 'Egyptian Pound'), ('GQE', 'Equatorial Guinean Ekwele'), ('ERN', 'Eritrean Nakfa'), ('EEK', 'Estonian Kroon'), ('ETB', 'Ethiopian Birr'), ('EUR', 'Euro'), ('XBA', 'European Composite Unit'), ('XEU', 'European Currency Unit'), ('XBB', 'European Monetary Unit'), ('XBC', 'European Unit of Account (XBC)'), ('XBD', 'European Unit of Account (XBD)'), ('FKP', 'Falkland Islands Pound'), ('FJD', 'Fijian Dollar'), ('FIM', 'Finnish Markka'), ('FRF', 'French Franc'), ('XFO', 'French Gold Franc'), ('XFU', 'French UIC-Franc'), ('GMD', 'Gambian Dalasi'), ('GEK', 'Georgian Kupon Larit'), ('GEL', 'Georgian Lari'), ('DEM', 'German Mark'), ('GHS', 'Ghanaian Cedi'), ('GHC', 'Ghanaian Cedi (1979–2007)'), ('GIP', 'Gibraltar Pound'), ('XAU', 'Gold'), ('GRD', 'Greek Drachma'), ('GTQ', 'Guatemalan Quetzal'), ('GWP', 'Guinea-Bissau Peso'), ('GNF', 'Guinean Franc'), ('GNS', 'Guinean Syli'), ('GYD', 'Guyanaese Dollar'), ('HTG', 'Haitian Gourde'), ('HNL', 'Honduran Lempira'), ('HKD', 'Hong Kong Dollar'), ('HUF', 'Hungarian Forint'), ('IMP', 'IMP'), ('ISK', 'Icelandic Króna'), ('ISJ', 'Icelandic Króna (1918–1981)'), ('INR', 'Indian Rupee'), ('IDR', 'Indonesian Rupiah'), ('IRR', 'Iranian Rial'), ('IQD', 'Iraqi Dinar'), ('IEP', 'Irish Pound'), ('ILS', 'Israeli New Shekel'), ('ILP', 'Israeli Pound'), ('ILR', 'Israeli Shekel (1980–1985)'), ('ITL', 'Italian Lira'), ('JMD', 'Jamaican Dollar'), ('JPY', 'Japanese Yen'), ('JOD', 'Jordanian Dinar'), ('KZT', 'Kazakhstani Tenge'), ('KES', 'Kenyan Shilling'), ('KWD', 'Kuwaiti Dinar'), ('KGS', 'Kyrgystani Som'), ('LAK', 'Laotian Kip'), ('LVL', 'Latvian Lats'), ('LVR', 'Latvian Ruble'), ('LBP', 'Lebanese Pound'), ('LSL', 'Lesotho Loti'), ('LRD', 'Liberian Dollar'), ('LYD', 'Libyan Dinar'), ('LTL', 'Lithuanian Litas'), ('LTT', 'Lithuanian Talonas'), ('LUL', 'Luxembourg Financial Franc'), ('LUC', 'Luxembourgian Convertible Franc'), ('LUF', 'Luxembourgian Franc'), ('MOP', 'Macanese Pataca'), ('MKD', 'Macedonian Denar'), ('MKN', 'Macedonian Denar (1992–1993)'), ('MGA', 'Malagasy Ariary'), ('MGF', 'Malagasy Franc'), ('MWK', 'Malawian Kwacha'), ('MYR', 'Malaysian Ringgit'), ('MVR', 'Maldivian Rufiyaa'), ('MVP', 'Maldivian Rupee (1947–1981)'), ('MLF', 'Malian Franc'), ('MTL', 'Maltese Lira'), ('MTP', 'Maltese Pound'), ('MRU', 'Mauritanian Ouguiya'), ('MRO', 'Mauritanian Ouguiya (1973–2017)'), ('MUR', 'Mauritian Rupee'), ('MXV', 'Mexican Investment Unit'), ('MXN', 'Mexican Peso'), ('MXP', 'Mexican Silver Peso (1861–1992)'), ('MDC', 'Moldovan Cupon'), ('MDL', 'Moldovan Leu'), ('MCF', 'Monegasque Franc'), ('MNT', 'Mongolian Tugrik'), ('MAD', 'Moroccan Dirham'), ('MAF', 'Moroccan Franc'), ('MZE', 'Mozambican Escudo'), ('MZN', 'Mozambican Metical'), ('MZM', 'Mozambican Metical (1980–2006)'), ('MMK', 'Myanmar Kyat'), ('NAD', 'Namibian Dollar'), ('NPR', 'Nepalese Rupee'), ('ANG', 'Netherlands Antillean Guilder'), ('TWD', 'New Taiwan Dollar'), ('NZD', 'New Zealand Dollar'), ('NIO', 'Nicaraguan Córdoba'), ('NIC', 'Nicaraguan Córdoba (1988–1991)'), ('NGN', 'Nigerian Naira'), ('KPW', 'North Korean Won'), ('NOK', 'Norwegian Krone'), ('OMR', 'Omani Rial'), ('PKR', 'Pakistani Rupee'), ('XPD', 'Palladium'), ('PAB', 'Panamanian Balboa'), ('PGK', 'Papua New Guinean Kina'), ('PYG', 'Paraguayan Guarani'), ('PEI', 'Peruvian Inti'), ('PEN', 'Peruvian Sol'), ('PES', 'Peruvian Sol (1863–1965)'), ('PHP', 'Philippine Peso'), ('XPT', 'Platinum'), ('PLN', 'Polish Zloty'), ('PLZ', 'Polish Zloty (1950–1995)'), ('PTE', 'Portuguese Escudo'), ('GWE', 'Portuguese Guinea Escudo'), ('QAR', 'Qatari Riyal'), ('XRE', 'RINET Funds'), ('RHD', 'Rhodesian Dollar'), ('RON', 'Romanian Leu'), ('ROL', 'Romanian Leu (1952–2006)'), ('RUB', 'Russian Ruble'), ('RUR', 'Russian Ruble (1991–1998)'), ('RWF', 'Rwandan Franc'), ('SVC', 'Salvadoran Colón'), ('WST', 'Samoan Tala'), ('SAR', 'Saudi Riyal'), ('RSD', 'Serbian Dinar'), ('CSD', 'Serbian Dinar (2002–2006)'), ('SCR', 'Seychellois Rupee'), ('SLE', 'Sierra Leonean Leone'), ('SLL', 'Sierra Leonean Leone (1964—2022)'), ('XAG', 'Silver'), ('SGD', 'Singapore Dollar'), ('SKK', 'Slovak Koruna'), ('SIT', 'Slovenian Tolar'), ('SBD', 'Solomon Islands Dollar'), ('SOS', 'Somali Shilling'), ('ZAR', 'South African Rand'), ('ZAL', 'South African Rand (financial)'), ('KRH', 'South Korean Hwan (1953–1962)'), ('KRW', 'South Korean Won'), ('KRO', 'South Korean Won (1945–1953)'), ('SSP', 'South Sudanese Pound'), ('SUR', 'Soviet Rouble'), ('ESP', 'Spanish Peseta'), ('ESA', 'Spanish Peseta (A account)'), ('ESB', 'Spanish Peseta (convertible account)'), ('XDR', 'Special Drawing Rights'), ('LKR', 'Sri Lankan Rupee'), ('SHP', 'St. Helena Pound'), ('XSU', 'Sucre'), ('SDD', 'Sudanese Dinar (1992–2007)'), ('SDG', 'Sudanese Pound'), ('SDP', 'Sudanese Pound (1957–1998)'), ('SRD', 'Surinamese Dollar'), ('SRG', 'Surinamese Guilder'), ('SZL', 'Swazi Lilangeni'), ('SEK', 'Swedish Krona'), ('CHF', 'Swiss Franc'), ('SYP', 'Syrian Pound'), ('STN', 'São Tomé & Príncipe Dobra'), ('STD', 'São Tomé & Príncipe Dobra (1977–2017)'), ('TVD', 'TVD'), ('TJR', 'Tajikistani Ruble'), ('TJS', 'Tajikistani Somoni'), ('TZS', 'Tanzanian Shilling'), ('XTS', 'Testing Currency Code'), ('THB', 'Thai Baht'), ('XXX', 'The codes assigned for transactions where no currency is involved'), ('TPE', 'Timorese Escudo'), ('TOP', 'Tongan Paʻanga'), ('TTD', 'Trinidad & Tobago Dollar'), ('TND', 'Tunisian Dinar'), ('TRY', 'Turkish Lira'), ('TRL', 'Turkish Lira (1922–2005)'), ('TMT', 'Turkmenistani Manat'), ('TMM', 'Turkmenistani Manat (1993–2009)'), ('USD', 'US Dollar'), ('USN', 'US Dollar (Next day)'), ('USS', 'US Dollar (Same day)'), ('UGX', 'Ugandan Shilling'), ('UGS', 'Ugandan Shilling (1966–1987)'), ('UAH', 'Ukrainian Hryvnia'), ('UAK', 'Ukrainian Karbovanets'), ('AED', 'United Arab Emirates Dirham'), ('UYW', 'Uruguayan Nominal Wage Index Unit'), ('UYU', 'Uruguayan Peso'), ('UYP', 'Uruguayan Peso (1975–1993)'), ('UYI', 'Uruguayan Peso (Indexed Units)'), ('UZS', 'Uzbekistani Som'), ('VUV', 'Vanuatu Vatu'), ('VES', 'Venezuelan Bolívar'), ('VEB', 'Venezuelan Bolívar (1871–2008)'), ('VEF', 'Venezuelan Bolívar (2008–2018)'), ('VND', 'Vietnamese Dong'), ('VNN', 'Vietnamese Dong (1978–1985)'), ('CHE', 'WIR Euro'), ('CHW', 'WIR Franc'), ('XOF', 'West African CFA Franc'), ('YDD', 'Yemeni Dinar'), ('YER', 'Yemeni Rial'), ('YUN', 'Yugoslavian Convertible Dinar (1990–1992)'), ('YUD', 'Yugoslavian Hard Dinar (1966–1990)'), ('YUM', 'Yugoslavian New Dinar (1994–2002)'), ('YUR', 'Yugoslavian Reformed Dinar (1992–1993)'), ('ZWN', 'ZWN'), ('ZRN', 'Zairean New Zaire (1993–1998)'), ('ZRZ', 'Zairean Zaire (1971–1993)'), ('ZMW', 'Zambian Kwacha'), ('ZMK', 'Zambian Kwacha (1968–2012)'), ('ZWD', 'Zimbabwean Dollar (1980–2008)'), ('ZWR', 'Zimbabwean Dollar (2008)'), ('ZWL', 'Zimbabwean Dollar (2009)')], default='AUD', editable=False, max_length=3)), + ('rate', djmoney.models.fields.MoneyField(decimal_places=3, default=Decimal('0.0'), default_currency='AUD', max_digits=11)), + ('description', models.TextField(blank=True)), + ('default_rate', models.BooleanField(default=False)), + ('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 timesheet rate', + 'verbose_name_plural': 'historical timesheet rates', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': ('history_date', 'history_id'), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name='HistoricalTimesheet', + fields=[ + ('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')), + ('deleted', models.BooleanField(default=False)), + ('shift_start', models.DateTimeField(blank=True, null=True)), + ('shift_end', models.DateTimeField(blank=True, null=True)), + ('break_start', models.DateTimeField(blank=True, null=True)), + ('break_end', models.DateTimeField(blank=True, null=True)), + ('submitted', models.DateTimeField(blank=True, null=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)), + ('rate', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='expenses.timesheetrate')), + ], + options={ + 'verbose_name': 'historical timesheet', + 'verbose_name_plural': 'historical timesheets', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': ('history_date', 'history_id'), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name='HistoricalExpense', + fields=[ + ('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')), + ('deleted', models.BooleanField(default=False)), + ('price_currency', djmoney.models.fields.CurrencyField(choices=[('XUA', 'ADB Unit of Account'), ('AFN', 'Afghan Afghani'), ('AFA', 'Afghan Afghani (1927–2002)'), ('ALL', 'Albanian Lek'), ('ALK', 'Albanian Lek (1946–1965)'), ('DZD', 'Algerian Dinar'), ('ADP', 'Andorran Peseta'), ('AOA', 'Angolan Kwanza'), ('AOK', 'Angolan Kwanza (1977–1991)'), ('AON', 'Angolan New Kwanza (1990–2000)'), ('AOR', 'Angolan Readjusted Kwanza (1995–1999)'), ('ARA', 'Argentine Austral'), ('ARS', 'Argentine Peso'), ('ARM', 'Argentine Peso (1881–1970)'), ('ARP', 'Argentine Peso (1983–1985)'), ('ARL', 'Argentine Peso Ley (1970–1983)'), ('AMD', 'Armenian Dram'), ('AWG', 'Aruban Florin'), ('AUD', 'Australian Dollar'), ('ATS', 'Austrian Schilling'), ('AZN', 'Azerbaijani Manat'), ('AZM', 'Azerbaijani Manat (1993–2006)'), ('BSD', 'Bahamian Dollar'), ('BHD', 'Bahraini Dinar'), ('BDT', 'Bangladeshi Taka'), ('BBD', 'Barbadian Dollar'), ('BYN', 'Belarusian Ruble'), ('BYB', 'Belarusian Ruble (1994–1999)'), ('BYR', 'Belarusian Ruble (2000–2016)'), ('BEF', 'Belgian Franc'), ('BEC', 'Belgian Franc (convertible)'), ('BEL', 'Belgian Franc (financial)'), ('BZD', 'Belize Dollar'), ('BMD', 'Bermudan Dollar'), ('BTN', 'Bhutanese Ngultrum'), ('BOB', 'Bolivian Boliviano'), ('BOL', 'Bolivian Boliviano (1863–1963)'), ('BOV', 'Bolivian Mvdol'), ('BOP', 'Bolivian Peso'), ('VED', 'Bolívar Soberano'), ('BAM', 'Bosnia-Herzegovina Convertible Mark'), ('BAD', 'Bosnia-Herzegovina Dinar (1992–1994)'), ('BAN', 'Bosnia-Herzegovina New Dinar (1994–1997)'), ('BWP', 'Botswanan Pula'), ('BRC', 'Brazilian Cruzado (1986–1989)'), ('BRZ', 'Brazilian Cruzeiro (1942–1967)'), ('BRE', 'Brazilian Cruzeiro (1990–1993)'), ('BRR', 'Brazilian Cruzeiro (1993–1994)'), ('BRN', 'Brazilian New Cruzado (1989–1990)'), ('BRB', 'Brazilian New Cruzeiro (1967–1986)'), ('BRL', 'Brazilian Real'), ('GBP', 'British Pound'), ('BND', 'Brunei Dollar'), ('BGL', 'Bulgarian Hard Lev'), ('BGN', 'Bulgarian Lev'), ('BGO', 'Bulgarian Lev (1879–1952)'), ('BGM', 'Bulgarian Socialist Lev'), ('BUK', 'Burmese Kyat'), ('BIF', 'Burundian Franc'), ('XPF', 'CFP Franc'), ('KHR', 'Cambodian Riel'), ('CAD', 'Canadian Dollar'), ('CVE', 'Cape Verdean Escudo'), ('KYD', 'Cayman Islands Dollar'), ('XAF', 'Central African CFA Franc'), ('CLE', 'Chilean Escudo'), ('CLP', 'Chilean Peso'), ('CLF', 'Chilean Unit of Account (UF)'), ('CNX', 'Chinese People’s Bank Dollar'), ('CNY', 'Chinese Yuan'), ('CNH', 'Chinese Yuan (offshore)'), ('COP', 'Colombian Peso'), ('COU', 'Colombian Real Value Unit'), ('KMF', 'Comorian Franc'), ('CDF', 'Congolese Franc'), ('CRC', 'Costa Rican Colón'), ('HRD', 'Croatian Dinar'), ('HRK', 'Croatian Kuna'), ('CUC', 'Cuban Convertible Peso'), ('CUP', 'Cuban Peso'), ('CYP', 'Cypriot Pound'), ('CZK', 'Czech Koruna'), ('CSK', 'Czechoslovak Hard Koruna'), ('DKK', 'Danish Krone'), ('DJF', 'Djiboutian Franc'), ('DOP', 'Dominican Peso'), ('NLG', 'Dutch Guilder'), ('XCD', 'East Caribbean Dollar'), ('DDM', 'East German Mark'), ('ECS', 'Ecuadorian Sucre'), ('ECV', 'Ecuadorian Unit of Constant Value'), ('EGP', 'Egyptian Pound'), ('GQE', 'Equatorial Guinean Ekwele'), ('ERN', 'Eritrean Nakfa'), ('EEK', 'Estonian Kroon'), ('ETB', 'Ethiopian Birr'), ('EUR', 'Euro'), ('XBA', 'European Composite Unit'), ('XEU', 'European Currency Unit'), ('XBB', 'European Monetary Unit'), ('XBC', 'European Unit of Account (XBC)'), ('XBD', 'European Unit of Account (XBD)'), ('FKP', 'Falkland Islands Pound'), ('FJD', 'Fijian Dollar'), ('FIM', 'Finnish Markka'), ('FRF', 'French Franc'), ('XFO', 'French Gold Franc'), ('XFU', 'French UIC-Franc'), ('GMD', 'Gambian Dalasi'), ('GEK', 'Georgian Kupon Larit'), ('GEL', 'Georgian Lari'), ('DEM', 'German Mark'), ('GHS', 'Ghanaian Cedi'), ('GHC', 'Ghanaian Cedi (1979–2007)'), ('GIP', 'Gibraltar Pound'), ('XAU', 'Gold'), ('GRD', 'Greek Drachma'), ('GTQ', 'Guatemalan Quetzal'), ('GWP', 'Guinea-Bissau Peso'), ('GNF', 'Guinean Franc'), ('GNS', 'Guinean Syli'), ('GYD', 'Guyanaese Dollar'), ('HTG', 'Haitian Gourde'), ('HNL', 'Honduran Lempira'), ('HKD', 'Hong Kong Dollar'), ('HUF', 'Hungarian Forint'), ('IMP', 'IMP'), ('ISK', 'Icelandic Króna'), ('ISJ', 'Icelandic Króna (1918–1981)'), ('INR', 'Indian Rupee'), ('IDR', 'Indonesian Rupiah'), ('IRR', 'Iranian Rial'), ('IQD', 'Iraqi Dinar'), ('IEP', 'Irish Pound'), ('ILS', 'Israeli New Shekel'), ('ILP', 'Israeli Pound'), ('ILR', 'Israeli Shekel (1980–1985)'), ('ITL', 'Italian Lira'), ('JMD', 'Jamaican Dollar'), ('JPY', 'Japanese Yen'), ('JOD', 'Jordanian Dinar'), ('KZT', 'Kazakhstani Tenge'), ('KES', 'Kenyan Shilling'), ('KWD', 'Kuwaiti Dinar'), ('KGS', 'Kyrgystani Som'), ('LAK', 'Laotian Kip'), ('LVL', 'Latvian Lats'), ('LVR', 'Latvian Ruble'), ('LBP', 'Lebanese Pound'), ('LSL', 'Lesotho Loti'), ('LRD', 'Liberian Dollar'), ('LYD', 'Libyan Dinar'), ('LTL', 'Lithuanian Litas'), ('LTT', 'Lithuanian Talonas'), ('LUL', 'Luxembourg Financial Franc'), ('LUC', 'Luxembourgian Convertible Franc'), ('LUF', 'Luxembourgian Franc'), ('MOP', 'Macanese Pataca'), ('MKD', 'Macedonian Denar'), ('MKN', 'Macedonian Denar (1992–1993)'), ('MGA', 'Malagasy Ariary'), ('MGF', 'Malagasy Franc'), ('MWK', 'Malawian Kwacha'), ('MYR', 'Malaysian Ringgit'), ('MVR', 'Maldivian Rufiyaa'), ('MVP', 'Maldivian Rupee (1947–1981)'), ('MLF', 'Malian Franc'), ('MTL', 'Maltese Lira'), ('MTP', 'Maltese Pound'), ('MRU', 'Mauritanian Ouguiya'), ('MRO', 'Mauritanian Ouguiya (1973–2017)'), ('MUR', 'Mauritian Rupee'), ('MXV', 'Mexican Investment Unit'), ('MXN', 'Mexican Peso'), ('MXP', 'Mexican Silver Peso (1861–1992)'), ('MDC', 'Moldovan Cupon'), ('MDL', 'Moldovan Leu'), ('MCF', 'Monegasque Franc'), ('MNT', 'Mongolian Tugrik'), ('MAD', 'Moroccan Dirham'), ('MAF', 'Moroccan Franc'), ('MZE', 'Mozambican Escudo'), ('MZN', 'Mozambican Metical'), ('MZM', 'Mozambican Metical (1980–2006)'), ('MMK', 'Myanmar Kyat'), ('NAD', 'Namibian Dollar'), ('NPR', 'Nepalese Rupee'), ('ANG', 'Netherlands Antillean Guilder'), ('TWD', 'New Taiwan Dollar'), ('NZD', 'New Zealand Dollar'), ('NIO', 'Nicaraguan Córdoba'), ('NIC', 'Nicaraguan Córdoba (1988–1991)'), ('NGN', 'Nigerian Naira'), ('KPW', 'North Korean Won'), ('NOK', 'Norwegian Krone'), ('OMR', 'Omani Rial'), ('PKR', 'Pakistani Rupee'), ('XPD', 'Palladium'), ('PAB', 'Panamanian Balboa'), ('PGK', 'Papua New Guinean Kina'), ('PYG', 'Paraguayan Guarani'), ('PEI', 'Peruvian Inti'), ('PEN', 'Peruvian Sol'), ('PES', 'Peruvian Sol (1863–1965)'), ('PHP', 'Philippine Peso'), ('XPT', 'Platinum'), ('PLN', 'Polish Zloty'), ('PLZ', 'Polish Zloty (1950–1995)'), ('PTE', 'Portuguese Escudo'), ('GWE', 'Portuguese Guinea Escudo'), ('QAR', 'Qatari Riyal'), ('XRE', 'RINET Funds'), ('RHD', 'Rhodesian Dollar'), ('RON', 'Romanian Leu'), ('ROL', 'Romanian Leu (1952–2006)'), ('RUB', 'Russian Ruble'), ('RUR', 'Russian Ruble (1991–1998)'), ('RWF', 'Rwandan Franc'), ('SVC', 'Salvadoran Colón'), ('WST', 'Samoan Tala'), ('SAR', 'Saudi Riyal'), ('RSD', 'Serbian Dinar'), ('CSD', 'Serbian Dinar (2002–2006)'), ('SCR', 'Seychellois Rupee'), ('SLE', 'Sierra Leonean Leone'), ('SLL', 'Sierra Leonean Leone (1964—2022)'), ('XAG', 'Silver'), ('SGD', 'Singapore Dollar'), ('SKK', 'Slovak Koruna'), ('SIT', 'Slovenian Tolar'), ('SBD', 'Solomon Islands Dollar'), ('SOS', 'Somali Shilling'), ('ZAR', 'South African Rand'), ('ZAL', 'South African Rand (financial)'), ('KRH', 'South Korean Hwan (1953–1962)'), ('KRW', 'South Korean Won'), ('KRO', 'South Korean Won (1945–1953)'), ('SSP', 'South Sudanese Pound'), ('SUR', 'Soviet Rouble'), ('ESP', 'Spanish Peseta'), ('ESA', 'Spanish Peseta (A account)'), ('ESB', 'Spanish Peseta (convertible account)'), ('XDR', 'Special Drawing Rights'), ('LKR', 'Sri Lankan Rupee'), ('SHP', 'St. Helena Pound'), ('XSU', 'Sucre'), ('SDD', 'Sudanese Dinar (1992–2007)'), ('SDG', 'Sudanese Pound'), ('SDP', 'Sudanese Pound (1957–1998)'), ('SRD', 'Surinamese Dollar'), ('SRG', 'Surinamese Guilder'), ('SZL', 'Swazi Lilangeni'), ('SEK', 'Swedish Krona'), ('CHF', 'Swiss Franc'), ('SYP', 'Syrian Pound'), ('STN', 'São Tomé & Príncipe Dobra'), ('STD', 'São Tomé & Príncipe Dobra (1977–2017)'), ('TVD', 'TVD'), ('TJR', 'Tajikistani Ruble'), ('TJS', 'Tajikistani Somoni'), ('TZS', 'Tanzanian Shilling'), ('XTS', 'Testing Currency Code'), ('THB', 'Thai Baht'), ('XXX', 'The codes assigned for transactions where no currency is involved'), ('TPE', 'Timorese Escudo'), ('TOP', 'Tongan Paʻanga'), ('TTD', 'Trinidad & Tobago Dollar'), ('TND', 'Tunisian Dinar'), ('TRY', 'Turkish Lira'), ('TRL', 'Turkish Lira (1922–2005)'), ('TMT', 'Turkmenistani Manat'), ('TMM', 'Turkmenistani Manat (1993–2009)'), ('USD', 'US Dollar'), ('USN', 'US Dollar (Next day)'), ('USS', 'US Dollar (Same day)'), ('UGX', 'Ugandan Shilling'), ('UGS', 'Ugandan Shilling (1966–1987)'), ('UAH', 'Ukrainian Hryvnia'), ('UAK', 'Ukrainian Karbovanets'), ('AED', 'United Arab Emirates Dirham'), ('UYW', 'Uruguayan Nominal Wage Index Unit'), ('UYU', 'Uruguayan Peso'), ('UYP', 'Uruguayan Peso (1975–1993)'), ('UYI', 'Uruguayan Peso (Indexed Units)'), ('UZS', 'Uzbekistani Som'), ('VUV', 'Vanuatu Vatu'), ('VES', 'Venezuelan Bolívar'), ('VEB', 'Venezuelan Bolívar (1871–2008)'), ('VEF', 'Venezuelan Bolívar (2008–2018)'), ('VND', 'Vietnamese Dong'), ('VNN', 'Vietnamese Dong (1978–1985)'), ('CHE', 'WIR Euro'), ('CHW', 'WIR Franc'), ('XOF', 'West African CFA Franc'), ('YDD', 'Yemeni Dinar'), ('YER', 'Yemeni Rial'), ('YUN', 'Yugoslavian Convertible Dinar (1990–1992)'), ('YUD', 'Yugoslavian Hard Dinar (1966–1990)'), ('YUM', 'Yugoslavian New Dinar (1994–2002)'), ('YUR', 'Yugoslavian Reformed Dinar (1992–1993)'), ('ZWN', 'ZWN'), ('ZRN', 'Zairean New Zaire (1993–1998)'), ('ZRZ', 'Zairean Zaire (1971–1993)'), ('ZMW', 'Zambian Kwacha'), ('ZMK', 'Zambian Kwacha (1968–2012)'), ('ZWD', 'Zimbabwean Dollar (1980–2008)'), ('ZWR', 'Zimbabwean Dollar (2008)'), ('ZWL', 'Zimbabwean Dollar (2009)')], default='AUD', editable=False, max_length=3)), + ('price', djmoney.models.fields.MoneyField(decimal_places=3, default=Decimal('0.0'), default_currency='AUD', max_digits=11)), + ('date', models.DateField()), + ('description', models.TextField(blank=True)), + ('merchant', models.CharField(blank=True, max_length=255)), + ('receipt', models.TextField(blank=True, max_length=100)), + ('link', models.URLField(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 expense', + 'verbose_name_plural': 'historical expenses', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': ('history_date', 'history_id'), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + ] diff --git a/expenses/migrations/0002_expensecategory_historicalexpensecategory_and_more.py b/expenses/migrations/0002_expensecategory_historicalexpensecategory_and_more.py new file mode 100644 index 0000000..39137c4 --- /dev/null +++ b/expenses/migrations/0002_expensecategory_historicalexpensecategory_and_more.py @@ -0,0 +1,62 @@ +# Generated by Django 4.2.5 on 2023-09-16 21:55 + +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', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='ExpenseCategory', + 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)), + ('default_rate', models.BooleanField(default=False)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='HistoricalExpenseCategory', + 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)), + ('default_rate', models.BooleanField(default=False)), + ('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 expense category', + 'verbose_name_plural': 'historical expense categorys', + '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='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='expenses', to='expenses.expensecategory'), + ), + migrations.AddField( + model_name='historicalexpense', + name='category', + field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='expenses.expensecategory'), + ), + ] diff --git a/expenses/migrations/__init__.py b/expenses/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/expenses/models.py b/expenses/models.py new file mode 100644 index 0000000..841b5f1 --- /dev/null +++ b/expenses/models.py @@ -0,0 +1,133 @@ +from datetime import datetime, timedelta +from django.db import models +from django.utils import timezone +from djmoney.models.fields import MoneyField, Money +from .deletable_model import DeletableModel +from util import truncate_string # new line +from django.db.models import Q + +# class ExpenseManager(models.Manager): +# def active(self): +# return self.filter(deleted=False) + +# # Soft delete only! +# def delete_model(self, request, obj): +# obj.deleted = True +# obj.save() + +# def get_actions(self, request): +# actions = super().get_actions(request) +# # Remove the default delete action from the actions list +# del actions["delete_selected"] +# return actions + +# def soft_delete_selected(self, request, queryset): +# # Mark selected entries as deleted (soft delete) +# queryset.update(deleted=True) + +# soft_delete_selected.short_description = "Soft delete selected entries" + +# actions = [soft_delete_selected] + + +class TimesheetRate(DeletableModel): + rate = MoneyField( + decimal_places=3, + default=0.000, + default_currency="AUD", + max_digits=11, + ) + description = models.TextField(blank=True) + default_rate = models.BooleanField(default=False) + + def __str__(self): + return f"{self.rate} {truncate_string(self.description,8)}" + + def save(self, *args, **kwargs): + # Ensure that only one entry can be marked as a favorite + if self.default_rate: + TimesheetRate.objects.filter(~Q(pk=self.pk)).update(default_rate=False) + super().save(*args, **kwargs) + + +class Timesheet(DeletableModel): + shift_start = models.DateTimeField(null=True, blank=True) + shift_end = models.DateTimeField(null=True, blank=True) + break_start = models.DateTimeField(null=True, blank=True) + break_end = models.DateTimeField(null=True, blank=True) + submitted = models.DateTimeField(null=True, blank=True) + rate = models.ForeignKey( + TimesheetRate, + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="timesheets", + ) + + @property + def shift_hours(self) -> timedelta: + if self.shift_start and self.shift_end: + return self.shift_end - self.shift_start + return timedelta(0) + + @property + def break_hours(self) -> timedelta: + if self.break_start and self.break_end: + return self.break_end - self.break_start + return timedelta(0) + + @property + def worked_hours(self) -> timedelta: + return self.shift_hours - self.break_hours + + @property + def worked_decimal(self) -> str: + return f"{self.worked_hours.total_seconds() / 60.0 / 60:.4f}" + + @property + def total(self) -> Money: + hours = self.worked_hours.total_seconds() / 60.0 / 60 + return hours * self.rate.rate + + def _rules(self): + hours = self.worked_hours.total_seconds() / 60.0 / 60 + return hours > 3 and hours <= 7.5 + + _rules.boolean = True + rules = property(_rules) + + def __str__(self): + return f"{self.shift_start} to {self.shift_end}" + + +class ExpenseCategory(DeletableModel): + name = models.TextField(blank=True) + description = models.TextField(blank=True) + default_rate = models.BooleanField(default=False) + + def __str__(self): + return self.name + + +class Expense(DeletableModel): + price = MoneyField( + decimal_places=3, + default=0.000, + default_currency="AUD", + max_digits=11, + ) + date = models.DateField() + description = models.TextField(blank=True) + merchant = models.CharField(max_length=255, blank=True) + receipt = models.FileField(upload_to="receipts/", blank=True) + link = models.URLField(blank=True) + category = models.ForeignKey( + ExpenseCategory, + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="expenses", + ) + + def __str__(self): + return f"{self.date} {self.price} {truncate_string(self.description,32)}" diff --git a/expenses/templates/admin/expense_changelist.html b/expenses/templates/admin/expense_changelist.html new file mode 100644 index 0000000..b2770d4 --- /dev/null +++ b/expenses/templates/admin/expense_changelist.html @@ -0,0 +1,9 @@ +{% extends "admin/change_list.html" %} + +{% block change_list_title %} +

Custom Title for Expense List

+{% endblock %} + +{% block content %} + {{ block.super }} +{% endblock %} diff --git a/expenses/templates/admin/expenses/change_form.html b/expenses/templates/admin/expenses/change_form.html new file mode 100644 index 0000000..cbcba49 --- /dev/null +++ b/expenses/templates/admin/expenses/change_form.html @@ -0,0 +1,27 @@ +{% extends "admin/change_form.html" %} {% load i18n admin_urls static +admin_modify %} {% load static %} {% block extrahead %} {{ block.super }} + + + +{% endblock %} diff --git a/expenses/templates/admin/expenses/submit_line.html b/expenses/templates/admin/expenses/submit_line.html new file mode 100644 index 0000000..2a9512d --- /dev/null +++ b/expenses/templates/admin/expenses/submit_line.html @@ -0,0 +1,23 @@ +{% load i18n admin_urls %} +
+{% block submit-row %} +{% if original and original.deleted %} + {% translate "Restore" %} +{% else %} + +{% if show_save %}{% endif %} +{% if show_save_as_new %}{% endif %} +{% if show_save_and_add_another %}{% endif %} +{% if show_save_and_continue %}{% endif %} +{% if show_close %} + {% url opts|admin_urlname:'changelist' as changelist_url %} + {% translate 'Close' %} +{% endif %} +{% if show_delete_link and original and not original.deleted %} + {% url opts|admin_urlname:'delete' original.pk|admin_urlquote as delete_url %} + {% translate "Delete" %} +{% endif %} +{% endif %} + +{% endblock %} +
diff --git a/expenses/tests.py b/expenses/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/expenses/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/expenses/views.py b/expenses/views.py new file mode 100644 index 0000000..112240b --- /dev/null +++ b/expenses/views.py @@ -0,0 +1,17 @@ +from django.shortcuts import render + +from django.http import HttpResponseRedirect +from django.urls import reverse +from django.contrib import messages +from .models import Expense + + +def restore_item(request, item_id): + try: + item = Expense.objects.get(pk=item_id) + item.deleted = False + item.save() + messages.success(request, "Item restored successfully.") + except Expense.DoesNotExist: + messages.error(request, "Item not found.") + return HttpResponseRedirect(reverse("admin:expenses_expense_changelist")) diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..858271d --- /dev/null +++ b/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'budget.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..3ebaeea --- /dev/null +++ b/requirements.txt @@ -0,0 +1,21 @@ +asgiref==3.7.2 +Babel==2.12.1 +backports.zoneinfo==0.2.1 +defusedxml==0.7.1 +diff-match-patch==20230430 +Django==4.2.5 +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 +odfpy==1.4.1 +openpyxl==3.1.2 +py-moneyed==3.0 +pytz==2023.3.post1 +PyYAML==6.0.1 +sqlparse==0.4.4 +tablib==3.5.0 +typing-extensions==4.7.1 +xlrd==2.0.1 +xlwt==1.3.0 diff --git a/summary/__init__.py b/summary/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/summary/admin.py b/summary/admin.py new file mode 100644 index 0000000..bdec9af --- /dev/null +++ b/summary/admin.py @@ -0,0 +1,54 @@ +from django.contrib import admin +from .models import SaleSummary +from django.db.models import ( + Sum, + Count, + F, + ExpressionWrapper, + DecimalField, + DurationField, + FloatField, +) +from djmoney.models.fields import MoneyField +from django.db.models.functions import TruncMonth + + +@admin.register(SaleSummary) +class SaleSummaryAdmin(admin.ModelAdmin): + change_list_template = "admin/sale_summary_change_list.html" + date_hierarchy = "shift_start" + + def changelist_view(self, request, extra_context=None): + response = super().changelist_view( + request, + extra_context=extra_context, + ) + + try: + qs = response.context_data["cl"].queryset + except (AttributeError, KeyError): + return response + + metrics = { + "month": TruncMonth("shift_start"), + "total": Sum(F("shift_end") - F("shift_start")), + "total_sales": Sum( + ExpressionWrapper( + (F("shift_end") - F("shift_start")) + / 60.0 + / 1000000 + / 60 + * F("rate__rate"), + # MoneyField does not work https://github.com/django-money/django-money/issues/627 + output_field=DecimalField(), + ) + ), + } + + response.context_data["summary"] = list( + qs.values("shift_start__month").annotate(**metrics).order_by("month") + ) + + metrics.pop("month") + response.context_data["summary_total"] = dict(qs.aggregate(**metrics)) + return response diff --git a/summary/apps.py b/summary/apps.py new file mode 100644 index 0000000..46b8fed --- /dev/null +++ b/summary/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class SummaryConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'summary' diff --git a/summary/migrations/0001_initial.py b/summary/migrations/0001_initial.py new file mode 100644 index 0000000..b48c97c --- /dev/null +++ b/summary/migrations/0001_initial.py @@ -0,0 +1,57 @@ +# Generated by Django 4.2.5 on 2023-09-16 21:55 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import simple_history.models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('expenses', '0002_expensecategory_historicalexpensecategory_and_more'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='SaleSummary', + fields=[ + ], + options={ + 'verbose_name': 'Sale Summary', + 'verbose_name_plural': 'Sales Summary', + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=('expenses.timesheet',), + ), + migrations.CreateModel( + name='HistoricalSaleSummary', + fields=[ + ('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')), + ('deleted', models.BooleanField(default=False)), + ('shift_start', models.DateTimeField(blank=True, null=True)), + ('shift_end', models.DateTimeField(blank=True, null=True)), + ('break_start', models.DateTimeField(blank=True, null=True)), + ('break_end', models.DateTimeField(blank=True, null=True)), + ('submitted', models.DateTimeField(blank=True, null=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)), + ('rate', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='expenses.timesheetrate')), + ], + options={ + 'verbose_name': 'historical Sale Summary', + 'verbose_name_plural': 'historical Sales Summary', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': ('history_date', 'history_id'), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + ] diff --git a/summary/migrations/__init__.py b/summary/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/summary/models.py b/summary/models.py new file mode 100644 index 0000000..bf96ac8 --- /dev/null +++ b/summary/models.py @@ -0,0 +1,9 @@ +from django.db import models +from expenses.models import Timesheet + + +class SaleSummary(Timesheet): + class Meta: + proxy = True + verbose_name = "Sale Summary" + verbose_name_plural = "Sales Summary" diff --git a/summary/templates/admin/sale_summary_change_list.html b/summary/templates/admin/sale_summary_change_list.html new file mode 100644 index 0000000..6367156 --- /dev/null +++ b/summary/templates/admin/sale_summary_change_list.html @@ -0,0 +1,62 @@ + + +{% extends "admin/change_list.html" %} {% block content_title %}{% load humanize +%}{% load month_name %} +

Sales Summary

+{% endblock %} {% block result_list %} + +
+ + + + + + + + + + + + {% for row in summary %} + + + + + + + + {% endfor %} + + + + + + + +
+
+ Category +
+
+
+ Total +
+
+ + + +
{{ row.month|date:"M Y" }}{{ row.total }}${{ row.total_sales|floatformat:3 }} + + {{ row.total_sales | default:0 | percentof:summary_total.total_sales + }} + +
Total{{ summary_total.total }}${{ summary_total.total_sales|floatformat:3}}100%
+
+ +{% endblock %} {% block pagination %}{% endblock %} diff --git a/summary/templatetags/__init__.py b/summary/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/summary/templatetags/month_name.py b/summary/templatetags/month_name.py new file mode 100644 index 0000000..57a66c7 --- /dev/null +++ b/summary/templatetags/month_name.py @@ -0,0 +1,13 @@ +from django import template +import calendar + +register = template.Library() + + +@register.filter +def month_name(month_number): + month_number = int(month_number) + return calendar.month_name[month_number] + + +register.filter("month_name", month_name) diff --git a/summary/tests.py b/summary/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/summary/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/summary/urls.py b/summary/urls.py new file mode 100644 index 0000000..dcda560 --- /dev/null +++ b/summary/urls.py @@ -0,0 +1,6 @@ +from django.urls import path +from . import admin + +urlpatterns = [ + path("summary/", admin.SaleSummaryAdmin, name="summary_dashboard"), +] diff --git a/summary/views.py b/summary/views.py new file mode 100644 index 0000000..59213c6 --- /dev/null +++ b/summary/views.py @@ -0,0 +1,32 @@ +# from django.shortcuts import render +# from django.db.models import Sum, F +# from .models import TimesheetSummary +# from expenses.models import Timesheet # Adjust the import as needed + + +# def summary_dashboard(request): +# # Calculate summary data (example: total hours and total sales per month) +# summary_data = ( +# Timesheet.objects.values("shift_start__month", "shift_start__year") +# .annotate( +# total_hours=Sum(F("shift_end") - F("shift_start")), +# total_sales=Sum("id"), +# ) +# .order_by("-shift_start__year", "-shift_start__month") +# ) + +# # Save or update the summary data in the TimesheetSummary model +# for data in summary_data: +# month = data["shift_start__month"] +# year = data["shift_start__year"] +# total_hours = data["total_hours"] +# total_sales = data["total_sales"] + +# # # Use get_or_create to avoid duplicates +# # summary_obj, _ = TimesheetSummary.objects.get_or_create( +# # month=month, +# # year=year, +# # defaults={"total_hours": total_hours, "total_sales": total_sales}, +# # ) +# context = {"summary_data": TimesheetSummary.objects.all()} +# return render(request, "admin/summary/summary_dashboard.html", context) diff --git a/util.py b/util.py new file mode 100644 index 0000000..43adcdb --- /dev/null +++ b/util.py @@ -0,0 +1,35 @@ +from datetime import datetime, timedelta + + +def truncate_string(text: str, max_length: int) -> str: + if len(text) <= max_length: + return text + else: + return text[: max_length - 3] + "..." + + +# Not efficient at all, but I'm done trying to find efficient ways to do this +# Nothing on the internet when I try to search up "Python pay day on monday +# every two weeks" without it turning into something like today -> next +# monday + 14 days +# Either way, it's bounded by 52 weeks so it's actually probably going to be good enough +def next_payday(day=datetime.now()) -> datetime: + first_monday = datetime(day.year, 1, 1) + if first_monday.weekday() != 0: + days_until_next_monday = (7 - first_monday.weekday()) % 7 + first_monday += timedelta(days=days_until_next_monday) + first_monday += timedelta(days=7) + + next_payday = first_monday + + # Calculate the next payday within a 14-day period + while next_payday <= day: + next_payday += timedelta(days=7) + if next_payday.weekday() != 0: + days_until_next_monday = (7 - next_payday.weekday()) % 7 + next_payday += timedelta(days=days_until_next_monday - 1) + else: + next_payday += timedelta(days=7) + next_payday -= timedelta(days=1) + + return next_payday