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 barcodes.attributes import AttributeWriterFactory
 | 
			
		||||
@@ -8,7 +9,7 @@ from schemas.barcode import PdfBarcodeGenData
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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_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)
 | 
			
		||||
 
 | 
			
		||||
@@ -20,6 +20,7 @@ class PDFGenerator:
 | 
			
		||||
        FONT_FILE_PATH = os.path.join(FONTS_FOLDER, 'DejaVuSans.ttf')
 | 
			
		||||
        self.page_width = 58 * mm
 | 
			
		||||
        self.page_height = 40 * mm
 | 
			
		||||
        self.number_of_spacing_pages = 2
 | 
			
		||||
 | 
			
		||||
        pdfmetrics.registerFont(TTFont('DejaVuSans', FONT_FILE_PATH))
 | 
			
		||||
 | 
			
		||||
@@ -40,12 +41,14 @@ class PDFGenerator:
 | 
			
		||||
        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)
 | 
			
		||||
        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)
 | 
			
		||||
@@ -61,10 +64,12 @@ class PDFGenerator:
 | 
			
		||||
 | 
			
		||||
        # Calculate barWidth
 | 
			
		||||
        bar_width = available_width / num_elements
 | 
			
		||||
        barcode = code128.Code128(barcode_value,
 | 
			
		||||
                                  barWidth=bar_width,
 | 
			
		||||
                                  barHeight=barcode_height,
 | 
			
		||||
                                  humanReadable=True)
 | 
			
		||||
        barcode = code128.Code128(
 | 
			
		||||
            barcode_value,
 | 
			
		||||
            barWidth=bar_width,
 | 
			
		||||
            barHeight=barcode_height,
 | 
			
		||||
            humanReadable=True
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # Function to draw barcode on canvas
 | 
			
		||||
        def add_barcode(canvas, doc):
 | 
			
		||||
@@ -85,22 +90,26 @@ class PDFGenerator:
 | 
			
		||||
        buffer.seek(0)
 | 
			
		||||
        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()
 | 
			
		||||
 | 
			
		||||
        # Создаем документ с указанным размером страницы
 | 
			
		||||
        doc = SimpleDocTemplate(buffer,
 | 
			
		||||
                                pagesize=(self.page_width, self.page_height),
 | 
			
		||||
                                rightMargin=1 * mm,
 | 
			
		||||
                                leftMargin=1 * mm,
 | 
			
		||||
                                topMargin=1 * mm,
 | 
			
		||||
                                bottomMargin=1 * mm)
 | 
			
		||||
        doc = self._create_doc(buffer)
 | 
			
		||||
 | 
			
		||||
        # Список элементов для добавления в документ
 | 
			
		||||
        elements = []
 | 
			
		||||
        product_barcode_canvases = []
 | 
			
		||||
        product_counter = 0
 | 
			
		||||
        product_barcodes_counter = 0
 | 
			
		||||
 | 
			
		||||
        for barcode_data in barcodes_data:
 | 
			
		||||
            # Создаем абзац с новым стилем
 | 
			
		||||
@@ -122,10 +131,12 @@ class PDFGenerator:
 | 
			
		||||
 | 
			
		||||
            # Рассчитываем ширину штриха
 | 
			
		||||
            bar_width = available_width / num_elements
 | 
			
		||||
            barcode = code128.Code128(barcode_data.barcode_value,
 | 
			
		||||
                                      barWidth=bar_width,
 | 
			
		||||
                                      barHeight=barcode_height,
 | 
			
		||||
                                      humanReadable=True)
 | 
			
		||||
            barcode = code128.Code128(
 | 
			
		||||
                barcode_data.barcode_value,
 | 
			
		||||
                barWidth=bar_width,
 | 
			
		||||
                barHeight=barcode_height,
 | 
			
		||||
                humanReadable=True
 | 
			
		||||
            )
 | 
			
		||||
            product_barcode_canvases.append(barcode)
 | 
			
		||||
 | 
			
		||||
            # Добавление штрихкодов в список элементов документа
 | 
			
		||||
@@ -134,24 +145,39 @@ class PDFGenerator:
 | 
			
		||||
                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
 | 
			
		||||
        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_x = (self.page_width - barcode_width) / 2  # Центрируем штрихкод
 | 
			
		||||
            barcode_y = human_readable_height + 2 * mm  # Размещаем штрихкод снизу с учетом отступа
 | 
			
		||||
            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
 | 
			
		||||
 | 
			
		||||
        buffer.seek(0)
 | 
			
		||||
        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.orm import selectinload, joinedload
 | 
			
		||||
@@ -76,8 +77,7 @@ class BarcodeService(BaseService):
 | 
			
		||||
        )
 | 
			
		||||
        return GetProductBarcodeResponse(barcode=barcode)
 | 
			
		||||
 | 
			
		||||
    async def get_barcode_pdf(self, request: GetProductBarcodePdfRequest) -> GetProductBarcodeResponse:
 | 
			
		||||
        # get product by id
 | 
			
		||||
    async def get_barcode_pdf(self, request: GetProductBarcodePdfRequest) -> Tuple[str, BytesIO]:
 | 
			
		||||
        stmt = (
 | 
			
		||||
            select(Product)
 | 
			
		||||
            .options(
 | 
			
		||||
@@ -102,7 +102,7 @@ class BarcodeService(BaseService):
 | 
			
		||||
        )
 | 
			
		||||
        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 = (
 | 
			
		||||
            select(Deal)
 | 
			
		||||
            .options(
 | 
			
		||||
 
 | 
			
		||||
@@ -1036,7 +1036,7 @@ class DealService(BaseService):
 | 
			
		||||
        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))
 | 
			
		||||
        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]] = []
 | 
			
		||||
        for product in deal.products:
 | 
			
		||||
 
 | 
			
		||||
@@ -60,14 +60,14 @@ hr {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.product-data {
 | 
			
		||||
    height: 160px;
 | 
			
		||||
    height: 130px;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-wrap: wrap;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.product-data > * {
 | 
			
		||||
    width: 170px;
 | 
			
		||||
    width: 200px;
 | 
			
		||||
    padding-right: 170px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -143,8 +143,8 @@ table tfoot {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.product-img {
 | 
			
		||||
    height: 300px;
 | 
			
		||||
    width: 200px;
 | 
			
		||||
    height: 240px;
 | 
			
		||||
    width: 160px;
 | 
			
		||||
    object-fit: cover;
 | 
			
		||||
    padding: 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