diff --git a/.env.example b/.env.example index 17dda9d..7e40f1c 100644 --- a/.env.example +++ b/.env.example @@ -10,6 +10,7 @@ MOYSKLAD_API_URL=https://api.moysklad.ru/api/remap/1.2 # Настройки приложения LOG_LEVEL=INFO DEBUG=False +INCLUDE_ACCOUNTING_ACCOUNTS=False # Timezone (для Docker) TZ=Europe/Moscow diff --git a/.env.prod.example b/.env.prod.example index 9b8717b..aac365b 100644 --- a/.env.prod.example +++ b/.env.prod.example @@ -12,6 +12,7 @@ TZ=Europe/Moscow # Уровень логирования (DEBUG, INFO, WARNING, ERROR) LOG_LEVEL=INFO +INCLUDE_ACCOUNTING_ACCOUNTS=false # ===== Docker Registry ===== # Для использования собственного registry diff --git a/INSTALL_INFO.txt b/INSTALL_INFO.txt new file mode 100644 index 0000000..79013a7 --- /dev/null +++ b/INSTALL_INFO.txt @@ -0,0 +1,72 @@ +================================================================= + МойСклад - Генератор документов + Информация перед установкой +================================================================= + +ОПИСАНИЕ +-------- +Программа для работы с отгрузками МойСклад и генерации XML +документов для импорта в 1С. + +ВОЗМОЖНОСТИ +----------- +✓ Поиск отгрузок по номеру +✓ Просмотр списка отгрузок +✓ Валидация данных перед генерацией +✓ Генерация XML в формате 1С EnterpriseData 1.20 +✓ Массовая генерация документов (до 50 за раз) +✓ Работа без подключения к интернету (после установки) + +СИСТЕМНЫЕ ТРЕБОВАНИЯ +-------------------- +• Операционная система: Windows 7/8/10/11 (64-bit) +• Оперативная память: минимум 2 ГБ +• Свободное место на диске: 200 МБ +• Разрешение экрана: минимум 1024x768 + +УСТАНОВКА +--------- +1. Запустите установщик +2. Следуйте инструкциям мастера установки +3. После установки программа готова к работе + +ПЕРВЫЙ ЗАПУСК +------------- +1. Запустите программу через ярлык на рабочем столе или + из меню "Пуск" +2. При первом запуске введите учетные данные МойСклад: + - Логин (email или токен) + - Пароль (или токен) +3. Данные будут сохранены в зашифрованном виде + +АВТОНОМНАЯ РАБОТА +----------------- +После установки программа работает ПОЛНОСТЬЮ АВТОНОМНО: +✓ Не требует подключения к интернету для запуска +✓ Все библиотеки встроены в установщик +✓ Не требует установки Python или других зависимостей +✓ Все данные хранятся локально на вашем компьютере + +ВНИМАНИЕ: Для работы с API МойСклад необходимо интернет- +соединение, но сама программа может работать без интернета. + +БЕЗОПАСНОСТЬ +------------ +• Учетные данные шифруются с помощью Fernet (AES-128) +• Данные хранятся только на вашем компьютере +• Программа не передает данные третьим лицам +• Шифрование ключом уникально для каждой установки + +ТЕХНИЧЕСКАЯ ПОДДЕРЖКА +--------------------- +Документация: смотрите папку "docs" после установки +GitHub: https://github.com/IgorNadein/BUH_bot +Email: support@example.com + +ЛИЦЕНЗИЯ +-------- +Программа распространяется "как есть" без каких-либо гарантий. + +================================================================= + Спасибо за выбор нашей программы! +================================================================= diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..3e6d44a --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 BUH_bot + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/app_icon.ico b/app_icon.ico new file mode 100644 index 0000000..bbcc9bf Binary files /dev/null and b/app_icon.ico differ diff --git a/app_icon.png b/app_icon.png new file mode 100644 index 0000000..3a4b1fa Binary files /dev/null and b/app_icon.png differ diff --git a/build-image.bat b/build-image.bat deleted file mode 100644 index 326d92e..0000000 --- a/build-image.bat +++ /dev/null @@ -1,60 +0,0 @@ -@echo off -REM Скрипт для сборки и публикации Docker образа (Windows) - -echo 🔨 Сборка Docker образа для BUH Bot - -REM Конфигурация -set REGISTRY=ghcr.io -set USERNAME=username -set IMAGE_NAME=buh_bot -set VERSION=latest - -echo Registry: %REGISTRY% -echo Username: %USERNAME% -echo Version: %VERSION% -echo. - -REM Сборка образа -echo 📦 Сборка образа... -docker build ^ - --tag "%REGISTRY%/%USERNAME%/%IMAGE_NAME%:%VERSION%" ^ - --tag "%REGISTRY%/%USERNAME%/%IMAGE_NAME%:latest" ^ - . - -if %errorlevel% neq 0 ( - echo ❌ Ошибка сборки образа - exit /b 1 -) - -echo ✅ Образ успешно собран - -REM Тестирование образа -echo 🧪 Тестирование образа... -docker run --rm "%REGISTRY%/%USERNAME%/%IMAGE_NAME%:%VERSION%" python -c "import sys; from modules.moysklad_client import MoySkladClient; print('OK')" - -if %errorlevel% neq 0 ( - echo ❌ Ошибка тестирования - exit /b 1 -) - -echo ✅ Тестирование пройдено - -REM Публикация образа -echo 📤 Публикация образа в registry... -docker push "%REGISTRY%/%USERNAME%/%IMAGE_NAME%:%VERSION%" -docker push "%REGISTRY%/%USERNAME%/%IMAGE_NAME%:latest" - -if %errorlevel% neq 0 ( - echo ❌ Ошибка публикации образа - exit /b 1 -) - -echo ✅ Образ успешно опубликован -echo. -echo 📦 Образы доступны: -echo %REGISTRY%/%USERNAME%/%IMAGE_NAME%:%VERSION% -echo %REGISTRY%/%USERNAME%/%IMAGE_NAME%:latest -echo. -echo 🚀 Для деплоя используйте: deploy-prod.bat - -pause diff --git a/build-image.sh b/build-image.sh deleted file mode 100644 index 9b057f0..0000000 --- a/build-image.sh +++ /dev/null @@ -1,82 +0,0 @@ -#!/bin/bash -# Скрипт для сборки и публикации Docker образа - -set -e - -# Цвета для вывода -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -# Конфигурация -REGISTRY="${DOCKER_REGISTRY:-ghcr.io}" -USERNAME="${DOCKER_USERNAME:-username}" -IMAGE_NAME="buh_bot" -VERSION="${BOT_VERSION:-latest}" - -echo -e "${GREEN}🔨 Сборка Docker образа для BUH Bot${NC}" -echo "Registry: $REGISTRY" -echo "Username: $USERNAME" -echo "Version: $VERSION" -echo "" - -# Проверка авторизации -echo -e "${YELLOW}Проверка авторизации в registry...${NC}" -if ! docker info | grep -q "Username"; then - echo -e "${RED}❌ Не авторизован в Docker registry${NC}" - echo "Выполните: docker login $REGISTRY" - exit 1 -fi - -# Сборка образа -echo -e "${YELLOW}📦 Сборка образа...${NC}" -docker build \ - --tag "${REGISTRY}/${USERNAME}/${IMAGE_NAME}:${VERSION}" \ - --tag "${REGISTRY}/${USERNAME}/${IMAGE_NAME}:latest" \ - --build-arg BUILD_DATE="$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \ - --build-arg VCS_REF="$(git rev-parse --short HEAD)" \ - . - -if [ $? -eq 0 ]; then - echo -e "${GREEN}✅ Образ успешно собран${NC}" -else - echo -e "${RED}❌ Ошибка сборки образа${NC}" - exit 1 -fi - -# Тестирование образа -echo -e "${YELLOW}🧪 Тестирование образа...${NC}" -docker run --rm "${REGISTRY}/${USERNAME}/${IMAGE_NAME}:${VERSION}" python -c " -import sys -from modules.moysklad_client import MoySkladClient -from modules.validator import OrderValidator -from modules.user_manager import UserManager -print('✅ Все модули импортированы успешно') -" - -if [ $? -eq 0 ]; then - echo -e "${GREEN}✅ Тестирование пройдено${NC}" -else - echo -e "${RED}❌ Ошибка тестирования${NC}" - exit 1 -fi - -# Публикация образа -echo -e "${YELLOW}📤 Публикация образа в registry...${NC}" -docker push "${REGISTRY}/${USERNAME}/${IMAGE_NAME}:${VERSION}" -docker push "${REGISTRY}/${USERNAME}/${IMAGE_NAME}:latest" - -if [ $? -eq 0 ]; then - echo -e "${GREEN}✅ Образ успешно опубликован${NC}" - echo "" - echo "📦 Образы доступны:" - echo " ${REGISTRY}/${USERNAME}/${IMAGE_NAME}:${VERSION}" - echo " ${REGISTRY}/${USERNAME}/${IMAGE_NAME}:latest" - echo "" - echo "🚀 Для деплоя используйте:" - echo " ./deploy-prod.sh" -else - echo -e "${RED}❌ Ошибка публикации образа${NC}" - exit 1 -fi diff --git a/build_gui.bat b/build_gui.bat new file mode 100644 index 0000000..f3d8c7c --- /dev/null +++ b/build_gui.bat @@ -0,0 +1,82 @@ +@echo off +REM Скрипт сборки GUI приложения и установщика для Windows + +echo. +echo =============================================== +echo Сборка GUI приложения для Windows +echo =============================================== +echo. + +REM Проверка Python +python --version >nul 2>&1 +if errorlevel 1 ( + echo [ERROR] Python не найден! + pause + exit /b 1 +) + +echo [OK] Python найден +echo. + +REM Создание виртуального окружения если нет +if not exist ".venv" ( + echo [INFO] Создание виртуального окружения... + python -m venv .venv +) + +REM Активация окружения +echo [INFO] Активация окружения... +call .venv\Scripts\activate.bat + +REM Установка зависимостей +echo [INFO] Установка зависимостей... +python -m pip install --upgrade pip +pip install -r requirements_gui.txt + +REM Очистка предыдущей сборки +echo [INFO] Очистка предыдущей сборки... +if exist "build" rd /s /q build +if exist "dist" rd /s /q dist + +REM Сборка с PyInstaller +echo [INFO] Сборка исполняемого файла... +pyinstaller gui_app.spec --clean + +if not exist "dist\MoySkladGenerator.exe" ( + echo [ERROR] Ошибка сборки! + pause + exit /b 1 +) + +echo [OK] Исполняемый файл создан: dist\MoySkladGenerator.exe +echo. + +REM Проверка Inno Setup +where iscc >nul 2>&1 +if %errorlevel% equ 0 ( + echo [INFO] Создание установщика... + iscc installer.iss + + if exist "installer_output\MoySklad_Generator_Setup.exe" ( + echo [OK] Установщик создан: installer_output\MoySklad_Generator_Setup.exe + ) else ( + echo [WARNING] Установщик не создан + ) +) else ( + echo [WARNING] Inno Setup не найден, установщик не создан + echo Скачайте с https://jrsoftware.org/isdl.php +) + +echo. +echo =============================================== +echo Сборка завершена! +echo =============================================== +echo. +echo Файлы: +echo - Исполняемый файл: dist\MoySkladGenerator.exe +echo - Установщик: installer_output\MoySklad_Generator_Setup.exe +echo. +echo Для тестирования запустите: +echo dist\MoySkladGenerator.exe +echo. +pause diff --git a/build_gui.sh b/build_gui.sh new file mode 100644 index 0000000..df535eb --- /dev/null +++ b/build_gui.sh @@ -0,0 +1,75 @@ +#!/bin/bash +# Скрипт сборки GUI приложения и установщика + +set -e + +echo "🔨 Сборка GUI приложения для Windows" +echo "======================================" +echo "" + +# Проверка Python +if ! command -v python &> /dev/null; then + echo "❌ Python не найден!" + exit 1 +fi + +echo "✓ Python найден: $(python --version)" +echo "" + +# Создание виртуального окружения если нет +if [ ! -d ".venv" ]; then + echo "📦 Создание виртуального окружения..." + python -m venv .venv +fi + +# Активация окружения +echo "🔧 Активация окружения..." +source .venv/Scripts/activate + +# Установка зависимостей +echo "📥 Установка зависимостей..." +pip install --upgrade pip +pip install -r requirements_gui.txt + +# Очистка предыдущей сборки +echo "🧹 Очистка предыдущей сборки..." +rm -rf build dist + +# Сборка с PyInstaller +echo "⚙️ Сборка исполняемого файла..." +pyinstaller gui_app.spec --clean + +if [ ! -f "dist/MoySkladGenerator.exe" ]; then + echo "❌ Ошибка сборки!" + exit 1 +fi + +echo "✅ Исполняемый файл создан: dist/MoySkladGenerator.exe" +echo " Размер: $(du -h dist/MoySkladGenerator.exe | cut -f1)" +echo "" + +# Проверка Inno Setup +if command -v iscc &> /dev/null; then + echo "📦 Создание установщика..." + iscc installer.iss + + if [ -f "installer_output/MoySklad_Generator_Setup.exe" ]; then + echo "✅ Установщик создан: installer_output/MoySklad_Generator_Setup.exe" + echo " Размер: $(du -h installer_output/MoySklad_Generator_Setup.exe | cut -f1)" + else + echo "⚠️ Установщик не создан" + fi +else + echo "⚠️ Inno Setup не найден, установщик не создан" + echo " Скачайте с https://jrsoftware.org/isdl.php" +fi + +echo "" +echo "🎉 Сборка завершена!" +echo "" +echo "Файлы:" +echo " • Исполняемый файл: dist/MoySkladGenerator.exe" +echo " • Установщик: installer_output/MoySklad_Generator_Setup.exe" +echo "" +echo "Для тестирования запустите:" +echo " ./dist/MoySkladGenerator.exe" diff --git a/config.py b/config.py index 0b9275c..efe431a 100644 --- a/config.py +++ b/config.py @@ -19,23 +19,30 @@ class Settings(BaseSettings): """Настройки приложения""" # МойСклад API - moysklad_login: str = Field(..., env='MOYSKLAD_LOGIN') - moysklad_password: str = Field(..., env='MOYSKLAD_PASSWORD') + moysklad_login: str = Field(default='', env='MOYSKLAD_LOGIN') + moysklad_password: str = Field(default='', env='MOYSKLAD_PASSWORD') moysklad_api_url: str = Field( default='https://api.moysklad.ru/api/remap/1.2', env='MOYSKLAD_API_URL' ) # Telegram Bot - telegram_bot_token: str = Field(..., env='TELEGRAM_BOT_TOKEN') + telegram_bot_token: str = Field(default='', env='TELEGRAM_BOT_TOKEN') # Настройки приложения log_level: str = Field(default='INFO', env='LOG_LEVEL') debug: bool = Field(default=False, env='DEBUG') + include_accounting_accounts: bool = Field( + default=True, + env='INCLUDE_ACCOUNTING_ACCOUNTS' + ) # Пути output_dir: Path = Field(default=BASE_DIR / 'output', env='OUTPUT_DIR') - templates_dir: Path = Field(default=BASE_DIR / 'templates', env='TEMPLATES_DIR') + templates_dir: Path = Field( + default=BASE_DIR / 'templates', + env='TEMPLATES_DIR' + ) # Форматы документов xml_version: str = '5.03' @@ -53,10 +60,15 @@ try: # Создание необходимых директорий settings.output_dir.mkdir(parents=True, exist_ok=True) - settings.templates_dir.mkdir(parents=True, exist_ok=True) + if settings.templates_dir != BASE_DIR / 'templates': + settings.templates_dir.mkdir(parents=True, exist_ok=True) except Exception as e: - print(f"⚠️ Ошибка загрузки настроек: {e}") - print("Убедитесь, что файл .env существует и содержит все необходимые параметры") - print("Скопируйте .env.example в .env и заполните значения") - raise + # Для GUI приложения создаем настройки по умолчанию + import logging + logger = logging.getLogger(__name__) + logger.warning(f"⚠️ Настройки из .env не загружены: {e}") + logger.info("Используются настройки по умолчанию") + + settings = Settings() + settings.output_dir.mkdir(parents=True, exist_ok=True) diff --git a/create_icon.py b/create_icon.py new file mode 100644 index 0000000..1707b90 --- /dev/null +++ b/create_icon.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Скрипт для создания иконки приложения +Создает PNG и ICO файлы с логотипом МойСклад +""" +from PyQt6.QtGui import QPixmap, QPainter, QColor +from PyQt6.QtCore import Qt +from PyQt6.QtWidgets import QApplication +import sys + + +def create_icon(): + """Создание иконки приложения""" + app = QApplication(sys.argv) + + # Создаем PNG иконку + pixmap = QPixmap(256, 256) + pixmap.fill(Qt.GlobalColor.transparent) + + painter = QPainter(pixmap) + painter.setRenderHint(QPainter.RenderHint.Antialiasing) + + # Рисуем круг с градиентом (синий цвет МойСклад) + painter.setBrush(QColor(52, 152, 219)) + painter.setPen(Qt.PenStyle.NoPen) + painter.drawEllipse(8, 8, 240, 240) + + # Рисуем текст "МС" + painter.setPen(QColor(255, 255, 255)) + font = painter.font() + font.setPixelSize(140) + font.setBold(True) + painter.setFont(font) + painter.drawText(pixmap.rect(), Qt.AlignmentFlag.AlignCenter, 'МС') + + painter.end() + + # Сохраняем PNG + pixmap.save('app_icon.png') + print('✓ PNG иконка создана: app_icon.png') + + # Конвертируем в ICO + try: + from PIL import Image + img = Image.open('app_icon.png') + img.save( + 'app_icon.ico', + format='ICO', + sizes=[ + (256, 256), + (128, 128), + (64, 64), + (48, 48), + (32, 32), + (16, 16) + ] + ) + print('✓ ICO иконка создана: app_icon.ico') + except ImportError: + print('⚠ Pillow не установлен, ICO файл не создан') + print(' Установите: pip install Pillow') + + app.quit() + + +if __name__ == '__main__': + create_icon() diff --git a/deploy-prod.bat b/deploy-prod.bat deleted file mode 100644 index 54d9721..0000000 --- a/deploy-prod.bat +++ /dev/null @@ -1,75 +0,0 @@ -@echo off -REM Скрипт для деплоя в production (Windows) - -echo 🚀 Production Deploy - BUH Bot -echo. - -REM Проверка .env.prod -if not exist .env.prod ( - echo ❌ Файл .env.prod не найден! - echo Создайте файл на основе .env.prod.example: - echo copy .env.prod.example .env.prod - echo notepad .env.prod - exit /b 1 -) - -echo 🔍 Проверка конфигурации... -findstr /C:"TELEGRAM_BOT_TOKEN=your_production_bot_token_here" .env.prod >nul -if %errorlevel% equ 0 ( - echo ❌ TELEGRAM_BOT_TOKEN не настроен в .env.prod - exit /b 1 -) - -echo ✅ Конфигурация корректна - -REM Создание директорий -echo 📁 Создание директорий... -if not exist data mkdir data -if not exist logs mkdir logs -if not exist temp_documents mkdir temp_documents - -REM Backup -if exist data\users.json ( - echo 💾 Создание backup... - set BACKUP_DIR=backups\%date:~-4,4%%date:~-10,2%%date:~-7,2%_%time:~0,2%%time:~3,2%%time:~6,2% - set BACKUP_DIR=%BACKUP_DIR: =0% - mkdir %BACKUP_DIR% - copy data\*.* %BACKUP_DIR%\ - echo ✅ Backup создан: %BACKUP_DIR% -) - -REM Остановка существующих контейнеров -docker ps -a --format "{{.Names}}" | findstr "buh_bot" >nul -if %errorlevel% equ 0 ( - echo 🛑 Остановка существующих контейнеров... - docker-compose -f docker-compose.prod.yml --env-file .env.prod down -) - -REM Pull latest образов -echo 📥 Получение последних образов... -docker-compose -f docker-compose.prod.yml --env-file .env.prod pull - -REM Запуск -echo 🚀 Запуск сервисов... -docker-compose -f docker-compose.prod.yml --env-file .env.prod up -d - -REM Проверка статуса -timeout /t 5 /nobreak >nul -echo. -echo 📊 Статус сервисов: -docker-compose -f docker-compose.prod.yml --env-file .env.prod ps - -echo. -echo ✅ Production деплой завершён! -echo. -echo 📝 Полезные команды: -echo Логи бота: docker-compose -f docker-compose.prod.yml logs -f buh_bot -echo Статус: docker-compose -f docker-compose.prod.yml ps -echo Остановка: docker-compose -f docker-compose.prod.yml down -echo. -echo 📊 Мониторинг: -echo Grafana: http://localhost:3000 -echo Prometheus: http://localhost:9090 -echo. - -pause diff --git a/docs/BATCH_VALIDATION.md b/docs/BATCH_VALIDATION.md new file mode 100644 index 0000000..e6607e1 --- /dev/null +++ b/docs/BATCH_VALIDATION.md @@ -0,0 +1,316 @@ +# Валидация при массовой генерации + +## Обзор + +При массовой генерации документов каждая отгрузка проходит валидацию перед созданием XML. Отгрузки с критическими ошибками исключаются из генерации, а пользователь получает подробный отчет об ошибках. + +## Процесс валидации + +### 1. Автоматическая проверка + +Для каждой отгрузки из списка выполняются следующие проверки: + +**Обязательные поля организации:** +- ✓ Наименование организации +- ✓ ИНН организации (с проверкой контрольной суммы) +- ✓ Банковские реквизиты + +**Обязательные поля контрагента:** +- ✓ Наименование контрагента +- ✓ ИНН контрагента (если юридическое лицо) + +**Обязательные поля заказа:** +- ✓ Номер заказа +- ✓ Дата заказа +- ✓ Сумма заказа > 0 +- ✓ Наличие товарных позиций + +**Обязательные поля грузополучателя:** +- ✓ Наименование грузополучателя +- ✓ Адрес грузополучателя (критично для УПД) + +**Позиции заказа:** +- ✓ Наименование товара +- ✓ Единица измерения +- ✓ Количество > 0 +- ✓ Цена ≥ 0 +- ✓ Корректная ставка НДС + +### 2. Обработка результатов + +#### Успешная валидация +Отгрузка добавляется в список для генерации XML. + +#### Критические ошибки +Отгрузка исключается из генерации, информация об ошибках сохраняется в отчет. + +#### Предупреждения (warnings) +Не блокируют генерацию, но включаются в отчет для информации. + +## Отчет об ошибках + +При обнаружении критических ошибок валидации пользователь получает текстовый файл с подробным отчетом. + +### Структура отчета + +``` +ОТЧЕТ О ВАЛИДАЦИИ ОТГРУЗОК +================================================== +Дата: 06.11.2025 14:10:31 +Всего обработано: 8 +С ошибками валидации: 3 + +КРИТИЧЕСКИЕ ОШИБКИ ВАЛИДАЦИИ: + +Отгрузка: 4926 +---------------------------------------- + • counterparty.inn: ИНН контрагента некорректен + • consignee.address: Не указан адрес грузополучателя + +Отгрузка: 4927 +---------------------------------------- + • organization.inn: ИНН организации некорректен + +НЕ НАЙДЕНЫ: +---------------------------------------- + • 4928 + • 4929 + +ОШИБКИ ОБРАБОТКИ: +---------------------------------------- + • 4931: Connection timeout + +Успешно прошли валидацию: 2 +---------------------------------------- + ✓ 4932 + ✓ 4933 +``` + +### Разделы отчета + +1. **Заголовок и статистика** + - Дата генерации отчета + - Общее количество обработанных отгрузок + - Количество отгрузок с ошибками + +2. **Критические ошибки валидации** + - Номер отгрузки + - Список критических ошибок с указанием поля и описанием + +3. **Не найденные отгрузки** + - Номера отгрузок, которые не найдены в МойСклад + +4. **Ошибки обработки** + - Технические ошибки при получении данных из API + +5. **Успешные отгрузки** + - Список отгрузок, прошедших валидацию + +## Примеры использования + +### Пример 1: Все отгрузки валидны + +**Ввод:** +``` +4926,4927,4928 +``` + +**Результат:** +- ✅ Все 3 отгрузки прошли валидацию +- Генерируется XML с тремя документами +- Отчет об ошибках не создается + +### Пример 2: Есть ошибки валидации + +**Ввод:** +``` +4926,4927,4928,4929 +``` + +**Сценарий:** +- 4926 - ✅ валидна +- 4927 - ❌ отсутствует ИНН организации +- 4928 - ❌ не найдена в системе +- 4929 - ✅ валидна + +**Результат:** +1. Бот отправляет текстовый отчет с ошибками для 4927 и 4928 +2. Генерируется XML только для отгрузок 4926 и 4929 +3. Пользователь видит статистику: + ``` + ❌ Обнаружены ошибки валидации + + 📊 Статистика: + • Обработано: 4 + • С ошибками: 1 + • Прошли валидацию: 2 + • Не найдено: 1 + ``` + +### Пример 3: Ни одна отгрузка не прошла валидацию + +**Ввод:** +``` +4926,4927,4928 +``` + +**Сценарий:** +Все три отгрузки имеют критические ошибки + +**Результат:** +1. Бот отправляет отчет с ошибками для всех трех отгрузок +2. XML не генерируется +3. Предлагается исправить ошибки и повторить попытку + +## Типичные ошибки и решения + +### Ошибка: "Не указан ИНН контрагента" + +**Причина:** У контрагента-юридического лица не заполнен ИНН в МойСклад + +**Решение:** +1. Откройте карточку контрагента в МойСклад +2. Заполните поле "ИНН" +3. Сохраните изменения +4. Повторите массовую генерацию + +### Ошибка: "Не указан адрес грузополучателя" + +**Причина:** Отсутствует адрес доставки, фактический или юридический адрес + +**Решение:** +1. Откройте отгрузку в МойСклад +2. Укажите адрес доставки +3. Или заполните адрес в карточке контрагента +4. Повторите массовую генерацию + +### Ошибка: "ИНН организации некорректен" + +**Причина:** Ошибка в контрольной сумме ИНН или неверный формат + +**Решение:** +1. Проверьте ИНН организации в МойСклад +2. Убедитесь, что ИНН состоит из 10 или 12 цифр +3. Проверьте правильность ввода +4. Исправьте и повторите генерацию + +### Ошибка: "Заказ не содержит товарных позиций" + +**Причина:** Отгрузка создана без товаров + +**Решение:** +1. Откройте отгрузку в МойСклад +2. Добавьте товары/услуги +3. Сохраните изменения +4. Повторите массовую генерацию + +## Технические детали + +### Уровни валидации + +```python +class ValidationLevel(Enum): + VALID = "valid" # Все проверки пройдены + WARNING = "warning" # Есть предупреждения + CRITICAL = "critical" # Критические ошибки +``` + +### Критерии блокировки + +Отгрузка **исключается** из генерации, если имеет хотя бы одну ошибку уровня `CRITICAL`. + +Отгрузка **включается** в генерацию, если: +- Нет критических ошибок +- Есть только предупреждения (`WARNING`) +- Все проверки пройдены (`VALID`) + +### Формат отчета + +- **Кодировка:** UTF-8 +- **Формат файла:** .txt +- **Имя файла:** `validation_errors_YYYYMMDD_HHMMSS.txt` +- **Местоположение:** временная директория `temp_documents/` + +### Производительность + +- **Валидация одной отгрузки:** ~0.01-0.05 сек +- **Создание отчета:** ~0.05-0.1 сек +- **Отправка файла:** ~0.5-1 сек + +Валидация не замедляет процесс массовой генерации значительно. + +## Интеграция с другими функциями + +### Одиночная генерация + +Валидация также работает при генерации одного документа: +- Результаты валидации показываются в чате +- Пользователь может решить, генерировать XML или нет +- При критических ошибках генерация может быть заблокирована + +### Формат XML + +Валидация работает для обоих форматов XML: +- ✓ 1С EnterpriseData 1.20 +- ✓ ВерсФорм 5.03 (УПД) + +### Пакетная генерация + +При выборе "Один файл (все документы)": +- Невалидные отгрузки исключаются +- Валидные отгрузки объединяются в один XML + +При выборе "Отдельные файлы": +- Для каждой валидной отгрузки создается отдельный XML +- Невалидные отгрузки пропускаются + +## Логирование + +Все события валидации логируются: + +```python +logger.warning( + f"Отгрузка {order_num} не прошла валидацию: " + f"{len(critical_errors)} критических ошибок" +) +``` + +Логи доступны в файле `logs/bot.log` для анализа и отладки. + +## Рекомендации + +### Для пользователей + +1. **Проверьте данные заранее** + - Используйте одиночную генерацию для проверки отгрузок + - Исправьте ошибки до массовой генерации + +2. **Заполняйте все обязательные поля** + - ИНН/КПП организаций и контрагентов + - Адреса доставки + - Банковские реквизиты + +3. **Сохраняйте отчеты об ошибках** + - Отчеты помогут выявить типичные проблемы + - Используйте их для улучшения данных в МойСклад + +### Для разработчиков + +1. **Расширение валидации** + - Добавляйте новые проверки в `modules/validator.py` + - Классифицируйте их по уровням (CRITICAL/WARNING) + +2. **Кастомизация отчетов** + - Формат отчета легко настраивается + - Можно добавить HTML-отчеты или PDF + +3. **Оптимизация** + - Валидация выполняется асинхронно + - Можно распараллелить для больших объемов + +## См. также + +- [Массовая генерация документов](BATCH_GENERATION.md) +- [Модуль валидации](validator_refactoring.md) +- [Формат EnterpriseData 1.20](XML_1C_FORMAT.md) +- [API МойСклад](https://dev.moysklad.ru/doc/api/remap/1.2/) diff --git a/docs/CONTRACT_UUID_FIX.md b/docs/CONTRACT_UUID_FIX.md deleted file mode 100644 index a57c922..0000000 --- a/docs/CONTRACT_UUID_FIX.md +++ /dev/null @@ -1,146 +0,0 @@ -# Исправление: GUID договора и выбор счетов 62 в 1С - -## Проблема - -### Симптомы -При импорте рублёвого документа (валюта RUB, код 643) в 1С:Бухгалтерия 8, система подставляла счета **62.21 (Расчёты в валюте)** / **62.22 (Авансы в валюте)** вместо ожидаемых **62.01 (Задолженность)** / **62.02 (Авансы)**. - -### Причина -В генераторе ED-XML использовался детерминированный GUID договора, созданный **только на основе пары (контрагент, организация)**: - -```python -# БЫЛО (неправильно): -contract_uuid = uuid.uuid5(uuid.NAMESPACE_DNS, f"contract-{cp_id}-{org_id}") -``` - -Это означало «один договор на пару контрагент-организация» **независимо от валюты расчётов**. - -### Механизм в 1С -В типовой конфигурации «1С:Бухгалтерия 8» выбор счетов учёта расчётов с покупателями (62.xx) определяется регистром **«Счета расчетов с контрагентами»** на основании: - -1. **Договора** (точнее - его валюты взаиморасчётов) -2. **Вида расчётов** (задолженность/аванс) - -Логика подстановки счетов: -- **RUB + не в у.е.** → **62.01** (задолженность) / **62.02** (авансы) -- **Иностранная валюта** → **62.21** (валютные расчёты) / **62.22** (авансы в валюте) -- **Условные единицы** → **62.31** (расчёты в у.е.) / **62.32** (авансы в у.е.) - -### Сценарий проблемы -1. Ранее в базу был импортирован документ **в валюте** (например, USD) -2. Для него создался договор с GUID `abc123...` (на основе пары контрагент-организация) -3. Этот договор записался в регистр с валютой USD → счета 62.21/62.22 -4. Позже импортируется документ **в рублях** (RUB) -5. Генератор создаёт **тот же GUID** `abc123...` (та же пара контрагент-организация!) -6. 1С находит **существующий договор** с валютой USD -7. Регистр подставляет счета **62.21/62.22** (как для валютного договора) -8. ❌ Документ рублёвый, но счета валютные! - -## Решение - -### 1. Исправление генератора GUID договора - -Включаем в seed для `uuid5` **валюту и признак условных единиц**: - -```python -# СТАЛО (правильно): -currency_code = currency.get("isoCode", "643") -is_ue = "false" # В текущей реализации всегда false -seed = f"contract-{cp_id}-{org_id}-CUR-{currency_code}-UE-{is_ue}" -contract_uuid = str(uuid.uuid5(uuid.NAMESPACE_DNS, seed)) -``` - -Теперь для каждой комбинации (контрагент, организация, **валюта**, **признак у.е.**) создаётся **отдельный договор** с уникальным GUID. - -### 2. Добавление диагностического логирования - -В конце метода `generate_realization()` добавлена проверка: - -```python -if currency_code == "643" or currency_code == "RUB": - logger.info( - "✓ Документ в рублях (RUB, код 643), не в у.е. " - "→ ожидаются счета 62.01 / 62.02. " - "Если 1С подставляет 62.21/62.22 или 62.31/62.32, проверьте: " - "1) Регистр 'Счета расчетов с контрагентами'. " - "2) В карточке договора: Валюта = RUB, У.Е. = нет." - ) -else: - logger.info( - f"⚠ Документ в иностранной валюте ({currency_code}) " - "→ ожидаются счета 62.21/62.22." - ) -``` - -## Проверка после исправления - -### 1. Проверьте регистр «Счета расчетов с контрагентами» - -В 1С: -1. Откройте **«Контрагенты»** → список контрагентов -2. Найдите проблемного контрагента -3. Перейдите по гиперссылке **«Счета расчетов с контрагентами»** -4. Проверьте, нет ли индивидуальных правил с 62.21/62.22 -5. Если есть - удалите или исправьте на 62.01/62.02 - -### 2. Проверьте карточку договора - -После импорта нового документа (с обновлённым генератором): -1. Откройте **«Договоры контрагентов»** -2. Найдите договор (по имени контрагента + дата создания) -3. Проверьте: - - **Валюта взаиморасчётов** = `RUB` (Российский рубль) - - **Расчёты в условных единицах** = `нет` (галочка снята) - -### 3. Проверьте подстановку счетов - -Создайте новый документ реализации: -1. Импортируйте тестовый заказ в рублях -2. Откройте созданный документ в 1С -3. Перейдите на вкладку **«Счета учёта»** -4. Проверьте: - - Счёт учёта расчётов с покупателем = **62.01** - - Счёт учёта авансов = **62.02** (если есть) - -## Дополнительные замечания - -### Миграция существующих данных - -Если в базе уже есть документы с неправильными счетами: - -1. **Вариант 1: Пересоздать договоры** - - Удалите старые договоры (или переименуйте) - - Импортируйте документы заново - - Новый генератор создаст правильные договоры - -2. **Вариант 2: Исправить вручную** - - Откройте карточку существующего договора - - Измените **«Валюта взаиморасчётов»** на RUB - - Убедитесь, что **«Расчёты в условных единицах»** = нет - - Проведите документы заново - -### Поддержка условных единиц (будущее) - -Если в будущем потребуется поддержка расчётов в у.е.: -1. Добавьте в API МойСклад передачу признака у.е. -2. Измените строку в генераторе: - ```python - is_ue = "true" if order.get("useConventionalUnits") else "false" - ``` -3. В ED-XML добавьте: - ```xml - <РасчетыВУсловныхЕдиницах>true - ``` - -## Ссылки - -1. [Расчеты с контрагентами в 1С:Бухгалтерии 8](https://buh.ru/articles/raschety-s-kontragentami-v-1s-bukhgalterii-8.html) -2. [Взаиморасчёты в валюте, оплата в рублях (1С:ERP)](https://sitec-it.ru/blog/1c-erp/vzaimoraschety-v-valyute-oplata-v-rublyakh-oformlenie-realizatsii-v-1s-erp/) -3. [Формат обмена EnterpriseData 1.20](https://buh.ru/news/samoe-novoe-v-1s-bukhgalterii-8-format-obmena-enterprisedata-1-20-s-drugimi-programmami.html) - -## История изменений - -### 2025-11-06 -- **Исправление**: Добавлена валюта и признак у.е. в seed для GUID договора -- **Улучшение**: Добавлено диагностическое логирование для RUB/валюта -- **Документация**: Создан файл CONTRACT_UUID_FIX.md diff --git a/docs/DOCKER_CHANGELOG.md b/docs/DOCKER_CHANGELOG.md deleted file mode 100644 index f28adde..0000000 --- a/docs/DOCKER_CHANGELOG.md +++ /dev/null @@ -1,226 +0,0 @@ -# Docker Containerization - 3 ноября 2025 - -## 🐳 Добавлена поддержка Docker - -### Что добавлено - -1. **Dockerfile** - - Базовый образ: Python 3.11-slim - - Многоступенчатая установка зависимостей - - Volumes для персистентных данных - - Healthcheck для мониторинга - - Оптимизирован размер образа - -2. **docker-compose.yml** - - Полная оркестрация сервиса - - Управление ресурсами (CPU/RAM) - - Автоматический перезапуск - - Логирование с ротацией - - Изолированная сеть - -3. **Скрипты запуска** - - `start.sh` / `start.bat` - запуск (Linux/Windows) - - `stop.sh` / `stop.bat` - остановка (Linux/Windows) - - Автоматическое создание директорий - - Проверка наличия .env - -4. **Makefile** - - 20+ команд для управления - - `make up` - запуск - - `make down` - остановка - - `make logs` - логи - - `make backup` - создание backup - - `make test` / `make lint` - разработка - -5. **Документация** - - `docs/DOCKER_GUIDE.md` - полное руководство (120+ строк) - - `docs/DOCKER_QUICKSTART.md` - быстрый старт - - Примеры использования - - Troubleshooting - -6. **Обновлён user_manager.py** - - Поддержка переменной `DATA_DIR` - - Автоматическое создание директорий - - Совместимость с Docker volumes - -### Изменения в существующих файлах - -**`.env.example`:** -- Удалены `MOYSKLAD_LOGIN` и `MOYSKLAD_PASSWORD` -- Добавлена `TZ=Europe/Moscow` для Docker -- Комментарий о многопользовательской системе - -**.gitignore:** -- Добавлена директория `data/` (для Docker volumes) -- Добавлена директория `logs/` - -**README.md:** -- Добавлен раздел "Вариант 1: Docker (рекомендуется)" -- Обновлены инструкции по запуску -- Ссылки на Docker документацию - -### Структура директорий для Docker - -``` -BUH_bot/ -├── Dockerfile -├── docker-compose.yml -├── .dockerignore -├── Makefile -├── start.sh / start.bat -├── stop.sh / stop.bat -├── data/ # Volume: пользовательские данные -│ ├── users.json -│ └── .encryption_key -├── logs/ # Volume: логи -└── temp_documents/ # Volume: временные файлы -``` - -### Volumes (персистентные данные) - -1. **`./data/` → `/app/data`** - - `users.json` - зашифрованные credentials пользователей - - `.encryption_key` - ключ шифрования - -2. **`./logs/` → `/app/logs`** - - Логи работы бота - -3. **`./temp_documents/` → `/app/temp_documents`** - - Временные XML/Excel файлы - -### Конфигурация контейнера - -**Ресурсы:** -- CPU: 0.5 cores (limit), 0.25 cores (reservation) -- RAM: 512MB (limit), 256MB (reservation) - -**Restart policy:** -- `unless-stopped` - автоматический перезапуск кроме явной остановки - -**Healthcheck:** -- Интервал: 30 секунд -- Timeout: 10 секунд -- Retries: 3 -- Start period: 40 секунд - -**Логирование:** -- Driver: json-file -- Max size: 10MB per file -- Max files: 3 (30MB total) - -### Использование - -#### Быстрый старт (Windows): -```cmd -copy .env.example .env -notepad .env -start.bat -``` - -#### Быстрый старт (Linux/Mac): -```bash -cp .env.example .env -nano .env -chmod +x start.sh -./start.sh -``` - -#### С Makefile: -```bash -make help # Показать команды -make up # Запустить -make logs # Логи -make down # Остановить -make backup # Backup данных -``` - -### Преимущества Docker-версии - -1. **Изоляция** - - Независимое окружение - - Нет конфликтов с системными пакетами - -2. **Простота развертывания** - - Один образ для всех платформ - - Не нужно устанавливать Python и зависимости - -3. **Управление ресурсами** - - Ограничение CPU/RAM - - Предсказуемое потребление ресурсов - -4. **Надёжность** - - Автоматический перезапуск - - Healthcheck для мониторинга - - Логирование с ротацией - -5. **Масштабируемость** - - Легко добавить reverse proxy (nginx) - - Готово к Docker Swarm / Kubernetes - -### Совместимость - -- ✅ Windows 10/11 (Docker Desktop) -- ✅ macOS (Docker Desktop) -- ✅ Linux (Docker Engine) -- ✅ Windows Server 2016+ -- ✅ Raspberry Pi (ARM) - -### Миграция с локальной версии - -Существующие пользователи могут: - -1. Продолжить использовать локальную версию (`python bot.py`) -2. Перейти на Docker: - ```bash - # Скопировать данные - mkdir data - mv users.json data/ - mv .encryption_key data/ - - # Запустить в Docker - ./start.sh - ``` - -Данные полностью совместимы! - -### Следующие шаги - -Планируется добавить: -- [ ] Docker Hub образ для быстрого деплоя -- [ ] Kubernetes манифесты -- [ ] CI/CD pipeline (GitHub Actions) -- [ ] Мониторинг (Prometheus + Grafana) -- [ ] Nginx reverse proxy для webhook режима - -### Тестирование - -Протестировано на: -- ✅ Windows 11 + Docker Desktop -- ✅ Ubuntu 22.04 + Docker Engine -- ✅ macOS Sonoma + Docker Desktop - -### Безопасность - -- Credentials не попадают в образ (через .dockerignore) -- Volumes для чувствительных данных -- Logs с ротацией (предотвращение переполнения диска) -- Изолированная сеть - -### Размер образа - -- Базовый образ: ~150MB -- С зависимостями: ~300MB -- Итого: **~300MB** (сжатый ~120MB) - -### Дополнительные материалы - -- [docs/DOCKER_GUIDE.md](DOCKER_GUIDE.md) - Полное руководство -- [docs/DOCKER_QUICKSTART.md](DOCKER_QUICKSTART.md) - Быстрый старт -- [docs/MULTI_USER_AUTH.md](MULTI_USER_AUTH.md) - Система авторизации -- [docs/MIGRATION_GUIDE.md](MIGRATION_GUIDE.md) - Миграция на многопользовательскую версию - ---- - -**Автор:** Docker containerization -**Дата:** 3 ноября 2025 -**Версия:** 1.0 diff --git a/docs/DOCKER_GUIDE.md b/docs/DOCKER_GUIDE.md deleted file mode 100644 index 54560cb..0000000 --- a/docs/DOCKER_GUIDE.md +++ /dev/null @@ -1,493 +0,0 @@ -# Docker Development & Deployment Guide - -## 📦 Структура Docker-окружения - -``` -BUH_bot/ -├── Dockerfile # Образ контейнера -├── docker-compose.yml # Оркестрация -├── .dockerignore # Исключения при сборке -├── start.sh / start.bat # Скрипт запуска (Linux/Windows) -├── stop.sh / stop.bat # Скрипт остановки (Linux/Windows) -├── .env # Переменные окружения -└── .env.example # Пример конфигурации -``` - -## 🚀 Быстрый старт - -### 1. Подготовка - -**Linux/Mac:** -```bash -# Клонируем репозиторий -git clone -cd BUH_bot - -# Создаём .env из примера -cp .env.example .env - -# Редактируем .env и указываем TELEGRAM_BOT_TOKEN -nano .env -``` - -**Windows:** -```cmd -git clone -cd BUH_bot - -copy .env.example .env -notepad .env -``` - -### 2. Запуск - -**Linux/Mac:** -```bash -chmod +x start.sh stop.sh -./start.sh -``` - -**Windows:** -```cmd -start.bat -``` - -**Или напрямую через docker-compose:** -```bash -docker-compose up -d -``` - -### 3. Проверка статуса - -```bash -# Статус контейнера -docker-compose ps - -# Логи в реальном времени -docker-compose logs -f - -# Последние 100 строк логов -docker-compose logs --tail=100 -``` - -### 4. Остановка - -**Linux/Mac:** -```bash -./stop.sh -``` - -**Windows:** -```cmd -stop.bat -``` - -**Или напрямую:** -```bash -docker-compose down -``` - -## 🏗️ Dockerfile - -### Основные особенности: - -- **Базовый образ:** `python:3.11-slim` (легкий, ~150MB) -- **Рабочая директория:** `/app` -- **Volumes:** - - `/app/data` - пользовательские данные (users.json, .encryption_key) - - `/app/logs` - логи бота - - `/app/temp_documents` - временные файлы -- **Healthcheck:** Проверка работоспособности каждые 30 секунд -- **Multi-stage build:** Нет (можно добавить для уменьшения размера) - -### Сборка образа вручную: - -```bash -# Сборка -docker build -t buh_bot:latest . - -# Запуск -docker run -d \ - --name buh_bot \ - --env-file .env \ - -v $(pwd)/data:/app/data \ - -v $(pwd)/logs:/app/logs \ - -v $(pwd)/temp_documents:/app/temp_documents \ - buh_bot:latest -``` - -## 🎛️ Docker Compose - -### Конфигурация - -```yaml -version: '3.8' - -services: - buh_bot: - build: . - container_name: buh_bot - restart: unless-stopped - - environment: - - TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN} - - MOYSKLAD_API_URL=${MOYSKLAD_API_URL} - - TZ=Europe/Moscow - - volumes: - - ./data:/app/data - - ./logs:/app/logs - - ./temp_documents:/app/temp_documents - - deploy: - resources: - limits: - cpus: '0.5' - memory: 512M -``` - -### Полезные команды: - -```bash -# Пересборка образа -docker-compose build --no-cache - -# Запуск с выводом логов -docker-compose up - -# Перезапуск -docker-compose restart - -# Остановка и удаление контейнера -docker-compose down - -# Остановка + удаление volumes -docker-compose down -v - -# Просмотр логов конкретного сервиса -docker-compose logs buh_bot - -# Вход в контейнер -docker-compose exec buh_bot bash - -# Просмотр использования ресурсов -docker stats buh_bot -``` - -## 📁 Volumes и данные - -### Bind Mounts (текущая конфигурация) - -Данные хранятся на хосте в директориях: -- `./data/` → `/app/data` - users.json, .encryption_key -- `./logs/` → `/app/logs` - логи бота -- `./temp_documents/` → `/app/temp_documents` - временные документы - -**Преимущества:** -- Простой доступ к данным с хоста -- Легко делать backup -- Данные сохраняются после `docker-compose down` - -**Backup:** -```bash -# Создание backup -tar -czf backup-$(date +%Y%m%d).tar.gz data/ logs/ - -# Восстановление -tar -xzf backup-20250103.tar.gz -``` - -### Named Volumes (альтернатива) - -В `docker-compose.yml` можно использовать named volumes: - -```yaml -volumes: - - bot_data:/app/data - - bot_logs:/app/logs - -volumes: - bot_data: - driver: local - bot_logs: - driver: local -``` - -**Преимущества:** -- Управляются Docker -- Лучшая производительность на Windows/Mac - -**Backup named volumes:** -```bash -# Создание backup -docker run --rm -v bot_data:/data -v $(pwd):/backup \ - alpine tar czf /backup/data-backup.tar.gz -C /data . - -# Восстановление -docker run --rm -v bot_data:/data -v $(pwd):/backup \ - alpine tar xzf /backup/data-backup.tar.gz -C /data -``` - -## 🔐 Безопасность - -### Environment Variables - -**Важно:** Никогда не коммитьте `.env` в Git! - -```bash -# .env должен быть в .gitignore -echo ".env" >> .gitignore -``` - -### Секреты (для продакшена) - -Для production рекомендуется использовать Docker secrets: - -```yaml -services: - buh_bot: - secrets: - - telegram_token - -secrets: - telegram_token: - file: ./secrets/telegram_token.txt -``` - -### Ключ шифрования - -Критически важно сохранить `.encryption_key`! - -```bash -# Backup ключа -cp data/.encryption_key data/.encryption_key.backup - -# Или в безопасное место -cp data/.encryption_key ~/secure-backup/ -``` - -Если ключ будет утерян, все пользовательские credentials станут недоступны. - -## 📊 Мониторинг - -### Логи - -```bash -# Все логи -docker-compose logs - -# Последние 100 строк -docker-compose logs --tail=100 - -# Следить за логами -docker-compose logs -f - -# Логи с временными метками -docker-compose logs -t -``` - -### Ресурсы - -```bash -# Использование CPU/RAM в реальном времени -docker stats buh_bot - -# Информация о контейнере -docker inspect buh_bot - -# Процессы внутри контейнера -docker-compose top -``` - -### Healthcheck - -Контейнер проверяет свою работоспособность каждые 30 секунд: - -```bash -# Проверка health status -docker inspect --format='{{.State.Health.Status}}' buh_bot -``` - -Статусы: -- `healthy` - всё работает -- `unhealthy` - проблемы (3 неудачных проверки) -- `starting` - контейнер запускается - -## 🔄 Обновление - -### Обновление кода - -```bash -# Остановка бота -docker-compose down - -# Получение обновлений -git pull - -# Пересборка образа -docker-compose build - -# Запуск -docker-compose up -d -``` - -### Обновление зависимостей - -После изменения `requirements.txt`: - -```bash -docker-compose build --no-cache -docker-compose up -d -``` - -## 🐛 Troubleshooting - -### Контейнер не запускается - -```bash -# Проверить логи -docker-compose logs - -# Проверить конфигурацию -docker-compose config - -# Запустить в foreground режиме -docker-compose up -``` - -### Проблемы с правами доступа - -**Linux:** -```bash -# Изменить владельца директорий -sudo chown -R $USER:$USER data logs temp_documents - -# Или дать полные права (не рекомендуется для продакшена) -chmod -R 777 data logs temp_documents -``` - -### Ошибка "port already in use" - -Если используются дополнительные сервисы с портами: -```bash -# Найти процесс использующий порт -netstat -tulpn | grep :8443 - -# Или -lsof -i :8443 - -# Остановить конфликтующий контейнер -docker stop -``` - -### Контейнер падает с OOM (Out of Memory) - -Увеличить лимит памяти в `docker-compose.yml`: -```yaml -deploy: - resources: - limits: - memory: 1G # было 512M -``` - -### Нет места на диске - -```bash -# Очистка неиспользуемых образов -docker image prune -a - -# Очистка всего неиспользуемого -docker system prune -a --volumes - -# Проверка использования места -docker system df -``` - -## 🚀 Production Deployment - -### Использование Docker Swarm - -```bash -# Инициализация Swarm -docker swarm init - -# Деплой stack -docker stack deploy -c docker-compose.yml buh_bot_stack - -# Масштабирование (если нужно) -docker service scale buh_bot_stack_buh_bot=2 - -# Просмотр сервисов -docker stack services buh_bot_stack - -# Удаление stack -docker stack rm buh_bot_stack -``` - -### Использование с nginx (reverse proxy) - -Если планируется использовать webhook режим: - -```yaml -# docker-compose.yml -services: - buh_bot: - # ... существующая конфигурация - expose: - - "8443" - - nginx: - image: nginx:alpine - ports: - - "80:80" - - "443:443" - volumes: - - ./nginx.conf:/etc/nginx/nginx.conf - - ./certs:/etc/nginx/certs -``` - -### CI/CD пример (GitHub Actions) - -```yaml -# .github/workflows/deploy.yml -name: Deploy Bot - -on: - push: - branches: [main] - -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - - name: Build and push Docker image - run: | - docker build -t myregistry/buh_bot:latest . - docker push myregistry/buh_bot:latest - - - name: Deploy to server - run: | - ssh user@server "cd /app/buh_bot && \ - docker-compose pull && \ - docker-compose up -d" -``` - -## 📚 Дополнительные ресурсы - -- [Docker Documentation](https://docs.docker.com/) -- [Docker Compose Documentation](https://docs.docker.com/compose/) -- [Best practices for writing Dockerfiles](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/) -- [Docker security best practices](https://docs.docker.com/engine/security/) - -## 🎯 Checklist перед деплоем - -- [ ] `.env` файл создан и заполнен -- [ ] `TELEGRAM_BOT_TOKEN` указан корректно -- [ ] Директории `data/`, `logs/`, `temp_documents/` созданы -- [ ] Docker и Docker Compose установлены -- [ ] Backup `.encryption_key` сделан -- [ ] Логирование настроено -- [ ] Healthcheck работает -- [ ] Тестовый запуск выполнен успешно diff --git a/docs/DOCKER_QUICKSTART.md b/docs/DOCKER_QUICKSTART.md deleted file mode 100644 index 91a0800..0000000 --- a/docs/DOCKER_QUICKSTART.md +++ /dev/null @@ -1,153 +0,0 @@ -# 🐳 Docker Quick Start - -## Запуск за 3 шага - -### 1️⃣ Создайте .env файл - -**Windows:** -```cmd -copy .env.example .env -notepad .env -``` - -**Linux/Mac:** -```bash -cp .env.example .env -nano .env -``` - -**Укажите в .env:** -```env -TELEGRAM_BOT_TOKEN=your_bot_token_from_botfather -``` - -### 2️⃣ Запустите - -**Windows:** -```cmd -start.bat -``` - -**Linux/Mac:** -```bash -chmod +x start.sh -./start.sh -``` - -**Или через docker-compose:** -```bash -docker-compose up -d -``` - -### 3️⃣ Готово! 🎉 - -Бот запущен и работает в фоне. - -## Управление - -### Просмотр логов -```bash -docker-compose logs -f -``` -*(Ctrl+C для выхода)* - -### Остановка -```bash -docker-compose down -``` - -### Перезапуск -```bash -docker-compose restart -``` - -### Статус -```bash -docker-compose ps -``` - -## Makefile (для удобства) - -Если установлен `make`: - -```bash -make help # Показать все команды -make up # Запустить -make down # Остановить -make logs # Логи -make restart # Перезапуск -make shell # Войти в контейнер -make backup # Создать backup данных -``` - -## Где хранятся данные? - -``` -./data/ → Пользовательские данные - ├── users.json (учетные данные, зашифрованные) - └── .encryption_key (ключ шифрования) - -./logs/ → Логи бота - -./temp_documents/ → Временные файлы (Excel/XML) -``` - -**⚠️ ВАЖНО:** Сделайте backup `./data/.encryption_key`! - -## Обновление бота - -```bash -# Остановка -docker-compose down - -# Обновление кода -git pull - -# Пересборка и запуск -docker-compose build -docker-compose up -d -``` - -**Или через Makefile:** -```bash -make update -``` - -## Troubleshooting - -### "docker-compose: command not found" - -Установите Docker и Docker Compose: -- [Docker Desktop for Windows](https://docs.docker.com/desktop/install/windows-install/) -- [Docker Desktop for Mac](https://docs.docker.com/desktop/install/mac-install/) -- [Docker Engine for Linux](https://docs.docker.com/engine/install/) - -### Бот не запускается - -```bash -# Проверьте логи -docker-compose logs - -# Проверьте .env файл -cat .env # Linux/Mac -type .env # Windows -``` - -### Недостаточно прав (Linux) - -```bash -# Добавьте пользователя в группу docker -sudo usermod -aG docker $USER - -# Выйдите и войдите заново -``` - -## Полная документация - -📖 [docs/DOCKER_GUIDE.md](DOCKER_GUIDE.md) - Подробное руководство - -## Нужна помощь? - -1. Проверьте [docs/DOCKER_GUIDE.md](DOCKER_GUIDE.md) -2. Посмотрите логи: `docker-compose logs -f` -3. Проверьте статус: `docker-compose ps` diff --git a/docs/PRODUCTION_DEPLOYMENT.md b/docs/PRODUCTION_DEPLOYMENT.md deleted file mode 100644 index 6e25240..0000000 --- a/docs/PRODUCTION_DEPLOYMENT.md +++ /dev/null @@ -1,436 +0,0 @@ -# Production Deployment Guide - -## 🎯 Архитектура Production - -``` -┌─────────────────────────────────────────────────┐ -│ Production Stack │ -├─────────────────────────────────────────────────┤ -│ │ -│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ -│ │ Nginx │ │ BUH Bot │ │Prometheus│ │ -│ │ Proxy │→ │ (Main) │← │Monitoring│ │ -│ └──────────┘ └──────────┘ └──────────┘ │ -│ ↓ ↓ │ -│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ -│ │ Grafana │← │ Node │ │cAdvisor │ │ -│ │Dashboard │ │ Exporter │ │Containers│ │ -│ └──────────┘ └──────────┘ └──────────┘ │ -│ ↓ │ -│ ┌──────────────────────────────────┐ │ -│ │ Watchtower (Auto-update) │ │ -│ └──────────────────────────────────┘ │ -└─────────────────────────────────────────────────┘ -``` - -## 📋 Компоненты - -### Основные сервисы - -1. **buh_bot** - Telegram бот (главный сервис) - - Использует готовый образ из registry - - Автоматический перезапуск - - Healthcheck каждые 30 секунд - - Limits: 1 CPU, 1GB RAM - -2. **nginx** - Reverse proxy - - Rate limiting - - SSL termination (опционально) - - Health endpoints - - Logs rotation - -### Мониторинг - -3. **prometheus** - Сбор метрик - - Retention: 30 дней - - Port: 9090 - - Метрики хоста и контейнеров - -4. **grafana** - Визуализация - - Дашборды - - Алерты - - Port: 3000 - -5. **node_exporter** - Метрики хоста - - CPU, RAM, Disk, Network - - Port: 9100 - -6. **cadvisor** - Метрики контейнеров - - Docker metrics - - Port: 8080 - -### Автоматизация - -7. **watchtower** - Автообновление - - Проверка обновлений каждый час - - Автоматический pull и restart - - Очистка старых образов - -## 🚀 Развертывание - -### Шаг 1: Подготовка образа - -#### Вариант А: Использование GitHub Container Registry - -1. **Форкните репозиторий** на GitHub - -2. **Включите GitHub Actions:** - - Settings → Actions → Allow all actions - -3. **Создайте тег и запушьте:** - ```bash - git tag v1.0.0 - git push origin v1.0.0 - ``` - -4. **GitHub Actions автоматически:** - - Соберёт образ - - Запустит тесты - - Опубликует в `ghcr.io/your-username/buh_bot` - -#### Вариант Б: Локальная сборка - -```bash -# Авторизация в registry -docker login ghcr.io - -# Сборка и публикация -chmod +x build-image.sh -./build-image.sh -``` - -**Windows:** -```cmd -docker login ghcr.io -build-image.bat -``` - -### Шаг 2: Настройка сервера - -```bash -# Установка Docker и Docker Compose -curl -fsSL https://get.docker.com | sh -sudo usermod -aG docker $USER - -# Клонирование конфигурации -git clone -cd BUH_bot -``` - -### Шаг 3: Конфигурация - -```bash -# Создание .env.prod -cp .env.prod.example .env.prod -nano .env.prod -``` - -**Обязательные переменные:** -```env -TELEGRAM_BOT_TOKEN=your_production_token -DOCKER_USERNAME=your_github_username -GRAFANA_PASSWORD=secure_password_here -``` - -### Шаг 4: Создание директорий - -```bash -# Директории для данных -sudo mkdir -p /var/lib/buh_bot/data -sudo mkdir -p /var/log/buh_bot -sudo mkdir -p /var/tmp/buh_bot - -# Права доступа -sudo chown -R $USER:$USER /var/lib/buh_bot -sudo chown -R $USER:$USER /var/log/buh_bot -sudo chown -R $USER:$USER /var/tmp/buh_bot -``` - -### Шаг 5: Деплой - -```bash -chmod +x deploy-prod.sh -./deploy-prod.sh -``` - -**Windows:** -```cmd -deploy-prod.bat -``` - -## 📊 Мониторинг - -### Grafana - -Откройте: `http://your-server:3000` - -**Логин по умолчанию:** -- Username: `admin` -- Password: (из .env.prod `GRAFANA_PASSWORD`) - -**Готовые дашборды:** -- Docker Containers -- Host Metrics -- Bot Statistics - -### Prometheus - -Откройте: `http://your-server:9090` - -**Полезные запросы:** -```promql -# CPU использование бота -rate(container_cpu_usage_seconds_total{name="buh_bot_prod"}[5m]) - -# Память бота -container_memory_usage_bytes{name="buh_bot_prod"} / 1024 / 1024 - -# Uptime -time() - container_start_time_seconds{name="buh_bot_prod"} -``` - -### cAdvisor - -Откройте: `http://your-server:8080` - -Подробная информация о контейнерах в реальном времени. - -## 🔧 Управление - -### Логи - -```bash -# Все сервисы -docker-compose -f docker-compose.prod.yml logs -f - -# Только бот -docker-compose -f docker-compose.prod.yml logs -f buh_bot - -# Последние 100 строк -docker-compose -f docker-compose.prod.yml logs --tail=100 buh_bot -``` - -### Статус - -```bash -# Статус контейнеров -docker-compose -f docker-compose.prod.yml ps - -# Healthcheck -docker inspect --format='{{.State.Health.Status}}' buh_bot_prod - -# Использование ресурсов -docker stats buh_bot_prod -``` - -### Перезапуск - -```bash -# Перезапуск бота -docker-compose -f docker-compose.prod.yml restart buh_bot - -# Перезапуск всех сервисов -docker-compose -f docker-compose.prod.yml restart - -# Остановка -docker-compose -f docker-compose.prod.yml down - -# Остановка + удаление volumes -docker-compose -f docker-compose.prod.yml down -v -``` - -### Обновление - -```bash -# Ручное обновление -docker-compose -f docker-compose.prod.yml pull -docker-compose -f docker-compose.prod.yml up -d - -# Автообновление (через Watchtower) -# Настроено автоматически, проверка каждый час -``` - -## 💾 Backup - -### Автоматический backup - -Настройте cronjob: - -```bash -# Редактирование crontab -crontab -e - -# Добавьте (backup каждый день в 2:00) -0 2 * * * /path/to/backup.sh -``` - -**backup.sh:** -```bash -#!/bin/bash -BACKUP_DIR="/var/backups/buh_bot/$(date +%Y%m%d_%H%M%S)" -mkdir -p "$BACKUP_DIR" -cp -r /var/lib/buh_bot/data/* "$BACKUP_DIR/" -find /var/backups/buh_bot/ -type d -mtime +30 -exec rm -rf {} + -``` - -### Ручной backup - -```bash -# Создание backup -tar -czf buh_bot_backup_$(date +%Y%m%d).tar.gz \ - /var/lib/buh_bot/data \ - /var/log/buh_bot - -# Восстановление -tar -xzf buh_bot_backup_20250103.tar.gz -C / -``` - -## 🔒 Безопасность - -### 1. Firewall - -```bash -# UFW (Ubuntu) -sudo ufw allow 22/tcp # SSH -sudo ufw allow 80/tcp # HTTP -sudo ufw allow 443/tcp # HTTPS -sudo ufw enable - -# Закрыть порты мониторинга извне -# (доступ только через SSH tunnel) -``` - -### 2. SSL/TLS - -Для webhook режима установите SSL: - -```bash -# Certbot для Let's Encrypt -sudo apt install certbot -sudo certbot certonly --standalone -d your-domain.com - -# Скопируйте сертификаты -sudo cp /etc/letsencrypt/live/your-domain.com/fullchain.pem nginx/ssl/ -sudo cp /etc/letsencrypt/live/your-domain.com/privkey.pem nginx/ssl/ - -# Раскомментируйте SSL блок в nginx/nginx.conf -``` - -### 3. Secrets - -Не храните секреты в Git: - -```bash -# Проверка -git secrets --scan - -# Ротация токенов -# Регулярно меняйте TELEGRAM_BOT_TOKEN в .env.prod -``` - -### 4. Updates - -```bash -# Обновление системы -sudo apt update && sudo apt upgrade -y - -# Обновление Docker -sudo apt install docker-ce docker-ce-cli containerd.io -``` - -## 📈 Масштабирование - -### Docker Swarm - -```bash -# Инициализация swarm -docker swarm init - -# Деплой stack -docker stack deploy -c docker-compose.prod.yml buh_bot - -# Масштабирование (если нужно несколько реплик) -docker service scale buh_bot_buh_bot=3 -``` - -### Kubernetes - -Для Kubernetes создайте манифесты на основе docker-compose.prod.yml: - -```bash -# Конвертация в k8s манифесты -kompose convert -f docker-compose.prod.yml -``` - -## 🆘 Troubleshooting - -### Бот не запускается - -```bash -# Проверить логи -docker-compose -f docker-compose.prod.yml logs buh_bot - -# Проверить конфигурацию -docker-compose -f docker-compose.prod.yml config - -# Проверить переменные окружения -docker exec buh_bot_prod env -``` - -### Ошибка "image not found" - -```bash -# Проверить доступ к registry -docker pull ghcr.io/your-username/buh_bot:latest - -# Авторизоваться заново -docker logout ghcr.io -docker login ghcr.io -``` - -### Высокое использование ресурсов - -```bash -# Увеличить лимиты в docker-compose.prod.yml -deploy: - resources: - limits: - cpus: '2.0' - memory: 2G -``` - -### Проблемы с volumes - -```bash -# Проверить права -ls -la /var/lib/buh_bot/data - -# Исправить права -sudo chown -R 1000:1000 /var/lib/buh_bot/data -``` - -## 📞 Поддержка - -- 📖 [DOCKER_GUIDE.md](DOCKER_GUIDE.md) - Docker основы -- 📖 [DOCKER_QUICKSTART.md](DOCKER_QUICKSTART.md) - Быстрый старт -- 🐛 GitHub Issues - Сообщить о проблеме - -## ✅ Checklist - -Перед production деплоем: - -- [ ] Образ собран и опубликован в registry -- [ ] `.env.prod` настроен (особенно `TELEGRAM_BOT_TOKEN`) -- [ ] Grafana пароль изменён с дефолтного -- [ ] Директории созданы на хосте -- [ ] Firewall настроен -- [ ] SSL сертификаты установлены (если нужен webhook) -- [ ] Backup настроен -- [ ] Мониторинг проверен -- [ ] Healthcheck работает -- [ ] Логи ротируются -- [ ] Документация обновлена - ---- - -**Production готов!** 🚀 diff --git a/docs/XML_1C_FORMAT.md b/docs/XML_1C_FORMAT.md deleted file mode 100644 index 9932df3..0000000 --- a/docs/XML_1C_FORMAT.md +++ /dev/null @@ -1,195 +0,0 @@ -# Генератор XML в формате 1С EnterpriseData 1.20 - -## Описание - -Создан новый класс `XML1CGenerator` для генерации XML документов в формате обмена данными 1С (EnterpriseData 1.20). Этот формат используется для загрузки документов реализации в конфигурации 1С. - -## Отличия от формата ВерсФорм 5.03 - -| Параметр | Формат 1С | ВерсФорм 5.03 (УПД/ТН) | -|----------|-----------|------------------------| -| **Назначение** | Обмен данными между 1С | УПД/ТОРГ-12 для ФНС | -| **Корневой элемент** | `` | `<Файл>` | -| **Namespace** | http://v8.1c.ru/edi/edi_stnd/EnterpriseData/1.20 | Нет | -| **Документ** | `<Документ.РеализацияТоваровУслуг>` | `<СвСчФакт>` | -| **Формат даты** | ISO 8601 (2025-10-23T09:37:11) | ДД.ММ.ГГГГ | -| **Суммы** | Рубли (целое число) | Рубли с копейками | - -## Использование - -### Базовый пример - -```python -from modules.xml_generator import XML1CGenerator - -# Создание генератора -generator = XML1CGenerator() - -# Подготовка данных заказа из МойСклад -order_data = { - "order": { - "id": "313256ef-b4b7-11f0-b68a-50ebf69bde57", - "name": "4869", - "moment": "2025-10-23T09:37:11Z", - "sum": 1059900, # В копейках! - "rate": { - "currency": { - "name": "руб.", - "isoCode": "643" - } - } - }, - "organization": { - "id": "288f7520-b8e5-11e8-80bc-e0d55e458e90", - "name": "Беляев А.Ю. ИП", - "legalTitle": "Индивидуальный предприниматель Беляев Андрей Юрьевич", - "inn": "263605160270", - "companyType": "entrepreneur", # или "legal" - "legalLastName": "Беляев", - "legalFirstName": "Андрей", - "legalMiddleName": "Юрьевич" - }, - "counterparty": { - "id": "f37958af-d807-11ef-b676-50ebf69bde57", - "name": "АСЕЕВА АННА КУЗЬМИНИЧНА ИП", - "legalTitle": "ИНДИВИДУАЛЬНЫЙ ПРЕДПРИНИМАТЕЛЬ АСЕЕВА АННА КУЗЬМИНИЧНА", - "inn": "383700097634", - "companyType": "entrepreneur" - }, - "org_accounts": [ - { - "id": "bdd39811-27ee-11ea-80e7-e0d55e458e90", - "accountNumber": "40802810860270001979", - "bankName": "СТАВРОПОЛЬСКОЕ ОТДЕЛЕНИЕ N5230 ПАО СБЕРБАНК", - "bic": "040702615", - "correspondentAccount": "30101810907020000615" - } - ], - "positions": [ - { - "assortment": { - "name": "Жевательная резинка RUSGUM Арбузное трио 24 мм 7*150 штук", - "code": "00-00000682", - "uom": {"name": "шт"} - }, - "quantity": 1, - "price": 337300, # В копейках! - "vat": 5 - } - ] -} - -# Генерация XML -xml_string = generator.generate_realization(order_data, "realization_1c.xml") -print("XML создан успешно!") -``` - -## Структура XML документа - -### 1. Message Header - -```xml - - http://v8.1c.ru/edi/edi_stnd/EnterpriseData/1.20 - 2025-11-05T10:39:56 - 1.20 - -``` - -### 2. Body с документом реализации - -```xml - - <Документ.РеализацияТоваровУслуг> - <КлючевыеСвойства>... - <ВидОперации>РеализацияКлиенту - <Товары>... - ... - - -``` - -## Обязательные поля - -### Организация (продавец) -- `id` - UUID организации -- `name` - Краткое наименование -- `legalTitle` - Полное наименование -- `inn` - ИНН -- `companyType` - Тип: "entrepreneur" или "legal" - -Для ИП дополнительно: -- `legalLastName` - Фамилия -- `legalFirstName` - Имя -- `legalMiddleName` - Отчество (опционально) - -### Контрагент (покупатель) -- `id` - UUID контрагента -- `name` - Наименование -- `legalTitle` - Полное наименование -- `inn` - ИНН -- `companyType` - Тип - -### Заказ -- `id` - UUID заказа -- `name` - Номер документа -- `moment` - Дата в ISO формате -- `sum` - Сумма в копейках - -### Позиции -- `assortment.name` - Наименование товара -- `assortment.code` - Код товара (опционально) -- `assortment.uom.name` - Единица измерения -- `quantity` - Количество -- `price` - Цена в копейках -- `vat` - Ставка НДС (5, 10, 20) - -## Интеграция с ботом - -В `bot.py` добавьте выбор формата: - -```python -from modules.xml_generator import XMLGenerator, XML1CGenerator - -# При генерации документа -def generate_document(order_data, format_type): - if format_type == "1c": - generator = XML1CGenerator() - xml = generator.generate_realization(order_data, "doc.xml") - elif format_type == "upd": - generator = XMLGenerator() - xml = generator.generate_upd(order_data, "doc.xml") - elif format_type == "torg12": - generator = XMLGenerator() - xml = generator.generate_tn(order_data, "doc.xml") - - return xml -``` - -## Особенности формата - -1. **Суммы в рублях** - Все денежные значения в целых рублях (без копеек) -2. **UUID везде** - Все ссылки используют UUID формат -3. **Дата ISO 8601** - Формат: `YYYY-MM-DDTHH:MM:SS` -4. **Номер с паддингом** - Номер документа дополняется пробелами до 11 символов -5. **Тип лица** - "ФизическоеЛицо" для ИП, "ЮридическоеЛицо" для ООО -6. **НДС извлекается** - Сумма НДС вычисляется из общей суммы (включающей НДС) - -## Проверка результата - -Сгенерированный XML можно загрузить в 1С через: -- Обработку "Универсальный обмен данными в формате XML" -- Загрузку из файла в документе "Реализация товаров и услуг" - -## Поддержка - -При возникновении проблем проверьте: -1. Все обязательные поля заполнены -2. Суммы передаются в копейках (умножить на 100) -3. Дата в ISO формате с временем -4. ИНН корректный (10 или 12 цифр) -5. UUID в формате: `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` - -## Версии - -- **1.0** (2025-11-05) - Начальная версия с поддержкой документа реализации diff --git a/docs/next_steps.md b/docs/next_steps.md deleted file mode 100644 index 246f86e..0000000 --- a/docs/next_steps.md +++ /dev/null @@ -1,284 +0,0 @@ -# Следующие шаги - 6 ноября 2025 - -## ✅ Выполнено сегодня - -### 1. Критичные исправления XML генератора -- ✅ Исправлена потеря копеек (`.2f` вместо `int()`) -- ✅ Удалено поле ВидЭД (отсутствует в ED 1.20) -- ✅ Fallback даты заменён на исключение -- ✅ Счета учёта закомментированы (опционально) - -### 2. Валидация -- ✅ Валидатор интегрирован и работает -- ✅ 0 критических ошибок в тестовых XML -- ✅ Все UUID корректны - -### 3. Docker -- ✅ Образ пересобран: `sha256:695531b0358c01bf06af94fd410dee234bb605f0db9ca3c6e1d775b433945440` -- ✅ Запушен в Docker Hub: `igornadein/buh_bot:latest` - ---- - -## 🔄 Что делать дальше - -### Вариант А: Деплой на сервер (быстро, без проверки клиента) - -#### 1. Подключиться к серверу -```bash -ssh user@172.11.0.13 -``` - -#### 2. Обновить контейнер -```bash -cd ~/BUH_bot -docker-compose -f docker-compose.prod.yml pull -docker-compose -f docker-compose.prod.yml up -d -``` - -#### 3. Проверить логи -```bash -docker-compose -f docker-compose.prod.yml logs -f buh_bot -``` - -#### 4. Протестировать генерацию XML -- Создать заказ в МойСклад -- Сгенерировать XML через бота -- Попробовать импортировать в 1С - -**Риски:** -- ⚠️ GUID справочников могут не совпадать с базой клиента -- ⚠️ Структура может не соответствовать XSD клиента -- ⚠️ Банк/договор могут вызвать ошибки - ---- - -### Вариант Б: Сначала проверка с клиентом (безопасно) - -#### 1. Запросить у клиента: - -**A. XSD EnterpriseData 1.20** -``` -Запрос клиенту: -"Для корректного формирования XML нам нужна актуальная схема EnterpriseData 1.20 -вашей версии 1С. Можете предоставить файл XSD или сообщить: -- Версию платформы 1С (например, 8.3.25.1394) -- Конфигурацию (УТ 11.5, КА 2.5, ERP 2.5 и т.д.)" -``` - -**Б. GUID справочников из их базы** -``` -Запрос клиенту: -"Для избежания ошибок импорта нужны UUID следующих объектов из вашей базы: -1. Валюта 'Рубль' (RUB, код 643) -2. Страна 'Россия' (код 643) -3. Единица измерения 'Штука' (код 796) - -Можно получить через запрос в 1С: -SELECT Ссылка FROM Справочник.Валюты WHERE Код = '643' -SELECT Ссылка FROM Справочник.КлассификаторСтранМира WHERE Код = '643' -SELECT Ссылка FROM Справочник.УпаковкиЕдиницыИзмерения WHERE Код = '796' -" -``` - -**В. Доступ к тестовой базе** -``` -Запрос клиенту: -"Для тестирования XML нужен доступ к тестовой базе 1С: -- HTTP-сервис для импорта (если есть) -- Или удалённый рабочий стол для ручного импорта -- Или возможность прислать скриншоты/логи ошибок импорта" -``` - -#### 2. После получения данных: -- Обновить GUID в коде (или убрать их) -- Адаптировать структуру под XSD -- Протестировать в их базе -- Задеплоить - ---- - -### Вариант В: Упрощённый подход (компромисс) - -#### Убрать все жёсткие GUID, использовать только коды - -**Изменения в коде:** - -```python -# Валюта - БЕЗ Ссылки -currency_elem = etree.SubElement(doc, "Валюта") -# НЕ создаём <Ссылка> -currency_data = etree.SubElement(currency_elem, "ДанныеКлассификатора") -etree.SubElement(currency_data, "Код").text = "643" -etree.SubElement(currency_data, "КодСимвольный").text = "RUB" -etree.SubElement(currency_data, "Наименование").text = "Российский рубль" - -# Страна - БЕЗ Ссылки -country_elem = etree.SubElement(cp_elem, "СтранаРегистрации") -# НЕ создаём <Ссылка> -country_data = etree.SubElement(country_elem, "ДанныеКлассификатора") -etree.SubElement(country_data, "Код").text = "643" -etree.SubElement(country_data, "Наименование").text = "РОССИЯ" - -# ЕИ - БЕЗ Ссылки -uom_elem = etree.SubElement(row_elem, "ЕдиницаИзмерения") -# НЕ создаём <Ссылка> -uom_data = etree.SubElement(uom_elem, "ДанныеКлассификатора") -etree.SubElement(uom_data, "Код").text = "796" -etree.SubElement(uom_data, "Наименование").text = "Штука" - -# Банк - БЕЗ Ссылки, только БИК -bank_elem = etree.SubElement(bank_acc_elem, "Банк") -# НЕ создаём <Ссылка> -bank_data = etree.SubElement(bank_elem, "ДанныеКлассификатораБанков") -etree.SubElement(bank_data, "БИК").text = acc.get("bic", "") -etree.SubElement(bank_data, "Наименование").text = acc.get("bankName", "") -``` - -**Преимущества:** -- ✅ Работает в любой базе (1С найдёт объекты по кодам) -- ✅ Не зависит от GUID клиента -- ✅ Более универсально - -**Недостатки:** -- ⚠️ Требует настройки "создавать отсутствующие объекты" в 1С -- ⚠️ Может не работать, если структура XSD другая - ---- - -## 📊 Рекомендация - -### Если клиент активен и готов помочь: -**→ Вариант Б** (запросить XSD и GUID) -- Время: 1-2 дня -- Риск: минимальный -- Результат: 100% совместимость - -### Если клиент недоступен, нужно быстро: -**→ Вариант В** (убрать GUID) -- Время: 2-3 часа -- Риск: средний -- Результат: ~80% вероятность работы - -### Если хочется попробовать "как есть": -**→ Вариант А** (деплой без изменений) -- Время: 30 минут -- Риск: высокий -- Результат: может сработать, может упасть - ---- - -## 🎯 Мой совет - -**Начни с Варианта А** (быстрый деплой): -1. Задеплой текущую версию на сервер -2. Попробуй сгенерировать XML через бота -3. Попробуй импортировать в 1С - -**Если работает** ✅: -- Отлично, копейки сохраняются, ВидЭД не ломает -- Значит GUID совпали (повезло) - -**Если НЕ работает** ❌ (ошибки импорта): -- Запроси у клиента: XSD + GUID + доступ к тестовой базе -- Реализуй Вариант В (убрать GUID) -- Доработай под их схему - ---- - -## 📝 Что отслеживать после деплоя - -### 1. Логи бота -```bash -docker-compose -f docker-compose.prod.yml logs -f buh_bot | grep -i "xml\|error\|critical" -``` - -### 2. Ошибки при генерации XML -- Ошибки парсинга даты (должны быть ValueError) -- Ошибки при обращении к МойСклад API -- Проблемы с кодировкой - -### 3. Отзывы пользователей -- Удаётся ли импортировать XML в 1С? -- Какие ошибки видят при импорте? -- Сходятся ли суммы с НДС? - -### 4. Контрольные проверки -```python -# После генерации XML проверять: -# 1. Копейки присутствуют? -assert "1234.56" in xml_content # не "1234" - -# 2. ВидЭД отсутствует? -assert "ВидЭД" not in xml_content - -# 3. Все GUID валидны? -import re -guids = re.findall(r'[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}', xml_content) -assert all(len(g) == 36 for g in guids) -``` - ---- - -## 🚀 Команды для деплоя - -### На локальной машине: -```bash -# Уже выполнено ✅ -docker build -t igornadein/buh_bot:latest . -docker push igornadein/buh_bot:latest -``` - -### На сервере (172.11.0.13): -```bash -# 1. Подключиться -ssh user@172.11.0.13 - -# 2. Перейти в директорию -cd ~/BUH_bot # или /var/www/buh_bot - -# 3. Pull новый образ -docker-compose -f docker-compose.prod.yml pull - -# 4. Перезапуск -docker-compose -f docker-compose.prod.yml up -d - -# 5. Проверка -docker-compose -f docker-compose.prod.yml ps -docker-compose -f docker-compose.prod.yml logs -f buh_bot -``` - -### Откат при проблемах: -```bash -# Откатиться на предыдущий образ -docker pull igornadein/buh_bot@sha256:753a07638bc9133ee086ca7c08c4905a55f4752e4559d9384b6cc9fb75f82c0e -docker-compose -f docker-compose.prod.yml up -d -``` - ---- - -## ✅ Чек-лист перед деплоем - -- [x] Docker образ собран -- [x] Docker образ запушен -- [x] Критичные баги исправлены -- [ ] Документация обновлена (этот файл) -- [ ] Решение принято (А, Б или В) -- [ ] Сервер доступен -- [ ] Есть план отката -- [ ] Пользователи предупреждены (если нужно) - ---- - -## 💡 Итого - -**Сейчас:** -- Образ готов к деплою -- Критичные баги исправлены -- Риски известны - -**Выбери подход:** -- **Быстро и рискованно** → Вариант А (деплой сейчас) -- **Безопасно и медленно** → Вариант Б (проверка с клиентом) -- **Компромисс** → Вариант В (убрать GUID) - -**Мой выбор:** Вариант А → если не работает → Вариант В → если всё ещё проблемы → Вариант Б diff --git a/docs/validator_refactoring.md b/docs/validator_refactoring.md deleted file mode 100644 index b1b3c83..0000000 --- a/docs/validator_refactoring.md +++ /dev/null @@ -1,219 +0,0 @@ -# Рефакторинг валидатора - 6 ноября 2025 - -## Что изменено - -### ✅ Добавлена проверка нулевой цены (КРИТИЧНО) - -**Новая валидация:** -```python -elif price == 0: - # КРИТИЧНО: нулевая цена недопустима для коммерческих документов - results.append(ValidationResult( - level=ValidationLevel.CRITICAL, - field=f"position[{index}].price", - message=f"Позиция {pos_num} ({name or 'без названия'}): " - f"цена равна нулю (недопустимо для УПД/ТОРГ-12)" - )) -``` - -**Почему критично:** -- Нулевая цена недопустима для первичных документов (УПД, ТОРГ-12) -- Нарушает налоговый учёт -- 1С не примет документ с нулевой ценой в коммерческих операциях - -### ✅ Улучшена валидация позиций - -**Было:** -- Простая проверка `price < 0` -- Неинформативные сообщения ("Позиция 1: некорректная цена") - -**Стало:** -- Отдельная проверка для `price is None`, `price < 0`, `price == 0` -- Детальные сообщения с названием товара -- Проверка суммы строки (если цена и количество > 0, но сумма = 0) -- Добавлена проверка кода товара (WARNING) - -**Примеры сообщений:** -``` -❌ Позиция 1 (Товар с нулевой ценой): цена равна нулю (недопустимо для УПД/ТОРГ-12) -❌ Позиция 2 (Услуга консультации): не указана цена -⚠️ Позиция 3 (Материал А): не указан код товара -``` - -### ✅ Улучшена структура кода - -**Изменения:** -1. Добавлена переменная `pos_num = index + 1` для читаемости -2. Все сообщения содержат название товара: `({name or 'без названия'})` -3. Разделены проверки: None / < 0 / == 0 -4. Добавлена проверка суммы строки -5. Добавлены подсказки в WARNING сообщениях - -## Тестирование - -### Тест 1: Нулевая цена -```python -position = { - 'quantity': 10, - 'price': 0, # НУЛЕВАЯ ЦЕНА - 'assortment': {'name': 'Товар'} -} -``` - -**Результат:** -``` -❌ Позиция 1 (Товар): цена равна нулю (недопустимо для УПД/ТОРГ-12) -``` -✅ **PASSED** - критическая ошибка обнаружена - -### Тест 2: Отсутствует цена -```python -position = { - 'quantity': 10, - 'price': None, # НЕТ ЦЕНЫ - 'assortment': {'name': 'Товар'} -} -``` - -**Результат:** -``` -❌ Позиция 1 (Товар): не указана цена -``` -✅ **PASSED** - критическая ошибка обнаружена - -### Тест 3: Отрицательная цена -```python -position = { - 'quantity': 10, - 'price': -100, # ОТРИЦАТЕЛЬНАЯ - 'assortment': {'name': 'Товар'} -} -``` - -**Результат:** -``` -❌ Позиция 1 (Товар): отрицательная цена -1.00 руб -``` -✅ **PASSED** - критическая ошибка обнаружена - -### Тест 4: XML валидация -```bash -python modules/validator.py test_fixed.xml -``` - -**Результат:** -``` -0 критических ошибок -5 предупреждений (не критичны) -``` -✅ **PASSED** - валидатор работает корректно - -## Матрица проверок позиций - -| Проверка | Уровень | Условие | Сообщение | -|----------|---------|---------|-----------| -| Название | CRITICAL | name is None | "не указано наименование товара" | -| Количество | CRITICAL | quantity is None | "не указано количество" | -| Количество | CRITICAL | quantity <= 0 | "некорректное количество {quantity}" | -| Цена | CRITICAL | price is None | "не указана цена" | -| Цена | CRITICAL | price < 0 | "отрицательная цена {price} руб" | -| **Цена** | **CRITICAL** | **price == 0** | **"цена равна нулю (недопустимо для УПД/ТОРГ-12)"** | -| Сумма строки | CRITICAL | price > 0 && qty > 0 && total == 0 | "сумма строки равна нулю" | -| НДС | WARNING | vat is None | "не указана ставка НДС (значение по умолчанию)" | -| ЕИ | WARNING | uom is None | "не указана единица измерения (будет 'шт')" | -| Код | WARNING | code is None | "не указан код товара" | - -## Что НЕ изменено - -### OrderValidator (строки 1-313) -- ✅ Валидация ИНН/КПП/ОГРН - без изменений -- ✅ Валидация организации - без изменений -- ✅ Валидация контрагента - без изменений -- ✅ Валидация заказа - без изменений - -### XML Валидатор (строки 450+) -- ✅ Валидация GUID - без изменений -- ✅ Валидация структуры ED 1.20 - без изменений -- ✅ Проверка арифметики - без изменений -- ✅ Проверка кодов валют/стран - без изменений - -## Покрытие тестами - -### ✅ Покрыто тестами: -- Нулевая цена (новое) ✅ -- Отсутствующая цена ✅ -- Отрицательная цена ✅ -- XML валидация ✅ - -### ⚠️ Требует тестирования: -- Интеграция с ботом -- Реальные данные из МойСклад -- Граничные случаи (очень большие/маленькие суммы) - -## Обратная совместимость - -✅ **Полная обратная совместимость** -- API не изменился -- Сигнатуры методов не изменились -- Добавлены только новые проверки -- Существующие проверки улучшены - -## Производительность - -- **До:** ~0.5ms на позицию -- **После:** ~0.6ms на позицию (+20%) -- **Вывод:** Незначительное увеличение, приемлемо - -## Следующие шаги - -### 1. Деплой ✅ -```bash -# Образ уже собран и запушен -docker pull igornadein/buh_bot:latest -``` - -### 2. Тестирование в боте -- Создать заказ с нулевой ценой в МойСклад -- Попробовать сгенерировать XML -- Проверить, что валидатор блокирует генерацию - -### 3. Документация для пользователей -Добавить в help бота: -``` -⚠️ ВАЖНО: Цена товара не может быть нулевой! -При попытке создания документа с нулевой ценой -вы получите ошибку валидации. - -Проверьте цены в МойСклад перед генерацией XML. -``` - -### 4. Логирование -Добавить детальное логирование критических ошибок: -```python -if price == 0: - logger.error( - f"Validation failed: zero price for position {index} " - f"({name}) in order {order_name}" - ) -``` - -## Итого - -### ✅ Выполнено: -1. Добавлена проверка нулевой цены (CRITICAL) -2. Улучшены сообщения об ошибках -3. Добавлена проверка кода товара (WARNING) -4. Добавлена проверка суммы строки -5. Протестировано на реальных данных - -### 📊 Статистика: -- Строк изменено: ~70 -- Новых проверок: 3 (нулевая цена, сумма строки, код товара) -- Улучшенных проверок: 4 (цена, количество, НДС, ЕИ) -- Тестов пройдено: 4/4 ✅ - -### 🎯 Готовность: -- **Код:** 100% ✅ -- **Тесты:** 100% ✅ -- **Документация:** 100% ✅ -- **Деплой:** Готов к запуску ✅ diff --git a/docs/xml_fixes_applied.md b/docs/xml_fixes_applied.md deleted file mode 100644 index 4042943..0000000 --- a/docs/xml_fixes_applied.md +++ /dev/null @@ -1,296 +0,0 @@ -# Исправления XML генератора - 6 ноября 2025 - -## Выполненные критичные исправления - -### ✅ 1. Исправлена потеря копеек (КРИТИЧНО) - -**Было:** -```python -etree.SubElement(row_elem, "Сумма").text = str(int(total)) # Отбрасывались копейки -etree.SubElement(row_elem, "Цена").text = str(int(price)) # Отбрасывались копейки -``` - -**Стало:** -```python -etree.SubElement(row_elem, "Сумма").text = f"{total:.2f}" # 2 знака после запятой -etree.SubElement(row_elem, "Цена").text = f"{price:.2f}" # 2 знака после запятой -``` - -**Результат тестирования:** -- Цена: 1234.56 руб ✅ (было бы 1234) -- Количество: 10 шт -- Сумма: 12345.60 руб ✅ (было бы 12345) -- **Разница: 56 коп на строку, 5.60 руб на документ** - это критично для налогового учёта! - ---- - -### ✅ 2. Удалено поле ВидЭД (КРИТИЧНО) - -**Было:** -```python -etree.SubElement(doc, "ВидЭД").text = "ТОРГ12" -``` - -**Стало:** -```python -# Поле удалено - его нет в схеме EnterpriseData 1.20 -``` - -**Причина:** -- Поле `ВидЭД` используется в УПД/ФормализованныхДокументах -- В EnterpriseData 1.20 этого поля нет -- Приводило к ошибке валидации по XSD - ---- - -### ✅ 3. Fallback даты заменён на исключение (КРИТИЧНО) - -**Было:** -```python -except Exception as e: - logger.error(f"Ошибка форматирования даты: {e}") - return datetime.now().strftime("%Y-%m-%dT%H:%M:%S") # Подставляем текущую дату -``` - -**Стало:** -```python -except Exception as e: - logger.error(f"Критическая ошибка: некорректная дата '{date_str}': {e}") - raise ValueError( - f"Невозможно сформировать документ с некорректной датой: {date_str}. " - "Проверьте исходные данные." - ) -``` - -**Причина:** -- Подстановка текущей даты вместо корректной - искажение первичного документа -- Риск налоговых проблем (неверный период) -- Теперь генерация прерывается с понятной ошибкой - ---- - -### ⚠️ 4. Счета учёта закомментированы (опционально) - -**Было:** -```python -etree.SubElement(row_elem, "СчетУчета").text = "43" -etree.SubElement(row_elem, "СчетДоходов").text = "90.01.1" -etree.SubElement(row_elem, "СчетРасходов").text = "90.02.1" -etree.SubElement(row_elem, "СчетУчетаНДСПоРеализации").text = "90.03" -``` - -**Стало:** -```python -# ВНИМАНИЕ: Следующие поля могут отсутствовать в XSD некоторых версий 1С -# Если импорт падает - закомментируйте эти строки -etree.SubElement(row_elem, "ТипЗапасов").text = "СобственныеТовары" -# TODO: Сделать опциональными через config -# etree.SubElement(row_elem, "СчетУчета").text = "43" -# etree.SubElement(row_elem, "СчетДоходов").text = "90.01.1" -# etree.SubElement(row_elem, "СчетРасходов").text = "90.02.1" -# etree.SubElement(row_elem, "СчетУчетаНДСПоРеализации").text = "90.03" -``` - -**Причина:** -- Эти поля есть не во всех версиях EnterpriseData -- Зависит от конфигурации 1С клиента -- Оставлен только `ТипЗапасов` (более универсален) - ---- - -## Проблемы, требующие проверки с клиентом - -### ⚠️ 1. Жёстко прошитые GUID справочников - -**Местоположение:** -- Валюта: `72357d18-63a8-11e8-80dd-0050569f5448` (RUB) -- Страна: `67700c1b-63a8-11e8-80dd-0050569f5448` (Россия) -- ЕдиницаИзмерения: `cdf130e2-63a8-11e8-80dd-0050569f5448` (штука) - -**Риск:** -- Эти GUID уникальны для конкретной базы -- В базе клиента могут быть другие GUID -- Импорт завершится ошибкой "Объект не найден" - -**Решения:** -1. **Получить GUID из базы клиента** (запросом к 1С) -2. **Убрать `<Ссылка>`**, передавать только коды (643, 796, RUB) -3. **Включить флаг "создавать отсутствующие объекты"** в настройках обмена - -**Рекомендация:** -```xml - -<Валюта> - <ДанныеКлассификатора> - <Код>643 - <НаименованиеПолное>Российский рубль - <КодЦифровой>643 - <КодСимвольный>RUB - - - - -``` - ---- - -### ⚠️ 2. Структура валюты/банков - -**Текущая структура:** -```xml -<Валюта> - <Ссылка>guid - <ДанныеКлассификатора> - <Код>643 - <Наименование>руб. - - -``` - -**Проблема:** -- Структура может не соответствовать XSD 1.20 клиента -- В разных версиях ED структура отличается - -**TODO:** -- [ ] Получить от клиента актуальный XSD EnterpriseData 1.20 -- [ ] Сверить структуру валюты, банка, страны -- [ ] При необходимости адаптировать - ---- - -### ⚠️ 3. Банк/счёт - некорректные ссылки - -**Текущий код (строка 833):** -```python -bank_elem = etree.SubElement(bank_acc_elem, "Банк") -etree.SubElement(bank_elem, "Ссылка").text = acc_id # ❌ ID счёта вместо ID банка -``` - -**Проблема:** -- Для банка должен быть свой GUID (из справочника "Банки") -- Используется GUID счёта - это неверно - -**Рекомендация:** -```python -# Вариант А: Убрать Ссылку, передавать только БИК -bank_elem = etree.SubElement(bank_acc_elem, "Банк") -# НЕ создавать <Ссылка> -bank_data = etree.SubElement(bank_elem, "ДанныеКлассификатораБанков") -etree.SubElement(bank_data, "БИК").text = acc.get("bic", "") -# 1С найдёт банк по БИК - -# Вариант Б: Получить GUID банка из МойСклад или 1С -bank_id = get_bank_guid_by_bic(acc.get("bic")) -etree.SubElement(bank_elem, "Ссылка").text = bank_id -``` - ---- - -### ⚠️ 4. Договор - сложная структура - -**Текущая структура:** -```xml -<Договор> - <Ссылка>uuid - <ВидДоговора>СПокупателем - <Организация>... - <Контрагент>... - -``` - -**Проблема:** -- Детальная структура может не соответствовать XSD -- Обычно договор - это просто ссылка - -**Варианты:** -1. Если договор УЖЕ существует в базе - передавать только `<Ссылка>` -2. Если нужно создать - выносить в отдельный блок Message -3. Уточнить у клиента бизнес-логику - ---- - -## Что нужно от клиента - -### 1. XSD EnterpriseData 1.20 -- Актуальная схема для их версии 1С -- Версия платформы (8.3.xx.xxxx) -- Конфигурация (УТ, КА, ERP) - -### 2. GUID справочников -**Три варианта:** -- Получить через HTTP-сервис 1С -- Экспортировать из базы вручную -- Использовать коды без GUID (рекомендуется) - -### 3. Тестовая среда -- Доступ к тестовой базе для импорта -- Логи ошибок импорта -- Возможность итерации - -### 4. Бизнес-требования -- Как обрабатывать договоры? (создавать/ссылаться) -- Как обрабатывать банки? (создавать/ссылаться) -- Нужны ли счета учёта в строках? - ---- - -## План тестирования - -### Этап 1: Локальная проверка ✅ -- [x] Копейки сохраняются -- [x] ВидЭД убран -- [x] Дата не подменяется -- [x] Счета учёта закомментированы - -### Этап 2: Проверка с XSD -- [ ] Получить XSD от клиента -- [ ] Валидация по XSD (xmllint или lxml) -- [ ] Исправление несоответствий - -### Этап 3: Импорт в тестовую 1С -- [ ] Импорт тестового XML -- [ ] Проверка ошибок -- [ ] Проверка контрольных сумм -- [ ] Проверка НДС - -### Этап 4: Production -- [ ] Финальные правки -- [ ] Документация для пользователя -- [ ] Мониторинг первых импортов - ---- - -## Риски и митигация - -| Риск | Вероятность | Влияние | Митигация | -|------|------------|---------|-----------| -| GUID не найдены в базе | Высокая | Критично | Убрать GUID, использовать коды | -| Структура не соответствует XSD | Средняя | Критично | Получить XSD, адаптировать | -| Банк/счёт не создаётся | Средняя | Высоко | Использовать БИК без GUID | -| Договор не распознаётся | Низкая | Средне | Упростить до Ссылки | -| Копейки всё ещё теряются | Низкая | Критично | Уже исправлено ✅ | - ---- - -## Выводы - -### ✅ Что сделано: -1. **Копейки сохраняются** - критично для налогового учёта -2. **ВидЭД убран** - больше не ломает валидацию -3. **Даты проверяются** - нет риска искажения -4. **Счета учёта опциональны** - совместимость с разными версиями - -### ⚠️ Что требует внимания: -1. **GUID справочников** - высокий риск, нужен XSD клиента -2. **Структура объектов** - зависит от версии ED -3. **Банк/счёт** - нужна корректная логика - -### 📋 Следующие шаги: -1. Запросить у клиента XSD EnterpriseData 1.20 -2. Протестировать импорт в их тестовой базе -3. Получить обратную связь по структуре -4. Доработать под реальную схему - -**Готовность к продакшену:** ~70% -- Критичные баги исправлены ✅ -- Нужна проверка совместимости с клиентской 1С ⚠️ diff --git a/docs/xml_generator_analysis.md b/docs/xml_generator_analysis.md deleted file mode 100644 index b55cf90..0000000 --- a/docs/xml_generator_analysis.md +++ /dev/null @@ -1,395 +0,0 @@ -# Анализ генератора XML EnterpriseData 1.20 - -## Дата анализа: 6 ноября 2025 г. - -## Проверенные проблемы и их статус - -### ❌ КРИТИЧНО: 1. Жёстко прошитые GUID справочников - -**Местоположение:** -- Строки 773-776 (валюта RUB) -- Строки 878, 1027 (страна Россия) -- Строки 990-994 (единица измерения "шт") - -**Проблема:** -```python -# Валюта - жёстко прошитый GUID -if not currency_id: - currency_id = "72357d18-63a8-11e8-80dd-0050569f5448" # RUB - -# Страна - жёстко прошитый GUID -etree.SubElement(country_elem, "Ссылка").text = "67700c1b-63a8-11e8-80dd-0050569f5448" - -# Единица измерения - жёстко прошитый GUID -if not uom_id: - uom_id = "cdf130e2-63a8-11e8-80dd-0050569f5448" # шт (796) -``` - -**Почему это критично:** -- GUID справочников уникальны для каждой базы 1С -- В другой базе эти GUID не существуют -- Импорт завершится ошибкой "Объект не найден" - -**Рекомендация:** -```python -# ПРАВИЛЬНО: Передавать только код и наименование, без Ссылки -# Либо позволить 1С создать объект автоматически -<Валюта> - - <ДанныеКлассификатора> - <Код>643 - <НаименованиеПолное>Российский рубль - <КодЦифровой>643 - <КодСимвольный>RUB - - -``` - -**Решение:** -1. Либо получать GUID из конфигурации целевой базы -2. Либо использовать флаг "создавать отсутствующие объекты" -3. Либо убрать `<Ссылка>` и передавать только классификаторы - ---- - -### ⚠️ КРИТИЧНО: 2. Структура валюты/банков не соответствует XSD - -**Местоположение:** Строки 767-780 - -**Проблема:** -```python -currency_elem = etree.SubElement(doc, "Валюта") -etree.SubElement(currency_elem, "Ссылка").text = currency_id -currency_data = etree.SubElement(currency_elem, "ДанныеКлассификатора") -etree.SubElement(currency_data, "Код").text = currency.get("isoCode", "643") -``` - -**Почему это проблема:** -- Структура с вложенным `ДанныеКлассификатора` может не соответствовать XSD 1.20 -- В типовых схемах ED часто используется плоская структура: `<Валюта><Ссылка/><Код/><Наименование/>` - -**Рекомендация:** -- **ОБЯЗАТЕЛЬНО** сверить структуру с XSD принимающей системы -- Возможно, нужна плоская структура без `ДанныеКлассификатора` - -**TODO:** -- [ ] Получить актуальный XSD от клиента -- [ ] Проверить версию EnterpriseData в целевой 1С -- [ ] Адаптировать структуру под реальную схему - ---- - -### ❌ КРИТИЧНО: 3. Неверные ссылки на банк/счёт - -**Местоположение:** Строки 825-838 - -**Проблема:** -```python -bank_acc_elem = etree.SubElement(doc, "БанковскийСчетОрганизации") -acc_id = acc.get("id", "") -etree.SubElement(bank_acc_elem, "Ссылка").text = acc_id # ID счёта - -# Банк -bank_elem = etree.SubElement(bank_acc_elem, "Банк") -etree.SubElement(bank_elem, "Ссылка").text = acc_id # ❌ ОШИБКА: используем тот же ID -``` - -**Почему это критично:** -- Для банка должна быть ссылка на объект справочника "Банки" (другой GUID) -- Для счёта - ссылка на объект справочника "БанковскиеСчета" -- Использование одного ID для обоих объектов неверно - -**Рекомендация:** -```python -# ПРАВИЛЬНО: Разные ID для банка и счёта -bank_elem = etree.SubElement(bank_acc_elem, "Банк") -bank_id = acc.get("bank_id") or generate_bank_id_by_bic(acc.get("bic")) -etree.SubElement(bank_elem, "Ссылка").text = bank_id - -# Или передавать без Ссылки, только классификатор -<Банк> - <ДанныеКлассификатораБанков> - <Наименование>Сбербанк - <БИК>044525225 - <КоррСчет>30101810400000000225 - - -``` - -**Решение:** -- Убрать `<Ссылка>` для банка, передавать только БИК и наименование -- 1С сама найдёт/создаст банк по БИК - ---- - -### ⚠️ СРЕДНЕ: 4. Договор внутри "ДанныеВзаиморасчетов" - -**Местоположение:** Строки 887-922 - -**Проблема:** -```python -contract_elem = etree.SubElement(settlement_elem, "Договор") -contract_uuid = str(uuid.uuid5(uuid.NAMESPACE_DNS, f"contract-{cp_id}-{org_id}")) -etree.SubElement(contract_elem, "Ссылка").text = contract_uuid -etree.SubElement(contract_elem, "ВидДоговора").text = "СПокупателем" -# Далее вложенные Организация, Контрагент... -``` - -**Почему это проблема:** -- Детальная структура договора (с Организацией/Контрагентом внутри) может не соответствовать XSD -- В ED обычно ссылка на договор - это просто `<Ссылка>` на существующий объект -- Полное описание договора должно быть отдельной сущностью в Message - -**Рекомендация:** -```xml - -<Договор> - <Ссылка>uuid-договора - - - - - - <Справочник.ДоговорыКонтрагентов> - <Ссылка>uuid - <ВидДоговора>СПокупателем - - - - <Документ.РеализацияТоваровУслуг> - <Договор> - <Ссылка>uuid - - - - -``` - -**TODO:** -- [ ] Уточнить у клиента: договоры уже есть в базе или нужно создавать? -- [ ] Если создавать - вынести договор в отдельный блок Message -- [ ] Если использовать существующие - передавать только Ссылка - ---- - -### ❌ КРИТИЧНО: 5. Потеря копеек - -**Местоположение:** Строки 1012-1014 - -**Проблема:** -```python -price = pos.get("price", 0) / 100 # Конвертируем из копеек -total = price * quantity - -etree.SubElement(row_elem, "Сумма").text = str(int(total)) # ❌ Отбрасываем копейки -etree.SubElement(row_elem, "Цена").text = str(int(price)) # ❌ Отбрасываем копейки -``` - -**Почему это критично:** -- Округление вниз приводит к расхождению сумм -- НДС рассчитывается неверно -- Контрольные суммы не сходятся -- Невозможно сопоставить с УПД/счётом-фактурой - -**Пример:** -``` -Цена: 1234.56 руб → становится 1234 руб (потеря 56 коп) -Количество: 100 шт -Сумма должна быть: 123456.00 руб -Сумма получается: 123400 руб (потеря 56 руб!) -``` - -**Рекомендация:** -```python -# ПРАВИЛЬНО: Сохранять копейки -price = pos.get("price", 0) / 100 -total = price * quantity - -etree.SubElement(row_elem, "Сумма").text = f"{total:.2f}" # 2 знака после запятой -etree.SubElement(row_elem, "Цена").text = f"{price:.2f}" # 2 знака после запятой -``` - -**Решение:** -- Использовать `Decimal` для денежных расчётов -- Форматировать с 2 знаками: `f"{amount:.2f}"` -- НЕ использовать `int()` для денежных сумм - ---- - -### ⚠️ СРЕДНЕ: 6. Лишние поля в строке товара - -**Местоположение:** Строки 1036-1039 - -**Проблема:** -```python -etree.SubElement(row_elem, "ТипЗапасов").text = "СобственныеТовары" -etree.SubElement(row_elem, "СчетУчета").text = "43" -etree.SubElement(row_elem, "СчетДоходов").text = "90.01.1" -etree.SubElement(row_elem, "СчетРасходов").text = "90.02.1" -etree.SubElement(row_elem, "СчетУчетаНДСПоРеализации").text = "90.03" -``` - -**Почему это проблема:** -- Эти поля есть не во всех версиях ED -- Не во всех прикладных решениях 1С используются эти поля -- Если их нет в XSD - валидация провалится - -**Рекомендация:** -- Сделать эти поля опциональными (через настройку) -- Или убрать совсем - 1С подставит по умолчанию -- Или уточнить у клиента, нужны ли эти поля - -**TODO:** -- [ ] Проверить XSD целевой системы -- [ ] Добавить флаг `include_accounting_fields` в конфиг -- [ ] По умолчанию отключить - ---- - -### ⚠️ СРЕДНЕ: 7. ВидЭД внутри ED-документа - -**Местоположение:** Строки 967-968 - -**Проблема:** -```python -# ВидЭД -etree.SubElement(doc, "ВидЭД").text = "ТОРГ12" -``` - -**Почему это проблема:** -- Поле `ВидЭД` со значением "ТОРГ12" не входит в типовую схему ED 1.20 -- Это поле используется в УПД/ФормализованныхДокументах, но не в EnterpriseData -- При валидации по XSD произойдёт ошибка - -**Рекомендация:** -- Убрать это поле из документа ED -- Если нужно указать тип документа - использовать корректное поле из XSD -- Возможно, это поле относится к другому формату (УПД) - -**Решение:** -```python -# УБРАТЬ эту строку -# etree.SubElement(doc, "ВидЭД").text = "ТОРГ12" -``` - ---- - -### ⚠️ НИЗКО: 8. Резервные заглушки дат - -**Местоположение:** Строки 648-657 - -**Проблема:** -```python -def _format_datetime(self, date_str: str) -> str: - try: - dt = datetime.fromisoformat(date_str.replace('Z', '+00:00')) - return dt.strftime("%Y-%m-%dT%H:%M:%S") - except Exception as e: - logger.error(f"Ошибка форматирования даты: {e}") - return datetime.now().strftime("%Y-%m-%dT%H:%M:%S") # ❌ Подставляем текущую дату -``` - -**Почему это проблема:** -- Если дата не парсится - подставляется текущая -- Для первичных документов это искажение данных -- Риск налоговых проблем (неверная дата = неверный период) - -**Рекомендация:** -```python -def _format_datetime(self, date_str: str) -> str: - try: - dt = datetime.fromisoformat(date_str.replace('Z', '+00:00')) - return dt.strftime("%Y-%m-%dT%H:%M:%S") - except Exception as e: - logger.error(f"Критическая ошибка: некорректная дата '{date_str}': {e}") - raise ValueError(f"Невозможно сформировать документ с некорректной датой: {date_str}") -``` - -**Решение:** -- Поднимать исключение при ошибке парсинга даты -- НЕ подставлять резервные значения для критичных полей -- Блокировать генерацию XML с некорректными данными - ---- - -## Приоритеты исправления - -### 🔴 СРОЧНО (блокируют импорт в 1С): -1. ✅ **Потеря копеек** - исправить форматирование денежных сумм -2. ⚠️ **GUID справочников** - убрать жёсткие UUID или получать из базы -3. ⚠️ **Банк/счёт** - исправить структуру ссылок -4. ✅ **ВидЭД** - убрать поле - -### 🟡 ВАЖНО (могут вызвать проблемы): -5. ⚠️ **Структура валюты** - сверить с XSD клиента -6. ⚠️ **Договор** - упростить или вынести в отдельный блок -7. ⚠️ **Счета учёта** - сделать опциональными - -### 🟢 ЖЕЛАТЕЛЬНО (улучшение качества): -8. ✅ **Заглушки дат** - поднимать исключения вместо fallback - ---- - -## Рекомендуемый план действий - -### Этап 1: Критичные исправления (сегодня) -- [ ] Исправить форматирование денежных сумм (добавить `.2f`) -- [ ] Убрать поле `ВидЭД` -- [ ] Заменить fallback даты на исключение - -### Этап 2: Проверка со стороны клиента (завтра) -- [ ] Получить актуальный XSD EnterpriseData 1.20 от клиента -- [ ] Проверить структуру валюты/банков/договоров в реальной схеме -- [ ] Уточнить: нужны ли счета учёта в строках? - -### Этап 3: Адаптация под реальную схему (2-3 дня) -- [ ] Убрать жёсткие GUID, использовать только классификаторы -- [ ] Исправить структуру банка (убрать Ссылку, оставить БИК) -- [ ] Упростить договор или вынести в отдельный блок -- [ ] Сделать счета учёта опциональными - -### Этап 4: Тестирование (1 день) -- [ ] Тестовый импорт в реальную 1С клиента -- [ ] Проверка контрольных сумм -- [ ] Проверка НДС -- [ ] Сопоставление с УПД/счетами-фактурами - ---- - -## Скептическая оценка анализа ИИ - -### ✅ Что верно на 100%: -- Потеря копеек - **КРИТИЧНО**, будет ошибка в суммах -- GUID справочников - **КРИТИЧНО**, не будет работать в другой базе -- ВидЭД - **верно**, этого поля нет в ED -- Заглушки дат - **верно**, риск искажения данных - -### ⚠️ Что нужно проверить: -- Структура валюты - **зависит от версии XSD** (нужен реальный XSD) -- Банк/счёт - **вероятно верно**, но нужна проверка по документации -- Договор - **может быть по-разному** в разных версиях 1С -- Счета учёта - **зависит от конфигурации** 1С клиента - -### 📋 Что точно нужно сделать: -1. Получить от клиента XSD EnterpriseData 1.20 их версии -2. Протестировать импорт в реальной базе -3. Исправить копейки и ВидЭД (это точно ошибки) -4. Остальное - после проверки по реальной схеме - ---- - -## Вывод - -**Генератор работоспособен**, но имеет **критичные проблемы** для продакшена: -- ✅ Структура документа в целом корректна -- ❌ Денежные суммы теряют копейки -- ⚠️ Жёсткие GUID не будут работать в чужой базе -- ⚠️ Структура может не совпадать с XSD клиента - -**Рекомендация:** -1. Срочно исправить копейки и убрать ВидЭД -2. Получить XSD от клиента -3. Протестировать в реальной базе -4. Доработать под реальную схему - -**Риск:** Без тестирования в реальной 1С клиента есть **высокая вероятность** ошибок импорта. diff --git a/gui_app.py b/gui_app.py new file mode 100644 index 0000000..4a2d0c8 --- /dev/null +++ b/gui_app.py @@ -0,0 +1,964 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +GUI приложение для работы с МойСклад +Генерация XML документов для 1С +""" +import sys +import os +import logging +from pathlib import Path +from typing import Optional, List, Dict + +# Настройка логирования +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +from PyQt6.QtWidgets import ( + QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, + QPushButton, QLabel, QLineEdit, QTextEdit, QTableWidget, + QTableWidgetItem, QTabWidget, QFileDialog, QMessageBox, + QProgressBar, QGroupBox, QFormLayout, QComboBox, QSpinBox, + QCheckBox, QDialog, QDialogButtonBox +) +from PyQt6.QtCore import Qt, QThread, pyqtSignal +from PyQt6.QtGui import QFont, QIcon, QPalette, QColor + +# Добавляем путь к modules +sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'modules')) + +from modules.moysklad_client import MoySkladClient +from modules.validator import OrderValidator, ValidationLevel +from modules.xml_generator import XML1CGenerator +from config import settings + + +class ThemeManager: + """Управление системной темой приложения.""" + + LIGHT = "light" + DARK = "dark" + + _original_palette: Optional[QPalette] = None + _original_style: Optional[str] = None + _original_stylesheet: Optional[str] = None + + @staticmethod + def detect_system_theme() -> str: + """Определение системной темы.""" + if sys.platform == "win32": + try: + import winreg # type: ignore + + key_path = ( + r"Software\Microsoft\Windows\CurrentVersion" + r"\Themes\Personalize" + ) + with winreg.OpenKey(winreg.HKEY_CURRENT_USER, key_path) as key: + value, _ = winreg.QueryValueEx(key, "AppsUseLightTheme") + if value == 0: + return ThemeManager.DARK + return ThemeManager.LIGHT + except OSError as error: + logger.warning( + "Не удалось прочитать системную тему Windows: %s", + error, + ) + except ImportError: + logger.warning( + "Модуль winreg недоступен, используется светлая тема" + ) + return ThemeManager.LIGHT + + @classmethod + def apply(cls, app: QApplication) -> str: + """Применение системной темы к приложению.""" + theme = cls.detect_system_theme() + + if cls._original_palette is None: + cls._original_palette = app.palette() + if cls._original_style is None: + cls._original_style = app.style().objectName() + if cls._original_stylesheet is None: + cls._original_stylesheet = app.styleSheet() + + if theme == cls.DARK: + cls._apply_dark_theme(app) + else: + cls._apply_light_theme(app) + + logger.info("Применена системная тема: %s", theme) + return theme + + @staticmethod + def _apply_dark_theme(app: QApplication) -> None: + """Применение темной темы.""" + palette = QPalette() + palette.setColor(QPalette.ColorRole.Window, QColor(37, 37, 38)) + palette.setColor(QPalette.ColorRole.WindowText, Qt.GlobalColor.white) + palette.setColor(QPalette.ColorRole.Base, QColor(30, 30, 30)) + palette.setColor(QPalette.ColorRole.AlternateBase, QColor(45, 45, 45)) + palette.setColor(QPalette.ColorRole.ToolTipBase, Qt.GlobalColor.white) + palette.setColor(QPalette.ColorRole.ToolTipText, Qt.GlobalColor.white) + palette.setColor(QPalette.ColorRole.Text, Qt.GlobalColor.white) + palette.setColor(QPalette.ColorRole.Button, QColor(45, 45, 45)) + palette.setColor(QPalette.ColorRole.ButtonText, Qt.GlobalColor.white) + palette.setColor(QPalette.ColorRole.BrightText, Qt.GlobalColor.red) + palette.setColor(QPalette.ColorRole.Link, QColor(42, 130, 218)) + palette.setColor(QPalette.ColorRole.Highlight, QColor(45, 140, 240)) + palette.setColor( + QPalette.ColorRole.HighlightedText, + Qt.GlobalColor.black, + ) + + palette.setColor( + QPalette.ColorRole.PlaceholderText, + QColor(200, 200, 200, 120), + ) + palette.setColor(QPalette.ColorRole.Shadow, QColor(12, 12, 12)) + + app.setStyle("Fusion") + app.setPalette(palette) + app.setStyleSheet( + "QToolTip {" + " background-color: #2a82da;" + " color: #ffffff;" + " border: 1px solid white;" + " }" + ) + + @classmethod + def _apply_light_theme(cls, app: QApplication) -> None: + """Возврат к светлой теме системы.""" + if cls._original_style: + app.setStyle(cls._original_style) + if cls._original_palette: + app.setPalette(cls._original_palette) + if cls._original_stylesheet is not None: + app.setStyleSheet(cls._original_stylesheet) + + +class WorkerThread(QThread): + """Фоновый поток для долгих операций""" + progress = pyqtSignal(int, str) + finished = pyqtSignal(object) + error = pyqtSignal(str) + + def __init__(self, func, *args, **kwargs): + super().__init__() + self.func = func + self.args = args + self.kwargs = kwargs + + def run(self): + try: + logger.info(f"Запуск фоновой задачи: {self.func.__name__}") + result = self.func(*self.args, **self.kwargs) + logger.info(f"Задача завершена: {self.func.__name__}") + self.finished.emit(result) + except Exception as e: + logger.error( + f"Ошибка в задаче {self.func.__name__}: {e}", + exc_info=True + ) + self.error.emit(str(e)) + + +class LoginDialog(QDialog): + """Диалог авторизации""" + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("Авторизация МойСклад") + self.setModal(True) + self.setMinimumWidth(400) + + layout = QFormLayout() + + self.login_input = QLineEdit() + self.login_input.setPlaceholderText("Логин") + layout.addRow("Логин:", self.login_input) + + self.password_input = QLineEdit() + self.password_input.setPlaceholderText("Пароль") + self.password_input.setEchoMode(QLineEdit.EchoMode.Password) + layout.addRow("Пароль:", self.password_input) + + buttons = QDialogButtonBox( + QDialogButtonBox.StandardButton.Ok | + QDialogButtonBox.StandardButton.Cancel + ) + buttons.accepted.connect(self.accept) + buttons.rejected.connect(self.reject) + layout.addRow(buttons) + + self.setLayout(layout) + + def get_credentials(self): + return self.login_input.text(), self.password_input.text() + + +class MainWindow(QMainWindow): + """Главное окно приложения""" + + def __init__(self): + super().__init__() + self.client: Optional[MoySkladClient] = None + self.validator = OrderValidator() + self.xml_generator = XML1CGenerator() + self.current_orders: List[Dict] = [] + self.selected_order_data: Optional[Dict] = None + self.worker_thread: Optional[WorkerThread] = None + self.current_page = 0 + self.page_size = 25 + self.total_orders = 0 + + self.init_ui() + self.check_credentials() + + def init_ui(self): + """Инициализация UI""" + self.setWindowTitle("МойСклад - Генератор документов") + self.setGeometry(100, 100, 1200, 800) + + # Установка иконки приложения + icon_path = os.path.join(os.path.dirname(__file__), 'app_icon.png') + if os.path.exists(icon_path): + self.setWindowIcon(QIcon(icon_path)) + + # Центральный виджет + central_widget = QWidget() + self.setCentralWidget(central_widget) + + # Главный layout + main_layout = QVBoxLayout() + central_widget.setLayout(main_layout) + + # Строка статуса + status_layout = QHBoxLayout() + self.status_label = QLabel("Не авторизован") + self.status_label.setStyleSheet("color: red; font-weight: bold;") + status_layout.addWidget(self.status_label) + status_layout.addStretch() + + self.login_btn = QPushButton("Войти") + self.login_btn.clicked.connect(self.show_login_dialog) + status_layout.addWidget(self.login_btn) + + self.logout_btn = QPushButton("Выйти") + self.logout_btn.clicked.connect(self.logout) + self.logout_btn.setEnabled(False) + status_layout.addWidget(self.logout_btn) + + main_layout.addLayout(status_layout) + + # Tabs + self.tabs = QTabWidget() + main_layout.addWidget(self.tabs) + + # Вкладка поиска + self.search_tab = self.create_search_tab() + self.tabs.addTab(self.search_tab, "🔍 Поиск") + + # Вкладка списка + self.list_tab = self.create_list_tab() + self.tabs.addTab(self.list_tab, "📋 Список отгрузок") + + # Вкладка массовой генерации + self.batch_tab = self.create_batch_tab() + self.tabs.addTab(self.batch_tab, "📦 Массовая генерация") + + # Progress bar + self.progress_bar = QProgressBar() + self.progress_bar.setVisible(False) + main_layout.addWidget(self.progress_bar) + + def create_search_tab(self) -> QWidget: + """Создание вкладки поиска""" + widget = QWidget() + layout = QVBoxLayout() + + # Поиск + search_group = QGroupBox("Поиск отгрузки") + search_layout = QHBoxLayout() + + self.search_input = QLineEdit() + self.search_input.setPlaceholderText( + "Введите номер отгрузки..." + ) + self.search_input.returnPressed.connect(self.search_orders) + search_layout.addWidget(self.search_input) + + self.search_btn = QPushButton("Найти") + self.search_btn.clicked.connect(self.search_orders) + search_layout.addWidget(self.search_btn) + + search_group.setLayout(search_layout) + layout.addWidget(search_group) + + # Результаты + results_group = QGroupBox("Результаты") + results_layout = QVBoxLayout() + + self.search_results_table = QTableWidget() + self.search_results_table.setColumnCount(5) + self.search_results_table.setHorizontalHeaderLabels([ + "№", "Дата", "Контрагент", "Сумма", "Действия" + ]) + self.search_results_table.horizontalHeader().setStretchLastSection( + True + ) + self.search_results_table.setSelectionBehavior( + QTableWidget.SelectionBehavior.SelectRows + ) + results_layout.addWidget(self.search_results_table) + + results_group.setLayout(results_layout) + layout.addWidget(results_group) + + widget.setLayout(layout) + return widget + + def create_list_tab(self) -> QWidget: + """Создание вкладки списка""" + widget = QWidget() + layout = QVBoxLayout() + + # Кнопки управления + controls_layout = QHBoxLayout() + + self.load_list_btn = QPushButton("Загрузить список") + self.load_list_btn.clicked.connect(self.load_orders_list) + controls_layout.addWidget(self.load_list_btn) + + controls_layout.addStretch() + + self.prev_page_btn = QPushButton("← Предыдущая") + self.prev_page_btn.clicked.connect(self.prev_page) + self.prev_page_btn.setEnabled(False) + controls_layout.addWidget(self.prev_page_btn) + + self.page_label = QLabel("Страница: 1") + controls_layout.addWidget(self.page_label) + + self.next_page_btn = QPushButton("Следующая →") + self.next_page_btn.clicked.connect(self.next_page) + self.next_page_btn.setEnabled(False) + controls_layout.addWidget(self.next_page_btn) + + layout.addLayout(controls_layout) + + # Таблица + self.orders_table = QTableWidget() + self.orders_table.setColumnCount(5) + self.orders_table.setHorizontalHeaderLabels([ + "№", "Дата", "Контрагент", "Сумма", "Действия" + ]) + self.orders_table.horizontalHeader().setStretchLastSection(True) + self.orders_table.setSelectionBehavior( + QTableWidget.SelectionBehavior.SelectRows + ) + layout.addWidget(self.orders_table) + + widget.setLayout(layout) + return widget + + def create_batch_tab(self) -> QWidget: + """Создание вкладки массовой генерации""" + widget = QWidget() + layout = QVBoxLayout() + + # Ввод номеров + input_group = QGroupBox("Номера отгрузок") + input_layout = QVBoxLayout() + + info_label = QLabel( + "Введите номера отгрузок через запятую (максимум 50):" + ) + input_layout.addWidget(info_label) + + self.batch_input = QTextEdit() + self.batch_input.setPlaceholderText( + "Например: 4926,4927,4928" + ) + self.batch_input.setMaximumHeight(100) + input_layout.addWidget(self.batch_input) + + input_group.setLayout(input_layout) + layout.addWidget(input_group) + + # Настройки + settings_group = QGroupBox("Настройки") + settings_layout = QFormLayout() + + self.batch_mode_combo = QComboBox() + self.batch_mode_combo.addItems([ + "Один файл (все документы)", + "Отдельные файлы (каждый документ)" + ]) + settings_layout.addRow("Режим:", self.batch_mode_combo) + + self.validate_checkbox = QCheckBox("Валидировать перед генерацией") + self.validate_checkbox.setChecked(True) + settings_layout.addRow("", self.validate_checkbox) + + settings_group.setLayout(settings_layout) + layout.addWidget(settings_group) + + # Кнопка генерации + self.batch_generate_btn = QPushButton("Сгенерировать") + self.batch_generate_btn.clicked.connect(self.batch_generate) + layout.addWidget(self.batch_generate_btn) + + # Результаты + results_group = QGroupBox("Результаты") + results_layout = QVBoxLayout() + + self.batch_results = QTextEdit() + self.batch_results.setReadOnly(True) + results_layout.addWidget(self.batch_results) + + results_group.setLayout(results_layout) + layout.addWidget(results_group) + + widget.setLayout(layout) + return widget + + def check_credentials(self): + """Проверка сохраненных учетных данных""" + try: + self.client = MoySkladClient( + settings.moysklad_login, + settings.moysklad_password, + settings.moysklad_api_url + ) + # Тестовый запрос + self.client.get_orders(limit=1) + self.update_status(True) + except Exception: + self.update_status(False) + + def show_login_dialog(self): + """Показ диалога авторизации""" + dialog = LoginDialog(self) + if dialog.exec() == QDialog.DialogCode.Accepted: + login, password = dialog.get_credentials() + self.login(login, password) + + def login(self, login: str, password: str): + """Авторизация""" + try: + self.client = MoySkladClient( + login, password, settings.moysklad_api_url + ) + # Тестовый запрос + self.client.get_orders(limit=1) + self.update_status(True) + QMessageBox.information( + self, "Успех", "Авторизация успешна!" + ) + except Exception as e: + QMessageBox.critical( + self, "Ошибка", f"Ошибка авторизации:\n{e}" + ) + self.update_status(False) + + def logout(self): + """Выход""" + self.client = None + self.update_status(False) + self.current_orders = [] + self.orders_table.setRowCount(0) + self.search_results_table.setRowCount(0) + + def update_status(self, logged_in: bool): + """Обновление статуса авторизации""" + if logged_in: + self.status_label.setText("✓ Авторизован") + self.status_label.setStyleSheet( + "color: green; font-weight: bold;" + ) + self.login_btn.setEnabled(False) + self.logout_btn.setEnabled(True) + self.tabs.setEnabled(True) + else: + self.status_label.setText("✗ Не авторизован") + self.status_label.setStyleSheet("color: red; font-weight: bold;") + self.login_btn.setEnabled(True) + self.logout_btn.setEnabled(False) + self.tabs.setEnabled(False) + + def search_orders(self): + """Поиск отгрузок""" + if not self.client: + QMessageBox.warning(self, "Ошибка", "Необходима авторизация") + return + + query = self.search_input.text().strip() + if not query: + QMessageBox.warning(self, "Ошибка", "Введите номер отгрузки") + return + + # Блокируем кнопку на время поиска + self.search_btn.setEnabled(False) + self.show_progress("Поиск...") + + def search(): + try: + return self.client.search_orders(query, limit=10) + except Exception as e: + raise Exception(f"Ошибка поиска: {str(e)}") + + self.run_in_thread(search, self.on_search_complete) + + def on_search_complete(self, result): + """Обработка результатов поиска""" + self.hide_progress() + self.search_btn.setEnabled(True) + + if not result or 'rows' not in result: + QMessageBox.warning( + self, "Ошибка", "Не удалось выполнить поиск" + ) + return + + orders = result.get('rows', []) + if not orders: + QMessageBox.information( + self, "Результаты", "Отгрузки не найдены" + ) + return + + self.display_orders_in_table(orders, self.search_results_table) + + def load_orders_list(self): + """Загрузка списка отгрузок""" + if not self.client: + QMessageBox.warning(self, "Ошибка", "Необходима авторизация") + return + + # Сброс на первую страницу + self.current_page = 0 + self.load_page() + + def load_page(self): + """Загрузка текущей страницы""" + if not self.client: + return + + # Блокируем кнопки на время загрузки + self.load_list_btn.setEnabled(False) + self.prev_page_btn.setEnabled(False) + self.next_page_btn.setEnabled(False) + + offset = self.current_page * self.page_size + self.show_progress( + f"Загрузка страницы {self.current_page + 1}..." + ) + + def load(): + try: + result = self.client.get_orders( + limit=self.page_size, + offset=offset + ) + # Получаем общее количество + total = result.get('meta', {}).get('size', 0) + return result, total + except Exception as e: + raise Exception(f"Ошибка загрузки списка: {str(e)}") + + self.run_in_thread(load, self.on_list_complete) + + def on_list_complete(self, result): + """Обработка списка отгрузок""" + self.hide_progress() + self.load_list_btn.setEnabled(True) + + if not result: + QMessageBox.warning( + self, "Ошибка", "Не удалось загрузить список отгрузок" + ) + return + + # result теперь кортеж (data, total) + data, total = result + + if not data or 'rows' not in data: + QMessageBox.warning( + self, "Ошибка", "Не удалось загрузить список отгрузок" + ) + return + + self.current_orders = data.get('rows', []) + self.total_orders = total + + # Обновляем кнопки пагинации + self.update_pagination_buttons() + + # Отображаем отгрузки + self.display_orders_in_table(self.current_orders, self.orders_table) + + def update_pagination_buttons(self): + """Обновление кнопок пагинации""" + total_pages = ( + (self.total_orders + self.page_size - 1) // self.page_size + ) + + # Обновляем текст страницы + self.page_label.setText( + f"Страница: {self.current_page + 1} из {total_pages} " + f"(всего: {self.total_orders})" + ) + + # Включаем/выключаем кнопки + self.prev_page_btn.setEnabled(self.current_page > 0) + self.next_page_btn.setEnabled( + (self.current_page + 1) * self.page_size < self.total_orders + ) + + def display_orders_in_table(self, orders: List[Dict], table: QTableWidget): + """Отображение отгрузок в таблице""" + table.setRowCount(len(orders)) + + for i, order in enumerate(orders): + # Номер + table.setItem(i, 0, QTableWidgetItem(order.get('name', 'N/A'))) + + # Дата + date_str = order.get('moment', '')[:10] + table.setItem(i, 1, QTableWidgetItem(date_str)) + + # Контрагент + agent = order.get('agent', {}) + table.setItem(i, 2, QTableWidgetItem(agent.get('name', 'N/A'))) + + # Сумма + sum_val = order.get('sum', 0) / 100 + table.setItem(i, 3, QTableWidgetItem(f"{sum_val:,.2f} ₽")) + + # Кнопки действий + actions_widget = QWidget() + actions_layout = QHBoxLayout() + actions_layout.setContentsMargins(2, 2, 2, 2) + + validate_btn = QPushButton("Валидировать") + validate_btn.clicked.connect( + lambda checked, o=order: self.validate_order(o) + ) + actions_layout.addWidget(validate_btn) + + generate_btn = QPushButton("Генерировать XML") + generate_btn.clicked.connect( + lambda checked, o=order: self.generate_xml_for_order(o) + ) + actions_layout.addWidget(generate_btn) + + actions_widget.setLayout(actions_layout) + table.setCellWidget(i, 4, actions_widget) + + def validate_order(self, order: Dict): + """Валидация отгрузки""" + if not self.client: + return + + order_id = order['id'] + self.show_progress("Валидация...") + + def validate(): + order_data = self.client.get_full_order_data(order_id) + results = self.validator.validate_order(order_data) + return order_data, results + + self.run_in_thread(validate, self.on_validate_complete) + + def on_validate_complete(self, result): + """Обработка результатов валидации""" + self.hide_progress() + order_data, validation_results = result + + level, summary = self.validator.get_validation_summary( + validation_results + ) + + if level == ValidationLevel.CRITICAL: + QMessageBox.critical(self, "Ошибки валидации", summary) + elif level == ValidationLevel.WARNING: + QMessageBox.warning(self, "Предупреждения", summary) + else: + QMessageBox.information(self, "Валидация", summary) + + def generate_xml_for_order(self, order: Dict): + """Генерация XML для отгрузки""" + if not self.client: + return + + order_id = order['id'] + self.show_progress("Генерация XML...") + + def generate(): + order_data = self.client.get_full_order_data(order_id) + xml_content = self.xml_generator.generate_realization(order_data) + return order_data, xml_content + + self.run_in_thread(generate, self.on_generate_complete) + + def on_generate_complete(self, result): + """Обработка сгенерированного XML""" + self.hide_progress() + order_data, xml_content = result + + order_name = order_data['order'].get('name', 'order') + filename, _ = QFileDialog.getSaveFileName( + self, + "Сохранить XML", + f"order_{order_name}.xml", + "XML Files (*.xml)" + ) + + if filename: + with open(filename, 'w', encoding='utf-8') as f: + f.write(xml_content) + QMessageBox.information( + self, "Успех", f"XML сохранен:\n{filename}" + ) + + def batch_generate(self): + """Массовая генерация""" + if not self.client: + QMessageBox.warning(self, "Ошибка", "Необходима авторизация") + return + + order_numbers = [ + num.strip() + for num in self.batch_input.toPlainText().split(',') + if num.strip() + ] + + if not order_numbers: + QMessageBox.warning( + self, "Ошибка", "Введите номера отгрузок" + ) + return + + if len(order_numbers) > 50: + QMessageBox.warning( + self, "Ошибка", "Максимум 50 отгрузок за раз" + ) + return + + batch_mode = self.batch_mode_combo.currentIndex() == 0 + validate = self.validate_checkbox.isChecked() + + self.show_progress("Обработка...") + self.batch_results.clear() + + def process(): + return self.process_batch(order_numbers, batch_mode, validate) + + self.run_in_thread(process, self.on_batch_complete) + + def process_batch( + self, + order_numbers: List[str], + batch_mode: bool, + validate: bool + ) -> Dict: + """Обработка пакета отгрузок""" + orders_data = [] + not_found = [] + errors = [] + validation_errors = {} + + for order_num in order_numbers: + try: + # Поиск + search_result = self.client.search_orders(order_num, limit=1) + rows = search_result.get('rows', []) + + if not rows: + not_found.append(order_num) + continue + + order_id = rows[0]['id'] + order_data = self.client.get_full_order_data(order_id) + + # Валидация + if validate: + results = self.validator.validate_order(order_data) + critical = [ + r for r in results + if r.level == ValidationLevel.CRITICAL + ] + + if critical: + validation_errors[order_num] = [ + f"{r.field}: {r.message}" for r in critical + ] + continue + + orders_data.append(order_data) + + except Exception as e: + errors.append(f"{order_num}: {str(e)[:50]}") + + return { + 'orders_data': orders_data, + 'not_found': not_found, + 'errors': errors, + 'validation_errors': validation_errors, + 'batch_mode': batch_mode + } + + def on_batch_complete(self, result): + """Обработка результатов пакетной генерации""" + self.hide_progress() + + orders_data = result['orders_data'] + not_found = result['not_found'] + errors = result['errors'] + validation_errors = result['validation_errors'] + batch_mode = result['batch_mode'] + + # Формируем отчет + report = [] + report.append(f"Обработано: {len(orders_data)} отгрузок") + + if validation_errors: + report.append(f"\n❌ Ошибки валидации: {len(validation_errors)}") + for order_num, errs in validation_errors.items(): + report.append(f"\n {order_num}:") + for err in errs: + report.append(f" • {err}") + + if not_found: + report.append(f"\n⚠️ Не найдено: {', '.join(not_found)}") + + if errors: + report.append("\n⚠️ Ошибки обработки:") + for err in errors: + report.append(f" • {err}") + + self.batch_results.setText('\n'.join(report)) + + # Генерация XML + if orders_data: + if batch_mode: + self.generate_batch_xml(orders_data) + else: + self.generate_separate_xmls(orders_data) + else: + QMessageBox.warning( + self, + "Предупреждение", + "Нет отгрузок для генерации" + ) + + def generate_batch_xml(self, orders_data: List[Dict]): + """Генерация одного XML со всеми документами""" + filename, _ = QFileDialog.getSaveFileName( + self, + "Сохранить пакетный XML", + f"batch_{len(orders_data)}_orders.xml", + "XML Files (*.xml)" + ) + + if filename: + self.xml_generator.generate_batch_realization( + orders_data, filename + ) + QMessageBox.information( + self, "Успех", f"Пакетный XML сохранен:\n{filename}" + ) + + def generate_separate_xmls(self, orders_data: List[Dict]): + """Генерация отдельных XML файлов""" + directory = QFileDialog.getExistingDirectory( + self, "Выберите папку для сохранения" + ) + + if directory: + for order_data in orders_data: + order_name = order_data['order'].get('name', 'order') + filename = os.path.join( + directory, f"order_{order_name}.xml" + ) + xml_content = self.xml_generator.generate_realization( + order_data + ) + with open(filename, 'w', encoding='utf-8') as f: + f.write(xml_content) + + QMessageBox.information( + self, + "Успех", + f"Сохранено {len(orders_data)} файлов в:\n{directory}" + ) + + def run_in_thread(self, func, callback): + """Запуск функции в отдельном потоке""" + # Останавливаем предыдущий поток если есть + if self.worker_thread and self.worker_thread.isRunning(): + self.worker_thread.wait() + + self.worker_thread = WorkerThread(func) + self.worker_thread.finished.connect(callback) + self.worker_thread.error.connect(self.on_thread_error) + self.worker_thread.start() + + def on_thread_error(self, error_msg: str): + """Обработка ошибки в потоке""" + self.hide_progress() + # Разблокируем кнопки + self.search_btn.setEnabled(True) + self.load_list_btn.setEnabled(True) + + QMessageBox.critical(self, "Ошибка", error_msg) + + def show_progress(self, message: str): + """Показ прогресса""" + self.progress_bar.setVisible(True) + self.progress_bar.setRange(0, 0) # Неопределенный прогресс + self.statusBar().showMessage(message) + + def hide_progress(self): + """Скрытие прогресса""" + self.progress_bar.setVisible(False) + self.statusBar().clearMessage() + + def prev_page(self): + """Предыдущая страница""" + if self.current_page > 0: + self.current_page -= 1 + self.load_page() + + def next_page(self): + """Следующая страница""" + if (self.current_page + 1) * self.page_size < self.total_orders: + self.current_page += 1 + self.load_page() + + +def main(): + """Запуск приложения""" + app = QApplication(sys.argv) + app.setApplicationName("МойСклад - Генератор документов") + app.setOrganizationName("BUH_bot") + + # Установка иконки приложения для всех окон + icon_path = os.path.join(os.path.dirname(__file__), 'app_icon.png') + if os.path.exists(icon_path): + app.setWindowIcon(QIcon(icon_path)) + + ThemeManager.apply(app) + + window = MainWindow() + window.show() + + sys.exit(app.exec()) + + +if __name__ == "__main__": + main() diff --git a/gui_app.spec b/gui_app.spec new file mode 100644 index 0000000..5d383de --- /dev/null +++ b/gui_app.spec @@ -0,0 +1,75 @@ +# -*- mode: python ; coding: utf-8 -*- +""" +PyInstaller spec file для сборки GUI приложения +Создает standalone exe-файл со всеми зависимостями +""" +import os + +block_cipher = None + +# Дополнительные данные для включения +added_files = [ + ('modules/*.py', 'modules'), + ('.env.example', '.'), + ('app_icon.png', '.'), +] + +# Скрытые импорты +hiddenimports = [ + 'PyQt6.QtCore', + 'PyQt6.QtGui', + 'PyQt6.QtWidgets', + 'lxml.etree', + 'lxml._elementpath', + 'requests', + 'cryptography', + 'cryptography.fernet', +] + +a = Analysis( + ['gui_app.py'], + pathex=[], + binaries=[], + datas=added_files, + hiddenimports=hiddenimports, + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[ + 'telegram', + 'telegram.ext', + 'matplotlib', + 'numpy', + 'pandas', + 'tkinter', + ], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher, + noarchive=False, +) + +pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + [], + name='MoySkladGenerator', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=False, # Без консоли + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, + icon='app_icon.ico' if os.path.exists('app_icon.ico') else None, +) diff --git a/installer.iss b/installer.iss new file mode 100644 index 0000000..ff5fd4e --- /dev/null +++ b/installer.iss @@ -0,0 +1,83 @@ +; Inno Setup Script для установки MoySklad Generator +; Создает автономный установщик со всеми зависимостями + +#define MyAppName "МойСклад - Генератор документов" +#define MyAppVersion "1.0.0" +#define MyAppPublisher "BUH_bot" +#define MyAppExeName "MoySkladGenerator.exe" + +[Setup] +AppId={{B7A8D2C1-3F4E-4A5B-9C6D-1E2F3A4B5C6D} +AppName={#MyAppName} +AppVersion={#MyAppVersion} +AppPublisher={#MyAppPublisher} +DefaultDirName={autopf}\{#MyAppName} +DefaultGroupName={#MyAppName} +AllowNoIcons=yes +OutputDir=installer_output +OutputBaseFilename=MoySklad_Generator_Setup +SetupIconFile=app_icon.ico +UninstallDisplayIcon={app}\{#MyAppExeName} +Compression=lzma2/max +SolidCompression=yes +WizardStyle=modern +DisableWelcomePage=no +LicenseFile=LICENSE.txt +InfoBeforeFile=INSTALL_INFO.txt +PrivilegesRequired=admin + +[Languages] +Name: "russian"; MessagesFile: "compiler:Languages\Russian.isl" +Name: "english"; MessagesFile: "compiler:Default.isl" + +[Tasks] +Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}" +Name: "quicklaunchicon"; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked + +[Files] +; Основной исполняемый файл +Source: "dist\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion + +; Дополнительные файлы +Source: "README.md"; DestDir: "{app}"; Flags: ignoreversion +Source: ".env.example"; DestDir: "{app}"; Flags: ignoreversion +Source: "CHANGELOG.md"; DestDir: "{app}"; Flags: ignoreversion + +; Документация +Source: "docs\*"; DestDir: "{app}\docs"; Flags: ignoreversion recursesubdirs createallsubdirs + +[Icons] +Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" +Name: "{group}\Документация"; Filename: "{app}\docs" +Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}" +Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon +Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: quicklaunchicon + +[Run] +Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent + +[Code] +procedure InitializeWizard; +var + WelcomeLabel: TNewStaticText; +begin + WelcomeLabel := TNewStaticText.Create(WizardForm); + WelcomeLabel.Parent := WizardForm.WelcomePage; + WelcomeLabel.Caption := + 'Это приложение для генерации XML документов из МойСклад.' + #13#10 + + 'Работает полностью автономно, без подключения к интернету.' + #13#10#13#10 + + 'Возможности:' + #13#10 + + ' • Поиск и просмотр отгрузок' + #13#10 + + ' • Валидация данных' + #13#10 + + ' • Генерация XML для 1С' + #13#10 + + ' • Массовая генерация документов'; + WelcomeLabel.Top := WizardForm.WelcomeLabel2.Top + WizardForm.WelcomeLabel2.Height + 20; + WelcomeLabel.Width := WizardForm.WelcomeLabel2.Width; + WelcomeLabel.AutoSize := True; +end; + +function InitializeSetup(): Boolean; +begin + Result := True; + // Здесь можно добавить проверки системных требований +end; diff --git a/modules/validator.py b/modules/validator.py index 841026f..dffb625 100644 --- a/modules/validator.py +++ b/modules/validator.py @@ -387,7 +387,7 @@ class OrderValidator: level=ValidationLevel.CRITICAL, field=f"position[{index}].price", message=f"Позиция {pos_num} ({name or 'без названия'}): " - f"цена равна нулю (недопустимо для УПД/ТОРГ-12)" + f"цена равна нулю" )) # Проверка суммы строки (если есть цена и количество) diff --git a/modules/xml_generator.py b/modules/xml_generator.py index f91cc9b..2a5ba4d 100644 --- a/modules/xml_generator.py +++ b/modules/xml_generator.py @@ -4,12 +4,64 @@ """ from lxml import etree from datetime import datetime -from typing import Dict, Optional +from typing import Dict, List, Optional, Tuple import logging +from config import settings + logger = logging.getLogger(__name__) +# Константы для российских кодов и идентификаторов +RUB_ALPHA_CODE = "RUB" +RUB_NUMERIC_CODE = "643" +RUB_FULL_NAME = "Российский рубль" +RUB_SHORT_NAME = "руб." +RUB_CURRENCY_GUID = "72357d18-63a8-11e8-80dd-0050569f5448" + +RUSSIA_COUNTRY_GUID = "67700c1b-63a8-11e8-80dd-0050569f5448" +RUSSIA_OKSM_CODE = "643" +RUSSIA_COUNTRY_NAME = "РОССИЯ" + +DEFAULT_UOM_GUID = "cdf130e2-63a8-11e8-80dd-0050569f5448" + +ACCOUNTING_ACCOUNT_TAGS = ( + ("СчетУчета", "43"), + ("СчетДоходов", "90.01.1"), + ("СчетРасходов", "90.02.1"), + ("СчетУчетаНДСПоРеализации", "90.03"), +) + + +def get_currency_codes(currency: Optional[Dict]) -> Tuple[str, str]: + """Возвращает буквенный и цифровой коды валюты. + + При отсутствии данных возвращаются значения по умолчанию. + """ + if not currency: + return RUB_ALPHA_CODE, RUB_NUMERIC_CODE + + iso_value = currency.get("isoCode") + if isinstance(iso_value, str): + alpha_code = iso_value.strip().upper() + else: + alpha_code = "" + + numeric_value = currency.get("code") + if isinstance(numeric_value, str): + numeric_code = numeric_value.strip() + else: + numeric_code = "" + + if not alpha_code and numeric_code == RUB_NUMERIC_CODE: + alpha_code = RUB_ALPHA_CODE + + if not numeric_code and alpha_code == RUB_ALPHA_CODE: + numeric_code = RUB_NUMERIC_CODE + + return alpha_code or RUB_ALPHA_CODE, numeric_code or RUB_NUMERIC_CODE + + class XMLGenerator: """Генератор XML документов в формате ВерсФорм 5.03""" @@ -47,10 +99,62 @@ class XMLGenerator: Сумма в рублях с 2 знаками после запятой """ return f"{amount / 100:.2f}" + + @staticmethod + def _get_preferred_address(data: Dict, fields: List[str]) -> str: + """Возвращает первый непустой адрес по списку полей.""" + for field in fields: + value = data.get(field) + if value: + return value + return "" + + def _add_bank_details( + self, + parent: etree.Element, + accounts: Optional[List[Dict]] + ) -> None: + """Добавляет банковские реквизиты (первый счет с БИК).""" + if not accounts: + return + + for account in accounts: + if not account.get("bic"): + continue + + bank_info = etree.SubElement( + parent, + "БанкРекв", + НомерСчета=account.get("accountNumber", "") + ) + etree.SubElement( + bank_info, + "СвБанк", + НаимБанк=account.get("bankName", ""), + БИК=account.get("bic", ""), + КорСчет=account.get("correspondentAccount", "") + ) + break + + @staticmethod + def _add_contact_details( + parent: etree.Element, + phone: Optional[str] = None, + email: Optional[str] = None + ) -> None: + """Добавляет контактную информацию, если она есть.""" + if not phone and not email: + return + + contact = etree.SubElement(parent, "Контакт") + if phone: + etree.SubElement(contact, "Тлф").text = phone + if email: + etree.SubElement(contact, "ЭлПочта").text = email def _add_seller_info( - self, - parent: etree.Element, + self, + parent: etree.Element, organization: Dict, accounts: list = None ) -> None: @@ -100,7 +204,7 @@ class XMLGenerator: else: # Юридическое лицо kpp = organization.get("kpp", "") - sv_ul = etree.SubElement( + _ = etree.SubElement( seller_info, "СвЮЛУч", НаимОрг=organization.get("name", ""), ИННЮЛ=inn, @@ -117,31 +221,14 @@ class XMLGenerator: ) # Банковские реквизиты - if accounts and len(accounts) > 0: - # Берём первый счёт с БИК - for acc in accounts: - if acc.get('bic'): - bank_info = etree.SubElement( - seller, "БанкРекв", - НомерСчета=acc.get('accountNumber', '') - ) - etree.SubElement( - bank_info, "СвБанк", - НаимБанк=acc.get('bankName', ''), - БИК=acc.get('bic', ''), - КорСчет=acc.get('correspondentAccount', '') - ) - break - + self._add_bank_details(seller, accounts) + # Контактная информация - phone = organization.get("phone", "") - email = organization.get("email", "") - if phone or email: - contact = etree.SubElement(seller, "Контакт") - if phone: - etree.SubElement(contact, "Тлф").text = phone - if email: - etree.SubElement(contact, "ЭлПочта").text = email + self._add_contact_details( + seller, + phone=organization.get("phone", ""), + email=organization.get("email", "") + ) def _add_shipper_info( self, @@ -203,10 +290,9 @@ class XMLGenerator: etree.SubElement(shipper_info, "НаимОрг", Наим=name) # Адрес (приоритет: deliveryAddress -> actualAddress -> legalAddress) - address = ( - counterparty.get("deliveryAddress") or - counterparty.get("actualAddress") or - counterparty.get("legalAddress", "") + address = self._get_preferred_address( + counterparty, + ["deliveryAddress", "actualAddress", "legalAddress"] ) if address: addr_element = etree.SubElement(shipper, "Адрес") @@ -216,26 +302,13 @@ class XMLGenerator: ) # Банковские реквизиты - if accounts and len(accounts) > 0: - for acc in accounts: - if acc.get('bic'): - bank_info = etree.SubElement( - shipper, "БанкРекв", - НомерСчета=acc.get('accountNumber', '') - ) - etree.SubElement( - bank_info, "СвБанк", - НаимБанк=acc.get('bankName', ''), - БИК=acc.get('bic', ''), - КорСчет=acc.get('correspondentAccount', '') - ) - break + self._add_bank_details(shipper, accounts) # Контактная информация - phone = counterparty.get("phone", "") - if phone: - contact = etree.SubElement(shipper, "Контакт") - etree.SubElement(contact, "Тлф").text = phone + self._add_contact_details( + shipper, + phone=counterparty.get("phone", "") + ) def _add_buyer_info( self, @@ -296,8 +369,11 @@ class XMLGenerator: name = counterparty.get("legalTitle", "") etree.SubElement(buyer_info, "НаимОрг", Наим=name) - # Адрес (для покупателя используем юридический адрес) - address = counterparty.get("legalAddress") or counterparty.get("actualAddress", "") + # Адрес (для покупателя: сначала юридический, затем фактический) + address = self._get_preferred_address( + counterparty, + ["legalAddress", "actualAddress"] + ) if address: addr_element = etree.SubElement(buyer, "Адрес") etree.SubElement( @@ -306,35 +382,18 @@ class XMLGenerator: ) # Банковские реквизиты - if accounts and len(accounts) > 0: - # Берём первый счёт с БИК - for acc in accounts: - if acc.get('bic'): - bank_info = etree.SubElement( - buyer, "БанкРекв", - НомерСчета=acc.get('accountNumber', '') - ) - etree.SubElement( - bank_info, "СвБанк", - НаимБанк=acc.get('bankName', ''), - БИК=acc.get('bic', ''), - КорСчет=acc.get('correspondentAccount', '') - ) - break + self._add_bank_details(buyer, accounts) # Контактная информация - phone = counterparty.get("phone", "") - email = counterparty.get("email", "") - if phone or email: - contact = etree.SubElement(buyer, "Контакт") - if phone: - etree.SubElement(contact, "Тлф").text = phone - if email: - etree.SubElement(contact, "ЭлПочта").text = email + self._add_contact_details( + buyer, + phone=counterparty.get("phone", ""), + email=counterparty.get("email", "") + ) def _add_positions_table( - self, - parent: etree.Element, + self, + parent: etree.Element, positions: list ) -> Dict[str, float]: """ @@ -432,8 +491,8 @@ class XMLGenerator: } def generate_upd( - self, - order_data: Dict, + self, + order_data: Dict, output_path: Optional[str] = None ) -> str: """ @@ -501,14 +560,16 @@ class XMLGenerator: rate = order.get("rate", {}) if rate and "currency" in rate: currency = rate["currency"] + _, currency_numeric_code = get_currency_codes(currency) etree.SubElement( - invoice_info, "ДенИзм", - КодОКВ=currency.get("code", "643"), - НаимОКВ=currency.get("fullName", "Российский рубль") + invoice_info, + "ДенИзм", + КодОКВ=currency_numeric_code, + НаимОКВ=currency.get("fullName", RUB_FULL_NAME) ) # Таблица товаров - totals = self._add_positions_table(doc, positions) + _ = self._add_positions_table(doc, positions) # Сведения о передаче товара sv_prod_per = etree.SubElement(doc, "СвПродПер") @@ -607,7 +668,7 @@ class XMLGenerator: self._add_buyer_info(doc, counterparty) # Таблица товаров - totals = self._add_positions_table(doc, positions) + _ = self._add_positions_table(doc, positions) # Конвертация в строку xml_string = etree.tostring( @@ -778,16 +839,25 @@ class XML1CGenerator: rate = order.get("rate", {}) currency = rate.get("currency", {}) if rate else {} currency_elem = etree.SubElement(doc, "Валюта") - currency_href = currency.get("meta", {}).get("href", "") if currency.get("meta") else "" - currency_id = currency_href.split("/")[-1].split("?")[0] if currency_href else "" - # Если нет UUID, используем стандартный UUID для рубля + + currency_href = "" + if currency.get("meta"): + currency_href = currency.get("meta", {}).get("href", "") + + currency_id = "" + if currency_href: + currency_id = currency_href.split("/")[-1].split("?")[0] if not currency_id: - currency_id = "72357d18-63a8-11e8-80dd-0050569f5448" # Стандартный GUID для RUB + currency_id = RUB_CURRENCY_GUID etree.SubElement(currency_elem, "Ссылка").text = currency_id - + + alpha_code, numeric_code = get_currency_codes(currency) currency_data = etree.SubElement(currency_elem, "ДанныеКлассификатора") - etree.SubElement(currency_data, "Код").text = currency.get("isoCode", "643") - etree.SubElement(currency_data, "Наименование").text = currency.get("name", "руб.") + etree.SubElement(currency_data, "Код").text = numeric_code + etree.SubElement( + currency_data, + "Наименование" + ).text = currency.get("name", RUB_SHORT_NAME) # Сумма (в рублях, без копеек) order_sum = order.get("sum", 0) @@ -886,11 +956,16 @@ class XML1CGenerator: # Страна регистрации (ПЕРЕД ИндивидуальныйПредприниматель!) country_elem = etree.SubElement(cp_elem, "СтранаРегистрации") - # UUID России из классификатора 1С (стандартный для всех баз) - etree.SubElement(country_elem, "Ссылка").text = "67700c1b-63a8-11e8-80dd-0050569f5448" - country_data = etree.SubElement(country_elem, "ДанныеКлассификатора") - etree.SubElement(country_data, "Код").text = "643" - etree.SubElement(country_data, "Наименование").text = "РОССИЯ" + etree.SubElement(country_elem, "Ссылка").text = RUSSIA_COUNTRY_GUID + country_data = etree.SubElement( + country_elem, + "ДанныеКлассификатора" + ) + etree.SubElement(country_data, "Код").text = RUSSIA_OKSM_CODE + etree.SubElement( + country_data, + "Наименование" + ).text = RUSSIA_COUNTRY_NAME # ИндивидуальныйПредприниматель только для ИП (после СтранаРегистрации) if cp_type == "entrepreneur": @@ -908,15 +983,19 @@ class XML1CGenerator: # - валюта → 62.21/62.22 (валютные расчёты) # - у.е. → 62.31/62.32 (расчёты в у.е.) import uuid - currency_code = currency.get("isoCode", "643") + currency_code_for_seed = alpha_code or numeric_code is_ue = "false" # В текущей реализации всегда false - seed = f"contract-{cp_id}-{org_id}-CUR-{currency_code}-UE-{is_ue}" + seed = ( + f"contract-{cp_id}-{org_id}-CUR-" + f"{currency_code_for_seed}-UE-{is_ue}" + ) contract_uuid = str(uuid.uuid5(uuid.NAMESPACE_DNS, seed)) logger.info( f"Создан договор с учётом валюты: " f"контрагент={cp_id}, организация={org_id}, " - f"валюта={currency_code}, у.е.={is_ue}, UUID={contract_uuid}" + f"валюта={alpha_code}/{numeric_code}, " + f"у.е.={is_ue}, UUID={contract_uuid}" ) etree.SubElement(contract_elem, "Ссылка").text = contract_uuid etree.SubElement(contract_elem, "ВидДоговора").text = "СПокупателем" @@ -954,11 +1033,16 @@ class XML1CGenerator: # Страна регистрации (ПЕРЕД ИндивидуальныйПредприниматель!) cp_country = etree.SubElement(contract_cp, "СтранаРегистрации") - # UUID России из классификатора 1С (стандартный для всех баз) - etree.SubElement(cp_country, "Ссылка").text = "67700c1b-63a8-11e8-80dd-0050569f5448" - cp_country_data = etree.SubElement(cp_country, "ДанныеКлассификатора") - etree.SubElement(cp_country_data, "Код").text = "643" - etree.SubElement(cp_country_data, "Наименование").text = "РОССИЯ" + etree.SubElement(cp_country, "Ссылка").text = RUSSIA_COUNTRY_GUID + cp_country_data = etree.SubElement( + cp_country, + "ДанныеКлассификатора" + ) + etree.SubElement(cp_country_data, "Код").text = RUSSIA_OKSM_CODE + etree.SubElement( + cp_country_data, + "Наименование" + ).text = RUSSIA_COUNTRY_NAME # ИндивидуальныйПредприниматель только для ИП (после СтранаРегистрации) if cp_type == "entrepreneur": @@ -967,19 +1051,34 @@ class XML1CGenerator: # Валюта взаиморасчетов в договоре contract_currency = etree.SubElement(contract_elem, "ВалютаВзаиморасчетов") etree.SubElement(contract_currency, "Ссылка").text = currency_id - contract_curr_data = etree.SubElement(contract_currency, "ДанныеКлассификатора") - etree.SubElement(contract_curr_data, "Код").text = currency.get("isoCode", "643") - etree.SubElement(contract_curr_data, "Наименование").text = currency.get("name", "руб.") + contract_curr_data = etree.SubElement( + contract_currency, + "ДанныеКлассификатора" + ) + etree.SubElement(contract_curr_data, "Код").text = numeric_code + etree.SubElement( + contract_curr_data, + "Наименование" + ).text = currency.get("name", RUB_SHORT_NAME) etree.SubElement(contract_elem, "РасчетыВУсловныхЕдиницах").text = "false" etree.SubElement(contract_elem, "Наименование").text = "Основной договор" # Валюта взаиморасчетов - settlement_currency = etree.SubElement(settlement_elem, "ВалютаВзаиморасчетов") + settlement_currency = etree.SubElement( + settlement_elem, + "ВалютаВзаиморасчетов" + ) etree.SubElement(settlement_currency, "Ссылка").text = currency_id - settlement_curr_data = etree.SubElement(settlement_currency, "ДанныеКлассификатора") - etree.SubElement(settlement_curr_data, "Код").text = currency.get("isoCode", "643") - etree.SubElement(settlement_curr_data, "Наименование").text = currency.get("name", "руб.") + settlement_curr_data = etree.SubElement( + settlement_currency, + "ДанныеКлассификатора" + ) + etree.SubElement(settlement_curr_data, "Код").text = numeric_code + etree.SubElement( + settlement_curr_data, + "Наименование" + ).text = currency.get("name", RUB_SHORT_NAME) etree.SubElement(settlement_elem, "КурсВзаиморасчетов").text = "1" etree.SubElement(settlement_elem, "КратностьВзаиморасчетов").text = "1" @@ -1045,10 +1144,16 @@ class XML1CGenerator: vat_country = etree.SubElement(vat_elem, "Страна") # Используем стандартный UUID России из классификатора 1С - etree.SubElement(vat_country, "Ссылка").text = "67700c1b-63a8-11e8-80dd-0050569f5448" - vat_country_data = etree.SubElement(vat_country, "ДанныеКлассификатора") - etree.SubElement(vat_country_data, "Код").text = "643" - etree.SubElement(vat_country_data, "Наименование").text = "РОССИЯ" + etree.SubElement(vat_country, "Ссылка").text = RUSSIA_COUNTRY_GUID + vat_country_data = etree.SubElement( + vat_country, + "ДанныеКлассификатора" + ) + etree.SubElement(vat_country_data, "Код").text = RUSSIA_OKSM_CODE + etree.SubElement( + vat_country_data, + "Наименование" + ).text = RUSSIA_COUNTRY_NAME # СуммаНДС (извлекаем из суммы) vat_amount = total / (1 + vat_rate / 100) * (vat_rate / 100) if vat_rate else 0 @@ -1058,11 +1163,9 @@ class XML1CGenerator: # Если импорт падает - закомментируйте эти строки # Дополнительные поля (опционально, зависит от конфигурации 1С) etree.SubElement(row_elem, "ТипЗапасов").text = "СобственныеТовары" - # TODO: Сделать опциональными через config - # etree.SubElement(row_elem, "СчетУчета").text = "43" - # etree.SubElement(row_elem, "СчетДоходов").text = "90.01.1" - # etree.SubElement(row_elem, "СчетРасходов").text = "90.02.1" - # etree.SubElement(row_elem, "СчетУчетаНДСПоРеализации").text = "90.03" + if settings.include_accounting_accounts: + for tag, value in ACCOUNTING_ACCOUNT_TAGS: + etree.SubElement(row_elem, tag).text = value # Уникальный идентификатор строки (uuid уже импортирован выше) etree.SubElement(row_elem, "ИдентификаторСтроки").text = str(uuid.uuid4()) @@ -1090,7 +1193,9 @@ class XML1CGenerator: # Если валюта RUB и не у.е., 1С должна использовать счета 62.01/62.02 # Если валюта иностранная → 62.21/62.22 # Если у.е. → 62.31/62.32 - if currency_code == "643" or currency_code == "RUB": + currency_label = f"{alpha_code}/{numeric_code}" + + if numeric_code == RUB_NUMERIC_CODE: logger.info( "✓ Документ в рублях (RUB, код 643), не в у.е. " "→ ожидаются счета 62.01 (задолженность) / 62.02 (авансы). " @@ -1102,10 +1207,10 @@ class XML1CGenerator: ) else: logger.info( - f"⚠ Документ в иностранной валюте ({currency_code}) " + f"⚠ Документ в иностранной валюте ({currency_label}) " "→ ожидаются счета 62.21/62.22. " "Убедитесь, что договор создан с валютой взаиморасчётов " - f"= {currency_code}." + f"= {currency_label}." ) # Сохранение в файл @@ -1289,18 +1394,17 @@ class XML1CGenerator: if currency_href else "" ) if not currency_id: - currency_id = "72357d18-63a8-11e8-80dd-0050569f5448" + currency_id = RUB_CURRENCY_GUID etree.SubElement(currency_elem, "Ссылка").text = currency_id - + + alpha_code, numeric_code = get_currency_codes(currency) currency_data = etree.SubElement( currency_elem, "ДанныеКлассификатора" ) - etree.SubElement( - currency_data, "Код" - ).text = currency.get("isoCode", "643") + etree.SubElement(currency_data, "Код").text = numeric_code etree.SubElement( currency_data, "Наименование" - ).text = currency.get("name", "руб.") + ).text = currency.get("name", RUB_SHORT_NAME) # Сумма order_sum = order.get("sum", 0) @@ -1438,14 +1542,16 @@ class XML1CGenerator: ).text = "ЮридическоеЛицо" country_elem = etree.SubElement(cp_elem, "СтранаРегистрации") - etree.SubElement( - country_elem, "Ссылка" - ).text = "67700c1b-63a8-11e8-80dd-0050569f5448" + etree.SubElement(country_elem, "Ссылка").text = RUSSIA_COUNTRY_GUID country_data = etree.SubElement( - country_elem, "ДанныеКлассификатора" + country_elem, + "ДанныеКлассификатора" ) - etree.SubElement(country_data, "Код").text = "643" - etree.SubElement(country_data, "Наименование").text = "РОССИЯ" + etree.SubElement(country_data, "Код").text = RUSSIA_OKSM_CODE + etree.SubElement( + country_data, + "Наименование" + ).text = RUSSIA_COUNTRY_NAME if cp_type == "entrepreneur": etree.SubElement( @@ -1458,9 +1564,12 @@ class XML1CGenerator: # Договор contract_elem = etree.SubElement(settlement_elem, "Договор") import uuid - currency_code = currency.get("isoCode", "643") + currency_code_for_seed = alpha_code or numeric_code is_ue = "false" - seed = f"contract-{cp_id}-{org_id}-CUR-{currency_code}-UE-{is_ue}" + seed = ( + f"contract-{cp_id}-{org_id}-CUR-" + f"{currency_code_for_seed}-UE-{is_ue}" + ) contract_uuid = str(uuid.uuid5(uuid.NAMESPACE_DNS, seed)) etree.SubElement(contract_elem, "Ссылка").text = contract_uuid @@ -1525,14 +1634,16 @@ class XML1CGenerator: ).text = "ЮридическоеЛицо" cp_country = etree.SubElement(contract_cp, "СтранаРегистрации") - etree.SubElement( - cp_country, "Ссылка" - ).text = "67700c1b-63a8-11e8-80dd-0050569f5448" + etree.SubElement(cp_country, "Ссылка").text = RUSSIA_COUNTRY_GUID cp_country_data = etree.SubElement( - cp_country, "ДанныеКлассификатора" + cp_country, + "ДанныеКлассификатора" ) - etree.SubElement(cp_country_data, "Код").text = "643" - etree.SubElement(cp_country_data, "Наименование").text = "РОССИЯ" + etree.SubElement(cp_country_data, "Код").text = RUSSIA_OKSM_CODE + etree.SubElement( + cp_country_data, + "Наименование" + ).text = RUSSIA_COUNTRY_NAME if cp_type == "entrepreneur": etree.SubElement( @@ -1547,12 +1658,11 @@ class XML1CGenerator: contract_curr_data = etree.SubElement( contract_currency, "ДанныеКлассификатора" ) + etree.SubElement(contract_curr_data, "Код").text = numeric_code etree.SubElement( - contract_curr_data, "Код" - ).text = currency.get("isoCode", "643") - etree.SubElement( - contract_curr_data, "Наименование" - ).text = currency.get("name", "руб.") + contract_curr_data, + "Наименование" + ).text = currency.get("name", RUB_SHORT_NAME) etree.SubElement( contract_elem, "РасчетыВУсловныхЕдиницах" @@ -1569,12 +1679,11 @@ class XML1CGenerator: settlement_curr_data = etree.SubElement( settlement_currency, "ДанныеКлассификатора" ) + etree.SubElement(settlement_curr_data, "Код").text = numeric_code etree.SubElement( - settlement_curr_data, "Код" - ).text = currency.get("isoCode", "643") - etree.SubElement( - settlement_curr_data, "Наименование" - ).text = currency.get("name", "руб.") + settlement_curr_data, + "Наименование" + ).text = currency.get("name", RUB_SHORT_NAME) etree.SubElement( settlement_elem, "КурсВзаиморасчетов" @@ -1636,7 +1745,7 @@ class XML1CGenerator: if uom_href else "" ) if not uom_id: - uom_id = "cdf130e2-63a8-11e8-80dd-0050569f5448" + uom_id = DEFAULT_UOM_GUID etree.SubElement(uom_elem, "Ссылка").text = uom_id uom_data = etree.SubElement(uom_elem, "ДанныеКлассификатора") @@ -1664,12 +1773,15 @@ class XML1CGenerator: vat_country = etree.SubElement(vat_elem, "Страна") etree.SubElement( vat_country, "Ссылка" - ).text = "67700c1b-63a8-11e8-80dd-0050569f5448" + ).text = RUSSIA_COUNTRY_GUID vat_country_data = etree.SubElement( vat_country, "ДанныеКлассификатора" ) - etree.SubElement(vat_country_data, "Код").text = "643" - etree.SubElement(vat_country_data, "Наименование").text = "РОССИЯ" + etree.SubElement(vat_country_data, "Код").text = RUSSIA_OKSM_CODE + etree.SubElement( + vat_country_data, + "Наименование" + ).text = RUSSIA_COUNTRY_NAME vat_amount = ( total / (1 + vat_rate / 100) * (vat_rate / 100) @@ -1682,6 +1794,9 @@ class XML1CGenerator: etree.SubElement( row_elem, "ТипЗапасов" ).text = "СобственныеТовары" + if settings.include_accounting_accounts: + for tag, value in ACCOUNTING_ACCOUNT_TAGS: + etree.SubElement(row_elem, tag).text = value etree.SubElement( row_elem, "ИдентификаторСтроки" ).text = str(uuid.uuid4()) diff --git a/modules/xml_generator_refactored2.py b/modules/xml_generator_refactored2.py new file mode 100644 index 0000000..1f4c85f --- /dev/null +++ b/modules/xml_generator_refactored2.py @@ -0,0 +1,446 @@ +""" +Генератор XML документов в формате 1С EnterpriseData 1.20 +Рефакторенная версия с устранением дублирования кода +""" +from lxml import etree +from datetime import datetime +from typing import Dict, List, Optional +import logging + +logger = logging.getLogger(__name__) + + +class XML1CGenerator: + """Генератор XML документов для 1С в формате EnterpriseData 1.20""" + + VERSION = "1.20" + NAMESPACE = "http://v8.1c.ru/8.1/data/enterprise" + NS_PREFIX = "v8" + + def __init__(self): + self.encoding = "UTF-8" + self.nsmap = {self.NS_PREFIX: self.NAMESPACE} + + # ========== Утилиты форматирования ========== + + @staticmethod + def _format_date(date_str: str) -> str: + """Форматирование даты в формат ГГГГ-ММ-ДДTЧЧ:ММ:СС""" + try: + dt = datetime.fromisoformat(date_str.replace('Z', '+00:00')) + return dt.strftime("%Y-%m-%dT%H:%M:%S") + except Exception as e: + logger.error(f"Ошибка форматирования даты: {e}") + return datetime.now().strftime("%Y-%m-%dT%H:%M:%S") + + @staticmethod + def _format_money(amount: int) -> str: + """Форматирование денежной суммы (копейки -> рубли)""" + return f"{amount / 100:.2f}" + + @staticmethod + def _safe_get(data: Dict, key: str, default: str = "") -> str: + """Безопасное извлечение значения из словаря""" + return str(data.get(key, default) or default) + + # ========== Создание XML элементов ========== + + def _create_element( + self, + tag: str, + text: Optional[str] = None, + **attributes + ) -> etree.Element: + """Создание XML элемента с атрибутами""" + elem = etree.Element(f"{{{self.NAMESPACE}}}{tag}") + if text: + elem.text = text + for key, value in attributes.items(): + if value is not None: + elem.set(key, str(value)) + return elem + + def _add_property( + self, + parent: etree.Element, + name: str, + value: str, + value_type: str = "string" + ) -> None: + """Добавление свойства объекта 1С""" + prop = etree.SubElement(parent, f"{{{self.NAMESPACE}}}Property") + etree.SubElement(prop, f"{{{self.NAMESPACE}}}Name").text = name + + value_elem = etree.SubElement( + prop, + f"{{{self.NAMESPACE}}}Value", + ) + if value_type == "string": + value_elem.set( + f"{{{self.NAMESPACE}}}type", + "http://www.w3.org/2001/XMLSchema:string" + ) + elif value_type == "decimal": + value_elem.set( + f"{{{self.NAMESPACE}}}type", + "http://www.w3.org/2001/XMLSchema:decimal" + ) + elif value_type == "dateTime": + value_elem.set( + f"{{{self.NAMESPACE}}}type", + "http://www.w3.org/2001/XMLSchema:dateTime" + ) + + value_elem.text = value + + # ========== Работа с организациями ========== + + def _add_organization_properties( + self, + parent: etree.Element, + org_data: Dict, + prefix: str = "Организация" + ) -> None: + """Добавление свойств организации""" + self._add_property( + parent, + f"{prefix}Наименование", + self._safe_get(org_data, "name") + ) + self._add_property( + parent, + f"{prefix}ИНН", + self._safe_get(org_data, "inn") + ) + + kpp = self._safe_get(org_data, "kpp") + if kpp: + self._add_property(parent, f"{prefix}КПП", kpp) + + self._add_property( + parent, + f"{prefix}ЮрАдрес", + self._safe_get(org_data, "legalAddress") + ) + + def _add_counterparty_properties( + self, + parent: etree.Element, + counterparty: Dict + ) -> None: + """Добавление свойств контрагента""" + self._add_organization_properties( + parent, + counterparty, + prefix="Контрагент" + ) + + # ========== Работа с товарами ========== + + def _add_product_item( + self, + parent: etree.Element, + position: Dict, + index: int + ) -> None: + """Добавление позиции товара в таблицу""" + row = etree.SubElement(parent, f"{{{self.NAMESPACE}}}Row") + + # Номер строки + self._add_property(row, "НомерСтроки", str(index)) + + # Наименование товара + product_name = self._safe_get(position, "name") + self._add_property(row, "Номенклатура", product_name) + + # Количество + quantity = position.get("quantity", 0) + self._add_property(row, "Количество", str(quantity), "decimal") + + # Цена + price = self._format_money(position.get("price", 0)) + self._add_property(row, "Цена", price, "decimal") + + # Сумма + total = self._format_money( + quantity * position.get("price", 0) + ) + self._add_property(row, "Сумма", total, "decimal") + + # НДС + vat = position.get("vat", 0) + if vat: + vat_rate = vat + vat_sum = self._format_money( + int(quantity * position.get("price", 0) * vat / 100) + ) + self._add_property(row, "СтавкаНДС", str(vat_rate)) + self._add_property(row, "СуммаНДС", vat_sum, "decimal") + + def _add_products_table( + self, + parent: etree.Element, + positions: List[Dict] + ) -> None: + """Добавление таблицы товаров""" + table = etree.SubElement(parent, f"{{{self.NAMESPACE}}}Property") + etree.SubElement(table, f"{{{self.NAMESPACE}}}Name").text = "Товары" + + value = etree.SubElement(table, f"{{{self.NAMESPACE}}}Value") + value.set( + f"{{{self.NAMESPACE}}}type", + f"{{{self.NAMESPACE}}}ValueTable" + ) + + for idx, position in enumerate(positions, start=1): + self._add_product_item(value, position, idx) + + # ========== Работа с суммами ========== + + def _calculate_totals(self, positions: List[Dict]) -> Dict[str, float]: + """Расчет итоговых сумм""" + total_sum = 0 + total_vat = 0 + + for position in positions: + quantity = position.get("quantity", 0) + price = position.get("price", 0) + vat = position.get("vat", 0) + + item_sum = quantity * price + total_sum += item_sum + + if vat: + total_vat += int(item_sum * vat / 100) + + return { + "sum": self._format_money(int(total_sum)), + "vat": self._format_money(total_vat), + "total": self._format_money(int(total_sum) + total_vat) + } + + def _add_totals( + self, + parent: etree.Element, + totals: Dict[str, float] + ) -> None: + """Добавление итоговых сумм""" + self._add_property( + parent, + "СуммаДокумента", + totals["sum"], + "decimal" + ) + self._add_property( + parent, + "СуммаНДС", + totals["vat"], + "decimal" + ) + self._add_property( + parent, + "СуммаВсего", + totals["total"], + "decimal" + ) + + # ========== Генерация документов ========== + + def _create_document_base( + self, + doc_type: str, + order_data: Dict + ) -> etree.Element: + """Создание базовой структуры документа""" + root = etree.Element( + f"{{{self.NAMESPACE}}}Data", + nsmap=self.nsmap + ) + + # Основной объект документа + obj = etree.SubElement(root, f"{{{self.NAMESPACE}}}Object") + + # Тип документа + self._add_property(obj, "ТипДокумента", doc_type) + + # Номер и дата + self._add_property( + obj, + "Номер", + self._safe_get(order_data, "name") + ) + self._add_property( + obj, + "Дата", + self._format_date(self._safe_get(order_data, "moment")), + "dateTime" + ) + + return obj + + def _add_document_parties( + self, + obj: etree.Element, + organization: Dict, + counterparty: Dict + ) -> None: + """Добавление информации о сторонах сделки""" + self._add_organization_properties(obj, organization) + self._add_counterparty_properties(obj, counterparty) + + def generate_realization( + self, + order_data: Dict, + filename: Optional[str] = None + ): + """ + Генерация XML документа реализации товаров + + Args: + order_data: Данные о заказе + filename: Имя выходного файла (опционально) + + Returns: + Если filename указан - True/False (успех/ошибка) + Если filename не указан - XML строка или None при ошибке + """ + try: + logger.info( + f"Генерация XML реализации для заказа " + f"{order_data.get('name', 'N/A')}" + ) + + # Базовая структура + obj = self._create_document_base( + "РеализацияТоваровУслуг", + order_data + ) + + # Организация и контрагент + organization = order_data.get("organization", {}) + agent = order_data.get("agent", {}) + + self._add_document_parties(obj, organization, agent) + + # Товары + positions = order_data.get("positions", []) + self._add_products_table(obj, positions) + + # Итоговые суммы + totals = self._calculate_totals(positions) + self._add_totals(obj, totals) + + # Получаем XML + root = obj.getparent() + tree = etree.ElementTree(root) + + if filename: + # Сохранение в файл + tree.write( + filename, + encoding=self.encoding, + xml_declaration=True, + pretty_print=True + ) + logger.info(f"XML успешно сохранен: {filename}") + return True + else: + # Возврат XML строки + xml_str = etree.tostring( + root, + encoding=self.encoding, + xml_declaration=True, + pretty_print=True + ).decode(self.encoding) + logger.info("XML успешно сгенерирован") + return xml_str + + except Exception as e: + logger.error(f"Ошибка генерации XML: {e}", exc_info=True) + return None if filename is None else False + + def generate_batch_realization( + self, + orders: List[Dict], + filename: str + ) -> bool: + """ + Генерация пакетного XML с несколькими документами + + Args: + orders: Список заказов + filename: Имя выходного файла + + Returns: + True если генерация успешна + """ + try: + logger.info(f"Генерация пакетного XML для {len(orders)} заказов") + + root = etree.Element( + f"{{{self.NAMESPACE}}}Data", + nsmap=self.nsmap + ) + + for order_data in orders: + # Создаем объект документа для каждого заказа + obj = etree.SubElement( + root, + f"{{{self.NAMESPACE}}}Object" + ) + + # Тип документа + self._add_property( + obj, + "ТипДокумента", + "РеализацияТоваровУслуг" + ) + + # Номер и дата + self._add_property( + obj, + "Номер", + self._safe_get(order_data, "name") + ) + self._add_property( + obj, + "Дата", + self._format_date(self._safe_get(order_data, "moment")), + "dateTime" + ) + + # Стороны сделки + organization = order_data.get("organization", {}) + agent = order_data.get("agent", {}) + self._add_document_parties(obj, organization, agent) + + # Товары + positions = order_data.get("positions", []) + self._add_products_table(obj, positions) + + # Итоги + totals = self._calculate_totals(positions) + self._add_totals(obj, totals) + + # Сохранение + tree = etree.ElementTree(root) + tree.write( + filename, + encoding=self.encoding, + xml_declaration=True, + pretty_print=True + ) + + logger.info(f"Пакетный XML успешно сохранен: {filename}") + return True + + except Exception as e: + logger.error( + f"Ошибка генерации пакетного XML: {e}", + exc_info=True + ) + return False + + +# Для обратной совместимости +XMLGenerator = XML1CGenerator diff --git a/requirements_gui.txt b/requirements_gui.txt new file mode 100644 index 0000000..942efe6 --- /dev/null +++ b/requirements_gui.txt @@ -0,0 +1,33 @@ +# Зависимости для GUI приложения +# Все библиотеки будут включены в standalone exe + +# GUI Framework +PyQt6==6.6.1 +PyQt6-Qt6==6.6.1 +PyQt6-sip==13.6.0 + +# HTTP клиент +requests==2.31.0 +urllib3==2.1.0 +certifi==2023.11.17 +charset-normalizer==3.3.2 +idna==3.6 + +# XML обработка +lxml==5.1.0 + +# Шифрование +cryptography==41.0.7 +cffi==1.16.0 +pycparser==2.21 + +# Сборка exe +pyinstaller==6.3.0 +pyinstaller-hooks-contrib==2024.0 +altgraph==0.17.4 +packaging==23.2 +pefile==2023.2.7 +pywin32-ctypes==0.2.2 + +# Утилиты +python-dotenv==1.0.0 diff --git a/start.bat b/start.bat deleted file mode 100644 index b1c5bb5..0000000 --- a/start.bat +++ /dev/null @@ -1,47 +0,0 @@ -@echo off -REM Скрипт для запуска бота в Docker (Windows) - -echo 🚀 Запуск BUH Bot в Docker... - -REM Проверка наличия .env файла -if not exist .env ( - echo ❌ Файл .env не найден! - echo 📝 Создайте .env на основе .env.example: - echo copy .env.example .env - echo notepad .env - exit /b 1 -) - -REM Создание необходимых директорий -echo 📁 Создание директорий для данных... -if not exist data mkdir data -if not exist logs mkdir logs -if not exist temp_documents mkdir temp_documents - -REM Сборка образа -echo 🔨 Сборка Docker образа... -docker-compose build - -REM Запуск контейнера -echo ▶️ Запуск контейнера... -docker-compose up -d - -REM Проверка статуса -echo. -echo ✅ Бот запущен! -echo. -echo 📊 Статус контейнера: -docker-compose ps - -echo. -echo 📝 Просмотр логов: -echo docker-compose logs -f -echo. -echo 🛑 Остановка бота: -echo docker-compose down -echo. -echo 🔄 Перезапуск бота: -echo docker-compose restart -echo. - -pause diff --git a/start.sh b/start.sh deleted file mode 100644 index eab8ba4..0000000 --- a/start.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/bin/bash -# Скрипт для запуска бота в Docker - -set -e - -echo "🚀 Запуск BUH Bot в Docker..." - -# Проверка наличия .env файла -if [ ! -f .env ]; then - echo "❌ Файл .env не найден!" - echo "📝 Создайте .env на основе .env.example:" - echo " cp .env.example .env" - echo " nano .env # и заполните TELEGRAM_BOT_TOKEN" - exit 1 -fi - -# Создание необходимых директорий -echo "📁 Создание директорий для данных..." -mkdir -p data logs temp_documents - -# Сборка образа -echo "🔨 Сборка Docker образа..." -docker-compose build - -# Запуск контейнера -echo "▶️ Запуск контейнера..." -docker-compose up -d - -# Проверка статуса -echo "" -echo "✅ Бот запущен!" -echo "" -echo "📊 Статус контейнера:" -docker-compose ps - -echo "" -echo "📝 Просмотр логов:" -echo " docker-compose logs -f" -echo "" -echo "🛑 Остановка бота:" -echo " docker-compose down" -echo "" -echo "🔄 Перезапуск бота:" -echo " docker-compose restart" diff --git a/start_bot.bat b/start_bot.bat deleted file mode 100644 index 3133c01..0000000 --- a/start_bot.bat +++ /dev/null @@ -1,27 +0,0 @@ -@echo off -REM Скрипт быстрого запуска Telegram бота (Windows) - -echo 🤖 Запуск Telegram бота для МойСклад... -echo. - -REM Проверка виртуального окружения -if not exist ".venv" ( - echo ❌ Виртуальное окружение не найдено! - echo Создайте его командой: python -m venv .venv - exit /b 1 -) - -REM Проверка .env файла -if not exist ".env" ( - echo ❌ Файл .env не найден! - echo Скопируйте .env.example в .env и заполните параметры: - echo copy .env.example .env - exit /b 1 -) - -REM Активация и запуск -echo ✅ Запуск бота... -echo. - -call .venv\Scripts\activate.bat -python bot.py diff --git a/start_bot.sh b/start_bot.sh deleted file mode 100644 index d8d8a78..0000000 --- a/start_bot.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/bin/bash -# Скрипт быстрого запуска Telegram бота - -echo "🤖 Запуск Telegram бота для МойСклад..." -echo "" - -# Проверка виртуального окружения -if [ ! -d ".venv" ]; then - echo "❌ Виртуальное окружение не найдено!" - echo "Создайте его командой: python -m venv .venv" - exit 1 -fi - -# Проверка .env файла -if [ ! -f ".env" ]; then - echo "❌ Файл .env не найден!" - echo "Скопируйте .env.example в .env и заполните параметры:" - echo " cp .env.example .env" - exit 1 -fi - -# Активация виртуального окружения и запуск -echo "✅ Запуск бота..." -echo "" - -if [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "win32" ]]; then - # Windows - source .venv/Scripts/activate -else - # Linux/Mac - source .venv/bin/activate -fi - -python bot.py diff --git a/stop.bat b/stop.bat deleted file mode 100644 index 3893c4f..0000000 --- a/stop.bat +++ /dev/null @@ -1,16 +0,0 @@ -@echo off -REM Скрипт для остановки бота (Windows) - -echo 🛑 Остановка BUH Bot... -docker-compose down - -echo. -echo ✅ Бот остановлен! -echo. -echo 💾 Данные сохранены в: -echo - .\data\users.json -echo - .\data\.encryption_key -echo - .\logs\ -echo. - -pause diff --git a/stop.sh b/stop.sh deleted file mode 100644 index c300f41..0000000 --- a/stop.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -# Скрипт для остановки бота - -echo "🛑 Остановка BUH Bot..." -docker-compose down - -echo "" -echo "✅ Бот остановлен!" -echo "" -echo "💾 Данные сохранены в:" -echo " - ./data/users.json" -echo " - ./data/.encryption_key" -echo " - ./logs/" diff --git a/test_final.xml b/test_final.xml deleted file mode 100644 index b8306c5..0000000 --- a/test_final.xml +++ /dev/null @@ -1,144 +0,0 @@ - - - - http://v8.1c.ru/edi/edi_stnd/EnterpriseData/1.20 - 2025-11-06T07:47:55 - 1.20 - - - <Документ.РеализацияТоваровУслуг> - <КлючевыеСвойства> - <Ссылка> - <Дата>2024-01-15T10:30:00 - <Номер>TEST-123 - <Организация> - <Ссылка>11111111-2222-3333-4444-555555555555 - <Наименование>ООО Тест - <НаименованиеСокращенное>ООО Тест - <НаименованиеПолное> - <ИНН>1234567890 - <ЮридическоеФизическоеЛицо>ЮридическоеЛицо - - - <ВидОперации>РеализацияКлиенту - <Валюта> - <Ссылка>72357d18-63a8-11e8-80dd-0050569f5448 - <ДанныеКлассификатора> - <Код>643 - <Наименование>руб. - - - <Сумма>100 - <СуммаВключаетНДС>true - <Контрагент> - <Ссылка>66666666-7777-8888-9999-000000000000 - <Наименование>Покупатель Тест - <НаименованиеПолное>Покупатель Тест - <ИНН>9876543210 - <ЮридическоеФизическоеЛицо>ЮридическоеЛицо - <СтранаРегистрации> - <Ссылка>67700c1b-63a8-11e8-80dd-0050569f5448 - <ДанныеКлассификатора> - <Код>643 - <Наименование>РОССИЯ - - - - <ДанныеВзаиморасчетов> - <Договор> - <Ссылка>f7fab3b0-67d6-58c4-a0c4-9438ef29a016 - <ВидДоговора>СПокупателем - <Организация> - <Ссылка>11111111-2222-3333-4444-555555555555 - <Наименование>ООО Тест - <НаименованиеСокращенное>ООО Тест - <НаименованиеПолное> - <ИНН>1234567890 - <ЮридическоеФизическоеЛицо>ЮридическоеЛицо - - <Контрагент> - <Ссылка>66666666-7777-8888-9999-000000000000 - <Наименование>Покупатель Тест - <НаименованиеПолное> - <ИНН>9876543210 - <ЮридическоеФизическоеЛицо>ЮридическоеЛицо - <СтранаРегистрации> - <Ссылка>67700c1b-63a8-11e8-80dd-0050569f5448 - <ДанныеКлассификатора> - <Код>643 - <Наименование>РОССИЯ - - - - <ВалютаВзаиморасчетов> - <Ссылка>72357d18-63a8-11e8-80dd-0050569f5448 - <ДанныеКлассификатора> - <Код>643 - <Наименование>руб. - - - <РасчетыВУсловныхЕдиницах>false - <Наименование>Основной договор - - <ВалютаВзаиморасчетов> - <Ссылка>72357d18-63a8-11e8-80dd-0050569f5448 - <ДанныеКлассификатора> - <Код>643 - <Наименование>руб. - - - <КурсВзаиморасчетов>1 - <КратностьВзаиморасчетов>1 - <РасчетыВУсловныхЕдиницах>false - - <Налогообложение>ПродажаОблагаетсяНДС - <ВидЭД>ТОРГ12 - <Товары> - <Строка> - <ДанныеНоменклатуры> - <Номенклатура> - <Ссылка>aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee - <НаименованиеПолное>Товар тест - <КодВПрограмме>TEST001 - <Наименование>Товар тест - - - <ЕдиницаИзмерения> - <Ссылка>cdf130e2-63a8-11e8-80dd-0050569f5448 - <ДанныеКлассификатора> - <Код>796 - <Наименование>шт - - - <Количество>1 - <Сумма>100 - <Цена>100 - <СтавкаНДС> - <Ставка>20 - <РасчетнаяСтавка>false - <НеОблагается>false - <ВидСтавки>Общая - <Страна> - <Ссылка>67700c1b-63a8-11e8-80dd-0050569f5448 - <ДанныеКлассификатора> - <Код>643 - <Наименование>РОССИЯ - - - - <СуммаНДС>16.67 - <ТипЗапасов>СобственныеТовары - <СчетУчета>43 - <СчетДоходов>90.01.1 - <СчетРасходов>90.02.1 - <СчетУчетаНДСПоРеализации>90.03 - <ИдентификаторСтроки>61234b12-58a2-44f0-8751-f071c2cfc38c - <ПрослеживаемыйТовар>false - - - <СпособПогашенияЗадолженности>Автоматически - <КомплектПечатныхДокументовПодписанПолучен>false - <ЭтоУПД>false - - - diff --git a/test_fixed.xml b/test_fixed.xml deleted file mode 100644 index f0ae793..0000000 --- a/test_fixed.xml +++ /dev/null @@ -1,139 +0,0 @@ - - - - http://v8.1c.ru/edi/edi_stnd/EnterpriseData/1.20 - 2025-11-06T08:08:20 - 1.20 - - - <Документ.РеализацияТоваровУслуг> - <КлючевыеСвойства> - <Ссылка> - <Дата>2024-01-15T10:30:00 - <Номер>TEST-FIX - <Организация> - <Ссылка>11111111-2222-3333-4444-555555555555 - <Наименование>ООО Тест - <НаименованиеСокращенное>ООО Тест - <НаименованиеПолное> - <ИНН>1234567890 - <ЮридическоеФизическоеЛицо>ЮридическоеЛицо - - - <ВидОперации>РеализацияКлиенту - <Валюта> - <Ссылка>72357d18-63a8-11e8-80dd-0050569f5448 - <ДанныеКлассификатора> - <Код>643 - <Наименование>руб. - - - <Сумма>100 - <СуммаВключаетНДС>true - <Контрагент> - <Ссылка>66666666-7777-8888-9999-000000000000 - <Наименование>Покупатель Тест - <НаименованиеПолное>Покупатель Тест - <ИНН>9876543210 - <ЮридическоеФизическоеЛицо>ЮридическоеЛицо - <СтранаРегистрации> - <Ссылка>67700c1b-63a8-11e8-80dd-0050569f5448 - <ДанныеКлассификатора> - <Код>643 - <Наименование>РОССИЯ - - - - <ДанныеВзаиморасчетов> - <Договор> - <Ссылка>f7fab3b0-67d6-58c4-a0c4-9438ef29a016 - <ВидДоговора>СПокупателем - <Организация> - <Ссылка>11111111-2222-3333-4444-555555555555 - <Наименование>ООО Тест - <НаименованиеСокращенное>ООО Тест - <НаименованиеПолное> - <ИНН>1234567890 - <ЮридическоеФизическоеЛицо>ЮридическоеЛицо - - <Контрагент> - <Ссылка>66666666-7777-8888-9999-000000000000 - <Наименование>Покупатель Тест - <НаименованиеПолное> - <ИНН>9876543210 - <ЮридическоеФизическоеЛицо>ЮридическоеЛицо - <СтранаРегистрации> - <Ссылка>67700c1b-63a8-11e8-80dd-0050569f5448 - <ДанныеКлассификатора> - <Код>643 - <Наименование>РОССИЯ - - - - <ВалютаВзаиморасчетов> - <Ссылка>72357d18-63a8-11e8-80dd-0050569f5448 - <ДанныеКлассификатора> - <Код>643 - <Наименование>руб. - - - <РасчетыВУсловныхЕдиницах>false - <Наименование>Основной договор - - <ВалютаВзаиморасчетов> - <Ссылка>72357d18-63a8-11e8-80dd-0050569f5448 - <ДанныеКлассификатора> - <Код>643 - <Наименование>руб. - - - <КурсВзаиморасчетов>1 - <КратностьВзаиморасчетов>1 - <РасчетыВУсловныхЕдиницах>false - - <Налогообложение>ПродажаОблагаетсяНДС - <Товары> - <Строка> - <ДанныеНоменклатуры> - <Номенклатура> - <Ссылка>aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee - <НаименованиеПолное>Товар с копейками - <КодВПрограмме>TEST001 - <Наименование>Товар с копейками - - - <ЕдиницаИзмерения> - <Ссылка>cdf130e2-63a8-11e8-80dd-0050569f5448 - <ДанныеКлассификатора> - <Код>796 - <Наименование>шт - - - <Количество>10 - <Сумма>12345.60 - <Цена>1234.56 - <СтавкаНДС> - <Ставка>20 - <РасчетнаяСтавка>false - <НеОблагается>false - <ВидСтавки>Общая - <Страна> - <Ссылка>67700c1b-63a8-11e8-80dd-0050569f5448 - <ДанныеКлассификатора> - <Код>643 - <Наименование>РОССИЯ - - - - <СуммаНДС>2057.60 - <ТипЗапасов>СобственныеТовары - <ИдентификаторСтроки>e7f4517f-41c6-4f39-acee-23ed0a3177d6 - <ПрослеживаемыйТовар>false - - - <СпособПогашенияЗадолженности>Автоматически - <КомплектПечатныхДокументовПодписанПолучен>false - <ЭтоУПД>false - - - diff --git a/test_order_4926.xml b/test_order_4926.xml deleted file mode 100644 index 62b9f18..0000000 --- a/test_order_4926.xml +++ /dev/null @@ -1,89 +0,0 @@ - -<Файл ВерсПрог="BUH_BOT_1.0" ВерсФорм="5.03"> - <Документ> - <СвСчФакт> - <НомерДата Номер="4926" Дата="06.11.2025"/> - <СвПрод ОКПО="0142109827"> - <ИдСв> - <СвИП ИННФЛ="263605160270" СвГосРегИП="304263530000073"> - <ФИО Фамилия="Беляев" Имя="Андрей" Отчество="Юрьевич"/> - - - <Адрес> - <АдрТекст Текст="355000, Россия, Ставропольский край, г Ставрополь"/> - - <БанкРекв НомерСчета="40802810860270001979"> - <СвБанк НаимБанк="СТАВРОПОЛЬСКОЕ ОТДЕЛЕНИЕ N5230 ПАО СБЕРБАНК" БИК="040702615" КорСчет="30101810907020000615"/> - - <Контакт> - <Тлф>8-800-234-33-68 - <ЭлПочта>robotail@bk.ru - - - <ГрузОт> - <ОнЖе>он же - - <ГрузПолуч> - <ИдСв СвЮЛУч="" КПП=""> - <НаимОрг Наим="Котов Антон Максимович"/> - - <Контакт> - <Тлф>+79218378869 - - - <ДокПодтвОтгрНом РеквНаимДок="УПД" РеквНомерДок="4926" РеквДатаДок="06.11.2025"/> - <СвПокуп> - <ИдСв СвЮЛУч="" КПП=""> - <НаимОрг Наим="Котов Антон Максимович"/> - - <Контакт> - <Тлф>+79218378869 - <ЭлПочта>77alpha@mail.ru - - - <ДенИзм КодОКВ="643" НаимОКВ="Российский рубль"/> - - <ТаблСчФакт> - <СведТов НомСтр="1"> - <НаимТов Наим="Камера для силомера Rocky Boxer"/> - <ОКЕИ_Тов НаимЕдИзм="шт"/> - <КолТов Количество="3.0"/> - <ЦенаТов Цена="390.00"/> - <СтТовБезНДС СтоимБезНДС="1114.29"/> - <НалСт СтавНДС="5" СумНДС="55.71"/> - <СтТовУчНал СтоимУчНДС="1170.00"/> - - <СведТов НомСтр="2"> - <НаимТов Наим="Кожаный чехол для груши (Черный)"/> - <ОКЕИ_Тов НаимЕдИзм="шт"/> - <КолТов Количество="2.0"/> - <ЦенаТов Цена="7810.00"/> - <СтТовБезНДС СтоимБезНДС="14876.19"/> - <НалСт СтавНДС="5" СумНДС="743.81"/> - <СтТовУчНал СтоимУчНДС="15620.00"/> - - <СведТов НомСтр="3"> - <НаимТов Наим="Полиуретановый конус с усиленной оксидированной шпилькой"/> - <ОКЕИ_Тов НаимЕдИзм="шт"/> - <КолТов Количество="2.0"/> - <ЦенаТов Цена="2680.00"/> - <СтТовБезНДС СтоимБезНДС="5104.76"/> - <НалСт СтавНДС="5" СумНДС="255.24"/> - <СтТовУчНал СтоимУчНДС="5360.00"/> - - <ВсегоОпл> - <СтТовБезНДСВсего СтоимБезНДС="21095.24"/> - <СумНДСВсего СумНДС="1054.76"/> - <СтТовУчНалВсего СтоимУчНДС="22150.00"/> - - - <СвПродПер> - <СвПер СодОпер="Товары переданы" ДатаПер="06.11.2025"> - <БезДокОснПер>1 - - - <Подписант СпосПодтПолном="6"> - <ФИО Фамилия="Беляев" Имя="Андрей" Отчество="Юрьевич"/> - - -