diff --git a/app/apps/common/models.py b/app/apps/common/models.py
index 7d33dc5..d446921 100644
--- a/app/apps/common/models.py
+++ b/app/apps/common/models.py
@@ -65,6 +65,18 @@ class SharedObject(models.Model):
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):
owner = models.ForeignKey(
settings.AUTH_USER_MODEL,
diff --git a/app/apps/transactions/forms.py b/app/apps/transactions/forms.py
index 456cd3a..5299901 100644
--- a/app/apps/transactions/forms.py
+++ b/app/apps/transactions/forms.py
@@ -7,6 +7,7 @@ from crispy_forms.layout import (
Column,
Field,
Div,
+ HTML,
)
from django import forms
from django.db.models import Q
@@ -29,8 +30,8 @@ from apps.transactions.models import (
InstallmentPlan,
RecurringTransaction,
TransactionEntity,
+ QuickTransaction,
)
-from apps.common.middleware.thread_local import get_current_user
class TransactionForm(forms.ModelForm):
@@ -247,6 +248,140 @@ class TransactionForm(forms.ModelForm):
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("
"),
+ 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):
is_paid = forms.NullBooleanField(required=False)
diff --git a/app/apps/transactions/migrations/0043_quicktransaction.py b/app/apps/transactions/migrations/0043_quicktransaction.py
new file mode 100644
index 0000000..275d854
--- /dev/null
+++ b/app/apps/transactions/migrations/0043_quicktransaction.py
@@ -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',
+ },
+ ),
+ ]
diff --git a/app/apps/transactions/migrations/0044_alter_quicktransaction_unique_together.py b/app/apps/transactions/migrations/0044_alter_quicktransaction_unique_together.py
new file mode 100644
index 0000000..fa07fb1
--- /dev/null
+++ b/app/apps/transactions/migrations/0044_alter_quicktransaction_unique_together.py
@@ -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')},
+ ),
+ ]
diff --git a/app/apps/transactions/models.py b/app/apps/transactions/models.py
index 6fd6d36..82cfe90 100644
--- a/app/apps/transactions/models.py
+++ b/app/apps/transactions/models.py
@@ -16,7 +16,12 @@ from apps.common.templatetags.decimal import localize_number, drop_trailing_zero
from apps.currencies.utils.convert import convert
from apps.transactions.validators import validate_decimal_places, validate_non_negative
from apps.common.middleware.thread_local import get_current_user
-from apps.common.models import SharedObject, SharedObjectManager, OwnedObject
+from apps.common.models import (
+ SharedObject,
+ SharedObjectManager,
+ OwnedObject,
+ OwnedObjectManager,
+)
logger = logging.getLogger()
@@ -886,3 +891,86 @@ class RecurringTransaction(models.Model):
"""
today = timezone.localdate(timezone.now())
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)
diff --git a/app/apps/transactions/urls.py b/app/apps/transactions/urls.py
index d1a8fac..b808872 100644
--- a/app/apps/transactions/urls.py
+++ b/app/apps/transactions/urls.py
@@ -307,4 +307,39 @@ urlpatterns = [
views.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//edit/",
+ views.quick_transaction_edit,
+ name="quick_transaction_edit",
+ ),
+ path(
+ "quick-transactions//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//create/",
+ views.quick_transaction_add_as_transaction,
+ name="quick_transaction_add_as_transaction",
+ ),
]
diff --git a/app/apps/transactions/views/__init__.py b/app/apps/transactions/views/__init__.py
index fa20bc8..b784666 100644
--- a/app/apps/transactions/views/__init__.py
+++ b/app/apps/transactions/views/__init__.py
@@ -5,3 +5,4 @@ from .categories import *
from .actions import *
from .installment_plans import *
from .recurring_transactions import *
+from .quick_transactions import *
diff --git a/app/apps/transactions/views/quick_transactions.py b/app/apps/transactions/views/quick_transactions.py
new file mode 100644
index 0000000..26deec7
--- /dev/null
+++ b/app/apps/transactions/views/quick_transactions.py
@@ -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",
+ },
+ )
diff --git a/app/apps/users/migrations/0021_alter_usersettings_timezone.py b/app/apps/users/migrations/0021_alter_usersettings_timezone.py
new file mode 100644
index 0000000..1eba65e
--- /dev/null
+++ b/app/apps/users/migrations/0021_alter_usersettings_timezone.py
@@ -0,0 +1,18 @@
+# Generated by Django 5.1.11 on 2025-06-20 03:57
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('users', '0020_alter_usersettings_language'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='usersettings',
+ name='timezone',
+ field=models.CharField(choices=[('auto', 'Auto'), ('Africa/Abidjan', 'Africa/Abidjan'), ('Africa/Accra', 'Africa/Accra'), ('Africa/Addis_Ababa', 'Africa/Addis_Ababa'), ('Africa/Algiers', 'Africa/Algiers'), ('Africa/Asmara', 'Africa/Asmara'), ('Africa/Bamako', 'Africa/Bamako'), ('Africa/Bangui', 'Africa/Bangui'), ('Africa/Banjul', 'Africa/Banjul'), ('Africa/Bissau', 'Africa/Bissau'), ('Africa/Blantyre', 'Africa/Blantyre'), ('Africa/Brazzaville', 'Africa/Brazzaville'), ('Africa/Bujumbura', 'Africa/Bujumbura'), ('Africa/Cairo', 'Africa/Cairo'), ('Africa/Casablanca', 'Africa/Casablanca'), ('Africa/Ceuta', 'Africa/Ceuta'), ('Africa/Conakry', 'Africa/Conakry'), ('Africa/Dakar', 'Africa/Dakar'), ('Africa/Dar_es_Salaam', 'Africa/Dar_es_Salaam'), ('Africa/Djibouti', 'Africa/Djibouti'), ('Africa/Douala', 'Africa/Douala'), ('Africa/El_Aaiun', 'Africa/El_Aaiun'), ('Africa/Freetown', 'Africa/Freetown'), ('Africa/Gaborone', 'Africa/Gaborone'), ('Africa/Harare', 'Africa/Harare'), ('Africa/Johannesburg', 'Africa/Johannesburg'), ('Africa/Juba', 'Africa/Juba'), ('Africa/Kampala', 'Africa/Kampala'), ('Africa/Khartoum', 'Africa/Khartoum'), ('Africa/Kigali', 'Africa/Kigali'), ('Africa/Kinshasa', 'Africa/Kinshasa'), ('Africa/Lagos', 'Africa/Lagos'), ('Africa/Libreville', 'Africa/Libreville'), ('Africa/Lome', 'Africa/Lome'), ('Africa/Luanda', 'Africa/Luanda'), ('Africa/Lubumbashi', 'Africa/Lubumbashi'), ('Africa/Lusaka', 'Africa/Lusaka'), ('Africa/Malabo', 'Africa/Malabo'), ('Africa/Maputo', 'Africa/Maputo'), ('Africa/Maseru', 'Africa/Maseru'), ('Africa/Mbabane', 'Africa/Mbabane'), ('Africa/Mogadishu', 'Africa/Mogadishu'), ('Africa/Monrovia', 'Africa/Monrovia'), ('Africa/Nairobi', 'Africa/Nairobi'), ('Africa/Ndjamena', 'Africa/Ndjamena'), ('Africa/Niamey', 'Africa/Niamey'), ('Africa/Nouakchott', 'Africa/Nouakchott'), ('Africa/Ouagadougou', 'Africa/Ouagadougou'), ('Africa/Porto-Novo', 'Africa/Porto-Novo'), ('Africa/Sao_Tome', 'Africa/Sao_Tome'), ('Africa/Tripoli', 'Africa/Tripoli'), ('Africa/Tunis', 'Africa/Tunis'), ('Africa/Windhoek', 'Africa/Windhoek'), ('America/Adak', 'America/Adak'), ('America/Anchorage', 'America/Anchorage'), ('America/Anguilla', 'America/Anguilla'), ('America/Antigua', 'America/Antigua'), ('America/Araguaina', 'America/Araguaina'), ('America/Argentina/Buenos_Aires', 'America/Argentina/Buenos_Aires'), ('America/Argentina/Catamarca', 'America/Argentina/Catamarca'), ('America/Argentina/Cordoba', 'America/Argentina/Cordoba'), ('America/Argentina/Jujuy', 'America/Argentina/Jujuy'), ('America/Argentina/La_Rioja', 'America/Argentina/La_Rioja'), ('America/Argentina/Mendoza', 'America/Argentina/Mendoza'), ('America/Argentina/Rio_Gallegos', 'America/Argentina/Rio_Gallegos'), ('America/Argentina/Salta', 'America/Argentina/Salta'), ('America/Argentina/San_Juan', 'America/Argentina/San_Juan'), ('America/Argentina/San_Luis', 'America/Argentina/San_Luis'), ('America/Argentina/Tucuman', 'America/Argentina/Tucuman'), ('America/Argentina/Ushuaia', 'America/Argentina/Ushuaia'), ('America/Aruba', 'America/Aruba'), ('America/Asuncion', 'America/Asuncion'), ('America/Atikokan', 'America/Atikokan'), ('America/Bahia', 'America/Bahia'), ('America/Bahia_Banderas', 'America/Bahia_Banderas'), ('America/Barbados', 'America/Barbados'), ('America/Belem', 'America/Belem'), ('America/Belize', 'America/Belize'), ('America/Blanc-Sablon', 'America/Blanc-Sablon'), ('America/Boa_Vista', 'America/Boa_Vista'), ('America/Bogota', 'America/Bogota'), ('America/Boise', 'America/Boise'), ('America/Cambridge_Bay', 'America/Cambridge_Bay'), ('America/Campo_Grande', 'America/Campo_Grande'), ('America/Cancun', 'America/Cancun'), ('America/Caracas', 'America/Caracas'), ('America/Cayenne', 'America/Cayenne'), ('America/Cayman', 'America/Cayman'), ('America/Chicago', 'America/Chicago'), ('America/Chihuahua', 'America/Chihuahua'), ('America/Ciudad_Juarez', 'America/Ciudad_Juarez'), ('America/Costa_Rica', 'America/Costa_Rica'), ('America/Coyhaique', 'America/Coyhaique'), ('America/Creston', 'America/Creston'), ('America/Cuiaba', 'America/Cuiaba'), ('America/Curacao', 'America/Curacao'), ('America/Danmarkshavn', 'America/Danmarkshavn'), ('America/Dawson', 'America/Dawson'), ('America/Dawson_Creek', 'America/Dawson_Creek'), ('America/Denver', 'America/Denver'), ('America/Detroit', 'America/Detroit'), ('America/Dominica', 'America/Dominica'), ('America/Edmonton', 'America/Edmonton'), ('America/Eirunepe', 'America/Eirunepe'), ('America/El_Salvador', 'America/El_Salvador'), ('America/Fort_Nelson', 'America/Fort_Nelson'), ('America/Fortaleza', 'America/Fortaleza'), ('America/Glace_Bay', 'America/Glace_Bay'), ('America/Goose_Bay', 'America/Goose_Bay'), ('America/Grand_Turk', 'America/Grand_Turk'), ('America/Grenada', 'America/Grenada'), ('America/Guadeloupe', 'America/Guadeloupe'), ('America/Guatemala', 'America/Guatemala'), ('America/Guayaquil', 'America/Guayaquil'), ('America/Guyana', 'America/Guyana'), ('America/Halifax', 'America/Halifax'), ('America/Havana', 'America/Havana'), ('America/Hermosillo', 'America/Hermosillo'), ('America/Indiana/Indianapolis', 'America/Indiana/Indianapolis'), ('America/Indiana/Knox', 'America/Indiana/Knox'), ('America/Indiana/Marengo', 'America/Indiana/Marengo'), ('America/Indiana/Petersburg', 'America/Indiana/Petersburg'), ('America/Indiana/Tell_City', 'America/Indiana/Tell_City'), ('America/Indiana/Vevay', 'America/Indiana/Vevay'), ('America/Indiana/Vincennes', 'America/Indiana/Vincennes'), ('America/Indiana/Winamac', 'America/Indiana/Winamac'), ('America/Inuvik', 'America/Inuvik'), ('America/Iqaluit', 'America/Iqaluit'), ('America/Jamaica', 'America/Jamaica'), ('America/Juneau', 'America/Juneau'), ('America/Kentucky/Louisville', 'America/Kentucky/Louisville'), ('America/Kentucky/Monticello', 'America/Kentucky/Monticello'), ('America/Kralendijk', 'America/Kralendijk'), ('America/La_Paz', 'America/La_Paz'), ('America/Lima', 'America/Lima'), ('America/Los_Angeles', 'America/Los_Angeles'), ('America/Lower_Princes', 'America/Lower_Princes'), ('America/Maceio', 'America/Maceio'), ('America/Managua', 'America/Managua'), ('America/Manaus', 'America/Manaus'), ('America/Marigot', 'America/Marigot'), ('America/Martinique', 'America/Martinique'), ('America/Matamoros', 'America/Matamoros'), ('America/Mazatlan', 'America/Mazatlan'), ('America/Menominee', 'America/Menominee'), ('America/Merida', 'America/Merida'), ('America/Metlakatla', 'America/Metlakatla'), ('America/Mexico_City', 'America/Mexico_City'), ('America/Miquelon', 'America/Miquelon'), ('America/Moncton', 'America/Moncton'), ('America/Monterrey', 'America/Monterrey'), ('America/Montevideo', 'America/Montevideo'), ('America/Montserrat', 'America/Montserrat'), ('America/Nassau', 'America/Nassau'), ('America/New_York', 'America/New_York'), ('America/Nome', 'America/Nome'), ('America/Noronha', 'America/Noronha'), ('America/North_Dakota/Beulah', 'America/North_Dakota/Beulah'), ('America/North_Dakota/Center', 'America/North_Dakota/Center'), ('America/North_Dakota/New_Salem', 'America/North_Dakota/New_Salem'), ('America/Nuuk', 'America/Nuuk'), ('America/Ojinaga', 'America/Ojinaga'), ('America/Panama', 'America/Panama'), ('America/Paramaribo', 'America/Paramaribo'), ('America/Phoenix', 'America/Phoenix'), ('America/Port-au-Prince', 'America/Port-au-Prince'), ('America/Port_of_Spain', 'America/Port_of_Spain'), ('America/Porto_Velho', 'America/Porto_Velho'), ('America/Puerto_Rico', 'America/Puerto_Rico'), ('America/Punta_Arenas', 'America/Punta_Arenas'), ('America/Rankin_Inlet', 'America/Rankin_Inlet'), ('America/Recife', 'America/Recife'), ('America/Regina', 'America/Regina'), ('America/Resolute', 'America/Resolute'), ('America/Rio_Branco', 'America/Rio_Branco'), ('America/Santarem', 'America/Santarem'), ('America/Santiago', 'America/Santiago'), ('America/Santo_Domingo', 'America/Santo_Domingo'), ('America/Sao_Paulo', 'America/Sao_Paulo'), ('America/Scoresbysund', 'America/Scoresbysund'), ('America/Sitka', 'America/Sitka'), ('America/St_Barthelemy', 'America/St_Barthelemy'), ('America/St_Johns', 'America/St_Johns'), ('America/St_Kitts', 'America/St_Kitts'), ('America/St_Lucia', 'America/St_Lucia'), ('America/St_Thomas', 'America/St_Thomas'), ('America/St_Vincent', 'America/St_Vincent'), ('America/Swift_Current', 'America/Swift_Current'), ('America/Tegucigalpa', 'America/Tegucigalpa'), ('America/Thule', 'America/Thule'), ('America/Tijuana', 'America/Tijuana'), ('America/Toronto', 'America/Toronto'), ('America/Tortola', 'America/Tortola'), ('America/Vancouver', 'America/Vancouver'), ('America/Whitehorse', 'America/Whitehorse'), ('America/Winnipeg', 'America/Winnipeg'), ('America/Yakutat', 'America/Yakutat'), ('Antarctica/Casey', 'Antarctica/Casey'), ('Antarctica/Davis', 'Antarctica/Davis'), ('Antarctica/DumontDUrville', 'Antarctica/DumontDUrville'), ('Antarctica/Macquarie', 'Antarctica/Macquarie'), ('Antarctica/Mawson', 'Antarctica/Mawson'), ('Antarctica/McMurdo', 'Antarctica/McMurdo'), ('Antarctica/Palmer', 'Antarctica/Palmer'), ('Antarctica/Rothera', 'Antarctica/Rothera'), ('Antarctica/Syowa', 'Antarctica/Syowa'), ('Antarctica/Troll', 'Antarctica/Troll'), ('Antarctica/Vostok', 'Antarctica/Vostok'), ('Arctic/Longyearbyen', 'Arctic/Longyearbyen'), ('Asia/Aden', 'Asia/Aden'), ('Asia/Almaty', 'Asia/Almaty'), ('Asia/Amman', 'Asia/Amman'), ('Asia/Anadyr', 'Asia/Anadyr'), ('Asia/Aqtau', 'Asia/Aqtau'), ('Asia/Aqtobe', 'Asia/Aqtobe'), ('Asia/Ashgabat', 'Asia/Ashgabat'), ('Asia/Atyrau', 'Asia/Atyrau'), ('Asia/Baghdad', 'Asia/Baghdad'), ('Asia/Bahrain', 'Asia/Bahrain'), ('Asia/Baku', 'Asia/Baku'), ('Asia/Bangkok', 'Asia/Bangkok'), ('Asia/Barnaul', 'Asia/Barnaul'), ('Asia/Beirut', 'Asia/Beirut'), ('Asia/Bishkek', 'Asia/Bishkek'), ('Asia/Brunei', 'Asia/Brunei'), ('Asia/Chita', 'Asia/Chita'), ('Asia/Colombo', 'Asia/Colombo'), ('Asia/Damascus', 'Asia/Damascus'), ('Asia/Dhaka', 'Asia/Dhaka'), ('Asia/Dili', 'Asia/Dili'), ('Asia/Dubai', 'Asia/Dubai'), ('Asia/Dushanbe', 'Asia/Dushanbe'), ('Asia/Famagusta', 'Asia/Famagusta'), ('Asia/Gaza', 'Asia/Gaza'), ('Asia/Hebron', 'Asia/Hebron'), ('Asia/Ho_Chi_Minh', 'Asia/Ho_Chi_Minh'), ('Asia/Hong_Kong', 'Asia/Hong_Kong'), ('Asia/Hovd', 'Asia/Hovd'), ('Asia/Irkutsk', 'Asia/Irkutsk'), ('Asia/Jakarta', 'Asia/Jakarta'), ('Asia/Jayapura', 'Asia/Jayapura'), ('Asia/Jerusalem', 'Asia/Jerusalem'), ('Asia/Kabul', 'Asia/Kabul'), ('Asia/Kamchatka', 'Asia/Kamchatka'), ('Asia/Karachi', 'Asia/Karachi'), ('Asia/Kathmandu', 'Asia/Kathmandu'), ('Asia/Khandyga', 'Asia/Khandyga'), ('Asia/Kolkata', 'Asia/Kolkata'), ('Asia/Krasnoyarsk', 'Asia/Krasnoyarsk'), ('Asia/Kuala_Lumpur', 'Asia/Kuala_Lumpur'), ('Asia/Kuching', 'Asia/Kuching'), ('Asia/Kuwait', 'Asia/Kuwait'), ('Asia/Macau', 'Asia/Macau'), ('Asia/Magadan', 'Asia/Magadan'), ('Asia/Makassar', 'Asia/Makassar'), ('Asia/Manila', 'Asia/Manila'), ('Asia/Muscat', 'Asia/Muscat'), ('Asia/Nicosia', 'Asia/Nicosia'), ('Asia/Novokuznetsk', 'Asia/Novokuznetsk'), ('Asia/Novosibirsk', 'Asia/Novosibirsk'), ('Asia/Omsk', 'Asia/Omsk'), ('Asia/Oral', 'Asia/Oral'), ('Asia/Phnom_Penh', 'Asia/Phnom_Penh'), ('Asia/Pontianak', 'Asia/Pontianak'), ('Asia/Pyongyang', 'Asia/Pyongyang'), ('Asia/Qatar', 'Asia/Qatar'), ('Asia/Qostanay', 'Asia/Qostanay'), ('Asia/Qyzylorda', 'Asia/Qyzylorda'), ('Asia/Riyadh', 'Asia/Riyadh'), ('Asia/Sakhalin', 'Asia/Sakhalin'), ('Asia/Samarkand', 'Asia/Samarkand'), ('Asia/Seoul', 'Asia/Seoul'), ('Asia/Shanghai', 'Asia/Shanghai'), ('Asia/Singapore', 'Asia/Singapore'), ('Asia/Srednekolymsk', 'Asia/Srednekolymsk'), ('Asia/Taipei', 'Asia/Taipei'), ('Asia/Tashkent', 'Asia/Tashkent'), ('Asia/Tbilisi', 'Asia/Tbilisi'), ('Asia/Tehran', 'Asia/Tehran'), ('Asia/Thimphu', 'Asia/Thimphu'), ('Asia/Tokyo', 'Asia/Tokyo'), ('Asia/Tomsk', 'Asia/Tomsk'), ('Asia/Ulaanbaatar', 'Asia/Ulaanbaatar'), ('Asia/Urumqi', 'Asia/Urumqi'), ('Asia/Ust-Nera', 'Asia/Ust-Nera'), ('Asia/Vientiane', 'Asia/Vientiane'), ('Asia/Vladivostok', 'Asia/Vladivostok'), ('Asia/Yakutsk', 'Asia/Yakutsk'), ('Asia/Yangon', 'Asia/Yangon'), ('Asia/Yekaterinburg', 'Asia/Yekaterinburg'), ('Asia/Yerevan', 'Asia/Yerevan'), ('Atlantic/Azores', 'Atlantic/Azores'), ('Atlantic/Bermuda', 'Atlantic/Bermuda'), ('Atlantic/Canary', 'Atlantic/Canary'), ('Atlantic/Cape_Verde', 'Atlantic/Cape_Verde'), ('Atlantic/Faroe', 'Atlantic/Faroe'), ('Atlantic/Madeira', 'Atlantic/Madeira'), ('Atlantic/Reykjavik', 'Atlantic/Reykjavik'), ('Atlantic/South_Georgia', 'Atlantic/South_Georgia'), ('Atlantic/St_Helena', 'Atlantic/St_Helena'), ('Atlantic/Stanley', 'Atlantic/Stanley'), ('Australia/Adelaide', 'Australia/Adelaide'), ('Australia/Brisbane', 'Australia/Brisbane'), ('Australia/Broken_Hill', 'Australia/Broken_Hill'), ('Australia/Darwin', 'Australia/Darwin'), ('Australia/Eucla', 'Australia/Eucla'), ('Australia/Hobart', 'Australia/Hobart'), ('Australia/Lindeman', 'Australia/Lindeman'), ('Australia/Lord_Howe', 'Australia/Lord_Howe'), ('Australia/Melbourne', 'Australia/Melbourne'), ('Australia/Perth', 'Australia/Perth'), ('Australia/Sydney', 'Australia/Sydney'), ('Canada/Atlantic', 'Canada/Atlantic'), ('Canada/Central', 'Canada/Central'), ('Canada/Eastern', 'Canada/Eastern'), ('Canada/Mountain', 'Canada/Mountain'), ('Canada/Newfoundland', 'Canada/Newfoundland'), ('Canada/Pacific', 'Canada/Pacific'), ('Europe/Amsterdam', 'Europe/Amsterdam'), ('Europe/Andorra', 'Europe/Andorra'), ('Europe/Astrakhan', 'Europe/Astrakhan'), ('Europe/Athens', 'Europe/Athens'), ('Europe/Belgrade', 'Europe/Belgrade'), ('Europe/Berlin', 'Europe/Berlin'), ('Europe/Bratislava', 'Europe/Bratislava'), ('Europe/Brussels', 'Europe/Brussels'), ('Europe/Bucharest', 'Europe/Bucharest'), ('Europe/Budapest', 'Europe/Budapest'), ('Europe/Busingen', 'Europe/Busingen'), ('Europe/Chisinau', 'Europe/Chisinau'), ('Europe/Copenhagen', 'Europe/Copenhagen'), ('Europe/Dublin', 'Europe/Dublin'), ('Europe/Gibraltar', 'Europe/Gibraltar'), ('Europe/Guernsey', 'Europe/Guernsey'), ('Europe/Helsinki', 'Europe/Helsinki'), ('Europe/Isle_of_Man', 'Europe/Isle_of_Man'), ('Europe/Istanbul', 'Europe/Istanbul'), ('Europe/Jersey', 'Europe/Jersey'), ('Europe/Kaliningrad', 'Europe/Kaliningrad'), ('Europe/Kirov', 'Europe/Kirov'), ('Europe/Kyiv', 'Europe/Kyiv'), ('Europe/Lisbon', 'Europe/Lisbon'), ('Europe/Ljubljana', 'Europe/Ljubljana'), ('Europe/London', 'Europe/London'), ('Europe/Luxembourg', 'Europe/Luxembourg'), ('Europe/Madrid', 'Europe/Madrid'), ('Europe/Malta', 'Europe/Malta'), ('Europe/Mariehamn', 'Europe/Mariehamn'), ('Europe/Minsk', 'Europe/Minsk'), ('Europe/Monaco', 'Europe/Monaco'), ('Europe/Moscow', 'Europe/Moscow'), ('Europe/Oslo', 'Europe/Oslo'), ('Europe/Paris', 'Europe/Paris'), ('Europe/Podgorica', 'Europe/Podgorica'), ('Europe/Prague', 'Europe/Prague'), ('Europe/Riga', 'Europe/Riga'), ('Europe/Rome', 'Europe/Rome'), ('Europe/Samara', 'Europe/Samara'), ('Europe/San_Marino', 'Europe/San_Marino'), ('Europe/Sarajevo', 'Europe/Sarajevo'), ('Europe/Saratov', 'Europe/Saratov'), ('Europe/Simferopol', 'Europe/Simferopol'), ('Europe/Skopje', 'Europe/Skopje'), ('Europe/Sofia', 'Europe/Sofia'), ('Europe/Stockholm', 'Europe/Stockholm'), ('Europe/Tallinn', 'Europe/Tallinn'), ('Europe/Tirane', 'Europe/Tirane'), ('Europe/Ulyanovsk', 'Europe/Ulyanovsk'), ('Europe/Vaduz', 'Europe/Vaduz'), ('Europe/Vatican', 'Europe/Vatican'), ('Europe/Vienna', 'Europe/Vienna'), ('Europe/Vilnius', 'Europe/Vilnius'), ('Europe/Volgograd', 'Europe/Volgograd'), ('Europe/Warsaw', 'Europe/Warsaw'), ('Europe/Zagreb', 'Europe/Zagreb'), ('Europe/Zurich', 'Europe/Zurich'), ('GMT', 'GMT'), ('Indian/Antananarivo', 'Indian/Antananarivo'), ('Indian/Chagos', 'Indian/Chagos'), ('Indian/Christmas', 'Indian/Christmas'), ('Indian/Cocos', 'Indian/Cocos'), ('Indian/Comoro', 'Indian/Comoro'), ('Indian/Kerguelen', 'Indian/Kerguelen'), ('Indian/Mahe', 'Indian/Mahe'), ('Indian/Maldives', 'Indian/Maldives'), ('Indian/Mauritius', 'Indian/Mauritius'), ('Indian/Mayotte', 'Indian/Mayotte'), ('Indian/Reunion', 'Indian/Reunion'), ('Pacific/Apia', 'Pacific/Apia'), ('Pacific/Auckland', 'Pacific/Auckland'), ('Pacific/Bougainville', 'Pacific/Bougainville'), ('Pacific/Chatham', 'Pacific/Chatham'), ('Pacific/Chuuk', 'Pacific/Chuuk'), ('Pacific/Easter', 'Pacific/Easter'), ('Pacific/Efate', 'Pacific/Efate'), ('Pacific/Fakaofo', 'Pacific/Fakaofo'), ('Pacific/Fiji', 'Pacific/Fiji'), ('Pacific/Funafuti', 'Pacific/Funafuti'), ('Pacific/Galapagos', 'Pacific/Galapagos'), ('Pacific/Gambier', 'Pacific/Gambier'), ('Pacific/Guadalcanal', 'Pacific/Guadalcanal'), ('Pacific/Guam', 'Pacific/Guam'), ('Pacific/Honolulu', 'Pacific/Honolulu'), ('Pacific/Kanton', 'Pacific/Kanton'), ('Pacific/Kiritimati', 'Pacific/Kiritimati'), ('Pacific/Kosrae', 'Pacific/Kosrae'), ('Pacific/Kwajalein', 'Pacific/Kwajalein'), ('Pacific/Majuro', 'Pacific/Majuro'), ('Pacific/Marquesas', 'Pacific/Marquesas'), ('Pacific/Midway', 'Pacific/Midway'), ('Pacific/Nauru', 'Pacific/Nauru'), ('Pacific/Niue', 'Pacific/Niue'), ('Pacific/Norfolk', 'Pacific/Norfolk'), ('Pacific/Noumea', 'Pacific/Noumea'), ('Pacific/Pago_Pago', 'Pacific/Pago_Pago'), ('Pacific/Palau', 'Pacific/Palau'), ('Pacific/Pitcairn', 'Pacific/Pitcairn'), ('Pacific/Pohnpei', 'Pacific/Pohnpei'), ('Pacific/Port_Moresby', 'Pacific/Port_Moresby'), ('Pacific/Rarotonga', 'Pacific/Rarotonga'), ('Pacific/Saipan', 'Pacific/Saipan'), ('Pacific/Tahiti', 'Pacific/Tahiti'), ('Pacific/Tarawa', 'Pacific/Tarawa'), ('Pacific/Tongatapu', 'Pacific/Tongatapu'), ('Pacific/Wake', 'Pacific/Wake'), ('Pacific/Wallis', 'Pacific/Wallis'), ('US/Alaska', 'US/Alaska'), ('US/Arizona', 'US/Arizona'), ('US/Central', 'US/Central'), ('US/Eastern', 'US/Eastern'), ('US/Hawaii', 'US/Hawaii'), ('US/Mountain', 'US/Mountain'), ('US/Pacific', 'US/Pacific'), ('UTC', 'UTC')], default='auto', max_length=50, verbose_name='Time Zone'),
+ ),
+ ]
diff --git a/app/templates/cotton/transaction/item.html b/app/templates/cotton/transaction/item.html
index 7df49bd..3f9df66 100644
--- a/app/templates/cotton/transaction/item.html
+++ b/app/templates/cotton/transaction/item.html
@@ -15,7 +15,8 @@
_="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">
-
+
{% if not transaction.deleted %}
+
diff --git a/app/templates/includes/navbar.html b/app/templates/includes/navbar.html
index 9df35f6..4b4b7f5 100644
--- a/app/templates/includes/navbar.html
+++ b/app/templates/includes/navbar.html
@@ -50,7 +50,7 @@
{% trans 'Insights' %}
-
@@ -68,6 +68,8 @@
{% endif %}
+
{% translate 'Quick Transactions' %}
{% translate 'Installment Plans' %}
+ {% crispy form %}
+
+{% endblock %}
diff --git a/app/templates/quick_transactions/fragments/create_menu.html b/app/templates/quick_transactions/fragments/create_menu.html
new file mode 100644
index 0000000..af2d84e
--- /dev/null
+++ b/app/templates/quick_transactions/fragments/create_menu.html
@@ -0,0 +1,17 @@
+{% extends 'extends/offcanvas.html' %}
+{% load i18n %}
+{% load crispy_forms_tags %}
+
+{% block title %}{% translate 'Add quick transaction' %}{% endblock %}
+
+{% block body %}
+
+ {% for qt in quick_transactions %}
+
{{ qt.name }}
+ {% empty %}
+
+ {% endfor %}
+
+
+{% endblock %}
diff --git a/app/templates/quick_transactions/fragments/edit.html b/app/templates/quick_transactions/fragments/edit.html
new file mode 100644
index 0000000..608da0f
--- /dev/null
+++ b/app/templates/quick_transactions/fragments/edit.html
@@ -0,0 +1,13 @@
+{% extends 'extends/offcanvas.html' %}
+{% load i18n %}
+{% load crispy_forms_tags %}
+
+{% block title %}{% translate 'Edit quick transaction' %}{% endblock %}
+
+{% block body %}
+
+{% endblock %}
diff --git a/app/templates/quick_transactions/fragments/list.html b/app/templates/quick_transactions/fragments/list.html
new file mode 100644
index 0000000..9682e84
--- /dev/null
+++ b/app/templates/quick_transactions/fragments/list.html
@@ -0,0 +1,59 @@
+{% load i18n %}
+
+
+
+ {% if quick_transactions %}
+
+
+
+
+
+
+ |
+ {% translate 'Name' %} |
+
+
+
+ {% for qt in quick_transactions %}
+
+ |
+
+ |
+
+
+ {{ qt.name }}
+
+ |
+
+ {% endfor %}
+
+
+
+ {% else %}
+
+ {% endif %}
+
+
+
diff --git a/app/templates/quick_transactions/pages/index.html b/app/templates/quick_transactions/pages/index.html
new file mode 100644
index 0000000..5db431a
--- /dev/null
+++ b/app/templates/quick_transactions/pages/index.html
@@ -0,0 +1,25 @@
+{% extends "layouts/base.html" %}
+
+{% load i18n %}
+
+{% block title %}{% translate 'Quick Transactions' %}{% endblock %}
+
+{% block content %}
+
+
+ {% spaceless %}
+
{% translate 'Quick Transactions' %}
+
+
+
+ {% endspaceless %}
+
+
+
+
+{% endblock %}