feat: executors and grouping by article in deal document
This commit is contained in:
		@@ -10,6 +10,7 @@ from utils.code128 import encode128
 | 
				
			|||||||
ENV = Environment(loader=FileSystemLoader(Path("templates") / Path("documents")))
 | 
					ENV = Environment(loader=FileSystemLoader(Path("templates") / Path("documents")))
 | 
				
			||||||
ENV.globals['now'] = datetime.now
 | 
					ENV.globals['now'] = datetime.now
 | 
				
			||||||
ENV.globals['encode128'] = encode128
 | 
					ENV.globals['encode128'] = encode128
 | 
				
			||||||
 | 
					ENV.globals['format_number'] = lambda x: '{:,}'.format(x).replace(',', ' ')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
APP_PATH = os.path.dirname(sys.executable) if getattr(sys, 'frozen', False) else os.path.dirname(__file__)
 | 
					APP_PATH = os.path.dirname(sys.executable) if getattr(sys, 'frozen', False) else os.path.dirname(__file__)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										0
									
								
								generators/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								generators/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								generators/deal_pdf_generator/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								generators/deal_pdf_generator/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										20
									
								
								generators/deal_pdf_generator/deal_data.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								generators/deal_pdf_generator/deal_data.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
				
			|||||||
 | 
					from typing import TypedDict, List, Dict, Tuple, Optional
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from models import DealProduct, Deal, DealStatusHistory
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class DocumentDealProductData(TypedDict):
 | 
				
			||||||
 | 
					    deal_products: List[DealProduct]
 | 
				
			||||||
 | 
					    total_one_product: int
 | 
				
			||||||
 | 
					    quantity: int
 | 
				
			||||||
 | 
					    additional_info: Optional[str]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class DocumentDealData(TypedDict):
 | 
				
			||||||
 | 
					    deal: Deal
 | 
				
			||||||
 | 
					    general_services_total: int
 | 
				
			||||||
 | 
					    products: Dict[str, DocumentDealProductData]
 | 
				
			||||||
 | 
					    current_status_str: str
 | 
				
			||||||
 | 
					    last_status: DealStatusHistory
 | 
				
			||||||
 | 
					    product_images: Tuple[str]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										102
									
								
								generators/deal_pdf_generator/generator.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								generators/deal_pdf_generator/generator.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,102 @@
 | 
				
			|||||||
 | 
					from io import BytesIO
 | 
				
			||||||
 | 
					from typing import List, Dict, Optional
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from sqlalchemy import select
 | 
				
			||||||
 | 
					from sqlalchemy.ext.asyncio import AsyncSession
 | 
				
			||||||
 | 
					from sqlalchemy.orm import selectinload, joinedload
 | 
				
			||||||
 | 
					from weasyprint import HTML, CSS
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from constants import DEAL_STATUS_STR, ENV, APP_PATH
 | 
				
			||||||
 | 
					from generators.deal_pdf_generator.deal_data import DocumentDealProductData
 | 
				
			||||||
 | 
					from models import Deal, DealProduct, DealService as DealServiceModel, Product
 | 
				
			||||||
 | 
					from utils.images_fetcher import fetch_images
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class DealPdfGenerator:
 | 
				
			||||||
 | 
					    def __init__(self, session: AsyncSession):
 | 
				
			||||||
 | 
					        self._session = session
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @staticmethod
 | 
				
			||||||
 | 
					    async def _get_product_services_totals(deal: Deal) -> List[Dict[str, int]]:
 | 
				
			||||||
 | 
					        totals: List[Dict[str, int]] = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for product in deal.products:
 | 
				
			||||||
 | 
					            total_one_product = sum((service.price for service in product.services))
 | 
				
			||||||
 | 
					            total = total_one_product * product.quantity
 | 
				
			||||||
 | 
					            totals.append({"total_one_product": total_one_product, "total": total})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return totals
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @staticmethod
 | 
				
			||||||
 | 
					    async def _group_deal_products_by_products(deal_products: List[DealProduct]) -> Dict[str, DocumentDealProductData]:
 | 
				
			||||||
 | 
					        products: Dict[str, DocumentDealProductData] = {}
 | 
				
			||||||
 | 
					        additional_info: Optional[str]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for deal_product in deal_products:
 | 
				
			||||||
 | 
					            # Для группировки по артикулу и услугам
 | 
				
			||||||
 | 
					            key = f"{deal_product.product.article} - " + ",".join(
 | 
				
			||||||
 | 
					                str(service.service_id) for service in deal_product.services
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if key not in products:
 | 
				
			||||||
 | 
					                total_one_product = sum(service.price for service in deal_product.services)
 | 
				
			||||||
 | 
					                products[key] = {
 | 
				
			||||||
 | 
					                    "deal_products": [deal_product],
 | 
				
			||||||
 | 
					                    "total_one_product": total_one_product,
 | 
				
			||||||
 | 
					                    "quantity": deal_product.quantity,
 | 
				
			||||||
 | 
					                    "additional_info": deal_product.product.additional_info,
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                products[key]["deal_products"].append(deal_product)
 | 
				
			||||||
 | 
					                products[key]["quantity"] += deal_product.quantity
 | 
				
			||||||
 | 
					                if not products[key]["additional_info"]:
 | 
				
			||||||
 | 
					                    products[key]["additional_info"] = deal_product.product.additional_info
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return products
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async def _create_detailed_deal_document_html(self, deal_id: int):
 | 
				
			||||||
 | 
					        deal: Deal | None = await self._session.scalar(
 | 
				
			||||||
 | 
					            select(Deal)
 | 
				
			||||||
 | 
					            .where(Deal.id == deal_id)
 | 
				
			||||||
 | 
					            .options(
 | 
				
			||||||
 | 
					                selectinload(Deal.products).selectinload(DealProduct.services),
 | 
				
			||||||
 | 
					                selectinload(Deal.products).selectinload(DealProduct.product).selectinload(Product.barcodes),
 | 
				
			||||||
 | 
					                selectinload(Deal.services).selectinload(DealServiceModel.service),
 | 
				
			||||||
 | 
					                selectinload(Deal.status_history),
 | 
				
			||||||
 | 
					                joinedload(Deal.client),
 | 
				
			||||||
 | 
					                joinedload(Deal.shipping_warehouse),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if not deal:
 | 
				
			||||||
 | 
					            return ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        products = await self._group_deal_products_by_products(deal.products)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        product_urls: List[Optional[str]] = []
 | 
				
			||||||
 | 
					        for product in products.values():
 | 
				
			||||||
 | 
					            if len(product["deal_products"][0].product.images) > 0:
 | 
				
			||||||
 | 
					                product_urls.append(product["deal_products"][0].product.images[0].image_url)
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                product_urls.append(None)
 | 
				
			||||||
 | 
					        product_images = await fetch_images(product_urls)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        document_deal_data = {
 | 
				
			||||||
 | 
					            "deal": deal,
 | 
				
			||||||
 | 
					            "general_services_total": sum((service.price * service.quantity for service in deal.services)),
 | 
				
			||||||
 | 
					            "products": products,
 | 
				
			||||||
 | 
					            "current_status_str": DEAL_STATUS_STR[deal.current_status],
 | 
				
			||||||
 | 
					            "last_status": max(deal.status_history, key=lambda status: status.changed_at),
 | 
				
			||||||
 | 
					            "product_images": product_images,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        template = ENV.get_template("deal/deal.html")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        result = template.render({"data": document_deal_data, "sign_place_text": "_" * 22})
 | 
				
			||||||
 | 
					        return result
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async def create_detailed_deal_document_pdf(self, deal_id) -> BytesIO:
 | 
				
			||||||
 | 
					        doc = await self._create_detailed_deal_document_html(deal_id)
 | 
				
			||||||
 | 
					        pdf_file = BytesIO()
 | 
				
			||||||
 | 
					        HTML(string=doc).write_pdf(pdf_file, stylesheets=[CSS(APP_PATH + '/static/css/deal.css')])
 | 
				
			||||||
 | 
					        return pdf_file
 | 
				
			||||||
@@ -7,6 +7,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from backend.dependecies import SessionDependency, CurrentUserDependency
 | 
					from backend.dependecies import SessionDependency, CurrentUserDependency
 | 
				
			||||||
from backend.session import get_session
 | 
					from backend.session import get_session
 | 
				
			||||||
 | 
					from generators.deal_pdf_generator.generator import DealPdfGenerator
 | 
				
			||||||
from models import User
 | 
					from models import User
 | 
				
			||||||
from schemas.barcode import GetDealProductsBarcodesPdfRequest, GetDealProductsBarcodesPdfResponse
 | 
					from schemas.barcode import GetDealProductsBarcodesPdfRequest, GetDealProductsBarcodesPdfResponse
 | 
				
			||||||
from schemas.deal import *
 | 
					from schemas.deal import *
 | 
				
			||||||
@@ -202,7 +203,7 @@ async def get_detailed_deal_document(
 | 
				
			|||||||
        deal_id: int,
 | 
					        deal_id: int,
 | 
				
			||||||
        session: Annotated[AsyncSession, Depends(get_session)],
 | 
					        session: Annotated[AsyncSession, Depends(get_session)],
 | 
				
			||||||
):
 | 
					):
 | 
				
			||||||
    pdf_file: BytesIO = await DealService(session).create_detailed_deal_document_pdf(deal_id)
 | 
					    pdf_file: BytesIO = await DealPdfGenerator(session).create_detailed_deal_document_pdf(deal_id)
 | 
				
			||||||
    return Response(pdf_file.getvalue(), media_type='application/pdf')
 | 
					    return Response(pdf_file.getvalue(), media_type='application/pdf')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,9 +1,10 @@
 | 
				
			|||||||
import datetime
 | 
					import datetime
 | 
				
			||||||
from typing import List, Optional, Union
 | 
					from collections import defaultdict
 | 
				
			||||||
 | 
					from typing import List, Optional, Union, Dict, Tuple, TypedDict
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from pydantic import constr, field_validator
 | 
					from pydantic import constr, field_validator
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from models import ServiceCategoryPrice, ServicePriceCategory
 | 
					from models import ServiceCategoryPrice, ServicePriceCategory, Deal, Product, DealProduct, DealStatusHistory
 | 
				
			||||||
from schemas.base import BaseSchema, OkMessageSchema
 | 
					from schemas.base import BaseSchema, OkMessageSchema
 | 
				
			||||||
from schemas.billing import DealBillRequestSchema
 | 
					from schemas.billing import DealBillRequestSchema
 | 
				
			||||||
from schemas.client import ClientSchema
 | 
					from schemas.client import ClientSchema
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,18 +1,12 @@
 | 
				
			|||||||
from io import BytesIO
 | 
					 | 
				
			||||||
from typing import Dict
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import lexorank
 | 
					import lexorank
 | 
				
			||||||
from fastapi import HTTPException
 | 
					from fastapi import HTTPException
 | 
				
			||||||
from sqlalchemy import select, func, update, delete, insert
 | 
					from sqlalchemy import select, func, update, delete, insert
 | 
				
			||||||
from sqlalchemy.orm import joinedload, selectinload
 | 
					from sqlalchemy.orm import joinedload, selectinload
 | 
				
			||||||
from starlette import status
 | 
					from starlette import status
 | 
				
			||||||
from weasyprint import HTML, CSS
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import constants
 | 
					 | 
				
			||||||
import models.deal
 | 
					import models.deal
 | 
				
			||||||
import models.secondary
 | 
					import models.secondary
 | 
				
			||||||
from constants import ENV, DEAL_STATUS_STR
 | 
					from models import User, Service, Client
 | 
				
			||||||
from models import User, Service, Client, DealProduct, DealService as DealServiceModel, Product
 | 
					 | 
				
			||||||
from models.deal import *
 | 
					from models.deal import *
 | 
				
			||||||
from schemas.client import ClientDetailsSchema
 | 
					from schemas.client import ClientDetailsSchema
 | 
				
			||||||
from schemas.deal import *
 | 
					from schemas.deal import *
 | 
				
			||||||
@@ -21,7 +15,6 @@ from services.base import BaseService
 | 
				
			|||||||
from services.client import ClientService
 | 
					from services.client import ClientService
 | 
				
			||||||
from services.service import ServiceService
 | 
					from services.service import ServiceService
 | 
				
			||||||
from services.shipping_warehouse import ShippingWarehouseService
 | 
					from services.shipping_warehouse import ShippingWarehouseService
 | 
				
			||||||
from utils.images_fetcher import fetch_images
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class DealService(BaseService):
 | 
					class DealService(BaseService):
 | 
				
			||||||
@@ -1008,64 +1001,3 @@ class DealService(BaseService):
 | 
				
			|||||||
        except Exception as e:
 | 
					        except Exception as e:
 | 
				
			||||||
            await self.session.rollback()
 | 
					            await self.session.rollback()
 | 
				
			||||||
            return DealCompleteResponse(ok=False, message=str(e))
 | 
					            return DealCompleteResponse(ok=False, message=str(e))
 | 
				
			||||||
 | 
					 | 
				
			||||||
    # region Deal documents
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @staticmethod
 | 
					 | 
				
			||||||
    async def _get_product_services_totals(deal: Deal) -> List[Dict[str, int]]:
 | 
					 | 
				
			||||||
        totals: List[Dict[str, int]] = []
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        for product in deal.products:
 | 
					 | 
				
			||||||
            total_one_product = sum((service.price for service in product.services))
 | 
					 | 
				
			||||||
            total = total_one_product * product.quantity
 | 
					 | 
				
			||||||
            totals.append({"total_one_product": total_one_product, "total": total})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return totals
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async def _create_detailed_deal_document_html(self, deal_id: int):
 | 
					 | 
				
			||||||
        deal: Deal | None = await self.session.scalar(
 | 
					 | 
				
			||||||
            select(Deal)
 | 
					 | 
				
			||||||
            .where(Deal.id == deal_id)
 | 
					 | 
				
			||||||
            .options(
 | 
					 | 
				
			||||||
                selectinload(Deal.products).selectinload(DealProduct.services),
 | 
					 | 
				
			||||||
                selectinload(Deal.products).selectinload(DealProduct.product).selectinload(Product.barcodes),
 | 
					 | 
				
			||||||
                selectinload(Deal.services).selectinload(DealServiceModel.service),
 | 
					 | 
				
			||||||
                selectinload(Deal.status_history),
 | 
					 | 
				
			||||||
                joinedload(Deal.client),
 | 
					 | 
				
			||||||
                joinedload(Deal.shipping_warehouse),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if not deal:
 | 
					 | 
				
			||||||
            return ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        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/deal.html")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        product_urls: List[Optional[str]] = []
 | 
					 | 
				
			||||||
        for product in deal.products:
 | 
					 | 
				
			||||||
            if len(product.product.images) > 0:
 | 
					 | 
				
			||||||
                product_urls.append(product.product.images[0].image_url)
 | 
					 | 
				
			||||||
            else:
 | 
					 | 
				
			||||||
                product_urls.append(None)
 | 
					 | 
				
			||||||
        product_images = await fetch_images(product_urls)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        result = template.render({
 | 
					 | 
				
			||||||
            "general_services_total": general_services_total,
 | 
					 | 
				
			||||||
            "product_services_totals": product_services_totals,
 | 
					 | 
				
			||||||
            "deal": deal,
 | 
					 | 
				
			||||||
            "current_status_str": DEAL_STATUS_STR[deal.current_status],
 | 
					 | 
				
			||||||
            "last_status": last_status,
 | 
					 | 
				
			||||||
            "product_images": product_images,
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
        return result
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async def create_detailed_deal_document_pdf(self, deal_id) -> BytesIO:
 | 
					 | 
				
			||||||
        doc = await self._create_detailed_deal_document_html(deal_id)
 | 
					 | 
				
			||||||
        pdf_file = BytesIO()
 | 
					 | 
				
			||||||
        HTML(string=doc).write_pdf(pdf_file, stylesheets=[CSS(constants.APP_PATH + '/static/css/deal.css')])
 | 
					 | 
				
			||||||
        return pdf_file
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # endregion
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -18,6 +18,10 @@ hr {
 | 
				
			|||||||
    margin-right: -100px;
 | 
					    margin-right: -100px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.sign-place {
 | 
				
			||||||
 | 
					    margin-top: 25px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.doc-container {
 | 
					.doc-container {
 | 
				
			||||||
    margin: -30px 0;
 | 
					    margin: -30px 0;
 | 
				
			||||||
    padding: 0 15px;
 | 
					    padding: 0 15px;
 | 
				
			||||||
@@ -42,7 +46,7 @@ hr {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
.small-text {
 | 
					.small-text {
 | 
				
			||||||
    font-size: 12px;
 | 
					    font-size: 12px;
 | 
				
			||||||
    margin-bottom: 4px;
 | 
					    margin-bottom: 3px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.medium-text {
 | 
					.medium-text {
 | 
				
			||||||
@@ -67,7 +71,7 @@ hr {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.product-data > * {
 | 
					.product-data > * {
 | 
				
			||||||
    width: 200px;
 | 
					    width: 170px;
 | 
				
			||||||
    padding-right: 170px;
 | 
					    padding-right: 170px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -152,8 +156,16 @@ table tfoot {
 | 
				
			|||||||
    border-radius: 5px;
 | 
					    border-radius: 5px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.barcode-container-table {
 | 
				
			||||||
 | 
					    margin: 10px 0 -15px -25px;
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    white-space: nowrap;
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					    align-items: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.barcode-container {
 | 
					.barcode-container {
 | 
				
			||||||
    margin: 15px 0 -5px 355px;
 | 
					    margin: 15px 0 -5px 375px;
 | 
				
			||||||
    display: flex;
 | 
					    display: flex;
 | 
				
			||||||
    white-space: nowrap;
 | 
					    white-space: nowrap;
 | 
				
			||||||
    flex-direction: column;
 | 
					    flex-direction: column;
 | 
				
			||||||
@@ -162,7 +174,7 @@ table tfoot {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
.barcode {
 | 
					.barcode {
 | 
				
			||||||
    font-family: "Libre Barcode 128", cursive;
 | 
					    font-family: "Libre Barcode 128", cursive;
 | 
				
			||||||
    font-size: 30pt;
 | 
					    font-size: 25pt;
 | 
				
			||||||
    transform: scale(1, 1.5);
 | 
					    transform: scale(1, 1.5);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,11 +1,13 @@
 | 
				
			|||||||
 | 
					{% with common_deal_product=product_data.deal_products[0], common_product=product_data.deal_products[0].product %}
 | 
				
			||||||
    <div style="page-break-inside: avoid">
 | 
					    <div style="page-break-inside: avoid">
 | 
				
			||||||
 | 
					        <!--#region Product header -->
 | 
				
			||||||
        <div style="page-break-inside: avoid; page-break-after: avoid">
 | 
					        <div style="page-break-inside: avoid; page-break-after: avoid">
 | 
				
			||||||
            <hr/>
 | 
					            <hr/>
 | 
				
			||||||
            <div style="display: flex">
 | 
					            <div style="display: flex">
 | 
				
			||||||
                <div class="img-container">
 | 
					                <div class="img-container">
 | 
				
			||||||
                {% if product.product.images|length > 0 %}
 | 
					                    {% if common_product.images|length > 0 %}
 | 
				
			||||||
                        <img class="product-img"
 | 
					                        <img class="product-img"
 | 
				
			||||||
                         src="data:image/jpeg;base64,{{ product_images[loop.index0] }}"
 | 
					                             src="data:image/jpeg;base64,{{ data.product_images[loop.index0] }}"
 | 
				
			||||||
                             alt="Фото товара"/>
 | 
					                             alt="Фото товара"/>
 | 
				
			||||||
                    {% else %}
 | 
					                    {% else %}
 | 
				
			||||||
                        <img class="product-img"
 | 
					                        <img class="product-img"
 | 
				
			||||||
@@ -15,47 +17,67 @@
 | 
				
			|||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
                <div style="margin: 12px 0; max-width: 400px">
 | 
					                <div style="margin: 12px 0; max-width: 400px">
 | 
				
			||||||
                    <div class="medium-text bold">
 | 
					                    <div class="medium-text bold">
 | 
				
			||||||
                    {{ index }}. {{ product.product.name }}
 | 
					                        {{ index }}. {{ common_product.name }}
 | 
				
			||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
 | 
					                    <div style="display: flex; flex-direction: row">
 | 
				
			||||||
                        <div class="product-data">
 | 
					                        <div class="product-data">
 | 
				
			||||||
                    <div class="small-text">Артикул: {{ product.product.article }}</div>
 | 
					                            <div class="small-text">Артикул: {{ common_product.article }}</div>
 | 
				
			||||||
                    {% if product.product.brand %}
 | 
					                            {% if common_product.brand %}
 | 
				
			||||||
                        <div class="small-text">Бренд: {{ product.product.brand }}</div>
 | 
					                                <div class="small-text">Бренд: {{ common_product.brand }}</div>
 | 
				
			||||||
                            {% endif %}
 | 
					                            {% endif %}
 | 
				
			||||||
                    {% if product.product.color %}
 | 
					                            {% if common_product.color %}
 | 
				
			||||||
                        <div class="small-text">Цвет: {{ product.product.color }}</div>
 | 
					                                <div class="small-text">Цвет: {{ common_product.color }}</div>
 | 
				
			||||||
                            {% endif %}
 | 
					                            {% endif %}
 | 
				
			||||||
                    {% if product.product.size %}
 | 
					                            {% if product_data.deal_products|length > 1 %}
 | 
				
			||||||
                        <div class="small-text">Размер: {{ product.product.size }}</div>
 | 
					                                {% if common_deal_product.quantity %}
 | 
				
			||||||
 | 
					                                    <div class="small-text">Суммарное количество: {{ product_data.quantity }}</div>
 | 
				
			||||||
                                {% endif %}
 | 
					                                {% endif %}
 | 
				
			||||||
                    {% if product.quantity %}
 | 
					                                {% if product_data.additional_info %}
 | 
				
			||||||
                        <div class="small-text">Количество: {{ product.quantity }}</div>
 | 
					 | 
				
			||||||
                    {% endif %}
 | 
					 | 
				
			||||||
                    {% if product.product.additional_info %}
 | 
					 | 
				
			||||||
                                    <div class="small-text">
 | 
					                                    <div class="small-text">
 | 
				
			||||||
                            Доп. информация: {{ product.product.additional_info }}
 | 
					                                        {{ product_data.additional_info }}
 | 
				
			||||||
                                    </div>
 | 
					                                    </div>
 | 
				
			||||||
                                {% endif %}
 | 
					                                {% endif %}
 | 
				
			||||||
 | 
					                            {% else %}
 | 
				
			||||||
 | 
					                                {% if common_product.size %}
 | 
				
			||||||
 | 
					                                    <div class="small-text">Размер: {{ common_product.size }}</div>
 | 
				
			||||||
 | 
					                                {% endif %}
 | 
				
			||||||
 | 
					                                {% if common_deal_product.quantity %}
 | 
				
			||||||
 | 
					                                    <div class="small-text">Количество: {{ common_deal_product.quantity }}</div>
 | 
				
			||||||
 | 
					                                {% endif %}
 | 
				
			||||||
 | 
					                            {% endif %}
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                        <div>
 | 
				
			||||||
 | 
					                            <div class="small-text">Исполнители:</div>
 | 
				
			||||||
 | 
					                            <div class="sign-place">{{ sign_place_text }}</div>
 | 
				
			||||||
 | 
					                            <div class="sign-place">{{ sign_place_text }}</div>
 | 
				
			||||||
 | 
					                            <div class="sign-place">{{ sign_place_text }}</div>
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
            <hr/>
 | 
					            <hr/>
 | 
				
			||||||
 | 
					            {% if product_data.deal_products|length > 1 %}
 | 
				
			||||||
 | 
					                <div style="margin-bottom: 40px"></div>
 | 
				
			||||||
 | 
					            {% else %}
 | 
				
			||||||
                <div style="text-align: right">
 | 
					                <div style="text-align: right">
 | 
				
			||||||
                    <div class="barcode-container">
 | 
					                    <div class="barcode-container">
 | 
				
			||||||
                {% if product.product.barcodes|length > 0 %}
 | 
					                        {% if product_data.deal_products[0].product.barcodes|length > 0 %}
 | 
				
			||||||
                            <div class="barcode">
 | 
					                            <div class="barcode">
 | 
				
			||||||
                        {{ encode128(product.product.barcodes[0].barcode, "A") }}
 | 
					                                {{ encode128(product_data.deal_products[0].product.barcodes[0].barcode, "A") }}
 | 
				
			||||||
                            </div>
 | 
					                            </div>
 | 
				
			||||||
                            <div class="barcode-text">
 | 
					                            <div class="barcode-text">
 | 
				
			||||||
                        {{ product.product.barcodes[0].barcode }}
 | 
					                                {{ product_data.deal_products[0].product.barcodes[0].barcode }}
 | 
				
			||||||
                            </div>
 | 
					                            </div>
 | 
				
			||||||
                        {% else %}
 | 
					                        {% else %}
 | 
				
			||||||
                            <div style="margin-bottom: 40px"></div>
 | 
					                            <div style="margin-bottom: 40px"></div>
 | 
				
			||||||
                        {% endif %}
 | 
					                        {% endif %}
 | 
				
			||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
 | 
					            {% endif %}
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
    {% if product.services|length > 0 %}
 | 
					        <!--#endregion -->
 | 
				
			||||||
 | 
					        <!--#region Product services -->
 | 
				
			||||||
 | 
					        {% if common_deal_product.services|length > 0 %}
 | 
				
			||||||
            <table>
 | 
					            <table>
 | 
				
			||||||
                <thead>
 | 
					                <thead>
 | 
				
			||||||
                <tr>
 | 
					                <tr>
 | 
				
			||||||
@@ -67,20 +89,68 @@
 | 
				
			|||||||
                </tfoot>
 | 
					                </tfoot>
 | 
				
			||||||
                </thead>
 | 
					                </thead>
 | 
				
			||||||
                <tbody>
 | 
					                <tbody>
 | 
				
			||||||
            {% for service in product.services %}
 | 
					                {% for service in common_deal_product.services %}
 | 
				
			||||||
                    <tr>
 | 
					                    <tr>
 | 
				
			||||||
                        <td>{{ service.service.name }}</td>
 | 
					                        <td>{{ service.service.name }}</td>
 | 
				
			||||||
                    <td>{{ '{:,}'.format(product.quantity) }} Р</td>
 | 
					                        <td>{{ format_number(service.price) }} Р</td>
 | 
				
			||||||
                    <td>{{ '{:,}'.format(service.price * product.quantity).replace(',', ' ') }} Р</td>
 | 
					                        <td>{{ format_number(service.price * product_data.quantity) }} Р</td>
 | 
				
			||||||
                    </tr>
 | 
					                    </tr>
 | 
				
			||||||
                {% endfor %}
 | 
					                {% endfor %}
 | 
				
			||||||
                </tbody>
 | 
					                </tbody>
 | 
				
			||||||
            </table>
 | 
					            </table>
 | 
				
			||||||
            <div class="medium-text total align-right bold">
 | 
					            <div class="medium-text total align-right bold">
 | 
				
			||||||
            Итого: {{ product_services_totals[loop.index0].total }} Р, за
 | 
					                Итого: {{ product_data.total_one_product * product_data.quantity }} Р, за
 | 
				
			||||||
            единицу: {{ product_services_totals[loop.index0].total_one_product }} Р
 | 
					                единицу: {{ product_data.total_one_product }} Р
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
        {% else %}
 | 
					        {% else %}
 | 
				
			||||||
            <div class="medium-text total align-right bold"></div>
 | 
					            <div class="medium-text total align-right bold"></div>
 | 
				
			||||||
        {% endif %}
 | 
					        {% endif %}
 | 
				
			||||||
 | 
					        <!--#endregion -->
 | 
				
			||||||
 | 
					        <!--#region Products table -->
 | 
				
			||||||
 | 
					        {% if product_data.deal_products|length > 1 %}
 | 
				
			||||||
 | 
					            <table>
 | 
				
			||||||
 | 
					                <thead>
 | 
				
			||||||
 | 
					                <tr>
 | 
				
			||||||
 | 
					                    <th>Штрихкод</th>
 | 
				
			||||||
 | 
					                    <th style="text-align: center">Размер</th>
 | 
				
			||||||
 | 
					                    <th style="text-align: center">Кол-во</th>
 | 
				
			||||||
 | 
					                    <th>Короба</th>
 | 
				
			||||||
 | 
					                </tr>
 | 
				
			||||||
 | 
					                <tfoot>
 | 
				
			||||||
 | 
					                </tfoot>
 | 
				
			||||||
 | 
					                </thead>
 | 
				
			||||||
 | 
					                <tbody>
 | 
				
			||||||
 | 
					                {% for deal_product in product_data.deal_products %}
 | 
				
			||||||
 | 
					                    <tr>
 | 
				
			||||||
 | 
					                        <td>
 | 
				
			||||||
 | 
					                            <div class="barcode-container-table">
 | 
				
			||||||
 | 
					                                {% if deal_product.product.barcodes|length > 0 %}
 | 
				
			||||||
 | 
					                                    <div class="barcode">
 | 
				
			||||||
 | 
					                                        {{ encode128(deal_product.product.barcodes[0].barcode, "A") }}
 | 
				
			||||||
                                    </div>
 | 
					                                    </div>
 | 
				
			||||||
 | 
					                                    <div class="barcode-text">
 | 
				
			||||||
 | 
					                                        {{ deal_product.product.barcodes[0].barcode }}
 | 
				
			||||||
 | 
					                                    </div>
 | 
				
			||||||
 | 
					                                {% else %}
 | 
				
			||||||
 | 
					                                    <div style="margin-bottom: 40px"></div>
 | 
				
			||||||
 | 
					                                {% endif %}
 | 
				
			||||||
 | 
					                            </div>
 | 
				
			||||||
 | 
					                        </td>
 | 
				
			||||||
 | 
					                        <td style="text-align: center">
 | 
				
			||||||
 | 
					                            {% if deal_product.product.size %}
 | 
				
			||||||
 | 
					                                {{ deal_product.product.size }}
 | 
				
			||||||
 | 
					                            {% else %}
 | 
				
			||||||
 | 
					                                -
 | 
				
			||||||
 | 
					                            {% endif %}
 | 
				
			||||||
 | 
					                        </td>
 | 
				
			||||||
 | 
					                        <td style="text-align: center">{{ deal_product.quantity }}</td>
 | 
				
			||||||
 | 
					                        <td></td>
 | 
				
			||||||
 | 
					                    </tr>
 | 
				
			||||||
 | 
					                {% endfor %}
 | 
				
			||||||
 | 
					                </tbody>
 | 
				
			||||||
 | 
					            </table>
 | 
				
			||||||
 | 
					            <div style="height: 30px"></div>
 | 
				
			||||||
 | 
					        {% endif %}
 | 
				
			||||||
 | 
					        <!--#endregion -->
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					{% endwith %}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,7 @@
 | 
				
			|||||||
<!--#region Header  -->
 | 
					<!--#region Header  -->
 | 
				
			||||||
<div class="deal-name-id-container">
 | 
					<div class="deal-name-id-container">
 | 
				
			||||||
    <div class="medium-text bold">
 | 
					    <div class="medium-text bold">
 | 
				
			||||||
        Название сделки: {{ deal.name }}
 | 
					        Название сделки: {{ data.deal.name }}
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
<hr/>
 | 
					<hr/>
 | 
				
			||||||
@@ -9,29 +9,29 @@
 | 
				
			|||||||
    Дата создания: {{ now().strftime("%d.%m.%Y, %H:%M") }}
 | 
					    Дата создания: {{ now().strftime("%d.%m.%Y, %H:%M") }}
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
<div class="medium-text">
 | 
					<div class="medium-text">
 | 
				
			||||||
    Текущий статус: {{ current_status_str }}
 | 
					    Текущий статус: {{ data.current_status_str }}
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
<div class="medium-text">
 | 
					<div class="medium-text">
 | 
				
			||||||
    {% if deal.comment %}
 | 
					    {% if data.deal.comment %}
 | 
				
			||||||
        Комментарий к сделке: {{ deal.comment }}
 | 
					        Комментарий к сделке: {{ data.deal.comment }}
 | 
				
			||||||
    {% endif %}
 | 
					    {% endif %}
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
<hr/>
 | 
					<hr/>
 | 
				
			||||||
<!--#endregion -->
 | 
					<!--#endregion -->
 | 
				
			||||||
<!--#region General services -->
 | 
					<!--#region General services -->
 | 
				
			||||||
<div class="large-text bold">
 | 
					<div class="large-text bold">
 | 
				
			||||||
    Клиент: {{ deal.client.name }}
 | 
					    Клиент: {{ data.deal.client.name }}
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
<div class="medium-text bold">
 | 
					<div class="medium-text bold">
 | 
				
			||||||
    Дата отгрузки: {{ last_status.next_status_deadline.strftime("%d.%m.%Y") }}
 | 
					    Дата отгрузки: {{ data.last_status.next_status_deadline.strftime("%d.%m.%Y") }}
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
{% if deal.base_marketplace.name %}
 | 
					{% if data.deal.base_marketplace.name %}
 | 
				
			||||||
    <div class="medium-text bold">Маркетплейс: {{ deal.base_marketplace.name }}</div>
 | 
					    <div class="medium-text bold">Маркетплейс: {{ data.deal.base_marketplace.name }}</div>
 | 
				
			||||||
{% endif %}
 | 
					{% endif %}
 | 
				
			||||||
{% if deal.shipping_warehouse.name %}
 | 
					{% if data.deal.shipping_warehouse.name %}
 | 
				
			||||||
    <div class="medium-text bold">Склад отгрузки: {{ deal.shipping_warehouse.name }}</div>
 | 
					    <div class="medium-text bold">Склад отгрузки: {{ data.deal.shipping_warehouse.name }}</div>
 | 
				
			||||||
{% endif %}
 | 
					{% endif %}
 | 
				
			||||||
{% if deal.services|length > 0 %}
 | 
					{% if data.deal.services|length > 0 %}
 | 
				
			||||||
    <table>
 | 
					    <table>
 | 
				
			||||||
        <thead>
 | 
					        <thead>
 | 
				
			||||||
        <tr>
 | 
					        <tr>
 | 
				
			||||||
@@ -43,7 +43,7 @@
 | 
				
			|||||||
        </tfoot>
 | 
					        </tfoot>
 | 
				
			||||||
        </thead>
 | 
					        </thead>
 | 
				
			||||||
        <tbody>
 | 
					        <tbody>
 | 
				
			||||||
        {% for service in deal.services %}
 | 
					        {% for service in data.deal.services %}
 | 
				
			||||||
            <tr>
 | 
					            <tr>
 | 
				
			||||||
                <td>{{ service.service.name }}</td>
 | 
					                <td>{{ service.service.name }}</td>
 | 
				
			||||||
                <td>{{ '{:,}'.format(service.quantity) }} шт.</td>
 | 
					                <td>{{ '{:,}'.format(service.quantity) }} шт.</td>
 | 
				
			||||||
@@ -53,7 +53,7 @@
 | 
				
			|||||||
        </tbody>
 | 
					        </tbody>
 | 
				
			||||||
    </table>
 | 
					    </table>
 | 
				
			||||||
    <div class="medium-text total align-right bold">
 | 
					    <div class="medium-text total align-right bold">
 | 
				
			||||||
        Итого: {{ general_services_total }} Р
 | 
					        Итого: {{ data.general_services_total }} Р
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
{% else %}
 | 
					{% else %}
 | 
				
			||||||
    <div class="medium-text total align-right bold">
 | 
					    <div class="medium-text total align-right bold">
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -29,7 +29,7 @@
 | 
				
			|||||||
                font-size: 18px;
 | 
					                font-size: 18px;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            @top-right {
 | 
					            @top-right {
 | 
				
			||||||
                content: "ID:{{ deal.id }}";
 | 
					                content: "ID:{{ data.deal.id }}";
 | 
				
			||||||
                border: solid black 1px;
 | 
					                border: solid black 1px;
 | 
				
			||||||
                border-radius: 6px;
 | 
					                border-radius: 6px;
 | 
				
			||||||
                padding: 12px 2px;
 | 
					                padding: 12px 2px;
 | 
				
			||||||
@@ -45,7 +45,7 @@
 | 
				
			|||||||
    <div>
 | 
					    <div>
 | 
				
			||||||
        <div>
 | 
					        <div>
 | 
				
			||||||
            {% include "deal/deal-services.html" %}
 | 
					            {% include "deal/deal-services.html" %}
 | 
				
			||||||
            {% for product in deal.products %}
 | 
					            {% for product_data in data.products.values() %}
 | 
				
			||||||
                {% with index = loop.index %}
 | 
					                {% with index = loop.index %}
 | 
				
			||||||
                    {% include "deal/deal-product-services.html" %}
 | 
					                    {% include "deal/deal-product-services.html" %}
 | 
				
			||||||
                {% endwith %}
 | 
					                {% endwith %}
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user