diff --git a/barcodes/generator/base.py b/barcodes/generator/base.py index 3636c73..8062463 100644 --- a/barcodes/generator/base.py +++ b/barcodes/generator/base.py @@ -1,13 +1,11 @@ from abc import abstractmethod +from typing import List, Dict -from models import ProductBarcode, Product, BarcodeTemplate +from models import BarcodeTemplate, Product class BaseBarcodeGenerator: @abstractmethod - def generate(self, - barcode: ProductBarcode, - product: Product, - template: BarcodeTemplate): + def generate(self, data: List[Dict[str, str | Product | BarcodeTemplate | int]]): pass diff --git a/barcodes/generator/default_generator.py b/barcodes/generator/default_generator.py index d07b8d2..12fe7b2 100644 --- a/barcodes/generator/default_generator.py +++ b/barcodes/generator/default_generator.py @@ -1,26 +1,40 @@ +from typing import List, Dict + from barcodes.attributes import AttributeWriterFactory from barcodes.generator.base import BaseBarcodeGenerator from barcodes.pdf import PDFGenerator -from models import ProductBarcode, Product, BarcodeTemplate +from models import Product, BarcodeTemplate +from schemas.barcode import PdfBarcodeGenData class DefaultBarcodeGenerator(BaseBarcodeGenerator): - def generate(self, - barcode: str, - product: Product, - template: BarcodeTemplate, - quantity: int = 1): + def generate(self, barcodes_data: List[Dict[str, str | Product | BarcodeTemplate | int]]): 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 or not value.strip(): - 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) + + pdf_barcodes_gen_data: List[PdfBarcodeGenData] = [] + + 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 not value or not value.strip(): + continue + attributes[attribute.name] = value + for additional_attribute in barcode_data["template"].additional_attributes: + attributes[additional_attribute.name] = additional_attribute.value + barcode_text = '
'.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"] + ) + ) + + print(f"value = {barcode_data['barcode']}, text = {barcode_text}, num = {barcode_data['quantity']}") + + return pdf_generator.generate(pdf_barcodes_gen_data) diff --git a/barcodes/pdf/generator.py b/barcodes/pdf/generator.py index e031120..7aa9f93 100644 --- a/barcodes/pdf/generator.py +++ b/barcodes/pdf/generator.py @@ -1,5 +1,7 @@ import os from io import BytesIO +from typing import List + from reportlab.graphics.barcode import code128 from reportlab.lib.pagesizes import mm from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle @@ -8,6 +10,7 @@ from reportlab.pdfbase.ttfonts import TTFont from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, PageBreak from constants import APP_PATH +from schemas.barcode import PdfBarcodeGenData class PDFGenerator: @@ -82,7 +85,7 @@ class PDFGenerator: buffer.seek(0) return buffer - def generate(self, barcode_value, text, num_duplicates=1): + def generate(self, barcodes_data: List[PdfBarcodeGenData]): buffer = BytesIO() # Создаем документ с указанным размером страницы @@ -93,43 +96,58 @@ class PDFGenerator: topMargin=1 * mm, bottomMargin=1 * mm) - # Создаем абзац с новым стилем - paragraph = Paragraph(text, self.small_style) + # Список элементов для добавления в документ + elements = [] + product_barcode_canvases = [] + product_counter = 0 + product_barcodes_counter = 0 - # Получаем ширину и высоту абзаца - paragraph_width, paragraph_height = paragraph.wrap(self.page_width - 2 * mm, self.page_height) + for barcode_data in barcodes_data: + # Создаем абзац с новым стилем + paragraph = Paragraph(barcode_data.text, self.small_style) - # Рассчитываем доступное пространство для штрихкода - 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 # Учитываем поля и отступы + # Получаем ширину и высоту абзаца + paragraph_width, paragraph_height = paragraph.wrap(self.page_width - 2 * mm, self.page_height) - # Создаем штрихкод - available_width = self.page_width - 2 * mm # Учитываем поля + # Рассчитываем доступное пространство для штрихкода + 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 # Учитываем поля и отступы - # Приблизительное количество элементов в штрихкоде Code 128 для средней длины - num_elements = 11 * len(barcode_value) # Примерная оценка: 11 элементов на символ + # Создаем штрихкод + available_width = self.page_width - 2 * mm # Учитываем поля - # Рассчитываем ширину штриха - bar_width = available_width / num_elements - barcode = code128.Code128(barcode_value, - barWidth=bar_width, - barHeight=barcode_height, - humanReadable=True) + # Приблизительное количество элементов в штрихкоде 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()) # Функция для отрисовки штрихкода на canvas def add_barcode(canvas, doc): - barcode_width = barcode.width + nonlocal product_barcodes_counter, product_counter + + barcode_canvas = product_barcode_canvases[product_counter] + barcode_width = barcode_canvas.width barcode_x = (self.page_width - barcode_width) / 2 # Центрируем штрихкод barcode_y = human_readable_height + 2 * mm # Размещаем штрихкод снизу с учетом отступа - barcode.drawOn(canvas, barcode_x, barcode_y) + barcode_canvas.drawOn(canvas, barcode_x, barcode_y) - # Создаем список элементов для добавления в документ - elements = [] - for _ in range(num_duplicates): - elements.append(paragraph) - elements.append(Spacer(1, space_between_text_and_barcode)) # Отступ между текстом и штрихкодом - elements.append(PageBreak()) + product_barcodes_counter += 1 + if product_barcodes_counter >= barcodes_data[product_counter].num_duplicates: + product_barcodes_counter = 0 + product_counter += 1 # Создаем документ doc.build(elements[:-1], onFirstPage=add_barcode, onLaterPages=add_barcode) # Убираем последний PageBreak diff --git a/routers/deal.py b/routers/deal.py index 04d1d57..5c80633 100644 --- a/routers/deal.py +++ b/routers/deal.py @@ -1,3 +1,4 @@ +import base64 from io import BytesIO from typing import Annotated @@ -7,8 +8,10 @@ from sqlalchemy.ext.asyncio import AsyncSession from backend.dependecies import SessionDependency, CurrentUserDependency from backend.session import get_session from models import User +from schemas.barcode import GetProductBarcodePdfResponse, GetDealProductsBarcodesPdfRequest from schemas.deal import * from services.auth import get_current_user, authorized_user, guest_user +from services.barcode import BarcodeService from services.billing import BillingService from services.deal import DealService @@ -391,4 +394,24 @@ async def add_kit_to_deal_product( user: CurrentUserDependency ): return await DealService(session).add_kit_to_deal_product(user, request) + + +@deal_router.post( + '/barcodes/get-pdf', + operation_id='get_deal_barcodes_pdf', + response_model=GetProductBarcodePdfResponse +) +async def get_deal_products_barcodes_pdf( + request: GetDealProductsBarcodesPdfRequest, + session: Annotated[AsyncSession, Depends(get_session)] +): + filename, pdf_buffer = await BarcodeService(session).get_deal_barcodes_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' + ) + # endregion diff --git a/schemas/barcode.py b/schemas/barcode.py index 859e10b..4f904ae 100644 --- a/schemas/barcode.py +++ b/schemas/barcode.py @@ -47,6 +47,12 @@ class BarcodeSchema(BaseSchema): additional_field: str | None = None +class PdfBarcodeGenData(BaseSchema): + barcode_value: str + text: str + num_duplicates: int = 1 + + # endregion # region Requests @@ -81,6 +87,10 @@ class GetProductBarcodePdfRequest(GetProductBarcodeRequest): quantity: int +class GetDealProductsBarcodesPdfRequest(BaseSchema): + deal_id: int + + # endregion # region Responses diff --git a/services/barcode.py b/services/barcode.py index b6d7450..c4d641e 100644 --- a/services/barcode.py +++ b/services/barcode.py @@ -1,10 +1,12 @@ +from typing import Dict + 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 + BarcodeTemplateAdditionalField, BarcodeTemplateSize, Deal, DealProduct from schemas.barcode import * from services.base import BaseService @@ -91,13 +93,50 @@ class BarcodeService(BaseService): default_generator = DefaultBarcodeGenerator() filename = f'{product.id}_barcode.pdf' pdf_buffer = default_generator.generate( - request.barcode, - product, - barcode_template, - request.quantity + [{ + "barcode": request.barcode, + "product": product, + "template": barcode_template, + "quantity": request.quantity + }] ) return filename, pdf_buffer + async def get_deal_barcodes_pdf(self, request: GetDealProductsBarcodesPdfRequest) -> GetProductBarcodeResponse: + stmt = ( + select(Deal) + .options( + selectinload(Deal.products).joinedload(DealProduct.product).selectinload(Product.client), + selectinload(Deal.products).joinedload(DealProduct.product).joinedload(Product.barcodes) + ) + .filter(Deal.id == request.deal_id) + ) + query = await self.session.execute(stmt) + deal: Deal = query.scalar() + if not deal: + raise ValueError('Сделка не найдена') + + barcodes_data: List[Dict[str, str | Product | BarcodeTemplate | int]] = [] + for deal_product in deal.products: + product_request = GetProductBarcodeRequest( + product_id=deal_product.product_id, + barcode="", + barcode_template_id=deal_product.product.barcode_template_id, + ) + 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, + "quantity": deal_product.quantity + }) + + default_generator = DefaultBarcodeGenerator() + filename = f'{deal.id}_deal_barcodes.pdf' + pdf_buffer = default_generator.generate(barcodes_data) + + return filename, pdf_buffer + # endregion # region Template diff --git a/services/billing.py b/services/billing.py index 4b87c2d..d3e29de 100644 --- a/services/billing.py +++ b/services/billing.py @@ -4,7 +4,7 @@ from typing import List from fastapi import HTTPException from number_to_string import get_string_by_number from sqlalchemy import select -from sqlalchemy.orm import selectinload +from sqlalchemy.orm import selectinload, joinedload from starlette import status from weasyprint import HTML, CSS @@ -168,6 +168,8 @@ class BillingService(BaseService): .options( selectinload(Deal.products).selectinload(DealProduct.services), selectinload(Deal.services).selectinload(DealServiceModel.service), + joinedload(Deal.shipping_warehouse), + joinedload(Deal.client), ) ) diff --git a/templates/documents/bill-of-payment.html b/templates/documents/bill-of-payment.html index 9fd999d..5c55890 100644 --- a/templates/documents/bill-of-payment.html +++ b/templates/documents/bill-of-payment.html @@ -24,6 +24,14 @@
Чат с менеджером: @denco_fulfilment_1
Телефон: +79957948525
+
+
Клиент: {{ deal.client.name }}
+ {% if deal.base_marketplace %} +
Маркетплейс: {{ deal.base_marketplace.name }}
+ {% endif %} + {% if deal.shipping_warehouse %} +
Склад отгрузки: {{ deal.shipping_warehouse.name }}
+ {% endif %}