# Анализ XML-документов ФНС

> **Область:** Парсинг и интерпретация XML-файлов Федеральной налоговой службы РФ.
> **Дополняет** skill `tax_law_ru` (нормативная база) и `accounting_docs_ru` (первичные документы).

## Структура 2-НДФЛ (Справка о доходах)

Корневой элемент: `<Файл>` → `<Документ>` → `<СвНА>`, `<ПолучДох>`, `<СведДох>`, `<СвВыч>`, `<СумИтог>`

| Элемент | Описание |
|---------|----------|
| `<СвНА>` | Сведения о налоговом агенте (работодатель): ИНН, КПП, наименование |
| `<ПолучДох>` | Получатель дохода (физлицо): ФИО, ИНН, дата рождения, паспорт |
| `<СведДох>` | Доходы по месяцам: `<ДохВыч>` с атрибутами `Месяц`, `КодДоход`, `СумДоход` |
| `<СвВыч>` | Налоговые вычеты: `<ВычетСВ>` с атрибутами `КодВычет`, `СумВычет` |
| `<СумИтог>` | Итоговые суммы: доход, облагаемая база, исчислено, удержано, перечислено |

### Атрибуты `<Документ>`

- `ОтчетГод` — налоговый период (год)
- `Призн` — признак (1 = обычная, 2 = невозможность удержания)
- `НомКорр` — номер корректировки (00 = первичная)

## Структура 3-НДФЛ (Декларация)

Корневой элемент: `<Файл>` → `<Документ>` → разделы и приложения

| Раздел | Описание |
|--------|----------|
| Раздел 1 | Сведения о суммах налога, подлежащих уплате/возврату |
| Раздел 2 | Расчёт налоговой базы и суммы налога |
| Приложение 1 (`<ДохРФ>`) | Доходы от источников в РФ |
| Приложение 2 | Доходы от источников за пределами РФ |
| Приложение 3 | Доходы от предпринимательской деятельности |
| Приложение 4 | Необлагаемые доходы |
| Приложение 5 (`<ВычСтанд>`, `<ВычСоц>`) | Стандартные и социальные вычеты |
| Приложение 6 (`<ВычИмущ>`) | Имущественные вычеты при продаже |
| Приложение 7 | Имущественные вычеты при покупке |
| Приложение 8 | Расходы и вычеты по операциям с ЦБ |

## Коды доходов

| Код | Значение |
|-----|----------|
| 1010 | Дивиденды |
| 2000 | Заработная плата |
| 2002 | Премия за производственные результаты |
| 2003 | Премия из прибыли (непроизводственная) |
| 2012 | Отпускные |
| 2300 | Пособие по временной нетрудоспособности (больничные) |
| 2510 | Оплата за налогоплательщика (аренда, питание) |
| 2610 | Материальная выгода от экономии на процентах |
| 2760 | Материальная помощь |
| 2762 | Материальная помощь при рождении ребёнка |
| 3020 | Проценты по вкладам |
| 4800 | Иные доходы |

## Коды вычетов

| Код | Значение |
|-----|----------|
| 126 | На первого ребёнка (1 400 руб./мес.) |
| 127 | На второго ребёнка (1 400 руб./мес.) |
| 128 | На третьего и последующих (3 000 руб./мес.) |
| 129 | На ребёнка-инвалида |
| 311 | Имущественный вычет (расходы на приобретение) |
| 312 | Имущественный вычет (проценты по ипотеке) |
| 320 | Социальный вычет (обучение) |
| 321 | Социальный вычет (обучение детей) |
| 324 | Социальный вычет (лечение) |
| 325 | Социальный вычет (ДМС) |
| 327 | Социальный вычет (пенсионные взносы) |
| 501 | Вычет из стоимости подарков (до 4 000 руб.) |
| 503 | Вычет из материальной помощи (до 4 000 руб.) |

## Что проверять при анализе

