diff --git a/bot.py b/bot.py
index 3b0ed3a..282539b 100644
--- a/bot.py
+++ b/bot.py
@@ -55,8 +55,9 @@ logger = logging.getLogger(__name__)
VALIDATE_ORDER,
SELECT_FORMAT,
SELECT_TEMPLATE,
- DOWNLOAD_DOCUMENT
-) = range(10)
+ DOWNLOAD_DOCUMENT,
+ BATCH_INPUT
+) = range(11)
# Менеджер пользователей
user_manager = UserManager()
@@ -87,6 +88,7 @@ def get_main_keyboard():
"""Возвращает основную клавиатуру"""
keyboard = [
["🔍 Поиск отгрузки", "📋 Список отгрузок"],
+ ["📦 Массовая генерация"],
["❓ Помощь"]
]
return ReplyKeyboardMarkup(keyboard, resize_keyboard=True)
@@ -1294,6 +1296,199 @@ async def generate_package(update: Update, context: ContextTypes.DEFAULT_TYPE):
return MAIN_MENU
+async def batch_generation_prompt(
+ update: Update,
+ context: ContextTypes.DEFAULT_TYPE
+):
+ """Запрос номеров заказов для массовой генерации"""
+ await update.message.reply_text(
+ "📦 Массовая генерация документов\n\n"
+ "Введите номера отгрузок через запятую без пробелов.\n\n"
+ "Примеры:\n"
+ " 4926,4927,4928\n"
+ " 00001,00002,00003\n\n"
+ "⚠️ Все отгрузки будут объединены в один XML файл "
+ "(EnterpriseData 1.20).",
+ parse_mode='HTML',
+ reply_markup=ReplyKeyboardMarkup(
+ [["⬅️ Назад"]],
+ resize_keyboard=True
+ )
+ )
+
+ return BATCH_INPUT
+
+
+async def batch_generation_process(
+ update: Update,
+ context: ContextTypes.DEFAULT_TYPE
+):
+ """Обработка массовой генерации"""
+ user_id = update.effective_user.id
+ text = update.message.text.strip()
+
+ # Проверка на отмену
+ if text == "⬅️ Назад":
+ await update.message.reply_text(
+ "Массовая генерация отменена.",
+ reply_markup=get_main_keyboard()
+ )
+ return MAIN_MENU
+
+ # Парсинг номеров
+ order_numbers = [num.strip() for num in text.split(',') if num.strip()]
+
+ if not order_numbers:
+ await update.message.reply_text(
+ "❌ Не указано ни одного номера отгрузки.\n"
+ "Попробуйте ещё раз:",
+ parse_mode='HTML'
+ )
+ return BATCH_INPUT
+
+ if len(order_numbers) > 50:
+ await update.message.reply_text(
+ "❌ Слишком много отгрузок (максимум 50).\n"
+ "Попробуйте ещё раз:",
+ parse_mode='HTML'
+ )
+ return BATCH_INPUT
+
+ # Уведомление о начале обработки
+ progress_msg = await update.message.reply_text(
+ f"⏳ Обработка {len(order_numbers)} отгрузок...\n"
+ f"Это может занять некоторое время."
+ )
+
+ try:
+ moysklad_client = get_moysklad_client(user_id)
+
+ # Сбор данных по всем отгрузкам
+ orders_data = []
+ not_found = []
+ errors = []
+
+ for idx, order_num in enumerate(order_numbers, 1):
+ try:
+ # Обновляем прогресс
+ await progress_msg.edit_text(
+ f"⏳ Обработка отгрузки {idx}/{len(order_numbers)}: {order_num}"
+ )
+
+ # Поиск отгрузки
+ search_results = moysklad_client.search_orders(
+ order_num,
+ limit=5
+ )
+
+ rows = search_results.get('rows', [])
+ if not rows:
+ not_found.append(order_num)
+ continue
+
+ # Берём первый результат
+ order_id = rows[0]['id']
+
+ # Получаем полные данные
+ order_data = moysklad_client.get_full_order_data(order_id)
+ orders_data.append(order_data)
+
+ except Exception as e:
+ logger.error(f"Ошибка обработки {order_num}: {e}")
+ errors.append(f"{order_num}: {str(e)[:50]}")
+
+ # Проверка результатов
+ if not orders_data:
+ error_text = "❌ Не удалось обработать ни одной отгрузки\n\n"
+ if not_found:
+ error_text += f"Не найдено: {', '.join(not_found)}\n"
+ if errors:
+ error_text += f"\nОшибки:\n" + "\n".join(errors[:5])
+
+ await progress_msg.edit_text(
+ error_text,
+ parse_mode='HTML'
+ )
+ await update.message.reply_text(
+ "Попробуйте ещё раз или вернитесь в главное меню:",
+ reply_markup=get_main_keyboard()
+ )
+ return MAIN_MENU
+
+ # Генерация XML
+ await progress_msg.edit_text(
+ f"✅ Найдено отгрузок: {len(orders_data)}\n"
+ f"⏳ Генерация XML..."
+ )
+
+ from xml_generator import XML1CGenerator
+ xml_gen = XML1CGenerator()
+
+ # Создаём временную директорию
+ output_dir = Path("temp_documents")
+ output_dir.mkdir(exist_ok=True)
+
+ # Генерируем пакетный XML
+ xml_filename = f"batch_{len(orders_data)}_orders.xml"
+ xml_path = str(output_dir / xml_filename)
+ xml_gen.generate_batch_realization(orders_data, xml_path)
+
+ # Формируем отчёт
+ order_names = [
+ od['order'].get('name', 'N/A')
+ for od in orders_data
+ ]
+
+ caption = (
+ f"✅ Пакетный XML создан!\n\n"
+ f"📦 Документов: {len(orders_data)}\n"
+ f"📋 Отгрузки: {', '.join(order_names)}\n\n"
+ )
+
+ if not_found:
+ caption += f"⚠️ Не найдено: {', '.join(not_found)}\n"
+ if errors:
+ caption += f"⚠️ Ошибок: {len(errors)}\n"
+
+ caption += "\n📄 Формат: 1С EnterpriseData 1.20"
+
+ # Отправляем файл
+ await update.message.reply_document(
+ document=open(xml_path, 'rb'),
+ caption=caption,
+ parse_mode='HTML'
+ )
+
+ # Удаляем временный файл
+ os.remove(xml_path)
+
+ # Показываем главное меню
+ await update.message.reply_text(
+ "✅ Готово! Используйте кнопки для продолжения:",
+ reply_markup=get_main_keyboard()
+ )
+
+ await progress_msg.delete()
+
+ return MAIN_MENU
+
+ except Exception as e:
+ logger.error(f"Ошибка массовой генерации: {e}", exc_info=True)
+
+ await progress_msg.edit_text(
+ f"❌ Ошибка массовой генерации\n\n"
+ f"{str(e)}",
+ parse_mode='HTML'
+ )
+
+ await update.message.reply_text(
+ "Попробуйте ещё раз или вернитесь в главное меню:",
+ reply_markup=get_main_keyboard()
+ )
+
+ return MAIN_MENU
+
+
async def handle_text(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Обработчик текстовых сообщений"""
text = update.message.text
@@ -1304,6 +1499,9 @@ async def handle_text(update: Update, context: ContextTypes.DEFAULT_TYPE):
elif text == "📋 Список отгрузок":
return await show_orders_list(update, context)
+ elif text == "📦 Массовая генерация":
+ return await batch_generation_prompt(update, context)
+
elif text == "❓ Помощь":
return await help_command(update, context)
@@ -1381,6 +1579,12 @@ def main():
CallbackQueryHandler(handle_callback),
MessageHandler(filters.TEXT & ~filters.COMMAND, handle_text),
],
+ BATCH_INPUT: [
+ MessageHandler(
+ filters.TEXT & ~filters.COMMAND,
+ batch_generation_process
+ ),
+ ],
},
fallbacks=[
CommandHandler("start", start),
diff --git a/modules/xml_generator.py b/modules/xml_generator.py
index b0600c3..f91cc9b 100644
--- a/modules/xml_generator.py
+++ b/modules/xml_generator.py
@@ -1115,4 +1115,592 @@ class XML1CGenerator:
logger.info(f"Документ реализации 1С сохранен: {output_path}")
return xml_string
+
+ def generate_batch_realization(
+ self,
+ orders_data: list,
+ output_path: Optional[str] = None
+ ) -> str:
+ """
+ Генерация множественных документов реализации в один XML
+
+ Args:
+ orders_data: Список полных данных заказов из МойСклад
+ output_path: Путь для сохранения файла
+
+ Returns:
+ XML в виде строки
+ """
+ # Корневой элемент 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})
+
+ logger.info(f"Генерация пакета из {len(orders_data)} документов")
+
+ # Генерируем каждый документ внутри одного Body
+ for idx, order_data in enumerate(orders_data, 1):
+ logger.info(
+ f"Обработка документа {idx}/{len(orders_data)}: "
+ f"{order_data['order'].get('name', 'N/A')}"
+ )
+
+ # Генерируем содержимое документа (без root/header/body)
+ doc_xml = self._generate_single_document_content(order_data)
+
+ # Добавляем документ в Body
+ body.append(doc_xml)
+
+ # Конвертация в строку
+ 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} "
+ f"({len(orders_data)} документов)"
+ )
+
+ return xml_string
+
+ def _generate_single_document_content(
+ self,
+ order_data: Dict
+ ) -> etree.Element:
+ """
+ Генерация содержимого одного документа (без обёртки Message)
+
+ Args:
+ order_data: Полные данные заказа
+
+ 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", [])
+
+ # Документ.РеализацияТоваровУслуг
+ doc = etree.Element("Документ.РеализацияТоваровУслуг")
+
+ # КлючевыеСвойства
+ 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", "").strip()
+ etree.SubElement(key_props, "Дата").text = order_date
+ etree.SubElement(key_props, "Номер").text = order_number
+
+ # Организация
+ org_elem = etree.SubElement(key_props, "Организация")
+ org_href = (
+ organization.get("meta", {}).get("href", "")
+ if organization.get("meta") else ""
+ )
+ org_id = (
+ org_href.split("/")[-1].split("?")[0]
+ if org_href else 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_href = (
+ currency.get("meta", {}).get("href", "")
+ if currency.get("meta") else ""
+ )
+ currency_id = (
+ currency_href.split("/")[-1].split("?")[0]
+ if currency_href else ""
+ )
+ if not currency_id:
+ currency_id = "72357d18-63a8-11e8-80dd-0050569f5448"
+ 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
+
+ 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", "")
+ else:
+ etree.SubElement(
+ org_ref, "НаименованиеСокращенное"
+ ).text = organization.get("name", "")
+ etree.SubElement(
+ org_ref, "НаименованиеПолное"
+ ).text = organization.get("legalTitle", "")
+
+ etree.SubElement(org_ref, "ИНН").text = organization.get("inn", "")
+
+ if company_type == "entrepreneur":
+ etree.SubElement(
+ org_ref, "ЮридическоеФизическоеЛицо"
+ ).text = "ФизическоеЛицо"
+ else:
+ etree.SubElement(
+ org_ref, "ЮридическоеФизическоеЛицо"
+ ).text = "ЮридическоеЛицо"
+
+ # Контрагент
+ cp_elem = etree.SubElement(doc, "Контрагент")
+ cp_href = (
+ counterparty.get("meta", {}).get("href", "")
+ if counterparty.get("meta") else ""
+ )
+ cp_id = (
+ cp_href.split("/")[-1].split("?")[0]
+ if cp_href else 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 = "ФизическоеЛицо"
+ else:
+ etree.SubElement(
+ cp_elem, "ЮридическоеФизическоеЛицо"
+ ).text = "ЮридическоеЛицо"
+
+ country_elem = etree.SubElement(cp_elem, "СтранаРегистрации")
+ etree.SubElement(
+ country_elem, "Ссылка"
+ ).text = "67700c1b-63a8-11e8-80dd-0050569f5448"
+ country_data = etree.SubElement(
+ country_elem, "ДанныеКлассификатора"
+ )
+ etree.SubElement(country_data, "Код").text = "643"
+ etree.SubElement(country_data, "Наименование").text = "РОССИЯ"
+
+ if cp_type == "entrepreneur":
+ etree.SubElement(
+ cp_elem, "ИндивидуальныйПредприниматель"
+ ).text = "true"
+
+ # ДанныеВзаиморасчетов
+ settlement_elem = etree.SubElement(doc, "ДанныеВзаиморасчетов")
+
+ # Договор
+ contract_elem = etree.SubElement(settlement_elem, "Договор")
+ import uuid
+ currency_code = currency.get("isoCode", "643")
+ is_ue = "false"
+ seed = f"contract-{cp_id}-{org_id}-CUR-{currency_code}-UE-{is_ue}"
+ contract_uuid = str(uuid.uuid5(uuid.NAMESPACE_DNS, seed))
+
+ etree.SubElement(contract_elem, "Ссылка").text = contract_uuid
+ 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", "")
+ else:
+ etree.SubElement(
+ contract_org, "НаименованиеСокращенное"
+ ).text = organization.get("name", "")
+ etree.SubElement(
+ contract_org, "НаименованиеПолное"
+ ).text = organization.get("legalTitle", "")
+
+ etree.SubElement(
+ contract_org, "ИНН"
+ ).text = organization.get("inn", "")
+
+ if company_type == "entrepreneur":
+ etree.SubElement(
+ contract_org, "ЮридическоеФизическоеЛицо"
+ ).text = "ФизическоеЛицо"
+ else:
+ etree.SubElement(
+ contract_org, "ЮридическоеФизическоеЛицо"
+ ).text = "ЮридическоеЛицо"
+
+ # Контрагент в договоре
+ 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 = "ФизическоеЛицо"
+ else:
+ etree.SubElement(
+ contract_cp, "ЮридическоеФизическоеЛицо"
+ ).text = "ЮридическоеЛицо"
+
+ cp_country = etree.SubElement(contract_cp, "СтранаРегистрации")
+ etree.SubElement(
+ cp_country, "Ссылка"
+ ).text = "67700c1b-63a8-11e8-80dd-0050569f5448"
+ cp_country_data = etree.SubElement(
+ cp_country, "ДанныеКлассификатора"
+ )
+ etree.SubElement(cp_country_data, "Код").text = "643"
+ etree.SubElement(cp_country_data, "Наименование").text = "РОССИЯ"
+
+ if cp_type == "entrepreneur":
+ etree.SubElement(
+ contract_cp, "ИндивидуальныйПредприниматель"
+ ).text = "true"
+
+ # Валюта взаиморасчетов в договоре
+ 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 = "ПродажаОблагаетсяНДС"
+
+ # Товары
+ goods_elem = etree.SubElement(doc, "Товары")
+
+ for pos in positions:
+ row_elem = etree.SubElement(goods_elem, "Строка")
+
+ assortment = pos.get("assortment", {})
+ product_href = (
+ assortment.get("meta", {}).get("href", "")
+ if assortment.get("meta") else ""
+ )
+ product_id = (
+ product_href.split("/")[-1].split("?")[0]
+ if product_href 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_href = (
+ uom.get("meta", {}).get("href", "")
+ if uom and uom.get("meta") else ""
+ )
+ uom_id = (
+ uom_href.split("/")[-1].split("?")[0]
+ if uom_href else ""
+ )
+ if not uom_id:
+ uom_id = "cdf130e2-63a8-11e8-80dd-0050569f5448"
+ 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 = f"{total:.2f}"
+ etree.SubElement(row_elem, "Цена").text = f"{price:.2f}"
+
+ 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 = "67700c1b-63a8-11e8-80dd-0050569f5448"
+ vat_country_data = etree.SubElement(
+ vat_country, "ДанныеКлассификатора"
+ )
+ etree.SubElement(vat_country_data, "Код").text = "643"
+ etree.SubElement(vat_country_data, "Наименование").text = "РОССИЯ"
+
+ 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 = str(uuid.uuid4())
+ etree.SubElement(
+ row_elem, "ПрослеживаемыйТовар"
+ ).text = "false"
+
+ # СпособПогашенияЗадолженности
+ etree.SubElement(
+ doc, "СпособПогашенияЗадолженности"
+ ).text = "Автоматически"
+
+ # КомплектПечатныхДокументовПодписанПолучен
+ etree.SubElement(
+ doc, "КомплектПечатныхДокументовПодписанПолучен"
+ ).text = "false"
+
+ # ЭтоУПД
+ etree.SubElement(doc, "ЭтоУПД").text = "false"
+
+ return doc