51 KiB
osv-result-mapping Specification
Purpose
Маппинг файла результата распознавания (recognition-result.schema.json v1.x, osvDocument) в document и
osv_entry отдельным фоновым циклом (§1.3, §1.5, §3 п. 4, §9.3); резолв счетов и контрагентов; проверки шапки;
статус документа.
Requirements
Requirement: Цикл маппинга отделён от inbox
Фоновая задача ядра SHALL с периодом MAPPING_POLL_SECONDS (5) выбирать одно задание со status ∈ {done, needs_check}, чей файл имеет status = PROCESSING и current_recognition_job_id = это задание (FOR UPDATE SKIP LOCKED), проверять, что result_s3_object относится к бакету хранилища и его ключ начинается с
results/{file_id}/{job_id}/ (OQ-70), читать объект из S3 вне транзакции, валидировать RecognitionResult v1.x и
в одной транзакции создать документы и строки (листы ОСВ и формы ББ/ОФР), записать recognition_job.group_schema,
перевести файл в PROCESSED и выполнить автозамену. Результат без документов SHALL давать неповторяемую ошибку
маппинга «в результате нет документов»; результат с формами, но без ОСВ, ошибкой MUST NOT быть. Inbox MUST NOT читать
файл результата и MUST NOT создавать документы.
Scenario: Результат смапплен
- WHEN в S3 лежит результат парсера для
rb/2024-FY/ОСВ_66 за 2024.xls, заданиеdone, файлPROCESSING - THEN не позднее чем через
MAPPING_POLL_SECONDS+ время маппинга файл —PROCESSED, создан одинdocument(OSV,osv_account_code = "66",period_year = 2024,period_quarter = Q4,is_cumulative = true,locator = sheet:Лист_1),osv_entry— все строки результата кромеTOTAL,group_schemaзадания содержит[{"locator": "sheet:Лист_1", "groups": [...]}]
Scenario: Ключ результата вне папки задания
- WHEN
result_s3_object.key = results/<другой file_id>/.../result.json - THEN файл —
ERRORс ошибкой маппинга, документы не созданы, повтор не выполняется
Scenario: Сбой маппинга с повтором
- WHEN вставка строк падает по ошибке БД
- THEN транзакция откачена,
metrics.mapping_attempts = 1, файл остаётсяPROCESSING; на третьей неудаче —ERRORсmetrics.mapping_error
Scenario: Повторный маппинг идемпотентен
- WHEN маппинг прерван после частичной вставки (имитация) и выполнен снова
- THEN у файла ровно один набор документов и строк, дублей по
(document_id, row_number)нет
Scenario: Файл только с формами
- WHEN смаплен golden-снимок
mezheninovskaya/2025-Q3/1_Форма1_бух_09(одна формаBALANCE, ОСВ нет) - THEN файл
PROCESSED, создан документBALANCE, ошибки «нет документов ОСВ» нет
Scenario: Пустой результат
- WHEN результат без документов (
documents = []): контракт v1.x такого результата не допускает — его останавливает проверка схемы, а прямой вызовmap_fileдаётMappingFatal - THEN файл
ERROR, причина вmetrics.mapping_error(при проверке схемы — ошибка схемы, вmap_file— «в результате нет документов»), повтор не выполняется
Requirement: Документ по листу ОСВ
На каждый osvDocument ядро SHALL создать document с document_type = OSV, osv_account_code = нормализованный
код из шапки (60.1 → 60.01, 58.03, буквенные сегменты как есть), period_year, period_quarter, is_cumulative
по шапке, locator, legal_entity_id владельца файла. Второй лист того же файла с тем же счётом MUST NOT создавать
документ: в metrics.mapping_warnings пишется предупреждение, первый документ получает NEEDS_CHECK.
Scenario: Два листа одного счёта
- WHEN результат содержит два
osvDocumentсо счётом60 - THEN один
document, статусNEEDS_CHECK, предупреждение называет обаlocator
Requirement: Строки ОСВ
Ядро SHALL сохранить все строки osvDocument.rows кроме group_type = TOTAL (OQ-01) как osv_entry: row_number,
level, row_value, group_type, group_name, data_type (включая NU), шесть сумм без изменений,
counterparty_name/counterparty_inn как в результате, parent_entry_id по parent_row_number, account_code
(как в результате), account_root (первый сегмент, None если он не цифровой — ASSUMPTION(OQ-103)), account_id
(точное совпадение нормализованного кода с chart_of_account.code, иначе NULL), owner_legal_entity_id,
recognition_job_id, is_modified = false.
Scenario: Дерево и счета
- WHEN смапплен результат
rb/2024-FY/ОСВ_66 за 2024.xls - THEN строк
osv_entry= строк результата − 1 (TOTAL); у «Юг-Финанс / ПЕКАРНЯ СЧАСТЛИВОЧНАЯ»parent_entry_idуказывает на строку66.03,account_code = "66.03",account_root = "66",account_id= строка плана счетов66.03
Scenario: Все golden-снимки парсера
- WHEN в БД маппятся все
../fixtures/golden/expected_osv_parse/**(82 результата, маркерfixtures) - THEN для каждого: число строк =
rowsбез TOTAL, у каждой строки уровня > 0 есть родитель, суммы равны снимку,account_idзаполнен у всех строк-счетов, код которых есть в плане счетов seed
Requirement: Резолв контрагента
Для строк group_type = COUNTERPARTY с data_type ∈ {BU, SUM} ядро SHALL искать counterparty_id по §3 п. 4: по
counterparty_inn в legal_entity.inn; иначе по точному counterparty_name в legal_entity.name; иначе по
normalize_alias(counterparty_name) среди нормализованных имён ЮЛ и legal_entity_alias.normalized_alias; больше
одного кандидата на шаге → NULL. Резолв SHALL выполняться по всему справочнику без фильтра видимости. Вместе с
counterparty_id SHALL записываться counterparty_match_kind: INN, NAME или ALIAS по шагу, на котором нашёлся
единственный кандидат (нормализованное имя ЮЛ и алиас — оба ALIAS); при NULL — NULL.
Scenario: По ИНН, по алиасу, неоднозначно
- WHEN в справочнике есть ЮЛ с ИНН
3528052718, ЮЛ с алиасомрусский бисквит, и два ЮЛ с одинаковым нормализованным именемальфа - THEN строка с ИНН
3528052718→ это ЮЛ,counterparty_match_kind = INN; строка «Русский бисквит» без ИНН → ЮЛ по алиасу,ALIAS; строка «ООО Альфа» →counterparty_id = NULL,counterparty_match_kind = NULL
Scenario: Алиас приоритетнее ЮЛ-тёзки
- WHEN в справочнике есть ЮЛ «ООО Гамма Сервис» и другое ЮЛ с алиасом
гамма сервис, строка — «Гамма Сервис» без ИНН - THEN
counterparty_id— владелец алиаса,counterparty_match_kind = ALIAS; тот же результат при дорезолве свода на чтение
Requirement: Проверки шапки и статус документа
counterparty_check SHALL быть MATCH, если ИНН шапки равен ИНН владельца, или (без ИНН) нормализованное имя шапки
совпадает с нормализованным именем или алиасом владельца; MISMATCH иначе; UNKNOWN при пустой шапке.
period_check SHALL быть UNKNOWN до появления версии консолидации (Срез 4). Для period_quarter ∈ {Q2, Q3, Q4} с
is_cumulative = false ядро SHALL сверить сальдо на начало корневой строки с документом Q1 того же года, ЮЛ и
счёта (файл не SUPERSEDED) и при совпадении (±0.01) установить is_cumulative = true с предупреждением
(ASSUMPTION(OQ-02)). Статус документа при маппинге SHALL быть RECOGNIZED, если задание done, checks без
нарушений, counterparty_check ≠ MISMATCH и нет предупреждений маппинга; иначе NEEDS_CHECK (OQ-05). Документ
RECOGNIZED в той же транзакции SHALL подтверждаться автоматически (требование «Автоподтверждение чистого документа»).
Scenario: Шапка другой организации
- WHEN файл загружен для ЮЛ «Пекарня Счастливочная», а в шапке
АО "Русский бисквит" - THEN
counterparty_check = MISMATCH,status = NEEDS_CHECK
Scenario: Нарастающая по данным
- WHEN у ЮЛ есть документ ОСВ 60 за 2024 Q1 (
opening_*корня = X) и маппится ОСВ 60 «за 3 квартал 2024» сopening_*корня = X - THEN
is_cumulative = true, предупреждение «нарастающая по данным Q1»
Scenario: Чистый результат
- WHEN задание
done, шапка совпадает с владельцем, нарушений нет - THEN после маппинга
status = CONFIRMED(автоподтверждение),counterparty_check = MATCH,period_check = UNKNOWN
Requirement: Документ NEEDS_CHECK переводит версию в корректировку
После фиксации документов файла и автоподтверждения чистых документов цикл маппинга SHALL для каждой версии в
DOCUMENTS_PROCESSING, в периметр которой входит ЮЛ файла, проверить документы файла: если среди них есть документ
NEEDS_CHECK, подходящий версии по периоду (правила привязки documents-api), версия SHALL перейти в DATA_CORRECTION
с changed_by = stored_file.uploaded_by в той же транзакции маппинга (версии блокируются FOR UPDATE по id после
файла). Если uploaded_by NULL, переход SHALL пропускаться с записью в лог. Привязка consolidation_document для
документа NEEDS_CHECK при маппинге MUST NOT создаваться (привязку при маппинге создаёт только автоподтверждение).
Scenario: Предупреждение маппинга двигает статус
- WHEN версия 2024 Q3 в
DOCUMENTS_PROCESSING; смапплен файл ЮЛ периметра с ОСВ 62 за 2024 Q3 вNEEDS_CHECK - THEN версия в
DATA_CORRECTION, запись истории сchanged_by= загрузивший;consolidation_documentнет
Scenario: Документ RECOGNIZED
- WHEN версия 2024 Q3 в
DOCUMENTS_PROCESSING; смапплен файл с единственным чистым документом за 2024 Q3 - THEN документ
CONFIRMEDи привязан; переход версии вDATA_CORRECTIONзаписан автоподтверждением (комментарий «автоподтверждение документа …»), этот хук второго перехода не делает
Scenario: Документ не подходит версии
- WHEN версия 2024 Q3 в
DOCUMENTS_PROCESSING; смапплен файл с единственным документомNEEDS_CHECKза 2025 Q1 - THEN статус версии не изменился
Requirement: Документ формы ББ/ОФР
Каждый statementDocument с document_type ∈ {BALANCE, INCOME} SHALL давать document (osv_account_code = NULL,
is_cumulative = NULL, locator из результата, period_check = UNKNOWN) и financial_statement_meta 1:1. Дата
периода — header.report_date, при её отсутствии — самая поздняя columns[].period_date; period_year — год даты,
period_quarter — квартал, если дата — последний день марта, июня, сентября или декабря, иначе NULL с
предупреждением; reporting_date — эта дата. Шапка: inn, inn_original_value = header.inn, inn_confidence,
inn_verified = (inn_confidence = 100); organization_name; report_date = header.report_date,
report_date_original_value = header.report_date_text; units = units_original_value = header.units; okud,
okpo, okopf, okfs, okei, address — как пришли (шапка 1.x — по полям 1.x); statement_type — balance/income. Колонки — по columns[],
is_current = true у колонки с самой поздней period_date. Строки — по lines[] с исходниками кода и названия; повтор
кода сохраняется как отдельная строка (Р-4), порядок строк — row_index (с 0). Значение SHALL создаваться на каждую пару «строка × колонка»: из результата —
amount, string_value, confidence, is_recognized; отсутствующая ячейка — пустая запись с needs_review = false; is_modified = false,
original_amount = NULL. Сохранение результата — требование «Хранение результата формы v2.0 (DBML v5.19)». Нарушения checks результата 2.0 — в document_issue; для 1.x §7 считает ядро при приёме (требование «Формы результата 1.x — §7 при приёме»). counterparty_check — тем же
правилом, что у листа ОСВ (ИНН шапки, иначе имя или алиас владельца).
Scenario: Форма из Excel
- WHEN смаплен golden-снимок
mezheninovskaya/2025-Q3/1_Форма1_бух_09 - THEN документ
BALANCE,period_year = 2025,period_quarter = Q3,reporting_date = 2025-09-30; уfinancial_statement_meta37 строк и по значению на каждую пару строка × колонка
Scenario: Книга с двумя формами
- WHEN смаплен golden-снимок книги НПЗ
npz/2025-Q3/…НОВОРОССИЙСКИЙ ПРОКАТНЫЙ ЗАВОД - THEN у файла документы
BALANCE(132 строки) иINCOME(43 строки), оба за 2025 Q3
Scenario: Форма из OCR без даты отчёта
- WHEN смаплен эталон нормализатора
ocr/expected_normalize/aep_6(дата отчётаnull, колонки баланса 2025-06-30, 2024-12-31, 2023-12-31) - THEN документ
BALANCEза 2025 Q2,is_current— у колонки 2025-06-30,report_dateв шапкеNULL,innNULL
Scenario: Дата не конец квартала
- WHEN дата отчёта формы — 2025-10-31
- THEN
period_year = 2025,period_quarter = NULL, предупреждение маппинга, документNEEDS_CHECK
Requirement: Повтор формы в файле и прочие документы
Каждый BALANCE / INCOME результата SHALL давать отдельный document с собственным file_ordinal; вторая форма того же типа в файле хранится (Р-10). Формы одного типа и одной пары period_year, period_quarter получают предупреждение маппинга «в файле есть другая форма <тип> за тот же период», документ — NEEDS_CHECK, автоподтверждения нет (Р-18). Документы DISCLOSURE и OTHER результата SHALL давать по одному document каждого типа на файл (file_ordinal — индекс первого такого документа в documents[]) без
данных (locator — все locator этого типа через запятую, статус при маппинге RECOGNIZED, затем автоподтверждение →
CONFIRMED). Форма BALANCE/INCOME без строк SHALL создаваться с шапкой и колонками (если колонки есть в результате).
Scenario: Расшифровки
- WHEN смаплен синтетический снимок
st06_header_variants(формаINCOMEи пятьDISCLOSURE) - THEN у файла два документа:
INCOMEи одинDISCLOSUREс пятьюlocatorчерез запятую;DISCLOSURE—CONFIRMED
Scenario: Форма без колонки кодов
- WHEN смаплен синтетический снимок
st03_no_codes(BALANCE, строк 0) - THEN документ
BALANCENEEDS_CHECKс шапкой, строк нет (колонок в снимке тоже нет — создаются те, что есть в результате)
Scenario: Две формы одного типа и периода
- WHEN результат содержит два
BALANCEс одной паройperiod_year,period_quarter - THEN создаются два документа с разными
file_ordinal, обаNEEDS_CHECKс предупреждением маппинга; ни один не автоподтверждается
Requirement: Статус документа формы
Документ BALANCE/INCOME, принятый по результату 1.x, SHALL быть RECOGNIZED, только если одновременно: задание done; нет записей document_issue вида relation без акцепта (§7, посчитанный ядром при приёме, statement-checks); counterparty_check ≠ MISMATCH; period_year и
period_quarter определены; ни у одного значения нет needs_review = true (при приёме 1.x needs_review = НЕ values_ok); нет предупреждений маппинга по документу; в файле нет второй формы того же типа и периода (Р-18); есть хотя бы одна строка. Иначе NEEDS_CHECK. Для результата 2.0 условие — требование «Условие RECOGNIZED при приёме 2.0». Статус
SHALL пересчитываться при маппинге, после правки формы и после акцепта, кроме документа CONFIRMED. Документ
RECOGNIZED после маппинга SHALL подтверждаться автоматически (требование «Автоподтверждение чистого документа»);
RECOGNIZED, полученный пересчётом после правки или акцепта, — только вручную. Документ формы NEEDS_CHECK SHALL
участвовать в переходе версии в DATA_CORRECTION наравне с ОСВ (требование «Документ NEEDS_CHECK переводит версию в
корректировку»).
Scenario: Чистая форма Excel
- WHEN смаплен
mezheninovskaya/2025-Q3/1_Форма2_бух_09(ОФР,done, соотношения сходятся, ИНН шапки = ИНН владельца файла) - THEN документ
INCOMECONFIRMED(автоподтверждение)
Scenario: Форма OCR
- WHEN смаплен эталон
ocr/expected_normalize/aep_3(уверенность значений < 100, ИНН шапки ≠ ИНН владельца) - THEN документ
BALANCENEEDS_CHECK,counterparty_check = MISMATCH
Scenario: Форма двигает версию
- WHEN версия ГК 2025 Q3 в
DOCUMENTS_PROCESSING, в периметре ЮЛ файла; смаплена формаBALANCEза 2025 Q3 вNEEDS_CHECK - THEN версия в
DATA_CORRECTION, привязкиconsolidation_documentнет
Scenario: Чистая после правки — вручную
- WHEN форма
NEEDS_CHECKисправлена аналитиком (PATCH …/statement) и пересчёт далRECOGNIZED - THEN документ остаётся
RECOGNIZEDдоPOST …/confirm
Requirement: Автоподтверждение чистого документа
В транзакции маппинга после автозамены прежних версий файла и до перехода версий по NEEDS_CHECK цикл маппинга SHALL
перевести каждый документ файла, получивший при маппинге статус RECOGNIZED, в CONFIRMED (ТЗ 03_9 п. 4, 10_10;
решение владельца 08.10.2026, OQ-157): confirmed_by = NULL, confirmed_at = now; у формы BALANCE/INCOME —
financial_statement_meta.reviewed_by = NULL, reviewed_at = now; audit_event(DOCUMENT_CONFIRM, PRIMARY_DOCUMENTS, object_type = document) с user_id = NULL, before_value.status = RECOGNIZED, after_value.status = CONFIRMED и
description = «автоподтверждение: без замечаний». Форма BALANCE/INCOME с шапкой SHALL подтверждаться
автоматически, только если выполнены условия ручного подтверждения (period_year и period_quarter определены, нет
непройденных неакцептованных жёстких проверок §7); иначе она MUST оставаться RECOGNIZED без ошибки. Документы
NEEDS_CHECK MUST NOT подтверждаться автоматически.
Автоподтверждённый документ SHALL привязываться к версиям так же, как при ручном подтверждении (documents-api
«Подтверждение документа»: все подходящие версии «в работе», замена привязки того же ключа R-1), и каждая версия,
получившая привязку, из INITIATED или DOCUMENTS_PROCESSING SHALL переходить в DATA_CORRECTION с changed_by = stored_file.uploaded_by и comment = «автоподтверждение документа <document_type> <osv_account_code>» (счёт — только у
ОСВ). Если uploaded_by NULL, привязка SHALL создаваться, а переход — пропускаться с записью в лог. Пересчёт статуса
формы после правки или акцепта MUST NOT подтверждать документ автоматически.
Scenario: Чистая ОСВ сразу «Загружен»
- WHEN смаплен результат
fixtures/osv/rb/2025-Q1/ОСВ сч 60 за 1 кв 2025 год.xlsxдля ЮЛ АО «Русский бисквит» (заданиеdone, шапка совпадает с владельцем, нарушений и предупреждений нет) - THEN документ
OSV60 —CONFIRMED,confirmed_byNULL,confirmed_atзаполнен; естьaudit_event(DOCUMENT_CONFIRM)сuser_idNULL и описанием «автоподтверждение: без замечаний»
Scenario: Привязка и переход версии
- WHEN версия ГК «Русский Бисквит» 2025 Q1 в
INITIATED, РБ в периметре; смаплен чистый результат ОСВ 60 РБ за 2025 Q1, файл загрузилka_go - THEN
consolidation_document(version, document)создан; версия вDATA_CORRECTION; запись историиINITIATED → DATA_CORRECTIONсchanged_by=ka_goи комментарием «автоподтверждение документа OSV 60»
Scenario: Документ с замечанием ждёт аналитика
- WHEN файл ОСВ загружен для ЮЛ «Пекарня Счастливочная», а в шапке
АО "Русский бисквит" - THEN документ
NEEDS_CHECK,confirmed_atпуст, событияDOCUMENT_CONFIRMнет, привязки нет
Scenario: Чистая форма Excel
- WHEN смаплен
statements-xlsx/mezheninovskaya/2025-Q3/1_Форма2_бух_09(ОФР,done, соотношения сходятся, период определён) - THEN документ
INCOMECONFIRMED,reviewed_byNULL,reviewed_atзаполнен
Scenario: Форма OCR не подтверждается сама
- WHEN смаплен эталон
ocr/expected_normalize/БО 1 кв 2025(уверенность значений < 100, дата отчёта не разобрана) - THEN документы
BALANCEиINCOME—NEEDS_CHECK, неCONFIRMED
Scenario: Файл без загрузившего
- WHEN версия в
INITIATED, ЮЛ в периметре; смаплен чистый результат файла сuploaded_byNULL - THEN документ
CONFIRMEDи привязан к версии; версия осталасьINITIATED; в логе — строка о пропуске перехода
Scenario: Автозамена чистой версией файла
- WHEN документ ОСВ 60 РБ за 2025 Q1 из файла A привязан к версии 2025 Q1; загружен и смаплен файл B того же ключа (чистый), A заменён автозаменой
- THEN привязка документа A снята, документ B
CONFIRMEDи привязан к версии
Requirement: Хранение результата формы v2.0 (DBML v5.19)
Маппер результата 2.0 SHALL писать документ формы как пришёл: строки, коды и значения не сливаются и не
дедуплицируются (повтор кода — отдельные строки), каждая строка результата — financial_statement_line (row_index,
source_ref = ref, line_status, line_code, line_code_original_value = line_code результата, line_code_text,
line_code_confidence, line_name, line_name_confidence, parent_row_index — row_index строки с ref = parent_ref,
needs_review, is_confirmed = false); каждая пара «строка × колонка» — одна financial_statement_value (amount,
string_value, source_text = values[].text, confidence, is_recognized, needs_review; значения, которого нет в
результате, — пустая запись с needs_review = false); колонки — financial_statement_column с period_kind,
label_confidence; шапка — financial_statement_meta с form_variant, needs_review_fields и header_source
(раздел «Шапка формы»); предупреждения и нарушения документа — document_issue (источник service); версия общего кода —
recognition_job.statements_version. Единственные случаи, когда элемент результата не записывается, — нарушение
уникальности ключей хранения, и каждый из них MUST фиксироваться document_issue MAPPING_WARNING (info, источник
core): значение с column_index вне колонок формы; повтор column_index колонки — остаётся первая колонка, остальные
пропускаются; повтор row_index или ref у строк — остаётся первая строка, остальные пропускаются; detail с parent_ref,
которого нет, хранится с parent_row_index = null. Уникальность (meta_id, line_code) снята; (meta_id, source_ref)
уникален там, где source_ref не null. Каждый BALANCE / INCOME результата SHALL становиться отдельным document;
вторая форма того же типа в файле хранится (Р-10); ОСВ по счёту, DISCLOSURE и OTHER — по-прежнему один документ на тип
(и счёт) на файл. Каждый создаваемый document (ОСВ, формы, DISCLOSURE, OTHER) при приёме результата любого
MAJOR (мапперы 1.x и 2.0) SHALL получать file_ordinal = индекс документа в documents[] результата (с 0);
документ, который не создаётся (второй лист ОСВ с тем же счётом, второй DISCLOSURE), номер не занимает, но и не
сдвигает остальные — номера уникальны в файле и могут идти с пропусками.
Scenario: Книга из двух ОСВ
- WHEN результат содержит две ОСВ по счетам 60 и 62 (
documents[0],documents[1]), принятый маппером1.xили2.0 - THEN два документа с
file_ordinal0 и 1; уникальность(stored_file_id, file_ordinal)не нарушена, файл неERROR
Scenario: PDF с ББ и ОФР по 1.x
- WHEN результат
1.2содержитBALANCEиINCOME - THEN документы с
file_ordinal0 и 1
Scenario: Повтор кода сохраняется
- WHEN в результате две строки
okс кодом 2410 (пустая и с(649)) - THEN в БД две строки 2410 с разными
row_indexиsource_ref, обе со своими значениями
Scenario: Форма 2.0 хранится как пришла
- WHEN результат
2.0содержит две строки с кодом 1230 и строку без кода (refсparent_ref) - THEN создано три строки с их
row_index,source_refиparent_row_index; повтор кода не сливается
Scenario: Строка без кода
- WHEN в результате строка
unreadableсline_code_text = "ИЧ,250"и суммой - THEN строка хранится с
line_code = null,line_code_original_value = null,line_status = unreadable,line_code_text = "ИЧ,250"иrow_indexиз результата
Scenario: Два баланса одного периода в одном файле
- WHEN результат содержит два
BALANCEс одной паройperiod_year,period_quarter(лист-продолжение другого типа или двойная печать) - THEN создаются два документа с разными
file_ordinal; ни один не автоподтверждается (Р-18)
Scenario: Два баланса разных периодов в одном файле
- WHEN результат содержит
BALANCEна 30.09.2025 иBALANCEна 31.12.2024 - THEN два документа с разными
file_ordinal; каждый получает статус по своим условиям, правило Р-18 их не касается
Scenario: Повтор column_index колонки
- WHEN в
columns[]результата два элемента сcolumn_index = 1 - THEN сохраняется первая колонка, вторая пропускается, у документа
MAPPING_WARNING
Scenario: Повтор ref строки
- WHEN в результате две строки с одним
ref - THEN сохраняется первая, вторая пропускается с
MAPPING_WARNING; приём не падает
Requirement: Признак строки, созданной правкой
Строка формы SHALL считаться созданной правкой аналитика (шаблонной, origin = template) тогда и только тогда, когда
row_index IS NULL. Маппер 1.x и маппер 2.0 SHALL всегда писать row_index (у 1.x — по порядку строк результата с
0), поэтому у строки из источника он не null. line_code_original_value SHALL хранить код строки из результата как
пришёл (null у unreadable / missing / detail и у строк, созданных правкой) и правками кода MUST NOT меняться.
Признак line_code_original_value IS NULL для origin MUST NOT использоваться.
Scenario: Строка источника без кода — не шаблонная
- WHEN у строки
unreadableиз результатаline_code_original_value = nullиrow_index = 7 - THEN
origin = source; правила шаблонных строк (удаление без сумм, 409) к ней не применяются
Scenario: Назначение одного кода двум строкам источника
- WHEN аналитик назначил код 1250 строке
unreadable(row_index = 7, без сумм), а затем тот же код — строкеmissing(row_index = 2) с суммой - THEN обе строки
okс кодом 1250, ни одна не удалена, 409 нет (повтор кода законен, Р-4)
Requirement: Шапка формы — сырой текст и уверенность
Маппер 2.0 SHALL писать в financial_statement_meta.header_source объект {"<поле>": {"text": <поле>_text, "confidence": <поле>_confidence}} для всех полей шапки результата (organization_name, inn, report_date, units,
okud, okpo, okopf, okfs, okei, address); разобранные значения — в одноимённые колонки; *_original_value —
как прежде: inn_original_value = inn, report_date_original_value = report_date_text, units_original_value = units_text; organization_name_original_value = organization_name, inn_confidence = inn_confidence.
header_source и *_original_value правками MUST NOT меняться. Маппер 1.x заполняет header_source из полей шапки
1.x. StatementOut.header.source SHALL отдавать header_source.
Scenario: Подсказка у поля шапки
- WHEN в результате
organization_name_text = "ООО «Ромашкa»",organization_name_confidence = 67 - THEN
header_source.organization_name = {"text": "ООО «Ромашкa»", "confidence": 67}; экран показывает их в подсказке у поля
Requirement: Условие RECOGNIZED при приёме 2.0
При приёме 2.0 статус документа формы SHALL быть RECOGNIZED ⇔ document.status = done и он согласован с
содержимым документа ∧ в metrics.warnings файла нет предупреждений уровня error ∧ counterparty_check ≠ MISMATCH ∧
период — конец квартала (period_year и period_quarter определены) ∧ у каждой строки ok с неуверенным кодом
(line_code_confidence < 100) распознанное наименование совпало со справочным по коду ∧ в файле нет второй формы того
же типа и периода (та же пара period_year, period_quarter; формы без квартала не считаются); иначе NEEDS_CHECK.
Согласованность: ядро SHALL вызывать общую функцию статуса пакета (statement-normalization, «Шаг 8 — статус
документа и конверта») по входу, собранному из результата: layout_found — lines[] непуст, has_organization_or_inn
и period_known — по шапке и колонкам результата, строки и значения с их needs_review (значения detail функция не
учитывает), число checks.relations_failed, accepted = false, уровни warnings[] документа. done согласован, если
функция тоже даёт done; иначе ядро считает документ needs_check и пишет document_issue
RESULT_STATUS_INCONSISTENT (info, источник core). Это вызов той же функции, а не пересчёт §7 и needs_review. Статус конверта и needs_check соседнего документа в условие
MUST NOT входить (Р-19). Проверки §7, needs_review и предупреждения сервиса при загрузке MUST NOT пересчитываться.
Совпадение наименований: обе строки приводятся к нижнему регистру, ё → е, всё, кроме букв и цифр, — пробел,
пробелы схлопываются, крайние пробелы обрезаются (strip); совпало, если строки равны, или более короткая (не короче 5
символов) входит в более длинную, или отношение сходства difflib.SequenceMatcher ≥ 0,8; сравнение — с любым
справочным именем кода (у кода может быть несколько имён — синонимы и законные повторы, Р-20) в любой редакции словаря
ядра. Если хотя бы одно из сравниваемых имён начинается с «итого по разделу», сравниваются номера раздела: номер —
начальная часть первого слова после фразы из букв i, v, x (латиница; кириллические «і», «х» приводятся к ним),
остальное в слове отбрасывается («v1» → V, «v» → V); совпало ⇔ номера равны; номер не выделен хотя бы у одного имени —
совпадение только при равенстве строк целиком. Кода нет в словаре имён или наименование пусто — не совпало.
Несовпадение — document_issue NAME_MISMATCH (info, источник core, строка). RECOGNIZED SHALL, как и прежде,
автоподтверждаться (OQ-157).
Scenario: Ячейка соседнего документа
- WHEN ББ
done, ОФР того же файлаneeds_check, предупреждений файла уровняerrorнет, ИНН совпал, период — 30.09.2025 - THEN ББ
RECOGNIZEDи автоподтверждается, ОФР —NEEDS_CHECK
Scenario: Потерянный json
- WHEN все документы
done, но вmetrics.warningsфайлаJSON_FAILED(илиTABLE_PARSE_ERROR) уровняerror - THEN все документы формы файла
NEEDS_CHECK
Scenario: Подмена неуверенного кода на соседа
- WHEN строка с кодом 1120 (уверенность 50) и наименованием «Нематериальные активы»
- THEN наименование не совпало со справочным «Результаты исследований и разработок», документ
NEEDS_CHECK,NAME_MISMATCHу строки
Scenario: Законный повтор с другим именем
- WHEN строка 1220 (уверенность 50) с наименованием «НДС, начисленный по отгрузке», а в словаре имён
fsbu4у 1220 есть это имя вторым - THEN наименование совпало,
NAME_MISMATCHнет
Scenario: Подмена итога раздела
- WHEN строка с кодом 1200 (уверенность 50) и наименованием «Итого по разделу I»
- THEN номер раздела I не равен II у справочного «Итого по разделу II»,
NAME_MISMATCH
Scenario: Хвост OCR после номера раздела (Счастливочная, РБ)
- WHEN строка 1500 (уверенность 56, разлепленная) с наименованием «Итого по разделу V1», или строка с наименованием «Итого по разделу V /»
- THEN номер раздела V равен номеру справочного «Итого по разделу V», наименование совпало,
NAME_MISMATCHнет
Scenario: Сервис прислал done при неразобранной строке без значений
- WHEN документ пришёл
done, но в нём строкаunreadableбез значений - THEN функция статуса даёт
needs_check(не все строкиok/detail), документNEEDS_CHECK,RESULT_STATUS_INCONSISTENT; автоподтверждения нет
Scenario: Сервис ошибся со статусом
- WHEN документ пришёл
done, но в нём строкаunreadableс суммой - THEN документ
NEEDS_CHECK,RESULT_STATUS_INCONSISTENTу документа; автоподтверждения нет
Scenario: Период не конец квартала
- WHEN документ
doneс датой отчёта 15.08.2025 - THEN квартал не определён, документ
NEEDS_CHECK(как в D-6) и не автоподтверждается; ручное подтверждение — 409PERIOD_REQUIRED, пока аналитик не поправит дату отчёта
Requirement: Формы результата 1.x — §7 при приёме
Для формы, принятой по результату 1.x (окно перехода после выкладки ядра; checks контракта 2.0 у неё нет), ядро
SHALL один раз при приёме посчитать §7 точкой входа пакета для пересчёта (statement-normalization; вариант формы —
по структуре кодов, как у 2.0) по суммам результата и записать нарушения в document_issue (источник core, виды
relation / soft, как в требовании о document_issue) и form_variant в financial_statement_meta; собственного
пересчёта §7 ядро MUST NOT выполнять. Условие D-6 «проверки сошлись» для 1.x SHALL браться из этого расчёта;
needs_review значения при приёме 1.x = НЕ values_ok (values_ok — is_recognized не false и confidence
равна 100 или пуста), needs_review строки — false. Предупреждения маппера 1.x по документу (повтор кода, значение
вне колонок) — document_issue MAPPING_WARNING (info, источник core); строки metrics.warnings результата 1.x —
в recognition_job.metrics.warnings структурами {code: "LEGACY_WARNING", severity: "info", message}. После правки такая
форма пересчитывается общим кодом, как форма 2.0; MAPPING_WARNING и LEGACY_WARNING статус не держат. Формы,
существовавшие в БД до миграции v5.19, §7 не пересчитываются (требования «Миграция DBML v5.19» и «Очистка форм перед
выкладкой»).
Scenario: Полярность needs_review при приёме 1.x
- WHEN в результате
1.2у значенияconfidence = 100,is_recognized = true, у другого —confidence = 80 - THEN у первого
needs_review = false, у второго —true
Scenario: Форма 1.x с нарушением
- WHEN результат
1.2содержит ББ с несошедшимся1600 = 1700 - THEN у формы запись
document_issuerelationRELATION_FAILEDсcodes = ["1600", "1700"](источникcore), документNEEDS_CHECK;POST …/confirmбез акцепта — 409CHECKS_NOT_PASSED
Scenario: 1.x-документ после PATCH
- WHEN форма принята по результату
1.2, аналитик правит сумму 1230 так, что1200 = 1210 + … + 1260сходится - THEN ядро пересчитывает §7 и статус общим кодом, запись
relationпо 1200 заменена (её больше нет), статус пересчитан;STATEMENTS_VERSION_MISMATCHне пишется (у задания 1.x версии нет)
Requirement: Проверки ядра при приёме результата 2.0
При приёме результата 2.0 ядро SHALL писать в document_issue формы предупреждения и checks результата (источник
service) и проверки ядра (источник core): CODE_NOT_IN_DICTIONARY и CODE_NOT_IN_EDITION (редакция — edition точки
входа пакета, а не по дате; одна запись на код документа, line_ids — все строки ok кода; только для кодов из ровно
четырёх цифр — пятизначные подстроки Excel не проверяются; запись не пишется, если пакет не определил редакцию),
NAME_MISMATCH, RESULT_STATUS_INCONSISTENT (согласованность done — вызовом engine.contract_document_status
пакета) и STATEMENTS_VERSION_MISMATCH (при отличии statements_version задания от версии копии ядра; у 1.x не
пишется). Проверки §7, needs_review и предупреждения сервиса MUST NOT пересчитываться при загрузке. Статус документа
формы — по требованию «Условие RECOGNIZED при приёме 2.0».
Scenario: Пятизначный код не даёт info
- WHEN в ББ строка с кодом
12301(подстрока Excel), которого нет в словаре - THEN
CODE_NOT_IN_DICTIONARYиCODE_NOT_IN_EDITIONдля него не пишутся
Scenario: Четырёхзначный код вне перечня
- WHEN в ББ строка 1120 на бланке
fsbu4, где такого кода нет - THEN запись
CODE_NOT_IN_EDITION(info,core) сline_idsстрокokэтого кода
Scenario: Версия копии отличается
- WHEN результат
2.0принят сstatements_version, отличной от версии копии ядра - THEN запись
STATEMENTS_VERSION_MISMATCH(info,core) уже при приёме
Requirement: Ведомость ОСВ по контракту 2.0
Для листа ОСВ результата 2.0 ядро SHALL создавать документ ОСВ по osvDocument, предупреждения листа
(osvDocument.warnings, OSV_*) писать в document_issue (вид warning, источник service), а статус документа
определять по требованию «Предупреждения файла и ОСВ в API, статус ОСВ при приёме 2.0». Статус ОСВ MUST NOT зависеть от
статуса соседнего документа книги и от статуса конверта.
Scenario: ОСВ в книге с ОФР, требующим проверки
- WHEN книга из ОСВ (
status = done, контрольные суммы сошлись) и ОФР (needs_check) - THEN документ ОСВ
RECOGNIZED, документ ОФРNEEDS_CHECK