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 %}
-
-
-
- |
- {% translate 'Name' %} |
-
-
-
- {% for account_group in account_groups %}
-
-
-
-
-
-
- {% if not account_group.owner %}
-
-
- {% endif %}
- {% if user == account_group.owner %}
-
-
- {% endif %}
-
- |
- {{ account_group.name }} |
+
+
+
+
+ |
+ {% translate 'Name' %} |
- {% endfor %}
-
-
+
+
+ {% 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 @@