diff --git a/.gitignore b/.gitignore index 08b5331..df5f857 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,6 @@ .venv .idea __pycache__ -/venv \ No newline at end of file +/venv +/test +/test/* \ No newline at end of file diff --git a/assets/fonts/DejaVuSans.ttf b/assets/fonts/DejaVuSans.ttf new file mode 100644 index 0000000..e5f7eec Binary files /dev/null and b/assets/fonts/DejaVuSans.ttf differ diff --git a/barcodes/generator/default_generator.py b/barcodes/generator/default_generator.py index 4256e5d..d9d091a 100644 --- a/barcodes/generator/default_generator.py +++ b/barcodes/generator/default_generator.py @@ -1,10 +1,26 @@ +from barcodes.attributes import AttributeWriterFactory from barcodes.generator.base import BaseBarcodeGenerator +from barcodes.pdf import PDFGenerator from models import ProductBarcode, Product, BarcodeTemplate class DefaultBarcodeGenerator(BaseBarcodeGenerator): def generate(self, - barcode: ProductBarcode, + barcode: str, product: Product, - template: BarcodeTemplate): - pass + template: BarcodeTemplate, + quantity: int = 1): + pdf_generator = PDFGenerator() + attributes = {} + for attribute in template.attributes: + attribute_getter = AttributeWriterFactory.get_writer(attribute.key) + if not attribute_getter: + continue + value = attribute_getter.get_value(product) + if not value: + continue + attributes[attribute.name] = value + for additional_attribute in template.additional_attributes: + attributes[additional_attribute.name] = additional_attribute.value + barcode_text = '
'.join([f'{key}: {value}' for key, value in attributes.items()]) + return pdf_generator.generate(barcode, barcode_text, num_duplicates=quantity) diff --git a/barcodes/pdf/__init__.py b/barcodes/pdf/__init__.py new file mode 100644 index 0000000..aee7d62 --- /dev/null +++ b/barcodes/pdf/__init__.py @@ -0,0 +1 @@ +from .generator import PDFGenerator diff --git a/barcodes/pdf/generator.py b/barcodes/pdf/generator.py new file mode 100644 index 0000000..a9eebf1 --- /dev/null +++ b/barcodes/pdf/generator.py @@ -0,0 +1,71 @@ +import os + +from io import BytesIO +from reportlab.graphics.barcode import code128 +from reportlab.lib.pagesizes import mm +from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle +from reportlab.pdfbase import pdfmetrics +from reportlab.pdfbase.ttfonts import TTFont +from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, PageBreak + +from constants import APP_PATH + + +class PDFGenerator: + def __init__(self): + ASSETS_FOLDER = os.path.join(APP_PATH, 'assets') + FONTS_FOLDER = os.path.join(ASSETS_FOLDER, 'fonts') + FONT_FILE_PATH = os.path.join(FONTS_FOLDER, 'DejaVuSans.ttf') + self.page_width = 58 * mm + self.page_height = 40 * mm + + pdfmetrics.registerFont(TTFont('DejaVuSans', FONT_FILE_PATH)) + + # Get standard styles and create a new style with a smaller font size + styles = getSampleStyleSheet() + self.small_style = ParagraphStyle( + 'Small', + parent=styles['Normal'], + fontName='DejaVuSans', # Specify the new font + fontSize=6, + leading=7, + spaceAfter=2, + leftIndent=2, + rightIndent=2 + ) + + def generate(self, barcode_value, text, num_duplicates=1): + buffer = BytesIO() + + # Create document with specified page size + doc = SimpleDocTemplate(buffer, + pagesize=(self.page_width, self.page_height), + rightMargin=1 * mm, + leftMargin=1 * mm, + topMargin=1 * mm, + bottomMargin=1 * mm) + + # Create paragraph with new style + paragraph = Paragraph(text, self.small_style) + + # Create barcode + barcode = code128.Code128(barcode_value, humanReadable=True) + + # Function to draw barcode on canvas + def add_barcode(canvas, doc): + barcode_width = barcode.width + barcode_x = (self.page_width - barcode_width) / 2 # Center the barcode + barcode.drawOn(canvas, barcode_x, self.page_height - barcode.height - 5) + + # Create list of elements to add to the document + elements = [] + for _ in range(num_duplicates): + elements.append(Spacer(1, 8 * mm)) + elements.append(paragraph) + elements.append(PageBreak()) + + # Build document + doc.build(elements[:-1], onFirstPage=add_barcode, onLaterPages=add_barcode) # Remove the last PageBreak + + buffer.seek(0) + return buffer diff --git a/constants.py b/constants.py index 9d92a81..400297f 100644 --- a/constants.py +++ b/constants.py @@ -1,3 +1,9 @@ +import os +import sys + +APP_PATH = os.path.dirname(sys.executable) if getattr(sys, 'frozen', False) else os.path.dirname(__file__) + + allowed_telegram_ids = [ 454777987, # Me 816217667, # Igor diff --git a/requirements.txt b/requirements.txt index b41f70e..5154b29 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,4 +21,7 @@ python-dotenv aiohttp aiohttp[speedups] openpyxl -lexorank-py \ No newline at end of file +lexorank-py + +# PDF +reportlab \ No newline at end of file diff --git a/routers/product.py b/routers/product.py index 494036a..fbc5bd9 100644 --- a/routers/product.py +++ b/routers/product.py @@ -1,11 +1,15 @@ +import base64 +from io import BytesIO from typing import Annotated, Union from fastapi import APIRouter, Depends, UploadFile from sqlalchemy.ext.asyncio import AsyncSession - +from starlette.responses import StreamingResponse +from fastapi.responses import FileResponse import utils.dependecies from backend.session import get_session -from schemas.barcode import GetProductBarcodeResponse, GetProductBarcodeRequest +from schemas.barcode import GetProductBarcodeResponse, GetProductBarcodeRequest, GetProductBarcodePdfResponse, \ + GetProductBarcodePdfRequest from schemas.base import PaginationSchema from schemas.product import * from services.auth import get_current_user @@ -127,6 +131,28 @@ async def get_product_barcode( return await BarcodeService(session).get_barcode(request) +@product_router.post( + '/barcode/get-pdf', + operation_id='get_product_barcode_pdf', + response_model=GetProductBarcodePdfResponse +) +async def get_product_barcode_pdf( + request: GetProductBarcodePdfRequest, + session: Annotated[AsyncSession, Depends(get_session)] +): + filename, pdf_buffer = await BarcodeService(session).get_barcode_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' + ) + # return StreamingResponse(content=pdf_buffer, + # media_type='application/pdf', + # headers={"Content-Disposition": f"inline; filename={filename}"}) + + @product_router.post( '/images/upload/{product_id}', response_model=ProductUploadImageResponse, diff --git a/schemas/barcode.py b/schemas/barcode.py index 672ef1b..6021ec6 100644 --- a/schemas/barcode.py +++ b/schemas/barcode.py @@ -77,6 +77,10 @@ class GetProductBarcodeRequest(CustomModelCamel): barcode_template_id: int | None = None +class GetProductBarcodePdfRequest(GetProductBarcodeRequest): + quantity: int + + # endregion # region Responses @@ -115,4 +119,10 @@ class GetProductBarcodeResponse(CustomModelCamel): class GetAllBarcodeTemplateSizesResponse(CustomModelCamel): sizes: list[BarcodeTemplateSizeSchema] + + +class GetProductBarcodePdfResponse(CustomModelCamel): + base64_string: str + filename: str + mime_type: str # endregion diff --git a/services/barcode.py b/services/barcode.py index e2f6ab3..b6d7450 100644 --- a/services/barcode.py +++ b/services/barcode.py @@ -2,6 +2,7 @@ from sqlalchemy import select, update, insert from sqlalchemy.orm import selectinload, joinedload from barcodes.attributes import AttributeWriterFactory +from barcodes.generator.default_generator import DefaultBarcodeGenerator from models import BarcodeTemplate, BarcodeTemplateAttribute, barcode_template_attribute_link, Product, \ BarcodeTemplateAdditionalField, BarcodeTemplateSize from schemas.barcode import * @@ -73,6 +74,30 @@ class BarcodeService(BaseService): ) return GetProductBarcodeResponse(barcode=barcode) + async def get_barcode_pdf(self, request: GetProductBarcodePdfRequest) -> GetProductBarcodeResponse: + # get product by id + stmt = ( + select(Product) + .options( + joinedload(Product.client) + ) + .filter(Product.id == request.product_id) + ) + query = await self.session.execute(stmt) + product: Product = query.scalar() + if not product: + raise ValueError('Товар не найден') + barcode_template = await self._get_barcode_template(request, product) + default_generator = DefaultBarcodeGenerator() + filename = f'{product.id}_barcode.pdf' + pdf_buffer = default_generator.generate( + request.barcode, + product, + barcode_template, + request.quantity + ) + return filename, pdf_buffer + # endregion # region Template