Skip to content

Commit 72ea611

Browse files
committed
feat: Improve balance synchronization and fix orphaned links
- Display a warning for orphaned transferred days - Enable synchronize button for orphaned days - Add logic to fix orphaned transferred days links - Improve serializer to fetch previous year's balance directly - Update log messages for balance and transferred days fixes
1 parent 7d72be3 commit 72ea611

3 files changed

Lines changed: 88 additions & 12 deletions

File tree

client/src/components/dashboard/AuditUserBalance.vue

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
<div class="d-flex justify-space-between align-end mb-4">
4646
<div>
4747
<h3 class="text-h6 font-weight-bold">Audit Results for {{ reportData.user_full_name
48-
}}</h3>
48+
}}</h3>
4949
<div class="text-body-2 text-medium-emphasis">Period: {{ reportData.year }} |
5050
Reason: <span class="text-capitalize">{{ reportData.reason }}</span></div>
5151
</div>
@@ -136,14 +136,23 @@
136136

137137
<v-alert v-if="reportData.discrepancy" type="info" variant="tonal" class="mt-6 border-red"
138138
color="error" icon="mdi-alert-circle">
139-
<div class="font-weight-bold">Correction Required</div>
139+
<div class="font-weight-bold">Balance Discrepancy Detected</div>
140140
The database balance is out of sync. This action will synchronize the records and notify
141141
the user.
142142
</v-alert>
143143

144+
<v-alert v-if="reportData.transferred_days_orphaned" type="warning" variant="tonal" class="mt-4"
145+
icon="mdi-link-variant-off">
146+
<div class="font-weight-bold">Orphaned Transferred Balance Link</div>
147+
The {{ reportData.year + 1 }} balance is pointing to an outdated transferred days record
148+
(showing {{ reportData.next_year_transferred_value }} instead of {{
149+
reportData.expected_remaining }}).
150+
This will be fixed when you click the synchronize button.
151+
</v-alert>
152+
144153
<div class="d-flex justify-end mt-4">
145-
<v-btn v-if="reportData.discrepancy" color="error" @click="showFixConfirm = true"
146-
:loading="isFixing" class="text-none font-weight-bold px-8">
154+
<v-btn v-if="reportData.discrepancy || reportData.transferred_days_orphaned" color="error"
155+
@click="showFixConfirm = true" :loading="isFixing" class="text-none font-weight-bold px-8">
147156
Synchronize User Balance
148157
</v-btn>
149158
</div>
@@ -251,6 +260,9 @@ export default {
251260
} else {
252261
addLog('Calculations match database records.', 'success')
253262
}
263+
if (result.transferred_days_orphaned) {
264+
addLog(`ALERT: ${selectedYear.value + 1} transferred balance is incorrectly linked (shows ${result.next_year_transferred_value} instead of ${result.expected_remaining}).`, 'warn')
265+
}
254266
} catch (err: any) {
255267
const errorMsg = err.response?.data?.message || err.message || 'Unknown error'
256268
addLog(`Error generating report: ${errorMsg}`, 'error')
@@ -271,8 +283,16 @@ export default {
271283
reason: selectedReason.value
272284
})
273285
274-
addLog(`SUCCESS: Balance updated to ${result.new_remaining}.`, 'success')
275-
addLog(`Notification sent to user. Process complete.`, 'info')
286+
if (result.balance_fixed) {
287+
addLog(`Balance updated to ${result.new_remaining}.`, 'success')
288+
}
289+
if (result.transferred_days_fixed) {
290+
addLog(`Fixed orphaned transferred balance link for ${selectedYear.value + 1}.`, 'success')
291+
}
292+
if (result.balance_fixed) {
293+
addLog(`Notification sent to user.`, 'info')
294+
}
295+
addLog(`Process complete.`, 'success')
276296
277297
// Refresh report
278298
await generateReport()

