423 lines
15 KiB
Python
423 lines
15 KiB
Python
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}"
|