diff --git a/app/apps/accounts/forms.py b/app/apps/accounts/forms.py index 0e81926..b1559f5 100644 --- a/app/apps/accounts/forms.py +++ b/app/apps/accounts/forms.py @@ -14,7 +14,7 @@ from apps.common.fields.forms.dynamic_select import ( from apps.common.widgets.crispy.submit import NoClassSubmit from apps.common.widgets.tom_select import TomSelect from apps.transactions.models import TransactionCategory, TransactionTag -from apps.transactions.widgets import ArbitraryDecimalDisplayNumberInput +from apps.common.widgets.decimal import ArbitraryDecimalDisplayNumberInput class AccountGroupForm(forms.ModelForm): diff --git a/app/apps/transactions/fields.py b/app/apps/common/fields/month_year.py similarity index 81% rename from app/apps/transactions/fields.py rename to app/apps/common/fields/month_year.py index 01af94f..ffb24bf 100644 --- a/app/apps/transactions/fields.py +++ b/app/apps/common/fields/month_year.py @@ -4,10 +4,10 @@ from django import forms from django.db import models from django.core.exceptions import ValidationError -from apps.transactions.widgets import MonthYearWidget +from apps.common.widgets.month_year import MonthYearWidget -class MonthYearField(models.DateField): +class MonthYearModelField(models.DateField): def to_python(self, value): if value is None or isinstance(value, datetime.date): return value @@ -27,6 +27,8 @@ class MonthYearField(models.DateField): class MonthYearFormField(forms.DateField): + widget = MonthYearWidget + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.input_formats = ["%Y-%m"] @@ -34,11 +36,13 @@ class MonthYearFormField(forms.DateField): def to_python(self, value): if value in self.empty_values: return None + if isinstance(value, datetime.datetime): + return value.date() try: date = datetime.datetime.strptime(value, "%Y-%m") return date.replace(day=1).date() except ValueError: - raise ValidationError("Invalid date format. Use YYYY-MM.") + raise ValidationError(_("Invalid date format. Use YYYY-MM.")) def prepare_value(self, value): if isinstance(value, datetime.date): diff --git a/app/apps/transactions/widgets.py b/app/apps/common/widgets/decimal.py similarity index 86% rename from app/apps/transactions/widgets.py rename to app/apps/common/widgets/decimal.py index 15f1d02..00028ea 100644 --- a/app/apps/transactions/widgets.py +++ b/app/apps/common/widgets/decimal.py @@ -1,9 +1,6 @@ -from datetime import datetime, date - -from django import forms from decimal import Decimal, InvalidOperation -from django.template.defaultfilters import floatformat +from django import forms from django.utils.formats import get_format, number_format @@ -25,19 +22,6 @@ def convert_to_decimal(value: str): return None -class MonthYearWidget(forms.DateInput): - """ - Custom widget to display a month-year picker. - """ - - input_type = "month" # Set the input type to 'month' - - def format_value(self, value): - if isinstance(value, (datetime, date)): - return value.strftime("%Y-%m") - return value - - class ArbitraryDecimalDisplayNumberInput(forms.TextInput): """A widget for displaying and inputing decimal numbers with the least amount of trailing zeros possible. You must set this on your Form's __init__ method.""" diff --git a/app/apps/common/widgets/month_year.py b/app/apps/common/widgets/month_year.py new file mode 100644 index 0000000..9dd4c75 --- /dev/null +++ b/app/apps/common/widgets/month_year.py @@ -0,0 +1,16 @@ +from datetime import datetime, date + +from django import forms + + +class MonthYearWidget(forms.DateInput): + """ + Custom widget to display a month-year picker. + """ + + input_type = "month" # Set the input type to 'month' + + def format_value(self, value): + if isinstance(value, (datetime, date)): + return value.strftime("%Y-%m") + return value diff --git a/app/apps/transactions/forms.py b/app/apps/transactions/forms.py index 43ff22c..40368c4 100644 --- a/app/apps/transactions/forms.py +++ b/app/apps/transactions/forms.py @@ -1,11 +1,10 @@ from crispy_bootstrap5.bootstrap5 import Switch from crispy_forms.bootstrap import FormActions from crispy_forms.helper import FormHelper -from crispy_forms.layout import Layout, Row, Column, Field, Fieldset +from crispy_forms.layout import Layout, Row, Column, Field from dateutil.relativedelta import relativedelta from django import forms from django.db import transaction -from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ from apps.accounts.models import Account @@ -14,17 +13,15 @@ from apps.common.fields.forms.dynamic_select import ( DynamicModelMultipleChoiceField, ) from apps.common.widgets.crispy.submit import NoClassSubmit -from apps.common.widgets.tom_select import TomSelect, TomSelectMultiple +from apps.common.widgets.tom_select import TomSelect from apps.transactions.models import ( Transaction, TransactionCategory, TransactionTag, InstallmentPlan, ) -from apps.transactions.widgets import ( - ArbitraryDecimalDisplayNumberInput, - MonthYearWidget, -) +from apps.common.widgets.decimal import ArbitraryDecimalDisplayNumberInput +from apps.common.fields.month_year import MonthYearFormField class TransactionForm(forms.ModelForm): @@ -40,6 +37,7 @@ class TransactionForm(forms.ModelForm): required=False, label=_("Tags"), ) + reference_date = MonthYearFormField(label=_("Reference Date"), required=False) class Meta: model = Transaction @@ -190,7 +188,7 @@ class TransferForm(forms.Form): date = forms.DateField( label="Date", widget=forms.DateInput(attrs={"type": "date"}, format="%Y-%m-%d") ) - reference_date = forms.CharField(label="Reference Date", widget=MonthYearWidget()) + reference_date = MonthYearFormField(label=_("Reference Date"), required=False) description = forms.CharField(max_length=500, label="Description") def __init__(self, *args, **kwargs): @@ -324,6 +322,7 @@ class InstallmentPlanForm(forms.Form): label=_("Start Date"), widget=forms.DateInput(attrs={"type": "date"}, format="%Y-%m-%d"), ) + reference_date = MonthYearFormField(label=_("Reference Date"), required=False) description = forms.CharField(max_length=500, label=_("Description")) number_of_installments = forms.IntegerField( min_value=1, label=_("Number of Installments") @@ -372,9 +371,13 @@ class InstallmentPlanForm(forms.Form): "account", "description", Row( - Column("start_date", css_class="form-group col-md-4 mb-0"), - Column("number_of_installments", css_class="form-group col-md-4 mb-0"), - Column("recurrence", css_class="form-group col-md-4 mb-0"), + Column("number_of_installments", css_class="form-group col-md-6 mb-0"), + Column("recurrence", css_class="form-group col-md-6 mb-0"), + css_class="form-row", + ), + 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"), css_class="form-row", ), "installment_amount", @@ -396,12 +399,16 @@ class InstallmentPlanForm(forms.Form): number_of_installments = self.cleaned_data["number_of_installments"] transaction_type = self.cleaned_data["type"] start_date = self.cleaned_data["start_date"] + reference_date = self.cleaned_data["reference_date"] or start_date recurrence = self.cleaned_data["recurrence"] account = self.cleaned_data["account"] description = self.cleaned_data["description"] installment_amount = self.cleaned_data["installment_amount"] category = self.cleaned_data["category"] + print(reference_date, type(reference_date)) + print(start_date, type(start_date)) + with transaction.atomic(): installment_plan = InstallmentPlan.objects.create( account=account, @@ -421,11 +428,13 @@ class InstallmentPlanForm(forms.Form): delta = relativedelta(days=i) transaction_date = start_date + delta + transaction_reference_date = (reference_date + delta).replace(day=1) new_transaction = Transaction.objects.create( account=account, type=transaction_type, date=transaction_date, - reference_date=transaction_date.replace(day=1), + is_paid=False, + reference_date=transaction_reference_date, amount=installment_amount, description=description, notes=f"{i + 1}/{number_of_installments}", diff --git a/app/apps/transactions/migrations/0001_initial.py b/app/apps/transactions/migrations/0001_initial.py index 4dab221..1024bcc 100644 --- a/app/apps/transactions/migrations/0001_initial.py +++ b/app/apps/transactions/migrations/0001_initial.py @@ -1,6 +1,6 @@ # Generated by Django 5.1.1 on 2024-09-19 02:11 -import apps.transactions.fields +import apps.common.fields.month_year from django.db import migrations, models @@ -8,19 +8,31 @@ class Migration(migrations.Migration): initial = True - dependencies = [ - ] + dependencies = [] operations = [ migrations.CreateModel( - name='Transaction', + name="Transaction", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('is_paid', models.BooleanField(default=True)), - ('date', models.DateField()), - ('reference_date', apps.transactions.fields.MonthYearField(help_text='Please enter a month and year in the format MM/YYYY.')), - ('description', models.CharField(max_length=500)), - ('notes', models.TextField(blank=True)), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("is_paid", models.BooleanField(default=True)), + ("date", models.DateField()), + ( + "reference_date", + apps.common.fields.month_year.MonthYearModelField( + help_text="Please enter a month and year in the format MM/YYYY." + ), + ), + ("description", models.CharField(max_length=500)), + ("notes", models.TextField(blank=True)), ], ), ] diff --git a/app/apps/transactions/migrations/0002_transaction_account_transaction_amount_and_more.py b/app/apps/transactions/migrations/0002_transaction_account_transaction_amount_and_more.py index b55e569..5bfd658 100644 --- a/app/apps/transactions/migrations/0002_transaction_account_transaction_amount_and_more.py +++ b/app/apps/transactions/migrations/0002_transaction_account_transaction_amount_and_more.py @@ -1,6 +1,6 @@ # Generated by Django 5.1.1 on 2024-09-19 13:35 -import apps.transactions.fields +import apps.common.fields.month_year import apps.transactions.validators import django.db.models.deletion from django.db import migrations, models @@ -9,46 +9,62 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('accounts', '0001_initial'), - ('transactions', '0001_initial'), + ("accounts", "0001_initial"), + ("transactions", "0001_initial"), ] operations = [ migrations.AddField( - model_name='transaction', - name='account', - field=models.ForeignKey(default=0, on_delete=django.db.models.deletion.PROTECT, to='accounts.account', verbose_name='Account'), + model_name="transaction", + name="account", + field=models.ForeignKey( + default=0, + on_delete=django.db.models.deletion.PROTECT, + to="accounts.account", + verbose_name="Account", + ), preserve_default=False, ), migrations.AddField( - model_name='transaction', - name='amount', - field=models.DecimalField(decimal_places=18, default=0, max_digits=30, validators=[apps.transactions.validators.validate_non_negative, apps.transactions.validators.validate_decimal_places], verbose_name='Amount'), + model_name="transaction", + name="amount", + field=models.DecimalField( + decimal_places=18, + default=0, + max_digits=30, + validators=[ + apps.transactions.validators.validate_non_negative, + apps.transactions.validators.validate_decimal_places, + ], + verbose_name="Amount", + ), preserve_default=False, ), migrations.AlterField( - model_name='transaction', - name='date', - field=models.DateField(verbose_name='Date'), + model_name="transaction", + name="date", + field=models.DateField(verbose_name="Date"), ), migrations.AlterField( - model_name='transaction', - name='description', - field=models.CharField(max_length=500, verbose_name='Description'), + model_name="transaction", + name="description", + field=models.CharField(max_length=500, verbose_name="Description"), ), migrations.AlterField( - model_name='transaction', - name='is_paid', - field=models.BooleanField(default=True, verbose_name='Paid'), + model_name="transaction", + name="is_paid", + field=models.BooleanField(default=True, verbose_name="Paid"), ), migrations.AlterField( - model_name='transaction', - name='notes', - field=models.TextField(blank=True, verbose_name='Notes'), + model_name="transaction", + name="notes", + field=models.TextField(blank=True, verbose_name="Notes"), ), migrations.AlterField( - model_name='transaction', - name='reference_date', - field=apps.transactions.fields.MonthYearField(verbose_name='Reference Date'), + model_name="transaction", + name="reference_date", + field=apps.common.fields.month_year.MonthYearModelField( + verbose_name="Reference Date" + ), ), ] diff --git a/app/apps/transactions/models.py b/app/apps/transactions/models.py index eeb77e5..efe73a9 100644 --- a/app/apps/transactions/models.py +++ b/app/apps/transactions/models.py @@ -6,9 +6,9 @@ from django.utils.functional import cached_property from django.utils.translation import gettext_lazy as _ from apps.common.functions.decimals import truncate_decimal -from apps.transactions.fields import MonthYearField from apps.transactions.validators import validate_decimal_places, validate_non_negative from apps.currencies.utils.convert import convert +from apps.common.fields.month_year import MonthYearModelField class TransactionCategory(models.Model): @@ -76,7 +76,7 @@ class Transaction(models.Model): ) is_paid = models.BooleanField(default=True, verbose_name=_("Paid")) date = models.DateField(verbose_name=_("Date")) - reference_date = MonthYearField(verbose_name=_("Reference Date")) + reference_date = MonthYearModelField(verbose_name=_("Reference Date")) amount = models.DecimalField( max_digits=42,