""" Валидация данных заказов МойСклад Проверка обязательных и желательных полей, валидация форматов """ import re from typing import Dict, List, Optional, Tuple from enum import Enum from dataclasses import dataclass import logging logger = logging.getLogger(__name__) class ValidationLevel(Enum): """Уровни валидации""" VALID = "valid" # Все проверки пройдены WARNING = "warning" # Отсутствуют желательные поля CRITICAL = "critical" # Отсутствуют обязательные поля @dataclass class ValidationResult: """Результат валидации""" level: ValidationLevel field: str message: str value: Optional[str] = None class OrderValidator: """Валидатор данных заказов""" # НДС ставки VAT_RATES = [0, 10, 20, "без НДС", "0%", "10%", "20%"] def __init__(self): self.results: List[ValidationResult] = [] def validate_inn(self, inn: str) -> bool: """ Валидация ИНН ИНН юр. лица: 10 цифр ИНН ИП: 12 цифр Включает проверку контрольной суммы Args: inn: ИНН для проверки Returns: True если ИНН корректен """ if not inn or not isinstance(inn, str): return False inn = inn.strip() if not inn.isdigit(): return False if len(inn) == 10: # ИНН юридического лица (10 цифр) coefficients = [2, 4, 10, 3, 5, 9, 4, 6, 8] checksum = sum( int(inn[i]) * coefficients[i] for i in range(9) ) % 11 % 10 return checksum == int(inn[9]) elif len(inn) == 12: # ИНН индивидуального предпринимателя (12 цифр) # Проверка первой контрольной суммы coefficients1 = [7, 2, 4, 10, 3, 5, 9, 4, 6, 8] checksum1 = sum( int(inn[i]) * coefficients1[i] for i in range(10) ) % 11 % 10 # Проверка второй контрольной суммы coefficients2 = [3, 7, 2, 4, 10, 3, 5, 9, 4, 6, 8] checksum2 = sum( int(inn[i]) * coefficients2[i] for i in range(11) ) % 11 % 10 return checksum1 == int(inn[10]) and checksum2 == int(inn[11]) return False def validate_kpp(self, kpp: str) -> bool: """ Валидация КПП КПП: 9 цифр Args: kpp: КПП для проверки Returns: True если КПП корректен """ if not kpp or not isinstance(kpp, str): return False kpp = kpp.strip() return len(kpp) == 9 and kpp.isdigit() def validate_ogrn(self, ogrn: str) -> bool: """ Валидация ОГРН ОГРН юр. лица: 13 цифр ОГРНИП: 15 цифр Args: ogrn: ОГРН для проверки Returns: True если ОГРН корректен """ if not ogrn or not isinstance(ogrn, str): return False ogrn = ogrn.strip() if not ogrn.isdigit(): return False if len(ogrn) == 13: # ОГРН юридического лица check = int(ogrn[:12]) % 11 return (check % 10) == int(ogrn[12]) elif len(ogrn) == 15: # ОГРНИП индивидуального предпринимателя check = int(ogrn[:14]) % 13 return (check % 10) == int(ogrn[14]) return False def validate_organization(self, org: Dict) -> List[ValidationResult]: """ Валидация данных организации (продавец) Args: org: Данные организации из МойСклад Returns: Список результатов валидации """ results = [] if not org: results.append(ValidationResult( level=ValidationLevel.CRITICAL, field="organization", message="Отсутствуют данные организации" )) return results # Обязательные поля name = org.get("name") or org.get("legalTitle") if not name: results.append(ValidationResult( level=ValidationLevel.CRITICAL, field="organization.name", message="Не указано наименование организации" )) # ИНН inn = org.get("inn") if not inn: results.append(ValidationResult( level=ValidationLevel.CRITICAL, field="organization.inn", message="Не указан ИНН организации" )) elif not self.validate_inn(inn): results.append(ValidationResult( level=ValidationLevel.CRITICAL, field="organization.inn", message=f"Некорректный ИНН: {inn}", value=inn )) # КПП (обязателен для юр. лиц) kpp = org.get("kpp") if inn and len(inn) == 10: # Юр. лицо if not kpp: results.append(ValidationResult( level=ValidationLevel.CRITICAL, field="organization.kpp", message="Не указан КПП организации" )) elif not self.validate_kpp(kpp): results.append(ValidationResult( level=ValidationLevel.CRITICAL, field="organization.kpp", message=f"Некорректный КПП: {kpp}", value=kpp )) # Юридический адрес legal_address = ( org.get("legalAddress") or org.get("actualAddress") ) if not legal_address: results.append(ValidationResult( level=ValidationLevel.CRITICAL, field="organization.legalAddress", message="Не указан юридический адрес организации" )) # Желательные поля # ОГРН ogrn = org.get("ogrn") if not ogrn: results.append(ValidationResult( level=ValidationLevel.WARNING, field="organization.ogrn", message="Не указан ОГРН организации" )) elif not self.validate_ogrn(ogrn): results.append(ValidationResult( level=ValidationLevel.WARNING, field="organization.ogrn", message=f"Некорректный ОГРН: {ogrn}", value=ogrn )) # Счета accounts = org.get("accounts", []) if not accounts: results.append(ValidationResult( level=ValidationLevel.WARNING, field="organization.accounts", message="Не указаны банковские реквизиты" )) return results def validate_counterparty(self, agent: Dict) -> List[ValidationResult]: """ Валидация данных контрагента (покупатель) Args: agent: Данные контрагента из МойСклад Returns: Список результатов валидации """ results = [] if not agent: results.append(ValidationResult( level=ValidationLevel.CRITICAL, field="counterparty", message="Отсутствуют данные контрагента" )) return results # Обязательные поля name = agent.get("name") or agent.get("legalTitle") if not name: results.append(ValidationResult( level=ValidationLevel.CRITICAL, field="counterparty.name", message="Не указано наименование контрагента" )) # ИНН inn = agent.get("inn") if not inn: results.append(ValidationResult( level=ValidationLevel.CRITICAL, field="counterparty.inn", message="Не указан ИНН контрагента" )) elif not self.validate_inn(inn): results.append(ValidationResult( level=ValidationLevel.CRITICAL, field="counterparty.inn", message=f"Некорректный ИНН: {inn}", value=inn )) # КПП (обязателен для юр. лиц) kpp = agent.get("kpp") if inn and len(inn) == 10: # Юр. лицо if not kpp: results.append(ValidationResult( level=ValidationLevel.CRITICAL, field="counterparty.kpp", message="Не указан КПП контрагента" )) elif not self.validate_kpp(kpp): results.append(ValidationResult( level=ValidationLevel.CRITICAL, field="counterparty.kpp", message=f"Некорректный КПП: {kpp}", value=kpp )) # Адрес address = agent.get("legalAddress") or agent.get("actualAddress") if not address: results.append(ValidationResult( level=ValidationLevel.CRITICAL, field="counterparty.address", message="Не указан адрес контрагента" )) return results def validate_position( self, position: Dict, index: int ) -> List[ValidationResult]: """ Валидация позиции заказа (товар) Args: position: Данные позиции index: Номер позиции Returns: Список результатов валидации """ results = [] # Наименование товара assortment = position.get("assortment", {}) name = assortment.get("name") if not name: results.append(ValidationResult( level=ValidationLevel.CRITICAL, field=f"position[{index}].name", message=f"Позиция {index + 1}: не указано наименование" )) # Количество quantity = position.get("quantity") if not quantity or quantity <= 0: results.append(ValidationResult( level=ValidationLevel.CRITICAL, field=f"position[{index}].quantity", message=f"Позиция {index + 1}: некорректное количество" )) # Цена price = position.get("price") if price is None or price < 0: results.append(ValidationResult( level=ValidationLevel.CRITICAL, field=f"position[{index}].price", message=f"Позиция {index + 1}: некорректная цена" )) # НДС vat = position.get("vat") if vat is None: results.append(ValidationResult( level=ValidationLevel.WARNING, field=f"position[{index}].vat", message=f"Позиция {index + 1}: не указана ставка НДС" )) # Единица измерения (желательно) uom = assortment.get("uom") if not uom: results.append(ValidationResult( level=ValidationLevel.WARNING, field=f"position[{index}].uom", message=f"Позиция {index + 1}: не указана единица измерения" )) return results def validate_order(self, order_data: Dict) -> List[ValidationResult]: """ Полная валидация заказа Args: order_data: Полные данные заказа (из get_full_order_data) Returns: Список всех результатов валидации """ results = [] order = order_data.get("order", {}) # Основные данные заказа if not order.get("name"): results.append(ValidationResult( level=ValidationLevel.CRITICAL, field="order.name", message="Не указан номер заказа" )) if not order.get("moment"): results.append(ValidationResult( level=ValidationLevel.CRITICAL, field="order.moment", message="Не указана дата заказа" )) total_sum = order.get("sum", 0) if total_sum <= 0: results.append(ValidationResult( level=ValidationLevel.CRITICAL, field="order.sum", message="Некорректная сумма заказа" )) # Валидация организации org = order_data.get("organization") results.extend(self.validate_organization(org)) # Валидация контрагента agent = order_data.get("counterparty") results.extend(self.validate_counterparty(agent)) # Валидация грузополучателя (КРИТИЧНО!) # Если грузополучатель не указан явно, используем контрагента consignee = order_data.get("consignee") if consignee is None: # Используем контрагента как грузополучателя по умолчанию consignee = order_data.get("counterparty") if consignee is None: results.append(ValidationResult( level=ValidationLevel.CRITICAL, field="order.consignee", message="Не указан грузополучатель (обязательно для УПД)" )) # Проверяем наличие базовых данных грузополучателя if consignee and isinstance(consignee, dict): if not consignee.get("name"): results.append(ValidationResult( level=ValidationLevel.CRITICAL, field="consignee.name", message="Не указано наименование грузополучателя" )) # Валидация позиций positions = order_data.get("positions", []) if not positions: results.append(ValidationResult( level=ValidationLevel.CRITICAL, field="order.positions", message="Заказ не содержит товарных позиций" )) else: for index, position in enumerate(positions): results.extend(self.validate_position(position, index)) return results def get_validation_summary( self, results: List[ValidationResult] ) -> Tuple[ValidationLevel, str]: """ Получение сводки по результатам валидации Args: results: Список результатов валидации Returns: Tuple: (общий уровень, текстовая сводка) """ if not results: return ValidationLevel.VALID, "✅ Все проверки пройдены успешно" critical = [r for r in results if r.level == ValidationLevel.CRITICAL] warnings = [r for r in results if r.level == ValidationLevel.WARNING] if critical: level = ValidationLevel.CRITICAL summary = f"❌ Критических ошибок: {len(critical)}\n" if warnings: summary += f"⚠️ Предупреждений: {len(warnings)}\n" summary += "\nКритические ошибки:\n" for r in critical: summary += f" • {r.message}\n" elif warnings: level = ValidationLevel.WARNING summary = f"⚠️ Предупреждений: {len(warnings)}\n" summary += "\nПредупреждения:\n" for r in warnings: summary += f" • {r.message}\n" else: level = ValidationLevel.VALID summary = "✅ Все проверки пройдены" return level, summary # Пример использования if __name__ == "__main__": validator = OrderValidator() # Тест валидации ИНН test_inns = [ "7707083893", # Верный ИНН (10 цифр) "500100732259", # Верный ИНН ИП (12 цифр) "1234567890", # Неверный ИНН ] for inn in test_inns: result = validator.validate_inn(inn) print(f"ИНН {inn}: {'✅ корректен' if result else '❌ некорректен'}")