1105 lines
48 KiB
Python
1105 lines
48 KiB
Python
"""
|
||
Генерация XML документов в формате ВерсФорм 5.03
|
||
УПД (Универсальный передаточный документ) и ТН (Товарная накладная)
|
||
"""
|
||
from lxml import etree
|
||
from datetime import datetime
|
||
from typing import Dict, Optional
|
||
import logging
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class XMLGenerator:
|
||
"""Генератор XML документов в формате ВерсФорм 5.03"""
|
||
|
||
VERSION = "5.03"
|
||
PROGRAM_VERSION = "BUH_BOT_1.0"
|
||
|
||
def __init__(self):
|
||
self.encoding = "UTF-8"
|
||
|
||
def _format_date(self, date_str: str) -> str:
|
||
"""
|
||
Форматирование даты в формат ДД.ММ.ГГГГ
|
||
|
||
Args:
|
||
date_str: Дата в ISO формате
|
||
|
||
Returns:
|
||
Дата в формате ДД.ММ.ГГГГ
|
||
"""
|
||
try:
|
||
dt = datetime.fromisoformat(date_str.replace('Z', '+00:00'))
|
||
return dt.strftime("%d.%m.%Y")
|
||
except Exception as e:
|
||
logger.error(f"Ошибка форматирования даты: {e}")
|
||
return datetime.now().strftime("%d.%m.%Y")
|
||
|
||
def _format_money(self, amount: int) -> str:
|
||
"""
|
||
Форматирование денежной суммы (копейки -> рубли)
|
||
|
||
Args:
|
||
amount: Сумма в копейках
|
||
|
||
Returns:
|
||
Сумма в рублях с 2 знаками после запятой
|
||
"""
|
||
return f"{amount / 100:.2f}"
|
||
|
||
def _add_seller_info(
|
||
self,
|
||
parent: etree.Element,
|
||
organization: Dict,
|
||
accounts: list = None
|
||
) -> None:
|
||
"""
|
||
Добавление информации о продавце
|
||
|
||
Args:
|
||
parent: Родительский XML элемент
|
||
organization: Данные организации
|
||
accounts: Список банковских счетов
|
||
"""
|
||
seller = etree.SubElement(parent, "СвПрод")
|
||
|
||
# ОКПО
|
||
okpo = organization.get("okpo", "")
|
||
if okpo:
|
||
seller.set("ОКПО", okpo)
|
||
|
||
# ИдСв - зависит от типа организации
|
||
seller_info = etree.SubElement(seller, "ИдСв")
|
||
|
||
company_type = organization.get("companyType", "")
|
||
inn = organization.get("inn", "")
|
||
|
||
if company_type == "entrepreneur":
|
||
# Индивидуальный предприниматель
|
||
ogrnip = organization.get("ogrnip", "")
|
||
sv_ip = etree.SubElement(
|
||
seller_info, "СвИП",
|
||
ИННФЛ=inn,
|
||
СвГосРегИП=ogrnip if ogrnip else ""
|
||
)
|
||
|
||
# ФИО
|
||
last_name = organization.get("legalLastName", "")
|
||
first_name = organization.get("legalFirstName", "")
|
||
middle_name = organization.get("legalMiddleName", "")
|
||
|
||
if last_name or first_name:
|
||
fio = etree.SubElement(sv_ip, "ФИО")
|
||
if last_name:
|
||
fio.set("Фамилия", last_name)
|
||
if first_name:
|
||
fio.set("Имя", first_name)
|
||
if middle_name:
|
||
fio.set("Отчество", middle_name)
|
||
else:
|
||
# Юридическое лицо
|
||
kpp = organization.get("kpp", "")
|
||
sv_ul = etree.SubElement(
|
||
seller_info, "СвЮЛУч",
|
||
НаимОрг=organization.get("name", ""),
|
||
ИННЮЛ=inn,
|
||
КПП=kpp if kpp else ""
|
||
)
|
||
|
||
# Адрес
|
||
address = organization.get("legalAddress", "")
|
||
if address:
|
||
addr_element = etree.SubElement(seller, "Адрес")
|
||
etree.SubElement(
|
||
addr_element, "АдрТекст",
|
||
Текст=address
|
||
)
|
||
|
||
# Банковские реквизиты
|
||
if accounts and len(accounts) > 0:
|
||
# Берём первый счёт с БИК
|
||
for acc in accounts:
|
||
if acc.get('bic'):
|
||
bank_info = etree.SubElement(
|
||
seller, "БанкРекв",
|
||
НомерСчета=acc.get('accountNumber', '')
|
||
)
|
||
etree.SubElement(
|
||
bank_info, "СвБанк",
|
||
НаимБанк=acc.get('bankName', ''),
|
||
БИК=acc.get('bic', ''),
|
||
КорСчет=acc.get('correspondentAccount', '')
|
||
)
|
||
break
|
||
|
||
# Контактная информация
|
||
phone = organization.get("phone", "")
|
||
email = organization.get("email", "")
|
||
if phone or email:
|
||
contact = etree.SubElement(seller, "Контакт")
|
||
if phone:
|
||
etree.SubElement(contact, "Тлф").text = phone
|
||
if email:
|
||
etree.SubElement(contact, "ЭлПочта").text = email
|
||
|
||
def _add_shipper_info(
|
||
self,
|
||
parent: etree.Element,
|
||
counterparty: Dict,
|
||
accounts: list = None
|
||
) -> None:
|
||
"""
|
||
Добавление информации о грузополучателе
|
||
|
||
Args:
|
||
parent: Родительский XML элемент
|
||
counterparty: Данные контрагента
|
||
accounts: Список банковских счетов
|
||
"""
|
||
shipper = etree.SubElement(parent, "ГрузПолуч")
|
||
|
||
# ОКПО
|
||
okpo = counterparty.get("okpo", "")
|
||
if okpo:
|
||
shipper.set("ОКПО", okpo)
|
||
|
||
# ИНН/КПП и определение типа компании
|
||
inn = counterparty.get("inn", "")
|
||
kpp = counterparty.get("kpp", "")
|
||
company_type = counterparty.get("companyType", "legal")
|
||
|
||
shipper_info = etree.SubElement(shipper, "ИдСв")
|
||
|
||
# Для ИП используем структуру СвИП
|
||
if company_type == "entrepreneur":
|
||
ogrnip = counterparty.get("ogrnip", "")
|
||
|
||
sv_ip = etree.SubElement(
|
||
shipper_info, "СвИП",
|
||
ИННФЛ=inn,
|
||
СвГосРегИП=ogrnip
|
||
)
|
||
|
||
# ФИО для ИП
|
||
last_name = counterparty.get("legalLastName", "")
|
||
first_name = counterparty.get("legalFirstName", "")
|
||
middle_name = counterparty.get("legalMiddleName", "")
|
||
|
||
fio = etree.SubElement(sv_ip, "ФИО")
|
||
fio.set("Фамилия", last_name)
|
||
fio.set("Имя", first_name)
|
||
if middle_name:
|
||
fio.set("Отчество", middle_name)
|
||
else:
|
||
# Для юрлиц используем СвЮЛУч
|
||
shipper_info.set("СвЮЛУч", inn)
|
||
shipper_info.set("КПП", kpp if kpp else "")
|
||
|
||
# Наименование
|
||
name = counterparty.get("name", "")
|
||
if not name:
|
||
name = counterparty.get("legalTitle", "")
|
||
etree.SubElement(shipper_info, "НаимОрг", Наим=name)
|
||
|
||
# Адрес
|
||
address = counterparty.get("legalAddress", "")
|
||
if address:
|
||
addr_element = etree.SubElement(shipper, "Адрес")
|
||
etree.SubElement(
|
||
addr_element, "АдрТекст",
|
||
Текст=address
|
||
)
|
||
|
||
# Банковские реквизиты
|
||
if accounts and len(accounts) > 0:
|
||
for acc in accounts:
|
||
if acc.get('bic'):
|
||
bank_info = etree.SubElement(
|
||
shipper, "БанкРекв",
|
||
НомерСчета=acc.get('accountNumber', '')
|
||
)
|
||
etree.SubElement(
|
||
bank_info, "СвБанк",
|
||
НаимБанк=acc.get('bankName', ''),
|
||
БИК=acc.get('bic', ''),
|
||
КорСчет=acc.get('correspondentAccount', '')
|
||
)
|
||
break
|
||
|
||
# Контактная информация
|
||
phone = counterparty.get("phone", "")
|
||
if phone:
|
||
contact = etree.SubElement(shipper, "Контакт")
|
||
etree.SubElement(contact, "Тлф").text = phone
|
||
|
||
def _add_buyer_info(
|
||
self,
|
||
parent: etree.Element,
|
||
counterparty: Dict,
|
||
accounts: list = None
|
||
) -> None:
|
||
"""
|
||
Добавление информации о покупателе
|
||
|
||
Args:
|
||
parent: Родительский XML элемент
|
||
counterparty: Данные контрагента
|
||
accounts: Список банковских счетов
|
||
"""
|
||
buyer = etree.SubElement(parent, "СвПокуп")
|
||
|
||
# ОКПО
|
||
okpo = counterparty.get("okpo", "")
|
||
if okpo:
|
||
buyer.set("ОКПО", okpo)
|
||
|
||
# ИНН/КПП и определение типа компании
|
||
inn = counterparty.get("inn", "")
|
||
kpp = counterparty.get("kpp", "")
|
||
company_type = counterparty.get("companyType", "legal")
|
||
|
||
buyer_info = etree.SubElement(buyer, "ИдСв")
|
||
|
||
# Для ИП используем структуру СвИП
|
||
if company_type == "entrepreneur":
|
||
ogrnip = counterparty.get("ogrnip", "")
|
||
|
||
sv_ip = etree.SubElement(
|
||
buyer_info, "СвИП",
|
||
ИННФЛ=inn,
|
||
СвГосРегИП=ogrnip
|
||
)
|
||
|
||
# ФИО для ИП
|
||
last_name = counterparty.get("legalLastName", "")
|
||
first_name = counterparty.get("legalFirstName", "")
|
||
middle_name = counterparty.get("legalMiddleName", "")
|
||
|
||
fio = etree.SubElement(sv_ip, "ФИО")
|
||
fio.set("Фамилия", last_name)
|
||
fio.set("Имя", first_name)
|
||
if middle_name:
|
||
fio.set("Отчество", middle_name)
|
||
else:
|
||
# Для юрлиц используем СвЮЛУч
|
||
buyer_info.set("СвЮЛУч", inn)
|
||
buyer_info.set("КПП", kpp if kpp else "")
|
||
|
||
# Наименование
|
||
name = counterparty.get("name", "")
|
||
if not name:
|
||
name = counterparty.get("legalTitle", "")
|
||
etree.SubElement(buyer_info, "НаимОрг", Наим=name)
|
||
|
||
# Адрес
|
||
address = counterparty.get("legalAddress", "")
|
||
if address:
|
||
addr_element = etree.SubElement(buyer, "Адрес")
|
||
etree.SubElement(
|
||
addr_element, "АдрТекст",
|
||
Текст=address
|
||
)
|
||
|
||
# Банковские реквизиты
|
||
if accounts and len(accounts) > 0:
|
||
# Берём первый счёт с БИК
|
||
for acc in accounts:
|
||
if acc.get('bic'):
|
||
bank_info = etree.SubElement(
|
||
buyer, "БанкРекв",
|
||
НомерСчета=acc.get('accountNumber', '')
|
||
)
|
||
etree.SubElement(
|
||
bank_info, "СвБанк",
|
||
НаимБанк=acc.get('bankName', ''),
|
||
БИК=acc.get('bic', ''),
|
||
КорСчет=acc.get('correspondentAccount', '')
|
||
)
|
||
break
|
||
|
||
# Контактная информация
|
||
phone = counterparty.get("phone", "")
|
||
email = counterparty.get("email", "")
|
||
if phone or email:
|
||
contact = etree.SubElement(buyer, "Контакт")
|
||
if phone:
|
||
etree.SubElement(contact, "Тлф").text = phone
|
||
if email:
|
||
etree.SubElement(contact, "ЭлПочта").text = email
|
||
|
||
def _add_positions_table(
|
||
self,
|
||
parent: etree.Element,
|
||
positions: list
|
||
) -> Dict[str, float]:
|
||
"""
|
||
Добавление таблицы товаров
|
||
|
||
Args:
|
||
parent: Родительский XML элемент
|
||
positions: Список позиций заказа
|
||
|
||
Returns:
|
||
Словарь с итоговыми суммами
|
||
"""
|
||
table = etree.SubElement(parent, "ТаблСчФакт")
|
||
|
||
total_without_vat = 0
|
||
total_vat = 0
|
||
total_with_vat = 0
|
||
|
||
for index, pos in enumerate(positions, start=1):
|
||
assortment = pos.get("assortment", {})
|
||
|
||
# Данные товара
|
||
name = assortment.get("name", "Товар без наименования")
|
||
quantity = pos.get("quantity", 0)
|
||
# Цена из API уже включает НДС!
|
||
price = pos.get("price", 0) / 100
|
||
vat_rate = pos.get("vat", 20)
|
||
|
||
# Расчет сумм (цена из API уже включает НДС!)
|
||
total_amount = quantity * price # Сумма С НДС
|
||
# Извлекаем НДС из общей суммы
|
||
amount = (total_amount / (1 + vat_rate / 100)
|
||
if vat_rate else total_amount)
|
||
vat_amount = total_amount - amount # Сумма НДС
|
||
|
||
total_without_vat += amount
|
||
total_vat += vat_amount
|
||
total_with_vat += total_amount
|
||
|
||
# Создание строки таблицы
|
||
row = etree.SubElement(
|
||
table, "СведТов",
|
||
НомСтр=str(index)
|
||
)
|
||
|
||
# Наименование товара
|
||
etree.SubElement(row, "НаимТов", Наим=name)
|
||
|
||
# Единица измерения
|
||
uom = assortment.get("uom", {})
|
||
uom_name = uom.get("name", "шт") if uom else "шт"
|
||
etree.SubElement(row, "ОКЕИ_Тов", НаимЕдИзм=uom_name)
|
||
|
||
# Количество
|
||
etree.SubElement(row, "КолТов", Количество=str(quantity))
|
||
|
||
# Цена
|
||
etree.SubElement(row, "ЦенаТов", Цена=f"{price:.2f}")
|
||
|
||
# Стоимость без НДС
|
||
etree.SubElement(row, "СтТовБезНДС", СтоимБезНДС=f"{amount:.2f}")
|
||
|
||
# НДС
|
||
etree.SubElement(
|
||
row, "НалСт",
|
||
СтавНДС=str(vat_rate),
|
||
СумНДС=f"{vat_amount:.2f}"
|
||
)
|
||
|
||
# Стоимость с НДС
|
||
etree.SubElement(
|
||
row, "СтТовУчНал",
|
||
СтоимУчНДС=f"{total_amount:.2f}"
|
||
)
|
||
|
||
# Итоговые суммы
|
||
totals = etree.SubElement(table, "ВсегоОпл")
|
||
etree.SubElement(
|
||
totals, "СтТовБезНДСВсего",
|
||
СтоимБезНДС=f"{total_without_vat:.2f}"
|
||
)
|
||
etree.SubElement(
|
||
totals, "СумНДСВсего",
|
||
СумНДС=f"{total_vat:.2f}"
|
||
)
|
||
etree.SubElement(
|
||
totals, "СтТовУчНалВсего",
|
||
СтоимУчНДС=f"{total_with_vat:.2f}"
|
||
)
|
||
|
||
return {
|
||
"without_vat": total_without_vat,
|
||
"vat": total_vat,
|
||
"with_vat": total_with_vat
|
||
}
|
||
|
||
def generate_upd(
|
||
self,
|
||
order_data: Dict,
|
||
output_path: Optional[str] = None
|
||
) -> str:
|
||
"""
|
||
Генерация УПД (Универсальный передаточный документ)
|
||
|
||
Args:
|
||
order_data: Полные данные заказа
|
||
output_path: Путь для сохранения файла
|
||
|
||
Returns:
|
||
XML в виде строки
|
||
"""
|
||
order = order_data["order"]
|
||
organization = order_data["organization"]
|
||
counterparty = order_data["counterparty"]
|
||
positions = order_data["positions"]
|
||
org_accounts = order_data.get("org_accounts", [])
|
||
cp_accounts = order_data.get("cp_accounts", [])
|
||
|
||
# Корневой элемент
|
||
root = etree.Element(
|
||
"Файл",
|
||
ВерсПрог=self.PROGRAM_VERSION,
|
||
ВерсФорм=self.VERSION
|
||
)
|
||
|
||
# Документ
|
||
doc = etree.SubElement(root, "Документ")
|
||
|
||
# Сведения о счет-фактуре
|
||
invoice_info = etree.SubElement(doc, "СвСчФакт")
|
||
|
||
# Номер и дата
|
||
order_number = order.get("name", "б/н")
|
||
order_date = self._format_date(order.get("moment", ""))
|
||
|
||
etree.SubElement(
|
||
invoice_info, "НомерДата",
|
||
Номер=order_number,
|
||
Дата=order_date
|
||
)
|
||
|
||
# Информация о продавце
|
||
self._add_seller_info(invoice_info, organization, org_accounts)
|
||
|
||
# Грузоотправитель (он же продавец)
|
||
gruz_ot = etree.SubElement(invoice_info, "ГрузОт")
|
||
etree.SubElement(gruz_ot, "ОнЖе").text = "он же"
|
||
|
||
# Грузополучатель (покупатель)
|
||
self._add_shipper_info(invoice_info, counterparty, cp_accounts)
|
||
|
||
# Документ подтверждающий отгрузку
|
||
etree.SubElement(
|
||
invoice_info, "ДокПодтвОтгрНом",
|
||
РеквНаимДок="УПД",
|
||
РеквНомерДок=order_number,
|
||
РеквДатаДок=order_date
|
||
)
|
||
|
||
# Информация о покупателе
|
||
self._add_buyer_info(invoice_info, counterparty, cp_accounts)
|
||
|
||
# Валюта
|
||
rate = order.get("rate", {})
|
||
if rate and "currency" in rate:
|
||
currency = rate["currency"]
|
||
etree.SubElement(
|
||
invoice_info, "ДенИзм",
|
||
КодОКВ=currency.get("code", "643"),
|
||
НаимОКВ=currency.get("fullName", "Российский рубль")
|
||
)
|
||
|
||
# Таблица товаров
|
||
totals = self._add_positions_table(doc, positions)
|
||
|
||
# Сведения о передаче товара
|
||
sv_prod_per = etree.SubElement(doc, "СвПродПер")
|
||
sv_per = etree.SubElement(
|
||
sv_prod_per, "СвПер",
|
||
СодОпер="Товары переданы",
|
||
ДатаПер=order_date
|
||
)
|
||
# Без документа-основания передачи
|
||
etree.SubElement(sv_per, "БезДокОснПер").text = "1"
|
||
|
||
# Подписант
|
||
podpisant = etree.SubElement(doc, "Подписант", СпосПодтПолном="6")
|
||
# Используем данные из организации
|
||
if organization.get("companyType") == "entrepreneur":
|
||
# Для ИП используем ФИО
|
||
fio = etree.SubElement(podpisant, "ФИО")
|
||
last_name = organization.get("legalLastName", "")
|
||
first_name = organization.get("legalFirstName", "")
|
||
middle_name = organization.get("legalMiddleName", "")
|
||
if last_name:
|
||
fio.set("Фамилия", last_name)
|
||
if first_name:
|
||
fio.set("Имя", first_name)
|
||
if middle_name:
|
||
fio.set("Отчество", middle_name)
|
||
else:
|
||
# Для ЮЛ можем указать директора (если данные есть)
|
||
# Пока просто заглушка
|
||
fio = etree.SubElement(podpisant, "ФИО")
|
||
fio.set("Фамилия", "Подписант")
|
||
fio.set("Имя", "Представитель")
|
||
|
||
# Конвертация в строку
|
||
xml_string = etree.tostring(
|
||
root,
|
||
pretty_print=True,
|
||
xml_declaration=True,
|
||
encoding=self.encoding
|
||
).decode(self.encoding)
|
||
|
||
# Сохранение в файл
|
||
if output_path:
|
||
with open(output_path, "w", encoding=self.encoding) as f:
|
||
f.write(xml_string)
|
||
logger.info(f"УПД сохранен: {output_path}")
|
||
|
||
return xml_string
|
||
|
||
def generate_tn(
|
||
self,
|
||
order_data: Dict,
|
||
output_path: Optional[str] = None
|
||
) -> str:
|
||
"""
|
||
Генерация ТН (Товарная накладная ТОРГ-12)
|
||
|
||
Args:
|
||
order_data: Полные данные заказа
|
||
output_path: Путь для сохранения файла
|
||
|
||
Returns:
|
||
XML в виде строки
|
||
"""
|
||
order = order_data["order"]
|
||
organization = order_data["organization"]
|
||
counterparty = order_data["counterparty"]
|
||
positions = order_data["positions"]
|
||
|
||
# Корневой элемент
|
||
root = etree.Element(
|
||
"Файл",
|
||
ВерсПрог=self.PROGRAM_VERSION,
|
||
ВерсФорм=self.VERSION,
|
||
ТипДок="ТОРГ-12"
|
||
)
|
||
|
||
# Документ
|
||
doc = etree.SubElement(root, "Документ")
|
||
|
||
# Номер и дата
|
||
order_number = order.get("name", "б/н")
|
||
order_date = self._format_date(order.get("moment", ""))
|
||
|
||
doc_info = etree.SubElement(doc, "СвНакл")
|
||
etree.SubElement(
|
||
doc_info, "НомерДата",
|
||
Номер=order_number,
|
||
Дата=order_date
|
||
)
|
||
|
||
# Информация о продавце
|
||
self._add_seller_info(doc, organization)
|
||
|
||
# Информация о покупателе
|
||
self._add_buyer_info(doc, counterparty)
|
||
|
||
# Таблица товаров
|
||
totals = self._add_positions_table(doc, positions)
|
||
|
||
# Конвертация в строку
|
||
xml_string = etree.tostring(
|
||
root,
|
||
pretty_print=True,
|
||
xml_declaration=True,
|
||
encoding=self.encoding
|
||
).decode(self.encoding)
|
||
|
||
# Сохранение в файл
|
||
if output_path:
|
||
with open(output_path, "w", encoding=self.encoding) as f:
|
||
f.write(xml_string)
|
||
logger.info(f"ТН сохранена: {output_path}")
|
||
|
||
return xml_string
|
||
|
||
|
||
class XML1CGenerator:
|
||
"""Генератор XML документов в формате 1С EnterpriseData 1.20"""
|
||
|
||
FORMAT_VERSION = "1.20"
|
||
FORMAT_URI = "http://v8.1c.ru/edi/edi_stnd/EnterpriseData/1.20"
|
||
|
||
def __init__(self):
|
||
self.encoding = "UTF-8"
|
||
# Регистрация namespace
|
||
self.nsmap = {
|
||
None: self.FORMAT_URI,
|
||
'msg': 'http://www.1c.ru/SSL/Exchange/Message',
|
||
'xs': 'http://www.w3.org/2001/XMLSchema',
|
||
'xsi': 'http://www.w3.org/2001/XMLSchema-instance'
|
||
}
|
||
|
||
def _format_datetime(self, date_str: str) -> str:
|
||
"""
|
||
Форматирование даты в ISO формат для 1С
|
||
|
||
Args:
|
||
date_str: Дата в ISO формате
|
||
|
||
Returns:
|
||
Дата в формате ISO 8601 (YYYY-MM-DDTHH:MM:SS)
|
||
"""
|
||
try:
|
||
dt = datetime.fromisoformat(date_str.replace('Z', '+00:00'))
|
||
return dt.strftime("%Y-%m-%dT%H:%M:%S")
|
||
except Exception as e:
|
||
logger.error(f"Ошибка форматирования даты: {e}")
|
||
return datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
|
||
|
||
def _format_money_1c(self, amount: int) -> str:
|
||
"""
|
||
Форматирование денежной суммы для 1С (копейки -> рубли без копеек)
|
||
|
||
Args:
|
||
amount: Сумма в копейках
|
||
|
||
Returns:
|
||
Сумма в рублях (целое число)
|
||
"""
|
||
return str(int(amount / 100))
|
||
|
||
def _format_money_with_decimals(self, amount: float) -> str:
|
||
"""
|
||
Форматирование денежной суммы с копейками
|
||
|
||
Args:
|
||
amount: Сумма в рублях
|
||
|
||
Returns:
|
||
Сумма с 2 знаками после запятой
|
||
"""
|
||
return f"{amount:.2f}"
|
||
|
||
def generate_realization(
|
||
self,
|
||
order_data: Dict,
|
||
output_path: Optional[str] = None
|
||
) -> str:
|
||
"""
|
||
Генерация документа Реализация товаров и услуг в формате 1С
|
||
|
||
Args:
|
||
order_data: Полные данные заказа из МойСклад
|
||
output_path: Путь для сохранения файла
|
||
|
||
Returns:
|
||
XML в виде строки
|
||
"""
|
||
order = order_data["order"]
|
||
organization = order_data["organization"]
|
||
counterparty = order_data["counterparty"]
|
||
positions = order_data["positions"]
|
||
org_accounts = order_data.get("org_accounts", [])
|
||
|
||
# Корневой элемент Message
|
||
root = etree.Element(
|
||
"Message",
|
||
nsmap={
|
||
'msg': self.nsmap['msg'],
|
||
'xs': self.nsmap['xs'],
|
||
'xsi': self.nsmap['xsi']
|
||
}
|
||
)
|
||
|
||
# Header
|
||
header = etree.SubElement(root, "{%s}Header" % self.nsmap['msg'])
|
||
etree.SubElement(header, "{%s}Format" % self.nsmap['msg']).text = self.FORMAT_URI
|
||
etree.SubElement(header, "{%s}CreationDate" % self.nsmap['msg']).text = datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
|
||
etree.SubElement(header, "{%s}AvailableVersion" % self.nsmap['msg']).text = self.FORMAT_VERSION
|
||
|
||
# Body
|
||
body = etree.SubElement(root, "Body", nsmap={None: self.FORMAT_URI})
|
||
|
||
# Документ.РеализацияТоваровУслуг
|
||
doc = etree.SubElement(body, "Документ.РеализацияТоваровУслуг")
|
||
|
||
# КлючевыеСвойства
|
||
key_props = etree.SubElement(doc, "КлючевыеСвойства")
|
||
|
||
# Ссылка (UUID документа)
|
||
order_id = order.get("id", "")
|
||
etree.SubElement(key_props, "Ссылка").text = order_id
|
||
|
||
# Дата и Номер
|
||
order_date = self._format_datetime(order.get("moment", ""))
|
||
order_number = order.get("name", "")
|
||
etree.SubElement(key_props, "Дата").text = order_date
|
||
etree.SubElement(key_props, "Номер").text = order_number.ljust(11) # Паддинг пробелами
|
||
|
||
# Организация
|
||
org_elem = etree.SubElement(key_props, "Организация")
|
||
org_id = organization.get("id", "")
|
||
etree.SubElement(org_elem, "Ссылка").text = org_id
|
||
etree.SubElement(org_elem, "Наименование").text = organization.get("name", "")
|
||
|
||
# Полные наименования для ИП
|
||
company_type = organization.get("companyType", "")
|
||
if company_type == "entrepreneur":
|
||
legal_name = organization.get("legalTitle", "")
|
||
etree.SubElement(org_elem, "НаименованиеСокращенное").text = legal_name
|
||
etree.SubElement(org_elem, "НаименованиеПолное").text = legal_name
|
||
else:
|
||
etree.SubElement(org_elem, "НаименованиеСокращенное").text = organization.get("name", "")
|
||
etree.SubElement(org_elem, "НаименованиеПолное").text = organization.get("legalTitle", "")
|
||
|
||
etree.SubElement(org_elem, "ИНН").text = organization.get("inn", "")
|
||
|
||
# Тип лица
|
||
if company_type == "entrepreneur":
|
||
etree.SubElement(org_elem, "ЮридическоеФизическоеЛицо").text = "ФизическоеЛицо"
|
||
else:
|
||
etree.SubElement(org_elem, "ЮридическоеФизическоеЛицо").text = "ЮридическоеЛицо"
|
||
|
||
# ВидОперации
|
||
etree.SubElement(doc, "ВидОперации").text = "РеализацияКлиенту"
|
||
|
||
# Валюта
|
||
rate = order.get("rate", {})
|
||
currency = rate.get("currency", {}) if rate else {}
|
||
currency_elem = etree.SubElement(doc, "Валюта")
|
||
currency_id = currency.get("meta", {}).get("href", "").split("/")[-1] if currency.get("meta") else ""
|
||
etree.SubElement(currency_elem, "Ссылка").text = currency_id
|
||
|
||
currency_data = etree.SubElement(currency_elem, "ДанныеКлассификатора")
|
||
etree.SubElement(currency_data, "Код").text = currency.get("isoCode", "643")
|
||
etree.SubElement(currency_data, "Наименование").text = currency.get("name", "руб.")
|
||
|
||
# Сумма (в рублях, без копеек)
|
||
order_sum = order.get("sum", 0)
|
||
etree.SubElement(doc, "Сумма").text = self._format_money_1c(order_sum)
|
||
|
||
# СуммаВключаетНДС
|
||
etree.SubElement(doc, "СуммаВключаетНДС").text = "true"
|
||
|
||
# Склад (если есть)
|
||
store = order.get("store", {})
|
||
if store:
|
||
store_elem = etree.SubElement(doc, "Склад")
|
||
store_id = store.get("meta", {}).get("href", "").split("/")[-1] if store.get("meta") else ""
|
||
etree.SubElement(store_elem, "Ссылка").text = store_id
|
||
etree.SubElement(store_elem, "Наименование").text = store.get("name", "Основной склад")
|
||
etree.SubElement(store_elem, "ТипСклада").text = "Оптовый"
|
||
|
||
# Руководитель (для ИП - сам ИП)
|
||
if company_type == "entrepreneur":
|
||
chief_elem = etree.SubElement(doc, "Руководитель")
|
||
etree.SubElement(chief_elem, "Ссылка").text = org_id
|
||
|
||
# ФИО
|
||
fio_parts = []
|
||
last_name = organization.get("legalLastName", "")
|
||
first_name = organization.get("legalFirstName", "")
|
||
middle_name = organization.get("legalMiddleName", "")
|
||
|
||
if last_name:
|
||
fio_parts.append(last_name)
|
||
if first_name:
|
||
fio_parts.append(first_name)
|
||
if middle_name:
|
||
fio_parts.append(middle_name)
|
||
|
||
full_fio = " ".join(fio_parts)
|
||
etree.SubElement(chief_elem, "ФИО").text = full_fio
|
||
etree.SubElement(chief_elem, "Фамилия").text = last_name
|
||
etree.SubElement(chief_elem, "Имя").text = first_name
|
||
etree.SubElement(chief_elem, "Отчество").text = middle_name
|
||
etree.SubElement(chief_elem, "ИНН").text = organization.get("inn", "")
|
||
|
||
# БанковскийСчетОрганизации
|
||
if org_accounts and len(org_accounts) > 0:
|
||
acc = org_accounts[0]
|
||
bank_acc_elem = etree.SubElement(doc, "БанковскийСчетОрганизации")
|
||
acc_id = acc.get("id", "")
|
||
etree.SubElement(bank_acc_elem, "Ссылка").text = acc_id
|
||
etree.SubElement(bank_acc_elem, "НомерСчета").text = acc.get("accountNumber", "")
|
||
|
||
# Банк
|
||
bank_elem = etree.SubElement(bank_acc_elem, "Банк")
|
||
etree.SubElement(bank_elem, "Ссылка").text = acc_id # Используем тот же ID
|
||
|
||
bank_data = etree.SubElement(bank_elem, "ДанныеКлассификатораБанков")
|
||
etree.SubElement(bank_data, "Наименование").text = acc.get("bankName", "")
|
||
etree.SubElement(bank_data, "БИК").text = acc.get("bic", "")
|
||
etree.SubElement(bank_data, "КоррСчет").text = acc.get("correspondentAccount", "")
|
||
|
||
# Владелец
|
||
owner_elem = etree.SubElement(bank_acc_elem, "Владелец")
|
||
org_ref = etree.SubElement(owner_elem, "ОрганизацииСсылка")
|
||
etree.SubElement(org_ref, "Ссылка").text = org_id
|
||
etree.SubElement(org_ref, "Наименование").text = organization.get("name", "")
|
||
|
||
if company_type == "entrepreneur":
|
||
etree.SubElement(org_ref, "НаименованиеСокращенное").text = organization.get("legalTitle", "")
|
||
etree.SubElement(org_ref, "НаименованиеПолное").text = organization.get("legalTitle", "")
|
||
etree.SubElement(org_ref, "ЮридическоеФизическоеЛицо").text = "ФизическоеЛицо"
|
||
else:
|
||
etree.SubElement(org_ref, "НаименованиеСокращенное").text = organization.get("name", "")
|
||
etree.SubElement(org_ref, "НаименованиеПолное").text = organization.get("legalTitle", "")
|
||
etree.SubElement(org_ref, "ЮридическоеФизическоеЛицо").text = "ЮридическоеЛицо"
|
||
|
||
etree.SubElement(org_ref, "ИНН").text = organization.get("inn", "")
|
||
|
||
# Контрагент
|
||
cp_elem = etree.SubElement(doc, "Контрагент")
|
||
cp_id = counterparty.get("id", "")
|
||
etree.SubElement(cp_elem, "Ссылка").text = cp_id
|
||
etree.SubElement(cp_elem, "Наименование").text = counterparty.get("name", "")
|
||
etree.SubElement(cp_elem, "НаименованиеПолное").text = counterparty.get("legalTitle", counterparty.get("name", ""))
|
||
etree.SubElement(cp_elem, "ИНН").text = counterparty.get("inn", "")
|
||
|
||
cp_type = counterparty.get("companyType", "legal")
|
||
if cp_type == "entrepreneur":
|
||
etree.SubElement(cp_elem, "ЮридическоеФизическоеЛицо").text = "ФизическоеЛицо"
|
||
etree.SubElement(cp_elem, "ИндивидуальныйПредприниматель").text = "true"
|
||
else:
|
||
etree.SubElement(cp_elem, "ЮридическоеФизическоеЛицо").text = "ЮридическоеЛицо"
|
||
|
||
# Страна регистрации
|
||
country_elem = etree.SubElement(cp_elem, "СтранаРегистрации")
|
||
etree.SubElement(country_elem, "Ссылка").text = ""
|
||
country_data = etree.SubElement(country_elem, "ДанныеКлассификатора")
|
||
etree.SubElement(country_data, "Код").text = "643"
|
||
etree.SubElement(country_data, "Наименование").text = "РОССИЯ"
|
||
|
||
# ДанныеВзаиморасчетов
|
||
settlement_elem = etree.SubElement(doc, "ДанныеВзаиморасчетов")
|
||
|
||
# Договор
|
||
contract_elem = etree.SubElement(settlement_elem, "Договор")
|
||
etree.SubElement(contract_elem, "Ссылка").text = ""
|
||
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", "")
|
||
etree.SubElement(contract_org, "ЮридическоеФизическоеЛицо").text = "ФизическоеЛицо"
|
||
else:
|
||
etree.SubElement(contract_org, "НаименованиеСокращенное").text = organization.get("name", "")
|
||
etree.SubElement(contract_org, "НаименованиеПолное").text = organization.get("legalTitle", "")
|
||
etree.SubElement(contract_org, "ЮридическоеФизическоеЛицо").text = "ЮридическоеЛицо"
|
||
etree.SubElement(contract_org, "ИНН").text = organization.get("inn", "")
|
||
|
||
# Контрагент в договоре
|
||
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 = "ФизическоеЛицо"
|
||
etree.SubElement(contract_cp, "ИндивидуальныйПредприниматель").text = "true"
|
||
else:
|
||
etree.SubElement(contract_cp, "ЮридическоеФизическоеЛицо").text = "ЮридическоеЛицо"
|
||
|
||
# Страна в контрагенте договора
|
||
cp_country = etree.SubElement(contract_cp, "СтранаРегистрации")
|
||
etree.SubElement(cp_country, "Ссылка").text = ""
|
||
cp_country_data = etree.SubElement(cp_country, "ДанныеКлассификатора")
|
||
etree.SubElement(cp_country_data, "Код").text = "643"
|
||
etree.SubElement(cp_country_data, "Наименование").text = "РОССИЯ"
|
||
|
||
# Валюта взаиморасчетов в договоре
|
||
contract_currency = etree.SubElement(contract_elem, "ВалютаВзаиморасчетов")
|
||
etree.SubElement(contract_currency, "Ссылка").text = currency_id
|
||
contract_curr_data = etree.SubElement(contract_currency, "ДанныеКлассификатора")
|
||
etree.SubElement(contract_curr_data, "Код").text = currency.get("isoCode", "643")
|
||
etree.SubElement(contract_curr_data, "Наименование").text = currency.get("name", "руб.")
|
||
|
||
etree.SubElement(contract_elem, "РасчетыВУсловныхЕдиницах").text = "false"
|
||
etree.SubElement(contract_elem, "Наименование").text = "Основной договор"
|
||
|
||
# Валюта взаиморасчетов
|
||
settlement_currency = etree.SubElement(settlement_elem, "ВалютаВзаиморасчетов")
|
||
etree.SubElement(settlement_currency, "Ссылка").text = currency_id
|
||
settlement_curr_data = etree.SubElement(settlement_currency, "ДанныеКлассификатора")
|
||
etree.SubElement(settlement_curr_data, "Код").text = currency.get("isoCode", "643")
|
||
etree.SubElement(settlement_curr_data, "Наименование").text = currency.get("name", "руб.")
|
||
|
||
etree.SubElement(settlement_elem, "КурсВзаиморасчетов").text = "1"
|
||
etree.SubElement(settlement_elem, "КратностьВзаиморасчетов").text = "1"
|
||
etree.SubElement(settlement_elem, "РасчетыВУсловныхЕдиницах").text = "false"
|
||
|
||
# Налогообложение
|
||
etree.SubElement(doc, "Налогообложение").text = "ПродажаОблагаетсяНДС"
|
||
|
||
# ВидЭД
|
||
etree.SubElement(doc, "ВидЭД").text = "ТОРГ12"
|
||
|
||
# Товары
|
||
goods_elem = etree.SubElement(doc, "Товары")
|
||
|
||
for pos in positions:
|
||
row_elem = etree.SubElement(goods_elem, "Строка")
|
||
|
||
assortment = pos.get("assortment", {})
|
||
product_id = assortment.get("meta", {}).get("href", "").split("/")[-1] if assortment.get("meta") 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_id = uom.get("meta", {}).get("href", "").split("/")[-1] if uom and uom.get("meta") else ""
|
||
etree.SubElement(uom_elem, "Ссылка").text = uom_id
|
||
|
||
uom_data = etree.SubElement(uom_elem, "ДанныеКлассификатора")
|
||
etree.SubElement(uom_data, "Код").text = "796" # Код ОКЕИ для "шт"
|
||
etree.SubElement(uom_data, "Наименование").text = uom.get("name", "шт") if uom else "шт"
|
||
|
||
# Количество
|
||
quantity = pos.get("quantity", 0)
|
||
etree.SubElement(row_elem, "Количество").text = str(quantity)
|
||
|
||
# Цена и суммы (в рублях)
|
||
price = pos.get("price", 0) / 100 # Конвертируем из копеек
|
||
total = price * quantity
|
||
|
||
etree.SubElement(row_elem, "Сумма").text = str(int(total))
|
||
etree.SubElement(row_elem, "Цена").text = str(int(price))
|
||
|
||
# СтавкаНДС
|
||
vat_rate = pos.get("vat", 20)
|
||
vat_elem = etree.SubElement(row_elem, "СтавкаНДС")
|
||
etree.SubElement(vat_elem, "Ставка").text = str(vat_rate)
|
||
etree.SubElement(vat_elem, "РасчетнаяСтавка").text = "false"
|
||
etree.SubElement(vat_elem, "НеОблагается").text = "false"
|
||
etree.SubElement(vat_elem, "ВидСтавки").text = "Общая"
|
||
|
||
vat_country = etree.SubElement(vat_elem, "Страна")
|
||
etree.SubElement(vat_country, "Ссылка").text = ""
|
||
vat_country_data = etree.SubElement(vat_country, "ДанныеКлассификатора")
|
||
etree.SubElement(vat_country_data, "Код").text = "643"
|
||
etree.SubElement(vat_country_data, "Наименование").text = "РОССИЯ"
|
||
|
||
# СуммаНДС (извлекаем из суммы)
|
||
vat_amount = total / (1 + vat_rate / 100) * (vat_rate / 100) if vat_rate else 0
|
||
etree.SubElement(row_elem, "СуммаНДС").text = self._format_money_with_decimals(vat_amount)
|
||
|
||
# Дополнительные поля
|
||
etree.SubElement(row_elem, "ТипЗапасов").text = "СобственныеТовары"
|
||
etree.SubElement(row_elem, "СчетУчета").text = "43"
|
||
etree.SubElement(row_elem, "СчетДоходов").text = "90.01.1"
|
||
etree.SubElement(row_elem, "СчетРасходов").text = "90.02.1"
|
||
etree.SubElement(row_elem, "СчетУчетаНДСПоРеализации").text = "90.03"
|
||
|
||
# Уникальный идентификатор строки
|
||
import uuid
|
||
etree.SubElement(row_elem, "ИдентификаторСтроки").text = str(uuid.uuid4())
|
||
|
||
etree.SubElement(row_elem, "ПрослеживаемыйТовар").text = "false"
|
||
|
||
# СпособПогашенияЗадолженности
|
||
etree.SubElement(doc, "СпособПогашенияЗадолженности").text = "Автоматически"
|
||
|
||
# КомплектПечатныхДокументовПодписанПолучен
|
||
etree.SubElement(doc, "КомплектПечатныхДокументовПодписанПолучен").text = "false"
|
||
|
||
# ЭтоУПД
|
||
etree.SubElement(doc, "ЭтоУПД").text = "false"
|
||
|
||
# Конвертация в строку
|
||
xml_string = etree.tostring(
|
||
root,
|
||
pretty_print=True,
|
||
xml_declaration=True,
|
||
encoding=self.encoding
|
||
).decode(self.encoding)
|
||
|
||
# Сохранение в файл
|
||
if output_path:
|
||
with open(output_path, "w", encoding=self.encoding) as f:
|
||
f.write(xml_string)
|
||
logger.info(f"Документ реализации 1С сохранен: {output_path}")
|
||
|
||
return xml_string
|
||
|
||
|
||
# Пример использования
|
||
if __name__ == "__main__":
|
||
logging.basicConfig(level=logging.INFO)
|
||
|
||
# Тестовые данные
|
||
test_order = {
|
||
"order": {
|
||
"id": "313256ef-b4b7-11f0-b68a-50ebf69bde57",
|
||
"name": "4869",
|
||
"moment": "2025-10-23T09:37:11Z",
|
||
"sum": 1059900, # 10599 рублей в копейках
|
||
"rate": {
|
||
"currency": {
|
||
"name": "руб.",
|
||
"isoCode": "643"
|
||
}
|
||
}
|
||
},
|
||
"organization": {
|
||
"id": "288f7520-b8e5-11e8-80bc-e0d55e458e90",
|
||
"name": "Беляев А.Ю. ИП",
|
||
"legalTitle": "Индивидуальный предприниматель Беляев Андрей Юрьевич",
|
||
"inn": "263605160270",
|
||
"companyType": "entrepreneur",
|
||
"legalLastName": "Беляев",
|
||
"legalFirstName": "Андрей",
|
||
"legalMiddleName": "Юрьевич"
|
||
},
|
||
"counterparty": {
|
||
"id": "f37958af-d807-11ef-b676-50ebf69bde57",
|
||
"name": "АСЕЕВА АННА КУЗЬМИНИЧНА ИП",
|
||
"legalTitle": "ИНДИВИДУАЛЬНЫЙ ПРЕДПРИНИМАТЕЛЬ АСЕЕВА АННА КУЗЬМИНИЧНА",
|
||
"inn": "383700097634",
|
||
"companyType": "entrepreneur"
|
||
},
|
||
"org_accounts": [
|
||
{
|
||
"id": "bdd39811-27ee-11ea-80e7-e0d55e458e90",
|
||
"accountNumber": "40802810860270001979",
|
||
"bankName": "СТАВРОПОЛЬСКОЕ ОТДЕЛЕНИЕ N5230 ПАО СБЕРБАНК",
|
||
"bic": "040702615",
|
||
"correspondentAccount": "30101810907020000615"
|
||
}
|
||
],
|
||
"positions": [
|
||
{
|
||
"assortment": {
|
||
"name": "Жевательная резинка RUSGUM Арбузное трио 24 мм 7*150 штук",
|
||
"code": "00-00000682",
|
||
"uom": {"name": "шт"}
|
||
},
|
||
"quantity": 1,
|
||
"price": 337300, # 3373 рубля в копейках
|
||
"vat": 5
|
||
}
|
||
]
|
||
}
|
||
|
||
# Тест формата 1С
|
||
generator_1c = XML1CGenerator()
|
||
xml_1c = generator_1c.generate_realization(test_order, "test_1c_realization.xml")
|
||
print("Документ реализации 1С создан")
|
||
|
||
# Тест старого формата УПД
|
||
generator = XMLGenerator()
|
||
upd_xml = generator.generate_upd(test_order, "test_upd.xml")
|
||
print("УПД создан")
|