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.
- package/#system/certs/russian_trusted_root_ca.pem +33 -0
- package/#system/mail.json.example +9 -0
- package/#system/package-manifest.json +986 -0
- package/#system/sandbox.json +19 -0
- package/#system/settings.json.example +3 -0
- package/package.json +1 -1
- package/run.mjs +78 -11
- package/scripts/check-ignored-code.mjs +25 -1
- package/sources/data/api.js +1071 -0
- package/sources/data/choices.js +125 -0
- package/sources/data/classes.js +412 -0
- package/sources/data/db.js +383 -0
- package/sources/data/errors.js +15 -0
- package/sources/data/expr.js +456 -0
- package/sources/data/objects.js +950 -0
- package/sources/data/posting.js +657 -0
- package/sources/data/print.js +524 -0
- package/sources/data/progress.md +526 -0
- package/sources/data/readme.md +77 -0
- package/sources/data/reports.js +1033 -0
- package/sources/data/schema.js +586 -0
- package/sources/data/scope.js +281 -0
- package/sources/data/sql-inspector.js +892 -0
- package/sources/data/time.js +67 -0
- package/sources/data/workspace.js +152 -0
- package/sources/host/auto-updater.js +52 -10
- package/.vscode/launch.json +0 -32
- package/.vscode/settings.json +0 -2
- package/.vscode/tasks.json +0 -65
- package/ORGANIZATIONS//320/221/320/270/320/267/320/275/320/265/321/201 /320/230/320/275/321/202/320/265/321/200 /320/241/320/276/321/204/321/202/orders/$structure/video/IMG_8524.MP4 +0 -0
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Схема класса данных, собранная по трёхосевому наследованию, и её проверка.
|
|
3
|
+
*
|
|
4
|
+
* Трёхосевое наследование метаданных (приоритет от базового к приоритетному):
|
|
5
|
+
* 1. Types (миксины: `types` в class.js) — расширяющие классы;
|
|
6
|
+
* 2. Ancestor (одноимённый класс в корневом модуле /MODULES или /DATA, либо явный `ancestor`/`extends`);
|
|
7
|
+
* 3. Parent (цепочка предков одного семейства в текущем дереве);
|
|
8
|
+
* 4. Сам класс (собственный слой class.js).
|
|
9
|
+
*
|
|
10
|
+
* Правила слияния слоёв:
|
|
11
|
+
* - FIELDS — слияние по id: более приоритетный слой уточняет свойства поля, новые поля — в конец;
|
|
12
|
+
* `removed: true` убирает унаследованное поле; поле с `to_inherit: false`
|
|
13
|
+
* действует только в своём классе (и его прямом наследнике по Ancestor);
|
|
14
|
+
* - POSTINGS — слияние по id: debit/credit и их analytics уточняются по ключам;
|
|
15
|
+
* `removed: true` убирает унаследованное правило;
|
|
16
|
+
* - OVERLAY — поля записей-расширений виртуального справочника (как FIELDS);
|
|
17
|
+
* - AUTO_REGISTER, SOURCE — ближайшее определённое значение;
|
|
18
|
+
* - STATIC — свойства самого класса (с учётом Ancestor).
|
|
19
|
+
*
|
|
20
|
+
* Если поле ссылается на класс-структуру ($struct):
|
|
21
|
+
* - при `table: false` (по умолчанию) тип поля — `'struct'` (вложенный объект со всеми полями структуры, рекурсивно);
|
|
22
|
+
* - при `table: true` тип поля — `'table'` (массив объектов этой структуры).
|
|
23
|
+
* Если поле ссылается на справочник ($catalog, $virtual) — тип поля `'link'`.
|
|
24
|
+
*/
|
|
25
|
+
import { getClass, ancestors, isLeaf, classesGeneration } from './classes.js';
|
|
26
|
+
import { compile, BUILTIN_FUNCTIONS } from './expr.js';
|
|
27
|
+
|
|
28
|
+
/** Семейство типа класса: схема наследуется внутри семейства. */
|
|
29
|
+
export const FAMILY = Object.freeze({
|
|
30
|
+
$catalog: 'catalog', $registry: 'catalog', $virtual: 'catalog',
|
|
31
|
+
$struct: 'struct',
|
|
32
|
+
$operation: 'operation',
|
|
33
|
+
$account: 'account', $register: 'account',
|
|
34
|
+
$structure: 'structure', $base: 'structure',
|
|
35
|
+
$module: 'module',
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
/** Канонические типы полей и их синонимы в class.js. */
|
|
39
|
+
export const FIELD_TYPES = Object.freeze(['string', 'text', 'number', 'money', 'integer', 'boolean', 'date', 'datetime', 'link', 'struct', 'table', 'enum', 'file']);
|
|
40
|
+
const TYPE_ALIASES = { timestamp: 'datetime', time: 'datetime', bool: 'boolean', int: 'integer', ref: 'link', array: 'table', structure: 'struct' };
|
|
41
|
+
|
|
42
|
+
/** Функции, которые ядро проведения передаёт в выражения правил. */
|
|
43
|
+
export const RULE_FUNCTIONS = Object.freeze(['norm', 'coef', 'unit_cost']);
|
|
44
|
+
|
|
45
|
+
/** Нормализованный тип поля ('' — неизвестный). */
|
|
46
|
+
export function fieldType(raw) {
|
|
47
|
+
const t = String(raw ?? 'string').trim().toLowerCase();
|
|
48
|
+
const n = TYPE_ALIASES[t] || t;
|
|
49
|
+
return FIELD_TYPES.includes(n) ? n : '';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function clone(v) {
|
|
53
|
+
return v === undefined ? undefined : structuredClone(v);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Слияние списков по id. inherited — признак слоя предка по Parent (учёт to_inherit:false). */
|
|
57
|
+
function mergeById(base, layer, inherited, mergeItem) {
|
|
58
|
+
const out = base;
|
|
59
|
+
for (const raw of Array.isArray(layer) ? layer : []) {
|
|
60
|
+
if (!raw || typeof raw !== 'object' || !raw.id)
|
|
61
|
+
continue;
|
|
62
|
+
if (inherited && raw.to_inherit === false)
|
|
63
|
+
continue;
|
|
64
|
+
const id = String(raw.id);
|
|
65
|
+
const i = out.findIndex(x => x.id === id);
|
|
66
|
+
if (raw.removed) {
|
|
67
|
+
if (i >= 0)
|
|
68
|
+
out.splice(i, 1);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (i >= 0)
|
|
72
|
+
out[i] = mergeItem(out[i], raw);
|
|
73
|
+
else
|
|
74
|
+
out.push(mergeItem({}, raw));
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function mergeField(prev, raw) {
|
|
80
|
+
const f = { ...prev, ...clone(raw), id: String(raw.id) };
|
|
81
|
+
const prevSub = prev.FIELDS ?? prev.fields;
|
|
82
|
+
const rawSub = raw.FIELDS ?? raw.fields;
|
|
83
|
+
if (Array.isArray(prevSub) && Array.isArray(rawSub)) {
|
|
84
|
+
f.FIELDS = mergeById(clone(prevSub), rawSub, false, mergeField);
|
|
85
|
+
delete f.fields;
|
|
86
|
+
}
|
|
87
|
+
delete f.to_inherit;
|
|
88
|
+
delete f.removed;
|
|
89
|
+
return f;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function mergeSide(prev, raw) {
|
|
93
|
+
if (!raw)
|
|
94
|
+
return prev;
|
|
95
|
+
return {
|
|
96
|
+
...prev,
|
|
97
|
+
...clone(raw),
|
|
98
|
+
analytics: { ...(prev?.analytics || {}), ...(clone(raw.analytics) || {}) },
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function mergeRule(prev, raw) {
|
|
103
|
+
const r = { ...prev, ...clone(raw), id: String(raw.id) };
|
|
104
|
+
r.debit = mergeSide(prev.debit, raw.debit);
|
|
105
|
+
r.credit = mergeSide(prev.credit, raw.credit);
|
|
106
|
+
if (r.quantity !== undefined && r.qty === undefined)
|
|
107
|
+
r.qty = r.quantity;
|
|
108
|
+
delete r.quantity;
|
|
109
|
+
delete r.removed;
|
|
110
|
+
delete r.to_inherit;
|
|
111
|
+
return r;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Путь организации ($base), внутри которой находится класс (или '' для корня). */
|
|
115
|
+
export function classBasePath(db, cid) {
|
|
116
|
+
const chain = ancestors(db, cid, { self: true });
|
|
117
|
+
for (let i = chain.length - 1; i >= 0; i--) {
|
|
118
|
+
if (chain[i].type === '$base')
|
|
119
|
+
return chain[i].path;
|
|
120
|
+
}
|
|
121
|
+
const m = /^(\/MODULES\/[^/]+)\//.exec(String(cid || ''));
|
|
122
|
+
if (m)
|
|
123
|
+
return m[1];
|
|
124
|
+
return '';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Разрешить ссылку на класс в контексте организации ($base):
|
|
129
|
+
* если внутри $base материализован одноимённый класс (`basePath + refPath`),
|
|
130
|
+
* используется он, иначе — указанный путь `refPath`.
|
|
131
|
+
*/
|
|
132
|
+
export function resolveInBase(db, basePath, refPath) {
|
|
133
|
+
const ref = String(refPath ?? '').trim();
|
|
134
|
+
if (!ref || !basePath || !ref.startsWith('/') || ref === basePath || ref.startsWith(basePath + '/'))
|
|
135
|
+
return ref;
|
|
136
|
+
const candidates = [basePath + ref];
|
|
137
|
+
if (ref.startsWith('/DATA/'))
|
|
138
|
+
candidates.push(basePath + '/MODULES/Register' + ref.slice('/DATA'.length), basePath + ref.slice('/DATA'.length));
|
|
139
|
+
else if (ref.startsWith('/MODULES/')) {
|
|
140
|
+
candidates.push(basePath + ref.slice('/MODULES'.length));
|
|
141
|
+
if (ref.startsWith('/MODULES/Register/'))
|
|
142
|
+
candidates.push(basePath + '/DATA' + ref.slice('/MODULES/Register'.length), basePath + ref.slice('/MODULES/Register'.length));
|
|
143
|
+
}
|
|
144
|
+
for (const cand of candidates) {
|
|
145
|
+
if (getClass(db, cand))
|
|
146
|
+
return cand;
|
|
147
|
+
}
|
|
148
|
+
return ref;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Найти класс-предок по оси Ancestor (одноимённый класс в корневом модуле /MODULES или /DATA,
|
|
153
|
+
* либо явно указанный в `ancestor`/`extends`).
|
|
154
|
+
*/
|
|
155
|
+
export function findAncestor(db, cls) {
|
|
156
|
+
if (!cls)
|
|
157
|
+
return null;
|
|
158
|
+
const explicit = cls.own?.ancestor ?? cls.own?.extends ?? cls.own?.METADATA?.ancestor ?? cls.own?.METADATA?.extends;
|
|
159
|
+
if (typeof explicit === 'string' && explicit.trim()) {
|
|
160
|
+
const hit = getClass(db, explicit.trim());
|
|
161
|
+
if (hit && hit.cid !== cls.cid)
|
|
162
|
+
return hit;
|
|
163
|
+
}
|
|
164
|
+
const basePath = classBasePath(db, cls.cid);
|
|
165
|
+
if (!basePath || cls.path === basePath)
|
|
166
|
+
return null;
|
|
167
|
+
const rel = cls.path.slice(basePath.length);
|
|
168
|
+
if (!rel || rel === '/')
|
|
169
|
+
return null;
|
|
170
|
+
const candidates = [rel];
|
|
171
|
+
if (rel.startsWith('/DATA/'))
|
|
172
|
+
candidates.push('/MODULES/Register' + rel.slice('/DATA'.length));
|
|
173
|
+
else if (rel.startsWith('/MODULES/Register/'))
|
|
174
|
+
candidates.push('/DATA' + rel.slice('/MODULES/Register'.length));
|
|
175
|
+
else if (!rel.startsWith('/MODULES/'))
|
|
176
|
+
candidates.push('/MODULES' + rel, '/DATA' + rel, '/MODULES/Register' + rel);
|
|
177
|
+
|
|
178
|
+
for (const cand of candidates) {
|
|
179
|
+
if (cand === cls.cid)
|
|
180
|
+
continue;
|
|
181
|
+
const hit = getClass(db, cand);
|
|
182
|
+
if (hit && FAMILY[hit.type] === FAMILY[cls.type])
|
|
183
|
+
return hit;
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Цепочка предков одного семейства от корня семейства до cls включительно. */
|
|
189
|
+
function sameFamilyChain(db, cls) {
|
|
190
|
+
const family = FAMILY[cls.type] || 'other';
|
|
191
|
+
const chain = ancestors(db, cls.cid);
|
|
192
|
+
let from = chain.length - 1;
|
|
193
|
+
while (from > 0 && FAMILY[chain[from - 1].type] === family)
|
|
194
|
+
from--;
|
|
195
|
+
return chain.slice(from);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Список ссылок `types` (миксинов) из собственного слоя класса. */
|
|
199
|
+
function ownTypes(own) {
|
|
200
|
+
const raw = own?.types ?? own?.TYPES ?? own?.METADATA?.types ?? own?.METADATA?.TYPES;
|
|
201
|
+
return Array.isArray(raw) ? raw.map(x => String(x || '').trim()).filter(Boolean) : [];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Нормализовать поле (рекурсивно для структур $struct и табличных полей table: true).
|
|
206
|
+
*/
|
|
207
|
+
function normalizeField(db, f, basePath = '', seenStructs = new Set()) {
|
|
208
|
+
const rawType = String(f.type ?? '').trim();
|
|
209
|
+
const normType = fieldType(f.type);
|
|
210
|
+
const isTable = f.table === true || normType === 'table';
|
|
211
|
+
const pathFromType = rawType.startsWith('/') ? rawType : '';
|
|
212
|
+
const rawRef = String(f.struct ?? f.target ?? f.catalog ?? pathFromType).trim();
|
|
213
|
+
const refPath = rawRef ? resolveInBase(db, basePath, rawRef) : '';
|
|
214
|
+
const refCls = (db && refPath) ? getClass(db, refPath) : null;
|
|
215
|
+
const isStruct = refCls?.type === '$struct' || normType === 'struct' || Boolean(f.struct);
|
|
216
|
+
|
|
217
|
+
if (isStruct) {
|
|
218
|
+
let baseFields = [];
|
|
219
|
+
if (refCls?.type === '$struct' && !seenStructs.has(refCls.cid)) {
|
|
220
|
+
const nextSeen = new Set(seenStructs);
|
|
221
|
+
nextSeen.add(refCls.cid);
|
|
222
|
+
const structSchema = classSchema(db, refCls.cid, nextSeen);
|
|
223
|
+
baseFields = (structSchema?.fields || []).map(clone);
|
|
224
|
+
}
|
|
225
|
+
const localRaw = (Array.isArray(f.FIELDS) ? f.FIELDS : Array.isArray(f.fields) ? f.fields : []).filter(x => x && x.id);
|
|
226
|
+
const merged = localRaw.length
|
|
227
|
+
? mergeById(baseFields, localRaw, false, mergeField).map(x => normalizeField(db, { ...x, id: String(x.id) }, basePath, seenStructs))
|
|
228
|
+
: baseFields;
|
|
229
|
+
const out = {
|
|
230
|
+
...f,
|
|
231
|
+
type: isTable ? 'table' : 'struct',
|
|
232
|
+
table: isTable,
|
|
233
|
+
fields: merged,
|
|
234
|
+
};
|
|
235
|
+
if (refCls)
|
|
236
|
+
out.struct = refCls.cid;
|
|
237
|
+
else if (refPath)
|
|
238
|
+
out.struct = refPath;
|
|
239
|
+
delete out.FIELDS;
|
|
240
|
+
delete out.catalog;
|
|
241
|
+
delete out.target;
|
|
242
|
+
out.label ??= f.id;
|
|
243
|
+
return out;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (isTable) {
|
|
247
|
+
const sub = (Array.isArray(f.FIELDS) ? f.FIELDS : Array.isArray(f.fields) ? f.fields : [])
|
|
248
|
+
.filter(x => x && x.id).map(x => normalizeField(db, { ...x, id: String(x.id) }, basePath, seenStructs));
|
|
249
|
+
const out = { ...f, type: 'table', table: true, fields: sub };
|
|
250
|
+
delete out.FIELDS;
|
|
251
|
+
out.label ??= f.id;
|
|
252
|
+
return out;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (normType === 'link' || pathFromType) {
|
|
256
|
+
const out = { ...f, type: 'link', target: refCls ? refCls.cid : refPath };
|
|
257
|
+
delete out.catalog;
|
|
258
|
+
delete out.FIELDS;
|
|
259
|
+
out.label ??= f.id;
|
|
260
|
+
return out;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const out = { ...f, type: normType || 'string' };
|
|
264
|
+
if (!normType)
|
|
265
|
+
out.unknownType = String(f.type);
|
|
266
|
+
delete out.FIELDS;
|
|
267
|
+
out.label ??= f.id;
|
|
268
|
+
return out;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const cache = new WeakMap();
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Схема класса по трёхосевому наследованию: Types → Ancestor → Parent → Сам класс.
|
|
275
|
+
* @param {import('node:sqlite').DatabaseSync} db
|
|
276
|
+
* @param {string} ref путь класса
|
|
277
|
+
* @param {Set<string>} [seenStructs] защита от циклов вложенных структур
|
|
278
|
+
* @returns {{cid, path, type, family, label, leaf, ancestor: string|null, types: string[],
|
|
279
|
+
* fields: object[], postings: object[], overlay: object[], autoRegister: boolean,
|
|
280
|
+
* source: object|null, static: object[]} | null}
|
|
281
|
+
*/
|
|
282
|
+
export function classSchema(db, ref, seenStructs = new Set()) {
|
|
283
|
+
const cls = getClass(db, ref);
|
|
284
|
+
if (!cls)
|
|
285
|
+
return null;
|
|
286
|
+
let byDb = cache.get(db);
|
|
287
|
+
if (!byDb || byDb.gen !== classesGeneration(db)) {
|
|
288
|
+
byDb = { gen: classesGeneration(db), map: new Map() };
|
|
289
|
+
cache.set(db, byDb);
|
|
290
|
+
}
|
|
291
|
+
const hit = byDb.map.get(cls.cid);
|
|
292
|
+
if (hit)
|
|
293
|
+
return hit;
|
|
294
|
+
|
|
295
|
+
const family = FAMILY[cls.type] || 'other';
|
|
296
|
+
const basePath = classBasePath(db, cls.cid);
|
|
297
|
+
const parentChain = sameFamilyChain(db, cls); // [P_0, ..., cls]
|
|
298
|
+
|
|
299
|
+
// Находим глубочайший класс в цепочке Parent, у которого есть Ancestor в корне/модуле
|
|
300
|
+
let matchedAnc = null;
|
|
301
|
+
let matchedAtSelf = false;
|
|
302
|
+
for (let i = parentChain.length - 1; i >= 0; i--) {
|
|
303
|
+
const a = findAncestor(db, parentChain[i]);
|
|
304
|
+
if (a) {
|
|
305
|
+
matchedAnc = a;
|
|
306
|
+
matchedAtSelf = (i === parentChain.length - 1);
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
const ancChain = matchedAnc ? sameFamilyChain(db, matchedAnc) : [];
|
|
311
|
+
|
|
312
|
+
// Собираем все ссылки Types (миксины) из Ancestor-цепочки и Parent-цепочки
|
|
313
|
+
const typeRefs = [];
|
|
314
|
+
for (const l of [...ancChain, ...parentChain]) {
|
|
315
|
+
for (const t of ownTypes(l.own)) {
|
|
316
|
+
const resolved = resolveInBase(db, basePath, t);
|
|
317
|
+
if (!typeRefs.includes(resolved) && resolved !== cls.cid)
|
|
318
|
+
typeRefs.push(resolved);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// 1. Types (миксины) → 2. Ancestor → 3. Parent → 4. Сам класс
|
|
323
|
+
const sequence = [];
|
|
324
|
+
const visitedTypeCids = new Set([cls.cid]);
|
|
325
|
+
for (const tRef of typeRefs) {
|
|
326
|
+
const tCls = getClass(db, tRef);
|
|
327
|
+
if (!tCls || visitedTypeCids.has(tCls.cid))
|
|
328
|
+
continue;
|
|
329
|
+
visitedTypeCids.add(tCls.cid);
|
|
330
|
+
const tChain = sameFamilyChain(db, tCls);
|
|
331
|
+
tChain.forEach((tl, idx) => {
|
|
332
|
+
sequence.push({ layer: tl, inherited: idx < tChain.length - 1 });
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
ancChain.forEach((al, idx) => {
|
|
336
|
+
const isDirectAncestorOfSelf = matchedAtSelf && idx === ancChain.length - 1;
|
|
337
|
+
sequence.push({ layer: al, inherited: !isDirectAncestorOfSelf });
|
|
338
|
+
});
|
|
339
|
+
parentChain.slice(0, -1).forEach(pl => {
|
|
340
|
+
sequence.push({ layer: pl, inherited: true });
|
|
341
|
+
});
|
|
342
|
+
sequence.push({ layer: cls, inherited: false });
|
|
343
|
+
|
|
344
|
+
const NUM_FIELD_CANDIDATES = ['num', 'doc_number', 'order_number', 'number', 'doc_num'];
|
|
345
|
+
let fields = [], postings = [], overlay = [], staticFields = [];
|
|
346
|
+
let autoRegister = false, source = null, norms = null, numerator = null;
|
|
347
|
+
for (const { layer, inherited } of sequence) {
|
|
348
|
+
const own = layer.own || {};
|
|
349
|
+
const meta = own.METADATA || {};
|
|
350
|
+
fields = mergeById(fields, meta.FIELDS, inherited, mergeField);
|
|
351
|
+
postings = mergeById(postings, meta.POSTINGS, inherited, mergeRule);
|
|
352
|
+
overlay = mergeById(overlay, own.OVERLAY ?? meta.OVERLAY, inherited, mergeField);
|
|
353
|
+
if (!inherited && Array.isArray(meta.STATIC))
|
|
354
|
+
staticFields = mergeById(staticFields, meta.STATIC, false, mergeField);
|
|
355
|
+
if (typeof own.AUTO_REGISTER === 'boolean')
|
|
356
|
+
autoRegister = own.AUTO_REGISTER;
|
|
357
|
+
if (own.SOURCE && typeof own.SOURCE === 'object')
|
|
358
|
+
source = { ...(source || {}), ...clone(own.SOURCE) };
|
|
359
|
+
if (own.NORMS !== undefined)
|
|
360
|
+
norms = clone(own.NORMS);
|
|
361
|
+
const rawNum = own.NUMERATOR ?? meta.NUMERATOR;
|
|
362
|
+
if (rawNum === false)
|
|
363
|
+
numerator = { enabled: false };
|
|
364
|
+
else if (rawNum && typeof rawNum === 'object')
|
|
365
|
+
numerator = { ...(numerator || {}), ...clone(rawNum) };
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
let resolvedNumerator = null;
|
|
369
|
+
if (family === 'operation') {
|
|
370
|
+
const baseNum = numerator || { enabled: true, reset: 'year', pad: 0, prefix: '' };
|
|
371
|
+
if (baseNum.enabled !== false) {
|
|
372
|
+
let numFieldId = baseNum.field || '';
|
|
373
|
+
if (!numFieldId || !fields.some(f => f.id === numFieldId)) {
|
|
374
|
+
const existing = fields.find(f => NUM_FIELD_CANDIDATES.includes(f.id));
|
|
375
|
+
numFieldId = existing ? existing.id : '';
|
|
376
|
+
}
|
|
377
|
+
if (!numFieldId && numerator && baseNum.autoField !== false) {
|
|
378
|
+
numFieldId = baseNum.field || 'num';
|
|
379
|
+
fields.unshift({ id: numFieldId, label: baseNum.label || 'Номер', type: 'string' });
|
|
380
|
+
}
|
|
381
|
+
if (numFieldId) {
|
|
382
|
+
resolvedNumerator = {
|
|
383
|
+
enabled: true,
|
|
384
|
+
field: numFieldId,
|
|
385
|
+
reset: ['year', 'quarter', 'month', 'day', 'never'].includes(baseNum.reset) ? baseNum.reset : 'year',
|
|
386
|
+
pad: Math.max(0, Number(baseNum.pad) || 0),
|
|
387
|
+
prefix: String(baseNum.prefix || ''),
|
|
388
|
+
scope: baseNum.scope === 'root' ? 'root' : 'class',
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
resolvedNumerator = { enabled: false };
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Если виртуальный справочник вложен в реальный справочник ($catalog) и не задал SOURCE.registry явно —
|
|
398
|
+
// его реестром-источником по умолчанию служит ближайший родительский справочник.
|
|
399
|
+
if (cls.type === '$virtual' && source && !source.registry) {
|
|
400
|
+
for (let i = parentChain.length - 2; i >= 0; i--) {
|
|
401
|
+
if (parentChain[i].type === '$catalog' && parentChain[i].parent) {
|
|
402
|
+
source.registry = parentChain[i].cid;
|
|
403
|
+
break;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Привязываем ссылки на счета и источники виртуального справочника к контексту $base
|
|
409
|
+
if (basePath) {
|
|
410
|
+
postings = postings.map(r => ({
|
|
411
|
+
...r,
|
|
412
|
+
debit: r.debit?.account ? { ...r.debit, account: resolveInBase(db, basePath, r.debit.account) } : r.debit,
|
|
413
|
+
credit: r.credit?.account ? { ...r.credit, account: resolveInBase(db, basePath, r.credit.account) } : r.credit,
|
|
414
|
+
}));
|
|
415
|
+
if (source) {
|
|
416
|
+
if (source.registry)
|
|
417
|
+
source.registry = resolveInBase(db, basePath, source.registry);
|
|
418
|
+
if (source.account)
|
|
419
|
+
source.account = resolveInBase(db, basePath, source.account);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const directAnc = findAncestor(db, cls);
|
|
424
|
+
// Класс является листом, если у него нет дочерних классов в текущем дереве И у его прямого Ancestor нет дочерних классов
|
|
425
|
+
const leaf = isLeaf(db, cls.cid) && (!directAnc || isLeaf(db, directAnc.cid));
|
|
426
|
+
|
|
427
|
+
const schema = {
|
|
428
|
+
cid: cls.cid,
|
|
429
|
+
path: cls.path,
|
|
430
|
+
type: cls.type,
|
|
431
|
+
family,
|
|
432
|
+
label: cls.label,
|
|
433
|
+
leaf,
|
|
434
|
+
ancestor: directAnc ? directAnc.cid : null,
|
|
435
|
+
types: typeRefs,
|
|
436
|
+
fields: fields.map(f => normalizeField(db, f, basePath, seenStructs)),
|
|
437
|
+
postings,
|
|
438
|
+
overlay: overlay.map(f => normalizeField(db, f, basePath, seenStructs)),
|
|
439
|
+
autoRegister,
|
|
440
|
+
numerator: resolvedNumerator,
|
|
441
|
+
source,
|
|
442
|
+
norms,
|
|
443
|
+
static: staticFields,
|
|
444
|
+
};
|
|
445
|
+
byDb.map.set(cls.cid, schema);
|
|
446
|
+
return schema;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** Поля-аналитики счёта (Link с analytic: true). */
|
|
450
|
+
export function analyticSlots(schema) {
|
|
451
|
+
return schema.fields.filter(f => f.analytic && f.type === 'link');
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/** Заголовок объекта по схеме: поле name, иначе первое строковое. */
|
|
455
|
+
export function objectTitle(schema, body = {}) {
|
|
456
|
+
if (body.name)
|
|
457
|
+
return String(body.name);
|
|
458
|
+
const f = schema?.fields.find(x => (x.type === 'string' || x.type === 'text') && body[x.id]);
|
|
459
|
+
return f ? String(body[f.id]) : '';
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Проверка схемы класса. Для листа операций — полнота правил проводок:
|
|
464
|
+
* оба счёта существуют, это листья-счета, выражения разбираются и ссылаются
|
|
465
|
+
* на известные поля, все аналитики счетов сопоставлены.
|
|
466
|
+
* @returns {string[]} ошибки (пусто — схема корректна)
|
|
467
|
+
*/
|
|
468
|
+
export function checkSchema(db, ref) {
|
|
469
|
+
const s = classSchema(db, ref);
|
|
470
|
+
if (!s)
|
|
471
|
+
return [`класс ${ref} не найден`];
|
|
472
|
+
const errors = [];
|
|
473
|
+
const at = `${s.path}`;
|
|
474
|
+
for (const tRef of s.types || []) {
|
|
475
|
+
if (!getClass(db, tRef))
|
|
476
|
+
errors.push(`${at}: тип-миксин (types) ${tRef} не найден`);
|
|
477
|
+
}
|
|
478
|
+
const checkFields = (list, where) => {
|
|
479
|
+
const ids = new Set();
|
|
480
|
+
for (const f of list) {
|
|
481
|
+
if (ids.has(f.id))
|
|
482
|
+
errors.push(`${at}: поле «${where}${f.id}» объявлено дважды`);
|
|
483
|
+
ids.add(f.id);
|
|
484
|
+
if (f.unknownType)
|
|
485
|
+
errors.push(`${at}: поле «${where}${f.id}» — неизвестный тип «${f.unknownType}»`);
|
|
486
|
+
if (f.type === 'link') {
|
|
487
|
+
if (!f.target)
|
|
488
|
+
errors.push(`${at}: поле-ссылка «${where}${f.id}» без целевого класса (target)`);
|
|
489
|
+
else if (!getClass(db, f.target))
|
|
490
|
+
errors.push(`${at}: поле-ссылка «${where}${f.id}» — класс ${f.target} не найден`);
|
|
491
|
+
}
|
|
492
|
+
if (f.type === 'struct' || f.type === 'table') {
|
|
493
|
+
if (f.struct) {
|
|
494
|
+
const sc = getClass(db, f.struct);
|
|
495
|
+
if (!sc)
|
|
496
|
+
errors.push(`${at}: поле «${where}${f.id}» — структура ${f.struct} не найдена`);
|
|
497
|
+
else if (sc.type !== '$struct')
|
|
498
|
+
errors.push(`${at}: поле «${where}${f.id}» — класс ${f.struct} не является структурой ($struct)`);
|
|
499
|
+
}
|
|
500
|
+
if (!f.fields.length)
|
|
501
|
+
errors.push(`${at}: ${f.type === 'table' ? 'табличная часть' : 'структура'} «${where}${f.id}» без полей`);
|
|
502
|
+
checkFields(f.fields, `${where}${f.id}.`);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
checkFields(s.fields, '');
|
|
507
|
+
checkFields(s.overlay, 'overlay.');
|
|
508
|
+
|
|
509
|
+
if (s.family === 'operation' && s.leaf)
|
|
510
|
+
for (const rule of s.postings)
|
|
511
|
+
errors.push(...checkRule(db, s, rule));
|
|
512
|
+
return errors;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function checkRule(db, s, rule) {
|
|
516
|
+
const errors = [];
|
|
517
|
+
const at = `${s.path}: правило «${rule.id}»`;
|
|
518
|
+
const top = new Set(s.fields.map(f => f.id));
|
|
519
|
+
let rowFields = null;
|
|
520
|
+
if (rule.each) {
|
|
521
|
+
const table = s.fields.find(f => f.id === rule.each);
|
|
522
|
+
if (!table || table.type !== 'table')
|
|
523
|
+
errors.push(`${at}: each — «${rule.each}» не табличная часть`);
|
|
524
|
+
else
|
|
525
|
+
rowFields = new Set(table.fields.map(f => f.id));
|
|
526
|
+
}
|
|
527
|
+
const known = new Set([...BUILTIN_FUNCTIONS, ...RULE_FUNCTIONS]);
|
|
528
|
+
const checkExpr = (src, what) => {
|
|
529
|
+
let c;
|
|
530
|
+
try {
|
|
531
|
+
c = compile(src);
|
|
532
|
+
}
|
|
533
|
+
catch (e) {
|
|
534
|
+
errors.push(`${at}: ${what} — ${e.message}`);
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
for (const name of c.names) {
|
|
538
|
+
if (name === 'row' && rowFields)
|
|
539
|
+
continue;
|
|
540
|
+
if (!top.has(name))
|
|
541
|
+
errors.push(`${at}: ${what} — неизвестное поле «${name}»`);
|
|
542
|
+
}
|
|
543
|
+
for (const fn of c.calls) {
|
|
544
|
+
if (!known.has(fn))
|
|
545
|
+
errors.push(`${at}: ${what} — неизвестная функция ${fn}()`);
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
if (rule.amount === undefined && rule.qty === undefined)
|
|
550
|
+
errors.push(`${at}: нет ни суммы (amount), ни количества (qty)`);
|
|
551
|
+
for (const key of ['amount', 'qty', 'qty2', 'unit', 'item', 'when']) {
|
|
552
|
+
if (rule[key] !== undefined && rule[key] !== '')
|
|
553
|
+
checkExpr(rule[key], key);
|
|
554
|
+
}
|
|
555
|
+
if (rule.unit && !rule.item)
|
|
556
|
+
errors.push(`${at}: единица (unit) без номенклатуры (item) — коэффициент к базовой единице не определить`);
|
|
557
|
+
for (const side of ['debit', 'credit']) {
|
|
558
|
+
const acc = rule[side]?.account;
|
|
559
|
+
const name = side === 'debit' ? 'дебет' : 'кредит';
|
|
560
|
+
if (!acc) {
|
|
561
|
+
errors.push(`${at}: не задан счёт (${name})`);
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
const accSchema = classSchema(db, acc);
|
|
565
|
+
if (!accSchema) {
|
|
566
|
+
errors.push(`${at}: счёт ${acc} (${name}) не найден`);
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
if (accSchema.type !== '$account')
|
|
570
|
+
errors.push(`${at}: ${acc} (${name}) — не счёт`);
|
|
571
|
+
else if (!accSchema.leaf)
|
|
572
|
+
errors.push(`${at}: ${acc} (${name}) — не лист, проводка возможна только на субсчёт`);
|
|
573
|
+
const map = rule[side].analytics || {};
|
|
574
|
+
for (const slot of analyticSlots(accSchema)) {
|
|
575
|
+
if (map[slot.id] === undefined || map[slot.id] === '')
|
|
576
|
+
errors.push(`${at}: аналитика «${slot.id}» счёта ${accSchema.label} (${name}) не сопоставлена`);
|
|
577
|
+
else
|
|
578
|
+
checkExpr(map[slot.id], `аналитика ${name} «${slot.id}»`);
|
|
579
|
+
}
|
|
580
|
+
for (const key of Object.keys(map)) {
|
|
581
|
+
if (!analyticSlots(accSchema).some(f => f.id === key))
|
|
582
|
+
errors.push(`${at}: у счёта ${accSchema.label} нет аналитики «${key}» (${name})`);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
return errors;
|
|
586
|
+
}
|