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}"