### Сверка данных
- Совпадение ИНН налогоплательщика между документами
- Корректность налогового периода (`ОтчетГод`)
- Сходимость: `НалИсчисл` = `НалУдерж` ± переплата/задолженность
- Полнота месяцев (все 12 месяцев при работе полный год)
- Соответствие ставки налога (13% / 15% / 30% / 35%)

### Типичные ошибки
- Расхождение сумм `СумДоход` по месяцам и итогового `СумДохОбщ`
- Вычеты превышают допустимый лимит (например, 120 000 руб. для социальных)
- Отсутствие обязательных реквизитов (ИНН, КПП агента)
- Неправильный признак справки (Призн=1 vs Призн=2)

### Сравнение 2-НДФЛ и 3-НДФЛ
- Общая сумма дохода в 2-НДФЛ должна совпадать с Приложением 1 в 3-НДФЛ
- Удержанный налог должен совпадать
- Вычеты у агента (2-НДФЛ) + дополнительные вычеты (3-НДФЛ) = итого вычетов

## Кодировки

| Формат | Кодировка | Примечание |
|--------|-----------|------------|
| Старые файлы ФНС (до ~2019) | windows-1251 | В `<?xml encoding="windows-1251"?>` |
| Новые файлы ФНС | UTF-8 | Стандарт с 2020+ |
| BOM | UTF-8 BOM (EF BB BF) | Встречается редко |

Система автоматически определяет кодировку по XML-декларации и содержимому файла.

## Типичные задачи пользователей

| Вопрос | Что делать |
|--------|------------|
| «Сколько заработал за год?» | Найти `СумДохОбщ` или суммировать `СумДоход` по месяцам |
| «Какие вычеты применены?» | Извлечь `<СвВыч>` → `КодВычет` + `СумВычет`, расшифровать коды |
| «Сходятся ли суммы?» | Проверить `СумДохОбщ` = сумма по месяцам, `НалИсчисл` ≈ `НалОблБаз` × ставка |
| «Есть ли расхождения?» | Сравнить данные 2-НДФЛ и 3-НДФЛ (суммы, вычеты, удержания) |
| «Правильно ли заполнена декларация?» | Проверить обязательные поля, формулы, лимиты вычетов |

## Другие XML-документы ФНС

| Тип документа | Корневой элемент | Описание |
|---------------|-----------------|----------|
| Выписка из ЕГРЮЛ | `<Файл>` → `<СвЮЛ>` | Сведения о юрлице: название, ИНН, ОГРН, учредители, виды деятельности |
| Выписка из ЕГРИП | `<Файл>` → `<СвИП>` | Сведения об ИП: ФИО, ОГРНИП, виды деятельности |
| Требование о предоставлении документов | `<Файл>` → `<Документ>` | Перечень запрашиваемых документов, сроки представления |
| Акт сверки | `<Файл>` → `<АктСвworking>` | Расчёты по налогам и сборам, задолженности |

## УПД в формате ЭДО (счёт-фактура + документ о передаче)

Электронный УПД — это XML-файл обмена по формату ФНС. Генерация УПД разрешена и описана ниже; она подчинена трём жёстким воротам: версия формата, XSD-валидация, арифметическая сверка. Разбор входящего УПД — по тем же правилам, только без записи файла. Конвертация входящего УПД в 5.03 (в том числе с 5.01) — тоже разрешена: она идёт через перенос реквизитов из исходного XML, см. «Источник — уже готовый XML».

### Версия формата — только 5.03

| Параметр | Значение |
|----------|----------|
| Нормативная база | Приказ ФНС России № ЕД-7-26/970@ |
| `ВерсФорм` в `<Файл>` | `5.03` |
| КНД | `1115131` |
| Префикс имени файла (продавец) | `ON_NSCHFDOPPR_` |
| Префикс имени файла (покупатель) | `ON_NSCHFDOPPOK_` |
| Кодировка | `windows-1251` в XML-декларации и при записи байтов |