server/cshr/serializers/vacations.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -188,10 +188,28 @@ def get_transferred_days(
188188
self, obj: UserVacationBalance
189189
) -> VacationBalanceSerializer:
190190
"""
191-
this function serialize transferred days
191+
Serialize transferred days by directly fetching the previous year's remaining balance.
192+
This is more reliable than using the transferred_days relationship which can become orphaned.
193+
Returns None if the balance is locked (not available for use).
192194
"""
193-
return (
194-
None
195-
if not obj.transferred_days
196-
else VacationBalanceSerializer(obj.transferred_days).data
197-
)
195+
# Always fetch the previous year's remaining_days directly for accuracy
196+
from cshr.models.vacations import UserVacationBalance as UVB
197+
198+
previous_year_balance = UVB.objects.filter(
199+
user=obj.user, year=obj.year - 1
200+
).select_related("remaining_days").first()
201+
202+
if previous_year_balance and previous_year_balance.remaining_days:
203+
# If the balance is locked, don't return it (user can't use old balance)
204+
if previous_year_balance.remaining_days.is_locked:
205+
return None
206+
return VacationBalanceSerializer(previous_year_balance.remaining_days).data
207+
208+
# Fallback to the stored transferred_days if no previous year exists
209+
# Also check if it's locked
210+
if obj.transferred_days:
211+
if obj.transferred_days.is_locked:
212+
return None
213+
return VacationBalanceSerializer(obj.transferred_days).data
214+
215+
return None

server/cshr/services/balance.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,14 @@ def audit_user_balance(user: User, year: int, reason: str) -> Dict[str, Any]:
9090

9191
expected_remaining = total_quota - total_recalc_current
9292

93+
# Check if next year's transferred_days is correctly linked
94+
next_year_balance = UserVacationBalance.objects.filter(user=user, year=year + 1).first()
95+
transferred_days_orphaned = False
96+
if next_year_balance and next_year_balance.transferred_days:
97+
# Check if it's pointing to the correct object (this year's remaining_days)
98+
if next_year_balance.transferred_days.id != balance_obj.remaining_days.id:
99+
transferred_days_orphaned = True
100+
93101
return {
94102
"user_full_name": user.full_name,
95103
"year": year,
@@ -99,14 +107,37 @@ def audit_user_balance(user: User, year: int, reason: str) -> Dict[str, Any]:
99107
"expected_remaining": expected_remaining,
100108
"vacations": reports,
101109
"discrepancy": expected_remaining != current_db_remaining,
110+
"transferred_days_orphaned": transferred_days_orphaned,
111+
"next_year_transferred_value": getattr(next_year_balance.transferred_days, reason, None) if (next_year_balance and next_year_balance.transferred_days) else None,
102112
}
103113

114+
@staticmethod
115+
def fix_transferred_days_link(user: User, year: int) -> bool:
116+
"""
117+
Fixes the transferred_days relationship for the next year's balance.
118+
Returns True if a fix was applied.
119+
"""
120+
current_balance = UserVacationBalance.objects.filter(user=user, year=year).first()
121+
next_year_balance = UserVacationBalance.objects.filter(user=user, year=year + 1).first()
122+
123+
if not current_balance or not next_year_balance:
124+
return False
125+
126+
if next_year_balance.transferred_days and next_year_balance.transferred_days.id != current_balance.remaining_days.id:
127+
# Fix the orphaned reference
128+
next_year_balance.transferred_days = current_balance.remaining_days
129+
next_year_balance.save()
130+
return True
131+
132+
return False
133+
104134
@staticmethod
105135
def fix_user_balance(
106136
user: User, year: int, reason: str, admin_sender: Optional[User] = None
107137
) -> Dict[str, Any]:
108138
"""
109139
Recalculates and fixes the user's balance. Sends a notification if fixed.
140+
Also fixes orphaned transferred_days references.
110141
"""
111142
audit_result = BalanceService.audit_user_balance(user, year, reason)
112143
if not audit_result:
@@ -147,10 +178,17 @@ def fix_user_balance(
147178
)
148179
notification_service.send()
149180

181+
# Always try to fix orphaned transferred_days link (independent of balance discrepancy)
182+
transferred_days_fixed = False
183+
if audit_result.get("transferred_days_orphaned"):
184+
transferred_days_fixed = BalanceService.fix_transferred_days_link(user, year)
185+
150186
return {
151187
"new_remaining": audit_result["expected_remaining"],
152188
"total_consumed": audit_result["total_quota"]
153189
- audit_result["expected_remaining"],
190+
"balance_fixed": audit_result["discrepancy"],
191+
"transferred_days_fixed": transferred_days_fixed,
154192
}
155193

156194
@staticmethod

0 commit comments

Comments
 (0)