March 11 2026: changed regular & special work mixing calculation issues 1
This commit is contained in:
@@ -9385,7 +9385,20 @@ def export_time_attendance_excel(records, project_name_for_filename, date_range_
|
|||||||
end_date = max(r.attendance_date for r in records)
|
end_date = max(r.attendance_date for r in records)
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# Enforce maximum 2-week (14-day) export window.
|
||||||
|
# If the selected range exceeds 14 days, cap end_date to start_date + 13 days.
|
||||||
|
MAX_EXPORT_DAYS = 14
|
||||||
|
if (end_date - start_date).days >= MAX_EXPORT_DAYS:
|
||||||
|
capped_end_date = start_date + timedelta(days=MAX_EXPORT_DAYS - 1)
|
||||||
|
logger_handler.logger.info(
|
||||||
|
f"TA Excel export: date range [{start_date} – {end_date}] exceeds {MAX_EXPORT_DAYS} days; "
|
||||||
|
f"capping end_date to {capped_end_date}."
|
||||||
|
)
|
||||||
|
end_date = capped_end_date
|
||||||
|
# Drop records that fall outside the capped window
|
||||||
|
records = [r for r in records if r.attendance_date <= end_date]
|
||||||
|
|
||||||
# Import parse function at the beginning for work type detection
|
# Import parse function at the beginning for work type detection
|
||||||
from working_hours_calculator import parse_employee_id_for_work_type
|
from working_hours_calculator import parse_employee_id_for_work_type
|
||||||
|
|
||||||
@@ -9976,54 +9989,97 @@ def export_time_attendance_excel(records, project_name_for_filename, date_range_
|
|||||||
})
|
})
|
||||||
print(f" Record at {record.check_in_time}: action='{record.action_description}', is_out={is_out}")
|
print(f" Record at {record.check_in_time}: action='{record.action_description}', is_out={is_out}")
|
||||||
|
|
||||||
# Create pairs based on IN -> OUT logic
|
# Odd-IN rule: if there are more INs than OUTs in this building group,
|
||||||
|
# discard all but the LATEST IN — those earlier INs become Missed Punch rows.
|
||||||
|
# The latest IN is then paired with the earliest OUT.
|
||||||
|
ins = [ri for ri in record_info if not ri['is_out']]
|
||||||
|
outs = [ri for ri in record_info if ri['is_out']]
|
||||||
|
|
||||||
pairs_to_write = []
|
pairs_to_write = []
|
||||||
i = 0
|
|
||||||
while i < len(record_info):
|
if len(ins) > len(outs) and len(outs) > 0:
|
||||||
if record_info[i]['used']:
|
# Sort INs by time ascending; keep only the last one for pairing
|
||||||
i += 1
|
ins_sorted = sorted(ins, key=lambda ri: ri['record'].check_in_time)
|
||||||
continue
|
latest_in = ins_sorted[-1]
|
||||||
|
excess_ins = ins_sorted[:-1] # all earlier INs become Missed Punch
|
||||||
current_is_out = record_info[i]['is_out']
|
|
||||||
|
# Mark excess INs as used and emit Missed Punch rows
|
||||||
if not current_is_out: # This is an IN
|
for ri in excess_ins:
|
||||||
# Look for next OUT
|
ri['used'] = True
|
||||||
out_found = False
|
pairs_to_write.append({
|
||||||
for j in range(i + 1, len(record_info)):
|
'check_in': ri['record'],
|
||||||
if record_info[j]['used']:
|
'check_out': None,
|
||||||
continue
|
'is_miss_punch': True
|
||||||
if record_info[j]['is_out']: # Found an OUT
|
})
|
||||||
# Missed punch only if an unused OUT exists between i and j
|
|
||||||
intervening_out = any(
|
# Pair latest IN with earliest OUT
|
||||||
record_info[k]['is_out'] and not record_info[k]['used']
|
outs_sorted = sorted(outs, key=lambda ri: ri['record'].check_in_time)
|
||||||
for k in range(i + 1, j)
|
earliest_out = outs_sorted[0]
|
||||||
)
|
latest_in['used'] = True
|
||||||
|
earliest_out['used'] = True
|
||||||
|
pairs_to_write.append({
|
||||||
|
'check_in': latest_in['record'],
|
||||||
|
'check_out': earliest_out['record'],
|
||||||
|
'is_miss_punch': False
|
||||||
|
})
|
||||||
|
|
||||||
|
# Any remaining unused OUTs after the first become Missed Punch
|
||||||
|
for ri in outs_sorted[1:]:
|
||||||
|
ri['used'] = True
|
||||||
|
pairs_to_write.append({
|
||||||
|
'check_in': None,
|
||||||
|
'check_out': ri['record'],
|
||||||
|
'is_miss_punch': True
|
||||||
|
})
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Standard pairing: each IN finds the next available OUT
|
||||||
|
i = 0
|
||||||
|
while i < len(record_info):
|
||||||
|
if record_info[i]['used']:
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
current_is_out = record_info[i]['is_out']
|
||||||
|
|
||||||
|
if not current_is_out: # This is an IN
|
||||||
|
# Look for next OUT
|
||||||
|
out_found = False
|
||||||
|
for j in range(i + 1, len(record_info)):
|
||||||
|
if record_info[j]['used']:
|
||||||
|
continue
|
||||||
|
if record_info[j]['is_out']: # Found an OUT
|
||||||
|
# Missed punch only if an unused OUT exists between i and j
|
||||||
|
intervening_out = any(
|
||||||
|
record_info[k]['is_out'] and not record_info[k]['used']
|
||||||
|
for k in range(i + 1, j)
|
||||||
|
)
|
||||||
|
pairs_to_write.append({
|
||||||
|
'check_in': record_info[i]['record'],
|
||||||
|
'check_out': record_info[j]['record'],
|
||||||
|
'is_miss_punch': intervening_out
|
||||||
|
})
|
||||||
|
record_info[i]['used'] = True
|
||||||
|
record_info[j]['used'] = True
|
||||||
|
out_found = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if not out_found: # No OUT found for this IN
|
||||||
pairs_to_write.append({
|
pairs_to_write.append({
|
||||||
'check_in': record_info[i]['record'],
|
'check_in': record_info[i]['record'],
|
||||||
'check_out': record_info[j]['record'],
|
'check_out': None,
|
||||||
'is_miss_punch': intervening_out
|
'is_miss_punch': True
|
||||||
})
|
})
|
||||||
record_info[i]['used'] = True
|
record_info[i]['used'] = True
|
||||||
record_info[j]['used'] = True
|
else: # This is an orphaned OUT
|
||||||
out_found = True
|
|
||||||
break
|
|
||||||
|
|
||||||
if not out_found: # No OUT found for this IN
|
|
||||||
pairs_to_write.append({
|
pairs_to_write.append({
|
||||||
'check_in': record_info[i]['record'],
|
'check_in': None,
|
||||||
'check_out': None,
|
'check_out': record_info[i]['record'],
|
||||||
'is_miss_punch': True
|
'is_miss_punch': True
|
||||||
})
|
})
|
||||||
record_info[i]['used'] = True
|
record_info[i]['used'] = True
|
||||||
else: # This is an orphaned OUT
|
|
||||||
pairs_to_write.append({
|
i += 1
|
||||||
'check_in': None,
|
|
||||||
'check_out': record_info[i]['record'],
|
|
||||||
'is_miss_punch': True
|
|
||||||
})
|
|
||||||
record_info[i]['used'] = True
|
|
||||||
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
print(f" Created {len(pairs_to_write)} pairs")
|
print(f" Created {len(pairs_to_write)} pairs")
|
||||||
|
|
||||||
@@ -10375,7 +10431,18 @@ def export_time_attendance_by_building_excel(records, project_name_for_filename,
|
|||||||
end_date = max(r.attendance_date for r in records)
|
end_date = max(r.attendance_date for r in records)
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# Enforce maximum 2-week (14-day) export window.
|
||||||
|
MAX_EXPORT_DAYS = 14
|
||||||
|
if (end_date - start_date).days >= MAX_EXPORT_DAYS:
|
||||||
|
capped_end_date = start_date + timedelta(days=MAX_EXPORT_DAYS - 1)
|
||||||
|
logger_handler.logger.info(
|
||||||
|
f"TA by-building Excel export: date range [{start_date} – {end_date}] exceeds {MAX_EXPORT_DAYS} days; "
|
||||||
|
f"capping end_date to {capped_end_date}."
|
||||||
|
)
|
||||||
|
end_date = capped_end_date
|
||||||
|
records = [r for r in records if r.attendance_date <= end_date]
|
||||||
|
|
||||||
# Import parse function for work type detection
|
# Import parse function for work type detection
|
||||||
from working_hours_calculator import parse_employee_id_for_work_type
|
from working_hours_calculator import parse_employee_id_for_work_type
|
||||||
|
|
||||||
@@ -10635,47 +10702,73 @@ def export_time_attendance_by_building_excel(records, project_name_for_filename,
|
|||||||
'is_out': is_out,
|
'is_out': is_out,
|
||||||
'used': False
|
'used': False
|
||||||
})
|
})
|
||||||
|
|
||||||
# Create pairs
|
# Create pairs
|
||||||
pairs = []
|
pairs = []
|
||||||
i = 0
|
ins = [ri for ri in record_info if not ri['is_out']]
|
||||||
while i < len(record_info):
|
outs = [ri for ri in record_info if ri['is_out']]
|
||||||
if record_info[i]['used']:
|
|
||||||
i += 1
|
if len(ins) > len(outs) and len(outs) > 0:
|
||||||
continue
|
# Odd-IN rule: discard all but the LATEST IN; pair it with the earliest OUT
|
||||||
|
ins_sorted = sorted(ins, key=lambda ri: ri['record'].check_in_time)
|
||||||
if not record_info[i]['is_out']: # IN
|
outs_sorted = sorted(outs, key=lambda ri: ri['record'].check_in_time)
|
||||||
out_found = False
|
|
||||||
for j in range(i + 1, len(record_info)):
|
latest_in = ins_sorted[-1]
|
||||||
if record_info[j]['used']:
|
excess_ins = ins_sorted[:-1]
|
||||||
continue
|
earliest_out = outs_sorted[0]
|
||||||
if record_info[j]['is_out']:
|
|
||||||
|
for ri in excess_ins:
|
||||||
|
ri['used'] = True
|
||||||
|
pairs.append({'check_in': ri['record'], 'check_out': None, 'is_miss_punch': True})
|
||||||
|
|
||||||
|
latest_in['used'] = True
|
||||||
|
earliest_out['used'] = True
|
||||||
|
pairs.append({'check_in': latest_in['record'], 'check_out': earliest_out['record'], 'is_miss_punch': False})
|
||||||
|
|
||||||
|
for ri in outs_sorted[1:]:
|
||||||
|
ri['used'] = True
|
||||||
|
pairs.append({'check_in': None, 'check_out': ri['record'], 'is_miss_punch': True})
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Standard pairing
|
||||||
|
i = 0
|
||||||
|
while i < len(record_info):
|
||||||
|
if record_info[i]['used']:
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not record_info[i]['is_out']: # IN
|
||||||
|
out_found = False
|
||||||
|
for j in range(i + 1, len(record_info)):
|
||||||
|
if record_info[j]['used']:
|
||||||
|
continue
|
||||||
|
if record_info[j]['is_out']:
|
||||||
|
pairs.append({
|
||||||
|
'check_in': record_info[i]['record'],
|
||||||
|
'check_out': record_info[j]['record'],
|
||||||
|
'is_miss_punch': False
|
||||||
|
})
|
||||||
|
record_info[i]['used'] = True
|
||||||
|
record_info[j]['used'] = True
|
||||||
|
out_found = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if not out_found:
|
||||||
pairs.append({
|
pairs.append({
|
||||||
'check_in': record_info[i]['record'],
|
'check_in': record_info[i]['record'],
|
||||||
'check_out': record_info[j]['record'],
|
'check_out': None,
|
||||||
'is_miss_punch': False
|
'is_miss_punch': True
|
||||||
})
|
})
|
||||||
record_info[i]['used'] = True
|
record_info[i]['used'] = True
|
||||||
record_info[j]['used'] = True
|
else: # Orphaned OUT
|
||||||
out_found = True
|
|
||||||
break
|
|
||||||
|
|
||||||
if not out_found:
|
|
||||||
pairs.append({
|
pairs.append({
|
||||||
'check_in': record_info[i]['record'],
|
'check_in': None,
|
||||||
'check_out': None,
|
'check_out': record_info[i]['record'],
|
||||||
'is_miss_punch': True
|
'is_miss_punch': True
|
||||||
})
|
})
|
||||||
record_info[i]['used'] = True
|
record_info[i]['used'] = True
|
||||||
else: # Orphaned OUT
|
|
||||||
pairs.append({
|
i += 1
|
||||||
'check_in': None,
|
|
||||||
'check_out': record_info[i]['record'],
|
|
||||||
'is_miss_punch': True
|
|
||||||
})
|
|
||||||
record_info[i]['used'] = True
|
|
||||||
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
# Calculate daily hours
|
# Calculate daily hours
|
||||||
daily_hours = 0
|
daily_hours = 0
|
||||||
|
|||||||
Reference in New Issue
Block a user