- Версия **5.01 запрещена** — не генерировать, не «поддерживать для совместимости», не предлагать как запасной вариант. Если пользователь просит 5.01 — объяснить, что действующий формат по приказу ЕД-7-26/970@ это 5.03, и сделать 5.03.
- `Функция` в `<Документ>`: `СЧФДОП` (счёт-фактура + передача), `СЧФ` (только счёт-фактура), `ДОП` (только документ о передаче). Значение выбирается из требований задачи и не меняется по ходу генерации.
- Перечни допустимых значений (`НалСт`, `ОКЕИ_Тов`, коды видов операций, признаки) берутся из действующей XSD, а не из памяти. Схема — источник истины по enum'ам.

### Каркас документа

```
<Файл ИдФайл="…" ВерсФорм="5.03" ВерсПрог="…">
  <Документ КНД="1115131" Функция="СЧФДОП" ДатаИнфПр="…" ВремИнфПр="…" НаимЭконСубСост="…">
    <СвСчФакт НомерДок="…" ДатаДок="…">
      <СвПрод><ИдСв><СвЮЛУч НаимОрг="…" ИННЮЛ="…" КПП="…"/></ИдСв><Адрес … /></СвПрод>
      <СвПокуп><ИдСв><СвЮЛУч НаимОрг="…" ИННЮЛ="…" КПП="…"/></ИдСв><Адрес … /></СвПокуп>
      <ДокПодтвОтгр … />
      <ДенИзм КодОКВ="643" НаимОКВ="Российский рубль"/>
    </СвСчФакт>
    <ТаблСчФакт>
      <СведТов НомСтр="1" НаимТов="…" ОКЕИ_Тов="796" КолТов="…" ЦенаТов="…"
               СтТовБезНДС="…" НалСт="…" СтТовУчНал="…">
        <Акциз><БезАкциз>без акциза</БезАкциз></Акциз>
        <СумНал><СумНал>…</СумНал></СумНал>
      </СведТов>
      <ВсегоОпл СтТовБезНДСВсего="…" СтТовУчНалВсего="…">
        <СумНалВсего><СумНал>…</СумНал></СумНалВсего>
      </ВсегоОпл>
    </ТаблСчФакт>
    <СвПродПер><СвПер СодОпер="…"><ОснПер … /></СвПер></СвПродПер>
    <Подписант СпосПодтПолном="1"><ФИО Фамилия="…" Имя="…" Отчество="…"/></Подписант>
  </Документ>
</Файл>
```

Имена реквизитов в 5.03 отличаются от привычных по 5.01 и от бумажной формы: номер и дата документа — `НомерДок`/`ДатаДок` (не `НомерСчФ`/`ДатаСчФ`), сумма налога — вложенный `<СумНал><СумНал>` (элемента `СумНалСчФ` в схеме нет), валюта — отдельный `<ДенИзм>` внутри `СвСчФакт` (не атрибут `КодОКВ`), участник — `<ИдСв><СвЮЛУч>` для юрлица и `<ИдСв><СвИП>` для ИП, `<Подписант>` обязателен. Проверять по XSD, а не по памяти.

Сборка и валидация — примитивами скила `fns_upd_xml_ru` (`save_upd_xml`, `validate_upd_xml`, `rows_to_positions`), а не ручной склейкой строк.

### Ворота 1 — XSD-валидация обязательна

Результат не отдаётся пользователю, пока файл не прошёл официальную XSD-схему формата 5.03.

```python
from lxml import etree

parser = etree.XMLParser(resolve_entities=False, no_network=True, load_dtd=False)
schema = etree.XMLSchema(etree.parse(xsd_path, parser))
doc = etree.parse(xml_path, parser)
if not schema.validate(doc):
    raise ValueError("УПД не прошёл XSD: " + "; ".join(str(e) for e in schema.error_log))
```

