2 Commits

Author SHA1 Message Date
816da1eb16 no message 2025-12-11 21:28:07 +03:00
03aca0ecb9 no message 2025-12-09 22:09:13 +03:00
10 changed files with 101 additions and 42 deletions

View File

@@ -13,22 +13,20 @@ class AbcpProvider:
"Content-Type": "application/x-www-form-urlencoded" "Content-Type": "application/x-www-form-urlencoded"
} }
def __init__(self): def __init__(self, account="SYSTEM"):
self.base_url = self.HOST self.base_url = self.HOST
self.login = os.getenv("ABCP_LOGIN")
password = os.getenv("ABCP_PASSWORD")
self.password = hashlib.md5(password.encode("utf-8")).hexdigest()
def get_stock(self, sku, manufacturer): def get_stock(self, sku, manufacturer, partner="SYSTEM"):
method = "GET" method = "GET"
path = "/search/articles" path = "/search/articles"
params = {"number": sku, "brand": manufacturer, "withOutAnalogs": "1"} params = {"number": sku, "brand": manufacturer, "withOutAnalogs": "1"}
return self._execute(path, method, params) return self._execute(partner, path, method, params)
def _execute(self, partner, path, method="GET", params={}, data=None, ):
params["userlogin"] = os.getenv(f"ABCP_LOGIN_{partner}")
params["userpsw"] = hashlib.md5(os.getenv(f"ABCP_PASSWORD_{partner}").encode("utf-8")).hexdigest()
def _execute(self, path, method="GET", params={}, data=None):
params["userlogin"] = self.login
params["userpsw"] = self.password
response = requests.request(method, self.HOST+path, data=data, headers=self.HEADERS, params=params) response = requests.request(method, self.HOST+path, data=data, headers=self.HEADERS, params=params)
payload = response.json() payload = response.json()
if response.status_code == 200: if response.status_code == 200:

View File

@@ -12,10 +12,15 @@ pipeline:
quantity: "Кол-во" quantity: "Кол-во"
total: "Сумма" total: "Сумма"
# Определяем логику обработки заказа (в данном случае все с локального склада)
- handler: DeliveryPeriodLocalStore
# Запрос остатков со склада
- handler: GetStock - handler: GetStock
- handler: LocalStoreOrder

View File

@@ -1,11 +1,11 @@
from .attachment_handler.attachment_handler import AttachmentHandler from .attachment_handler.attachment_handler import AttachmentHandler
from .excel_parcers.order_parcer_basic import BasicExcelParser from .excel_parcers.order_parcer_basic import BasicExcelParser
from .destination_time.local_store import DeliveryPeriodLocalStore from .destination_time.local_store import DeliveryPeriodLocalStore
from .logic.only_local_store_order import LocalStoreOrder
from .abcp.get_stock import GetStock
from .abcp.create_order import InstantOrderTest from .abcp.api_get_stock import GetStock
from .abcp.api_create_order import InstantOrderTest

View File

@@ -7,7 +7,7 @@ from mail_order_bot.abcp_api.abcp_provider import AbcpProvider
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class GetStock(AbstractTask): class APIGetStock(AbstractTask):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.abcp_provider = AbcpProvider() self.abcp_provider = AbcpProvider()
@@ -15,10 +15,12 @@ class GetStock(AbstractTask):
def do(self) -> None: def do(self) -> None:
attachments = self.context.data.get("attachments", []) attachments = self.context.data.get("attachments", [])
for attachment in attachments: for attachment in attachments:
for position in attachment["order"].positions: order = attachment["order"]
position.update_stock(self.get_stock(position.sku, position.manufacturer)) for position in order.positions:
stock = self.get_stock(position.sku, position.manufacturer)
position.update_stock(stock, order.delivery_period)
position.fill_from_stock()
logger.info(f"Получены позиции со склада для файла {attachment.get('name', "no name")}") logger.info(f"Получены позиции со склада для файла {attachment.get('name', "no name")}")
def get_stock(self, sku: str, manufacturer: str) -> int: def get_stock(self, sku: str, manufacturer: str) -> int:
return self.abcp_provider.get_stock(sku, manufacturer) return self.abcp_provider.get_stock(sku, manufacturer)

View File

@@ -14,7 +14,7 @@ class DeliveryPeriodLocalStore(AbstractTask):
for attachment in attachments: for attachment in attachments:
order = attachment["order"] order = attachment["order"]
order.set_delivery_period(0) order.set_delivery_period(0)
logger.debug(f"Доставка только с локального склада, срок 1 день.") logger.info(f"Доставка только с локального склада, срок 1 день.")

View File

@@ -21,4 +21,4 @@ class LocalStoreOrder(AbstractTask):
attachments = self.context.data["attachments"] attachments = self.context.data["attachments"]
for attachment in attachments: for attachment in attachments:
order = attachment["order"] order = attachment["order"]
order.fill_from_local_supplier()

View File

