feat: pdf generation using product barcode images

This commit is contained in:
2024-11-02 00:53:45 +04:00
parent 5ed42d99dc
commit 25060322a1
9 changed files with 212 additions and 125 deletions

View File

@@ -1,43 +1,44 @@
from io import BytesIO from io import BytesIO
from typing import List, Dict from typing import List
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 Product, BarcodeTemplate from barcodes.types import BarcodeData, PdfBarcodeGenData, PdfBarcodeImageGenData
from schemas.barcode import PdfBarcodeGenData
class DefaultBarcodeGenerator(BaseBarcodeGenerator): class DefaultBarcodeGenerator(BaseBarcodeGenerator):
def generate(self, barcodes_data: List[Dict[str, str | Product | BarcodeTemplate | int]]) -> BytesIO: def generate(self, barcodes_data: List[BarcodeData | PdfBarcodeImageGenData]) -> BytesIO:
pdf_generator = PDFGenerator() pdf_generator = PDFGenerator()
pdf_barcodes_gen_data: List[PdfBarcodeGenData] = [] pdf_barcodes_gen_data: List[PdfBarcodeGenData | PdfBarcodeImageGenData] = []
for barcode_data in barcodes_data: for barcode_data in barcodes_data:
attributes = {} if "barcode" in barcode_data:
for attribute in barcode_data["template"].attributes: attributes = {}
attribute_getter = AttributeWriterFactory.get_writer(attribute.key) for attribute in barcode_data["template"].attributes:
if not attribute_getter: attribute_getter = AttributeWriterFactory.get_writer(attribute.key)
continue if not attribute_getter:
value = attribute_getter.get_value(barcode_data["product"]) continue
value = attribute_getter.get_value(barcode_data["product"])
if not value or not value.strip(): if not value or not value.strip():
continue continue
attributes[attribute.name] = value attributes[attribute.name] = value
for additional_attribute in barcode_data["template"].additional_attributes: for additional_attribute in barcode_data["template"].additional_attributes:
value = additional_attribute.value value = additional_attribute.value
if not value: if not value:
continue continue
attributes[additional_attribute.name] = value attributes[additional_attribute.name] = value
barcode_text = '<br/>'.join([f'{key}: {value}' for key, value in attributes.items()]) barcode_text = '<br/>'.join([f'{key}: {value}' for key, value in attributes.items()])
pdf_barcodes_gen_data.append( pdf_barcodes_gen_data.append({
PdfBarcodeGenData( "barcode_value": barcode_data["barcode"],
barcode_value=barcode_data["barcode"], "text": barcode_text,
text=barcode_text, "num_duplicates": barcode_data["num_duplicates"]
num_duplicates=barcode_data["quantity"] })
) else:
) pdf_barcodes_gen_data.append(barcode_data)
return pdf_generator.generate(pdf_barcodes_gen_data) return pdf_generator.generate(pdf_barcodes_gen_data)

View File

@@ -9,6 +9,10 @@ class BaseImagesUploader:
def get_url(self, filename: str) -> bytes: def get_url(self, filename: str) -> bytes:
pass pass
@abstractmethod
def get_abs_path(self, filename: str) -> bytes:
pass
@abstractmethod @abstractmethod
def delete(self, filename: str): def delete(self, filename: str):
pass pass

View File

@@ -20,6 +20,10 @@ class BarcodeImagesUploader(BaseImagesUploader):
file_location = self.relative_path / filename file_location = self.relative_path / filename
return f"{API_ROOT}/{file_location}" return f"{API_ROOT}/{file_location}"
def get_abs_path(self, filename: str) -> str:
file_location = self.storage_path / filename
return file_location
def delete(self, filename: str): def delete(self, filename: str):
file_location = self.storage_path / filename file_location = self.storage_path / filename
if file_location.exists(): if file_location.exists():

View File

