# DaData Connector
**name**: dadata_connector
**version**: 19.0.2025.12.03
**author**: MK.Lab
**depends**: `base`, `web`, `contacts`, `account`, `l10n_ru_doc`
**external_dependencies**: `dadata==21.10.1`

## Архитектура

Модуль состоит из трёх частей: серверная логика запроса к DaData и разбор ответа, визард подтверждения найденных данных, и OWL-компонент (виджет) который добавляет кнопку поиска к полям Tax ID (ИНН) и ОГРН прямо в форме.

```
dadata_connector
├── res.partner (расширение)          — запрос к DaData, разбор ответа
├── res.partner.auto_data.wizard      — диалог подтверждения данных
├── res.config.settings (расширение)  — хранение токена DaData
└── SearchField (OWL-компонент)       — виджет с кнопкой поиска в форме
```

---

## Модели

### `res.partner` — Расширение карточки контрагента

**Файл:** `models/res_partner.py`

Добавляет логику поиска и обработки данных из DaData. Новых полей в базу не добавляет — только методы и переопределение виджета.

#### Переопределение виджета (`_get_view`)

При загрузке формы контрагента метод `_get_view` программно устанавливает виджет `dadata_search` для поля `vat` (ИНН). Это сделано через код, а не через XML, чтобы установить приоритет над стандартным виджетом `partner_autocomplete` от Odoo. Он подключается к полю первым, и XML-переопределение его не вытесняет.

Для поля `ogrn` виджет устанавливается через XML (`views/res_partner_views.xml`).

#### Метод `get_legal_entity_data(vat, widget=True)`

Точка входа вызывается из JS-компонента при нажатии кнопки поиска. Выполняет три шага:

1. Получает токен через `get_dadata_token()`
2. Отправляет запрос в DaData: `dadata.find_by_id("party", vat, branch_type="MAIN")` — ищет только головные организации, без филиалов
3. Разбирает ответ через `_parse_dadata_response()` и открывает визард подтверждения

Параметр `widget=True` означает режим работы через интерфейс (возвращает действие открытия визарда). При `widget=False` возвращает словарь с данными напрямую, что используется в тестах.

#### Метод `get_dadata_token()`

Читает токен из системных параметров (`dadata_connector.dadata_token`). Если токен не задан, возникает `ValidationError` с подсказкой, где его найти в настройках.

#### Метод `_parse_dadata_response(data)`

Разбирает JSON-ответ от DaData и возвращает два словаря:

- **`wizard_data`** — данные для показа в диалоге подтверждения: статус организации, тип (юрлицо / ИП), название, полный адрес строкой
- **`new_data`** — данные для записи в карточку контрагента: все поля ниже

**Маппинг полей DaData → Odoo:**

| Поле Odoo | Поле DaData | Примечание |
|---|---|---|
| `vat` | `inn` | ИНН |
| `ogrn` | `ogrn` | ОГРН |
| `kpp` | `kpp` | Только для юрлиц |
| `okpo` | `okpo` | ОКПО |
| `arceat` | `okved` | Основной ОКВЭД |
| `company_form` | `opf.code` → таблица `okopf` | Код ОКОПФ переводится в тип: `sp`, `plc`, `jsc` и т.д. |
| `sp_register_number` | `documents.fts_registration` | Серия и номер через пробел |
| `sp_register_date` | `documents.fts_registration.issue_date` | Дата регистрации в налоговой |
| `name` | `name.short_with_opf` (юрлицо) или `fio` (ИП) | |
| `country_id` | `address.data.country_iso_code` | Поиск по коду страны |
| `state_id` | `address.data.region_iso_code` | Поиск по коду региона |
| `city` | `address.data.city` | |
| `street` | Собирается из нескольких полей | `street_with_type`, `house_type_full`, `house`, `flat_type_full`, `flat` через запятую |
| `zip` | `address.data.postal_code` | |
| `management` | `management.name` и `management.post` | Не поле, а вложенный словарь для создания контакта |

