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:
admin i 2025-11-07 16:19:25 +03:00
parent 6c8259d306
commit 4038ff9f92
3 changed files with 550 additions and 657 deletions

141
SERVICES_FIX.md Normal file
View 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С.

View File

@ -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(

View File

@ -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