feat: executors and grouping by article in deal document

This commit is contained in:
2024-10-02 02:40:54 +04:00
parent 766ded04a5
commit ca6e7d5b37
12 changed files with 308 additions and 169 deletions

View File

@@ -10,6 +10,7 @@ from utils.code128 import encode128
ENV = Environment(loader=FileSystemLoader(Path("templates") / Path("documents")))
ENV.globals['now'] = datetime.now
ENV.globals['encode128'] = encode128
ENV.globals['format_number'] = lambda x: '{:,}'.format(x).replace(',', ' ')
APP_PATH = os.path.dirname(sys.executable) if getattr(sys, 'frozen', False) else os.path.dirname(__file__)

0
generators/__init__.py Normal file
View File

View File

@@ -0,0 +1,20 @@
from typing import TypedDict, List, Dict, Tuple, Optional
from models import DealProduct, Deal, DealStatusHistory
class DocumentDealProductData(TypedDict):
deal_products: List[DealProduct]
total_one_product: int
quantity: int
additional_info: Optional[str]
class DocumentDealData(TypedDict):
deal: Deal
general_services_total: int
products: Dict[str, DocumentDealProductData]
current_status_str: str
last_status: DealStatusHistory
product_images: Tuple[str]

View File

@@ -0,0 +1,102 @@
from io import BytesIO
from typing import List, Dict, Optional
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload, joinedload
from weasyprint import HTML, CSS
from constants import DEAL_STATUS_STR, ENV, APP_PATH
from generators.deal_pdf_generator.deal_data import DocumentDealProductData
from models import Deal, DealProduct, DealService as DealServiceModel, Product
from utils.images_fetcher import fetch_images
class DealPdfGenerator:
def __init__(self, session: AsyncSession):
self._session = session
@staticmethod
async def _get_product_services_totals(deal: Deal) -> List[Dict[str, int]]:
totals: List[Dict[str, int]] = []
for product in deal.products:
total_one_product = sum((service.price for service in product.services))
total = total_one_product * product.quantity
totals.append({"total_one_product": total_one_product, "total": total})
return totals
@staticmethod
async def _group_deal_products_by_products(deal_products: List[DealProduct]) -> Dict[str, DocumentDealProductData]:
products: Dict[str, DocumentDealProductData] = {}
additional_info: Optional[str]
for deal_product in deal_products:
# Для группировки по артикулу и услугам
key = f"{deal_product.product.article} - " + ",".join(
str(service.service_id) for service in deal_product.services
)
if key not in products:
total_one_product = sum(service.price for service in deal_product.services)
products[key] = {
"deal_products": [deal_product],
"total_one_product": total_one_product,
"quantity": deal_product.quantity,
"additional_info": deal_product.product.additional_info,
}
else:
products[key]["deal_products"].append(deal_product)
products[key]["quantity"] += deal_product.quantity
if not products[key]["additional_info"]:
products[key]["additional_info"] = deal_product.product.additional_info
return products
async def _create_detailed_deal_document_html(self, deal_id: int):
deal: Deal | None = await self._session.scalar(
select(Deal)
.where(Deal.id == deal_id)
.options(
selectinload(Deal.products).selectinload(DealProduct.services),
selectinload(Deal.products).selectinload(DealProduct.product).selectinload(Product.barcodes),
selectinload(Deal.services).selectinload(DealServiceModel.service),
selectinload(Deal.status_history),
joinedload(Deal.client),
joinedload(Deal.shipping_warehouse),
)
)
if not deal:
return ""
products = await self._group_deal_products_by_products(deal.products)
product_urls: List[Optional[str]] = []
for product in products.values():
if len(product["deal_products"][0].product.images) > 0:
product_urls.append(product["deal_products"][0].product.images[0].image_url)
else:
product_urls.append(None)
product_images = await fetch_images(product_urls)
document_deal_data = {
"deal": deal,
"general_services_total": sum((service.price * service.quantity for service in deal.services)),
"products": products,
"current_status_str": DEAL_STATUS_STR[deal.current_status],
"last_status": max(deal.status_history, key=lambda status: status.changed_at),
"product_images": product_images,
}
template = ENV.get_template("deal/deal.html")
result = template.render({"data": document_deal_data, "sign_place_text": "_" * 22})
return result
async def create_detailed_deal_document_pdf(self, deal_id) -> BytesIO:
doc = await self._create_detailed_deal_document_html(deal_id)
pdf_file = BytesIO()
HTML(string=doc).write_pdf(pdf_file, stylesheets=[CSS(APP_PATH + '/static/css/deal.css')])
return pdf_file

