Merge remote-tracking branch 'origin/master'
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