feat: spacers between barcodes of diff products, avoid product cut in deal document, deal document refactoring
This commit is contained in:
		@@ -1,3 +1,4 @@
 | 
				
			|||||||
 | 
					from io import BytesIO
 | 
				
			||||||
from typing import List, Dict
 | 
					from typing import List, Dict
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from barcodes.attributes import AttributeWriterFactory
 | 
					from barcodes.attributes import AttributeWriterFactory
 | 
				
			||||||
@@ -8,7 +9,7 @@ from schemas.barcode import PdfBarcodeGenData
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class DefaultBarcodeGenerator(BaseBarcodeGenerator):
 | 
					class DefaultBarcodeGenerator(BaseBarcodeGenerator):
 | 
				
			||||||
    def generate(self, barcodes_data: List[Dict[str, str | Product | BarcodeTemplate | int]]):
 | 
					    def generate(self, barcodes_data: List[Dict[str, str | Product | BarcodeTemplate | int]]) -> BytesIO:
 | 
				
			||||||
        pdf_generator = PDFGenerator()
 | 
					        pdf_generator = PDFGenerator()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        pdf_barcodes_gen_data: List[PdfBarcodeGenData] = []
 | 
					        pdf_barcodes_gen_data: List[PdfBarcodeGenData] = []
 | 
				
			||||||
