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 @@
|
|||||||
<div style="page-break-inside: avoid">
|
{% with common_deal_product=product_data.deal_products[0], common_product=product_data.deal_products[0].product %}
|
||||||
|
<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 %}
|
||||||
</div>
|
<!--#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 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