Compare commits

...

27 Commits

Author SHA1 Message Date
coopops 5e58c173ee chore(release): publish
Build bootstrap container / build (push) Successful in 3m45s
Release / publish-packages (push) Successful in 29m23s
Release / release (push) Successful in 29m7s
Release / trigger-mono-docs (push) Successful in 2s
Release / trigger-coopenomics-docs (push) Successful in 2s
2026-07-21 20:19:26 +00:00
coopops c1ce7a095d [989-10][@ant] fix: убрать поле СФР и сделать поля организации/подписанта в форме нулёвых отчётов read-only
Поле «Регистрационный номер СФР» в форме ЕФС-1 не использовалось генератором вовсе (в документ и в имя файла идёт только pfrRegNumber) — убрано вместе с ложной проверкой в fss4.generator.ts, требовавшей его заполнения.

Остальные поля секций «Организация»/«Подписант» в ZeroReportEditor.vue (ФИО, СНИЛС, ИНН/КПП/ОКВЭД/ОГРН/ОКТМО, рег. номер ПФР, должность, тип подписанта) всегда приходят из Реквизитов/БД кооператива и правка через этот черновик никуда не сохранялась — редактируемые поля создавали иллюзию, что правка что-то меняет. Переведены в read-only (readonly+disable, как уже сделано для орг-полей в SettingsPage) — правка доступна только на странице Реквизитов. Раздел «Отчётный год/период/корректировка» остался редактируемым — он не приходит из Реквизитов, это per-документ данные.
2026-07-21 20:17:24 +00:00
coopops 995f65f524 [989-10][@ant] fix: убрать лишний поясняющий хинт у поля «Рег. номер ПФР» — по замечанию пользователя ни у одного другого поля в реквизитах/форме ЕФС-1 такого хинта нет, заполняющий и так знает, что это за номер 2026-07-21 19:12:09 +00:00
coopops 5298e55a7b [989-10][@ant] fix: ЕФС-1 (4-ФСС) — использовать рег. номер ПФР, а не СФР, в имени файла и <ЕФС8:РегНомер>
Сторонние бухгалтерские системы (СБИС и др.) при приёме ЕФС-1 отклоняли выгруженный XML: "Рег. номер отправителя из наименования файла не соответствует ни рег. номеру ПФР организации, ни рег. номеру СФР". Причина в двух местах:
1) generateGenericFileName для FSS4 хардкодил сегмент отправителя как "0000000000" — реальный номер туда не попадал вообще (тот же класс бага, что чинили в idFile для нулёвок ранее).
2) fss4.generator.ts писал в <ЕФС8:РегНомер> sfrRegNumber (10-значный унифицированный номер СФР) — но внешние системы сверяют этот тег с рег. номером ПФР организации (формат XXX-XXX-XXXXXX), который у них уже настроен через канал ФСС ЭДО, а не с номером СФР.

