diff --git a/CHANGELOG.md b/CHANGELOG.md index e473a15..d3db594 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,30 @@ +# Changelog + +## [2025-11-05] - Формат 1С EnterpriseData 1.20 + +### Added +- 🆕 **Формат 1С EnterpriseData 1.20** - новый генератор XML для прямой загрузки документов реализации в конфигурации 1С +- 📄 Класс `XML1CGenerator` в `modules/xml_generator.py` для генерации документов в формате обмена 1С +- 📖 Документация по формату 1С: `docs/XML_1C_FORMAT.md` +- ✨ Поддержка UUID, ISO 8601 дат, сумм в рублях (без копеек) +- 🏢 Корректная обработка ИП и юридических лиц в формате 1С +- 💼 Экспорт банковских реквизитов организации +- 📦 Детализация товарных позиций с кодами ОКЕИ +- 🔄 Полная структура документа "Реализация товаров и услуг" для 1С + +### Changed +- 📝 Обновлен README с информацией о новом формате +- 🔄 Сохранены оба формата: ВерсФорм 5.03 (для ФНС) и 1С EnterpriseData 1.20 (для 1С) + +### Technical Details +- Namespace: `http://v8.1c.ru/edi/edi_stnd/EnterpriseData/1.20` +- Формат даты: ISO 8601 (YYYY-MM-DDTHH:MM:SS) +- Денежные суммы: рубли (целое число) +- UUID для всех ссылок на объекты +- Полная интеграция с API МойСклад + +--- + # Обновление бота - Поиск заказов ## ✅ Исправлено diff --git a/README.md b/README.md index cd2ef33..e297d15 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Бот для бухгалтеров -Telegram-бот для автоматизации работы бухгалтеров: получение данных из МойСклад, валидация и генерация документов (УПД, ТН) в форматах XML (ВерсФорм 5.03) и Excel. +Telegram-бот для автоматизации работы бухгалтеров: получение данных из МойСклад, валидация и генерация документов (УПД, ТН) в форматах XML (ВерсФорм 5.03, 1С EnterpriseData 1.20) и Excel. ## 🔐 Многопользовательская система @@ -17,7 +17,7 @@ Telegram-бот для автоматизации работы бухгалте - 📦 **Интеграция с МойСклад** - автоматическое получение данных о заказах через API - ✅ **Валидация данных** - проверка обязательных и желательных полей (ИНН, КПП, адреса, товары) -- 📄 **Генерация XML** - создание документов в формате ВерсФорм 5.03 (УПД/ТН) +- 📄 **Генерация XML** - создание документов в форматах ВерсФорм 5.03 (УПД/ТН) и 1С EnterpriseData 1.20 (Реализация) - 📊 **Генерация Excel** - выгрузка в форматы УПД и ТОРГ-12 - 🤖 **Telegram интерфейс** - удобное управление через бота @@ -275,12 +275,17 @@ BUH_bot/ ## 📄 Форматы документов -### XML (ВерсФорм 5.03) +### XML (ВерсФорм 5.03) - для ФНС - **УПД** - Универсальный передаточный документ (Счет-фактура + ТОРГ-12) - **ТН** - Товарная накладная (ТОРГ-12) Соответствие приказу ФНС России для электронного документооборота. +### XML (1С EnterpriseData 1.20) - для загрузки в 1С +- **Документ реализации товаров и услуг** - прямая загрузка в конфигурации 1С + +📖 Подробная документация: [docs/XML_1C_FORMAT.md](docs/XML_1C_FORMAT.md) + ### Excel - **УПД** - Печатная форма с полными реквизитами - **ТОРГ-12** - Товарная накладная diff --git a/bot.py b/bot.py index e681a35..3b0ed3a 100644 --- a/bot.py +++ b/bot.py @@ -507,6 +507,8 @@ async def handle_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): "Выберите действие:", reply_markup=get_main_keyboard() ) + # Очищаем данные пользователя + context.user_data.clear() return MAIN_MENU elif data.startswith("page:"): @@ -541,13 +543,41 @@ async def handle_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): context.user_data['format'] = format_type if format_type == "xml": - # XML - сразу генерируем - return await generate_xml(update, context) + # XML - показываем выбор типа XML + return await show_xml_format_choice(update, context) + elif format_type == "package": + # Комплект документов - генерируем сразу + return await generate_package(update, context) else: # Excel - сбрасываем флаг и показываем шаблоны context.user_data['show_all_templates'] = False return await show_templates(update, context) + elif data.startswith("xml_format:"): + # Выбран тип XML + xml_format = data.split(":")[1] + context.user_data['xml_format'] = xml_format + return await generate_xml(update, context) + + elif data == "generate_another": + # Генерировать другой файл - возвращаемся к выбору формата + order_id = context.user_data.get('selected_order_id') + logger.info(f"generate_another clicked, order_id: {order_id}") + if order_id: + # Убираем кнопки с предыдущего сообщения + await query.edit_message_reply_markup(reply_markup=None) + # Вызываем validate_order напрямую с query + # validate_order сам покажет данные валидации + return await validate_order(update, context) + else: + await query.answer("❌ Ошибка: отгрузка не найдена") + await query.message.reply_text( + "Выберите действие:", + reply_markup=get_main_keyboard() + ) + context.user_data.clear() + return MAIN_MENU + elif data.startswith("template:"): # Выбран шаблон template_id = data.split(":")[1] @@ -560,7 +590,9 @@ async def handle_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): context.user_data['show_all_templates'] = True return await show_templates(update, context) - return SELECT_ORDER + # Если callback не распознан - логируем и возвращаем текущее состояние + logger.warning(f"Неизвестный callback: {data}") + return SELECT_FORMAT async def validate_order(update: Update, context: ContextTypes.DEFAULT_TYPE): @@ -680,15 +712,30 @@ async def validate_order(update: Update, context: ContextTypes.DEFAULT_TYPE): "📊 Excel", callback_data="format:excel" )], + [InlineKeyboardButton( + "📦 Комплект (XML 1С + Excel)", + callback_data="format:package" + )], [InlineKeyboardButton("❌ Отмена", callback_data="cancel")] ] if update.callback_query: - await update.callback_query.edit_message_text( - validation_text, - parse_mode='HTML', - reply_markup=InlineKeyboardMarkup(keyboard) - ) + # Проверяем, есть ли у сообщения reply_markup + # Если кнопки уже убраны (generate_another), отправляем новое + try: + await update.callback_query.edit_message_text( + validation_text, + parse_mode='HTML', + reply_markup=InlineKeyboardMarkup(keyboard) + ) + except Exception as e: + # Если редактирование не удалось, отправляем новое сообщение + logger.warning(f"Не удалось отредактировать сообщение: {e}") + await update.callback_query.message.reply_text( + validation_text, + parse_mode='HTML', + reply_markup=InlineKeyboardMarkup(keyboard) + ) else: await update.message.reply_text( validation_text, @@ -717,13 +764,53 @@ async def validate_order(update: Update, context: ContextTypes.DEFAULT_TYPE): return MAIN_MENU +async def show_xml_format_choice(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Показывает выбор формата XML""" + query = update.callback_query + + text = ( + "📄 Выберите формат XML:\n\n" + "🏛 ВерсФорм 5.03 (УПД/ТН)\n" + "Для ФНС и электронного документооборота\n" + "Стандартный формат для налоговой\n\n" + "💼 1С EnterpriseData 1.20\n" + "Для загрузки в программы 1С\n" + "Формат обмена с конфигурациями 1С" + ) + + keyboard = [ + [InlineKeyboardButton( + "🏛 ВерсФорм 5.03 (УПД)", + callback_data="xml_format:versform" + )], + [InlineKeyboardButton( + "💼 1С EnterpriseData 1.20", + callback_data="xml_format:1c" + )], + [InlineKeyboardButton("❌ Отмена", callback_data="cancel")] + ] + + await query.edit_message_text( + text, + parse_mode='HTML', + reply_markup=InlineKeyboardMarkup(keyboard) + ) + + return SELECT_FORMAT + + async def generate_xml(update: Update, context: ContextTypes.DEFAULT_TYPE): """Генерация XML документа""" user_id = update.effective_user.id query = update.callback_query + # Получаем выбранный формат XML + xml_format = context.user_data.get('xml_format', 'versform') + + format_name = "ВерсФорм 5.03 (УПД)" if xml_format == 'versform' else "1С EnterpriseData 1.20" + await query.edit_message_text( - "🔄 Генерация XML документа...", + f"🔄 Генерация XML документа ({format_name})...", reply_markup=None ) @@ -739,8 +826,16 @@ async def generate_xml(update: Update, context: ContextTypes.DEFAULT_TYPE): # Получаем полные данные отгрузки order_data = moysklad_client.get_full_order_data(order_id) - # Создаём генератор XML - xml_gen = XMLGenerator() + # Создаём генератор XML в зависимости от выбранного формата + if xml_format == '1c': + from xml_generator import XML1CGenerator + xml_gen = XML1CGenerator() + generate_method = xml_gen.generate_realization + caption_format = "1С EnterpriseData 1.20" + else: + xml_gen = XMLGenerator() + generate_method = xml_gen.generate_upd + caption_format = "УПД ВерсФорм 5.03" # Создаём временную директорию output_dir = Path("temp_documents") @@ -749,27 +844,56 @@ async def generate_xml(update: Update, context: ContextTypes.DEFAULT_TYPE): # Генерируем XML файл filename = f"{order_data.get('name', order_id)}.xml" output_path = str(output_dir / filename) - xml_gen.generate_upd(order_data, output_path) + generate_method(order_data, output_path) - # Отправляем файл + # Формируем информацию об отгрузке + order = order_data.get('order', {}) + order_name = order.get('name', 'N/A') + order_moment = order.get('moment', '')[:10] + order_sum = order.get('sum', 0) / 100 + counterparty = order_data.get('counterparty', {}) + counterparty_name = counterparty.get('name', 'N/A') + + caption = ( + f"✅ XML документ создан!\n\n" + f"📄 Формат: {caption_format}\n" + f"📋 Отгрузка: №{order_name} от {order_moment}\n" + f"👤 Контрагент: {counterparty_name}\n" + f"💰 Сумма: {order_sum:,.2f} ₽" + ) + + # Кнопка для генерации другого файла + keyboard = [ + [InlineKeyboardButton( + "📝 Сгенерировать другой файл", + callback_data="generate_another" + )], + [InlineKeyboardButton( + "🏠 Главное меню", + callback_data="cancel" + )] + ] + + # Отправляем файл с inline кнопками await query.message.reply_document( document=open(output_path, 'rb'), - caption="✅ XML документ (УПД 5.03) успешно создан!", - reply_markup=get_main_keyboard() + caption=caption, + parse_mode='HTML', + reply_markup=InlineKeyboardMarkup(keyboard) ) # Удаляем временный файл os.remove(output_path) - # Очищаем данные пользователя - context.user_data.clear() - - await query.message.reply_text( - "Выберите действие:", + # Показываем главное меню (ReplyKeyboard) + await context.bot.send_message( + chat_id=update.effective_chat.id, + text="Используйте кнопки ниже для продолжения работы:", reply_markup=get_main_keyboard() ) - return MAIN_MENU + # НЕ очищаем user_data - нужны данные для "Сгенерировать другой" + return SELECT_FORMAT except Exception as e: logger.error(f"Ошибка генерации XML: {e}") @@ -971,25 +1095,54 @@ async def generate_excel(update: Update, context: ContextTypes.DEFAULT_TYPE): output_path=str(output_dir / f"{order_id}.xls") ) + # Получаем информацию об отгрузке + order_data = context.user_data.get('order_data', {}) + order = order_data.get('order', {}) + order_name = order.get('name', 'N/A') + order_moment = order.get('moment', '')[:10] + order_sum = order.get('sum', 0) / 100 + counterparty = order_data.get('counterparty', {}) + counterparty_name = counterparty.get('name', 'N/A') + + caption = ( + f"✅ Excel документ создан!\n\n" + f"📋 Отгрузка: №{order_name} от {order_moment}\n" + f"👤 Контрагент: {counterparty_name}\n" + f"💰 Сумма: {order_sum:,.2f} ₽" + ) + + # Кнопка для генерации другого файла + keyboard = [ + [InlineKeyboardButton( + "📝 Сгенерировать другой файл", + callback_data="generate_another" + )], + [InlineKeyboardButton( + "🏠 Главное меню", + callback_data="cancel" + )] + ] + # Отправляем файл await query.message.reply_document( document=open(output_path, 'rb'), - caption="✅ Документ успешно создан!", - reply_markup=get_main_keyboard() + caption=caption, + parse_mode='HTML', + reply_markup=InlineKeyboardMarkup(keyboard) ) # Удаляем временный файл os.remove(output_path) - # Очищаем данные пользователя - context.user_data.clear() - - await query.message.reply_text( - "Выберите действие:", + # Показываем главное меню (ReplyKeyboard) + await context.bot.send_message( + chat_id=update.effective_chat.id, + text="Используйте кнопки ниже для продолжения работы:", reply_markup=get_main_keyboard() ) - return MAIN_MENU + # НЕ очищаем user_data + return SELECT_FORMAT except Exception as e: logger.error(f"Ошибка генерации документа: {e}") @@ -1004,6 +1157,143 @@ async def generate_excel(update: Update, context: ContextTypes.DEFAULT_TYPE): return MAIN_MENU +async def generate_package(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Генерация комплекта документов (XML 1С + рекомендованный Excel)""" + user_id = update.effective_user.id + query = update.callback_query + + await query.edit_message_text( + "🔄 Генерация комплекта документов...\n" + "Это может занять несколько секунд", + reply_markup=None + ) + + try: + order_id = context.user_data.get('selected_order_id') + + if not order_id: + raise ValueError("ID отгрузки не найден") + + # Получаем клиента для пользователя + moysklad_client = get_moysklad_client(user_id) + + # Получаем полные данные отгрузки + order_data = context.user_data.get('order_data') + if not order_data: + order_data = moysklad_client.get_full_order_data(order_id) + context.user_data['order_data'] = order_data + + order = order_data.get('order', {}) + counterparty = order_data.get('counterparty', {}) + + # Создаём временную директорию + output_dir = Path("temp_documents") + output_dir.mkdir(exist_ok=True) + + # 1. Генерируем XML 1С + from xml_generator import XML1CGenerator + xml_gen = XML1CGenerator() + xml_filename = f"{order.get('name', order_id)}_1c.xml" + xml_path = str(output_dir / xml_filename) + xml_gen.generate_realization(order_data, xml_path) + + # 2. Определяем рекомендованный Excel шаблон + inn = counterparty.get('inn', '') + kpp = counterparty.get('kpp', '') + name = counterparty.get('name', '').upper() + + # Определяем тип клиента + is_individual = True + if len(inn) == 10: + is_individual = False + elif len(inn) == 12: + if kpp or 'ИП ' in name or name.startswith('ИП'): + is_individual = False + + # ID рекомендованных шаблонов + TN_DEFAULT_ID = "92743bbb-c8c8-42e2-8686-350eace81edd" + UPD_DEFAULT_ID = "be6d441a-5982-48fe-ac21-46754e051f8c" + template_id = TN_DEFAULT_ID if is_individual else UPD_DEFAULT_ID + + # Генерируем Excel + excel_path = moysklad_client.print_document( + entity_type="demand", + entity_id=order_id, + template_id=template_id, + template_type="customtemplate", + extension="xls", + output_path=str(output_dir / f"{order_id}.xls") + ) + + # Формируем информацию об отгрузке + order_name = order.get('name', 'N/A') + order_moment = order.get('moment', '')[:10] + order_sum = order.get('sum', 0) / 100 + counterparty_name = counterparty.get('name', 'N/A') + + caption = ( + f"✅ Комплект документов создан!\n\n" + f"📦 Включено:\n" + f" • XML 1С EnterpriseData 1.20\n" + f" • Excel (рекомендованный шаблон)\n\n" + f"📋 Отгрузка: №{order_name} от {order_moment}\n" + f"👤 Контрагент: {counterparty_name}\n" + f"💰 Сумма: {order_sum:,.2f} ₽" + ) + + # Кнопка для генерации другого файла + keyboard = [ + [InlineKeyboardButton( + "📝 Сгенерировать другой файл", + callback_data="generate_another" + )], + [InlineKeyboardButton( + "🏠 Главное меню", + callback_data="cancel" + )] + ] + + # Отправляем XML файл + await query.message.reply_document( + document=open(xml_path, 'rb'), + caption="📄 XML 1С EnterpriseData 1.20" + ) + + # Отправляем Excel файл с полной информацией + await query.message.reply_document( + document=open(excel_path, 'rb'), + caption=caption, + parse_mode='HTML', + reply_markup=InlineKeyboardMarkup(keyboard) + ) + + # Удаляем временные файлы + os.remove(xml_path) + os.remove(excel_path) + + # Показываем главное меню (ReplyKeyboard) + await context.bot.send_message( + chat_id=update.effective_chat.id, + text="Используйте кнопки ниже для продолжения работы:", + reply_markup=get_main_keyboard() + ) + + # НЕ очищаем user_data + return SELECT_FORMAT + + except Exception as e: + logger.error(f"Ошибка генерации комплекта: {e}") + + await query.message.reply_text( + f"❌ Ошибка генерации комплекта: {str(e)}\n\n" + f"Выберите действие:", + reply_markup=get_main_keyboard() + ) + + context.user_data.clear() + return MAIN_MENU + + async def handle_text(update: Update, context: ContextTypes.DEFAULT_TYPE): """Обработчик текстовых сообщений""" text = update.message.text @@ -1036,8 +1326,7 @@ def main(): settings.telegram_bot_token ).build() - # Обработчики - application.add_handler(CommandHandler("start", start)) + # Обработчики команд (не входящих в ConversationHandler) application.add_handler(CommandHandler("help", help_command)) application.add_handler(CommandHandler("logout", logout_command)) application.add_handler( @@ -1047,6 +1336,7 @@ def main(): # Обработчик состояний conv_handler = ConversationHandler( entry_points=[ + CommandHandler("start", start), MessageHandler( filters.Regex("^🔍 Поиск отгрузки$"), search_order_prompt @@ -1081,12 +1371,15 @@ def main(): ], SELECT_ORDER: [ CallbackQueryHandler(handle_callback), + MessageHandler(filters.TEXT & ~filters.COMMAND, handle_text), ], SELECT_FORMAT: [ CallbackQueryHandler(handle_callback), + MessageHandler(filters.TEXT & ~filters.COMMAND, handle_text), ], SELECT_TEMPLATE: [ CallbackQueryHandler(handle_callback), + MessageHandler(filters.TEXT & ~filters.COMMAND, handle_text), ], }, fallbacks=[ diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index c84c629..ddfdd32 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -6,7 +6,7 @@ version: '3.8' services: # Telegram бот buh_bot: - image: ${DOCKER_REGISTRY:-ghcr.io}/${DOCKER_USERNAME:-username}/buh_bot:${BOT_VERSION:-latest} + image: igornadein/buh_bot:latest container_name: buh_bot_prod restart: always @@ -20,9 +20,9 @@ services: volumes: # Персистентные данные на хосте - - /var/lib/buh_bot/data:/app/data - - /var/log/buh_bot:/app/logs - - /var/tmp/buh_bot:/app/temp_documents + - ~/BUH_bot/data:/app/data + - ~/BUH_bot/logs:/app/logs + - ~/BUH_bot/temp_documents:/app/temp_documents networks: - buh_network diff --git a/docs/XML_1C_FORMAT.md b/docs/XML_1C_FORMAT.md new file mode 100644 index 0000000..9932df3 --- /dev/null +++ b/docs/XML_1C_FORMAT.md @@ -0,0 +1,195 @@ +# Генератор 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/modules/xml_generator.py b/modules/xml_generator.py index 3cc9e2a..03bcd06 100644 --- a/modules/xml_generator.py +++ b/modules/xml_generator.py @@ -622,6 +622,419 @@ class XMLGenerator: return xml_string +class XML1CGenerator: + """Генератор XML документов в формате 1С EnterpriseData 1.20""" + + FORMAT_VERSION = "1.20" + FORMAT_URI = "http://v8.1c.ru/edi/edi_stnd/EnterpriseData/1.20" + + def __init__(self): + self.encoding = "UTF-8" + # Регистрация namespace + self.nsmap = { + None: self.FORMAT_URI, + 'msg': 'http://www.1c.ru/SSL/Exchange/Message', + 'xs': 'http://www.w3.org/2001/XMLSchema', + 'xsi': 'http://www.w3.org/2001/XMLSchema-instance' + } + + def _format_datetime(self, date_str: str) -> str: + """ + Форматирование даты в ISO формат для 1С + + Args: + date_str: Дата в ISO формате + + Returns: + Дата в формате ISO 8601 (YYYY-MM-DDTHH:MM:SS) + """ + 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") + + def _format_money_1c(self, amount: int) -> str: + """ + Форматирование денежной суммы для 1С (копейки -> рубли без копеек) + + Args: + amount: Сумма в копейках + + Returns: + Сумма в рублях (целое число) + """ + return str(int(amount / 100)) + + def _format_money_with_decimals(self, amount: float) -> str: + """ + Форматирование денежной суммы с копейками + + Args: + amount: Сумма в рублях + + Returns: + Сумма с 2 знаками после запятой + """ + return f"{amount:.2f}" + + def generate_realization( + self, + order_data: Dict, + output_path: Optional[str] = None + ) -> str: + """ + Генерация документа Реализация товаров и услуг в формате 1С + + Args: + order_data: Полные данные заказа из МойСклад + output_path: Путь для сохранения файла + + Returns: + XML в виде строки + """ + order = order_data["order"] + organization = order_data["organization"] + counterparty = order_data["counterparty"] + positions = order_data["positions"] + org_accounts = order_data.get("org_accounts", []) + + # Корневой элемент Message + root = etree.Element( + "Message", + nsmap={ + 'msg': self.nsmap['msg'], + 'xs': self.nsmap['xs'], + 'xsi': self.nsmap['xsi'] + } + ) + + # Header + header = etree.SubElement(root, "{%s}Header" % self.nsmap['msg']) + etree.SubElement(header, "{%s}Format" % self.nsmap['msg']).text = self.FORMAT_URI + etree.SubElement(header, "{%s}CreationDate" % self.nsmap['msg']).text = datetime.now().strftime("%Y-%m-%dT%H:%M:%S") + etree.SubElement(header, "{%s}AvailableVersion" % self.nsmap['msg']).text = self.FORMAT_VERSION + + # Body + body = etree.SubElement(root, "Body", nsmap={None: self.FORMAT_URI}) + + # Документ.РеализацияТоваровУслуг + doc = etree.SubElement(body, "Документ.РеализацияТоваровУслуг") + + # КлючевыеСвойства + key_props = etree.SubElement(doc, "КлючевыеСвойства") + + # Ссылка (UUID документа) + order_id = order.get("id", "") + etree.SubElement(key_props, "Ссылка").text = order_id + + # Дата и Номер + order_date = self._format_datetime(order.get("moment", "")) + order_number = order.get("name", "") + etree.SubElement(key_props, "Дата").text = order_date + etree.SubElement(key_props, "Номер").text = order_number.ljust(11) # Паддинг пробелами + + # Организация + org_elem = etree.SubElement(key_props, "Организация") + org_id = organization.get("id", "") + etree.SubElement(org_elem, "Ссылка").text = org_id + etree.SubElement(org_elem, "Наименование").text = organization.get("name", "") + + # Полные наименования для ИП + company_type = organization.get("companyType", "") + if company_type == "entrepreneur": + legal_name = organization.get("legalTitle", "") + etree.SubElement(org_elem, "НаименованиеСокращенное").text = legal_name + etree.SubElement(org_elem, "НаименованиеПолное").text = legal_name + else: + etree.SubElement(org_elem, "НаименованиеСокращенное").text = organization.get("name", "") + etree.SubElement(org_elem, "НаименованиеПолное").text = organization.get("legalTitle", "") + + etree.SubElement(org_elem, "ИНН").text = organization.get("inn", "") + + # Тип лица + if company_type == "entrepreneur": + etree.SubElement(org_elem, "ЮридическоеФизическоеЛицо").text = "ФизическоеЛицо" + else: + etree.SubElement(org_elem, "ЮридическоеФизическоеЛицо").text = "ЮридическоеЛицо" + + # ВидОперации + etree.SubElement(doc, "ВидОперации").text = "РеализацияКлиенту" + + # Валюта + rate = order.get("rate", {}) + currency = rate.get("currency", {}) if rate else {} + currency_elem = etree.SubElement(doc, "Валюта") + currency_id = currency.get("meta", {}).get("href", "").split("/")[-1] if currency.get("meta") else "" + etree.SubElement(currency_elem, "Ссылка").text = currency_id + + currency_data = etree.SubElement(currency_elem, "ДанныеКлассификатора") + etree.SubElement(currency_data, "Код").text = currency.get("isoCode", "643") + etree.SubElement(currency_data, "Наименование").text = currency.get("name", "руб.") + + # Сумма (в рублях, без копеек) + order_sum = order.get("sum", 0) + etree.SubElement(doc, "Сумма").text = self._format_money_1c(order_sum) + + # СуммаВключаетНДС + etree.SubElement(doc, "СуммаВключаетНДС").text = "true" + + # Склад (если есть) + store = order.get("store", {}) + if store: + store_elem = etree.SubElement(doc, "Склад") + store_id = store.get("meta", {}).get("href", "").split("/")[-1] if store.get("meta") else "" + etree.SubElement(store_elem, "Ссылка").text = store_id + etree.SubElement(store_elem, "Наименование").text = store.get("name", "Основной склад") + etree.SubElement(store_elem, "ТипСклада").text = "Оптовый" + + # Руководитель (для ИП - сам ИП) + if company_type == "entrepreneur": + chief_elem = etree.SubElement(doc, "Руководитель") + etree.SubElement(chief_elem, "Ссылка").text = org_id + + # ФИО + fio_parts = [] + last_name = organization.get("legalLastName", "") + first_name = organization.get("legalFirstName", "") + middle_name = organization.get("legalMiddleName", "") + + if last_name: + fio_parts.append(last_name) + if first_name: + fio_parts.append(first_name) + if middle_name: + fio_parts.append(middle_name) + + full_fio = " ".join(fio_parts) + etree.SubElement(chief_elem, "ФИО").text = full_fio + etree.SubElement(chief_elem, "Фамилия").text = last_name + etree.SubElement(chief_elem, "Имя").text = first_name + etree.SubElement(chief_elem, "Отчество").text = middle_name + etree.SubElement(chief_elem, "ИНН").text = organization.get("inn", "") + + # БанковскийСчетОрганизации + if org_accounts and len(org_accounts) > 0: + acc = org_accounts[0] + bank_acc_elem = etree.SubElement(doc, "БанковскийСчетОрганизации") + acc_id = acc.get("id", "") + etree.SubElement(bank_acc_elem, "Ссылка").text = acc_id + etree.SubElement(bank_acc_elem, "НомерСчета").text = acc.get("accountNumber", "") + + # Банк + bank_elem = etree.SubElement(bank_acc_elem, "Банк") + etree.SubElement(bank_elem, "Ссылка").text = acc_id # Используем тот же ID + + bank_data = etree.SubElement(bank_elem, "ДанныеКлассификатораБанков") + etree.SubElement(bank_data, "Наименование").text = acc.get("bankName", "") + etree.SubElement(bank_data, "БИК").text = acc.get("bic", "") + etree.SubElement(bank_data, "КоррСчет").text = acc.get("correspondentAccount", "") + + # Владелец + owner_elem = etree.SubElement(bank_acc_elem, "Владелец") + org_ref = etree.SubElement(owner_elem, "ОрганизацииСсылка") + etree.SubElement(org_ref, "Ссылка").text = org_id + etree.SubElement(org_ref, "Наименование").text = organization.get("name", "") + + if company_type == "entrepreneur": + etree.SubElement(org_ref, "НаименованиеСокращенное").text = organization.get("legalTitle", "") + etree.SubElement(org_ref, "НаименованиеПолное").text = organization.get("legalTitle", "") + etree.SubElement(org_ref, "ЮридическоеФизическоеЛицо").text = "ФизическоеЛицо" + else: + etree.SubElement(org_ref, "НаименованиеСокращенное").text = organization.get("name", "") + etree.SubElement(org_ref, "НаименованиеПолное").text = organization.get("legalTitle", "") + etree.SubElement(org_ref, "ЮридическоеФизическоеЛицо").text = "ЮридическоеЛицо" + + etree.SubElement(org_ref, "ИНН").text = organization.get("inn", "") + + # Контрагент + cp_elem = etree.SubElement(doc, "Контрагент") + cp_id = counterparty.get("id", "") + etree.SubElement(cp_elem, "Ссылка").text = cp_id + etree.SubElement(cp_elem, "Наименование").text = counterparty.get("name", "") + etree.SubElement(cp_elem, "НаименованиеПолное").text = counterparty.get("legalTitle", counterparty.get("name", "")) + etree.SubElement(cp_elem, "ИНН").text = counterparty.get("inn", "") + + cp_type = counterparty.get("companyType", "legal") + if cp_type == "entrepreneur": + etree.SubElement(cp_elem, "ЮридическоеФизическоеЛицо").text = "ФизическоеЛицо" + etree.SubElement(cp_elem, "ИндивидуальныйПредприниматель").text = "true" + else: + etree.SubElement(cp_elem, "ЮридическоеФизическоеЛицо").text = "ЮридическоеЛицо" + + # Страна регистрации + country_elem = etree.SubElement(cp_elem, "СтранаРегистрации") + etree.SubElement(country_elem, "Ссылка").text = "" + country_data = etree.SubElement(country_elem, "ДанныеКлассификатора") + etree.SubElement(country_data, "Код").text = "643" + etree.SubElement(country_data, "Наименование").text = "РОССИЯ" + + # ДанныеВзаиморасчетов + settlement_elem = etree.SubElement(doc, "ДанныеВзаиморасчетов") + + # Договор + contract_elem = etree.SubElement(settlement_elem, "Договор") + etree.SubElement(contract_elem, "Ссылка").text = "" + etree.SubElement(contract_elem, "ВидДоговора").text = "СПокупателем" + + # Организация в договоре (ссылка) + contract_org = etree.SubElement(contract_elem, "Организация") + etree.SubElement(contract_org, "Ссылка").text = org_id + etree.SubElement(contract_org, "Наименование").text = organization.get("name", "") + if company_type == "entrepreneur": + etree.SubElement(contract_org, "НаименованиеСокращенное").text = organization.get("legalTitle", "") + etree.SubElement(contract_org, "НаименованиеПолное").text = organization.get("legalTitle", "") + etree.SubElement(contract_org, "ЮридическоеФизическоеЛицо").text = "ФизическоеЛицо" + else: + etree.SubElement(contract_org, "НаименованиеСокращенное").text = organization.get("name", "") + etree.SubElement(contract_org, "НаименованиеПолное").text = organization.get("legalTitle", "") + etree.SubElement(contract_org, "ЮридическоеФизическоеЛицо").text = "ЮридическоеЛицо" + etree.SubElement(contract_org, "ИНН").text = organization.get("inn", "") + + # Контрагент в договоре + contract_cp = etree.SubElement(contract_elem, "Контрагент") + etree.SubElement(contract_cp, "Ссылка").text = cp_id + etree.SubElement(contract_cp, "Наименование").text = counterparty.get("name", "") + etree.SubElement(contract_cp, "НаименованиеПолное").text = counterparty.get("legalTitle", "") + etree.SubElement(contract_cp, "ИНН").text = counterparty.get("inn", "") + + if cp_type == "entrepreneur": + etree.SubElement(contract_cp, "ЮридическоеФизическоеЛицо").text = "ФизическоеЛицо" + etree.SubElement(contract_cp, "ИндивидуальныйПредприниматель").text = "true" + else: + etree.SubElement(contract_cp, "ЮридическоеФизическоеЛицо").text = "ЮридическоеЛицо" + + # Страна в контрагенте договора + cp_country = etree.SubElement(contract_cp, "СтранаРегистрации") + etree.SubElement(cp_country, "Ссылка").text = "" + cp_country_data = etree.SubElement(cp_country, "ДанныеКлассификатора") + etree.SubElement(cp_country_data, "Код").text = "643" + etree.SubElement(cp_country_data, "Наименование").text = "РОССИЯ" + + # Валюта взаиморасчетов в договоре + 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", "руб.") + + etree.SubElement(contract_elem, "РасчетыВУсловныхЕдиницах").text = "false" + etree.SubElement(contract_elem, "Наименование").text = "Основной договор" + + # Валюта взаиморасчетов + 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", "руб.") + + etree.SubElement(settlement_elem, "КурсВзаиморасчетов").text = "1" + etree.SubElement(settlement_elem, "КратностьВзаиморасчетов").text = "1" + etree.SubElement(settlement_elem, "РасчетыВУсловныхЕдиницах").text = "false" + + # Налогообложение + etree.SubElement(doc, "Налогообложение").text = "ПродажаОблагаетсяНДС" + + # ВидЭД + etree.SubElement(doc, "ВидЭД").text = "ТОРГ12" + + # Товары + goods_elem = etree.SubElement(doc, "Товары") + + for pos in positions: + row_elem = etree.SubElement(goods_elem, "Строка") + + assortment = pos.get("assortment", {}) + product_id = assortment.get("meta", {}).get("href", "").split("/")[-1] if assortment.get("meta") else "" + product_name = assortment.get("name", "") + + # ДанныеНоменклатуры + nomenclature_data = etree.SubElement(row_elem, "ДанныеНоменклатуры") + nomenclature = etree.SubElement(nomenclature_data, "Номенклатура") + etree.SubElement(nomenclature, "Ссылка").text = product_id + etree.SubElement(nomenclature, "НаименованиеПолное").text = product_name + etree.SubElement(nomenclature, "КодВПрограмме").text = assortment.get("code", "") + etree.SubElement(nomenclature, "Наименование").text = product_name + + # ЕдиницаИзмерения + uom = assortment.get("uom", {}) + uom_elem = etree.SubElement(row_elem, "ЕдиницаИзмерения") + uom_id = uom.get("meta", {}).get("href", "").split("/")[-1] if uom and uom.get("meta") else "" + etree.SubElement(uom_elem, "Ссылка").text = uom_id + + uom_data = etree.SubElement(uom_elem, "ДанныеКлассификатора") + etree.SubElement(uom_data, "Код").text = "796" # Код ОКЕИ для "шт" + etree.SubElement(uom_data, "Наименование").text = uom.get("name", "шт") if uom else "шт" + + # Количество + quantity = pos.get("quantity", 0) + etree.SubElement(row_elem, "Количество").text = str(quantity) + + # Цена и суммы (в рублях) + 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)) + + # СтавкаНДС + vat_rate = pos.get("vat", 20) + vat_elem = etree.SubElement(row_elem, "СтавкаНДС") + etree.SubElement(vat_elem, "Ставка").text = str(vat_rate) + etree.SubElement(vat_elem, "РасчетнаяСтавка").text = "false" + etree.SubElement(vat_elem, "НеОблагается").text = "false" + etree.SubElement(vat_elem, "ВидСтавки").text = "Общая" + + vat_country = etree.SubElement(vat_elem, "Страна") + etree.SubElement(vat_country, "Ссылка").text = "" + vat_country_data = etree.SubElement(vat_country, "ДанныеКлассификатора") + etree.SubElement(vat_country_data, "Код").text = "643" + etree.SubElement(vat_country_data, "Наименование").text = "РОССИЯ" + + # СуммаНДС (извлекаем из суммы) + vat_amount = total / (1 + vat_rate / 100) * (vat_rate / 100) if vat_rate else 0 + etree.SubElement(row_elem, "СуммаНДС").text = self._format_money_with_decimals(vat_amount) + + # Дополнительные поля + 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" + + # Уникальный идентификатор строки + import uuid + etree.SubElement(row_elem, "ИдентификаторСтроки").text = str(uuid.uuid4()) + + etree.SubElement(row_elem, "ПрослеживаемыйТовар").text = "false" + + # СпособПогашенияЗадолженности + etree.SubElement(doc, "СпособПогашенияЗадолженности").text = "Автоматически" + + # КомплектПечатныхДокументовПодписанПолучен + etree.SubElement(doc, "КомплектПечатныхДокументовПодписанПолучен").text = "false" + + # ЭтоУПД + etree.SubElement(doc, "ЭтоУПД").text = "false" + + # Конвертация в строку + xml_string = etree.tostring( + root, + pretty_print=True, + xml_declaration=True, + encoding=self.encoding + ).decode(self.encoding) + + # Сохранение в файл + if output_path: + with open(output_path, "w", encoding=self.encoding) as f: + f.write(xml_string) + logger.info(f"Документ реализации 1С сохранен: {output_path}") + + return xml_string + + # Пример использования if __name__ == "__main__": logging.basicConfig(level=logging.INFO) @@ -629,38 +1042,63 @@ if __name__ == "__main__": # Тестовые данные test_order = { "order": { - "name": "00001", - "moment": "2024-01-15T10:00:00Z", - "sum": 120000 + "id": "313256ef-b4b7-11f0-b68a-50ebf69bde57", + "name": "4869", + "moment": "2025-10-23T09:37:11Z", + "sum": 1059900, # 10599 рублей в копейках + "rate": { + "currency": { + "name": "руб.", + "isoCode": "643" + } + } }, "organization": { - "name": "ООО Тестовая организация", - "inn": "7707083893", - "kpp": "770701001", - "legalAddress": "123456, г. Москва, ул. Тестовая, д. 1" + "id": "288f7520-b8e5-11e8-80bc-e0d55e458e90", + "name": "Беляев А.Ю. ИП", + "legalTitle": "Индивидуальный предприниматель Беляев Андрей Юрьевич", + "inn": "263605160270", + "companyType": "entrepreneur", + "legalLastName": "Беляев", + "legalFirstName": "Андрей", + "legalMiddleName": "Юрьевич" }, "counterparty": { - "name": "ООО Покупатель", - "inn": "7707083894", - "kpp": "770701002", - "legalAddress": "654321, г. Москва, ул. Другая, д. 2" + "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": "Товар 1", "uom": {"name": "шт"}}, - "quantity": 10, - "price": 10000, - "vat": 20 + "assortment": { + "name": "Жевательная резинка RUSGUM Арбузное трио 24 мм 7*150 штук", + "code": "00-00000682", + "uom": {"name": "шт"} + }, + "quantity": 1, + "price": 337300, # 3373 рубля в копейках + "vat": 5 } ] } - generator = XMLGenerator() + # Тест формата 1С + generator_1c = XML1CGenerator() + xml_1c = generator_1c.generate_realization(test_order, "test_1c_realization.xml") + print("Документ реализации 1С создан") - # Генерация УПД + # Тест старого формата УПД + generator = XMLGenerator() upd_xml = generator.generate_upd(test_order, "test_upd.xml") print("УПД создан") - - # Генерация ТН - tn_xml = generator.generate_tn(test_order, "test_tn.xml") - print("ТН создана") diff --git a/test_1c_realization.xml b/test_1c_realization.xml new file mode 100644 index 0000000..c708039 --- /dev/null +++ b/test_1c_realization.xml @@ -0,0 +1,176 @@ + + + + http://v8.1c.ru/edi/edi_stnd/EnterpriseData/1.20 + 2025-11-05T14:08:06 + 1.20 + + + <Документ.РеализацияТоваровУслуг> + <КлючевыеСвойства> + <Ссылка>313256ef-b4b7-11f0-b68a-50ebf69bde57 + <Дата>2025-10-23T09:37:11 + <Номер>4869 + <Организация> + <Ссылка>288f7520-b8e5-11e8-80bc-e0d55e458e90 + <Наименование>Беляев А.Ю. ИП + <НаименованиеСокращенное>Индивидуальный предприниматель Беляев Андрей Юрьевич + <НаименованиеПолное>Индивидуальный предприниматель Беляев Андрей Юрьевич + <ИНН>263605160270 + <ЮридическоеФизическоеЛицо>ФизическоеЛицо + + + <ВидОперации>РеализацияКлиенту + <Валюта> + <Ссылка> + <ДанныеКлассификатора> + <Код>643 + <Наименование>руб. + + + <Сумма>10599 + <СуммаВключаетНДС>true + <Руководитель> + <Ссылка>288f7520-b8e5-11e8-80bc-e0d55e458e90 + <ФИО>Беляев Андрей Юрьевич + <Фамилия>Беляев + <Имя>Андрей + <Отчество>Юрьевич + <ИНН>263605160270 + + <БанковскийСчетОрганизации> + <Ссылка>bdd39811-27ee-11ea-80e7-e0d55e458e90 + <НомерСчета>40802810860270001979 + <Банк> + <Ссылка>bdd39811-27ee-11ea-80e7-e0d55e458e90 + <ДанныеКлассификатораБанков> + <Наименование>СТАВРОПОЛЬСКОЕ ОТДЕЛЕНИЕ N5230 ПАО СБЕРБАНК + <БИК>040702615 + <КоррСчет>30101810907020000615 + + + <Владелец> + <ОрганизацииСсылка> + <Ссылка>288f7520-b8e5-11e8-80bc-e0d55e458e90 + <Наименование>Беляев А.Ю. ИП + <НаименованиеСокращенное>Индивидуальный предприниматель Беляев Андрей Юрьевич + <НаименованиеПолное>Индивидуальный предприниматель Беляев Андрей Юрьевич + <ЮридическоеФизическоеЛицо>ФизическоеЛицо + <ИНН>263605160270 + + + + <Контрагент> + <Ссылка>f37958af-d807-11ef-b676-50ebf69bde57 + <Наименование>АСЕЕВА АННА КУЗЬМИНИЧНА ИП + <НаименованиеПолное>ИНДИВИДУАЛЬНЫЙ ПРЕДПРИНИМАТЕЛЬ АСЕЕВА АННА КУЗЬМИНИЧНА + <ИНН>383700097634 + <ЮридическоеФизическоеЛицо>ФизическоеЛицо + <ИндивидуальныйПредприниматель>true + <СтранаРегистрации> + <Ссылка> + <ДанныеКлассификатора> + <Код>643 + <Наименование>РОССИЯ + + + + <ДанныеВзаиморасчетов> + <Договор> + <Ссылка> + <ВидДоговора>СПокупателем + <Организация> + <Ссылка>288f7520-b8e5-11e8-80bc-e0d55e458e90 + <Наименование>Беляев А.Ю. ИП + <НаименованиеСокращенное>Индивидуальный предприниматель Беляев Андрей Юрьевич + <НаименованиеПолное>Индивидуальный предприниматель Беляев Андрей Юрьевич + <ЮридическоеФизическоеЛицо>ФизическоеЛицо + <ИНН>263605160270 + + <Контрагент> + <Ссылка>f37958af-d807-11ef-b676-50ebf69bde57 + <Наименование>АСЕЕВА АННА КУЗЬМИНИЧНА ИП + <НаименованиеПолное>ИНДИВИДУАЛЬНЫЙ ПРЕДПРИНИМАТЕЛЬ АСЕЕВА АННА КУЗЬМИНИЧНА + <ИНН>383700097634 + <ЮридическоеФизическоеЛицо>ФизическоеЛицо + <ИндивидуальныйПредприниматель>true + <СтранаРегистрации> + <Ссылка> + <ДанныеКлассификатора> + <Код>643 + <Наименование>РОССИЯ + + + + <ВалютаВзаиморасчетов> + <Ссылка> + <ДанныеКлассификатора> + <Код>643 + <Наименование>руб. + + + <РасчетыВУсловныхЕдиницах>false + <Наименование>Основной договор + + <ВалютаВзаиморасчетов> + <Ссылка> + <ДанныеКлассификатора> + <Код>643 + <Наименование>руб. + + + <КурсВзаиморасчетов>1 + <КратностьВзаиморасчетов>1 + <РасчетыВУсловныхЕдиницах>false + + <Налогообложение>ПродажаОблагаетсяНДС + <ВидЭД>ТОРГ12 + <Товары> + <Строка> + <ДанныеНоменклатуры> + <Номенклатура> + <Ссылка> + <НаименованиеПолное>Жевательная резинка RUSGUM Арбузное трио 24 мм 7*150 штук + <КодВПрограмме>00-00000682 + <Наименование>Жевательная резинка RUSGUM Арбузное трио 24 мм 7*150 штук + + + <ЕдиницаИзмерения> + <Ссылка> + <ДанныеКлассификатора> + <Код>796 + <Наименование>шт + + + <Количество>1 + <Сумма>3373 + <Цена>3373 + <СтавкаНДС> + <Ставка>5 + <РасчетнаяСтавка>false + <НеОблагается>false + <ВидСтавки>Общая + <Страна> + <Ссылка> + <ДанныеКлассификатора> + <Код>643 + <Наименование>РОССИЯ + + + + <СуммаНДС>160.62 + <ТипЗапасов>СобственныеТовары + <СчетУчета>43 + <СчетДоходов>90.01.1 + <СчетРасходов>90.02.1 + <СчетУчетаНДСПоРеализации>90.03 + <ИдентификаторСтроки>09ef6d80-f310-4c63-a242-a23db0ee7a55 + <ПрослеживаемыйТовар>false + + + <СпособПогашенияЗадолженности>Автоматически + <КомплектПечатныхДокументовПодписанПолучен>false + <ЭтоУПД>false + + + diff --git a/test_upd.xml b/test_upd.xml new file mode 100644 index 0000000..df5eb61 --- /dev/null +++ b/test_upd.xml @@ -0,0 +1,61 @@ + +<Файл ВерсПрог="BUH_BOT_1.0" ВерсФорм="5.03"> + <Документ> + <СвСчФакт> + <НомерДата Номер="4869" Дата="23.10.2025"/> + <СвПрод> + <ИдСв> + <СвИП ИННФЛ="263605160270" СвГосРегИП=""> + <ФИО Фамилия="Беляев" Имя="Андрей" Отчество="Юрьевич"/> + + + <БанкРекв НомерСчета="40802810860270001979"> + <СвБанк НаимБанк="СТАВРОПОЛЬСКОЕ ОТДЕЛЕНИЕ N5230 ПАО СБЕРБАНК" БИК="040702615" КорСчет="30101810907020000615"/> + + + <ГрузОт> + <ОнЖе>он же + + <ГрузПолуч> + <ИдСв> + <СвИП ИННФЛ="383700097634" СвГосРегИП=""> + <ФИО Фамилия="" Имя=""/> + + + + <ДокПодтвОтгрНом РеквНаимДок="УПД" РеквНомерДок="4869" РеквДатаДок="23.10.2025"/> + <СвПокуп> + <ИдСв> + <СвИП ИННФЛ="383700097634" СвГосРегИП=""> + <ФИО Фамилия="" Имя=""/> + + + + <ДенИзм КодОКВ="643" НаимОКВ="Российский рубль"/> + + <ТаблСчФакт> + <СведТов НомСтр="1"> + <НаимТов Наим="Жевательная резинка RUSGUM Арбузное трио 24 мм 7*150 штук"/> + <ОКЕИ_Тов НаимЕдИзм="шт"/> + <КолТов Количество="1"/> + <ЦенаТов Цена="3373.00"/> + <СтТовБезНДС СтоимБезНДС="3212.38"/> + <НалСт СтавНДС="5" СумНДС="160.62"/> + <СтТовУчНал СтоимУчНДС="3373.00"/> + + <ВсегоОпл> + <СтТовБезНДСВсего СтоимБезНДС="3212.38"/> + <СумНДСВсего СумНДС="160.62"/> + <СтТовУчНалВсего СтоимУчНДС="3373.00"/> + + + <СвПродПер> + <СвПер СодОпер="Товары переданы" ДатаПер="23.10.2025"> + <БезДокОснПер>1 + + + <Подписант СпосПодтПолном="6"> + <ФИО Фамилия="Беляев" Имя="Андрей" Отчество="Юрьевич"/> + + +