first commit

This commit is contained in:
2025-07-24 20:13:47 +03:00
commit 94b7585f8b
175 changed files with 85264 additions and 0 deletions

View 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')

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

422
app/api/v1/deal/deal.py Normal file
View 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}"

View 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)

View 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)

View 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
View 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)

View 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)