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 typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends, Response
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
import enums.service
|
import enums.service
|
||||||
from backend.dependecies import SessionDependency
|
from backend.dependecies import SessionDependency
|
||||||
from backend.session import get_session
|
from backend.session import get_session
|
||||||
|
from generators.price_list_pdf_generator.generator import PriceListPdfGenerator
|
||||||
from schemas.base import BaseEnumListSchema
|
from schemas.base import BaseEnumListSchema
|
||||||
from schemas.service import *
|
from schemas.service import *
|
||||||
from services.auth import guest_user, authorized_user
|
from services.auth import guest_user, authorized_user
|
||||||
@@ -84,6 +86,17 @@ async def reorder(
|
|||||||
return await ServiceService(session).reorder(request)
|
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
|
# endregion
|
||||||
|
|
||||||
# region Categories
|
# 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