odant-work 1.0.12 → 1.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1071 @@
1
+ /**
2
+ * Методы учётного ядра у классов дерева (`$class`): объекты, проведение, отчёты, выбор ссылки.
3
+ *
4
+ * Метод вызывается на классе, к которому относится запрос: создание — на листе
5
+ * справочника или операции, выборка — на классе с потомками, отчёты — на счёте
6
+ * (или журнале), состав — на виртуальном справочнике. Права — правила этого класса:
7
+ * чтение — `assertAccess(read)` и видимость данных по `scope` ссылок рабочих мест (scope.js):
8
+ * ссылка на класс или его предка — поддерево класса, ссылка ниже — только её ветка (сводный
9
+ * отчёт предка), строки — по подразделению, месту или автору; назначение в самом классе,
10
+ * системная сессия, DEV, админ WORK и класс с GUEST — всё. Запись — `_assertDataWrite`
11
+ * (автор — субъект сессии) и права ссылок на запись; подразделение и место создаваемого
12
+ * объекта — из мест записи пользователя. Закрытие периода — ADMIN. Объект по `id` доступен
13
+ * только в поддереве класса вызова и в его видимости; чтение через виртуальный справочник
14
+ * видит объекты его реестра (на них ссылаются его поля).
15
+ *
16
+ * Кэш классов в БД обновляется перед первым обращением и после правок class.js
17
+ * (`markClassesDirty` — из save_file и наблюдателя за диском); неизвестный класс
18
+ * под корнями данных обновляет кэш ещё раз (класс только что создан).
19
+ *
20
+ * Параметры HTTP приходят строками: флаги ('true'/'1'), JSON-объекты и списки
21
+ * (`where`, `analytics`, `units`, `status`, `by`) разбираются здесь; тело — `post` или `body`.
22
+ */
23
+ import fs from 'node:fs';
24
+ import { DEV_MODE } from '../host/config.js';
25
+ import { dataDb, baseDbFile, dataGeneration } from './db.js';
26
+ import { refreshClasses, getClass, descendants, isLeaf, classesOfType, classesGeneration, DEFAULT_ROOTS, STRUCTURE_TYPES, clearScanCache } from './classes.js';
27
+ import { classSchema, checkSchema, analyticSlots, FAMILY } from './schema.js';
28
+ import {
29
+ createObject, readObject, objectVersions, updateObject, deleteObject, restoreObject, queryObjects, linkCids, nextOperationNumber,
30
+ getDocumentBinder, linkDocuments, unlinkDocuments, candidateBinderOperations,
31
+ } from './objects.js';
32
+ import {
33
+ registerOperation, correctOperation, cancelOperation, previewEntries, operationEntries, closePeriod, closedUntil,
34
+ } from './posting.js';
35
+ import {
36
+ journal, trialBalance, accountCard, accountTurnovers, readEntry, objectState, objectTrajectory, virtualMembers, countVirtualMembers, isVirtualMember, namesOf,
37
+ } from './reports.js';
38
+ import { linkChoices } from './choices.js';
39
+ import { printTemplates, renderPrint, savePrintTemplate, deletePrintTemplate } from './print.js';
40
+ import {
41
+ scopeGrants, entryGrants, withoutBranches, objectCond, visibleUnits, writeContexts, resolvePlace,
42
+ } from './scope.js';
43
+ import { DataError } from './errors.js';
44
+ import { localDay } from './time.js';
45
+ import { buildWorkspace } from './workspace.js';
46
+ import * as SQL_INSPECTOR from './sql-inspector.js';
47
+ import { normalizeSecurity, walkPlaces, trailKey } from '../server/access/workplaces.js';
48
+
49
+ /** Ссылки рабочих мест — модулем по требованию: links.js тянет сервер, который сам импортирует этот модуль. */
50
+ const linksModule = () => import('../server/access/links.js');
51
+
52
+ const storeStates = new Map();
53
+ let dirtyGeneration = 1;
54
+
55
+ /** Путь организации ($base) для вызова: из параметра `base` или из пути элемента `/ORGANIZATIONS/<Имя>`. */
56
+ export function resolveBasePath(item, p = {}) {
57
+ if (typeof p?.base === 'string' && p.base.trim())
58
+ return '/' + p.base.trim().replace(/^\/+|\/+$/g, '');
59
+ const raw = typeof item === 'string' ? item : String(item?.path ?? '');
60
+ const m = raw.match(/^\/(ORGANIZATIONS|BASES)\/([^/]+)/);
61
+ return m ? `/${m[1]}/${m[2]}` : '';
62
+ }
63
+
64
+ function rootsForBase(basePath) {
65
+ if (!basePath)
66
+ return DEFAULT_ROOTS;
67
+ return ['MODULES', 'DATA', basePath.replace(/^\/+/, '')];
68
+ }
69
+
70
+ /** Кэш классов устарел (правка class.js): обновится перед следующим обращением. */
71
+ export function markClassesDirty() {
72
+ dirtyGeneration++;
73
+ clearScanCache();
74
+ }
75
+
76
+ /** БД данных организации ($base) с актуальным кэшем классов. */
77
+ export async function dataStore(basePath = '') {
78
+ const key = String(basePath ?? '').trim().replace(/\/+$/, '');
79
+ const db = await dataDb(key);
80
+ let st = storeStates.get(db);
81
+ if (!st) {
82
+ st = { gen: 0, refreshing: null };
83
+ storeStates.set(db, st);
84
+ }
85
+ while (st.gen < dirtyGeneration || st.refreshing) {
86
+ if (!st.refreshing) {
87
+ const targetGen = dirtyGeneration;
88
+ st.refreshing = refreshClasses(db, process.cwd(), rootsForBase(key))
89
+ .then(r => {
90
+ st.gen = targetGen;
91
+ if (r.problems.length)
92
+ console.warn('[data] классы:', r.problems.join('; '));
93
+ })
94
+ .finally(() => {
95
+ st.refreshing = null;
96
+ });
97
+ }
98
+ await st.refreshing;
99
+ }
100
+ return db;
101
+ }
102
+
103
+ /** Есть ли у класса ядра собственные неудалённые объекты (инвариант листа при создании дочернего класса). */
104
+ export async function classHasObjects(ref) {
105
+ const pathStr = String(ref ?? '');
106
+ const db = await dataStore(resolveBasePath(pathStr));
107
+ const cls = getClass(db, pathStr);
108
+ return !!cls && !!db.prepare("SELECT 1 FROM objects WHERE cid = ? AND status <> 'deleted' LIMIT 1").get(cls.cid);
109
+ }
110
+
111
+ const COUNTABLE_DATA_TYPES = new Set(['$catalog', '$virtual', '$operation', '$account', '$register']);
112
+ const dbSummaryCache = new WeakMap();
113
+ const baseDbExistsCache = new Map();
114
+
115
+ /**
116
+ * Кэш сводных счётчиков объектов/записей счетов и дат операций по БД.
117
+ * Автоматически сбрасывается при любом изменении классов (`classesGeneration`) или данных (`dataGeneration`).
118
+ */
119
+ function getDbSummary(db) {
120
+ const clsGen = classesGeneration(db);
121
+ const dataGen = dataGeneration(db);
122
+ let summary = dbSummaryCache.get(db);
123
+ if (summary && summary.clsGen === clsGen && summary.dataGen === dataGen)
124
+ return summary;
125
+
126
+ const ownObjCounts = new Map();
127
+ for (const r of db.prepare("SELECT cid, count(*) n FROM objects WHERE status <> 'deleted' AND ext_of IS NULL GROUP BY cid").all())
128
+ ownObjCounts.set(r.cid, Number(r.n) || 0);
129
+
130
+ const ownAccCounts = new Map();
131
+ for (const r of db.prepare('SELECT account_cid cid, count(*) n FROM account_entries WHERE NOT storno GROUP BY account_cid').all())
132
+ ownAccCounts.set(r.cid, Number(r.n) || 0);
133
+
134
+ const subtreeCounts = new Map();
135
+ const opDaysByCid = new Map();
136
+
137
+ summary = {
138
+ clsGen,
139
+ dataGen,
140
+ ownObjCounts,
141
+ ownAccCounts,
142
+ subtreeCounts,
143
+ opDaysByCid,
144
+ countFor(clsPath, clsType = '') {
145
+ const cls = getClass(db, clsPath);
146
+ const type = cls?.type || clsType;
147
+ if (!type || type === '$base' || type === '$structure' || type === '$domain')
148
+ return 0;
149
+ const key = `${type}:${cls?.cid || clsPath}`;
150
+ if (subtreeCounts.has(key))
151
+ return subtreeCounts.get(key);
152
+ let total = 0;
153
+ if (type === '$virtual') {
154
+ total = countVirtualMembers(db, cls?.cid || clsPath);
155
+ }
156
+ else if (type === '$account' || type === '$register') {
157
+ const accs = cls ? descendants(db, cls.cid) : [clsPath];
158
+ for (const cid of accs)
159
+ total += ownAccCounts.get(cid) || 0;
160
+ }
161
+ else if (type === '$catalog' || type === '$operation') {
162
+ const cids = cls ? descendants(db, cls.cid) : [clsPath];
163
+ for (const cid of cids) {
164
+ if (getClass(db, cid)?.type !== '$virtual')
165
+ total += ownObjCounts.get(cid) || 0;
166
+ }
167
+ }
168
+ subtreeCounts.set(key, total);
169
+ return total;
170
+ },
171
+ opDaysFor(cid, desc) {
172
+ if (opDaysByCid.has(cid))
173
+ return opDaysByCid.get(cid);
174
+ const daySet = new Set();
175
+ if (desc.length) {
176
+ for (const r of db.prepare(`SELECT DISTINCT time FROM objects WHERE cid IN (${desc.map(() => '?').join(',')}) AND status <> 'deleted' AND ext_of IS NULL AND time IS NOT NULL ORDER BY time`).all(...desc)) {
177
+ if (Number.isFinite(r.time))
178
+ daySet.add(localDay(r.time));
179
+ }
180
+ }
181
+ const days = [...daySet].sort();
182
+ opDaysByCid.set(cid, days);
183
+ return days;
184
+ },
185
+ };
186
+ dbSummaryCache.set(db, summary);
187
+ return summary;
188
+ }
189
+
190
+ /**
191
+ * Количество объектов (или записей счёта/участников виртуального справочника) у класса и его поддерева.
192
+ * Если файла БД организации ещё нет на диске — возвращает 0 без создания файла БД.
193
+ */
194
+ export async function classObjectCount(clsPath, clsType = '') {
195
+ if (clsType && !COUNTABLE_DATA_TYPES.has(clsType))
196
+ return 0;
197
+ const p = String(clsPath ?? '').trim();
198
+ if (!p || p === '/' || p.startsWith('/MODULES/'))
199
+ return 0;
200
+ const basePath = resolveBasePath(p);
201
+ const dbFile = baseDbFile(process.cwd(), basePath);
202
+ const now = Date.now();
203
+ let entry = baseDbExistsCache.get(dbFile);
204
+ if (!entry || now - entry.ts > 1000) {
205
+ entry = { ok: fs.existsSync(dbFile), ts: now };
206
+ baseDbExistsCache.set(dbFile, entry);
207
+ }
208
+ if (!entry.ok)
209
+ return 0;
210
+ try {
211
+ const db = await dataStore(basePath);
212
+ return getDbSummary(db).countFor(p, clsType);
213
+ }
214
+ catch {
215
+ return 0;
216
+ }
217
+ }
218
+
219
+ /* ---------- параметры ---------- */
220
+
221
+ const isEmpty = v => v === undefined || v === null || v === '';
222
+ const flag = v => v === true || v === 1 || v === '1' || v === 'true';
223
+ const optFlag = v => (isEmpty(v) ? undefined : flag(v));
224
+
225
+ /** JSON-строка объекта или списка — разобрать, иначе как есть. */
226
+ function json(v, what) {
227
+ if (typeof v !== 'string' || !/^\s*[[{]/.test(v))
228
+ return v;
229
+ try {
230
+ return JSON.parse(v);
231
+ }
232
+ catch {
233
+ throw new DataError(`${what}: неверный JSON`);
234
+ }
235
+ }
236
+
237
+ function list(v, what) {
238
+ const x = json(v, what);
239
+ return isEmpty(x) ? undefined : [].concat(x);
240
+ }
241
+
242
+ /** Тело запроса: объект, JSON-строка или Buffer. */
243
+ function bodyOf(p) {
244
+ let v = p.post ?? p.body;
245
+ if (Buffer.isBuffer(v))
246
+ v = v.toString('utf-8');
247
+ if (isEmpty(v))
248
+ return undefined;
249
+ v = json(v, 'тело');
250
+ if (!v || typeof v !== 'object' || Array.isArray(v))
251
+ throw new DataError('тело — JSON-объект');
252
+ return v;
253
+ }
254
+
255
+ /** Общие фильтры проводок отчётов. */
256
+ function entryFilters(p) {
257
+ return {
258
+ units: list(p.units, 'units'),
259
+ unit: p.unit,
260
+ op_cid: p.op_cid,
261
+ op: p.op,
262
+ author: p.author,
263
+ place: p.place,
264
+ };
265
+ }
266
+
267
+ /* ---------- класс вызова и права ---------- */
268
+
269
+ const underData = path => DEFAULT_ROOTS.some(r => path === '/' + r || path.startsWith('/' + r + '/'));
270
+
271
+ async function classOf(db, item, basePath = '') {
272
+ const ref = String(item?.path ?? '');
273
+ let cls = getClass(db, ref);
274
+ if (!cls && underData(ref)) {
275
+ markClassesDirty();
276
+ await dataStore(basePath);
277
+ cls = getClass(db, ref);
278
+ }
279
+ if (!cls)
280
+ throw new DataError(`${ref || 'элемент'} — не класс учётного ядра`);
281
+ return cls;
282
+ }
283
+
284
+ function inside(db, cls, ref, what) {
285
+ const c = getClass(db, ref);
286
+ if (!c || !descendants(db, cls.cid).includes(c.cid))
287
+ throw new DataError(`${what} ${ref} не найден в «${cls.label}»`);
288
+ return c.cid;
289
+ }
290
+
291
+ /** Объект поддерева класса вызова в видимости вызова (для записи и проведения — права записи). */
292
+ function ownObject(db, cls, id, vis) {
293
+ return objectIn(db, cls, id, descendants(db, cls.cid), vis);
294
+ }
295
+
296
+ /**
297
+ * Объект, видимый через класс: поддерево класса. У виртуального справочника — объект его
298
+ * реестра (на них ссылаются его поля): видимый в самом реестре или участник видимого состава.
299
+ */
300
+ async function visibleObject(db, cls, id, vis, p) {
301
+ const registry = linkCids(db, cls.cid);
302
+ if (!registry)
303
+ return objectIn(db, cls, id, descendants(db, cls.cid), vis);
304
+ try {
305
+ return objectIn(db, cls, id, registry, await registryGrants(db, cls, vis, p));
306
+ }
307
+ catch (e) {
308
+ if (!isEmpty(id) && objectIn(db, cls, id, registry, null)
309
+ && isVirtualMember(db, cls.cid, String(id), undefined, entryGrants(withoutBranches(vis), 'catalog')))
310
+ return String(id);
311
+ throw e;
312
+ }
313
+ }
314
+
315
+ /**
316
+ * Права на объекты реестра-источника виртуального справочника: состав «Клиентов» ограничен
317
+ * видимостью справочника, а поиск по реестру (новый покупатель) и чтение его объектов — видимостью
318
+ * реестра для того же пользователя. Состав виден целиком — реестр тоже (как до scope).
319
+ */
320
+ async function registryGrants(db, cls, vis, p) {
321
+ if (vis === null)
322
+ return null;
323
+ const reg = getClass(db, classSchema(db, cls.cid)?.source?.registry);
324
+ let item = reg ? await globalThis.WORK?.get_item(reg.path).catch(() => null) : null;
325
+ if (Array.isArray(item))
326
+ item = item.at(-1);
327
+ if (!item?.assertAccess)
328
+ return [];
329
+ await item.init;
330
+ return (await scopeOf(item, p, db, reg, false)).grants;
331
+ }
332
+
333
+ function objectIn(db, cls, id, cids, vis) {
334
+ const row = isEmpty(id) ? null : db.prepare('SELECT cid FROM objects WHERE id = ?').get(String(id));
335
+ let ok = !!row && cids.includes(row.cid);
336
+ const cond = ok ? objectCond(vis) : null;
337
+ if (cond)
338
+ ok = !!db.prepare(`SELECT 1 FROM objects o WHERE o.id = ? AND ${cond[0]}`).get(String(id), ...cond[1]);
339
+ if (!ok)
340
+ throw new DataError(`объект ${id ?? ''} не найден в «${cls.label}»`);
341
+ return String(id);
342
+ }
343
+
344
+ /** Права на проводки отчёта об объекте: ветки, в которые входит объект (у виртуального — его реестр). */
345
+ function objectEntryGrants(db, cls, id, vis) {
346
+ if (linkCids(db, cls.cid))
347
+ return entryGrants(withoutBranches(vis), 'catalog');
348
+ return entryGrants(vis, familyOf(cls), db.prepare('SELECT cid FROM objects WHERE id = ?').get(id)?.cid);
349
+ }
350
+
351
+ /** Названия объектов, на которые ссылаются найденные объекты (поля-ссылки и табличные части). */
352
+ function linkNames(db, ids) {
353
+ const refs = [];
354
+ for (let i = 0; i < ids.length; i += 500) {
355
+ const chunk = ids.slice(i, i + 500);
356
+ refs.push(...db.prepare(`SELECT DISTINCT ref_id FROM refs WHERE obj_id IN (${chunk.map(() => '?').join(',')})`)
357
+ .all(...chunk).map(r => r.ref_id));
358
+ }
359
+ return namesOf(db, refs).objects;
360
+ }
361
+
362
+ function familyOf(cls) {
363
+ return FAMILY[cls.type] || 'other';
364
+ }
365
+
366
+ /** Счёт отчёта: параметр внутри класса-счёта (журнала) вызова или сам класс. */
367
+ function reportAccount(db, cls, ref) {
368
+ if (familyOf(cls) !== 'account')
369
+ throw new DataError(`«${cls.label}» — не счёт и не журнал`);
370
+ return isEmpty(ref) ? cls.cid : inside(db, cls, ref, 'счёт');
371
+ }
372
+
373
+ /**
374
+ * Данные класса видны целиком, без scope ссылок: вызов без сессии, системная сессия, DEV,
375
+ * админ WORK, назначение в самом классе (роль #security, системные — и сверху); для чтения —
376
+ * и класс, открытый гостям. Для записи назначение должно давать запись.
377
+ */
378
+ async function fullAccess(item, p, write) {
379
+ if (!p?.session || DEV_MODE || p.session.$user === globalThis.WORK)
380
+ return true;
381
+ const users = item.DATA?.['#security']?.USER;
382
+ if (!write && Array.isArray(users) && (users.includes('GUEST') || users.includes('GUESTS')))
383
+ return true;
384
+ if (await item._isWorkBoss?.(p).catch(() => false))
385
+ return true;
386
+ const level = await (await linksModule()).directAccess(item, p).catch(() => null);
387
+ return write ? level === 'write' || level === 'admin' : level != null;
388
+ }
389
+
390
+ /**
391
+ * Видимость вызова: { full, links — ссылки рабочих мест пользователя на класс, его предков
392
+ * и поддерево, grants — права (null — всё, [] — ничего) }. Для записи — только ссылки на запись.
393
+ */
394
+ async function scopeOf(item, p, db, cls, write) {
395
+ const full = await fullAccess(item, p, write);
396
+ const uid = item.constructor.resolveUid?.(p);
397
+ const links = await (await linksModule()).scopeLinks(item, p).catch(() => []);
398
+ return { full, links, grants: full ? null : scopeGrants(db, cls, links, { uid, write }) };
399
+ }
400
+
401
+ const admin = async (item, p) => item.assertAccess(p, item.constructor.ACCESS_LEVEL.ADMIN);
402
+
403
+ /**
404
+ * Обёртка метода: права, БД, класс вызова, видимость (`vis` — права scope.js).
405
+ * Чтение: страница класса видна (предок ссылки — как каркас цепочки) и есть хоть одна видимая
406
+ * ветка: ссылка на класс, его предка или ниже.
407
+ */
408
+ function method(level, fn) {
409
+ return async (item, p = {}) => {
410
+ if (typeof item?.isModuleBlocked === 'function' && await item.isModuleBlocked())
411
+ throw new DataError('Модуль заблокирован', [], 403);
412
+ let uid = null;
413
+ if (level === 'read')
414
+ await item.assertAccess(p, item.constructor.ACCESS_LEVEL.READ);
415
+ else if (level === 'write')
416
+ uid = await item._assertDataWrite(p);
417
+ else
418
+ await admin(item, p);
419
+ const basePath = resolveBasePath(item, p);
420
+ const db = await dataStore(basePath);
421
+ const cls = await classOf(db, item, basePath);
422
+ const scope = level === 'admin'
423
+ ? { full: true, links: [], grants: null }
424
+ : await scopeOf(item, p, db, cls, level === 'write');
425
+ if (scope.grants?.length === 0)
426
+ throw new Error('Доступ запрещён');
427
+ return fn({ db, cls, item, p, uid, scope, vis: scope.grants });
428
+ };
429
+ }
430
+
431
+ /**
432
+ * Записать факт операции в ленту событий (data.logs) подразделения (или класса операции) и автора.
433
+ */
434
+ async function logOperationFact(db, item, op, action, uid, p) {
435
+ if (!op || !globalThis.WORK)
436
+ return;
437
+ try {
438
+ const LOGS = await import('../server/logs.js');
439
+ let storage = item;
440
+ const unitPath = op.unit || p?.unit;
441
+ if (unitPath) {
442
+ let u = await globalThis.WORK.get_item(unitPath).catch(() => null);
443
+ if (Array.isArray(u))
444
+ u = u.at(-1);
445
+ if (u?.save_file)
446
+ storage = u;
447
+ }
448
+ if (!storage?.save_file)
449
+ return;
450
+ const cls = getClass(db, op.cid);
451
+ const title = op.title || op.name || cls?.label || op.id;
452
+ const senderUid = uid || op.author || p?.uid || 'system';
453
+ const row = {
454
+ time: Date.now(),
455
+ sender: senderUid,
456
+ ext: cls?.type === '$catalog' ? 'object' : 'operation',
457
+ path: `${op.cid}/${op.id}`,
458
+ classPath: cls?.path || op.cid,
459
+ objectId: op.id,
460
+ unit: unitPath || '',
461
+ content: `${action}: ${cls?.label && cls.label !== title ? cls.label + ' — ' : ''}${title}`,
462
+ };
463
+ await LOGS.appendRow(storage, row, { ...p, uid: senderUid });
464
+ }
465
+ catch {
466
+ /* логи не должны блокировать транзакцию БД */
467
+ }
468
+ }
469
+
470
+ /**
471
+ * Схема для форм: у полей-ссылок (и в строках табличных частей, и в расширениях) к `target` (cid)
472
+ * добавлен `targetPath` — форма и поле-ссылка открывают класс по пути дерева (`WORK.get_item`), а не по cid.
473
+ */
474
+ function withTargetPaths(db, schema) {
475
+ if (!schema)
476
+ return schema;
477
+ const walk = list => (list || []).map(f => ({
478
+ ...f,
479
+ ...(f.type === 'link' && f.target ? { targetPath: getClass(db, f.target)?.path ?? f.target } : {}),
480
+ ...(f.type === 'table' || f.type === 'struct' ? { fields: walk(f.fields) } : {}),
481
+ }));
482
+ return { ...schema, fields: walk(schema.fields), overlay: walk(schema.overlay) };
483
+ }
484
+
485
+ /**
486
+ * Собрать аналитические признаки по полям прилинкованного справочника слота счёта
487
+ * (Наименование + поля схемы $catalog, вложенных структур $struct и расширений OVERLAY у $virtual)
488
+ * со списком реально существующих значений для панели «Анализ счёта».
489
+ */
490
+ function buildCatalogAnalyticFields(db, slotField) {
491
+ const slotId = slotField.id;
492
+ const slotLabel = slotField.label || slotId;
493
+ const targetPath = slotField.targetPath || slotField.target;
494
+ const targetSchema = slotField.target ? withTargetPaths(db, classSchema(db, slotField.target)) : null;
495
+ if (!targetSchema) {
496
+ return [{
497
+ id: slotId,
498
+ spec: slotId,
499
+ slotId,
500
+ fieldId: 'name',
501
+ label: 'Наименование',
502
+ slotLabel,
503
+ isObject: true,
504
+ type: 'link',
505
+ catalog: targetPath,
506
+ options: [],
507
+ }];
508
+ }
509
+ const isVirt = targetSchema.type === '$virtual';
510
+ const regCid = isVirt ? (targetSchema.source?.registry || targetSchema.cid) : targetSchema.cid;
511
+ const descCids = descendants(db, regCid);
512
+
513
+ let objRows = [];
514
+ if (descCids.length) {
515
+ objRows = db.prepare(
516
+ `SELECT id, cid, body FROM objects WHERE cid IN (${descCids.map(() => '?').join(',')}) AND status <> 'deleted' AND ext_of IS NULL ORDER BY updated DESC LIMIT 300`
517
+ ).all(...descCids);
518
+ }
519
+ const extByObj = new Map();
520
+ if (isVirt) {
521
+ const virtCids = descendants(db, targetSchema.cid);
522
+ if (virtCids.length) {
523
+ for (const r of db.prepare(
524
+ `SELECT ext_of, body FROM objects WHERE cid IN (${virtCids.map(() => '?').join(',')}) AND status <> 'deleted' AND ext_of IS NOT NULL LIMIT 300`
525
+ ).all(...virtCids)) {
526
+ try {
527
+ extByObj.set(r.ext_of, JSON.parse(r.body || '{}'));
528
+ }
529
+ catch { /* ignore */ }
530
+ }
531
+ }
532
+ }
533
+
534
+ const parsedObjs = objRows.map(r => {
535
+ let body = {};
536
+ try { body = JSON.parse(r.body || '{}'); } catch { /* ignore */ }
537
+ const title = String(body.name || body.label || body.title || r.id).trim();
538
+ return { id: r.id, title, body, ext: extByObj.get(r.id) || null };
539
+ });
540
+
541
+ const objectOptions = parsedObjs
542
+ .map(o => ({ value: o.id, label: o.title || o.id }))
543
+ .sort((a, b) => a.label.localeCompare(b.label, 'ru', { numeric: true }));
544
+
545
+ const fieldDefs = [];
546
+ const seenFieldIds = new Set(['name', 'time']);
547
+ const collectFields = (list, isOverlay = false, prefix = '', prefixLabel = '') => {
548
+ for (const f of list || []) {
549
+ if (!f?.id || f.type === 'table')
550
+ continue;
551
+ const fullId = prefix ? `${prefix}.${f.id}` : f.id;
552
+ const fullLabel = prefixLabel ? `${prefixLabel}: ${f.label || f.id}` : (f.label || f.id);
553
+ if (f.type === 'struct' && Array.isArray(f.fields) && f.fields.length) {
554
+ collectFields(f.fields, isOverlay, fullId, fullLabel);
555
+ continue;
556
+ }
557
+ if (seenFieldIds.has(fullId))
558
+ continue;
559
+ seenFieldIds.add(fullId);
560
+ fieldDefs.push({
561
+ fieldId: fullId,
562
+ label: fullLabel,
563
+ type: f.type || 'string',
564
+ target: f.target,
565
+ values: f.values || f.options,
566
+ isOverlay,
567
+ });
568
+ }
569
+ };
570
+
571
+ collectFields(targetSchema.fields, false);
572
+ if (isVirt)
573
+ collectFields(targetSchema.overlay, true);
574
+ for (const cid of descCids) {
575
+ if (cid === targetSchema.cid)
576
+ continue;
577
+ const cs = classSchema(db, cid);
578
+ if (cs)
579
+ collectFields(cs.fields, false);
580
+ }
581
+
582
+ const refIdsToResolve = new Set();
583
+ const rawValuesByField = new Map();
584
+ for (const fd of fieldDefs) {
585
+ const vals = new Set();
586
+ for (const o of parsedObjs) {
587
+ let v;
588
+ if (fd.isOverlay && o.ext) {
589
+ v = o.ext[fd.fieldId];
590
+ }
591
+ else {
592
+ let cur = o.body;
593
+ for (const part of fd.fieldId.split('.')) {
594
+ if (!cur || typeof cur !== 'object') { cur = undefined; break; }
595
+ cur = cur[part];
596
+ }
597
+ v = cur;
598
+ }
599
+ if (v === undefined || v === null || v === '' || typeof v === 'object')
600
+ continue;
601
+ const strVal = String(v).trim();
602
+ if (!strVal)
603
+ continue;
604
+ vals.add(strVal);
605
+ if (fd.type === 'link')
606
+ refIdsToResolve.add(strVal);
607
+ }
608
+ rawValuesByField.set(fd.fieldId, [...vals]);
609
+ }
610
+
611
+ const refNames = refIdsToResolve.size ? namesOf(db, [...refIdsToResolve]).objects : {};
612
+
613
+ const out = [{
614
+ id: slotId,
615
+ spec: slotId,
616
+ slotId,
617
+ fieldId: 'name',
618
+ label: 'Наименование',
619
+ slotLabel,
620
+ isObject: true,
621
+ type: 'link',
622
+ catalog: targetPath,
623
+ options: objectOptions,
624
+ }];
625
+
626
+ for (const fd of fieldDefs) {
627
+ const rawVals = rawValuesByField.get(fd.fieldId) || [];
628
+ let options = [];
629
+ if (fd.type === 'boolean') {
630
+ options = [{ value: '1', label: 'Да' }, { value: '0', label: 'Нет' }];
631
+ }
632
+ else if (fd.type === 'enum' && Array.isArray(fd.values) && fd.values.length) {
633
+ options = fd.values.map(v => (v && typeof v === 'object'
634
+ ? { value: String(v.id ?? v.value), label: String(v.label ?? v.id ?? v.value) }
635
+ : { value: String(v), label: String(v) }));
636
+ }
637
+ else {
638
+ options = rawVals.map(v => ({
639
+ value: v,
640
+ label: fd.type === 'link' ? (refNames[v] || v) : v,
641
+ })).sort((a, b) => a.label.localeCompare(b.label, 'ru', { numeric: true }));
642
+ }
643
+ out.push({
644
+ id: `${slotId}.${fd.fieldId}`,
645
+ spec: `${slotId}.${fd.fieldId}`,
646
+ slotId,
647
+ fieldId: fd.fieldId,
648
+ label: fd.label,
649
+ slotLabel,
650
+ isObject: false,
651
+ type: fd.type,
652
+ options,
653
+ });
654
+ }
655
+ return out;
656
+ }
657
+
658
+ /**
659
+ * Поддерево класса для форм: `classes` (cid → описание потомков), `leaves` (листья для создания
660
+ * объектов в группе) и `slots` (слоты аналитик счёта или журнала по всему поддереву счетов).
661
+ */
662
+ function schemaSubtree(db, cls) {
663
+ const classes = {};
664
+ const leaves = [];
665
+ const slots = [];
666
+ const seenSlots = new Set();
667
+ const fam = familyOf(cls);
668
+ const desc = descendants(db, cls.cid);
669
+ const summary = (desc.length && (fam === 'catalog' || fam === 'operation')) ? getDbSummary(db) : null;
670
+ for (const cid of desc) {
671
+ const c = getClass(db, cid);
672
+ if (!c)
673
+ continue;
674
+ const leaf = isLeaf(db, cid);
675
+ const count = summary ? (summary.ownObjCounts.get(cid) || 0) : 0;
676
+ classes[cid] = {
677
+ cid,
678
+ path: c.path,
679
+ parent: c.parent || '',
680
+ label: c.label,
681
+ type: c.type,
682
+ icon: c.own?.icon || '',
683
+ leaf,
684
+ count,
685
+ };
686
+ if (leaf && ['$catalog', '$registry', '$operation'].includes(c.type))
687
+ leaves.push({ cid, path: c.path, label: c.label, count });
688
+ if (fam === 'account') {
689
+ const s = withTargetPaths(db, classSchema(db, cid));
690
+ for (const f of analyticSlots(s || { fields: [] })) {
691
+ if (!seenSlots.has(f.id)) {
692
+ seenSlots.add(f.id);
693
+ slots.push({
694
+ id: f.id,
695
+ label: f.label || f.id,
696
+ target: f.target,
697
+ targetPath: f.targetPath || f.target,
698
+ fields: buildCatalogAnalyticFields(db, f),
699
+ });
700
+ }
701
+ }
702
+ }
703
+ }
704
+ const rootModPath = cls.path.replace(/^\/(ORGANIZATIONS|BASES)\/[^/]+/, '');
705
+ const defaultIcon = cls.own?.icon
706
+ || (rootModPath !== cls.path ? getClass(db, rootModPath)?.own?.icon : '')
707
+ || (fam === 'operation' ? 'carbon:task-complete' : 'carbon:catalog');
708
+ for (const c of Object.values(classes)) {
709
+ const parts = [];
710
+ let cur = c;
711
+ let icon = c.icon;
712
+ const seen = new Set();
713
+ while (cur && cur.cid !== cls.cid && !seen.has(cur.cid)) {
714
+ seen.add(cur.cid);
715
+ parts.unshift(cur.label || cur.cid.split('/').pop());
716
+ cur = cur.parent ? classes[cur.parent] : null;
717
+ if (!icon && cur?.icon)
718
+ icon = cur.icon;
719
+ }
720
+ c.icon = icon || defaultIcon;
721
+ c.group = parts.join('/');
722
+ }
723
+ leaves.sort((a, b) => a.label.localeCompare(b.label, 'ru'));
724
+ const opDays = (desc.length && fam === 'operation' && summary)
725
+ ? summary.opDaysFor(cls.cid, desc)
726
+ : undefined;
727
+ return { classes, leaves, ...(fam === 'account' ? { slots } : {}), ...(opDays ? { days: opDays } : {}) };
728
+ }
729
+
730
+ /** Пути мест подразделения (дерево ROLES его #security). */
731
+ async function structurePlaces(path) {
732
+ if (!path)
733
+ return [];
734
+ let g = await globalThis.WORK?.get_item(path).catch(() => null);
735
+ if (Array.isArray(g))
736
+ g = g.at(-1);
737
+ await g?.init;
738
+ return walkPlaces(normalizeSecurity(g?.DATA?.['#security']).places).map(({ trail }) => trailKey(trail));
739
+ }
740
+
741
+ /** Подразделение и место создаваемого объекта: указанные клиентом (проверка) или единственные доступные. */
742
+ async function writePlace(db, cls, p, scope) {
743
+ const known = isEmpty(p.unit) || isEmpty(p.place) ? null : await structurePlaces(getClass(db, p.unit)?.path);
744
+ return resolvePlace(db, writeContexts(db, cls, scope.links), { unit: p.unit, place: p.place }, { full: scope.full, known });
745
+ }
746
+
747
+ /* ---------- методы ---------- */
748
+
749
+ function buildScopeInfo(db, cls, scope) {
750
+ return {
751
+ all: scope.grants === null,
752
+ units: (visibleUnits(scope.grants) ?? classesOfType(db, STRUCTURE_TYPES).map(c => c.cid)).map(cid => {
753
+ const c = getClass(db, cid);
754
+ return { cid, path: c?.path ?? cid, label: c?.label ?? cid };
755
+ }),
756
+ contexts: writeContexts(db, cls, scope.links),
757
+ };
758
+ }
759
+
760
+ /**
761
+ * Методы ядра: имя → { level — уровень шлюза (access/gateway.js), fn(item, params) }.
762
+ * Устанавливаются в прототип `$class` (sources/server/class.js).
763
+ */
764
+ export const DATA_METHODS = Object.freeze({
765
+ /**
766
+ * Схема класса по наследованию и ошибки конфигурации: { schema, errors }.
767
+ * Схема вложена: ответ с `type`/`path` на верхнем уровне клиент (WORK.__bind) превратил бы в элемент дерева.
768
+ */
769
+ db_schema: { level: 'read', fn: method('read', ({ db, cls, p, scope }) => ({
770
+ schema: withTargetPaths(db, classSchema(db, cls.cid)),
771
+ errors: checkSchema(db, cls.cid),
772
+ ...schemaSubtree(db, cls),
773
+ ...(flag(p?.withScope) ? { scope: buildScopeInfo(db, cls, scope) } : {}),
774
+ })) },
775
+
776
+ /** Вычислить следующий номер операции по нумератору класса и дате: { time? } → { number, field, reset, numerator }. */
777
+ db_next_number: { level: 'read', fn: method('read', ({ db, cls, p }) => {
778
+ const schema = classSchema(db, cls.cid);
779
+ const number = nextOperationNumber(db, schema, p?.time);
780
+ return {
781
+ number,
782
+ field: schema?.numerator?.field || 'num',
783
+ reset: schema?.numerator?.reset || 'year',
784
+ numerator: schema?.numerator || null,
785
+ };
786
+ }) },
787
+
788
+ /**
789
+ * Видимость и места записи пользователя в классе: { all, units, contexts }.
790
+ * all — видны все данные; units — подразделения для фильтра отчётов и выборок
791
+ * ({ cid, path, label }; при all — все подразделения); contexts — подразделения и места,
792
+ * от которых можно создать объект ({ unit, unit_label, place, place_label, any? }).
793
+ */
794
+ db_scope: { level: 'read', fn: method('read', ({ db, cls, scope }) => buildScopeInfo(db, cls, scope)) },
795
+
796
+ /**
797
+ * Рабочее пространство пользователя в подразделении вызова и вложенных: подразделения и места
798
+ * с меню (ссылки мест на классы), контексты работы и описание классов меню — см. workspace.js.
799
+ * Вызывается на подразделении (`$structure`, `$base`); права — чтение подразделения.
800
+ */
801
+ db_workspace: { level: 'read', fn: async (item, p = {}) => {
802
+ const basePath = resolveBasePath(item, p);
803
+ const db = await dataStore(basePath);
804
+ const root = await classOf(db, item, basePath);
805
+ if (!STRUCTURE_TYPES.includes(root.type))
806
+ throw new DataError(`«${root.label}» — не подразделение`);
807
+ const units = [];
808
+ for (const cid of descendants(db, root.cid)) {
809
+ const c = getClass(db, cid);
810
+ if (!STRUCTURE_TYPES.includes(c.type))
811
+ continue;
812
+ let g = await globalThis.WORK.get_item(c.path);
813
+ if (Array.isArray(g))
814
+ g = g.at(-1);
815
+ await g?.init;
816
+ const roles = await g?.roles(p).catch(() => []) ?? [];
817
+ const admin = DEV_MODE || p.session?.$user === globalThis.WORK || roles.includes('BOSS')
818
+ || await g?._isWorkBoss?.(p).catch(() => false) || false;
819
+ units.push({ ...c, parent: c.cid === root.cid ? null : c.parent, roles, admin, security: g?.DATA?.['#security'] });
820
+ }
821
+ return buildWorkspace(db, { units, uid: item.constructor.resolveUid?.(p) ?? null });
822
+ } },
823
+
824
+ /**
825
+ * Создать объект в листе: { post|body, time?, unit?, place?, ext_of?, valid_from? }.
826
+ * Подразделение и место — из мест записи пользователя: не указаны — единственные доступные.
827
+ */
828
+ db_create: { level: 'write', fn: method('write', async ({ db, cls, item, p, uid, scope }) => {
829
+ const at = await writePlace(db, cls, p, scope);
830
+ const obj = createObject(db, {
831
+ cid: cls.cid, body: bodyOf(p) ?? {}, time: p.time, unit: at.unit, place: at.place,
832
+ ext_of: p.ext_of, valid_from: p.valid_from, basis_id: p.basis_id, basis_note: p.basis_note, author: uid,
833
+ });
834
+ if (obj)
835
+ await logOperationFact(db, item, obj, obj.status === 'registered' ? 'Проведено' : 'Создано', uid, p);
836
+ return obj;
837
+ }) },
838
+
839
+ /**
840
+ * Объект: { id, at? } — состояние на момент. На виртуальном справочнике — объект его реестра.
841
+ * `names` — названия объектов по его ссылкам (форма показывает ссылки без запроса на каждую).
842
+ */
843
+ db_read: { level: 'read', fn: method('read', async ({ db, cls, p, vis }) => {
844
+ const obj = readObject(db, await visibleObject(db, cls, p.id, vis, p), { at: p.at });
845
+ return obj && { ...obj, names: linkNames(db, [obj.id]) };
846
+ }) },
847
+
848
+ /** Версии объекта: { id }. */
849
+ db_versions: { level: 'read', fn: method('read', async ({ db, cls, p, vis }) => objectVersions(db, await visibleObject(db, cls, p.id, vis, p))) },
850
+
851
+ /** Изменить объект: { id, post|body, replace?, time?, valid_from?, version? }. */
852
+ db_update: { level: 'write', fn: method('write', ({ db, cls, p, uid, vis }) => updateObject(db, {
853
+ id: ownObject(db, cls, p.id, vis), body: bodyOf(p), replace: flag(p.replace), time: p.time,
854
+ valid_from: p.valid_from, version: p.version, author: uid,
855
+ })) },
856
+
857
+ /** Удалить объект (отметка): { id, version? }. */
858
+ db_delete: { level: 'write', fn: method('write', ({ db, cls, p, uid, vis }) => deleteObject(db, { id: ownObject(db, cls, p.id, vis), version: p.version, author: uid })) },
859
+
860
+ /** Восстановить удалённый: { id }. */
861
+ db_restore: { level: 'write', fn: method('write', ({ db, cls, p, uid, vis }) => restoreObject(db, { id: ownObject(db, cls, p.id, vis), author: uid })) },
862
+
863
+ /** Выборка объектов класса (с потомками): параметры queryObjects; `names` — названия объектов по ссылкам. */
864
+ db_query: { level: 'read', fn: method('read', ({ db, cls, p, vis }) => {
865
+ const res = queryObjects(db, {
866
+ cid: cls.cid, deep: optFlag(p.deep), status: list(p.status, 'status'), where: json(p.where, 'where'),
867
+ search: p.search, from: p.from, to: p.to, units: list(p.units, 'units'), unit: p.unit, author: p.author,
868
+ ext_of: p.ext_of, order: p.order, limit: p.limit, offset: p.offset, visible: vis,
869
+ });
870
+ return { ...res, names: linkNames(db, res.items.map(o => o.id)) };
871
+ }) },
872
+
873
+ /** Кандидаты для ссылки на этот справочник: { at?, search?, all?, limit? }. */
874
+ db_choices: { level: 'read', fn: method('read', async ({ db, cls, p, vis }) => linkChoices(db, {
875
+ target: cls.cid, at: p.at, search: p.search, all: flag(p.all), limit: p.limit,
876
+ units: list(p.units, 'units'), unit: p.unit, visible: vis,
877
+ registryVisible: cls.type === '$virtual' ? await registryGrants(db, cls, vis, p) : undefined,
878
+ })) },
879
+
880
+ /** Состав виртуального справочника на момент: { at?, mode? }. */
881
+ db_members: { level: 'read', fn: method('read', ({ db, cls, p, vis }) => virtualMembers(db, {
882
+ cid: cls.cid, at: p.at, mode: p.mode, units: list(p.units, 'units'), unit: p.unit, visible: entryGrants(withoutBranches(vis), 'catalog'),
883
+ })) },
884
+
885
+ /** Зарегистрировать черновик: { id, version? }. */
886
+ db_register: { level: 'write', fn: method('write', async ({ db, cls, item, p, uid, vis }) => {
887
+ const op = registerOperation(db, { id: ownObject(db, cls, p.id, vis), version: p.version, author: uid });
888
+ await logOperationFact(db, item, op, 'Проведено', uid, p);
889
+ return op;
890
+ }) },
891
+
892
+ /** Исправить зарегистрированную: { id, post|body, replace?, time?, version? }. */
893
+ db_correct: { level: 'write', fn: method('write', async ({ db, cls, item, p, uid, vis }) => {
894
+ const op = correctOperation(db, {
895
+ id: ownObject(db, cls, p.id, vis), body: bodyOf(p), replace: flag(p.replace), time: p.time, version: p.version, author: uid,
896
+ });
897
+ await logOperationFact(db, item, op, 'Исправлено', uid, p);
898
+ return op;
899
+ }) },
900
+
901
+ /** Отменить зарегистрированную: { id, version? }. */
902
+ db_cancel: { level: 'write', fn: method('write', async ({ db, cls, item, p, uid, vis }) => {
903
+ const op = cancelOperation(db, { id: ownObject(db, cls, p.id, vis), version: p.version, author: uid });
904
+ await logOperationFact(db, item, op, 'Отменено', uid, p);
905
+ return op;
906
+ }) },
907
+
908
+ /**
909
+ * Проводки без записи: { id } или { post|body, time? } для листа операции.
910
+ * `names` — названия счетов (cid) и объектов аналитик для показа.
911
+ */
912
+ db_preview: { level: 'read', fn: method('read', ({ db, cls, p, vis }) => {
913
+ const res = isEmpty(p.id)
914
+ ? previewEntries(db, { cid: cls.cid, body: bodyOf(p) ?? {}, time: p.time })
915
+ : previewEntries(db, { id: ownObject(db, cls, p.id, vis) });
916
+ return {
917
+ ...res,
918
+ names: namesOf(db,
919
+ res.entries.flatMap(e => [...Object.values(e.debit_analytics), ...Object.values(e.credit_analytics)]),
920
+ res.entries.flatMap(e => [e.debit, e.credit])),
921
+ };
922
+ }) },
923
+
924
+ /** Проводки операции: { id, history? }. */
925
+ db_entries: { level: 'read', fn: method('read', ({ db, cls, p, vis }) => operationEntries(db, ownObject(db, cls, p.id, vis), { history: flag(p.history) })) },
926
+
927
+ /**
928
+ * Печатные формы операции: { id? } — формы класса объекта (с унаследованными), без id — класса вызова.
929
+ * Не операция — пустой список. Ответ: [{ name, label, source }].
930
+ */
931
+ db_print_list: { level: 'read', fn: method('read', async ({ db, cls, p, vis }) => {
932
+ const cid = isEmpty(p.id) ? cls.cid : readObject(db, ownObject(db, cls, p.id, vis))?.cid;
933
+ if (!cid || familyOf(getClass(db, cid)) !== 'operation')
934
+ return [];
935
+ return (await printTemplates(db, process.cwd(), cid)).map(({ name, label, source }) => ({ name, label, source }));
936
+ }) },
937
+
938
+ /** Печатная форма объекта: { id, name?, fallback?, customHtml? } → { name, label, source, html, templateHtml } (name не нужен, если форма одна). */
939
+ db_print: { level: 'read', fn: method('read', async ({ db, cls, p, vis }) => renderPrint(db, {
940
+ id: ownObject(db, cls, p.id, vis),
941
+ name: p.name,
942
+ root: process.cwd(),
943
+ fallback: flag(p.fallback),
944
+ customHtml: p.customHtml || (bodyOf(p)?.customHtml ?? ''),
945
+ })) },
946
+
947
+ /** Сохранить HTML-шаблон печатной формы в класс операции: { name, html } (или в post body). */
948
+ db_print_save: { level: 'write', fn: method('write', async ({ db, cls, p }) => {
949
+ const b = bodyOf(p) || {};
950
+ return savePrintTemplate(db, {
951
+ ref: cls.cid,
952
+ name: p.name || b.name,
953
+ html: p.html || b.html,
954
+ root: process.cwd(),
955
+ });
956
+ }) },
957
+
958
+ /** Удалить собственный HTML-шаблон печатной формы из класса операции: { name }. */
959
+ db_print_delete: { level: 'write', fn: method('write', async ({ db, cls, p }) => deletePrintTemplate(db, {
960
+ ref: cls.cid,
961
+ name: p.name,
962
+ root: process.cwd(),
963
+ })) },
964
+
965
+ /** Подшивка операции: документы-основания (`parents`) и документы-следствия (`children`). */
966
+ db_binder: { level: 'read', fn: method('read', ({ db, cls, p, vis }) => getDocumentBinder(db, ownObject(db, cls, p.id, vis))) },
967
+
968
+ /** Кандидаты операций для подшивки к документу `id`: { id?, search?, samePartyOnly?, limit? }. */
969
+ db_binder_candidates: { level: 'read', fn: method('read', ({ db, cls, p, vis }) => candidateBinderOperations(db, p.id ? ownObject(db, cls, p.id, vis) : '', {
970
+ search: p.search,
971
+ samePartyOnly: flag(p.samePartyOnly),
972
+ limit: p.limit,
973
+ })) },
974
+
975
+ /** Связать две операции в подшивке: { id, targetId, direction?: 'parent'|'child', note? }. */
976
+ db_binder_link: { level: 'write', fn: method('write', ({ db, cls, p, uid, vis }) => {
977
+ const curId = ownObject(db, cls, p.id, vis);
978
+ const otherId = String(p.targetId ?? '').trim();
979
+ const dir = String(p.direction || 'parent').trim();
980
+ const sourceId = dir === 'child' ? curId : otherId;
981
+ const targetId = dir === 'child' ? otherId : curId;
982
+ linkDocuments(db, { sourceId, targetId, note: p.note || '', author: uid });
983
+ return getDocumentBinder(db, curId);
984
+ }) },
985
+
986
+ /** Открепить связь между двумя операциями в подшивке: { id, targetId }. */
987
+ db_binder_unlink: { level: 'write', fn: method('write', ({ db, cls, p, vis }) => {
988
+ const curId = ownObject(db, cls, p.id, vis);
989
+ unlinkDocuments(db, { id: curId, targetId: p.targetId });
990
+ return getDocumentBinder(db, curId);
991
+ }) },
992
+
993
+ /**
994
+ * Журнал проводок. На счёте (журнале) — проводки его счетов, на классе операций —
995
+ * проводки его операций. { from?, to?, account?, debit?, credit?, object?, effective?, desc?, limit?, offset? }
996
+ * Видимость ветки-счёта — по любой стороне проводки, ветки операций — по классу операции.
997
+ */
998
+ db_journal: { level: 'read', fn: method('read', ({ db, cls, p, vis }) => {
999
+ const q = {
1000
+ ...entryFilters(p), from: p.from, to: p.to, object: p.object,
1001
+ effective: flag(p.effective), desc: flag(p.desc), limit: p.limit, offset: p.offset,
1002
+ visible: entryGrants(vis, familyOf(cls)),
1003
+ };
1004
+ if (familyOf(cls) === 'operation') {
1005
+ q.op_cid = isEmpty(p.op_cid) ? cls.cid : inside(db, cls, p.op_cid, 'класс операций');
1006
+ for (const k of ['account', 'debit', 'credit'])
1007
+ q[k] = p[k];
1008
+ }
1009
+ else {
1010
+ q.account = reportAccount(db, cls, p.account);
1011
+ if (!isEmpty(p.debit))
1012
+ q.debit = inside(db, cls, p.debit, 'счёт');
1013
+ if (!isEmpty(p.credit))
1014
+ q.credit = inside(db, cls, p.credit, 'счёт');
1015
+ }
1016
+ return journal(db, q);
1017
+ }) },
1018
+
1019
+ /** ОСВ по счёту (журналу) вызова: { from?, to?, account?, by?, analytics?, subanalytics? }. */
1020
+ db_sheet: { level: 'read', fn: method('read', ({ db, cls, p, vis }) => trialBalance(db, {
1021
+ ...entryFilters(p), from: p.from, to: p.to, account: reportAccount(db, cls, p.account), by: list(p.by, 'by'),
1022
+ analytics: json(p.analytics, 'analytics'), subanalytics: json(p.subanalytics, 'subanalytics'),
1023
+ visible: entryGrants(vis, 'account'),
1024
+ })) },
1025
+
1026
+ /** Карточка счёта: { from?, to?, account?, analytics?, subanalytics?, limit?, offset? }. */
1027
+ db_card: { level: 'read', fn: method('read', ({ db, cls, p, vis }) => accountCard(db, {
1028
+ ...entryFilters(p), from: p.from, to: p.to, account: reportAccount(db, cls, p.account),
1029
+ analytics: json(p.analytics, 'analytics'), subanalytics: json(p.subanalytics, 'subanalytics'),
1030
+ limit: p.limit, offset: p.offset, visible: entryGrants(vis, 'account'),
1031
+ })) },
1032
+
1033
+ /** Двухпанельные Обороты счёта (Экран 3 ODANT): { from?, to?, account?, groupBy?, group?, analytics?, subanalytics?, limit? }. */
1034
+ db_turnovers: { level: 'read', fn: method('read', ({ db, cls, p, vis }) => accountTurnovers(db, {
1035
+ ...entryFilters(p), from: p.from, to: p.to, account: reportAccount(db, cls, p.account),
1036
+ groupBy: p.groupBy, group: p.group,
1037
+ analytics: json(p.analytics, 'analytics'), subanalytics: json(p.subanalytics, 'subanalytics'),
1038
+ limit: p.limit, visible: entryGrants(vis, 'account'),
1039
+ })) },
1040
+
1041
+ /** Сборная карточка проводки (Экран 4 ODANT) из записей счетов Дт и Кт: { entry|id }. */
1042
+ db_entry: { level: 'read', fn: method('read', ({ db, p }) => readEntry(db, p.entry ?? p.id)) },
1043
+
1044
+ /** Состояние объекта класса на момент: { id, at?, account? }. */
1045
+ db_state: { level: 'read', fn: method('read', async ({ db, cls, p, vis }) => {
1046
+ const id = await visibleObject(db, cls, p.id, vis, p);
1047
+ return objectState(db, { ...entryFilters(p), id, at: p.at, account: p.account, visible: objectEntryGrants(db, cls, id, vis) });
1048
+ }) },
1049
+
1050
+ /** Траектория объекта класса: { id, from?, to?, account?, limit?, offset? }. */
1051
+ db_trajectory: { level: 'read', fn: method('read', async ({ db, cls, p, vis }) => {
1052
+ const id = await visibleObject(db, cls, p.id, vis, p);
1053
+ return objectTrajectory(db, {
1054
+ ...entryFilters(p), id, from: p.from, to: p.to, account: p.account,
1055
+ limit: p.limit, offset: p.offset, visible: objectEntryGrants(db, cls, id, vis),
1056
+ });
1057
+ }) },
1058
+
1059
+ /** Закрыть период по день включительно ({ day }; пусто — открыть) или узнать дату закрытия ({}). */
1060
+ db_close_period: { level: 'admin', fn: method('admin', ({ db, p }) => ({
1061
+ closed_until: 'day' in p ? closePeriod(db, p.day) : closedUntil(db),
1062
+ })) },
1063
+
1064
+ /** Инспектор и редактор SQL-баз данных (sql_databases, sql_schema, sql_rows, sql_update_row, sql_insert_row, sql_delete_row). */
1065
+ sql_databases: { level: 'admin', fn: (item, p) => SQL_INSPECTOR.listSqlDatabases(item, p) },
1066
+ sql_schema: { level: 'admin', fn: (item, p) => SQL_INSPECTOR.getSqlSchema(item, p) },
1067
+ sql_rows: { level: 'admin', fn: (item, p) => SQL_INSPECTOR.querySqlRows(item, p) },
1068
+ sql_update_row: { level: 'admin', fn: (item, p) => SQL_INSPECTOR.updateSqlRow(item, p) },
1069
+ sql_insert_row: { level: 'admin', fn: (item, p) => SQL_INSPECTOR.insertSqlRow(item, p) },
1070
+ sql_delete_row: { level: 'admin', fn: (item, p) => SQL_INSPECTOR.deleteSqlRows(item, p) },
1071
+ });