mirror of
https://github.com/eitchtee/WYGIWYH.git
synced 2026-03-25 02:41:15 +01:00
changes
This commit is contained in:
@@ -1,16 +1,18 @@
|
||||
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, Field, Row, Column
|
||||
from crispy_forms.layout import Layout, Field, Row, Column, HTML
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from apps.common.widgets.crispy.submit import NoClassSubmit
|
||||
from apps.common.widgets.crispy.submit import NoClassSubmit
|
||||
from apps.common.widgets.tom_select import TomSelect, TransactionSelect
|
||||
from apps.rules.models import TransactionRule, UpdateOrCreateTransactionRuleAction
|
||||
from apps.rules.models import TransactionRuleAction
|
||||
from apps.common.fields.forms.dynamic_select import DynamicModelChoiceField
|
||||
from apps.transactions.forms import BulkEditTransactionForm
|
||||
from apps.transactions.models import Transaction
|
||||
|
||||
|
||||
@@ -431,6 +433,17 @@ class DryRunCreatedTransacion(forms.Form):
|
||||
),
|
||||
)
|
||||
|
||||
if self.data.get("transaction"):
|
||||
try:
|
||||
transaction = Transaction.objects.get(id=self.data.get("transaction"))
|
||||
except Transaction.DoesNotExist:
|
||||
transaction = None
|
||||
|
||||
if transaction:
|
||||
self.fields["transaction"].queryset = Transaction.objects.filter(
|
||||
id=transaction.id
|
||||
)
|
||||
|
||||
|
||||
class DryRunDeletedTransacion(forms.Form):
|
||||
transaction = DynamicModelChoiceField(
|
||||
@@ -456,3 +469,49 @@ class DryRunDeletedTransacion(forms.Form):
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
if self.data.get("transaction"):
|
||||
try:
|
||||
transaction = Transaction.objects.get(id=self.data.get("transaction"))
|
||||
except Transaction.DoesNotExist:
|
||||
transaction = None
|
||||
|
||||
if transaction:
|
||||
self.fields["transaction"].queryset = Transaction.objects.filter(
|
||||
id=transaction.id
|
||||
)
|
||||
|
||||
|
||||
class DryRunUpdatedTransactionForm(BulkEditTransactionForm):
|
||||
transaction = DynamicModelChoiceField(
|
||||
model=Transaction,
|
||||
to_field_name="id",
|
||||
label=_("Transaction"),
|
||||
required=True,
|
||||
queryset=Transaction.objects.none(),
|
||||
widget=TransactionSelect(clear_button=False, income=True, expense=True),
|
||||
help_text=_("Type to search for a transaction"),
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.helper.layout.insert(0, "transaction")
|
||||
self.helper.layout.insert(1, HTML("<hr/>"))
|
||||
|
||||
# Change submit button
|
||||
self.helper.layout[-1] = FormActions(
|
||||
NoClassSubmit(
|
||||
"submit", _("Test"), css_class="btn btn-outline-primary w-100"
|
||||
)
|
||||
)
|
||||
|
||||
if self.data.get("transaction"):
|
||||
try:
|
||||
transaction = Transaction.objects.get(id=self.data.get("transaction"))
|
||||
except Transaction.DoesNotExist:
|
||||
transaction = None
|
||||
|
||||
if transaction:
|
||||
self.fields["transaction"].queryset = Transaction.objects.filter(
|
||||
id=transaction.id
|
||||
)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import decimal
|
||||
import logging
|
||||
import traceback
|
||||
from copy import deepcopy
|
||||
from datetime import datetime, date
|
||||
from decimal import Decimal
|
||||
from itertools import chain
|
||||
@@ -53,7 +52,7 @@ class DryRunResults:
|
||||
):
|
||||
result = {
|
||||
"type": "edit_transaction",
|
||||
"transaction": deepcopy(instance),
|
||||
"transaction": instance.deepcopy(),
|
||||
"action": action,
|
||||
"old_value": old_value,
|
||||
"new_value": new_value,
|
||||
@@ -248,7 +247,7 @@ def check_for_transaction_rules(
|
||||
if searched_transactions.exists():
|
||||
transaction = searched_transactions.first()
|
||||
existing = True
|
||||
starting_instance = deepcopy(transaction)
|
||||
starting_instance = transaction.deepcopy()
|
||||
_log("Found at least one matching transaction, using latest:")
|
||||
_log("{}".format(pformat(model_to_dict(transaction))))
|
||||
else:
|
||||
@@ -322,82 +321,62 @@ def check_for_transaction_rules(
|
||||
else:
|
||||
transaction.category = TransactionCategory.objects.get(name=value)
|
||||
|
||||
if dry_run:
|
||||
if not transaction.id:
|
||||
_log("Transaction would be created as:")
|
||||
else:
|
||||
_log("Trasanction would be updated as:")
|
||||
|
||||
_log(
|
||||
"{}".format(
|
||||
pformat(model_to_dict(transaction, exclude=["tags", "entities"])),
|
||||
)
|
||||
)
|
||||
if not transaction.id:
|
||||
_log("Transaction will be created as:")
|
||||
else:
|
||||
if not transaction.id:
|
||||
_log("Transaction will be created as:")
|
||||
else:
|
||||
_log("Trasanction will be updated as:")
|
||||
_log("Trasanction will be updated as:")
|
||||
|
||||
_log(
|
||||
"{}".format(
|
||||
pformat(model_to_dict(transaction, exclude=["tags", "entities"])),
|
||||
)
|
||||
_log(
|
||||
"{}".format(
|
||||
pformat(model_to_dict(transaction, exclude=["tags", "entities"])),
|
||||
)
|
||||
transaction.save()
|
||||
)
|
||||
transaction.save()
|
||||
|
||||
# Handle M2M fields after save
|
||||
tags = []
|
||||
if processed_action.set_tags:
|
||||
tags = simple.eval(processed_action.set_tags)
|
||||
if dry_run:
|
||||
_log(f" And tags would be set as: {tags}")
|
||||
else:
|
||||
_log(f" And tags will be set as: {tags}")
|
||||
transaction.tags.clear()
|
||||
if isinstance(tags, (list, tuple)):
|
||||
for tag in tags:
|
||||
if isinstance(tag, int):
|
||||
transaction.tags.add(TransactionTag.objects.get(id=tag))
|
||||
else:
|
||||
transaction.tags.add(TransactionTag.objects.get(name=tag))
|
||||
elif isinstance(tags, (int, str)):
|
||||
if isinstance(tags, int):
|
||||
transaction.tags.add(TransactionTag.objects.get(id=tags))
|
||||
_log(f" And tags will be set as: {tags}")
|
||||
transaction.tags.clear()
|
||||
if isinstance(tags, (list, tuple)):
|
||||
for tag in tags:
|
||||
if isinstance(tag, int):
|
||||
transaction.tags.add(TransactionTag.objects.get(id=tag))
|
||||
else:
|
||||
transaction.tags.add(TransactionTag.objects.get(name=tags))
|
||||
transaction.tags.add(TransactionTag.objects.get(name=tag))
|
||||
elif isinstance(tags, (int, str)):
|
||||
if isinstance(tags, int):
|
||||
transaction.tags.add(TransactionTag.objects.get(id=tags))
|
||||
else:
|
||||
transaction.tags.add(TransactionTag.objects.get(name=tags))
|
||||
|
||||
entities = []
|
||||
if processed_action.set_entities:
|
||||
entities = simple.eval(processed_action.set_entities)
|
||||
if dry_run:
|
||||
_log(f" And entities would be set as: {entities}")
|
||||
else:
|
||||
_log(f" And entities will be set as: {entities}")
|
||||
transaction.entities.clear()
|
||||
if isinstance(entities, (list, tuple)):
|
||||
for entity in entities:
|
||||
if isinstance(entity, int):
|
||||
transaction.entities.add(
|
||||
TransactionEntity.objects.get(id=entity)
|
||||
)
|
||||
else:
|
||||
transaction.entities.add(
|
||||
TransactionEntity.objects.get(name=entity)
|
||||
)
|
||||
elif isinstance(entities, (int, str)):
|
||||
if isinstance(entities, int):
|
||||
_log(f" And entities will be set as: {entities}")
|
||||
transaction.entities.clear()
|
||||
if isinstance(entities, (list, tuple)):
|
||||
for entity in entities:
|
||||
if isinstance(entity, int):
|
||||
transaction.entities.add(
|
||||
TransactionEntity.objects.get(id=entities)
|
||||
TransactionEntity.objects.get(id=entity)
|
||||
)
|
||||
else:
|
||||
transaction.entities.add(
|
||||
TransactionEntity.objects.get(name=entities)
|
||||
TransactionEntity.objects.get(name=entity)
|
||||
)
|
||||
elif isinstance(entities, (int, str)):
|
||||
if isinstance(entities, int):
|
||||
transaction.entities.add(TransactionEntity.objects.get(id=entities))
|
||||
else:
|
||||
transaction.entities.add(
|
||||
TransactionEntity.objects.get(name=entities)
|
||||
)
|
||||
|
||||
dry_run_results.update_or_create_transaction(
|
||||
start_instance=starting_instance,
|
||||
end_instance=deepcopy(transaction),
|
||||
end_instance=transaction.deepcopy(),
|
||||
updated=existing,
|
||||
action=processed_action,
|
||||
query=search_query,
|
||||
@@ -438,19 +417,19 @@ def check_for_transaction_rules(
|
||||
transaction.category = category
|
||||
|
||||
elif field == TransactionRuleAction.Field.tags:
|
||||
if not dry_run:
|
||||
transaction.tags.clear()
|
||||
transaction.tags.clear()
|
||||
|
||||
if isinstance(new_value, list):
|
||||
for tag_value in new_value:
|
||||
if isinstance(tag_value, int):
|
||||
tag = TransactionTag.objects.get(id=tag_value)
|
||||
if not dry_run:
|
||||
transaction.tags.add(tag)
|
||||
|
||||
transaction.tags.add(tag)
|
||||
tags.append(tag)
|
||||
elif isinstance(tag_value, str):
|
||||
tag = TransactionTag.objects.get(name=tag_value)
|
||||
if not dry_run:
|
||||
transaction.tags.add(tag)
|
||||
|
||||
transaction.tags.add(tag)
|
||||
tags.append(tag)
|
||||
|
||||
elif isinstance(new_value, (int, str)):
|
||||
@@ -459,24 +438,22 @@ def check_for_transaction_rules(
|
||||
else:
|
||||
tag = TransactionTag.objects.get(name=new_value)
|
||||
|
||||
if not dry_run:
|
||||
transaction.tags.add(tag)
|
||||
transaction.tags.add(tag)
|
||||
tags.append(tag)
|
||||
|
||||
elif field == TransactionRuleAction.Field.entities:
|
||||
if not dry_run:
|
||||
transaction.entities.clear()
|
||||
transaction.entities.clear()
|
||||
if isinstance(new_value, list):
|
||||
for entity_value in new_value:
|
||||
if isinstance(entity_value, int):
|
||||
entity = TransactionEntity.objects.get(id=entity_value)
|
||||
if not dry_run:
|
||||
transaction.entities.add(entity)
|
||||
|
||||
transaction.entities.add(entity)
|
||||
entities.append(entity)
|
||||
elif isinstance(entity_value, str):
|
||||
entity = TransactionEntity.objects.get(name=entity_value)
|
||||
if not dry_run:
|
||||
transaction.entities.add(entity)
|
||||
|
||||
transaction.entities.add(entity)
|
||||
entities.append(entity)
|
||||
|
||||
elif isinstance(new_value, (int, str)):
|
||||
@@ -484,8 +461,8 @@ def check_for_transaction_rules(
|
||||
entity = TransactionEntity.objects.get(id=new_value)
|
||||
else:
|
||||
entity = TransactionEntity.objects.get(name=new_value)
|
||||
if not dry_run:
|
||||
transaction.entities.add(entity)
|
||||
|
||||
transaction.entities.add(entity)
|
||||
entities.append(entity)
|
||||
|
||||
else:
|
||||
@@ -496,7 +473,7 @@ def check_for_transaction_rules(
|
||||
)
|
||||
|
||||
dry_run_results.edit_transaction(
|
||||
instance=deepcopy(transaction),
|
||||
instance=transaction.deepcopy(),
|
||||
action=processed_action,
|
||||
old_value=original_value,
|
||||
new_value=new_value,
|
||||
@@ -530,7 +507,7 @@ def check_for_transaction_rules(
|
||||
# Regular transaction processing for creates and updates
|
||||
instance = Transaction.objects.get(id=instance_id)
|
||||
|
||||
dry_run_results.triggering_transaction(deepcopy(instance))
|
||||
dry_run_results.triggering_transaction(instance.deepcopy())
|
||||
|
||||
functions = {
|
||||
"relativedelta": relativedelta,
|
||||
@@ -667,7 +644,7 @@ def check_for_transaction_rules(
|
||||
level="error",
|
||||
)
|
||||
# Save at the end
|
||||
if not dry_run and signal != "transaction_deleted":
|
||||
if signal != "transaction_deleted":
|
||||
instance.save()
|
||||
else:
|
||||
_log(
|
||||
@@ -702,7 +679,7 @@ def check_for_transaction_rules(
|
||||
if rule.sequenced:
|
||||
# Update names for next actions
|
||||
simple.names.update(_get_names(instance))
|
||||
if not dry_run and signal != "transaction_deleted":
|
||||
if signal != "transaction_deleted":
|
||||
instance.save()
|
||||
|
||||
for action in update_or_create_actions:
|
||||
@@ -741,7 +718,6 @@ def check_for_transaction_rules(
|
||||
|
||||
delete_current_user()
|
||||
|
||||
if dry_run:
|
||||
return logs, dry_run_results.results
|
||||
return logs, dry_run_results.results
|
||||
|
||||
return None
|
||||
|
||||
@@ -52,6 +52,11 @@ urlpatterns = [
|
||||
views.dry_run_rule_deleted,
|
||||
name="transaction_rule_dry_run_deleted",
|
||||
),
|
||||
path(
|
||||
"rules/transaction/<int:pk>/dry-run/updated/",
|
||||
views.dry_run_rule_updated,
|
||||
name="transaction_rule_dry_run_updated",
|
||||
),
|
||||
path(
|
||||
"rules/transaction/<int:pk>/share/",
|
||||
views.transaction_rule_share,
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
from itertools import chain
|
||||
|
||||
from copy import deepcopy
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.db import transaction
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import render, get_object_or_404, redirect
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@@ -14,6 +17,7 @@ from apps.rules.forms import (
|
||||
UpdateOrCreateTransactionRuleActionForm,
|
||||
DryRunCreatedTransacion,
|
||||
DryRunDeletedTransacion,
|
||||
DryRunUpdatedTransactionForm,
|
||||
)
|
||||
from apps.rules.models import (
|
||||
TransactionRule,
|
||||
@@ -25,6 +29,9 @@ from apps.common.forms import SharedObjectForm
|
||||
from apps.common.decorators.demo import disabled_on_demo
|
||||
from apps.rules.tasks import check_for_transaction_rules
|
||||
from apps.common.middleware.thread_local import get_current_user
|
||||
from apps.rules.signals import transaction_created, transaction_updated
|
||||
from apps.rules.utils.transactions import serialize_transaction
|
||||
from apps.transactions.models import Transaction
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -436,14 +443,28 @@ def dry_run_rule_created(request, pk):
|
||||
if request.method == "POST":
|
||||
form = DryRunCreatedTransacion(request.POST)
|
||||
if form.is_valid():
|
||||
logs, results = check_for_transaction_rules(
|
||||
instance_id=form.cleaned_data["transaction"].id,
|
||||
signal="transaction_created",
|
||||
dry_run=True,
|
||||
rule_id=rule.id,
|
||||
user_id=get_current_user().id,
|
||||
)
|
||||
logs = "\n".join(logs)
|
||||
try:
|
||||
with transaction.atomic():
|
||||
logs, results = check_for_transaction_rules(
|
||||
instance_id=form.cleaned_data["transaction"].id,
|
||||
signal="transaction_created",
|
||||
dry_run=True,
|
||||
rule_id=rule.id,
|
||||
user_id=get_current_user().id,
|
||||
)
|
||||
logs = "\n".join(logs)
|
||||
|
||||
response = render(
|
||||
request,
|
||||
"rules/fragments/transaction_rule/dry_run/created.html",
|
||||
{"form": form, "rule": rule, "logs": logs, "results": results},
|
||||
)
|
||||
|
||||
raise Exception("ROLLBACK")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return response
|
||||
|
||||
else:
|
||||
form = DryRunCreatedTransacion()
|
||||
@@ -467,14 +488,28 @@ def dry_run_rule_deleted(request, pk):
|
||||
if request.method == "POST":
|
||||
form = DryRunDeletedTransacion(request.POST)
|
||||
if form.is_valid():
|
||||
logs, results = check_for_transaction_rules(
|
||||
instance_id=form.cleaned_data["transaction"].id,
|
||||
signal="transaction_deleted",
|
||||
dry_run=True,
|
||||
rule_id=rule.id,
|
||||
user_id=get_current_user().id,
|
||||
)
|
||||
logs = "\n".join(logs)
|
||||
try:
|
||||
with transaction.atomic():
|
||||
logs, results = check_for_transaction_rules(
|
||||
instance_id=form.cleaned_data["transaction"].id,
|
||||
signal="transaction_deleted",
|
||||
dry_run=True,
|
||||
rule_id=rule.id,
|
||||
user_id=get_current_user().id,
|
||||
)
|
||||
logs = "\n".join(logs)
|
||||
|
||||
response = render(
|
||||
request,
|
||||
"rules/fragments/transaction_rule/dry_run/created.html",
|
||||
{"form": form, "rule": rule, "logs": logs, "results": results},
|
||||
)
|
||||
|
||||
raise Exception("ROLLBACK")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return response
|
||||
|
||||
else:
|
||||
form = DryRunDeletedTransacion()
|
||||
@@ -484,3 +519,66 @@ def dry_run_rule_deleted(request, pk):
|
||||
"rules/fragments/transaction_rule/dry_run/deleted.html",
|
||||
{"form": form, "rule": rule, "logs": logs, "results": results},
|
||||
)
|
||||
|
||||
|
||||
@only_htmx
|
||||
@login_required
|
||||
@disabled_on_demo
|
||||
@require_http_methods(["GET", "POST"])
|
||||
def dry_run_rule_updated(request, pk):
|
||||
rule = get_object_or_404(TransactionRule, id=pk)
|
||||
logs = None
|
||||
results = None
|
||||
|
||||
if request.method == "POST":
|
||||
form = DryRunUpdatedTransactionForm(request.POST)
|
||||
if form.is_valid():
|
||||
base_transaction = Transaction.objects.get(
|
||||
id=request.POST.get("transaction")
|
||||
)
|
||||
old_data = deepcopy(base_transaction)
|
||||
try:
|
||||
with transaction.atomic():
|
||||
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":
|
||||
base_transaction.tags.set(value)
|
||||
elif field_name == "entities":
|
||||
base_transaction.entities.set(value)
|
||||
else:
|
||||
setattr(base_transaction, field_name, value)
|
||||
|
||||
base_transaction.save()
|
||||
|
||||
logs, results = check_for_transaction_rules(
|
||||
instance_id=base_transaction.id,
|
||||
signal="transaction_updated",
|
||||
dry_run=True,
|
||||
rule_id=rule.id,
|
||||
user_id=get_current_user().id,
|
||||
old_data=old_data,
|
||||
)
|
||||
logs = "\n".join(logs) if logs else ""
|
||||
|
||||
response = render(
|
||||
request,
|
||||
"rules/fragments/transaction_rule/dry_run/created.html",
|
||||
{"form": form, "rule": rule, "logs": logs, "results": results},
|
||||
)
|
||||
|
||||
# This will rollback the transaction
|
||||
raise Exception("ROLLBACK")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return response
|
||||
else:
|
||||
form = DryRunUpdatedTransactionForm(initial={"is_paid": None, "type": None})
|
||||
|
||||
return render(
|
||||
request,
|
||||
"rules/fragments/transaction_rule/dry_run/updated.html",
|
||||
{"form": form, "rule": rule, "logs": logs, "results": results},
|
||||
)
|
||||
|
||||
@@ -389,35 +389,115 @@ class QuickTransactionForm(forms.ModelForm):
|
||||
)
|
||||
|
||||
|
||||
class BulkEditTransactionForm(TransactionForm):
|
||||
is_paid = forms.NullBooleanField(required=False)
|
||||
class BulkEditTransactionForm(forms.Form):
|
||||
type = forms.ChoiceField(
|
||||
choices=(Transaction.Type.choices),
|
||||
required=False,
|
||||
label=_("Type"),
|
||||
)
|
||||
is_paid = forms.NullBooleanField(
|
||||
required=False,
|
||||
label=_("Paid"),
|
||||
)
|
||||
account = DynamicModelChoiceField(
|
||||
model=Account,
|
||||
required=False,
|
||||
label=_("Account"),
|
||||
queryset=Account.objects.filter(is_archived=False),
|
||||
widget=TomSelect(clear_button=False, group_by="group"),
|
||||
)
|
||||
date = forms.DateField(
|
||||
label=_("Date"),
|
||||
required=False,
|
||||
widget=AirDatePickerInput(clear_button=False),
|
||||
)
|
||||
reference_date = forms.DateField(
|
||||
widget=AirMonthYearPickerInput(),
|
||||
label=_("Reference Date"),
|
||||
required=False,
|
||||
)
|
||||
amount = forms.DecimalField(
|
||||
max_digits=42,
|
||||
decimal_places=30,
|
||||
required=False,
|
||||
label=_("Amount"),
|
||||
widget=ArbitraryDecimalDisplayNumberInput(),
|
||||
)
|
||||
description = forms.CharField(
|
||||
max_length=500, required=False, label=_("Description")
|
||||
)
|
||||
notes = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.Textarea(attrs={"rows": 3}),
|
||||
label=_("Notes"),
|
||||
)
|
||||
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"),
|
||||
queryset=TransactionEntity.objects.all(),
|
||||
)
|
||||
|
||||
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.fields["account"].queryset = Account.objects.filter(
|
||||
is_archived=False,
|
||||
)
|
||||
|
||||
self.helper.layout.insert(
|
||||
0,
|
||||
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/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(
|
||||
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",
|
||||
FormActions(
|
||||
NoClassSubmit(
|
||||
"submit", _("Update"), css_class="btn btn-outline-primary w-100"
|
||||
@@ -425,6 +505,9 @@ class BulkEditTransactionForm(TransactionForm):
|
||||
),
|
||||
)
|
||||
|
||||
self.fields["amount"].widget = ArbitraryDecimalDisplayNumberInput()
|
||||
self.fields["date"].widget = AirDatePickerInput(clear_button=False)
|
||||
|
||||
|
||||
class TransferForm(forms.Form):
|
||||
from_account = forms.ModelChoiceField(
|
||||
|
||||
@@ -462,6 +462,48 @@ class Transaction(OwnedObject):
|
||||
description = self.description or _("No description")
|
||||
return f"[{frmt_date}][{type_display}][{account}] {description} • {category} • {tags} • {amount}"
|
||||
|
||||
def deepcopy(self, memo=None):
|
||||
"""
|
||||
Creates a deep copy of the transaction instance.
|
||||
|
||||
This method returns a new, unsaved Transaction instance with the same
|
||||
values as the original, including its many-to-many relationships.
|
||||
The primary key and any other unique fields are reset to avoid
|
||||
database integrity errors upon saving.
|
||||
"""
|
||||
if memo is None:
|
||||
memo = {}
|
||||
|
||||
# Create a new instance of the class
|
||||
new_obj = self.__class__()
|
||||
memo[id(self)] = new_obj
|
||||
|
||||
# Copy all concrete fields from the original to the new object
|
||||
for field in self._meta.concrete_fields:
|
||||
# Skip the primary key to allow the database to generate a new one
|
||||
if field.primary_key:
|
||||
continue
|
||||
|
||||
# Reset any unique fields to None to avoid constraint violations
|
||||
if field.unique and field.name == "internal_id":
|
||||
setattr(new_obj, field.name, None)
|
||||
continue
|
||||
|
||||
# Copy the value of the field
|
||||
setattr(new_obj, field.name, getattr(self, field.name))
|
||||
|
||||
# Save the new object to the database to get a primary key
|
||||
new_obj.save()
|
||||
|
||||
# Copy the many-to-many relationships
|
||||
for field in self._meta.many_to_many:
|
||||
source_manager = getattr(self, field.name)
|
||||
destination_manager = getattr(new_obj, field.name)
|
||||
# Set the M2M relationships for the new object
|
||||
destination_manager.set(source_manager.all())
|
||||
|
||||
return new_obj
|
||||
|
||||
|
||||
class InstallmentPlan(models.Model):
|
||||
class Recurrence(models.TextChoices):
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
<div class="card tw:max-h-full tw:overflow-auto tw:overflow-x-auto">
|
||||
<div class="card-body">
|
||||
<pre>
|
||||
{{ logs|linebreaks }}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,16 @@
|
||||
{% extends 'extends/offcanvas.html' %}
|
||||
{% load i18n %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block title %}{% translate 'Edit transaction rule' %}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<form hx-post="{% url 'transaction_rule_dry_run_updated' pk=rule.id %}" hx-target="#generic-offcanvas"
|
||||
hx-indicator="#dry-run-updated-result, closest form" class="show-loading" novalidate>
|
||||
{% crispy form %}
|
||||
</form>
|
||||
<hr>
|
||||
<div id="dry-run-updated-result" class="show-loading">
|
||||
{% include 'rules/fragments/transaction_rule/dry_run/visual.html' with logs=logs results=results %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -53,8 +53,7 @@
|
||||
<span class="badge text-bg-secondary">{{ result.new_value }}</span>
|
||||
</div>
|
||||
<c-transaction.item :transaction="result.transaction" :dummy="True"
|
||||
:disable-selection="True" :overriden_tags="result.tags"
|
||||
:overriden_entities="result.entities"></c-transaction.item>
|
||||
:disable-selection="True"></c-transaction.item>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -73,8 +72,7 @@
|
||||
{% endif %}
|
||||
<div class="text-center h3 my-2"><i class="fa-solid fa-arrow-down"></i></div>
|
||||
<c-transaction.item :transaction="result.end_transaction" :dummy="True"
|
||||
:disable-selection="True" :overriden_tags="result.tags"
|
||||
:overriden_entities="result.entities"></c-transaction.item>
|
||||
:disable-selection="True"></c-transaction.item>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
@@ -124,17 +124,17 @@
|
||||
{% if transaction_rule.on_create %}
|
||||
<li><a class="dropdown-item" role="link" href="#"
|
||||
hx-get="{% url 'transaction_rule_dry_run_created' pk=transaction_rule.id %}"
|
||||
hx-target="#generic-offcanvas">{% trans 'On creation' %}</a></li>
|
||||
hx-target="#generic-offcanvas">{% trans 'Create' %}</a></li>
|
||||
{% endif %}
|
||||
{% if transaction_rule.on_update %}
|
||||
<li><a class="dropdown-item" role="link" href="#"
|
||||
hx-get="{% url 'transaction_rule_dry_run_deleted' pk=transaction_rule.id %}"
|
||||
hx-target="#generic-offcanvas">{% trans 'On update' %}</a></li>
|
||||
hx-get="{% url 'transaction_rule_dry_run_updated' pk=transaction_rule.id %}"
|
||||
hx-target="#generic-offcanvas">{% trans 'Update' %}</a></li>
|
||||
{% endif %}
|
||||
{% if transaction_rule.on_delete %}
|
||||
<li><a class="dropdown-item" role="link" href="#"
|
||||
hx-get="{% url 'transaction_rule_dry_run_deleted' pk=transaction_rule.id %}"
|
||||
hx-target="#generic-offcanvas">{% trans 'On delete' %}</a></li>
|
||||
hx-target="#generic-offcanvas">{% trans 'Delete' %}</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user