fix: fixed shifts time tracking
This commit is contained in:
@@ -17,4 +17,4 @@ class JsonFormatter(logging.Formatter):
|
||||
if record.exc_info:
|
||||
log_record["exception"] = self.formatException(record.exc_info)
|
||||
|
||||
return json.dumps(log_record)
|
||||
return json.dumps(log_record, ensure_ascii=False)
|
||||
|
||||
18
main.py
18
main.py
@@ -24,24 +24,24 @@ async def lifespan(app: FastAPI):
|
||||
except KafkaConnectionError as e:
|
||||
print(e)
|
||||
|
||||
consumer_task = asyncio.create_task(consume_messages())
|
||||
# consumer_task = asyncio.create_task(consume_messages())
|
||||
|
||||
yield
|
||||
producer = await get_producer()
|
||||
if producer:
|
||||
await producer.stop()
|
||||
consumer_task.cancel()
|
||||
# consumer_task.cancel()
|
||||
|
||||
|
||||
app = FastAPI(lifespan=lifespan, separate_input_output_schemas=False, root_path='/api')
|
||||
|
||||
# app.add_middleware(
|
||||
# CORSMiddleware,
|
||||
# allow_origins=origins,
|
||||
# allow_credentials=True,
|
||||
# allow_methods=["*"],
|
||||
# allow_headers=["*"],
|
||||
# )
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
routers_list = [
|
||||
routers.attribute_router,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from datetime import datetime, date
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from sqlalchemy import ForeignKey, Table, Column
|
||||
from sqlalchemy.sql import expression
|
||||
@@ -15,12 +15,8 @@ class WorkShift(BaseModel):
|
||||
__tablename__ = "work_shifts"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
started_at: Mapped[datetime] = mapped_column(
|
||||
nullable=False,
|
||||
)
|
||||
finished_at: Mapped[datetime] = mapped_column(
|
||||
nullable=True,
|
||||
)
|
||||
started_at: Mapped[datetime] = mapped_column()
|
||||
finished_at: Mapped[Optional[datetime]] = mapped_column()
|
||||
is_paused: Mapped[bool] = mapped_column(
|
||||
default=False,
|
||||
server_default=expression.false(),
|
||||
@@ -48,12 +44,8 @@ class WorkShiftPause(BaseModel):
|
||||
__tablename__ = "work_shifts_pauses"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
started_at: Mapped[datetime] = mapped_column(
|
||||
nullable=False,
|
||||
)
|
||||
finished_at: Mapped[datetime] = mapped_column(
|
||||
nullable=True,
|
||||
)
|
||||
started_at: Mapped[datetime] = mapped_column()
|
||||
finished_at: Mapped[Optional[datetime]] = mapped_column()
|
||||
|
||||
work_shift_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("work_shifts.id"),
|
||||
@@ -77,10 +69,10 @@ class PlannedWorkShift(BaseModel):
|
||||
__tablename__ = "planned_work_shifts"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
shift_date: Mapped[date] = mapped_column(nullable=False, index=True)
|
||||
created_at: Mapped[datetime] = mapped_column(nullable=False)
|
||||
shift_date: Mapped[date] = mapped_column(index=True)
|
||||
created_at: Mapped[datetime] = mapped_column()
|
||||
|
||||
user_id: Mapped[int] = mapped_column(ForeignKey('users.id'), nullable=False, index=True)
|
||||
user_id: Mapped[int] = mapped_column(ForeignKey('users.id'), index=True)
|
||||
user: Mapped["User"] = relationship(lazy="selectin", backref="planned_work_shifts")
|
||||
|
||||
positions: Mapped[list["Position"]] = relationship(
|
||||
|
||||
@@ -59,17 +59,17 @@ class TimeTrackingService(BaseService):
|
||||
records.append(record)
|
||||
return GetTimeTrackingRecordsResponse(records=records)
|
||||
|
||||
async def update_record(
|
||||
async def update_work_record(
|
||||
self,
|
||||
user: User,
|
||||
request: UpdateTimeTrackingRecordRequest
|
||||
) -> UpdateTimeTrackingRecordResponse:
|
||||
try:
|
||||
request: UpdateTimeTrackingRecordRequest,
|
||||
commit: bool = True,
|
||||
) -> tuple[bool, str]:
|
||||
record_user = await UserService(self.session).get_by_id(user_id=request.user_id)
|
||||
if not record_user:
|
||||
return UpdateTimeTrackingRecordResponse(ok=False, message="Указанный пользователь не найден!")
|
||||
return False, "Указанный пользователь не найден!"
|
||||
if not record_user.pay_rate:
|
||||
return UpdateTimeTrackingRecordResponse(ok=False, message="У пользователя не указана схема оплаты!")
|
||||
return False, "У пользователя не указана схема оплаты!"
|
||||
existing_record_stmt = (
|
||||
select(
|
||||
PaymentRecord
|
||||
@@ -106,7 +106,17 @@ class TimeTrackingService(BaseService):
|
||||
work_units=request.hours
|
||||
)
|
||||
self.session.add(new_record)
|
||||
if commit:
|
||||
await self.session.commit()
|
||||
return UpdateTimeTrackingRecordResponse(ok=True, message="Запись успешно обновлена")
|
||||
return True, "Запись успешно добавлена"
|
||||
|
||||
async def update_record(
|
||||
self,
|
||||
user: User,
|
||||
request: UpdateTimeTrackingRecordRequest
|
||||
) -> UpdateTimeTrackingRecordResponse:
|
||||
try:
|
||||
ok, message = await self.update_work_record(user, request)
|
||||
return UpdateTimeTrackingRecordResponse(ok=ok, message=message)
|
||||
except Exception as e:
|
||||
return UpdateTimeTrackingRecordResponse(ok=False, message=str(e))
|
||||
|
||||
@@ -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]:
|
||||
logger = logger_builder.get_logger()
|
||||
try:
|
||||
if not work_shift or work_shift.finished_at:
|
||||
return False, "Смена для сотрудника еще не начата"
|
||||
|
||||
# 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()
|
||||
|
||||
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)]
|
||||
|
||||
# Subtract pauses from work intervals
|
||||
work_intervals = self.subtract_pauses(raw_intervals, pauses)
|
||||
|
||||
# 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
|
||||
|
||||
if pure_work_seconds >= 60:
|
||||
data = UpdateTimeTrackingRecordRequest(
|
||||
user_id=work_shift.user_id,
|
||||
date=work_shift.started_at.date(),
|
||||
date=day,
|
||||
hours=hours,
|
||||
)
|
||||
await TimeTrackingService(self.session).update_record(user, data)
|
||||
ok, msg = await tts.update_work_record(user, data, False)
|
||||
if not ok:
|
||||
raise Exception(msg)
|
||||
|
||||
hours, minutes = hours_to_hours_and_minutes(total_work_time)
|
||||
return True, f"Смена закончена. Отработано {hours} ч. {minutes} мин."
|
||||
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