first commit
This commit is contained in:
22
Dockerfile
Normal file
22
Dockerfile
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
FROM python:3.11-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
build-base \
|
||||||
|
gcc \
|
||||||
|
musl-dev \
|
||||||
|
libffi-dev \
|
||||||
|
python3-dev \
|
||||||
|
py3-setuptools \
|
||||||
|
py3-pip \
|
||||||
|
py3-wheel \
|
||||||
|
cargo
|
||||||
|
|
||||||
|
RUN pip install --upgrade pip setuptools wheel
|
||||||
|
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
CMD uvicorn main:app --port 8081 --host=0.0.0.0 --use-colors --reload
|
||||||
BIN
__pycache__/main.cpython-311.pyc
Normal file
BIN
__pycache__/main.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/config.cpython-311.pyc
Normal file
BIN
app/__pycache__/config.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/mongo.cpython-310.pyc
Normal file
BIN
app/__pycache__/mongo.cpython-310.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/mongo.cpython-311.pyc
Normal file
BIN
app/__pycache__/mongo.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/schedules.cpython-311.pyc
Normal file
BIN
app/__pycache__/schedules.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/task_manager.cpython-310.pyc
Normal file
BIN
app/__pycache__/task_manager.cpython-310.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/task_manager.cpython-311.pyc
Normal file
BIN
app/__pycache__/task_manager.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/tasks.cpython-311.pyc
Normal file
BIN
app/__pycache__/tasks.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/api/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
app/api/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
35
app/api/v1/__init__.py
Normal file
35
app/api/v1/__init__.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
from app.api.v1 import (
|
||||||
|
project, board, client, service, product, barcode, deal,
|
||||||
|
shipping_warehouse, statistics, user, role, marketplace,
|
||||||
|
residues, department, position, payroll, status, auth, task, billing, work_shifts, transaction, time_tracking, shipping
|
||||||
|
)
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
router.include_router(barcode.router, prefix='/barcode')
|
||||||
|
router.include_router(deal.router, prefix='/deal')
|
||||||
|
router.include_router(payroll.router, prefix='/payroll')
|
||||||
|
router.include_router(product.router, prefix='/product')
|
||||||
|
router.include_router(service.router, prefix='/service')
|
||||||
|
|
||||||
|
router.include_router(auth.router, prefix='/auth')
|
||||||
|
router.include_router(billing.router, prefix='/billing')
|
||||||
|
router.include_router(board.router, prefix='/board')
|
||||||
|
router.include_router(client.router, prefix='/client')
|
||||||
|
router.include_router(department.router, prefix='/department')
|
||||||
|
router.include_router(marketplace.router, prefix='/marketplace')
|
||||||
|
router.include_router(position.router, prefix='/position')
|
||||||
|
router.include_router(project.router, prefix='/project')
|
||||||
|
router.include_router(residues.router, prefix='/residues')
|
||||||
|
router.include_router(role.router, prefix='/role')
|
||||||
|
router.include_router(shipping.router, prefix='/shipping')
|
||||||
|
router.include_router(shipping_warehouse.router, prefix='/shipping-warehouse')
|
||||||
|
router.include_router(statistics.router, prefix='/statistics')
|
||||||
|
router.include_router(status.router, prefix='/status')
|
||||||
|
router.include_router(task.router, prefix='/task')
|
||||||
|
router.include_router(time_tracking.router, prefix='/time-tracking')
|
||||||
|
router.include_router(transaction.router, prefix='/transaction')
|
||||||
|
router.include_router(user.router, prefix='/user')
|
||||||
|
router.include_router(work_shifts.router, prefix='/work-shifts')
|
||||||
BIN
app/api/v1/__pycache__/Image.cpython-310.pyc
Normal file
BIN
app/api/v1/__pycache__/Image.cpython-310.pyc
Normal file
Binary file not shown.
BIN
app/api/v1/__pycache__/Image_vector.cpython-310.pyc
Normal file
BIN
app/api/v1/__pycache__/Image_vector.cpython-310.pyc
Normal file
Binary file not shown.
BIN
app/api/v1/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
app/api/v1/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
app/api/v1/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
app/api/v1/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/api/v1/__pycache__/auth.cpython-311.pyc
Normal file
BIN
app/api/v1/__pycache__/auth.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/api/v1/__pycache__/barcode.cpython-311.pyc
Normal file
BIN
app/api/v1/__pycache__/barcode.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/api/v1/__pycache__/billing.cpython-311.pyc
Normal file
BIN
app/api/v1/__pycache__/billing.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/api/v1/__pycache__/board.cpython-311.pyc
Normal file
BIN
app/api/v1/__pycache__/board.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/api/v1/__pycache__/client.cpython-311.pyc
Normal file
BIN
app/api/v1/__pycache__/client.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/api/v1/__pycache__/collection.cpython-310.pyc
Normal file
BIN
app/api/v1/__pycache__/collection.cpython-310.pyc
Normal file
Binary file not shown.
BIN
app/api/v1/__pycache__/deal.cpython-311.pyc
Normal file
BIN
app/api/v1/__pycache__/deal.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/api/v1/__pycache__/department.cpython-311.pyc
Normal file
BIN
app/api/v1/__pycache__/department.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/api/v1/__pycache__/graf.cpython-310.pyc
Normal file
BIN
app/api/v1/__pycache__/graf.cpython-310.pyc
Normal file
Binary file not shown.
BIN
app/api/v1/__pycache__/graf.cpython-311.pyc
Normal file
BIN
app/api/v1/__pycache__/graf.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/api/v1/__pycache__/marketplace.cpython-311.pyc
Normal file
BIN
app/api/v1/__pycache__/marketplace.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/api/v1/__pycache__/position.cpython-311.pyc
Normal file
BIN
app/api/v1/__pycache__/position.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/api/v1/__pycache__/product.cpython-311.pyc
Normal file
BIN
app/api/v1/__pycache__/product.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/api/v1/__pycache__/project.cpython-311.pyc
Normal file
BIN
app/api/v1/__pycache__/project.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/api/v1/__pycache__/residues.cpython-311.pyc
Normal file
BIN
app/api/v1/__pycache__/residues.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/api/v1/__pycache__/role.cpython-311.pyc
Normal file
BIN
app/api/v1/__pycache__/role.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/api/v1/__pycache__/service.cpython-311.pyc
Normal file
BIN
app/api/v1/__pycache__/service.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/api/v1/__pycache__/shipping.cpython-311.pyc
Normal file
BIN
app/api/v1/__pycache__/shipping.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/api/v1/__pycache__/shipping_warehouse.cpython-311.pyc
Normal file
BIN
app/api/v1/__pycache__/shipping_warehouse.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/api/v1/__pycache__/statistics.cpython-311.pyc
Normal file
BIN
app/api/v1/__pycache__/statistics.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/api/v1/__pycache__/status.cpython-311.pyc
Normal file
BIN
app/api/v1/__pycache__/status.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/api/v1/__pycache__/storage.cpython-311.pyc
Normal file
BIN
app/api/v1/__pycache__/storage.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/api/v1/__pycache__/task.cpython-311.pyc
Normal file
BIN
app/api/v1/__pycache__/task.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/api/v1/__pycache__/time_tracking.cpython-311.pyc
Normal file
BIN
app/api/v1/__pycache__/time_tracking.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/api/v1/__pycache__/training.cpython-310.pyc
Normal file
BIN
app/api/v1/__pycache__/training.cpython-310.pyc
Normal file
Binary file not shown.
BIN
app/api/v1/__pycache__/transaction.cpython-311.pyc
Normal file
BIN
app/api/v1/__pycache__/transaction.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/api/v1/__pycache__/user.cpython-311.pyc
Normal file
BIN
app/api/v1/__pycache__/user.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/api/v1/__pycache__/vector.cpython-310.pyc
Normal file
BIN
app/api/v1/__pycache__/vector.cpython-310.pyc
Normal file
Binary file not shown.
BIN
app/api/v1/__pycache__/vector.cpython-311.pyc
Normal file
BIN
app/api/v1/__pycache__/vector.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/api/v1/__pycache__/work_shifts.cpython-311.pyc
Normal file
BIN
app/api/v1/__pycache__/work_shifts.cpython-311.pyc
Normal file
Binary file not shown.
75
app/api/v1/auth.py
Normal file
75
app/api/v1/auth.py
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from time import time
|
||||||
|
|
||||||
|
import jwt
|
||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
from app import mongo
|
||||||
|
from app.config import config
|
||||||
|
from app.utils.response_util import response
|
||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
def _string_generator(data_incoming):
|
||||||
|
data = data_incoming.copy()
|
||||||
|
del data["hash"]
|
||||||
|
keys = sorted(data.keys())
|
||||||
|
string_arr = []
|
||||||
|
for key in keys:
|
||||||
|
if data[key] is not None:
|
||||||
|
string_arr.append(key + "=" + str(data[key]))
|
||||||
|
string_cat = "\n".join(string_arr)
|
||||||
|
return string_cat
|
||||||
|
|
||||||
|
|
||||||
|
def _data_check(BOT_TOKEN, tg_data):
|
||||||
|
data_check_string = _string_generator(tg_data)
|
||||||
|
secret_key = hashlib.sha256(BOT_TOKEN.encode("utf-8")).digest()
|
||||||
|
secret_key_bytes = secret_key
|
||||||
|
data_check_string_bytes = bytes(data_check_string, "utf-8")
|
||||||
|
hmac_string = hmac.new(secret_key_bytes, data_check_string_bytes, hashlib.sha256).hexdigest()
|
||||||
|
if hmac_string == tg_data["hash"]:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def authorize(telegram_data: dict):
|
||||||
|
return _data_check(config['BOT_TOKEN'], telegram_data)
|
||||||
|
|
||||||
|
def create_access_token(data: dict) -> str:
|
||||||
|
to_encode = data.copy()
|
||||||
|
expire = datetime.now(timezone.utc) + timedelta(minutes=int(config["JWT_ACCESS_TOKEN_EXPIRE_MINUTES"]))
|
||||||
|
to_encode.update({"exp": expire})
|
||||||
|
encoded_jwt = jwt.encode(to_encode, config["JWT_SECRET_KEY"], algorithm=config["JWT_ALGORITHM"])
|
||||||
|
return encoded_jwt
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/login", tags=[""])
|
||||||
|
async def login(data: dict):
|
||||||
|
start_time = time()
|
||||||
|
if not authorize(data):
|
||||||
|
return response({
|
||||||
|
"detail": "Ошибка авторизации"
|
||||||
|
}, start_time=start_time, code=401)
|
||||||
|
|
||||||
|
|
||||||
|
user = await mongo.users_collection.find_one({
|
||||||
|
"telegramId": data["id"]
|
||||||
|
})
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
return response({
|
||||||
|
"detail": "Пользователь не найден"
|
||||||
|
}, start_time=start_time, code=401)
|
||||||
|
|
||||||
|
access_token = create_access_token({
|
||||||
|
"sub": str(user["id"]),
|
||||||
|
"role": user["role"]["key"],
|
||||||
|
})
|
||||||
|
|
||||||
|
return response({
|
||||||
|
"accessToken": access_token
|
||||||
|
}, start_time=start_time)
|
||||||
8
app/api/v1/barcode/__init__.py
Normal file
8
app/api/v1/barcode/__init__.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
from fastapi import APIRouter
|
||||||
|
from app.api.v1.barcode import barcode, template
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
router.include_router(barcode.router)
|
||||||
|
router.include_router(template.router, prefix='/template')
|
||||||
BIN
app/api/v1/barcode/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
app/api/v1/barcode/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/api/v1/barcode/__pycache__/attribute.cpython-311.pyc
Normal file
BIN
app/api/v1/barcode/__pycache__/attribute.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/api/v1/barcode/__pycache__/barcode.cpython-311.pyc
Normal file
BIN
app/api/v1/barcode/__pycache__/barcode.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/api/v1/barcode/__pycache__/size.cpython-311.pyc
Normal file
BIN
app/api/v1/barcode/__pycache__/size.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/api/v1/barcode/__pycache__/template.cpython-311.pyc
Normal file
BIN
app/api/v1/barcode/__pycache__/template.cpython-311.pyc
Normal file
Binary file not shown.
9
app/api/v1/barcode/barcode.py
Normal file
9
app/api/v1/barcode/barcode.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import json
|
||||||
|
from time import time
|
||||||
|
from fastapi import APIRouter
|
||||||
|
from app.config import config
|
||||||
|
from app.utils.logger_util import logger
|
||||||
|
from app.utils.response_util import response
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
9
app/api/v1/barcode/template/__init__.py
Normal file
9
app/api/v1/barcode/template/__init__.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
from fastapi import APIRouter
|
||||||
|
from app.api.v1.barcode.template import template, size, attribute
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
router.include_router(template.router)
|
||||||
|
router.include_router(size.router, prefix='/size')
|
||||||
|
router.include_router(attribute.router, prefix='/attribute')
|
||||||
BIN
app/api/v1/barcode/template/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
app/api/v1/barcode/template/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
app/api/v1/barcode/template/__pycache__/size.cpython-311.pyc
Normal file
BIN
app/api/v1/barcode/template/__pycache__/size.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/api/v1/barcode/template/__pycache__/template.cpython-311.pyc
Normal file
BIN
app/api/v1/barcode/template/__pycache__/template.cpython-311.pyc
Normal file
Binary file not shown.
93
app/api/v1/barcode/template/attribute.py
Normal file
93
app/api/v1/barcode/template/attribute.py
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import json
|
||||||
|
from time import time
|
||||||
|
from fastapi import APIRouter
|
||||||
|
from app.config import config
|
||||||
|
from app.utils.logger_util import logger
|
||||||
|
from app.utils.response_util import response
|
||||||
|
from app import mongo
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/get-all", tags=[""])
|
||||||
|
async def template_get_all():
|
||||||
|
start_time = time()
|
||||||
|
|
||||||
|
attributes = await mongo.template_attributes_collection.find({}, {
|
||||||
|
"_id": False
|
||||||
|
}).sort("id", mongo.asc).to_list()
|
||||||
|
|
||||||
|
return response({
|
||||||
|
"attributes": attributes
|
||||||
|
}, start_time=start_time)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/create", tags=[""])
|
||||||
|
async def create(params: dict):
|
||||||
|
start_time = time()
|
||||||
|
data = params["attribute"]
|
||||||
|
|
||||||
|
data["id"] = await mongo.get_next_id(mongo.template_attributes_collection)
|
||||||
|
|
||||||
|
logger.json(data)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await mongo.template_attributes_collection.insert_one(data)
|
||||||
|
|
||||||
|
return response({
|
||||||
|
"message": "Атрибут создан",
|
||||||
|
"ok": True
|
||||||
|
}, start_time=start_time)
|
||||||
|
except Exception as e:
|
||||||
|
return response({
|
||||||
|
"message": str(e),
|
||||||
|
"ok": False
|
||||||
|
}, start_time=start_time, code=400)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/update", tags=[""])
|
||||||
|
async def update(params: dict):
|
||||||
|
start_time = time()
|
||||||
|
data = params["data"]
|
||||||
|
|
||||||
|
logger.json(data)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await mongo.template_attributes_collection.update_one({
|
||||||
|
"id": data["id"]
|
||||||
|
}, {
|
||||||
|
"$set": data
|
||||||
|
})
|
||||||
|
|
||||||
|
return response({
|
||||||
|
"message": "Данные атрибута обновлены",
|
||||||
|
"ok": True
|
||||||
|
}, start_time=start_time)
|
||||||
|
except Exception as e:
|
||||||
|
return response({
|
||||||
|
"message": str(e),
|
||||||
|
"ok": False
|
||||||
|
}, start_time=start_time, code=400)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/delete", tags=[""])
|
||||||
|
async def delete(params: dict):
|
||||||
|
start_time = time()
|
||||||
|
attribute_id = params["attributeId"]
|
||||||
|
|
||||||
|
logger.json(attribute_id)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await mongo.template_attributes_collection.delete_one({
|
||||||
|
"id": attribute_id
|
||||||
|
})
|
||||||
|
|
||||||
|
return response({
|
||||||
|
"message": "Атрибут удален",
|
||||||
|
"ok": True
|
||||||
|
}, start_time=start_time)
|
||||||
|
except Exception as e:
|
||||||
|
return response({
|
||||||
|
"message": str(e),
|
||||||
|
"ok": False
|
||||||
|
}, start_time=start_time, code=400)
|
||||||
93
app/api/v1/barcode/template/size.py
Normal file
93
app/api/v1/barcode/template/size.py
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import json
|
||||||
|
from time import time
|
||||||
|
from fastapi import APIRouter
|
||||||
|
from app.config import config
|
||||||
|
from app.utils.logger_util import logger
|
||||||
|
from app.utils.response_util import response
|
||||||
|
from app import mongo
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/get-all", tags=[""])
|
||||||
|
async def get_all():
|
||||||
|
start_time = time()
|
||||||
|
|
||||||
|
sizes = await mongo.template_sizes_collection.find({}, {
|
||||||
|
"_id": False
|
||||||
|
}).sort("id", mongo.asc).to_list()
|
||||||
|
|
||||||
|
return response({
|
||||||
|
"sizes": sizes
|
||||||
|
}, start_time=start_time)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/create", tags=[""])
|
||||||
|
async def create(params: dict):
|
||||||
|
start_time = time()
|
||||||
|
data = params["size"]
|
||||||
|
|
||||||
|
data["id"] = await mongo.get_next_id(mongo.template_sizes_collection)
|
||||||
|
|
||||||
|
logger.json(data)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await mongo.template_sizes_collection.insert_one(data)
|
||||||
|
|
||||||
|
return response({
|
||||||
|
"message": "Размер создан",
|
||||||
|
"ok": True
|
||||||
|
}, start_time=start_time)
|
||||||
|
except Exception as e:
|
||||||
|
return response({
|
||||||
|
"message": str(e),
|
||||||
|
"ok": False
|
||||||
|
}, start_time=start_time, code=400)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/update", tags=[""])
|
||||||
|
async def update(params: dict):
|
||||||
|
start_time = time()
|
||||||
|
data = params["data"]
|
||||||
|
|
||||||
|
logger.json(data)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await mongo.template_sizes_collection.update_one({
|
||||||
|
"id": data["id"]
|
||||||
|
}, {
|
||||||
|
"$set": data
|
||||||
|
})
|
||||||
|
|
||||||
|
return response({
|
||||||
|
"message": "Данные размера обновлены",
|
||||||
|
"ok": True
|
||||||
|
}, start_time=start_time)
|
||||||
|
except Exception as e:
|
||||||
|
return response({
|
||||||
|
"message": str(e),
|
||||||
|
"ok": False
|
||||||
|
}, start_time=start_time, code=400)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/delete", tags=[""])
|
||||||
|
async def delete(params: dict):
|
||||||
|
start_time = time()
|
||||||
|
size_id = params["templateId"]
|
||||||
|
|
||||||
|
logger.json(size_id)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await mongo.template_sizes_collection.delete_one({
|
||||||
|
"id": size_id
|
||||||
|
})
|
||||||
|
|
||||||
|
return response({
|
||||||
|
"message": "Размер удален",
|
||||||
|
"ok": True
|
||||||
|
}, start_time=start_time)
|
||||||
|
except Exception as e:
|
||||||
|
return response({
|
||||||
|
"message": str(e),
|
||||||
|
"ok": False
|
||||||
|
}, start_time=start_time, code=400)
|
||||||
91
app/api/v1/barcode/template/template.py
Normal file
91
app/api/v1/barcode/template/template.py
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import json
|
||||||
|
from time import time
|
||||||
|
from fastapi import APIRouter
|
||||||
|
from app.config import config
|
||||||
|
from app.utils.logger_util import logger
|
||||||
|
from app.utils.response_util import response
|
||||||
|
from app import mongo
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/get-all", tags=[""])
|
||||||
|
async def get_all():
|
||||||
|
start_time = time()
|
||||||
|
|
||||||
|
templates = await mongo.templates_collection.find({}, {
|
||||||
|
"_id": False
|
||||||
|
}).sort("id", mongo.desc).to_list()
|
||||||
|
|
||||||
|
return response({
|
||||||
|
"templates": templates
|
||||||
|
}, start_time=start_time)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/create", tags=[""])
|
||||||
|
async def create(data: dict):
|
||||||
|
start_time = time()
|
||||||
|
|
||||||
|
data["id"] = await mongo.get_next_id(mongo.templates_collection)
|
||||||
|
|
||||||
|
logger.json(data)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await mongo.templates_collection.insert_one(data)
|
||||||
|
|
||||||
|
return response({
|
||||||
|
"message": "Шаблон создан",
|
||||||
|
"ok": True
|
||||||
|
}, start_time=start_time)
|
||||||
|
except Exception as e:
|
||||||
|
return response({
|
||||||
|
"message": str(e),
|
||||||
|
"ok": False
|
||||||
|
}, start_time=start_time, code=400)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/update", tags=[""])
|
||||||
|
async def update(data: dict):
|
||||||
|
start_time = time()
|
||||||
|
|
||||||
|
logger.json(data)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await mongo.templates_collection.update_one({
|
||||||
|
"id": data["id"]
|
||||||
|
}, {
|
||||||
|
"$set": data
|
||||||
|
})
|
||||||
|
|
||||||
|
return response({
|
||||||
|
"message": "Данные шаблона обновлены",
|
||||||
|
"ok": True
|
||||||
|
}, start_time=start_time)
|
||||||
|
except Exception as e:
|
||||||
|
return response({
|
||||||
|
"message": str(e),
|
||||||
|
"ok": False
|
||||||
|
}, start_time=start_time, code=400)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/delete", tags=[""])
|
||||||
|
async def delete(params: dict):
|
||||||
|
start_time = time()
|
||||||
|
id = params["id"]
|
||||||
|
|
||||||
|
logger.json(id)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await mongo.templates_collection.delete_one({
|
||||||
|
"id": id
|
||||||
|
})
|
||||||
|
|
||||||
|
return response({
|
||||||
|
"message": "Шаблон удален",
|
||||||
|
"ok": True
|
||||||
|
}, start_time=start_time)
|
||||||
|
except Exception as e:
|
||||||
|
return response({
|
||||||
|
"message": str(e),
|
||||||
|
"ok": False
|
||||||
|
}, start_time=start_time, code=400)
|
||||||
69
app/api/v1/billing.py
Normal file
69
app/api/v1/billing.py
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
from time import time
|
||||||
|
|
||||||
|
from aiohttp import ClientResponseError
|
||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
from app import mongo
|
||||||
|
from app.providers import tinkoff
|
||||||
|
from app.utils.response_util import response
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/create-deal-bill", tags=[""])
|
||||||
|
async def create_deal_bill(params: dict):
|
||||||
|
start_time = time()
|
||||||
|
deal_id = params["dealId"]
|
||||||
|
|
||||||
|
deal = await mongo.deals_collection.find_one({
|
||||||
|
"id": deal_id
|
||||||
|
}, {
|
||||||
|
"_id": False
|
||||||
|
})
|
||||||
|
|
||||||
|
client = await mongo.clients_collection.find_one({
|
||||||
|
"name": deal["clientName"]
|
||||||
|
}, {
|
||||||
|
"_id": False
|
||||||
|
})
|
||||||
|
|
||||||
|
try:
|
||||||
|
total_price = await mongo.get_deals_total_prices(deal)
|
||||||
|
bill_data = await tinkoff.create_bill(deal_id, total_price[deal_id], client)
|
||||||
|
|
||||||
|
bill_request_data = {
|
||||||
|
"invoiceId": bill_data["invoiceId"],
|
||||||
|
"pdfUrl": bill_data["pdfUrl"],
|
||||||
|
"paid": False
|
||||||
|
}
|
||||||
|
|
||||||
|
await mongo.deals_collection.update_one(
|
||||||
|
{"id": deal_id},
|
||||||
|
{"$set": {"billRequest": bill_request_data}}
|
||||||
|
)
|
||||||
|
|
||||||
|
return response({
|
||||||
|
"ok": True,
|
||||||
|
"message": "Счёт успешно создан"
|
||||||
|
}, start_time=start_time)
|
||||||
|
except ClientResponseError:
|
||||||
|
return response({
|
||||||
|
"ok": False,
|
||||||
|
"message": "Не получилось создать счёт"
|
||||||
|
}, start_time=start_time)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/cancel-deal-bill", tags=[""])
|
||||||
|
async def cancel_deal_bill(params: dict):
|
||||||
|
start_time = time()
|
||||||
|
deal_id = params["dealId"]
|
||||||
|
|
||||||
|
await mongo.deals_collection.update_one(
|
||||||
|
{"id": deal_id},
|
||||||
|
{"$set": {"billRequest": None}}
|
||||||
|
)
|
||||||
|
|
||||||
|
return response({
|
||||||
|
"ok": True,
|
||||||
|
"message": "Счёт успешно отозван"
|
||||||
|
}, start_time=start_time)
|
||||||
82
app/api/v1/board.py
Normal file
82
app/api/v1/board.py
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
from time import time
|
||||||
|
|
||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
from app import mongo
|
||||||
|
from app.utils.response_util import response
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{board_id}", tags=[""])
|
||||||
|
async def get(board_id: int):
|
||||||
|
start_time = time()
|
||||||
|
|
||||||
|
boards = await mongo.boards_collection.find({
|
||||||
|
"projectId": board_id
|
||||||
|
}, {
|
||||||
|
"_id": False
|
||||||
|
}).to_list()
|
||||||
|
|
||||||
|
for board in boards:
|
||||||
|
board["project"] = await mongo.projects_collection.find({
|
||||||
|
"id": board["projectId"]
|
||||||
|
}, {
|
||||||
|
"_id": False
|
||||||
|
}).to_list()
|
||||||
|
|
||||||
|
board["dealStatuses"] = await mongo.statuses_collection.find({
|
||||||
|
"boardId": board["id"]
|
||||||
|
}, {
|
||||||
|
"_id": False
|
||||||
|
}).to_list()
|
||||||
|
|
||||||
|
return response({
|
||||||
|
"boards": boards
|
||||||
|
}, start_time=start_time)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/", tags=[""])
|
||||||
|
async def post(params: dict):
|
||||||
|
start_time = time()
|
||||||
|
data = params["board"]
|
||||||
|
|
||||||
|
data["id"] = await mongo.get_next_id(mongo.boards_collection)
|
||||||
|
|
||||||
|
await mongo.boards_collection.insert_one(data)
|
||||||
|
|
||||||
|
return response({
|
||||||
|
"message": "Доска создана",
|
||||||
|
"ok": True
|
||||||
|
}, start_time=start_time)
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch("/", tags=[""])
|
||||||
|
async def patch(params: dict):
|
||||||
|
start_time = time()
|
||||||
|
data = params["board"]
|
||||||
|
|
||||||
|
await mongo.boards_collection.update_one({
|
||||||
|
"id": data["id"]
|
||||||
|
}, {
|
||||||
|
"$set": data
|
||||||
|
})
|
||||||
|
|
||||||
|
return response({
|
||||||
|
"message": "Доска обновлена",
|
||||||
|
"ok": True
|
||||||
|
}, start_time=start_time)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{board_id}", tags=[""])
|
||||||
|
async def delete(board_id: int):
|
||||||
|
start_time = time()
|
||||||
|
|
||||||
|
await mongo.boards_collection.delete_one({
|
||||||
|
"id": board_id
|
||||||
|
})
|
||||||
|
|
||||||
|
return response({
|
||||||
|
"message": "Доска удален",
|
||||||
|
"ok": True
|
||||||
|
}, start_time=start_time)
|
||||||
143
app/api/v1/client.py
Normal file
143
app/api/v1/client.py
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
from time import time
|
||||||
|
|
||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
from app import mongo
|
||||||
|
from app.utils.logger_util import logger
|
||||||
|
from app.utils.response_util import response
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/get-all", tags=[""])
|
||||||
|
async def get_all():
|
||||||
|
start_time = time()
|
||||||
|
|
||||||
|
clients = await mongo.clients_collection.find({}, {
|
||||||
|
"_id": False
|
||||||
|
}).sort("id", mongo.desc).to_list()
|
||||||
|
|
||||||
|
return response({
|
||||||
|
"clients": clients
|
||||||
|
}, start_time=start_time)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/search", tags=[""])
|
||||||
|
async def search(name: str):
|
||||||
|
start_time = time()
|
||||||
|
|
||||||
|
clients = await mongo.clients_collection.find({
|
||||||
|
"name": {
|
||||||
|
"$regex": name
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
"_id": False
|
||||||
|
}).sort("id", mongo.asc).to_list()
|
||||||
|
|
||||||
|
return response({
|
||||||
|
"clients": clients
|
||||||
|
}, start_time=start_time)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/get/{client_id}", tags=[""])
|
||||||
|
async def get(client_id: int):
|
||||||
|
start_time = time()
|
||||||
|
|
||||||
|
client = await mongo.clients_collection.find_one({
|
||||||
|
"id": client_id
|
||||||
|
}, {
|
||||||
|
"_id": False
|
||||||
|
})
|
||||||
|
|
||||||
|
boxes = await mongo.client_boxes_collection.find({
|
||||||
|
"clientId": client_id
|
||||||
|
}, {
|
||||||
|
"_id": False
|
||||||
|
}).to_list()
|
||||||
|
client["boxes"] = boxes
|
||||||
|
|
||||||
|
pallets = await mongo.client_pallets_collection.find({
|
||||||
|
"clientId": client_id
|
||||||
|
}, {
|
||||||
|
"_id": False
|
||||||
|
}).to_list()
|
||||||
|
|
||||||
|
for pallet in pallets:
|
||||||
|
pallet["boxes"] = await mongo.client_boxes_collection.find({
|
||||||
|
"palletId": pallet["id"]
|
||||||
|
}, {
|
||||||
|
"_id": False
|
||||||
|
}).to_list()
|
||||||
|
|
||||||
|
client["pallets"] = pallets
|
||||||
|
|
||||||
|
return response({
|
||||||
|
"client": client
|
||||||
|
}, start_time=start_time)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/create", tags=[""])
|
||||||
|
async def create(params: dict):
|
||||||
|
start_time = time()
|
||||||
|
data = params["data"]
|
||||||
|
|
||||||
|
existing_client = await mongo.clients_collection.find_one({"name": data["name"]})
|
||||||
|
if existing_client:
|
||||||
|
return response({
|
||||||
|
"message": f"Клиент с именем '{data['name']}' уже существует",
|
||||||
|
"ok": False
|
||||||
|
}, start_time=start_time)
|
||||||
|
|
||||||
|
data["id"] = await mongo.get_next_id(mongo.clients_collection)
|
||||||
|
await mongo.clients_collection.insert_one(data)
|
||||||
|
|
||||||
|
return response({
|
||||||
|
"message": "Клиент создан",
|
||||||
|
"ok": True
|
||||||
|
}, start_time=start_time)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/update", tags=[""])
|
||||||
|
async def update(params: dict):
|
||||||
|
start_time = time()
|
||||||
|
data = params["data"]
|
||||||
|
|
||||||
|
logger.json(data)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await mongo.clients_collection.update_one({
|
||||||
|
"id": data["id"]
|
||||||
|
}, {
|
||||||
|
"$set": data
|
||||||
|
})
|
||||||
|
|
||||||
|
return response({
|
||||||
|
"message": "Данные клиента обновлены",
|
||||||
|
"ok": True
|
||||||
|
}, start_time=start_time)
|
||||||
|
except Exception as e:
|
||||||
|
return response({
|
||||||
|
"message": str(e),
|
||||||
|
"ok": False
|
||||||
|
}, start_time=start_time, code=400)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/delete", tags=[""])
|
||||||
|
async def delete(params: dict):
|
||||||
|
start_time = time()
|
||||||
|
client_id = params["clientId"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
await mongo.clients_collection.delete_one({
|
||||||
|
"id": client_id
|
||||||
|
})
|
||||||
|
|
||||||
|
return response({
|
||||||
|
"message": "Клиент удален",
|
||||||
|
"ok": True
|
||||||
|
}, start_time=start_time)
|
||||||
|
except Exception as e:
|
||||||
|
return response({
|
||||||
|
"message": str(e),
|
||||||
|
"ok": False
|
||||||
|
}, start_time=start_time, code=400)
|
||||||
12
app/api/v1/deal/__init__.py
Normal file
12
app/api/v1/deal/__init__.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
from app.api.v1.deal import deal, summaries, employee, services, product, products
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
router.include_router(deal.router)
|
||||||
|
router.include_router(summaries.router, prefix='/summaries')
|
||||||
|
router.include_router(employee.router, prefix='/employee')
|
||||||
|
router.include_router(services.router, prefix='/services')
|
||||||
|
router.include_router(product.router, prefix='/product')
|
||||||
|
router.include_router(products.router, prefix='/products')
|
||||||
BIN
app/api/v1/deal/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
app/api/v1/deal/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/api/v1/deal/__pycache__/deal.cpython-311.pyc
Normal file
BIN
app/api/v1/deal/__pycache__/deal.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/api/v1/deal/__pycache__/employee.cpython-311.pyc
Normal file
BIN
app/api/v1/deal/__pycache__/employee.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/api/v1/deal/__pycache__/product.cpython-311.pyc
Normal file
BIN
app/api/v1/deal/__pycache__/product.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/api/v1/deal/__pycache__/products.cpython-311.pyc
Normal file
BIN
app/api/v1/deal/__pycache__/products.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/api/v1/deal/__pycache__/services.cpython-311.pyc
Normal file
BIN
app/api/v1/deal/__pycache__/services.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/api/v1/deal/__pycache__/summaries.cpython-311.pyc
Normal file
BIN
app/api/v1/deal/__pycache__/summaries.cpython-311.pyc
Normal file
Binary file not shown.
422
app/api/v1/deal/deal.py
Normal file
422
app/api/v1/deal/deal.py
Normal file
@ -0,0 +1,422 @@
|
|||||||
|
import base64
|
||||||
|
from datetime import datetime
|
||||||
|
from io import BytesIO
|
||||||
|
from time import time
|
||||||
|
|
||||||
|
import barcode
|
||||||
|
from barcode.writer import ImageWriter
|
||||||
|
from fastapi import APIRouter
|
||||||
|
from fastapi.responses import Response
|
||||||
|
from num2words import num2words
|
||||||
|
from reportlab.lib import colors
|
||||||
|
from reportlab.lib.pagesizes import A4
|
||||||
|
from reportlab.lib.styles import ParagraphStyle
|
||||||
|
from reportlab.pdfbase import pdfmetrics
|
||||||
|
from reportlab.pdfbase.ttfonts import TTFont
|
||||||
|
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, Image
|
||||||
|
from starlette.requests import Request
|
||||||
|
from starlette.templating import Jinja2Templates
|
||||||
|
|
||||||
|
from app import mongo
|
||||||
|
from app.utils.response_util import response
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
templates = Jinja2Templates(directory="assets/templates")
|
||||||
|
|
||||||
|
MONTHS_RU = {
|
||||||
|
1: "января",
|
||||||
|
2: "февраля",
|
||||||
|
3: "марта",
|
||||||
|
4: "апреля",
|
||||||
|
5: "мая",
|
||||||
|
6: "июня",
|
||||||
|
7: "июля",
|
||||||
|
8: "августа",
|
||||||
|
9: "сентября",
|
||||||
|
10: "октября",
|
||||||
|
11: "ноября",
|
||||||
|
12: "декабря",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def format_date_russian(date: datetime) -> str:
|
||||||
|
return f"{date.day} {MONTHS_RU[date.month]} {date.year} г."
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/quickCreate", tags=[""])
|
||||||
|
async def quick_create_deal(data: dict):
|
||||||
|
start_time = time()
|
||||||
|
|
||||||
|
data["id"] = await mongo.get_next_id(mongo.deals_collection)
|
||||||
|
data["createdAt"] = mongo.created_at()
|
||||||
|
|
||||||
|
status = await mongo.statuses_collection.find_one({"id": data["statusId"]}, {"_id": False})
|
||||||
|
if not status:
|
||||||
|
return response({"message": "Статус не найден", "ok": False}, start_time=start_time)
|
||||||
|
|
||||||
|
board = await mongo.boards_collection.find_one({"id": status["boardId"]}, {"_id": False})
|
||||||
|
if not board:
|
||||||
|
return response({"message": "Доска не найдена", "ok": False}, start_time=start_time)
|
||||||
|
|
||||||
|
data["boardId"] = status["boardId"]
|
||||||
|
data["projectId"] = board["projectId"]
|
||||||
|
|
||||||
|
await mongo.deals_collection.insert_one(data)
|
||||||
|
return response({
|
||||||
|
"message": "Сделка создана",
|
||||||
|
"ok": True,
|
||||||
|
"dealId": data["id"]
|
||||||
|
}, start_time=start_time)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/get/{deal_id}", tags=[""])
|
||||||
|
async def get_deal(deal_id: int):
|
||||||
|
start_time = time()
|
||||||
|
|
||||||
|
deal = await mongo.deals_collection.find_one({
|
||||||
|
"id": deal_id
|
||||||
|
}, {
|
||||||
|
"_id": False
|
||||||
|
})
|
||||||
|
|
||||||
|
deals = await mongo.additional_deals_data(deal, full=True)
|
||||||
|
return response(deals[0], start_time=start_time)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/update-general-info", tags=[""])
|
||||||
|
async def update_deal_general_info(params: dict, request: Request):
|
||||||
|
start_time = time()
|
||||||
|
data = params["data"]
|
||||||
|
deal_id = params["dealId"]
|
||||||
|
|
||||||
|
deal = await mongo.deals_collection.find_one({"id": deal_id}, {"_id": False})
|
||||||
|
if not deal:
|
||||||
|
return response({"message": "Сделка не найдена", "ok": False}, start_time=start_time)
|
||||||
|
|
||||||
|
new_status = await mongo.statuses_collection.find_one({"id": data["statusId"]}, {"_id": False})
|
||||||
|
if new_status and new_status["name"] == "Завершено":
|
||||||
|
data["completedAt"] = mongo.created_at()
|
||||||
|
|
||||||
|
old_status_id = deal.get("statusId")
|
||||||
|
await mongo.deals_collection.update_one({
|
||||||
|
"id": deal_id
|
||||||
|
}, {
|
||||||
|
"$set": data
|
||||||
|
})
|
||||||
|
|
||||||
|
if old_status_id != data["statusId"]:
|
||||||
|
new_entry = {
|
||||||
|
"changedAt": mongo.created_at(),
|
||||||
|
"fromStatusId": old_status_id,
|
||||||
|
"toStatusId": data["statusId"],
|
||||||
|
"userId": request.state.user["id"],
|
||||||
|
"comment": ""
|
||||||
|
}
|
||||||
|
|
||||||
|
await mongo.deals_collection.update_one(
|
||||||
|
{"id": deal_id},
|
||||||
|
{"$push": {"statusHistory": new_entry}}
|
||||||
|
)
|
||||||
|
|
||||||
|
return response({
|
||||||
|
"message": "Данные сделки обновлены",
|
||||||
|
"ok": True
|
||||||
|
}, start_time=start_time)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/delete", tags=[""])
|
||||||
|
async def delete_deal(params: dict):
|
||||||
|
start_time = time()
|
||||||
|
deal_id = params["dealId"]
|
||||||
|
|
||||||
|
await mongo.deals_collection.delete_one({"id": deal_id})
|
||||||
|
return response({
|
||||||
|
"message": "Сделка удалена",
|
||||||
|
"ok": True
|
||||||
|
}, start_time=start_time)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/complete", tags=[""])
|
||||||
|
async def complete_deal(params: dict):
|
||||||
|
start_time = time()
|
||||||
|
deal_id = params["dealId"]
|
||||||
|
|
||||||
|
await mongo.deals_collection.update_one(
|
||||||
|
{"id": deal_id},
|
||||||
|
{"$set": {"isCompleted": True}}
|
||||||
|
)
|
||||||
|
|
||||||
|
return response({
|
||||||
|
"message": "Сделка завершена",
|
||||||
|
"ok": True
|
||||||
|
}, start_time=start_time)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/prefill", tags=[""])
|
||||||
|
async def prefill_deal(params: dict):
|
||||||
|
start_time = time()
|
||||||
|
old_deal_id = params["oldDealId"]
|
||||||
|
new_deal_id = params["newDealId"]
|
||||||
|
|
||||||
|
old_deal = await mongo.deals_collection.find_one({"id": old_deal_id}, {"_id": False})
|
||||||
|
if not old_deal:
|
||||||
|
return response({"message": "Сделка не найдена", "ok": False}, start_time=start_time)
|
||||||
|
|
||||||
|
await mongo.deals_collection.update_one(
|
||||||
|
{"id": new_deal_id},
|
||||||
|
{"$set": {
|
||||||
|
"name": old_deal["name"],
|
||||||
|
"clientName": old_deal["clientName"],
|
||||||
|
"services": old_deal.get("services", []),
|
||||||
|
"products": old_deal.get("products", [])
|
||||||
|
}}
|
||||||
|
)
|
||||||
|
|
||||||
|
return response({
|
||||||
|
"message": "Сделка предзаполнена",
|
||||||
|
"ok": True
|
||||||
|
}, start_time=start_time)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/billing-document/{deal_id}", tags=[""])
|
||||||
|
async def deal_billing_document(deal_id: int):
|
||||||
|
pdfmetrics.registerFont(TTFont("Arial", "assets/arial.ttf"))
|
||||||
|
pdfmetrics.registerFont(TTFont("Arial Bold", "assets/arial_bold.ttf"))
|
||||||
|
|
||||||
|
center = ParagraphStyle(name="center", fontName="Arial", fontSize=10, alignment=1)
|
||||||
|
bold_center = ParagraphStyle(name="bold_center", fontName="Arial Bold", fontSize=10, alignment=1)
|
||||||
|
title = ParagraphStyle(name="title", fontName="Arial Bold", fontSize=14, alignment=1)
|
||||||
|
|
||||||
|
deal = await mongo.deals_collection.find_one({"id": deal_id}, {"_id": False})
|
||||||
|
buffer = BytesIO()
|
||||||
|
doc = SimpleDocTemplate(buffer, pagesize=A4, leftMargin=20, rightMargin=20, topMargin=30, bottomMargin=20)
|
||||||
|
elements = []
|
||||||
|
|
||||||
|
logo = Image("assets/logo.jpg", width=152, height=56)
|
||||||
|
logo.hAlign = "LEFT"
|
||||||
|
elements.append(logo)
|
||||||
|
elements.append(Spacer(1, 10))
|
||||||
|
|
||||||
|
elements.append(Paragraph("Расчет стоимости услуг", title))
|
||||||
|
elements.append(Spacer(1, 6))
|
||||||
|
elements.append(Paragraph(f"№ {deal['id']} от {format_date_russian(datetime.now())}", center))
|
||||||
|
elements.append(Paragraph("Адрес: 115516, г.Москва, ул. Промышленная 11", center))
|
||||||
|
elements.append(Paragraph(f"Клиент: {deal.get('clientName', 'Не указан')}", center))
|
||||||
|
elements.append(Paragraph(f"Маркетплейс: {deal.get('baseMarketplace', {}).get('name', 'Не указан')}", center))
|
||||||
|
elements.append(Paragraph(f"Склад отгрузки: {deal.get('shippingWarehouse', 'Не указан')}", center))
|
||||||
|
elements.append(Spacer(1, 10))
|
||||||
|
|
||||||
|
total_sum = 0
|
||||||
|
product_infos = []
|
||||||
|
table_width = 450
|
||||||
|
|
||||||
|
data_1 = [[
|
||||||
|
Paragraph("Артикул ВБ", bold_center),
|
||||||
|
Paragraph("Артикул продавца", bold_center),
|
||||||
|
Paragraph("Размер", bold_center),
|
||||||
|
Paragraph("Кол-во", bold_center),
|
||||||
|
Paragraph("Цена", bold_center),
|
||||||
|
Paragraph("Сумма", bold_center),
|
||||||
|
]]
|
||||||
|
|
||||||
|
for product in deal.get("products", []):
|
||||||
|
product_data = await mongo.products_collection.find_one({"id": product["productId"]}, {"_id": False})
|
||||||
|
if not product_data:
|
||||||
|
continue
|
||||||
|
|
||||||
|
article = product_data.get("article", "-")
|
||||||
|
vendor_code = product_data.get("vendorCode", "-")
|
||||||
|
size = product_data.get("size", "-")
|
||||||
|
quantity = product["quantity"]
|
||||||
|
price = sum(service["price"] * service.get("quantity", 1) for service in product["services"])
|
||||||
|
amount = quantity * price
|
||||||
|
total_sum += amount
|
||||||
|
|
||||||
|
data_1.append([
|
||||||
|
Paragraph(str(article), center),
|
||||||
|
Paragraph(str(vendor_code), center),
|
||||||
|
Paragraph(str(size or 0), center),
|
||||||
|
Paragraph(str(quantity or 1), center),
|
||||||
|
Paragraph(f"{price} ₽", center),
|
||||||
|
Paragraph(f"{amount} ₽", center),
|
||||||
|
])
|
||||||
|
|
||||||
|
product_infos.append({
|
||||||
|
"name": product_data.get("name") or "Без названия",
|
||||||
|
"quantity": product["quantity"],
|
||||||
|
"services": await get_services(product.get("services", []))
|
||||||
|
})
|
||||||
|
|
||||||
|
table_1 = Table(data_1, colWidths=[table_width / 6] * 5)
|
||||||
|
table_1.setStyle(TableStyle([
|
||||||
|
("BOX", (0, 0), (-1, -1), 0.8, colors.black),
|
||||||
|
("INNERGRID", (0, 0), (-1, -1), 0.4, colors.grey),
|
||||||
|
("BACKGROUND", (0, 0), (-1, 0), colors.lightgrey),
|
||||||
|
("ALIGN", (0, 0), (-1, -1), "CENTER"),
|
||||||
|
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
|
||||||
|
("ROUNDEDCORNERS", (16, 16, 16, 16)),
|
||||||
|
]))
|
||||||
|
|
||||||
|
elements.append(Paragraph("Товары", bold_center))
|
||||||
|
elements.append(Spacer(1, 4))
|
||||||
|
elements.append(table_1)
|
||||||
|
elements.append(Spacer(1, 12))
|
||||||
|
|
||||||
|
data_2 = [[
|
||||||
|
Paragraph("Товар / Услуга", bold_center),
|
||||||
|
Paragraph("Кол-во", bold_center),
|
||||||
|
Paragraph("Цена", bold_center),
|
||||||
|
Paragraph("Сумма", bold_center),
|
||||||
|
]]
|
||||||
|
|
||||||
|
for product in product_infos:
|
||||||
|
for service in product["services"]:
|
||||||
|
quantity = product.get("quantity", 0)
|
||||||
|
price = service.get("price", 0)
|
||||||
|
amount = quantity * price
|
||||||
|
data_2.append([
|
||||||
|
Paragraph(f"{product['name']} {service['name']}", center),
|
||||||
|
Paragraph(str(quantity), center),
|
||||||
|
Paragraph(f"{price} ₽", center),
|
||||||
|
Paragraph(f"{amount} ₽", center),
|
||||||
|
])
|
||||||
|
|
||||||
|
for service in await get_services(deal.get("services", [])):
|
||||||
|
quantity = service.get("quantity", 0)
|
||||||
|
price = service.get("price", 0)
|
||||||
|
amount = quantity * price
|
||||||
|
total_sum += amount
|
||||||
|
data_2.append([
|
||||||
|
Paragraph(service['name'], center),
|
||||||
|
Paragraph(str(quantity), center),
|
||||||
|
Paragraph(f"{price} ₽", center),
|
||||||
|
Paragraph(f"{amount} ₽", center)
|
||||||
|
])
|
||||||
|
|
||||||
|
table_2 = Table(data_2, colWidths=[
|
||||||
|
table_width * 0.4, table_width * 0.2, table_width * 0.2, table_width * 0.2
|
||||||
|
])
|
||||||
|
|
||||||
|
table_2.setStyle(TableStyle([
|
||||||
|
("BOX", (0, 0), (-1, -1), 0.8, colors.black),
|
||||||
|
("INNERGRID", (0, 0), (-1, -1), 0.4, colors.grey),
|
||||||
|
("BACKGROUND", (0, 0), (-1, 0), colors.lightgrey),
|
||||||
|
("ALIGN", (0, 0), (-1, -1), "CENTER"),
|
||||||
|
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
|
||||||
|
("ROUNDEDCORNERS", (16, 16, 16, 16)),
|
||||||
|
]))
|
||||||
|
|
||||||
|
elements.append(Paragraph("Услуги", bold_center))
|
||||||
|
elements.append(Spacer(1, 4))
|
||||||
|
elements.append(table_2)
|
||||||
|
elements.append(Spacer(1, 12))
|
||||||
|
elements.append(Paragraph(f"+79013618377 Ангелина К. (Т-Банк) Итого к оплате: {total_sum:,.0f} ₽".replace(",", " "), bold_center))
|
||||||
|
elements.append(Paragraph(num2words(total_sum, lang='ru').capitalize() + " рублей", center))
|
||||||
|
|
||||||
|
doc.build(elements)
|
||||||
|
buffer.seek(0)
|
||||||
|
return Response(buffer.getvalue(), media_type="application/pdf", headers={"Content-Disposition": "inline; filename=billing_document.pdf"})
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/tech-spec/{deal_id}", tags=[""])
|
||||||
|
async def deal_tech_spec(request: Request, deal_id: int):
|
||||||
|
deal = await mongo.deals_collection.find_one({"id": deal_id}, {"_id": False})
|
||||||
|
|
||||||
|
status = await mongo.statuses_collection.find_one({"id": deal["statusId"]}, {"_id": False})
|
||||||
|
deal_status = status.get("name", "Статус не определен") if status else "Статус не найден"
|
||||||
|
|
||||||
|
deal_services = await get_services(deal.get("services", []))
|
||||||
|
deal_data = {
|
||||||
|
"id": deal["id"],
|
||||||
|
"name": deal.get("name") or "Без названия",
|
||||||
|
"status": deal_status,
|
||||||
|
"createdAt": datetime.strptime(deal["createdAt"], "%Y-%m-%dT%H:%M:%S").strftime("%d.%m.%Y, %H:%M"),
|
||||||
|
"clientName": deal.get("clientName") or "Не указан",
|
||||||
|
"deliveryDate": (
|
||||||
|
datetime.strptime(deal["deliveryDate"], "%Y-%m-%dT%H:%M:%S.%f").strftime("%d.%m.%Y")
|
||||||
|
if deal.get("deliveryDate") else "Не указана"
|
||||||
|
),
|
||||||
|
"baseMarketplace": deal.get("baseMarketplace", {}).get("name") or "Не указан",
|
||||||
|
"shippingWarehouse": deal.get("shippingWarehouse") or "Не указан",
|
||||||
|
"products": [],
|
||||||
|
"services": deal_services
|
||||||
|
}
|
||||||
|
|
||||||
|
for product in deal.get("products", []):
|
||||||
|
product_data = await mongo.products_collection.find_one({"id": product["productId"]}, {"_id": False})
|
||||||
|
if not product_data:
|
||||||
|
continue
|
||||||
|
|
||||||
|
barcode_value = product_data.get("barcodes", [None])[0]
|
||||||
|
barcode_image = generate_barcode_base64(barcode_value) if barcode_value else None
|
||||||
|
|
||||||
|
product_services = await get_services(product.get("services", []))
|
||||||
|
deal_data["products"].append({
|
||||||
|
"name": product_data.get("name") or "Без названия",
|
||||||
|
"article": product_data.get("vendorCode") or "Нет артикула",
|
||||||
|
"imageUrl": product_data.get("imageUrl"),
|
||||||
|
"size": product_data.get("size") or 0,
|
||||||
|
"quantity": product.get("quantity") or 0,
|
||||||
|
"barcode": barcode_image,
|
||||||
|
"services": product_services,
|
||||||
|
"comment": product.get("comment")
|
||||||
|
})
|
||||||
|
|
||||||
|
return templates.TemplateResponse("deal_tech_spec.html", {"request": request, "deal": deal_data})
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/add-kit")
|
||||||
|
async def add_kits(params: dict):
|
||||||
|
start_time = time()
|
||||||
|
deal_id = params["dealId"]
|
||||||
|
kit_id = params["kitId"]
|
||||||
|
|
||||||
|
deal = await mongo.deals_collection.find_one({"id": deal_id}, {"_id": False})
|
||||||
|
if not deal:
|
||||||
|
return response({"message": "Сделка не найдена", "ok": False}, start_time=start_time)
|
||||||
|
|
||||||
|
service_kit = await mongo.service_kits_collection.find_one({"id": kit_id})
|
||||||
|
if not service_kit:
|
||||||
|
return response({"message": "Комплект не найден", "ok": False}, start_time=start_time)
|
||||||
|
|
||||||
|
for service in service_kit.get("services", []):
|
||||||
|
price = service.get("price", 0)
|
||||||
|
if service.get("priceRanges"):
|
||||||
|
price_range = service["priceRanges"][0]
|
||||||
|
price = price_range.get("price", price)
|
||||||
|
|
||||||
|
deal["services"].append({
|
||||||
|
"serviceId": service["id"],
|
||||||
|
"price": price,
|
||||||
|
"isFixedPrice": False,
|
||||||
|
"quantity": 1
|
||||||
|
})
|
||||||
|
|
||||||
|
await mongo.deals_collection.update_one({"id": deal_id}, {"$set": {"services": deal["services"]}})
|
||||||
|
return response({
|
||||||
|
"message": "Сервисы из комплекта добавлены в сделку",
|
||||||
|
"ok": True
|
||||||
|
}, start_time=start_time)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_services(services):
|
||||||
|
service_list = []
|
||||||
|
for service in services:
|
||||||
|
service_data = await mongo.services_collection.find_one({"id": service["serviceId"]}, {"_id": False})
|
||||||
|
if service_data:
|
||||||
|
service_list.append({
|
||||||
|
"name": service_data.get("name") or "Неизвестная услуга",
|
||||||
|
"quantity": service.get("quantity") or 0,
|
||||||
|
"price": service.get("price") or 0
|
||||||
|
})
|
||||||
|
|
||||||
|
return service_list
|
||||||
|
|
||||||
|
|
||||||
|
def generate_barcode_base64(barcode_text: str) -> str:
|
||||||
|
buffer = BytesIO()
|
||||||
|
code128 = barcode.get("code128", barcode_text, writer=ImageWriter())
|
||||||
|
code128.write(buffer, {"module_height": 10.0, "font_size": 10, "quiet_zone": 2.0})
|
||||||
|
buffer.seek(0)
|
||||||
|
encoded = base64.b64encode(buffer.read()).decode("utf-8")
|
||||||
|
return f"data:image/png;base64,{encoded}"
|
||||||
51
app/api/v1/deal/employee.py
Normal file
51
app/api/v1/deal/employee.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
from time import time
|
||||||
|
|
||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
from app import mongo
|
||||||
|
from app.utils.response_util import response
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/available/{user_id}", tags=[""])
|
||||||
|
async def get(user_id: int):
|
||||||
|
start_time = time()
|
||||||
|
|
||||||
|
employees = await mongo.users_collection.find({}, {
|
||||||
|
"_id": False
|
||||||
|
}).sort("id", mongo.asc).to_list()
|
||||||
|
|
||||||
|
return response({
|
||||||
|
"employees": employees
|
||||||
|
}, start_time=start_time)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("", tags=[""])
|
||||||
|
async def add(params: dict):
|
||||||
|
start_time = time()
|
||||||
|
params["createdAt"] = mongo.created_at()
|
||||||
|
|
||||||
|
deal = await mongo.deals_collection.find_one(
|
||||||
|
{"id": params["dealId"]},
|
||||||
|
{"_id": False}
|
||||||
|
)
|
||||||
|
|
||||||
|
employees = deal.get("employees", [])
|
||||||
|
user_entry = {"userId": params["userId"], "createdAt": params["createdAt"]}
|
||||||
|
|
||||||
|
if params["isAssign"]:
|
||||||
|
if user_entry not in employees:
|
||||||
|
employees.append(user_entry)
|
||||||
|
else:
|
||||||
|
employees = [employee for employee in employees if employee["userId"] != params["userId"]]
|
||||||
|
|
||||||
|
await mongo.deals_collection.update_one(
|
||||||
|
{"id": params["dealId"]},
|
||||||
|
{"$set": {"employees": employees}}
|
||||||
|
)
|
||||||
|
|
||||||
|
return response({
|
||||||
|
"message": "Данные обновлены",
|
||||||
|
"ok": True
|
||||||
|
}, start_time=start_time)
|
||||||
82
app/api/v1/deal/product.py
Normal file
82
app/api/v1/deal/product.py
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
from time import time
|
||||||
|
|
||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
from app import mongo
|
||||||
|
from app.utils.response_util import response
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/update")
|
||||||
|
async def update_product(params: dict):
|
||||||
|
start_time = time()
|
||||||
|
|
||||||
|
deal_id = params["dealId"]
|
||||||
|
product_id = params["product"]["productId"]
|
||||||
|
quantity = params["product"]["quantity"]
|
||||||
|
services = params["product"]["services"] = [
|
||||||
|
{"serviceId": service["service"]["id"], "price": service["price"], "isFixedPrice": service.get("isFixedPrice", False), "quantity": 1}
|
||||||
|
for service in params["product"]["services"]
|
||||||
|
]
|
||||||
|
|
||||||
|
deal = await mongo.deals_collection.find_one({"id": deal_id})
|
||||||
|
if not deal:
|
||||||
|
return response({"message": "Сделка не найдена", "ok": False}, start_time=start_time)
|
||||||
|
|
||||||
|
products = deal.get("products", [])
|
||||||
|
for product in products:
|
||||||
|
if product["productId"] == product_id:
|
||||||
|
if "comment" in params["product"]:
|
||||||
|
product["comment"] = params["product"]["comment"]
|
||||||
|
|
||||||
|
product["quantity"] = quantity
|
||||||
|
product["services"] = services
|
||||||
|
break
|
||||||
|
|
||||||
|
await mongo.deals_collection.update_one({"id": deal_id}, {"$set": {"products": products}})
|
||||||
|
return response({
|
||||||
|
"message": "Товар обновлён в сделке",
|
||||||
|
"ok": True
|
||||||
|
}, start_time=start_time)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/add-kit")
|
||||||
|
async def add_kits(params: dict):
|
||||||
|
start_time = time()
|
||||||
|
deal_id = params["dealId"]
|
||||||
|
kit_id = params["kitId"]
|
||||||
|
product_id = params["productId"]
|
||||||
|
|
||||||
|
deal = await mongo.deals_collection.find_one({"id": deal_id})
|
||||||
|
if not deal:
|
||||||
|
return response({"message": "Сделка не найдена", "ok": False}, start_time=start_time)
|
||||||
|
|
||||||
|
service_kit = await mongo.service_kits_collection.find_one({"id": kit_id})
|
||||||
|
if not service_kit:
|
||||||
|
return response({"message": "Комплект не найден", "ok": False}, start_time=start_time)
|
||||||
|
|
||||||
|
kit_services = []
|
||||||
|
for service in service_kit.get("services", []):
|
||||||
|
price = service.get("price", 0)
|
||||||
|
if service.get("priceRanges"):
|
||||||
|
price_range = service["priceRanges"][0]
|
||||||
|
price = price_range.get("price", price)
|
||||||
|
|
||||||
|
kit_services.append({
|
||||||
|
"serviceId": service["id"],
|
||||||
|
"price": price,
|
||||||
|
"isFixedPrice": False,
|
||||||
|
"quantity": 1
|
||||||
|
})
|
||||||
|
|
||||||
|
for product in deal.get("products", []):
|
||||||
|
if product["productId"] == product_id:
|
||||||
|
product["services"].extend(kit_services)
|
||||||
|
break
|
||||||
|
|
||||||
|
await mongo.deals_collection.update_one({"id": deal_id}, {"$set": {"products": deal["products"]}})
|
||||||
|
return response({
|
||||||
|
"message": "Сервисы из комплекта добавлены в продукт сделки",
|
||||||
|
"ok": True
|
||||||
|
}, start_time=start_time)
|
||||||
57
app/api/v1/deal/products.py
Normal file
57
app/api/v1/deal/products.py
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
from time import time
|
||||||
|
|
||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
from app import mongo
|
||||||
|
from app.utils.response_util import response
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/add")
|
||||||
|
async def add_product(params: dict):
|
||||||
|
start_time = time()
|
||||||
|
|
||||||
|
deal_id = params["dealId"]
|
||||||
|
product_data = {
|
||||||
|
"productId": params["product"]["product"]["id"],
|
||||||
|
"quantity": params["product"]["quantity"],
|
||||||
|
"services": [
|
||||||
|
{"serviceId": service["service"]["id"], "price": service["price"], "isFixedPrice": service.get("isFixedPrice", False), "quantity": 1}
|
||||||
|
for service in params["product"]["services"]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
deal = await mongo.deals_collection.find_one({"id": deal_id})
|
||||||
|
if not deal:
|
||||||
|
return response({"message": "Сделка не найдена", "ok": False}, start_time=start_time)
|
||||||
|
|
||||||
|
products = deal.get("products", [])
|
||||||
|
products.append(product_data)
|
||||||
|
|
||||||
|
await mongo.deals_collection.update_one({"id": deal_id}, {"$set": {"products": products}})
|
||||||
|
return response({
|
||||||
|
"message": "Товар добавлен к сделке",
|
||||||
|
"ok": True
|
||||||
|
}, start_time=start_time)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/delete")
|
||||||
|
async def delete_product(params: dict):
|
||||||
|
start_time = time()
|
||||||
|
|
||||||
|
deal_id = params["dealId"]
|
||||||
|
product_id = params["productId"]
|
||||||
|
|
||||||
|
deal = await mongo.deals_collection.find_one({"id": deal_id})
|
||||||
|
if not deal:
|
||||||
|
return response({"message": "Сделка не найдена", "ok": False}, start_time=start_time)
|
||||||
|
|
||||||
|
products = deal.get("products", [])
|
||||||
|
products = [product for product in products if product["productId"] != product_id]
|
||||||
|
|
||||||
|
await mongo.deals_collection.update_one({"id": deal_id}, {"$set": {"products": products}})
|
||||||
|
return response({
|
||||||
|
"message": "Товар удалён из сделки",
|
||||||
|
"ok": True
|
||||||
|
}, start_time=start_time)
|
||||||
139
app/api/v1/deal/services.py
Normal file
139
app/api/v1/deal/services.py
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
from time import time
|
||||||
|
|
||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
from app import mongo
|
||||||
|
from app.utils.response_util import response
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/add")
|
||||||
|
async def add_service(params: dict):
|
||||||
|
start_time = time()
|
||||||
|
|
||||||
|
deal_id = params["dealId"]
|
||||||
|
service_data = {
|
||||||
|
"serviceId": params["serviceId"],
|
||||||
|
"quantity": params["quantity"],
|
||||||
|
"price": params["price"]
|
||||||
|
}
|
||||||
|
|
||||||
|
deal = await mongo.deals_collection.find_one({"id": deal_id})
|
||||||
|
if not deal:
|
||||||
|
return response({"message": "Сделка не найдена", "ok": False}, start_time=start_time)
|
||||||
|
|
||||||
|
services = deal.get("services", [])
|
||||||
|
services.append(service_data)
|
||||||
|
|
||||||
|
await mongo.deals_collection.update_one({"id": deal_id}, {"$set": {"services": services}})
|
||||||
|
return response({
|
||||||
|
"message": "Услуга добавлена к сделке",
|
||||||
|
"ok": True
|
||||||
|
}, start_time=start_time)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/update")
|
||||||
|
async def update_service(params: dict):
|
||||||
|
start_time = time()
|
||||||
|
|
||||||
|
deal_id = params["dealId"]
|
||||||
|
service_id = params["service"]["serviceId"]
|
||||||
|
|
||||||
|
deal = await mongo.deals_collection.find_one({"id": deal_id})
|
||||||
|
if not deal:
|
||||||
|
return response({"message": "Сделка не найдена", "ok": False}, start_time=start_time)
|
||||||
|
|
||||||
|
services = deal.get("services", [])
|
||||||
|
for service in services:
|
||||||
|
if service["serviceId"] == service_id:
|
||||||
|
for key, value in params["service"].items():
|
||||||
|
service[key] = value
|
||||||
|
break
|
||||||
|
|
||||||
|
await mongo.deals_collection.update_one({"id": deal_id}, {"$set": {"services": services}})
|
||||||
|
return response({
|
||||||
|
"message": "Услуга обновлена в сделке",
|
||||||
|
"ok": True
|
||||||
|
}, start_time=start_time)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/delete")
|
||||||
|
async def delete_service(params: dict):
|
||||||
|
start_time = time()
|
||||||
|
|
||||||
|
deal_id = params["dealId"]
|
||||||
|
service_id = params["serviceId"]
|
||||||
|
|
||||||
|
deal = await mongo.deals_collection.find_one({"id": deal_id})
|
||||||
|
if not deal:
|
||||||
|
return response({"message": "Сделка не найдена", "ok": False}, start_time=start_time)
|
||||||
|
|
||||||
|
services = deal.get("services", [])
|
||||||
|
updated_services = [service for service in services if service["serviceId"] != service_id]
|
||||||
|
|
||||||
|
await mongo.deals_collection.update_one({"id": deal_id}, {"$set": {"services": updated_services}})
|
||||||
|
return response({
|
||||||
|
"message": "Услуга удалена из сделки",
|
||||||
|
"ok": True
|
||||||
|
}, start_time=start_time)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/copy")
|
||||||
|
async def copy_services(params: dict):
|
||||||
|
start_time = time()
|
||||||
|
|
||||||
|
deal_id = params["dealId"]
|
||||||
|
source_product_id = params["sourceProductId"]
|
||||||
|
destination_product_ids = params["destinationProductIds"]
|
||||||
|
|
||||||
|
deal = await mongo.deals_collection.find_one({"id": deal_id})
|
||||||
|
if not deal:
|
||||||
|
return response({
|
||||||
|
"message": "Сделка не найдена",
|
||||||
|
"ok": False
|
||||||
|
}, start_time=start_time)
|
||||||
|
|
||||||
|
products = deal.get("products", [])
|
||||||
|
source_product_services = []
|
||||||
|
|
||||||
|
for product in products:
|
||||||
|
if product["productId"] == source_product_id:
|
||||||
|
source_product_services = product.get("services", [])
|
||||||
|
break
|
||||||
|
|
||||||
|
source_services_map = {s["serviceId"]: s for s in source_product_services}
|
||||||
|
service_quantities = {}
|
||||||
|
|
||||||
|
for product in products:
|
||||||
|
if product["productId"] in destination_product_ids:
|
||||||
|
new_services = []
|
||||||
|
for service_id, service in source_services_map.items():
|
||||||
|
new_service = service.copy()
|
||||||
|
new_services.append(new_service)
|
||||||
|
service_quantities[service_id] = service_quantities.get(service_id, 0) + product["quantity"]
|
||||||
|
product["services"] = new_services
|
||||||
|
|
||||||
|
for service_id, total_quantity in service_quantities.items():
|
||||||
|
service_data = await mongo.services_collection.find_one({"id": service_id})
|
||||||
|
if not service_data or "priceRanges" not in service_data:
|
||||||
|
continue
|
||||||
|
|
||||||
|
price_ranges = sorted(service_data["priceRanges"], key=lambda x: x["fromQuantity"])
|
||||||
|
new_price = None
|
||||||
|
|
||||||
|
for price_range in price_ranges:
|
||||||
|
if total_quantity >= price_range["fromQuantity"]:
|
||||||
|
new_price = price_range["price"]
|
||||||
|
|
||||||
|
if new_price is not None:
|
||||||
|
for product in products:
|
||||||
|
for service in product.get("services", []):
|
||||||
|
if service["serviceId"] == service_id and not service.get("isFixedPrice", False):
|
||||||
|
service["price"] = new_price
|
||||||
|
|
||||||
|
await mongo.deals_collection.update_one({"id": deal_id}, {"$set": {"products": products}})
|
||||||
|
return response({
|
||||||
|
"message": "Услуги скопированы и обновлены",
|
||||||
|
"ok": True
|
||||||
|
}, start_time=start_time)
|
||||||
66
app/api/v1/deal/summaries.py
Normal file
66
app/api/v1/deal/summaries.py
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
from time import time
|
||||||
|
|
||||||
|
from fastapi import APIRouter
|
||||||
|
from starlette.requests import Request
|
||||||
|
|
||||||
|
from app import mongo
|
||||||
|
from app.utils.response_util import response
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("", tags=[""])
|
||||||
|
async def get(full: bool):
|
||||||
|
start_time = time()
|
||||||
|
|
||||||
|
return response({
|
||||||
|
"summaries": await mongo.get_all_summaries(full)
|
||||||
|
}, start_time=start_time)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/reorder", tags=[""])
|
||||||
|
async def reorder(params: dict, request: Request):
|
||||||
|
start_time = time()
|
||||||
|
|
||||||
|
deal_id = params["dealId"]
|
||||||
|
status_id = params["statusId"]
|
||||||
|
index = params["index"]
|
||||||
|
comment = params["comment"]
|
||||||
|
|
||||||
|
deal = await mongo.deals_collection.find_one({"id": deal_id}, {"_id": False})
|
||||||
|
if not deal:
|
||||||
|
return response({"message": "Сделка не найдена", "ok": False}, start_time=start_time)
|
||||||
|
|
||||||
|
update_fields = {
|
||||||
|
"statusId": status_id,
|
||||||
|
"index": index
|
||||||
|
}
|
||||||
|
|
||||||
|
new_status = await mongo.statuses_collection.find_one({"id": status_id}, {"_id": False})
|
||||||
|
if new_status and new_status.get("name") == "Завершено":
|
||||||
|
update_fields["completedAt"] = mongo.created_at()
|
||||||
|
|
||||||
|
old_status_id = deal.get("statusId")
|
||||||
|
await mongo.deals_collection.update_one(
|
||||||
|
{"id": deal_id},
|
||||||
|
{"$set": update_fields}
|
||||||
|
)
|
||||||
|
|
||||||
|
if old_status_id != status_id:
|
||||||
|
new_entry = {
|
||||||
|
"changedAt": mongo.created_at(),
|
||||||
|
"fromStatusId": old_status_id,
|
||||||
|
"toStatusId": status_id,
|
||||||
|
"userId": request.state.user["id"],
|
||||||
|
"comment": comment
|
||||||
|
}
|
||||||
|
|
||||||
|
await mongo.deals_collection.update_one(
|
||||||
|
{"id": deal_id},
|
||||||
|
{"$push": {"statusHistory": new_entry}}
|
||||||
|
)
|
||||||
|
|
||||||
|
return response({
|
||||||
|
"ok": True,
|
||||||
|
"summaries": await mongo.get_all_summaries(False)
|
||||||
|
}, start_time=start_time)
|
||||||
198
app/api/v1/department.py
Normal file
198
app/api/v1/department.py
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
from time import time
|
||||||
|
|
||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
from app import mongo
|
||||||
|
from app.utils.response_util import response
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/", tags=[""])
|
||||||
|
async def get_departments():
|
||||||
|
start_time = time()
|
||||||
|
|
||||||
|
departments = await mongo.departments_collection.find(
|
||||||
|
{}, {"_id": False}
|
||||||
|
).sort("id", mongo.asc).to_list(length=None)
|
||||||
|
|
||||||
|
async def build_section_tree(parent_section):
|
||||||
|
users = []
|
||||||
|
for user in parent_section.get("users", []):
|
||||||
|
user_data = await mongo.users_collection.find_one({"id": user["userId"]}, {"_id": False})
|
||||||
|
if user_data:
|
||||||
|
user["user"] = user_data
|
||||||
|
users.append(user)
|
||||||
|
|
||||||
|
parent_section["users"] = users
|
||||||
|
|
||||||
|
children = await mongo.department_sections_collection.find(
|
||||||
|
{"parentDepartmentSectionId": parent_section["id"]},
|
||||||
|
{"_id": False}
|
||||||
|
).sort("id", mongo.asc).to_list(length=None)
|
||||||
|
|
||||||
|
parent_section["sections"] = []
|
||||||
|
for child in children:
|
||||||
|
await build_section_tree(child)
|
||||||
|
parent_section["sections"].append(child)
|
||||||
|
|
||||||
|
for department in departments:
|
||||||
|
top_sections = await mongo.department_sections_collection.find(
|
||||||
|
{
|
||||||
|
"departmentId": department["id"],
|
||||||
|
"$or": [{"parentDepartmentSectionId": None}, {"parentDepartmentSectionId": {"$exists": False}}]
|
||||||
|
},
|
||||||
|
{"_id": False}
|
||||||
|
).sort("id", mongo.asc).to_list(length=None)
|
||||||
|
|
||||||
|
department["sections"] = []
|
||||||
|
for section in top_sections:
|
||||||
|
await build_section_tree(section)
|
||||||
|
department["sections"].append(section)
|
||||||
|
|
||||||
|
return response({
|
||||||
|
"departments": departments
|
||||||
|
}, start_time=start_time)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/", tags=[""])
|
||||||
|
async def create_department(params: dict):
|
||||||
|
start_time = time()
|
||||||
|
data = params["department"]
|
||||||
|
|
||||||
|
data["id"] = await mongo.get_next_id(mongo.departments_collection)
|
||||||
|
await mongo.departments_collection.insert_one(data)
|
||||||
|
|
||||||
|
return response({"message": "Департамент создан", "ok": True}, start_time=start_time)
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch("/", tags=[""])
|
||||||
|
async def update_department(params: dict):
|
||||||
|
start_time = time()
|
||||||
|
data = params["department"]
|
||||||
|
|
||||||
|
await mongo.departments_collection.update_one({"id": data["id"]}, {"$set": data})
|
||||||
|
return response({"message": "Департамент обновлён", "ok": True}, start_time=start_time)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{department_id}", tags=[""])
|
||||||
|
async def delete_department(department_id: int):
|
||||||
|
start_time = time()
|
||||||
|
|
||||||
|
sections = await mongo.department_sections_collection.find(
|
||||||
|
{"departmentId": department_id}, {"_id": False}
|
||||||
|
).to_list(length=None)
|
||||||
|
|
||||||
|
for section in sections:
|
||||||
|
await delete_section_recursive(section["id"])
|
||||||
|
|
||||||
|
await mongo.departments_collection.delete_one({"id": department_id})
|
||||||
|
return response({"message": "Департамент удалён", "ok": True}, start_time=start_time)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/section", tags=[""])
|
||||||
|
async def get_sections():
|
||||||
|
start_time = time()
|
||||||
|
department_sections = await mongo.department_sections_collection.find(
|
||||||
|
{}, {"_id": False}
|
||||||
|
).sort("id", mongo.asc).to_list(length=None)
|
||||||
|
|
||||||
|
return response({
|
||||||
|
"departmentSections": department_sections
|
||||||
|
}, start_time=start_time)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/section", tags=[""])
|
||||||
|
async def create_section(params: dict):
|
||||||
|
start_time = time()
|
||||||
|
data = params["section"]
|
||||||
|
|
||||||
|
if "departmentId" not in data:
|
||||||
|
return response({
|
||||||
|
"message": "Необходимо выбрать департамент",
|
||||||
|
"ok": False
|
||||||
|
}, start_time=start_time)
|
||||||
|
|
||||||
|
data["id"] = await mongo.get_next_id(mongo.department_sections_collection)
|
||||||
|
await mongo.department_sections_collection.insert_one(data)
|
||||||
|
|
||||||
|
return response({
|
||||||
|
"message": "Отдел создан",
|
||||||
|
"ok": True
|
||||||
|
}, start_time=start_time)
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch("/section", tags=[""])
|
||||||
|
async def update_section(params: dict):
|
||||||
|
start_time = time()
|
||||||
|
data = params["section"]
|
||||||
|
|
||||||
|
await mongo.department_sections_collection.update_one({"id": data["id"]}, {"$set": data})
|
||||||
|
return response({"message": "Отдел обновлен", "ok": True}, start_time=start_time)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/section/{section_id}", tags=[""])
|
||||||
|
async def delete_section(section_id: int):
|
||||||
|
start_time = time()
|
||||||
|
|
||||||
|
await delete_section_recursive(section_id)
|
||||||
|
return response({"message": "Отдел удален", "ok": True}, start_time=start_time)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/users/{section_id}", tags=[""])
|
||||||
|
async def get_available_users(section_id: int):
|
||||||
|
start_time = time()
|
||||||
|
|
||||||
|
users = await mongo.users_collection.find(
|
||||||
|
{"isDeleted": False}, {"_id": False}
|
||||||
|
).to_list(length=None)
|
||||||
|
|
||||||
|
return response({
|
||||||
|
"users": users
|
||||||
|
}, start_time=start_time)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/users", tags=[""])
|
||||||
|
async def add_user_to_section(params: dict):
|
||||||
|
start_time = time()
|
||||||
|
section_id = params["sectionId"]
|
||||||
|
user_id = params["userId"]
|
||||||
|
is_chief = params.get("isChief", False)
|
||||||
|
|
||||||
|
await mongo.department_sections_collection.update_one(
|
||||||
|
{"id": section_id},
|
||||||
|
{"$addToSet": {"users": {"userId": user_id, "isChief": is_chief}}}
|
||||||
|
)
|
||||||
|
|
||||||
|
return response({
|
||||||
|
"message": "Пользователь добавлен в секцию",
|
||||||
|
"ok": True
|
||||||
|
}, start_time=start_time)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/users/delete", tags=[""])
|
||||||
|
async def delete_user_from_section(params: dict):
|
||||||
|
start_time = time()
|
||||||
|
section_id = params["sectionId"]
|
||||||
|
user_id = params["userId"]
|
||||||
|
|
||||||
|
await mongo.department_sections_collection.update_one(
|
||||||
|
{"id": section_id},
|
||||||
|
{"$pull": {"users": {"userId": user_id}}}
|
||||||
|
)
|
||||||
|
|
||||||
|
return response({
|
||||||
|
"message": "Пользователь удалён из секции",
|
||||||
|
"ok": True
|
||||||
|
}, start_time=start_time)
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_section_recursive(section_id: int):
|
||||||
|
children = await mongo.department_sections_collection.find(
|
||||||
|
{"parentDepartmentSectionId": section_id}, {"_id": False}
|
||||||
|
).to_list(length=None)
|
||||||
|
|
||||||
|
for child in children:
|
||||||
|
await delete_section_recursive(child["id"])
|
||||||
|
|
||||||
|
await mongo.department_sections_collection.delete_one({"id": section_id})
|
||||||
94
app/api/v1/marketplace.py
Normal file
94
app/api/v1/marketplace.py
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
from time import time
|
||||||
|
|
||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
from app import mongo
|
||||||
|
from app.utils.response_util import response
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/get", tags=[""])
|
||||||
|
async def get(data: dict):
|
||||||
|
start_time = time()
|
||||||
|
client_id = data["clientId"]
|
||||||
|
|
||||||
|
marketplaces = await mongo.marketplaces_collection.find({
|
||||||
|
"client.id": client_id
|
||||||
|
}, {
|
||||||
|
"_id": False
|
||||||
|
}).to_list()
|
||||||
|
|
||||||
|
return response({
|
||||||
|
"marketplaces": marketplaces
|
||||||
|
}, start_time=start_time)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/base/get-all", tags=[""])
|
||||||
|
async def base_get_all():
|
||||||
|
start_time = time()
|
||||||
|
|
||||||
|
baseMarketplaces = await mongo.base_marketplaces_collection.find({}, {
|
||||||
|
"_id": False
|
||||||
|
}).to_list()
|
||||||
|
|
||||||
|
return response({
|
||||||
|
"baseMarketplaces": baseMarketplaces
|
||||||
|
}, start_time=start_time)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/create", tags=[""])
|
||||||
|
async def create(params: dict):
|
||||||
|
start_time = time()
|
||||||
|
data = params["marketplace"]
|
||||||
|
data["id"] = await mongo.get_next_id(mongo.marketplaces_collection)
|
||||||
|
|
||||||
|
await mongo.marketplaces_collection.insert_one(data)
|
||||||
|
return response({
|
||||||
|
"message": "Маркетплейс клиента создан",
|
||||||
|
"ok": True
|
||||||
|
}, start_time=start_time)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/update", tags=[""])
|
||||||
|
async def update(params: dict):
|
||||||
|
start_time = time()
|
||||||
|
data = params["marketplace"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
await mongo.marketplaces_collection.update_one({
|
||||||
|
"id": data["id"]
|
||||||
|
}, {
|
||||||
|
"$set": data
|
||||||
|
})
|
||||||
|
|
||||||
|
return response({
|
||||||
|
"message": "Данные маркетплейса клиента обновлены",
|
||||||
|
"ok": True
|
||||||
|
}, start_time=start_time)
|
||||||
|
except Exception as e:
|
||||||
|
return response({
|
||||||
|
"message": str(e),
|
||||||
|
"ok": False
|
||||||
|
}, start_time=start_time, code=400)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/delete", tags=[""])
|
||||||
|
async def delete(params: dict):
|
||||||
|
start_time = time()
|
||||||
|
marketplace_id = params["marketplaceId"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
await mongo.marketplaces_collection.delete_one({
|
||||||
|
"id": marketplace_id
|
||||||
|
})
|
||||||
|
|
||||||
|
return response({
|
||||||
|
"message": "Маркетплейс клиента удален",
|
||||||
|
"ok": True
|
||||||
|
}, start_time=start_time)
|
||||||
|
except Exception as e:
|
||||||
|
return response({
|
||||||
|
"message": str(e),
|
||||||
|
"ok": False
|
||||||
|
}, start_time=start_time, code=400)
|
||||||
9
app/api/v1/payroll/__init__.py
Normal file
9
app/api/v1/payroll/__init__.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
from app.api.v1.payroll import scheme, pay_rate, payment_record
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
router.include_router(scheme.router, prefix='/scheme')
|
||||||
|
router.include_router(pay_rate.router, prefix='/pay-rate')
|
||||||
|
router.include_router(payment_record.router, prefix='/payment-record')
|
||||||
BIN
app/api/v1/payroll/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
app/api/v1/payroll/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/api/v1/payroll/__pycache__/deal.cpython-311.pyc
Normal file
BIN
app/api/v1/payroll/__pycache__/deal.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/api/v1/payroll/__pycache__/pay_rate.cpython-311.pyc
Normal file
BIN
app/api/v1/payroll/__pycache__/pay_rate.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/api/v1/payroll/__pycache__/payment_record.cpython-311.pyc
Normal file
BIN
app/api/v1/payroll/__pycache__/payment_record.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/api/v1/payroll/__pycache__/payroll.cpython-311.pyc
Normal file
BIN
app/api/v1/payroll/__pycache__/payroll.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/api/v1/payroll/__pycache__/scheme.cpython-311.pyc
Normal file
BIN
app/api/v1/payroll/__pycache__/scheme.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/api/v1/payroll/__pycache__/summaries.cpython-311.pyc
Normal file
BIN
app/api/v1/payroll/__pycache__/summaries.cpython-311.pyc
Normal file
Binary file not shown.
57
app/api/v1/payroll/pay_rate.py
Normal file
57
app/api/v1/payroll/pay_rate.py
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
from time import time
|
||||||
|
|
||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
from app import mongo
|
||||||
|
from app.utils.response_util import response
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/get-all", tags=[""])
|
||||||
|
async def get_all_pay_rates():
|
||||||
|
start_time = time()
|
||||||
|
|
||||||
|
pay_rates = await mongo.pay_rates_collection.find(
|
||||||
|
{}, {"_id": False}
|
||||||
|
).sort("id", mongo.asc).to_list()
|
||||||
|
|
||||||
|
return response({
|
||||||
|
"payRates": pay_rates
|
||||||
|
}, start_time=start_time)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/create", tags=[""])
|
||||||
|
async def create_pay_rate(params: dict):
|
||||||
|
start_time = time()
|
||||||
|
data = params["data"]
|
||||||
|
data["id"] = await mongo.get_next_id(mongo.pay_rates_collection)
|
||||||
|
|
||||||
|
await mongo.pay_rates_collection.insert_one(data)
|
||||||
|
return response({
|
||||||
|
"message": "Тариф успешно создан",
|
||||||
|
"ok": True
|
||||||
|
}, start_time=start_time)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/update", tags=[""])
|
||||||
|
async def update_pay_rate(params: dict):
|
||||||
|
start_time = time()
|
||||||
|
data = params["data"]
|
||||||
|
|
||||||
|
await mongo.pay_rates_collection.update_one({"id": data["id"]}, {"$set": data})
|
||||||
|
return response({
|
||||||
|
"message": "Тариф успешно обновлен",
|
||||||
|
"ok": True
|
||||||
|
}, start_time=start_time)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/delete", tags=[""])
|
||||||
|
async def delete_pay_rate(params: dict):
|
||||||
|
start_time = time()
|
||||||
|
|
||||||
|
await mongo.pay_rates_collection.delete_one({"id": params["payRateId"]})
|
||||||
|
return response({
|
||||||
|
"message": "Тариф успешно удален",
|
||||||
|
"ok": True
|
||||||
|
}, start_time=start_time)
|
||||||
86
app/api/v1/payroll/payment_record.py
Normal file
86
app/api/v1/payroll/payment_record.py
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
from time import time
|
||||||
|
|
||||||
|
from fastapi import APIRouter
|
||||||
|
from starlette.requests import Request
|
||||||
|
|
||||||
|
from app import mongo
|
||||||
|
from app.utils.response_util import response
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/get", tags=[""])
|
||||||
|
async def get(page: int = 0, items_per_page: int = 10):
|
||||||
|
start_time = time()
|
||||||
|
|
||||||
|
total = await mongo.payment_records_collection.count_documents({})
|
||||||
|
payment_records = await mongo.payment_records_collection.find({}, {
|
||||||
|
"_id": False
|
||||||
|
}).limit(items_per_page).skip(items_per_page * (page - 1)).to_list(length=None)
|
||||||
|
|
||||||
|
for payment_record in payment_records:
|
||||||
|
payment_record["user"] = await mongo.users_collection.find_one({"id": payment_record["userId"]}, {"_id": False})
|
||||||
|
payment_record["createdByUser"] = await mongo.users_collection.find_one({"id": payment_record["createdByUserId"]}, {"_id": False})
|
||||||
|
|
||||||
|
return response({
|
||||||
|
"paginationInfo": {
|
||||||
|
"totalItems": total,
|
||||||
|
"totalPages": round(total / items_per_page)
|
||||||
|
},
|
||||||
|
"paymentRecords": payment_records
|
||||||
|
}, start_time=start_time)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/create", tags=[""])
|
||||||
|
async def create(params: dict, request: Request):
|
||||||
|
start_time = time()
|
||||||
|
|
||||||
|
data = params["data"]
|
||||||
|
work_units = data.get("workUnits", 0)
|
||||||
|
|
||||||
|
user = await mongo.users_collection.find_one({"id": data["user"]["id"]}, {"_id": False})
|
||||||
|
pay_rate = await mongo.pay_rates_collection.find_one(
|
||||||
|
{"id": user.get("payRate", {}).get("id")},
|
||||||
|
{"_id": False}
|
||||||
|
)
|
||||||
|
|
||||||
|
payroll_scheme = pay_rate.get("payrollScheme", {})
|
||||||
|
total_amount = 0.0
|
||||||
|
|
||||||
|
if payroll_scheme.get("key") == "hourly":
|
||||||
|
overtime_threshold = pay_rate.get("overtimeThreshold", 0)
|
||||||
|
base_hours = min(work_units, overtime_threshold)
|
||||||
|
overtime_hours = max(work_units - overtime_threshold, 0)
|
||||||
|
|
||||||
|
base_rate = pay_rate.get("baseRate", 0)
|
||||||
|
overtime_rate = pay_rate.get("overtimeRate", base_rate)
|
||||||
|
|
||||||
|
total_amount = round(base_hours * base_rate + overtime_hours * overtime_rate, 2)
|
||||||
|
|
||||||
|
elif payroll_scheme.get("key") == "hourly":
|
||||||
|
base_rate = pay_rate.get("baseRate", 0)
|
||||||
|
total_amount = base_rate * work_units
|
||||||
|
|
||||||
|
data["id"] = await mongo.get_next_id(mongo.payment_records_collection)
|
||||||
|
data["userId"] = user["id"]
|
||||||
|
data["createdByUserId"] = request.state.user["id"]
|
||||||
|
data["createdAt"] = mongo.created_at()
|
||||||
|
data["amount"] = total_amount
|
||||||
|
data["payrollScheme"] = payroll_scheme
|
||||||
|
|
||||||
|
await mongo.payment_records_collection.insert_one(data)
|
||||||
|
return response({
|
||||||
|
"message": "Начисление создано",
|
||||||
|
"ok": True
|
||||||
|
}, start_time=start_time)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/delete", tags=[""])
|
||||||
|
async def delete(params: dict):
|
||||||
|
start_time = time()
|
||||||
|
|
||||||
|
await mongo.payment_records_collection.delete_one({"id": params["paymentRecordId"]})
|
||||||
|
return response({
|
||||||
|
"message": "Начисление удалено",
|
||||||
|
"ok": True
|
||||||
|
}, start_time=start_time)
|
||||||
21
app/api/v1/payroll/payroll.py
Normal file
21
app/api/v1/payroll/payroll.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
from time import time
|
||||||
|
|
||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
from app import mongo
|
||||||
|
from app.utils.response_util import response
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/scheme/get-all", tags=[""])
|
||||||
|
async def get_all_schemes():
|
||||||
|
start_time = time()
|
||||||
|
|
||||||
|
schemes = await mongo.pay_rate_schemes_collection.find(
|
||||||
|
{}, {"_id": False}
|
||||||
|
).sort("id", mongo.asc).to_list()
|
||||||
|
|
||||||
|
return response({
|
||||||
|
"payrollSchemas": schemes
|
||||||
|
}, start_time=start_time)
|
||||||
21
app/api/v1/payroll/scheme.py
Normal file
21
app/api/v1/payroll/scheme.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
from time import time
|
||||||
|
|
||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
from app import mongo
|
||||||
|
from app.utils.response_util import response
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/get-all", tags=[""])
|
||||||
|
async def get_all_schemes():
|
||||||
|
start_time = time()
|
||||||
|
|
||||||
|
schemes = await mongo.pay_rate_schemes_collection.find(
|
||||||
|
{}, {"_id": False}
|
||||||
|
).sort("id", mongo.asc).to_list()
|
||||||
|
|
||||||
|
return response({
|
||||||
|
"payrollSchemas": schemes
|
||||||
|
}, start_time=start_time)
|
||||||
46
app/api/v1/position.py
Normal file
46
app/api/v1/position.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
from time import time
|
||||||
|
|
||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
from app import mongo
|
||||||
|
from app.utils.response_util import response
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/get-all", tags=[""])
|
||||||
|
async def get_all():
|
||||||
|
start_time = time()
|
||||||
|
|
||||||
|
positions = await mongo.positions_collection.find({}, {
|
||||||
|
"_id": False
|
||||||
|
}).sort("id", mongo.asc).to_list()
|
||||||
|
|
||||||
|
return response({
|
||||||
|
"positions": positions
|
||||||
|
}, start_time=start_time)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/create", tags=[""])
|
||||||
|
async def create_position(params: dict):
|
||||||
|
start_time = time()
|
||||||
|
|
||||||
|
data = params["data"]
|
||||||
|
data["id"] = await mongo.get_next_id(mongo.positions_collection)
|
||||||
|
|
||||||
|
await mongo.positions_collection.insert_one(data)
|
||||||
|
return response({
|
||||||
|
"message": "Должность успешно создана",
|
||||||
|
"ok": True
|
||||||
|
}, start_time=start_time)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/delete", tags=[""])
|
||||||
|
async def delete_position(params: dict):
|
||||||
|
start_time = time()
|
||||||
|
|
||||||
|
await mongo.positions_collection.delete_one({"key": params["positionKey"]})
|
||||||
|
return response({
|
||||||
|
"message": "Должность успешно удалена",
|
||||||
|
"ok": True
|
||||||
|
}, start_time=start_time)
|
||||||
9
app/api/v1/product/__init__.py
Normal file
9
app/api/v1/product/__init__.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
from app.api.v1.product import product, barcode, images
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
router.include_router(product.router)
|
||||||
|
router.include_router(barcode.router, prefix='/barcode')
|
||||||
|
router.include_router(images.router, prefix='/images')
|
||||||
BIN
app/api/v1/product/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
app/api/v1/product/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/api/v1/product/__pycache__/barcode.cpython-311.pyc
Normal file
BIN
app/api/v1/product/__pycache__/barcode.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/api/v1/product/__pycache__/images.cpython-311.pyc
Normal file
BIN
app/api/v1/product/__pycache__/images.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/api/v1/product/__pycache__/product.cpython-311.pyc
Normal file
BIN
app/api/v1/product/__pycache__/product.cpython-311.pyc
Normal file
Binary file not shown.
246
app/api/v1/product/barcode.py
Normal file
246
app/api/v1/product/barcode.py
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
import base64
|
||||||
|
import os
|
||||||
|
import textwrap
|
||||||
|
from io import BytesIO
|
||||||
|
from time import time
|
||||||
|
|
||||||
|
import fitz
|
||||||
|
from PyPDF2 import PdfReader, PdfWriter
|
||||||
|
from fastapi import APIRouter, UploadFile, File
|
||||||
|
from reportlab.graphics.barcode import code128
|
||||||
|
from reportlab.lib.units import mm
|
||||||
|
from reportlab.pdfbase import pdfmetrics
|
||||||
|
from reportlab.pdfbase.ttfonts import TTFont
|
||||||
|
from reportlab.pdfgen import canvas
|
||||||
|
from starlette.exceptions import HTTPException
|
||||||
|
from starlette.requests import Request
|
||||||
|
|
||||||
|
from app import mongo
|
||||||
|
from app.utils.response_util import response
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
STATIC_FOLDER = "static"
|
||||||
|
BARCODES_FOLDER = os.path.join(STATIC_FOLDER, "barcodes")
|
||||||
|
os.makedirs(BARCODES_FOLDER, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/get", tags=[""])
|
||||||
|
async def get_barcode(params: dict):
|
||||||
|
start_time = time()
|
||||||
|
return response({
|
||||||
|
"barcode": params["barcode"]
|
||||||
|
}, start_time=start_time)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/add", tags=[""])
|
||||||
|
async def add_barcode(params: dict):
|
||||||
|
start_time = time()
|
||||||
|
|
||||||
|
product = await mongo.products_collection.find_one({
|
||||||
|
"id": params["productId"]
|
||||||
|
}, {
|
||||||
|
"_id": False
|
||||||
|
})
|
||||||
|
|
||||||
|
barcodes = product["barcodes"]
|
||||||
|
barcodes.append(params["barcode"])
|
||||||
|
|
||||||
|
await mongo.products_collection.update_one({
|
||||||
|
"id": params["productId"]
|
||||||
|
}, {
|
||||||
|
"$set": {"barcodes": barcodes}
|
||||||
|
})
|
||||||
|
|
||||||
|
return response({
|
||||||
|
"message": "Штрихкод добавлен",
|
||||||
|
"ok": True
|
||||||
|
}, start_time=start_time)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/exists", tags=[""])
|
||||||
|
async def barcode_exists(product_id: int, barcode: str):
|
||||||
|
start_time = time()
|
||||||
|
|
||||||
|
product = await mongo.products_collection.find_one({
|
||||||
|
"id": product_id
|
||||||
|
}, {
|
||||||
|
"_id": False
|
||||||
|
})
|
||||||
|
|
||||||
|
return response({
|
||||||
|
"exists": barcode in product["barcodes"]
|
||||||
|
}, start_time=start_time)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/get-pdf", tags=[""])
|
||||||
|
async def get_barcode_pdf(params: dict):
|
||||||
|
product_id = params["productId"]
|
||||||
|
quantity = params['quantity']
|
||||||
|
|
||||||
|
pdf_path = os.path.join(BARCODES_FOLDER, f"{product_id}.pdf")
|
||||||
|
if os.path.exists(pdf_path):
|
||||||
|
with open(pdf_path, "rb") as file:
|
||||||
|
existing_pdf = PdfReader(file)
|
||||||
|
|
||||||
|
buffer = BytesIO()
|
||||||
|
new_pdf = PdfWriter()
|
||||||
|
|
||||||
|
for _ in range(quantity):
|
||||||
|
for page_num in range(len(existing_pdf.pages)):
|
||||||
|
page = existing_pdf.pages[page_num]
|
||||||
|
new_pdf.add_page(page)
|
||||||
|
|
||||||
|
new_pdf.write(buffer)
|
||||||
|
buffer.seek(0)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"base64String": base64.b64encode(buffer.getvalue()).decode("utf-8"),
|
||||||
|
"mimeType": "application/pdf"
|
||||||
|
}
|
||||||
|
|
||||||
|
product = await mongo.products_collection.find_one(
|
||||||
|
{"id": product_id}, {"_id": False}
|
||||||
|
)
|
||||||
|
|
||||||
|
client = await mongo.clients_collection.find_one(
|
||||||
|
{"id": product["clientId"]}, {"_id": False}
|
||||||
|
)
|
||||||
|
|
||||||
|
barcode_template = await mongo.templates_collection.find_one(
|
||||||
|
{"id": product["barcodeTemplate"]["id"]} if product["barcodeTemplate"] else {}, {"_id": False}
|
||||||
|
)
|
||||||
|
|
||||||
|
attributes = {
|
||||||
|
attribute["key"]: attribute["name"]
|
||||||
|
for attribute in barcode_template["attributes"]
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer = BytesIO()
|
||||||
|
pdf = canvas.Canvas(buffer, pagesize=(
|
||||||
|
barcode_template["size"]["width"] * mm,
|
||||||
|
barcode_template["size"]["height"] * mm
|
||||||
|
))
|
||||||
|
|
||||||
|
pdf.setTitle("Product Barcode Information")
|
||||||
|
pdfmetrics.registerFont(TTFont('Arial', 'assets/arial.ttf'))
|
||||||
|
pdfmetrics.registerFont(TTFont('Arial Bold', 'assets/arial_bold.ttf'))
|
||||||
|
|
||||||
|
for _ in range(quantity):
|
||||||
|
name_lines = textwrap.wrap(product["name"], width=24)
|
||||||
|
text_y = 36 * mm
|
||||||
|
|
||||||
|
pdf.setFont("Arial Bold", 9)
|
||||||
|
for line in name_lines:
|
||||||
|
pdf.drawCentredString(29 * mm, text_y, line)
|
||||||
|
text_y -= 3 * mm
|
||||||
|
|
||||||
|
pdf.setFont("Arial", 7)
|
||||||
|
if "article" in attributes:
|
||||||
|
pdf.drawString(6 * mm, text_y, f"{attributes['article']}: {product['article']}")
|
||||||
|
text_y -= 3 * mm
|
||||||
|
|
||||||
|
if "brand" in attributes and product.get("brand"):
|
||||||
|
pdf.drawString(6 * mm, text_y, f"{attributes['brand']}: {product['brand']}")
|
||||||
|
text_y -= 3 * mm
|
||||||
|
|
||||||
|
if "client.name" in attributes and client:
|
||||||
|
pdf.drawString(6 * mm, text_y, client["name"])
|
||||||
|
text_y -= 3 * mm
|
||||||
|
|
||||||
|
if "color" in attributes and product.get("color"):
|
||||||
|
pdf.drawString(6 * mm, text_y, f"{attributes['color']}: {product['color']}")
|
||||||
|
text_y -= 3 * mm
|
||||||
|
|
||||||
|
if "size" in attributes and product.get("size"):
|
||||||
|
pdf.drawString(6 * mm, text_y, f"{attributes['size']}: {product['size']}")
|
||||||
|
text_y -= 3 * mm
|
||||||
|
|
||||||
|
barcode = code128.Code128(params["barcode"], barWidth=0.3 * mm, barHeight=10 * mm)
|
||||||
|
barcode.drawOn(pdf, 5 * mm, text_y - 8 * mm)
|
||||||
|
|
||||||
|
pdf.drawCentredString(29 * mm, text_y - 11 * mm, params["barcode"])
|
||||||
|
pdf.showPage()
|
||||||
|
|
||||||
|
pdf.save()
|
||||||
|
|
||||||
|
buffer.seek(0)
|
||||||
|
return {
|
||||||
|
"base64String": base64.b64encode(buffer.getvalue()).decode("utf-8"),
|
||||||
|
"mimeType": "application/pdf"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/upload-image/{product_id}", tags=[""])
|
||||||
|
async def upload_product_barcode_image(
|
||||||
|
request: Request,
|
||||||
|
product_id: int,
|
||||||
|
upload_file: UploadFile = File(...)
|
||||||
|
):
|
||||||
|
start_time = time()
|
||||||
|
|
||||||
|
product = await mongo.products_collection.find_one({"id": product_id})
|
||||||
|
if not product:
|
||||||
|
raise HTTPException(status_code=404, detail="Product not found")
|
||||||
|
|
||||||
|
extension = os.path.splitext(upload_file.filename)[1] or ".pdf"
|
||||||
|
filename = f"{product_id}{extension}"
|
||||||
|
file_path = os.path.join(BARCODES_FOLDER, filename)
|
||||||
|
|
||||||
|
with open(file_path, "wb") as file:
|
||||||
|
file.write(await upload_file.read())
|
||||||
|
|
||||||
|
document = fitz.open(file_path)
|
||||||
|
page = document.load_page(0)
|
||||||
|
pixmap = page.get_pixmap()
|
||||||
|
output_image_filename = f"{product_id}.png"
|
||||||
|
output_image_path = os.path.join(BARCODES_FOLDER, output_image_filename)
|
||||||
|
pixmap.save(output_image_path)
|
||||||
|
document.close()
|
||||||
|
|
||||||
|
barcode_image_url = f"{str(request.base_url).rstrip('/')}/api/files/barcodes/{output_image_filename}"
|
||||||
|
return response({
|
||||||
|
"barcodeImageUrl": barcode_image_url,
|
||||||
|
"message": "Штрихкод успешно загружен!",
|
||||||
|
"ok": True
|
||||||
|
}, start_time=start_time)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/delete-image/{product_id}", tags=[""])
|
||||||
|
async def delete_product_barcode_image(product_id: int):
|
||||||
|
start_time = time()
|
||||||
|
|
||||||
|
pdf_path = os.path.join(BARCODES_FOLDER, f"{product_id}.pdf")
|
||||||
|
png_path = os.path.join(BARCODES_FOLDER, f"{product_id}.png")
|
||||||
|
|
||||||
|
if not os.path.exists(pdf_path):
|
||||||
|
raise HTTPException(status_code=404, detail="Barcode PDF not found")
|
||||||
|
|
||||||
|
if os.path.exists(pdf_path):
|
||||||
|
os.remove(pdf_path)
|
||||||
|
|
||||||
|
if os.path.exists(png_path):
|
||||||
|
os.remove(png_path)
|
||||||
|
|
||||||
|
return response({
|
||||||
|
"message": "Штрихкод успешно удалён!",
|
||||||
|
"ok": True
|
||||||
|
}, start_time=start_time)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/image/{product_id}", tags=[""])
|
||||||
|
async def get_product_barcode_image(request: Request, product_id: int):
|
||||||
|
start_time = time()
|
||||||
|
|
||||||
|
image_filename = f"{product_id}.png"
|
||||||
|
image_path = os.path.join(BARCODES_FOLDER, image_filename)
|
||||||
|
|
||||||
|
if not os.path.exists(image_path):
|
||||||
|
raise HTTPException(status_code=404, detail="Barcode image not found")
|
||||||
|
|
||||||
|
barcode_image_url = f"{str(request.base_url).rstrip('/')}/api/files/barcodes/{image_filename}"
|
||||||
|
return response({
|
||||||
|
"barcodeImageUrl": barcode_image_url,
|
||||||
|
"ok": True,
|
||||||
|
"message": "Штрихкод найден!"
|
||||||
|
}, start_time=start_time)
|
||||||
40
app/api/v1/product/images.py
Normal file
40
app/api/v1/product/images.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import os
|
||||||
|
from time import time
|
||||||
|
|
||||||
|
from fastapi import APIRouter, UploadFile, File, HTTPException, Request
|
||||||
|
|
||||||
|
from app import mongo
|
||||||
|
from app.utils.response_util import response
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
STATIC_FOLDER = "static"
|
||||||
|
PRODUCTS_FOLDER = os.path.join(STATIC_FOLDER, "images")
|
||||||
|
os.makedirs(PRODUCTS_FOLDER, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/upload/{product_id}", tags=["Products"])
|
||||||
|
async def upload_product_image(
|
||||||
|
request: Request,
|
||||||
|
product_id: int,
|
||||||
|
upload_file: UploadFile = File(...)
|
||||||
|
):
|
||||||
|
start_time = time()
|
||||||
|
|
||||||
|
product = await mongo.products_collection.find_one({"id": product_id})
|
||||||
|
if not product:
|
||||||
|
raise HTTPException(status_code=404, detail="Product not found")
|
||||||
|
|
||||||
|
extension = os.path.splitext(upload_file.filename)[1] or ".jpg"
|
||||||
|
filename = f"{product_id}{extension}"
|
||||||
|
file_path = os.path.join(PRODUCTS_FOLDER, filename)
|
||||||
|
|
||||||
|
with open(file_path, "wb") as file:
|
||||||
|
file.write(await upload_file.read())
|
||||||
|
|
||||||
|
image_url = f"{str(request.base_url).rstrip('/')}/api/files/images/{filename}"
|
||||||
|
return response({
|
||||||
|
"imageUrl": image_url,
|
||||||
|
"message": "Фото успешно загружено!",
|
||||||
|
"ok": True
|
||||||
|
}, start_time=start_time)
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user