diff --git a/generators/price_list_pdf_generator/__init__.py b/generators/price_list_pdf_generator/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/generators/price_list_pdf_generator/generator.py b/generators/price_list_pdf_generator/generator.py new file mode 100644 index 0000000..70c2f9c --- /dev/null +++ b/generators/price_list_pdf_generator/generator.py @@ -0,0 +1,88 @@ +from collections import defaultdict +from io import BytesIO + +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import joinedload +from weasyprint import HTML, CSS + +from constants import ENV, APP_PATH +from enums.service import ServiceType +from models import Service, ServiceCategory + + +class PriceListPdfGenerator: + def __init__(self, session: AsyncSession): + self._session = session + + async def _get_services_data(self): + # Получаем услуги из базы данных, отсортированные по рангу + services = (await ( + self._session.scalars( + select(Service) + .options(joinedload(Service.category)) + .filter(Service.is_deleted == False) + .order_by(Service.rank) + ) + )).all() + + # Группируем услуги по типу сервиса и категории + intermediate_result = defaultdict(lambda: defaultdict(list)) + + for service in services: + intermediate_result[service.service_type][service.category_id].append(service) + + # Формируем окончательный результат с сортировкой категорий + final_result = defaultdict(dict) + + for service_type, categories_dict in intermediate_result.items(): + # Извлекаем уникальные категории + categories = {service.category for services in categories_dict.values() for service in services} + + # Определяем функцию сортировки категорий по рангу + def category_sort_key(category): + if service_type == ServiceType.DEAL_SERVICE: + return category.deal_service_rank + else: + return category.product_service_rank + + # Сортируем категории по определенному рангу + sorted_categories = sorted(categories, key=category_sort_key) + + # Строим словарь категорий в отсортированном порядке + sorted_categories_dict = {} + for category in sorted_categories: + sorted_categories_dict[category.id] = categories_dict[category.id] + + final_result[service_type] = sorted_categories_dict + + final_final_result = {} + for service_type in [ServiceType.DEAL_SERVICE, ServiceType.PRODUCT_SERVICE]: + final_final_result[service_type] = final_result[service_type] + return dict(final_final_result) + + async def _create_price_list_html(self): + categories = await self._session.scalars(select(ServiceCategory)) + categories_dict = {category.id: category.name for category in categories} + + services_data = await self._get_services_data() + + service_type_dict = { + ServiceType.DEAL_SERVICE: "Общие услуги", + ServiceType.PRODUCT_SERVICE: "Услуги фулфилмента", + } + + template = ENV.get_template("price-list.html") + + result = template.render({ + "services_data": services_data, + "categories_dict": categories_dict, + "service_type_dict": service_type_dict, + }) + return result + + async def create_price_list_pdf(self) -> BytesIO: + doc = await self._create_price_list_html() + pdf_file = BytesIO() + HTML(string=doc).write_pdf(pdf_file, stylesheets=[CSS(APP_PATH + '/static/css/price-list.css')]) + return pdf_file diff --git a/routers/service.py b/routers/service.py index 53751a7..d43b3e2 100644 --- a/routers/service.py +++ b/routers/service.py @@ -1,11 +1,13 @@ +from io import BytesIO from typing import Annotated -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, Response from sqlalchemy.ext.asyncio import AsyncSession import enums.service from backend.dependecies import SessionDependency from backend.session import get_session +from generators.price_list_pdf_generator.generator import PriceListPdfGenerator from schemas.base import BaseEnumListSchema from schemas.service import * from services.auth import guest_user, authorized_user @@ -84,6 +86,17 @@ async def reorder( return await ServiceService(session).reorder(request) +@service_router.get( + '/price-list', + operation_id='get_price_list', + # dependencies=[Depends(authorized_user)], +) +async def get_price_list( + session: Annotated[AsyncSession, Depends(get_session)], +): + pdf_file: BytesIO = await PriceListPdfGenerator(session).create_price_list_pdf() + return Response(pdf_file.getvalue(), media_type='application/pdf') + # endregion # region Categories diff --git a/static/css/price-list.css b/static/css/price-list.css new file mode 100644 index 0000000..9ffb413 --- /dev/null +++ b/static/css/price-list.css @@ -0,0 +1,98 @@ +@media print { + #header, + #footer, + #nav { + display: none !important; + } +} + +html { + font-family: sans-serif; +} + +.doc-container { + margin: -30px 0; + padding: 0 15px; + font-size: 13px; +} + +img { + width: 170px; + margin-bottom: 15px; +} + +table { + width: 100%; + border-collapse: separate; + border-spacing: 0; + border: solid gray 2px; + border-radius: 15px; + margin-bottom: 10px; +} + +thead { + display: table-header-group; +} + +tfoot { + display: table-footer-group; +} + +td:nth-child(5) { + border-right: 1px solid white; +} + +table tbody tr:nth-child(odd) { + background-color: #e5e5e5; +} + +tr { + page-break-inside: avoid; +} + +td, th { + padding: 10px 13px; +} + +td:first-child, th:first-child { + padding-left: 50px; +} + +td:last-child, th:last-child { + padding-right: 50px; +} + +td:first-child, th:first-child { + text-align: left; +} + +td:first-child { + white-space: normal; + word-wrap: break-word; +} + +td:not(:first-child), th:not(:first-child) { + text-align: right; +} + +td:not(:first-child) { + overflow: hidden; + text-overflow: ellipsis; +} + +table tfoot { + border-top: 2px solid black; +} + +.category-title { + text-align: center; + background-color: #e5e5e5; +} + +.service-type { + text-align: center; + font-size: 27px; + margin-top: 20px; + margin-bottom: 10px; + font-weight: bold; +} \ No newline at end of file diff --git a/templates/documents/price-list.html b/templates/documents/price-list.html new file mode 100644 index 0000000..143a12f --- /dev/null +++ b/templates/documents/price-list.html @@ -0,0 +1,76 @@ + + + + + + + Price-list + + + +
+
+ Логотип DENCO +
+ {% for service_type, obj in services_data.items() %} +
+
+ {{ service_type_dict[service_type] }} +
+ {% for category_id, services in obj.items() %} + + + + + + + + + + + + + + {% for service in services %} + + + {% if service.price_ranges %} + {% if service.price_ranges|length == 1 %} + + {% else %} + + {% endif %} + {% else %} + + {% endif %} + + {% endfor %} + +
+ {{ categories_dict[category_id] }} +
УслугаЦена
{{ service.name }}{{ service.price_ranges[0].price }} ₽ + {% for price_range in service.price_ranges[:-1] %} +
{{ price_range.from_quantity }} шт - {{ price_range.to_quantity }} + шт: {{ price_range.price|int }} ₽ +
+ {% endfor %} + {% with last_price=service.price_ranges[-1] %} +
от {{ last_price.from_quantity }} шт: {{ last_price.price|int }} ₽
+ {% endwith %} +
{{ service.price|int }} ₽
+ {% endfor %} +
+ {% endfor %} +
+ + \ No newline at end of file