BUH_bot/modules/xml_generator.py
Игорь Надеин 2c866e2ada
Some checks failed
Build and Publish Docker Image / build-and-push (push) Has been cancelled
Fix discounted positions in 1C exports
2026-09-16 14:43:12 +03:00

2089 lines
92 KiB
Python
Raw Permalink 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 decimal import Decimal, ROUND_HALF_UP
from typing import Dict, List, Optional, Tuple
import logging
from config import settings
logger = logging.getLogger(__name__)
# Константы для российских кодов и идентификаторов
RUB_ALPHA_CODE = "RUB"
RUB_NUMERIC_CODE = "643"
RUB_FULL_NAME = "Российский рубль"
RUB_SHORT_NAME = "руб."
RUB_CURRENCY_GUID = "72357d18-63a8-11e8-80dd-0050569f5448"
RUSSIA_COUNTRY_GUID = "67700c1b-63a8-11e8-80dd-0050569f5448"
RUSSIA_OKSM_CODE = "643"
RUSSIA_COUNTRY_NAME = "РОССИЯ"
DEFAULT_UOM_GUID = "cdf130e2-63a8-11e8-80dd-0050569f5448"
ACCOUNTING_ACCOUNT_TAGS = (
("СчетУчета", "43"),
("СчетДоходов", "90.01.1"),
("СчетРасходов", "90.02.1"),
("СчетУчетаНДСПоРеализации", "90.03"),
)
def get_currency_codes(currency: Optional[Dict]) -> Tuple[str, str]:
"""Возвращает буквенный и цифровой коды валюты.
При отсутствии данных возвращаются значения по умолчанию.
"""
if not currency:
return RUB_ALPHA_CODE, RUB_NUMERIC_CODE
iso_value = currency.get("isoCode")
if isinstance(iso_value, str):
alpha_code = iso_value.strip().upper()
else:
alpha_code = ""
numeric_value = currency.get("code")
if isinstance(numeric_value, str):
numeric_code = numeric_value.strip()
else:
numeric_code = ""
if not alpha_code and numeric_code == RUB_NUMERIC_CODE:
alpha_code = RUB_ALPHA_CODE
if not numeric_code and alpha_code == RUB_ALPHA_CODE:
numeric_code = RUB_NUMERIC_CODE
return alpha_code or RUB_ALPHA_CODE, numeric_code or RUB_NUMERIC_CODE
def _is_service(assortment: Dict) -> bool:
"""Определяет, является ли номенклатурная позиция услугой.
Проверяет как meta.type, так и meta.href для надёжности.
Args:
assortment: Данные ассортимента из МойСклад
Returns:
True, если это услуга, False в противном случае
"""
meta = (assortment or {}).get("meta") or {}
t = (meta.get("type") or "").lower()
href = (meta.get("href") or "").lower()
return t == "service" or "/service/" in href
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}"
@staticmethod
def _get_preferred_address(data: Dict, fields: List[str]) -> str:
"""Возвращает первый непустой адрес по списку полей."""
for field in fields:
value = data.get(field)
if value:
return value
return ""
def _add_bank_details(
self,
parent: etree.Element,
accounts: Optional[List[Dict]]
) -> None:
"""Добавляет банковские реквизиты (первый счет с БИК)."""
if not accounts:
return
for account in accounts:
if not account.get("bic"):
continue
bank_info = etree.SubElement(
parent,
"БанкРекв",
НомерСчета=account.get("accountNumber", "")
)
etree.SubElement(
bank_info,
"СвБанк",
НаимБанк=account.get("bankName", ""),
БИК=account.get("bic", ""),
КорСчет=account.get("correspondentAccount", "")
)
break
@staticmethod
def _add_contact_details(
parent: etree.Element,
phone: Optional[str] = None,
email: Optional[str] = None
) -> None:
"""Добавляет контактную информацию, если она есть."""
if not phone and not email:
return
contact = etree.SubElement(parent, "Контакт")
if phone:
etree.SubElement(contact, "Тлф").text = phone
if email:
etree.SubElement(contact, "ЭлПочта").text = email
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", "")
_ = 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
)
# Банковские реквизиты
self._add_bank_details(seller, accounts)
# Контактная информация
self._add_contact_details(
seller,
phone=organization.get("phone", ""),
email=organization.get("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 = self._get_preferred_address(
counterparty,
["deliveryAddress", "actualAddress", "legalAddress"]
)
if address:
addr_element = etree.SubElement(shipper, "Адрес")
etree.SubElement(
addr_element, "АдрТекст",
Текст=address
)
# Банковские реквизиты
self._add_bank_details(shipper, accounts)
# Контактная информация
self._add_contact_details(
shipper,
phone=counterparty.get("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 = self._get_preferred_address(
counterparty,
["legalAddress", "actualAddress"]
)
if address:
addr_element = etree.SubElement(buyer, "Адрес")
etree.SubElement(
addr_element, "АдрТекст",
Текст=address
)
# Банковские реквизиты
self._add_bank_details(buyer, accounts)
# Контактная информация
self._add_contact_details(
buyer,
phone=counterparty.get("phone", ""),
email=counterparty.get("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"]
_, currency_numeric_code = get_currency_codes(currency)
etree.SubElement(
invoice_info,
"ДенИзм",
КодОКВ=currency_numeric_code,
НаимОКВ=currency.get("fullName", RUB_FULL_NAME)
)
# Таблица товаров
_ = 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)
# Таблица товаров
_ = 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:
Сумма в рублях с копейками, если они есть
"""
rubles = Decimal(str(amount)) / 100
return str(int(rubles)) if rubles == int(rubles) else f"{rubles:.2f}"
@staticmethod
def _position_amounts(pos: Dict) -> Dict[str, Decimal]:
"""Суммы позиции в рублях с учетом процентной скидки МойСклад."""
price = Decimal(str(pos.get("price", 0))) / 100
quantity = Decimal(str(pos.get("quantity", 0)))
discount = Decimal(str(pos.get("discount") or 0))
vat_rate = Decimal(str(pos.get("vat", 20)))
discounted_price = price * (100 - discount) / 100
gross_total = (price * quantity).quantize(
Decimal("0.01"), rounding=ROUND_HALF_UP
)
total = (discounted_price * quantity).quantize(
Decimal("0.01"), rounding=ROUND_HALF_UP
)
vat_amount = (
(total * vat_rate / (100 + vat_rate)).quantize(
Decimal("0.01"), rounding=ROUND_HALF_UP
) if vat_rate else Decimal("0.00")
)
return {
"price": price,
"discounted_price": discounted_price,
"gross_total": gross_total,
"total": total,
"discount": discount,
"discount_amount": gross_total - total,
"vat_amount": vat_amount,
}
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 = ""
if currency.get("meta"):
currency_href = currency.get("meta", {}).get("href", "")
currency_id = ""
if currency_href:
currency_id = currency_href.split("/")[-1].split("?")[0]
if not currency_id:
currency_id = RUB_CURRENCY_GUID
etree.SubElement(currency_elem, "Ссылка").text = currency_id
alpha_code, numeric_code = get_currency_codes(currency)
currency_data = etree.SubElement(currency_elem, "ДанныеКлассификатора")
etree.SubElement(currency_data, "Код").text = numeric_code
etree.SubElement(
currency_data,
"Наименование"
).text = currency.get("name", RUB_SHORT_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, "СтранаРегистрации")
etree.SubElement(country_elem, "Ссылка").text = RUSSIA_COUNTRY_GUID
country_data = etree.SubElement(
country_elem,
"ДанныеКлассификатора"
)
etree.SubElement(country_data, "Код").text = RUSSIA_OKSM_CODE
etree.SubElement(
country_data,
"Наименование"
).text = RUSSIA_COUNTRY_NAME
# ИндивидуальныйПредприниматель только для ИП (после СтранаРегистрации)
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_for_seed = alpha_code or numeric_code
is_ue = "false" # В текущей реализации всегда false
seed = (
f"contract-{cp_id}-{org_id}-CUR-"
f"{currency_code_for_seed}-UE-{is_ue}"
)
contract_uuid = str(uuid.uuid5(uuid.NAMESPACE_DNS, seed))
logger.info(
f"Создан договор с учётом валюты: "
f"контрагент={cp_id}, организация={org_id}, "
f"валюта={alpha_code}/{numeric_code}, "
f"у.е.={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, "СтранаРегистрации")
etree.SubElement(cp_country, "Ссылка").text = RUSSIA_COUNTRY_GUID
cp_country_data = etree.SubElement(
cp_country,
"ДанныеКлассификатора"
)
etree.SubElement(cp_country_data, "Код").text = RUSSIA_OKSM_CODE
etree.SubElement(
cp_country_data,
"Наименование"
).text = RUSSIA_COUNTRY_NAME
# ИндивидуальныйПредприниматель только для ИП (после СтранаРегистрации)
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 = numeric_code
etree.SubElement(
contract_curr_data,
"Наименование"
).text = currency.get("name", RUB_SHORT_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 = numeric_code
etree.SubElement(
settlement_curr_data,
"Наименование"
).text = currency.get("name", RUB_SHORT_NAME)
etree.SubElement(settlement_elem, "КурсВзаиморасчетов").text = "1"
etree.SubElement(settlement_elem, "КратностьВзаиморасчетов").text = "1"
etree.SubElement(settlement_elem, "РасчетыВУсловныхЕдиницах").text = "false"
# Налогообложение
etree.SubElement(doc, "Налогообложение").text = "ПродажаОблагаетсяНДС"
# Разделяем позиции на товары и услуги
товары = []
услуги = []
for pos in positions:
assortment = pos.get("assortment", {})
if _is_service(assortment):
услуги.append(pos)
else:
товары.append(pos)
# Товары (если есть)
if товары:
goods_elem = etree.SubElement(doc, "Товары")
for pos in товары:
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
# ВидНоменклатуры (для конфигураций УТ 11, ERP)
kind_elem = etree.SubElement(nomenclature, "ВидНоменклатуры")
etree.SubElement(kind_elem, "Наименование").text = "Товар"
# ЕдиницаИзмерения
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)
# Для товаров схема 1С передает скидку отдельными полями.
amounts = self._position_amounts(pos)
# Форматируем с 2 знаками после запятой (НЕ округляем до целых!)
etree.SubElement(row_elem, "Сумма").text = f"{amounts['gross_total']:.2f}"
etree.SubElement(row_elem, "Цена").text = f"{amounts['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 = RUSSIA_COUNTRY_GUID
vat_country_data = etree.SubElement(
vat_country,
"ДанныеКлассификатора"
)
etree.SubElement(vat_country_data, "Код").text = RUSSIA_OKSM_CODE
etree.SubElement(
vat_country_data,
"Наименование"
).text = RUSSIA_COUNTRY_NAME
# СуммаНДС (извлекаем из суммы)
etree.SubElement(row_elem, "СуммаНДС").text = self._format_money_with_decimals(amounts["vat_amount"])
if amounts["discount"]:
etree.SubElement(row_elem, "ВидСкидки").text = "НаТовар"
etree.SubElement(row_elem, "ПроцентСкидки").text = f"{amounts['discount']:.2f}"
etree.SubElement(row_elem, "СуммаСкидки").text = f"{amounts['discount_amount']:.2f}"
etree.SubElement(row_elem, "СуммаСУчетомСкидки").text = f"{amounts['total']:.2f}"
# ТипЗапасов для товаров
etree.SubElement(
row_elem, "ТипЗапасов"
).text = "СобственныеТовары"
# Счета учета (только для товаров)
if settings.include_accounting_accounts:
for tag, value in ACCOUNTING_ACCOUNT_TAGS:
etree.SubElement(row_elem, tag).text = value
# Уникальный идентификатор строки (uuid уже импортирован выше)
etree.SubElement(row_elem, "ИдентификаторСтроки").text = str(uuid.uuid4())
etree.SubElement(row_elem, "ПрослеживаемыйТовар").text = "false"
# Услуги (если есть)
if услуги:
services_elem = etree.SubElement(doc, "Услуги")
for pos in услуги:
row_elem = etree.SubElement(services_elem, "Строка")
assortment = pos.get("assortment", {})
# Очищаем GUID от query-параметров
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 = etree.SubElement(row_elem, "Номенклатура")
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
# Флаг услуги (обязателен для услуг)
etree.SubElement(nomenclature, "Услуга").text = "true"
# ВидНоменклатуры
kind_elem = etree.SubElement(nomenclature, "ВидНоменклатуры")
etree.SubElement(kind_elem, "Наименование").text = "Услуга"
# Характеристика (опционально, может отсутствовать)
# etree.SubElement(row_elem, "Характеристика")
# Количество
quantity = pos.get("quantity", 0)
etree.SubElement(row_elem, "Количество").text = str(quantity)
# В секции услуг 1С нет полей скидки: передаем цену и сумму после скидки.
amounts = self._position_amounts(pos)
# Форматируем с 2 знаками после запятой
etree.SubElement(row_elem, "Сумма").text = f"{amounts['total']:.2f}"
etree.SubElement(row_elem, "Цена").text = f"{amounts['discounted_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 = RUSSIA_COUNTRY_GUID
vat_country_data = etree.SubElement(
vat_country,
"ДанныеКлассификатора"
)
etree.SubElement(vat_country_data, "Код").text = RUSSIA_OKSM_CODE
etree.SubElement(
vat_country_data,
"Наименование"
).text = RUSSIA_COUNTRY_NAME
# СуммаНДС (извлекаем из суммы)
etree.SubElement(row_elem, "СуммаНДС").text = self._format_money_with_decimals(amounts["vat_amount"])
# Содержание (опционально)
etree.SubElement(row_elem, "Содержание").text = product_name
# Счета учета для услуг (немного другие, чем для товаров)
if settings.include_accounting_accounts:
etree.SubElement(row_elem, "СчетДоходов").text = "90.01.1"
etree.SubElement(row_elem, "СчетРасходов").text = "90.02.1"
etree.SubElement(row_elem, "СчетУчетаНДСПоРеализации").text = "90.03"
# Уникальный идентификатор строки
etree.SubElement(row_elem, "ИдентификаторСтроки").text = str(uuid.uuid4())
# СпособПогашенияЗадолженности
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
currency_label = f"{alpha_code}/{numeric_code}"
if numeric_code == RUB_NUMERIC_CODE:
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_label}) "
"→ ожидаются счета 62.21/62.22. "
"Убедитесь, что договор создан с валютой взаиморасчётов "
f"= {currency_label}."
)
# Сохранение в файл
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 = RUB_CURRENCY_GUID
etree.SubElement(currency_elem, "Ссылка").text = currency_id
alpha_code, numeric_code = get_currency_codes(currency)
currency_data = etree.SubElement(
currency_elem, "ДанныеКлассификатора"
)
etree.SubElement(currency_data, "Код").text = numeric_code
etree.SubElement(
currency_data, "Наименование"
).text = currency.get("name", RUB_SHORT_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 = RUSSIA_COUNTRY_GUID
country_data = etree.SubElement(
country_elem,
"ДанныеКлассификатора"
)
etree.SubElement(country_data, "Код").text = RUSSIA_OKSM_CODE
etree.SubElement(
country_data,
"Наименование"
).text = RUSSIA_COUNTRY_NAME
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_for_seed = alpha_code or numeric_code
is_ue = "false"
seed = (
f"contract-{cp_id}-{org_id}-CUR-"
f"{currency_code_for_seed}-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 = RUSSIA_COUNTRY_GUID
cp_country_data = etree.SubElement(
cp_country,
"ДанныеКлассификатора"
)
etree.SubElement(cp_country_data, "Код").text = RUSSIA_OKSM_CODE
etree.SubElement(
cp_country_data,
"Наименование"
).text = RUSSIA_COUNTRY_NAME
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 = numeric_code
etree.SubElement(
contract_curr_data,
"Наименование"
).text = currency.get("name", RUB_SHORT_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 = numeric_code
etree.SubElement(
settlement_curr_data,
"Наименование"
).text = currency.get("name", RUB_SHORT_NAME)
etree.SubElement(
settlement_elem, "КурсВзаиморасчетов"
).text = "1"
etree.SubElement(
settlement_elem, "КратностьВзаиморасчетов"
).text = "1"
etree.SubElement(
settlement_elem, "РасчетыВУсловныхЕдиницах"
).text = "false"
# Налогообложение
etree.SubElement(
doc, "Налогообложение"
).text = "ПродажаОблагаетсяНДС"
# Разделяем позиции на товары и услуги
товары = []
услуги = []
for pos in positions:
assortment = pos.get("assortment", {})
if _is_service(assortment):
услуги.append(pos)
else:
товары.append(pos)
# Секция Товары (если есть товары)
if товары:
goods_elem = etree.SubElement(doc, "Товары")
for pos in товары:
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
# ВидНоменклатуры
kind_elem = etree.SubElement(nomenclature, "ВидНоменклатуры")
etree.SubElement(kind_elem, "Наименование").text = "Товар"
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 = DEFAULT_UOM_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)
amounts = self._position_amounts(pos)
etree.SubElement(row_elem, "Сумма").text = f"{amounts['gross_total']:.2f}"
etree.SubElement(row_elem, "Цена").text = f"{amounts['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 = RUSSIA_COUNTRY_GUID
vat_country_data = etree.SubElement(
vat_country, "ДанныеКлассификатора"
)
etree.SubElement(vat_country_data, "Код").text = RUSSIA_OKSM_CODE
etree.SubElement(
vat_country_data,
"Наименование"
).text = RUSSIA_COUNTRY_NAME
etree.SubElement(
row_elem, "СуммаНДС"
).text = self._format_money_with_decimals(amounts["vat_amount"])
if amounts["discount"]:
etree.SubElement(row_elem, "ВидСкидки").text = "НаТовар"
etree.SubElement(row_elem, "ПроцентСкидки").text = f"{amounts['discount']:.2f}"
etree.SubElement(row_elem, "СуммаСкидки").text = f"{amounts['discount_amount']:.2f}"
etree.SubElement(row_elem, "СуммаСУчетомСкидки").text = f"{amounts['total']:.2f}"
# ТипЗапасов для товаров
etree.SubElement(
row_elem, "ТипЗапасов"
).text = "СобственныеТовары"
# Счета учета (опционально)
if settings.include_accounting_accounts:
for tag, value in ACCOUNTING_ACCOUNT_TAGS:
etree.SubElement(row_elem, tag).text = value
etree.SubElement(
row_elem, "ИдентификаторСтроки"
).text = str(uuid.uuid4())
etree.SubElement(
row_elem, "ПрослеживаемыйТовар"
).text = "false"
# Секция Услуги (если есть услуги)
if услуги:
services_elem = etree.SubElement(doc, "Услуги")
for pos in услуги:
row_elem = etree.SubElement(services_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 = etree.SubElement(row_elem, "Номенклатура")
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
# Флаг услуги
etree.SubElement(nomenclature, "Услуга").text = "true"
# ВидНоменклатуры
kind_elem = etree.SubElement(nomenclature, "ВидНоменклатуры")
etree.SubElement(kind_elem, "Наименование").text = "Услуга"
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 = DEFAULT_UOM_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 "шт"
# Содержание для услуг
etree.SubElement(row_elem, "Содержание").text = product_name
quantity = pos.get("quantity", 0)
etree.SubElement(row_elem, "Количество").text = str(quantity)
amounts = self._position_amounts(pos)
etree.SubElement(row_elem, "Сумма").text = f"{amounts['total']:.2f}"
etree.SubElement(row_elem, "Цена").text = f"{amounts['discounted_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 = RUSSIA_COUNTRY_GUID
vat_country_data = etree.SubElement(
vat_country, "ДанныеКлассификатора"
)
etree.SubElement(vat_country_data, "Код").text = RUSSIA_OKSM_CODE
etree.SubElement(
vat_country_data,
"Наименование"
).text = RUSSIA_COUNTRY_NAME
etree.SubElement(
row_elem, "СуммаНДС"
).text = self._format_money_with_decimals(amounts["vat_amount"])
# Счета учета для услуг (БЕЗ счета 43)
if settings.include_accounting_accounts:
etree.SubElement(row_elem, "СчетДоходов").text = "90.01.1"
etree.SubElement(row_elem, "СчетРасходов").text = "90.02.1"
etree.SubElement(row_elem, "СчетУчетаНДСПоРеализации").text = "90.03"
etree.SubElement(
row_elem, "ИдентификаторСтроки"
).text = str(uuid.uuid4())
# СпособПогашенияЗадолженности
etree.SubElement(
doc, "СпособПогашенияЗадолженности"
).text = "Автоматически"
# КомплектПечатныхДокументовПодписанПолучен
etree.SubElement(
doc, "КомплектПечатныхДокументовПодписанПолучен"
).text = "false"
# ЭтоУПД
etree.SubElement(doc, "ЭтоУПД").text = "false"
return doc