@@ -18,6 +18,7 @@ class AutoPartOrder:
self.status = OrderStatus.NEW self.status = OrderStatus.NEW
self.delivery_period = 0 self.delivery_period = 0
self.reason = "" self.reason = ""
self.errors = []
def add_position(self, position: AutoPartPosition) -> None: def add_position(self, position: AutoPartPosition) -> None:
self.positions.append(position) self.positions.append(position)
@@ -33,9 +34,25 @@ class AutoPartOrder:
def set_delivery_period(self, delivery_period: int) -> None: def set_delivery_period(self, delivery_period: int) -> None:
self.delivery_period = delivery_period self.delivery_period = delivery_period
def fill_from_local_supplier(self) -> None: def fill_from_local_supplier(self) -> None:
for position in self.positions: for position in self.positions:
position.fill_from_local_supplier() errors = position.fill_from_stock()
self.errors += errors
def check_order(self, config) -> None:
""" Проверяет заказ на возможность исполнения"""
# 1. Проверка общего количества отказов
order_refusal_threshold = config.get("order_refusal_threshold", 1)
refusal_positions_count = len([position for position in self.positions if str(position.status) in
[PositionStatus.REFUSED, PositionStatus.STOCK_FAILED]])
order_refusal_rate = refusal_positions_count / len(self.positions)
if order_refusal_rate > order_refusal_threshold:
self.errors.append(f"Превышен порог отказов в заказе - {order_refusal_rate:.0%} "
f"({refusal_positions_count} из {len(self.positions)})")
self.status = OrderStatus.OPERATOR_REQUIRED
def __len__(self): def __len__(self):

View File

@@ -8,6 +8,7 @@ class PositionStatus(Enum):
NEW = "new" # Новая позиция NEW = "new" # Новая позиция
STOCK_RECIEVED = "stock_received" # Получен остаток STOCK_RECIEVED = "stock_received" # Получен остаток
STOCK_FAILED = "stock_failed" # Остаток не получен STOCK_FAILED = "stock_failed" # Остаток не получен
NO_AVAILABLE_STOCK = "no_available_stock" #Нет доступных складов
READY = "ready" READY = "ready"
READY_PARTIAL = "ready_partial" READY_PARTIAL = "ready_partial"
ORDERED = "ordered" # Заказано ORDERED = "ordered" # Заказано
@@ -16,7 +17,7 @@ class PositionStatus(Enum):
@dataclass @dataclass
class AutoPartPosition: class AutoPartPosition:
SUPPLIER_CODE = "11282996" DISTRIBUTOR_ID = "1577730"
""" """
Унифицированная модель позиции для заказа. Унифицированная модель позиции для заказа.
Все контрагенты приводятся к этой структуре. Все контрагенты приводятся к этой структуре.
@@ -47,35 +48,71 @@ class AutoPartPosition:
if self.requested_price < 0: if self.requested_price < 0:
raise ValueError(f"Цена не может быть отрицательной: {self.requested_price}") raise ValueError(f"Цена не может быть отрицательной: {self.requested_price}")
def update_stock(self, stock: Dict[str, Any]): def update_stock(self, stock: Dict[str, Any], delivery_period: int = 0) -> None:
if stock["success"]: if stock["success"]:
self.stock = stock["data"] available_distributors = stock["data"]
self.status = PositionStatus.STOCK_RECIEVED
# Для доставки только с локального склада сперва убираем все остальные склады
if delivery_period == 0:
available_distributors = self._filter_only_local_storage(available_distributors)
#Отбираем склады по сроку доставки
available_distributors = self._filter_proper_delivery_time(available_distributors, delivery_period)
# Убираем дорогие склады с ценой выше запрошенной
available_distributors = self._filter_proper_price(available_distributors)
# Убираем отрицательные остатки
available_distributors = self._filter_proper_availability(available_distributors)
# Сортируем по цене
available_distributors.sort(key=lambda item: Decimal(item["price"]), reverse=False)
self.stock = available_distributors
if len (self.stock):
self.status = PositionStatus.STOCK_RECIEVED
else:
self.status = PositionStatus.NO_AVAILABLE_STOCK
else: else:
self.status = PositionStatus.STOCK_FAILED self.status = PositionStatus.STOCK_FAILED
def fill_from_local_supplier(self): def fill_from_stock(self):
if self.status != PositionStatus.STOCK_RECIEVED: if self.status == PositionStatus.STOCK_RECIEVED:
return
supplier_stock_items = [item for item in self.stock if str(item["supplierCode"]) == self.SUPPLIER_CODE] for distributor in self.stock:
supplier_stock_items.sort(key=lambda item: Decimal(item["price"]), reverse=False) distributor["profit"] = int(distributor["availability"]) * self.requested_price - int(distributor["availability"]) * Decimal(distributor["price"])
if len(supplier_stock_items) == 0: self.stock.sort(key=lambda item: item["profit"], reverse=True)
self.status = PositionStatus.REFUSED
self.desc = "Нет на складе" self.order_quantity = self.stock[0]["availability"]
return self.order_price = self.requested_price
self.order_item = self.stock[0]
elif self.requested_quantity <= supplier_stock_items[0]["availability"]:
self.order_quantity = self.requested_quantity
self.status = PositionStatus.READY self.status = PositionStatus.READY
self.desc = "Готов к заказу"
else:
self.order_quantity = supplier_stock_items[0]["availability"] def _filter_only_local_storage(self, distributors):
self.status = PositionStatus.READY_PARTIAL return [item for item in distributors if str(item["distributorId"]) == self.DISTRIBUTOR_ID]
self.desc = "Частичный остаток"
def _filter_proper_delivery_time(self, distributors, delivery_period):
return [item for item in distributors if item["deliveryPeriod"] <= delivery_period]
def _filter_proper_price(self, distributors):
return [item for item in distributors if Decimal(item["price"]) <= self.requested_price]
def _filter_proper_availability(self, distributors):
return [item for item in distributors if Decimal(item["availability"]) > 0]

View File

@@ -64,7 +64,7 @@ class EmailProcessor:
logger.error(f"Конфиг для клиента {client} не найден") logger.error(f"Конфиг для клиента {client} не найден")
for attachment in self.context.data["attachments"]: for attachment in self.context.data["attachments"]:
print(attachment["order"]) print(attachment["order"].__dict__)
#except Exception as e: #except Exception as e:
# logger.error(f"Произошла другая ошибка: {e}") # logger.error(f"Произошла другая ошибка: {e}")