BUH_bot/modules/xml_generator_refactored2.py

447 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

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

"""
Генератор XML документов в формате 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