Skip to content

Commit 6bd13a0

Browse files
committed
Processing receivable fees
1 parent fd87304 commit 6bd13a0

File tree

8 files changed

+266
-220
lines changed

8 files changed

+266
-220
lines changed

thebook/members/admin.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from django import forms
12
from django.contrib import admin
23

34
from thebook.members.models import (

thebook/members/managers.py

Lines changed: 6 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,42 +5,19 @@
55

66

77
class ReceivableFeeManager(models.Manager):
8-
def create_for_next_month(self):
9-
from thebook.members.models import FeePaymentStatus, Membership
108

11-
today = datetime.date.today()
9+
def create_for_next_month(self):
10+
return self.create_for_next_period()
1211

13-
next_month = today.month + 1 if today.month < 12 else 1
14-
next_year = today.year + 1 if today.month == 12 else today.year
12+
def create_for_next_period(self):
13+
from thebook.members.models import FeePaymentStatus, Membership
1514

1615
receivable_fees = []
1716

1817
memberships = Membership.objects.filter(active=True)
1918
for membership in memberships:
20-
_, last_day_of_next_payment_month = calendar.monthrange(
21-
membership.next_membership_fee_payment_date.year,
22-
membership.next_membership_fee_payment_date.month,
23-
)
24-
due_date = datetime.date(
25-
membership.next_membership_fee_payment_date.year,
26-
membership.next_membership_fee_payment_date.month,
27-
last_day_of_next_payment_month,
28-
)
29-
30-
if (due_date.year, due_date.month) != (next_year, next_month):
31-
continue
32-
33-
start_date = datetime.date(due_date.year, due_date.month, 1)
34-
35-
receivable_fees.append(
36-
self.model(
37-
membership=membership,
38-
start_date=start_date,
39-
due_date=due_date,
40-
amount=membership.membership_fee_amount,
41-
status=FeePaymentStatus.UNPAID,
42-
)
43-
)
19+
receivable_fees.append(self.create_for_next_period(commit=False))
20+
4421
return self.model.objects.bulk_create(
4522
receivable_fees,
4623
update_conflicts=True,
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Generated by Django 5.2.2 on 2025-08-11 21:15
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("members", "0011_remove_receivablefeetransactionmatchrule_priority"),
10+
]
11+
12+
operations = [
13+
migrations.AlterField(
14+
model_name="membership",
15+
name="active",
16+
field=models.BooleanField(
17+
default=False,
18+
help_text="Indicates is we are expecting to be receiving fees from this membership",
19+
verbose_name="Active Membership",
20+
),
21+
),
22+
]
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Generated by Django 5.2.2 on 2025-09-08 23:29
2+
3+
import django.db.models.deletion
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
(
11+
"bookkeeping",
12+
"0014_remove_categorymatchrule_valid_comparison_function_and_more",
13+
),
14+
("members", "0012_alter_membership_active"),
15+
]
16+
17+
operations = [
18+
migrations.AlterField(
19+
model_name="membership",
20+
name="payment_method",
21+
field=models.IntegerField(
22+
choices=[(1, "PayPal"), (2, "Pix"), (3, "Recurring Pix")],
23+
default=1,
24+
help_text="How fees are paid?",
25+
verbose_name="Payment Method",
26+
),
27+
),
28+
migrations.AlterField(
29+
model_name="receivablefee",
30+
name="status",
31+
field=models.IntegerField(
32+
choices=[(1, "Paid"), (2, "Unpaid"), (3, "Due"), (4, "Skipped")],
33+
default=2,
34+
verbose_name="Payment Status",
35+
),
36+
),
37+
migrations.AlterField(
38+
model_name="receivablefee",
39+
name="transaction",
40+
field=models.ForeignKey(
41+
blank=True,
42+
null=True,
43+
on_delete=django.db.models.deletion.SET_NULL,
44+
to="bookkeeping.transaction",
45+
),
46+
),
47+
]

