diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6bfa430..e9ace85 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,7 +12,7 @@ on: required: true type: string ref: - description: 'Git ref to checkout (branch, tag, or SHA)' + description: 'Git ref to checkout' required: true default: 'main' type: string @@ -29,73 +29,57 @@ jobs: runs-on: ubuntu-latest permissions: contents: read + packages: write # Needed if you switch to GHCR, good practice steps: - name: Checkout code uses: actions/checkout@v4 with: - ref: ${{ github.event.inputs.ref }} - if: github.event_name == 'workflow_dispatch' - - - name: Checkout code (non-manual) - uses: actions/checkout@v4 - if: github.event_name != 'workflow_dispatch' + ref: ${{ inputs.ref || github.ref }} - name: Log in to Docker Hub - uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + # This action handles all the logic for tags (nightly vs release vs custom) + - name: Docker Metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }} + tags: | + # Logic for Push to Main -> nightly + type=raw,value=nightly,enable=${{ github.event_name == 'push' }} + # Logic for Release -> semver and latest + type=semver,pattern={{version}},enable=${{ github.event_name == 'release' }} + type=raw,value=latest,enable=${{ github.event_name == 'release' }} + # Logic for Manual Dispatch -> custom input + type=raw,value=${{ inputs.tag }},enable=${{ github.event_name == 'workflow_dispatch' }} + - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Build and push nightly image - if: github.event_name == 'push' + - name: Build and push uses: docker/build-push-action@v6 with: context: . file: ./docker/prod/django/Dockerfile push: true provenance: false + # Pass the calculated tags from the meta step + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} build-args: | - VERSION=nightly - tags: ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }}:nightly + VERSION=${{ steps.meta.outputs.version }} platforms: linux/amd64,linux/arm64 - cache-from: type=gha - cache-to: type=gha,mode=max - - name: Build and push release image - if: github.event_name == 'release' - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/prod/django/Dockerfile - push: true - provenance: false - build-args: | - VERSION=${{ github.event.release.tag_name }} - tags: | - ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }}:latest - ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }}:${{ github.event.release.tag_name }} - platforms: linux/amd64,linux/arm64 - cache-from: type=gha - cache-to: type=gha,mode=max - - - name: Build and push custom image - if: github.event_name == 'workflow_dispatch' - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/prod/django/Dockerfile - push: true - provenance: false - build-args: | - VERSION=${{ github.event.inputs.tag }} - tags: ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }}:${{ github.event.inputs.tag }} - platforms: linux/amd64,linux/arm64 - cache-from: type=gha - cache-to: type=gha,mode=max + # --- CACHE CONFIGURATION --- + # We set a specific 'scope' key. + # This allows the Release tag to see the cache created by the Main branch. + cache-from: type=gha,scope=build-cache + cache-to: type=gha,mode=max,scope=build-cache diff --git a/.gitignore b/.gitignore index bfff5f4..d6d659c 100644 --- a/.gitignore +++ b/.gitignore @@ -123,6 +123,7 @@ celerybeat.pid # Environments .env +.prod.env .venv env/ venv/ @@ -161,5 +162,6 @@ cython_debug/ # option (not recommended) you can uncomment the following to ignore the entire idea folder. .idea/ +node_modules/ postgres_data/ -.prod.env \ No newline at end of file +.prod.env diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..db09e10 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "djlint.showInstallError": false, + "files.associations": { + "*.css": "tailwindcss" + }, + "tailwindCSS.experimental.configFile": "frontend/src/styles/tailwind.css", + "djlint.profile": "django", +} \ No newline at end of file diff --git a/app/WYGIWYH/settings.py b/app/WYGIWYH/settings.py index cf5db80..4fab03a 100644 --- a/app/WYGIWYH/settings.py +++ b/app/WYGIWYH/settings.py @@ -11,6 +11,7 @@ https://docs.djangoproject.com/en/5.1/ref/settings/ """ import os +import re import sys from pathlib import Path @@ -46,7 +47,7 @@ INSTALLED_APPS = [ "django.contrib.sites", "whitenoise.runserver_nostatic", "django.contrib.staticfiles", - "webpack_boilerplate", + "django_vite", "django.contrib.humanize", "django.contrib.postgres", "django_browser_reload", @@ -128,6 +129,14 @@ STORAGES = { WHITENOISE_MANIFEST_STRICT = False + +def immutable_file_test(path, url): + # Match vite (rollup)-generated hashes, à la, `some_file-CSliV9zW.js` + return re.match(r"^.+[.-][0-9a-zA-Z_-]{8,12}\..+$", url) + + +WHITENOISE_IMMUTABLE_FILE_TEST = immutable_file_test + WSGI_APPLICATION = "WYGIWYH.wsgi.application" @@ -289,7 +298,7 @@ STATIC_URL = "static/" STATIC_ROOT = BASE_DIR / "static_files" STATICFILES_DIRS = [ - ROOT_DIR / "frontend/build", + ROOT_DIR / "frontend" / "build", BASE_DIR / "static", ] @@ -305,9 +314,11 @@ CACHES = { } } -WEBPACK_LOADER = { - "MANIFEST_FILE": ROOT_DIR / "frontend/build/manifest.json", -} +DJANGO_VITE_ASSETS_PATH = STATIC_ROOT +DJANGO_VITE_MANIFEST_PATH = DJANGO_VITE_ASSETS_PATH / "manifest.json" +DJANGO_VITE_DEV_MODE = DEBUG +DJANGO_VITE_DEV_SERVER_PORT = 5173 +DJANGO_VITE_DEV_SERVER_HOST = "localhost" # Default primary key field type # https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field @@ -354,8 +365,11 @@ ACCOUNT_ADAPTER = "allauth.account.adapter.DefaultAccountAdapter" SOCIALACCOUNT_ADAPTER = "allauth.socialaccount.adapter.DefaultSocialAccountAdapter" # CRISPY FORMS -CRISPY_ALLOWED_TEMPLATE_PACKS = ["bootstrap5", "crispy_forms/pure_text"] -CRISPY_TEMPLATE_PACK = "bootstrap5" +CRISPY_ALLOWED_TEMPLATE_PACKS = [ + "crispy_forms/pure_text", + "crispy-daisyui", +] +CRISPY_TEMPLATE_PACK = "crispy-daisyui" SESSION_EXPIRE_AT_BROWSER_CLOSE = False SESSION_COOKIE_AGE = int(os.getenv("SESSION_EXPIRY_TIME", 2678400)) # 31 days diff --git a/app/apps/accounts/forms.py b/app/apps/accounts/forms.py index c4dc2f1..801ff12 100644 --- a/app/apps/accounts/forms.py +++ b/app/apps/accounts/forms.py @@ -1,23 +1,21 @@ -from crispy_bootstrap5.bootstrap5 import Switch +from apps.accounts.models import Account, AccountGroup +from apps.common.fields.forms.dynamic_select import ( + DynamicModelChoiceField, + DynamicModelMultipleChoiceField, +) +from apps.common.widgets.crispy.daisyui import Switch +from apps.common.widgets.crispy.submit import NoClassSubmit +from apps.common.widgets.decimal import ArbitraryDecimalDisplayNumberInput +from apps.common.widgets.tom_select import TomSelect +from apps.currencies.models import Currency +from apps.transactions.models import TransactionCategory, TransactionTag from crispy_forms.bootstrap import FormActions from crispy_forms.helper import FormHelper -from crispy_forms.layout import Layout, Field, Column, Row +from crispy_forms.layout import Column, Field, Layout, Row from django import forms from django.db.models import Q from django.utils.translation import gettext_lazy as _ -from apps.accounts.models import Account -from apps.accounts.models import AccountGroup -from apps.common.fields.forms.dynamic_select import ( - DynamicModelMultipleChoiceField, - DynamicModelChoiceField, -) -from apps.common.widgets.crispy.submit import NoClassSubmit -from apps.common.widgets.tom_select import TomSelect -from apps.transactions.models import TransactionCategory, TransactionTag -from apps.common.widgets.decimal import ArbitraryDecimalDisplayNumberInput -from apps.currencies.models import Currency - class AccountGroupForm(forms.ModelForm): class Meta: @@ -38,17 +36,13 @@ class AccountGroupForm(forms.ModelForm): if self.instance and self.instance.pk: self.helper.layout.append( FormActions( - NoClassSubmit( - "submit", _("Update"), css_class="btn btn-outline-primary w-100" - ), + NoClassSubmit("submit", _("Update"), css_class="btn btn-primary"), ), ) else: self.helper.layout.append( FormActions( - NoClassSubmit( - "submit", _("Add"), css_class="btn btn-outline-primary w-100" - ), + NoClassSubmit("submit", _("Add"), css_class="btn btn-primary"), ), ) @@ -108,17 +102,13 @@ class AccountForm(forms.ModelForm): if self.instance and self.instance.pk: self.helper.layout.append( FormActions( - NoClassSubmit( - "submit", _("Update"), css_class="btn btn-outline-primary w-100" - ), + NoClassSubmit("submit", _("Update"), css_class="btn btn-primary"), ), ) else: self.helper.layout.append( FormActions( - NoClassSubmit( - "submit", _("Add"), css_class="btn btn-outline-primary w-100" - ), + NoClassSubmit("submit", _("Add"), css_class="btn btn-primary"), ), ) @@ -156,9 +146,8 @@ class AccountBalanceForm(forms.Form): self.helper.layout = Layout( "new_balance", Row( - Column("category", css_class="form-group col-md-6 mb-0"), - Column("tags", css_class="form-group col-md-6 mb-0"), - css_class="form-row", + Column("category"), + Column("tags"), ), Field("account_id"), ) diff --git a/app/apps/common/forms.py b/app/apps/common/forms.py index ccde74b..9f6ca95 100644 --- a/app/apps/common/forms.py +++ b/app/apps/common/forms.py @@ -1,14 +1,13 @@ -from crispy_forms.bootstrap import FormActions -from django import forms -from django.contrib.auth import get_user_model -from django.utils.translation import gettext_lazy as _ -from django.core.exceptions import ValidationError -from crispy_forms.helper import FormHelper -from crispy_forms.layout import Layout, Field, Submit, Div, HTML - -from apps.common.widgets.tom_select import TomSelect, TomSelectMultiple from apps.common.models import SharedObject from apps.common.widgets.crispy.submit import NoClassSubmit +from apps.common.widgets.tom_select import TomSelect, TomSelectMultiple +from crispy_forms.bootstrap import FormActions +from crispy_forms.helper import FormHelper +from crispy_forms.layout import HTML, Div, Field, Layout, Submit +from django import forms +from django.contrib.auth import get_user_model +from django.core.exceptions import ValidationError +from django.utils.translation import gettext_lazy as _ User = get_user_model() @@ -39,6 +38,7 @@ class SharedObjectForm(forms.Form): choices=SharedObject.Visibility.choices, required=True, label=_("Visibility"), + widget=TomSelect(clear_button=False), help_text=_( "Private: Only shown for the owner and shared users. Only editable by the owner." "
" @@ -48,9 +48,6 @@ class SharedObjectForm(forms.Form): class Meta: fields = ["visibility", "shared_with_users"] - widgets = { - "visibility": TomSelect(clear_button=False), - } def __init__(self, *args, **kwargs): # Get the current user to filter available sharing options @@ -73,12 +70,10 @@ class SharedObjectForm(forms.Form): self.helper.layout = Layout( Field("owner"), Field("visibility"), - HTML("
"), + HTML('
'), Field("shared_with_users"), FormActions( - NoClassSubmit( - "submit", _("Save"), css_class="btn btn-outline-primary w-100" - ), + NoClassSubmit("submit", _("Save"), css_class="btn btn-primary"), ), ) diff --git a/app/apps/common/templatetags/crispy_extra.py b/app/apps/common/templatetags/crispy_extra.py new file mode 100644 index 0000000..eb54776 --- /dev/null +++ b/app/apps/common/templatetags/crispy_extra.py @@ -0,0 +1,13 @@ +from django import forms, template + +register = template.Library() + + +@register.filter +def is_input(field): + return isinstance(field.field.widget, forms.TextInput) + + +@register.filter +def is_textarea(field): + return isinstance(field.field.widget, forms.Textarea) diff --git a/app/apps/common/templatetags/toast_bg.py b/app/apps/common/templatetags/toast_bg.py index 5c1b8ed..f9858d8 100644 --- a/app/apps/common/templatetags/toast_bg.py +++ b/app/apps/common/templatetags/toast_bg.py @@ -11,7 +11,7 @@ def toast_bg(tags): elif "warning" in tags: return "warning" elif "error" in tags: - return "danger" + return "error" elif "info" in tags: return "info" diff --git a/app/apps/common/widgets/crispy/daisyui.py b/app/apps/common/widgets/crispy/daisyui.py new file mode 100644 index 0000000..93d4b07 --- /dev/null +++ b/app/apps/common/widgets/crispy/daisyui.py @@ -0,0 +1,5 @@ +from crispy_forms.layout import Field + + +class Switch(Field): + template = "crispy-daisyui/layout/switch.html" diff --git a/app/apps/common/widgets/datepicker.py b/app/apps/common/widgets/datepicker.py index 9928ba1..e875084 100644 --- a/app/apps/common/widgets/datepicker.py +++ b/app/apps/common/widgets/datepicker.py @@ -1,15 +1,14 @@ import datetime -from django.forms import widgets -from django.utils import formats, translation, dates -from django.utils.translation import gettext_lazy as _ - +from apps.common.functions.format import get_format from apps.common.utils.django import ( - django_to_python_datetime, django_to_airdatepicker_datetime, django_to_airdatepicker_datetime_separated, + django_to_python_datetime, ) -from apps.common.functions.format import get_format +from django.forms import widgets +from django.utils import dates, formats, translation +from django.utils.translation import gettext_lazy as _ class AirDatePickerInput(widgets.DateInput): @@ -52,6 +51,8 @@ class AirDatePickerInput(widgets.DateInput): def build_attrs(self, base_attrs, extra_attrs=None): attrs = super().build_attrs(base_attrs, extra_attrs) + attrs["class"] = attrs.get("class", "") + " input" + attrs["data-now-button-txt"] = _("Today") attrs["data-auto-close"] = str(self.auto_close).lower() attrs["data-clear-button"] = str(self.clear_button).lower() diff --git a/app/apps/common/widgets/tom_select.py b/app/apps/common/widgets/tom_select.py index 125610a..5c9b054 100644 --- a/app/apps/common/widgets/tom_select.py +++ b/app/apps/common/widgets/tom_select.py @@ -1,4 +1,4 @@ -from django.forms import widgets, SelectMultiple +from django.forms import SelectMultiple, widgets from django.urls import reverse from django.utils.translation import gettext_lazy as _ @@ -17,7 +17,7 @@ class TomSelect(widgets.Select): checkboxes=False, group_by=None, *args, - **kwargs + **kwargs, ): super().__init__(attrs, *args, **kwargs) self.remove_button = remove_button diff --git a/app/apps/currencies/forms.py b/app/apps/currencies/forms.py index cab3120..3c9ff64 100644 --- a/app/apps/currencies/forms.py +++ b/app/apps/currencies/forms.py @@ -1,16 +1,15 @@ -from crispy_bootstrap5.bootstrap5 import Switch -from crispy_forms.bootstrap import FormActions -from crispy_forms.helper import FormHelper -from crispy_forms.layout import Layout, Row, Column -from django import forms -from django.forms import CharField -from django.utils.translation import gettext_lazy as _ - +from apps.common.widgets.crispy.daisyui import Switch from apps.common.widgets.crispy.submit import NoClassSubmit from apps.common.widgets.datepicker import AirDateTimePickerInput from apps.common.widgets.decimal import ArbitraryDecimalDisplayNumberInput from apps.common.widgets.tom_select import TomSelect from apps.currencies.models import Currency, ExchangeRate, ExchangeRateService +from crispy_forms.bootstrap import FormActions +from crispy_forms.helper import FormHelper +from crispy_forms.layout import Column, Layout, Row +from django import forms +from django.forms import CharField +from django.utils.translation import gettext_lazy as _ class CurrencyForm(forms.ModelForm): @@ -51,17 +50,13 @@ class CurrencyForm(forms.ModelForm): if self.instance and self.instance.pk: self.helper.layout.append( FormActions( - NoClassSubmit( - "submit", _("Update"), css_class="btn btn-outline-primary w-100" - ), + NoClassSubmit("submit", _("Update"), css_class="btn btn-primary"), ), ) else: self.helper.layout.append( FormActions( - NoClassSubmit( - "submit", _("Add"), css_class="btn btn-outline-primary w-100" - ), + NoClassSubmit("submit", _("Add"), css_class="btn btn-primary"), ), ) @@ -89,17 +84,13 @@ class ExchangeRateForm(forms.ModelForm): if self.instance and self.instance.pk: self.helper.layout.append( FormActions( - NoClassSubmit( - "submit", _("Update"), css_class="btn btn-outline-primary w-100" - ), + NoClassSubmit("submit", _("Update"), css_class="btn btn-primary"), ), ) else: self.helper.layout.append( FormActions( - NoClassSubmit( - "submit", _("Add"), css_class="btn btn-outline-primary w-100" - ), + NoClassSubmit("submit", _("Add"), css_class="btn btn-primary"), ), ) @@ -132,8 +123,8 @@ class ExchangeRateServiceForm(forms.ModelForm): Switch("singleton"), "api_key", Row( - Column("interval_type", css_class="form-group col-md-6"), - Column("fetch_interval", css_class="form-group col-md-6"), + Column("interval_type"), + Column("fetch_interval"), ), "target_currencies", "target_accounts", @@ -142,16 +133,12 @@ class ExchangeRateServiceForm(forms.ModelForm): if self.instance and self.instance.pk: self.helper.layout.append( FormActions( - NoClassSubmit( - "submit", _("Update"), css_class="btn btn-outline-primary w-100" - ), + NoClassSubmit("submit", _("Update"), css_class="btn btn-primary"), ), ) else: self.helper.layout.append( FormActions( - NoClassSubmit( - "submit", _("Add"), css_class="btn btn-outline-primary w-100" - ), + NoClassSubmit("submit", _("Add"), css_class="btn btn-primary"), ), ) diff --git a/app/apps/dca/forms.py b/app/apps/dca/forms.py index db045aa..39b73ac 100644 --- a/app/apps/dca/forms.py +++ b/app/apps/dca/forms.py @@ -1,22 +1,20 @@ -from crispy_bootstrap5.bootstrap5 import Switch, BS5Accordion -from crispy_forms.bootstrap import FormActions, AccordionGroup -from crispy_forms.helper import FormHelper -from crispy_forms.layout import Layout, Row, Column, HTML -from django import forms -from django.utils.translation import gettext_lazy as _ - from apps.accounts.models import Account -from apps.common.widgets.crispy.submit import NoClassSubmit -from apps.common.widgets.datepicker import AirDatePickerInput -from apps.common.widgets.decimal import ArbitraryDecimalDisplayNumberInput -from apps.common.widgets.tom_select import TomSelect -from apps.dca.models import DCAStrategy, DCAEntry -from apps.common.widgets.tom_select import TransactionSelect -from apps.transactions.models import Transaction, TransactionTag, TransactionCategory from apps.common.fields.forms.dynamic_select import ( DynamicModelChoiceField, DynamicModelMultipleChoiceField, ) +from apps.common.widgets.crispy.daisyui import Switch +from apps.common.widgets.crispy.submit import NoClassSubmit +from apps.common.widgets.datepicker import AirDatePickerInput +from apps.common.widgets.decimal import ArbitraryDecimalDisplayNumberInput +from apps.common.widgets.tom_select import TomSelect, TransactionSelect +from apps.dca.models import DCAEntry, DCAStrategy +from apps.transactions.models import Transaction, TransactionCategory, TransactionTag +from crispy_forms.bootstrap import AccordionGroup, FormActions, Accordion +from crispy_forms.helper import FormHelper +from crispy_forms.layout import HTML, Column, Layout, Row +from django import forms +from django.utils.translation import gettext_lazy as _ class DCAStrategyForm(forms.ModelForm): @@ -36,8 +34,8 @@ class DCAStrategyForm(forms.ModelForm): self.helper.layout = Layout( "name", Row( - Column("payment_currency", css_class="form-group col-md-6"), - Column("target_currency", css_class="form-group col-md-6"), + Column("payment_currency"), + Column("target_currency"), ), "notes", ) @@ -45,17 +43,13 @@ class DCAStrategyForm(forms.ModelForm): if self.instance and self.instance.pk: self.helper.layout.append( FormActions( - NoClassSubmit( - "submit", _("Update"), css_class="btn btn-outline-primary w-100" - ), + NoClassSubmit("submit", _("Update"), css_class="btn btn-primary"), ), ) else: self.helper.layout.append( FormActions( - NoClassSubmit( - "submit", _("Add"), css_class="btn btn-outline-primary w-100" - ), + NoClassSubmit("submit", _("Add"), css_class="btn btn-primary"), ), ) @@ -155,11 +149,11 @@ class DCAEntryForm(forms.ModelForm): self.helper.layout = Layout( "date", Row( - Column("amount_paid", css_class="form-group col-md-6"), - Column("amount_received", css_class="form-group col-md-6"), + Column("amount_paid"), + Column("amount_received"), ), "notes", - BS5Accordion( + Accordion( AccordionGroup( _("Create transaction"), Switch("create_transaction"), @@ -168,19 +162,11 @@ class DCAEntryForm(forms.ModelForm): Row( Column( "from_account", - css_class="form-group", ), - css_class="form-row", ), Row( - Column( - "from_category", - css_class="form-group col-md-6 mb-0", - ), - Column( - "from_tags", css_class="form-group col-md-6 mb-0" - ), - css_class="form-row", + Column("from_category"), + Column("from_tags"), ), ), css_class="p-1 mx-1 my-3 border rounded-3", @@ -192,14 +178,10 @@ class DCAEntryForm(forms.ModelForm): "to_account", css_class="form-group", ), - css_class="form-row", ), Row( - Column( - "to_category", css_class="form-group col-md-6 mb-0" - ), - Column("to_tags", css_class="form-group col-md-6 mb-0"), - css_class="form-row", + Column("to_category"), + Column("to_tags"), ), ), css_class="p-1 mx-1 my-3 border rounded-3", @@ -220,17 +202,13 @@ class DCAEntryForm(forms.ModelForm): if self.instance and self.instance.pk: self.helper.layout.append( FormActions( - NoClassSubmit( - "submit", _("Update"), css_class="btn btn-outline-primary w-100" - ), + NoClassSubmit("submit", _("Update"), css_class="btn btn-primary"), ), ) else: self.helper.layout.append( FormActions( - NoClassSubmit( - "submit", _("Add"), css_class="btn btn-outline-primary w-100" - ), + NoClassSubmit("submit", _("Add"), css_class="btn btn-primary"), ), ) diff --git a/app/apps/export_app/forms.py b/app/apps/export_app/forms.py index 3100c2b..1581791 100644 --- a/app/apps/export_app/forms.py +++ b/app/apps/export_app/forms.py @@ -1,11 +1,10 @@ +from apps.common.widgets.crispy.submit import NoClassSubmit from crispy_forms.bootstrap import FormActions from crispy_forms.helper import FormHelper -from crispy_forms.layout import Layout, HTML +from crispy_forms.layout import HTML, Layout from django import forms from django.utils.translation import gettext_lazy as _ -from apps.common.widgets.crispy.submit import NoClassSubmit - class ExportForm(forms.Form): users = forms.BooleanField( @@ -115,9 +114,7 @@ class ExportForm(forms.Form): "dca", "import_profiles", FormActions( - NoClassSubmit( - "submit", _("Export"), css_class="btn btn-outline-primary w-100" - ), + NoClassSubmit("submit", _("Export"), css_class="btn btn-primary"), ), ) @@ -162,7 +159,7 @@ class RestoreForm(forms.Form): self.helper.form_method = "post" self.helper.layout = Layout( "zip_file", - HTML("
"), + HTML('
'), "users", "accounts", "currencies", @@ -181,9 +178,7 @@ class RestoreForm(forms.Form): "dca_entries", "import_profiles", FormActions( - NoClassSubmit( - "submit", _("Restore"), css_class="btn btn-outline-primary w-100" - ), + NoClassSubmit("submit", _("Restore"), css_class="btn btn-primary"), ), ) diff --git a/app/apps/import_app/forms.py b/app/apps/import_app/forms.py index 83eb6c4..0b60165 100644 --- a/app/apps/import_app/forms.py +++ b/app/apps/import_app/forms.py @@ -1,3 +1,5 @@ +from apps.common.widgets.crispy.submit import NoClassSubmit +from apps.import_app.models import ImportProfile from crispy_forms.bootstrap import FormActions from crispy_forms.helper import FormHelper from crispy_forms.layout import ( @@ -6,9 +8,6 @@ from crispy_forms.layout import ( from django import forms from django.utils.translation import gettext_lazy as _ -from apps.import_app.models import ImportProfile -from apps.common.widgets.crispy.submit import NoClassSubmit - class ImportProfileForm(forms.ModelForm): class Meta: @@ -30,17 +29,13 @@ class ImportProfileForm(forms.ModelForm): if self.instance and self.instance.pk: self.helper.layout.append( FormActions( - NoClassSubmit( - "submit", _("Update"), css_class="btn btn-outline-primary w-100" - ), + NoClassSubmit("submit", _("Update"), css_class="btn btn-primary"), ), ) else: self.helper.layout.append( FormActions( - NoClassSubmit( - "submit", _("Add"), css_class="btn btn-outline-primary w-100" - ), + NoClassSubmit("submit", _("Add"), css_class="btn btn-primary"), ), ) @@ -57,8 +52,6 @@ class ImportRunFileUploadForm(forms.Form): self.helper.layout = Layout( "file", FormActions( - NoClassSubmit( - "submit", _("Import"), css_class="btn btn-outline-primary w-100" - ), + NoClassSubmit("submit", _("Import"), css_class="btn btn-primary"), ), ) diff --git a/app/apps/insights/forms.py b/app/apps/insights/forms.py index 4efabf9..0414149 100644 --- a/app/apps/insights/forms.py +++ b/app/apps/insights/forms.py @@ -1,15 +1,14 @@ -from crispy_forms.helper import FormHelper -from crispy_forms.layout import Layout, Field, Row, Column -from django import forms -from django.utils.translation import gettext_lazy as _ - from apps.common.widgets.datepicker import ( + AirDatePickerInput, AirMonthYearPickerInput, AirYearPickerInput, - AirDatePickerInput, ) -from apps.transactions.models import TransactionCategory from apps.common.widgets.tom_select import TomSelect +from apps.transactions.models import TransactionCategory +from crispy_forms.helper import FormHelper +from crispy_forms.layout import Column, Field, Layout, Row +from django import forms +from django.utils.translation import gettext_lazy as _ class SingleMonthForm(forms.Form): @@ -59,8 +58,8 @@ class MonthRangeForm(forms.Form): self.helper.layout = Layout( Row( - Column("month_from", css_class="form-group col-md-6"), - Column("month_to", css_class="form-group col-md-6"), + Column("month_from"), + Column("month_to"), ), ) @@ -82,8 +81,8 @@ class YearRangeForm(forms.Form): self.helper.layout = Layout( Row( - Column("year_from", css_class="form-group col-md-6"), - Column("year_to", css_class="form-group col-md-6"), + Column("year_from"), + Column("year_to"), ), ) @@ -105,8 +104,8 @@ class DateRangeForm(forms.Form): self.helper.layout = Layout( Row( - Column("date_from", css_class="form-group col-md-6"), - Column("date_to", css_class="form-group col-md-6"), + Column("date_from"), + Column("date_to"), css_class="mb-0", ), ) diff --git a/app/apps/rules/forms.py b/app/apps/rules/forms.py index 64c6201..c4e06af 100644 --- a/app/apps/rules/forms.py +++ b/app/apps/rules/forms.py @@ -1,20 +1,21 @@ -from crispy_bootstrap5.bootstrap5 import Switch, BS5Accordion -from crispy_forms.bootstrap import FormActions, AccordionGroup +from apps.common.fields.forms.dynamic_select import DynamicModelChoiceField +from apps.common.widgets.crispy.daisyui import Switch +from apps.common.widgets.crispy.submit import NoClassSubmit +from apps.common.widgets.tom_select import TomSelect, TransactionSelect +from apps.rules.models import ( + TransactionRule, + TransactionRuleAction, + UpdateOrCreateTransactionRuleAction, +) +from apps.transactions.forms import BulkEditTransactionForm +from apps.transactions.models import Transaction +from crispy_forms.bootstrap import AccordionGroup, FormActions, Accordion from crispy_forms.helper import FormHelper -from crispy_forms.layout import Layout, Field, Row, Column, HTML +from crispy_forms.layout import HTML, Column, Field, Layout, Row from django import forms from django.core.exceptions import ValidationError from django.utils.translation import gettext_lazy as _ -from apps.common.widgets.crispy.submit import NoClassSubmit -from apps.common.widgets.crispy.submit import NoClassSubmit -from apps.common.widgets.tom_select import TomSelect, TransactionSelect -from apps.rules.models import TransactionRule, UpdateOrCreateTransactionRuleAction -from apps.rules.models import TransactionRuleAction -from apps.common.fields.forms.dynamic_select import DynamicModelChoiceField -from apps.transactions.forms import BulkEditTransactionForm -from apps.transactions.models import Transaction - class TransactionRuleForm(forms.ModelForm): class Meta: @@ -53,17 +54,13 @@ class TransactionRuleForm(forms.ModelForm): if self.instance and self.instance.pk: self.helper.layout.append( FormActions( - NoClassSubmit( - "submit", _("Update"), css_class="btn btn-outline-primary w-100" - ), + NoClassSubmit("submit", _("Update"), css_class="btn btn-primary"), ), ) else: self.helper.layout.append( FormActions( - NoClassSubmit( - "submit", _("Add"), css_class="btn btn-outline-primary w-100" - ), + NoClassSubmit("submit", _("Add"), css_class="btn btn-primary"), ), ) @@ -97,17 +94,13 @@ class TransactionRuleActionForm(forms.ModelForm): if self.instance and self.instance.pk: self.helper.layout.append( FormActions( - NoClassSubmit( - "submit", _("Update"), css_class="btn btn-outline-primary w-100" - ), + NoClassSubmit("submit", _("Update"), css_class="btn btn-primary"), ), ) else: self.helper.layout.append( FormActions( - NoClassSubmit( - "submit", _("Add"), css_class="btn btn-outline-primary w-100" - ), + NoClassSubmit("submit", _("Add"), css_class="btn btn-primary"), ), ) @@ -214,148 +207,148 @@ class UpdateOrCreateTransactionRuleActionForm(forms.ModelForm): self.helper.layout = Layout( "order", - BS5Accordion( + Accordion( AccordionGroup( _("Search Criteria"), Field("filter", rows=1), Row( Column( Field("search_type_operator"), - css_class="form-group col-md-4", + css_class="col-span-12 md:col-span-4", ), Column( Field("search_type", rows=1), - css_class="form-group col-md-8", + css_class="col-span-12 md:col-span-8", ), ), Row( Column( Field("search_is_paid_operator"), - css_class="form-group col-md-4", + css_class="col-span-12 md:col-span-4", ), Column( Field("search_is_paid", rows=1), - css_class="form-group col-md-8", + css_class="col-span-12 md:col-span-8", ), ), Row( Column( Field("search_mute_operator"), - css_class="form-group col-md-4", + css_class="col-span-12 md:col-span-4", ), Column( Field("search_mute", rows=1), - css_class="form-group col-md-8", + css_class="col-span-12 md:col-span-8", ), ), Row( Column( Field("search_account_operator"), - css_class="form-group col-md-4", + css_class="col-span-12 md:col-span-4", ), Column( Field("search_account", rows=1), - css_class="form-group col-md-8", + css_class="col-span-12 md:col-span-8", ), ), Row( Column( Field("search_entities_operator"), - css_class="form-group col-md-4", + css_class="col-span-12 md:col-span-4", ), Column( Field("search_entities", rows=1), - css_class="form-group col-md-8", + css_class="col-span-12 md:col-span-8", ), ), Row( Column( Field("search_date_operator"), - css_class="form-group col-md-4", + css_class="col-span-12 md:col-span-4", ), Column( Field("search_date", rows=1), - css_class="form-group col-md-8", + css_class="col-span-12 md:col-span-8", ), ), Row( Column( Field("search_reference_date_operator"), - css_class="form-group col-md-4", + css_class="col-span-12 md:col-span-4", ), Column( Field("search_reference_date", rows=1), - css_class="form-group col-md-8", + css_class="col-span-12 md:col-span-8", ), ), Row( Column( Field("search_description_operator"), - css_class="form-group col-md-4", + css_class="col-span-12 md:col-span-4", ), Column( Field("search_description", rows=1), - css_class="form-group col-md-8", + css_class="col-span-12 md:col-span-8", ), ), Row( Column( Field("search_amount_operator"), - css_class="form-group col-md-4", + css_class="col-span-12 md:col-span-4", ), Column( Field("search_amount", rows=1), - css_class="form-group col-md-8", + css_class="col-span-12 md:col-span-8", ), ), Row( Column( Field("search_category_operator"), - css_class="form-group col-md-4", + css_class="col-span-12 md:col-span-4", ), Column( Field("search_category", rows=1), - css_class="form-group col-md-8", + css_class="col-span-12 md:col-span-8", ), ), Row( Column( Field("search_tags_operator"), - css_class="form-group col-md-4", + css_class="col-span-12 md:col-span-4", ), Column( Field("search_tags", rows=1), - css_class="form-group col-md-8", + css_class="col-span-12 md:col-span-8", ), ), Row( Column( Field("search_notes_operator"), - css_class="form-group col-md-4", + css_class="col-span-12 md:col-span-4", ), Column( Field("search_notes", rows=1), - css_class="form-group col-md-8", + css_class="col-span-12 md:col-span-8", ), ), Row( Column( Field("search_internal_note_operator"), - css_class="form-group col-md-4", + css_class="col-span-12 md:col-span-4", ), Column( Field("search_internal_note", rows=1), - css_class="form-group col-md-8", + css_class="col-span-12 md:col-span-8", ), ), Row( Column( Field("search_internal_id_operator"), - css_class="form-group col-md-4", + css_class="col-span-12 md:col-span-4", ), Column( Field("search_internal_id", rows=1), - css_class="form-group col-md-8", + css_class="col-span-12 md:col-span-8", ), ), active=True, @@ -386,17 +379,13 @@ class UpdateOrCreateTransactionRuleActionForm(forms.ModelForm): if self.instance and self.instance.pk: self.helper.layout.append( FormActions( - NoClassSubmit( - "submit", _("Update"), css_class="btn btn-outline-primary w-100" - ), + NoClassSubmit("submit", _("Update"), css_class="btn btn-primary"), ), ) else: self.helper.layout.append( FormActions( - NoClassSubmit( - "submit", _("Add"), css_class="btn btn-outline-primary w-100" - ), + NoClassSubmit("submit", _("Add"), css_class="btn btn-primary"), ), ) @@ -427,9 +416,7 @@ class DryRunCreatedTransacion(forms.Form): self.helper.layout = Layout( "transaction", FormActions( - NoClassSubmit( - "submit", _("Test"), css_class="btn btn-outline-primary w-100" - ), + NoClassSubmit("submit", _("Test"), css_class="btn btn-primary"), ), ) @@ -464,9 +451,7 @@ class DryRunDeletedTransacion(forms.Form): self.helper.layout = Layout( "transaction", FormActions( - NoClassSubmit( - "submit", _("Test"), css_class="btn btn-outline-primary w-100" - ), + NoClassSubmit("submit", _("Test"), css_class="btn btn-primary"), ), ) @@ -496,13 +481,11 @@ class DryRunUpdatedTransactionForm(BulkEditTransactionForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.helper.layout.insert(0, "transaction") - self.helper.layout.insert(1, HTML("
")) + self.helper.layout.insert(1, HTML('
')) # Change submit button self.helper.layout[-1] = FormActions( - NoClassSubmit( - "submit", _("Test"), css_class="btn btn-outline-primary w-100" - ) + NoClassSubmit("submit", _("Test"), css_class="btn btn-primary") ) if self.data.get("transaction"): diff --git a/app/apps/rules/views.py b/app/apps/rules/views.py index da49954..1fb44a6 100644 --- a/app/apps/rules/views.py +++ b/app/apps/rules/views.py @@ -564,7 +564,7 @@ def dry_run_rule_updated(request, pk): response = render( request, - "rules/fragments/transaction_rule/dry_run/created.html", + "rules/fragments/transaction_rule/dry_run/updated.html", {"form": form, "rule": rule, "logs": logs, "results": results}, ) diff --git a/app/apps/transactions/filters.py b/app/apps/transactions/filters.py index 257ab6a..25e8329 100644 --- a/app/apps/transactions/filters.py +++ b/app/apps/transactions/filters.py @@ -1,11 +1,4 @@ import django_filters -from crispy_forms.helper import FormHelper -from crispy_forms.layout import Layout, Field, Row, Column -from django import forms -from django.db.models import Q -from django.utils.translation import gettext_lazy as _ -from django_filters import Filter - from apps.accounts.models import Account from apps.common.fields.month_year import MonthYearFormField from apps.common.widgets.datepicker import AirDatePickerInput @@ -15,9 +8,15 @@ from apps.currencies.models import Currency from apps.transactions.models import ( Transaction, TransactionCategory, - TransactionTag, TransactionEntity, + TransactionTag, ) +from crispy_forms.helper import FormHelper +from crispy_forms.layout import Column, Field, Layout, Row +from django import forms +from django.db.models import Q +from django.utils.translation import gettext_lazy as _ +from django_filters import Filter SITUACAO_CHOICES = ( ("1", _("Paid")), @@ -159,14 +158,12 @@ class TransactionsFilter(django_filters.FilterSet): Field("description"), Row(Column("date_start"), Column("date_end")), Row( - Column("reference_date_start", css_class="form-group col-md-6 mb-0"), - Column("reference_date_end", css_class="form-group col-md-6 mb-0"), - css_class="form-row", + Column("reference_date_start"), + Column("reference_date_end"), ), Row( - Column("from_amount", css_class="form-group col-md-6 mb-0"), - Column("to_amount", css_class="form-group col-md-6 mb-0"), - css_class="form-row", + Column("from_amount"), + Column("to_amount"), ), Field("account", size=1), Field("currency", size=1), diff --git a/app/apps/transactions/forms.py b/app/apps/transactions/forms.py index 684e6d8..f6f9b70 100644 --- a/app/apps/transactions/forms.py +++ b/app/apps/transactions/forms.py @@ -1,39 +1,38 @@ from copy import deepcopy -from crispy_bootstrap5.bootstrap5 import Switch, BS5Accordion -from crispy_forms.bootstrap import FormActions, AccordionGroup, AppendedText -from crispy_forms.helper import FormHelper -from crispy_forms.layout import ( - Layout, - Row, - Column, - Field, - Div, - HTML, -) -from django import forms -from django.db.models import Q -from django.utils.translation import gettext_lazy as _ - from apps.accounts.models import Account from apps.common.fields.forms.dynamic_select import ( DynamicModelChoiceField, DynamicModelMultipleChoiceField, ) +from apps.common.widgets.crispy.daisyui import Switch from apps.common.widgets.crispy.submit import NoClassSubmit from apps.common.widgets.datepicker import AirDatePickerInput, AirMonthYearPickerInput from apps.common.widgets.decimal import ArbitraryDecimalDisplayNumberInput from apps.common.widgets.tom_select import TomSelect from apps.rules.signals import transaction_created, transaction_updated from apps.transactions.models import ( + InstallmentPlan, + QuickTransaction, + RecurringTransaction, Transaction, TransactionCategory, - TransactionTag, - InstallmentPlan, - RecurringTransaction, TransactionEntity, - QuickTransaction, + TransactionTag, ) +from crispy_forms.bootstrap import AccordionGroup, AppendedText, FormActions, Accordion +from crispy_forms.helper import FormHelper +from crispy_forms.layout import ( + HTML, + Column, + Div, + Field, + Layout, + Row, +) +from django import forms +from django.db.models import Q +from django.utils.translation import gettext_lazy as _ class TransactionForm(forms.ModelForm): @@ -134,21 +133,18 @@ class TransactionForm(forms.ModelForm): ), Field("is_paid", template="transactions/widgets/paid_toggle_button.html"), Row( - Column("account", css_class="form-group col-md-6 mb-0"), - Column("entities", css_class="form-group col-md-6 mb-0"), - css_class="form-row", + Column("account"), + Column("entities"), ), Row( - Column(Field("date"), css_class="form-group col-md-6 mb-0"), - Column(Field("reference_date"), css_class="form-group col-md-6 mb-0"), - css_class="form-row", + Column(Field("date")), + Column(Field("reference_date")), ), "description", Field("amount", inputmode="decimal"), Row( - Column("category", css_class="form-group col-md-6 mb-0"), - Column("tags", css_class="form-group col-md-6 mb-0"), - css_class="form-row", + Column("category"), + Column("tags"), ), "notes", ) @@ -164,20 +160,18 @@ class TransactionForm(forms.ModelForm): Field("is_paid", template="transactions/widgets/paid_toggle_button.html"), "account", Row( - Column(Field("date"), css_class="form-group col-md-6 mb-0"), - Column(Field("reference_date"), css_class="form-group col-md-6 mb-0"), - css_class="form-row", + Column(Field("date")), + Column(Field("reference_date")), ), "description", Field("amount", inputmode="decimal"), - BS5Accordion( + Accordion( AccordionGroup( _("More"), "entities", Row( - Column("category", css_class="form-group col-md-6 mb-0"), - Column("tags", css_class="form-group col-md-6 mb-0"), - css_class="form-row", + Column("category"), + Column("tags"), ), "notes", active=False, @@ -187,9 +181,7 @@ class TransactionForm(forms.ModelForm): css_class="mb-3", ), FormActions( - NoClassSubmit( - "submit", _("Add"), css_class="btn btn-outline-primary w-100" - ), + NoClassSubmit("submit", _("Add"), css_class="btn btn-primary"), ), ) @@ -202,29 +194,25 @@ class TransactionForm(forms.ModelForm): ) self.helper.layout.append( FormActions( - NoClassSubmit( - "submit", _("Update"), css_class="btn btn-outline-primary w-100" - ), + NoClassSubmit("submit", _("Update"), css_class="btn btn-primary"), ), ) else: self.fields["amount"].widget = ArbitraryDecimalDisplayNumberInput() self.helper.layout.append( Div( - NoClassSubmit( - "submit", _("Add"), css_class="btn btn-outline-primary" - ), + NoClassSubmit("submit", _("Add"), css_class="btn btn-primary"), NoClassSubmit( "submit_and_similar", _("Save and add similar"), - css_class="btn btn-outline-primary", + css_class="btn btn-primary btn-soft", ), NoClassSubmit( "submit_and_another", _("Save and add another"), - css_class="btn btn-outline-primary", + css_class="btn btn-primary btn-soft", ), - css_class="d-grid gap-2", + css_class="flex flex-col gap-2 mt-3", ), ) @@ -348,18 +336,16 @@ class QuickTransactionForm(forms.ModelForm): ), Field("is_paid", template="transactions/widgets/paid_toggle_button.html"), "name", - HTML("
"), + HTML('
'), Row( - Column("account", css_class="form-group col-md-6 mb-0"), - Column("entities", css_class="form-group col-md-6 mb-0"), - css_class="form-row", + Column("account"), + Column("entities"), ), "description", Field("amount", inputmode="decimal"), Row( - Column("category", css_class="form-group col-md-6 mb-0"), - Column("tags", css_class="form-group col-md-6 mb-0"), - css_class="form-row", + Column("category"), + Column("tags"), ), "notes", Switch("mute"), @@ -372,19 +358,14 @@ class QuickTransactionForm(forms.ModelForm): ) self.helper.layout.append( FormActions( - NoClassSubmit( - "submit", _("Update"), css_class="btn btn-outline-primary w-100" - ), + NoClassSubmit("submit", _("Update"), css_class="btn btn-primary"), ), ) else: self.fields["amount"].widget = ArbitraryDecimalDisplayNumberInput() self.helper.layout.append( - Div( - NoClassSubmit( - "submit", _("Add"), css_class="btn btn-outline-primary" - ), - css_class="d-grid gap-2", + FormActions( + NoClassSubmit("submit", _("Add"), css_class="btn btn-primary"), ), ) @@ -481,27 +462,22 @@ class BulkEditTransactionForm(forms.Form): template="transactions/widgets/unselectable_paid_toggle_button.html", ), Row( - Column("account", css_class="form-group col-md-6 mb-0"), - Column("entities", css_class="form-group col-md-6 mb-0"), - css_class="form-row", + Column("account"), + Column("entities"), ), Row( - Column(Field("date"), css_class="form-group col-md-6 mb-0"), - Column(Field("reference_date"), css_class="form-group col-md-6 mb-0"), - css_class="form-row", + Column(Field("date")), + Column(Field("reference_date")), ), "description", Field("amount", inputmode="decimal"), Row( - Column("category", css_class="form-group col-md-6 mb-0"), - Column("tags", css_class="form-group col-md-6 mb-0"), - css_class="form-row", + Column("category"), + Column("tags"), ), "notes", FormActions( - NoClassSubmit( - "submit", _("Update"), css_class="btn btn-outline-primary w-100" - ), + NoClassSubmit("submit", _("Update"), css_class="btn btn-primary"), ), ) @@ -600,62 +576,34 @@ class TransferForm(forms.Form): self.helper.layout = Layout( Row( - Column(Field("date"), css_class="form-group col-md-6 mb-0"), + Column(Field("date")), Column( Field("reference_date"), - css_class="form-group col-md-6 mb-0", ), - css_class="form-row", ), Field("description"), Field("notes"), Switch("mute"), Row( - Column( - Row( - Column( - "from_account", - css_class="form-group col-md-6 mb-0", - ), - Column( - Field("from_amount"), - css_class="form-group col-md-6 mb-0", - ), - css_class="form-row", - ), - Row( - Column("from_category", css_class="form-group col-md-6 mb-0"), - Column("from_tags", css_class="form-group col-md-6 mb-0"), - css_class="form-row", - ), - ), - css_class="p-1 mx-1 my-3 border rounded-3", + Column("from_account"), + Column(Field("from_amount")), + Column("from_category"), + Column("from_tags"), + css_class="bg-base-100 rounded-box p-4 border-base-content/60 border my-3", ), Row( Column( - Row( - Column( - "to_account", - css_class="form-group col-md-6 mb-0", - ), - Column( - Field("to_amount"), - css_class="form-group col-md-6 mb-0", - ), - css_class="form-row", - ), - Row( - Column("to_category", css_class="form-group col-md-6 mb-0"), - Column("to_tags", css_class="form-group col-md-6 mb-0"), - css_class="form-row", - ), + "to_account", ), - css_class="p-1 mx-1 my-3 border rounded-3", + Column( + Field("to_amount"), + ), + Column("to_category"), + Column("to_tags"), + css_class="bg-base-100 rounded-box p-4 border-base-content/60 border", ), FormActions( - NoClassSubmit( - "submit", _("Transfer"), css_class="btn btn-outline-primary w-100" - ), + NoClassSubmit("submit", _("Transfer"), css_class="btn btn-primary"), ), ) @@ -841,30 +789,26 @@ class InstallmentPlanForm(forms.ModelForm): template="transactions/widgets/income_expense_toggle_buttons.html", ), Row( - Column("account", css_class="form-group col-md-6 mb-0"), - Column("entities", css_class="form-group col-md-6 mb-0"), - css_class="form-row", + Column("account"), + Column("entities"), ), "description", Switch("add_description_to_transaction"), "notes", Switch("add_notes_to_transaction"), Row( - Column("number_of_installments", css_class="form-group col-md-6 mb-0"), - Column("installment_start", css_class="form-group col-md-6 mb-0"), - css_class="form-row", + Column("number_of_installments"), + Column("installment_start"), ), Row( - Column("start_date", css_class="form-group col-md-4 mb-0"), - Column("reference_date", css_class="form-group col-md-4 mb-0"), - Column("recurrence", css_class="form-group col-md-4 mb-0"), - css_class="form-row", + Column("start_date", css_class="col-span-12 md:col-span-4"), + Column("reference_date", css_class="col-span-12 md:col-span-4"), + Column("recurrence", css_class="col-span-12 md:col-span-4"), ), "installment_amount", Row( - Column("category", css_class="form-group col-md-6 mb-0"), - Column("tags", css_class="form-group col-md-6 mb-0"), - css_class="form-row", + Column("category"), + Column("tags"), ), ) @@ -874,17 +818,13 @@ class InstallmentPlanForm(forms.ModelForm): if self.instance and self.instance.pk: self.helper.layout.append( FormActions( - NoClassSubmit( - "submit", _("Update"), css_class="btn btn-outline-primary w-100" - ), + NoClassSubmit("submit", _("Update"), css_class="btn btn-primary"), ), ) else: self.helper.layout.append( FormActions( - NoClassSubmit( - "submit", _("Add"), css_class="btn btn-outline-primary w-100" - ), + NoClassSubmit("submit", _("Add"), css_class="btn btn-primary"), ), ) @@ -917,17 +857,13 @@ class TransactionTagForm(forms.ModelForm): if self.instance and self.instance.pk: self.helper.layout.append( FormActions( - NoClassSubmit( - "submit", _("Update"), css_class="btn btn-outline-primary w-100" - ), + NoClassSubmit("submit", _("Update"), css_class="btn btn-primary"), ), ) else: self.helper.layout.append( FormActions( - NoClassSubmit( - "submit", _("Add"), css_class="btn btn-outline-primary w-100" - ), + NoClassSubmit("submit", _("Add"), css_class="btn btn-primary"), ), ) @@ -949,17 +885,13 @@ class TransactionEntityForm(forms.ModelForm): if self.instance and self.instance.pk: self.helper.layout.append( FormActions( - NoClassSubmit( - "submit", _("Update"), css_class="btn btn-outline-primary w-100" - ), + NoClassSubmit("submit", _("Update"), css_class="btn btn-primary"), ), ) else: self.helper.layout.append( FormActions( - NoClassSubmit( - "submit", _("Add"), css_class="btn btn-outline-primary w-100" - ), + NoClassSubmit("submit", _("Add"), css_class="btn btn-primary"), ), ) @@ -984,17 +916,13 @@ class TransactionCategoryForm(forms.ModelForm): if self.instance and self.instance.pk: self.helper.layout.append( FormActions( - NoClassSubmit( - "submit", _("Update"), css_class="btn btn-outline-primary w-100" - ), + NoClassSubmit("submit", _("Update"), css_class="btn btn-primary"), ), ) else: self.helper.layout.append( FormActions( - NoClassSubmit( - "submit", _("Add"), css_class="btn btn-outline-primary w-100" - ), + NoClassSubmit("submit", _("Add"), css_class="btn btn-primary"), ), ) @@ -1103,30 +1031,26 @@ class RecurringTransactionForm(forms.ModelForm): template="transactions/widgets/income_expense_toggle_buttons.html", ), Row( - Column("account", css_class="form-group col-md-6 mb-0"), - Column("entities", css_class="form-group col-md-6 mb-0"), - css_class="form-row", + Column("account"), + Column("entities"), ), "description", Switch("add_description_to_transaction"), "amount", Row( - Column("category", css_class="form-group col-md-6 mb-0"), - Column("tags", css_class="form-group col-md-6 mb-0"), - css_class="form-row", + Column("category"), + Column("tags"), ), "notes", Switch("add_notes_to_transaction"), Row( - Column("start_date", css_class="form-group col-md-6 mb-0"), - Column("reference_date", css_class="form-group col-md-6 mb-0"), - css_class="form-row", + Column("start_date"), + Column("reference_date"), ), Row( - Column("recurrence_interval", css_class="form-group col-md-4 mb-0"), - Column("recurrence_type", css_class="form-group col-md-4 mb-0"), - Column("end_date", css_class="form-group col-md-4 mb-0"), - css_class="form-row", + Column("recurrence_interval", css_class="col-span-12 md:col-span-4"), + Column("recurrence_type", css_class="col-span-12 md:col-span-4"), + Column("end_date", css_class="col-span-12 md:col-span-4"), ), AppendedText("keep_at_most", _("future transactions")), ) @@ -1138,17 +1062,13 @@ class RecurringTransactionForm(forms.ModelForm): if self.instance and self.instance.pk: self.helper.layout.append( FormActions( - NoClassSubmit( - "submit", _("Update"), css_class="btn btn-outline-primary w-100" - ), + NoClassSubmit("submit", _("Update"), css_class="btn btn-primary"), ), ) else: self.helper.layout.append( FormActions( - NoClassSubmit( - "submit", _("Add"), css_class="btn btn-outline-primary w-100" - ), + NoClassSubmit("submit", _("Add"), css_class="btn btn-primary"), ), ) diff --git a/app/apps/transactions/models.py b/app/apps/transactions/models.py index 7f56229..12a65cf 100644 --- a/app/apps/transactions/models.py +++ b/app/apps/transactions/models.py @@ -2,29 +2,29 @@ import decimal import logging from copy import deepcopy +from apps.common.fields.month_year import MonthYearModelField +from apps.common.functions.decimals import truncate_decimal +from apps.common.middleware.thread_local import get_current_user +from apps.common.models import ( + OwnedObject, + OwnedObjectManager, + SharedObject, + SharedObjectManager, +) +from apps.common.templatetags.decimal import drop_trailing_zeros, localize_number +from apps.currencies.utils.convert import convert +from apps.transactions.validators import validate_decimal_places, validate_non_negative from dateutil.relativedelta import relativedelta from django.conf import settings from django.core.validators import MinValueValidator from django.db import models, transaction from django.db.models import Q from django.dispatch import Signal +from django.forms import ValidationError from django.template.defaultfilters import date from django.utils import timezone from django.utils.translation import gettext_lazy as _ -from apps.common.fields.month_year import MonthYearModelField -from apps.common.functions.decimals import truncate_decimal -from apps.common.templatetags.decimal import localize_number, drop_trailing_zeros -from apps.currencies.utils.convert import convert -from apps.transactions.validators import validate_decimal_places, validate_non_negative -from apps.common.middleware.thread_local import get_current_user -from apps.common.models import ( - SharedObject, - SharedObjectManager, - OwnedObject, - OwnedObjectManager, -) - logger = logging.getLogger() @@ -381,21 +381,32 @@ class Transaction(OwnedObject): db_table = "transactions" default_manager_name = "objects" - def clean_fields(self, *args, **kwargs): + def clean(self): + super().clean() + + # Only process amount and reference_date if account exists + # If account is missing, Django's required field validation will handle it + try: + account = self.account + except Transaction.account.RelatedObjectDoesNotExist: + # Account doesn't exist, skip processing that depends on it + # Django will add the required field error + return + + # Validate and normalize amount if isinstance(self.amount, (str, int, float)): self.amount = decimal.Decimal(str(self.amount)) self.amount = truncate_decimal( - value=self.amount, decimal_places=self.account.currency.decimal_places + value=self.amount, decimal_places=account.currency.decimal_places ) + # Normalize reference_date if self.reference_date: self.reference_date = self.reference_date.replace(day=1) elif not self.reference_date and self.date: self.reference_date = self.date.replace(day=1) - super().clean_fields(*args, **kwargs) - def save(self, *args, **kwargs): # This is not recommended as it will run twice on some cases like form and API saves. # We only do this here because we forgot to independently call it on multiple places. diff --git a/app/apps/transactions/templatetags/currency_display.py b/app/apps/transactions/templatetags/currency_display.py index 43106e6..b0eff12 100644 --- a/app/apps/transactions/templatetags/currency_display.py +++ b/app/apps/transactions/templatetags/currency_display.py @@ -3,7 +3,6 @@ from decimal import Decimal from django import template from django.utils.formats import number_format - register = template.Library() @@ -13,13 +12,27 @@ def _format_string(prefix, amount, decimal_places, suffix): value=abs(amount), decimal_pos=decimal_places, force_grouping=True ) if amount < 0: + return "-", prefix, formatted_amount, suffix return f"-{prefix}{formatted_amount}{suffix}" else: + return "", prefix, formatted_amount, suffix return f"{prefix}{formatted_amount}{suffix}" else: - return "ERR" + return "", "", "ERR", "" @register.simple_tag(name="currency_display") -def currency_display(amount, prefix, suffix, decimal_places): - return _format_string(prefix, amount, decimal_places, suffix) +def currency_display(amount, prefix, suffix, decimal_places, string=False): + sign, prefix, amount, suffix = _format_string( + prefix, amount, decimal_places, suffix + ) + + if string: + return f"{sign}{prefix}{amount}{suffix}" + + return { + "sign": sign, + "prefix": prefix, + "amount": amount, + "suffix": suffix, + } diff --git a/app/apps/transactions/views/quick_transactions.py b/app/apps/transactions/views/quick_transactions.py index 6393bf6..ee7dc4c 100644 --- a/app/apps/transactions/views/quick_transactions.py +++ b/app/apps/transactions/views/quick_transactions.py @@ -137,6 +137,7 @@ def quick_transaction_add_as_transaction(request, quick_transaction_id): "category", "tags", "entities", + "internal_id", ], ) @@ -206,6 +207,7 @@ def quick_transaction_add_as_quick_transaction(request, transaction_id): "recurring_transaction", "deleted", "deleted_at", + "internal_id", ], ) diff --git a/app/apps/users/forms.py b/app/apps/users/forms.py index defa4fe..5d12917 100644 --- a/app/apps/users/forms.py +++ b/app/apps/users/forms.py @@ -1,35 +1,43 @@ +from apps.common.middleware.thread_local import get_current_user +from apps.common.widgets.crispy.submit import NoClassSubmit +from apps.users.models import UserSettings from crispy_forms.bootstrap import ( FormActions, ) from crispy_forms.helper import FormHelper -from crispy_forms.layout import Layout, Submit, Row, Column, Field, Div, HTML +from crispy_forms.layout import HTML, Column, Div, Field, Layout, Row, Submit from django import forms from django.contrib.auth import get_user_model from django.contrib.auth.forms import ( - UsernameField, AuthenticationForm, UserCreationForm, + UsernameField, ) from django.db import transaction from django.utils.translation import gettext_lazy as _ -from apps.common.widgets.crispy.submit import NoClassSubmit -from apps.users.models import UserSettings -from apps.common.middleware.thread_local import get_current_user - class LoginForm(AuthenticationForm): username = UsernameField( label=_("E-mail"), widget=forms.EmailInput( - attrs={"class": "form-control", "placeholder": "E-mail", "name": "email"} + attrs={ + "class": "input", + "placeholder": _("E-mail"), + "name": "email", + "autocomplete": "email", + } ), ) password = forms.CharField( label=_("Password"), strip=False, widget=forms.PasswordInput( - attrs={"class": "form-control", "placeholder": "Senha"} + attrs={ + "class": "input", + "placeholder": _("Password"), + "autocomplete": "current-password", + } ), ) @@ -45,7 +53,7 @@ class LoginForm(AuthenticationForm): self.helper.layout = Layout( "username", "password", - Submit("Submit", "Login", css_class="btn btn-primary w-100"), + Submit("Submit", "Login", css_class="w-full mt-3"), ) @@ -138,9 +146,7 @@ class UserSettingsForm(forms.ModelForm): HTML("
"), "volume", FormActions( - NoClassSubmit( - "submit", _("Save"), css_class="btn btn-outline-primary w-100" - ), + NoClassSubmit("submit", _("Save"), css_class="btn btn-primary"), ), ) @@ -191,8 +197,8 @@ class UserUpdateForm(forms.ModelForm): # Define the layout using Crispy Forms, including the new fields self.helper.layout = Layout( Row( - Column("first_name", css_class="form-group col-md-6"), - Column("last_name", css_class="form-group col-md-6"), + Column("first_name"), + Column("last_name"), css_class="row", ), Field("email"), @@ -213,17 +219,13 @@ class UserUpdateForm(forms.ModelForm): if self.instance and self.instance.pk: self.helper.layout.append( FormActions( - NoClassSubmit( - "submit", _("Update"), css_class="btn btn-outline-primary w-100" - ), + NoClassSubmit("submit", _("Update"), css_class="btn btn-primary"), ), ) else: self.helper.layout.append( FormActions( - NoClassSubmit( - "submit", _("Add"), css_class="btn btn-outline-primary w-100" - ), + NoClassSubmit("submit", _("Add"), css_class="btn btn-primary"), ), ) @@ -354,8 +356,8 @@ class UserAddForm(UserCreationForm): self.helper.layout = Layout( Field("email"), Row( - Column("first_name", css_class="form-group col-md-6"), - Column("last_name", css_class="form-group col-md-6"), + Column("first_name"), + Column("last_name"), css_class="row", ), # UserCreationForm provides 'password1' and 'password2' fields @@ -375,17 +377,13 @@ class UserAddForm(UserCreationForm): if self.instance and self.instance.pk: self.helper.layout.append( FormActions( - NoClassSubmit( - "submit", _("Update"), css_class="btn btn-outline-primary w-100" - ), + NoClassSubmit("submit", _("Update"), css_class="btn btn-primary"), ), ) else: self.helper.layout.append( FormActions( - NoClassSubmit( - "submit", _("Add"), css_class="btn btn-outline-primary w-100" - ), + NoClassSubmit("submit", _("Add"), css_class="btn btn-primary"), ), ) diff --git a/app/apps/users/urls.py b/app/apps/users/urls.py index 6576bb8..b98c743 100644 --- a/app/apps/users/urls.py +++ b/app/apps/users/urls.py @@ -18,10 +18,15 @@ urlpatterns = [ name="toggle_sound_playing", ), path( - "user/toggle-sidebar/", + "user/session/toggle-sidebar/", views.toggle_sidebar_status, name="toggle_sidebar_status", ), + path( + "user/session/toggle-theme/", + views.toggle_theme, + name="toggle_theme", + ), path( "user/settings/", views.update_settings, diff --git a/app/apps/users/views.py b/app/apps/users/views.py index f8f0ef9..c7bf625 100644 --- a/app/apps/users/views.py +++ b/app/apps/users/views.py @@ -1,27 +1,26 @@ +from apps.common.decorators.demo import disabled_on_demo +from apps.common.decorators.htmx import only_htmx +from apps.common.decorators.user import htmx_login_required, is_superuser +from apps.users.forms import ( + LoginForm, + UserAddForm, + UserSettingsForm, + UserUpdateForm, +) +from apps.users.models import UserSettings from django.contrib import messages -from django.contrib.auth import logout, get_user_model +from django.contrib.auth import get_user_model, logout from django.contrib.auth.decorators import login_required from django.contrib.auth.views import ( LoginView, ) from django.core.exceptions import PermissionDenied from django.http import HttpResponse -from django.shortcuts import redirect, render, get_object_or_404 +from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse from django.utils.translation import gettext_lazy as _ from django.views.decorators.http import require_http_methods -from apps.common.decorators.htmx import only_htmx -from apps.common.decorators.user import is_superuser, htmx_login_required -from apps.users.forms import ( - LoginForm, - UserSettingsForm, - UserUpdateForm, - UserAddForm, -) -from apps.users.models import UserSettings -from apps.common.decorators.demo import disabled_on_demo - def logout_view(request): logout(request) @@ -118,6 +117,7 @@ def update_settings(request): @only_htmx @htmx_login_required +@require_http_methods(["GET"]) def toggle_sidebar_status(request): if not request.session.get("sidebar_status"): request.session["sidebar_status"] = "floating" @@ -134,6 +134,24 @@ def toggle_sidebar_status(request): ) +@htmx_login_required +@require_http_methods(["GET"]) +def toggle_theme(request): + if not request.session.get("theme"): + request.session["theme"] = "wygiwyh_dark" + + if request.session["theme"] == "wygiwyh_dark": + request.session["theme"] = "wygiwyh_light" + elif request.session["theme"] == "wygiwyh_light": + request.session["theme"] = "wygiwyh_dark" + else: + request.session["theme"] = "wygiwyh_light" + + return HttpResponse( + status=204, + ) + + @htmx_login_required @is_superuser @require_http_methods(["GET"]) diff --git a/app/static/img/logo-icon.svg b/app/static/img/logo-icon.svg index cb9a674..708605a 100644 --- a/app/static/img/logo-icon.svg +++ b/app/static/img/logo-icon.svg @@ -1 +1,3 @@ - \ No newline at end of file + + + \ No newline at end of file diff --git a/app/templates/account_groups/fragments/list.html b/app/templates/account_groups/fragments/list.html index adc7a78..540adb1 100644 --- a/app/templates/account_groups/fragments/list.html +++ b/app/templates/account_groups/fragments/list.html @@ -1,78 +1,72 @@ {% load i18n %} -
-
+ + +
+
{% spaceless %} -
{% translate 'Account Groups' %} - - -
+
{% translate 'Account Groups' %}
{% endspaceless %}
-
-
+
+
{% if account_groups %} - - - - - - - - - {% for account_group in account_groups %} - - - +
+
{% translate 'Name' %}
+ + + + - {% endfor %} - -
{% translate 'Name' %}
+ + + {% for account_group in account_groups %} + + +
+ + + {% if not account_group.owner %} + + + {% endif %} + {% if user == account_group.owner %} + + + {% endif %} + +
+ + {{ account_group.name }} + + {% endfor %} + + +
{% else %} {% endif %} diff --git a/app/templates/accounts/fragments/account_reconciliation.html b/app/templates/accounts/fragments/account_reconciliation.html index 9189421..8ad2656 100644 --- a/app/templates/accounts/fragments/account_reconciliation.html +++ b/app/templates/accounts/fragments/account_reconciliation.html @@ -9,65 +9,59 @@
{% csrf_token %} {{ form.management_form }} -
+
{% for form in form.forms %} -
-

