feat: barcode printing

This commit is contained in:
2025-10-10 23:00:39 +04:00
parent 6b0f8a1aa5
commit 4c871e1e1b
16 changed files with 413 additions and 5 deletions

View File

@ -0,0 +1,2 @@
from .barcode_pdf_generator import BarcodePdfGenerator as BarcodePdfGenerator
from .types import BarcodeData as BarcodeData

View File

@ -0,0 +1,161 @@
from io import BytesIO
from typing import Any, Optional
from reportlab.graphics.barcode import code128
from reportlab.lib.units import mm
from reportlab.platypus import Spacer, PageBreak, Paragraph
from modules.fulfillment_base.barcodes_pdf_gen.types import *
from utils.pdf import PdfMaker, PDFGenerator
class BarcodePdfGenerator(PDFGenerator):
def _get_attr_by_path(
self, value: Any, path: str
) -> Optional[str | int | float | bool]:
keys = path.split(".")
for key in keys:
try:
if isinstance(value, dict):
value = value[key]
else:
value = getattr(value, key)
except (KeyError, AttributeError, TypeError):
return None
return value
def generate(
self, barcodes_data: list[BarcodeData | PdfBarcodeImageGenData]
) -> BytesIO:
pdf_barcodes_gen_data: list[PdfBarcodeGenData | PdfBarcodeImageGenData] = []
for barcode_data in barcodes_data:
if "barcode" not in barcode_data:
pdf_barcodes_gen_data.append(barcode_data)
continue
attributes = {}
for attribute in barcode_data["template"].attributes:
value = self._get_attr_by_path(barcode_data["product"], attribute.key)
if not value or not value.strip():
continue
attributes[attribute.name] = value
barcode_text = "<br/>".join(
[f"{key}: {value}" for key, value in attributes.items()]
)
pdf_barcodes_gen_data.append(
{
"barcode_value": barcode_data["barcode"],
"text": barcode_text,
"num_duplicates": barcode_data["num_duplicates"],
}
)
return self._generate(pdf_barcodes_gen_data)
def _generate(
self, barcodes_data: list[PdfBarcodeGenData | PdfBarcodeImageGenData]
) -> BytesIO:
pdf_maker = PdfMaker((self.page_width, self.page_height))
pdf_files: list[BytesIO] = []
for barcode_data in barcodes_data:
if "barcode_value" in barcode_data:
pdf_files.append(self._generate_for_one_product(barcode_data))
else:
pdf_files.append(self._generate_for_one_product_using_img(barcode_data))
pdf_files.append(self._generate_spacers())
for file in pdf_files[:-1]:
pdf_maker.add_pdfs(file)
return pdf_maker.get_bytes()
def _generate_for_one_product(self, barcode_data: PdfBarcodeGenData) -> BytesIO:
buffer = BytesIO()
doc = self._create_doc(buffer)
# Создаем абзац с новым стилем
paragraph = Paragraph(barcode_data["text"], self.small_style)
# Получаем ширину и высоту абзаца
paragraph_width, paragraph_height = paragraph.wrap(
self.page_width - 2 * mm, self.page_height
)
# Рассчитываем доступное пространство для штрихкода
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
) # Учитываем поля и отступы
# Создаем штрихкод
available_width = self.page_width - 4 * mm # Учитываем поля
# Приблизительное количество элементов в штрихкоде Code 128 для средней длины
num_elements = 11 * len(
barcode_data["barcode_value"]
) # Примерная оценка: 11 элементов на символ
# Рассчитываем ширину штриха
bar_width = available_width / num_elements
barcode = code128.Code128(
barcode_data["barcode_value"],
barWidth=bar_width,
barHeight=barcode_height,
humanReadable=True,
)
# Добавление штрихкодов в список элементов документа
elements = []
for _ in range(barcode_data["num_duplicates"]):
elements.append(paragraph)
elements.append(
Spacer(1, space_between_text_and_barcode)
) # Отступ между текстом и штрихкодом
elements.append(PageBreak())
# Функция для отрисовки штрихкода на canvas
def add_barcode(canvas, doc):
barcode_width = barcode.width
barcode_x = (self.page_width - barcode_width) / 2 # Центрируем штрихкод
# Размещаем штрихкод снизу с учетом отступа
barcode_y = human_readable_height + 2 * mm
barcode.drawOn(canvas, barcode_x, barcode_y)
# Создаем документ
doc.build(
elements, onFirstPage=add_barcode, onLaterPages=add_barcode
) # Убираем последний PageBreak
buffer.seek(0)
return buffer
def _generate_for_one_product_using_img(
self, barcode_data: PdfBarcodeImageGenData
) -> BytesIO:
with open(barcode_data["barcode_image_url"], "rb") as pdf_file:
pdf_bytes = pdf_file.read()
pdf_maker = PdfMaker((self.page_width, self.page_height))
for _ in range(barcode_data["num_duplicates"]):
pdf_maker.add_pdfs(BytesIO(pdf_bytes))
return pdf_maker.get_bytes()
def _generate_spacers(self) -> BytesIO:
buffer = BytesIO()
doc = self._create_doc(buffer)
elements = []
for _ in range(self.number_of_spacing_pages):
elements.append(PageBreak())
doc.build(elements)
buffer.seek(0)
return buffer

View File

