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">
+ <ФИО Фамилия="Беляев" Имя="Андрей" Отчество="Юрьевич"/>
+ Подписант>
+ Документ>
+Файл>