feat: tech specification for a group of deals
This commit is contained in:
@@ -4,16 +4,22 @@ from models import DealProduct, Deal, DealStatusHistory
|
|||||||
|
|
||||||
|
|
||||||
class DealTechSpecProductData(TypedDict):
|
class DealTechSpecProductData(TypedDict):
|
||||||
deal_products: List[DealProduct]
|
deal: Deal
|
||||||
|
last_status: DealStatusHistory
|
||||||
total_one_product: int
|
total_one_product: int
|
||||||
quantity: int
|
quantity: int
|
||||||
additional_info: Optional[str]
|
additional_info: Optional[str]
|
||||||
|
|
||||||
|
# Поле для группировки товаров с одним артикулом и вывода таблицы [Штрихкод, Размер, Кол-во, Короба]
|
||||||
|
deal_products: List[DealProduct]
|
||||||
|
|
||||||
|
# Поле для группировки товаров из нескольких сделок и вывода таблицы [Склад отгрузки, Кол-во]
|
||||||
|
warehouses_and_quantities: List[Tuple[str, int]]
|
||||||
|
|
||||||
|
|
||||||
class DealTechSpecData(TypedDict):
|
class DealTechSpecData(TypedDict):
|
||||||
deal: Deal
|
deals: List[Deal]
|
||||||
products: Dict[str, DealTechSpecProductData]
|
products: Dict[str, DealTechSpecProductData]
|
||||||
current_status_str: str
|
product_images: Tuple
|
||||||
last_status: DealStatusHistory
|
deal_ids_header: str
|
||||||
product_images: Tuple[str]
|
deal_status_str: list[str]
|
||||||
|
|
||||||
|
|||||||
@@ -8,13 +8,46 @@ from weasyprint import HTML, CSS
|
|||||||
|
|
||||||
from constants import DEAL_STATUS_STR, ENV, APP_PATH
|
from constants import DEAL_STATUS_STR, ENV, APP_PATH
|
||||||
from generators.deal_pdf_generator.deal_data import DealTechSpecProductData, DealTechSpecData
|
from generators.deal_pdf_generator.deal_data import DealTechSpecProductData, DealTechSpecData
|
||||||
from models import Deal, DealProduct, DealService as DealServiceModel, Product
|
from models import Deal, DealProduct, DealService as DealServiceModel, Product, DealGroup
|
||||||
from utils.images_fetcher import fetch_images
|
from utils.images_fetcher import fetch_images
|
||||||
|
|
||||||
|
|
||||||
|
# Генерация ключа для группировки deal_product по артикулу и услугам
|
||||||
|
def _gen_key_for_product(deal_product: DealProduct) -> str:
|
||||||
|
return f"{deal_product.product.article} - " + ",".join(
|
||||||
|
str(service.service_id) for service in deal_product.services
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Генерация ключа для группировки deal_product из группы сделок по артикулу, сервисам, а также товарам
|
||||||
|
def _regen_key_for_product(product_data: DealTechSpecProductData) -> str:
|
||||||
|
if len(product_data['deal_products']) == 0:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
article = product_data['deal_products'][0].product.article
|
||||||
|
services_ids = ",".join(str(service.service_id) for service in product_data['deal_products'][0].services)
|
||||||
|
|
||||||
|
if len(product_data['deal_products']) == 1:
|
||||||
|
products = product_data['deal_products'][0].product_id
|
||||||
|
else:
|
||||||
|
products = ",".join(
|
||||||
|
f"{deal_product.product_id}-{deal_product.quantity}" for deal_product in product_data['deal_products']
|
||||||
|
)
|
||||||
|
|
||||||
|
return f"{article}+{services_ids}+{products}"
|
||||||
|
|
||||||
|
|
||||||
class DealTechSpecPdfGenerator:
|
class DealTechSpecPdfGenerator:
|
||||||
def __init__(self, session: AsyncSession):
|
def __init__(self, session: AsyncSession):
|
||||||
self._session = session
|
self._session = session
|
||||||
|
self.deal_doc: DealTechSpecData = {
|
||||||
|
"deals": [],
|
||||||
|
"products": {},
|
||||||
|
"product_images": (),
|
||||||
|
"deal_ids_header": "",
|
||||||
|
"deal_status_str": DEAL_STATUS_STR,
|
||||||
|
}
|
||||||
|
self.deal: Deal
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def _group_deal_products_by_products(deal_products: List[DealProduct]) -> Dict[str, DealTechSpecProductData]:
|
async def _group_deal_products_by_products(deal_products: List[DealProduct]) -> Dict[str, DealTechSpecProductData]:
|
||||||
@@ -23,15 +56,15 @@ class DealTechSpecPdfGenerator:
|
|||||||
|
|
||||||
for deal_product in deal_products:
|
for deal_product in deal_products:
|
||||||
# Для группировки по артикулу и услугам
|
# Для группировки по артикулу и услугам
|
||||||
key = f"{deal_product.product.article} - " + ",".join(
|
key = _gen_key_for_product(deal_product)
|
||||||
str(service.service_id) for service in deal_product.services
|
|
||||||
)
|
|
||||||
|
|
||||||
if key not in products:
|
if key not in products:
|
||||||
products[key] = {
|
products[key] = {
|
||||||
|
"deal": deal_product.deal,
|
||||||
"deal_products": [deal_product],
|
"deal_products": [deal_product],
|
||||||
"quantity": deal_product.quantity,
|
"quantity": deal_product.quantity,
|
||||||
"additional_info": deal_product.product.additional_info,
|
"additional_info": deal_product.product.additional_info,
|
||||||
|
"warehouses_and_quantities": [],
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
products[key]["deal_products"].append(deal_product)
|
products[key]["deal_products"].append(deal_product)
|
||||||
@@ -50,39 +83,72 @@ class DealTechSpecPdfGenerator:
|
|||||||
selectinload(Deal.products).selectinload(DealProduct.product).selectinload(Product.barcodes),
|
selectinload(Deal.products).selectinload(DealProduct.product).selectinload(Product.barcodes),
|
||||||
selectinload(Deal.services).selectinload(DealServiceModel.service),
|
selectinload(Deal.services).selectinload(DealServiceModel.service),
|
||||||
selectinload(Deal.status_history),
|
selectinload(Deal.status_history),
|
||||||
|
selectinload(Deal.group).selectinload(DealGroup.deals),
|
||||||
joinedload(Deal.client),
|
joinedload(Deal.client),
|
||||||
joinedload(Deal.shipping_warehouse),
|
joinedload(Deal.shipping_warehouse),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return deal
|
return deal
|
||||||
|
|
||||||
|
async def _get_deals_by_group_id(self, group_id: int) -> List[Deal]:
|
||||||
|
group: DealGroup | None = await self._session.scalar(
|
||||||
|
select(DealGroup)
|
||||||
|
.where(DealGroup.id == group_id)
|
||||||
|
.options(
|
||||||
|
selectinload(DealGroup.deals).selectinload(Deal.products).selectinload(DealProduct.services),
|
||||||
|
selectinload(DealGroup.deals).selectinload(Deal.products)
|
||||||
|
.selectinload(DealProduct.product).selectinload(Product.barcodes),
|
||||||
|
selectinload(DealGroup.deals).selectinload(Deal.services).selectinload(DealServiceModel.service),
|
||||||
|
selectinload(DealGroup.deals).selectinload(Deal.status_history),
|
||||||
|
selectinload(DealGroup.deals).selectinload(Deal.group).selectinload(DealGroup.deals),
|
||||||
|
selectinload(DealGroup.deals).joinedload(Deal.client),
|
||||||
|
selectinload(DealGroup.deals).joinedload(Deal.shipping_warehouse),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return group.deals if group else []
|
||||||
|
|
||||||
|
def _set_deals_ids_header(self):
|
||||||
|
self.deal_doc["deal_ids_header"] = f"ID: {self.deal.id}"
|
||||||
|
if self.deal.group:
|
||||||
|
self.deal_doc["deal_ids_header"] = "ID: " + ", ".join(str(d.id) for d in self.deal.group.deals)
|
||||||
|
|
||||||
async def _create_deal_tech_spec_document_html(self, deal_id: int):
|
async def _create_deal_tech_spec_document_html(self, deal_id: int):
|
||||||
deal = await self._get_deal_by_id(deal_id)
|
deal = await self._get_deal_by_id(deal_id)
|
||||||
|
|
||||||
if not deal:
|
if not deal:
|
||||||
return ""
|
return ""
|
||||||
|
self.deal = deal
|
||||||
|
|
||||||
products = await self._group_deal_products_by_products(deal.products)
|
self._set_deals_ids_header()
|
||||||
|
|
||||||
|
if deal.group:
|
||||||
|
deals = await self._get_deals_by_group_id(deal.group.id)
|
||||||
|
for d in deals:
|
||||||
|
self.deal_doc["deals"].append(d)
|
||||||
|
grouped_products = await self._group_deal_products_by_products(d.products)
|
||||||
|
for product in grouped_products.values():
|
||||||
|
key = _regen_key_for_product(product)
|
||||||
|
if key not in self.deal_doc["products"]:
|
||||||
|
self.deal_doc["products"][key] = product
|
||||||
|
else:
|
||||||
|
self.deal_doc["products"][key]["quantity"] += product["quantity"]
|
||||||
|
self.deal_doc["products"][key]["warehouses_and_quantities"].append((
|
||||||
|
product["deal"].shipping_warehouse.name, product["quantity"],
|
||||||
|
))
|
||||||
|
else:
|
||||||
|
self.deal_doc["deals"] = [deal]
|
||||||
|
self.deal_doc["products"] = await self._group_deal_products_by_products(deal.products)
|
||||||
|
|
||||||
product_urls: List[Optional[str]] = []
|
product_urls: List[Optional[str]] = []
|
||||||
for product in products.values():
|
for product in self.deal_doc["products"].values():
|
||||||
if len(product["deal_products"][0].product.images) > 0:
|
if len(product["deal_products"][0].product.images) > 0:
|
||||||
product_urls.append(product["deal_products"][0].product.images[0].image_url)
|
product_urls.append(product["deal_products"][0].product.images[0].image_url)
|
||||||
else:
|
else:
|
||||||
product_urls.append(None)
|
product_urls.append(None)
|
||||||
product_images = await fetch_images(product_urls)
|
self.deal_doc["product_images"] = await fetch_images(product_urls)
|
||||||
|
|
||||||
document_deal_data: DealTechSpecData = {
|
|
||||||
"deal": deal,
|
|
||||||
"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-tech-spec.html")
|
template = ENV.get_template("deal/deal-tech-spec.html")
|
||||||
|
|
||||||
result = template.render({"data": document_deal_data, "sign_place_text": "_" * 22})
|
result = template.render({"data": self.deal_doc, "sign_place_text": "_" * 22})
|
||||||
return result
|
return result
|
||||||
|
|
||||||
async def create_deal_tech_spec_pdf(self, deal_id) -> BytesIO:
|
async def create_deal_tech_spec_pdf(self, deal_id) -> BytesIO:
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ hr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.deal-name-id-container {
|
.deal-name-id-container {
|
||||||
|
margin-top: 14px;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -28,8 +28,8 @@
|
|||||||
{% if common_product.color %}
|
{% if common_product.color %}
|
||||||
<div class="small-text">Цвет: {{ common_product.color }}</div>
|
<div class="small-text">Цвет: {{ common_product.color }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if product_data.deal_products|length > 1 %}
|
{% if product_data.deal_products|length > 1 or product_data.warehouses_and_quantities|length > 1 %}
|
||||||
{% if common_deal_product.quantity %}
|
{% if product_data.quantity %}
|
||||||
<div class="small-text">Суммарное количество: {{ product_data.quantity }}</div>
|
<div class="small-text">Суммарное количество: {{ product_data.quantity }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if product_data.additional_info %}
|
{% if product_data.additional_info %}
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<!--#endregion -->
|
<!--#endregion -->
|
||||||
<!--#region Product comment -->
|
<!--#region Product comment -->
|
||||||
{% if product_data.deal_products|length == 1 and common_deal_product.comment %}
|
{% if product_data.deal_products|length == 1 and common_deal_product.comment %}
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -88,9 +88,29 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>{{ common_deal_product.comment }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
<!--#endregion -->
|
||||||
|
<!--#region Product warehouses and quantities -->
|
||||||
|
{% if product_data.warehouses_and_quantities|length > 1 %}
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Склад отгрузки</th>
|
||||||
|
<th>Количество</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for row in product_data.warehouses_and_quantities %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ common_deal_product.comment }}</td>
|
<td>{{ row[0] }}</td>
|
||||||
|
<td>{{ row[1] }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -1,62 +1,67 @@
|
|||||||
<!--#region Header -->
|
<!--#region Header -->
|
||||||
<div class="deal-name-id-container">
|
<div style="page-break-inside: avoid">
|
||||||
<div class="medium-text bold">
|
{% if index != 0 %}
|
||||||
Название сделки: {{ data.deal.name }}
|
<hr/>
|
||||||
|
{% endif %}
|
||||||
|
<div class="deal-name-id-container">
|
||||||
|
<div class="medium-text bold">
|
||||||
|
Название сделки: {{ deal.name }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<hr/>
|
||||||
<hr/>
|
<div class="medium-text">
|
||||||
<div class="medium-text">
|
Дата создания: {{ now().strftime("%d.%m.%Y, %H:%M") }}
|
||||||
Дата создания: {{ now().strftime("%d.%m.%Y, %H:%M") }}
|
</div>
|
||||||
</div>
|
<div class="medium-text">
|
||||||
<div class="medium-text">
|
Текущий статус: {{ data.deal_status_str[deal.current_status] }}
|
||||||
Текущий статус: {{ 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">
|
|
||||||
|
|
||||||
Дата отгрузки:
|
Дата отгрузки:
|
||||||
{% if data.deal.delivery_date %}
|
{% if deal.delivery_date %}
|
||||||
{{ data.deal.delivery_date.strftime("%d.%m.%Y") }}
|
{{ deal.delivery_date.strftime("%d.%m.%Y") }}
|
||||||
|
{% else %}
|
||||||
|
Не указана
|
||||||
|
{% endif %}
|
||||||
|
</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 style="margin-bottom: 40px">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<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>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
{% else %}
|
{% else %}
|
||||||
Не указана
|
<div style="margin-bottom: 40px"></div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% if data.deal.base_marketplace.name %}
|
|
||||||
<div class="medium-text bold">Маркетплейс: {{ data.deal.base_marketplace.name }}</div>
|
|
||||||
{% endif %}
|
|
||||||
{% if data.deal.shipping_warehouse.name %}
|
|
||||||
<div class="medium-text bold">Склад отгрузки: {{ data.deal.shipping_warehouse.name }}</div>
|
|
||||||
{% endif %}
|
|
||||||
{% if data.deal.services|length > 0 %}
|
|
||||||
<table style="margin-bottom: 40px">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Общие услуги</th>
|
|
||||||
<th>Количество</th>
|
|
||||||
</tr>
|
|
||||||
<tfoot>
|
|
||||||
</tfoot>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for service in data.deal.services %}
|
|
||||||
<tr>
|
|
||||||
<td>{{ service.service.name }}</td>
|
|
||||||
<td>{{ '{:,}'.format(service.quantity) }} шт.</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{% else %}
|
|
||||||
<div style="margin-bottom: 40px"></div>
|
|
||||||
{% endif %}
|
|
||||||
<!--#endregion -->
|
<!--#endregion -->
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
@top-right {
|
@top-right {
|
||||||
content: "ID:{{ data.deal.id }}";
|
content: "{{ data.deal_ids_header }}";
|
||||||
border: solid black 1px;
|
border: solid black 1px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 12px 2px;
|
padding: 12px 2px;
|
||||||
@@ -44,7 +44,11 @@
|
|||||||
<div class="doc-container">
|
<div class="doc-container">
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
{% include "deal/deal-services.html" %}
|
{% for deal in data.deals %}
|
||||||
|
{% with index = loop.index0 %}
|
||||||
|
{% include "deal/deal-services.html" %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endfor %}
|
||||||
{% for product_data in data.products.values() %}
|
{% 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" %}
|
||||||
|
|||||||
Reference in New Issue
Block a user