@@ -10,8 +10,9 @@ from reportlab.pdfbase.ttfonts import TTFont
from reportlab.pdfgen import canvas from reportlab.pdfgen import canvas
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, PageBreak from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, PageBreak
from barcodes.pdf.pdf_maker import PdfMaker
from barcodes.types import PdfBarcodeImageGenData, PdfBarcodeGenData
from constants import APP_PATH from constants import APP_PATH
from schemas.barcode import PdfBarcodeGenData
class PDFGenerator: class PDFGenerator:
@@ -102,90 +103,94 @@ class PDFGenerator:
bottomMargin=1 * mm bottomMargin=1 * mm
) )
def generate(self, barcodes_data: List[PdfBarcodeGenData]) -> BytesIO: def _generate_for_one_product(self, barcode_data: PdfBarcodeGenData) -> BytesIO:
buffer = BytesIO() buffer = BytesIO()
# Создаем документ с указанным размером страницы
doc = self._create_doc(buffer) 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 - 2 * 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 = [] elements = []
product_barcode_canvases = [] for _ in range(barcode_data['num_duplicates']):
elements.append(paragraph)
for barcode_data in barcodes_data: elements.append(Spacer(1, space_between_text_and_barcode)) # Отступ между текстом и штрихкодом
# Создаем абзац с новым стилем elements.append(PageBreak())
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 - 2 * 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
)
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())
# Добавление спейсеров
for _ in range(self.number_of_spacing_pages):
elements.append(PageBreak())
# Удалить последние спейсеры
elements = elements[:-2]
product_counter, product_barcodes_counter = 0, 0
# Функция для отрисовки штрихкода на canvas # Функция для отрисовки штрихкода на canvas
def add_barcode(canvas, doc): def add_barcode(canvas, doc):
nonlocal product_barcodes_counter, product_counter barcode_width = barcode.width
barcode_canvas = product_barcode_canvases[product_counter]
product_barcodes_counter += 1
# Если данная страница это спейсер, то оставить пустой
num_duplicates = barcodes_data[product_counter].num_duplicates
if product_barcodes_counter > num_duplicates:
if product_barcodes_counter >= num_duplicates + self.number_of_spacing_pages:
product_barcodes_counter = 0
product_counter += 1
return
# Отрисовка штрихкода
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_canvas.drawOn(canvas, barcode_x, barcode_y) barcode.drawOn(canvas, barcode_x, barcode_y)
# Создаем документ # Создаем документ
doc.build(elements[:-1], onFirstPage=add_barcode, onLaterPages=add_barcode) # Убираем последний PageBreak doc.build(elements, onFirstPage=add_barcode, onLaterPages=add_barcode) # Убираем последний PageBreak
buffer.seek(0) buffer.seek(0)
return buffer 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
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_barcode_image(self, barcode_image_url: str, path_to_save_pdf: str): def generate_barcode_image(self, barcode_image_url: str, path_to_save_pdf: str):
print(type(path_to_save_pdf))
print(path_to_save_pdf)
c = canvas.Canvas(path_to_save_pdf, pagesize=(self.page_width, self.page_height)) c = canvas.Canvas(path_to_save_pdf, pagesize=(self.page_width, self.page_height))
c.drawImage(barcode_image_url, 0, 0, width=self.page_width, height=self.page_height) c.drawImage(barcode_image_url, 0, 0, width=self.page_width, height=self.page_height)
c.save() c.save()

38
barcodes/pdf/pdf_maker.py Normal file
View File

@@ -0,0 +1,38 @@
from io import BytesIO
from fpdf import FPDF
import pdfrw
class PdfMaker:
def __init__(self, size: tuple):
self.size = size
self.writer = pdfrw.PdfWriter()
def clear(self):
del self.writer
self.writer = pdfrw.PdfWriter()
def add_image(self, image_data):
size = self.size
fpdf = FPDF(format=size, unit="pt")
width, height = self.size
fpdf.add_page()
fpdf.image(image_data, 0, 0, width, height)
fpdf_reader: pdfrw.PdfReader = pdfrw.PdfReader(fdata=bytes(fpdf.output()))
self.writer.addpage(fpdf_reader.getPage(0))
def add_pdf(self, pdf_data: BytesIO):
pdf_reader = pdfrw.PdfReader(fdata=bytes(pdf_data.read()))
self.writer.addpage(pdf_reader.getPage(0))
def add_pdfs(self, pdf_data: BytesIO):
pdf_reader = pdfrw.PdfReader(fdata=bytes(pdf_data.read()))
self.writer.addpages(pdf_reader.readpages(pdf_reader.Root))
def get_bytes(self):
result_io = BytesIO()
self.writer.write(result_io)
result_io.seek(0)
return result_io

21
barcodes/types.py Normal file
View File

@@ -0,0 +1,21 @@
from typing import TypedDict
from 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):
num_duplicates: int
barcode_image_url: str

