feat: end-point for pdf of all deal's barcodes, client and marketplace info in bill of payment

This commit is contained in:
2024-09-28 19:38:06 +04:00
parent d9c43624c0
commit 489f65a087
8 changed files with 169 additions and 57 deletions

View File

@@ -1,13 +1,11 @@
from abc import abstractmethod from abc import abstractmethod
from typing import List, Dict
from models import ProductBarcode, Product, BarcodeTemplate from models import BarcodeTemplate, Product
class BaseBarcodeGenerator: class BaseBarcodeGenerator:
@abstractmethod @abstractmethod
def generate(self, def generate(self, data: List[Dict[str, str | Product | BarcodeTemplate | int]]):
barcode: ProductBarcode,
product: Product,
template: BarcodeTemplate):
pass pass

View File

@@ -1,26 +1,40 @@
from typing import List, Dict
from barcodes.attributes import AttributeWriterFactory from barcodes.attributes import AttributeWriterFactory
from barcodes.generator.base import BaseBarcodeGenerator from barcodes.generator.base import BaseBarcodeGenerator
from barcodes.pdf import PDFGenerator from barcodes.pdf import PDFGenerator
from models import ProductBarcode, Product, BarcodeTemplate from models import Product, BarcodeTemplate
from schemas.barcode import PdfBarcodeGenData
class DefaultBarcodeGenerator(BaseBarcodeGenerator): class DefaultBarcodeGenerator(BaseBarcodeGenerator):
def generate(self, def generate(self, barcodes_data: List[Dict[str, str | Product | BarcodeTemplate | int]]):
barcode: str,
product: Product,
template: BarcodeTemplate,
quantity: int = 1):
pdf_generator = PDFGenerator() pdf_generator = PDFGenerator()
attributes = {}
for attribute in template.attributes: pdf_barcodes_gen_data: List[PdfBarcodeGenData] = []
attribute_getter = AttributeWriterFactory.get_writer(attribute.key)
if not attribute_getter: for barcode_data in barcodes_data:
continue attributes = {}
value = attribute_getter.get_value(product) for attribute in barcode_data["template"].attributes:
if not value or not value.strip(): attribute_getter = AttributeWriterFactory.get_writer(attribute.key)
continue if not attribute_getter:
attributes[attribute.name] = value continue
for additional_attribute in template.additional_attributes: value = attribute_getter.get_value(barcode_data["product"])
attributes[additional_attribute.name] = additional_attribute.value if not value or not value.strip():
barcode_text = '<br/>'.join([f'{key}: {value}' for key, value in attributes.items()]) continue
return pdf_generator.generate(barcode, barcode_text, num_duplicates=quantity) attributes[attribute.name] = value
for additional_attribute in barcode_data["template"].additional_attributes:
attributes[additional_attribute.name] = additional_attribute.value
barcode_text = '<br/>'.join([f'{key}: {value}' for key, value in attributes.items()])
pdf_barcodes_gen_data.append(
PdfBarcodeGenData(
barcode_value=barcode_data["barcode"],
text=barcode_text,
num_duplicates=barcode_data["quantity"]
)
)
print(f"value = {barcode_data['barcode']}, text = {barcode_text}, num = {barcode_data['quantity']}")
return pdf_generator.generate(pdf_barcodes_gen_data)

View File

