Skip to content

Commit 3e8736e

Browse files
committed
feat: adds movement templates to ease the pain of writing records
1 parent 5962f66 commit 3e8736e

File tree

5 files changed

+117
-12
lines changed

5 files changed

+117
-12
lines changed

src/transactions/admin.py

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,48 @@
11
from django.contrib import admin
22

3-
# Register your models here.
4-
from .models import Movement
3+
from django.contrib import admin
4+
from .models import Movement, MovementTemplate
5+
from django.urls import path, reverse
6+
from django.http import HttpResponseRedirect
7+
from django.utils.html import format_html
8+
9+
@admin.register(Movement)
10+
class MovementAdmin(admin.ModelAdmin):
11+
readonly_fields = ('balance_after',)
12+
list_display = ('type', 'description', 'amount_usd', 'balance_after', 'created_at') # optional, shows these columns in the list view
13+
14+
def get_changeform_initial_data(self, request):
15+
return {
16+
key: request.GET.get(key) for key in ['type', 'description', 'notes', 'amount_usd']
17+
if key in request.GET
18+
}
19+
20+
@admin.register(MovementTemplate)
21+
class MovementTemplateAdmin(admin.ModelAdmin):
22+
list_display = ('name', 'use_button', 'type', 'description', 'amount_usd')
23+
24+
def get_urls(self):
25+
urls = super().get_urls()
26+
custom_urls = [
27+
path(
28+
'use/<int:pk>/',
29+
self.admin_site.admin_view(self.use_recurrent_movement),
30+
name='use-recurrent-movement',
31+
),
32+
]
33+
return custom_urls + urls
34+
35+
def use_button(self, obj):
36+
url = reverse('admin:use-recurrent-movement', args=[obj.pk])
37+
return format_html('<a class="button" href="{}">Use</a>', url)
38+
use_button.short_description = "Use"
39+
use_button.allow_tags = True
540

6-
admin.site.register(Movement)
41+
def use_recurrent_movement(self, _, pk):
42+
recurrent = MovementTemplate.objects.get(pk=pk)
43+
add_url = (
44+
reverse("admin:transactions_movement_add") +
45+
f"?type={recurrent.type}&description={recurrent.description}"
46+
f"&notes={recurrent.notes or ''}&amount_usd={recurrent.amount_usd}"
47+
)
48+
return HttpResponseRedirect(add_url)

src/transactions/api/serializers.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from rest_framework import serializers
22

3-
from .calc import recalculate_balances
43
from ..models import Movement
54

65

@@ -12,5 +11,4 @@ class Meta:
1211

1312
def create(self, validated_data):
1413
instance = Movement.objects.create(**validated_data)
15-
recalculate_balances()
1614
return instance
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Generated by Django 5.1.7 on 2025-03-31 00:38
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('transactions', '0001_initial'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='movement',
15+
name='description',
16+
field=models.CharField(max_length=560),
17+
preserve_default=False,
18+
),
19+
migrations.AddField(
20+
model_name='movement',
21+
name='notes',
22+
field=models.TextField(blank=True, null=True),
23+
),
24+
]
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Generated by Django 5.1.7 on 2025-04-06 00:02
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('transactions', '0002_movement_description_movement_notes'),
10+
]
11+
12+
operations = [
13+
migrations.CreateModel(
14+
name='MovementTemplate',
15+
fields=[
16+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
17+
('name', models.CharField(max_length=100)),
18+
('type', models.CharField(choices=[('income', 'Income'), ('expense', 'Expense')], max_length=10)),
19+
('description', models.CharField(max_length=560)),
20+
('notes', models.TextField(blank=True, null=True)),
21+
('amount_usd', models.DecimalField(decimal_places=2, max_digits=10)),
22+
],
23+
),
24+
]

src/transactions/models.py

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77

88
def recalculate_balances():
9+
"""Recalculate the balance after each movement. This way the balance is updated even if you add a movement that happened in the past."""
10+
911
movements = Movement.objects.all().order_by("created_at")
1012
balance = Decimal('0.00')
1113

@@ -19,14 +21,17 @@ def recalculate_balances():
1921
movement.balance_after = balance
2022
movement.save(update_fields=['balance_after'])
2123

24+
MOVEMENT_TYPES = [
25+
("income", "Income"),
26+
("expense", "Expense"),
27+
]
2228

23-
class Movement(models.Model):
24-
MOVEMENT_TYPES = [
25-
("income", "Income"),
26-
("expense", "Expense"),
27-
]
2829

30+
class Movement(models.Model):
31+
"""Represents a money movement (income or expense)."""
2932
type = models.CharField(max_length=10, choices=MOVEMENT_TYPES)
33+
description = models.CharField(max_length=560)
34+
notes = models.TextField(blank=True, null=True)
3035
amount_usd = models.DecimalField(max_digits=10, decimal_places=2)
3136
created_at = models.DateTimeField(default=timezone.now)
3237
balance_after = models.DecimalField(max_digits=10, decimal_places=2, editable=False)
@@ -42,7 +47,19 @@ def save(self, *args, **kwargs):
4247
self.balance_after = Decimal("0.00")
4348

4449
super().save(*args, **kwargs) # Save once so the row exists (with a temporary value)
45-
46-
# Recalculate all balances in chronological order
4750
recalculate_balances()
4851

52+
def __str__(self):
53+
return f"{self.created_at.date()} - {self.type.capitalize()}: ${self.amount_usd} - {self.description}"
54+
55+
56+
class MovementTemplate(models.Model):
57+
"""Represents a template of sort, to automatically create movements that are recurrent in time."""
58+
name = models.CharField(max_length=100)
59+
type = models.CharField(max_length=10, choices=MOVEMENT_TYPES)
60+
description = models.CharField(max_length=560)
61+
notes = models.TextField(blank=True, null=True)
62+
amount_usd = models.DecimalField(max_digits=10, decimal_places=2)
63+
64+
def __str__(self):
65+
return self.name

0 commit comments

Comments
 (0)