Skip to content

Commit c8be9b4

Browse files
committed
feat(lending): Track and display item condition on return
1 parent 84d7923 commit c8be9b4

File tree

5 files changed

+125
-38
lines changed

5 files changed

+125
-38
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 5.2.7 on 2025-10-26 07:16
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('inventory', '0013_userprofile'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='checkinlog',
15+
name='condition',
16+
field=models.CharField(choices=[('OK', 'OK'), ('DAMAGED', 'Damaged')], default='OK', help_text='The condition of the item upon return.', max_length=10),
17+
),
18+
]

inventory/models.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,12 +268,23 @@ def is_overdue(self):
268268

269269
class CheckInLog(models.Model):
270270
"""A record of a partial or full return for a specific checkout."""
271+
class Condition(models.TextChoices):
272+
OK = 'OK', 'OK'
273+
DAMAGED = 'DAMAGED', 'Damaged'
274+
271275
checkout_log = models.ForeignKey(CheckoutLog, on_delete=models.CASCADE, related_name="check_in_logs")
272276
quantity_returned = models.PositiveIntegerField()
273277
return_date = models.DateTimeField(auto_now_add=True)
278+
279+
condition = models.CharField(
280+
max_length=10,
281+
choices=Condition.choices,
282+
default=Condition.OK,
283+
help_text="The condition of the item upon return."
284+
)
274285

275286
def __str__(self):
276-
return f"{self.quantity_returned} units of {self.checkout_log.item.name} returned on {self.return_date.strftime('%Y-%m-%d')}"
287+
return f"{self.quantity_returned} units of {self.checkout_log.item.name} returned on {self.return_date.strftime('%Y-%m-%d')} (Condition: {self.get_condition_display()})"
277288

278289
class ItemLog(models.Model):
279290
"""A permanent record of a change in an item's stock quantity."""

inventory/templates/inventory/check_in_page.html

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,37 @@ <h3>Loan Details</h3>
2222
<hr style="margin: 2em 0;">
2323

2424
<h3>Process a Return</h3>
25-
<form action="{% url 'inventory:process_check_in' log_entry.id %}" method="post">
25+
<form action="{% url 'inventory:process_check_in' log_entry.id %}" method="post" class="styled-form">
2626
{% csrf_token %}
27-
<label for="quantity_returned">Quantity to Return:</label>
28-
<input type="number" name="quantity_returned" value="{{ quantity_still_on_loan }}" min="1" max="{{ quantity_still_on_loan }}" autofocus required>
29-
<button type="submit">Confirm Return</button>
27+
28+
<div class="form-field">
29+
<label for="id_quantity_returned">Quantity to Return:</label>
30+
<input type="number" id="id_quantity_returned" name="quantity_returned" value="{{ quantity_still_on_loan }}" min="1" max="{{ quantity_still_on_loan }}" autofocus required>
31+
</div>
32+
33+
<div class="form-field">
34+
<label>Condition on Return:</label>
35+
<div class="radio-group">
36+
<div class="radio-item">
37+
<input type="radio" id="id_condition_ok" name="condition" value="OK" checked>
38+
<label for="id_condition_ok">
39+
<i class="fas fa-check-circle text-success"></i> OK
40+
</label>
41+
</div>
42+
<div class="radio-item">
43+
<input type="radio" id="id_condition_damaged" name="condition" value="DAMAGED">
44+
<label for="id_condition_damaged">
45+
<i class="fas fa-times-circle text-danger"></i> Damaged
46+
</label>
47+
</div>
48+
</div>
49+
</div>
50+
51+
<div class="form-actions">
52+
<button type="submit" class="btn btn-primary">
53+
<i class="fas fa-undo-alt"></i> Confirm Return
54+
</button>
55+
</div>
3056
</form>
3157
</div>
3258
{% endblock %}

inventory/templates/inventory/student_detail.html

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -45,26 +45,37 @@ <h2>Items Currently on Loan</h2>
4545
<p>This student has no items currently on loan.</p>
4646
{% endif %}
4747

