biz-a-cli 2.3.80-15419 → 2.3.80

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.
Files changed (80) hide show
  1. package/bin/app.js +20 -255
  2. package/bin/hub.js +0 -16
  3. package/bin/hubEvent.js +5 -98
  4. package/bin/tunnelTarget.js +0 -4
  5. package/engine/bpm/workflow.js +6 -7
  6. package/engine/cc/configValidate.js +0 -53
  7. package/engine/cc/guides/V2-SCREEN-AUTHORING.md +512 -531
  8. package/engine/cc/layer3Behavior.js +1 -2
  9. package/engine/cc/layer3Schema.js +1 -3
  10. package/engine/cc/livePipelines.js +3 -9
  11. package/engine/cc/publish.js +105 -86
  12. package/engine/domain/appConfig.js +18 -108
  13. package/engine/domain/applicationConfig.js +12 -87
  14. package/engine/domain/dialect.js +0 -138
  15. package/engine/domain/domainNavigation.js +50 -86
  16. package/engine/domain/entityConfigLoader.js +4 -16
  17. package/engine/domain/numericScale.js +0 -2
  18. package/engine/domain/system.js +0 -2
  19. package/engine/ext/publicTheme.js +1 -8
  20. package/engine/orm/apiRoute.js +61 -187
  21. package/engine/orm/entityConfig.js +2 -32
  22. package/engine/orm/finaAuth.js +0 -0
  23. package/engine/orm/finaEndpoint.js +0 -19
  24. package/engine/orm/imgQuery.js +0 -92
  25. package/engine/orm/listQuery.js +1 -6
  26. package/package.json +1 -1
  27. package/worker/cliScriptWorker.js +0 -5
  28. package/engine/attachments/store.js +0 -195
  29. package/engine/domain/domainRegistration.js +0 -65
  30. package/engine/fina/approval.js +0 -141
  31. package/engine/fina/approvalService.js +0 -59
  32. package/engine/fina/attachments.js +0 -165
  33. package/engine/fina/client.js +0 -84
  34. package/engine/fina/config.js +0 -26
  35. package/engine/fina/context.js +0 -58
  36. package/engine/fina/document.js +0 -67
  37. package/engine/fina/documentWrite.js +0 -173
  38. package/engine/fina/documents/index.js +0 -28
  39. package/engine/fina/documents/jv.js +0 -105
  40. package/engine/fina/documents/jvWrite.js +0 -260
  41. package/engine/fina/documents/si.js +0 -40
  42. package/engine/fina/errors.js +0 -17
  43. package/engine/fina/features.js +0 -86
  44. package/engine/fina/guard.js +0 -102
  45. package/engine/fina/identity.js +0 -87
  46. package/engine/fina/live.js +0 -19
  47. package/engine/fina/lock.js +0 -35
  48. package/engine/fina/master.js +0 -331
  49. package/engine/fina/masters/coa.js +0 -104
  50. package/engine/fina/masters/currency.js +0 -39
  51. package/engine/fina/masters/department.js +0 -32
  52. package/engine/fina/masters/index.js +0 -21
  53. package/engine/fina/masters/project.js +0 -53
  54. package/engine/fina/period.js +0 -56
  55. package/engine/fina/print.js +0 -136
  56. package/engine/fina/readPolicy.js +0 -67
  57. package/engine/fina/recurring.js +0 -200
  58. package/engine/fina/route.js +0 -61
  59. package/engine/fina/routes/attachment.js +0 -41
  60. package/engine/fina/routes/document.js +0 -66
  61. package/engine/fina/routes/master.js +0 -26
  62. package/engine/fina/routes/period.js +0 -24
  63. package/engine/fina/routes/print.js +0 -19
  64. package/engine/fina/routes/recurring.js +0 -27
  65. package/engine/fina/routes/session.js +0 -23
  66. package/engine/fina/routes/setup.js +0 -58
  67. package/engine/fina/routes/submission.js +0 -46
  68. package/engine/fina/routes/template.js +0 -31
  69. package/engine/fina/settings.js +0 -159
  70. package/engine/fina/sql.js +0 -34
  71. package/engine/fina/submission.js +0 -290
  72. package/engine/fina/submissionCompare.js +0 -171
  73. package/engine/fina/submissionRepo.js +0 -114
  74. package/engine/fina/template.js +0 -118
  75. package/engine/fina/userMap.js +0 -79
  76. package/engine/orm/companyLocale.js +0 -74
  77. package/engine/orm/userApiDataset.js +0 -153
  78. package/migrations/postgres/1790600000000__fina.sql +0 -33
  79. package/migrations/postgres/1790700000000__attachment.sql +0 -29
  80. package/migrations/postgres/1790800000000__fina_jv_submission.sql +0 -41
