fix: fixed shifts time tracking
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
from datetime import date, timedelta
|
||||
|
||||
import math
|
||||
from datetime import date, timedelta, time
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
from sqlalchemy import select, func, extract, literal, label, Select
|
||||
from sqlalchemy.orm import joinedload, selectinload
|
||||
|
||||
from logger import logger_builder
|
||||
from models import WorkShift, User
|
||||
from models.work_shifts import WorkShiftPause
|
||||
from schemas.base import PaginationSchema
|
||||
@@ -74,33 +75,106 @@ class WorkShiftsService(BaseService):
|
||||
return FinishShiftByIdResponse(ok=ok, message=message)
|
||||
|
||||
async def _finish_shift_common(self, user: User, work_shift: Optional[WorkShift]) -> tuple[bool, str]:
|
||||
if not work_shift or work_shift.finished_at:
|
||||
return False, "Смена для сотрудника еще не начата"
|
||||
logger = logger_builder.get_logger()
|
||||
try:
|
||||
if not work_shift or work_shift.finished_at:
|
||||
return False, "Смена для сотрудника еще не начата"
|
||||
|
||||
if work_shift.is_paused:
|
||||
await self.finish_pause_by_shift_id(work_shift.id)
|
||||
# End active pause
|
||||
if work_shift.is_paused:
|
||||
await self.finish_pause_by_shift_id(work_shift.id)
|
||||
|
||||
work_shift.finished_at = datetime.now()
|
||||
await self.session.commit()
|
||||
work_shift.finished_at = datetime.now()
|
||||
|
||||
pause_time = timedelta()
|
||||
for pause in work_shift.pauses:
|
||||
pause_time += pause.finished_at - pause.started_at
|
||||
# Collect pauses
|
||||
pauses = [
|
||||
(pause.started_at, pause.finished_at)
|
||||
for pause in work_shift.pauses
|
||||
]
|
||||
|
||||
total_work_time: timedelta = work_shift.finished_at - work_shift.started_at
|
||||
pure_work_seconds = total_work_time.total_seconds() - pause_time.total_seconds()
|
||||
hours = pure_work_seconds / 3600
|
||||
# Build raw work intervals
|
||||
# Start with one interval: whole shift
|
||||
raw_intervals = [(work_shift.started_at, work_shift.finished_at)]
|
||||
|
||||
if pure_work_seconds >= 60:
|
||||
data = UpdateTimeTrackingRecordRequest(
|
||||
user_id=work_shift.user_id,
|
||||
date=work_shift.started_at.date(),
|
||||
hours=hours,
|
||||
)
|
||||
await TimeTrackingService(self.session).update_record(user, data)
|
||||
# Subtract pauses from work intervals
|
||||
work_intervals = self.subtract_pauses(raw_intervals, pauses)
|
||||
|
||||
hours, minutes = hours_to_hours_and_minutes(total_work_time)
|
||||
return True, f"Смена закончена. Отработано {hours} ч. {minutes} мин."
|
||||
# Split intervals by days
|
||||
daily_hours = self.split_intervals_by_days(work_intervals)
|
||||
|
||||
# Create work records per day
|
||||
tts = TimeTrackingService(self.session)
|
||||
for day, hours in daily_hours.items():
|
||||
if hours < (1 / 60): # ignore <1 minute
|
||||
continue
|
||||
|
||||
data = UpdateTimeTrackingRecordRequest(
|
||||
user_id=work_shift.user_id,
|
||||
date=day,
|
||||
hours=hours,
|
||||
)
|
||||
ok, msg = await tts.update_work_record(user, data, False)
|
||||
if not ok:
|
||||
raise Exception(msg)
|
||||
|
||||
await self.session.commit()
|
||||
|
||||
# Build human-readable result message
|
||||
total_work_seconds = sum(hours * 3600 for hours in daily_hours.values())
|
||||
total_td = timedelta(seconds=total_work_seconds)
|
||||
h, m = hours_to_hours_and_minutes(total_td)
|
||||
logger.info(f"Успешное завершение смены. userID: {work_shift.user_id}. Отработано суммарно: {h} ч. {m} мин.")
|
||||
return True, f"Смена закончена. Отработано {h} ч. {m} мин."
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка завершения смены. userID: {work_shift.user_id}. Ошибка: {str(e)}")
|
||||
await self.session.rollback()
|
||||
return False, str(e)
|
||||
|
||||
def subtract_pauses(
|
||||
self,
|
||||
work_intervals: list[tuple[datetime, datetime]],
|
||||
pauses: list[tuple[datetime, datetime]]
|
||||
) -> list[tuple[datetime, datetime]]:
|
||||
result = []
|
||||
|
||||
for w_start, w_end in work_intervals:
|
||||
temp = [(w_start, w_end)]
|
||||
for p_start, p_end in pauses:
|
||||
new_temp = []
|
||||
for s, e in temp:
|
||||
# pause outside interval → keep original
|
||||
if p_end <= s or p_start >= e:
|
||||
new_temp.append((s, e))
|
||||
else:
|
||||
# pause cuts interval
|
||||
if p_start > s:
|
||||
new_temp.append((s, p_start))
|
||||
if p_end < e:
|
||||
new_temp.append((p_end, e))
|
||||
temp = new_temp
|
||||
result.extend(temp)
|
||||
|
||||
return result
|
||||
|
||||
def split_intervals_by_days(self, intervals: list[tuple[datetime, datetime]]) -> dict[date, float]:
|
||||
from collections import defaultdict
|
||||
res = defaultdict(float)
|
||||
|
||||
for start, end in intervals:
|
||||
cur = start
|
||||
while cur.date() < end.date():
|
||||
# end of current day
|
||||
day_end = datetime.combine(cur.date(), time.max)
|
||||
seconds = (day_end - cur).total_seconds()
|
||||
res[cur.date()] += seconds / 3600
|
||||
cur = day_end + timedelta(seconds=1)
|
||||
|
||||
# last segment (same day)
|
||||
seconds = (end - cur).total_seconds()
|
||||
res[cur.date()] += seconds / 3600
|
||||
|
||||
return res
|
||||
|
||||
@staticmethod
|
||||
def get_work_shifts_history_stmt() -> Select:
|
||||
|
||||
Reference in New Issue
Block a user