BUH_bot/modules/xml_generator.py

1707 lines
76 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Генерация 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