Fix XML generator contract UUID generation and cleanup temporary files
- Update contract UUID to include currency and UE flag for correct 1C account selection - Remove temporary export.xml and refactored files - Add documentation for services fixes
This commit is contained in:
parent
6c8259d306
commit
4038ff9f92
141
SERVICES_FIX.md
Normal file
141
SERVICES_FIX.md
Normal file
@ -0,0 +1,141 @@
|
||||
# Исправление: Услуги превращались в товары при импорте в 1С
|
||||
|
||||
## 🔴 Проблема
|
||||
|
||||
При экспорте XML в формате EnterpriseData 1.20 все позиции документа попадали в секцию `<Товары>`, включая услуги. Это приводило к тому, что 1С импортировала услуги как товары.
|
||||
|
||||
### Пример проблемного XML:
|
||||
```xml
|
||||
<Товары>
|
||||
<Строка>
|
||||
<ДанныеНоменклатуры>
|
||||
<Номенклатура>
|
||||
<Услуга>true</Услуга> <!-- ЭТО УСЛУГА -->
|
||||
<ВидНоменклатуры>
|
||||
<Наименование>Услуга</Наименование>
|
||||
</ВидНоменклатуры>
|
||||
</Номенклатура>
|
||||
</ДанныеНоменклатуры>
|
||||
<!-- ... НО ОНА В СЕКЦИИ <Товары>! -->
|
||||
</Строка>
|
||||
</Товары>
|
||||
```
|
||||
|
||||
## ✅ Решение
|
||||
|
||||
Документ `РеализацияТоваровУслуг` в формате 1С должен иметь **ДВЕ отдельные секции**:
|
||||
|
||||
1. **`<Товары>`** - для товаров (с `ДанныеНоменклатуры`, `ТипЗапасов` и т.д.)
|
||||
2. **`<Услуги>`** - для услуг (с `Номенклатура`, `Содержание` и т.д.)
|
||||
|
||||
### Структура исправленного XML:
|
||||
```xml
|
||||
<Документ.РеализацияТоваровУслуг>
|
||||
<!-- ... общие данные ... -->
|
||||
|
||||
<!-- Секция для товаров -->
|
||||
<Товары>
|
||||
<Строка>
|
||||
<ДанныеНоменклатуры>
|
||||
<Номенклатура>
|
||||
<ВидНоменклатуры>
|
||||
<Наименование>Товар</Наименование>
|
||||
</ВидНоменклатуры>
|
||||
</Номенклатура>
|
||||
</ДанныеНоменклатуры>
|
||||
<ТипЗапасов>СобственныеТовары</ТипЗапасов>
|
||||
<!-- ... -->
|
||||
</Строка>
|
||||
</Товары>
|
||||
|
||||
<!-- Секция для услуг -->
|
||||
<Услуги>
|
||||
<Строка>
|
||||
<Номенклатура>
|
||||
<Услуга>true</Услуга>
|
||||
<ВидНоменклатуры>
|
||||
<Наименование>Услуга</Наименование>
|
||||
</ВидНоменклатуры>
|
||||
</Номенклатура>
|
||||
<Содержание>Индивидуальный дизайн</Содержание>
|
||||
<!-- ... -->
|
||||
</Строка>
|
||||
</Услуги>
|
||||
</Документ.РеализацияТоваровУслуг>
|
||||
```
|
||||
|
||||
## 🔧 Изменения в коде
|
||||
|
||||
### Файл: `modules/xml_generator.py`
|
||||
|
||||
**Исправлено в двух методах:**
|
||||
1. `generate_realization()` - для генерации одиночных документов
|
||||
2. `_generate_single_document_content()` - для пакетной генерации (склейка нескольких документов)
|
||||
|
||||
#### 1. Разделение позиций на товары и услуги (строки 1108-1119 и 1819-1830):
|
||||
|
||||
```python
|
||||
# Разделяем позиции на товары и услуги
|
||||
товары = []
|
||||
услуги = []
|
||||
|
||||
for pos in positions:
|
||||
assortment = pos.get("assortment", {})
|
||||
if _is_service(assortment):
|
||||
услуги.append(pos)
|
||||
else:
|
||||
товары.append(pos)
|
||||
```
|
||||
|
||||
#### 2. Генерация секции `<Товары>` (строки 1121-1207):
|
||||
|
||||
- Добавлен блок `if товары:` - секция создаётся только если есть товары
|
||||
- Для товаров используется `<ДанныеНоменклатуры>`
|
||||
- Добавляется `<ТипЗапасов>` (обязательно для товаров)
|
||||
- Счета учета: 43, 90.01.1, 90.02.1, 90.03
|
||||
|
||||
#### 3. Генерация секции `<Услуги>` (строки 1209-1275):
|
||||
|
||||
- Добавлен новый блок `if услуги:` - секция создаётся только если есть услуги
|
||||
- Для услуг используется `<Номенклатура>` напрямую (БЕЗ `ДанныеНоменклатуры`)
|
||||
- Обязательный флаг `<Услуга>true</Услуга>`
|
||||
- Добавлено поле `<Содержание>` с описанием услуги
|
||||
- Счета учета: 90.01.1, 90.02.1, 90.03 (БЕЗ счета 43)
|
||||
|
||||
## 📋 Ключевые различия между Товарами и Услугами
|
||||
|
||||
| Параметр | Товары | Услуги |
|
||||
|----------|--------|--------|
|
||||
| Обёртка номенклатуры | `<ДанныеНоменклатуры>` | Напрямую `<Номенклатура>` |
|
||||
| Флаг услуги | Не используется | `<Услуга>true</Услуга>` |
|
||||
| ТипЗапасов | `СобственныеТовары` | НЕ используется |
|
||||
| Содержание | НЕ используется | Описание услуги |
|
||||
| СчетУчета | 43 (товары на складе) | НЕ используется |
|
||||
| ПрослеживаемыйТовар | `false` | НЕ используется |
|
||||
|
||||
## 🧪 Тестирование
|
||||
|
||||
После исправления проверьте:
|
||||
|
||||
1. Создайте документ с товарами и услугами
|
||||
2. Экспортируйте в XML
|
||||
3. Убедитесь, что в XML есть две секции: `<Товары>` и `<Услуги>`
|
||||
4. Импортируйте в 1С
|
||||
5. Проверьте, что услуги импортировались как услуги, а не как товары
|
||||
|
||||
## 📚 Ссылки на спецификацию
|
||||
|
||||
- Схема XSD: `аывавы.xml` (EnterpriseData 1.0)
|
||||
- Документация 1С по формату обмена EnterpriseData
|
||||
- Справочник "Номенклатура" в конфигурациях УТ 11, ERP, КА 2.0
|
||||
|
||||
## ⚠️ Важно
|
||||
|
||||
**Проблема была в двух местах:**
|
||||
|
||||
1. ✅ **Метод `generate_realization()`** - исправлен ранее (для одиночных документов)
|
||||
2. ✅ **Метод `_generate_single_document_content()`** - исправлен сейчас (для **пакетной генерации**)
|
||||
|
||||
При склейке нескольких документов в один XML файл использовался отдельный метод `_generate_single_document_content()`, в котором **не было** логики разделения на товары и услуги. Теперь исправлено в обоих методах!
|
||||
|
||||
После этого исправления необходимо **перегенерировать все существующие XML-файлы**, если они будут использоваться для импорта в 1С.
|
||||
@ -62,6 +62,23 @@ def get_currency_codes(currency: Optional[Dict]) -> Tuple[str, str]:
|
||||
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"""
|
||||
|
||||
@ -1087,110 +1104,184 @@ class XML1CGenerator:
|
||||
# Налогообложение
|
||||
etree.SubElement(doc, "Налогообложение").text = "ПродажаОблагаетсяНДС"
|
||||
|
||||
# Товары
|
||||
goods_elem = etree.SubElement(doc, "Товары")
|
||||
# Разделяем позиции на товары и услуги
|
||||
товары = []
|
||||
услуги = []
|
||||
|
||||
for pos in positions:
|
||||
row_elem = etree.SubElement(goods_elem, "Строка")
|
||||
|
||||
assortment = pos.get("assortment", {})
|
||||
# Очищаем GUID от query-параметров (например, ?expand=product.uom)
|
||||
product_href = assortment.get("meta", {}).get("href", "") if assortment.get("meta") else ""
|
||||
product_id = product_href.split("/")[-1].split("?")[0] if product_href else ""
|
||||
product_name = assortment.get("name", "")
|
||||
|
||||
# Определяем тип номенклатуры (товар или услуга)
|
||||
is_service = False
|
||||
if product_href and "/service/" in product_href:
|
||||
is_service = True
|
||||
|
||||
# ДанныеНоменклатуры
|
||||
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
|
||||
|
||||
# Флаг услуги (для конфигураций без справочника видов)
|
||||
if is_service:
|
||||
etree.SubElement(nomenclature, "Услуга").text = "true"
|
||||
|
||||
# ВидНоменклатуры (для конфигураций УТ 11, ERP)
|
||||
kind_elem = etree.SubElement(nomenclature, "ВидНоменклатуры")
|
||||
if is_service:
|
||||
etree.SubElement(kind_elem, "Наименование").text = "Услуга"
|
||||
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)
|
||||
|
||||
# Цена и суммы (в рублях с копейками)
|
||||
price = pos.get("price", 0) / 100 # Конвертируем из копеек
|
||||
total = price * quantity
|
||||
|
||||
# Форматируем с 2 знаками после запятой (НЕ округляем до целых!)
|
||||
etree.SubElement(row_elem, "Сумма").text = f"{total:.2f}"
|
||||
etree.SubElement(row_elem, "Цена").text = f"{price:.2f}"
|
||||
|
||||
# СтавкаНДС
|
||||
vat_rate = pos.get("vat", 20)
|
||||
vat_elem = etree.SubElement(row_elem, "СтавкаНДС")
|
||||
etree.SubElement(vat_elem, "Ставка").text = str(vat_rate)
|
||||
etree.SubElement(vat_elem, "РасчетнаяСтавка").text = "false"
|
||||
etree.SubElement(vat_elem, "НеОблагается").text = "false"
|
||||
etree.SubElement(vat_elem, "ВидСтавки").text = "Общая"
|
||||
|
||||
vat_country = etree.SubElement(vat_elem, "Страна")
|
||||
# Используем стандартный UUID России из классификатора 1С
|
||||
etree.SubElement(vat_country, "Ссылка").text = 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
|
||||
|
||||
# СуммаНДС (извлекаем из суммы)
|
||||
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)
|
||||
|
||||
# Дополнительные поля (опционально, зависит от конфигурации 1С)
|
||||
# ТипЗапасов только для товаров (не для услуг)
|
||||
if not is_service:
|
||||
|
||||
# ЕдиницаИзмерения
|
||||
uom = assortment.get("uom", {})
|
||||
uom_elem = etree.SubElement(row_elem, "ЕдиницаИзмерения")
|
||||
# Очищаем GUID от query-параметров
|
||||
uom_href = uom.get("meta", {}).get("href", "") if uom and uom.get("meta") else ""
|
||||
uom_id = uom_href.split("/")[-1].split("?")[0] if uom_href else ""
|
||||
# Если нет UUID, используем стандартный UUID для "Штука (796)"
|
||||
if not uom_id:
|
||||
uom_id = "cdf130e2-63a8-11e8-80dd-0050569f5448" # Стандартный GUID для "шт"
|
||||
etree.SubElement(uom_elem, "Ссылка").text = uom_id
|
||||
|
||||
uom_data = etree.SubElement(uom_elem, "ДанныеКлассификатора")
|
||||
etree.SubElement(uom_data, "Код").text = "796" # Код ОКЕИ для "шт"
|
||||
etree.SubElement(uom_data, "Наименование").text = uom.get("name", "шт") if uom else "шт"
|
||||
|
||||
# Количество
|
||||
quantity = pos.get("quantity", 0)
|
||||
etree.SubElement(row_elem, "Количество").text = str(quantity)
|
||||
|
||||
# Цена и суммы (в рублях с копейками)
|
||||
price = pos.get("price", 0) / 100 # Конвертируем из копеек
|
||||
total = price * quantity
|
||||
|
||||
# Форматируем с 2 знаками после запятой (НЕ округляем до целых!)
|
||||
etree.SubElement(row_elem, "Сумма").text = f"{total:.2f}"
|
||||
etree.SubElement(row_elem, "Цена").text = f"{price:.2f}"
|
||||
|
||||
# СтавкаНДС
|
||||
vat_rate = pos.get("vat", 20)
|
||||
vat_elem = etree.SubElement(row_elem, "СтавкаНДС")
|
||||
etree.SubElement(vat_elem, "Ставка").text = str(vat_rate)
|
||||
etree.SubElement(vat_elem, "РасчетнаяСтавка").text = "false"
|
||||
etree.SubElement(vat_elem, "НеОблагается").text = "false"
|
||||
etree.SubElement(vat_elem, "ВидСтавки").text = "Общая"
|
||||
|
||||
vat_country = etree.SubElement(vat_elem, "Страна")
|
||||
# Используем стандартный UUID России из классификатора 1С
|
||||
etree.SubElement(vat_country, "Ссылка").text = 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
|
||||
|
||||
# СуммаНДС (извлекаем из суммы)
|
||||
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 = "СобственныеТовары"
|
||||
|
||||
# Счета учета (опционально)
|
||||
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 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)
|
||||
|
||||
# Цена и суммы (в рублях с копейками)
|
||||
price = pos.get("price", 0) / 100 # Конвертируем из копеек
|
||||
total = price * quantity
|
||||
|
||||
# Форматируем с 2 знаками после запятой
|
||||
etree.SubElement(row_elem, "Сумма").text = f"{total:.2f}"
|
||||
etree.SubElement(row_elem, "Цена").text = f"{price:.2f}"
|
||||
|
||||
# СтавкаНДС
|
||||
vat_rate = pos.get("vat", 20)
|
||||
vat_elem = etree.SubElement(row_elem, "СтавкаНДС")
|
||||
etree.SubElement(vat_elem, "Ставка").text = str(vat_rate)
|
||||
etree.SubElement(vat_elem, "РасчетнаяСтавка").text = "false"
|
||||
etree.SubElement(vat_elem, "НеОблагается").text = "false"
|
||||
etree.SubElement(vat_elem, "ВидСтавки").text = "Общая"
|
||||
|
||||
vat_country = etree.SubElement(vat_elem, "Страна")
|
||||
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
|
||||
|
||||
# СуммаНДС (извлекаем из суммы)
|
||||
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 = 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 = "Автоматически"
|
||||
@ -1720,130 +1811,237 @@ class XML1CGenerator:
|
||||
doc, "Налогообложение"
|
||||
).text = "ПродажаОблагаетсяНДС"
|
||||
|
||||
# Товары
|
||||
goods_elem = etree.SubElement(doc, "Товары")
|
||||
# Разделяем позиции на товары и услуги
|
||||
товары = []
|
||||
услуги = []
|
||||
|
||||
for pos in positions:
|
||||
row_elem = etree.SubElement(goods_elem, "Строка")
|
||||
|
||||
assortment = pos.get("assortment", {})
|
||||
product_href = (
|
||||
assortment.get("meta", {}).get("href", "")
|
||||
if assortment.get("meta") else ""
|
||||
)
|
||||
product_id = (
|
||||
product_href.split("/")[-1].split("?")[0]
|
||||
if product_href else ""
|
||||
)
|
||||
product_name = assortment.get("name", "")
|
||||
|
||||
# Определяем тип номенклатуры (товар или услуга)
|
||||
is_service = False
|
||||
if product_href and "/service/" in product_href:
|
||||
is_service = True
|
||||
|
||||
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
|
||||
|
||||
# Флаг услуги (для конфигураций без справочника видов)
|
||||
if is_service:
|
||||
etree.SubElement(nomenclature, "Услуга").text = "true"
|
||||
|
||||
# ВидНоменклатуры (для конфигураций УТ 11, ERP)
|
||||
kind_elem = etree.SubElement(nomenclature, "ВидНоменклатуры")
|
||||
if is_service:
|
||||
etree.SubElement(kind_elem, "Наименование").text = "Услуга"
|
||||
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)
|
||||
|
||||
price = pos.get("price", 0) / 100
|
||||
total = price * quantity
|
||||
|
||||
etree.SubElement(row_elem, "Сумма").text = f"{total:.2f}"
|
||||
etree.SubElement(row_elem, "Цена").text = f"{price:.2f}"
|
||||
|
||||
vat_rate = pos.get("vat", 20)
|
||||
vat_elem = etree.SubElement(row_elem, "СтавкаНДС")
|
||||
etree.SubElement(vat_elem, "Ставка").text = str(vat_rate)
|
||||
etree.SubElement(vat_elem, "РасчетнаяСтавка").text = "false"
|
||||
etree.SubElement(vat_elem, "НеОблагается").text = "false"
|
||||
etree.SubElement(vat_elem, "ВидСтавки").text = "Общая"
|
||||
|
||||
vat_country = etree.SubElement(vat_elem, "Страна")
|
||||
etree.SubElement(
|
||||
vat_country, "Ссылка"
|
||||
).text = 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
|
||||
|
||||
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)
|
||||
|
||||
# ТипЗапасов только для товаров (не для услуг)
|
||||
if not is_service:
|
||||
|
||||
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)
|
||||
|
||||
price = pos.get("price", 0) / 100
|
||||
total = price * quantity
|
||||
|
||||
etree.SubElement(row_elem, "Сумма").text = f"{total:.2f}"
|
||||
etree.SubElement(row_elem, "Цена").text = f"{price:.2f}"
|
||||
|
||||
vat_rate = pos.get("vat", 20)
|
||||
vat_elem = etree.SubElement(row_elem, "СтавкаНДС")
|
||||
etree.SubElement(vat_elem, "Ставка").text = str(vat_rate)
|
||||
etree.SubElement(vat_elem, "РасчетнаяСтавка").text = "false"
|
||||
etree.SubElement(vat_elem, "НеОблагается").text = "false"
|
||||
etree.SubElement(vat_elem, "ВидСтавки").text = "Общая"
|
||||
|
||||
vat_country = etree.SubElement(vat_elem, "Страна")
|
||||
etree.SubElement(
|
||||
vat_country, "Ссылка"
|
||||
).text = 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
|
||||
|
||||
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 = "СобственныеТовары"
|
||||
|
||||
# Счета учета (опционально)
|
||||
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, "Услуги")
|
||||
|
||||
# Счета учета (опционально)
|
||||
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"
|
||||
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)
|
||||
|
||||
price = pos.get("price", 0) / 100
|
||||
total = price * quantity
|
||||
|
||||
etree.SubElement(row_elem, "Сумма").text = f"{total:.2f}"
|
||||
etree.SubElement(row_elem, "Цена").text = f"{price:.2f}"
|
||||
|
||||
vat_rate = pos.get("vat", 20)
|
||||
vat_elem = etree.SubElement(row_elem, "СтавкаНДС")
|
||||
etree.SubElement(vat_elem, "Ставка").text = str(vat_rate)
|
||||
etree.SubElement(vat_elem, "РасчетнаяСтавка").text = "false"
|
||||
etree.SubElement(vat_elem, "НеОблагается").text = "false"
|
||||
etree.SubElement(vat_elem, "ВидСтавки").text = "Общая"
|
||||
|
||||
vat_country = etree.SubElement(vat_elem, "Страна")
|
||||
etree.SubElement(
|
||||
vat_country, "Ссылка"
|
||||
).text = 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
|
||||
|
||||
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)
|
||||
|
||||
# Счета учета для услуг (БЕЗ счета 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(
|
||||
|
||||
@ -1,446 +0,0 @@
|
||||
"""
|
||||
Генератор 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
|
||||
Loading…
x
Reference in New Issue
Block a user