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
@@ -0,0 +1,28 @@
1
+ /**
2
+ * A CSV reader without dependencies (RFC 0014 §5.1): the encoding is read from the byte-order mark, else UTF-8 when
3
+ * the bytes are valid UTF-8, else Windows-1252 (spreadsheets saved by older Excel); the separator is guessed from the
4
+ * first lines (`,` `;` tab `|`); quoted cells may hold separators, quotes (`""`) and newlines (RFC 4180).
5
+ */
6
+ export type CsvEncoding = 'utf-8' | 'utf-16le' | 'utf-16be' | 'windows-1252';
7
+ export interface CsvOptions {
8
+ /** Default: guessed. */
9
+ readonly separator?: string;
10
+ /** Default: from the byte-order mark or the bytes. */
11
+ readonly encoding?: CsvEncoding;
12
+ }
13
+ export interface CsvTable {
14
+ readonly rows: string[][];
15
+ readonly separator: string;
16
+ readonly encoding: CsvEncoding;
17
+ }
18
+ /** The text of `bytes` and the encoding it was read in. */
19
+ export declare function decodeText(bytes: Uint8Array, given?: CsvEncoding): {
20
+ text: string;
21
+ encoding: CsvEncoding;
22
+ };
23
+ /** The separator found the same number of times on the most of the first lines (ties: the earlier in the list). */
24
+ export declare function guessSeparator(text: string): string;
25
+ /** Splits `text` into rows of cells. Trailing empty rows are dropped. */
26
+ export declare function parseCsvText(text: string, separator: string): string[][];
27
+ /** Reads a CSV file. */
28
+ export declare function readCsv(bytes: Uint8Array, options?: CsvOptions): CsvTable;
@@ -0,0 +1,115 @@
1
+ /**
2
+ * A CSV reader without dependencies (RFC 0014 §5.1): the encoding is read from the byte-order mark, else UTF-8 when
3
+ * the bytes are valid UTF-8, else Windows-1252 (spreadsheets saved by older Excel); the separator is guessed from the
4
+ * first lines (`,` `;` tab `|`); quoted cells may hold separators, quotes (`""`) and newlines (RFC 4180).
5
+ */
6
+ const SEPARATORS = [',', ';', '\t', '|'];
7
+ /** The text of `bytes` and the encoding it was read in. */
8
+ export function decodeText(bytes, given) {
9
+ const [a, b, c] = bytes;
10
+ let encoding = given;
11
+ if (encoding === undefined) {
12
+ if (a === 0xef && b === 0xbb && c === 0xbf)
13
+ encoding = 'utf-8';
14
+ else if (a === 0xff && b === 0xfe)
15
+ encoding = 'utf-16le';
16
+ else if (a === 0xfe && b === 0xff)
17
+ encoding = 'utf-16be';
18
+ }
19
+ if (encoding !== undefined)
20
+ // TextDecoder drops the byte-order mark of its own encoding.
21
+ return { text: new TextDecoder(encoding).decode(bytes), encoding };
22
+ try {
23
+ return { text: new TextDecoder('utf-8', { fatal: true }).decode(bytes), encoding: 'utf-8' };
24
+ }
25
+ catch {
26
+ return { text: new TextDecoder('windows-1252').decode(bytes), encoding: 'windows-1252' };
27
+ }
28
+ }
29
+ /** Counts of `separator` outside quotes in each of the first lines. */
30
+ function counts(text, separator, lines) {
31
+ const out = [];
32
+ let quoted = false;
33
+ let n = 0;
34
+ for (let i = 0; i < text.length && out.length < lines; i++) {
35
+ const ch = text[i];
36
+ if (ch === '"')
37
+ quoted = !quoted;
38
+ else if (!quoted && ch === separator)
39
+ n += 1;
40
+ else if (!quoted && ch === '\n') {
41
+ out.push(n);
42
+ n = 0;
43
+ }
44
+ }
45
+ if (out.length < lines && n > 0)
46
+ out.push(n);
47
+ return out;
48
+ }
49
+ /** The separator found the same number of times on the most of the first lines (ties: the earlier in the list). */
50
+ export function guessSeparator(text) {
51
+ let best = ',';
52
+ let bestScore = 0;
53
+ for (const separator of SEPARATORS) {
54
+ const seen = counts(text, separator, 10).filter((n) => n > 0);
55
+ const first = seen[0];
56
+ if (first === undefined)
57
+ continue;
58
+ const score = seen.filter((n) => n === first).length * 1000 + first;
59
+ if (score > bestScore) {
60
+ best = separator;
61
+ bestScore = score;
62
+ }
63
+ }
64
+ return best;
65
+ }
66
+ /** Splits `text` into rows of cells. Trailing empty rows are dropped. */
67
+ export function parseCsvText(text, separator) {
68
+ const rows = [];
69
+ let row = [];
70
+ let cell = '';
71
+ let quoted = false;
72
+ for (let i = 0; i < text.length; i++) {
73
+ const ch = text[i];
74
+ if (quoted) {
75
+ if (ch !== '"')
76
+ cell += ch;
77
+ else if (text[i + 1] === '"') {
78
+ cell += '"';
79
+ i += 1;
80
+ }
81
+ else
82
+ quoted = false;
83
+ }
84
+ else if (ch === '"' && cell === '')
85
+ quoted = true;
86
+ else if (ch === separator) {
87
+ row.push(cell);
88
+ cell = '';
89
+ }
90
+ else if (ch === '\n' || ch === '\r') {
91
+ if (ch === '\r' && text[i + 1] === '\n')
92
+ i += 1;
93
+ row.push(cell);
94
+ rows.push(row);
95
+ row = [];
96
+ cell = '';
97
+ }
98
+ else
99
+ cell += ch;
100
+ }
101
+ if (cell !== '' || row.length > 0) {
102
+ row.push(cell);
103
+ rows.push(row);
104
+ }
105
+ while (rows.length > 0 && rows.at(-1).every((c) => c === ''))
106
+ rows.pop();
107
+ return rows;
108
+ }
109
+ /** Reads a CSV file. */
110
+ export function readCsv(bytes, options = {}) {
111
+ const { text, encoding } = decodeText(bytes, options.encoding);
112
+ const body = text.startsWith('') ? text.slice(1) : text;
113
+ const separator = options.separator ?? guessSeparator(body);
114
+ return { rows: parseCsvText(body, separator), separator, encoding };
115
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * @packageDocumentation
3
+ * `shutro-erp/import` (RFC 0014): CSV and XLSX files become records of any served model — columns mapped to fields
4
+ * (guessed from keys and labels), cells converted (decimals exact, dates in the file's order, Bangla digits),
5
+ * relations matched by external id, unique key or name as the user, a dry run with every error on its row and cell,
6
+ * and an all-or-nothing import that is audited. Nothing is bypassed: record rules, field security, constraints and
7
+ * sequences apply as for a record typed in a form.
8
+ */
9
+ import { type KoshModule } from '../modules-sdk/index.js';
10
+ import { type ImportRoutesOptions } from './routes.js';
11
+ export { asciiDigits, type Cell, type CellError, type Converted, type ConvertField, type ConvertOptions, convertCell, importable, parseDate, parseDateTime, plainNumber, } from './convert.js';
12
+ export { type CsvEncoding, type CsvTable, decodeText, guessSeparator, parseCsvText, readCsv, } from './csv.js';
13
+ export { type ColumnMapping, guessMapping, type MappableField, normalize } from './mapping.js';
14
+ export { type Match, type MatchTarget, matchAll, matchOne } from './match.js';
15
+ export { importShutro } from './models.js';
16
+ export { type ImportRoutesOptions, registerImportRoutes } from './routes.js';
17
+ export { EXTERNAL_ID_KEY, externalIdOf, ID_KEY, IMPORT_MODULE, type ImportField, type ImportModels, type ImportOptions, type ImportResult, importFields, type RowError, type RowErrorCode, runImport, type UndoData, type UndoResult, undoImport, WRITE_BATCH, } from './run.js';
18
+ export { createImportService, FINISHED_TOPIC, IMPORT_MAX_BYTES, IMPORT_ROWS, type ImportContext, type ImportService, INLINE_ROWS, parseOptions, RUN_JOB, type SavedMapping, type Session, UNDO_DAYS, } from './service.js';
19
+ export { importWorker } from './worker.js';
20
+ export { fromSerial, isDateFormat, readXlsx, unzip, type XlsxCell, XlsxError, type XlsxSheet, xlsxSheetNames, } from './xlsx.js';
21
+ /** Package identity. */
22
+ export declare const PACKAGE: {
23
+ readonly name: string;
24
+ readonly rfc: string;
25
+ };
26
+ export interface ImportModuleOptions extends Omit<ImportRoutesOptions, 'service'> {
27
+ /** Run background imports in this process (default: unless `KOSH_SCHEDULES=off`). */
28
+ readonly worker?: boolean;
29
+ }
30
+ /** The import module with explicit limits (tests). */
31
+ export declare function createImport(options?: ImportModuleOptions): KoshModule;
32
+ /** Data import, limits from the environment (`KOSH_IMPORT_MAX_BYTES`, `KOSH_IMPORT_MAX_ROWS`). */
33
+ export declare const dataImport: KoshModule;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * @packageDocumentation
3
+ * `shutro-erp/import` (RFC 0014): CSV and XLSX files become records of any served model — columns mapped to fields
4
+ * (guessed from keys and labels), cells converted (decimals exact, dates in the file's order, Bangla digits),
5
+ * relations matched by external id, unique key or name as the user, a dry run with every error on its row and cell,
6
+ * and an all-or-nothing import that is audited. Nothing is bypassed: record rules, field security, constraints and
7
+ * sequences apply as for a record typed in a form.
8
+ */
9
+ import { defineModule } from '../modules-sdk/index.js';
10
+ import { base } from '../server/index.js';
11
+ import { importShutro } from './models.js';
12
+ import { registerImportRoutes } from './routes.js';
13
+ import { createImportService } from './service.js';
14
+ import { importWorker } from './worker.js';
15
+ export { asciiDigits, convertCell, importable, parseDate, parseDateTime, plainNumber, } from './convert.js';
16
+ export { decodeText, guessSeparator, parseCsvText, readCsv, } from './csv.js';
17
+ export { guessMapping, normalize } from './mapping.js';
18
+ export { matchAll, matchOne } from './match.js';
19
+ export { importShutro } from './models.js';
20
+ export { registerImportRoutes } from './routes.js';
21
+ export { EXTERNAL_ID_KEY, externalIdOf, ID_KEY, IMPORT_MODULE, importFields, runImport, undoImport, WRITE_BATCH, } from './run.js';
22
+ export { createImportService, FINISHED_TOPIC, IMPORT_MAX_BYTES, IMPORT_ROWS, INLINE_ROWS, parseOptions, RUN_JOB, UNDO_DAYS, } from './service.js';
23
+ export { importWorker } from './worker.js';
24
+ export { fromSerial, isDateFormat, readXlsx, unzip, XlsxError, xlsxSheetNames, } from './xlsx.js';
25
+ /** Package identity. */
26
+ export const PACKAGE = Object.freeze({
27
+ name: 'shutro-erp/import',
28
+ rfc: 'RFC 0014',
29
+ });
30
+ /** The import module with explicit limits (tests). */
31
+ export function createImport(options = {}) {
32
+ const env = process.env;
33
+ const maxBytes = Number(env['KOSH_IMPORT_MAX_BYTES'] ?? Number.NaN);
34
+ const maxRows = Number(env['KOSH_IMPORT_MAX_ROWS'] ?? Number.NaN);
35
+ return defineModule({
36
+ shutro: importShutro,
37
+ kosh: '^0.1.0',
38
+ depends: [base],
39
+ accessLevels: { user: { label: 'Import data', groups: ['import.user'] } },
40
+ privateModels: ['import.session', 'import.progress', 'import.mapping'],
41
+ routes: (context) => {
42
+ const { worker, ...limits } = options;
43
+ const service = createImportService(context, limits.inlineRows === undefined ? {} : { inlineRows: limits.inlineRows });
44
+ registerImportRoutes(context, {
45
+ ...(Number.isFinite(maxBytes) && maxBytes > 0 ? { maxBytes } : {}),
46
+ ...(Number.isFinite(maxRows) && maxRows > 0 ? { maxRows } : {}),
47
+ ...limits,
48
+ service,
49
+ });
50
+ // Background work is off in the CLI (kosh init, migrate), like the schedules.
51
+ if (!(worker ?? env['KOSH_SCHEDULES'] !== 'off'))
52
+ return;
53
+ const running = importWorker(context, service);
54
+ running.start();
55
+ context.app.addHook('onClose', () => running.stop());
56
+ },
57
+ });
58
+ }
59
+ /** Data import, limits from the environment (`KOSH_IMPORT_MAX_BYTES`, `KOSH_IMPORT_MAX_ROWS`). */
60
+ export const dataImport = createImport();
@@ -0,0 +1,15 @@
1
+ export interface MappableField {
2
+ readonly key: string;
3
+ readonly kind: string;
4
+ readonly label: string;
5
+ }
6
+ /** A column of the file and the field it fills, or null to skip it. */
7
+ export interface ColumnMapping {
8
+ readonly index: number;
9
+ readonly header: string;
10
+ readonly field: string | null;
11
+ }
12
+ /** Lower case, letters and digits only (Bangla letters kept). */
13
+ export declare function normalize(text: string): string;
14
+ /** The field each header most likely fills: an exact key, then an exact label; each field once. */
15
+ export declare function guessMapping(headers: readonly string[], fields: readonly MappableField[]): ColumnMapping[];
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Columns to fields (RFC 0014 §5.1 step 2): each header is matched to a field by its key or its label, ignoring case,
3
+ * spaces and punctuation, so an export of Kosh (headers are labels) re-imports without edits. Unmatched columns
4
+ * are skipped until the user maps them.
5
+ */
6
+ import { importable } from './convert.js';
7
+ /** Lower case, letters and digits only (Bangla letters kept). */
8
+ export function normalize(text) {
9
+ return text
10
+ .normalize('NFKC')
11
+ .toLowerCase()
12
+ .replace(/[^\p{L}\p{M}\p{N}]+/gu, '');
13
+ }
14
+ /** The field each header most likely fills: an exact key, then an exact label; each field once. */
15
+ export function guessMapping(headers, fields) {
16
+ const candidates = fields.filter((f) => importable(f.kind));
17
+ const taken = new Set();
18
+ const byKey = new Map(candidates.map((f) => [normalize(f.key), f.key]));
19
+ const byLabel = new Map();
20
+ for (const f of candidates) {
21
+ const label = normalize(f.label);
22
+ if (!byLabel.has(label))
23
+ byLabel.set(label, f.key);
24
+ }
25
+ return headers.map((header, index) => {
26
+ const n = normalize(header);
27
+ const found = byKey.get(n) ?? byLabel.get(n) ?? null;
28
+ if (found === null || taken.has(found) || n === '')
29
+ return { index, header, field: null };
30
+ taken.add(found);
31
+ return { index, header, field: found };
32
+ });
33
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Relation cells to record ids (RFC 0014 §5.1 step 3), read as the user: an external id (`contacts.partner_acme`),
3
+ * then a field the target keeps unique (a code, an email), then its display name — exactly, ignoring case, then as
4
+ * the one record whose name contains the text. Several matches are an error, never a guess.
5
+ */
6
+ import { type Env } from '../server/orm.js';
7
+ /** How the records of a target model are found from text. */
8
+ export interface MatchTarget {
9
+ readonly model: string;
10
+ /** The field the display name comes from. */
11
+ readonly recName: string;
12
+ /** Stored char fields with a unique constraint of their own (besides the company). */
13
+ readonly keys: readonly string[];
14
+ }
15
+ export type Match = {
16
+ readonly id: number;
17
+ readonly by: 'id' | 'external-id' | 'key' | 'name';
18
+ } | {
19
+ readonly error: 'not-found' | 'ambiguous';
20
+ };
21
+ /** The record `text` names in `target`, as the user sees it. */
22
+ export declare function matchOne(env: Env, target: MatchTarget, text: string): Promise<Match>;
23
+ /** Each distinct text once (a file names the same customer on many rows). */
24
+ export declare function matchAll(env: Env, target: MatchTarget, texts: Iterable<string>): Promise<Map<string, Match>>;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Relation cells to record ids (RFC 0014 §5.1 step 3), read as the user: an external id (`contacts.partner_acme`),
3
+ * then a field the target keeps unique (a code, an email), then its display name — exactly, ignoring case, then as
4
+ * the one record whose name contains the text. Several matches are an error, never a guess.
5
+ */
6
+ import { orm } from '../server/orm.js';
7
+ const ref = (env, name) => env.model(name);
8
+ const EXTERNAL_ID = /^[a-z0-9_]+\.[a-z0-9_.-]+$/i;
9
+ const escapeLike = (text) => text.replace(/[%_\\]/g, (m) => `\\${m}`);
10
+ /** The record `text` names in `target`, as the user sees it. */
11
+ export async function matchOne(env, target, text) {
12
+ const model = ref(env, target.model);
13
+ if (/^#\d+$/.test(text)) {
14
+ // `#42`: a record id, as an export with ids writes it.
15
+ const found = await model.search({ where: { id: Number(text.slice(1)) }, limit: 1 });
16
+ return found.length === 1 ? { id: found.ids[0], by: 'id' } : { error: 'not-found' };
17
+ }
18
+ if (EXTERNAL_ID.test(text)) {
19
+ try {
20
+ const id = Number(await env.refId(target.model, text));
21
+ const found = await model.search({ where: { id }, limit: 1 });
22
+ if (found.length === 1)
23
+ return { id, by: 'external-id' };
24
+ }
25
+ catch (error) {
26
+ if (!(error instanceof orm.ShutroError))
27
+ throw error; // an unknown id: try the other ways
28
+ }
29
+ }
30
+ for (const key of target.keys) {
31
+ const found = await model.search({ where: { [key]: text }, limit: 2 });
32
+ if (found.length === 1)
33
+ return { id: found.ids[0], by: 'key' };
34
+ if (found.length > 1)
35
+ return { error: 'ambiguous' };
36
+ }
37
+ const exact = await model.search({
38
+ where: { [target.recName]: { ilike: escapeLike(text) } },
39
+ limit: 2,
40
+ });
41
+ if (exact.length === 1)
42
+ return { id: exact.ids[0], by: 'name' };
43
+ if (exact.length > 1)
44
+ return { error: 'ambiguous' };
45
+ const partial = await model.search({
46
+ where: { [target.recName]: { ilike: `%${escapeLike(text)}%` } },
47
+ limit: 2,
48
+ });
49
+ if (partial.length === 1)
50
+ return { id: partial.ids[0], by: 'name' };
51
+ return { error: partial.length > 1 ? 'ambiguous' : 'not-found' };
52
+ }
53
+ /** Each distinct text once (a file names the same customer on many rows). */
54
+ export async function matchAll(env, target, texts) {
55
+ const out = new Map();
56
+ for (const text of texts)
57
+ if (!out.has(text))
58
+ out.set(text, await matchOne(env, target, text));
59
+ return out;
60
+ }
@@ -0,0 +1,2 @@
1
+ import { orm } from '../server/orm.js';
2
+ export declare const importShutro: ReturnType<typeof orm.defineModule>;
@@ -0,0 +1,102 @@
1
+ /**
2
+ * The import's models (RFC 0014):
3
+ *
4
+ * - `import.session` (§5.1): the uploaded file, its mapping and options, and what became of it. A session belongs to
5
+ * the user who uploaded it; the routes read it as that user (record rule) and nobody else sees the file.
6
+ * - `import.progress` (§5.3): how far a background run is, written by the job in short transactions of its own (the
7
+ * job's transaction commits only at the end); read through the routes.
8
+ * - `import.mapping` (§5.4): a saved mapping (columns by header, options) for a model, the user's own or shared with
9
+ * every import user of the company.
10
+ */
11
+ import { baseShutro } from '../server/index.js';
12
+ import { orm } from '../server/orm.js';
13
+ const { defineGroup, defineModel, defineModule, f } = orm;
14
+ const Session = defineModel('import.session', {
15
+ label: 'Import',
16
+ order: ['id desc'],
17
+ companyScoped: { required: true },
18
+ access: { 'import.user': ['read', 'write', 'create', 'unlink'] },
19
+ rules: {
20
+ own: {
21
+ groups: ['import.user'],
22
+ perms: ['read', 'write', 'create', 'unlink'],
23
+ domain: { user: { $env: 'uid' } },
24
+ },
25
+ },
26
+ fields: {
27
+ user: f.many2one('res.users', { required: true, ondelete: 'cascade' }),
28
+ /** The model the rows become (`res.partner`). */
29
+ resModel: f.char({ required: true, label: 'Model' }),
30
+ fileName: f.char({ required: true, size: 255 }),
31
+ fileType: f.selection({ csv: 'CSV', xlsx: 'Excel' }, { required: true }),
32
+ file: f.binary({ required: true }),
33
+ /** The XLSX sheet read (the first by default). */
34
+ sheet: f.char({ size: 100 }),
35
+ /** `[{ index, header, field }]`. */
36
+ columns: f.json({ required: true, default: [] }),
37
+ /** Conversion options and the mode (`ImportOptions`). */
38
+ options: f.json({ required: true, default: {} }),
39
+ state: f.selection({ draft: 'Draft', running: 'Running', done: 'Imported', undone: 'Undone' }, { required: true, default: 'draft' }),
40
+ rowCount: f.integer({ required: true, default: 0, label: 'Rows' }),
41
+ /** The ids the commit created. */
42
+ createdIds: f.json({ label: 'Created records' }),
43
+ /** What undo needs (`UndoData`, RFC 0014 §5.5). */
44
+ undo: f.json(),
45
+ /** The errors and file rows of the records left out (`onError: 'skip'`), for the rejected-rows file. */
46
+ rejected: f.json(),
47
+ importedAt: f.datetime({ label: 'Imported on' }),
48
+ /** The last dry run or commit as the API shows it (background runs leave it here). */
49
+ result: f.json(),
50
+ /** Whether `result` is a dry run's. */
51
+ resultDry: f.boolean({ required: true, default: false }),
52
+ /** Why a background run could not finish (a fault, not the data). */
53
+ failure: f.char({ size: 500 }),
54
+ },
55
+ });
56
+ const Progress = defineModel('import.progress', {
57
+ label: 'Import progress',
58
+ access: {},
59
+ fields: {
60
+ session: f.many2one('import.session', { required: true, ondelete: 'cascade' }),
61
+ done: f.integer({ required: true, default: 0 }),
62
+ total: f.integer({ required: true, default: 0 }),
63
+ },
64
+ sqlConstraints: {
65
+ onePerSession: { unique: ['session'], message: 'One progress row per import.' },
66
+ },
67
+ });
68
+ const Mapping = defineModel('import.mapping', {
69
+ label: 'Import mapping',
70
+ order: ['name'],
71
+ companyScoped: { required: true },
72
+ access: { 'import.user': ['read', 'write', 'create', 'unlink'] },
73
+ rules: {
74
+ visible: {
75
+ groups: ['import.user'],
76
+ perms: ['read'],
77
+ domain: { or: [{ user: { $env: 'uid' } }, { shared: true }] },
78
+ },
79
+ own: {
80
+ groups: ['import.user'],
81
+ perms: ['write', 'create', 'unlink'],
82
+ domain: { user: { $env: 'uid' } },
83
+ },
84
+ },
85
+ fields: {
86
+ name: f.char({ required: true, size: 100 }),
87
+ resModel: f.char({ required: true, label: 'Model' }),
88
+ user: f.many2one('res.users', { required: true, ondelete: 'cascade', label: 'Owner' }),
89
+ /** Every import user of the company sees it (only its owner changes it). */
90
+ shared: f.boolean({ required: true, default: false }),
91
+ /** `[{ header, field }]`: by header, so a file with the columns in another order still matches. */
92
+ columns: f.json({ required: true, default: [] }),
93
+ options: f.json({ required: true, default: {} }),
94
+ },
95
+ });
96
+ export const importShutro = defineModule({
97
+ name: 'import',
98
+ version: '0.1.0',
99
+ depends: [baseShutro],
100
+ groups: [defineGroup({ code: 'import.user', label: 'Import data', implies: ['base.user'] })],
101
+ models: [Session, Progress, Mapping],
102
+ });
@@ -0,0 +1,13 @@
1
+ import { type ModuleContext } from '../server/index.js';
2
+ import { type ImportService } from './service.js';
3
+ export interface ImportRoutesOptions {
4
+ /** Bytes per file. */
5
+ readonly maxBytes?: number;
6
+ /** Rows per file. */
7
+ readonly maxRows?: number;
8
+ /** Rows run in the request; above, a background job. */
9
+ readonly inlineRows?: number;
10
+ /** The service the worker shares (made from the context by default). */
11
+ readonly service?: ImportService;
12
+ }
13
+ export declare function registerImportRoutes(context: ModuleContext, options?: ImportRoutesOptions): void;