@@ -35,6 +36,4 @@ class DefaultBarcodeGenerator(BaseBarcodeGenerator):
 | 
				
			|||||||
                )
 | 
					                )
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            print(f"value = {barcode_data['barcode']}, text = {barcode_text}, num = {barcode_data['quantity']}")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return pdf_generator.generate(pdf_barcodes_gen_data)
 | 
					        return pdf_generator.generate(pdf_barcodes_gen_data)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -20,6 +20,7 @@ class PDFGenerator:
 | 
				
			|||||||
        FONT_FILE_PATH = os.path.join(FONTS_FOLDER, 'DejaVuSans.ttf')
 | 
					        FONT_FILE_PATH = os.path.join(FONTS_FOLDER, 'DejaVuSans.ttf')
 | 
				
			||||||
        self.page_width = 58 * mm
 | 
					        self.page_width = 58 * mm
 | 
				
			||||||
        self.page_height = 40 * mm
 | 
					        self.page_height = 40 * mm
 | 
				
			||||||
 | 
					        self.number_of_spacing_pages = 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        pdfmetrics.registerFont(TTFont('DejaVuSans', FONT_FILE_PATH))
 | 
					        pdfmetrics.registerFont(TTFont('DejaVuSans', FONT_FILE_PATH))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -40,12 +41,14 @@ class PDFGenerator:
 | 
				
			|||||||
        buffer = BytesIO()
 | 
					        buffer = BytesIO()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Create document with specified page size
 | 
					        # Create document with specified page size
 | 
				
			||||||
        doc = SimpleDocTemplate(buffer,
 | 
					        doc = SimpleDocTemplate(
 | 
				
			||||||
                                pagesize=(self.page_width, self.page_height),
 | 
					            buffer,
 | 
				
			||||||
                                rightMargin=1 * mm,
 | 
					            pagesize=(self.page_width, self.page_height),
 | 
				
			||||||
                                leftMargin=1 * mm,
 | 
					            rightMargin=1 * mm,
 | 
				
			||||||
                                topMargin=1 * mm,
 | 
					            leftMargin=1 * mm,
 | 
				
			||||||
                                bottomMargin=1 * mm)
 | 
					            topMargin=1 * mm,
 | 
				
			||||||
 | 
					            bottomMargin=1 * mm
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Create paragraph with new style
 | 
					        # Create paragraph with new style
 | 
				
			||||||
        paragraph = Paragraph(text, self.small_style)
 | 
					        paragraph = Paragraph(text, self.small_style)
 | 
				
			||||||
@@ -61,10 +64,12 @@ class PDFGenerator:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        # Calculate barWidth
 | 
					        # Calculate barWidth
 | 
				
			||||||
        bar_width = available_width / num_elements
 | 
					        bar_width = available_width / num_elements
 | 
				
			||||||
        barcode = code128.Code128(barcode_value,
 | 
					        barcode = code128.Code128(
 | 
				
			||||||
                                  barWidth=bar_width,
 | 
					            barcode_value,
 | 
				
			||||||
                                  barHeight=barcode_height,
 | 
					            barWidth=bar_width,
 | 
				
			||||||
                                  humanReadable=True)
 | 
					            barHeight=barcode_height,
 | 
				
			||||||
 | 
					            humanReadable=True
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Function to draw barcode on canvas
 | 
					        # Function to draw barcode on canvas
 | 
				
			||||||
        def add_barcode(canvas, doc):
 | 
					        def add_barcode(canvas, doc):
 | 
				
			||||||
@@ -85,22 +90,26 @@ class PDFGenerator:
 | 
				
			|||||||
        buffer.seek(0)
 | 
					        buffer.seek(0)
 | 
				
			||||||
        return buffer
 | 
					        return buffer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def generate(self, barcodes_data: List[PdfBarcodeGenData]):
 | 
					    # Создание документа с указанным размером страницы
 | 
				
			||||||
 | 
					    def _create_doc(self, buffer):
 | 
				
			||||||
 | 
					        return SimpleDocTemplate(
 | 
				
			||||||
 | 
					            buffer,
 | 
				
			||||||
 | 
					            pagesize=(self.page_width, self.page_height),
 | 
				
			||||||
 | 
					            rightMargin=1 * mm,
 | 
				
			||||||
 | 
					            leftMargin=1 * mm,
 | 
				
			||||||
 | 
					            topMargin=1 * mm,
 | 
				
			||||||
 | 
					            bottomMargin=1 * mm
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def generate(self, barcodes_data: List[PdfBarcodeGenData]) -> BytesIO:
 | 
				
			||||||
        buffer = BytesIO()
 | 
					        buffer = BytesIO()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Создаем документ с указанным размером страницы
 | 
					        # Создаем документ с указанным размером страницы
 | 
				
			||||||
        doc = SimpleDocTemplate(buffer,
 | 
					        doc = self._create_doc(buffer)
 | 
				
			||||||
                                pagesize=(self.page_width, self.page_height),
 | 
					 | 
				
			||||||
                                rightMargin=1 * mm,
 | 
					 | 
				
			||||||
                                leftMargin=1 * mm,
 | 
					 | 
				
			||||||
                                topMargin=1 * mm,
 | 
					 | 
				
			||||||
                                bottomMargin=1 * mm)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Список элементов для добавления в документ
 | 
					        # Список элементов для добавления в документ
 | 
				
			||||||
        elements = []
 | 
					        elements = []
 | 
				
			||||||
        product_barcode_canvases = []
 | 
					        product_barcode_canvases = []
 | 
				
			||||||
        product_counter = 0
 | 
					 | 
				
			||||||
        product_barcodes_counter = 0
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for barcode_data in barcodes_data:
 | 
					        for barcode_data in barcodes_data:
 | 
				
			||||||
            # Создаем абзац с новым стилем
 | 
					            # Создаем абзац с новым стилем
 | 
				
			||||||
@@ -122,10 +131,12 @@ class PDFGenerator:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            # Рассчитываем ширину штриха
 | 
					            # Рассчитываем ширину штриха
 | 
				
			||||||
            bar_width = available_width / num_elements
 | 
					            bar_width = available_width / num_elements
 | 
				
			||||||
            barcode = code128.Code128(barcode_data.barcode_value,
 | 
					            barcode = code128.Code128(
 | 
				
			||||||
                                      barWidth=bar_width,
 | 
					                barcode_data.barcode_value,
 | 
				
			||||||
                                      barHeight=barcode_height,
 | 
					                barWidth=bar_width,
 | 
				
			||||||
                                      humanReadable=True)
 | 
					                barHeight=barcode_height,
 | 
				
			||||||
 | 
					                humanReadable=True
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
            product_barcode_canvases.append(barcode)
 | 
					            product_barcode_canvases.append(barcode)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # Добавление штрихкодов в список элементов документа
 | 
					            # Добавление штрихкодов в список элементов документа
 | 
				
			||||||
@@ -134,24 +145,39 @@ class PDFGenerator:
 | 
				
			|||||||
                elements.append(Spacer(1, space_between_text_and_barcode))  # Отступ между текстом и штрихкодом
 | 
					                elements.append(Spacer(1, space_between_text_and_barcode))  # Отступ между текстом и штрихкодом
 | 
				
			||||||
                elements.append(PageBreak())
 | 
					                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
 | 
					            nonlocal product_barcodes_counter, product_counter
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            barcode_canvas = product_barcode_canvases[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_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_canvas.drawOn(canvas, barcode_x, barcode_y)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            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
 | 
					        doc.build(elements[:-1], onFirstPage=add_barcode, onLaterPages=add_barcode)  # Убираем последний PageBreak
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        buffer.seek(0)
 | 
					        buffer.seek(0)
 | 
				
			||||||
        return buffer
 | 
					        return buffer
 | 
				
			||||||
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,5 @@
 | 
				
			|||||||
from typing import Dict
 | 
					from io import BytesIO
 | 
				
			||||||
 | 
					from typing import Dict, Tuple
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from sqlalchemy import select, update, insert
 | 
					from sqlalchemy import select, update, insert
 | 
				
			||||||
from sqlalchemy.orm import selectinload, joinedload
 | 
					from sqlalchemy.orm import selectinload, joinedload
 | 
				
			||||||
@@ -76,8 +77,7 @@ class BarcodeService(BaseService):
 | 
				
			|||||||
        )
 | 
					        )
 | 
				
			||||||
        return GetProductBarcodeResponse(barcode=barcode)
 | 
					        return GetProductBarcodeResponse(barcode=barcode)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async def get_barcode_pdf(self, request: GetProductBarcodePdfRequest) -> GetProductBarcodeResponse:
 | 
					    async def get_barcode_pdf(self, request: GetProductBarcodePdfRequest) -> Tuple[str, BytesIO]:
 | 
				
			||||||
        # get product by id
 | 
					 | 
				
			||||||
        stmt = (
 | 
					        stmt = (
 | 
				
			||||||
            select(Product)
 | 
					            select(Product)
 | 
				
			||||||
            .options(
 | 
					            .options(
 | 
				
			||||||
@@ -102,7 +102,7 @@ class BarcodeService(BaseService):
 | 
				
			|||||||
        )
 | 
					        )
 | 
				
			||||||
        return filename, pdf_buffer
 | 
					        return filename, pdf_buffer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async def get_deal_barcodes_pdf(self, request: GetDealProductsBarcodesPdfRequest) -> GetProductBarcodeResponse:
 | 
					    async def get_deal_barcodes_pdf(self, request: GetDealProductsBarcodesPdfRequest) -> Tuple[str, BytesIO]:
 | 
				
			||||||
        stmt = (
 | 
					        stmt = (
 | 
				
			||||||
            select(Deal)
 | 
					            select(Deal)
 | 
				
			||||||
            .options(
 | 
					            .options(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1036,7 +1036,7 @@ class DealService(BaseService):
 | 
				
			|||||||
        last_status: DealStatusHistory = max(deal.status_history, key=lambda status: status.changed_at)
 | 
					        last_status: DealStatusHistory = max(deal.status_history, key=lambda status: status.changed_at)
 | 
				
			||||||
        general_services_total = sum((service.price * service.quantity for service in deal.services))
 | 
					        general_services_total = sum((service.price * service.quantity for service in deal.services))
 | 
				
			||||||
        product_services_totals: List[Dict[str, int]] = await self._get_product_services_totals(deal)
 | 
					        product_services_totals: List[Dict[str, int]] = await self._get_product_services_totals(deal)
 | 
				
			||||||
        template = ENV.get_template("deal.html")
 | 
					        template = ENV.get_template("deal/deal.html")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        product_urls: List[Optional[str]] = []
 | 
					        product_urls: List[Optional[str]] = []
 | 
				
			||||||
        for product in deal.products:
 | 
					        for product in deal.products:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -60,14 +60,14 @@ hr {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.product-data {
 | 
					.product-data {
 | 
				
			||||||
    height: 160px;
 | 
					    height: 130px;
 | 
				
			||||||
    display: flex;
 | 
					    display: flex;
 | 
				
			||||||
    flex-wrap: wrap;
 | 
					    flex-wrap: wrap;
 | 
				
			||||||
    flex-direction: column;
 | 
					    flex-direction: column;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.product-data > * {
 | 
					.product-data > * {
 | 
				
			||||||
    width: 170px;
 | 
					    width: 200px;
 | 
				
			||||||
    padding-right: 170px;
 | 
					    padding-right: 170px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -143,8 +143,8 @@ table tfoot {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.product-img {
 | 
					.product-img {
 | 
				
			||||||
    height: 300px;
 | 
					    height: 240px;
 | 
				
			||||||
    width: 200px;
 | 
					    width: 160px;
 | 
				
			||||||
    object-fit: cover;
 | 
					    object-fit: cover;
 | 
				
			||||||
    padding: 0;
 | 
					    padding: 0;
 | 
				
			||||||
    margin: 0;
 | 
					    margin: 0;
 | 
				
			||||||
 
 | 
				
			|||||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										86
									
								
								templates/documents/deal/deal-product-services.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								templates/documents/deal/deal-product-services.html
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										62
									
								
								templates/documents/deal/deal-services.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								templates/documents/deal/deal-services.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,62 @@
 | 
				
			|||||||
 | 
					<!--#region Header  -->
 | 
				
			||||||
 | 
					<div class="deal-name-id-container">
 | 
				
			||||||
 | 
					    <div class="medium-text bold">
 | 
				
			||||||
 | 
					        Название сделки: {{ deal.name }}
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					<hr/>
 | 
				
			||||||
 | 
					<div class="medium-text">
 | 
				
			||||||
 | 
					    Дата создания: {{ now().strftime("%d.%m.%Y, %H:%M") }}
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					<div class="medium-text">
 | 
				
			||||||
 | 
					    Текущий статус: {{ current_status_str }}
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					<div class="medium-text">
 | 
				
			||||||
 | 
					    {% if deal.comment %}
 | 
				
			||||||
 | 
					        Комментарий к сделке: {{ deal.comment }}
 | 
				
			||||||
 | 
					    {% endif %}
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					<hr/>
 | 
				
			||||||
 | 
					<!--#endregion -->
 | 
				
			||||||
 | 
					<!--#region General services -->
 | 
				
			||||||
 | 
					<div class="large-text bold">
 | 
				
			||||||
 | 
					    Клиент: {{ deal.client.name }}
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					<div class="medium-text bold">
 | 
				
			||||||
 | 
					    Дата отгрузки: {{ last_status.next_status_deadline.strftime("%d.%m.%Y") }}
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					{% if deal.base_marketplace.name %}
 | 
				
			||||||
 | 
					    <div class="medium-text bold">Маркетплейс: {{ deal.base_marketplace.name }}</div>
 | 
				
			||||||
 | 
					{% endif %}
 | 
				
			||||||
 | 
					{% if deal.shipping_warehouse.name %}
 | 
				
			||||||
 | 
					    <div class="medium-text bold">Склад отгрузки: {{ deal.shipping_warehouse.name }}</div>
 | 
				
			||||||
 | 
					{% endif %}
 | 
				
			||||||
 | 
					{% if deal.services|length > 0 %}
 | 
				
			||||||
 | 
					    <table>
 | 
				
			||||||
 | 
					        <thead>
 | 
				
			||||||
 | 
					        <tr>
 | 
				
			||||||
 | 
					            <th>Общие услуги</th>
 | 
				
			||||||
 | 
					            <th>Количество</th>
 | 
				
			||||||
 | 
					            <th>Сумма</th>
 | 
				
			||||||
 | 
					        </tr>
 | 
				
			||||||
 | 
					        <tfoot>
 | 
				
			||||||
 | 
					        </tfoot>
 | 
				
			||||||
 | 
					        </thead>
 | 
				
			||||||
 | 
					        <tbody>
 | 
				
			||||||
 | 
					        {% for service in deal.services %}
 | 
				
			||||||
 | 
					            <tr>
 | 
				
			||||||
 | 
					                <td>{{ service.service.name }}</td>
 | 
				
			||||||
 | 
					                <td>{{ '{:,}'.format(service.quantity) }} шт.</td>
 | 
				
			||||||
 | 
					                <td>{{ '{:,}'.format(service.price * service.quantity).replace(',', ' ') }} Р</td>
 | 
				
			||||||
 | 
					            </tr>
 | 
				
			||||||
 | 
					        {% endfor %}
 | 
				
			||||||
 | 
					        </tbody>
 | 
				
			||||||
 | 
					    </table>
 | 
				
			||||||
 | 
					    <div class="medium-text total align-right bold">
 | 
				
			||||||
 | 
					        Итого: {{ general_services_total }} Р
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					{% else %}
 | 
				
			||||||
 | 
					    <div class="medium-text total align-right bold">
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					{% endif %}
 | 
				
			||||||
 | 
					<!--#endregion -->
 | 
				
			||||||
							
								
								
									
										57
									
								
								templates/documents/deal/deal.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								templates/documents/deal/deal.html
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
		Reference in New Issue
	
	Block a user