shutro-erp 0.2.0 → 0.3.0

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 (232) hide show
  1. package/README.md +17 -10
  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 +1350 -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 +142 -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/cli/index.d.ts +1 -1
  27. package/dist/cli/index.js +1 -1
  28. package/dist/cli/init.d.ts +5 -0
  29. package/dist/cli/init.js +195 -53
  30. package/dist/cli/launch.d.ts +16 -0
  31. package/dist/cli/launch.js +138 -0
  32. package/dist/cli/prompt.d.ts +35 -0
  33. package/dist/cli/prompt.js +46 -0
  34. package/dist/cli/templates.d.ts +11 -3
  35. package/dist/cli/templates.js +32 -7
  36. package/dist/cli/tui.d.ts +7 -0
  37. package/dist/cli/tui.js +254 -0
  38. package/dist/cli/ui.d.ts +25 -5
  39. package/dist/cli/ui.js +49 -9
  40. package/dist/contacts/.kosh/bindings.js +1 -1
  41. package/dist/contacts/.kosh/schema.d.ts +3 -1
  42. package/dist/contacts/index.js +24 -1
  43. package/dist/contacts/module.d.ts +42 -0
  44. package/dist/contacts/module.js +3 -1
  45. package/dist/import/convert.d.ts +55 -0
  46. package/dist/import/convert.js +224 -0
  47. package/dist/import/csv.d.ts +28 -0
  48. package/dist/import/csv.js +115 -0
  49. package/dist/import/index.d.ts +33 -0
  50. package/dist/import/index.js +60 -0
  51. package/dist/import/mapping.d.ts +15 -0
  52. package/dist/import/mapping.js +33 -0
  53. package/dist/import/match.d.ts +24 -0
  54. package/dist/import/match.js +60 -0
  55. package/dist/import/models.d.ts +2 -0
  56. package/dist/import/models.js +102 -0
  57. package/dist/import/routes.d.ts +13 -0
  58. package/dist/import/routes.js +392 -0
  59. package/dist/import/run.d.ts +131 -0
  60. package/dist/import/run.js +568 -0
  61. package/dist/import/service.d.ts +153 -0
  62. package/dist/import/service.js +367 -0
  63. package/dist/import/web/ImportButton.d.ts +7 -0
  64. package/dist/import/web/ImportButton.js +31 -0
  65. package/dist/import/web/ImportWizard.d.ts +75 -0
  66. package/dist/import/web/ImportWizard.js +217 -0
  67. package/dist/import/web/index.d.ts +13 -0
  68. package/dist/import/web/index.js +18 -0
  69. package/dist/import/web/messages.d.ts +2 -0
  70. package/dist/import/web/messages.js +139 -0
  71. package/dist/import/worker.d.ts +3 -0
  72. package/dist/import/worker.js +52 -0
  73. package/dist/import/xlsx.d.ts +20 -0
  74. package/dist/import/xlsx.js +192 -0
  75. package/dist/l10n_bd/.kosh/bindings.d.ts +22 -0
  76. package/dist/l10n_bd/.kosh/bindings.js +15 -0
  77. package/dist/l10n_bd/.kosh/schema.d.ts +1281 -0
  78. package/dist/l10n_bd/documents.d.ts +8 -0
  79. package/dist/l10n_bd/documents.js +183 -0
  80. package/dist/l10n_bd/index.d.ts +8 -0
  81. package/dist/l10n_bd/index.js +156 -0
  82. package/dist/l10n_bd/module.d.ts +5705 -0
  83. package/dist/l10n_bd/module.js +163 -0
  84. package/dist/l10n_bd/mushak.d.ts +67 -0
  85. package/dist/l10n_bd/mushak.js +139 -0
  86. package/dist/l10n_bd/routes.d.ts +3 -0
  87. package/dist/l10n_bd/routes.js +205 -0
  88. package/dist/l10n_bd/web/MushakPage.d.ts +7 -0
  89. package/dist/l10n_bd/web/MushakPage.js +53 -0
  90. package/dist/l10n_bd/web/index.d.ts +13 -0
  91. package/dist/l10n_bd/web/index.js +18 -0
  92. package/dist/l10n_bd/web/messages.d.ts +2 -0
  93. package/dist/l10n_bd/web/messages.js +101 -0
  94. package/dist/l10n_in/.kosh/bindings.d.ts +22 -0
  95. package/dist/l10n_in/.kosh/bindings.js +15 -0
  96. package/dist/l10n_in/.kosh/schema.d.ts +1343 -0
  97. package/dist/l10n_in/documents.d.ts +8 -0
  98. package/dist/l10n_in/documents.js +114 -0
  99. package/dist/l10n_in/einvoice.d.ts +253 -0
  100. package/dist/l10n_in/einvoice.js +485 -0
  101. package/dist/l10n_in/gstr.d.ts +109 -0
  102. package/dist/l10n_in/gstr.js +430 -0
  103. package/dist/l10n_in/index.d.ts +20 -0
  104. package/dist/l10n_in/index.js +232 -0
  105. package/dist/l10n_in/module.d.ts +7823 -0
  106. package/dist/l10n_in/module.js +427 -0
  107. package/dist/l10n_in/routes-einvoice.d.ts +15 -0
  108. package/dist/l10n_in/routes-einvoice.js +124 -0
  109. package/dist/l10n_in/routes.d.ts +17 -0
  110. package/dist/l10n_in/routes.js +282 -0
  111. package/dist/l10n_in/web/EInvoicePanel.d.ts +8 -0
  112. package/dist/l10n_in/web/EInvoicePanel.js +63 -0
  113. package/dist/l10n_in/web/GstPage.d.ts +7 -0
  114. package/dist/l10n_in/web/GstPage.js +57 -0
  115. package/dist/l10n_in/web/GstSettings.d.ts +10 -0
  116. package/dist/l10n_in/web/GstSettings.js +36 -0
  117. package/dist/l10n_in/web/index.d.ts +15 -0
  118. package/dist/l10n_in/web/index.js +22 -0
  119. package/dist/l10n_in/web/messages.d.ts +2 -0
  120. package/dist/l10n_in/web/messages.js +166 -0
  121. package/dist/modules-sdk/demo.d.ts +2 -0
  122. package/dist/modules-sdk/demo.js +7 -3
  123. package/dist/modules-sdk/index.d.ts +2 -2
  124. package/dist/modules-sdk/index.js +1 -1
  125. package/dist/modules-sdk/manifest.d.ts +132 -0
  126. package/dist/modules-sdk/manifest.js +22 -0
  127. package/dist/notify/inbox.js +21 -0
  128. package/dist/notify/messages.js +4 -0
  129. package/dist/notify/models.d.ts +1 -1
  130. package/dist/notify/models.js +2 -0
  131. package/dist/notify/web/messages.js +4 -0
  132. package/dist/portal/.kosh/bindings.d.ts +22 -0
  133. package/dist/portal/.kosh/bindings.js +15 -0
  134. package/dist/portal/.kosh/schema.d.ts +346 -0
  135. package/dist/portal/announce.d.ts +16 -0
  136. package/dist/portal/announce.js +117 -0
  137. package/dist/portal/documents.d.ts +17 -0
  138. package/dist/portal/documents.js +61 -0
  139. package/dist/portal/index.d.ts +20 -0
  140. package/dist/portal/index.js +65 -0
  141. package/dist/portal/module.d.ts +1405 -0
  142. package/dist/portal/module.js +84 -0
  143. package/dist/portal/routes-documents.d.ts +2 -0
  144. package/dist/portal/routes-documents.js +391 -0
  145. package/dist/portal/routes.d.ts +13 -0
  146. package/dist/portal/routes.js +313 -0
  147. package/dist/portal/rules.d.ts +11 -0
  148. package/dist/portal/rules.js +25 -0
  149. package/dist/portal/web/PortalAccess.d.ts +8 -0
  150. package/dist/portal/web/PortalAccess.js +66 -0
  151. package/dist/portal/web/index.d.ts +12 -0
  152. package/dist/portal/web/index.js +17 -0
  153. package/dist/portal/web/messages.d.ts +2 -0
  154. package/dist/portal/web/messages.js +33 -0
  155. package/dist/product/.kosh/bindings.js +1 -1
  156. package/dist/product/.kosh/schema.d.ts +12 -3
  157. package/dist/product/demo.js +6 -5
  158. package/dist/product/index.d.ts +1 -0
  159. package/dist/product/index.js +17 -4
  160. package/dist/product/module.d.ts +346 -14
  161. package/dist/product/module.js +48 -18
  162. package/dist/product/taxes.d.ts +59 -0
  163. package/dist/product/taxes.js +61 -0
  164. package/dist/project/.kosh/bindings.js +1 -1
  165. package/dist/project/.kosh/schema.d.ts +3 -1
  166. package/dist/project/module.d.ts +20 -0
  167. package/dist/project_portal/.kosh/bindings.d.ts +22 -0
  168. package/dist/project_portal/.kosh/bindings.js +15 -0
  169. package/dist/project_portal/.kosh/schema.d.ts +671 -0
  170. package/dist/project_portal/index.d.ts +9 -0
  171. package/dist/project_portal/index.js +42 -0
  172. package/dist/project_portal/module.d.ts +2142 -0
  173. package/dist/project_portal/module.js +22 -0
  174. package/dist/purchase/.kosh/bindings.js +1 -1
  175. package/dist/purchase/.kosh/schema.d.ts +15 -4
  176. package/dist/purchase/demo.js +3 -3
  177. package/dist/purchase/index.js +1 -1
  178. package/dist/purchase/module.d.ts +232 -8
  179. package/dist/purchase/module.js +51 -9
  180. package/dist/purchase_account/.kosh/bindings.js +1 -1
  181. package/dist/purchase_account/.kosh/schema.d.ts +144 -10
  182. package/dist/purchase_account/module.d.ts +2008 -641
  183. package/dist/purchase_account/module.js +2 -2
  184. package/dist/purchase_portal/.kosh/bindings.d.ts +22 -0
  185. package/dist/purchase_portal/.kosh/bindings.js +15 -0
  186. package/dist/purchase_portal/.kosh/schema.d.ts +822 -0
  187. package/dist/purchase_portal/index.d.ts +9 -0
  188. package/dist/purchase_portal/index.js +53 -0
  189. package/dist/purchase_portal/module.d.ts +2758 -0
  190. package/dist/purchase_portal/module.js +18 -0
  191. package/dist/reporting/documents.js +20 -3
  192. package/dist/reporting/index.d.ts +1 -0
  193. package/dist/reporting/index.js +1 -0
  194. package/dist/reporting/routes-documents.js +9 -2
  195. package/dist/reporting/words.d.ts +11 -0
  196. package/dist/reporting/words.js +229 -0
  197. package/dist/sale_account/.kosh/bindings.js +1 -1
  198. package/dist/sale_account/.kosh/schema.d.ts +144 -10
  199. package/dist/sale_account/module.d.ts +2008 -641
  200. package/dist/sale_account/module.js +3 -3
  201. package/dist/sale_portal/.kosh/bindings.d.ts +22 -0
  202. package/dist/sale_portal/.kosh/bindings.js +15 -0
  203. package/dist/sale_portal/.kosh/schema.d.ts +736 -0
  204. package/dist/sale_portal/index.d.ts +9 -0
  205. package/dist/sale_portal/index.js +71 -0
  206. package/dist/sale_portal/module.d.ts +2718 -0
  207. package/dist/sale_portal/module.js +19 -0
  208. package/dist/sale_project/.kosh/bindings.js +1 -1
  209. package/dist/sale_project/.kosh/schema.d.ts +15 -4
  210. package/dist/sale_project/module.d.ts +390 -10
  211. package/dist/sales/.kosh/bindings.js +1 -1
  212. package/dist/sales/.kosh/schema.d.ts +15 -4
  213. package/dist/sales/demo.js +3 -3
  214. package/dist/sales/index.js +67 -1
  215. package/dist/sales/module.d.ts +232 -8
  216. package/dist/sales/module.js +51 -12
  217. package/dist/server/auth/base.js +2 -0
  218. package/dist/server/auth/service.d.ts +2 -0
  219. package/dist/server/auth/service.js +7 -1
  220. package/dist/server/index.d.ts +1 -1
  221. package/dist/server/index.js +1 -1
  222. package/dist/server/server.js +26 -1
  223. package/dist/ui/api.d.ts +2 -0
  224. package/dist/ui/app.js +1 -1
  225. package/dist/ui/context.d.ts +3 -1
  226. package/dist/ui/context.js +2 -2
  227. package/dist/ui/index.d.ts +1 -1
  228. package/dist/ui/index.js +1 -1
  229. package/dist/ui/lib/format.d.ts +2 -0
  230. package/dist/ui/lib/format.js +3 -1
  231. package/dist/ui/views/FormView.js +19 -6
  232. package/package.json +50 -2
@@ -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
+ }
@@ -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[];