@@ -1,5 +1,7 @@
import os import os
from io import BytesIO from io import BytesIO
from typing import List
from reportlab.graphics.barcode import code128 from reportlab.graphics.barcode import code128
from reportlab.lib.pagesizes import mm from reportlab.lib.pagesizes import mm
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
@@ -8,6 +10,7 @@ from reportlab.pdfbase.ttfonts import TTFont
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, PageBreak from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, PageBreak
from constants import APP_PATH from constants import APP_PATH
from schemas.barcode import PdfBarcodeGenData
class PDFGenerator: class PDFGenerator:
@@ -82,7 +85,7 @@ class PDFGenerator:
buffer.seek(0) buffer.seek(0)
return buffer return buffer
def generate(self, barcode_value, text, num_duplicates=1): def generate(self, barcodes_data: List[PdfBarcodeGenData]):
buffer = BytesIO() buffer = BytesIO()
# Создаем документ с указанным размером страницы # Создаем документ с указанным размером страницы
@@ -93,43 +96,58 @@ class PDFGenerator:
topMargin=1 * mm, topMargin=1 * mm,
bottomMargin=1 * mm) bottomMargin=1 * mm)
# Создаем абзац с новым стилем # Список элементов для добавления в документ
paragraph = Paragraph(text, self.small_style) elements = []
product_barcode_canvases = []
product_counter = 0
product_barcodes_counter = 0
# Получаем ширину и высоту абзаца for barcode_data in barcodes_data:
paragraph_width, paragraph_height = paragraph.wrap(self.page_width - 2 * mm, self.page_height) # Создаем абзац с новым стилем
paragraph = Paragraph(barcode_data.text, self.small_style)
# Рассчитываем доступное пространство для штрихкода # Получаем ширину и высоту абзаца
human_readable_height = 6 * mm # Высота human-readable текста paragraph_width, paragraph_height = paragraph.wrap(self.page_width - 2 * mm, self.page_height)
space_between_text_and_barcode = 4 * mm # Отступ между текстом и штрихкодом
barcode_height = self.page_height - paragraph_height - human_readable_height - space_between_text_and_barcode - 4 * mm # Учитываем поля и отступы
# Создаем штрихкод # Рассчитываем доступное пространство для штрихкода
available_width = self.page_width - 2 * mm # Учитываем поля human_readable_height = 6 * mm # Высота human-readable текста
space_between_text_and_barcode = 4 * mm # Отступ между текстом и штрихкодом
barcode_height = self.page_height - paragraph_height - human_readable_height - space_between_text_and_barcode - 4 * mm # Учитываем поля и отступы
# Приблизительное количество элементов в штрихкоде Code 128 для средней длины # Создаем штрихкод
num_elements = 11 * len(barcode_value) # Примерная оценка: 11 элементов на символ available_width = self.page_width - 2 * mm # Учитываем поля
# Рассчитываем ширину штриха # Приблизительное количество элементов в штрихкоде Code 128 для средней длины
bar_width = available_width / num_elements num_elements = 11 * len(barcode_data.barcode_value) # Примерная оценка: 11 элементов на символ
barcode = code128.Code128(barcode_value,
barWidth=bar_width, # Рассчитываем ширину штриха
barHeight=barcode_height, bar_width = available_width / num_elements
humanReadable=True) barcode = code128.Code128(barcode_data.barcode_value,
barWidth=bar_width,
barHeight=barcode_height,
humanReadable=True)
product_barcode_canvases.append(barcode)
# Добавление штрихкодов в список элементов документа
for _ in range(barcode_data.num_duplicates):
elements.append(paragraph)
elements.append(Spacer(1, space_between_text_and_barcode)) # Отступ между текстом и штрихкодом
elements.append(PageBreak())
# Функция для отрисовки штрихкода на canvas # Функция для отрисовки штрихкода на canvas
def add_barcode(canvas, doc): def add_barcode(canvas, doc):
barcode_width = barcode.width nonlocal product_barcodes_counter, product_counter
barcode_canvas = product_barcode_canvases[product_counter]
barcode_width = barcode_canvas.width
barcode_x = (self.page_width - barcode_width) / 2 # Центрируем штрихкод barcode_x = (self.page_width - barcode_width) / 2 # Центрируем штрихкод
barcode_y = human_readable_height + 2 * mm # Размещаем штрихкод снизу с учетом отступа barcode_y = human_readable_height + 2 * mm # Размещаем штрихкод снизу с учетом отступа
barcode.drawOn(canvas, barcode_x, barcode_y) barcode_canvas.drawOn(canvas, barcode_x, barcode_y)
# Создаем список элементов для добавления в документ product_barcodes_counter += 1
elements = [] if product_barcodes_counter >= barcodes_data[product_counter].num_duplicates:
for _ in range(num_duplicates): product_barcodes_counter = 0
elements.append(paragraph) product_counter += 1
elements.append(Spacer(1, space_between_text_and_barcode)) # Отступ между текстом и штрихкодом
elements.append(PageBreak())
# Создаем документ # Создаем документ
doc.build(elements[:-1], onFirstPage=add_barcode, onLaterPages=add_barcode) # Убираем последний PageBreak doc.build(elements[:-1], onFirstPage=add_barcode, onLaterPages=add_barcode) # Убираем последний PageBreak

