From 915a1d5f28f4882db31528ef71ad9a2b54d63286 Mon Sep 17 00:00:00 2001 From: AlexSserb Date: Sun, 10 Nov 2024 15:32:58 +0400 Subject: [PATCH] feat: tech specification for a group of deals --- generators/deal_pdf_generator/deal_data.py | 18 ++- generators/deal_pdf_generator/generator.py | 100 ++++++++++++--- static/css/deal-tech-spec.css | 1 + .../documents/deal/deal-product-services.html | 28 ++++- templates/documents/deal/deal-services.html | 115 +++++++++--------- templates/documents/deal/deal-tech-spec.html | 8 +- 6 files changed, 186 insertions(+), 84 deletions(-) diff --git a/generators/deal_pdf_generator/deal_data.py b/generators/deal_pdf_generator/deal_data.py index c9781b9..8d2f4d9 100644 --- a/generators/deal_pdf_generator/deal_data.py +++ b/generators/deal_pdf_generator/deal_data.py @@ -4,16 +4,22 @@ from models import DealProduct, Deal, DealStatusHistory class DealTechSpecProductData(TypedDict): - deal_products: List[DealProduct] + deal: Deal + last_status: DealStatusHistory total_one_product: int quantity: int additional_info: Optional[str] + # Поле для группировки товаров с одним артикулом и вывода таблицы [Штрихкод, Размер, Кол-во, Короба] + deal_products: List[DealProduct] + + # Поле для группировки товаров из нескольких сделок и вывода таблицы [Склад отгрузки, Кол-во] + warehouses_and_quantities: List[Tuple[str, int]] + class DealTechSpecData(TypedDict): - deal: Deal + deals: List[Deal] products: Dict[str, DealTechSpecProductData] - current_status_str: str - last_status: DealStatusHistory - product_images: Tuple[str] - + product_images: Tuple + deal_ids_header: str + deal_status_str: list[str] diff --git a/generators/deal_pdf_generator/generator.py b/generators/deal_pdf_generator/generator.py index 4950cc1..2e93319 100644 --- a/generators/deal_pdf_generator/generator.py +++ b/generators/deal_pdf_generator/generator.py @@ -8,13 +8,46 @@ from weasyprint import HTML, CSS from constants import DEAL_STATUS_STR, ENV, APP_PATH 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 +# Генерация ключа для группировки 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: def __init__(self, session: AsyncSession): self._session = session + self.deal_doc: DealTechSpecData = { + "deals": [], + "products": {}, + "product_images": (), + "deal_ids_header": "", + "deal_status_str": DEAL_STATUS_STR, + } + self.deal: Deal @staticmethod 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: # Для группировки по артикулу и услугам - key = f"{deal_product.product.article} - " + ",".join( - str(service.service_id) for service in deal_product.services - ) + key = _gen_key_for_product(deal_product) if key not in products: products[key] = { + "deal": deal_product.deal, "deal_products": [deal_product], "quantity": deal_product.quantity, "additional_info": deal_product.product.additional_info, + "warehouses_and_quantities": [], } else: products[key]["deal_products"].append(deal_product) @@ -50,39 +83,72 @@ class DealTechSpecPdfGenerator: selectinload(Deal.products).selectinload(DealProduct.product).selectinload(Product.barcodes), selectinload(Deal.services).selectinload(DealServiceModel.service), selectinload(Deal.status_history), + selectinload(Deal.group).selectinload(DealGroup.deals), joinedload(Deal.client), joinedload(Deal.shipping_warehouse), ) ) 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): deal = await self._get_deal_by_id(deal_id) - if not deal: 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]] = [] - for product in products.values(): + for product in self.deal_doc["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: 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, - } + self.deal_doc["product_images"] = await fetch_images(product_urls) 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 async def create_deal_tech_spec_pdf(self, deal_id) -> BytesIO: diff --git a/static/css/deal-tech-spec.css b/static/css/deal-tech-spec.css index da001ff..3980aab 100644 --- a/static/css/deal-tech-spec.css +++ b/static/css/deal-tech-spec.css @@ -29,6 +29,7 @@ hr { } .deal-name-id-container { + margin-top: 14px; justify-content: space-between; display: flex; align-items: center; diff --git a/templates/documents/deal/deal-product-services.html b/templates/documents/deal/deal-product-services.html index 27319e3..d0a5881 100644 --- a/templates/documents/deal/deal-product-services.html +++ b/templates/documents/deal/deal-product-services.html @@ -28,8 +28,8 @@ {% if common_product.color %}
Цвет: {{ common_product.color }}
{% endif %} - {% if product_data.deal_products|length > 1 %} - {% if common_deal_product.quantity %} + {% if product_data.deal_products|length > 1 or product_data.warehouses_and_quantities|length > 1 %} + {% if product_data.quantity %}
Суммарное количество: {{ product_data.quantity }}
{% endif %} {% if product_data.additional_info %} @@ -80,7 +80,7 @@ - {% if product_data.deal_products|length == 1 and common_deal_product.comment %} + {% if product_data.deal_products|length == 1 and common_deal_product.comment %} @@ -88,9 +88,29 @@ + + + + +
{{ common_deal_product.comment }}
+ {% endif %} + + + {% if product_data.warehouses_and_quantities|length > 1 %} + + + + + + + + + {% for row in product_data.warehouses_and_quantities %} - + + + {% endfor %}
Склад отгрузкиКоличество
{{ common_deal_product.comment }}{{ row[0] }}{{ row[1] }}
{% endif %} diff --git a/templates/documents/deal/deal-services.html b/templates/documents/deal/deal-services.html index 25ec48f..da1a0b9 100644 --- a/templates/documents/deal/deal-services.html +++ b/templates/documents/deal/deal-services.html @@ -1,62 +1,67 @@ -
-
- Название сделки: {{ data.deal.name }} +
+ {% if index != 0 %} +
+ {% endif %} +
+
+ Название сделки: {{ deal.name }} +
-
-
-
- Дата создания: {{ now().strftime("%d.%m.%Y, %H:%M") }} -
-
- Текущий статус: {{ data.current_status_str }} -
-
- {% if data.deal.comment %} - Комментарий к сделке: {{ data.deal.comment }} - {% endif %} -
-
- - -
- Клиент: {{ data.deal.client.name }} -
-
+
+
+ Дата создания: {{ now().strftime("%d.%m.%Y, %H:%M") }} +
+
+ Текущий статус: {{ data.deal_status_str[deal.current_status] }} +
+
+ {% if deal.comment %} + Комментарий к сделке: {{ deal.comment }} + {% endif %} +
+
+ + +
+ Клиент: {{ deal.client.name }} +
+
- Дата отгрузки: - {% if data.deal.delivery_date %} - {{ data.deal.delivery_date.strftime("%d.%m.%Y") }} + Дата отгрузки: + {% if deal.delivery_date %} + {{ deal.delivery_date.strftime("%d.%m.%Y") }} + {% else %} + Не указана + {% endif %} +
+ {% if deal.base_marketplace.name %} +
Маркетплейс: {{ deal.base_marketplace.name }}
+ {% endif %} + {% if deal.shipping_warehouse.name %} +
Склад отгрузки: {{ deal.shipping_warehouse.name }}
+ {% endif %} + {% if deal.services|length > 0 %} + + + + + + + + + + + {% for service in deal.services %} + + + + + {% endfor %} + +
Общие услугиКоличество
{{ service.service.name }}{{ '{:,}'.format(service.quantity) }} шт.
{% else %} - Не указана +
{% endif %}
-{% if data.deal.base_marketplace.name %} -
Маркетплейс: {{ data.deal.base_marketplace.name }}
-{% endif %} -{% if data.deal.shipping_warehouse.name %} -
Склад отгрузки: {{ data.deal.shipping_warehouse.name }}
-{% endif %} -{% if data.deal.services|length > 0 %} - - - - - - - - - - - {% for service in data.deal.services %} - - - - - {% endfor %} - -
Общие услугиКоличество
{{ service.service.name }}{{ '{:,}'.format(service.quantity) }} шт.
-{% else %} -
-{% endif %} \ No newline at end of file diff --git a/templates/documents/deal/deal-tech-spec.html b/templates/documents/deal/deal-tech-spec.html index 795221e..2e7d2c2 100644 --- a/templates/documents/deal/deal-tech-spec.html +++ b/templates/documents/deal/deal-tech-spec.html @@ -29,7 +29,7 @@ font-size: 18px; } @top-right { - content: "ID:{{ data.deal.id }}"; + content: "{{ data.deal_ids_header }}"; border: solid black 1px; border-radius: 6px; padding: 12px 2px; @@ -44,7 +44,11 @@
- {% 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() %} {% with index = loop.index %} {% include "deal/deal-product-services.html" %}