Добавлено отдельное обязательное поле pfrRegNumber (report_requisites.pfr_reg_number, XXX-XXX-XXXXXX) — не заменяет sfrRegNumber (оставлен, вдруг понадобится), а используется отдельно для генерации ЕФС-1: и в сегменте имени файла, и в теге <ЕФС8:РегНомер>. Заодно сузили маску sfrRegNumber до строго 10 цифр (раньше маска дублировала формат ПФР и два поля выглядели взаимозаменяемыми — по замечанию в ревью). Цепочка regen пройдена (generate-schema → generate-client → sdk build), controller tsc и desktop vue-tsc чистые, добавлены/обновлены unit-тесты Fss4Generator.
2026-07-21 18:45:12 +00:00
coopops db28ba1137 [989-10][@ant] fix: чинить генерацию имени файла нулёвых отчётов (idFile/ИдФайл) — терялся код формы ФНС/СФР
generateGenericFileName брал префикс как первый underscore-сегмент имени XSD (xsd.split('_')[0]), а для всех форм с xsdFile вида 'NO_<Форма>_...' первый сегмент всегда равен 'NO' — код формы (NDFL6.2, RASCHSV, PERSSVFL, USN, UVISCHSUMNAL) терялся из имени файла. Внешние системы (в частности приёмка Сбера) отклоняли выгруженный XML как нераспознанный по имени, а после ручного добавления '_NDFL6.2_' в имя падала уже сверка тега <ИдФайл> с фактическим именем файла — оба генерировались одной и той же испорченной функцией. Исправлено: префикс строится из первых двух сегментов XSD-имени (slice(0, 2)), что совпадает с реальным форматом имени файла ФНС/СФР и с эталонными фикстурами tests/fixtures/reports-references/*.
2026-07-21 16:23:21 +00:00
coopops 6a05e88ba1 [989-10][@ant] fix: убрать stack-раскладку (иконка над текстом) у кнопок панели действий в ReportEditorDialog расширения reports — canon-паддинг q-btn (0 14px, без вертикального зазора) при вертикальной раскладке icon+label прижимал иконку к верхней границе кнопки, из-за чего панель «Действия»/«Скачать»/«Отметить сданным» выглядела недоделанной; иконка возвращена в строку рядом с текстом 2026-07-21 15:56:09 +00:00
coopops 09eaa6192c [reports][@ant] fix: показывать серверные ошибки валидации формы отчёта для полей шапки и строк баланса — счётчик «Ошибок: N» был непрозрачным без единой подсказки
BuhotchEditor/ZeroReportEditor подключали fieldErrors только для части полей
(организация, часть подписанта). Поля шапки (reportYear/period/docDate/
correctionNumber) и все строки баланса (BalanceRowEditor — otch/prev/prePrev)
не имели inline-подсветки вообще, хотя backend (validateReportEdits) исправно
возвращал path+message. В проде это привело к невидимой ошибке
organization.ogrn — 12 vs 13 цифр (опечатка в реквизитах, отдельно исправлена
data-фиксом в organizations, не кодом).

- BalanceRowEditor: добавлены basePath/fieldErrors пропсы, подсветка otch/prev/prePrev.
- BuhotchEditor: прокинул field-errors во все 6 BalanceRowEditor, подключил
  header.reportYear/docDate/correctionNumber.
- ZeroReportEditor: подключил header.reportYear/period.
- ReportEditorDialog: добавлен явный список ошибок (path → человекочитаемое
  сообщение) в панели действий — страховка на случай ещё не подключённых полей.
2026-07-21 15:45:20 +00:00
coopops 81df662cfa chore(release): publish
Build bootstrap container / build (push) Successful in 46s
Release / trigger-coopenomics-docs (push) Successful in 2s
Release / release (push) Successful in 34m17s
Release / trigger-mono-docs (push) Successful in 2s
Release / publish-packages (push) Failing after 13m36s
2026-07-21 08:50:38 +00:00
coopops 845c13605d fix(reports): разрешить 10-значный рег.номер СФР наравне с XXX-XXX-XXXXXX
XSD-схема ЕФС-1 (ТипРегНомерОбщ) допускает регистрационный номер
страхователя в двух форматах: действующий XXX-XXX-XXXXXX (12 цифр,
3-3-6) или прежний — 10 цифр без разделителей. Валидация на бэке и
фронте (форма реквизитов кооператива + редактор нулевого отчёта
ЕФС-1) принимала только 12-значный формат с тире, из-за чего
кооперативы со старым 10-значным номером ФСС не могли его сохранить.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 08:43:46 +00:00
Alex Ant 76b4988da8 migrate 2026-07-20 11:34:49 +03:00
coopops 3db80a30e9 chore(release): publish
Release / release (push) Failing after 2m16s
Release / publish-packages (push) Has been skipped
Release / trigger-coopenomics-docs (push) Has been skipped
Release / trigger-mono-docs (push) Has been skipped
Build bootstrap container / build (push) Successful in 49s
2026-07-19 17:01:42 +00:00
coopops 1b457b8f87 fix(controller): stop migration failures being masked as success
Backfill-миграция V2.3.2 падала на testnet TS-ошибкой компиляции
(mongo.db возможно undefined, strictNullChecks), но deploy-скрипт
(blue-green-deploy.sh) рапортовал успех — 2.3.2 не появилась в
таблице migrations, extensions.capital_program_doc_data_hash осталась
пустой, а Semaphore-лог показывал "Database migrations completed
successfully".

Причина ложноположительного результата — не баг деплой-скрипта, а
Sentry: в production Sentry.init() (enabled: config.env==='production')
ставит process.on('unhandledRejection', ...) с дефолтным mode:'warn',
который перехватывает необработанный reject из bootstrap()'а --migrate
ветки и не роняет процесс — в отличие от штатного поведения голого
Node. В dev (Sentry выключен) тот же баг воспроизводимо валит процесс
exit 1; на testnet — exit 0. Подтверждено локальным воспроизведением
обеих версий на идентичном коде.

Фикс на двух уровнях:
1. index.ts: явный try/catch + process.exit(1) вокруг --migrate и
   --migrations-only веток. Не полагаемся на ambient unhandled-rejection
   поведение (Sentry/Node/версия) — exit code теперь детерминирован
   независимо от окружения.
2. V2.3.2: mongo.db действительно типизирован как возможно undefined —
   явная runtime-проверка с throw вместо каста.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-19 16:57:59 +00:00
coopops ba6d3d1310 chore(release): publish
Build bootstrap container / build (push) Successful in 1m10s
Release / publish-packages (push) Has been skipped
Release / trigger-mono-docs (push) Has been skipped
Release / release (push) Successful in 28m13s
Release / trigger-coopenomics-docs (push) Has been skipped
2026-07-19 15:20:08 +00:00
ant 8e01373179 Merge pull request 'feat(capital): CPP document templates via PrivateData' (#154) from feature/capital-cpp-private-data-templates into dev
Build bootstrap container / build (push) Successful in 3m30s
Reviewed-on: #154
Reviewed-by: Алексей Муравьев <chairman.voskhod@gmail.com>
2026-07-19 15:14:39 +00:00
coopops 024d46ff64 fix update 2026-07-19 07:55:49 +00:00
coopops ed9b0879d5 chore(release): publish
Build bootstrap container / build (push) Successful in 2m42s
Release / release (push) Successful in 22m23s
Release / publish-packages (push) Successful in 14m53s
Release / trigger-coopenomics-docs (push) Successful in 2s
Release / trigger-mono-docs (push) Successful in 2s
2026-07-17 14:41:21 +00:00
coopops c94043e77f fix(docker): не скачивать бинарники electron/puppeteer в mono-base
Их postinstall тянет тяжёлые сетевые загрузки (Electron zip, Chromium),
не нужные ни для build (unbuild/tsc/vite), ни для runtime — периодически
валил release job TLS-таймаутом, из-за чего publish-packages (needs: release)
не запускался вообще.
2026-07-17 14:40:39 +00:00
coopops c1a8f3e637 fix(boot/Dockerfile): изолировать pnpm install от electron/puppeteer
Build bootstrap container / build (push) Successful in 2m37s
--filter не ограничивает lifecycle-скрипты общего lockfile store — тянулись
и падали по сети чужие тяжёлые постинсталлы (electron/puppeteer из
desktop/notifications), boot их не использует. --ignore-scripts + точечный
pnpm rebuild --filter решают.
2026-07-17 14:13:35 +00:00
coopops 15fdf35dd3 chore(release): publish
Release / trigger-coopenomics-docs (push) Has been skipped
Release / trigger-mono-docs (push) Has been skipped
Build bootstrap container / build (push) Failing after 2m10s
Release / release (push) Failing after 15m13s
Release / publish-packages (push) Has been skipped
2026-07-17 14:02:54 +00:00
coopops 79b89695d1 chore(release): publish 2026-07-17 13:58:44 +00:00
coopops 7b27d0805c chore(release): publish
Release / release (push) Successful in 43m2s
Release / publish-packages (push) Has been cancelled
Release / trigger-coopenomics-docs (push) Has been cancelled
Release / trigger-mono-docs (push) Has been cancelled
Build bootstrap container / build (push) Successful in 46s
2026-07-17 12:54:54 +00:00
coopops e089dbd3fb ci(release): чинить publish-packages — генерировать controller/schema.gql перед publish
С 24 июня (минимум) publish-packages падает на каждом релизе main:
@coopenomics/sdk's prepublishOnly (pnpm run docs → generate-index-comments.ts)
читает components/controller/schema.gql, которого нет в чекауте и никто не
генерирует — ENOENT. lerna publish атомарный, поэтому НИ ОДИН пакет не
публикуется. Итог: main давно на v2026.7.9, а npm видел последний раз
2026.5.30-3 — полтора месяца фиксов (включая убранный kpp из
BankAccountDetailsInput, PR #81) не долетали до потребителей SDK.

Фикс: добавлен шаг generate-schema (изолированный, GraphQLSchemaBuilderModule,
без БД) ПОСЛЕ lerna run build — порядок важен, иначе ts-node падает на
устаревших dist соседних workspace-пакетов (проверено локально: cooptypes →
factory → inter → notifications, каждый был stale и валил компиляцию
controller'а по цепочке).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 12:54:33 +00:00
Alex Ant 5de7e7c65b update 2026-07-13 20:53:21 +03:00
coopops bfbcd2d57f fix(desktop): починить «Продолжить» в оверлее выбора КУ
Кнопка снова submit формы из footer, BranchSelector синхронизирует selectedBranch, ошибки генерации документа показываются пользователю.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-09 16:02:19 +00:00
coopops ff5df68bce chore(release): publish
Release / publish-packages (push) Failing after 21m11s
Release / release (push) Successful in 1h35m7s
Release / trigger-coopenomics-docs (push) Successful in 3s
Release / trigger-mono-docs (push) Successful in 3s
Build bootstrap container / build (push) Successful in 10m29s
2026-07-09 05:49:14 +00:00
coopops a948f07a1b fix(desktop): закрепить кнопки оверлея выбора КУ на мобильных
Кнопки «Продолжить» и «подписать» вынесены в footer BaseDialog, чтобы они не уезжали за пределы экрана при прокрутке контента.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-09 05:48:16 +00:00
coopops 85bd044cd6 fix(desktop): перезагрузка списка КУ при показе оверлея выбора
Оверлей выбора кооперативного участка теперь подгружает список при переходе
в мажоритарный режим, показывает лоадер и блокирует «Продолжить» без выбора.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 20:46:55 +00:00
61 changed files with 531 additions and 199 deletions
+12
View File
@@ -390,6 +390,18 @@ jobs:
- run: pnpm install --frozen-lockfile
- run: pnpm lerna run build
# schema.gql не коммитится и не создаётся ничем в этом job'е, а
# @coopenomics/sdk's prepublishOnly (pnpm run docs → generate-index-comments.ts)
# читает components/controller/schema.gql и падает ENOENT без него — валя весь
# lerna publish (атомарный) молча с 30 мая. Скрипт изолированный (GraphQLSchemaBuilderModule,
# без TypeORM/Mongo/Redis) — не требует БД, но типизация controller'а резолвится через
# dist соседних workspace-пакетов (cooptypes/factory/notifications/inter) — ОБЯЗАТЕЛЬНО
# после `lerna run build`, иначе ts-node падает на их устаревших dist-типах.
- name: Generate controller schema.gql (required by sdk prepublishOnly)
working-directory: components/controller
run: pnpm run generate-schema
- run: pnpm lerna publish from-package --yes
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+8
View File
@@ -45,6 +45,14 @@ RUN corepack enable && npm install -g lerna --no-fund --no-audit
# выкидывает node_modules/dist/.git, поэтому `COPY .` лёгкий.
COPY . .
# electron (devDep desktop → @vue/devtools) и puppeteer нигде в build/runtime
# не запускаются (unbuild/tsc/vite их не трогают) — только их JS-обёртка нужна
# для типов. Их postinstall тянет тяжёлые бинарники (Electron zip с GitHub
# Releases, Chromium) по сети — периодически валит билд TLS-таймаутом.
# Отключаем сами загрузки, пакеты всё равно ставятся.
ENV ELECTRON_SKIP_BINARY_DOWNLOAD=1 \
PUPPETEER_SKIP_DOWNLOAD=true
# Используем существующий lockfile. Если он не совпадает с workspace —
# падать сразу, не дрейфовать незаметно.
RUN pnpm install --frozen-lockfile
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@coopenomics/blago-cli",
"version": "2026.6.24",
"version": "2026.7.21-1",
"description": "CLI синхронизации артефактов Благорост с бэкендом через @coopenomics/sdk",
"type": "module",
"private": true,
+8 -1
View File
@@ -35,7 +35,14 @@ RUN npm install -g pnpm@10.33.0
COPY . .
RUN pnpm install --frozen-lockfile --filter "@coopenomics/boot..."
# --ignore-scripts + точечный rebuild: --filter ограничивает только то, какие
# проекты получают node_modules, но НЕ то, для каких пакетов общего lockfile
# store выполняет lifecycle-скрипты при --frozen-lockfile. Без --ignore-scripts
# сюда тянутся electron/puppeteer (из @coopenomics/desktop/notifications,
# boot их не использует) — тяжёлые сетевые загрузки, изредка валящие билд по
# TLS-таймауту. rebuild --filter уже честно скопирован на подграф boot.
RUN pnpm install --frozen-lockfile --filter "@coopenomics/boot..." --ignore-scripts \
&& pnpm rebuild --filter "@coopenomics/boot..."
# Билд графа: cooptypes/factory собирают свои dist/, boot собирает
# свой bundle с inlined workspace-deps.
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@coopenomics/boot",
"type": "module",
"version": "2026.6.24",
"version": "2026.7.21-1",
"private": true,
"packageManager": "pnpm@9.0.6",
"description": "CLI-утилита инициализации блокчейна и кооператива",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@coopenomics/cleos",
"version": "2026.6.24",
"version": "2026.7.21-1",
"private": true,
"description": "Обёртка над кошельком cleos для EOSIO блокчейна",
"scripts": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@coopenomics/contracts",
"version": "2026.6.24",
"version": "2026.7.21-1",
"private": true,
"type": "module",
"scripts": {
@@ -72,6 +72,9 @@ export default {
try {
mongo = await mongoose.createConnection(config.mongoose.url).asPromise();
const db = mongo.db;
if (!db) {
throw new Error('Подключение к MongoDB установлено, но db не инициализирована');
}
const collection = db.collection(COLLECTION_NAME);
await collection.createIndex({ hash: 1 }, { unique: true });
await collection.updateOne(
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "create-nodejs-express-app",
"version": "2026.6.24",
"version": "2026.7.21-1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "create-nodejs-express-app",
"version": "2026.6.24",
"version": "2026.7.21-1",
"license": "MIT",
"dependencies": {
"@a2seven/yoo-checkout": "^1.1.4",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@coopenomics/controller",
"version": "2026.6.24",
"version": "2026.7.21-1",
"description": "Бэкенд GraphQL API кооператива на NestJS",
"private": true,
"bin": "bin/createNodejsApp.js",
@@ -2,9 +2,9 @@
## Возможности
**Удобное общение** - групповые чаты, личные сообщения, голосовые и видео-звонки
🔐 **Безопасность** - сообщения зашифрованы и защищены
👥 **Для всех участников** - каждый член кооператива автоматически получает доступ к чату пайщиков своего кооператива и общему чату кооперативной экономики.
👥 **Для членов совета** - каждый член совета добавляется в зашифрованный чат совета своего кооператива.
📱 **На всех устройствах** - работает на компьютере, телефоне и планшете
**Быстрая установка** - просто установите расширение, и оно заработает
- **Удобное общение** групповые чаты, личные сообщения, голосовые и видео-звонки
- 🔐 **Безопасность** сообщения зашифрованы и защищены
- 👥 **Для всех участников** каждый член кооператива автоматически получает доступ к чату пайщиков своего кооператива и общему чату кооперативной экономики
- 👥 **Для членов совета** каждый член совета добавляется в зашифрованный чат совета своего кооператива
- 📱 **На всех устройствах** работает на компьютере, телефоне и планшете
- **Быстрая установка** просто установите расширение, и оно заработает
@@ -1,6 +1,7 @@
import { ObjectType, Field, InputType, registerEnumType } from '@nestjs/graphql';
import { IsOptional, IsString, IsIn, Matches, MaxLength } from 'class-validator';
import { ReportType } from '../../domain/enums/report-type.enum';
import { SFR_REG_NUMBER_PATTERN, PFR_REG_NUMBER_PATTERN } from '../../domain/patterns';
export enum RequisiteSource {
DATABASE = 'database',
@@ -43,6 +44,7 @@ export class ReportRequisitesViewDTO {
@Field(() => RequisiteFieldViewDTO) oktmo!: RequisiteFieldViewDTO;
@Field(() => RequisiteFieldViewDTO) okpo!: RequisiteFieldViewDTO;
@Field(() => RequisiteFieldViewDTO) sfrRegNumber!: RequisiteFieldViewDTO;
@Field(() => RequisiteFieldViewDTO) pfrRegNumber!: RequisiteFieldViewDTO;
@Field(() => RequisiteFieldViewDTO) chairmanPosition!: RequisiteFieldViewDTO;
@Field(() => RequisiteFieldViewDTO) signerSnils!: RequisiteFieldViewDTO;
@Field(() => RequisiteFieldViewDTO) signerRepDoc!: RequisiteFieldViewDTO;
@@ -65,7 +67,10 @@ export class ReportRequisitesViewDTO {
* ОКТМО — `\d{8}` или `\d{11}`
* ОКПО — `\d{8}` или `\d{10}`
* СНИЛС — `\d{3}-\d{3}-\d{3} \d{2}` (14 симв.) или 11 цифр подряд
* РегНом СФР — `\d{3}-\d{3}-\d{6}` (14 симв. с тире)
* РегНом СФР — `\d{10}` (10 цифр подряд, без тире)
* РегНом ПФР — `\d{3}-\d{3}-\d{6}` (14 симв. с тире), только этот формат —
* отдельное поле от РегНом СФР, требуется для ЕФС-1 (внешние
* бухгалтерские системы сверяют его при приёме отчёта)
*/
@InputType('UpdateReportRequisitesInput')
export class UpdateReportRequisitesInputDTO {
@@ -99,12 +104,18 @@ export class UpdateReportRequisitesInputDTO {
@Matches(/^\d{8}(\d{2})?$/, { message: 'ОКПО — 8 или 10 цифр' })
okpo?: string | null;
@Field(() => String, { nullable: true, description: 'Рег. номер СФР — XXX-XXX-XXXXXX' })
@Field(() => String, { nullable: true, description: 'Рег. номер СФР — 10 цифр' })
@IsOptional()
@IsString()
@Matches(/^\d{3}-\d{3}-\d{6}$/, { message: 'Рег. номер СФР — XXX-XXX-XXXXXX (14 симв.)' })
@Matches(SFR_REG_NUMBER_PATTERN, { message: 'Рег. номер СФР — 10 цифр' })
sfrRegNumber?: string | null;
@Field(() => String, { nullable: true, description: 'Рег. номер ПФР — XXX-XXX-XXXXXX (для ЕФС-1, отдельно от рег. номера СФР)' })
@IsOptional()
@IsString()
@Matches(PFR_REG_NUMBER_PATTERN, { message: 'Рег. номер ПФР — XXX-XXX-XXXXXX' })
pfrRegNumber?: string | null;
@Field(() => String, { nullable: true })
@IsOptional()
@IsString()
@@ -21,6 +21,7 @@ import {
OKTMO_PATTERN,
OKVED_PATTERN,
SFR_REG_NUMBER_PATTERN,
PFR_REG_NUMBER_PATTERN,
SNILS_PATTERN,
DATE_DDMMYYYY_PATTERN,
} from '../../domain/patterns';
@@ -200,9 +201,14 @@ export class ZeroReportSignerEditsInputDTO {
@Field(() => String, { nullable: true })
@IsOptional()
@Matches(SFR_REG_NUMBER_PATTERN, { message: 'Рег.номер СФР — XXX-XXX-XXXXXX' })
@Matches(SFR_REG_NUMBER_PATTERN, { message: 'Рег.номер СФР — 10 цифр' })
sfrRegNumber!: string | null;
@Field(() => String, { nullable: true })
@IsOptional()
@Matches(PFR_REG_NUMBER_PATTERN, { message: 'Рег.номер ПФР — XXX-XXX-XXXXXX' })
pfrRegNumber!: string | null;
@Field(() => String, { nullable: true })
@IsOptional()
@IsString()
@@ -219,6 +225,7 @@ export class ZeroReportSignerEditsDTO {
@Field(() => String, { nullable: true }) repDoc!: string | null;
@Field(() => String, { nullable: true }) snils!: string | null;
@Field(() => String, { nullable: true }) sfrRegNumber!: string | null;
@Field(() => String, { nullable: true }) pfrRegNumber!: string | null;
@Field(() => String, { nullable: true }) chairmanPosition!: string | null;
}
@@ -53,6 +53,7 @@ export class ReportRequisitesResolver {
oktmo: input.oktmo ?? undefined,
okpo: input.okpo ?? undefined,
sfr_reg_number: input.sfrRegNumber ?? undefined,
pfr_reg_number: input.pfrRegNumber ?? undefined,
chairman_position: input.chairmanPosition ?? undefined,
signer_snils: input.signerSnils ?? undefined,
signer_rep_doc: input.signerRepDoc ?? undefined,
@@ -109,6 +110,7 @@ function toView(m: MergedRequisites): ReportRequisitesViewDTO {
oktmo: f(m.oktmo),
okpo: f(m.okpo),
sfrRegNumber: f(m.sfrRegNumber),
pfrRegNumber: f(m.pfrRegNumber),
chairmanPosition: f(m.chairmanPosition),
signerSnils: f(m.signerSnils),
signerRepDoc: f(m.signerRepDoc),
@@ -51,6 +51,14 @@ export interface ZeroReportSignerShape {
snils: string | null;
/** Регистрационный номер в СФР — обязателен для ЕФС-1. */
sfrRegNumber: string | null;
/**
* Регистрационный номер в ПФР (XXX-XXX-XXXXXX) — обязателен для ЕФС-1,
* попадает и в <ЕФС8:РегНомер>, и в имя файла. Отдельное поле от
* sfrRegNumber: сторонние бухгалтерские системы (СБИС и др.) при приёме
* ЕФС-1 сверяют номер отправителя именно с рег. номером ПФР организации,
* не с 10-значным номером СФР.
*/
pfrRegNumber: string | null;
/** Должность председателя — для ЕФС-1 (<УТ8:Должность>). */
chairmanPosition: string | null;
}
@@ -47,7 +47,7 @@ export interface ReportInput {
correctionNumber?: number;
/** СНИЛС председателя (для ПСВ) */
signerSnils?: string;
/** Регистрационный номер страхователя в СФР/ПФР (для ЕФС-1). */
/** Регистрационный номер страхователя в СФР (для ЕФС-1). Legacy-путь — актуальные генераторы берут ZeroReportSignerShape.pfrRegNumber отдельно. */
sfrRegNumber?: string;
/** Должность руководителя для подачи в СФР (по умолчанию «Председатель Совета»). */
chairmanPosition?: string;
@@ -43,8 +43,19 @@ export const OKPO_PATTERN = /^\d{8}(\d{2})?$/;
// СНИЛС: XXX-XXX-XXX YY (с пробелом/тире) или 11 цифр подряд.
export const SNILS_PATTERN = /^(\d{3}-\d{3}-\d{3} \d{2}|\d{11})$/;
// Регистрационный номер СФР: XXX-XXX-XXXXXX (3-3-6).
export const SFR_REG_NUMBER_PATTERN = /^\d{3}-\d{3}-\d{6}$/;
// Регистрационный номер страхователя в СФР: только 10 цифр без разделителей
// (унифицированный номер СФР, ТипРегНомерОбщ в XSD ЕФС-1). Формат
// XXX-XXX-XXXXXX сюда больше не входит — он теперь отдельное поле
// PFR_REG_NUMBER_PATTERN, чтобы два похожих номера не путались в одной маске.
export const SFR_REG_NUMBER_PATTERN = /^\d{10}$/;
// Регистрационный номер страхователя в ПФР: только формат XXX-XXX-XXXXXX
// (3-3-6, 12 цифр с тире) — старый/«действующий» формат, который сверяют
// сторонние бухгалтерские системы (СБИС и др.) при приёме ЕФС-1. Это
// отдельное поле от sfrRegNumber (10-значный унифицированный номер СФР):
// внешние системы могут не знать организацию по 10-значному номеру, но
// знать по этому.
export const PFR_REG_NUMBER_PATTERN = /^\d{3}-\d{3}-\d{6}$/;
/** Дата в формате `DD.MM.YYYY` (XSD `xs:date`-совместимый ФНС-формат). */
export const DATE_DDMMYYYY_PATTERN = /^\d{2}\.\d{2}\.\d{4}$/;
@@ -10,6 +10,7 @@ export interface ReportRequisitesRecord {
oktmo: string | null;
okpo: string | null;
sfr_reg_number: string | null;
pfr_reg_number: string | null;
chairman_position: string | null;
signer_snils: string | null;
signer_rep_doc: string | null;
@@ -28,6 +29,7 @@ export interface UpsertReportRequisitesInput {
oktmo?: string | null;
okpo?: string | null;
sfr_reg_number?: string | null;
pfr_reg_number?: string | null;
chairman_position?: string | null;
signer_snils?: string | null;
signer_rep_doc?: string | null;
@@ -203,7 +203,12 @@ export class ReportEditsBuilderService {
return {
header: {
idFile: this.generateGenericFileName(reportType, merged.inn.value ?? '', merged.kpp.value ?? ''),
idFile: this.generateGenericFileName(
reportType,
merged.inn.value ?? '',
merged.kpp.value ?? '',
merged.pfrRegNumber.value,
),
versProgram: 'Платформа отчётности кооператива 1.0',
docDate: formatDate(new Date()),
reportYear: year,
@@ -230,6 +235,7 @@ export class ReportEditsBuilderService {
repDoc: merged.signerRepDoc.value,
snils: merged.signerSnils.value,
sfrRegNumber: merged.sfrRegNumber.value,
pfrRegNumber: merged.pfrRegNumber.value,
chairmanPosition:
merged.chairmanPosition.value || merged.chairmanPositionFromOrg.value,
},
@@ -243,19 +249,33 @@ export class ReportEditsBuilderService {
return 1;
}
private generateGenericFileName(reportType: ReportType, inn: string, kpp: string): string {
// Упрощённый префикс по типу отчёта — точный формат имени зависит от
// формы (NO_NDFL6.2_..., СФР_..._ЕФС-1_...), но для предварительного
// idFile достаточно. Генератор сам перезапишет его при необходимости.
private generateGenericFileName(
reportType: ReportType,
inn: string,
kpp: string,
pfrRegNumber?: string | null,
): string {
// Префикс = первые два underscore-сегмента имени XSD (например,
// 'NO_NDFL6.2_1_231_...xsd' → 'NO_NDFL6.2', 'UT_UVISCHSUMNAL_...xsd' →
// 'UT_UVISCHSUMNAL') — это и есть код формата в имени файла по формату
// ФНС/СФР. Брать только первый сегмент нельзя: для всех форм с 'NO_'
// в начале он всегда равен 'NO' и код формы (NDFL6.2/RASCHSV/PERSSVFL/
// USN) терялся — импорт стороннего ПО (Сбер и др.) отклонял файл как
// нераспознанный по имени.
const xsd = REPORT_CONFIG[reportType]?.xsdFile ?? 'REPORT';
const prefix = xsd.split('_')[0] ?? 'REPORT';
const prefix = xsd.split('_').slice(0, 2).join('_') || 'REPORT';
const now = new Date();
const dateStr = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}`;
const unit = `${inn}${kpp}`;
const tax = kpp.substring(0, 4);
const uuid = randomUUID();
if (reportType === ReportType.FSS4) {
return `СФР_0000000000_ЕФС-1_${dateStr}_${uuid}`;
// Сегмент отправителя в имени файла — рег. номер ПФР организации, не
// заглушка из нулей: сторонние бухгалтерские системы (СБИС и др.)
// сверяют этот сегмент со своим профилем организации и отклоняют файл,
// если он не совпадает ни с известным им номером ПФР, ни с номером
// СФР. См. также <ЕФС8:РегНомер> в fss4.generator.ts — то же значение.
return `СФР_${pfrRegNumber || '0000000000'}_ЕФС-1_${dateStr}_${uuid}`;
}
return `${prefix}_${tax}_${tax}_${unit}_${dateStr}_${uuid}`;
}
@@ -39,6 +39,8 @@ export interface MergedRequisites {
oktmo: RequisiteField;
okpo: RequisiteField;
sfrRegNumber: RequisiteField;
/** Рег. номер страхователя в ПФР (XXX-XXX-XXXXXX) — отдельный от sfrRegNumber, требуется для ЕФС-1. */
pfrRegNumber: RequisiteField;
chairmanPosition: RequisiteField;
signerSnils: RequisiteField;
signerRepDoc: RequisiteField;
@@ -94,6 +96,7 @@ const REQUIRED_BY_TYPE: Record<ReportType, RequiredFieldSpec[]> = {
[ReportType.FSS4]: [
{ key: 'oktmo', label: 'ОКТМО', source: 'manual' },
{ key: 'sfrRegNumber', label: 'Регистрационный номер в СФР', source: 'manual' },
{ key: 'pfrRegNumber', label: 'Регистрационный номер в ПФР', source: 'manual' },
{ key: 'chairmanPosition', label: 'Должность руководителя', source: 'manual' },
],
[ReportType.PSV]: [{ key: 'signerSnils', label: 'СНИЛС подписанта', source: 'manual' }],
@@ -152,6 +155,7 @@ export class ReportRequisitesService {
oktmo: mn(manual?.oktmo),
okpo: mn(manual?.okpo),
sfrRegNumber: mn(manual?.sfr_reg_number),
pfrRegNumber: mn(manual?.pfr_reg_number),
chairmanPosition: mn(manual?.chairman_position),
signerSnils: mn(manual?.signer_snils),
signerRepDoc: mn(manual?.signer_rep_doc),
@@ -218,6 +222,7 @@ export class ReportRequisitesService {
oktmo: input.oktmo ?? undefined,
okpo: input.okpo ?? undefined,
sfr_reg_number: input.sfr_reg_number ?? undefined,
pfr_reg_number: input.pfr_reg_number ?? undefined,
chairman_position: input.chairman_position ?? undefined,
signer_snils: input.signer_snils ?? undefined,
signer_rep_doc: input.signer_rep_doc ?? undefined,
@@ -5,6 +5,7 @@ import { Entity, Column, PrimaryColumn, UpdateDateColumn } from 'typeorm';
*
* Хранятся поля, которых нет в профиле организации (`IOrganizationData` в
* основной БД кооператива): ОКВЭД, ОКФС, ОКОПФ, ОКТМО, ОКПО, регномер СФР,
* регномер ПФР (для ЕФС-1 — отдельный от regномера СФР, см. patterns.ts),
* должность руководителя, СНИЛС подписанта, описание доверенности. Плюс
* возможные override'ы телефона и адреса (если в отчётности нужен не тот,
* что в профиле организации).
@@ -34,6 +35,9 @@ export class ReportRequisitesEntity {
@Column({ type: 'varchar', length: 32, nullable: true })
sfr_reg_number?: string | null;
@Column({ type: 'varchar', length: 32, nullable: true })
pfr_reg_number?: string | null;
@Column({ type: 'varchar', length: 255, nullable: true })
chairman_position?: string | null;
@@ -66,8 +66,14 @@ export class Fss4Generator implements IReportGenerator {
const edits = input as ZeroReportEditsShape;
const fileName = edits.header.idFile;
const errors: string[] = [];
if (!edits.signer.sfrRegNumber) {
errors.push('Для ЕФС-1 обязателен рег. номер страхователя в СФР (поле signer.sfrRegNumber)');
if (!edits.signer.pfrRegNumber) {
// В документ идёт только pfrRegNumber (см. strah.ele('ЕФС8:РегНомер')
// ниже) — sfrRegNumber в ЕФС-1 не используется вовсе, поэтому не
// проверяем его здесь, хотя в реквизитах кооператива оно и остаётся
// обязательным полем (может понадобиться другим формам/интеграциям).
errors.push('Для ЕФС-1 обязателен рег. номер страхователя в ПФР (поле signer.pfrRegNumber)');
}
if (errors.length) {
return { reportType: this.reportType, xml: '', fileName, errors, isValid: false };
}
try {
@@ -100,8 +106,13 @@ export class Fss4Generator implements IReportGenerator {
const efs1 = edsfr.ele('ЕФС-1');
const strah = efs1.ele('Страхователь');
// sfrRegNumber гарантированно определён — guard в generate() выше.
strah.ele('ЕФС8:РегНомер').txt(signer.sfrRegNumber ?? '').up();
// Тег «Действующий регистрационный номер страхователя» (см. XSD
// efs-types.xsd/ТипСтрахователь) — сюда идёт номер ПФР, а не СФР:
// сторонние бухгалтерские системы (СБИС и др.) при приёме ЕФС-1 сверяют
// рег. номер отправителя в имени файла и в этом теге со своим профилем
// организации, где обычно заведён именно старый номер ПФР (XXX-XXX-XXXXXX).
// pfrRegNumber гарантированно определён — guard в generate() выше.
strah.ele('ЕФС8:РегНомер').txt(signer.pfrRegNumber ?? '').up();
strah.ele('ЕФС8:Наименование').txt(organization.orgName).up();
strah.ele('УТ8:ИНН').txt(organization.inn).up();
strah.ele('УТ8:КПП').txt(organization.kpp).up();
@@ -39,6 +39,7 @@ export class ReportRequisitesTypeormRepository implements ReportRequisitesReposi
if (input.oktmo !== undefined) row.oktmo = input.oktmo;
if (input.okpo !== undefined) row.okpo = input.okpo;
if (input.sfr_reg_number !== undefined) row.sfr_reg_number = input.sfr_reg_number;
if (input.pfr_reg_number !== undefined) row.pfr_reg_number = input.pfr_reg_number;
if (input.chairman_position !== undefined) row.chairman_position = input.chairman_position;
if (input.signer_snils !== undefined) row.signer_snils = input.signer_snils;
if (input.signer_rep_doc !== undefined) row.signer_rep_doc = input.signer_rep_doc;
@@ -66,6 +67,7 @@ export class ReportRequisitesTypeormRepository implements ReportRequisitesReposi
oktmo: entity.oktmo ?? null,
okpo: entity.okpo ?? null,
sfr_reg_number: entity.sfr_reg_number ?? null,
pfr_reg_number: entity.pfr_reg_number ?? null,
chairman_position: entity.chairman_position ?? null,
signer_snils: entity.signer_snils ?? null,
signer_rep_doc: entity.signer_rep_doc ?? null,
+23 -5
View File
@@ -62,15 +62,33 @@ async function bootstrap() {
// Проверяем, был ли запущен режим миграций
const args = process.argv.slice(2);
if (args.includes('--migrate')) {
await migrateData();
process.exit(0);
try {
await migrateData();
process.exit(0);
} catch (error) {
// migrateData() уже логирует причину; здесь важен только ненулевой
// exit code. В production Sentry.init() ставит свой глобальный
// process.on('unhandledRejection', ...) с дефолтным mode: 'warn' —
// он перехватывает необработанный reject этого await и НЕ роняет
// процесс (в отличие от штатного поведения Node без Sentry). Без
// явного process.exit(1) здесь deploy-скрипт получал ложноположительный
// exit code 0 при реально упавшей миграции (кейс V2.3.2 на testnet
// 2026-07-19 — TS-ошибка компиляции миграции проглатывалась именно так).
logger.error('Процесс миграции завершился с ошибкой, выходим с кодом 1', error);
process.exit(1);
}
}
// Проверяем, был ли запущен режим только миграций (для обратной совместимости)
if (args.includes('--migrations-only')) {
await migrateData();
logger.info('Режим только миграций - миграции выполнены, сервер не будет запущен');
process.exit(0);
try {
await migrateData();
logger.info('Режим только миграций - миграции выполнены, сервер не будет запущен');
process.exit(0);
} catch (error) {
logger.error('Процесс миграции завершился с ошибкой, выходим с кодом 1', error);
process.exit(1);
}
}
// Подключение к MongoDB
@@ -202,6 +202,7 @@ const zeroBaseEdits: ZeroReportEditsShape = {
repDoc: 'Доверенность №1 от 01.01.2024',
snils: '123-456-789 00',
sfrRegNumber: '7701234567',
pfrRegNumber: '087-701-579643',
chairmanPosition: 'Председатель Совета',
},
};
@@ -564,7 +565,7 @@ describe('ЕФС-1 (Fss4Generator, СФР)', () => {
expect(gen.reportType).toBe(ReportType.FSS4);
});
it('генерирует XML при переданном sfrRegNumber', () => {
it('генерирует XML при переданном pfrRegNumber', () => {
const result = gen.generate(withPeriod(1));
expect(result.isValid).toBe(true);
expect(result.xml).toContain('<?xml');
@@ -572,13 +573,27 @@ describe('ЕФС-1 (Fss4Generator, СФР)', () => {
expect(result.xml).toContain('<ЭДСФР');
});
it('возвращает ошибку без sfrRegNumber', () => {
it('генерируется без sfrRegNumber — ЕФС-1 его не использует', () => {
const result = gen.generate({
...withPeriod(1),
signer: { ...zeroBaseEdits.signer, sfrRegNumber: null },
});
expect(result.isValid).toBe(true);
});
it('возвращает ошибку без pfrRegNumber', () => {
const result = gen.generate({
...withPeriod(1),
signer: { ...zeroBaseEdits.signer, pfrRegNumber: null },
});
expect(result.isValid).toBe(false);
expect(result.errors.join('|')).toContain('sfrRegNumber');
expect(result.errors.join('|')).toContain('pfrRegNumber');
});
it('использует pfrRegNumber (не sfrRegNumber) в <ЕФС8:РегНомер> — сторонние бухгалтерские системы сверяют именно рег. номер ПФР', () => {
const result = gen.generate(withPeriod(1));
expect(result.xml).toContain('<ЕФС8:РегНомер>087-701-579643</ЕФС8:РегНомер>');
expect(result.xml).not.toContain('<ЕФС8:РегНомер>7701234567</ЕФС8:РегНомер>');
});
it('эхом возвращает header.idFile в result.fileName', () => {
+2
View File
@@ -4821,6 +4821,7 @@ export const ReturnTypes: Record<string,any> = {
oktmo:"RequisiteFieldView",
okved:"RequisiteFieldView",
orgName:"RequisiteFieldView",
pfrRegNumber:"RequisiteFieldView",
phone:"RequisiteFieldView",
sfrRegNumber:"RequisiteFieldView",
signerFirstName:"RequisiteFieldView",
@@ -5063,6 +5064,7 @@ export const ReturnTypes: Record<string,any> = {
firstName:"String",
lastName:"String",
middleName:"String",
pfrRegNumber:"String",
repDoc:"String",
sfrRegNumber:"String",
snils:"String",
+20 -4
View File
@@ -9043,6 +9043,7 @@ validateReportEdits?: [{ editsJson: string | Variable<any, string>, reportType:
oktmo?:ValueTypes["RequisiteFieldView"],
okved?:ValueTypes["RequisiteFieldView"],
orgName?:ValueTypes["RequisiteFieldView"],
pfrRegNumber?:ValueTypes["RequisiteFieldView"],
phone?:ValueTypes["RequisiteFieldView"],
sfrRegNumber?:ValueTypes["RequisiteFieldView"],
signerFirstName?:ValueTypes["RequisiteFieldView"],
@@ -10179,8 +10180,10 @@ validateReportEdits?: [{ editsJson: string | Variable<any, string>, reportType:
oktmo?: string | undefined | null | Variable<any, string>,
/** ОКВЭД — напр. 94.99, 46.73.7 */
okved?: string | undefined | null | Variable<any, string>,
/** Рег. номер ПФР — XXX-XXX-XXXXXX (для ЕФС-1, отдельно от рег. номера СФР) */
pfrRegNumber?: string | undefined | null | Variable<any, string>,
phoneOverride?: string | undefined | null | Variable<any, string>,
/** Рег. номер СФР — XXX-XXX-XXXXXX */
/** Рег. номер СФР — 10 цифр */
sfrRegNumber?: string | undefined | null | Variable<any, string>,
signerRepDoc?: string | undefined | null | Variable<any, string>,
/** СНИЛС — XXX-XXX-XXX YY или 11 цифр */
@@ -10482,6 +10485,7 @@ validateReportEdits?: [{ editsJson: string | Variable<any, string>, reportType:
firstName?:boolean | `@${string}`,
lastName?:boolean | `@${string}`,
middleName?:boolean | `@${string}`,
pfrRegNumber?:boolean | `@${string}`,
repDoc?:boolean | `@${string}`,
sfrRegNumber?:boolean | `@${string}`,
snils?:boolean | `@${string}`,
@@ -18204,6 +18208,7 @@ validateReportEdits?: [{ editsJson: string, reportType: ResolverInputTypes["Repo
oktmo?:ResolverInputTypes["RequisiteFieldView"],
okved?:ResolverInputTypes["RequisiteFieldView"],
orgName?:ResolverInputTypes["RequisiteFieldView"],
pfrRegNumber?:ResolverInputTypes["RequisiteFieldView"],
phone?:ResolverInputTypes["RequisiteFieldView"],
sfrRegNumber?:ResolverInputTypes["RequisiteFieldView"],
signerFirstName?:ResolverInputTypes["RequisiteFieldView"],
@@ -19317,8 +19322,10 @@ validateReportEdits?: [{ editsJson: string, reportType: ResolverInputTypes["Repo
oktmo?: string | undefined | null,
/** ОКВЭД — напр. 94.99, 46.73.7 */
okved?: string | undefined | null,
/** Рег. номер ПФР — XXX-XXX-XXXXXX (для ЕФС-1, отдельно от рег. номера СФР) */
pfrRegNumber?: string | undefined | null,
phoneOverride?: string | undefined | null,
/** Рег. номер СФР — XXX-XXX-XXXXXX */
/** Рег. номер СФР — 10 цифр */
sfrRegNumber?: string | undefined | null,
signerRepDoc?: string | undefined | null,
/** СНИЛС — XXX-XXX-XXX YY или 11 цифр */
@@ -19614,6 +19621,7 @@ validateReportEdits?: [{ editsJson: string, reportType: ResolverInputTypes["Repo
firstName?:boolean | `@${string}`,
lastName?:boolean | `@${string}`,
middleName?:boolean | `@${string}`,
pfrRegNumber?:boolean | `@${string}`,
repDoc?:boolean | `@${string}`,
sfrRegNumber?:boolean | `@${string}`,
snils?:boolean | `@${string}`,
@@ -27847,6 +27855,7 @@ export type ModelTypes = {
oktmo: ModelTypes["RequisiteFieldView"],
okved: ModelTypes["RequisiteFieldView"],
orgName: ModelTypes["RequisiteFieldView"],
pfrRegNumber: ModelTypes["RequisiteFieldView"],
phone: ModelTypes["RequisiteFieldView"],
sfrRegNumber: ModelTypes["RequisiteFieldView"],
signerFirstName: ModelTypes["RequisiteFieldView"],
@@ -28930,8 +28939,10 @@ export type ModelTypes = {
oktmo?: string | undefined | null,
/** ОКВЭД — напр. 94.99, 46.73.7 */
okved?: string | undefined | null,
/** Рег. номер ПФР — XXX-XXX-XXXXXX (для ЕФС-1, отдельно от рег. номера СФР) */
pfrRegNumber?: string | undefined | null,
phoneOverride?: string | undefined | null,
/** Рег. номер СФР — XXX-XXX-XXXXXX */
/** Рег. номер СФР — 10 цифр */
sfrRegNumber?: string | undefined | null,
signerRepDoc?: string | undefined | null,
/** СНИЛС — XXX-XXX-XXX YY или 11 цифр */
@@ -29214,6 +29225,7 @@ export type ModelTypes = {
firstName: string,
lastName: string,
middleName?: string | undefined | null,
pfrRegNumber?: string | undefined | null,
repDoc?: string | undefined | null,
sfrRegNumber?: string | undefined | null,
snils?: string | undefined | null,
@@ -37955,6 +37967,7 @@ export type GraphQLTypes = {
oktmo: GraphQLTypes["RequisiteFieldView"],
okved: GraphQLTypes["RequisiteFieldView"],
orgName: GraphQLTypes["RequisiteFieldView"],
pfrRegNumber: GraphQLTypes["RequisiteFieldView"],
phone: GraphQLTypes["RequisiteFieldView"],
sfrRegNumber: GraphQLTypes["RequisiteFieldView"],
signerFirstName: GraphQLTypes["RequisiteFieldView"],
@@ -39090,8 +39103,10 @@ export type GraphQLTypes = {
oktmo?: string | undefined | null,
/** ОКВЭД — напр. 94.99, 46.73.7 */
okved?: string | undefined | null,
/** Рег. номер ПФР — XXX-XXX-XXXXXX (для ЕФС-1, отдельно от рег. номера СФР) */
pfrRegNumber?: string | undefined | null,
phoneOverride?: string | undefined | null,
/** Рег. номер СФР — XXX-XXX-XXXXXX */
/** Рег. номер СФР — 10 цифр */
sfrRegNumber?: string | undefined | null,
signerRepDoc?: string | undefined | null,
/** СНИЛС — XXX-XXX-XXX YY или 11 цифр */
@@ -39395,6 +39410,7 @@ export type GraphQLTypes = {
firstName: string,
lastName: string,
middleName?: string | undefined | null,
pfrRegNumber?: string | undefined | null,
repDoc?: string | undefined | null,
sfrRegNumber?: string | undefined | null,
snils?: string | undefined | null,
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "cooptypes",
"type": "module",
"version": "2026.6.24",
"version": "2026.7.21-1",
"description": "Shared TypeScript типы и интерфейсы экосистемы кооперативов",
"author": "Alex Ant <dacom.dark.sun@gmail.com>",
"license": "MIT",
@@ -116,6 +116,21 @@ q-dialog(
span(v-if='isValid') Форма валидна
span(v-else) Ошибок: {{ errorsCount }}
//- Явный список ошибок полей — страховка на случай, если конкретное
//- поле формы не подсвечивается инлайн (не все секции формы ещё
//- подключены к fieldErrors). Без этого списка счётчик показывал
//- «Ошибок: N» без единой подсказки, где именно искать.
q-list.error-list.q-mb-sm(
v-if='!isValid && fieldErrorEntries.length'
dense bordered separator
)
q-item(v-for='err in fieldErrorEntries' :key='err.key' dense)
q-item-section(avatar top)
q-icon(name='fa-solid fa-circle-exclamation' color='negative' size='14px')
q-item-section
q-item-label.text-caption.text-weight-medium {{ err.label }}
q-item-label(caption) {{ err.message }}
.text-subtitle2.q-mb-sm Действия
q-btn.q-mb-sm(
@@ -126,7 +141,6 @@ q-dialog(
:loading='isGenerating'
@click='downloadXml'
no-caps
stack
)
q-tooltip Сохраняет черновик, генерирует XML, валидирует XSD и скачивает файл
q-btn.q-mb-sm(
@@ -137,7 +151,6 @@ q-dialog(
:loading='pdfLoading'
@click='downloadPdf'
no-caps
stack
)
q-tooltip(v-if='!hasPdfPaperView') PDF-экспорт для {{ reportTitle }} пока не поддерживается
q-tooltip(v-else) Заполненный отчёт в PDF (из бумажного вида)
@@ -152,7 +165,6 @@ q-dialog(
:disable='isLoading'
@click='regenerate'
no-caps
stack
)
q-tooltip Подтянуть актуальные данные из реестра; ваши правки сохраняются
@@ -164,7 +176,6 @@ q-dialog(
label='Удалить черновик'
@click='clearDraft'
no-caps
stack
)
q-tooltip Вернуть форму к дефолтам
@@ -192,7 +203,6 @@ q-dialog(
:loading='markLoading'
@click='clearMark'
no-caps
stack
)
q-tooltip Вернуть обычный статус периода
@@ -207,7 +217,6 @@ q-dialog(
:loading='markLoading'
@click='clearMark'
no-caps
stack
)
q-tooltip Вернуть обычный статус периода
@@ -220,7 +229,6 @@ q-dialog(
:loading='markLoading'
@click='markSubmittedExternally'
no-caps
stack
)
q-tooltip Если отчёт уже сдан в бумаге / через стороннюю систему. В архив XML не попадает, но ячейка станет зелёной.
q-btn(
@@ -231,7 +239,6 @@ q-dialog(
:loading='markLoading'
@click='markNotRequired'
no-caps
stack
)
q-tooltip Отметить, что этот период сдавать не нужно ячейка станет серой
@@ -484,6 +491,7 @@ interface ZeroReportEdits {
repDoc: string | null
snils: string | null
sfrRegNumber: string | null
pfrRegNumber: string | null
chairmanPosition: string | null
}
}
@@ -513,6 +521,34 @@ const errorsCount = computed(() => {
return count
})
// Человекочитаемые названия секций формы — для расшифровки JSONPath из
// серверной ошибки валидации (см. error-list ниже).
const FIELD_SECTION_LABELS: Record<string, string> = {
header: 'Шапка',
organization: 'Организация',
signer: 'Подписант',
balance: 'Баланс',
notes: 'Пояснения',
}
function humanizeFieldPath(path: string): string {
const [section, ...rest] = path.split('.')
const sectionLabel = FIELD_SECTION_LABELS[section] ?? section
return rest.length ? `${sectionLabel}${rest.join(' → ')}` : sectionLabel
}
interface FieldErrorEntry { key: string; label: string; message: string }
const fieldErrorEntries = computed<FieldErrorEntry[]>(() => {
const out: FieldErrorEntry[] = []
for (const [path, messages] of Object.entries(fieldErrors.value)) {
messages.forEach((message, i) => {
out.push({ key: `${path}#${i}`, label: humanizeFieldPath(path), message })
})
}
return out
})
const saveStatusColor = computed(() => {
if (isSaving.value) return 'orange'
if (hasDraft.value) return 'positive'
@@ -839,6 +875,22 @@ function goToRequisites(): void {
}
}
.error-list {
background: var(--p-surface-1, #fff);
border-color: var(--p-neg-soft);
max-height: 220px;
overflow-y: auto;
:deep(.q-item) {
min-height: unset;
padding: 6px 8px;
}
:deep(.q-item__label--caption) {
color: var(--p-ink-2);
}
}
.mark-hint {
font-size: var(--p-fs-meta, 12px);
color: var(--p-ink-2);
@@ -36,9 +36,10 @@ const props = defineProps<{
mask?: string
/** Подставлять плейсхолдеры маски при вводе */
fillMask?: boolean
/** Разрешить только цифры и точку (ОКВЭД: 94.99, 46.73.7). Блокирует
* keypress/paste — буквы вообще не появляются в поле. */
digitsDotsOnly?: boolean
/** Разрешить только цифры + перечисленные доп. символы (напр. '.' для
* ОКВЭД: 94.99, 46.73.7; '-' для Рег. номера СФР). Блокирует
* keypress/paste — посторонние символы вообще не появляются в поле. */
digitsExtraChars?: string
/** HTML maxlength. */
maxLength?: number
/** Допустимые точные длины (ИЛИ). Напр. `[8, 11]` для ОКТМО. */
@@ -60,12 +61,20 @@ const labelText = computed(() =>
)
const inputModeComputed = computed<'numeric' | 'decimal' | 'text'>(() => {
if (props.digitsDotsOnly) return 'decimal'
// '.' — десятичная клавиатура мобильных устройств её показывает; для
// прочих доп.символов (напр. '-') нужна полная клавиатура — 'text'.
if (props.digitsExtraChars === '.') return 'decimal'
if (props.digitsExtraChars) return 'text'
// mask из одних `#` = чистая цифровая клавиатура на мобиле.
if (props.mask && /^#+$/.test(props.mask)) return 'numeric'
return 'text'
})
/** Экранирует спецсимволы regex, чтобы вставить строку в символьный класс `[...]`. */
function escapeForCharClass(chars: string): string {
return chars.replace(/[\]\\^-]/g, '\\$&')
}
const NAV_KEYS = new Set([
'Backspace',
'Delete',
@@ -81,19 +90,21 @@ const NAV_KEYS = new Set([
])
// Блокируем keypress не-допустимых символов — буквы не попадают в поле даже
// на миллисекунды. Работает только для digitsDotsOnly (для чисто цифровых
// на миллисекунды. Работает только для digitsExtraChars (для чисто цифровых
// полей используем Quasar-mask — он нативно блокирует не-# символы).
function onKeydown(e: KeyboardEvent): void {
if (!props.digitsDotsOnly) return
if (!props.digitsExtraChars) return
if (e.ctrlKey || e.metaKey || e.altKey) return
if (NAV_KEYS.has(e.key)) return
if (!/^[\d.]$/.test(e.key)) e.preventDefault()
const re = new RegExp(`^[\\d${escapeForCharClass(props.digitsExtraChars)}]$`)
if (!re.test(e.key)) e.preventDefault()
}
function onPaste(e: ClipboardEvent): void {
if (!props.digitsDotsOnly) return
if (!props.digitsExtraChars) return
const text = e.clipboardData?.getData('text') ?? ''
if (!/^[\d.]*$/.test(text)) e.preventDefault()
const re = new RegExp(`^[\\d${escapeForCharClass(props.digitsExtraChars)}]*$`)
if (!re.test(text)) e.preventDefault()
}
const rules = computed(() => {
@@ -128,7 +139,10 @@ function onInput(raw: string | number | null): void {
// Фоллбек-фильтр на случай programmatic ввода (drop/autofill). Для ручного
// набора актуальна либо mask (q-input сам режет), либо keydown-блокер.
let s = String(raw ?? '')
if (props.digitsDotsOnly) s = s.replace(/[^\d.]+/g, '')
if (props.digitsExtraChars) {
const re = new RegExp(`[^\\d${escapeForCharClass(props.digitsExtraChars)}]+`, 'g')
s = s.replace(re, '')
}
if (props.maxLength && s.length > props.maxLength) s = s.slice(0, props.maxLength)
emit('update:value', s)
}
@@ -37,7 +37,7 @@ q-form.settings-page(@submit.prevent='save' @validation-error='onValidationError
:value='manualInput[f.key]'
:placeholder='f.placeholder'
:mask='f.mask'
:digits-dots-only='f.digitsDotsOnly'
:digits-extra-chars='f.digitsExtraChars'
:max-length='f.maxLength'
:exact-lengths='f.exactLengths'
:pattern='f.pattern'
@@ -46,11 +46,11 @@ q-form.settings-page(@submit.prevent='save' @validation-error='onValidationError
@update:value='v => (manualInput[f.key] = v)'
)
//- СФР (ЕФС-1)
//- СФР/ПФР (ЕФС-1)
q-card.q-mt-md(flat)
q-card-section.q-py-sm
.text-h6 СФР (для ЕФС-1)
.text-caption.t-muted Регистрационный номер СФР и должность председателя
.text-h6 СФР/ПФР (для ЕФС-1)
.text-caption.t-muted Регистрационные номера страхователя и должность председателя
q-separator
@@ -60,13 +60,27 @@ q-form.settings-page(@submit.prevent='save' @validation-error='onValidationError
id='field-sfrRegNumber'
label='Рег. номер СФР'
:value='manualInput.sfrRegNumber'
placeholder='XXX-XXX-XXXXXX'
mask='###-###-######'
placeholder='10 цифр'
mask='##########'
:max-length='10'
:exact-lengths='[10]'
:pattern='SFR_REG_PATTERN'
pattern-message='Формат: XXX-XXX-XXXXXX (12 цифр)'
pattern-message='Формат: 10 цифр без разделителей'
required
@update:value='v => (manualInput.sfrRegNumber = v)'
)
RequisiteField.col-md-6.col-12(
id='field-pfrRegNumber'
label='Рег. номер ПФР'
:value='manualInput.pfrRegNumber'
placeholder='XXX-XXX-XXXXXX'
mask='###-###-######'
:max-length='14'
:pattern='PFR_REG_PATTERN'
pattern-message='Формат: XXX-XXX-XXXXXX (12 цифр с тире)'
required
@update:value='v => (manualInput.pfrRegNumber = v)'
)
RequisiteField.col-md-6.col-12(
id='field-chairmanPosition'
label='Должность председателя'
@@ -163,6 +177,7 @@ type ManualKey =
| 'oktmo'
| 'okpo'
| 'sfrRegNumber'
| 'pfrRegNumber'
| 'chairmanPosition'
| 'signerSnils'
| 'signerRepDoc'
@@ -176,6 +191,7 @@ const manualInput = reactive<Record<ManualKey, string>>({
oktmo: '',
okpo: '',
sfrRegNumber: '',
pfrRegNumber: '',
chairmanPosition: '',
signerSnils: '',
signerRepDoc: '',
@@ -202,7 +218,7 @@ interface ClassifierField {
label: string
placeholder?: string
mask?: string
digitsDotsOnly?: boolean
digitsExtraChars?: string
maxLength?: number
exactLengths?: number[]
pattern?: RegExp
@@ -212,13 +228,13 @@ interface ClassifierField {
// Placeholder'ы — правило ввода (кол-во цифр), а не конкретные примеры.
// Для цифровых полей фиксированной/короткой длины — используем Quasar-mask
// (`#` = только цифра, буквы блокируются на keypress нативно).
// Для ОКВЭД (цифры + точка переменной длины) — digitsDotsOnly с @keydown-блокером.
// Для ОКВЭД (цифры + точка переменной длины) — digitsExtraChars='.' с @keydown-блокером.
const classifierFields: ClassifierField[] = [
{
key: 'okved',
label: 'ОКВЭД',
placeholder: 'XX.XX или XX.XX.XX',
digitsDotsOnly: true,
digitsExtraChars: '.',
maxLength: 8,
pattern: /^\d{2}(\.\d{1,2}){0,2}$/,
patternMessage: 'Формат: XX.XX или XX.XX.XX',
@@ -265,7 +281,8 @@ const signerTypeOptions = [
// пользователь прожал пару цифр и нажал «Сохранить»: mask не мешает
// сохранить частичный ввод, паттерн-правило блокирует.
const SNILS_PATTERN = /^\d{3}-\d{3}-\d{3} \d{2}$/
const SFR_REG_PATTERN = /^\d{3}-\d{3}-\d{6}$/
const SFR_REG_PATTERN = /^\d{10}$/
const PFR_REG_PATTERN = /^\d{3}-\d{3}-\d{6}$/
function getValue(key: keyof IReportRequisitesView): string {
const v = requisites.value?.[key] as any
@@ -9,6 +9,8 @@
@update:model-value='v => emitField("otch", v)'
dense outlined
input-class='text-right'
:error='errFor("otch")'
:error-message='msgFor("otch")'
)
.br-input
q-input(
@@ -17,6 +19,8 @@
@update:model-value='v => emitField("prev", v)'
dense outlined
input-class='text-right'
:error='errFor("prev")'
:error-message='msgFor("prev")'
)
.br-input
q-input(
@@ -25,6 +29,8 @@
@update:model-value='v => emitField("prePrev", v)'
dense outlined
input-class='text-right'
:error='errFor("prePrev")'
:error-message='msgFor("prePrev")'
)
</template>
@@ -39,6 +45,10 @@ const props = defineProps<{
label: string
code: string
row: BalanceRow | null
/** JSONPath строки без учёта суффикса (например "balance.assetsTotal") — для сопоставления с fieldErrors. */
basePath?: string
/** Серверные ошибки валидации всей формы: ключ = JSONPath поля. */
fieldErrors?: Record<string, string[]>
}>()
const emit = defineEmits<{
@@ -58,6 +68,18 @@ function emitField(key: keyof BalanceRow, raw: unknown): void {
const next: BalanceRow = { ...base, [key]: clamp(raw) }
emit('update', next)
}
function pathFor(key: keyof BalanceRow): string {
return props.basePath ? `${props.basePath}.${key}` : key
}
function errFor(key: keyof BalanceRow): boolean {
return (props.fieldErrors?.[pathFor(key)]?.length ?? 0) > 0
}
function msgFor(key: keyof BalanceRow): string {
return props.fieldErrors?.[pathFor(key)]?.[0] ?? ''
}
</script>
<style scoped lang="scss">
@@ -11,6 +11,8 @@
:model-value='editsValue.header.correctionNumber'
@update:model-value='v => updateField("header.correctionNumber", clampInt(v, 0, 999))'
:rules='[v => (v >= 0 && v <= 999) || "0..999"]'
:error='errFor("header.correctionNumber")'
:error-message='msgFor("header.correctionNumber")'
dense filled
)
q-input(
@@ -18,6 +20,8 @@
type='number'
:model-value='editsValue.header.reportYear'
@update:model-value='v => updateField("header.reportYear", clampInt(v, 2000, 2100))'
:error='errFor("header.reportYear")'
:error-message='msgFor("header.reportYear")'
dense filled
)
q-input(
@@ -26,6 +30,8 @@
@update:model-value='v => updateField("header.docDate", v)'
mask='##.##.####'
:rules='[reportRules.dateDdMmYyyy()]'
:error='errFor("header.docDate")'
:error-message='msgFor("header.docDate")'
dense filled
)
@@ -191,24 +197,32 @@
label='Нематериальные, финансовые и др. внеоборотные активы'
code='1170'
:row='editsValue.balance.nonMaterialAndLongFin'
base-path='balance.nonMaterialAndLongFin'
:field-errors='props.fieldErrors'
@update='row => updateRow("balance.nonMaterialAndLongFin", row)'
)
BalanceRowEditor(
label='Денежные средства и денежные эквиваленты'
code='1250'
:row='editsValue.balance.cash'
base-path='balance.cash'
:field-errors='props.fieldErrors'
@update='row => updateRow("balance.cash", row)'
)
BalanceRowEditor(
label='Финансовые и другие оборотные активы'
code='1260'
:row='editsValue.balance.shortTermFin'
base-path='balance.shortTermFin'
:field-errors='props.fieldErrors'
@update='row => updateRow("balance.shortTermFin", row)'
)
BalanceRowEditor.total(
label='БАЛАНС (актив)'
code='1600'
:row='editsValue.balance.assetsTotal'
base-path='balance.assetsTotal'
:field-errors='props.fieldErrors'
@update='row => updateRow("balance.assetsTotal", row)'
)
@@ -218,12 +232,16 @@
label='Целевые средства (паевой фонд + целевые поступления)'
code='1350'
:row='editsValue.balance.targetFunds'
base-path='balance.targetFunds'
:field-errors='props.fieldErrors'
@update='row => updateRow("balance.targetFunds", row)'
)
BalanceRowEditor.total(
label='БАЛАНС (пассив)'
code='1700'
:row='editsValue.balance.passivesTotal'
base-path='balance.passivesTotal'
:field-errors='props.fieldErrors'
@update='row => updateRow("balance.passivesTotal", row)'
)
@@ -26,8 +26,8 @@
.kv-row
.kv-cell
.kv-label Регистрационный номер в СФР
.kv-value.code {{ header.sfrRegNumber || '—' }}
.kv-label Регистрационный номер страхователя
.kv-value.code {{ header.pfrRegNumber || '—' }}
.kv-cell
.kv-label ОГРН
.kv-value.code {{ ogrn || '—' }}
@@ -9,6 +9,8 @@
type='number'
:model-value='editsValue.header.reportYear'
@update:model-value='v => updateField("header.reportYear", clampInt(v, 2000, 2100))'
:error='errFor("header.reportYear")'
:error-message='msgFor("header.reportYear")'
dense filled
)
q-input(
@@ -17,6 +19,8 @@
type='number'
:model-value='editsValue.header.period'
@update:model-value='v => updateField("header.period", clampInt(v, 1, periodKind === "quarter" ? 4 : 12))'
:error='errFor("header.period")'
:error-message='msgFor("header.period")'
dense filled
)
q-input(
@@ -32,14 +36,12 @@
.editor-section
h3.section-title Организация
.text-caption.t-muted.q-mb-sm Данные берутся из Реквизитов правка доступна только там
q-input(
label='Наименование организации'
:model-value='editsValue.organization.orgName'
@update:model-value='v => updateField("organization.orgName", v)'
:rules='[reportRules.length(1, 1000)]'
:error='errFor("organization.orgName")'
:error-message='msgFor("organization.orgName")'
readonly disable
dense filled
)
@@ -47,30 +49,20 @@
q-input(
label='ИНН'
:model-value='editsValue.organization.inn'
@update:model-value='v => updateField("organization.inn", v)'
:rules='[reportRules.innUl()]'
:error='errFor("organization.inn")'
:error-message='msgFor("organization.inn")'
readonly disable
dense filled
)
q-input(
label='КПП'
:model-value='editsValue.organization.kpp'
@update:model-value='v => updateField("organization.kpp", String(v || "").toUpperCase())'
:rules='[reportRules.kpp()]'
:error='errFor("organization.kpp")'
:error-message='msgFor("organization.kpp")'
readonly disable
dense filled
maxlength='9'
)
q-input(
v-if='needs.oktmo'
label='ОКТМО'
:model-value='editsValue.organization.oktmo || ""'
@update:model-value='v => updateField("organization.oktmo", v || null)'
:rules='[reportRules.oktmo()]'
:error='errFor("organization.oktmo")'
:error-message='msgFor("organization.oktmo")'
readonly disable
dense filled
)
@@ -78,29 +70,24 @@
q-input(
label='ОКВЭД'
:model-value='editsValue.organization.okved || ""'
@update:model-value='v => updateField("organization.okved", v || null)'
:rules='[reportRules.okved()]'
:error='errFor("organization.okved")'
:error-message='msgFor("organization.okved")'
readonly disable
dense filled
)
q-input(
label='ОГРН'
:model-value='editsValue.organization.ogrn || ""'
@update:model-value='v => updateField("organization.ogrn", v || null)'
:rules='[reportRules.optionalRegex(/^\\d{13}$/, "ОГРН ЮЛ — 13 цифр")]'
:error='errFor("organization.ogrn")'
:error-message='msgFor("organization.ogrn")'
readonly disable
dense filled
)
.editor-section
h3.section-title Подписант
.text-caption.t-muted.q-mb-sm Данные берутся из Реквизитов правка доступна только там
q-option-group(
:model-value='editsValue.signer.type'
@update:model-value='v => updateField("signer.type", v)'
:options='signerTypeOptions'
disable
inline
)
@@ -108,25 +95,19 @@
q-input(
label='Фамилия'
:model-value='editsValue.signer.lastName'
@update:model-value='v => updateField("signer.lastName", v)'
readonly disable
dense filled
:rules='[v => !!v || "обязательно"]'
:error='errFor("signer.lastName")'
:error-message='msgFor("signer.lastName")'
)
q-input(
label='Имя'
:model-value='editsValue.signer.firstName'
@update:model-value='v => updateField("signer.firstName", v)'
readonly disable
dense filled
:rules='[v => !!v || "обязательно"]'
:error='errFor("signer.firstName")'
:error-message='msgFor("signer.firstName")'
)
q-input(
label='Отчество'
:model-value='editsValue.signer.middleName || ""'
@update:model-value='v => updateField("signer.middleName", v || null)'
readonly disable
dense filled
)
@@ -134,10 +115,7 @@
v-if='editsValue.signer.type === "representative"'
label='Документ, подтверждающий полномочия'
:model-value='editsValue.signer.repDoc || ""'
@update:model-value='v => updateField("signer.repDoc", v || null)'
:rules='[reportRules.length(1, 120)]'
:error='errFor("signer.repDoc")'
:error-message='msgFor("signer.repDoc")'
readonly disable
dense filled
)
@@ -145,25 +123,15 @@
v-if='needs.snils'
label='СНИЛС председателя'
:model-value='editsValue.signer.snils || ""'
@update:model-value='v => updateField("signer.snils", v || null)'
:rules='[reportRules.snils()]'
:error='errFor("signer.snils")'
:error-message='msgFor("signer.snils")'
mask='###-###-### ##'
hint='Обязателен для ПСВ — попадает в ПерсСвФЛ @СНИЛС'
readonly disable
dense filled
)
q-input(
v-if='needs.sfrExtras'
label='Регистрационный номер СФР'
:model-value='editsValue.signer.sfrRegNumber || ""'
@update:model-value='v => updateField("signer.sfrRegNumber", v || null)'
:rules='[reportRules.sfrRegNumber()]'
:error='errFor("signer.sfrRegNumber")'
:error-message='msgFor("signer.sfrRegNumber")'
mask='###-###-######'
hint='XXX-XXX-XXXXXX — обязателен для ЕФС-1'
label='Регистрационный номер ПФР'
:model-value='editsValue.signer.pfrRegNumber || ""'
readonly disable
dense filled
)
@@ -171,19 +139,15 @@
v-if='needs.sfrExtras'
label='Должность подписанта'
:model-value='editsValue.signer.chairmanPosition || ""'
@update:model-value='v => updateField("signer.chairmanPosition", v || null)'
hint='По умолчанию — "Председатель Совета"'
readonly disable
dense filled
)
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { Selectors } from '@coopenomics/sdk'
import type { IReportType } from 'src/entities/Report'
const { reportRules } = Selectors
type SignerType = 'chairman' | 'representative'
interface ZeroReportEdits {
@@ -215,6 +179,7 @@ interface ZeroReportEdits {
repDoc: string | null
snils: string | null
sfrRegNumber: string | null
pfrRegNumber: string | null
chairmanPosition: string | null
}
}
@@ -33,7 +33,8 @@ export interface BaseHeader {
signerMiddleName: string
signerRepDoc: string
signerSnils: string
sfrRegNumber: string
/** Рег. номер ПФР — читается из тега `<РегНомер>` ЕФС-1 (fss4.generator.ts кладёт туда именно ПФР, не СФР). */
pfrRegNumber: string
}
export interface UseReportXmlReturn {
@@ -117,7 +118,7 @@ export function useReportXml(
signerLastName: '', signerFirstName: '', signerMiddleName: '',
signerRepDoc: '',
signerSnils: '',
sfrRegNumber: '',
pfrRegNumber: '',
}
if (!doc.value) return withRequisites(empty)
@@ -147,7 +148,7 @@ export function useReportXml(
signerMiddleName: getAttr('Подписант ФИО', 'Отчество') || getText('Отчество'),
signerRepDoc: getAttr('Подписант СвПред', 'НаимДок'),
signerSnils: getAttr('ПерсСвФЛ', 'СНИЛС'),
sfrRegNumber: getText('РегНомер'),
pfrRegNumber: getText('РегНомер'),
}
return withRequisites(h)
})
@@ -174,7 +175,7 @@ export function useReportXml(
signerFirstName: h.signerFirstName || get('signerFirstName'),
signerMiddleName: h.signerMiddleName || get('signerMiddleName'),
signerSnils: h.signerSnils || get('signerSnils'),
sfrRegNumber: h.sfrRegNumber || get('sfrRegNumber'),
pfrRegNumber: h.pfrRegNumber || get('pfrRegNumber'),
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@coopenomics/desktop",
"version": "2026.6.24",
"version": "2026.7.21-1",
"description": "Рабочий стол кооператива — Vue 3 + Quasar Framework",
"productName": "Desktop App",
"author": "Alex Ant <dacom.dark.sun@gmail.com>",
@@ -7,18 +7,19 @@ BaseDialog(
:close-on-backdrop='false',
:close-on-escape='false'
)
div.row.justify-center
div(style="padding-bottom: 100px;").col-md-8.col-col-xs-12
div.row.justify-center.select-branch-overlay__content
div.col-md-8.col-xs-12
div(v-if="step === 1")
p.q-mt-lg
| Кооператив перешёл на двухэтапную систему управления на основании общего собрания уполномоченных председателей кооперативных участков...
Form(
Loader(v-if="branchesLoading" text="Загружаем список кооперативных участков...")
Form#select-branch-form(
v-else
:handler-submit="next"
:is-submitting="isSubmitting"
:showSubmit="!isLoading"
:showSubmit="false"
:showCancel="false"
:button-submit-txt="'Продолжить'"
)
BranchSelector(
v-model:selectedBranch="selectedBranch"
@@ -27,15 +28,29 @@ BaseDialog(
div(v-else-if="step === 2")
Loader(v-if="isLoading" :text="`Формируем документ...`")
DocumentHtmlReader(v-else :html="document.html")
div(v-else)
DocumentHtmlReader(:html="document.html")
q-btn(@click="back" flat) назад
q-btn(@click="sign" color="primary") подписать
template(#footer)
.select-branch-overlay__actions(v-if="step === 1 && !branchesLoading")
BaseButton(
variant='primary',
type='submit',
form='select-branch-form',
:loading='isLoading',
:disabled='!selectedBranch || isLoading',
) Продолжить
.select-branch-overlay__actions(v-else-if="step === 2 && !isLoading")
BaseButton(variant='ghost', @click='back') назад
BaseButton(
variant='primary',
:loading='isSubmitting',
@click='sign'
) подписать
</template>
<script setup lang="ts">
import { BaseDialog } from 'src/shared/ui/base/BaseDialog'
import { BaseButton } from 'src/shared/ui/base/BaseButton'
import { Form } from 'src/shared/ui/Form'
import { Loader } from 'src/shared/ui/Loader'
import { BranchSelector } from 'src/shared/ui/BranchSelector'
@@ -53,12 +68,30 @@ BaseDialog(
document,
isSubmitting,
isLoading,
branchesLoading,
next,
back,
sign
} = useSelectBranchProcess()
</script>
<style scoped lang="scss">
/* Кнопки в слоте footer BaseDialog вне скроллящегося body,
поэтому «Продолжить» / «подписать» всегда видны на мобильных. */
.select-branch-overlay__actions {
width: 100%;
display: flex;
gap: var(--p-2, 8px);
justify-content: flex-end;
flex-wrap: wrap;
padding-bottom: env(safe-area-inset-bottom, 0px);
}
.select-branch-overlay__content {
padding-bottom: var(--p-2, 8px);
}
</style>
<style>
.digital-document .header{
text-align: center;
@@ -236,9 +236,7 @@ const goToCooperativeSite = () => {
flex-direction: column;
gap: 0.75rem;
padding: 1rem;
background: rgba(255, 255, 255, 0.5);
border-radius: 8px;
border: 1px solid rgba(0, 0, 0, 0.05);
}
/* Адаптивность */
@@ -20,31 +20,62 @@ export function useSelectBranchProcess() {
const system = useSystemStore()
const session = useSessionStore()
const branchStore = useBranchStore()
const { selectBranch } = useSelectBranch()
const { isVisible, selectBranch } = useSelectBranch()
const branches = computed(() => branchStore.publicBranches)
const branchesLoading = ref(false)
// Вотчер на авторизацию
const loadBranches = async () => {
if (!session.isAuth || !system.info.coopname) return
try {
branchesLoading.value = true
await branchStore.loadPublicBranches({ coopname: system.info.coopname })
} catch (e: unknown) {
FailAlert(e)
} finally {
branchesLoading.value = false
}
}
// Загрузка при входе
watch(
() => session.isAuth,
(isAuth, wasAuth) => {
if (isAuth && !wasAuth) {
branchStore.loadPublicBranches({ coopname: system.info.coopname })
void loadBranches()
}
},
{ immediate: true }
)
// Перезагрузка при переходе в мажоритарный режим или показе оверлея
watch(
[() => isVisible.value, () => system.info?.cooperator_account?.is_branched],
([visible, branched]) => {
if (visible && branched && session.isAuth) {
void loadBranches()
}
}
)
const next = async () => {
isLoading.value = true
document.value = await digitalDocument.generate<Cooperative.Registry.SelectBranchStatement.Action>({
registry_id: Cooperative.Registry.SelectBranchStatement.registry_id,
coopname: system.info.coopname,
username: session.username,
braname: selectedBranch.value,
})
isLoading.value = false
step.value++
if (!selectedBranch.value || isLoading.value) return
try {
isLoading.value = true
document.value = await digitalDocument.generate<Cooperative.Registry.SelectBranchStatement.Action>({
registry_id: Cooperative.Registry.SelectBranchStatement.registry_id,
coopname: system.info.coopname,
username: session.username,
braname: selectedBranch.value,
})
step.value++
} catch (e: unknown) {
FailAlert(e)
} finally {
isLoading.value = false
}
}
const back = () => {
@@ -80,6 +111,7 @@ export function useSelectBranchProcess() {
document,
isSubmitting,
isLoading,
branchesLoading,
next,
back,
sign,
@@ -9,7 +9,6 @@ div
emit-value
standout="bg-teal text-white"
map-options
@update:model-value="$emit('update:selectedBranch', $event)"
)
template(v-slot:option="scope")
@@ -27,22 +26,13 @@ div
<script lang="ts" setup>
import { type IPublicBranch } from 'src/entities/Branch/model';
import { ref, watch } from 'vue'
const props = defineProps({
defineProps({
branches: {
type: Object as () => IPublicBranch[],
required: true,
},
modelValue: {
type: String,
required: false,
},
})
const selectedBranch = ref(props.modelValue)
watch(() => props.modelValue, (newVal) => {
selectedBranch.value = newVal
})
const selectedBranch = defineModel<string>('selectedBranch', { default: '' })
</script>
@@ -77,6 +77,9 @@ const maxWidthBySize: Record<BaseDialogSize, string> = {
max-width: none;
max-height: none;
}
.base-dialog--maximized .base-dialog__foot {
flex-shrink: 0;
}
.base-dialog__head {
display: flex;
flex: 0 0 auto;
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "@coopenomics/docs-harness",
"version": "2026.6.24",
"version": "2026.7.21-1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@coopenomics/docs-harness",
"version": "2026.6.24",
"version": "2026.7.21-1",
"license": "ISC",
"devDependencies": {
"@playwright/test": "^1.59.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@coopenomics/docs-harness",
"version": "2026.6.24",
"version": "2026.7.21-1",
"private": true,
"description": "Playwright harness для съёмки скриншотов в документацию (см. bin/shoot.mjs)",
"type": "module",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@coopenomics/docs",
"type": "module",
"version": "2026.6.24",
"version": "2026.7.21-1",
"private": true,
"packageManager": "pnpm@9.9.0",
"description": "Документация для монорепозитория",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@coopenomics/factory",
"type": "module",
"version": "2026.6.24",
"version": "2026.7.21-1",
"description": "Фабрика юридических документов кооператива",
"author": "Alex Ant <chairman.voskhod@gmail.com>",
"license": "MIT",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@coopenomics/inter",
"type": "module",
"version": "2026.6.24",
"version": "2026.7.21-1",
"description": "Контракты и DI-токены для связи расширений контроллера без прямых зависимостей",
"author": "Alex Ant <dacom.dark.sun@gmail.com>",
"license": "MIT",
+2 -2
View File
@@ -1,5 +1,5 @@
{
"local": "049_migrate_phase2_progwallets.ts",
"test": "051_edit_selected_draft.ts",
"prod": "051_edit_selected_draft.ts"
"test": "052_edit_selected_draft.ts",
"prod": "052_edit_selected_draft.ts"
}
@@ -7,7 +7,7 @@ import { api } from '../src/eos'
export class InitialMigration implements Migration {
async run(): Promise {
const ids = [100]
const ids = [994, 995, 996, 998, 999, 1000]
for (const id of ids) {
try {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "migrator",
"version": "2026.6.24",
"version": "2026.7.21-1",
"description": "Утилита миграции данных между версиями",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@coopenomics/notifications",
"version": "2026.6.24",
"version": "2026.7.21-1",
"description": "Каталог типобезопасных типов уведомлений и шаблонов Центра уведомлений",
"type": "module",
"private": false,
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "pkg-placeholder",
"version": "2026.6.24",
"version": "2026.7.21-1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "pkg-placeholder",
"version": "2026.6.24",
"version": "2026.7.21-1",
"license": "MIT",
"devDependencies": {
"@antfu/eslint-config": "^2.16.0",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@coopenomics/parser",
"type": "module",
"version": "2026.6.24",
"version": "2026.7.21-1",
"private": true,
"packageManager": "pnpm@9.0.6",
"description": "Индексатор блокчейна через State History Plugin",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@coopenomics/sdk",
"type": "module",
"version": "2026.6.24",
"version": "2026.7.21-1",
"private": false,
"packageManager": "pnpm@9.9.0",
"description": "TypeScript SDK для работы с API кооператива",
@@ -41,8 +41,11 @@ export const OKPO_PATTERN = /^\d{8}(\d{2})?$/;
/** СНИЛС: XXX-XXX-XXX YY или 11 цифр подряд. */
export const SNILS_PATTERN = /^(\d{3}-\d{3}-\d{3} \d{2}|\d{11})$/;
/** Регистрационный номер СФР: XXX-XXX-XXXXXX (3-3-6). */
export const SFR_REG_NUMBER_PATTERN = /^\d{3}-\d{3}-\d{6}$/;
/** Регистрационный номер страхователя в СФР: 10 цифр без разделителей (унифицированный номер). */
export const SFR_REG_NUMBER_PATTERN = /^\d{10}$/;
/** Регистрационный номер страхователя в ПФР: XXX-XXX-XXXXXX (3-3-6, 12 цифр с тире) — отдельное поле от SFR_REG_NUMBER_PATTERN, требуется для ЕФС-1. */
export const PFR_REG_NUMBER_PATTERN = /^\d{3}-\d{3}-\d{6}$/;
/** Дата в формате DD.MM.YYYY (ФНС). */
export const DATE_DDMMYYYY_PATTERN = /^\d{2}\.\d{2}\.\d{4}$/;
@@ -70,7 +73,8 @@ export const reportRules = {
okopf: (message = 'ОКОПФ — 5 цифр') => (v: unknown) => OKOPF_PATTERN.test(String(v ?? '')) || message,
okpo: (message = 'ОКПО — 8 или 10 цифр') => (v: unknown) => OKPO_PATTERN.test(String(v ?? '')) || message,
snils: (message = 'СНИЛС — XXX-XXX-XXX YY или 11 цифр') => (v: unknown) => SNILS_PATTERN.test(String(v ?? '')) || message,
sfrRegNumber: (message = 'Рег. номер СФР — XXX-XXX-XXXXXX') => (v: unknown) => SFR_REG_NUMBER_PATTERN.test(String(v ?? '')) || message,
sfrRegNumber: (message = 'Рег. номер СФР — 10 цифр') => (v: unknown) => SFR_REG_NUMBER_PATTERN.test(String(v ?? '')) || message,
pfrRegNumber: (message = 'Рег. номер ПФР — XXX-XXX-XXXXXX') => (v: unknown) => PFR_REG_NUMBER_PATTERN.test(String(v ?? '')) || message,
dateDdMmYyyy: (message = 'Дата — DD.MM.YYYY') => (v: unknown) => DATE_DDMMYYYY_PATTERN.test(String(v ?? '')) || message,
kbk: (message = 'КБК — 20 цифр') => (v: unknown) => KBK_PATTERN.test(String(v ?? '')) || message,
/** Непустая строка длиной от `min` до `max` символов. */
@@ -24,6 +24,7 @@ export const rawReportRequisitesViewSelector = {
oktmo: rawRequisiteFieldViewSelector,
okpo: rawRequisiteFieldViewSelector,
sfrRegNumber: rawRequisiteFieldViewSelector,
pfrRegNumber: rawRequisiteFieldViewSelector,
chairmanPosition: rawRequisiteFieldViewSelector,
signerSnils: rawRequisiteFieldViewSelector,
signerRepDoc: rawRequisiteFieldViewSelector,
+2
View File
@@ -4821,6 +4821,7 @@ export const ReturnTypes: Record<string,any> = {
oktmo:"RequisiteFieldView",
okved:"RequisiteFieldView",
orgName:"RequisiteFieldView",
pfrRegNumber:"RequisiteFieldView",
phone:"RequisiteFieldView",
sfrRegNumber:"RequisiteFieldView",
signerFirstName:"RequisiteFieldView",
@@ -5063,6 +5064,7 @@ export const ReturnTypes: Record<string,any> = {
firstName:"String",
lastName:"String",
middleName:"String",
pfrRegNumber:"String",
repDoc:"String",
sfrRegNumber:"String",
snils:"String",
+20 -4
View File
@@ -9043,6 +9043,7 @@ validateReportEdits?: [{ editsJson: string | Variable<any, string>, reportType:
oktmo?:ValueTypes["RequisiteFieldView"],
okved?:ValueTypes["RequisiteFieldView"],
orgName?:ValueTypes["RequisiteFieldView"],
pfrRegNumber?:ValueTypes["RequisiteFieldView"],
phone?:ValueTypes["RequisiteFieldView"],
sfrRegNumber?:ValueTypes["RequisiteFieldView"],
signerFirstName?:ValueTypes["RequisiteFieldView"],
@@ -10179,8 +10180,10 @@ validateReportEdits?: [{ editsJson: string | Variable<any, string>, reportType:
oktmo?: string | undefined | null | Variable<any, string>,
/** ОКВЭД — напр. 94.99, 46.73.7 */
okved?: string | undefined | null | Variable<any, string>,
/** Рег. номер ПФР — XXX-XXX-XXXXXX (для ЕФС-1, отдельно от рег. номера СФР) */
pfrRegNumber?: string | undefined | null | Variable<any, string>,
phoneOverride?: string | undefined | null | Variable<any, string>,
/** Рег. номер СФР — XXX-XXX-XXXXXX */
/** Рег. номер СФР — 10 цифр */
sfrRegNumber?: string | undefined | null | Variable<any, string>,
signerRepDoc?: string | undefined | null | Variable<any, string>,
/** СНИЛС — XXX-XXX-XXX YY или 11 цифр */
@@ -10482,6 +10485,7 @@ validateReportEdits?: [{ editsJson: string | Variable<any, string>, reportType:
firstName?:boolean | `@${string}`,
lastName?:boolean | `@${string}`,
middleName?:boolean | `@${string}`,
pfrRegNumber?:boolean | `@${string}`,
repDoc?:boolean | `@${string}`,
sfrRegNumber?:boolean | `@${string}`,
snils?:boolean | `@${string}`,
@@ -18204,6 +18208,7 @@ validateReportEdits?: [{ editsJson: string, reportType: ResolverInputTypes["Repo
oktmo?:ResolverInputTypes["RequisiteFieldView"],
okved?:ResolverInputTypes["RequisiteFieldView"],
orgName?:ResolverInputTypes["RequisiteFieldView"],
pfrRegNumber?:ResolverInputTypes["RequisiteFieldView"],
phone?:ResolverInputTypes["RequisiteFieldView"],
sfrRegNumber?:ResolverInputTypes["RequisiteFieldView"],
signerFirstName?:ResolverInputTypes["RequisiteFieldView"],
@@ -19317,8 +19322,10 @@ validateReportEdits?: [{ editsJson: string, reportType: ResolverInputTypes["Repo
oktmo?: string | undefined | null,
/** ОКВЭД — напр. 94.99, 46.73.7 */
okved?: string | undefined | null,
/** Рег. номер ПФР — XXX-XXX-XXXXXX (для ЕФС-1, отдельно от рег. номера СФР) */
pfrRegNumber?: string | undefined | null,
phoneOverride?: string | undefined | null,
/** Рег. номер СФР — XXX-XXX-XXXXXX */
/** Рег. номер СФР — 10 цифр */
sfrRegNumber?: string | undefined | null,
signerRepDoc?: string | undefined | null,
/** СНИЛС — XXX-XXX-XXX YY или 11 цифр */
@@ -19614,6 +19621,7 @@ validateReportEdits?: [{ editsJson: string, reportType: ResolverInputTypes["Repo
firstName?:boolean | `@${string}`,
lastName?:boolean | `@${string}`,
middleName?:boolean | `@${string}`,
pfrRegNumber?:boolean | `@${string}`,
repDoc?:boolean | `@${string}`,
sfrRegNumber?:boolean | `@${string}`,
snils?:boolean | `@${string}`,
@@ -27847,6 +27855,7 @@ export type ModelTypes = {
oktmo: ModelTypes["RequisiteFieldView"],
okved: ModelTypes["RequisiteFieldView"],
orgName: ModelTypes["RequisiteFieldView"],
pfrRegNumber: ModelTypes["RequisiteFieldView"],
phone: ModelTypes["RequisiteFieldView"],
sfrRegNumber: ModelTypes["RequisiteFieldView"],
signerFirstName: ModelTypes["RequisiteFieldView"],
@@ -28930,8 +28939,10 @@ export type ModelTypes = {
oktmo?: string | undefined | null,
/** ОКВЭД — напр. 94.99, 46.73.7 */
okved?: string | undefined | null,
/** Рег. номер ПФР — XXX-XXX-XXXXXX (для ЕФС-1, отдельно от рег. номера СФР) */
pfrRegNumber?: string | undefined | null,
phoneOverride?: string | undefined | null,
/** Рег. номер СФР — XXX-XXX-XXXXXX */
/** Рег. номер СФР — 10 цифр */
sfrRegNumber?: string | undefined | null,
signerRepDoc?: string | undefined | null,
/** СНИЛС — XXX-XXX-XXX YY или 11 цифр */
@@ -29214,6 +29225,7 @@ export type ModelTypes = {
firstName: string,
lastName: string,
middleName?: string | undefined | null,
pfrRegNumber?: string | undefined | null,
repDoc?: string | undefined | null,
sfrRegNumber?: string | undefined | null,
snils?: string | undefined | null,
@@ -37955,6 +37967,7 @@ export type GraphQLTypes = {
oktmo: GraphQLTypes["RequisiteFieldView"],
okved: GraphQLTypes["RequisiteFieldView"],
orgName: GraphQLTypes["RequisiteFieldView"],
pfrRegNumber: GraphQLTypes["RequisiteFieldView"],
phone: GraphQLTypes["RequisiteFieldView"],
sfrRegNumber: GraphQLTypes["RequisiteFieldView"],
signerFirstName: GraphQLTypes["RequisiteFieldView"],
@@ -39090,8 +39103,10 @@ export type GraphQLTypes = {
oktmo?: string | undefined | null,
/** ОКВЭД — напр. 94.99, 46.73.7 */
okved?: string | undefined | null,
/** Рег. номер ПФР — XXX-XXX-XXXXXX (для ЕФС-1, отдельно от рег. номера СФР) */
pfrRegNumber?: string | undefined | null,
phoneOverride?: string | undefined | null,
/** Рег. номер СФР — XXX-XXX-XXXXXX */
/** Рег. номер СФР — 10 цифр */
sfrRegNumber?: string | undefined | null,
signerRepDoc?: string | undefined | null,
/** СНИЛС — XXX-XXX-XXX YY или 11 цифр */
@@ -39395,6 +39410,7 @@ export type GraphQLTypes = {
firstName: string,
lastName: string,
middleName?: string | undefined | null,
pfrRegNumber?: string | undefined | null,
repDoc?: string | undefined | null,
sfrRegNumber?: string | undefined | null,
snils?: string | undefined | null,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@coopenomics/setup",
"version": "2026.6.24",
"version": "2026.7.21-1",
"private": true,
"description": "Интерактивный мастер настройки окружения",
"main": "index.js",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
"version": "2026.6.24",
"version": "2026.7.21-1",
"packages": [
"components/*",
"components/controller/src/plugins/*"
+1 -1
View File
@@ -53,7 +53,7 @@
"deploy": "lerna run deploy --scope @coopenomics/boot --",
"clear": "lerna run clear --scope @coopenomics/boot",
"enter": "lerna run run --scope @coopenomics/cleos",
"build:lib": "lerna run build --scope @coopenomics/factory --scope @coopenomics/cooptypes",
"build:lib": "lerna run build --scope @coopenomics/cooptypes --scope @coopenomics/factory --scope @coopenomics/inter",
"build:contracts:all": "lerna run build:all --scope @coopenomics/contracts",
"build:contract": "lerna run build:one --scope @coopenomics/contracts --",
"build:contracts:all:test": "lerna run build:all:test --scope @coopenomics/contracts",