@ -0,0 +1,21 @@
from typing import TypedDict
from modules.fulfillment_base.models import BarcodeTemplate, Product
class BarcodeData(TypedDict):
barcode: str
template: BarcodeTemplate
product: Product
num_duplicates: int
class PdfBarcodeGenData(TypedDict):
barcode_value: str
text: str
num_duplicates: int
class PdfBarcodeImageGenData(TypedDict):
barcode_image_url: str
num_duplicates: int

View File

@ -32,6 +32,12 @@ class BarcodeTemplateRepository(
.order_by(BarcodeTemplate.id)
)
def _process_get_by_id_stmt(self, stmt: Select) -> Select:
return stmt.options(
selectinload(BarcodeTemplate.attributes),
joinedload(BarcodeTemplate.size),
)
async def _get_size_by_id(self, size_id: int) -> Optional[BarcodeTemplateSize]:
stmt = select(BarcodeTemplateSize).where(BarcodeTemplateSize.id == size_id)
result = await self.session.scalars(stmt)

View File

@ -1,7 +1,7 @@
from sqlalchemy import or_, delete
from sqlalchemy.orm import selectinload
from sqlalchemy.orm import selectinload, joinedload
from modules.fulfillment_base.models import Product, ProductBarcode
from modules.fulfillment_base.models import Product, ProductBarcode, BarcodeTemplate
from modules.fulfillment_base.schemas.product import (
CreateProductSchema,
UpdateProductSchema,
@ -55,7 +55,13 @@ class ProductRepository(
return list(result.scalars().all()), total_items
def _process_get_by_id_stmt(self, stmt: Select) -> Select:
return stmt.options(selectinload(Product.barcodes))
return stmt.options(
selectinload(Product.barcodes),
joinedload(Product.client),
joinedload(Product.barcode_template).selectinload(
BarcodeTemplate.attributes
),
)
async def _after_create(self, product: Product, data: CreateProductSchema) -> None:
new_barcodes = [

View File

@ -2,7 +2,7 @@ from fastapi import APIRouter, Path, Query
from backend.dependecies import SessionDependency, PaginationDependency
from modules.fulfillment_base.schemas.product import *
from modules.fulfillment_base.services import ProductService
from modules.fulfillment_base.services import ProductService, BarcodePrinterService
router = APIRouter(tags=["product"])
@ -56,3 +56,18 @@ async def delete_product(
pk: int = Path(),
):
return await ProductService(session).delete(pk)
@router.post(
"/barcode/get-pdf",
operation_id="get_product_barcode_pdf",
response_model=GetProductBarcodePdfResponse,
)
async def get_product_barcode_pdf(
request: GetProductBarcodePdfRequest, session: SessionDependency
):
service = BarcodePrinterService(session)
filename, base64_string = await service.generate_base64(request)
return GetProductBarcodePdfResponse(
base64_string=base64_string, filename=filename, mime_type="application/pdf"
)

View File

@ -4,7 +4,7 @@ from pydantic import field_validator
from modules.fulfillment_base.models import ProductBarcode
from modules.fulfillment_base.schemas.barcode_template import BarcodeTemplateSchema
from schemas.base import BaseSchema, BaseResponse, PaginationInfoSchema
from schemas.base import BaseSchema, BaseResponse, PaginationInfoSchema, BasePdfResponse
# region Entity
@ -69,6 +69,12 @@ class UpdateProductRequest(BaseSchema):
entity: UpdateProductSchema
class GetProductBarcodePdfRequest(BaseSchema):
quantity: int
product_id: int
barcode: str
# endregion
# region Response
@ -91,4 +97,8 @@ class DeleteProductResponse(BaseResponse):
pass
class GetProductBarcodePdfResponse(BasePdfResponse):
pass
# endregion

View File

@ -6,3 +6,4 @@ from .service import ServiceModelService as ServiceModelService
from .services_kit import ServicesKitService as ServicesKitService
from .service_category import ServiceCategoryService as ServiceCategoryService
from .barcode_template import BarcodeTemplateService as BarcodeTemplateService
from .barcode_printer_service import BarcodePrinterService as BarcodePrinterService

View File

@ -0,0 +1,41 @@
import base64
from io import BytesIO
from sqlalchemy.ext.asyncio import AsyncSession
from modules.fulfillment_base.barcodes_pdf_gen import BarcodePdfGenerator, BarcodeData
from modules.fulfillment_base.models import Product
from modules.fulfillment_base.repositories import ProductRepository
from modules.fulfillment_base.schemas.product import GetProductBarcodePdfRequest
class BarcodePrinterService:
session: AsyncSession
def __init__(self, session: AsyncSession):
self.session = session
async def generate_pdf(
self, request: GetProductBarcodePdfRequest
) -> tuple[str, BytesIO]:
product: Product = await ProductRepository(self.session).get_by_id(
request.product_id
)
barcode_data: BarcodeData = {
"barcode": request.barcode,
"template": product.barcode_template,
"product": product,
"num_duplicates": request.quantity,
}
filename = f"{product.id}_barcode.pdf"
size = product.barcode_template.size
generator = BarcodePdfGenerator(size.width, size.height)
return filename, generator.generate([barcode_data])
async def generate_base64(
self, request: GetProductBarcodePdfRequest
) -> tuple[str, str]:
filename, pdf_buffer = await self.generate_pdf(request)
base64_string = base64.b64encode(pdf_buffer.read()).decode("utf-8")
return filename, base64_string