""" Генерация 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) # Адрес address = 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", "") 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) """ try: dt = datetime.fromisoformat(date_str.replace('Z', '+00:00')) return dt.strftime("%Y-%m-%dT%H:%M:%S") except Exception as e: logger.error(f"Ошибка форматирования даты: {e}") return datetime.now().strftime("%Y-%m-%dT%H:%M:%S") def _format_money_1c(self, amount: int) -> str: """ Форматирование денежной суммы для 1С (копейки -> рубли без копеек) Args: amount: Сумма в копейках Returns: Сумма в рублях (целое число) """ return str(int(amount / 100)) def _format_money_with_decimals(self, amount: float) -> str: """ Форматирование денежной суммы с копейками Args: amount: Сумма в рублях Returns: Сумма с 2 знаками после запятой """ return f"{amount:.2f}" def generate_realization( self, order_data: Dict, output_path: Optional[str] = None ) -> str: """ Генерация документа Реализация товаров и услуг в формате 1С Args: order_data: Полные данные заказа из МойСклад output_path: Путь для сохранения файла Returns: XML в виде строки """ order = order_data["order"] organization = order_data["organization"] counterparty = order_data["counterparty"] positions = order_data["positions"] org_accounts = order_data.get("org_accounts", []) # Корневой элемент Message root = etree.Element( "Message", nsmap={ 'msg': self.nsmap['msg'], 'xs': self.nsmap['xs'], 'xsi': self.nsmap['xsi'] } ) # Header header = etree.SubElement(root, "{%s}Header" % self.nsmap['msg']) etree.SubElement(header, "{%s}Format" % self.nsmap['msg']).text = self.FORMAT_URI etree.SubElement(header, "{%s}CreationDate" % self.nsmap['msg']).text = datetime.now().strftime("%Y-%m-%dT%H:%M:%S") etree.SubElement(header, "{%s}AvailableVersion" % self.nsmap['msg']).text = self.FORMAT_VERSION # Body body = etree.SubElement(root, "Body", nsmap={None: self.FORMAT_URI}) # Документ.РеализацияТоваровУслуг doc = etree.SubElement(body, "Документ.РеализацияТоваровУслуг") # КлючевыеСвойства key_props = etree.SubElement(doc, "КлючевыеСвойства") # Ссылка (UUID документа) order_id = order.get("id", "") etree.SubElement(key_props, "Ссылка").text = order_id # Дата и Номер order_date = self._format_datetime(order.get("moment", "")) order_number = order.get("name", "").strip() # Убираем лишние пробелы! etree.SubElement(key_props, "Дата").text = order_date etree.SubElement(key_props, "Номер").text = order_number # Организация org_elem = etree.SubElement(key_props, "Организация") org_id = organization.get("id", "") etree.SubElement(org_elem, "Ссылка").text = org_id etree.SubElement(org_elem, "Наименование").text = organization.get("name", "") # Полные наименования для ИП company_type = organization.get("companyType", "") if company_type == "entrepreneur": legal_name = organization.get("legalTitle", "") etree.SubElement(org_elem, "НаименованиеСокращенное").text = legal_name etree.SubElement(org_elem, "НаименованиеПолное").text = legal_name else: etree.SubElement(org_elem, "НаименованиеСокращенное").text = organization.get("name", "") etree.SubElement(org_elem, "НаименованиеПолное").text = organization.get("legalTitle", "") etree.SubElement(org_elem, "ИНН").text = organization.get("inn", "") # Тип лица if company_type == "entrepreneur": etree.SubElement(org_elem, "ЮридическоеФизическоеЛицо").text = "ФизическоеЛицо" else: etree.SubElement(org_elem, "ЮридическоеФизическоеЛицо").text = "ЮридическоеЛицо" # ВидОперации etree.SubElement(doc, "ВидОперации").text = "РеализацияКлиенту" # Валюта rate = order.get("rate", {}) currency = rate.get("currency", {}) if rate else {} currency_elem = etree.SubElement(doc, "Валюта") currency_id = currency.get("meta", {}).get("href", "").split("/")[-1] if currency.get("meta") else "" etree.SubElement(currency_elem, "Ссылка").text = currency_id currency_data = etree.SubElement(currency_elem, "ДанныеКлассификатора") etree.SubElement(currency_data, "Код").text = currency.get("isoCode", "643") etree.SubElement(currency_data, "Наименование").text = currency.get("name", "руб.") # Сумма (в рублях, без копеек) order_sum = order.get("sum", 0) etree.SubElement(doc, "Сумма").text = self._format_money_1c(order_sum) # СуммаВключаетНДС etree.SubElement(doc, "СуммаВключаетНДС").text = "true" # Склад (если есть) store = order.get("store", {}) if store: store_elem = etree.SubElement(doc, "Склад") store_id = store.get("meta", {}).get("href", "").split("/")[-1] if store.get("meta") else "" etree.SubElement(store_elem, "Ссылка").text = store_id etree.SubElement(store_elem, "Наименование").text = store.get("name", "Основной склад") etree.SubElement(store_elem, "ТипСклада").text = "Оптовый" # Руководитель (для ИП - сам ИП) if company_type == "entrepreneur": chief_elem = etree.SubElement(doc, "Руководитель") etree.SubElement(chief_elem, "Ссылка").text = org_id # ФИО fio_parts = [] last_name = organization.get("legalLastName", "") first_name = organization.get("legalFirstName", "") middle_name = organization.get("legalMiddleName", "") if last_name: fio_parts.append(last_name) if first_name: fio_parts.append(first_name) if middle_name: fio_parts.append(middle_name) full_fio = " ".join(fio_parts) etree.SubElement(chief_elem, "ФИО").text = full_fio etree.SubElement(chief_elem, "Фамилия").text = last_name etree.SubElement(chief_elem, "Имя").text = first_name etree.SubElement(chief_elem, "Отчество").text = middle_name etree.SubElement(chief_elem, "ИНН").text = organization.get("inn", "") # БанковскийСчетОрганизации if org_accounts and len(org_accounts) > 0: acc = org_accounts[0] bank_acc_elem = etree.SubElement(doc, "БанковскийСчетОрганизации") acc_id = acc.get("id", "") etree.SubElement(bank_acc_elem, "Ссылка").text = acc_id etree.SubElement(bank_acc_elem, "НомерСчета").text = acc.get("accountNumber", "") # Банк bank_elem = etree.SubElement(bank_acc_elem, "Банк") etree.SubElement(bank_elem, "Ссылка").text = acc_id # Используем тот же ID bank_data = etree.SubElement(bank_elem, "ДанныеКлассификатораБанков") etree.SubElement(bank_data, "Наименование").text = acc.get("bankName", "") etree.SubElement(bank_data, "БИК").text = acc.get("bic", "") etree.SubElement(bank_data, "КоррСчет").text = acc.get("correspondentAccount", "") # Владелец owner_elem = etree.SubElement(bank_acc_elem, "Владелец") org_ref = etree.SubElement(owner_elem, "ОрганизацииСсылка") etree.SubElement(org_ref, "Ссылка").text = org_id etree.SubElement(org_ref, "Наименование").text = organization.get("name", "") if company_type == "entrepreneur": etree.SubElement(org_ref, "НаименованиеСокращенное").text = organization.get("legalTitle", "") etree.SubElement(org_ref, "НаименованиеПолное").text = organization.get("legalTitle", "") 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_id = counterparty.get("id", "") etree.SubElement(cp_elem, "Ссылка").text = cp_id etree.SubElement(cp_elem, "Наименование").text = counterparty.get("name", "") etree.SubElement(cp_elem, "НаименованиеПолное").text = counterparty.get("legalTitle", counterparty.get("name", "")) etree.SubElement(cp_elem, "ИНН").text = counterparty.get("inn", "") cp_type = counterparty.get("companyType", "legal") if cp_type == "entrepreneur": etree.SubElement(cp_elem, "ЮридическоеФизическоеЛицо").text = "ФизическоеЛицо" 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 для договора на основе контрагента import uuid contract_uuid = str(uuid.uuid5(uuid.NAMESPACE_DNS, f"contract-{cp_id}-{org_id}")) 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 = "ПродажаОблагаетсяНДС" # ВидЭД etree.SubElement(doc, "ВидЭД").text = "ТОРГ12" # Товары 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 etree.SubElement(row_elem, "Сумма").text = str(int(total)) etree.SubElement(row_elem, "Цена").text = str(int(price)) # СтавкаНДС vat_rate = pos.get("vat", 20) vat_elem = etree.SubElement(row_elem, "СтавкаНДС") etree.SubElement(vat_elem, "Ставка").text = str(vat_rate) etree.SubElement(vat_elem, "РасчетнаяСтавка").text = "false" etree.SubElement(vat_elem, "НеОблагается").text = "false" etree.SubElement(vat_elem, "ВидСтавки").text = "Общая" vat_country = etree.SubElement(vat_elem, "Страна") # Используем стандартный 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) # Дополнительные поля etree.SubElement(row_elem, "ТипЗапасов").text = "СобственныеТовары" etree.SubElement(row_elem, "СчетУчета").text = "43" etree.SubElement(row_elem, "СчетДоходов").text = "90.01.1" etree.SubElement(row_elem, "СчетРасходов").text = "90.02.1" etree.SubElement(row_elem, "СчетУчетаНДСПоРеализации").text = "90.03" # Уникальный идентификатор строки (uuid уже импортирован выше) etree.SubElement(row_elem, "ИдентификаторСтроки").text = str(uuid.uuid4()) etree.SubElement(row_elem, "ПрослеживаемыйТовар").text = "false" # СпособПогашенияЗадолженности etree.SubElement(doc, "СпособПогашенияЗадолженности").text = "Автоматически" # КомплектПечатныхДокументовПодписанПолучен etree.SubElement(doc, "КомплектПечатныхДокументовПодписанПолучен").text = "false" # ЭтоУПД etree.SubElement(doc, "ЭтоУПД").text = "false" # Конвертация в строку xml_string = etree.tostring( root, pretty_print=True, xml_declaration=True, encoding=self.encoding ).decode(self.encoding) # Сохранение в файл if output_path: with open(output_path, "w", encoding=self.encoding) as f: f.write(xml_string) logger.info(f"Документ реализации 1С сохранен: {output_path}") return xml_string # Пример использования