- XML пользователя или контрагента — недоверенный ввод. Любой разбор XML (валидация, конвертация, анализ) идёт только через парсер с `resolve_entities=False, no_network=True, load_dtd=False`. Голый `etree.parse(path)` / `etree.fromstring(payload)` запрещён: внешние сущности в присланном файле читают локальные файлы сервера (XXE), а DTD с вложенными сущностями раздувается в отказ обслуживания.
- Схема берётся из официального комплекта ФНС для 5.03 (основная схема + подключаемые типы в том же каталоге; относительные `xs:include` ломаются при разрозненном копировании файлов).
- Валидируется тот самый файл, который уйдёт пользователю: те же байты, та же кодировка, после всех правок.
- XSD недоступна — файл **не отдаётся**. Сказать прямо: схемы нет, валидация не выполнена, результат не выдан. «Скорее всего валидный» — не результат.
- В ответе указывается, какой схемой прогнали и что валидация прошла.

### Ворота 2 — арифметическая сверка

Проверяется до XSD и после любой правки сумм. Все суммы — `Decimal`, округление до 2 знаков `ROUND_HALF_UP`, сравнение точное.

По каждой строке:

- `ЦенаТов × КолТов = СтТовБезНДС`
- `СтТовБезНДС × ставка = СумНал` (значение в `<СведТов><СумНал><СумНал>`)
- `СтТовБезНДС + СумНал = СтТовУчНал`

По документу:

- `Σ СтТовБезНДС = СтТовБезНДСВсего`
- `Σ СумНал = СумНалВсего/СумНал`
- `Σ СтТовУчНал = СтТовУчНалВсего`

```python
from decimal import Decimal, ROUND_HALF_UP

def money(value) -> Decimal:
    return Decimal(str(value)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)

for row in rows:
    expected = money(money(row["price"]) * Decimal(str(row["qty"])))
    if expected != money(row["sum_no_vat"]):
        raise ValueError(
            f"строка {row['num']}: цена×количество={expected}, в документе {money(row['sum_no_vat'])}"
        )

total = money(sum(money(r["sum_no_vat"]) for r in rows))
if total != money(doc_total):
    raise ValueError(f"итог документа {money(doc_total)} != сумма строк {total}")
```

- Любое расхождение — жёсткая ошибка и остановка: файл не пишется и не отдаётся, пользователю называется строка и обе величины.
- Молча подгонять итог, «размазывать» копейку по строкам, дописывать корректирующую строку или менять исходные данные — запрещено. Расхождение — это вопрос к исходным данным, а не к генератору.
- Исходные суммы из xlsx/CSV не переокругляются «для красоты»: если данные не сходятся сами по себе, об этом сообщается, а не исправляется.

### Ворота 3 — единый режим НДС по документу

- Все строки одного УПД идут в одном режиме НДС: либо все со ставкой, либо все «без НДС». Смешивать облагаемые и необлагаемые позиции в одном документе нельзя.
- Ставка `НалСт` одинакова для всех строк документа. Разные ставки — разные документы.
- Обнаружен смешанный режим во входных данных — жёсткая ошибка с перечислением строк и предложением разбить на отдельные УПД. Не выбирать ставку «по большинству» и не подставлять её молча.

### Источник — уже готовый XML: конвертировать, а не пересобирать

Если на входе есть XML-файл документа — свой УПД версии 5.01, файл контрагента, выгрузка из 1С или из личного кабинета оператора — **этот файл и есть источник данных**. Задача «переложи в 5.03» — это перенос реквизитов, а не новая сборка документа.

