""" Генерация XML документов в формате ВерсФорм 5.03 УПД (Универсальный передаточный документ) и ТН (Товарная накладная) """ from lxml import etree from datetime import datetime from typing import Dict, Optional import logging logger = logging.getLogger(__name__) class XMLGenerator: """Генератор XML документов в формате ВерсФорм 5.03""" VERSION = "5.03" PROGRAM_VERSION = "BUH_BOT_1.0" def __init__(self): self.encoding = "UTF-8" def _format_date(self, date_str: str) -> str: """ Форматирование даты в формат ДД.ММ.ГГГГ Args: date_str: Дата в ISO формате Returns: Дата в формате ДД.ММ.ГГГГ """ try: dt = datetime.fromisoformat(date_str.replace('Z', '+00:00')) return dt.strftime("%d.%m.%Y") except Exception as e: logger.error(f"Ошибка форматирования даты: {e}") return datetime.now().strftime("%d.%m.%Y") def _format_money(self, amount: int) -> str: """ Форматирование денежной суммы (копейки -> рубли) Args: amount: Сумма в копейках Returns: Сумма в рублях с 2 знаками после запятой """ return f"{amount / 100:.2f}" def _add_seller_info( self, parent: etree.Element, organization: Dict, accounts: list = None ) -> None: """ Добавление информации о продавце Args: parent: Родительский XML элемент organization: Данные организации accounts: Список банковских счетов """ seller = etree.SubElement(parent, "СвПрод") # ОКПО okpo = organization.get("okpo", "") if okpo: seller.set("ОКПО", okpo) # ИдСв - зависит от типа организации seller_info = etree.SubElement(seller, "ИдСв") company_type = organization.get("companyType", "") inn = organization.get("inn", "") if company_type == "entrepreneur": # Индивидуальный предприниматель ogrnip = organization.get("ogrnip", "") sv_ip = etree.SubElement( seller_info, "СвИП", ИННФЛ=inn, СвГосРегИП=ogrnip if ogrnip else "" ) # ФИО last_name = organization.get("legalLastName", "") first_name = organization.get("legalFirstName", "") middle_name = organization.get("legalMiddleName", "") if last_name or first_name: fio = etree.SubElement(sv_ip, "ФИО") if last_name: fio.set("Фамилия", last_name) if first_name: fio.set("Имя", first_name) if middle_name: fio.set("Отчество", middle_name) else: # Юридическое лицо kpp = organization.get("kpp", "") sv_ul = etree.SubElement( seller_info, "СвЮЛУч", НаимОрг=organization.get("name", ""), ИННЮЛ=inn, КПП=kpp if kpp else "" ) # Адрес address = organization.get("legalAddress", "") if address: addr_element = etree.SubElement(seller, "Адрес") etree.SubElement( addr_element, "АдрТекст", Текст=address ) # Банковские реквизиты if accounts and len(accounts) > 0: # Берём первый счёт с БИК for acc in accounts: if acc.get('bic'): bank_info = etree.SubElement( seller, "БанкРекв", НомерСчета=acc.get('accountNumber', '') ) etree.SubElement( bank_info, "СвБанк", НаимБанк=acc.get('bankName', ''), БИК=acc.get('bic', ''), КорСчет=acc.get('correspondentAccount', '') ) break # Контактная информация phone = organization.get("phone", "") email = organization.get("email", "") if phone or email: contact = etree.SubElement(seller, "Контакт") if phone: etree.SubElement(contact, "Тлф").text = phone if email: etree.SubElement(contact, "ЭлПочта").text = email def _add_shipper_info( self, parent: etree.Element, counterparty: Dict, accounts: list = None ) -> None: """ Добавление информации о грузополучателе Args: parent: Родительский XML элемент counterparty: Данные контрагента accounts: Список банковских счетов """ shipper = etree.SubElement(parent, "ГрузПолуч") # ОКПО okpo = counterparty.get("okpo", "") if okpo: shipper.set("ОКПО", okpo) # ИНН/КПП и определение типа компании inn = counterparty.get("inn", "") kpp = counterparty.get("kpp", "") company_type = counterparty.get("companyType", "legal") shipper_info = etree.SubElement(shipper, "ИдСв") # Для ИП используем структуру СвИП if company_type == "entrepreneur": ogrnip = counterparty.get("ogrnip", "") sv_ip = etree.SubElement( shipper_info, "СвИП", ИННФЛ=inn, СвГосРегИП=ogrnip ) # ФИО для ИП last_name = counterparty.get("legalLastName", "") first_name = counterparty.get("legalFirstName", "") middle_name = counterparty.get("legalMiddleName", "") fio = etree.SubElement(sv_ip, "ФИО") fio.set("Фамилия", last_name) fio.set("Имя", first_name) if middle_name: fio.set("Отчество", middle_name) else: # Для юрлиц используем СвЮЛУч shipper_info.set("СвЮЛУч", inn) shipper_info.set("КПП", kpp if kpp else "") # Наименование name = counterparty.get("name", "") if not name: name = counterparty.get("legalTitle", "") etree.SubElement(shipper_info, "НаимОрг", Наим=name) # Адрес (приоритет: deliveryAddress -> actualAddress -> legalAddress) address = ( counterparty.get("deliveryAddress") or counterparty.get("actualAddress") or counterparty.get("legalAddress", "") ) if address: addr_element = etree.SubElement(shipper, "Адрес") etree.SubElement( addr_element, "АдрТекст", Текст=address ) # Банковские реквизиты if accounts and len(accounts) > 0: for acc in accounts: if acc.get('bic'): bank_info = etree.SubElement( shipper, "БанкРекв", НомерСчета=acc.get('accountNumber', '') ) etree.SubElement( bank_info, "СвБанк", НаимБанк=acc.get('bankName', ''), БИК=acc.get('bic', ''), КорСчет=acc.get('correspondentAccount', '') ) break # Контактная информация phone = counterparty.get("phone", "") if phone: contact = etree.SubElement(shipper, "Контакт") etree.SubElement(contact, "Тлф").text = phone def _add_buyer_info( self, parent: etree.Element, counterparty: Dict, accounts: list = None ) -> None: """ Добавление информации о покупателе Args: parent: Родительский XML элемент counterparty: Данные контрагента accounts: Список банковских счетов """ buyer = etree.SubElement(parent, "СвПокуп") # ОКПО okpo = counterparty.get("okpo", "") if okpo: buyer.set("ОКПО", okpo) # ИНН/КПП и определение типа компании inn = counterparty.get("inn", "") kpp = counterparty.get("kpp", "") company_type = counterparty.get("companyType", "legal") buyer_info = etree.SubElement(buyer, "ИдСв") # Для ИП используем структуру СвИП if company_type == "entrepreneur": ogrnip = counterparty.get("ogrnip", "") sv_ip = etree.SubElement( buyer_info, "СвИП", ИННФЛ=inn, СвГосРегИП=ogrnip ) # ФИО для ИП last_name = counterparty.get("legalLastName", "") first_name = counterparty.get("legalFirstName", "") middle_name = counterparty.get("legalMiddleName", "") fio = etree.SubElement(sv_ip, "ФИО") fio.set("Фамилия", last_name) fio.set("Имя", first_name) if middle_name: fio.set("Отчество", middle_name) else: # Для юрлиц используем СвЮЛУч buyer_info.set("СвЮЛУч", inn) buyer_info.set("КПП", kpp if kpp else "") # Наименование name = counterparty.get("name", "") if not name: name = counterparty.get("legalTitle", "") etree.SubElement(buyer_info, "НаимОрг", Наим=name) # Адрес (для покупателя используем юридический адрес) address = counterparty.get("legalAddress") or counterparty.get("actualAddress", "") if address: addr_element = etree.SubElement(buyer, "Адрес") etree.SubElement( addr_element, "АдрТекст", Текст=address ) # Банковские реквизиты if accounts and len(accounts) > 0: # Берём первый счёт с БИК for acc in accounts: if acc.get('bic'): bank_info = etree.SubElement( buyer, "БанкРекв", НомерСчета=acc.get('accountNumber', '') ) etree.SubElement( bank_info, "СвБанк", НаимБанк=acc.get('bankName', ''), БИК=acc.get('bic', ''), КорСчет=acc.get('correspondentAccount', '') ) break # Контактная информация phone = counterparty.get("phone", "") email = counterparty.get("email", "") if phone or email: contact = etree.SubElement(buyer, "Контакт") if phone: etree.SubElement(contact, "Тлф").text = phone if email: etree.SubElement(contact, "ЭлПочта").text = email def _add_positions_table( self, parent: etree.Element, positions: list ) -> Dict[str, float]: """ Добавление таблицы товаров Args: parent: Родительский XML элемент positions: Список позиций заказа Returns: Словарь с итоговыми суммами """ table = etree.SubElement(parent, "ТаблСчФакт") total_without_vat = 0 total_vat = 0 total_with_vat = 0 for index, pos in enumerate(positions, start=1): assortment = pos.get("assortment", {}) # Данные товара name = assortment.get("name", "Товар без наименования") quantity = pos.get("quantity", 0) # Цена из API уже включает НДС! price = pos.get("price", 0) / 100 vat_rate = pos.get("vat", 20) # Расчет сумм (цена из API уже включает НДС!) total_amount = quantity * price # Сумма С НДС # Извлекаем НДС из общей суммы amount = (total_amount / (1 + vat_rate / 100) if vat_rate else total_amount) vat_amount = total_amount - amount # Сумма НДС total_without_vat += amount total_vat += vat_amount total_with_vat += total_amount # Создание строки таблицы row = etree.SubElement( table, "СведТов", НомСтр=str(index) ) # Наименование товара etree.SubElement(row, "НаимТов", Наим=name) # Единица измерения uom = assortment.get("uom", {}) uom_name = uom.get("name", "шт") if uom else "шт" etree.SubElement(row, "ОКЕИ_Тов", НаимЕдИзм=uom_name) # Количество etree.SubElement(row, "КолТов", Количество=str(quantity)) # Цена etree.SubElement(row, "ЦенаТов", Цена=f"{price:.2f}") # Стоимость без НДС etree.SubElement(row, "СтТовБезНДС", СтоимБезНДС=f"{amount:.2f}") # НДС etree.SubElement( row, "НалСт", СтавНДС=str(vat_rate), СумНДС=f"{vat_amount:.2f}" ) # Стоимость с НДС etree.SubElement( row, "СтТовУчНал", СтоимУчНДС=f"{total_amount:.2f}" ) # Итоговые суммы totals = etree.SubElement(table, "ВсегоОпл") etree.SubElement( totals, "СтТовБезНДСВсего", СтоимБезНДС=f"{total_without_vat:.2f}" ) etree.SubElement( totals, "СумНДСВсего", СумНДС=f"{total_vat:.2f}" ) etree.SubElement( totals, "СтТовУчНалВсего", СтоимУчНДС=f"{total_with_vat:.2f}" ) return { "without_vat": total_without_vat, "vat": total_vat, "with_vat": total_with_vat } def generate_upd( self, order_data: Dict, output_path: Optional[str] = None ) -> str: """ Генерация УПД (Универсальный передаточный документ) 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", []) cp_accounts = order_data.get("cp_accounts", []) # Корневой элемент root = etree.Element( "Файл", ВерсПрог=self.PROGRAM_VERSION, ВерсФорм=self.VERSION ) # Документ doc = etree.SubElement(root, "Документ") # Сведения о счет-фактуре invoice_info = etree.SubElement(doc, "СвСчФакт") # Номер и дата order_number = order.get("name", "б/н") order_date = self._format_date(order.get("moment", "")) etree.SubElement( invoice_info, "НомерДата", Номер=order_number, Дата=order_date ) # Информация о продавце self._add_seller_info(invoice_info, organization, org_accounts) # Грузоотправитель (он же продавец) gruz_ot = etree.SubElement(invoice_info, "ГрузОт") etree.SubElement(gruz_ot, "ОнЖе").text = "он же" # Грузополучатель (покупатель) self._add_shipper_info(invoice_info, counterparty, cp_accounts) # Документ подтверждающий отгрузку etree.SubElement( invoice_info, "ДокПодтвОтгрНом", РеквНаимДок="УПД", РеквНомерДок=order_number, РеквДатаДок=order_date ) # Информация о покупателе self._add_buyer_info(invoice_info, counterparty, cp_accounts) # Валюта rate = order.get("rate", {}) if rate and "currency" in rate: currency = rate["currency"] etree.SubElement( invoice_info, "ДенИзм", КодОКВ=currency.get("code", "643"), НаимОКВ=currency.get("fullName", "Российский рубль") ) # Таблица товаров totals = self._add_positions_table(doc, positions) # Сведения о передаче товара sv_prod_per = etree.SubElement(doc, "СвПродПер") sv_per = etree.SubElement( sv_prod_per, "СвПер", СодОпер="Товары переданы", ДатаПер=order_date ) # Без документа-основания передачи etree.SubElement(sv_per, "БезДокОснПер").text = "1" # Подписант podpisant = etree.SubElement(doc, "Подписант", СпосПодтПолном="6") # Используем данные из организации if organization.get("companyType") == "entrepreneur": # Для ИП используем ФИО fio = etree.SubElement(podpisant, "ФИО") last_name = organization.get("legalLastName", "") first_name = organization.get("legalFirstName", "") middle_name = organization.get("legalMiddleName", "") if last_name: fio.set("Фамилия", last_name) if first_name: fio.set("Имя", first_name) if middle_name: fio.set("Отчество", middle_name) else: # Для ЮЛ можем указать директора (если данные есть) # Пока просто заглушка fio = etree.SubElement(podpisant, "ФИО") fio.set("Фамилия", "Подписант") fio.set("Имя", "Представитель") # Конвертация в строку 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"УПД сохранен: {output_path}") return xml_string def generate_tn( self, order_data: Dict, output_path: Optional[str] = None ) -> str: """ Генерация ТН (Товарная накладная ТОРГ-12) Args: order_data: Полные данные заказа output_path: Путь для сохранения файла Returns: XML в виде строки """ order = order_data["order"] organization = order_data["organization"] counterparty = order_data["counterparty"] positions = order_data["positions"] # Корневой элемент root = etree.Element( "Файл", ВерсПрог=self.PROGRAM_VERSION, ВерсФорм=self.VERSION, ТипДок="ТОРГ-12" ) # Документ doc = etree.SubElement(root, "Документ") # Номер и дата order_number = order.get("name", "б/н") order_date = self._format_date(order.get("moment", "")) doc_info = etree.SubElement(doc, "СвНакл") etree.SubElement( doc_info, "НомерДата", Номер=order_number, Дата=order_date ) # Информация о продавце self._add_seller_info(doc, organization) # Информация о покупателе self._add_buyer_info(doc, counterparty) # Таблица товаров totals = self._add_positions_table(doc, positions) # Конвертация в строку 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"ТН сохранена: {output_path}") 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) Raises: ValueError: Если дата некорректна """ try: dt = datetime.fromisoformat(date_str.replace('Z', '+00:00')) return dt.strftime("%Y-%m-%dT%H:%M:%S") except Exception as e: logger.error(f"Критическая ошибка: некорректная дата '{date_str}': {e}") raise ValueError( f"Невозможно сформировать документ с некорректной датой: {date_str}. " "Проверьте исходные данные." ) 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", "").strip() # Убираем лишние пробелы! etree.SubElement(key_props, "Дата").text = order_date etree.SubElement(key_props, "Номер").text = order_number # Организация org_elem = etree.SubElement(key_props, "Организация") # Извлекаем UUID из meta.href 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 "" # Если нет UUID, используем стандартный UUID для рубля if not currency_id: currency_id = "72357d18-63a8-11e8-80dd-0050569f5448" # Стандартный GUID для RUB 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", "") 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, "Контрагент") # Извлекаем UUID из meta.href 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, "СтранаРегистрации") # UUID России из классификатора 1С (стандартный для всех баз) etree.SubElement(country_elem, "Ссылка").text = "67700c1b-63a8-11e8-80dd-0050569f5448" country_data = etree.SubElement(country_elem, "ДанныеКлассификатора") etree.SubElement(country_data, "Код").text = "643" etree.SubElement(country_data, "Наименование").text = "РОССИЯ" # ИндивидуальныйПредприниматель только для ИП (после СтранаРегистрации) if cp_type == "entrepreneur": etree.SubElement(cp_elem, "ИндивидуальныйПредприниматель").text = "true" # ДанныеВзаиморасчетов settlement_elem = etree.SubElement(doc, "ДанныеВзаиморасчетов") # Договор contract_elem = etree.SubElement(settlement_elem, "Договор") # Генерируем детерминированный UUID для договора на основе контрагента # ВАЖНО: включаем валюту и признак у.е. в seed для uuid5 # Это обеспечивает создание отдельных договоров для разных валют: # - RUB/не-у.е. → 62.01/62.02 (задолженность/авансы) # - валюта → 62.21/62.22 (валютные расчёты) # - у.е. → 62.31/62.32 (расчёты в у.е.) import uuid currency_code = currency.get("isoCode", "643") is_ue = "false" # В текущей реализации всегда false seed = f"contract-{cp_id}-{org_id}-CUR-{currency_code}-UE-{is_ue}" contract_uuid = str(uuid.uuid5(uuid.NAMESPACE_DNS, seed)) logger.info( f"Создан договор с учётом валюты: " f"контрагент={cp_id}, организация={org_id}, " f"валюта={currency_code}, у.е.={is_ue}, UUID={contract_uuid}" ) 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, "СтранаРегистрации") # UUID России из классификатора 1С (стандартный для всех баз) etree.SubElement(cp_country, "Ссылка").text = "67700c1b-63a8-11e8-80dd-0050569f5448" cp_country_data = etree.SubElement(cp_country, "ДанныеКлассификатора") etree.SubElement(cp_country_data, "Код").text = "643" etree.SubElement(cp_country_data, "Наименование").text = "РОССИЯ" # ИндивидуальныйПредприниматель только для ИП (после СтранаРегистрации) 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", {}) # Очищаем GUID от query-параметров (например, ?expand=product.uom) 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, "ЕдиницаИзмерения") # Очищаем GUID от query-параметров 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 "" # Если нет UUID, используем стандартный UUID для "Штука (796)" if not uom_id: uom_id = "cdf130e2-63a8-11e8-80dd-0050569f5448" # Стандартный GUID для "шт" 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 # Форматируем с 2 знаками после запятой (НЕ округляем до целых!) 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, "Страна") # Используем стандартный UUID России из классификатора 1С etree.SubElement(vat_country, "Ссылка").text = "67700c1b-63a8-11e8-80dd-0050569f5448" vat_country_data = etree.SubElement(vat_country, "ДанныеКлассификатора") etree.SubElement(vat_country_data, "Код").text = "643" etree.SubElement(vat_country_data, "Наименование").text = "РОССИЯ" # СуммаНДС (извлекаем из суммы) 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) # ВНИМАНИЕ: Следующие поля могут отсутствовать в XSD некоторых версий 1С # Если импорт падает - закомментируйте эти строки # Дополнительные поля (опционально, зависит от конфигурации 1С) etree.SubElement(row_elem, "ТипЗапасов").text = "СобственныеТовары" # TODO: Сделать опциональными через config # etree.SubElement(row_elem, "СчетУчета").text = "43" # etree.SubElement(row_elem, "СчетДоходов").text = "90.01.1" # etree.SubElement(row_elem, "СчетРасходов").text = "90.02.1" # etree.SubElement(row_elem, "СчетУчетаНДСПоРеализации").text = "90.03" # Уникальный идентификатор строки (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) # ДИАГНОСТИКА: Проверяем соответствие валюты документа и договора # Если валюта RUB и не у.е., 1С должна использовать счета 62.01/62.02 # Если валюта иностранная → 62.21/62.22 # Если у.е. → 62.31/62.32 if currency_code == "643" or currency_code == "RUB": logger.info( "✓ Документ в рублях (RUB, код 643), не в у.е. " "→ ожидаются счета 62.01 (задолженность) / 62.02 (авансы). " "Если 1С подставляет 62.21/62.22 или 62.31/62.32, проверьте: " "1) Регистр 'Счета расчетов с контрагентами' - нет ли " "индивидуальных правил на контрагента/договор. " "2) В карточке договора: Валюта взаиморасчётов = RUB, " "Расчёты в условных единицах = нет." ) else: logger.info( f"⚠ Документ в иностранной валюте ({currency_code}) " "→ ожидаются счета 62.21/62.22. " "Убедитесь, что договор создан с валютой взаиморасчётов " f"= {currency_code}." ) # Сохранение в файл 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 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