View File

@@ -30,3 +30,5 @@ aioshutil
reportlab reportlab
weasyprint weasyprint
number_to_string number_to_string
pdfrw
fpdf

View File

@@ -47,12 +47,6 @@ 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

View File

@@ -6,6 +6,7 @@ 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 barcodes.images_uploader import BarcodeImagesUploader
from models import BarcodeTemplate, BarcodeTemplateAttribute, barcode_template_attribute_link, Product, \ from models import BarcodeTemplate, BarcodeTemplateAttribute, barcode_template_attribute_link, Product, \
BarcodeTemplateAdditionalField, BarcodeTemplateSize, Deal, DealProduct BarcodeTemplateAdditionalField, BarcodeTemplateSize, Deal, DealProduct
from schemas.barcode import * from schemas.barcode import *
@@ -89,17 +90,27 @@ class BarcodeService(BaseService):
product: Product = query.scalar() product: Product = query.scalar()
if not product: if not product:
raise ValueError('Товар не найден') raise ValueError('Товар не найден')
barcode_template = await self._get_barcode_template(request, product)
default_generator = DefaultBarcodeGenerator()
filename = f'{product.id}_barcode.pdf' filename = f'{product.id}_barcode.pdf'
pdf_buffer = default_generator.generate( default_generator = DefaultBarcodeGenerator()
[{
"barcode": request.barcode, if product.barcode_image:
"product": product, uploader = BarcodeImagesUploader()
"template": barcode_template, pdf_buffer = default_generator.generate(
"quantity": request.quantity [{
}] "barcode_image_url": uploader.get_abs_path(product.barcode_image.filename),
) "num_duplicates": request.quantity
}]
)
else:
barcode_template = await self._get_barcode_template(request, product)
pdf_buffer = default_generator.generate(
[{
"barcode": request.barcode,
"product": product,
"template": barcode_template,
"num_duplicates": request.quantity
}]
)
return filename, pdf_buffer return filename, pdf_buffer
async def get_deal_barcodes_pdf(self, request: GetDealProductsBarcodesPdfRequest) -> Tuple[str, BytesIO]: async def get_deal_barcodes_pdf(self, request: GetDealProductsBarcodesPdfRequest) -> Tuple[str, BytesIO]:
@@ -107,7 +118,7 @@ class BarcodeService(BaseService):
select(Deal) select(Deal)
.options( .options(
selectinload(Deal.products).joinedload(DealProduct.product).selectinload(Product.client), selectinload(Deal.products).joinedload(DealProduct.product).selectinload(Product.client),
selectinload(Deal.products).joinedload(DealProduct.product).joinedload(Product.barcodes) selectinload(Deal.products).joinedload(DealProduct.product).joinedload(Product.barcodes),
) )
.filter(Deal.id == request.deal_id) .filter(Deal.id == request.deal_id)
) )
@@ -116,20 +127,27 @@ class BarcodeService(BaseService):
if not deal: if not deal:
raise ValueError('Сделка не найдена') raise ValueError('Сделка не найдена')
uploader = BarcodeImagesUploader()
barcodes_data: List[Dict[str, str | Product | BarcodeTemplate | int]] = [] barcodes_data: List[Dict[str, str | Product | BarcodeTemplate | int]] = []
for deal_product in deal.products: for deal_product in deal.products:
product_request = GetProductBarcodeRequest( if deal_product.product.barcode_image:
product_id=deal_product.product_id, barcodes_data.append({
barcode="", "barcode_image_url": uploader.get_abs_path(deal_product.product.barcode_image.filename),
barcode_template_id=deal_product.product.barcode_template_id, "num_duplicates": deal_product.quantity
) })
barcode_template = await self._get_barcode_template(product_request, deal_product.product) else:
barcodes_data.append({ product_request = GetProductBarcodeRequest(
"barcode": deal_product.product.barcodes[0].barcode, product_id=deal_product.product_id,
"product": deal_product.product, barcode="",
"template": barcode_template, barcode_template_id=deal_product.product.barcode_template_id,
"quantity": deal_product.quantity )
}) 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,
"num_duplicates": deal_product.quantity
})
default_generator = DefaultBarcodeGenerator() default_generator = DefaultBarcodeGenerator()
filename = f'{deal.id}_deal_barcodes.pdf' filename = f'{deal.id}_deal_barcodes.pdf'