View File

@@ -7,6 +7,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from backend.dependecies import SessionDependency, CurrentUserDependency
from backend.session import get_session
from generators.deal_pdf_generator.generator import DealPdfGenerator
from models import User
from schemas.barcode import GetDealProductsBarcodesPdfRequest, GetDealProductsBarcodesPdfResponse
from schemas.deal import *
@@ -202,7 +203,7 @@ async def get_detailed_deal_document(
deal_id: int,
session: Annotated[AsyncSession, Depends(get_session)],
):
pdf_file: BytesIO = await DealService(session).create_detailed_deal_document_pdf(deal_id)
pdf_file: BytesIO = await DealPdfGenerator(session).create_detailed_deal_document_pdf(deal_id)
return Response(pdf_file.getvalue(), media_type='application/pdf')

View File

@@ -1,9 +1,10 @@
import datetime
from typing import List, Optional, Union
from collections import defaultdict
from typing import List, Optional, Union, Dict, Tuple, TypedDict
from pydantic import constr, field_validator
from models import ServiceCategoryPrice, ServicePriceCategory
from models import ServiceCategoryPrice, ServicePriceCategory, Deal, Product, DealProduct, DealStatusHistory
from schemas.base import BaseSchema, OkMessageSchema
from schemas.billing import DealBillRequestSchema
from schemas.client import ClientSchema

View File

@@ -1,18 +1,12 @@
from io import BytesIO
from typing import Dict
import lexorank
from fastapi import HTTPException
from sqlalchemy import select, func, update, delete, insert
from sqlalchemy.orm import joinedload, selectinload
from starlette import status
from weasyprint import HTML, CSS
import constants
import models.deal
import models.secondary
from constants import ENV, DEAL_STATUS_STR
from models import User, Service, Client, DealProduct, DealService as DealServiceModel, Product
from models import User, Service, Client
from models.deal import *
from schemas.client import ClientDetailsSchema
from schemas.deal import *
@@ -21,7 +15,6 @@ from services.base import BaseService
from services.client import ClientService
from services.service import ServiceService
from services.shipping_warehouse import ShippingWarehouseService
from utils.images_fetcher import fetch_images
class DealService(BaseService):
@@ -1008,64 +1001,3 @@ class DealService(BaseService):
except Exception as e:
await self.session.rollback()
return DealCompleteResponse(ok=False, message=str(e))
# region Deal documents
@staticmethod
async def _get_product_services_totals(deal: Deal) -> List[Dict[str, int]]:
totals: List[Dict[str, int]] = []
for product in deal.products:
total_one_product = sum((service.price for service in product.services))
total = total_one_product * product.quantity
totals.append({"total_one_product": total_one_product, "total": total})
return totals
async def _create_detailed_deal_document_html(self, deal_id: int):
deal: Deal | None = await self.session.scalar(
select(Deal)
.where(Deal.id == deal_id)
.options(
selectinload(Deal.products).selectinload(DealProduct.services),
selectinload(Deal.products).selectinload(DealProduct.product).selectinload(Product.barcodes),
selectinload(Deal.services).selectinload(DealServiceModel.service),
selectinload(Deal.status_history),
joinedload(Deal.client),
joinedload(Deal.shipping_warehouse),
)
)
if not deal:
return ""
last_status: DealStatusHistory = max(deal.status_history, key=lambda status: status.changed_at)
general_services_total = sum((service.price * service.quantity for service in deal.services))
product_services_totals: List[Dict[str, int]] = await self._get_product_services_totals(deal)
template = ENV.get_template("deal/deal.html")
product_urls: List[Optional[str]] = []
for product in deal.products:
if len(product.product.images) > 0:
product_urls.append(product.product.images[0].image_url)
else:
product_urls.append(None)
product_images = await fetch_images(product_urls)
result = template.render({
"general_services_total": general_services_total,
"product_services_totals": product_services_totals,
"deal": deal,
"current_status_str": DEAL_STATUS_STR[deal.current_status],
"last_status": last_status,
"product_images": product_images,
})
return result
async def create_detailed_deal_document_pdf(self, deal_id) -> BytesIO:
doc = await self._create_detailed_deal_document_html(deal_id)
pdf_file = BytesIO()
HTML(string=doc).write_pdf(pdf_file, stylesheets=[CSS(constants.APP_PATH + '/static/css/deal.css')])
return pdf_file
# endregion