- -

-
-
-
-
- {% translate 'Current balance' %} -
-
- {% currency_display amount=form.current_balance prefix=form.currency_prefix suffix=form.currency_suffix decimal_places=form.currency_decimal_places %} -
-
-
- {% crispy form %} -
-
-
- {% translate 'Difference' %} -
-
-
+ + + {% if form.account_group %}{{ form.account_group.name }}{% endif %}{{ form.account_name }} + + +
+ {% translate 'Current balance' %} +
+
-
-
+
+ {% crispy form %} +
+
+ {% translate 'Difference' %} +
-
+
+ + {% endfor %}
- +
diff --git a/app/templates/accounts/fragments/list.html b/app/templates/accounts/fragments/list.html index f5ef5ed..ea4ffe4 100644 --- a/app/templates/accounts/fragments/list.html +++ b/app/templates/accounts/fragments/list.html @@ -1,101 +1,96 @@ {% load i18n %} -
-
+ + +
+
{% spaceless %} -
{% translate 'Accounts' %} - - -
+
{% translate 'Accounts' %}
{% endspaceless %}
-
-
+
+
{% if accounts %} - - - - - - - - - - - - - - - {% for account in accounts %} - - - - - - - - - - {% endfor %} - -
{% translate 'Name' %}{% translate 'Group' %}{% translate 'Currency' %}{% translate 'Exchange Currency' %}{% translate 'Is Asset' %}{% translate 'Archived' %}
+ +
+ + + + + + + + + + + + + + {% for account in accounts %} + + + + + + + + + + {% endfor %} + +
{% translate 'Name' %}{% translate 'Group' %}{% translate 'Currency' %}{% translate 'Exchange Currency' %}{% translate 'Is Asset' %}{% translate 'Archived' %}
+
{% else %} - + {% endif %}
diff --git a/app/templates/calendar_view/fragments/list.html b/app/templates/calendar_view/fragments/list.html index 18f6c24..85777ed 100644 --- a/app/templates/calendar_view/fragments/list.html +++ b/app/templates/calendar_view/fragments/list.html @@ -2,67 +2,67 @@ {% load i18n %}
-
-
+