447 lines
14 KiB
Python
447 lines
14 KiB
Python
"""
|
||
Генератор XML документов в формате 1С EnterpriseData 1.20
|
||
Рефакторенная версия с устранением дублирования кода
|
||
"""
|
||
from lxml import etree
|
||
from datetime import datetime
|
||
from typing import Dict, List, Optional
|
||
import logging
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class XML1CGenerator:
|
||
"""Генератор XML документов для 1С в формате EnterpriseData 1.20"""
|
||
|
||
VERSION = "1.20"
|
||
NAMESPACE = "http://v8.1c.ru/8.1/data/enterprise"
|
||
NS_PREFIX = "v8"
|
||
|
||
def __init__(self):
|
||
self.encoding = "UTF-8"
|
||
self.nsmap = {self.NS_PREFIX: self.NAMESPACE}
|
||
|
||
# ========== Утилиты форматирования ==========
|
||
|
||
@staticmethod
|
||
def _format_date(date_str: str) -> str:
|
||
"""Форматирование даты в формат ГГГГ-ММ-ДДTЧЧ:ММ:СС"""
|
||
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")
|
||
|
||
@staticmethod
|
||
def _format_money(amount: int) -> str:
|
||
"""Форматирование денежной суммы (копейки -> рубли)"""
|
||
return f"{amount / 100:.2f}"
|
||
|
||
@staticmethod
|
||
def _safe_get(data: Dict, key: str, default: str = "") -> str:
|
||
"""Безопасное извлечение значения из словаря"""
|
||
return str(data.get(key, default) or default)
|
||
|
||
# ========== Создание XML элементов ==========
|
||
|
||
def _create_element(
|
||
self,
|
||
tag: str,
|
||
text: Optional[str] = None,
|
||
**attributes
|
||
) -> etree.Element:
|
||
"""Создание XML элемента с атрибутами"""
|
||
elem = etree.Element(f"{{{self.NAMESPACE}}}{tag}")
|
||
if text:
|
||
elem.text = text
|
||
for key, value in attributes.items():
|
||
if value is not None:
|
||
elem.set(key, str(value))
|
||
return elem
|
||
|
||
def _add_property(
|
||
self,
|
||
parent: etree.Element,
|
||
name: str,
|
||
value: str,
|
||
value_type: str = "string"
|
||
) -> None:
|
||
"""Добавление свойства объекта 1С"""
|
||
prop = etree.SubElement(parent, f"{{{self.NAMESPACE}}}Property")
|
||
etree.SubElement(prop, f"{{{self.NAMESPACE}}}Name").text = name
|
||
|
||
value_elem = etree.SubElement(
|
||
prop,
|
||
f"{{{self.NAMESPACE}}}Value",
|
||
)
|
||
if value_type == "string":
|
||
value_elem.set(
|
||
f"{{{self.NAMESPACE}}}type",
|
||
"http://www.w3.org/2001/XMLSchema:string"
|
||
)
|
||
elif value_type == "decimal":
|
||
value_elem.set(
|
||
f"{{{self.NAMESPACE}}}type",
|
||
"http://www.w3.org/2001/XMLSchema:decimal"
|
||
)
|
||
elif value_type == "dateTime":
|
||
value_elem.set(
|
||
f"{{{self.NAMESPACE}}}type",
|
||
"http://www.w3.org/2001/XMLSchema:dateTime"
|
||
)
|
||
|
||
value_elem.text = value
|
||
|
||
# ========== Работа с организациями ==========
|
||
|
||
def _add_organization_properties(
|
||
self,
|
||
parent: etree.Element,
|
||
org_data: Dict,
|
||
prefix: str = "Организация"
|
||
) -> None:
|
||
"""Добавление свойств организации"""
|
||
self._add_property(
|
||
parent,
|
||
f"{prefix}Наименование",
|
||
self._safe_get(org_data, "name")
|
||
)
|
||
self._add_property(
|
||
parent,
|
||
f"{prefix}ИНН",
|
||
self._safe_get(org_data, "inn")
|
||
)
|
||
|
||
kpp = self._safe_get(org_data, "kpp")
|
||
if kpp:
|
||
self._add_property(parent, f"{prefix}КПП", kpp)
|
||
|
||
self._add_property(
|
||
parent,
|
||
f"{prefix}ЮрАдрес",
|
||
self._safe_get(org_data, "legalAddress")
|
||
)
|
||
|
||
def _add_counterparty_properties(
|
||
self,
|
||
parent: etree.Element,
|
||
counterparty: Dict
|
||
) -> None:
|
||
"""Добавление свойств контрагента"""
|
||
self._add_organization_properties(
|
||
parent,
|
||
counterparty,
|
||
prefix="Контрагент"
|
||
)
|
||
|
||
# ========== Работа с товарами ==========
|
||
|
||
def _add_product_item(
|
||
self,
|
||
parent: etree.Element,
|
||
position: Dict,
|
||
index: int
|
||
) -> None:
|
||
"""Добавление позиции товара в таблицу"""
|
||
row = etree.SubElement(parent, f"{{{self.NAMESPACE}}}Row")
|
||
|
||
# Номер строки
|
||
self._add_property(row, "НомерСтроки", str(index))
|
||
|
||
# Наименование товара
|
||
product_name = self._safe_get(position, "name")
|
||
self._add_property(row, "Номенклатура", product_name)
|
||
|
||
# Количество
|
||
quantity = position.get("quantity", 0)
|
||
self._add_property(row, "Количество", str(quantity), "decimal")
|
||
|
||
# Цена
|
||
price = self._format_money(position.get("price", 0))
|
||
self._add_property(row, "Цена", price, "decimal")
|
||
|
||
# Сумма
|
||
total = self._format_money(
|
||
quantity * position.get("price", 0)
|
||
)
|
||
self._add_property(row, "Сумма", total, "decimal")
|
||
|
||
# НДС
|
||
vat = position.get("vat", 0)
|
||
if vat:
|
||
vat_rate = vat
|
||
vat_sum = self._format_money(
|
||
int(quantity * position.get("price", 0) * vat / 100)
|
||
)
|
||
self._add_property(row, "СтавкаНДС", str(vat_rate))
|
||
self._add_property(row, "СуммаНДС", vat_sum, "decimal")
|
||
|
||
def _add_products_table(
|
||
self,
|
||
parent: etree.Element,
|
||
positions: List[Dict]
|
||
) -> None:
|
||
"""Добавление таблицы товаров"""
|
||
table = etree.SubElement(parent, f"{{{self.NAMESPACE}}}Property")
|
||
etree.SubElement(table, f"{{{self.NAMESPACE}}}Name").text = "Товары"
|
||
|
||
value = etree.SubElement(table, f"{{{self.NAMESPACE}}}Value")
|
||
value.set(
|
||
f"{{{self.NAMESPACE}}}type",
|
||
f"{{{self.NAMESPACE}}}ValueTable"
|
||
)
|
||
|
||
for idx, position in enumerate(positions, start=1):
|
||
self._add_product_item(value, position, idx)
|
||
|
||
# ========== Работа с суммами ==========
|
||
|
||
def _calculate_totals(self, positions: List[Dict]) -> Dict[str, float]:
|
||
"""Расчет итоговых сумм"""
|
||
total_sum = 0
|
||
total_vat = 0
|
||
|
||
for position in positions:
|
||
quantity = position.get("quantity", 0)
|
||
price = position.get("price", 0)
|
||
vat = position.get("vat", 0)
|
||
|
||
item_sum = quantity * price
|
||
total_sum += item_sum
|
||
|
||
if vat:
|
||
total_vat += int(item_sum * vat / 100)
|
||
|
||
return {
|
||
"sum": self._format_money(int(total_sum)),
|
||
"vat": self._format_money(total_vat),
|
||
"total": self._format_money(int(total_sum) + total_vat)
|
||
}
|
||
|
||
def _add_totals(
|
||
self,
|
||
parent: etree.Element,
|
||
totals: Dict[str, float]
|
||
) -> None:
|
||
"""Добавление итоговых сумм"""
|
||
self._add_property(
|
||
parent,
|
||
"СуммаДокумента",
|
||
totals["sum"],
|
||
"decimal"
|
||
)
|
||
self._add_property(
|
||
parent,
|
||
"СуммаНДС",
|
||
totals["vat"],
|
||
"decimal"
|
||
)
|
||
self._add_property(
|
||
parent,
|
||
"СуммаВсего",
|
||
totals["total"],
|
||
"decimal"
|
||
)
|
||
|
||
# ========== Генерация документов ==========
|
||
|
||
def _create_document_base(
|
||
self,
|
||
doc_type: str,
|
||
order_data: Dict
|
||
) -> etree.Element:
|
||
"""Создание базовой структуры документа"""
|
||
root = etree.Element(
|
||
f"{{{self.NAMESPACE}}}Data",
|
||
nsmap=self.nsmap
|
||
)
|
||
|
||
# Основной объект документа
|
||
obj = etree.SubElement(root, f"{{{self.NAMESPACE}}}Object")
|
||
|
||
# Тип документа
|
||
self._add_property(obj, "ТипДокумента", doc_type)
|
||
|
||
# Номер и дата
|
||
self._add_property(
|
||
obj,
|
||
"Номер",
|
||
self._safe_get(order_data, "name")
|
||
)
|
||
self._add_property(
|
||
obj,
|
||
"Дата",
|
||
self._format_date(self._safe_get(order_data, "moment")),
|
||
"dateTime"
|
||
)
|
||
|
||
return obj
|
||
|
||
def _add_document_parties(
|
||
self,
|
||
obj: etree.Element,
|
||
organization: Dict,
|
||
counterparty: Dict
|
||
) -> None:
|
||
"""Добавление информации о сторонах сделки"""
|
||
self._add_organization_properties(obj, organization)
|
||
self._add_counterparty_properties(obj, counterparty)
|
||
|
||
def generate_realization(
|
||
self,
|
||
order_data: Dict,
|
||
filename: Optional[str] = None
|
||
):
|
||
"""
|
||
Генерация XML документа реализации товаров
|
||
|
||
Args:
|
||
order_data: Данные о заказе
|
||
filename: Имя выходного файла (опционально)
|
||
|
||
Returns:
|
||
Если filename указан - True/False (успех/ошибка)
|
||
Если filename не указан - XML строка или None при ошибке
|
||
"""
|
||
try:
|
||
logger.info(
|
||
f"Генерация XML реализации для заказа "
|
||
f"{order_data.get('name', 'N/A')}"
|
||
)
|
||
|
||
# Базовая структура
|
||
obj = self._create_document_base(
|
||
"РеализацияТоваровУслуг",
|
||
order_data
|
||
)
|
||
|
||
# Организация и контрагент
|
||
organization = order_data.get("organization", {})
|
||
agent = order_data.get("agent", {})
|
||
|
||
self._add_document_parties(obj, organization, agent)
|
||
|
||
# Товары
|
||
positions = order_data.get("positions", [])
|
||
self._add_products_table(obj, positions)
|
||
|
||
# Итоговые суммы
|
||
totals = self._calculate_totals(positions)
|
||
self._add_totals(obj, totals)
|
||
|
||
# Получаем XML
|
||
root = obj.getparent()
|
||
tree = etree.ElementTree(root)
|
||
|
||
if filename:
|
||
# Сохранение в файл
|
||
tree.write(
|
||
filename,
|
||
encoding=self.encoding,
|
||
xml_declaration=True,
|
||
pretty_print=True
|
||
)
|
||
logger.info(f"XML успешно сохранен: {filename}")
|
||
return True
|
||
else:
|
||
# Возврат XML строки
|
||
xml_str = etree.tostring(
|
||
root,
|
||
encoding=self.encoding,
|
||
xml_declaration=True,
|
||
pretty_print=True
|
||
).decode(self.encoding)
|
||
logger.info("XML успешно сгенерирован")
|
||
return xml_str
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка генерации XML: {e}", exc_info=True)
|
||
return None if filename is None else False
|
||
|
||
def generate_batch_realization(
|
||
self,
|
||
orders: List[Dict],
|
||
filename: str
|
||
) -> bool:
|
||
"""
|
||
Генерация пакетного XML с несколькими документами
|
||
|
||
Args:
|
||
orders: Список заказов
|
||
filename: Имя выходного файла
|
||
|
||
Returns:
|
||
True если генерация успешна
|
||
"""
|
||
try:
|
||
logger.info(f"Генерация пакетного XML для {len(orders)} заказов")
|
||
|
||
root = etree.Element(
|
||
f"{{{self.NAMESPACE}}}Data",
|
||
nsmap=self.nsmap
|
||
)
|
||
|
||
for order_data in orders:
|
||
# Создаем объект документа для каждого заказа
|
||
obj = etree.SubElement(
|
||
root,
|
||
f"{{{self.NAMESPACE}}}Object"
|
||
)
|
||
|
||
# Тип документа
|
||
self._add_property(
|
||
obj,
|
||
"ТипДокумента",
|
||
"РеализацияТоваровУслуг"
|
||
)
|
||
|
||
# Номер и дата
|
||
self._add_property(
|
||
obj,
|
||
"Номер",
|
||
self._safe_get(order_data, "name")
|
||
)
|
||
self._add_property(
|
||
obj,
|
||
"Дата",
|
||
self._format_date(self._safe_get(order_data, "moment")),
|
||
"dateTime"
|
||
)
|
||
|
||
# Стороны сделки
|
||
organization = order_data.get("organization", {})
|
||
agent = order_data.get("agent", {})
|
||
self._add_document_parties(obj, organization, agent)
|
||
|
||
# Товары
|
||
positions = order_data.get("positions", [])
|
||
self._add_products_table(obj, positions)
|
||
|
||
# Итоги
|
||
totals = self._calculate_totals(positions)
|
||
self._add_totals(obj, totals)
|
||
|
||
# Сохранение
|
||
tree = etree.ElementTree(root)
|
||
tree.write(
|
||
filename,
|
||
encoding=self.encoding,
|
||
xml_declaration=True,
|
||
pretty_print=True
|
||
)
|
||
|
||
logger.info(f"Пакетный XML успешно сохранен: {filename}")
|
||
return True
|
||
|
||
except Exception as e:
|
||
logger.error(
|
||
f"Ошибка генерации пакетного XML: {e}",
|
||
exc_info=True
|
||
)
|
||
return False
|
||
|
||
|
||
# Для обратной совместимости
|
||
XMLGenerator = XML1CGenerator
|