feat: barcode printing
This commit is contained in:
2
modules/fulfillment_base/barcodes_pdf_gen/__init__.py
Normal file
2
modules/fulfillment_base/barcodes_pdf_gen/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
from .barcode_pdf_generator import BarcodePdfGenerator as BarcodePdfGenerator
|
||||
from .types import BarcodeData as BarcodeData
|
||||
@ -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
|
||||
21
modules/fulfillment_base/barcodes_pdf_gen/types.py
Normal file
21
modules/fulfillment_base/barcodes_pdf_gen/types.py
Normal 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
|
||||
@ -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)
|
||||
|
||||
@ -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 = [
|
||||
|
||||
@ -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"
|
||||
)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
41
modules/fulfillment_base/services/barcode_printer_service.py
Normal file
41
modules/fulfillment_base/services/barcode_printer_service.py
Normal 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
|
||||
Reference in New Issue
Block a user