feat: price list generator
This commit is contained in:
0
generators/price_list_pdf_generator/__init__.py
Normal file
0
generators/price_list_pdf_generator/__init__.py
Normal file
88
generators/price_list_pdf_generator/generator.py
Normal file
88
generators/price_list_pdf_generator/generator.py
Normal file
@@ -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
|
||||
@@ -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
|
||||
|
||||
98
static/css/price-list.css
Normal file
98
static/css/price-list.css
Normal file
@@ -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;
|
||||
}
|
||||
76
templates/documents/price-list.html
Normal file
76
templates/documents/price-list.html
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user