48-
<h2 style="margin-top: 2em;">Full Loan History</h2>
49-
{% if loan_history %}
50-
<table class="open-table">
51-
<thead>
52-
<tr><th>Item</th><th>Quantity</th><th>Checked Out</th><th>Returned On</th></tr>
53-
</thead>
54-
<tbody>
55-
{% for log in loan_history %}
56-
<tr>
57-
<td><a href="{{ log.item.get_absolute_url }}">{{ log.item.name }}</a></td>
58-
<td>{{ log.quantity }}</td>
59-
<td>{{ log.checkout_date }}</td>
60-
<td>{{ log.return_date }}</td>
61-
</tr>
62-
{% endfor %}
63-
</tbody>
64-
</table>
65-
{% else %}
66-
<p>This student has not returned any items yet.</p>
67-
{% endif %}
48+
<h2 style="margin-top: 2em;">Return History</h2>
49+
{% if return_history %}
50+
<table class="open-table">
51+
<thead>
52+
<tr>
53+
<th>Item</th>
54+
<th>Quantity Returned</th>
55+
<th>Return Date</th>
56+
<th>Condition</th>
57+
</tr>
58+
</thead>
59+
<tbody>
60+
{% for log in return_history %}
61+
<tr>
62+
<td><a href="{{ log.checkout_log.item.get_absolute_url }}">{{ log.checkout_log.item.name }}</a></td>
63+
<td>{{ log.quantity_returned }}</td>
64+
<td>{{ log.return_date }}</td>
65+
<td>
66+
{% if log.condition == 'OK' %}
67+
<span class="badge bg-success">{{ log.get_condition_display }}</span>
68+
{% else %}
69+
<span class="badge bg-danger">{{ log.get_condition_display }}</span>
70+
{% endif %}
71+
</td>
72+
</tr>
73+
{% endfor %}
74+
</tbody>
75+
</table>
76+
{% else %}
77+
<p>This student has no return history.</p>
78+
{% endif %}
6879

6980
{% if request.user.profile.role == 'ADMIN' %}
7081
<h2 class="destructive">Delete this student</h2>

inventory/views.py

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -472,20 +472,21 @@ def student_list(request):
472472
@login_required
473473
def student_detail(request, student_id):
474474
student = get_object_or_404(Student, id=student_id)
475+
475476
items_on_loan = CheckoutLog.objects.filter(
476477
student=student,
477478
return_date__isnull=True
478-
).order_by('-checkout_date')
479+
).select_related('item').order_by('-checkout_date')
479480

480-
loan_history = CheckoutLog.objects.filter(
481-
student=student,
482-
return_date__isnull=False
483-
).order_by('-return_date')
481+
# This is the new query: it fetches individual return logs instead of closed loans.
482+
return_history = CheckInLog.objects.filter(
483+
checkout_log__student=student
484+
).select_related('checkout_log__item').order_by('-return_date')
484485

485486
context = {
486487
'student': student,
487488
'items_on_loan': items_on_loan,
488-
'loan_history': loan_history,
489+
'return_history': return_history, # We now pass the new queryset to the template
489490
}
490491
return render(request, 'inventory/student_detail.html', context)
491492

@@ -892,30 +893,50 @@ def check_in_page(request, log_id):
892893
@login_required
893894
def process_check_in(request, log_id):
894895
"""
895-
Handles the logic of a partial or full return.
896+
Handles the logic of a partial or full return, including item condition.
897+
If an item is returned as 'Damaged', its total quantity in the main
898+
inventory is permanently reduced.
896899
"""
897900
if request.method == 'POST':
898901
log_entry = get_object_or_404(CheckoutLog, id=log_id, return_date__isnull=True)
899902

900903
try:
901904
quantity_to_return = int(request.POST.get('quantity_returned', 0))
902-
quantity_still_on_loan = log_entry.quantity - log_entry.quantity_returned_so_far
905+
return_condition = request.POST.get('condition', CheckInLog.Condition.OK)
906+
quantity_still_on_loan = log_entry.quantity_still_on_loan
903907

904-
if quantity_to_return <= 0:
908+
if return_condition not in CheckInLog.Condition.values:
909+
messages.error(request, "Invalid return condition specified.")
910+
elif quantity_to_return <= 0:
905911
messages.error(request, "Quantity to return must be a positive number.")
906912
elif quantity_to_return > quantity_still_on_loan:
907913
messages.error(request, f"Cannot return {quantity_to_return}. Only {quantity_still_on_loan} units are on loan.")
908914
else:
909915
CheckInLog.objects.create(
910916
checkout_log=log_entry,
911-
quantity_returned=quantity_to_return
917+
quantity_returned=quantity_to_return,
918+
condition=return_condition
912919
)
913-
920+
921+
if return_condition == CheckInLog.Condition.DAMAGED:
922+
item = log_entry.item
923+
item.quantity -= quantity_to_return
924+
item.save()
925+
926+
ItemLog.objects.create(
927+
item=item,
928+
user=request.user,
929+
action=ItemLog.Action.DAMAGED,
930+
quantity_change=-quantity_to_return,
931+
notes=f"Reported damaged during return by student {log_entry.student.name}."
932+
)
933+
messages.warning(request, f"{quantity_to_return} x '{item.name}' were marked as damaged and removed from total stock.")
934+
914935
log_entry.refresh_from_db()
915936

916-
messages.success(request, f"Successfully returned {quantity_to_return} x '{log_entry.item.name}'.")
937+
messages.success(request, f"Successfully processed return of {quantity_to_return} x '{log_entry.item.name}'.")
917938

918-
if log_entry.quantity_returned_so_far == log_entry.quantity:
939+
if log_entry.quantity_still_on_loan == 0:
919940
log_entry.return_date = timezone.now()
920941
log_entry.save()
921942
messages.info(request, "This loan is now fully returned and closed.")

0 commit comments

Comments
 (0)