From 107c5a82c34b3ec302e6545c047b77b0b02e38d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=B3=D0=BE=D1=80=D1=8C=20=D0=9D=D0=B0=D0=B4=D0=B5?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Thu, 6 Nov 2025 13:47:52 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=BC=D0=B0=D1=81=D1=81=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D1=8F=20=D0=B3=D0=B5=D0=BD=D0=B5=D1=80=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=B4=D0=BE=D0=BA=D1=83=D0=BC=D0=B5=D0=BD=D1=82=D0=BE?= =?UTF-8?q?=D0=B2=20=D0=B2=20=D0=BE=D0=B4=D0=B8=D0=BD=20XML=20(EnterpriseD?= =?UTF-8?q?ata=201.20)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot.py | 208 +++++++++++++- modules/xml_generator.py | 588 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 794 insertions(+), 2 deletions(-) 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