View File

@@ -1,3 +1,4 @@
import base64
from io import BytesIO from io import BytesIO
from typing import Annotated from typing import Annotated
@@ -7,8 +8,10 @@ from sqlalchemy.ext.asyncio import AsyncSession
from backend.dependecies import SessionDependency, CurrentUserDependency from backend.dependecies import SessionDependency, CurrentUserDependency
from backend.session import get_session from backend.session import get_session
from models import User from models import User
from schemas.barcode import GetProductBarcodePdfResponse, GetDealProductsBarcodesPdfRequest
from schemas.deal import * from schemas.deal import *
from services.auth import get_current_user, authorized_user, guest_user from services.auth import get_current_user, authorized_user, guest_user
from services.barcode import BarcodeService
from services.billing import BillingService from services.billing import BillingService
from services.deal import DealService from services.deal import DealService
@@ -391,4 +394,24 @@ async def add_kit_to_deal_product(
user: CurrentUserDependency user: CurrentUserDependency
): ):
return await DealService(session).add_kit_to_deal_product(user, request) return await DealService(session).add_kit_to_deal_product(user, request)
@deal_router.post(
'/barcodes/get-pdf',
operation_id='get_deal_barcodes_pdf',
response_model=GetProductBarcodePdfResponse
)
async def get_deal_products_barcodes_pdf(
request: GetDealProductsBarcodesPdfRequest,
session: Annotated[AsyncSession, Depends(get_session)]
):
filename, pdf_buffer = await BarcodeService(session).get_deal_barcodes_pdf(request)
pdf_buffer: BytesIO
base64_string = base64.b64encode(pdf_buffer.read()).decode('utf-8')
return GetProductBarcodePdfResponse(
base64_string=base64_string,
filename=filename,
mime_type='application/pdf'
)
# endregion # endregion

View File

@@ -47,6 +47,12 @@ class BarcodeSchema(BaseSchema):
additional_field: str | None = None additional_field: str | None = None
class PdfBarcodeGenData(BaseSchema):
barcode_value: str
text: str
num_duplicates: int = 1
# endregion # endregion
# region Requests # region Requests
@@ -81,6 +87,10 @@ class GetProductBarcodePdfRequest(GetProductBarcodeRequest):
quantity: int quantity: int
class GetDealProductsBarcodesPdfRequest(BaseSchema):
deal_id: int
# endregion # endregion
# region Responses # region Responses

View File

