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.
Files changed (273) hide show
  1. package/README.md +74 -67
  2. package/dist/account_portal/.kosh/bindings.d.ts +22 -0
  3. package/dist/account_portal/.kosh/bindings.js +15 -0
  4. package/dist/account_portal/.kosh/schema.d.ts +1380 -0
  5. package/dist/account_portal/index.d.ts +3 -0
  6. package/dist/account_portal/index.js +73 -0
  7. package/dist/account_portal/module.d.ts +5983 -0
  8. package/dist/account_portal/module.js +18 -0
  9. package/dist/accounting/.kosh/bindings.js +1 -1
  10. package/dist/accounting/.kosh/schema.d.ts +172 -9
  11. package/dist/accounting/chart.d.ts +40 -8
  12. package/dist/accounting/chart.js +124 -30
  13. package/dist/accounting/closing.d.ts +6 -1
  14. package/dist/accounting/closing.js +5 -1
  15. package/dist/accounting/demo.js +6 -5
  16. package/dist/accounting/index.d.ts +3 -3
  17. package/dist/accounting/index.js +200 -23
  18. package/dist/accounting/module.d.ts +3326 -1086
  19. package/dist/accounting/module.js +408 -66
  20. package/dist/accounting/payments.d.ts +25 -0
  21. package/dist/accounting/payments.js +96 -4
  22. package/dist/accounting/web/RegisterPayment.d.ts +3 -2
  23. package/dist/accounting/web/RegisterPayment.js +39 -6
  24. package/dist/accounting/web/SettingsPage.js +5 -1
  25. package/dist/accounting/web/messages.js +10 -0
  26. package/dist/assistant/routes.js +1 -1
  27. package/dist/automation/engine.js +1 -1
  28. package/dist/automation/web/messages.js +2 -2
  29. package/dist/cli/apps.d.ts +1 -1
  30. package/dist/cli/apps.js +14 -14
  31. package/dist/cli/bin.js +3 -0
  32. package/dist/cli/index.js +11 -11
  33. package/dist/cli/init.d.ts +1 -1
  34. package/dist/cli/init.js +224 -78
  35. package/dist/cli/launch.d.ts +16 -0
  36. package/dist/cli/launch.js +138 -0
  37. package/dist/cli/project.d.ts +7 -7
  38. package/dist/cli/project.js +25 -25
  39. package/dist/cli/prompt.d.ts +35 -0
  40. package/dist/cli/prompt.js +48 -2
  41. package/dist/cli/templates.d.ts +14 -6
  42. package/dist/cli/templates.js +61 -34
  43. package/dist/cli/tui.d.ts +7 -0
  44. package/dist/cli/tui.js +254 -0
  45. package/dist/cli/ui.d.ts +21 -8
  46. package/dist/cli/ui.js +65 -19
  47. package/dist/contacts/.kosh/bindings.js +1 -1
  48. package/dist/contacts/.kosh/schema.d.ts +33 -1
  49. package/dist/contacts/index.js +24 -1
  50. package/dist/contacts/module.d.ts +42 -0
  51. package/dist/contacts/module.js +3 -1
  52. package/dist/import/convert.d.ts +55 -0
  53. package/dist/import/convert.js +224 -0
  54. package/dist/import/csv.d.ts +28 -0
  55. package/dist/import/csv.js +115 -0
  56. package/dist/import/index.d.ts +33 -0
  57. package/dist/import/index.js +60 -0
  58. package/dist/import/mapping.d.ts +15 -0
  59. package/dist/import/mapping.js +33 -0
  60. package/dist/import/match.d.ts +24 -0
  61. package/dist/import/match.js +60 -0
  62. package/dist/import/models.d.ts +2 -0
  63. package/dist/import/models.js +102 -0
  64. package/dist/import/routes.d.ts +13 -0
  65. package/dist/import/routes.js +392 -0
  66. package/dist/import/run.d.ts +131 -0
  67. package/dist/import/run.js +568 -0
  68. package/dist/import/service.d.ts +153 -0
  69. package/dist/import/service.js +367 -0
  70. package/dist/import/web/ImportButton.d.ts +7 -0
  71. package/dist/import/web/ImportButton.js +31 -0
  72. package/dist/import/web/ImportWizard.d.ts +75 -0
  73. package/dist/import/web/ImportWizard.js +217 -0
  74. package/dist/import/web/index.d.ts +13 -0
  75. package/dist/import/web/index.js +18 -0
  76. package/dist/import/web/messages.d.ts +2 -0
  77. package/dist/import/web/messages.js +139 -0
  78. package/dist/import/worker.d.ts +3 -0
  79. package/dist/import/worker.js +52 -0
  80. package/dist/import/xlsx.d.ts +20 -0
  81. package/dist/import/xlsx.js +192 -0
  82. package/dist/l10n_bd/.kosh/bindings.d.ts +22 -0
  83. package/dist/l10n_bd/.kosh/bindings.js +15 -0
  84. package/dist/l10n_bd/.kosh/schema.d.ts +1311 -0
  85. package/dist/l10n_bd/documents.d.ts +8 -0
  86. package/dist/l10n_bd/documents.js +183 -0
  87. package/dist/l10n_bd/index.d.ts +8 -0
  88. package/dist/l10n_bd/index.js +156 -0
  89. package/dist/l10n_bd/module.d.ts +5705 -0
  90. package/dist/l10n_bd/module.js +163 -0
  91. package/dist/l10n_bd/mushak.d.ts +67 -0
  92. package/dist/l10n_bd/mushak.js +139 -0
  93. package/dist/l10n_bd/routes.d.ts +3 -0
  94. package/dist/l10n_bd/routes.js +205 -0
  95. package/dist/l10n_bd/web/MushakPage.d.ts +7 -0
  96. package/dist/l10n_bd/web/MushakPage.js +53 -0
  97. package/dist/l10n_bd/web/index.d.ts +13 -0
  98. package/dist/l10n_bd/web/index.js +18 -0
  99. package/dist/l10n_bd/web/messages.d.ts +2 -0
  100. package/dist/l10n_bd/web/messages.js +101 -0
  101. package/dist/l10n_in/.kosh/bindings.d.ts +22 -0
  102. package/dist/l10n_in/.kosh/bindings.js +15 -0
  103. package/dist/l10n_in/.kosh/schema.d.ts +1373 -0
  104. package/dist/l10n_in/documents.d.ts +8 -0
  105. package/dist/l10n_in/documents.js +114 -0
  106. package/dist/l10n_in/einvoice.d.ts +253 -0
  107. package/dist/l10n_in/einvoice.js +485 -0
  108. package/dist/l10n_in/gstr.d.ts +109 -0
  109. package/dist/l10n_in/gstr.js +430 -0
  110. package/dist/l10n_in/index.d.ts +20 -0
  111. package/dist/l10n_in/index.js +232 -0
  112. package/dist/l10n_in/module.d.ts +7823 -0
  113. package/dist/l10n_in/module.js +427 -0
  114. package/dist/l10n_in/routes-einvoice.d.ts +15 -0
  115. package/dist/l10n_in/routes-einvoice.js +124 -0
  116. package/dist/l10n_in/routes.d.ts +17 -0
  117. package/dist/l10n_in/routes.js +282 -0
  118. package/dist/l10n_in/web/EInvoicePanel.d.ts +8 -0
  119. package/dist/l10n_in/web/EInvoicePanel.js +63 -0
  120. package/dist/l10n_in/web/GstPage.d.ts +7 -0
  121. package/dist/l10n_in/web/GstPage.js +57 -0
  122. package/dist/l10n_in/web/GstSettings.d.ts +10 -0
  123. package/dist/l10n_in/web/GstSettings.js +36 -0
  124. package/dist/l10n_in/web/index.d.ts +15 -0
  125. package/dist/l10n_in/web/index.js +22 -0
  126. package/dist/l10n_in/web/messages.d.ts +2 -0
  127. package/dist/l10n_in/web/messages.js +166 -0
  128. package/dist/mail/index.js +2 -2
  129. package/dist/modules-sdk/demo.d.ts +2 -0
  130. package/dist/modules-sdk/demo.js +7 -3
  131. package/dist/modules-sdk/index.d.ts +2 -2
  132. package/dist/modules-sdk/index.js +1 -1
  133. package/dist/modules-sdk/loader.js +2 -2
  134. package/dist/modules-sdk/manifest.d.ts +132 -0
  135. package/dist/modules-sdk/manifest.js +22 -0
  136. package/dist/notify/inbox.js +21 -0
  137. package/dist/notify/messages.js +7 -3
  138. package/dist/notify/models.d.ts +1 -1
  139. package/dist/notify/models.js +2 -0
  140. package/dist/notify/routes.js +1 -1
  141. package/dist/notify/web/messages.js +5 -1
  142. package/dist/portal/.kosh/bindings.d.ts +22 -0
  143. package/dist/portal/.kosh/bindings.js +15 -0
  144. package/dist/portal/.kosh/schema.d.ts +376 -0
  145. package/dist/portal/announce.d.ts +16 -0
  146. package/dist/portal/announce.js +117 -0
  147. package/dist/portal/documents.d.ts +17 -0
  148. package/dist/portal/documents.js +61 -0
  149. package/dist/portal/index.d.ts +20 -0
  150. package/dist/portal/index.js +65 -0
  151. package/dist/portal/module.d.ts +1405 -0
  152. package/dist/portal/module.js +84 -0
  153. package/dist/portal/routes-documents.d.ts +2 -0
  154. package/dist/portal/routes-documents.js +391 -0
  155. package/dist/portal/routes.d.ts +13 -0
  156. package/dist/portal/routes.js +313 -0
  157. package/dist/portal/rules.d.ts +11 -0
  158. package/dist/portal/rules.js +25 -0
  159. package/dist/portal/web/PortalAccess.d.ts +8 -0
  160. package/dist/portal/web/PortalAccess.js +66 -0
  161. package/dist/portal/web/index.d.ts +12 -0
  162. package/dist/portal/web/index.js +17 -0
  163. package/dist/portal/web/messages.d.ts +2 -0
  164. package/dist/portal/web/messages.js +33 -0
  165. package/dist/product/.kosh/bindings.js +1 -1
  166. package/dist/product/.kosh/schema.d.ts +42 -3
  167. package/dist/product/demo.js +6 -5
  168. package/dist/product/index.d.ts +1 -0
  169. package/dist/product/index.js +17 -4
  170. package/dist/product/module.d.ts +346 -14
  171. package/dist/product/module.js +48 -18
  172. package/dist/product/taxes.d.ts +59 -0
  173. package/dist/product/taxes.js +61 -0
  174. package/dist/project/.kosh/bindings.js +1 -1
  175. package/dist/project/.kosh/schema.d.ts +33 -1
  176. package/dist/project/module.d.ts +20 -0
  177. package/dist/project_portal/.kosh/bindings.d.ts +22 -0
  178. package/dist/project_portal/.kosh/bindings.js +15 -0
  179. package/dist/project_portal/.kosh/schema.d.ts +701 -0
  180. package/dist/project_portal/index.d.ts +9 -0
  181. package/dist/project_portal/index.js +42 -0
  182. package/dist/project_portal/module.d.ts +2142 -0
  183. package/dist/project_portal/module.js +22 -0
  184. package/dist/purchase/.kosh/bindings.js +1 -1
  185. package/dist/purchase/.kosh/schema.d.ts +45 -4
  186. package/dist/purchase/demo.js +3 -3
  187. package/dist/purchase/index.js +1 -1
  188. package/dist/purchase/module.d.ts +232 -8
  189. package/dist/purchase/module.js +51 -9
  190. package/dist/purchase_account/.kosh/bindings.js +1 -1
  191. package/dist/purchase_account/.kosh/schema.d.ts +174 -10
  192. package/dist/purchase_account/module.d.ts +2008 -641
  193. package/dist/purchase_account/module.js +2 -2
  194. package/dist/purchase_portal/.kosh/bindings.d.ts +22 -0
  195. package/dist/purchase_portal/.kosh/bindings.js +15 -0
  196. package/dist/purchase_portal/.kosh/schema.d.ts +852 -0
  197. package/dist/purchase_portal/index.d.ts +9 -0
  198. package/dist/purchase_portal/index.js +53 -0
  199. package/dist/purchase_portal/module.d.ts +2758 -0
  200. package/dist/purchase_portal/module.js +18 -0
  201. package/dist/reporting/documents.js +20 -3
  202. package/dist/reporting/index.d.ts +1 -0
  203. package/dist/reporting/index.js +1 -0
  204. package/dist/reporting/routes-documents.js +9 -2
  205. package/dist/reporting/words.d.ts +11 -0
  206. package/dist/reporting/words.js +229 -0
  207. package/dist/sale_account/.kosh/bindings.js +1 -1
  208. package/dist/sale_account/.kosh/schema.d.ts +174 -10
  209. package/dist/sale_account/module.d.ts +2008 -641
  210. package/dist/sale_account/module.js +3 -3
  211. package/dist/sale_portal/.kosh/bindings.d.ts +22 -0
  212. package/dist/sale_portal/.kosh/bindings.js +15 -0
  213. package/dist/sale_portal/.kosh/schema.d.ts +766 -0
  214. package/dist/sale_portal/index.d.ts +9 -0
  215. package/dist/sale_portal/index.js +71 -0
  216. package/dist/sale_portal/module.d.ts +2718 -0
  217. package/dist/sale_portal/module.js +19 -0
  218. package/dist/sale_project/.kosh/bindings.js +1 -1
  219. package/dist/sale_project/.kosh/schema.d.ts +45 -4
  220. package/dist/sale_project/module.d.ts +390 -10
  221. package/dist/sales/.kosh/bindings.js +1 -1
  222. package/dist/sales/.kosh/schema.d.ts +45 -4
  223. package/dist/sales/demo.js +3 -3
  224. package/dist/sales/index.js +67 -1
  225. package/dist/sales/module.d.ts +232 -8
  226. package/dist/sales/module.js +51 -12
  227. package/dist/server/api/client.js +1 -1
  228. package/dist/server/api/openapi.js +17 -1
  229. package/dist/server/apps.js +5 -5
  230. package/dist/server/auth/api-keys.d.ts +13 -0
  231. package/dist/server/auth/api-keys.js +65 -0
  232. package/dist/server/auth/base.js +26 -0
  233. package/dist/server/auth/crypto.js +1 -1
  234. package/dist/server/auth/scopes.d.ts +12 -0
  235. package/dist/server/auth/scopes.js +51 -0
  236. package/dist/server/auth/service.d.ts +59 -1
  237. package/dist/server/auth/service.js +236 -6
  238. package/dist/server/demo.d.ts +1 -1
  239. package/dist/server/demo.js +1 -1
  240. package/dist/server/env-names.d.ts +2 -0
  241. package/dist/server/env-names.js +16 -0
  242. package/dist/server/host.js +5 -1
  243. package/dist/server/identity.d.ts +7 -0
  244. package/dist/server/index.d.ts +2 -1
  245. package/dist/server/index.js +2 -1
  246. package/dist/server/registry.js +1 -1
  247. package/dist/server/server.d.ts +5 -0
  248. package/dist/server/server.js +71 -6
  249. package/dist/settings/routes.js +1 -1
  250. package/dist/settings/web/ApiKeysPage.d.ts +8 -0
  251. package/dist/settings/web/ApiKeysPage.js +70 -0
  252. package/dist/settings/web/index.d.ts +1 -0
  253. package/dist/settings/web/index.js +4 -0
  254. package/dist/settings/web/messages.js +62 -0
  255. package/dist/storage/routes.js +1 -1
  256. package/dist/studio/build.js +1 -1
  257. package/dist/studio/generate.js +4 -4
  258. package/dist/studio/module.js +5 -5
  259. package/dist/studio/web/messages.js +2 -2
  260. package/dist/ui/api.d.ts +2 -0
  261. package/dist/ui/app.js +27 -4
  262. package/dist/ui/context.d.ts +3 -1
  263. package/dist/ui/context.js +2 -2
  264. package/dist/ui/i18n/messages.js +2 -2
  265. package/dist/ui/index.d.ts +1 -1
  266. package/dist/ui/index.js +1 -1
  267. package/dist/ui/lib/format.d.ts +2 -0
  268. package/dist/ui/lib/format.js +3 -1
  269. package/dist/ui/pwa/worker.js +2 -2
  270. package/dist/ui/registry.d.ts +15 -0
  271. package/dist/ui/registry.js +20 -0
  272. package/dist/ui/views/FormView.js +19 -6
  273. 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 with icons, spinners for long
3
- * work, status lines (✔ ▲ ✖) and a summary card. Colour, Unicode and animation are detected from the terminal
4
- * (NO_COLOR, FORCE_COLOR, KOSH_ASCII=1, TERM=dumb, Windows consoles without Unicode); output that is not a terminal
5
- * (tests, logs, CI) gets plain lines with the same words and no escape codes.
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
- const unicode = env['KOSH_ASCII'] !== '1' && (platform === 'win32' ? modernWindows : env['TERM'] !== 'linux');
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 = (name, text) => styles[name](terminal.color)(text);
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', 'K O S H')} ${subtitle}\n`);
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(` ${s('gray', `${sym.dot} ${text}`)}\n`),
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: c572e760d811a5f6
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: c572e760d811a5f6
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
  }
@@ -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', [k.group('where', ['street', 'city', 'zip', 'country'])]),
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;
@@ -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.1.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
+ }