shutro-erp 0.2.1 → 0.3.1
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/README.md +74 -67
- package/dist/account_portal/.kosh/bindings.d.ts +22 -0
- package/dist/account_portal/.kosh/bindings.js +15 -0
- package/dist/account_portal/.kosh/schema.d.ts +1380 -0
- package/dist/account_portal/index.d.ts +3 -0
- package/dist/account_portal/index.js +73 -0
- package/dist/account_portal/module.d.ts +5983 -0
- package/dist/account_portal/module.js +18 -0
- package/dist/accounting/.kosh/bindings.js +1 -1
- package/dist/accounting/.kosh/schema.d.ts +172 -9
- package/dist/accounting/chart.d.ts +40 -8
- package/dist/accounting/chart.js +124 -30
- package/dist/accounting/closing.d.ts +6 -1
- package/dist/accounting/closing.js +5 -1
- package/dist/accounting/demo.js +6 -5
- package/dist/accounting/index.d.ts +3 -3
- package/dist/accounting/index.js +200 -23
- package/dist/accounting/module.d.ts +3326 -1086
- package/dist/accounting/module.js +408 -66
- package/dist/accounting/payments.d.ts +25 -0
- package/dist/accounting/payments.js +96 -4
- package/dist/accounting/web/RegisterPayment.d.ts +3 -2
- package/dist/accounting/web/RegisterPayment.js +39 -6
- package/dist/accounting/web/SettingsPage.js +5 -1
- package/dist/accounting/web/messages.js +10 -0
- package/dist/assistant/routes.js +1 -1
- package/dist/automation/engine.js +1 -1
- package/dist/automation/web/messages.js +2 -2
- package/dist/cli/apps.d.ts +1 -1
- package/dist/cli/apps.js +14 -14
- package/dist/cli/bin.js +3 -0
- package/dist/cli/index.js +11 -11
- package/dist/cli/init.d.ts +1 -1
- package/dist/cli/init.js +224 -78
- package/dist/cli/launch.d.ts +16 -0
- package/dist/cli/launch.js +138 -0
- package/dist/cli/project.d.ts +7 -7
- package/dist/cli/project.js +25 -25
- package/dist/cli/prompt.d.ts +35 -0
- package/dist/cli/prompt.js +48 -2
- package/dist/cli/templates.d.ts +14 -6
- package/dist/cli/templates.js +61 -34
- package/dist/cli/tui.d.ts +7 -0
- package/dist/cli/tui.js +254 -0
- package/dist/cli/ui.d.ts +21 -8
- package/dist/cli/ui.js +65 -19
- package/dist/contacts/.kosh/bindings.js +1 -1
- package/dist/contacts/.kosh/schema.d.ts +33 -1
- package/dist/contacts/index.js +24 -1
- package/dist/contacts/module.d.ts +42 -0
- package/dist/contacts/module.js +3 -1
- package/dist/import/convert.d.ts +55 -0
- package/dist/import/convert.js +224 -0
- package/dist/import/csv.d.ts +28 -0
- package/dist/import/csv.js +115 -0
- package/dist/import/index.d.ts +33 -0
- package/dist/import/index.js +60 -0
- package/dist/import/mapping.d.ts +15 -0
- package/dist/import/mapping.js +33 -0
- package/dist/import/match.d.ts +24 -0
- package/dist/import/match.js +60 -0
- package/dist/import/models.d.ts +2 -0
- package/dist/import/models.js +102 -0
- package/dist/import/routes.d.ts +13 -0
- package/dist/import/routes.js +392 -0
- package/dist/import/run.d.ts +131 -0
- package/dist/import/run.js +568 -0
- package/dist/import/service.d.ts +153 -0
- package/dist/import/service.js +367 -0
- package/dist/import/web/ImportButton.d.ts +7 -0
- package/dist/import/web/ImportButton.js +31 -0
- package/dist/import/web/ImportWizard.d.ts +75 -0
- package/dist/import/web/ImportWizard.js +217 -0
- package/dist/import/web/index.d.ts +13 -0
- package/dist/import/web/index.js +18 -0
- package/dist/import/web/messages.d.ts +2 -0
- package/dist/import/web/messages.js +139 -0
- package/dist/import/worker.d.ts +3 -0
- package/dist/import/worker.js +52 -0
- package/dist/import/xlsx.d.ts +20 -0
- package/dist/import/xlsx.js +192 -0
- package/dist/l10n_bd/.kosh/bindings.d.ts +22 -0
- package/dist/l10n_bd/.kosh/bindings.js +15 -0
- package/dist/l10n_bd/.kosh/schema.d.ts +1311 -0
- package/dist/l10n_bd/documents.d.ts +8 -0
- package/dist/l10n_bd/documents.js +183 -0
- package/dist/l10n_bd/index.d.ts +8 -0
- package/dist/l10n_bd/index.js +156 -0
- package/dist/l10n_bd/module.d.ts +5705 -0
- package/dist/l10n_bd/module.js +163 -0
- package/dist/l10n_bd/mushak.d.ts +67 -0
- package/dist/l10n_bd/mushak.js +139 -0
- package/dist/l10n_bd/routes.d.ts +3 -0
- package/dist/l10n_bd/routes.js +205 -0
- package/dist/l10n_bd/web/MushakPage.d.ts +7 -0
- package/dist/l10n_bd/web/MushakPage.js +53 -0
- package/dist/l10n_bd/web/index.d.ts +13 -0
- package/dist/l10n_bd/web/index.js +18 -0
- package/dist/l10n_bd/web/messages.d.ts +2 -0
- package/dist/l10n_bd/web/messages.js +101 -0
- package/dist/l10n_in/.kosh/bindings.d.ts +22 -0
- package/dist/l10n_in/.kosh/bindings.js +15 -0
- package/dist/l10n_in/.kosh/schema.d.ts +1373 -0
- package/dist/l10n_in/documents.d.ts +8 -0
- package/dist/l10n_in/documents.js +114 -0
- package/dist/l10n_in/einvoice.d.ts +253 -0
- package/dist/l10n_in/einvoice.js +485 -0
- package/dist/l10n_in/gstr.d.ts +109 -0
- package/dist/l10n_in/gstr.js +430 -0
- package/dist/l10n_in/index.d.ts +20 -0
- package/dist/l10n_in/index.js +232 -0
- package/dist/l10n_in/module.d.ts +7823 -0
- package/dist/l10n_in/module.js +427 -0
- package/dist/l10n_in/routes-einvoice.d.ts +15 -0
- package/dist/l10n_in/routes-einvoice.js +124 -0
- package/dist/l10n_in/routes.d.ts +17 -0
- package/dist/l10n_in/routes.js +282 -0
- package/dist/l10n_in/web/EInvoicePanel.d.ts +8 -0
- package/dist/l10n_in/web/EInvoicePanel.js +63 -0
- package/dist/l10n_in/web/GstPage.d.ts +7 -0
- package/dist/l10n_in/web/GstPage.js +57 -0
- package/dist/l10n_in/web/GstSettings.d.ts +10 -0
- package/dist/l10n_in/web/GstSettings.js +36 -0
- package/dist/l10n_in/web/index.d.ts +15 -0
- package/dist/l10n_in/web/index.js +22 -0
- package/dist/l10n_in/web/messages.d.ts +2 -0
- package/dist/l10n_in/web/messages.js +166 -0
- package/dist/mail/index.js +2 -2
- package/dist/modules-sdk/demo.d.ts +2 -0
- package/dist/modules-sdk/demo.js +7 -3
- package/dist/modules-sdk/index.d.ts +2 -2
- package/dist/modules-sdk/index.js +1 -1
- package/dist/modules-sdk/loader.js +2 -2
- package/dist/modules-sdk/manifest.d.ts +132 -0
- package/dist/modules-sdk/manifest.js +22 -0
- package/dist/notify/inbox.js +21 -0
- package/dist/notify/messages.js +7 -3
- package/dist/notify/models.d.ts +1 -1
- package/dist/notify/models.js +2 -0
- package/dist/notify/routes.js +1 -1
- package/dist/notify/web/messages.js +5 -1
- package/dist/portal/.kosh/bindings.d.ts +22 -0
- package/dist/portal/.kosh/bindings.js +15 -0
- package/dist/portal/.kosh/schema.d.ts +376 -0
- package/dist/portal/announce.d.ts +16 -0
- package/dist/portal/announce.js +117 -0
- package/dist/portal/documents.d.ts +17 -0
- package/dist/portal/documents.js +61 -0
- package/dist/portal/index.d.ts +20 -0
- package/dist/portal/index.js +65 -0
- package/dist/portal/module.d.ts +1405 -0
- package/dist/portal/module.js +84 -0
- package/dist/portal/routes-documents.d.ts +2 -0
- package/dist/portal/routes-documents.js +391 -0
- package/dist/portal/routes.d.ts +13 -0
- package/dist/portal/routes.js +313 -0
- package/dist/portal/rules.d.ts +11 -0
- package/dist/portal/rules.js +25 -0
- package/dist/portal/web/PortalAccess.d.ts +8 -0
- package/dist/portal/web/PortalAccess.js +66 -0
- package/dist/portal/web/index.d.ts +12 -0
- package/dist/portal/web/index.js +17 -0
- package/dist/portal/web/messages.d.ts +2 -0
- package/dist/portal/web/messages.js +33 -0
- package/dist/product/.kosh/bindings.js +1 -1
- package/dist/product/.kosh/schema.d.ts +42 -3
- package/dist/product/demo.js +6 -5
- package/dist/product/index.d.ts +1 -0
- package/dist/product/index.js +17 -4
- package/dist/product/module.d.ts +346 -14
- package/dist/product/module.js +48 -18
- package/dist/product/taxes.d.ts +59 -0
- package/dist/product/taxes.js +61 -0
- package/dist/project/.kosh/bindings.js +1 -1
- package/dist/project/.kosh/schema.d.ts +33 -1
- package/dist/project/module.d.ts +20 -0
- package/dist/project_portal/.kosh/bindings.d.ts +22 -0
- package/dist/project_portal/.kosh/bindings.js +15 -0
- package/dist/project_portal/.kosh/schema.d.ts +701 -0
- package/dist/project_portal/index.d.ts +9 -0
- package/dist/project_portal/index.js +42 -0
- package/dist/project_portal/module.d.ts +2142 -0
- package/dist/project_portal/module.js +22 -0
- package/dist/purchase/.kosh/bindings.js +1 -1
- package/dist/purchase/.kosh/schema.d.ts +45 -4
- package/dist/purchase/demo.js +3 -3
- package/dist/purchase/index.js +1 -1
- package/dist/purchase/module.d.ts +232 -8
- package/dist/purchase/module.js +51 -9
- package/dist/purchase_account/.kosh/bindings.js +1 -1
- package/dist/purchase_account/.kosh/schema.d.ts +174 -10
- package/dist/purchase_account/module.d.ts +2008 -641
- package/dist/purchase_account/module.js +2 -2
- package/dist/purchase_portal/.kosh/bindings.d.ts +22 -0
- package/dist/purchase_portal/.kosh/bindings.js +15 -0
- package/dist/purchase_portal/.kosh/schema.d.ts +852 -0
- package/dist/purchase_portal/index.d.ts +9 -0
- package/dist/purchase_portal/index.js +53 -0
- package/dist/purchase_portal/module.d.ts +2758 -0
- package/dist/purchase_portal/module.js +18 -0
- package/dist/reporting/documents.js +20 -3
- package/dist/reporting/index.d.ts +1 -0
- package/dist/reporting/index.js +1 -0
- package/dist/reporting/routes-documents.js +9 -2
- package/dist/reporting/words.d.ts +11 -0
- package/dist/reporting/words.js +229 -0
- package/dist/sale_account/.kosh/bindings.js +1 -1
- package/dist/sale_account/.kosh/schema.d.ts +174 -10
- package/dist/sale_account/module.d.ts +2008 -641
- package/dist/sale_account/module.js +3 -3
- package/dist/sale_portal/.kosh/bindings.d.ts +22 -0
- package/dist/sale_portal/.kosh/bindings.js +15 -0
- package/dist/sale_portal/.kosh/schema.d.ts +766 -0
- package/dist/sale_portal/index.d.ts +9 -0
- package/dist/sale_portal/index.js +71 -0
- package/dist/sale_portal/module.d.ts +2718 -0
- package/dist/sale_portal/module.js +19 -0
- package/dist/sale_project/.kosh/bindings.js +1 -1
- package/dist/sale_project/.kosh/schema.d.ts +45 -4
- package/dist/sale_project/module.d.ts +390 -10
- package/dist/sales/.kosh/bindings.js +1 -1
- package/dist/sales/.kosh/schema.d.ts +45 -4
- package/dist/sales/demo.js +3 -3
- package/dist/sales/index.js +67 -1
- package/dist/sales/module.d.ts +232 -8
- package/dist/sales/module.js +51 -12
- package/dist/server/api/client.js +1 -1
- package/dist/server/api/openapi.js +17 -1
- package/dist/server/apps.js +5 -5
- package/dist/server/auth/api-keys.d.ts +13 -0
- package/dist/server/auth/api-keys.js +65 -0
- package/dist/server/auth/base.js +26 -0
- package/dist/server/auth/crypto.js +1 -1
- package/dist/server/auth/scopes.d.ts +12 -0
- package/dist/server/auth/scopes.js +51 -0
- package/dist/server/auth/service.d.ts +59 -1
- package/dist/server/auth/service.js +236 -6
- package/dist/server/demo.d.ts +1 -1
- package/dist/server/demo.js +1 -1
- package/dist/server/env-names.d.ts +2 -0
- package/dist/server/env-names.js +16 -0
- package/dist/server/host.js +5 -1
- package/dist/server/identity.d.ts +7 -0
- package/dist/server/index.d.ts +2 -1
- package/dist/server/index.js +2 -1
- package/dist/server/registry.js +1 -1
- package/dist/server/server.d.ts +5 -0
- package/dist/server/server.js +71 -6
- package/dist/settings/routes.js +1 -1
- package/dist/settings/web/ApiKeysPage.d.ts +8 -0
- package/dist/settings/web/ApiKeysPage.js +70 -0
- package/dist/settings/web/index.d.ts +1 -0
- package/dist/settings/web/index.js +4 -0
- package/dist/settings/web/messages.js +62 -0
- package/dist/storage/routes.js +1 -1
- package/dist/studio/build.js +1 -1
- package/dist/studio/generate.js +4 -4
- package/dist/studio/module.js +5 -5
- package/dist/studio/web/messages.js +2 -2
- package/dist/ui/api.d.ts +2 -0
- package/dist/ui/app.js +27 -4
- package/dist/ui/context.d.ts +3 -1
- package/dist/ui/context.js +2 -2
- package/dist/ui/i18n/messages.js +2 -2
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/index.js +1 -1
- package/dist/ui/lib/format.d.ts +2 -0
- package/dist/ui/lib/format.js +3 -1
- package/dist/ui/pwa/worker.js +2 -2
- package/dist/ui/registry.d.ts +15 -0
- package/dist/ui/registry.js +20 -0
- package/dist/ui/views/FormView.js +19 -6
- package/package.json +52 -4
package/dist/cli/ui.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The installer's look in the terminal, without dependencies: a logo, numbered steps
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* (
|
|
2
|
+
* The installer's look in the terminal, without dependencies: a logo, numbered steps, spinners for long work,
|
|
3
|
+
* status lines (✔ ▲ ✖) and a summary card. On a Unicode terminal everything hangs on one rail down the left (│),
|
|
4
|
+
* the questions of the interactive prompter included (`tui.ts`). Colour, Unicode and animation are detected from
|
|
5
|
+
* the terminal (NO_COLOR, FORCE_COLOR, SHUTRO_ERP_ASCII=1, TERM=dumb, Windows consoles without Unicode); output that is not
|
|
6
|
+
* a terminal (tests, logs, CI) gets plain lines with the same words and no escape codes.
|
|
6
7
|
*/
|
|
8
|
+
import { release as osRelease } from 'node:os';
|
|
7
9
|
/** Plain output: logs, tests, pipes. */
|
|
8
10
|
export const PLAIN_TERMINAL = Object.freeze({
|
|
9
11
|
color: false,
|
|
@@ -13,7 +15,7 @@ export const PLAIN_TERMINAL = Object.freeze({
|
|
|
13
15
|
columns: 80,
|
|
14
16
|
});
|
|
15
17
|
/** Detects what `stream` (process.stdout) can show. */
|
|
16
|
-
export function detectTerminal(stream, env = process.env, platform = process.platform) {
|
|
18
|
+
export function detectTerminal(stream, env = process.env, platform = process.platform, release = osRelease()) {
|
|
17
19
|
const tty = stream.isTTY === true && env['TERM'] !== 'dumb' && env['CI'] === undefined;
|
|
18
20
|
const forced = env['FORCE_COLOR'] !== undefined && env['FORCE_COLOR'] !== '0';
|
|
19
21
|
const color = env['NO_COLOR'] === undefined && (forced || tty);
|
|
@@ -23,20 +25,27 @@ export function detectTerminal(stream, env = process.env, platform = process.pla
|
|
|
23
25
|
env['ConEmuTask'] === '{cmd::Cmder}' ||
|
|
24
26
|
env['TERM'] === 'xterm-256color' ||
|
|
25
27
|
env['TERM'] === 'alacritty';
|
|
26
|
-
|
|
28
|
+
// Windows 10 and later: the console host shows box drawing and colours too (UTF-8, virtual terminal); pictographs
|
|
29
|
+
// only in the modern terminals. Older Windows keeps ASCII.
|
|
30
|
+
const windows10 = Number(release.split('.')[0]) >= 10;
|
|
31
|
+
const unicode = env['KOSH_ASCII'] !== '1' &&
|
|
32
|
+
env['SHUTRO_ERP_ASCII'] !== '1' &&
|
|
33
|
+
(platform === 'win32' ? modernWindows || windows10 : env['TERM'] !== 'linux');
|
|
27
34
|
return {
|
|
28
35
|
color,
|
|
29
36
|
unicode,
|
|
30
|
-
emoji: unicode && tty,
|
|
37
|
+
emoji: unicode && tty && (platform !== 'win32' || modernWindows),
|
|
31
38
|
animate: tty,
|
|
32
39
|
columns: Math.max(40, Math.min(stream.columns ?? 80, 120)),
|
|
33
40
|
};
|
|
34
41
|
}
|
|
35
|
-
const ESC = '\u001b[';
|
|
42
|
+
export const ESC = '\u001b[';
|
|
36
43
|
const paint = (open, close) => (enabled) => (text) => enabled ? `${ESC}${open}m${text}${ESC}${close}m` : text;
|
|
37
44
|
const styles = {
|
|
38
45
|
bold: paint('1', '22'),
|
|
39
46
|
dim: paint('2', '22'),
|
|
47
|
+
inverse: paint('7', '27'),
|
|
48
|
+
underline: paint('4', '24'),
|
|
40
49
|
green: paint('32', '39'),
|
|
41
50
|
yellow: paint('33', '39'),
|
|
42
51
|
red: paint('31', '39'),
|
|
@@ -44,6 +53,22 @@ const styles = {
|
|
|
44
53
|
magenta: paint('35', '39'),
|
|
45
54
|
gray: paint('90', '39'),
|
|
46
55
|
};
|
|
56
|
+
/** Styles text for `terminal` (no escape codes without colour). */
|
|
57
|
+
export function stylerFor(terminal) {
|
|
58
|
+
return (name, text) => styles[name](terminal.color)(text);
|
|
59
|
+
}
|
|
60
|
+
/** The rail and its marks (Unicode terminals that animate), shared with the interactive prompter. */
|
|
61
|
+
export const RAIL = {
|
|
62
|
+
bar: '│',
|
|
63
|
+
start: '┌',
|
|
64
|
+
end: '└',
|
|
65
|
+
active: '◆',
|
|
66
|
+
done: '◇',
|
|
67
|
+
error: '▲',
|
|
68
|
+
cancel: '■',
|
|
69
|
+
};
|
|
70
|
+
/** Whether `terminal` gets the rail look (a TTY that shows Unicode). */
|
|
71
|
+
export const railed = (terminal) => terminal.unicode && terminal.animate;
|
|
47
72
|
/** Columns a string takes: escape codes take none, pictographs two. */
|
|
48
73
|
export function width(text) {
|
|
49
74
|
let n = 0;
|
|
@@ -73,12 +98,12 @@ export const ICONS = {
|
|
|
73
98
|
rocket: ['🚀', '◆'],
|
|
74
99
|
};
|
|
75
100
|
const LOGO = [
|
|
76
|
-
'
|
|
77
|
-
'
|
|
78
|
-
'
|
|
79
|
-
'
|
|
80
|
-
'██║
|
|
81
|
-
'
|
|
101
|
+
'███████╗██╗ ██╗██╗ ██╗████████╗██████╗ ██████╗',
|
|
102
|
+
'██╔════╝██║ ██║██║ ██║╚══██╔══╝██╔══██╗██╔═══██╗',
|
|
103
|
+
'███████╗███████║██║ ██║ ██║ ██████╔╝██║ ██║',
|
|
104
|
+
'╚════██║██╔══██║██║ ██║ ██║ ██╔══██╗██║ ██║',
|
|
105
|
+
'███████║██║ ██║╚██████╔╝ ██║ ██║ ██║╚██████╔╝',
|
|
106
|
+
'╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝',
|
|
82
107
|
];
|
|
83
108
|
/** Cyan to violet, one 256-colour step per logo line. */
|
|
84
109
|
const GRADIENT = [51, 45, 39, 33, 63, 99];
|
|
@@ -86,7 +111,9 @@ const SPINNER_UNICODE = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧',
|
|
|
86
111
|
const SPINNER_ASCII = ['|', '/', '-', '\\'];
|
|
87
112
|
/** A {@link Ui} writing through `out`. */
|
|
88
113
|
export function createUi(out, terminal = PLAIN_TERMINAL, options = {}) {
|
|
89
|
-
const s = (
|
|
114
|
+
const s = stylerFor(terminal);
|
|
115
|
+
const rail = railed(terminal);
|
|
116
|
+
const bar = s('gray', RAIL.bar);
|
|
90
117
|
const sym = terminal.unicode
|
|
91
118
|
? { ok: '✔', info: 'ℹ', warn: '▲', fail: '✖', dot: '•', bar: '█', rest: '░' }
|
|
92
119
|
: { ok: '[ok]', info: '[i]', warn: '[!]', fail: '[x]', dot: '-', bar: '#', rest: '.' };
|
|
@@ -104,14 +131,14 @@ export function createUi(out, terminal = PLAIN_TERMINAL, options = {}) {
|
|
|
104
131
|
return;
|
|
105
132
|
const f = s('cyan', frames[spin.frame % frames.length] ?? '');
|
|
106
133
|
const extra = spin.extra === '' ? '' : s('gray', ` ${spin.extra}`);
|
|
107
|
-
out(`\r${ESC}2K ${f} ${spin.label}${extra}`);
|
|
134
|
+
out(rail ? `\r${ESC}2K${f} ${spin.label}${extra}` : `\r${ESC}2K ${f} ${spin.label}${extra}`);
|
|
108
135
|
};
|
|
109
136
|
const write = (text) => {
|
|
110
137
|
clear();
|
|
111
138
|
out(text);
|
|
112
139
|
draw();
|
|
113
140
|
};
|
|
114
|
-
const line = (mark, text) => write(` ${mark} ${text}\n`);
|
|
141
|
+
const line = (mark, text) => write(rail ? `${bar} ${mark} ${text}\n` : ` ${mark} ${text}\n`);
|
|
115
142
|
const icon = (name) => (terminal.emoji ? ICONS[name][0] : s('cyan', ICONS[name][1]));
|
|
116
143
|
return {
|
|
117
144
|
terminal,
|
|
@@ -120,7 +147,7 @@ export function createUi(out, terminal = PLAIN_TERMINAL, options = {}) {
|
|
|
120
147
|
icon,
|
|
121
148
|
async banner(subtitle) {
|
|
122
149
|
if (!terminal.unicode) {
|
|
123
|
-
write(`\n ${s('bold', '
|
|
150
|
+
write(`\n ${s('bold', 'S H U T R O E R P')} ${subtitle}\n`);
|
|
124
151
|
return;
|
|
125
152
|
}
|
|
126
153
|
write('\n');
|
|
@@ -132,8 +159,25 @@ export function createUi(out, terminal = PLAIN_TERMINAL, options = {}) {
|
|
|
132
159
|
}
|
|
133
160
|
write(` ${s('bold', subtitle)}\n`);
|
|
134
161
|
},
|
|
162
|
+
intro(title, hint) {
|
|
163
|
+
if (!rail) {
|
|
164
|
+
write(` ${s('gray', hint)}\n`);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
write(`\n${s('gray', RAIL.start)} ${s('inverse', ` ${title} `)}\n${bar} ${s('gray', hint)}\n`);
|
|
168
|
+
},
|
|
169
|
+
outro(text) {
|
|
170
|
+
write(rail ? `${bar}\n${s('gray', RAIL.end)} ${text}\n` : `\n ${text}\n`);
|
|
171
|
+
},
|
|
135
172
|
step(name, title, hint) {
|
|
136
173
|
stepNo += 1;
|
|
174
|
+
if (rail) {
|
|
175
|
+
const n = options.steps === undefined ? '' : s('gray', ` ${stepNo}/${options.steps}`);
|
|
176
|
+
write(`${bar}\n${s('cyan', RAIL.active)} ${s('bold', title)}${n}\n`);
|
|
177
|
+
if (hint !== undefined)
|
|
178
|
+
write(`${bar} ${s('gray', hint)}\n`);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
137
181
|
const n = options.steps === undefined ? '' : s('gray', `Step ${stepNo}/${options.steps} · `);
|
|
138
182
|
const rule = terminal.unicode ? '─' : '-';
|
|
139
183
|
const head = `${icon(name)} ${n}${s('bold', title)}`;
|
|
@@ -146,7 +190,9 @@ export function createUi(out, terminal = PLAIN_TERMINAL, options = {}) {
|
|
|
146
190
|
info: (text) => line(s('cyan', sym.info), text),
|
|
147
191
|
warn: (text) => line(s('yellow', sym.warn), text),
|
|
148
192
|
fail: (text) => line(s('red', sym.fail), text),
|
|
149
|
-
detail: (text) => write(
|
|
193
|
+
detail: (text) => write(rail
|
|
194
|
+
? `${bar} ${s('gray', `${sym.dot} ${text}`)}\n`
|
|
195
|
+
: ` ${s('gray', `${sym.dot} ${text}`)}\n`),
|
|
150
196
|
async task(label, work) {
|
|
151
197
|
const started = Date.now();
|
|
152
198
|
const previous = spin;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// GENERATED by `pnpm generate:models` (Shutro’s generator). Do not edit. definitions-hash:
|
|
1
|
+
// GENERATED by `pnpm generate:models` (Shutro’s generator). Do not edit. definitions-hash: 2271aaa68e1a18a5
|
|
2
2
|
// Bindings of module `contacts`: import the definition API from here to get types scoped to what it can see.
|
|
3
3
|
export { defineModel, defineModule, extendModel, f, replace } from '../../server/orm.js';
|
|
4
4
|
import { definitions } from '../../server/orm.js';
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
// GENERATED by `pnpm generate:models` (Shutro’s generator). Do not edit. definitions-hash:
|
|
1
|
+
// GENERATED by `pnpm generate:models` (Shutro’s generator). Do not edit. definitions-hash: 2271aaa68e1a18a5
|
|
2
2
|
// Schema view of module `contacts` (RFC 0002 §7.7): the models it owns or depends on, with the extensions it can see.
|
|
3
3
|
import type { F } from '../../server/orm.js';
|
|
4
4
|
|
|
5
5
|
/** Model name → model type (RFC 0002 §7.7). */
|
|
6
6
|
export interface SchemaView {
|
|
7
7
|
readonly 'audit.event': AuditEvent;
|
|
8
|
+
readonly 'auth.api_key': AuthApiKey;
|
|
8
9
|
readonly 'auth.credential': AuthCredential;
|
|
9
10
|
readonly 'auth.session': AuthSession;
|
|
10
11
|
readonly 'auth.user_grant': AuthUserGrant;
|
|
@@ -42,6 +43,34 @@ export interface AuditEvent {
|
|
|
42
43
|
export interface AuditEventMethods extends F.Methods<typeof import('../../server/index.js').baseShutro, 'audit.event'> {
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
/** `auth.api_key` (owner: base). */
|
|
47
|
+
export interface AuthApiKey {
|
|
48
|
+
readonly name: 'auth.api_key';
|
|
49
|
+
readonly fields: {
|
|
50
|
+
readonly id: F.Id<'auth.api_key'>;
|
|
51
|
+
readonly createdAt: F.Datetime<{ readonly required: true; readonly readonly: true }>;
|
|
52
|
+
readonly updatedAt: F.Datetime<{ readonly required: true; readonly readonly: true }>;
|
|
53
|
+
readonly createdBy: F.Many2one<'res.users', { readonly readonly: true }>;
|
|
54
|
+
readonly updatedBy: F.Many2one<'res.users', { readonly readonly: true }>;
|
|
55
|
+
readonly displayName: F.Char<{ readonly required: true; readonly readonly: true; readonly compute: true }>;
|
|
56
|
+
readonly name: F.Char<{ readonly required: true }>;
|
|
57
|
+
readonly user: F.Many2one<'res.users', { readonly required: true }>;
|
|
58
|
+
readonly tokenHash: F.Char<{ readonly required: true }>;
|
|
59
|
+
readonly prefix: F.Char<{ readonly required: true }>;
|
|
60
|
+
readonly scopes: F.Text<{ readonly required: true }>;
|
|
61
|
+
readonly expiresAt: F.Datetime<{ readonly required: true }>;
|
|
62
|
+
readonly lastUsedAt: F.Datetime;
|
|
63
|
+
readonly lastUsedIp: F.Char;
|
|
64
|
+
readonly revoked: F.Bool<{ readonly required: true; readonly default: true }>;
|
|
65
|
+
readonly issuedBy: F.Many2one<'res.users'>;
|
|
66
|
+
};
|
|
67
|
+
readonly methods: AuthApiKeyMethods;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Methods of `auth.api_key`: declared ones and transitions. */
|
|
71
|
+
export interface AuthApiKeyMethods extends F.Methods<typeof import('../../server/index.js').baseShutro, 'auth.api_key'> {
|
|
72
|
+
}
|
|
73
|
+
|
|
45
74
|
/** `auth.credential` (owner: base). */
|
|
46
75
|
export interface AuthCredential {
|
|
47
76
|
readonly name: 'auth.credential';
|
|
@@ -173,6 +202,7 @@ export interface ResCompany {
|
|
|
173
202
|
readonly displayName: F.Char<{ readonly required: true; readonly readonly: true; readonly compute: true }>;
|
|
174
203
|
readonly name: F.Char<{ readonly required: true }>;
|
|
175
204
|
readonly currency: F.Many2one<'res.currency', { readonly required: true }>;
|
|
205
|
+
readonly numberGrouping: F.Selection<'international' | 'lakh', { readonly required: true; readonly default: true }>;
|
|
176
206
|
};
|
|
177
207
|
readonly methods: ResCompanyMethods;
|
|
178
208
|
}
|
|
@@ -226,6 +256,7 @@ export interface ResPartner {
|
|
|
226
256
|
readonly city: F.Char;
|
|
227
257
|
readonly zip: F.Char;
|
|
228
258
|
readonly country: F.Char;
|
|
259
|
+
readonly state: F.Char;
|
|
229
260
|
readonly taxId: F.Char;
|
|
230
261
|
readonly isCustomer: F.Bool<{ readonly required: true; readonly default: true }>;
|
|
231
262
|
readonly isVendor: F.Bool<{ readonly required: true; readonly default: true }>;
|
|
@@ -258,6 +289,7 @@ export interface ResUsers {
|
|
|
258
289
|
readonly companies: F.Many2many<'res.company', { readonly required: true }>;
|
|
259
290
|
readonly lang: F.Char<{ readonly default: true }>;
|
|
260
291
|
readonly tz: F.Char<{ readonly default: true }>;
|
|
292
|
+
readonly isService: F.Bool<{ readonly required: true; readonly default: true }>;
|
|
261
293
|
};
|
|
262
294
|
readonly methods: ResUsersMethods;
|
|
263
295
|
}
|
package/dist/contacts/index.js
CHANGED
|
@@ -27,6 +27,27 @@ export const contacts = defineModule({
|
|
|
27
27
|
user: { label: 'User', groups: ['contacts.user'] },
|
|
28
28
|
manager: { label: 'Manager', groups: ['contacts.manager'], implies: ['user'] },
|
|
29
29
|
},
|
|
30
|
+
// A sample customer sheet for the import screen (RFC 0014 §5.4), when Data import is installed.
|
|
31
|
+
importTemplates: [
|
|
32
|
+
{
|
|
33
|
+
key: 'customers',
|
|
34
|
+
model: 'res.partner',
|
|
35
|
+
label: 'Customers',
|
|
36
|
+
columns: [
|
|
37
|
+
{ header: 'Name', field: 'name' },
|
|
38
|
+
{ header: 'Company', field: 'isCompany' },
|
|
39
|
+
{ header: 'Email', field: 'email' },
|
|
40
|
+
{ header: 'Phone', field: 'phone' },
|
|
41
|
+
{ header: 'Mobile', field: 'mobile' },
|
|
42
|
+
{ header: 'Street', field: 'street' },
|
|
43
|
+
{ header: 'City', field: 'city' },
|
|
44
|
+
{ header: 'Postal code', field: 'zip' },
|
|
45
|
+
{ header: 'Country', field: 'country' },
|
|
46
|
+
{ header: 'Tax ID', field: 'taxId' },
|
|
47
|
+
{ header: 'Customer', field: 'isCustomer' },
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
],
|
|
30
51
|
actions: {
|
|
31
52
|
'contacts.action_partners': action(ResPartner, {
|
|
32
53
|
label: 'Contacts',
|
|
@@ -57,7 +78,9 @@ export const contacts = defineModule({
|
|
|
57
78
|
k.group('identity', ['name', 'isCompany', 'parent', 'taxId']),
|
|
58
79
|
k.group('reach', ['email', 'phone', 'mobile', 'website']),
|
|
59
80
|
k.notebook('more', [
|
|
60
|
-
k.page('address', 'Address', [
|
|
81
|
+
k.page('address', 'Address', [
|
|
82
|
+
k.group('where', ['street', 'city', 'zip', 'state', 'country']),
|
|
83
|
+
]),
|
|
61
84
|
k.page('sales', 'Sales & purchase', [
|
|
62
85
|
k.group('roles', ['isCustomer', 'isVendor', 'currency']),
|
|
63
86
|
]),
|
|
@@ -162,6 +162,16 @@ export declare const ResPartner: import("../server/orm.js").Model<"res.partner",
|
|
|
162
162
|
readonly hasDefault: false;
|
|
163
163
|
readonly readonly: false;
|
|
164
164
|
}, string>;
|
|
165
|
+
readonly state: import("../server/orm.js").CharField<{
|
|
166
|
+
readonly required: false;
|
|
167
|
+
readonly computed: false;
|
|
168
|
+
readonly inverse: false;
|
|
169
|
+
readonly sequence: false;
|
|
170
|
+
readonly computeName: undefined;
|
|
171
|
+
readonly store: true;
|
|
172
|
+
readonly hasDefault: false;
|
|
173
|
+
readonly readonly: false;
|
|
174
|
+
}, string>;
|
|
165
175
|
readonly taxId: import("../server/orm.js").CharField<{
|
|
166
176
|
readonly required: false;
|
|
167
177
|
readonly computed: false;
|
|
@@ -335,6 +345,17 @@ export declare const ResPartner: import("../server/orm.js").Model<"res.partner",
|
|
|
335
345
|
readonly hasDefault: false;
|
|
336
346
|
readonly readonly: false;
|
|
337
347
|
}, string>;
|
|
348
|
+
/** State or province (ISO 3166-2: IN-MH, US-CA): the place of supply of taxes split by state. */
|
|
349
|
+
readonly state: import("../server/orm.js").CharField<{
|
|
350
|
+
readonly required: false;
|
|
351
|
+
readonly computed: false;
|
|
352
|
+
readonly inverse: false;
|
|
353
|
+
readonly sequence: false;
|
|
354
|
+
readonly computeName: undefined;
|
|
355
|
+
readonly store: true;
|
|
356
|
+
readonly hasDefault: false;
|
|
357
|
+
readonly readonly: false;
|
|
358
|
+
}, string>;
|
|
338
359
|
/** VAT / BIN / tax identification number. */
|
|
339
360
|
readonly taxId: import("../server/orm.js").CharField<{
|
|
340
361
|
readonly required: false;
|
|
@@ -552,6 +573,16 @@ export declare const contactsShutro: import("../server/orm.js").Module<readonly
|
|
|
552
573
|
readonly hasDefault: false;
|
|
553
574
|
readonly readonly: false;
|
|
554
575
|
}, string>;
|
|
576
|
+
readonly state: import("../server/orm.js").CharField<{
|
|
577
|
+
readonly required: false;
|
|
578
|
+
readonly computed: false;
|
|
579
|
+
readonly inverse: false;
|
|
580
|
+
readonly sequence: false;
|
|
581
|
+
readonly computeName: undefined;
|
|
582
|
+
readonly store: true;
|
|
583
|
+
readonly hasDefault: false;
|
|
584
|
+
readonly readonly: false;
|
|
585
|
+
}, string>;
|
|
555
586
|
readonly taxId: import("../server/orm.js").CharField<{
|
|
556
587
|
readonly required: false;
|
|
557
588
|
readonly computed: false;
|
|
@@ -725,6 +756,17 @@ export declare const contactsShutro: import("../server/orm.js").Module<readonly
|
|
|
725
756
|
readonly hasDefault: false;
|
|
726
757
|
readonly readonly: false;
|
|
727
758
|
}, string>;
|
|
759
|
+
/** State or province (ISO 3166-2: IN-MH, US-CA): the place of supply of taxes split by state. */
|
|
760
|
+
readonly state: import("../server/orm.js").CharField<{
|
|
761
|
+
readonly required: false;
|
|
762
|
+
readonly computed: false;
|
|
763
|
+
readonly inverse: false;
|
|
764
|
+
readonly sequence: false;
|
|
765
|
+
readonly computeName: undefined;
|
|
766
|
+
readonly store: true;
|
|
767
|
+
readonly hasDefault: false;
|
|
768
|
+
readonly readonly: false;
|
|
769
|
+
}, string>;
|
|
728
770
|
/** VAT / BIN / tax identification number. */
|
|
729
771
|
readonly taxId: import("../server/orm.js").CharField<{
|
|
730
772
|
readonly required: false;
|
package/dist/contacts/module.js
CHANGED
|
@@ -32,6 +32,8 @@ export const ResPartner = defineModel('res.partner', {
|
|
|
32
32
|
zip: f.char({ label: 'Postal code' }),
|
|
33
33
|
/** ISO 3166 country code (BD, IN, US, ...). */
|
|
34
34
|
country: f.char({ size: 2 }),
|
|
35
|
+
/** State or province (ISO 3166-2: IN-MH, US-CA): the place of supply of taxes split by state. */
|
|
36
|
+
state: f.char({ size: 6, label: 'State / province' }),
|
|
35
37
|
/** VAT / BIN / tax identification number. */
|
|
36
38
|
taxId: f.char({ label: 'Tax ID' }),
|
|
37
39
|
isCustomer: f.boolean({ default: false, label: 'Customer' }),
|
|
@@ -49,7 +51,7 @@ export const ResPartner = defineModel('res.partner', {
|
|
|
49
51
|
});
|
|
50
52
|
export const contactsShutro = defineModule({
|
|
51
53
|
name: 'contacts',
|
|
52
|
-
version: '0.
|
|
54
|
+
version: '0.2.0',
|
|
53
55
|
depends: [baseShutro],
|
|
54
56
|
groups: [
|
|
55
57
|
orm.defineGroup({ code: 'contacts.user', label: 'Contacts user', implies: ['base.user'] }),
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cells to field values (RFC 0014 §5.1 step 3): numbers with the file's decimal separator, exact decimals as text
|
|
3
|
+
* (money is never a JS `number`), dates in the file's day order or ISO, booleans in English and Bangla, selections by
|
|
4
|
+
* value or label. Bangla digits read as digits. Relations are matched elsewhere (`match.ts`): here they are text.
|
|
5
|
+
*/
|
|
6
|
+
/** A cell as read from a file: CSV cells are text, XLSX cells are typed. */
|
|
7
|
+
export type Cell = string | number | boolean | Date | null;
|
|
8
|
+
export interface ConvertOptions {
|
|
9
|
+
/** Day order of dates written as text (`03/04/2026`). Default `dmy`. */
|
|
10
|
+
readonly dateOrder?: 'dmy' | 'mdy' | 'ymd';
|
|
11
|
+
/** Decimal separator of numbers written as text. Default `.`. */
|
|
12
|
+
readonly decimal?: '.' | ',';
|
|
13
|
+
/** Time zone of date-times without an offset. Default `UTC`. */
|
|
14
|
+
readonly timeZone?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface ConvertField {
|
|
17
|
+
readonly key: string;
|
|
18
|
+
readonly kind: string;
|
|
19
|
+
readonly label: string;
|
|
20
|
+
readonly options: readonly (readonly [string, string])[] | null;
|
|
21
|
+
}
|
|
22
|
+
/** Why a cell could not be read: a stable code for the UI's messages, and an English text. */
|
|
23
|
+
export interface CellError {
|
|
24
|
+
readonly code: 'not-a-number' | 'not-an-integer' | 'not-a-date' | 'not-a-datetime' | 'not-a-boolean' | 'not-an-option' | 'not-importable';
|
|
25
|
+
readonly message: string;
|
|
26
|
+
}
|
|
27
|
+
export type Converted = {
|
|
28
|
+
readonly value: unknown;
|
|
29
|
+
} | {
|
|
30
|
+
readonly error: CellError;
|
|
31
|
+
};
|
|
32
|
+
/** Text with Bangla digits as ASCII digits. */
|
|
33
|
+
export declare function asciiDigits(text: string): string;
|
|
34
|
+
/** Whether a field kind can be imported from a cell (relations included; binary and json are not). */
|
|
35
|
+
export declare function importable(kind: string): boolean;
|
|
36
|
+
/** `1,234.50` (or `1.234,50`) → `1234.50`; null when it is not a number. */
|
|
37
|
+
export declare function plainNumber(text: string, decimal: '.' | ','): string | null;
|
|
38
|
+
/** The calendar date of `text` (`2026-04-03`, `03/04/2026` in the day order, `3.4.26`), or null. */
|
|
39
|
+
export declare function parseDate(text: string, order: 'dmy' | 'mdy' | 'ymd'): string | null;
|
|
40
|
+
/** The instant of a wall-clock time in `timeZone`. */
|
|
41
|
+
export declare function zoned(wall: {
|
|
42
|
+
y: number;
|
|
43
|
+
mo: number;
|
|
44
|
+
d: number;
|
|
45
|
+
h: number;
|
|
46
|
+
mi: number;
|
|
47
|
+
s: number;
|
|
48
|
+
}, timeZone: string): Date;
|
|
49
|
+
/** A date and time from text: ISO with an offset as is, else a wall-clock time in `timeZone`. */
|
|
50
|
+
export declare function parseDateTime(text: string, order: 'dmy' | 'mdy' | 'ymd', timeZone: string): Date | null;
|
|
51
|
+
/**
|
|
52
|
+
* One cell as a value of `field`. Empty cells are `undefined` (left out) — the caller decides between leaving the
|
|
53
|
+
* field to its default and clearing it.
|
|
54
|
+
*/
|
|
55
|
+
export declare function convertCell(field: ConvertField, cell: Cell, options?: ConvertOptions): Converted | undefined;
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cells to field values (RFC 0014 §5.1 step 3): numbers with the file's decimal separator, exact decimals as text
|
|
3
|
+
* (money is never a JS `number`), dates in the file's day order or ISO, booleans in English and Bangla, selections by
|
|
4
|
+
* value or label. Bangla digits read as digits. Relations are matched elsewhere (`match.ts`): here they are text.
|
|
5
|
+
*/
|
|
6
|
+
const BANGLA_DIGITS = /[০-৯]/g;
|
|
7
|
+
/** Text with Bangla digits as ASCII digits. */
|
|
8
|
+
export function asciiDigits(text) {
|
|
9
|
+
return text.replace(BANGLA_DIGITS, (d) => String(d.charCodeAt(0) - 0x09e6));
|
|
10
|
+
}
|
|
11
|
+
const TRUE = new Set(['true', 'yes', 'y', '1', 'x', '✓', 'on', 'হ্যাঁ', 'সত্য', 'হাঁ']);
|
|
12
|
+
const FALSE = new Set(['false', 'no', 'n', '0', 'off', 'না', 'মিথ্যা']);
|
|
13
|
+
/** Whether a field kind can be imported from a cell (relations included; binary and json are not). */
|
|
14
|
+
export function importable(kind) {
|
|
15
|
+
return kind !== 'binary' && kind !== 'json' && kind !== 'one2many';
|
|
16
|
+
}
|
|
17
|
+
/** `1,234.50` (or `1.234,50`) → `1234.50`; null when it is not a number. */
|
|
18
|
+
export function plainNumber(text, decimal) {
|
|
19
|
+
let v = asciiDigits(text.trim()).replace(/[\s '৳$€£¥₹]/g, '');
|
|
20
|
+
let negative = false;
|
|
21
|
+
if (/^\(.*\)$/.test(v)) {
|
|
22
|
+
negative = true; // accounting negatives: (1,200.00)
|
|
23
|
+
v = v.slice(1, -1);
|
|
24
|
+
}
|
|
25
|
+
if (v.startsWith('-')) {
|
|
26
|
+
negative = !negative;
|
|
27
|
+
v = v.slice(1);
|
|
28
|
+
}
|
|
29
|
+
else if (v.startsWith('+'))
|
|
30
|
+
v = v.slice(1);
|
|
31
|
+
const thousands = decimal === '.' ? ',' : '.';
|
|
32
|
+
if (v.includes(thousands)) {
|
|
33
|
+
// Groups of three after the first: 1,234,567 — anything else is not a thousands separator.
|
|
34
|
+
const [whole, ...rest] = v.split(decimal);
|
|
35
|
+
if (rest.length > 1)
|
|
36
|
+
return null;
|
|
37
|
+
const groups = (whole ?? '').split(thousands);
|
|
38
|
+
if (groups.slice(1).some((g) => g.length !== 3) || (groups[0] ?? '').length === 0)
|
|
39
|
+
return null;
|
|
40
|
+
v = [groups.join(''), ...rest].join('.');
|
|
41
|
+
}
|
|
42
|
+
else if (decimal === ',')
|
|
43
|
+
v = v.replace(',', '.');
|
|
44
|
+
if (!/^(\d+(\.\d*)?|\.\d+)$/.test(v))
|
|
45
|
+
return null;
|
|
46
|
+
if (v.startsWith('.'))
|
|
47
|
+
v = `0${v}`;
|
|
48
|
+
if (v.endsWith('.'))
|
|
49
|
+
v = v.slice(0, -1);
|
|
50
|
+
return negative && !/^0(\.0*)?$/.test(v) ? `-${v}` : v;
|
|
51
|
+
}
|
|
52
|
+
/** A JS number as plain decimal text (no exponent). */
|
|
53
|
+
function numberText(n) {
|
|
54
|
+
if (Number.isInteger(n))
|
|
55
|
+
return BigInt(n).toString();
|
|
56
|
+
const text = String(n);
|
|
57
|
+
if (!/e/i.test(text))
|
|
58
|
+
return text;
|
|
59
|
+
return n.toFixed(20).replace(/0+$/, '').replace(/\.$/, '');
|
|
60
|
+
}
|
|
61
|
+
const pad = (n, width = 2) => String(n).padStart(width, '0');
|
|
62
|
+
const ymd = (y, m, d) => `${pad(y, 4)}-${pad(m)}-${pad(d)}`;
|
|
63
|
+
function validDate(y, m, d) {
|
|
64
|
+
const date = new Date(Date.UTC(y, m - 1, d));
|
|
65
|
+
return date.getUTCFullYear() === y && date.getUTCMonth() === m - 1 && date.getUTCDate() === d;
|
|
66
|
+
}
|
|
67
|
+
/** The calendar date of `text` (`2026-04-03`, `03/04/2026` in the day order, `3.4.26`), or null. */
|
|
68
|
+
export function parseDate(text, order) {
|
|
69
|
+
const v = asciiDigits(text.trim());
|
|
70
|
+
const iso = /^(\d{4})-(\d{1,2})-(\d{1,2})/.exec(v);
|
|
71
|
+
let parts = null;
|
|
72
|
+
if (iso !== null)
|
|
73
|
+
parts = [Number(iso[1]), Number(iso[2]), Number(iso[3])];
|
|
74
|
+
else {
|
|
75
|
+
const m = /^(\d{1,4})[/.\-\s](\d{1,2})[/.\-\s](\d{1,4})$/.exec(v);
|
|
76
|
+
if (m === null)
|
|
77
|
+
return null;
|
|
78
|
+
const [a, b, c] = [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
79
|
+
if (m[1].length === 4)
|
|
80
|
+
parts = [a, b, c];
|
|
81
|
+
else {
|
|
82
|
+
const year = m[3].length <= 2 ? 2000 + c : c;
|
|
83
|
+
parts = order === 'mdy' ? [year, a, b] : [year, b, a];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const [y, mo, d] = parts;
|
|
87
|
+
return validDate(y, mo, d) ? ymd(y, mo, d) : null;
|
|
88
|
+
}
|
|
89
|
+
/** The offset of `timeZone` from UTC at `utc`, in milliseconds. */
|
|
90
|
+
function offsetOf(timeZone, utc) {
|
|
91
|
+
const parts = new Intl.DateTimeFormat('en-US', {
|
|
92
|
+
timeZone,
|
|
93
|
+
hourCycle: 'h23',
|
|
94
|
+
year: 'numeric',
|
|
95
|
+
month: '2-digit',
|
|
96
|
+
day: '2-digit',
|
|
97
|
+
hour: '2-digit',
|
|
98
|
+
minute: '2-digit',
|
|
99
|
+
second: '2-digit',
|
|
100
|
+
}).formatToParts(new Date(utc));
|
|
101
|
+
const get = (type) => Number(parts.find((p) => p.type === type)?.value ?? 0);
|
|
102
|
+
const local = Date.UTC(get('year'), get('month') - 1, get('day'), get('hour'), get('minute'), get('second'));
|
|
103
|
+
return local - utc;
|
|
104
|
+
}
|
|
105
|
+
/** The instant of a wall-clock time in `timeZone`. */
|
|
106
|
+
export function zoned(wall, timeZone) {
|
|
107
|
+
const guess = Date.UTC(wall.y, wall.mo - 1, wall.d, wall.h, wall.mi, wall.s);
|
|
108
|
+
const first = guess - offsetOf(timeZone, guess);
|
|
109
|
+
return new Date(guess - offsetOf(timeZone, first));
|
|
110
|
+
}
|
|
111
|
+
/** A date and time from text: ISO with an offset as is, else a wall-clock time in `timeZone`. */
|
|
112
|
+
export function parseDateTime(text, order, timeZone) {
|
|
113
|
+
const v = asciiDigits(text.trim());
|
|
114
|
+
if (/^\d{4}-\d{2}-\d{2}T.*(Z|[+-]\d{2}:?\d{2})$/.test(v)) {
|
|
115
|
+
const date = new Date(v);
|
|
116
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
117
|
+
}
|
|
118
|
+
const m = /^(.+?)(?:[T\s]+(\d{1,2}):(\d{2})(?::(\d{2}))?\s*([AaPp][Mm])?)?$/.exec(v);
|
|
119
|
+
if (m === null)
|
|
120
|
+
return null;
|
|
121
|
+
const day = parseDate(m[1], order);
|
|
122
|
+
if (day === null)
|
|
123
|
+
return null;
|
|
124
|
+
let h = Number(m[2] ?? 0);
|
|
125
|
+
const mi = Number(m[3] ?? 0);
|
|
126
|
+
const s = Number(m[4] ?? 0);
|
|
127
|
+
const meridiem = m[5]?.toLowerCase();
|
|
128
|
+
if (meridiem !== undefined) {
|
|
129
|
+
if (h < 1 || h > 12)
|
|
130
|
+
return null;
|
|
131
|
+
h = (h % 12) + (meridiem === 'pm' ? 12 : 0);
|
|
132
|
+
}
|
|
133
|
+
if (h > 23 || mi > 59 || s > 59)
|
|
134
|
+
return null;
|
|
135
|
+
const [y, mo, d] = day.split('-').map(Number);
|
|
136
|
+
return zoned({ y, mo, d, h, mi, s }, timeZone);
|
|
137
|
+
}
|
|
138
|
+
const fail = (code, field, cell, what) => ({
|
|
139
|
+
error: { code, message: `${field.label}: "${String(cell)}" is not ${what}.` },
|
|
140
|
+
});
|
|
141
|
+
/**
|
|
142
|
+
* One cell as a value of `field`. Empty cells are `undefined` (left out) — the caller decides between leaving the
|
|
143
|
+
* field to its default and clearing it.
|
|
144
|
+
*/
|
|
145
|
+
export function convertCell(field, cell, options = {}) {
|
|
146
|
+
if (cell === null || (typeof cell === 'string' && cell.trim() === ''))
|
|
147
|
+
return undefined;
|
|
148
|
+
const order = options.dateOrder ?? 'dmy';
|
|
149
|
+
const decimal = options.decimal ?? '.';
|
|
150
|
+
const timeZone = options.timeZone ?? 'UTC';
|
|
151
|
+
switch (field.kind) {
|
|
152
|
+
case 'char':
|
|
153
|
+
case 'text':
|
|
154
|
+
case 'html':
|
|
155
|
+
case 'many2one':
|
|
156
|
+
case 'many2many':
|
|
157
|
+
if (cell instanceof Date)
|
|
158
|
+
return { value: cell.toISOString().slice(0, cell.getUTCHours() === 0 ? 10 : 19) };
|
|
159
|
+
return { value: typeof cell === 'number' ? numberText(cell) : String(cell).trim() };
|
|
160
|
+
case 'boolean': {
|
|
161
|
+
if (typeof cell === 'boolean')
|
|
162
|
+
return { value: cell };
|
|
163
|
+
if (typeof cell === 'number' && (cell === 0 || cell === 1))
|
|
164
|
+
return { value: cell === 1 };
|
|
165
|
+
const v = String(cell).trim().toLowerCase();
|
|
166
|
+
if (TRUE.has(v))
|
|
167
|
+
return { value: true };
|
|
168
|
+
if (FALSE.has(v))
|
|
169
|
+
return { value: false };
|
|
170
|
+
return fail('not-a-boolean', field, cell, 'yes or no');
|
|
171
|
+
}
|
|
172
|
+
case 'integer':
|
|
173
|
+
case 'bigint': {
|
|
174
|
+
const text = typeof cell === 'number' ? numberText(cell) : plainNumber(String(cell), decimal);
|
|
175
|
+
if (text === null || !/^-?\d+$/.test(text))
|
|
176
|
+
return fail('not-an-integer', field, cell, 'a whole number');
|
|
177
|
+
return { value: field.kind === 'bigint' ? text : Number(text) };
|
|
178
|
+
}
|
|
179
|
+
case 'float':
|
|
180
|
+
case 'decimal':
|
|
181
|
+
case 'monetary':
|
|
182
|
+
case 'quantity': {
|
|
183
|
+
if (typeof cell === 'boolean' || cell instanceof Date)
|
|
184
|
+
return fail('not-a-number', field, cell, 'a number');
|
|
185
|
+
const text = typeof cell === 'number' ? numberText(cell) : plainNumber(cell, decimal);
|
|
186
|
+
if (text === null)
|
|
187
|
+
return fail('not-a-number', field, cell, 'a number');
|
|
188
|
+
return { value: field.kind === 'float' ? Number(text) : text };
|
|
189
|
+
}
|
|
190
|
+
case 'date': {
|
|
191
|
+
if (cell instanceof Date)
|
|
192
|
+
return { value: cell.toISOString().slice(0, 10) };
|
|
193
|
+
const day = typeof cell === 'string' ? parseDate(cell, order) : null;
|
|
194
|
+
return day === null ? fail('not-a-date', field, cell, 'a date') : { value: day };
|
|
195
|
+
}
|
|
196
|
+
case 'datetime': {
|
|
197
|
+
if (cell instanceof Date) {
|
|
198
|
+
// Spreadsheet dates have no zone: their wall-clock time is in the file's time zone.
|
|
199
|
+
const [y, mo, d, h, mi, s] = [
|
|
200
|
+
cell.getUTCFullYear(),
|
|
201
|
+
cell.getUTCMonth() + 1,
|
|
202
|
+
cell.getUTCDate(),
|
|
203
|
+
cell.getUTCHours(),
|
|
204
|
+
cell.getUTCMinutes(),
|
|
205
|
+
cell.getUTCSeconds(),
|
|
206
|
+
];
|
|
207
|
+
return { value: zoned({ y, mo, d, h, mi, s }, timeZone) };
|
|
208
|
+
}
|
|
209
|
+
const at = typeof cell === 'string' ? parseDateTime(cell, order, timeZone) : null;
|
|
210
|
+
return at === null ? fail('not-a-datetime', field, cell, 'a date and time') : { value: at };
|
|
211
|
+
}
|
|
212
|
+
case 'selection':
|
|
213
|
+
case 'state': {
|
|
214
|
+
const v = (typeof cell === 'number' ? numberText(cell) : String(cell)).trim();
|
|
215
|
+
const lower = v.toLowerCase();
|
|
216
|
+
const option = (field.options ?? []).find(([value, label]) => value === v || label.toLowerCase() === lower);
|
|
217
|
+
return option === undefined
|
|
218
|
+
? fail('not-an-option', field, cell, `one of ${(field.options ?? []).map(([, l]) => l).join(', ')}`)
|
|
219
|
+
: { value: option[0] };
|
|
220
|
+
}
|
|
221
|
+
default:
|
|
222
|
+
return fail('not-importable', field, cell, 'importable');
|
|
223
|
+
}
|
|
224
|
+
}
|