**Таблица ОКОПФ** (словарь `okopf` в начале файла): переводит цифровые коды организационно-правовых форм в технические коды Odoo. Например, `50102` → `sp` (индивидуальный предприниматель), `12300` → `plc` (ООО).

---

### `res.partner.auto_data.wizard` — Диалог подтверждения

**Файл:** `wizard/res_partner_auto_data_wizard.py`

Временная модель (`TransientModel`), работает только пока открыт диалог. Показывает пользователю найденные данные перед тем, как они будут применены к карточке.

| Поле | Тип | Описание |
|---|---|---|
| `partner_id` | Many2one → `res.partner` | Контрагент, к которому применяются данные |
| `status` | Selection | Статус: active / liquidating / liquidated / bankrupt / reorganizing |
| `organization_type` | Selection | Тип: legal (юрлицо) / individual (ИП) |
| `name` | Char | Название организации |
| `full_address` | Text | Полный юридический адрес строкой |

Форма открывается в режиме только для чтения (`create="0" delete="0" edit="0"`), редактировать данные в диалоге нельзя.

**Метод `button_yes()`** — при нажатии «Да» закрывает диалог и передаёт во фронтенд сигнал `{"update": True}`. JS-компонент перехватывает этот сигнал и применяет данные к карточке.

---

### `res.config.settings` — Расширение настроек

**Файл:** `models/res_config_settings.py`

Добавляет одно поле в раздел «Интеграции» в настройках Odoo. Значение хранится в системных параметрах.

| Поле | Параметр | Описание |
|---|---|---|
| `dadata_token` | `dadata_connector.dadata_token` | API-токен сервиса DaData |

---

## OWL-компонент: виджет `dadata_search`

**Файл:** `static/src/views/fields/search/search_field.js`

Кастомный виджет для полей типа `char`. Визуально выглядит как стандартное текстовое поле с иконкой поиска справа. Зарегистрирован в реестре виджетов Odoo как `dadata_search`.

#### Логика метода `search()`

Вызывается при нажатии иконки поиска. Последовательность:

1. Вызывает серверный метод `res.partner.get_legal_entity_data()` через ORM с текущим значением поля
2. Получает в ответ действие открытия визарда подтверждения
3. Открывает визард и ждёт его закрытия
4. При закрытии проверяет флаг `closeInfo.update`:
   - Если `True` — применяет данные к карточке и сохраняет
   - Если `False` (нажали «Нет») — ничего не делает

#### Применение данных к карточке

Перед обновлением карточки фильтрует только те поля, которые реально существуют в модели (`record.fields`). Many2one-поля (например, `country_id`, `state_id`) конвертируются в формат `[id, ""]` — так требует OWL.

После обновления полей принудительно устанавливает `company_type: "company"` — контрагент всегда сохраняется как организация.

#### Создание контакта-руководителя

Если в ответе DaData есть данные об управляющем (`management`), компонент проверяет нет ли уже контакта с таким именем и должностью среди дочерних записей контрагента. Если нет — создаёт новый контакт через `res.partner.create()` с привязкой `parent_id` к текущему контрагенту.

---

## Представления

**Файл:** `views/res_partner_views.xml`

Два расширения формы контрагента:

- Расширение `l10n_ru_doc.view_partner_ru_form` — добавляет виджет `dadata_search` к полю `ogrn`
- Расширение `base.view_partner_form` — добавляет виджет `dadata_search` к полю `vat`. Приоритет `2`, но виджет также устанавливается программно через `_get_view` для гарантированного перебития `partner_autocomplete`

**Файл:** `views/res_config_settings_view.xml`

Добавляет поле токена в настройки.

---

## Тесты

**Файл:** `tests/`

| Файл | Что покрывает |
|---|---|
| `test_res_partner.py` | Запрос к DaData, разбор ответа, маппинг полей |
| `test_wizard.py` | Создание визарда, отображение полей, кнопка подтверждения |
| `common.py` | Общие фикстуры: клиент DaData, тестовый контрагент |
