feat: pdf generation using product barcode images
This commit is contained in:
		@@ -1,43 +1,44 @@
 | 
			
		||||
from io import BytesIO
 | 
			
		||||
from typing import List, Dict
 | 
			
		||||
from typing import List
 | 
			
		||||
 | 
			
		||||
from barcodes.attributes import AttributeWriterFactory
 | 
			
		||||
from barcodes.generator.base import BaseBarcodeGenerator
 | 
			
		||||
from barcodes.pdf import PDFGenerator
 | 
			
		||||
from models import Product, BarcodeTemplate
 | 
			
		||||
from schemas.barcode import PdfBarcodeGenData
 | 
			
		||||
from barcodes.types import BarcodeData, PdfBarcodeGenData, PdfBarcodeImageGenData
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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_barcodes_gen_data: List[PdfBarcodeGenData] = []
 | 
			
		||||
        pdf_barcodes_gen_data: List[PdfBarcodeGenData | PdfBarcodeImageGenData] = []
 | 
			
		||||
 | 
			
		||||
        for barcode_data in barcodes_data:
 | 
			
		||||
            attributes = {}
 | 
			
		||||
            for attribute in barcode_data["template"].attributes:
 | 
			
		||||
                attribute_getter = AttributeWriterFactory.get_writer(attribute.key)
 | 
			
		||||
                if not attribute_getter:
 | 
			
		||||
                    continue
 | 
			
		||||
                value = attribute_getter.get_value(barcode_data["product"])
 | 
			
		||||
            if "barcode" in barcode_data:
 | 
			
		||||
                attributes = {}
 | 
			
		||||
                for attribute in barcode_data["template"].attributes:
 | 
			
		||||
                    attribute_getter = AttributeWriterFactory.get_writer(attribute.key)
 | 
			
		||||
                    if not attribute_getter:
 | 
			
		||||
                        continue
 | 
			
		||||
                    value = attribute_getter.get_value(barcode_data["product"])
 | 
			
		||||
 | 
			
		||||
                if not value or not value.strip():
 | 
			
		||||
                    continue
 | 
			
		||||
                attributes[attribute.name] = value
 | 
			
		||||
            for additional_attribute in barcode_data["template"].additional_attributes:
 | 
			
		||||
                value = additional_attribute.value
 | 
			
		||||
                if not value:
 | 
			
		||||
                    continue
 | 
			
		||||
                attributes[additional_attribute.name] = value
 | 
			
		||||
            barcode_text = '<br/>'.join([f'{key}: {value}' for key, value in attributes.items()])
 | 
			
		||||
                    if not value or not value.strip():
 | 
			
		||||
                        continue
 | 
			
		||||
                    attributes[attribute.name] = value
 | 
			
		||||
                for additional_attribute in barcode_data["template"].additional_attributes:
 | 
			
		||||
                    value = additional_attribute.value
 | 
			
		||||
                    if not value:
 | 
			
		||||
                        continue
 | 
			
		||||
                    attributes[additional_attribute.name] = 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"]
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
                pdf_barcodes_gen_data.append({
 | 
			
		||||
                    "barcode_value": barcode_data["barcode"],
 | 
			
		||||
                    "text": barcode_text,
 | 
			
		||||
                    "num_duplicates": barcode_data["num_duplicates"]
 | 
			
		||||
                })
 | 
			
		||||
            else:
 | 
			
		||||
                pdf_barcodes_gen_data.append(barcode_data)
 | 
			
		||||
 | 
			
		||||
        return pdf_generator.generate(pdf_barcodes_gen_data)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,10 @@ class BaseImagesUploader:
 | 
			
		||||
    def get_url(self, filename: str) -> bytes:
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    @abstractmethod
 | 
			
		||||
    def get_abs_path(self, filename: str) -> bytes:
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    @abstractmethod
 | 
			
		||||
    def delete(self, filename: str):
 | 
			
		||||
        pass
 | 
			
		||||
 
 | 
			
		||||
@@ -20,6 +20,10 @@ class BarcodeImagesUploader(BaseImagesUploader):
 | 
			
		||||
        file_location = self.relative_path / filename
 | 
			
		||||
        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):
 | 
			
		||||
        file_location = self.storage_path / filename
 | 
			
		||||
        if file_location.exists():
 | 
			
		||||
 
 | 
			
		||||
@@ -10,8 +10,9 @@ from reportlab.pdfbase.ttfonts import TTFont
 | 
			
		||||
from reportlab.pdfgen import canvas
 | 
			
		||||
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 schemas.barcode import PdfBarcodeGenData
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PDFGenerator:
 | 
			
		||||
@@ -102,90 +103,94 @@ class PDFGenerator:
 | 
			
		||||
            bottomMargin=1 * mm
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def generate(self, barcodes_data: List[PdfBarcodeGenData]) -> BytesIO:
 | 
			
		||||
    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 - 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 = []
 | 
			
		||||
        product_barcode_canvases = []
 | 
			
		||||
 | 
			
		||||
        for barcode_data in barcodes_data:
 | 
			
		||||
            # Создаем абзац с новым стилем
 | 
			
		||||
            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
 | 
			
		||||
        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):
 | 
			
		||||
            nonlocal product_barcodes_counter, product_counter
 | 
			
		||||
 | 
			
		||||
            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_width = barcode.width
 | 
			
		||||
            barcode_x = (self.page_width - barcode_width) / 2  # Центрируем штрихкод
 | 
			
		||||
            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)
 | 
			
		||||
        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):
 | 
			
		||||
        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.drawImage(barcode_image_url, 0, 0, width=self.page_width, height=self.page_height)
 | 
			
		||||
        c.save()
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										38
									
								
								barcodes/pdf/pdf_maker.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								barcodes/pdf/pdf_maker.py
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										21
									
								
								barcodes/types.py
									
									
									
									
									
										Normal 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
 | 
			
		||||
		Reference in New Issue
	
	Block a user