feat: first batch of work

This commit is contained in:
Herculino Trotta
2025-11-01 03:15:44 -03:00
parent e600d87968
commit a63367a772
175 changed files with 3433 additions and 2245 deletions
+11 -12
View File
@@ -1,11 +1,4 @@
import django_filters
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Field, Row, Column
from django import forms
from django.db.models import Q
from django.utils.translation import gettext_lazy as _
from django_filters import Filter
from apps.accounts.models import Account
from apps.common.fields.month_year import MonthYearFormField
from apps.common.widgets.datepicker import AirDatePickerInput
@@ -15,9 +8,15 @@ from apps.currencies.models import Currency
from apps.transactions.models import (
Transaction,
TransactionCategory,
TransactionTag,
TransactionEntity,
TransactionTag,
)
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Column, Field, Layout, Row
from django import forms
from django.db.models import Q
from django.utils.translation import gettext_lazy as _
from django_filters import Filter
SITUACAO_CHOICES = (
("1", _("Paid")),
@@ -159,13 +158,13 @@ class TransactionsFilter(django_filters.FilterSet):
Field("description"),
Row(Column("date_start"), Column("date_end")),
Row(
Column("reference_date_start", css_class="form-group col-md-6 mb-0"),
Column("reference_date_end", css_class="form-group col-md-6 mb-0"),
Column("reference_date_start", css_class="md:col-span-6 mb-0"),
Column("reference_date_end", css_class="md:col-span-6 mb-0"),
css_class="form-row",
),
Row(
Column("from_amount", css_class="form-group col-md-6 mb-0"),
Column("to_amount", css_class="form-group col-md-6 mb-0"),
Column("from_amount", css_class="md:col-span-6 mb-0"),
Column("to_amount", css_class="md:col-span-6 mb-0"),
css_class="form-row",
),
Field("account", size=1),
+76 -93
View File
@@ -1,20 +1,5 @@
from copy import deepcopy
from crispy_bootstrap5.bootstrap5 import Switch, BS5Accordion
from crispy_forms.bootstrap import FormActions, AccordionGroup, AppendedText
from crispy_forms.helper import FormHelper
from crispy_forms.layout import (
Layout,
Row,
Column,
Field,
Div,
HTML,
)
from django import forms
from django.db.models import Q
from django.utils.translation import gettext_lazy as _
from apps.accounts.models import Account
from apps.common.fields.forms.dynamic_select import (
DynamicModelChoiceField,
@@ -26,14 +11,28 @@ from apps.common.widgets.decimal import ArbitraryDecimalDisplayNumberInput
from apps.common.widgets.tom_select import TomSelect
from apps.rules.signals import transaction_created, transaction_updated
from apps.transactions.models import (
InstallmentPlan,
QuickTransaction,
RecurringTransaction,
Transaction,
TransactionCategory,
TransactionTag,
InstallmentPlan,
RecurringTransaction,
TransactionEntity,
QuickTransaction,
TransactionTag,
)
from crispy_bootstrap5.bootstrap5 import BS5Accordion, Switch
from crispy_forms.bootstrap import AccordionGroup, AppendedText, FormActions
from crispy_forms.helper import FormHelper
from crispy_forms.layout import (
HTML,
Column,
Div,
Field,
Layout,
Row,
)
from django import forms
from django.db.models import Q
from django.utils.translation import gettext_lazy as _
class TransactionForm(forms.ModelForm):
@@ -134,20 +133,20 @@ class TransactionForm(forms.ModelForm):
),
Field("is_paid", template="transactions/widgets/paid_toggle_button.html"),
Row(
Column("account", css_class="form-group col-md-6 mb-0"),
Column("entities", css_class="form-group col-md-6 mb-0"),
Column("account", css_class="md:col-span-6 mb-0"),
Column("entities", css_class="md:col-span-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"),
Column(Field("date"), css_class="md:col-span-6 mb-0"),
Column(Field("reference_date"), css_class="md:col-span-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"),
Column("category", css_class="md:col-span-6 mb-0"),
Column("tags", css_class="md:col-span-6 mb-0"),
css_class="form-row",
),
"notes",
@@ -164,8 +163,8 @@ class TransactionForm(forms.ModelForm):
Field("is_paid", template="transactions/widgets/paid_toggle_button.html"),
"account",
Row(
Column(Field("date"), css_class="form-group col-md-6 mb-0"),
Column(Field("reference_date"), css_class="form-group col-md-6 mb-0"),
Column(Field("date"), css_class="md:col-span-6 mb-0"),
Column(Field("reference_date"), css_class="md:col-span-6 mb-0"),
css_class="form-row",
),
"description",
@@ -175,8 +174,8 @@ class TransactionForm(forms.ModelForm):
_("More"),
"entities",
Row(
Column("category", css_class="form-group col-md-6 mb-0"),
Column("tags", css_class="form-group col-md-6 mb-0"),
Column("category", css_class="md:col-span-6 mb-0"),
Column("tags", css_class="md:col-span-6 mb-0"),
css_class="form-row",
),
"notes",
@@ -350,15 +349,15 @@ class QuickTransactionForm(forms.ModelForm):
"name",
HTML("<hr />"),
Row(
Column("account", css_class="form-group col-md-6 mb-0"),
Column("entities", css_class="form-group col-md-6 mb-0"),
Column("account", css_class="md:col-span-6 mb-0"),
Column("entities", css_class="md:col-span-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"),
Column("category", css_class="md:col-span-6 mb-0"),
Column("tags", css_class="md:col-span-6 mb-0"),
css_class="form-row",
),
"notes",
@@ -481,20 +480,20 @@ class BulkEditTransactionForm(forms.Form):
template="transactions/widgets/unselectable_paid_toggle_button.html",
),
Row(
Column("account", css_class="form-group col-md-6 mb-0"),
Column("entities", css_class="form-group col-md-6 mb-0"),
Column("account", css_class="md:col-span-6 mb-0"),
Column("entities", css_class="md:col-span-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"),
Column(Field("date"), css_class="md:col-span-6 mb-0"),
Column(Field("reference_date"), css_class="md:col-span-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"),
Column("category", css_class="md:col-span-6 mb-0"),
Column("tags", css_class="md:col-span-6 mb-0"),
css_class="form-row",
),
"notes",
@@ -600,10 +599,10 @@ class TransferForm(forms.Form):
self.helper.layout = Layout(
Row(
Column(Field("date"), css_class="form-group col-md-6 mb-0"),
Column(Field("date"), css_class="md:col-span-6 mb-0"),
Column(
Field("reference_date"),
css_class="form-group col-md-6 mb-0",
css_class="md:col-span-6 mb-0",
),
css_class="form-row",
),
@@ -612,45 +611,29 @@ class TransferForm(forms.Form):
Switch("mute"),
Row(
Column(
Row(
Column(
"from_account",
css_class="form-group col-md-6 mb-0",
),
Column(
Field("from_amount"),
css_class="form-group col-md-6 mb-0",
),
css_class="form-row",
),
Row(
Column("from_category", css_class="form-group col-md-6 mb-0"),
Column("from_tags", css_class="form-group col-md-6 mb-0"),
css_class="form-row",
),
"from_account",
css_class="md:col-span-6",
),
css_class="p-1 mx-1 my-3 border rounded-3",
Column(
Field("from_amount"),
css_class="md:col-span-6",
),
Column("from_category", css_class="md:col-span-6"),
Column("from_tags", css_class="md:col-span-6"),
css_class="bg-base-200 rounded-box p-4 border-base-content/60 border mb-3",
),
Row(
Column(
Row(
Column(
"to_account",
css_class="form-group col-md-6 mb-0",
),
Column(
Field("to_amount"),
css_class="form-group col-md-6 mb-0",
),
css_class="form-row",
),
Row(
Column("to_category", css_class="form-group col-md-6 mb-0"),
Column("to_tags", css_class="form-group col-md-6 mb-0"),
css_class="form-row",
),
"to_account",
css_class="md:col-span-6 mb-0",
),
css_class="p-1 mx-1 my-3 border rounded-3",
Column(
Field("to_amount"),
css_class="md:col-span-6 mb-0",
),
Column("to_category", css_class="md:col-span-6 mb-0"),
Column("to_tags", css_class="md:col-span-6 mb-0"),
css_class="bg-base-200 rounded-box p-4 border-base-content/60 border mb-3",
),
FormActions(
NoClassSubmit(
@@ -841,8 +824,8 @@ class InstallmentPlanForm(forms.ModelForm):
template="transactions/widgets/income_expense_toggle_buttons.html",
),
Row(
Column("account", css_class="form-group col-md-6 mb-0"),
Column("entities", css_class="form-group col-md-6 mb-0"),
Column("account", css_class="md:col-span-6 mb-0"),
Column("entities", css_class="md:col-span-6 mb-0"),
css_class="form-row",
),
"description",
@@ -850,20 +833,20 @@ class InstallmentPlanForm(forms.ModelForm):
"notes",
Switch("add_notes_to_transaction"),
Row(
Column("number_of_installments", css_class="form-group col-md-6 mb-0"),
Column("installment_start", css_class="form-group col-md-6 mb-0"),
Column("number_of_installments", css_class="md:col-span-6 mb-0"),
Column("installment_start", css_class="md:col-span-6 mb-0"),
css_class="form-row",
),
Row(
Column("start_date", css_class="form-group col-md-4 mb-0"),
Column("reference_date", css_class="form-group col-md-4 mb-0"),
Column("recurrence", css_class="form-group col-md-4 mb-0"),
Column("start_date", css_class="md:col-span-4 mb-0"),
Column("reference_date", css_class="md:col-span-4 mb-0"),
Column("recurrence", css_class="md:col-span-4 mb-0"),
css_class="form-row",
),
"installment_amount",
Row(
Column("category", css_class="form-group col-md-6 mb-0"),
Column("tags", css_class="form-group col-md-6 mb-0"),
Column("category", css_class="md:col-span-6 mb-0"),
Column("tags", css_class="md:col-span-6 mb-0"),
css_class="form-row",
),
)
@@ -1103,29 +1086,29 @@ class RecurringTransactionForm(forms.ModelForm):
template="transactions/widgets/income_expense_toggle_buttons.html",
),
Row(
Column("account", css_class="form-group col-md-6 mb-0"),
Column("entities", css_class="form-group col-md-6 mb-0"),
Column("account", css_class="md:col-span-6 mb-0"),
Column("entities", css_class="md:col-span-6 mb-0"),
css_class="form-row",
),
"description",
Switch("add_description_to_transaction"),
"amount",
Row(
Column("category", css_class="form-group col-md-6 mb-0"),
Column("tags", css_class="form-group col-md-6 mb-0"),
Column("category", css_class="md:col-span-6 mb-0"),
Column("tags", css_class="md:col-span-6 mb-0"),
css_class="form-row",
),
"notes",
Switch("add_notes_to_transaction"),
Row(
Column("start_date", css_class="form-group col-md-6 mb-0"),
Column("reference_date", css_class="form-group col-md-6 mb-0"),
Column("start_date", css_class="md:col-span-6 mb-0"),
Column("reference_date", css_class="md:col-span-6 mb-0"),
css_class="form-row",
),
Row(
Column("recurrence_interval", css_class="form-group col-md-4 mb-0"),
Column("recurrence_type", css_class="form-group col-md-4 mb-0"),
Column("end_date", css_class="form-group col-md-4 mb-0"),
Column("recurrence_interval", css_class="md:col-span-4 mb-0"),
Column("recurrence_type", css_class="md:col-span-4 mb-0"),
Column("end_date", css_class="md:col-span-4 mb-0"),
css_class="form-row",
),
AppendedText("keep_at_most", _("future transactions")),
+28 -17
View File
@@ -2,29 +2,29 @@ import decimal
import logging
from copy import deepcopy
from apps.common.fields.month_year import MonthYearModelField
from apps.common.functions.decimals import truncate_decimal
from apps.common.middleware.thread_local import get_current_user
from apps.common.models import (
OwnedObject,
OwnedObjectManager,
SharedObject,
SharedObjectManager,
)
from apps.common.templatetags.decimal import drop_trailing_zeros, localize_number
from apps.currencies.utils.convert import convert
from apps.transactions.validators import validate_decimal_places, validate_non_negative
from dateutil.relativedelta import relativedelta
from django.conf import settings
from django.core.validators import MinValueValidator
from django.db import models, transaction
from django.db.models import Q
from django.dispatch import Signal
from django.forms import ValidationError
from django.template.defaultfilters import date
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from apps.common.fields.month_year import MonthYearModelField
from apps.common.functions.decimals import truncate_decimal
from apps.common.templatetags.decimal import localize_number, drop_trailing_zeros
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,
OwnedObjectManager,
)
logger = logging.getLogger()
@@ -381,21 +381,32 @@ class Transaction(OwnedObject):
db_table = "transactions"
default_manager_name = "objects"
def clean_fields(self, *args, **kwargs):
def clean(self):
super().clean()
# Only process amount and reference_date if account exists
# If account is missing, Django's required field validation will handle it
try:
account = self.account
except Transaction.account.RelatedObjectDoesNotExist:
# Account doesn't exist, skip processing that depends on it
# Django will add the required field error
return
# Validate and normalize amount
if isinstance(self.amount, (str, int, float)):
self.amount = decimal.Decimal(str(self.amount))
self.amount = truncate_decimal(
value=self.amount, decimal_places=self.account.currency.decimal_places
value=self.amount, decimal_places=account.currency.decimal_places
)
# Normalize reference_date
if self.reference_date:
self.reference_date = self.reference_date.replace(day=1)
elif not self.reference_date and self.date:
self.reference_date = self.date.replace(day=1)
super().clean_fields(*args, **kwargs)
def save(self, *args, **kwargs):
# This is not recommended as it will run twice on some cases like form and API saves.
# We only do this here because we forgot to independently call it on multiple places.