И еще один шаг к успеху
This commit is contained in:
@@ -1,6 +1,9 @@
|
|||||||
from .abcp_clients.check_avaiability import CheckAvailabilityTest
|
from .abcp_clients.check_stock import GetStock
|
||||||
from .abcp_clients.order_creator import InstantOrderTest
|
from .abcp_clients.create_order import InstantOrderTest
|
||||||
|
|
||||||
from .excel_parcers.basic_excel_parcer import BasicExcelParser
|
from .excel_parcers.basic_excel_parcer import BasicExcelParser
|
||||||
|
|
||||||
from .notifications.test_notifier import TestNotifier
|
from .notifications.test_notifier import TestNotifier
|
||||||
|
|
||||||
|
|
||||||
|
from .validators.price_quantity_ckecker import CheckOrder
|
||||||
@@ -9,7 +9,7 @@ logger = logging.getLogger(__name__)
|
|||||||
def get_stock(brand, part_number):
|
def get_stock(brand, part_number):
|
||||||
return random.randint(0, 10)
|
return random.randint(0, 10)
|
||||||
|
|
||||||
class CheckAvailabilityTest(AbstractTask):
|
class GetStock(AbstractTask):
|
||||||
|
|
||||||
def do(self) -> None:
|
def do(self) -> None:
|
||||||
positions = self.order.positions
|
positions = self.order.positions
|
||||||
@@ -18,8 +18,9 @@ class CheckAvailabilityTest(AbstractTask):
|
|||||||
|
|
||||||
def _update_stock(self, position):
|
def _update_stock(self, position):
|
||||||
# Эмулируем получение данных
|
# Эмулируем получение данных
|
||||||
stock = random.randint(0, 10)
|
max_stock = self.config.get('max_stock',10)
|
||||||
price = position.price
|
stock = random.randint(0, max_stock)
|
||||||
|
price = position.requested_price
|
||||||
|
|
||||||
position.stock_price = price
|
position.stock_price = price
|
||||||
position.stock_remaining = stock
|
position.stock_quantity = stock
|
||||||
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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)
|
|
||||||
@@ -74,11 +74,11 @@ class BasicExcelParser(AbstractTask):
|
|||||||
|
|
||||||
# Создаем объект позиции
|
# Создаем объект позиции
|
||||||
position = AutoPartPosition(
|
position = AutoPartPosition(
|
||||||
article=str(row[mapping['article']]).strip(),
|
sku=str(row[mapping['article']]).strip(),
|
||||||
manufacturer=str(row[mapping.get('manufacturer', "")]).strip(),
|
manufacturer=str(row[mapping.get('manufacturer', "")]).strip(),
|
||||||
name=name,
|
name=name,
|
||||||
price=price,
|
requested_price=price,
|
||||||
quantity=quantity,
|
requested_quantity=quantity,
|
||||||
total=total,
|
total=total,
|
||||||
additional_attrs=self._extract_additional_attrs(row, mapping)
|
additional_attrs=self._extract_additional_attrs(row, mapping)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -11,5 +11,5 @@ class TestNotifier(AbstractTask):
|
|||||||
|
|
||||||
print(f"\nПолучено {len(positions)} позиций от {self.context["client"]}:")
|
print(f"\nПолучено {len(positions)} позиций от {self.context["client"]}:")
|
||||||
for pos in positions: # Первые 5
|
for pos in positions: # Первые 5
|
||||||
print(f" - {pos.article}: {pos.name} "
|
print(f" - {pos.sku}: {pos.name} "
|
||||||
f"({pos.quantity} x {pos.price} = {pos.total})")
|
f"({pos.requested_quantity} x {pos.requested_price} = {pos.total})")
|
||||||
|
|||||||
@@ -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 = "Превышен порог отказов"
|
||||||
|
|
||||||
@@ -1,11 +1,21 @@
|
|||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
from .auto_part_position import AutoPartPosition
|
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:
|
class AutoPartOrder:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.positions: List[AutoPartPosition] = []
|
self.positions: List[AutoPartPosition] = []
|
||||||
self.status = OrderStatus.NEW
|
self.status = OrderStatus.NEW
|
||||||
|
self.reason = ""
|
||||||
|
|
||||||
def add_position(self, position: AutoPartPosition) -> None:
|
def add_position(self, position: AutoPartPosition) -> None:
|
||||||
self.positions.append(position)
|
self.positions.append(position)
|
||||||
@@ -15,7 +25,7 @@ class AutoPartOrder:
|
|||||||
def find_positions(self, brand: Optional[str] = None, sku: Optional[str] = None) -> List[AutoPartPosition]:
|
def find_positions(self, brand: Optional[str] = None, sku: Optional[str] = None) -> List[AutoPartPosition]:
|
||||||
results = self.positions
|
results = self.positions
|
||||||
if brand is not None:
|
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:
|
if sku is not None:
|
||||||
results = [p for p in results if p.sku == sku]
|
results = [p for p in results if p.sku == sku]
|
||||||
return results
|
return results
|
||||||
|
|||||||
@@ -10,22 +10,24 @@ class AutoPartPosition:
|
|||||||
Унифицированная модель позиции для заказа.
|
Унифицированная модель позиции для заказа.
|
||||||
Все контрагенты приводятся к этой структуре.
|
Все контрагенты приводятся к этой структуре.
|
||||||
"""
|
"""
|
||||||
article: str # Артикул товара
|
sku: str # Артикул товара
|
||||||
manufacturer: str # Производитель
|
manufacturer: str # Производитель
|
||||||
name: str # Наименование
|
name: str # Наименование
|
||||||
price: Decimal # Цена за единицу
|
requested_price: Decimal # Цена за единицу
|
||||||
quantity: int # Количество
|
requested_quantity: int # Количество
|
||||||
total: Decimal # Общая сумма
|
total: Decimal # Общая сумма
|
||||||
stock_remaining: int = 0 # Остаток на складе
|
stock_quantity: int = 0 # Остаток на складе
|
||||||
stock_price: Decimal = Decimal('0.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)
|
additional_attrs: Dict[str, Any] = field(default_factory=dict)
|
||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
"""Валидация после инициализации"""
|
"""Валидация после инициализации"""
|
||||||
if self.quantity < 0:
|
if self.requested_quantity < 0:
|
||||||
raise ValueError(f"Количество не может быть отрицательным: {self.quantity}")
|
raise ValueError(f"Количество не может быть отрицательным: {self.requested_quantity}")
|
||||||
if self.price < 0:
|
if self.requested_price < 0:
|
||||||
raise ValueError(f"Цена не может быть отрицательной: {self.price}")
|
raise ValueError(f"Цена не может быть отрицательной: {self.requested_price}")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -1,12 +1,10 @@
|
|||||||
# Конфигурационный файл для контрагента todx.ru
|
# Конфигурационный файл для контрагента mikado-parts.ru
|
||||||
|
|
||||||
pipeline:
|
pipeline:
|
||||||
-
|
# Обработчик вложений - извлекает из экселя данные
|
||||||
# Обработчик вложений
|
- handler: BasicExcelParser
|
||||||
- handler: "BasicExcelParser"
|
|
||||||
config:
|
config:
|
||||||
sheet_name: 0 # Можно указать индекс листа
|
sheet_name: 0 # Можно указать индекс листа
|
||||||
key_field: "артикул"
|
key_field: "артикул" # Поле, по которому будет определяться заголовок блока с данными и будут отсекаться незаполненные строки
|
||||||
mapping:
|
mapping:
|
||||||
article: "артикул"
|
article: "артикул"
|
||||||
manufacturer: "бренд"
|
manufacturer: "бренд"
|
||||||
@@ -14,14 +12,26 @@ pipeline:
|
|||||||
price: "цена"
|
price: "цена"
|
||||||
quantity: "количество"
|
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
|
- handler: InstantOrderTest
|
||||||
config:
|
config:
|
||||||
api_key: "8056899069:AAFEfw9QRMvmEwQyH0CI4e_v_sZuOSdNWcE"
|
api_key: "8056899069:AAFEfw9QRMvmEwQyH0CI4e_v_sZuOSdNWcE"
|
||||||
chat_id: 211945135
|
chat_id: 211945135
|
||||||
|
|
||||||
- handler: "TestNotifier"
|
# Отправка уведомлений менеджерам
|
||||||
|
#- handler: "TestNotifier"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user