@@ -1,331 +0,0 @@
1
- /*
2
- * Doc 8 M3 — FINA's masters, read and written for a VERIFIED user (E-04, E-05, S-02, S-03).
3
- *
4
- * ONE generic pipeline (E-02): a master's table, key, columns, hierarchy and rules come from its server-owned
5
- * descriptor (masters/*.js); nothing here knows a master by name.
6
- *
7
- * save: identity → right (create / edit, D4) → the values (types, sizes, required, G-07) → the code is unique (G-02,
8
- * L-13) → the hierarchy (parent exists, is active, is not a descendant; the derived columns) → the master's
9
- * rules (E-09) → FINA's dataset class through json2fina, every column sent → descendants re-levelled when the
10
- * record moved.
11
- * remove: identity → right (edit, D4) → no sub-records → nothing references it → json2fina delete.
12
- *
13
- * ⚠️ "IN USE" IS READ FROM FINA'S OWN FOREIGN KEYS: whatever references the record (GLHIST, JVDET, DEFACCNT, ITEM, a
14
- * budget, the company's default currency…). One statement asks every referencing table at once. A SET NULL reference
15
- * does not count (the database lets the delete through), nor what the master names as cleaned up by FINA itself.
16
- *
17
- * ⚠️ REFUSED, NOT DONE, in M3 (Agung 2026-10-09): picking a parent that is already in use (the desktop's
18
- * "becomes-a-parent" rename), and a delete the desktop follows with clean-up of other rows — both say what is in the
19
- * way and to use FINA desktop.
20
- *
21
- * Every value reaching a statement goes through sql.js; table and column names come from the descriptor or FINA's
22
- * catalogue, never from the request.
23
- */
24
- import { finaError } from "./errors.js";
25
- import { requireRight } from "./identity.js";
26
- import { masterOf } from "./masters/index.js";
27
- import currencyMaster from "./masters/currency.js";
28
- import { sqlString, sqlValue } from "./sql.js";
29
-
30
- const IDENT = /^[A-Z0-9_$]+$/;
31
- const MAX_BLOCK_CHARS = 30000;
32
-
33
- const blank = (v) => v === null || v === undefined || (typeof v === "string" && v.trim() === "");
34
- const same = (a, b) => !blank(a) && !blank(b) && String(a).trim() === String(b).trim();
35
- const pick = (data, column) => {
36
- if (!data || typeof data !== "object") return undefined;
37
- if (column in data) return data[column];
38
- return data[column.toLowerCase()];
39
- };
40
-
41
- /* FINA's foreign keys onto a table, cached per FINA database: the catalogue changes with FINA upgrades, not requests. */
42
- const referenceCache = new Map();
43
- const fkSql = (table) =>
44
- "SELECT TRIM(RC.RDB$RELATION_NAME) AS T, TRIM(S.RDB$FIELD_NAME) AS C, TRIM(RF.RDB$DELETE_RULE) AS D " +
45
- "FROM RDB$RELATION_CONSTRAINTS RC JOIN RDB$REF_CONSTRAINTS RF ON RF.RDB$CONSTRAINT_NAME = RC.RDB$CONSTRAINT_NAME " +
46
- "JOIN RDB$RELATION_CONSTRAINTS PK ON PK.RDB$CONSTRAINT_NAME = RF.RDB$CONST_NAME_UQ " +
47
- "JOIN RDB$INDEX_SEGMENTS S ON S.RDB$INDEX_NAME = RC.RDB$INDEX_NAME " +
48
- `WHERE RC.RDB$CONSTRAINT_TYPE = 'FOREIGN KEY' AND PK.RDB$RELATION_NAME = ${sqlString(table)}`;
49
-
50
- export const resetReferenceCache = () => referenceCache.clear();
51
-
52
- /* The values a save sends: typed, trimmed, checked — every problem named at once (INVALID). */
53
- const normalize = (master, data) => {
54
- const values = {};
55
- const problems = [];
56
- for (const [column, def] of Object.entries(master.columns)) {
57
- let v = pick(data, column);
58
- switch (def.type) {
59
- case "string":
60
- v = blank(v) ? null : String(v).trim();
61
- if (v !== null && def.size && v.length > def.size) problems.push(`${column} is longer than ${def.size} characters`);
62
- /* G-07: FINA's string validator refuses an apostrophe and DEL. */
63
- if (v !== null && /['\u007f]/.test(v)) problems.push(`${column} may not contain an apostrophe (')`);
64
- break;
65
- case "integer":
66
- if (blank(v)) v = null;
67
- else if (!Number.isInteger(Number(v))) problems.push(`${column} must be a whole number`);
68
- else v = Number(v);
69
- if (v !== null && def.oneOf && !def.oneOf.includes(v)) problems.push(`${column} must be one of ${def.oneOf.join(", ")}`);
70
- break;
71
- case "flag":
72
- v = blank(v) ? (def.default ?? 0) : v === true || v === 1 || ["1", "true"].includes(String(v).trim().toLowerCase()) ? 1 : 0;
73
- break;
74
- case "decimal4": {
75
- if (blank(v)) {
76
- v = def.default ?? null;
77
- break;
78
- }
79
- const text = String(v).trim();
80
- if (!/^-?\d+(\.\d{1,4})?$/.test(text)) problems.push(`${column} must be a number with at most 4 decimals`);
81
- else v = Number(text);
82
- break;
83
- }
84
- case "date":
85
- if (blank(v)) v = null;
86
- else if (!/^\d{4}-\d{2}-\d{2}$/.test(String(v).slice(0, 10))) problems.push(`${column} must be a date (YYYY-MM-DD)`);
87
- else v = String(v).slice(0, 10);
88
- break;
89
- default:
90
- throw new Error(`master ${master.name}: unknown column type ${def.type}`);
91
- }
92
- if (def.required && (v === null || v === undefined)) problems.push(`${column} is required`);
93
- values[column] = v ?? null;
94
- }
95
- if (problems.length > 0) throw finaError("INVALID", `${problems.join("; ")}.`);
96
- return values;
97
- };
98
-
99
- /*
100
- * A record as the forms need it: every number column a number. ⚠️ FINA's ExecuteSQL answers a SMALLINT as TEXT
101
- * ("19", "0") while an INTEGER comes as a number (found live, M3) — a form select of numbers then matched nothing.
102
- */
103
- const typedRow = (master, row) => {
104
- if (!row) return null;
105
- const numeric = new Set(
106
- [
107
- ...Object.entries(master.columns)
108
- .filter(([, def]) => ["integer", "flag", "decimal4"].includes(def.type))
109
- .map(([column]) => column),
110
- ...(master.keyType === "integer" ? [master.key] : []),
111
- ...(master.hierarchy ? [master.hierarchy.level, master.hierarchy.isChild, ...(master.keyType === "integer" ? [master.hierarchy.root] : [])] : []),
112
- ].map((c) => c.toLowerCase()),
113
- );
114
- return Object.fromEntries(
115
- Object.entries(row).map(([k, v]) => [k, numeric.has(k) && !blank(v) && Number.isFinite(Number(v)) ? Number(v) : v]),
116
- );
117
- };
118
-
119
- export const createMasterService = ({ client, identity }) => {
120
- const readRow = async (master, key) => {
121
- const columns = [...new Set([master.key, ...Object.keys(master.columns), ...(master.hierarchy ? [master.hierarchy.level, master.hierarchy.root, master.hierarchy.isChild] : [])])];
122
- const rows = await client.executeSql(`SELECT ${columns.join(", ")} FROM ${master.table} WHERE ${master.key} = ${sqlValue(key, master.keyType)}`);
123
- return typedRow(master, rows?.[0] ?? null);
124
- };
125
-
126
- /* Every record's key, parent, level, root and suspended flag: the hierarchy checks and the re-levelling. */
127
- const readTree = async (master) => {
128
- const h = master.hierarchy;
129
- const rows = await client.executeSql(`SELECT ${master.key} AS K, ${h.parent} AS P, ${h.level} AS L, ${h.root} AS R, SUSPENDED AS S FROM ${master.table}`);
130
- return (rows ?? []).map((r) => ({ k: r.k, p: r.p, l: Number(r.l ?? 0), r: r.r, s: Number(r.s ?? 0) }));
131
- };
132
-
133
- const references = async (table) => {
134
- const cacheKey = `${client.dbIndex ?? ""}:${table}`;
135
- if (!referenceCache.has(cacheKey)) {
136
- const loading = client
137
- .executeSql(fkSql(table))
138
- .then((rows) => (rows ?? []).map((r) => ({ table: String(r.t ?? "").trim(), column: String(r.c ?? "").trim(), rule: String(r.d ?? "").trim().toUpperCase() })));
139
- loading.catch(() => referenceCache.delete(cacheKey));
140
- referenceCache.set(cacheKey, loading);
141
- }
142
- return referenceCache.get(cacheKey);
143
- };
144
-
145
- /* The tables that reference this record (blocking references only), in one statement. */
146
- const usedIn = async (master, key) => {
147
- const refs = (await references(master.table)).filter(
148
- (r) =>
149
- r.table !== master.table &&
150
- IDENT.test(r.table) &&
151
- IDENT.test(r.column) &&
152
- r.rule !== "SET NULL" &&
153
- !(master.ignoreReferences ?? []).includes(r.table),
154
- );
155
- if (refs.length === 0) return [];
156
- const literal = sqlValue(key, master.keyType);
157
- const select = refs.map((r, i) => `(SELECT FIRST 1 1 FROM ${r.table} WHERE ${r.column} = ${literal}) AS U${i}`).join(", ");
158
- const [row] = (await client.executeSql(`SELECT ${select} FROM RDB$DATABASE`)) ?? [];
159
- return [...new Set(refs.filter((_r, i) => row?.[`u${i}`] !== null && row?.[`u${i}`] !== undefined).map((r) => r.table))];
160
- };
161
-
162
- const caller = async (principal, master, action) => requireRight(await identity.resolve(principal), master.rights, action);
163
-
164
- const assertUniqueCode = async (master, values, selfKey) => {
165
- const code = values[master.code];
166
- const others = selfKey === null ? "" : ` AND ${master.key} <> ${sqlValue(selfKey, master.keyType)}`;
167
- const rows = await client.executeSql(`SELECT FIRST 1 ${master.key} AS K FROM ${master.table} WHERE UPPER(${master.code}) = UPPER(${sqlString(code)})${others}`);
168
- if (rows?.length) throw finaError("MASTER_DUPLICATE", `${master.code} ${code} already exists: another ${master.label} uses it.`);
169
- };
170
-
171
- const isDescendant = (tree, candidate, ancestor) => {
172
- const byKey = new Map(tree.map((r) => [String(r.k), r]));
173
- const seen = new Set();
174
- for (let at = byKey.get(String(candidate)); at && !seen.has(String(at.k)); at = byKey.get(String(at.p))) {
175
- if (same(at.k, ancestor)) return true;
176
- seen.add(String(at.k));
177
- }
178
- return false;
179
- };
180
-
181
- /* COA-19 / DEP-11 / PRJ-14: after a record moves, its descendants take their level and root from it. One block. */
182
- const relevelDescendants = async (master, key) => {
183
- const h = master.hierarchy;
184
- const tree = await readTree(master);
185
- const byKey = new Map(tree.map((r) => [String(r.k), r]));
186
- const updates = [];
187
- const queue = [String(key)];
188
- const seen = new Set();
189
- while (queue.length) {
190
- const at = byKey.get(queue.shift());
191
- if (!at || seen.has(String(at.k))) continue;
192
- seen.add(String(at.k));
193
- for (const child of tree.filter((r) => same(r.p, at.k))) {
194
- const level = at.l + 1;
195
- const root = blank(at.r) ? at.k : at.r;
196
- if (child.l !== level || !same(child.r, root)) {
197
- updates.push(`UPDATE ${master.table} SET ${h.level} = ${level}, ${h.root} = ${sqlValue(root, master.keyType)} WHERE ${master.key} = ${sqlValue(child.k, master.keyType)};`);
198
- child.l = level;
199
- child.r = root;
200
- }
201
- queue.push(String(child.k));
202
- }
203
- }
204
- let chunk = [];
205
- const flush = async () => {
206
- if (chunk.length) await client.block(`EXECUTE BLOCK AS BEGIN ${chunk.join(" ")} END`);
207
- chunk = [];
208
- };
209
- for (const update of updates) {
210
- if ([...chunk, update].join(" ").length > MAX_BLOCK_CHARS) await flush();
211
- chunk.push(update);
212
- }
213
- await flush();
214
- };
215
-
216
- const get = async (principal, name, key) => {
217
- const master = masterOf(name);
218
- await caller(principal, master, null);
219
- const row = await readRow(master, key);
220
- if (!row) throw finaError("MASTER_NOT_FOUND", `${master.label} ${key} does not exist in FINA.`);
221
- return row;
222
- };
223
-
224
- const save = async (principal, name, { key = null, data = {} } = {}) => {
225
- const master = masterOf(name);
226
- const isNew = blank(key);
227
- const who = await caller(principal, master, isNew ? "create" : "edit");
228
-
229
- const existing = isNew ? null : await readRow(master, key);
230
- if (!isNew && !existing) throw finaError("MASTER_NOT_FOUND", `${master.label} ${key} does not exist in FINA.`);
231
- /* An edit keeps what the request leaves out. */
232
- const merged = isNew ? data : Object.fromEntries(Object.keys(master.columns).map((c) => [c, pick(data, c) === undefined ? existing[c.toLowerCase()] : pick(data, c)]));
233
- const values = normalize(master, merged);
234
-
235
- if (!isNew && master.keyFromData && !same(values[master.key], key)) {
236
- throw finaError("MASTER_RULE", `The ${master.label} number ${key} cannot be changed here (it is renamed across FINA): use FINA desktop.`);
237
- }
238
- await assertUniqueCode(master, values, isNew ? null : key);
239
-
240
- const selfKey = isNew ? (master.keyFromData ? values[master.key] : null) : key;
241
- let parent = null;
242
- let children = [];
243
- let moved = false;
244
- if (master.hierarchy) {
245
- const h = master.hierarchy;
246
- const tree = await readTree(master);
247
- children = selfKey === null ? [] : tree.filter((r) => same(r.p, selfKey));
248
- const parentKey = values[h.parent];
249
- moved = isNew || String(parentKey ?? "") !== String(existing[h.parent.toLowerCase()] ?? "");
250
- if (parentKey !== null) {
251
- if (selfKey !== null && same(parentKey, selfKey)) throw finaError("MASTER_RULE", `A ${master.label} cannot be its own parent.`);
252
- parent = await readRow(master, parentKey);
253
- if (!parent) throw finaError("MASTER_RULE", `The parent ${master.label} ${parentKey} does not exist.`);
254
- if (moved) {
255
- if (Number(parent.suspended) === 1) throw finaError("MASTER_RULE", `The parent ${master.label} ${parentKey} is suspended.`);
256
- if (selfKey !== null && isDescendant(tree, parentKey, selfKey)) {
257
- throw finaError("MASTER_RULE", `A ${master.label} cannot move under one of its own sub-records.`);
258
- }
259
- const parentHasChildren = tree.some((r) => same(r.p, parentKey) && !same(r.k, selfKey));
260
- if (!parentHasChildren) {
261
- const used = await usedIn(master, parentKey);
262
- if (used.length > 0) {
263
- throw finaError(
264
- "MASTER_PARENT_IN_USE",
265
- `The parent ${master.label} ${parentKey} is already used in ${used.join(", ")}: making it a parent renames it in FINA, which is done in FINA desktop.`,
266
- );
267
- }
268
- }
269
- }
270
- }
271
- values[h.isChild] = parent ? 1 : 0;
272
- values[h.level] = parent ? Number(parent[h.level.toLowerCase()] ?? 0) + 1 : 0;
273
- values[h.root] = parent ? (blank(parent[h.root.toLowerCase()]) ? parentKey : parent[h.root.toLowerCase()]) : selfKey;
274
- /* COA-20 / DEP-13 / PRJ-16 */
275
- if (!isNew && Number(existing.suspended) !== 1 && values.SUSPENDED === 1 && children.some((c) => c.s !== 1)) {
276
- throw finaError("MASTER_RULE", `This ${master.label} has active sub-records: suspend them first.`);
277
- }
278
- }
279
-
280
- let usage = null;
281
- await master.beforeSave?.({
282
- values,
283
- existing,
284
- isNew,
285
- key: selfKey,
286
- parent,
287
- children,
288
- usage: async () => (usage ??= new Set(selfKey === null || isNew ? [] : await usedIn(master, selfKey))),
289
- defaccntNames: async () =>
290
- ((await client.executeSql(`SELECT NAME FROM DEFACCNT WHERE GLACCOUNT = ${sqlString(selfKey)}`)) ?? []).map((r) => String(r.name ?? "").trim()),
291
- companyCurrency: async () => {
292
- const d = (await client.executeSql("SELECT DEFAULTCURRENCYID AS D FROM COMPANY"))?.[0]?.d;
293
- return blank(d) ? null : Number(d);
294
- },
295
- currencyExists: async (id) => (await readRow(currencyMaster, id)) !== null,
296
- });
297
-
298
- const sent = Object.fromEntries(Object.entries(values).map(([column, v]) => [column, v === null || v === undefined ? null : String(v)]));
299
- const { id } = await client.json2fina({
300
- id: isNew ? -1 : String(key),
301
- dataModule: master.dataModule,
302
- transType: "",
303
- userId: who.finaUser.id,
304
- data: { master: sent },
305
- });
306
- const savedKey = isNew ? (master.keyFromData ? values[master.key] : id) : key;
307
- if (master.hierarchy && moved && !isNew) await relevelDescendants(master, savedKey);
308
- return (await readRow(master, savedKey)) ?? { [master.key.toLowerCase()]: savedKey };
309
- };
310
-
311
- const remove = async (principal, name, key) => {
312
- const master = masterOf(name);
313
- const who = await caller(principal, master, "delete");
314
- const existing = await readRow(master, key);
315
- if (!existing) throw finaError("MASTER_NOT_FOUND", `${master.label} ${key} does not exist in FINA.`);
316
- const code = existing[master.code.toLowerCase()] ?? key;
317
-
318
- /* COA-25 / DEP-18 / PRJ-18 */
319
- if (master.hierarchy && (await readTree(master)).some((r) => same(r.p, key))) {
320
- throw finaError("MASTER_IN_USE", `Cannot delete ${master.label} ${code}: delete its sub-records first.`);
321
- }
322
- /* COA-26 / DEP-16 / PRJ-18 / CUR-08 — and, in M3, what the desktop would clean up (COA-27, DEP-17, PRJ-19, CUR-09). */
323
- const used = await usedIn(master, key);
324
- if (used.length > 0) throw finaError("MASTER_IN_USE", `Cannot delete ${master.label} ${code}: it is used in ${used.join(", ")}.`);
325
-
326
- await client.json2fina({ id: String(key), delete: "1", dataModule: master.dataModule, transType: "", userId: who.finaUser.id, data: { master: {} } });
327
- return { key };
328
- };
329
-
330
- return { get, save, remove };
331
- };
@@ -1,104 +0,0 @@
1
- /*
2
- * Doc 8 M3 — Chart of Accounts (GLACCNT), written through FINA's TAccountDataset.
3
- *
4
- * The rules are the desktop's (docs/DOC8-APPENDIX-RULE-INVENTORIES.md §2.1; bank/GLAccountDM.pas), which the API path
5
- * does not run. Rights: COA C / E (D4). The account number is the key and is entered by the user (no auto-number).
6
- */
7
- import { finaError } from "../errors.js";
8
-
9
- /* §2.1 types: 7 Bank … 21 Other Income. */
10
- export const ACCOUNT_TYPES = [7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21];
11
- const BANK = 7;
12
- const AR = 8;
13
- const INVENTORY = 9;
14
- const AP = 13;
15
- /* COA-06/08: only Bank, AR and AP carry a currency. */
16
- const WITH_CURRENCY = [BANK, AR, AP];
17
- /* COA-09/12: the types whose accounts other modules post to by type. */
18
- const LOCKED_TYPES = [BANK, AR, AP, INVENTORY];
19
- /* COA-10: DEFACCNT names of the accounts whose type never changes (GLAccountDM.pas CanChangeOBEAndREAcc). */
20
- const FIXED_TYPE_NAMES = ["RETAINED_EARNING", "OPENING BALANCE EQUITY"];
21
-
22
- const refuse = (message) => {
23
- throw finaError("MASTER_RULE", message);
24
- };
25
-
26
- export default {
27
- name: "coa",
28
- label: "account",
29
- rights: "coa",
30
- table: "GLACCNT",
31
- key: "GLACCOUNT",
32
- keyType: "string",
33
- /* The number IS the key: entered by the user, and not renamed here (a rename cascades across FINA). */
34
- keyFromData: true,
35
- code: "GLACCOUNT",
36
- dataModule: "TAccountDataset",
37
- columns: {
38
- GLACCOUNT: { type: "string", size: 20, required: true },
39
- ACCOUNTNAME: { type: "string", size: 60, required: true },
40
- ACCOUNTTYPE: { type: "integer", required: true, oneOf: ACCOUNT_TYPES },
41
- CURRENCYID: { type: "integer" },
42
- PARENTACCOUNT: { type: "string", size: 20 },
43
- /* COA-01 defaults, sent explicitly: the API leaves what is not sent. */
44
- SUSPENDED: { type: "flag", default: 0 },
45
- MEMO: { type: "string", size: 240 },
46
- ISFISCAL: { type: "flag", default: 1 },
47
- ISALLOCTOPROD: { type: "flag", default: 0 },
48
- },
49
- /* COA-17: a child's level is its parent's + 1 and its root is its parent's root; a root is its own. */
50
- hierarchy: { parent: "PARENTACCOUNT", level: "INDENTLEVEL", root: "FIRSTPARENTACCOUNT", isChild: "SUBACCOUNT" },
51
- /* COA-28: a trigger deletes the account's GLACTUAL rows; GLBALANCETEMP is a report scratch table. */
52
- ignoreReferences: ["GLACTUAL", "GLBALANCETEMP"],
53
-
54
- beforeSave: async ({ values, existing, isNew, parent, children, usage, defaccntNames, companyCurrency, currencyExists }) => {
55
- const type = values.ACCOUNTTYPE;
56
-
57
- /* COA-16: a child has its parent's type. */
58
- if (parent && Number(parent.accounttype) !== type) {
59
- refuse(`Account ${values.GLACCOUNT} must have the type of its parent ${parent.glaccount} (${parent.accounttype}).`);
60
- }
61
-
62
- /* COA-06/07/08: Bank/AR/AP carry a currency — the company's when none is given; any other type carries none. */
63
- if (WITH_CURRENCY.includes(type)) {
64
- if (values.CURRENCYID === null) values.CURRENCYID = await companyCurrency();
65
- if (values.CURRENCYID === null || !(await currencyExists(values.CURRENCYID))) {
66
- refuse(`Currency ${values.CURRENCYID} does not exist.`);
67
- }
68
- } else {
69
- values.CURRENCYID = null;
70
- }
71
-
72
- if (isNew) return;
73
-
74
- const oldType = Number(existing.accounttype);
75
- const typeChanged = oldType !== type;
76
- const oldCurrency = existing.currencyid === null || existing.currencyid === undefined ? null : Number(existing.currencyid);
77
- const currencyChanged = oldCurrency !== values.CURRENCYID;
78
- const posted = typeChanged || currencyChanged ? (await usage()).has("GLHIST") : false;
79
-
80
- if (typeChanged) {
81
- if (children.length > 0) refuse(`Account ${values.GLACCOUNT} has sub-accounts, which carry its type: its type cannot change.`);
82
- const names = await defaccntNames();
83
- if (names.some((n) => FIXED_TYPE_NAMES.includes(n))) {
84
- refuse(`Account ${values.GLACCOUNT} is the company's ${names.join(", ")} account: its type cannot change.`);
85
- }
86
- if (posted && LOCKED_TYPES.includes(oldType)) {
87
- refuse(`Account ${values.GLACCOUNT} has transactions: a Bank, AR, AP or Inventory account keeps its type (COA-12).`);
88
- }
89
- if (posted && LOCKED_TYPES.includes(type)) {
90
- refuse(`Account ${values.GLACCOUNT} has transactions: it cannot become a Bank, AR, AP or Inventory account (COA-09).`);
91
- }
92
- }
93
- /* COA-11 */
94
- if (currencyChanged && posted && WITH_CURRENCY.includes(oldType)) {
95
- refuse(`Account ${values.GLACCOUNT} has transactions: its currency cannot change.`);
96
- }
97
-
98
- /* COA-21: a default account is not suspended here (the desktop clears it from DEFACCNT first). */
99
- if (Number(existing.suspended) !== 1 && values.SUSPENDED === 1) {
100
- const names = await defaccntNames();
101
- if (names.length > 0) refuse(`Account ${values.GLACCOUNT} is a default account (${names.join(", ")}): it cannot be suspended here.`);
102
- }
103
- },
104
- };
@@ -1,39 +0,0 @@
1
- /*
2
- * Doc 8 M3 — Kurs (CURRENCY), written through FINA's TCurrencyDataset.
3
- *
4
- * docs/DOC8-APPENDIX-RULE-INVENTORIES.md §2.4. No rights column: the desktop's Kurs menu needs user level 0 or 1, which
5
- * identity.js reads as the `currency` rights. The name is the code (unique, G-02). EXCHANGERATE is NUMERIC(18,4): one
6
- * current rate per currency, 4 decimals. Adding a second currency is what turns Multi Currency on (D3, D3a).
7
- * The dataset itself makes a lone rate-1 currency the company default (CUR-04/05).
8
- */
9
- import { finaError } from "../errors.js";
10
-
11
- const refuse = (message) => {
12
- throw finaError("MASTER_RULE", message);
13
- };
14
-
15
- export default {
16
- name: "currency",
17
- label: "currency",
18
- rights: "currency",
19
- table: "CURRENCY",
20
- key: "CURRENCYID",
21
- keyType: "integer",
22
- code: "CURRENCYNAME",
23
- dataModule: "TCurrencyDataset",
24
- columns: {
25
- CURRENCYNAME: { type: "string", size: 20, required: true },
26
- EXCHANGERATE: { type: "decimal4", required: true },
27
- COUNTRYNAME: { type: "string", size: 80 },
28
- CURRENCYSYMBOL: { type: "string", size: 10 },
29
- },
30
-
31
- beforeSave: async ({ values, key, isNew, companyCurrency }) => {
32
- /* CUR-02 */
33
- if (values.EXCHANGERATE === 0) refuse("Rate cannot be zero.");
34
- if (values.EXCHANGERATE < 0) refuse(`A rate cannot be negative (${values.EXCHANGERATE}).`);
35
- if (!isNew && Number(key) === Number(await companyCurrency()) && values.EXCHANGERATE !== 1) {
36
- refuse(`${values.CURRENCYNAME} is the company's default currency: its rate is 1.`);
37
- }
38
- },
39
- };
@@ -1,32 +0,0 @@
1
- /*
2
- * Doc 8 M3 — Departemen (DEPARTMENT), written through FINA's TDepartmentDataset.
3
- *
4
- * docs/DOC8-APPENDIX-RULE-INVENTORIES.md §2.2. Rights: DEPT C / E (D4). The id is FINA's (Get_DeptID); the number is
5
- * the code the user enters, unique case-insensitively (G-02).
6
- *
7
- * Hierarchy: SUBDEPTID = the parent's id, ISSUBDEPT = 1 under a parent, INDENTLEVEL = parent's + 1, PARENTDEPTID = the
8
- * root's id (DEP-08). A new ROOT sends PARENTDEPTID empty: FINA sets it to the new id (DEP-02), which is not known
9
- * before the insert.
10
- */
11
- export default {
12
- name: "department",
13
- label: "department",
14
- rights: "dept",
15
- table: "DEPARTMENT",
16
- key: "DEPTID",
17
- keyType: "integer",
18
- code: "DEPTNO",
19
- dataModule: "TDepartmentDataset",
20
- columns: {
21
- /* DEP-07: the desktop refuses a number over 18 characters. */
22
- DEPTNO: { type: "string", size: 18, required: true },
23
- DEPTNAME: { type: "string", size: 60, required: true },
24
- SUBDEPTID: { type: "integer" },
25
- /* DEP-04: the API leaves SUSPENDED / ISSUBDEPT NULL unless sent. */
26
- SUSPENDED: { type: "flag", default: 0 },
27
- DESCRIPTION: { type: "string", size: 240 },
28
- CONTACT_NAME: { type: "string", size: 60 },
29
- },
30
- hierarchy: { parent: "SUBDEPTID", level: "INDENTLEVEL", root: "PARENTDEPTID", isChild: "ISSUBDEPT" },
31
- ignoreReferences: ["GLBALANCETEMP"],
32
- };
@@ -1,21 +0,0 @@
1
- /*
2
- * Doc 8 M3 — the FINA masters engine/fina serves, by the name the browser uses.
3
- *
4
- * ⚠️ SERVER-OWNED. A request names a master; the table, its key, its columns and FINA's dataset class come from here.
5
- * Nothing a browser sends can point a write at another table or dataset.
6
- */
7
- import { finaError } from "../errors.js";
8
- import coa from "./coa.js";
9
- import department from "./department.js";
10
- import project from "./project.js";
11
- import currency from "./currency.js";
12
-
13
- const MASTERS = { coa, department, project, currency };
14
-
15
- export const MASTER_NAMES = Object.keys(MASTERS);
16
-
17
- export const masterOf = (name) => {
18
- const master = MASTERS[String(name ?? "").trim().toLowerCase()];
19
- if (!master) throw finaError("INVALID", `Unknown master ${JSON.stringify(name)}: it is one of ${MASTER_NAMES.join(", ")}.`);
20
- return master;
21
- };
@@ -1,53 +0,0 @@
1
- /*
2
- * Doc 8 M3 — Proyek (PROJECT), written through FINA's TProjectDataset.
3
- *
4
- * docs/DOC8-APPENDIX-RULE-INVENTORIES.md §2.3. Rights: PROJ C / E (D4). Hierarchy as Departemen: SUBPROJECTID,
5
- * SUBPROJECT, INDENTLEVEL, PARENTPROJECTID (PRJ-08); HEADER is not maintained (derived from children).
6
- * ⚠️ FINA's column is FINSHED_DATE (sic).
7
- */
8
- import { finaError } from "../errors.js";
9
-
10
- const refuse = (message) => {
11
- throw finaError("MASTER_RULE", message);
12
- };
13
- const today = () => new Date().toISOString().slice(0, 10);
14
-
15
- export default {
16
- name: "project",
17
- label: "project",
18
- rights: "proj",
19
- table: "PROJECT",
20
- key: "PROJECTID",
21
- keyType: "integer",
22
- code: "PROJECTNO",
23
- dataModule: "TProjectDataset",
24
- columns: {
25
- PROJECTNO: { type: "string", size: 20, required: true },
26
- PROJECTNAME: { type: "string", size: 60, required: true },
27
- CONTACTNAME: { type: "string", size: 60 },
28
- MANAGERNAME: { type: "string", size: 60 },
29
- START_DATE: { type: "date" },
30
- FINSHED_DATE: { type: "date" },
31
- SUBPROJECTID: { type: "integer" },
32
- /* PRJ-05: the API leaves SUBPROJECT / SUSPENDED / FINISHED NULL unless sent. */
33
- SUSPENDED: { type: "flag", default: 0 },
34
- FINISHED: { type: "flag", default: 0 },
35
- PERCENTCOMPLETED: { type: "decimal4", default: 0 },
36
- DESCRIPTION: { type: "string", size: 240 },
37
- },
38
- hierarchy: { parent: "SUBPROJECTID", level: "INDENTLEVEL", root: "PARENTPROJECTID", isChild: "SUBPROJECT" },
39
- ignoreReferences: ["GLBALANCETEMP"],
40
-
41
- beforeSave: async ({ values }) => {
42
- /* PRJ-07: the dates default to today; the finish is not before the start. */
43
- if (values.START_DATE === null) values.START_DATE = today();
44
- if (values.FINSHED_DATE === null) values.FINSHED_DATE = values.START_DATE > today() ? values.START_DATE : today();
45
- if (values.FINSHED_DATE < values.START_DATE) refuse(`The finish date ${values.FINSHED_DATE} is before the start date ${values.START_DATE}.`);
46
-
47
- /* PRJ-12: percent completed is 0–100; 100 means finished, and finished means 100. */
48
- const percent = values.PERCENTCOMPLETED ?? 0;
49
- if (percent < 0 || percent > 100) refuse(`Percent completed must be between 0 and 100 (it is ${percent}).`);
50
- if (values.FINISHED === 1) values.PERCENTCOMPLETED = 100;
51
- else values.FINISHED = percent === 100 ? 1 : 0;
52
- },
53
- };
@@ -1,56 +0,0 @@
1
- /*
2
- * Doc 8 M5a — FINA's own period check (contract §6, §8; FinaXE10 model/utility/CheckPeriodUtility.pas, Language.pas).
3
- *
4
- * `SELECT RES FROM CHECK_PERIOD(month, year)`: 0 open; 1 the fiscal year is not defined; 2 before the current fiscal
5
- * year; 3 outside the allowed range; 4 outside the EXPECTED range — a yes/no dialog on the desktop, which the FINA API
6
- * would raise on its own server (and hang on) under HEADLESS, so Biz-A refuses it too (contract §8, safe default).
7
- *
8
- * ⚠️ The API checks only the date a JV HAD on an edit (appendix V12): the writer asks the new date here as well.
9
- */
10
- import { finaError } from "./errors.js";
11
- import { toDateOnly } from "./lock.js";
12
-
13
- const WORDS = {
14
- 1: () => "FINA's current fiscal year is not yet defined (Company Info).",
15
- 2: (day) => `${day} is prior to the current fiscal year in FINA.`,
16
- 3: (day) => `${day} is not within the allowed range defined by FINA's Company Info.`,
17
- 4: (day) => `${day} is not within the expected range defined by FINA's Company Info; FINA asks for a confirmation there, so Biz-A does not save it.`,
18
- };
19
-
20
- export const checkPeriods = async (client, dates) => {
21
- const months = new Map();
22
- for (const date of dates ?? []) {
23
- if (date === null || date === undefined || date === "") continue;
24
- const day = toDateOnly(date);
25
- if (!day) throw finaError("INVALID", `${JSON.stringify(date)} is not a date.`);
26
- const [year, month] = day.split("-").map(Number);
27
- const key = `${year}-${month}`;
28
- if (!months.has(key)) months.set(key, { year, month, day });
29
- }
30
- for (const { year, month, day } of months.values()) {
31
- const rows = await client.executeSql(`SELECT RES FROM CHECK_PERIOD(${month}, ${year})`);
32
- const res = Number(rows?.[0]?.res ?? 0);
33
- if (res !== 0) throw finaError("PERIOD_CLOSED", (WORDS[res] ?? (() => `FINA refuses the period of ${day} (CHECK_PERIOD ${res}).`))(day));
34
- }
35
- };
36
-
37
- /*
38
- * Doc 8 M5b — L-08: FINA's answer for each month ('YYYY-MM' → RES, 0 open), one question per month. Daftar JV asks for
39
- * the months of the rows it shows and disables Ubah / Hapus where the period is not open; the writer still refuses.
40
- */
41
- export const periodStates = async (client, months) => {
42
- if (!Array.isArray(months)) throw finaError("INVALID", "The months are a list of YYYY-MM.");
43
- const wanted = [];
44
- for (const month of months) {
45
- const m = /^(\d{4})-(0[1-9]|1[0-2])$/.exec(String(month ?? ""));
46
- if (!m) throw finaError("INVALID", `${JSON.stringify(month)} is not a month (YYYY-MM).`);
47
- if (!wanted.includes(month)) wanted.push(month);
48
- }
49
- const states = {};
50
- for (const month of wanted) {
51
- const [year, mm] = month.split("-").map(Number);
52
- const rows = await client.executeSql(`SELECT RES FROM CHECK_PERIOD(${mm}, ${year})`);
53
- states[month] = Number(rows?.[0]?.res ?? 0);
54
- }
55
- return states;
56
- };