- Разобрать входящий XML и перенести значения поэлементно по карте соответствия. Реквизиты, которые есть в файле, берутся из файла: ни из таблицы, ни из памяти, ни повторным вопросом пользователю.
- Пересобирать документ из xlsx/CSV, когда XML уже дан, — запрещено: так теряются реквизиты, которых в таблице нет (ИНН/КПП, адреса, основание передачи, подписант, `ДокПодтвОтгр`, номера ГТД, код вида операции).
- Номер и дату документа не менять: конвертация версии формата — не новый документ. Меняются только `ВерсФорм`, `ИдФайл` (он привязан к имени нового файла) и имена реквизитов по карте.
- Входящий файл не правится: он остаётся байт-в-байт как получен, результат — **новый** файл 5.03. Запрет «не изменять XML ФНС» — про правку присланного файла на месте, а не про конвертацию в новый.
- Реквизит, обязательный в 5.03, но отсутствующий во входящем документе (в 5.01 части блоков не было) — спросить у пользователя. Заглушки и «типовые» значения не подставлять.
- Расхождение сумм в исходном XML не исправляется молча: сработали те же ворота 2 и 3 — назвать строку и обе величины.
- После переноса — обязательно ворота 2 (арифметика), затем ворота 1 (XSD). Совпадение с исходником по суммам показать пользователю: число позиций, итог без НДС, НДС, итог с НДС до и после конвертации.

Карта соответствия 5.01 → 5.03 (проверять по действующей XSD, она источник истины):

| В 5.01 / в бумажной форме | В 5.03 |
|---|---|
| `ВерсФорм="5.01"` | `ВерсФорм="5.03"` |
| `НомерСчФ`, `ДатаСчФ` | `НомерДок`, `ДатаДок` |
| `СвЮЛ` | `<ИдСв><СвЮЛУч>` (ИП — `<ИдСв><СвИП>` + `ФИО`) |
| `<СвТов>` | `<ТаблСчФакт>` → `<СведТов>` |
| `СумНал` атрибутом, `СумНалСчФ` | `<СумНал><СумНал>…</СумНал></СумНал>` |
| `КодОКВ` атрибутом на `СвСчФакт` | `<ДенИзм КодОКВ="643" НаимОКВ="Российский рубль"/>` |
| `НалСт="0%"` на необлагаемой позиции | `НалСт="без НДС"` + `<СумНал><БезНДС>без НДС</БезНДС></СумНал>` |
| Подписант отсутствовал | `<Подписант>` обязателен (`СпосПодтПолном` + `ФИО`) |

`0%` и `без НДС` при переносе не взаимозаменяемы: `0%` — экспортная ставка, `без НДС` — освобождение. Если в исходнике стоит одно, а по смыслу операции нужно другое — спросить, не переставлять самостоятельно.

### Порядок отдачи результата

1. Собрать данные: из входящего XML, если он есть (см. «Источник — уже готовый XML»), иначе из таблицы. Привести суммы к `Decimal`.
2. Проверить единый режим НДС.
3. Провести арифметическую сверку строк и итогов.
4. Сформировать XML 5.03 в `windows-1251`.
5. Прогнать XSD.
6. Только после успешных 2–5 — сохранить файл и отдать пользователю карточку артефакта, а не текст XML в чат.

## Anti-patterns

- **Не править** присланный XML-файл ФНС или контрагента на месте — он остаётся как получен; конвертация в **новый** файл 5.03 разрешена и описана выше
- **Не пересобирать** документ из таблицы, когда на входе уже есть XML — переносить реквизиты из него
- **Не отдавать** сгенерированный УПД без успешной XSD-валидации и арифметической сверки
- **Не генерировать** УПД версии 5.01 и не смешивать режимы НДС в одном документе
- **Не печатать** готовый XML текстом в чат — отдавать файлом
- **Не разбирать** XML парсером с сущностями и DTD по умолчанию — только `etree.XMLParser(resolve_entities=False, no_network=True, load_dtd=False)`; это касается и входящих файлов, и собственного результата
- **Не генерировать** прочие XML-форматы ФНС — отчётность и декларации (для них нужны 1С, Контур); УПД, счёт-фактура и ДОП — исключение, они разрешены и описаны выше
- **Не давать** налоговых консультаций — только анализ данных из документа
- При неуверенности в интерпретации — рекомендовать обращение к налоговому специалисту
