mirror of
https://github.com/eitchtee/WYGIWYH.git
synced 2026-02-25 17:04:51 +01:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9d1b5b5f3 | ||
|
|
a22a95cb9f | ||
|
|
5c46a2c15e | ||
|
|
4f091c601e | ||
|
|
0fac78d15a | ||
|
|
aa171c0e76 | ||
|
|
73ca418dc8 | ||
|
|
7c34f36ffb | ||
|
|
2b6be8c6ac | ||
|
|
f643c41cf1 | ||
|
|
1ef7a780fb | ||
|
|
c3a753d221 | ||
|
|
c474b6cda9 | ||
|
|
aff3aa7ed2 | ||
|
|
5f202a3820 | ||
|
|
e71775292a | ||
|
|
01aa8acb71 | ||
|
|
d030f9686b | ||
|
|
56d7e41bc5 | ||
|
|
0857b44fc3 | ||
|
|
d4b5afd8b2 | ||
|
|
9c4ba3a6de | ||
|
|
ec8b0e21d8 | ||
|
|
6c60c3659c | ||
|
|
a040b8acd2 | ||
|
|
e72d6cd1ea | ||
|
|
3fb670ef00 | ||
|
|
b9cd97f0b8 | ||
|
|
011e0ad7c9 | ||
|
|
97465c07fe | ||
|
|
36cbe2935a | ||
|
|
dbea78cd3c | ||
|
|
d50c84f8e6 |
@@ -163,7 +163,7 @@ AUTH_USER_MODEL = "users.User"
|
||||
LANGUAGE_CODE = "en"
|
||||
LANGUAGES = (
|
||||
("en", "English"),
|
||||
("nl", "Nederlands"),
|
||||
# ("nl", "Nederlands"),
|
||||
("pt-br", "Português (Brasil)"),
|
||||
)
|
||||
|
||||
@@ -363,7 +363,13 @@ PWA_APP_SPLASH_SCREEN = [
|
||||
]
|
||||
PWA_APP_DIR = "ltr"
|
||||
PWA_APP_LANG = "en-US"
|
||||
PWA_APP_SHORTCUTS = []
|
||||
PWA_APP_SHORTCUTS = [
|
||||
{
|
||||
"name": "New Transaction",
|
||||
"url": "/add/",
|
||||
"description": "Add new transaction",
|
||||
}
|
||||
]
|
||||
PWA_APP_SCREENSHOTS = [
|
||||
{
|
||||
"src": "/static/img/pwa/splash-750x1334.png",
|
||||
@@ -377,6 +383,7 @@ PWA_APP_SCREENSHOTS = [
|
||||
"type": "image/png",
|
||||
},
|
||||
]
|
||||
PWA_SERVICE_WORKER_PATH = BASE_DIR / "templates" / "pwa" / "serviceworker.js"
|
||||
|
||||
ENABLE_SOFT_DELETE = os.getenv("ENABLE_SOFT_DELETE", "false").lower() == "true"
|
||||
KEEP_DELETED_TRANSACTIONS_FOR = int(os.getenv("KEEP_DELETED_ENTRIES_FOR", "365"))
|
||||
|
||||
@@ -2,9 +2,7 @@ from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
from django.urls import reverse
|
||||
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 apps.accounts.forms import AccountGroupForm
|
||||
@@ -89,7 +87,6 @@ def account_group_edit(request, pk):
|
||||
|
||||
@only_htmx
|
||||
@login_required
|
||||
@csrf_exempt
|
||||
@require_http_methods(["DELETE"])
|
||||
def account_group_delete(request, 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.http import HttpResponse
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
from django.urls import reverse
|
||||
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 apps.accounts.forms import AccountForm
|
||||
@@ -89,7 +87,6 @@ def account_edit(request, pk):
|
||||
|
||||
@only_htmx
|
||||
@login_required
|
||||
@csrf_exempt
|
||||
@require_http_methods(["DELETE"])
|
||||
def account_delete(request, 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.http import HttpResponse
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
from django.urls import reverse
|
||||
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 apps.common.decorators.htmx import only_htmx
|
||||
@@ -89,7 +87,6 @@ def currency_edit(request, pk):
|
||||
|
||||
@only_htmx
|
||||
@login_required
|
||||
@csrf_exempt
|
||||
@require_http_methods(["DELETE"])
|
||||
def currency_delete(request, pk):
|
||||
currency = get_object_or_404(Currency, id=pk)
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
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.http import HttpResponse
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
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 apps.common.decorators.htmx import only_htmx
|
||||
@@ -135,7 +134,6 @@ def exchange_rate_edit(request, pk):
|
||||
|
||||
@only_htmx
|
||||
@login_required
|
||||
@csrf_exempt
|
||||
@require_http_methods(["DELETE"])
|
||||
def exchange_rate_delete(request, 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.shortcuts import render, get_object_or_404
|
||||
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 apps.common.decorators.htmx import only_htmx
|
||||
from apps.dca.models import DCAStrategy, DCAEntry
|
||||
from apps.dca.forms import DCAEntryForm, DCAStrategyForm
|
||||
from apps.dca.models import DCAStrategy, DCAEntry
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -82,7 +81,6 @@ def strategy_edit(request, strategy_id):
|
||||
|
||||
@only_htmx
|
||||
@login_required
|
||||
@csrf_exempt
|
||||
@require_http_methods(["DELETE"])
|
||||
def strategy_delete(request, 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
|
||||
@login_required
|
||||
@csrf_exempt
|
||||
@require_http_methods(["DELETE"])
|
||||
def strategy_entry_delete(request, entry_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 Versions(models.IntegerChoices):
|
||||
VERSION_1 = 1, _("Version") + " 1"
|
||||
VERSION_1 = 1, "Version 1"
|
||||
|
||||
name = models.CharField(max_length=100, verbose_name=_("Name"), unique=True)
|
||||
yaml_config = models.TextField(verbose_name=_("YAML Configuration"))
|
||||
@@ -25,6 +25,10 @@ class ImportProfile(models.Model):
|
||||
class Meta:
|
||||
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):
|
||||
if self.version and self.version == self.Versions.VERSION_1:
|
||||
try:
|
||||
|
||||
@@ -38,7 +38,7 @@ class PresetService:
|
||||
preset["schema_version"]
|
||||
) # Check if schema version is valid
|
||||
except Exception as e:
|
||||
print(e)
|
||||
pass
|
||||
else:
|
||||
presets.append(preset)
|
||||
|
||||
|
||||
@@ -201,7 +201,6 @@ class ImportService:
|
||||
),
|
||||
None,
|
||||
)
|
||||
print(category_mapping)
|
||||
|
||||
try:
|
||||
if category_mapping:
|
||||
|
||||
@@ -5,15 +5,14 @@ from django.contrib.auth.decorators import login_required
|
||||
from django.core.files.storage import FileSystemStorage
|
||||
from django.http import HttpResponse
|
||||
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.views.decorators.http import require_http_methods
|
||||
|
||||
from apps.common.decorators.htmx import only_htmx
|
||||
from apps.import_app.forms import ImportRunFileUploadForm, ImportProfileForm
|
||||
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.tasks import process_import
|
||||
|
||||
|
||||
def import_view(request):
|
||||
@@ -33,7 +32,6 @@ def import_view(request):
|
||||
@require_http_methods(["GET"])
|
||||
def import_presets_list(request):
|
||||
presets = PresetService.get_all_presets()
|
||||
print(presets)
|
||||
return render(
|
||||
request,
|
||||
"import_app/fragments/profiles/list_presets.html",
|
||||
@@ -67,9 +65,9 @@ def import_profile_list(request):
|
||||
@login_required
|
||||
@require_http_methods(["GET", "POST"])
|
||||
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)
|
||||
|
||||
if form.is_valid():
|
||||
@@ -83,12 +81,11 @@ def import_profile_add(request):
|
||||
},
|
||||
)
|
||||
else:
|
||||
print(int(request.GET.get("version", 1)))
|
||||
form = ImportProfileForm(
|
||||
initial={
|
||||
"name": request.GET.get("name"),
|
||||
"version": int(request.GET.get("version", 1)),
|
||||
"yaml_config": request.GET.get("yaml_config"),
|
||||
"name": request.POST.get("name"),
|
||||
"version": int(request.POST.get("version", 1)),
|
||||
"yaml_config": request.POST.get("yaml_config"),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -130,7 +127,6 @@ def import_profile_edit(request, profile_id):
|
||||
|
||||
@only_htmx
|
||||
@login_required
|
||||
@csrf_exempt
|
||||
@require_http_methods(["DELETE"])
|
||||
def import_profile_delete(request, profile_id):
|
||||
profile = ImportProfile.objects.get(id=profile_id)
|
||||
@@ -215,7 +211,6 @@ def import_run_add(request, profile_id):
|
||||
|
||||
@only_htmx
|
||||
@login_required
|
||||
@csrf_exempt
|
||||
@require_http_methods(["DELETE"])
|
||||
def import_run_delete(request, profile_id, run_id):
|
||||
run = ImportRun.objects.get(profile__id=profile_id, id=run_id)
|
||||
|
||||
@@ -52,19 +52,4 @@ urlpatterns = [
|
||||
views.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.shortcuts import render, get_object_or_404, redirect
|
||||
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 apps.common.decorators.htmx import only_htmx
|
||||
@@ -118,7 +117,6 @@ def transaction_rule_view(request, transaction_rule_id):
|
||||
|
||||
@only_htmx
|
||||
@login_required
|
||||
@csrf_exempt
|
||||
@require_http_methods(["DELETE"])
|
||||
def transaction_rule_delete(request, 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
|
||||
@login_required
|
||||
@csrf_exempt
|
||||
@require_http_methods(["DELETE"])
|
||||
def transaction_rule_action_delete(request, transaction_rule_action_id):
|
||||
transaction_rule_action = get_object_or_404(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from crispy_bootstrap5.bootstrap5 import Switch
|
||||
from crispy_forms.bootstrap import FormActions
|
||||
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,
|
||||
@@ -115,7 +115,7 @@ class TransactionForm(forms.ModelForm):
|
||||
"type",
|
||||
template="transactions/widgets/income_expense_toggle_buttons.html",
|
||||
),
|
||||
Switch("is_paid"),
|
||||
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"),
|
||||
@@ -136,6 +136,46 @@ class TransactionForm(forms.ModelForm):
|
||||
"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["date"].widget = AirDatePickerInput(clear_button=False, user=user)
|
||||
|
||||
@@ -183,6 +223,43 @@ class TransactionForm(forms.ModelForm):
|
||||
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):
|
||||
from_account = forms.ModelChoiceField(
|
||||
queryset=Account.objects.filter(is_archived=False),
|
||||
|
||||
@@ -12,7 +12,7 @@ urlpatterns = [
|
||||
name="transactions_all_summary",
|
||||
),
|
||||
path(
|
||||
"transactions/actions/pay",
|
||||
"transactions/actions/pay/",
|
||||
views.bulk_pay_transactions,
|
||||
name="transactions_bulk_pay",
|
||||
),
|
||||
@@ -27,32 +27,47 @@ urlpatterns = [
|
||||
name="transactions_bulk_delete",
|
||||
),
|
||||
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,
|
||||
name="transaction_pay",
|
||||
),
|
||||
path(
|
||||
"transaction/<int:transaction_id>/delete",
|
||||
"transaction/<int:transaction_id>/delete/",
|
||||
views.transaction_delete,
|
||||
name="transaction_delete",
|
||||
),
|
||||
path(
|
||||
"transaction/<int:transaction_id>/edit",
|
||||
"transaction/<int:transaction_id>/edit/",
|
||||
views.transaction_edit,
|
||||
name="transaction_edit",
|
||||
),
|
||||
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,
|
||||
name="transaction_clone",
|
||||
),
|
||||
path(
|
||||
"transaction/add",
|
||||
"transaction/add/",
|
||||
views.transaction_add,
|
||||
name="transaction_add",
|
||||
),
|
||||
path(
|
||||
"transactions/transfer",
|
||||
"add/",
|
||||
views.transaction_simple_add,
|
||||
name="transaction_simple_add",
|
||||
),
|
||||
path(
|
||||
"transactions/transfer/",
|
||||
views.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.http import HttpResponse
|
||||
from django.utils.translation import gettext_lazy as _, ngettext_lazy
|
||||
|
||||
from apps.common.decorators.htmx import only_htmx
|
||||
from apps.transactions.models import Transaction
|
||||
@@ -9,7 +13,19 @@ from apps.transactions.models import Transaction
|
||||
@login_required
|
||||
def bulk_pay_transactions(request):
|
||||
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(
|
||||
status=204,
|
||||
@@ -21,7 +37,19 @@ def bulk_pay_transactions(request):
|
||||
@login_required
|
||||
def bulk_unpay_transactions(request):
|
||||
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(
|
||||
status=204,
|
||||
@@ -33,7 +61,54 @@ def bulk_unpay_transactions(request):
|
||||
@login_required
|
||||
def bulk_delete_transactions(request):
|
||||
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(
|
||||
status=204,
|
||||
|
||||
@@ -2,9 +2,7 @@ from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
from django.urls import reverse
|
||||
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 apps.common.decorators.htmx import only_htmx
|
||||
@@ -111,7 +109,6 @@ def category_edit(request, category_id):
|
||||
|
||||
@only_htmx
|
||||
@login_required
|
||||
@csrf_exempt
|
||||
@require_http_methods(["DELETE"])
|
||||
def category_delete(request, 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.shortcuts import render, get_object_or_404
|
||||
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 apps.common.decorators.htmx import only_htmx
|
||||
@@ -110,7 +109,6 @@ def entity_edit(request, entity_id):
|
||||
|
||||
@only_htmx
|
||||
@login_required
|
||||
@csrf_exempt
|
||||
@require_http_methods(["DELETE"])
|
||||
def entity_delete(request, 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.utils import timezone
|
||||
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 apps.common.decorators.htmx import only_htmx
|
||||
@@ -152,7 +151,6 @@ def installment_plan_refresh(request, installment_plan_id):
|
||||
|
||||
@only_htmx
|
||||
@login_required
|
||||
@csrf_exempt
|
||||
@require_http_methods(["DELETE"])
|
||||
def installment_plan_delete(request, installment_plan_id):
|
||||
installment_plan = get_object_or_404(InstallmentPlan, id=installment_plan_id)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
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.utils import timezone
|
||||
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 apps.common.decorators.htmx import only_htmx
|
||||
@@ -230,7 +228,6 @@ def recurring_transaction_finish(request, recurring_transaction_id):
|
||||
|
||||
@only_htmx
|
||||
@login_required
|
||||
@csrf_exempt
|
||||
@require_http_methods(["DELETE"])
|
||||
def recurring_transaction_delete(request, recurring_transaction_id):
|
||||
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.shortcuts import render, get_object_or_404
|
||||
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 apps.common.decorators.htmx import only_htmx
|
||||
@@ -110,7 +109,6 @@ def tag_edit(request, tag_id):
|
||||
|
||||
@only_htmx
|
||||
@login_required
|
||||
@csrf_exempt
|
||||
@require_http_methods(["DELETE"])
|
||||
def tag_delete(request, 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.shortcuts import render, get_object_or_404
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.utils.translation import gettext_lazy as _, ngettext_lazy
|
||||
from django.views.decorators.http import require_http_methods
|
||||
|
||||
from apps.common.decorators.htmx import only_htmx
|
||||
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.forms import TransactionForm, TransferForm
|
||||
from apps.transactions.forms import (
|
||||
TransactionForm,
|
||||
TransferForm,
|
||||
BulkEditTransactionForm,
|
||||
)
|
||||
from apps.transactions.models import Transaction
|
||||
from apps.transactions.utils.calculations import (
|
||||
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
|
||||
@login_required
|
||||
@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
|
||||
@login_required
|
||||
@require_http_methods(["GET", "POST"])
|
||||
@@ -102,6 +205,7 @@ def transaction_clone(request, transaction_id, **kwargs):
|
||||
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())
|
||||
@@ -143,7 +247,6 @@ def transaction_clone(request, transaction_id, **kwargs):
|
||||
|
||||
@only_htmx
|
||||
@login_required
|
||||
@csrf_exempt
|
||||
@require_http_methods(["DELETE"])
|
||||
def transaction_delete(request, transaction_id, **kwargs):
|
||||
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
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"
|
||||
_="on change from #transactions-list or htmx:afterSettle from window
|
||||
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
|
||||
remove .tw-hidden from #actions-bar
|
||||
then trigger selected_transactions_updated
|
||||
end
|
||||
end">
|
||||
<div class="card slide-in-left">
|
||||
<div class="card-body p-2">
|
||||
<div class="card slide-in-bottom">
|
||||
<div class="card-body p-2 d-flex justify-content-between align-items-center gap-3">
|
||||
{% spaceless %}
|
||||
<div class="btn-group" role="group">
|
||||
<button class="btn btn-secondary btn-sm"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-title="{% translate 'Select All' %}"
|
||||
_="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>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-secondary btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown"
|
||||
aria-expanded="false">
|
||||
<i class="fa-regular fa-square-check fa-fw"></i>
|
||||
</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 class="vr mx-3 tw-align-middle"></div>
|
||||
<div class="btn-group me-3" role="group">
|
||||
<div class="vr tw-align-middle"></div>
|
||||
<div class="btn-group">
|
||||
<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"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-title="{% translate 'Mark as paid' %}">
|
||||
<i class="fa-regular fa-circle-check tw-text-green-400"></i>
|
||||
data-bs-title="{% translate 'Edit' %}">
|
||||
<i class="fa-solid fa-pencil"></i>
|
||||
</button>
|
||||
<button class="btn btn-secondary btn-sm"
|
||||
hx-get="{% url 'transactions_bulk_unpay' %}"
|
||||
hx-include=".transaction"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-title="{% translate 'Mark as unpaid' %}">
|
||||
<i class="fa-regular fa-circle tw-text-red-400"></i>
|
||||
<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">
|
||||
<span class="visually-hidden">{% trans "Toggle Dropdown" %}</span>
|
||||
</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>
|
||||
<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"
|
||||
hx-get="{% url 'transactions_bulk_delete' %}"
|
||||
hx-include=".transaction"
|
||||
@@ -55,9 +85,9 @@
|
||||
_="install prompt_swal">
|
||||
<i class="fa-solid fa-trash text-danger"></i>
|
||||
</button>
|
||||
<div class="vr mx-3 tw-align-middle"></div>
|
||||
<div class="vr tw-align-middle"></div>
|
||||
<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 flatTotal to math.bignumber(0)
|
||||
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 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
|
||||
end"
|
||||
>
|
||||
end">
|
||||
<button class="btn btn-secondary btn-sm" _="on click
|
||||
set original_value to #real-total-front's innerText
|
||||
writeText(original_value) on navigator.clipboard
|
||||
@@ -102,8 +131,8 @@
|
||||
wait 1s
|
||||
put original_value into #real-total-front's innerText
|
||||
end">
|
||||
<i class="fa-solid fa-plus fa-fw me-2 text-primary"></i>
|
||||
<span id="real-total-front">0</span>
|
||||
<i class="fa-solid fa-plus fa-fw me-md-2 text-primary"></i>
|
||||
<span class="d-none d-md-inline-block" id="real-total-front">0</span>
|
||||
</button>
|
||||
<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">
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
{% for preset in presets %}
|
||||
<a class="text-decoration-none"
|
||||
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-target="#generic-offcanvas">
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{% load i18n %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block title %}{% translate 'Runs for ' %}{{ profile.name }}{% endblock %}
|
||||
{% block title %}{% translate 'Runs for' %} {{ profile.name }}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<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
|
||||
|
||||
on reset
|
||||
for elm in <select/> in event.target
|
||||
call elm.tomselect.clear()
|
||||
end
|
||||
end
|
||||
</script>
|
||||
|
||||
@@ -28,7 +28,8 @@
|
||||
<body class="font-monospace">
|
||||
<div _="install hide_amounts
|
||||
install htmx_error_handler
|
||||
{% block body_hyperscript %}{% endblock %}">
|
||||
{% block body_hyperscript %}{% endblock %}"
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
|
||||
{% include 'includes/navbar.html' %}
|
||||
|
||||
<div id="content">
|
||||
|
||||
@@ -129,6 +129,7 @@
|
||||
id="filter">
|
||||
{% crispy filter.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>
|
||||
{# Transactions list#}
|
||||
|
||||
79
app/templates/offline.html
Normal file
79
app/templates/offline.html
Normal file
@@ -0,0 +1,79 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Offline</title>
|
||||
<style>
|
||||
.offline, body {
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
background-color: #222;
|
||||
color: #fbb700;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
.wifi-icon {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
@keyframes flash {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.3; }
|
||||
}
|
||||
.flashing {
|
||||
animation: flash 1s infinite;
|
||||
}
|
||||
#offline-countdown {
|
||||
margin-top: 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="offline">
|
||||
<svg class="wifi-icon flashing" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1 9l2 2c4.97-4.97 13.03-4.97 18 0l2-2C16.93 2.93 7.08 2.93 1 9zm8 8l3 3 3-3c-1.65-1.66-4.34-1.66-6 0zm-4-4l2 2c2.76-2.76 7.24-2.76 10 0l2-2C15.14 9.14 8.87 9.14 5 13z" fill="#fbb700"/>
|
||||
<path d="M23 21L1 3" stroke="#fbb700" stroke-width="2"/>
|
||||
</svg>
|
||||
<p>Either you or your WYGIWYH instance is offline.</p>
|
||||
<div id="offline-countdown"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function attemptReload() {
|
||||
const countdownElement = document.getElementById('offline-countdown');
|
||||
let secondsLeft = 30;
|
||||
|
||||
function updateCountdown() {
|
||||
countdownElement.textContent = `Retrying in ${secondsLeft} seconds...`;
|
||||
secondsLeft--;
|
||||
|
||||
if (secondsLeft < 0) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
setTimeout(updateCountdown, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
updateCountdown();
|
||||
}
|
||||
|
||||
// Start the reload attempt process immediately
|
||||
attemptReload();
|
||||
|
||||
// Also attempt reload when coming back online
|
||||
window.addEventListener('online', () => {
|
||||
window.location.reload();
|
||||
});
|
||||
|
||||
// For HTMX compatibility
|
||||
document.body.addEventListener('htmx:load', attemptReload);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
74
app/templates/pwa/serviceworker.js
Normal file
74
app/templates/pwa/serviceworker.js
Normal file
@@ -0,0 +1,74 @@
|
||||
// Base Service Worker implementation. To use your own Service Worker, set the PWA_SERVICE_WORKER_PATH variable in settings.py
|
||||
|
||||
var staticCacheName = "django-pwa-v" + new Date().getTime();
|
||||
var filesToCache = [
|
||||
'/offline/',
|
||||
'/static/css/django-pwa-app.css',
|
||||
'/static/img/favicon/android-icon-192x192.png',
|
||||
'/static/img/favicon/apple-icon-180x180.png',
|
||||
'/static/img/pwa/splash-640x1136.png',
|
||||
'/static/img/pwa/splash-750x1334.png',
|
||||
];
|
||||
|
||||
// Cache on install
|
||||
self.addEventListener("install", event => {
|
||||
this.skipWaiting();
|
||||
event.waitUntil(
|
||||
caches.open(staticCacheName)
|
||||
.then(cache => {
|
||||
return cache.addAll(filesToCache);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Clear cache on activate
|
||||
self.addEventListener('activate', event => {
|
||||
event.waitUntil(
|
||||
caches.keys().then(cacheNames => {
|
||||
return Promise.all(
|
||||
cacheNames
|
||||
.filter(cacheName => (cacheName.startsWith("django-pwa-")))
|
||||
.filter(cacheName => (cacheName !== staticCacheName))
|
||||
.map(cacheName => caches.delete(cacheName))
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Serve from Cache
|
||||
self.addEventListener("fetch", event => {
|
||||
event.respondWith(
|
||||
caches.match(event.request)
|
||||
.then(response => {
|
||||
if (response) {
|
||||
return response;
|
||||
}
|
||||
return fetch(event.request).catch(() => {
|
||||
const isHtmxRequest = event.request.headers.get('HX-Request') === 'true';
|
||||
const isHtmxBoosted = event.request.headers.get('HX-Boosted') === 'true';
|
||||
|
||||
if (!isHtmxRequest || isHtmxBoosted) {
|
||||
// Serve offline content without changing URL
|
||||
return caches.match('/offline/').then(offlineResponse => {
|
||||
if (offlineResponse) {
|
||||
return offlineResponse.text().then(offlineText => {
|
||||
return new Response(offlineText, {
|
||||
status: 200,
|
||||
headers: {'Content-Type': 'text/html'}
|
||||
});
|
||||
});
|
||||
}
|
||||
// If offline page is not in cache, return a simple offline message
|
||||
return new Response('<h1>Offline</h1><p>The page is not available offline.</p>', {
|
||||
status: 200,
|
||||
headers: {'Content-Type': 'text/html'}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// For non-boosted HTMX requests, let it fail normally
|
||||
throw new Error('Network request failed');
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
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="me-auto"><h4><i class="fa-solid fa-filter me-2"></i>{% translate 'Filter' %}</h4></div>
|
||||
<div class="align-self-center">
|
||||
<a href="{% url 'transactions_all_index' %}" type="button" class="btn btn-outline-danger btn-sm"
|
||||
hx-target="body" hx-boost="true">{% translate 'Clear' %}</a>
|
||||
<button class="btn btn-outline-danger btn-sm" _="on click call #filter.reset() then trigger change on #filter">{% translate 'Clear' %}</button>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
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 %}"
|
||||
<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>
|
||||
|
||||
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 }}"
|
||||
value="{{ choice.0 }}"
|
||||
{% 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 }}">
|
||||
{{ choice.1 }}
|
||||
</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>
|
||||
@@ -58,13 +58,21 @@
|
||||
|
||||
// HTMX Loading
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
0% { opacity: 0; }
|
||||
100% { opacity: 1; }
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.show-loading.htmx-request {
|
||||
@@ -103,7 +111,7 @@
|
||||
}
|
||||
|
||||
.swing-out-top-bck {
|
||||
animation: swing-out-top-bck 0.45s cubic-bezier(0.600, -0.280, 0.735, 0.045) both;
|
||||
animation: swing-out-top-bck 0.45s cubic-bezier(0.600, -0.280, 0.735, 0.045) both;
|
||||
}
|
||||
|
||||
/* ----------------------------------------------
|
||||
@@ -155,7 +163,7 @@
|
||||
}
|
||||
|
||||
.scale-in-center {
|
||||
animation: scale-in-center 0.5s cubic-bezier(0.250, 0.460, 0.450, 0.940) both;
|
||||
animation: scale-in-center 0.5s cubic-bezier(0.250, 0.460, 0.450, 0.940) both;
|
||||
}
|
||||
|
||||
/* ----------------------------------------------
|
||||
@@ -182,5 +190,50 @@
|
||||
}
|
||||
|
||||
.scale-out-center {
|
||||
animation: scale-out-center 0.5s cubic-bezier(0.550, 0.085, 0.680, 0.530) both;
|
||||
animation: scale-out-center 0.5s cubic-bezier(0.550, 0.085, 0.680, 0.530) both;
|
||||
}
|
||||
|
||||
@keyframes flash {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
.flashing {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,3 +53,27 @@ select[multiple] {
|
||||
.transaction:has(input[type="checkbox"]:checked) > .transaction-item {
|
||||
background-color: $primary-bg-subtle-dark;
|
||||
}
|
||||
|
||||
.offline {
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
background-color: #222;
|
||||
color: #fbb700;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
|
||||
.wifi-icon {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
#offline-countdown {
|
||||
margin-top: 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user