thebook/members/models.py

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,25 +31,29 @@ class FeePaymentStatus:
3131
PAID = 1
3232
UNPAID = 2
3333
DUE = 3
34+
SKIPPED = 4
3435

3536
@classproperty
3637
def choices(cls):
3738
return (
3839
(cls.PAID, _("Paid")),
3940
(cls.UNPAID, _("Unpaid")),
4041
(cls.DUE, _("Due")),
42+
(cls.SKIPPED, _("Skipped")),
4143
)
4244

4345

4446
class PaymentMethod:
4547
PAYPAL = 1
4648
PIX = 2
49+
PIX_RECURRING = 3
4750

4851
@classproperty
4952
def choices(cls):
5053
return (
5154
(cls.PAYPAL, _("PayPal")),
5255
(cls.PIX, _("Pix")),
56+
(cls.PIX_RECURRING, _("Recurring Pix")),
5357
)
5458

5559

@@ -80,7 +84,7 @@ class Membership(models.Model):
8084
help_text=_("How fees are paid?"),
8185
)
8286
active = models.BooleanField(
83-
default=True,
87+
default=False,
8488
verbose_name=_("Active Membership"),
8589
help_text=_(
8690
"Indicates is we are expecting to be receiving fees from this membership"
@@ -97,6 +101,60 @@ def __str__(self):
97101
active_status = "Active" if self.active else "Inactive"
98102
return f"{active_status} membership of {self.member.name}"
99103

104+
def create_next_receivable_fee(self, commit=True):
105+
if not self.active:
106+
return None
107+
108+
def _next_period(current_date, payment_interval):
109+
month_increment = {
110+
FeeIntervals.MONTHLY: 1,
111+
FeeIntervals.QUARTERLY: 3,
112+
FeeIntervals.BIANNUALLY: 6,
113+
FeeIntervals.ANNUALLY: 12,
114+
}
115+
next_possible_month = current_date.month + month_increment[payment_interval]
116+
117+
if (
118+
next_possible_month % 12 == next_possible_month
119+
or next_possible_month % 12 == 0
120+
):
121+
next_month = next_possible_month
122+
next_year = current_date.year
123+
else:
124+
next_month = next_possible_month % 12
125+
next_year = current_date.year + 1
126+
127+
return next_month, next_year
128+
129+
last_receivable_fee = self.receivable_fees.first()
130+
if last_receivable_fee is None:
131+
_, last_day_of_month = calendar.monthrange(
132+
self.start_date.year, self.start_date.month
133+
)
134+
start_date = datetime.date(self.start_date.year, self.start_date.month, 1)
135+
due_date = datetime.date(
136+
self.start_date.year, self.start_date.month, last_day_of_month
137+
)
138+
else:
139+
next_month, next_year = _next_period(
140+
last_receivable_fee.start_date, self.payment_interval
141+
)
142+
_, last_day_of_month = calendar.monthrange(next_year, next_month)
143+
start_date = datetime.date(next_year, next_month, 1)
144+
due_date = datetime.date(next_year, next_month, last_day_of_month)
145+
146+
receivable_fee = ReceivableFee(
147+
membership=self,
148+
start_date=start_date,
149+
due_date=due_date,
150+
amount=self.membership_fee_amount,
151+
status=FeePaymentStatus.UNPAID,
152+
)
153+
if commit:
154+
receivable_fee.save()
155+
156+
return receivable_fee
157+
100158
@property
101159
def next_membership_fee_payment_date(self):
102160
allowed_months = sorted(

thebook/members/signals.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ def _send_onboarding_message(membership):
2424
@receiver(post_save, sender=Membership)
2525
def check_active_status(sender, instance, created, **kwargs):
2626
if created:
27+
instance.create_next_receivable_fee()
2728
return
2829

2930
if instance.active and not instance._original_active:

0 commit comments

Comments
 (0)