@@ -1,10 +1,12 @@
from typing import Dict
from sqlalchemy import select, update, insert from sqlalchemy import select, update, insert
from sqlalchemy.orm import selectinload, joinedload from sqlalchemy.orm import selectinload, joinedload
from barcodes.attributes import AttributeWriterFactory from barcodes.attributes import AttributeWriterFactory
from barcodes.generator.default_generator import DefaultBarcodeGenerator from barcodes.generator.default_generator import DefaultBarcodeGenerator
from models import BarcodeTemplate, BarcodeTemplateAttribute, barcode_template_attribute_link, Product, \ from models import BarcodeTemplate, BarcodeTemplateAttribute, barcode_template_attribute_link, Product, \
BarcodeTemplateAdditionalField, BarcodeTemplateSize BarcodeTemplateAdditionalField, BarcodeTemplateSize, Deal, DealProduct
from schemas.barcode import * from schemas.barcode import *
from services.base import BaseService from services.base import BaseService
@@ -91,13 +93,50 @@ class BarcodeService(BaseService):
default_generator = DefaultBarcodeGenerator() default_generator = DefaultBarcodeGenerator()
filename = f'{product.id}_barcode.pdf' filename = f'{product.id}_barcode.pdf'
pdf_buffer = default_generator.generate( pdf_buffer = default_generator.generate(
request.barcode, [{
product, "barcode": request.barcode,
barcode_template, "product": product,
request.quantity "template": barcode_template,
"quantity": request.quantity
}]
) )
return filename, pdf_buffer return filename, pdf_buffer
async def get_deal_barcodes_pdf(self, request: GetDealProductsBarcodesPdfRequest) -> GetProductBarcodeResponse:
stmt = (
select(Deal)
.options(
selectinload(Deal.products).joinedload(DealProduct.product).selectinload(Product.client),
selectinload(Deal.products).joinedload(DealProduct.product).joinedload(Product.barcodes)
)
.filter(Deal.id == request.deal_id)
)
query = await self.session.execute(stmt)
deal: Deal = query.scalar()
if not deal:
raise ValueError('Сделка не найдена')
barcodes_data: List[Dict[str, str | Product | BarcodeTemplate | int]] = []
for deal_product in deal.products:
product_request = GetProductBarcodeRequest(
product_id=deal_product.product_id,
barcode="",
barcode_template_id=deal_product.product.barcode_template_id,
)
barcode_template = await self._get_barcode_template(product_request, deal_product.product)
barcodes_data.append({
"barcode": deal_product.product.barcodes[0].barcode,
"product": deal_product.product,
"template": barcode_template,
"quantity": deal_product.quantity
})
default_generator = DefaultBarcodeGenerator()
filename = f'{deal.id}_deal_barcodes.pdf'
pdf_buffer = default_generator.generate(barcodes_data)
return filename, pdf_buffer
# endregion # endregion
# region Template # region Template

View File

@@ -4,7 +4,7 @@ from typing import List
from fastapi import HTTPException from fastapi import HTTPException
from number_to_string import get_string_by_number from number_to_string import get_string_by_number
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload, joinedload
from starlette import status from starlette import status
from weasyprint import HTML, CSS from weasyprint import HTML, CSS
@@ -168,6 +168,8 @@ class BillingService(BaseService):
.options( .options(
selectinload(Deal.products).selectinload(DealProduct.services), selectinload(Deal.products).selectinload(DealProduct.services),
selectinload(Deal.services).selectinload(DealServiceModel.service), selectinload(Deal.services).selectinload(DealServiceModel.service),
joinedload(Deal.shipping_warehouse),
joinedload(Deal.client),
) )
) )

View File

@@ -24,6 +24,14 @@
</div> </div>
<div class="info-line">Чат с менеджером: <a href="@denco_fulfilment_1">@denco_fulfilment_1</a></div> <div class="info-line">Чат с менеджером: <a href="@denco_fulfilment_1">@denco_fulfilment_1</a></div>
<div class="info-line">Телефон: +79957948525</div> <div class="info-line">Телефон: +79957948525</div>
<div class="info-line"></div>
<div class="info-line">Клиент: {{ deal.client.name }}</div>
{% if deal.base_marketplace %}
<div class="info-line">Маркетплейс: {{ deal.base_marketplace.name }}</div>
{% endif %}
{% if deal.shipping_warehouse %}
<div class="info-line">Склад отгрузки: {{ deal.shipping_warehouse.name }}</div>
{% endif %}
</div> </div>
<div> <div>
<h2 class="title"> <h2 class="title">