View File

@@ -18,6 +18,10 @@ hr {
margin-right: -100px;
}
.sign-place {
margin-top: 25px;
}
.doc-container {
margin: -30px 0;
padding: 0 15px;
@@ -42,7 +46,7 @@ hr {
.small-text {
font-size: 12px;
margin-bottom: 4px;
margin-bottom: 3px;
}
.medium-text {
@@ -67,7 +71,7 @@ hr {
}
.product-data > * {
width: 200px;
width: 170px;
padding-right: 170px;
}
@@ -152,8 +156,16 @@ table tfoot {
border-radius: 5px;
}
.barcode-container-table {
margin: 10px 0 -15px -25px;
display: flex;
white-space: nowrap;
flex-direction: column;
align-items: center;
}
.barcode-container {
margin: 15px 0 -5px 355px;
margin: 15px 0 -5px 375px;
display: flex;
white-space: nowrap;
flex-direction: column;
@@ -162,7 +174,7 @@ table tfoot {
.barcode {
font-family: "Libre Barcode 128", cursive;
font-size: 30pt;
font-size: 25pt;
transform: scale(1, 1.5);
}

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +1,7 @@
<!--#region Header -->
<div class="deal-name-id-container">
<div class="medium-text bold">
Название сделки: {{ deal.name }}
Название сделки: {{ data.deal.name }}
</div>
</div>
<hr/>
@@ -9,29 +9,29 @@
Дата создания: {{ now().strftime("%d.%m.%Y, %H:%M") }}
</div>
<div class="medium-text">
Текущий статус: {{ current_status_str }}
Текущий статус: {{ data.current_status_str }}
</div>
<div class="medium-text">
{% if deal.comment %}
Комментарий к сделке: {{ deal.comment }}
{% if data.deal.comment %}
Комментарий к сделке: {{ data.deal.comment }}
{% endif %}
</div>
<hr/>
<!--#endregion -->
<!--#region General services -->
<div class="large-text bold">
Клиент: {{ deal.client.name }}
Клиент: {{ data.deal.client.name }}
</div>
<div class="medium-text bold">
Дата отгрузки: {{ last_status.next_status_deadline.strftime("%d.%m.%Y") }}
Дата отгрузки: {{ data.last_status.next_status_deadline.strftime("%d.%m.%Y") }}
</div>
{% if deal.base_marketplace.name %}
<div class="medium-text bold">Маркетплейс: {{ deal.base_marketplace.name }}</div>
{% if data.deal.base_marketplace.name %}
<div class="medium-text bold">Маркетплейс: {{ data.deal.base_marketplace.name }}</div>
{% endif %}
{% if deal.shipping_warehouse.name %}
<div class="medium-text bold">Склад отгрузки: {{ deal.shipping_warehouse.name }}</div>
{% if data.deal.shipping_warehouse.name %}
<div class="medium-text bold">Склад отгрузки: {{ data.deal.shipping_warehouse.name }}</div>
{% endif %}
{% if deal.services|length > 0 %}
{% if data.deal.services|length > 0 %}
<table>
<thead>
<tr>
@@ -43,7 +43,7 @@
</tfoot>
</thead>
<tbody>
{% for service in deal.services %}
{% for service in data.deal.services %}
<tr>
<td>{{ service.service.name }}</td>
<td>{{ '{:,}'.format(service.quantity) }} шт.</td>
@@ -53,7 +53,7 @@
</tbody>
</table>
<div class="medium-text total align-right bold">
Итого: {{ general_services_total }} Р
Итого: {{ data.general_services_total }} Р
</div>
{% else %}
<div class="medium-text total align-right bold">

View File

@@ -29,7 +29,7 @@
font-size: 18px;
}
@top-right {
content: "ID:{{ deal.id }}";
content: "ID:{{ data.deal.id }}";
border: solid black 1px;
border-radius: 6px;
padding: 12px 2px;
@@ -45,7 +45,7 @@
<div>
<div>
{% include "deal/deal-services.html" %}
{% for product in deal.products %}
{% for product_data in data.products.values() %}
{% with index = loop.index %}
{% include "deal/deal-product-services.html" %}
{% endwith %}