From b8d4e0ddd1653657c239c5d4b13643119f7fc28d Mon Sep 17 00:00:00 2001 From: zosimovaa Date: Thu, 13 Nov 2025 22:46:49 +0300 Subject: [PATCH] =?UTF-8?q?=D0=98=20=D0=B5=D1=89=D0=B5=20=D0=BE=D0=B4?= =?UTF-8?q?=D0=B8=D0=BD=20=D1=88=D0=B0=D0=B3=20=D0=BA=20=D1=83=D1=81=D0=BF?= =?UTF-8?q?=D0=B5=D1=85=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../task_handler/handlers/__init__.py | 7 ++- .../{check_avaiability.py => check_stock.py} | 9 ++-- .../handlers/abcp_clients/create_order.py | 37 ++++++++++++++ .../handlers/abcp_clients/order_creator.py | 21 -------- .../handlers/checks/price_quantity_ckecker.py | 0 .../excel_parcers/basic_excel_parcer.py | 6 +-- .../handlers/notifications/test_notifier.py | 4 +- .../validators/price_quantity_ckecker.py | 50 +++++++++++++++++++ .../task_handler/order/auto_part_order.py | 14 +++++- .../task_handler/order/auto_part_position.py | 18 ++++--- .../task_handler/order/ordet_status.py | 10 ---- .../configs/mikado-parts.ru.yml | 26 +++++++--- 12 files changed, 142 insertions(+), 60 deletions(-) rename src/mail_order_bot/task_handler/handlers/abcp_clients/{check_avaiability.py => check_stock.py} (70%) create mode 100644 src/mail_order_bot/task_handler/handlers/abcp_clients/create_order.py delete mode 100644 src/mail_order_bot/task_handler/handlers/abcp_clients/order_creator.py delete mode 100644 src/mail_order_bot/task_handler/handlers/checks/price_quantity_ckecker.py create mode 100644 src/mail_order_bot/task_handler/handlers/validators/price_quantity_ckecker.py delete mode 100644 src/mail_order_bot/task_handler/order/ordet_status.py diff --git a/src/mail_order_bot/task_handler/handlers/__init__.py b/src/mail_order_bot/task_handler/handlers/__init__.py index 7852316..da50114 100644 --- a/src/mail_order_bot/task_handler/handlers/__init__.py +++ b/src/mail_order_bot/task_handler/handlers/__init__.py @@ -1,6 +1,9 @@ -from .abcp_clients.check_avaiability import CheckAvailabilityTest -from .abcp_clients.order_creator import InstantOrderTest +from .abcp_clients.check_stock import GetStock +from .abcp_clients.create_order import InstantOrderTest from .excel_parcers.basic_excel_parcer import BasicExcelParser from .notifications.test_notifier import TestNotifier + + +from .validators.price_quantity_ckecker import CheckOrder \ No newline at end of file diff --git a/src/mail_order_bot/task_handler/handlers/abcp_clients/check_avaiability.py b/src/mail_order_bot/task_handler/handlers/abcp_clients/check_stock.py similarity index 70% rename from src/mail_order_bot/task_handler/handlers/abcp_clients/check_avaiability.py rename to src/mail_order_bot/task_handler/handlers/abcp_clients/check_stock.py index b2476c6..ae8b1aa 100644 --- a/src/mail_order_bot/task_handler/handlers/abcp_clients/check_avaiability.py +++ b/src/mail_order_bot/task_handler/handlers/abcp_clients/check_stock.py @@ -9,7 +9,7 @@ logger = logging.getLogger(__name__) def get_stock(brand, part_number): return random.randint(0, 10) -class CheckAvailabilityTest(AbstractTask): +class GetStock(AbstractTask): def do(self) -> None: positions = self.order.positions @@ -18,8 +18,9 @@ class CheckAvailabilityTest(AbstractTask): def _update_stock(self, position): # Эмулируем получение данных - stock = random.randint(0, 10) - price = position.price + max_stock = self.config.get('max_stock',10) + stock = random.randint(0, max_stock) + price = position.requested_price position.stock_price = price - position.stock_remaining = stock + position.stock_quantity = stock diff --git a/src/mail_order_bot/task_handler/handlers/abcp_clients/create_order.py b/src/mail_order_bot/task_handler/handlers/abcp_clients/create_order.py new file mode 100644 index 0000000..2f233e6 --- /dev/null +++ b/src/mail_order_bot/task_handler/handlers/abcp_clients/create_order.py @@ -0,0 +1,37 @@ +import logging +import requests +from mail_order_bot.task_handler.handlers.abstract_task import AbstractTask +from mail_order_bot.task_handler.order.auto_part_order import OrderStatus + +logger = logging.getLogger(__name__) + +class InstantOrderTest(AbstractTask): + URL = "https://api.telegram.org/bot{0}/sendMessage?chat_id={1}&text={2}" + + def do(self) -> None: + api_key = self.config["api_key"] + chat_id = self.config["chat_id"] + + if self.order.status == OrderStatus.IN_PROGRESS: + positions = self.order.positions + + message = f"Запрос на создание заказа от {self.context['client']}:\n" + message += "\n".join(f"{pos.sku}: {pos.name} ({pos.order_quantity} x {pos.order_price} = {pos.total})" for pos in positions) + + elif self.order.status == OrderStatus.OPERATOR_REQUIRED: + message = f"Запрос на создание заказа от {self.context['client']} отклонен - необходима ручная обработка.\n" + message += f"Причина: {self.order.reason}" + + else: + message = f"Запрос на создание заказа от {self.context['client']} отклонен.\n" + message += f" Статус заказа: {self.order.status}" + + #url = self.URL.format(api_key, chat_id, message) + #resp = requests.get(url).json() + print(message) + #logger.info(resp) + + + + + diff --git a/src/mail_order_bot/task_handler/handlers/abcp_clients/order_creator.py b/src/mail_order_bot/task_handler/handlers/abcp_clients/order_creator.py deleted file mode 100644 index 6af89ad..0000000 --- a/src/mail_order_bot/task_handler/handlers/abcp_clients/order_creator.py +++ /dev/null @@ -1,21 +0,0 @@ -import logging -import requests -from mail_order_bot.task_handler.handlers.abstract_task import AbstractTask - -logger = logging.getLogger(__name__) - -class InstantOrderTest(AbstractTask): - URL = "https://api.telegram.org/bot{0}/sendMessage?chat_id={1}&text={2}" - - def do(self) -> None: - - positions = self.order.positions - - message = f"Запрос на создание заказа от {self.context['client']}:\n" - message += "\n".join(f"{pos.article}: {pos.name} ({pos.quantity} x {pos.price} = {pos.total})" for pos in positions) - - api_key = self.config["api_key"] - chat_id = self.config["chat_id"] - url = self.URL.format(api_key, chat_id, message) - resp = requests.get(url).json() - logger.info(resp) diff --git a/src/mail_order_bot/task_handler/handlers/checks/price_quantity_ckecker.py b/src/mail_order_bot/task_handler/handlers/checks/price_quantity_ckecker.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/mail_order_bot/task_handler/handlers/excel_parcers/basic_excel_parcer.py b/src/mail_order_bot/task_handler/handlers/excel_parcers/basic_excel_parcer.py index 785cd24..ebc130c 100644 --- a/src/mail_order_bot/task_handler/handlers/excel_parcers/basic_excel_parcer.py +++ b/src/mail_order_bot/task_handler/handlers/excel_parcers/basic_excel_parcer.py @@ -74,11 +74,11 @@ class BasicExcelParser(AbstractTask): # Создаем объект позиции position = AutoPartPosition( - article=str(row[mapping['article']]).strip(), + sku=str(row[mapping['article']]).strip(), manufacturer=str(row[mapping.get('manufacturer', "")]).strip(), name=name, - price=price, - quantity=quantity, + requested_price=price, + requested_quantity=quantity, total=total, additional_attrs=self._extract_additional_attrs(row, mapping) ) diff --git a/src/mail_order_bot/task_handler/handlers/notifications/test_notifier.py b/src/mail_order_bot/task_handler/handlers/notifications/test_notifier.py index c2a8e64..055fb12 100644 --- a/src/mail_order_bot/task_handler/handlers/notifications/test_notifier.py +++ b/src/mail_order_bot/task_handler/handlers/notifications/test_notifier.py @@ -11,5 +11,5 @@ class TestNotifier(AbstractTask): print(f"\nПолучено {len(positions)} позиций от {self.context["client"]}:") for pos in positions: # Первые 5 - print(f" - {pos.article}: {pos.name} " - f"({pos.quantity} x {pos.price} = {pos.total})") + print(f" - {pos.sku}: {pos.name} " + f"({pos.requested_quantity} x {pos.requested_price} = {pos.total})") diff --git a/src/mail_order_bot/task_handler/handlers/validators/price_quantity_ckecker.py b/src/mail_order_bot/task_handler/handlers/validators/price_quantity_ckecker.py new file mode 100644 index 0000000..1954f3d --- /dev/null +++ b/src/mail_order_bot/task_handler/handlers/validators/price_quantity_ckecker.py @@ -0,0 +1,50 @@ +import random +import logging +from mail_order_bot.task_handler.handlers.abstract_task import AbstractTask +from mail_order_bot.task_handler.order.auto_part_order import OrderStatus +from decimal import Decimal +import random +logger = logging.getLogger(__name__) + + +class CheckOrder(AbstractTask): + + def do(self) -> None: + refused = 0 + positions = self.order.positions + for position in positions: + self._set_order_price(position) + self._set_order_quantity(position) + + if position.order_price == 0 or position.order_quantity == 0: + refused += 1 + + self._check_refusal_threshold(refused) + + + def _set_order_price(self, position): + # Эмулируем получение данных + acceptable_price_reduction = self.config.get("acceptable_price_reduction") + acceptable_price = position.stock_price* Decimal(str((1-acceptable_price_reduction/100))) + + if position.requested_price < acceptable_price: + position.order_price = 0 + else: + position.order_price = position.requested_price + + def _set_order_quantity(self, position): + max_stock = self.config.get("max_stock", 100) + min_stock = self.config.get("min_stock", 0) + + stock_quantity = random.randint(min_stock, max_stock) + + position.order_quantity = max(0, min(position.stock_quantity, stock_quantity)) + + def _check_refusal_threshold(self, refused): + refusal_threshold_limit = self.config.get("refusal_threshold", 1) + refusal_level = refused/len(self.order.positions) + + if refusal_level > refusal_threshold_limit: + self.order.status = OrderStatus.OPERATOR_REQUIRED + self.order.reason = "Превышен порог отказов" + diff --git a/src/mail_order_bot/task_handler/order/auto_part_order.py b/src/mail_order_bot/task_handler/order/auto_part_order.py index 33aed49..24e0f91 100644 --- a/src/mail_order_bot/task_handler/order/auto_part_order.py +++ b/src/mail_order_bot/task_handler/order/auto_part_order.py @@ -1,11 +1,21 @@ from typing import List, Optional from .auto_part_position import AutoPartPosition -from .ordet_status import OrderStatus +from enum import Enum + +class OrderStatus(Enum): + NEW = "new" + IN_PROGRESS = "in progress" + FAILED = "failed" + COMPLETED = "completed" + OPERATOR_REQUIRED = "operator required" + INVALID = "invalid" + class AutoPartOrder: def __init__(self): self.positions: List[AutoPartPosition] = [] self.status = OrderStatus.NEW + self.reason = "" def add_position(self, position: AutoPartPosition) -> None: self.positions.append(position) @@ -15,7 +25,7 @@ class AutoPartOrder: def find_positions(self, brand: Optional[str] = None, sku: Optional[str] = None) -> List[AutoPartPosition]: results = self.positions if brand is not None: - results = [p for p in results if p.brand == brand] + results = [p for p in results if p.manufacturer == brand] if sku is not None: results = [p for p in results if p.sku == sku] return results diff --git a/src/mail_order_bot/task_handler/order/auto_part_position.py b/src/mail_order_bot/task_handler/order/auto_part_position.py index afbc1cd..6b5c65f 100644 --- a/src/mail_order_bot/task_handler/order/auto_part_position.py +++ b/src/mail_order_bot/task_handler/order/auto_part_position.py @@ -10,22 +10,24 @@ class AutoPartPosition: Унифицированная модель позиции для заказа. Все контрагенты приводятся к этой структуре. """ - article: str # Артикул товара + sku: str # Артикул товара manufacturer: str # Производитель name: str # Наименование - price: Decimal # Цена за единицу - quantity: int # Количество + requested_price: Decimal # Цена за единицу + requested_quantity: int # Количество total: Decimal # Общая сумма - stock_remaining: int = 0 # Остаток на складе + stock_quantity: int = 0 # Остаток на складе stock_price: Decimal = Decimal('0.0') # Цена на складе + order_quantity: int = 0 # Количество для заказа + order_price: Decimal = Decimal('0.0') # Цена в заказе additional_attrs: Dict[str, Any] = field(default_factory=dict) def __post_init__(self): """Валидация после инициализации""" - if self.quantity < 0: - raise ValueError(f"Количество не может быть отрицательным: {self.quantity}") - if self.price < 0: - raise ValueError(f"Цена не может быть отрицательной: {self.price}") + if self.requested_quantity < 0: + raise ValueError(f"Количество не может быть отрицательным: {self.requested_quantity}") + if self.requested_price < 0: + raise ValueError(f"Цена не может быть отрицательной: {self.requested_price}") diff --git a/src/mail_order_bot/task_handler/order/ordet_status.py b/src/mail_order_bot/task_handler/order/ordet_status.py deleted file mode 100644 index d43e558..0000000 --- a/src/mail_order_bot/task_handler/order/ordet_status.py +++ /dev/null @@ -1,10 +0,0 @@ -from enum import Enum - - -class OrderStatus(Enum): - NEW = 1 - IN_PROGRESS = 2 - FAILED = 4 - COMPLETED = 3 - OPERATOR_REQUIRED = 5 - INVALID = 6 \ No newline at end of file diff --git a/tests/excel_processor/configs/mikado-parts.ru.yml b/tests/excel_processor/configs/mikado-parts.ru.yml index 589ae2b..692a078 100644 --- a/tests/excel_processor/configs/mikado-parts.ru.yml +++ b/tests/excel_processor/configs/mikado-parts.ru.yml @@ -1,12 +1,10 @@ -# Конфигурационный файл для контрагента todx.ru - +# Конфигурационный файл для контрагента mikado-parts.ru pipeline: - - - # Обработчик вложений - - handler: "BasicExcelParser" + # Обработчик вложений - извлекает из экселя данные + - handler: BasicExcelParser config: sheet_name: 0 # Можно указать индекс листа - key_field: "артикул" + key_field: "артикул" # Поле, по которому будет определяться заголовок блока с данными и будут отсекаться незаполненные строки mapping: article: "артикул" manufacturer: "бренд" @@ -14,14 +12,26 @@ pipeline: price: "цена" quantity: "количество" - - handler: "CheckAvailabilityTest" + # Обработчик получает данные со склада о цене и остатках по каждой позиций + - handler: GetStock + config: + max_stock: 2 + min_stock: 0 + # Обработчик проверяет заказ на возможность автоматической обработки + - handler: CheckOrder + config: + acceptable_price_reduction: 2 + refusal_threshold: 0.1 + + # Создание заказа - handler: InstantOrderTest config: api_key: "8056899069:AAFEfw9QRMvmEwQyH0CI4e_v_sZuOSdNWcE" chat_id: 211945135 - - handler: "TestNotifier" + # Отправка уведомлений менеджерам + #- handler: "TestNotifier"