mirror of
https://github.com/eitchtee/WYGIWYH.git
synced 2026-04-26 10:38:36 +02:00
changes
This commit is contained in:
@@ -1,16 +1,18 @@
|
|||||||
from crispy_bootstrap5.bootstrap5 import Switch, BS5Accordion
|
from crispy_bootstrap5.bootstrap5 import Switch, BS5Accordion
|
||||||
from crispy_forms.bootstrap import FormActions, AccordionGroup
|
from crispy_forms.bootstrap import FormActions, AccordionGroup
|
||||||
from crispy_forms.helper import FormHelper
|
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 import forms
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.utils.translation import gettext_lazy as _
|
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.crispy.submit import NoClassSubmit
|
||||||
from apps.common.widgets.tom_select import TomSelect, TransactionSelect
|
from apps.common.widgets.tom_select import TomSelect, TransactionSelect
|
||||||
from apps.rules.models import TransactionRule, UpdateOrCreateTransactionRuleAction
|
from apps.rules.models import TransactionRule, UpdateOrCreateTransactionRuleAction
|
||||||
from apps.rules.models import TransactionRuleAction
|
from apps.rules.models import TransactionRuleAction
|
||||||
from apps.common.fields.forms.dynamic_select import DynamicModelChoiceField
|
from apps.common.fields.forms.dynamic_select import DynamicModelChoiceField
|
||||||
|
from apps.transactions.forms import BulkEditTransactionForm
|
||||||
from apps.transactions.models import Transaction
|
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):
|
class DryRunDeletedTransacion(forms.Form):
|
||||||
transaction = DynamicModelChoiceField(
|
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 decimal
|
||||||
import logging
|
import logging
|
||||||
import traceback
|
import traceback
|
||||||
from copy import deepcopy
|
|
||||||
from datetime import datetime, date
|
from datetime import datetime, date
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
@@ -53,7 +52,7 @@ class DryRunResults:
|
|||||||
):
|
):
|
||||||
result = {
|
result = {
|
||||||
"type": "edit_transaction",
|
"type": "edit_transaction",
|
||||||
"transaction": deepcopy(instance),
|
"transaction": instance.deepcopy(),
|
||||||
"action": action,
|
"action": action,
|
||||||
"old_value": old_value,
|
"old_value": old_value,
|
||||||
"new_value": new_value,
|
"new_value": new_value,
|
||||||
@@ -248,7 +247,7 @@ def check_for_transaction_rules(
|
|||||||
if searched_transactions.exists():
|
if searched_transactions.exists():
|
||||||
transaction = searched_transactions.first()
|
transaction = searched_transactions.first()
|
||||||
existing = True
|
existing = True
|
||||||
starting_instance = deepcopy(transaction)
|
starting_instance = transaction.deepcopy()
|
||||||
_log("Found at least one matching transaction, using latest:")
|
_log("Found at least one matching transaction, using latest:")
|
||||||
_log("{}".format(pformat(model_to_dict(transaction))))
|
_log("{}".format(pformat(model_to_dict(transaction))))
|
||||||
else:
|
else:
|
||||||
@@ -322,82 +321,62 @@ def check_for_transaction_rules(
|
|||||||
else:
|
else:
|
||||||
transaction.category = TransactionCategory.objects.get(name=value)
|
transaction.category = TransactionCategory.objects.get(name=value)
|
||||||
|
|
||||||
if dry_run:
|
if not transaction.id:
|
||||||
if not transaction.id:
|
_log("Transaction will be created as:")
|
||||||
_log("Transaction would be created as:")
|
|
||||||
else:
|
|
||||||
_log("Trasanction would be updated as:")
|
|
||||||
|
|
||||||
_log(
|
|
||||||
"{}".format(
|
|
||||||
pformat(model_to_dict(transaction, exclude=["tags", "entities"])),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
if not transaction.id:
|
_log("Trasanction will be updated as:")
|
||||||
_log("Transaction will be created as:")
|
|
||||||
else:
|
|
||||||
_log("Trasanction will be updated as:")
|
|
||||||
|
|
||||||
_log(
|
_log(
|
||||||
"{}".format(
|
"{}".format(
|
||||||
pformat(model_to_dict(transaction, exclude=["tags", "entities"])),
|
pformat(model_to_dict(transaction, exclude=["tags", "entities"])),
|
||||||
)
|
|
||||||
)
|
)
|
||||||
transaction.save()
|
)
|
||||||
|
transaction.save()
|
||||||
|
|
||||||
# Handle M2M fields after save
|
# Handle M2M fields after save
|
||||||
tags = []
|
tags = []
|
||||||
if processed_action.set_tags:
|
if processed_action.set_tags:
|
||||||
tags = simple.eval(processed_action.set_tags)
|
tags = simple.eval(processed_action.set_tags)
|
||||||
if dry_run:
|
_log(f" And tags will be set as: {tags}")
|
||||||
_log(f" And tags would be set as: {tags}")
|
transaction.tags.clear()
|
||||||
else:
|
if isinstance(tags, (list, tuple)):
|
||||||
_log(f" And tags will be set as: {tags}")
|
for tag in tags:
|
||||||
transaction.tags.clear()
|
if isinstance(tag, int):
|
||||||
if isinstance(tags, (list, tuple)):
|
transaction.tags.add(TransactionTag.objects.get(id=tag))
|
||||||
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))
|
|
||||||
else:
|
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 = []
|
entities = []
|
||||||
if processed_action.set_entities:
|
if processed_action.set_entities:
|
||||||
entities = simple.eval(processed_action.set_entities)
|
entities = simple.eval(processed_action.set_entities)
|
||||||
if dry_run:
|
_log(f" And entities will be set as: {entities}")
|
||||||
_log(f" And entities would be set as: {entities}")
|
transaction.entities.clear()
|
||||||
else:
|
if isinstance(entities, (list, tuple)):
|
||||||
_log(f" And entities will be set as: {entities}")
|
for entity in entities:
|
||||||
transaction.entities.clear()
|
if isinstance(entity, int):
|
||||||
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):
|
|
||||||
transaction.entities.add(
|
transaction.entities.add(
|
||||||
TransactionEntity.objects.get(id=entities)
|
TransactionEntity.objects.get(id=entity)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
transaction.entities.add(
|
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(
|
dry_run_results.update_or_create_transaction(
|
||||||
start_instance=starting_instance,
|
start_instance=starting_instance,
|
||||||
end_instance=deepcopy(transaction),
|
end_instance=transaction.deepcopy(),
|
||||||
updated=existing,
|
updated=existing,
|
||||||
action=processed_action,
|
action=processed_action,
|
||||||
query=search_query,
|
query=search_query,
|
||||||
@@ -438,19 +417,19 @@ def check_for_transaction_rules(
|
|||||||
transaction.category = category
|
transaction.category = category
|
||||||
|
|
||||||
elif field == TransactionRuleAction.Field.tags:
|
elif field == TransactionRuleAction.Field.tags:
|
||||||
if not dry_run:
|
transaction.tags.clear()
|
||||||
transaction.tags.clear()
|
|
||||||
if isinstance(new_value, list):
|
if isinstance(new_value, list):
|
||||||
for tag_value in new_value:
|
for tag_value in new_value:
|
||||||
if isinstance(tag_value, int):
|
if isinstance(tag_value, int):
|
||||||
tag = TransactionTag.objects.get(id=tag_value)
|
tag = TransactionTag.objects.get(id=tag_value)
|
||||||
if not dry_run:
|
|
||||||
transaction.tags.add(tag)
|
transaction.tags.add(tag)
|
||||||
tags.append(tag)
|
tags.append(tag)
|
||||||
elif isinstance(tag_value, str):
|
elif isinstance(tag_value, str):
|
||||||
tag = TransactionTag.objects.get(name=tag_value)
|
tag = TransactionTag.objects.get(name=tag_value)
|
||||||
if not dry_run:
|
|
||||||
transaction.tags.add(tag)
|
transaction.tags.add(tag)
|
||||||
tags.append(tag)
|
tags.append(tag)
|
||||||
|
|
||||||
elif isinstance(new_value, (int, str)):
|
elif isinstance(new_value, (int, str)):
|
||||||
@@ -459,24 +438,22 @@ def check_for_transaction_rules(
|
|||||||
else:
|
else:
|
||||||
tag = TransactionTag.objects.get(name=new_value)
|
tag = TransactionTag.objects.get(name=new_value)
|
||||||
|
|
||||||
if not dry_run:
|
transaction.tags.add(tag)
|
||||||
transaction.tags.add(tag)
|
|
||||||
tags.append(tag)
|
tags.append(tag)
|
||||||
|
|
||||||
elif field == TransactionRuleAction.Field.entities:
|
elif field == TransactionRuleAction.Field.entities:
|
||||||
if not dry_run:
|
transaction.entities.clear()
|
||||||
transaction.entities.clear()
|
|
||||||
if isinstance(new_value, list):
|
if isinstance(new_value, list):
|
||||||
for entity_value in new_value:
|
for entity_value in new_value:
|
||||||
if isinstance(entity_value, int):
|
if isinstance(entity_value, int):
|
||||||
entity = TransactionEntity.objects.get(id=entity_value)
|
entity = TransactionEntity.objects.get(id=entity_value)
|
||||||
if not dry_run:
|
|
||||||
transaction.entities.add(entity)
|
transaction.entities.add(entity)
|
||||||
entities.append(entity)
|
entities.append(entity)
|
||||||
elif isinstance(entity_value, str):
|
elif isinstance(entity_value, str):
|
||||||
entity = TransactionEntity.objects.get(name=entity_value)
|
entity = TransactionEntity.objects.get(name=entity_value)
|
||||||
if not dry_run:
|
|
||||||
transaction.entities.add(entity)
|
transaction.entities.add(entity)
|
||||||
entities.append(entity)
|
entities.append(entity)
|
||||||
|
|
||||||
elif isinstance(new_value, (int, str)):
|
elif isinstance(new_value, (int, str)):
|
||||||
@@ -484,8 +461,8 @@ def check_for_transaction_rules(
|
|||||||
entity = TransactionEntity.objects.get(id=new_value)
|
entity = TransactionEntity.objects.get(id=new_value)
|
||||||
else:
|
else:
|
||||||
entity = TransactionEntity.objects.get(name=new_value)
|
entity = TransactionEntity.objects.get(name=new_value)
|
||||||
if not dry_run:
|
|
||||||
transaction.entities.add(entity)
|
transaction.entities.add(entity)
|
||||||
entities.append(entity)
|
entities.append(entity)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@@ -496,7 +473,7 @@ def check_for_transaction_rules(
|
|||||||
)
|
)
|
||||||
|
|
||||||
dry_run_results.edit_transaction(
|
dry_run_results.edit_transaction(
|
||||||
instance=deepcopy(transaction),
|
instance=transaction.deepcopy(),
|
||||||
action=processed_action,
|
action=processed_action,
|
||||||
old_value=original_value,
|
old_value=original_value,
|
||||||
new_value=new_value,
|
new_value=new_value,
|
||||||
@@ -530,7 +507,7 @@ def check_for_transaction_rules(
|
|||||||
# Regular transaction processing for creates and updates
|
# Regular transaction processing for creates and updates
|
||||||
instance = Transaction.objects.get(id=instance_id)
|
instance = Transaction.objects.get(id=instance_id)
|
||||||
|
|
||||||
dry_run_results.triggering_transaction(deepcopy(instance))
|
dry_run_results.triggering_transaction(instance.deepcopy())
|
||||||
|
|
||||||
functions = {
|
functions = {
|
||||||
"relativedelta": relativedelta,
|
"relativedelta": relativedelta,
|
||||||
@@ -667,7 +644,7 @@ def check_for_transaction_rules(
|
|||||||
level="error",
|
level="error",
|
||||||
)
|
)
|
||||||
# Save at the end
|
# Save at the end
|
||||||
if not dry_run and signal != "transaction_deleted":
|
if signal != "transaction_deleted":
|
||||||
instance.save()
|
instance.save()
|
||||||
else:
|
else:
|
||||||
_log(
|
_log(
|
||||||
@@ -702,7 +679,7 @@ def check_for_transaction_rules(
|
|||||||
if rule.sequenced:
|
if rule.sequenced:
|
||||||
# Update names for next actions
|
# Update names for next actions
|
||||||
simple.names.update(_get_names(instance))
|
simple.names.update(_get_names(instance))
|
||||||
if not dry_run and signal != "transaction_deleted":
|
if signal != "transaction_deleted":
|
||||||
instance.save()
|
instance.save()
|
||||||
|
|
||||||
for action in update_or_create_actions:
|
for action in update_or_create_actions:
|
||||||
@@ -741,7 +718,6 @@ def check_for_transaction_rules(
|
|||||||
|
|
||||||
delete_current_user()
|
delete_current_user()
|
||||||
|
|
||||||
if dry_run:
|
return logs, dry_run_results.results
|
||||||
return logs, dry_run_results.results
|
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -52,6 +52,11 @@ urlpatterns = [
|
|||||||
views.dry_run_rule_deleted,
|
views.dry_run_rule_deleted,
|
||||||
name="transaction_rule_dry_run_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(
|
path(
|
||||||
"rules/transaction/<int:pk>/share/",
|
"rules/transaction/<int:pk>/share/",
|
||||||
views.transaction_rule_share,
|
views.transaction_rule_share,
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
from itertools import chain
|
from itertools import chain
|
||||||
|
|
||||||
|
from copy import deepcopy
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.db import transaction
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import render, get_object_or_404, redirect
|
from django.shortcuts import render, get_object_or_404, redirect
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
@@ -14,6 +17,7 @@ from apps.rules.forms import (
|
|||||||
UpdateOrCreateTransactionRuleActionForm,
|
UpdateOrCreateTransactionRuleActionForm,
|
||||||
DryRunCreatedTransacion,
|
DryRunCreatedTransacion,
|
||||||
DryRunDeletedTransacion,
|
DryRunDeletedTransacion,
|
||||||
|
DryRunUpdatedTransactionForm,
|
||||||
)
|
)
|
||||||
from apps.rules.models import (
|
from apps.rules.models import (
|
||||||
TransactionRule,
|
TransactionRule,
|
||||||
@@ -25,6 +29,9 @@ from apps.common.forms import SharedObjectForm
|
|||||||
from apps.common.decorators.demo import disabled_on_demo
|
from apps.common.decorators.demo import disabled_on_demo
|
||||||
from apps.rules.tasks import check_for_transaction_rules
|
from apps.rules.tasks import check_for_transaction_rules
|
||||||
from apps.common.middleware.thread_local import get_current_user
|
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
|
@login_required
|
||||||
@@ -436,14 +443,28 @@ def dry_run_rule_created(request, pk):
|
|||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
form = DryRunCreatedTransacion(request.POST)
|
form = DryRunCreatedTransacion(request.POST)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
logs, results = check_for_transaction_rules(
|
try:
|
||||||
instance_id=form.cleaned_data["transaction"].id,
|
with transaction.atomic():
|
||||||
signal="transaction_created",
|
logs, results = check_for_transaction_rules(
|
||||||
dry_run=True,
|
instance_id=form.cleaned_data["transaction"].id,
|
||||||
rule_id=rule.id,
|
signal="transaction_created",
|
||||||
user_id=get_current_user().id,
|
dry_run=True,
|
||||||
)
|
rule_id=rule.id,
|
||||||
logs = "\n".join(logs)
|
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:
|
else:
|
||||||
form = DryRunCreatedTransacion()
|
form = DryRunCreatedTransacion()
|
||||||
@@ -467,14 +488,28 @@ def dry_run_rule_deleted(request, pk):
|
|||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
form = DryRunDeletedTransacion(request.POST)
|
form = DryRunDeletedTransacion(request.POST)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
logs, results = check_for_transaction_rules(
|
try:
|
||||||
instance_id=form.cleaned_data["transaction"].id,
|
with transaction.atomic():
|
||||||
signal="transaction_deleted",
|
logs, results = check_for_transaction_rules(
|
||||||
dry_run=True,
|
instance_id=form.cleaned_data["transaction"].id,
|
||||||
rule_id=rule.id,
|
signal="transaction_deleted",
|
||||||
user_id=get_current_user().id,
|
dry_run=True,
|
||||||
)
|
rule_id=rule.id,
|
||||||
logs = "\n".join(logs)
|
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:
|
else:
|
||||||
form = DryRunDeletedTransacion()
|
form = DryRunDeletedTransacion()
|
||||||
@@ -484,3 +519,66 @@ def dry_run_rule_deleted(request, pk):
|
|||||||
"rules/fragments/transaction_rule/dry_run/deleted.html",
|
"rules/fragments/transaction_rule/dry_run/deleted.html",
|
||||||
{"form": form, "rule": rule, "logs": logs, "results": results},
|
{"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):
|
class BulkEditTransactionForm(forms.Form):
|
||||||
is_paid = forms.NullBooleanField(required=False)
|
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):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*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
|
self.fields["account"].queryset = Account.objects.filter(
|
||||||
del self.helper.layout[0:2] # Remove type, is_paid field
|
is_archived=False,
|
||||||
|
)
|
||||||
|
|
||||||
self.helper.layout.insert(
|
self.fields["category"].queryset = TransactionCategory.objects.filter(
|
||||||
0,
|
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(
|
Field(
|
||||||
"type",
|
"type",
|
||||||
template="transactions/widgets/unselectable_income_expense_toggle_buttons.html",
|
template="transactions/widgets/unselectable_income_expense_toggle_buttons.html",
|
||||||
),
|
),
|
||||||
)
|
|
||||||
|
|
||||||
self.helper.layout.insert(
|
|
||||||
1,
|
|
||||||
Field(
|
Field(
|
||||||
"is_paid",
|
"is_paid",
|
||||||
template="transactions/widgets/unselectable_paid_toggle_button.html",
|
template="transactions/widgets/unselectable_paid_toggle_button.html",
|
||||||
),
|
),
|
||||||
)
|
Row(
|
||||||
|
Column("account", css_class="form-group col-md-6 mb-0"),
|
||||||
self.helper.layout.append(
|
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(
|
FormActions(
|
||||||
NoClassSubmit(
|
NoClassSubmit(
|
||||||
"submit", _("Update"), css_class="btn btn-outline-primary w-100"
|
"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):
|
class TransferForm(forms.Form):
|
||||||
from_account = forms.ModelChoiceField(
|
from_account = forms.ModelChoiceField(
|
||||||
|
|||||||
@@ -462,6 +462,48 @@ class Transaction(OwnedObject):
|
|||||||
description = self.description or _("No description")
|
description = self.description or _("No description")
|
||||||
return f"[{frmt_date}][{type_display}][{account}] {description} • {category} • {tags} • {amount}"
|
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 InstallmentPlan(models.Model):
|
||||||
class Recurrence(models.TextChoices):
|
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>
|
<span class="badge text-bg-secondary">{{ result.new_value }}</span>
|
||||||
</div>
|
</div>
|
||||||
<c-transaction.item :transaction="result.transaction" :dummy="True"
|
<c-transaction.item :transaction="result.transaction" :dummy="True"
|
||||||
:disable-selection="True" :overriden_tags="result.tags"
|
:disable-selection="True"></c-transaction.item>
|
||||||
:overriden_entities="result.entities"></c-transaction.item>
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
@@ -73,8 +72,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="text-center h3 my-2"><i class="fa-solid fa-arrow-down"></i></div>
|
<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"
|
<c-transaction.item :transaction="result.end_transaction" :dummy="True"
|
||||||
:disable-selection="True" :overriden_tags="result.tags"
|
:disable-selection="True"></c-transaction.item>
|
||||||
:overriden_entities="result.entities"></c-transaction.item>
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|||||||
@@ -124,17 +124,17 @@
|
|||||||
{% if transaction_rule.on_create %}
|
{% if transaction_rule.on_create %}
|
||||||
<li><a class="dropdown-item" role="link" href="#"
|
<li><a class="dropdown-item" role="link" href="#"
|
||||||
hx-get="{% url 'transaction_rule_dry_run_created' pk=transaction_rule.id %}"
|
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 %}
|
{% endif %}
|
||||||
{% if transaction_rule.on_update %}
|
{% if transaction_rule.on_update %}
|
||||||
<li><a class="dropdown-item" role="link" href="#"
|
<li><a class="dropdown-item" role="link" href="#"
|
||||||
hx-get="{% url 'transaction_rule_dry_run_deleted' pk=transaction_rule.id %}"
|
hx-get="{% url 'transaction_rule_dry_run_updated' pk=transaction_rule.id %}"
|
||||||
hx-target="#generic-offcanvas">{% trans 'On update' %}</a></li>
|
hx-target="#generic-offcanvas">{% trans 'Update' %}</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if transaction_rule.on_delete %}
|
{% if transaction_rule.on_delete %}
|
||||||
<li><a class="dropdown-item" role="link" href="#"
|
<li><a class="dropdown-item" role="link" href="#"
|
||||||
hx-get="{% url 'transaction_rule_dry_run_deleted' pk=transaction_rule.id %}"
|
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 %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user