feat: tech specification for a group of deals

This commit is contained in:
2024-11-10 15:32:58 +04:00
parent 30ec0528a4
commit 915a1d5f28
6 changed files with 186 additions and 84 deletions

View File

@@ -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]

View File

@@ -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:

View File

@@ -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;

View File

@@ -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 %}

View File

@@ -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 -->

View File

@@ -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" %}