mirror of
https://github.com/eitchtee/WYGIWYH.git
synced 2026-04-24 17:48:41 +02:00
feat: add quick transactions
This commit is contained in:
@@ -65,6 +65,18 @@ class SharedObject(models.Model):
|
|||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class OwnedObjectManager(models.Manager):
|
||||||
|
def get_queryset(self):
|
||||||
|
"""Return only objects the user can access"""
|
||||||
|
user = get_current_user()
|
||||||
|
base_qs = super().get_queryset()
|
||||||
|
|
||||||
|
if user and user.is_authenticated:
|
||||||
|
return base_qs.filter(Q(owner=user) | Q(owner=None)).distinct()
|
||||||
|
|
||||||
|
return base_qs
|
||||||
|
|
||||||
|
|
||||||
class OwnedObject(models.Model):
|
class OwnedObject(models.Model):
|
||||||
owner = models.ForeignKey(
|
owner = models.ForeignKey(
|
||||||
settings.AUTH_USER_MODEL,
|
settings.AUTH_USER_MODEL,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from crispy_forms.layout import (
|
|||||||
Column,
|
Column,
|
||||||
Field,
|
Field,
|
||||||
Div,
|
Div,
|
||||||
|
HTML,
|
||||||
)
|
)
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
@@ -29,8 +30,8 @@ from apps.transactions.models import (
|
|||||||
InstallmentPlan,
|
InstallmentPlan,
|
||||||
RecurringTransaction,
|
RecurringTransaction,
|
||||||
TransactionEntity,
|
TransactionEntity,
|
||||||
|
QuickTransaction,
|
||||||
)
|
)
|
||||||
from apps.common.middleware.thread_local import get_current_user
|
|
||||||
|
|
||||||
|
|
||||||
class TransactionForm(forms.ModelForm):
|
class TransactionForm(forms.ModelForm):
|
||||||
@@ -247,6 +248,140 @@ class TransactionForm(forms.ModelForm):
|
|||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
class QuickTransactionForm(forms.ModelForm):
|
||||||
|
category = DynamicModelChoiceField(
|
||||||
|
create_field="name",
|
||||||
|
model=TransactionCategory,
|
||||||
|
required=False,
|
||||||
|
label=_("Category"),
|
||||||
|
queryset=TransactionCategory.objects.filter(active=True),
|
||||||
|
)
|
||||||
|
tags = DynamicModelMultipleChoiceField(
|
||||||
|
model=TransactionTag,
|
||||||
|
to_field_name="name",
|
||||||
|
create_field="name",
|
||||||
|
required=False,
|
||||||
|
label=_("Tags"),
|
||||||
|
queryset=TransactionTag.objects.filter(active=True),
|
||||||
|
)
|
||||||
|
entities = DynamicModelMultipleChoiceField(
|
||||||
|
model=TransactionEntity,
|
||||||
|
to_field_name="name",
|
||||||
|
create_field="name",
|
||||||
|
required=False,
|
||||||
|
label=_("Entities"),
|
||||||
|
)
|
||||||
|
account = forms.ModelChoiceField(
|
||||||
|
queryset=Account.objects.filter(is_archived=False),
|
||||||
|
label=_("Account"),
|
||||||
|
widget=TomSelect(clear_button=False, group_by="group"),
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = QuickTransaction
|
||||||
|
fields = [
|
||||||
|
"name",
|
||||||
|
"account",
|
||||||
|
"type",
|
||||||
|
"is_paid",
|
||||||
|
"amount",
|
||||||
|
"description",
|
||||||
|
"notes",
|
||||||
|
"category",
|
||||||
|
"tags",
|
||||||
|
"entities",
|
||||||
|
]
|
||||||
|
widgets = {
|
||||||
|
"notes": forms.Textarea(attrs={"rows": 3}),
|
||||||
|
"account": TomSelect(clear_button=False, group_by="group"),
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
# if editing a transaction display non-archived items and it's own item even if it's archived
|
||||||
|
if self.instance.id:
|
||||||
|
self.fields["account"].queryset = Account.objects.filter(
|
||||||
|
Q(is_archived=False) | Q(transactions=self.instance.id),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.fields["category"].queryset = TransactionCategory.objects.filter(
|
||||||
|
Q(active=True) | Q(transaction=self.instance.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.fields["tags"].queryset = TransactionTag.objects.filter(
|
||||||
|
Q(active=True) | Q(transaction=self.instance.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.fields["entities"].queryset = TransactionEntity.objects.filter(
|
||||||
|
Q(active=True) | Q(transactions=self.instance.id)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.fields["account"].queryset = Account.objects.filter(
|
||||||
|
is_archived=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.fields["category"].queryset = TransactionCategory.objects.filter(
|
||||||
|
active=True
|
||||||
|
)
|
||||||
|
self.fields["tags"].queryset = TransactionTag.objects.filter(active=True)
|
||||||
|
self.fields["entities"].queryset = TransactionEntity.objects.all()
|
||||||
|
|
||||||
|
self.helper = FormHelper()
|
||||||
|
self.helper.form_tag = False
|
||||||
|
self.helper.form_method = "post"
|
||||||
|
self.helper.layout = Layout(
|
||||||
|
Field(
|
||||||
|
"type",
|
||||||
|
template="transactions/widgets/income_expense_toggle_buttons.html",
|
||||||
|
),
|
||||||
|
Field("is_paid", template="transactions/widgets/paid_toggle_button.html"),
|
||||||
|
"name",
|
||||||
|
HTML("<hr />"),
|
||||||
|
Row(
|
||||||
|
Column("account", css_class="form-group col-md-6 mb-0"),
|
||||||
|
Column("entities", css_class="form-group col-md-6 mb-0"),
|
||||||
|
css_class="form-row",
|
||||||
|
),
|
||||||
|
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"),
|
||||||
|
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",
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.instance and self.instance.pk:
|
||||||
|
decimal_places = self.instance.account.currency.decimal_places
|
||||||
|
self.fields["amount"].widget = ArbitraryDecimalDisplayNumberInput(
|
||||||
|
decimal_places=decimal_places
|
||||||
|
)
|
||||||
|
self.helper.layout.append(
|
||||||
|
FormActions(
|
||||||
|
NoClassSubmit(
|
||||||
|
"submit", _("Update"), css_class="btn btn-outline-primary w-100"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.fields["amount"].widget = ArbitraryDecimalDisplayNumberInput()
|
||||||
|
self.helper.layout.append(
|
||||||
|
Div(
|
||||||
|
NoClassSubmit(
|
||||||
|
"submit", _("Add"), css_class="btn btn-outline-primary"
|
||||||
|
),
|
||||||
|
css_class="d-grid gap-2",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BulkEditTransactionForm(TransactionForm):
|
class BulkEditTransactionForm(TransactionForm):
|
||||||
is_paid = forms.NullBooleanField(required=False)
|
is_paid = forms.NullBooleanField(required=False)
|
||||||
|
|
||||||
|
|||||||
45
app/apps/transactions/migrations/0043_quicktransaction.py
Normal file
45
app/apps/transactions/migrations/0043_quicktransaction.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# Generated by Django 5.1.11 on 2025-06-20 03:57
|
||||||
|
|
||||||
|
import apps.transactions.validators
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0014_alter_account_options_alter_accountgroup_options'),
|
||||||
|
('transactions', '0042_alter_transactioncategory_options_and_more'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='QuickTransaction',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=100, verbose_name='Name')),
|
||||||
|
('type', models.CharField(choices=[('IN', 'Income'), ('EX', 'Expense')], default='EX', max_length=2, verbose_name='Type')),
|
||||||
|
('is_paid', models.BooleanField(default=True, verbose_name='Paid')),
|
||||||
|
('amount', models.DecimalField(decimal_places=30, max_digits=42, validators=[apps.transactions.validators.validate_non_negative, apps.transactions.validators.validate_decimal_places], verbose_name='Amount')),
|
||||||
|
('description', models.CharField(blank=True, max_length=500, verbose_name='Description')),
|
||||||
|
('notes', models.TextField(blank=True, verbose_name='Notes')),
|
||||||
|
('internal_note', models.TextField(blank=True, verbose_name='Internal Note')),
|
||||||
|
('internal_id', models.TextField(blank=True, null=True, unique=True, verbose_name='Internal ID')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='quick_transactions', to='accounts.account', verbose_name='Account')),
|
||||||
|
('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='transactions.transactioncategory', verbose_name='Category')),
|
||||||
|
('entities', models.ManyToManyField(blank=True, related_name='quick_transactions', to='transactions.transactionentity', verbose_name='Entities')),
|
||||||
|
('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_owned', to=settings.AUTH_USER_MODEL)),
|
||||||
|
('tags', models.ManyToManyField(blank=True, to='transactions.transactiontag', verbose_name='Tags')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Quick Transaction',
|
||||||
|
'verbose_name_plural': 'Quick Transactions',
|
||||||
|
'db_table': 'quick_transactions',
|
||||||
|
'default_manager_name': 'objects',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 5.1.11 on 2025-06-20 04:02
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('transactions', '0043_quicktransaction'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='quicktransaction',
|
||||||
|
unique_together={('name', 'owner')},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -16,7 +16,12 @@ from apps.common.templatetags.decimal import localize_number, drop_trailing_zero
|
|||||||
from apps.currencies.utils.convert import convert
|
from apps.currencies.utils.convert import convert
|
||||||
from apps.transactions.validators import validate_decimal_places, validate_non_negative
|
from apps.transactions.validators import validate_decimal_places, validate_non_negative
|
||||||
from apps.common.middleware.thread_local import get_current_user
|
from apps.common.middleware.thread_local import get_current_user
|
||||||
from apps.common.models import SharedObject, SharedObjectManager, OwnedObject
|
from apps.common.models import (
|
||||||
|
SharedObject,
|
||||||
|
SharedObjectManager,
|
||||||
|
OwnedObject,
|
||||||
|
OwnedObjectManager,
|
||||||
|
)
|
||||||
|
|
||||||
logger = logging.getLogger()
|
logger = logging.getLogger()
|
||||||
|
|
||||||
@@ -886,3 +891,86 @@ class RecurringTransaction(models.Model):
|
|||||||
"""
|
"""
|
||||||
today = timezone.localdate(timezone.now())
|
today = timezone.localdate(timezone.now())
|
||||||
self.transactions.filter(is_paid=False, date__gt=today).delete()
|
self.transactions.filter(is_paid=False, date__gt=today).delete()
|
||||||
|
|
||||||
|
|
||||||
|
class QuickTransaction(OwnedObject):
|
||||||
|
class Type(models.TextChoices):
|
||||||
|
INCOME = "IN", _("Income")
|
||||||
|
EXPENSE = "EX", _("Expense")
|
||||||
|
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=100,
|
||||||
|
null=False,
|
||||||
|
blank=False,
|
||||||
|
verbose_name=_("Name"),
|
||||||
|
)
|
||||||
|
|
||||||
|
account = models.ForeignKey(
|
||||||
|
"accounts.Account",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
verbose_name=_("Account"),
|
||||||
|
related_name="quick_transactions",
|
||||||
|
)
|
||||||
|
type = models.CharField(
|
||||||
|
max_length=2,
|
||||||
|
choices=Type,
|
||||||
|
default=Type.EXPENSE,
|
||||||
|
verbose_name=_("Type"),
|
||||||
|
)
|
||||||
|
is_paid = models.BooleanField(default=True, verbose_name=_("Paid"))
|
||||||
|
|
||||||
|
amount = models.DecimalField(
|
||||||
|
max_digits=42,
|
||||||
|
decimal_places=30,
|
||||||
|
verbose_name=_("Amount"),
|
||||||
|
validators=[validate_non_negative, validate_decimal_places],
|
||||||
|
)
|
||||||
|
|
||||||
|
description = models.CharField(
|
||||||
|
max_length=500, verbose_name=_("Description"), blank=True
|
||||||
|
)
|
||||||
|
notes = models.TextField(blank=True, verbose_name=_("Notes"))
|
||||||
|
category = models.ForeignKey(
|
||||||
|
TransactionCategory,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
verbose_name=_("Category"),
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
)
|
||||||
|
tags = models.ManyToManyField(
|
||||||
|
TransactionTag,
|
||||||
|
verbose_name=_("Tags"),
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
entities = models.ManyToManyField(
|
||||||
|
TransactionEntity,
|
||||||
|
verbose_name=_("Entities"),
|
||||||
|
blank=True,
|
||||||
|
related_name="quick_transactions",
|
||||||
|
)
|
||||||
|
|
||||||
|
internal_note = models.TextField(blank=True, verbose_name=_("Internal Note"))
|
||||||
|
internal_id = models.TextField(
|
||||||
|
blank=True, null=True, unique=True, verbose_name=_("Internal ID")
|
||||||
|
)
|
||||||
|
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
objects = OwnedObjectManager()
|
||||||
|
all_objects = models.Manager() # Unfiltered manager
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Quick Transaction")
|
||||||
|
verbose_name_plural = _("Quick Transactions")
|
||||||
|
unique_together = ("name", "owner")
|
||||||
|
db_table = "quick_transactions"
|
||||||
|
default_manager_name = "objects"
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
self.amount = truncate_decimal(
|
||||||
|
value=self.amount, decimal_places=self.account.currency.decimal_places
|
||||||
|
)
|
||||||
|
|
||||||
|
self.full_clean()
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|||||||
@@ -307,4 +307,39 @@ urlpatterns = [
|
|||||||
views.recurring_transaction_finish,
|
views.recurring_transaction_finish,
|
||||||
name="recurring_transaction_finish",
|
name="recurring_transaction_finish",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"quick-transactions/",
|
||||||
|
views.quick_transactions_index,
|
||||||
|
name="quick_transactions_index",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"quick-transactions/list/",
|
||||||
|
views.quick_transactions_list,
|
||||||
|
name="quick_transactions_list",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"quick-transactions/add/",
|
||||||
|
views.quick_transaction_add,
|
||||||
|
name="quick_transaction_add",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"quick-transactions/<int:quick_transaction_id>/edit/",
|
||||||
|
views.quick_transaction_edit,
|
||||||
|
name="quick_transaction_edit",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"quick-transactions/<int:quick_transaction_id>/delete/",
|
||||||
|
views.quick_transaction_delete,
|
||||||
|
name="quick_transaction_delete",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"quick-transactions/create-menu/",
|
||||||
|
views.quick_transactions_create_menu,
|
||||||
|
name="quick_transactions_create_menu",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"quick-transactions/<int:quick_transaction_id>/create/",
|
||||||
|
views.quick_transaction_add_as_transaction,
|
||||||
|
name="quick_transaction_add_as_transaction",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -5,3 +5,4 @@ from .categories import *
|
|||||||
from .actions import *
|
from .actions import *
|
||||||
from .installment_plans import *
|
from .installment_plans import *
|
||||||
from .recurring_transactions import *
|
from .recurring_transactions import *
|
||||||
|
from .quick_transactions import *
|
||||||
|
|||||||
152
app/apps/transactions/views/quick_transactions.py
Normal file
152
app/apps/transactions/views/quick_transactions.py
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
from django.contrib import messages
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.forms import model_to_dict
|
||||||
|
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.http import require_http_methods
|
||||||
|
|
||||||
|
from apps.common.decorators.htmx import only_htmx
|
||||||
|
from apps.transactions.forms import QuickTransactionForm
|
||||||
|
from apps.transactions.models import QuickTransaction
|
||||||
|
from apps.transactions.models import Transaction
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@require_http_methods(["GET"])
|
||||||
|
def quick_transactions_index(request):
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"quick_transactions/pages/index.html",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@only_htmx
|
||||||
|
@login_required
|
||||||
|
@require_http_methods(["GET"])
|
||||||
|
def quick_transactions_list(request):
|
||||||
|
quick_transactions = QuickTransaction.objects.all().order_by("name")
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"quick_transactions/fragments/list.html",
|
||||||
|
context={"quick_transactions": quick_transactions},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@only_htmx
|
||||||
|
@login_required
|
||||||
|
@require_http_methods(["GET", "POST"])
|
||||||
|
def quick_transaction_add(request):
|
||||||
|
if request.method == "POST":
|
||||||
|
form = QuickTransactionForm(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
form.save()
|
||||||
|
messages.success(request, _("Item added successfully"))
|
||||||
|
|
||||||
|
return HttpResponse(
|
||||||
|
status=204,
|
||||||
|
headers={
|
||||||
|
"HX-Trigger": "updated, hide_offcanvas",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
form = QuickTransactionForm()
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"quick_transactions/fragments/add.html",
|
||||||
|
{"form": form},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@only_htmx
|
||||||
|
@login_required
|
||||||
|
@require_http_methods(["GET", "POST"])
|
||||||
|
def quick_transaction_edit(request, quick_transaction_id):
|
||||||
|
quick_transaction = get_object_or_404(QuickTransaction, id=quick_transaction_id)
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
form = QuickTransactionForm(request.POST, instance=quick_transaction)
|
||||||
|
if form.is_valid():
|
||||||
|
form.save()
|
||||||
|
messages.success(request, _("Item updated successfully"))
|
||||||
|
|
||||||
|
return HttpResponse(
|
||||||
|
status=204,
|
||||||
|
headers={
|
||||||
|
"HX-Trigger": "updated, hide_offcanvas",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
form = QuickTransactionForm(instance=quick_transaction)
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"quick_transactions/fragments/edit.html",
|
||||||
|
{"form": form, "quick_transaction": quick_transaction},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@only_htmx
|
||||||
|
@login_required
|
||||||
|
@require_http_methods(["DELETE"])
|
||||||
|
def quick_transaction_delete(request, quick_transaction_id):
|
||||||
|
quick_transaction = get_object_or_404(QuickTransaction, id=quick_transaction_id)
|
||||||
|
|
||||||
|
quick_transaction.delete()
|
||||||
|
|
||||||
|
messages.success(request, _("Item deleted successfully"))
|
||||||
|
|
||||||
|
return HttpResponse(
|
||||||
|
status=204,
|
||||||
|
headers={
|
||||||
|
"HX-Trigger": "updated, hide_offcanvas",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@only_htmx
|
||||||
|
@login_required
|
||||||
|
@require_http_methods(["GET"])
|
||||||
|
def quick_transactions_create_menu(request):
|
||||||
|
quick_transactions = QuickTransaction.objects.all().order_by("name")
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"quick_transactions/fragments/create_menu.html",
|
||||||
|
context={"quick_transactions": quick_transactions},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@only_htmx
|
||||||
|
@login_required
|
||||||
|
@require_http_methods(["GET"])
|
||||||
|
def quick_transaction_add_as_transaction(request, quick_transaction_id):
|
||||||
|
quick_transaction: QuickTransaction = get_object_or_404(
|
||||||
|
QuickTransaction, id=quick_transaction_id
|
||||||
|
)
|
||||||
|
today = timezone.localdate(timezone.now())
|
||||||
|
|
||||||
|
quick_transaction_data = model_to_dict(
|
||||||
|
quick_transaction,
|
||||||
|
exclude=["id", "name", "owner", "account", "category", "tags", "entities"],
|
||||||
|
)
|
||||||
|
|
||||||
|
new_transaction = Transaction(**quick_transaction_data)
|
||||||
|
new_transaction.account = quick_transaction.account
|
||||||
|
new_transaction.category = quick_transaction.category
|
||||||
|
|
||||||
|
new_transaction.date = today
|
||||||
|
new_transaction.reference_date = today.replace(day=1)
|
||||||
|
new_transaction.save()
|
||||||
|
new_transaction.tags.set(quick_transaction.tags.all())
|
||||||
|
new_transaction.entities.set(quick_transaction.entities.all())
|
||||||
|
|
||||||
|
messages.success(request, _("Transaction added successfully"))
|
||||||
|
|
||||||
|
return HttpResponse(
|
||||||
|
status=204,
|
||||||
|
headers={
|
||||||
|
"HX-Trigger": "updated, hide_offcanvas",
|
||||||
|
},
|
||||||
|
)
|
||||||
File diff suppressed because one or more lines are too long
@@ -15,7 +15,8 @@
|
|||||||
_="on mouseover remove .tw-invisible from the first .transaction-actions in me end
|
_="on mouseover remove .tw-invisible from the first .transaction-actions in me end
|
||||||
on mouseout add .tw-invisible to the first .transaction-actions in me end">
|
on mouseout add .tw-invisible to the first .transaction-actions in me end">
|
||||||
<div class="row font-monospace tw-text-sm align-items-center">
|
<div class="row font-monospace tw-text-sm align-items-center">
|
||||||
<div class="col-lg-auto col-12 d-flex align-items-center tw-text-2xl lg:tw-text-xl text-lg-center text-center p-0 ps-1">
|
<div
|
||||||
|
class="col-lg-auto col-12 d-flex align-items-center tw-text-2xl lg:tw-text-xl text-lg-center text-center p-0 ps-1">
|
||||||
{% if not transaction.deleted %}
|
{% if not transaction.deleted %}
|
||||||
<a class="text-decoration-none p-3 tw-text-gray-500"
|
<a class="text-decoration-none p-3 tw-text-gray-500"
|
||||||
title="{% if transaction.is_paid %}{% trans 'Paid' %}{% else %}{% trans 'Projected' %}{% endif %}"
|
title="{% if transaction.is_paid %}{% trans 'Paid' %}{% else %}{% trans 'Projected' %}{% endif %}"
|
||||||
|
|||||||
@@ -50,4 +50,11 @@
|
|||||||
url="{% url 'account_reconciliation' %}"
|
url="{% url 'account_reconciliation' %}"
|
||||||
icon="fa-solid fa-scale-balanced"
|
icon="fa-solid fa-scale-balanced"
|
||||||
title="{% translate "Balance" %}"></c-components.fab_menu_button>
|
title="{% translate "Balance" %}"></c-components.fab_menu_button>
|
||||||
|
<c-components.fab_menu_button
|
||||||
|
color="secondary"
|
||||||
|
hx_target="#generic-offcanvas"
|
||||||
|
hx_trigger="click, quick_transaction from:window"
|
||||||
|
url="{% url 'quick_transactions_create_menu' %}"
|
||||||
|
icon="fa-solid fa-person-running"
|
||||||
|
title="{% translate "Quick Transaction" %}"></c-components.fab_menu_button>
|
||||||
</c-components.fab>
|
</c-components.fab>
|
||||||
|
|||||||
@@ -50,7 +50,7 @@
|
|||||||
<a class="nav-link {% active_link views='insights_index' %}" href="{% url 'insights_index' %}">{% trans 'Insights' %}</a>
|
<a class="nav-link {% active_link views='insights_index' %}" href="{% url 'insights_index' %}">{% trans 'Insights' %}</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item dropdown">
|
<li class="nav-item dropdown">
|
||||||
<a class="nav-link dropdown-toggle {% active_link views='installment_plans_index||recurring_trasanctions_index||transactions_all_index||transactions_trash_index' %}"
|
<a class="nav-link dropdown-toggle {% active_link views='installment_plans_index||quick_transactions_index||recurring_trasanctions_index||transactions_all_index||transactions_trash_index' %}"
|
||||||
href="#" role="button"
|
href="#" role="button"
|
||||||
data-bs-toggle="dropdown"
|
data-bs-toggle="dropdown"
|
||||||
aria-expanded="false">
|
aria-expanded="false">
|
||||||
@@ -68,6 +68,8 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<hr class="dropdown-divider">
|
<hr class="dropdown-divider">
|
||||||
</li>
|
</li>
|
||||||
|
<li><a class="dropdown-item {% active_link views='quick_transactions_index' %}"
|
||||||
|
href="{% url 'quick_transactions_index' %}">{% translate 'Quick Transactions' %}</a></li>
|
||||||
<li><a class="dropdown-item {% active_link views='installment_plans_index' %}"
|
<li><a class="dropdown-item {% active_link views='installment_plans_index' %}"
|
||||||
href="{% url 'installment_plans_index' %}">{% translate 'Installment Plans' %}</a></li>
|
href="{% url 'installment_plans_index' %}">{% translate 'Installment Plans' %}</a></li>
|
||||||
<li><a class="dropdown-item {% active_link views='recurring_trasanctions_index' %}"
|
<li><a class="dropdown-item {% active_link views='recurring_trasanctions_index' %}"
|
||||||
|
|||||||
11
app/templates/quick_transactions/fragments/add.html
Normal file
11
app/templates/quick_transactions/fragments/add.html
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{% extends 'extends/offcanvas.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load crispy_forms_tags %}
|
||||||
|
|
||||||
|
{% block title %}{% translate 'Add quick transaction' %}{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<form hx-post="{% url 'quick_transaction_add' %}" hx-target="#generic-offcanvas" novalidate>
|
||||||
|
{% crispy form %}
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
17
app/templates/quick_transactions/fragments/create_menu.html
Normal file
17
app/templates/quick_transactions/fragments/create_menu.html
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{% extends 'extends/offcanvas.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load crispy_forms_tags %}
|
||||||
|
|
||||||
|
{% block title %}{% translate 'Add quick transaction' %}{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<div class="list-group list-group-flush">
|
||||||
|
{% for qt in quick_transactions %}
|
||||||
|
<a hx-get="{% url 'quick_transaction_add_as_transaction' quick_transaction_id=qt.id %}"
|
||||||
|
class="list-group-item list-group-item-action tw-cursor-pointer {% if qt.type == 'EX' %}!tw-text-red-400{% else %}!tw-text-green-400{% endif %}">{{ qt.name }}</a>
|
||||||
|
{% empty %}
|
||||||
|
<c-msg.empty title="{% translate "Nothing to see here..." %}" remove-padding></c-msg.empty>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
13
app/templates/quick_transactions/fragments/edit.html
Normal file
13
app/templates/quick_transactions/fragments/edit.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{% extends 'extends/offcanvas.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load crispy_forms_tags %}
|
||||||
|
|
||||||
|
{% block title %}{% translate 'Edit quick transaction' %}{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<form hx-post="{% url 'quick_transaction_edit' quick_transaction_id=quick_transaction.id %}"
|
||||||
|
hx-target="#generic-offcanvas"
|
||||||
|
novalidate>
|
||||||
|
{% crispy form %}
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
59
app/templates/quick_transactions/fragments/list.html
Normal file
59
app/templates/quick_transactions/fragments/list.html
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="quick-transactions-table">
|
||||||
|
{% if quick_transactions %}
|
||||||
|
<c-config.search></c-config.search>
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover align-middle">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col" class="col-auto"></th>
|
||||||
|
<th scope="col" class="col">{% translate 'Name' %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for qt in quick_transactions %}
|
||||||
|
<tr class="recurring_transaction">
|
||||||
|
<td class="col-auto text-center">
|
||||||
|
<div class="btn-group" role="group" aria-label="{% translate 'Actions' %}">
|
||||||
|
<a class="btn btn-secondary btn-sm"
|
||||||
|
role="button"
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
data-bs-title="{% translate "Edit" %}"
|
||||||
|
hx-get="{% url 'quick_transaction_edit' quick_transaction_id=qt.id %}"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-target="#generic-offcanvas">
|
||||||
|
<i class="fa-solid fa-pencil fa-fw"></i></a>
|
||||||
|
<a class="btn btn-secondary btn-sm text-danger"
|
||||||
|
role="button"
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
data-bs-title="{% translate "Delete" %}"
|
||||||
|
hx-delete="{% url 'quick_transaction_delete' quick_transaction_id=qt.id %}"
|
||||||
|
hx-trigger='confirmed'
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
data-bypass-on-ctrl="true"
|
||||||
|
data-title="{% translate "Are you sure?" %}"
|
||||||
|
data-text="{% translate "This will delete this item" %}"
|
||||||
|
data-confirm-text="{% translate "Yes, delete it!" %}"
|
||||||
|
_="install prompt_swal"><i class="fa-solid fa-trash fa-fw"></i></a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="col">
|
||||||
|
<div
|
||||||
|
class="{% if qt.type == 'EX' %}tw-text-red-400{% else %}tw-text-green-400{% endif %}">
|
||||||
|
{{ qt.name }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<c-msg.empty title="{% translate "Nothing to see here..." %}" remove-padding></c-msg.empty>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
25
app/templates/quick_transactions/pages/index.html
Normal file
25
app/templates/quick_transactions/pages/index.html
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{% extends "layouts/base.html" %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% translate 'Quick Transactions' %}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container px-md-3 py-3 column-gap-5">
|
||||||
|
<div class="tw-text-3xl fw-bold font-monospace tw-w-full mb-3">
|
||||||
|
{% spaceless %}
|
||||||
|
<div>{% translate 'Quick Transactions' %}<span>
|
||||||
|
<a class="text-decoration-none tw-text-2xl p-1 category-action"
|
||||||
|
role="button"
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
data-bs-title="{% translate "Add" %}"
|
||||||
|
hx-get="{% url 'quick_transaction_add' %}"
|
||||||
|
hx-target="#generic-offcanvas">
|
||||||
|
<i class="fa-solid fa-circle-plus fa-fw"></i></a>
|
||||||
|
</span></div>
|
||||||
|
{% endspaceless %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="quick-transactions-table" class="show-loading" hx-get="{% url 'quick_transactions_list' %}" hx-trigger="load, updated from:window"></div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user