mirror of
https://github.com/eitchtee/WYGIWYH.git
synced 2026-04-24 09:38:35 +02:00
Merge branch 'eitchtee-main'
This commit is contained in:
@@ -163,7 +163,7 @@ AUTH_USER_MODEL = "users.User"
|
|||||||
LANGUAGE_CODE = "en"
|
LANGUAGE_CODE = "en"
|
||||||
LANGUAGES = (
|
LANGUAGES = (
|
||||||
("en", "English"),
|
("en", "English"),
|
||||||
("nl", "Nederlands"),
|
# ("nl", "Nederlands"),
|
||||||
("pt-br", "Português (Brasil)"),
|
("pt-br", "Português (Brasil)"),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -363,7 +363,13 @@ PWA_APP_SPLASH_SCREEN = [
|
|||||||
]
|
]
|
||||||
PWA_APP_DIR = "ltr"
|
PWA_APP_DIR = "ltr"
|
||||||
PWA_APP_LANG = "en-US"
|
PWA_APP_LANG = "en-US"
|
||||||
PWA_APP_SHORTCUTS = []
|
PWA_APP_SHORTCUTS = [
|
||||||
|
{
|
||||||
|
"name": "New Transaction",
|
||||||
|
"url": "/add/",
|
||||||
|
"description": "Add new transaction",
|
||||||
|
}
|
||||||
|
]
|
||||||
PWA_APP_SCREENSHOTS = [
|
PWA_APP_SCREENSHOTS = [
|
||||||
{
|
{
|
||||||
"src": "/static/img/pwa/splash-750x1334.png",
|
"src": "/static/img/pwa/splash-750x1334.png",
|
||||||
|
|||||||
@@ -2,9 +2,7 @@ from django.contrib import messages
|
|||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import render, get_object_or_404
|
from django.shortcuts import render, get_object_or_404
|
||||||
from django.urls import reverse
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
|
||||||
from django.views.decorators.http import require_http_methods
|
from django.views.decorators.http import require_http_methods
|
||||||
|
|
||||||
from apps.accounts.forms import AccountGroupForm
|
from apps.accounts.forms import AccountGroupForm
|
||||||
@@ -89,7 +87,6 @@ def account_group_edit(request, pk):
|
|||||||
|
|
||||||
@only_htmx
|
@only_htmx
|
||||||
@login_required
|
@login_required
|
||||||
@csrf_exempt
|
|
||||||
@require_http_methods(["DELETE"])
|
@require_http_methods(["DELETE"])
|
||||||
def account_group_delete(request, pk):
|
def account_group_delete(request, pk):
|
||||||
account_group = get_object_or_404(AccountGroup, id=pk)
|
account_group = get_object_or_404(AccountGroup, id=pk)
|
||||||
|
|||||||
@@ -2,9 +2,7 @@ from django.contrib import messages
|
|||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import render, get_object_or_404
|
from django.shortcuts import render, get_object_or_404
|
||||||
from django.urls import reverse
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
|
||||||
from django.views.decorators.http import require_http_methods
|
from django.views.decorators.http import require_http_methods
|
||||||
|
|
||||||
from apps.accounts.forms import AccountForm
|
from apps.accounts.forms import AccountForm
|
||||||
@@ -89,7 +87,6 @@ def account_edit(request, pk):
|
|||||||
|
|
||||||
@only_htmx
|
@only_htmx
|
||||||
@login_required
|
@login_required
|
||||||
@csrf_exempt
|
|
||||||
@require_http_methods(["DELETE"])
|
@require_http_methods(["DELETE"])
|
||||||
def account_delete(request, pk):
|
def account_delete(request, pk):
|
||||||
account = get_object_or_404(Account, id=pk)
|
account = get_object_or_404(Account, id=pk)
|
||||||
|
|||||||
@@ -2,9 +2,7 @@ from django.contrib import messages
|
|||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import render, get_object_or_404
|
from django.shortcuts import render, get_object_or_404
|
||||||
from django.urls import reverse
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
|
||||||
from django.views.decorators.http import require_http_methods
|
from django.views.decorators.http import require_http_methods
|
||||||
|
|
||||||
from apps.common.decorators.htmx import only_htmx
|
from apps.common.decorators.htmx import only_htmx
|
||||||
@@ -89,7 +87,6 @@ def currency_edit(request, pk):
|
|||||||
|
|
||||||
@only_htmx
|
@only_htmx
|
||||||
@login_required
|
@login_required
|
||||||
@csrf_exempt
|
|
||||||
@require_http_methods(["DELETE"])
|
@require_http_methods(["DELETE"])
|
||||||
def currency_delete(request, pk):
|
def currency_delete(request, pk):
|
||||||
currency = get_object_or_404(Currency, id=pk)
|
currency = get_object_or_404(Currency, id=pk)
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
from django.db.models import F, CharField, Value
|
from django.db.models import CharField, Value
|
||||||
from django.db.models.functions import Concat
|
from django.db.models.functions import Concat
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import render, get_object_or_404
|
from django.shortcuts import render, get_object_or_404
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
|
||||||
from django.views.decorators.http import require_http_methods
|
from django.views.decorators.http import require_http_methods
|
||||||
|
|
||||||
from apps.common.decorators.htmx import only_htmx
|
from apps.common.decorators.htmx import only_htmx
|
||||||
@@ -135,7 +134,6 @@ def exchange_rate_edit(request, pk):
|
|||||||
|
|
||||||
@only_htmx
|
@only_htmx
|
||||||
@login_required
|
@login_required
|
||||||
@csrf_exempt
|
|
||||||
@require_http_methods(["DELETE"])
|
@require_http_methods(["DELETE"])
|
||||||
def exchange_rate_delete(request, pk):
|
def exchange_rate_delete(request, pk):
|
||||||
exchange_rate = get_object_or_404(ExchangeRate, id=pk)
|
exchange_rate = get_object_or_404(ExchangeRate, id=pk)
|
||||||
|
|||||||
@@ -6,12 +6,11 @@ from django.db.models.functions import TruncMonth
|
|||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import render, get_object_or_404
|
from django.shortcuts import render, get_object_or_404
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
|
||||||
from django.views.decorators.http import require_http_methods
|
from django.views.decorators.http import require_http_methods
|
||||||
|
|
||||||
from apps.common.decorators.htmx import only_htmx
|
from apps.common.decorators.htmx import only_htmx
|
||||||
from apps.dca.models import DCAStrategy, DCAEntry
|
|
||||||
from apps.dca.forms import DCAEntryForm, DCAStrategyForm
|
from apps.dca.forms import DCAEntryForm, DCAStrategyForm
|
||||||
|
from apps.dca.models import DCAStrategy, DCAEntry
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@@ -82,7 +81,6 @@ def strategy_edit(request, strategy_id):
|
|||||||
|
|
||||||
@only_htmx
|
@only_htmx
|
||||||
@login_required
|
@login_required
|
||||||
@csrf_exempt
|
|
||||||
@require_http_methods(["DELETE"])
|
@require_http_methods(["DELETE"])
|
||||||
def strategy_delete(request, strategy_id):
|
def strategy_delete(request, strategy_id):
|
||||||
dca_strategy = get_object_or_404(DCAStrategy, id=strategy_id)
|
dca_strategy = get_object_or_404(DCAStrategy, id=strategy_id)
|
||||||
@@ -209,7 +207,6 @@ def strategy_entry_edit(request, strategy_id, entry_id):
|
|||||||
|
|
||||||
@only_htmx
|
@only_htmx
|
||||||
@login_required
|
@login_required
|
||||||
@csrf_exempt
|
|
||||||
@require_http_methods(["DELETE"])
|
@require_http_methods(["DELETE"])
|
||||||
def strategy_entry_delete(request, entry_id, strategy_id):
|
def strategy_entry_delete(request, entry_id, strategy_id):
|
||||||
dca_entry = get_object_or_404(DCAEntry, id=entry_id, strategy__id=strategy_id)
|
dca_entry = get_object_or_404(DCAEntry, id=entry_id, strategy__id=strategy_id)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from apps.import_app.schemas import version_1
|
|||||||
|
|
||||||
class ImportProfile(models.Model):
|
class ImportProfile(models.Model):
|
||||||
class Versions(models.IntegerChoices):
|
class Versions(models.IntegerChoices):
|
||||||
VERSION_1 = 1, _("Version") + " 1"
|
VERSION_1 = 1, "Version 1"
|
||||||
|
|
||||||
name = models.CharField(max_length=100, verbose_name=_("Name"), unique=True)
|
name = models.CharField(max_length=100, verbose_name=_("Name"), unique=True)
|
||||||
yaml_config = models.TextField(verbose_name=_("YAML Configuration"))
|
yaml_config = models.TextField(verbose_name=_("YAML Configuration"))
|
||||||
@@ -25,6 +25,10 @@ class ImportProfile(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
ordering = ["name"]
|
ordering = ["name"]
|
||||||
|
|
||||||
|
def get_version_display(self):
|
||||||
|
version_number = self.Versions(self.version).name.split("_")[1]
|
||||||
|
return _("Version {number}").format(number=version_number)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
if self.version and self.version == self.Versions.VERSION_1:
|
if self.version and self.version == self.Versions.VERSION_1:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -5,15 +5,14 @@ from django.contrib.auth.decorators import login_required
|
|||||||
from django.core.files.storage import FileSystemStorage
|
from django.core.files.storage import FileSystemStorage
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import render, get_object_or_404
|
from django.shortcuts import render, get_object_or_404
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
|
||||||
from django.views.decorators.http import require_http_methods
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
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.htmx import only_htmx
|
||||||
from apps.import_app.forms import ImportRunFileUploadForm, ImportProfileForm
|
from apps.import_app.forms import ImportRunFileUploadForm, ImportProfileForm
|
||||||
from apps.import_app.models import ImportRun, ImportProfile
|
from apps.import_app.models import ImportRun, ImportProfile
|
||||||
from apps.import_app.tasks import process_import
|
|
||||||
from apps.import_app.services import PresetService
|
from apps.import_app.services import PresetService
|
||||||
|
from apps.import_app.tasks import process_import
|
||||||
|
|
||||||
|
|
||||||
def import_view(request):
|
def import_view(request):
|
||||||
@@ -66,9 +65,9 @@ def import_profile_list(request):
|
|||||||
@login_required
|
@login_required
|
||||||
@require_http_methods(["GET", "POST"])
|
@require_http_methods(["GET", "POST"])
|
||||||
def import_profile_add(request):
|
def import_profile_add(request):
|
||||||
message = request.GET.get("message", None) or request.POST.get("message", None)
|
message = request.POST.get("message", None)
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST" and request.POST.get("submit"):
|
||||||
form = ImportProfileForm(request.POST)
|
form = ImportProfileForm(request.POST)
|
||||||
|
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
@@ -84,9 +83,9 @@ def import_profile_add(request):
|
|||||||
else:
|
else:
|
||||||
form = ImportProfileForm(
|
form = ImportProfileForm(
|
||||||
initial={
|
initial={
|
||||||
"name": request.GET.get("name"),
|
"name": request.POST.get("name"),
|
||||||
"version": int(request.GET.get("version", 1)),
|
"version": int(request.POST.get("version", 1)),
|
||||||
"yaml_config": request.GET.get("yaml_config"),
|
"yaml_config": request.POST.get("yaml_config"),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -128,7 +127,6 @@ def import_profile_edit(request, profile_id):
|
|||||||
|
|
||||||
@only_htmx
|
@only_htmx
|
||||||
@login_required
|
@login_required
|
||||||
@csrf_exempt
|
|
||||||
@require_http_methods(["DELETE"])
|
@require_http_methods(["DELETE"])
|
||||||
def import_profile_delete(request, profile_id):
|
def import_profile_delete(request, profile_id):
|
||||||
profile = ImportProfile.objects.get(id=profile_id)
|
profile = ImportProfile.objects.get(id=profile_id)
|
||||||
@@ -213,7 +211,6 @@ def import_run_add(request, profile_id):
|
|||||||
|
|
||||||
@only_htmx
|
@only_htmx
|
||||||
@login_required
|
@login_required
|
||||||
@csrf_exempt
|
|
||||||
@require_http_methods(["DELETE"])
|
@require_http_methods(["DELETE"])
|
||||||
def import_run_delete(request, profile_id, run_id):
|
def import_run_delete(request, profile_id, run_id):
|
||||||
run = ImportRun.objects.get(profile__id=profile_id, id=run_id)
|
run = ImportRun.objects.get(profile__id=profile_id, id=run_id)
|
||||||
|
|||||||
@@ -52,19 +52,4 @@ urlpatterns = [
|
|||||||
views.transaction_rule_action_delete,
|
views.transaction_rule_action_delete,
|
||||||
name="transaction_rule_action_delete",
|
name="transaction_rule_action_delete",
|
||||||
),
|
),
|
||||||
# path(
|
|
||||||
# "rules/<int:installment_plan_id>/transactions/",
|
|
||||||
# views.installment_plan_transactions,
|
|
||||||
# name="rule_view",
|
|
||||||
# ),
|
|
||||||
# path(
|
|
||||||
# "rules/<int:installment_plan_id>/edit/",
|
|
||||||
# views.installment_plan_edit,
|
|
||||||
# name="rule_edit",
|
|
||||||
# ),
|
|
||||||
# path(
|
|
||||||
# "rules/<int:installment_plan_id>/delete/",
|
|
||||||
# views.installment_plan_delete,
|
|
||||||
# name="rule_delete",
|
|
||||||
# ),
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ from django.contrib.auth.decorators import login_required
|
|||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import render, get_object_or_404, redirect
|
from django.shortcuts import render, get_object_or_404, redirect
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
|
||||||
from django.views.decorators.http import require_http_methods
|
from django.views.decorators.http import require_http_methods
|
||||||
|
|
||||||
from apps.common.decorators.htmx import only_htmx
|
from apps.common.decorators.htmx import only_htmx
|
||||||
@@ -118,7 +117,6 @@ def transaction_rule_view(request, transaction_rule_id):
|
|||||||
|
|
||||||
@only_htmx
|
@only_htmx
|
||||||
@login_required
|
@login_required
|
||||||
@csrf_exempt
|
|
||||||
@require_http_methods(["DELETE"])
|
@require_http_methods(["DELETE"])
|
||||||
def transaction_rule_delete(request, transaction_rule_id):
|
def transaction_rule_delete(request, transaction_rule_id):
|
||||||
transaction_rule = get_object_or_404(TransactionRule, id=transaction_rule_id)
|
transaction_rule = get_object_or_404(TransactionRule, id=transaction_rule_id)
|
||||||
@@ -201,7 +199,6 @@ def transaction_rule_action_edit(request, transaction_rule_action_id):
|
|||||||
|
|
||||||
@only_htmx
|
@only_htmx
|
||||||
@login_required
|
@login_required
|
||||||
@csrf_exempt
|
|
||||||
@require_http_methods(["DELETE"])
|
@require_http_methods(["DELETE"])
|
||||||
def transaction_rule_action_delete(request, transaction_rule_action_id):
|
def transaction_rule_action_delete(request, transaction_rule_action_id):
|
||||||
transaction_rule_action = get_object_or_404(
|
transaction_rule_action = get_object_or_404(
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from crispy_bootstrap5.bootstrap5 import Switch
|
from crispy_bootstrap5.bootstrap5 import Switch, BS5Accordion
|
||||||
from crispy_forms.bootstrap import FormActions
|
from crispy_forms.bootstrap import FormActions, AccordionGroup
|
||||||
from crispy_forms.helper import FormHelper
|
from crispy_forms.helper import FormHelper
|
||||||
from crispy_forms.layout import (
|
from crispy_forms.layout import (
|
||||||
Layout,
|
Layout,
|
||||||
@@ -115,7 +115,7 @@ class TransactionForm(forms.ModelForm):
|
|||||||
"type",
|
"type",
|
||||||
template="transactions/widgets/income_expense_toggle_buttons.html",
|
template="transactions/widgets/income_expense_toggle_buttons.html",
|
||||||
),
|
),
|
||||||
Switch("is_paid"),
|
Field("is_paid", template="transactions/widgets/paid_toggle_button.html"),
|
||||||
Row(
|
Row(
|
||||||
Column("account", css_class="form-group col-md-6 mb-0"),
|
Column("account", css_class="form-group col-md-6 mb-0"),
|
||||||
Column("entities", css_class="form-group col-md-6 mb-0"),
|
Column("entities", css_class="form-group col-md-6 mb-0"),
|
||||||
@@ -136,6 +136,46 @@ class TransactionForm(forms.ModelForm):
|
|||||||
"notes",
|
"notes",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.helper_simple = FormHelper()
|
||||||
|
self.helper_simple.form_tag = False
|
||||||
|
self.helper_simple.form_method = "post"
|
||||||
|
self.helper_simple.layout = Layout(
|
||||||
|
Field(
|
||||||
|
"type",
|
||||||
|
template="transactions/widgets/income_expense_toggle_buttons.html",
|
||||||
|
),
|
||||||
|
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",
|
||||||
|
),
|
||||||
|
"description",
|
||||||
|
Field("amount", inputmode="decimal"),
|
||||||
|
BS5Accordion(
|
||||||
|
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",
|
||||||
|
),
|
||||||
|
"notes",
|
||||||
|
active=False,
|
||||||
|
),
|
||||||
|
flush=False,
|
||||||
|
always_open=False,
|
||||||
|
css_class="mb-3",
|
||||||
|
),
|
||||||
|
FormActions(
|
||||||
|
NoClassSubmit(
|
||||||
|
"submit", _("Add"), css_class="btn btn-outline-primary w-100"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
self.fields["reference_date"].required = False
|
self.fields["reference_date"].required = False
|
||||||
self.fields["date"].widget = AirDatePickerInput(clear_button=False, user=user)
|
self.fields["date"].widget = AirDatePickerInput(clear_button=False, user=user)
|
||||||
|
|
||||||
@@ -183,6 +223,43 @@ class TransactionForm(forms.ModelForm):
|
|||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
class BulkEditTransactionForm(TransactionForm):
|
||||||
|
is_paid = forms.NullBooleanField(required=False)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
# Make all fields optional
|
||||||
|
for field_name, field in self.fields.items():
|
||||||
|
field.required = False
|
||||||
|
|
||||||
|
del self.helper.layout[-1] # Remove button
|
||||||
|
del self.helper.layout[0:2] # Remove type, is_paid field
|
||||||
|
|
||||||
|
self.helper.layout.insert(
|
||||||
|
0,
|
||||||
|
Field(
|
||||||
|
"type",
|
||||||
|
template="transactions/widgets/unselectable_income_expense_toggle_buttons.html",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.helper.layout.insert(
|
||||||
|
1,
|
||||||
|
Field(
|
||||||
|
"is_paid",
|
||||||
|
template="transactions/widgets/unselectable_paid_toggle_button.html",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.helper.layout.append(
|
||||||
|
FormActions(
|
||||||
|
NoClassSubmit(
|
||||||
|
"submit", _("Update"), css_class="btn btn-outline-primary w-100"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TransferForm(forms.Form):
|
class TransferForm(forms.Form):
|
||||||
from_account = forms.ModelChoiceField(
|
from_account = forms.ModelChoiceField(
|
||||||
queryset=Account.objects.filter(is_archived=False),
|
queryset=Account.objects.filter(is_archived=False),
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ urlpatterns = [
|
|||||||
name="transactions_all_summary",
|
name="transactions_all_summary",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"transactions/actions/pay",
|
"transactions/actions/pay/",
|
||||||
views.bulk_pay_transactions,
|
views.bulk_pay_transactions,
|
||||||
name="transactions_bulk_pay",
|
name="transactions_bulk_pay",
|
||||||
),
|
),
|
||||||
@@ -27,32 +27,47 @@ urlpatterns = [
|
|||||||
name="transactions_bulk_delete",
|
name="transactions_bulk_delete",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"transaction/<int:transaction_id>/pay",
|
"transactions/actions/duplicate/",
|
||||||
|
views.bulk_clone_transactions,
|
||||||
|
name="transactions_bulk_clone",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"transaction/<int:transaction_id>/pay/",
|
||||||
views.transaction_pay,
|
views.transaction_pay,
|
||||||
name="transaction_pay",
|
name="transaction_pay",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"transaction/<int:transaction_id>/delete",
|
"transaction/<int:transaction_id>/delete/",
|
||||||
views.transaction_delete,
|
views.transaction_delete,
|
||||||
name="transaction_delete",
|
name="transaction_delete",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"transaction/<int:transaction_id>/edit",
|
"transaction/<int:transaction_id>/edit/",
|
||||||
views.transaction_edit,
|
views.transaction_edit,
|
||||||
name="transaction_edit",
|
name="transaction_edit",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"transaction/<int:transaction_id>/clone",
|
"transactions/bulk-edit/",
|
||||||
|
views.transactions_bulk_edit,
|
||||||
|
name="transactions_bulk_edit",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"transaction/<int:transaction_id>/clone/",
|
||||||
views.transaction_clone,
|
views.transaction_clone,
|
||||||
name="transaction_clone",
|
name="transaction_clone",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"transaction/add",
|
"transaction/add/",
|
||||||
views.transaction_add,
|
views.transaction_add,
|
||||||
name="transaction_add",
|
name="transaction_add",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"transactions/transfer",
|
"add/",
|
||||||
|
views.transaction_simple_add,
|
||||||
|
name="transaction_simple_add",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"transactions/transfer/",
|
||||||
views.transactions_transfer,
|
views.transactions_transfer,
|
||||||
name="transactions_transfer",
|
name="transactions_transfer",
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
|
from copy import deepcopy
|
||||||
|
|
||||||
|
from django.contrib import messages
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
|
from django.utils.translation import gettext_lazy as _, ngettext_lazy
|
||||||
|
|
||||||
from apps.common.decorators.htmx import only_htmx
|
from apps.common.decorators.htmx import only_htmx
|
||||||
from apps.transactions.models import Transaction
|
from apps.transactions.models import Transaction
|
||||||
@@ -9,7 +13,19 @@ from apps.transactions.models import Transaction
|
|||||||
@login_required
|
@login_required
|
||||||
def bulk_pay_transactions(request):
|
def bulk_pay_transactions(request):
|
||||||
selected_transactions = request.GET.getlist("transactions", [])
|
selected_transactions = request.GET.getlist("transactions", [])
|
||||||
Transaction.objects.filter(id__in=selected_transactions).update(is_paid=True)
|
transactions = Transaction.objects.filter(id__in=selected_transactions)
|
||||||
|
count = transactions.count()
|
||||||
|
transactions.update(is_paid=True)
|
||||||
|
|
||||||
|
messages.success(
|
||||||
|
request,
|
||||||
|
ngettext_lazy(
|
||||||
|
"%(count)s transaction marked as paid",
|
||||||
|
"%(count)s transactions marked as paid",
|
||||||
|
count,
|
||||||
|
)
|
||||||
|
% {"count": count},
|
||||||
|
)
|
||||||
|
|
||||||
return HttpResponse(
|
return HttpResponse(
|
||||||
status=204,
|
status=204,
|
||||||
@@ -21,7 +37,19 @@ def bulk_pay_transactions(request):
|
|||||||
@login_required
|
@login_required
|
||||||
def bulk_unpay_transactions(request):
|
def bulk_unpay_transactions(request):
|
||||||
selected_transactions = request.GET.getlist("transactions", [])
|
selected_transactions = request.GET.getlist("transactions", [])
|
||||||
Transaction.objects.filter(id__in=selected_transactions).update(is_paid=False)
|
transactions = Transaction.objects.filter(id__in=selected_transactions)
|
||||||
|
count = transactions.count()
|
||||||
|
transactions.update(is_paid=False)
|
||||||
|
|
||||||
|
messages.success(
|
||||||
|
request,
|
||||||
|
ngettext_lazy(
|
||||||
|
"%(count)s transaction marked as not paid",
|
||||||
|
"%(count)s transactions marked as not paid",
|
||||||
|
count,
|
||||||
|
)
|
||||||
|
% {"count": count},
|
||||||
|
)
|
||||||
|
|
||||||
return HttpResponse(
|
return HttpResponse(
|
||||||
status=204,
|
status=204,
|
||||||
@@ -33,7 +61,54 @@ def bulk_unpay_transactions(request):
|
|||||||
@login_required
|
@login_required
|
||||||
def bulk_delete_transactions(request):
|
def bulk_delete_transactions(request):
|
||||||
selected_transactions = request.GET.getlist("transactions", [])
|
selected_transactions = request.GET.getlist("transactions", [])
|
||||||
Transaction.objects.filter(id__in=selected_transactions).delete()
|
transactions = Transaction.objects.filter(id__in=selected_transactions)
|
||||||
|
count = transactions.count()
|
||||||
|
transactions.delete()
|
||||||
|
|
||||||
|
messages.success(
|
||||||
|
request,
|
||||||
|
ngettext_lazy(
|
||||||
|
"%(count)s transaction deleted successfully",
|
||||||
|
"%(count)s transactions deleted successfully",
|
||||||
|
count,
|
||||||
|
)
|
||||||
|
% {"count": count},
|
||||||
|
)
|
||||||
|
|
||||||
|
return HttpResponse(
|
||||||
|
status=204,
|
||||||
|
headers={"HX-Trigger": "updated"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@only_htmx
|
||||||
|
@login_required
|
||||||
|
def bulk_clone_transactions(request):
|
||||||
|
selected_transactions = request.GET.getlist("transactions", [])
|
||||||
|
transactions = Transaction.objects.filter(id__in=selected_transactions)
|
||||||
|
count = transactions.count()
|
||||||
|
|
||||||
|
for transaction in transactions:
|
||||||
|
new_transaction = deepcopy(transaction)
|
||||||
|
new_transaction.pk = None
|
||||||
|
new_transaction.installment_plan = None
|
||||||
|
new_transaction.installment_id = None
|
||||||
|
new_transaction.recurring_transaction = None
|
||||||
|
new_transaction.internal_id = None
|
||||||
|
new_transaction.save()
|
||||||
|
|
||||||
|
new_transaction.tags.add(*transaction.tags.all())
|
||||||
|
new_transaction.entities.add(*transaction.entities.all())
|
||||||
|
|
||||||
|
messages.success(
|
||||||
|
request,
|
||||||
|
ngettext_lazy(
|
||||||
|
"%(count)s transaction duplicated successfully",
|
||||||
|
"%(count)s transactions duplicated successfully",
|
||||||
|
count,
|
||||||
|
)
|
||||||
|
% {"count": count},
|
||||||
|
)
|
||||||
|
|
||||||
return HttpResponse(
|
return HttpResponse(
|
||||||
status=204,
|
status=204,
|
||||||
|
|||||||
@@ -2,9 +2,7 @@ from django.contrib import messages
|
|||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import render, get_object_or_404
|
from django.shortcuts import render, get_object_or_404
|
||||||
from django.urls import reverse
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
|
||||||
from django.views.decorators.http import require_http_methods
|
from django.views.decorators.http import require_http_methods
|
||||||
|
|
||||||
from apps.common.decorators.htmx import only_htmx
|
from apps.common.decorators.htmx import only_htmx
|
||||||
@@ -111,7 +109,6 @@ def category_edit(request, category_id):
|
|||||||
|
|
||||||
@only_htmx
|
@only_htmx
|
||||||
@login_required
|
@login_required
|
||||||
@csrf_exempt
|
|
||||||
@require_http_methods(["DELETE"])
|
@require_http_methods(["DELETE"])
|
||||||
def category_delete(request, category_id):
|
def category_delete(request, category_id):
|
||||||
category = get_object_or_404(TransactionCategory, id=category_id)
|
category = get_object_or_404(TransactionCategory, id=category_id)
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ from django.contrib.auth.decorators import login_required
|
|||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import render, get_object_or_404
|
from django.shortcuts import render, get_object_or_404
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
|
||||||
from django.views.decorators.http import require_http_methods
|
from django.views.decorators.http import require_http_methods
|
||||||
|
|
||||||
from apps.common.decorators.htmx import only_htmx
|
from apps.common.decorators.htmx import only_htmx
|
||||||
@@ -110,7 +109,6 @@ def entity_edit(request, entity_id):
|
|||||||
|
|
||||||
@only_htmx
|
@only_htmx
|
||||||
@login_required
|
@login_required
|
||||||
@csrf_exempt
|
|
||||||
@require_http_methods(["DELETE"])
|
@require_http_methods(["DELETE"])
|
||||||
def entity_delete(request, entity_id):
|
def entity_delete(request, entity_id):
|
||||||
entity = get_object_or_404(TransactionEntity, id=entity_id)
|
entity = get_object_or_404(TransactionEntity, id=entity_id)
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ from django.http import HttpResponse
|
|||||||
from django.shortcuts import render, get_object_or_404
|
from django.shortcuts import render, get_object_or_404
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
|
||||||
from django.views.decorators.http import require_http_methods
|
from django.views.decorators.http import require_http_methods
|
||||||
|
|
||||||
from apps.common.decorators.htmx import only_htmx
|
from apps.common.decorators.htmx import only_htmx
|
||||||
@@ -152,7 +151,6 @@ def installment_plan_refresh(request, installment_plan_id):
|
|||||||
|
|
||||||
@only_htmx
|
@only_htmx
|
||||||
@login_required
|
@login_required
|
||||||
@csrf_exempt
|
|
||||||
@require_http_methods(["DELETE"])
|
@require_http_methods(["DELETE"])
|
||||||
def installment_plan_delete(request, installment_plan_id):
|
def installment_plan_delete(request, installment_plan_id):
|
||||||
installment_plan = get_object_or_404(InstallmentPlan, id=installment_plan_id)
|
installment_plan = get_object_or_404(InstallmentPlan, id=installment_plan_id)
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
from dateutil.relativedelta import relativedelta
|
from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
@@ -7,7 +6,6 @@ from django.http import HttpResponse
|
|||||||
from django.shortcuts import render, get_object_or_404
|
from django.shortcuts import render, get_object_or_404
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
|
||||||
from django.views.decorators.http import require_http_methods
|
from django.views.decorators.http import require_http_methods
|
||||||
|
|
||||||
from apps.common.decorators.htmx import only_htmx
|
from apps.common.decorators.htmx import only_htmx
|
||||||
@@ -230,7 +228,6 @@ def recurring_transaction_finish(request, recurring_transaction_id):
|
|||||||
|
|
||||||
@only_htmx
|
@only_htmx
|
||||||
@login_required
|
@login_required
|
||||||
@csrf_exempt
|
|
||||||
@require_http_methods(["DELETE"])
|
@require_http_methods(["DELETE"])
|
||||||
def recurring_transaction_delete(request, recurring_transaction_id):
|
def recurring_transaction_delete(request, recurring_transaction_id):
|
||||||
recurring_transaction = get_object_or_404(
|
recurring_transaction = get_object_or_404(
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ from django.contrib.auth.decorators import login_required
|
|||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import render, get_object_or_404
|
from django.shortcuts import render, get_object_or_404
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
|
||||||
from django.views.decorators.http import require_http_methods
|
from django.views.decorators.http import require_http_methods
|
||||||
|
|
||||||
from apps.common.decorators.htmx import only_htmx
|
from apps.common.decorators.htmx import only_htmx
|
||||||
@@ -110,7 +109,6 @@ def tag_edit(request, tag_id):
|
|||||||
|
|
||||||
@only_htmx
|
@only_htmx
|
||||||
@login_required
|
@login_required
|
||||||
@csrf_exempt
|
|
||||||
@require_http_methods(["DELETE"])
|
@require_http_methods(["DELETE"])
|
||||||
def tag_delete(request, tag_id):
|
def tag_delete(request, tag_id):
|
||||||
tag = get_object_or_404(TransactionTag, id=tag_id)
|
tag = get_object_or_404(TransactionTag, id=tag_id)
|
||||||
|
|||||||
@@ -7,15 +7,18 @@ from django.core.paginator import Paginator
|
|||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import render, get_object_or_404
|
from django.shortcuts import render, get_object_or_404
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _, ngettext_lazy
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
|
||||||
from django.views.decorators.http import require_http_methods
|
from django.views.decorators.http import require_http_methods
|
||||||
|
|
||||||
from apps.common.decorators.htmx import only_htmx
|
from apps.common.decorators.htmx import only_htmx
|
||||||
from apps.common.utils.dicts import remove_falsey_entries
|
from apps.common.utils.dicts import remove_falsey_entries
|
||||||
from apps.rules.signals import transaction_created
|
from apps.rules.signals import transaction_created, transaction_updated
|
||||||
from apps.transactions.filters import TransactionsFilter
|
from apps.transactions.filters import TransactionsFilter
|
||||||
from apps.transactions.forms import TransactionForm, TransferForm
|
from apps.transactions.forms import (
|
||||||
|
TransactionForm,
|
||||||
|
TransferForm,
|
||||||
|
BulkEditTransactionForm,
|
||||||
|
)
|
||||||
from apps.transactions.models import Transaction
|
from apps.transactions.models import Transaction
|
||||||
from apps.transactions.utils.calculations import (
|
from apps.transactions.utils.calculations import (
|
||||||
calculate_currency_totals,
|
calculate_currency_totals,
|
||||||
@@ -66,6 +69,50 @@ def transaction_add(request):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@require_http_methods(["GET", "POST"])
|
||||||
|
def transaction_simple_add(request):
|
||||||
|
month = int(request.GET.get("month", timezone.localdate(timezone.now()).month))
|
||||||
|
year = int(request.GET.get("year", timezone.localdate(timezone.now()).year))
|
||||||
|
transaction_type = Transaction.Type(request.GET.get("type", "IN"))
|
||||||
|
|
||||||
|
now = timezone.localdate(timezone.now())
|
||||||
|
expected_date = datetime.datetime(
|
||||||
|
day=now.day if month == now.month and year == now.year else 1,
|
||||||
|
month=month,
|
||||||
|
year=year,
|
||||||
|
).date()
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
form = TransactionForm(request.POST, user=request.user)
|
||||||
|
if form.is_valid():
|
||||||
|
form.save()
|
||||||
|
messages.success(request, _("Transaction added successfully"))
|
||||||
|
|
||||||
|
form = TransactionForm(
|
||||||
|
user=request.user,
|
||||||
|
initial={
|
||||||
|
"date": expected_date,
|
||||||
|
"type": transaction_type,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
form = TransactionForm(
|
||||||
|
user=request.user,
|
||||||
|
initial={
|
||||||
|
"date": expected_date,
|
||||||
|
"type": transaction_type,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"transactions/pages/add.html",
|
||||||
|
{"form": form},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@only_htmx
|
@only_htmx
|
||||||
@login_required
|
@login_required
|
||||||
@require_http_methods(["GET", "POST"])
|
@require_http_methods(["GET", "POST"])
|
||||||
@@ -92,6 +139,62 @@ def transaction_edit(request, transaction_id, **kwargs):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@only_htmx
|
||||||
|
@login_required
|
||||||
|
@require_http_methods(["GET", "POST"])
|
||||||
|
def transactions_bulk_edit(request):
|
||||||
|
# Get selected transaction IDs from the URL parameter
|
||||||
|
transaction_ids = request.GET.getlist("transactions") or request.POST.getlist(
|
||||||
|
"transactions"
|
||||||
|
)
|
||||||
|
# Load the selected transactions
|
||||||
|
transactions = Transaction.objects.filter(id__in=transaction_ids)
|
||||||
|
count = transactions.count()
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
form = BulkEditTransactionForm(request.POST, user=request.user)
|
||||||
|
if form.is_valid():
|
||||||
|
# Apply changes from the form to all selected transactions
|
||||||
|
for transaction in transactions:
|
||||||
|
for field_name, value in form.cleaned_data.items():
|
||||||
|
if value or isinstance(
|
||||||
|
value, bool
|
||||||
|
): # Only update fields that have been filled in the form
|
||||||
|
if field_name == "tags":
|
||||||
|
transaction.tags.set(value)
|
||||||
|
elif field_name == "entities":
|
||||||
|
transaction.entities.set(value)
|
||||||
|
else:
|
||||||
|
setattr(transaction, field_name, value)
|
||||||
|
|
||||||
|
transaction.save()
|
||||||
|
transaction_updated.send(sender=transaction)
|
||||||
|
|
||||||
|
messages.success(
|
||||||
|
request,
|
||||||
|
ngettext_lazy(
|
||||||
|
"%(count)s transaction updated successfully",
|
||||||
|
"%(count)s transactions updated successfully",
|
||||||
|
count,
|
||||||
|
)
|
||||||
|
% {"count": count},
|
||||||
|
)
|
||||||
|
return HttpResponse(
|
||||||
|
status=204,
|
||||||
|
headers={"HX-Trigger": "updated, hide_offcanvas"},
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
form = BulkEditTransactionForm(
|
||||||
|
initial={"is_paid": None, "type": None}, user=request.user
|
||||||
|
)
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"form": form,
|
||||||
|
"transactions": transactions,
|
||||||
|
}
|
||||||
|
return render(request, "transactions/fragments/bulk_edit.html", context)
|
||||||
|
|
||||||
|
|
||||||
@only_htmx
|
@only_htmx
|
||||||
@login_required
|
@login_required
|
||||||
@require_http_methods(["GET", "POST"])
|
@require_http_methods(["GET", "POST"])
|
||||||
@@ -102,6 +205,7 @@ def transaction_clone(request, transaction_id, **kwargs):
|
|||||||
new_transaction.installment_plan = None
|
new_transaction.installment_plan = None
|
||||||
new_transaction.installment_id = None
|
new_transaction.installment_id = None
|
||||||
new_transaction.recurring_transaction = None
|
new_transaction.recurring_transaction = None
|
||||||
|
new_transaction.internal_id = None
|
||||||
new_transaction.save()
|
new_transaction.save()
|
||||||
|
|
||||||
new_transaction.tags.add(*transaction.tags.all())
|
new_transaction.tags.add(*transaction.tags.all())
|
||||||
@@ -143,7 +247,6 @@ def transaction_clone(request, transaction_id, **kwargs):
|
|||||||
|
|
||||||
@only_htmx
|
@only_htmx
|
||||||
@login_required
|
@login_required
|
||||||
@csrf_exempt
|
|
||||||
@require_http_methods(["DELETE"])
|
@require_http_methods(["DELETE"])
|
||||||
def transaction_delete(request, transaction_id, **kwargs):
|
def transaction_delete(request, transaction_id, **kwargs):
|
||||||
transaction = get_object_or_404(Transaction, id=transaction_id)
|
transaction = get_object_or_404(Transaction, id=transaction_id)
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.1.5 on 2025-01-24 19:29
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('users', '0014_alter_usersettings_date_format_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='usersettings',
|
||||||
|
name='language',
|
||||||
|
field=models.CharField(choices=[('auto', 'Auto'), ('en', 'English'), ('pt-br', 'Português (Brasil)')], default='auto', max_length=10, verbose_name='Language'),
|
||||||
|
),
|
||||||
|
]
|
||||||
File diff suppressed because it is too large
Load Diff
2377
app/locale/nl/LC_MESSAGES/django.po.orig
Normal file
2377
app/locale/nl/LC_MESSAGES/django.po.orig
Normal file
File diff suppressed because it is too large
Load Diff
2377
app/locale/nl/LC_MESSAGES/django_BACKUP_1584.po
Normal file
2377
app/locale/nl/LC_MESSAGES/django_BACKUP_1584.po
Normal file
File diff suppressed because it is too large
Load Diff
2062
app/locale/nl/LC_MESSAGES/django_BASE_1584.po
Normal file
2062
app/locale/nl/LC_MESSAGES/django_BASE_1584.po
Normal file
File diff suppressed because it is too large
Load Diff
2094
app/locale/nl/LC_MESSAGES/django_LOCAL_1584.po
Normal file
2094
app/locale/nl/LC_MESSAGES/django_LOCAL_1584.po
Normal file
File diff suppressed because it is too large
Load Diff
2299
app/locale/nl/LC_MESSAGES/django_REMOTE_1584.po
Normal file
2299
app/locale/nl/LC_MESSAGES/django_REMOTE_1584.po
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -2,46 +2,76 @@
|
|||||||
<div class="tw-sticky tw-bottom-4 tw-left-0 tw-right-0 tw-z-50 tw-hidden mx-auto tw-w-fit" id="actions-bar"
|
<div class="tw-sticky tw-bottom-4 tw-left-0 tw-right-0 tw-z-50 tw-hidden mx-auto tw-w-fit" id="actions-bar"
|
||||||
_="on change from #transactions-list or htmx:afterSettle from window
|
_="on change from #transactions-list or htmx:afterSettle from window
|
||||||
if no <input[type='checkbox']:checked/> in #transactions-list
|
if no <input[type='checkbox']:checked/> in #transactions-list
|
||||||
add .tw-hidden to #actions-bar
|
add .slide-in-bottom-reverse then settle
|
||||||
|
then add .tw-hidden to #actions-bar
|
||||||
|
then remove .slide-in-bottom-reverse
|
||||||
else
|
else
|
||||||
remove .tw-hidden from #actions-bar
|
remove .tw-hidden from #actions-bar
|
||||||
then trigger selected_transactions_updated
|
then trigger selected_transactions_updated
|
||||||
end
|
end
|
||||||
end">
|
end">
|
||||||
<div class="card slide-in-left">
|
<div class="card slide-in-bottom">
|
||||||
<div class="card-body p-2">
|
<div class="card-body p-2 d-flex justify-content-between align-items-center gap-3">
|
||||||
{% spaceless %}
|
{% spaceless %}
|
||||||
<div class="btn-group" role="group">
|
<div class="dropdown">
|
||||||
<button class="btn btn-secondary btn-sm"
|
<button class="btn btn-secondary btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown"
|
||||||
data-bs-toggle="tooltip"
|
aria-expanded="false">
|
||||||
data-bs-title="{% translate 'Select All' %}"
|
<i class="fa-regular fa-square-check fa-fw"></i>
|
||||||
_="on click set <#transactions-list input[type='checkbox']/>'s checked to true then call me.blur() then trigger change">
|
|
||||||
<i class="fa-regular fa-square-check tw-text-green-400"></i>
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-secondary btn-sm"
|
|
||||||
data-bs-toggle="tooltip"
|
|
||||||
data-bs-title="{% translate 'Unselect All' %}"
|
|
||||||
_="on click set <#transactions-list input[type='checkbox']/>'s checked to false then call me.blur() then trigger change">
|
|
||||||
<i class="fa-regular fa-square tw-text-red-400"></i>
|
|
||||||
</button>
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li>
|
||||||
|
<div class="dropdown-item px-3 tw-cursor-pointer"
|
||||||
|
_="on click set <#transactions-list input[type='checkbox']/>'s checked to true then call me.blur() then trigger change">
|
||||||
|
<i class="fa-regular fa-square-check tw-text-green-400 me-3"></i>{% translate 'Select All' %}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div class="dropdown-item px-3 tw-cursor-pointer"
|
||||||
|
_="on click set <#transactions-list input[type='checkbox']/>'s checked to false then call me.blur() then trigger change">
|
||||||
|
<i class="fa-regular fa-square tw-text-red-400 me-3"></i>{% translate 'Unselect All' %}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="vr mx-3 tw-align-middle"></div>
|
<div class="vr tw-align-middle"></div>
|
||||||
<div class="btn-group me-3" role="group">
|
<div class="btn-group">
|
||||||
<button class="btn btn-secondary btn-sm"
|
<button class="btn btn-secondary btn-sm"
|
||||||
hx-get="{% url 'transactions_bulk_pay' %}"
|
hx-get="{% url 'transactions_bulk_edit' %}"
|
||||||
|
hx-target="#generic-offcanvas"
|
||||||
hx-include=".transaction"
|
hx-include=".transaction"
|
||||||
data-bs-toggle="tooltip"
|
data-bs-toggle="tooltip"
|
||||||
data-bs-title="{% translate 'Mark as paid' %}">
|
data-bs-title="{% translate 'Edit' %}">
|
||||||
<i class="fa-regular fa-circle-check tw-text-green-400"></i>
|
<i class="fa-solid fa-pencil"></i>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-secondary btn-sm"
|
<button type="button" class="btn btn-sm btn-secondary dropdown-toggle dropdown-toggle-split"
|
||||||
hx-get="{% url 'transactions_bulk_unpay' %}"
|
data-bs-toggle="dropdown" aria-expanded="false" data-bs-auto-close="outside">
|
||||||
hx-include=".transaction"
|
<span class="visually-hidden">{% trans "Toggle Dropdown" %}</span>
|
||||||
data-bs-toggle="tooltip"
|
|
||||||
data-bs-title="{% translate 'Mark as unpaid' %}">
|
|
||||||
<i class="fa-regular fa-circle tw-text-red-400"></i>
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li>
|
||||||
|
<div class="dropdown-item px-3 tw-cursor-pointer"
|
||||||
|
hx-get="{% url 'transactions_bulk_unpay' %}"
|
||||||
|
hx-include=".transaction">
|
||||||
|
<i class="fa-regular fa-circle tw-text-red-400 fa-fw me-3"></i>{% translate 'Mark as unpaid' %}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div class="dropdown-item px-3 tw-cursor-pointer"
|
||||||
|
hx-get="{% url 'transactions_bulk_pay' %}"
|
||||||
|
hx-include=".transaction">
|
||||||
|
<i class="fa-regular fa-circle-check tw-text-green-400 fa-fw me-3"></i>{% translate 'Mark as paid' %}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
<button class="btn btn-secondary btn-sm"
|
||||||
|
hx-get="{% url 'transactions_bulk_clone' %}"
|
||||||
|
hx-include=".transaction"
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
data-bs-title="{% translate 'Duplicate' %}">
|
||||||
|
<i class="fa-solid fa-clone fa-fw"></i>
|
||||||
|
</button>
|
||||||
<button class="btn btn-secondary btn-sm"
|
<button class="btn btn-secondary btn-sm"
|
||||||
hx-get="{% url 'transactions_bulk_delete' %}"
|
hx-get="{% url 'transactions_bulk_delete' %}"
|
||||||
hx-include=".transaction"
|
hx-include=".transaction"
|
||||||
@@ -55,9 +85,9 @@
|
|||||||
_="install prompt_swal">
|
_="install prompt_swal">
|
||||||
<i class="fa-solid fa-trash text-danger"></i>
|
<i class="fa-solid fa-trash text-danger"></i>
|
||||||
</button>
|
</button>
|
||||||
<div class="vr mx-3 tw-align-middle"></div>
|
<div class="vr tw-align-middle"></div>
|
||||||
<div class="btn-group"
|
<div class="btn-group"
|
||||||
_="on selected_transactions_updated from #actions-bar
|
_="on selected_transactions_updated from #actions-bar
|
||||||
set realTotal to math.bignumber(0)
|
set realTotal to math.bignumber(0)
|
||||||
set flatTotal to math.bignumber(0)
|
set flatTotal to math.bignumber(0)
|
||||||
set transactions to <.transaction:has(input[name='transactions']:checked)/>
|
set transactions to <.transaction:has(input[name='transactions']:checked)/>
|
||||||
@@ -93,8 +123,7 @@
|
|||||||
put Math.min.apply(Math, realAmountValues).toLocaleString(undefined, {minimumFractionDigits: 0, maximumFractionDigits: 40}) into #calc-menu-min's innerText
|
put Math.min.apply(Math, realAmountValues).toLocaleString(undefined, {minimumFractionDigits: 0, maximumFractionDigits: 40}) into #calc-menu-min's innerText
|
||||||
put mean.toLocaleString(undefined, {minimumFractionDigits: 0, maximumFractionDigits: 40}) into #calc-menu-mean's innerText
|
put mean.toLocaleString(undefined, {minimumFractionDigits: 0, maximumFractionDigits: 40}) into #calc-menu-mean's innerText
|
||||||
put flatAmountValues.length.toLocaleString(undefined, {minimumFractionDigits: 0, maximumFractionDigits: 40}) into #calc-menu-count's innerText
|
put flatAmountValues.length.toLocaleString(undefined, {minimumFractionDigits: 0, maximumFractionDigits: 40}) into #calc-menu-count's innerText
|
||||||
end"
|
end">
|
||||||
>
|
|
||||||
<button class="btn btn-secondary btn-sm" _="on click
|
<button class="btn btn-secondary btn-sm" _="on click
|
||||||
set original_value to #real-total-front's innerText
|
set original_value to #real-total-front's innerText
|
||||||
writeText(original_value) on navigator.clipboard
|
writeText(original_value) on navigator.clipboard
|
||||||
@@ -102,8 +131,8 @@
|
|||||||
wait 1s
|
wait 1s
|
||||||
put original_value into #real-total-front's innerText
|
put original_value into #real-total-front's innerText
|
||||||
end">
|
end">
|
||||||
<i class="fa-solid fa-plus fa-fw me-2 text-primary"></i>
|
<i class="fa-solid fa-plus fa-fw me-md-2 text-primary"></i>
|
||||||
<span id="real-total-front">0</span>
|
<span class="d-none d-md-inline-block" id="real-total-front">0</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-sm btn-secondary dropdown-toggle dropdown-toggle-split"
|
<button type="button" class="btn btn-sm btn-secondary dropdown-toggle dropdown-toggle-split"
|
||||||
data-bs-toggle="dropdown" aria-expanded="false" data-bs-auto-close="outside">
|
data-bs-toggle="dropdown" aria-expanded="false" data-bs-auto-close="outside">
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
{% for preset in presets %}
|
{% for preset in presets %}
|
||||||
<a class="text-decoration-none"
|
<a class="text-decoration-none"
|
||||||
role="button"
|
role="button"
|
||||||
hx-get="{% url 'import_profiles_add' %}"
|
hx-post="{% url 'import_profiles_add' %}"
|
||||||
hx-vals='{"yaml_config": {{ preset.config }}, "name": "{{ preset.name }}", "version": "{{ preset.schema_version }}", "message": {{ preset.message }}}'
|
hx-vals='{"yaml_config": {{ preset.config }}, "name": "{{ preset.name }}", "version": "{{ preset.schema_version }}", "message": {{ preset.message }}}'
|
||||||
hx-target="#generic-offcanvas">
|
hx-target="#generic-offcanvas">
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load crispy_forms_tags %}
|
{% load crispy_forms_tags %}
|
||||||
|
|
||||||
{% block title %}{% translate 'Runs for ' %}{{ profile.name }}{% endblock %}
|
{% block title %}{% translate 'Runs for' %} {{ profile.name }}{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div hx-get="{% url "import_profile_runs_list" profile_id=profile.id %}"
|
<div hx-get="{% url "import_profile_runs_list" profile_id=profile.id %}"
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
{% extends "layouts/base.html" %}
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block title %}{% translate 'Import Runs' %}{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div hx-get="{% url 'impor' %}" hx-trigger="load, updated from:window" class="show-loading"></div>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -9,4 +9,10 @@
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
on reset
|
||||||
|
for elm in <select/> in event.target
|
||||||
|
call elm.tomselect.clear()
|
||||||
|
end
|
||||||
|
end
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -28,7 +28,8 @@
|
|||||||
<body class="font-monospace">
|
<body class="font-monospace">
|
||||||
<div _="install hide_amounts
|
<div _="install hide_amounts
|
||||||
install htmx_error_handler
|
install htmx_error_handler
|
||||||
{% block body_hyperscript %}{% endblock %}">
|
{% block body_hyperscript %}{% endblock %}"
|
||||||
|
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
|
||||||
{% include 'includes/navbar.html' %}
|
{% include 'includes/navbar.html' %}
|
||||||
|
|
||||||
<div id="content">
|
<div id="content">
|
||||||
|
|||||||
@@ -129,6 +129,7 @@
|
|||||||
id="filter">
|
id="filter">
|
||||||
{% crispy filter.form %}
|
{% crispy filter.form %}
|
||||||
</form>
|
</form>
|
||||||
|
<button class="btn btn-outline-danger btn-sm" _="on click call #filter.reset() then trigger change on #filter">{% translate 'Clear' %}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{# Transactions list#}
|
{# Transactions list#}
|
||||||
|
|||||||
17
app/templates/transactions/fragments/bulk_edit.html
Normal file
17
app/templates/transactions/fragments/bulk_edit.html
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{% extends 'extends/offcanvas.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load crispy_forms_tags %}
|
||||||
|
|
||||||
|
{% block title %}{% translate 'Bulk Editing' %}{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<p>{% trans 'Editing' %} {{ transactions|length }} {% trans 'transactions' %}</p>
|
||||||
|
<div class="editing-transactions">
|
||||||
|
{% for transaction in transactions %}
|
||||||
|
<input type="hidden" name="transactions" value="{{ transaction.id }}"/>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<form hx-post="{% url 'transactions_bulk_edit' %}" hx-target="#generic-offcanvas" hx-include=".editing-transactions" novalidate>
|
||||||
|
{% crispy form %}
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
15
app/templates/transactions/pages/add.html
Normal file
15
app/templates/transactions/pages/add.html
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{% extends 'layouts/base.html' %}
|
||||||
|
{% load crispy_forms_tags %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% translate 'New transaction' %}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container py-3 column-gap-5"
|
||||||
|
_="install init_tom_select
|
||||||
|
install init_datepicker">
|
||||||
|
<form hx-post="{% url 'transaction_simple_add' %}" hx-swap="outerHTML" hx-target="body" novalidate>
|
||||||
|
{% crispy form form.helper_simple %}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -14,8 +14,7 @@
|
|||||||
<div class="d-flex mb-3 align-self-center">
|
<div class="d-flex mb-3 align-self-center">
|
||||||
<div class="me-auto"><h4><i class="fa-solid fa-filter me-2"></i>{% translate 'Filter' %}</h4></div>
|
<div class="me-auto"><h4><i class="fa-solid fa-filter me-2"></i>{% translate 'Filter' %}</h4></div>
|
||||||
<div class="align-self-center">
|
<div class="align-self-center">
|
||||||
<a href="{% url 'transactions_all_index' %}" type="button" class="btn btn-outline-danger btn-sm"
|
<button class="btn btn-outline-danger btn-sm" _="on click call #filter.reset() then trigger change on #filter">{% translate 'Clear' %}</button>
|
||||||
hx-target="body" hx-boost="true">{% translate 'Clear' %}</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
id="{{ field.html_name }}_{{ forloop.counter }}_tr"
|
id="{{ field.html_name }}_{{ forloop.counter }}_tr"
|
||||||
value="{{ choice.0 }}"
|
value="{{ choice.0 }}"
|
||||||
{% if choice.0 == field.value %}checked{% endif %}>
|
{% if choice.0 == field.value %}checked{% endif %}>
|
||||||
<label class="btn {% if forloop.first %}btn-outline-success{% elif forloop.last %}btn-outline-danger{% else %}btn-outline-primary{% endif %} {% if field.errors %}is-invalid{% endif %}"
|
<label class="btn {% if forloop.first %}btn-outline-success{% elif forloop.last %}btn-outline-danger{% else %}btn-outline-primary{% endif %} {% if field.errors %}is-invalid{% endif %} w-100"
|
||||||
for="{{ field.html_name }}_{{ forloop.counter }}_tr">
|
for="{{ field.html_name }}_{{ forloop.counter }}_tr">
|
||||||
{{ choice.1 }}
|
{{ choice.1 }}
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
18
app/templates/transactions/widgets/paid_toggle_button.html
Normal file
18
app/templates/transactions/widgets/paid_toggle_button.html
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
{% load crispy_forms_field %}
|
||||||
|
|
||||||
|
<div class="form-group mb-3">
|
||||||
|
<div class="btn-group w-100" role="group" aria-label="{{ field.label }}">
|
||||||
|
<input type="radio" class="btn-check" name="{{ field.name }}" id="{{ field.id_for_label }}_false"
|
||||||
|
value="false" {% if not field.value %}checked{% endif %}>
|
||||||
|
<label class="btn btn-outline-primary w-50" for="{{ field.id_for_label }}_false">{% trans 'Projected' %}</label>
|
||||||
|
|
||||||
|
<input type="radio" class="btn-check" name="{{ field.name }}" id="{{ field.id_for_label }}_true"
|
||||||
|
value="true" {% if field.value %}checked{% endif %}>
|
||||||
|
<label class="btn btn-outline-success w-50" for="{{ field.id_for_label }}_true">{% trans 'Paid' %}</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if field.help_text %}
|
||||||
|
<div class="help-text">{{ field.help_text|safe }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
id="{{ field.html_name }}_{{ forloop.counter }}"
|
id="{{ field.html_name }}_{{ forloop.counter }}"
|
||||||
value="{{ choice.0 }}"
|
value="{{ choice.0 }}"
|
||||||
{% if choice.0 in field.value %}checked{% endif %}>
|
{% if choice.0 in field.value %}checked{% endif %}>
|
||||||
<label class="btn btn-outline-dark"
|
<label class="btn btn-outline-dark w-100"
|
||||||
for="{{ field.html_name }}_{{ forloop.counter }}">
|
for="{{ field.html_name }}_{{ forloop.counter }}">
|
||||||
{{ choice.1 }}
|
{{ choice.1 }}
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
{% load crispy_forms_field %}
|
||||||
|
|
||||||
|
<div class="form-group mb-3">
|
||||||
|
<div class="btn-group w-100" role="group" aria-label="{{ field.label }}">
|
||||||
|
<input type="radio"
|
||||||
|
class="btn-check"
|
||||||
|
name="{{ field.html_name }}"
|
||||||
|
id="{{ field.html_name }}_none_tr"
|
||||||
|
value=""
|
||||||
|
{% if field.value is None %}checked{% endif %}>
|
||||||
|
<label class="btn btn-outline-secondary {% if field.errors %}is-invalid{% endif %} w-100"
|
||||||
|
for="{{ field.html_name }}_none_tr">
|
||||||
|
{% trans 'Unchanged' %}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{% for choice in field.field.choices %}
|
||||||
|
<input type="radio"
|
||||||
|
class="btn-check"
|
||||||
|
name="{{ field.html_name }}"
|
||||||
|
id="{{ field.html_name }}_{{ forloop.counter }}_tr"
|
||||||
|
value="{{ choice.0 }}"
|
||||||
|
{% if choice.0 == field.value %}checked{% endif %}>
|
||||||
|
<label class="btn {% if forloop.first %}btn-outline-success{% elif forloop.last %}btn-outline-danger{% else %}btn-outline-primary{% endif %} {% if field.errors %}is-invalid{% endif %} w-100"
|
||||||
|
for="{{ field.html_name }}_{{ forloop.counter }}_tr">
|
||||||
|
{{ choice.1 }}
|
||||||
|
</label>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% if field.errors %}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{% for error in field.errors %}
|
||||||
|
{{ error }}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if field.help_text %}
|
||||||
|
<small class="form-text text-muted">{{ field.help_text }}</small>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
{% load crispy_forms_field %}
|
||||||
|
|
||||||
|
<div class="form-group mb-3">
|
||||||
|
<div class="btn-group w-100" role="group" aria-label="{{ field.label }}">
|
||||||
|
<input type="radio" class="btn-check" name="{{ field.name }}" id="{{ field.id_for_label }}_null"
|
||||||
|
value="" {% if field.value is None %}checked{% endif %}>
|
||||||
|
<label class="btn btn-outline-secondary w-100" for="{{ field.id_for_label }}_null">{% trans 'Unchanged' %}</label>
|
||||||
|
|
||||||
|
<input type="radio" class="btn-check" name="{{ field.name }}" id="{{ field.id_for_label }}_false"
|
||||||
|
value="false" {% if field.value is False %}checked{% endif %}">
|
||||||
|
<label class="btn btn-outline-primary w-100" for="{{ field.id_for_label }}_false">{% trans 'Projected' %}</label>
|
||||||
|
|
||||||
|
<input type="radio" class="btn-check" name="{{ field.name }}" id="{{ field.id_for_label }}_true"
|
||||||
|
value="true" {% if field.value is True %}checked{% endif %}>
|
||||||
|
<label class="btn btn-outline-success w-100" for="{{ field.id_for_label }}_true">{% trans 'Paid' %}</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if field.help_text %}
|
||||||
|
<div class="help-text">{{ field.help_text|safe }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
@@ -205,3 +205,35 @@
|
|||||||
.flashing {
|
.flashing {
|
||||||
animation: flash 1s infinite;
|
animation: flash 1s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.slide-in-bottom {
|
||||||
|
animation: slide-in-bottom 0.3s cubic-bezier(0.250, 0.460, 0.450, 0.940) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-in-bottom-reverse {
|
||||||
|
animation: slide-in-bottom 0.3s cubic-bezier(0.250, 0.460, 0.450, 0.940) reverse both;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ----------------------------------------------
|
||||||
|
* Generated by Animista on 2025-1-25 12:30:4
|
||||||
|
* Licensed under FreeBSD License.
|
||||||
|
* See http://animista.net/license for more info.
|
||||||
|
* w: http://animista.net, t: @cssanimista
|
||||||
|
* ---------------------------------------------- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ----------------------------------------
|
||||||
|
* animation slide-in-bottom
|
||||||
|
* ----------------------------------------
|
||||||
|
*/
|
||||||
|
@keyframes slide-in-bottom {
|
||||||
|
0% {
|
||||||
|
transform: translateY(1000px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user