feat: time tracking
This commit is contained in:
1
main.py
1
main.py
@@ -41,6 +41,7 @@ routers_list = [
|
|||||||
routers.role_router,
|
routers.role_router,
|
||||||
routers.marketplace_router,
|
routers.marketplace_router,
|
||||||
routers.payroll_router,
|
routers.payroll_router,
|
||||||
|
routers.time_tracking_router,
|
||||||
]
|
]
|
||||||
for router in routers_list:
|
for router in routers_list:
|
||||||
app.include_router(router)
|
app.include_router(router)
|
||||||
|
|||||||
@@ -10,3 +10,4 @@ from .user import user_router
|
|||||||
from .role import role_router
|
from .role import role_router
|
||||||
from .marketplace import marketplace_router
|
from .marketplace import marketplace_router
|
||||||
from .payroll import payroll_router
|
from .payroll import payroll_router
|
||||||
|
from .time_tracking import time_tracking_router
|
||||||
|
|||||||
36
routers/time_tracking.py
Normal file
36
routers/time_tracking.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
from backend.dependecies import SessionDependency, CurrentUserDependency
|
||||||
|
from schemas.time_tracking import *
|
||||||
|
from services.time_tracking import TimeTrackingService
|
||||||
|
|
||||||
|
time_tracking_router = APIRouter(
|
||||||
|
prefix="/time-tracking",
|
||||||
|
tags=["time-tracking"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@time_tracking_router.post(
|
||||||
|
'/get-records',
|
||||||
|
operation_id='get_time_tracking_records',
|
||||||
|
response_model=GetTimeTrackingRecordsResponse
|
||||||
|
)
|
||||||
|
async def get_data(
|
||||||
|
session: SessionDependency,
|
||||||
|
request: GetTimeTrackingRecordsRequest
|
||||||
|
):
|
||||||
|
return await TimeTrackingService(session).get_records(request)
|
||||||
|
|
||||||
|
|
||||||
|
@time_tracking_router.post(
|
||||||
|
'/update-record',
|
||||||
|
operation_id='update_time_tracking_record',
|
||||||
|
response_model=UpdateTimeTrackingRecordResponse
|
||||||
|
)
|
||||||
|
async def get_data(
|
||||||
|
session: SessionDependency,
|
||||||
|
request: UpdateTimeTrackingRecordRequest,
|
||||||
|
user: CurrentUserDependency
|
||||||
|
):
|
||||||
|
return await TimeTrackingService(session).update_record(user, request)
|
||||||
|
|
||||||
45
schemas/time_tracking.py
Normal file
45
schemas/time_tracking.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import datetime
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from schemas.base import BaseSchema, OkMessageSchema
|
||||||
|
from schemas.user import UserSchema
|
||||||
|
|
||||||
|
|
||||||
|
# region Entities
|
||||||
|
class TimeTrackingData(BaseSchema):
|
||||||
|
date: datetime.date
|
||||||
|
hours: int
|
||||||
|
amount: int
|
||||||
|
|
||||||
|
|
||||||
|
class TimeTrackingRecord(BaseSchema):
|
||||||
|
user: UserSchema
|
||||||
|
total_amount: int
|
||||||
|
data: List[TimeTrackingData]
|
||||||
|
|
||||||
|
|
||||||
|
# endregion
|
||||||
|
|
||||||
|
|
||||||
|
# region Requests
|
||||||
|
class GetTimeTrackingRecordsRequest(BaseSchema):
|
||||||
|
date: datetime.date
|
||||||
|
user_ids: list[int]
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateTimeTrackingRecordRequest(BaseSchema):
|
||||||
|
user_id: int
|
||||||
|
date: datetime.date
|
||||||
|
hours: int
|
||||||
|
|
||||||
|
|
||||||
|
# endregion
|
||||||
|
|
||||||
|
# region Responses
|
||||||
|
class GetTimeTrackingRecordsResponse(BaseSchema):
|
||||||
|
records: List[TimeTrackingRecord]
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateTimeTrackingRecordResponse(OkMessageSchema):
|
||||||
|
pass
|
||||||
|
# endregion
|
||||||
@@ -167,11 +167,10 @@ class PayrollService(BaseService):
|
|||||||
)
|
)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
async def _create_payment_record_hourly(
|
def get_amount(
|
||||||
self,
|
self,
|
||||||
creator: User,
|
|
||||||
user: User,
|
user: User,
|
||||||
record_schema: PaymentRecordCreateSchema
|
work_units: int
|
||||||
):
|
):
|
||||||
pay_rate: PayRate = user.pay_rate
|
pay_rate: PayRate = user.pay_rate
|
||||||
overtime_threshold = 0
|
overtime_threshold = 0
|
||||||
@@ -183,13 +182,22 @@ class PayrollService(BaseService):
|
|||||||
overtime_rate = pay_rate.overtime_rate
|
overtime_rate = pay_rate.overtime_rate
|
||||||
|
|
||||||
if overtime_threshold == 0 or overtime_rate == 0:
|
if overtime_threshold == 0 or overtime_rate == 0:
|
||||||
base_units = record_schema.work_units
|
base_units = work_units
|
||||||
overtime_units = 0
|
overtime_units = 0
|
||||||
else:
|
else:
|
||||||
overtime_units = max(0, record_schema.work_units - overtime_threshold)
|
overtime_units = max(0, work_units - overtime_threshold)
|
||||||
base_units = record_schema.work_units - overtime_units
|
base_units = work_units - overtime_units
|
||||||
|
|
||||||
amount = pay_rate.base_rate * base_units + overtime_rate * overtime_units
|
return pay_rate.base_rate * base_units + overtime_rate * overtime_units
|
||||||
|
|
||||||
|
async def _create_payment_record_hourly(
|
||||||
|
self,
|
||||||
|
creator: User,
|
||||||
|
user: User,
|
||||||
|
record_schema: PaymentRecordCreateSchema
|
||||||
|
):
|
||||||
|
pay_rate: PayRate = user.pay_rate
|
||||||
|
amount = self.get_amount(user, record_schema.work_units)
|
||||||
payment_record_dict = record_schema.model_dump()
|
payment_record_dict = record_schema.model_dump()
|
||||||
del payment_record_dict['user']
|
del payment_record_dict['user']
|
||||||
payment_record_dict.update({
|
payment_record_dict.update({
|
||||||
@@ -202,54 +210,14 @@ class PayrollService(BaseService):
|
|||||||
stmt = (
|
stmt = (
|
||||||
insert(
|
insert(
|
||||||
PaymentRecord
|
PaymentRecord
|
||||||
).values(
|
)
|
||||||
|
.values(
|
||||||
**payment_record_dict
|
**payment_record_dict
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
await self.session.execute(stmt)
|
await self.session.execute(stmt)
|
||||||
await self.session.commit()
|
await self.session.commit()
|
||||||
|
|
||||||
async def _create_payment_record_daily(
|
|
||||||
self,
|
|
||||||
creator: User,
|
|
||||||
user: User,
|
|
||||||
record_schema: PaymentRecordCreateSchema
|
|
||||||
):
|
|
||||||
pay_rate: PayRate = user.pay_rate
|
|
||||||
amount = pay_rate.base_rate * record_schema.work_units
|
|
||||||
payment_record_dict = record_schema.model_dump()
|
|
||||||
del payment_record_dict['user']
|
|
||||||
|
|
||||||
payment_record_dict.update({
|
|
||||||
'created_by_user_id': creator.id,
|
|
||||||
'created_at': datetime.datetime.now(),
|
|
||||||
'payroll_scheme_key': pay_rate.payroll_scheme.key,
|
|
||||||
'amount': amount,
|
|
||||||
'user_id': record_schema.user.id
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
async def _create_payment_record_monthly(
|
|
||||||
self,
|
|
||||||
creator: User,
|
|
||||||
|
|
||||||
user: User,
|
|
||||||
record_schema: PaymentRecordCreateSchema
|
|
||||||
) -> CreatePaymentRecordResponse:
|
|
||||||
pay_rate: PayRate = user.pay_rate
|
|
||||||
amount = pay_rate.base_rate * record_schema.work_units
|
|
||||||
payment_record_dict = record_schema.model_dump()
|
|
||||||
del payment_record_dict['user']
|
|
||||||
|
|
||||||
payment_record_dict.update({
|
|
||||||
'created_by_user_id': creator.id,
|
|
||||||
'created_at': datetime.datetime.now(),
|
|
||||||
'payroll_scheme_key': pay_rate.payroll_scheme.key,
|
|
||||||
'amount': amount,
|
|
||||||
'user_id': record_schema.user.id
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
async def create_payment_record(
|
async def create_payment_record(
|
||||||
self,
|
self,
|
||||||
request: CreatePaymentRecordRequest,
|
request: CreatePaymentRecordRequest,
|
||||||
@@ -282,7 +250,8 @@ class PayrollService(BaseService):
|
|||||||
stmt = (
|
stmt = (
|
||||||
delete(
|
delete(
|
||||||
PaymentRecord
|
PaymentRecord
|
||||||
).where(
|
)
|
||||||
|
.where(
|
||||||
PaymentRecord.id == request.payment_record_id
|
PaymentRecord.id == request.payment_record_id
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
111
services/time_tracking.py
Normal file
111
services/time_tracking.py
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
from sqlalchemy import select, func
|
||||||
|
from sqlalchemy.orm import joinedload
|
||||||
|
|
||||||
|
from models import PaymentRecord, User
|
||||||
|
from schemas.time_tracking import *
|
||||||
|
from services.base import BaseService
|
||||||
|
from services.payroll import PayrollService
|
||||||
|
from services.user import UserService
|
||||||
|
|
||||||
|
|
||||||
|
class TimeTrackingService(BaseService):
|
||||||
|
async def get_records(self, request: GetTimeTrackingRecordsRequest) -> GetTimeTrackingRecordsResponse:
|
||||||
|
stmt = (
|
||||||
|
select(
|
||||||
|
PaymentRecord,
|
||||||
|
)
|
||||||
|
.options(
|
||||||
|
joinedload(
|
||||||
|
PaymentRecord.user
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
func.date(func.date_trunc('month', PaymentRecord.start_date)) == request.date,
|
||||||
|
func.date(func.date_trunc('month', PaymentRecord.end_date)) == request.date,
|
||||||
|
PaymentRecord.start_date == PaymentRecord.end_date,
|
||||||
|
# PaymentRecord.user_id.in_(request.user_ids)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
query_result = (await self.session.scalars(stmt)).all()
|
||||||
|
records_dict = defaultdict(list)
|
||||||
|
users_dict = {}
|
||||||
|
amount_dict = defaultdict(list)
|
||||||
|
for payment_record in query_result:
|
||||||
|
user = UserSchema.model_validate(payment_record.user)
|
||||||
|
data = TimeTrackingData(
|
||||||
|
date=payment_record.start_date,
|
||||||
|
hours=payment_record.work_units,
|
||||||
|
amount=payment_record.amount
|
||||||
|
)
|
||||||
|
users_dict[user.id] = user
|
||||||
|
records_dict[user.id].append(data)
|
||||||
|
amount_dict[user.id].append(payment_record.amount)
|
||||||
|
|
||||||
|
records = []
|
||||||
|
for user_id, data_list in records_dict.items():
|
||||||
|
amount = sum(amount_dict[user_id])
|
||||||
|
user = users_dict[user_id]
|
||||||
|
record = TimeTrackingRecord(
|
||||||
|
user=user,
|
||||||
|
data=data_list,
|
||||||
|
total_amount=amount
|
||||||
|
)
|
||||||
|
records.append(
|
||||||
|
record
|
||||||
|
)
|
||||||
|
return GetTimeTrackingRecordsResponse(
|
||||||
|
records=records
|
||||||
|
)
|
||||||
|
|
||||||
|
async def update_record(self,
|
||||||
|
user: User,
|
||||||
|
request: UpdateTimeTrackingRecordRequest
|
||||||
|
) -> UpdateTimeTrackingRecordResponse:
|
||||||
|
try:
|
||||||
|
record_user = await UserService(self.session).get_by_id(user_id=request.user_id)
|
||||||
|
if not record_user:
|
||||||
|
return UpdateTimeTrackingRecordResponse(ok=False, message="Указанный пользователь не найден!")
|
||||||
|
if not record_user.pay_rate:
|
||||||
|
return UpdateTimeTrackingRecordResponse(ok=False, message="У пользователя не указана схема оплаты!")
|
||||||
|
existing_record_stmt = (
|
||||||
|
select(
|
||||||
|
PaymentRecord
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
PaymentRecord.user_id == request.user_id,
|
||||||
|
PaymentRecord.start_date == request.date,
|
||||||
|
PaymentRecord.end_date == request.date,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
amount = (
|
||||||
|
PayrollService(
|
||||||
|
self.session
|
||||||
|
)
|
||||||
|
.get_amount(
|
||||||
|
user=record_user,
|
||||||
|
work_units=request.hours
|
||||||
|
)
|
||||||
|
)
|
||||||
|
existing_record = await self.session.scalar(existing_record_stmt)
|
||||||
|
if existing_record:
|
||||||
|
existing_record: PaymentRecord
|
||||||
|
existing_record.work_units = request.hours
|
||||||
|
existing_record.amount = amount
|
||||||
|
else:
|
||||||
|
new_record = PaymentRecord(
|
||||||
|
user_id=request.user_id,
|
||||||
|
created_by_user_id=user.id,
|
||||||
|
start_date=request.date,
|
||||||
|
end_date=request.date,
|
||||||
|
created_at=datetime.datetime.now(),
|
||||||
|
payroll_scheme_key=record_user.pay_rate.payroll_scheme_key,
|
||||||
|
amount=amount,
|
||||||
|
work_units=request.hours
|
||||||
|
)
|
||||||
|
self.session.add(new_record)
|
||||||
|
await self.session.commit()
|
||||||
|
return UpdateTimeTrackingRecordResponse(ok=True, message="Запись успешно обновлена")
|
||||||
|
except Exception as e:
|
||||||
|
return UpdateTimeTrackingRecordResponse(ok=False, message=str(e))
|
||||||
45
test.py
45
test.py
@@ -1,25 +1,44 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
import datetime
|
||||||
|
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select, func
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy.orm import joinedload
|
||||||
|
|
||||||
from backend.session import session_maker
|
from backend.session import session_maker
|
||||||
from models import User
|
from models import User, PaymentRecord
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
work_units = 15
|
|
||||||
base_rate = 0
|
|
||||||
overtime_units = max([0, work_units - base_rate])
|
|
||||||
base_units = work_units - overtime_units
|
|
||||||
print(overtime_units, base_units)
|
|
||||||
return
|
|
||||||
session: AsyncSession = session_maker()
|
session: AsyncSession = session_maker()
|
||||||
a = await session.scalar(
|
try:
|
||||||
select(User).where(User.first_name == "Абид")
|
d = datetime.date.today()
|
||||||
|
d = d.replace(day=1)
|
||||||
)
|
print(d)
|
||||||
print(a)
|
stmt = (
|
||||||
|
select(
|
||||||
|
PaymentRecord
|
||||||
|
)
|
||||||
|
.select_from(PaymentRecord)
|
||||||
|
.options(
|
||||||
|
joinedload(
|
||||||
|
PaymentRecord.user
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
func.date(func.date_trunc('month', PaymentRecord.start_date)) == d,
|
||||||
|
func.date(func.date_trunc('month', PaymentRecord.end_date)) == d,
|
||||||
|
PaymentRecord.start_date == PaymentRecord.end_date,
|
||||||
|
# PaymentRecord.user_id.in_(request.user_ids)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
print(stmt.compile(compile_kwargs={
|
||||||
|
'literal_binds': True
|
||||||
|
}))
|
||||||
|
query_result = (await session.scalars(stmt)).all()
|
||||||
|
print(query_result)
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
await session.close()
|
await session.close()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user