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
@@ -320,12 +320,18 @@ export class AuthService {
320
320
  const [credential] = await fetch(model(env, 'auth.credential').search({ where: { user: session.user } }), ['totpEnabled']);
321
321
  const administrator = await this.#holdsAdministrator(env, session.user);
322
322
  const twoFactorEnabled = credential?.['totpEnabled'] === true;
323
+ const companyIds = user?.['companies'] ?? [];
324
+ const groupings = {};
325
+ if (companyIds.length > 0)
326
+ for (const c of await fetch(model(env, 'res.company').search({ where: { id: { in: companyIds } } }), ['numberGrouping']))
327
+ groupings[String(c['id'])] = c['numberGrouping'] === 'lakh' ? 'lakh' : 'international';
323
328
  return {
324
329
  id: session.user,
325
330
  login: String(user?.['login']),
326
331
  name: String(user?.['name']),
327
332
  company: typeof user?.['company'] === 'number' ? user['company'] : null,
328
- companies: user?.['companies'] ?? [],
333
+ companies: companyIds,
334
+ groupings,
329
335
  twoFactorEnabled,
330
336
  administrator,
331
337
  administratorNeedsSecondFactor: administrator && !(session.secondFactor && twoFactorEnabled),
@@ -5,7 +5,7 @@
5
5
  */
6
6
  export { announceChange, CHANGES_CHANNEL, parseChange, type RecordChange } from './api/changes.js';
7
7
  export { generateClient, typeOf } from './api/client.js';
8
- export { IDEMPOTENCY_TABLE, idempotencyTableStatements, type Outcome } from './api/idempotency.js';
8
+ export { IDEMPOTENCY_TABLE, idempotencyTableStatements, idempotent, type Outcome, } from './api/idempotency.js';
9
9
  export { type ApiField, type ApiModel, apiModels, defaultFields, publicApiModels, schemaName, } from './api/metadata.js';
10
10
  export { type FoundRecord, type MenuNode, moduleVisible, Navigation } from './api/navigation.js';
11
11
  export { fieldSchema, openApiDocument, openApiOf } from './api/openapi.js';
@@ -5,7 +5,7 @@
5
5
  */
6
6
  export { announceChange, CHANGES_CHANNEL, parseChange } from './api/changes.js';
7
7
  export { generateClient, typeOf } from './api/client.js';
8
- export { IDEMPOTENCY_TABLE, idempotencyTableStatements } from './api/idempotency.js';
8
+ export { IDEMPOTENCY_TABLE, idempotencyTableStatements, idempotent, } from './api/idempotency.js';
9
9
  export { apiModels, defaultFields, publicApiModels, schemaName, } from './api/metadata.js';
10
10
  export { moduleVisible, Navigation } from './api/navigation.js';
11
11
  export { fieldSchema, openApiDocument, openApiOf } from './api/openapi.js';
@@ -14,7 +14,7 @@ import { registerRestApi } from './api/rest.js';
14
14
  import { ViewRegistry } from './api/views.js';
15
15
  import { registerAuthRoutes } from './auth/routes.js';
16
16
  import { AuthService } from './auth/service.js';
17
- import { HttpProblem, problemOf } from './errors.js';
17
+ import { HttpProblem, problemOf, ServerConfigError } from './errors.js';
18
18
  import { denyAll, runOptionsOf } from './identity.js';
19
19
  import { orm, pg } from './orm.js';
20
20
  import { buildKoshRegistry, KOSH_VERSION } from './registry.js';
@@ -25,6 +25,22 @@ const REDACT = [
25
25
  'req.headers["x-api-key"]',
26
26
  'res.headers["set-cookie"]',
27
27
  ];
28
+ /** What a portal user reaches: the portal API, and signing in and out (RFC 0015 §5.1). */
29
+ const PORTAL_PATHS = ['/api/portal/', '/api/v1/auth/'];
30
+ /**
31
+ * The external groups the modules declare (RFC 0015 §5.1). @throws {ServerConfigError} for one implying
32
+ * `base.user`: its holders would be internal users.
33
+ */
34
+ function externalGroupsOf(registry) {
35
+ const groups = new Set(registry.loaded.modules.flatMap((m) => m.externalGroups ?? []));
36
+ for (const group of groups) {
37
+ if (!registry.registry.security.groups.has(group))
38
+ throw new ServerConfigError(`The external group '${group}' is not defined.`, 'Define it in the module that declares it.');
39
+ if (registry.registry.security.closure([group]).has('base.user'))
40
+ throw new ServerConfigError(`The external group '${group}' implies base.user: its users would be internal.`, 'Remove base.user from what it implies.');
41
+ }
42
+ return groups;
43
+ }
28
44
  /** Creates the server (not listening yet). @throws {ServerConfigError} for modules that cannot load together */
29
45
  export async function createServer(options) {
30
46
  const registry = buildKoshRegistry(options.modules, {
@@ -37,6 +53,7 @@ export async function createServer(options) {
37
53
  applicationName: 'kosh',
38
54
  });
39
55
  const hasBase = registry.registry.models.has('auth.session');
56
+ const external = externalGroupsOf(registry);
40
57
  let shutro;
41
58
  const auth = hasBase ? new AuthService(() => shutro, options.auth) : undefined;
42
59
  const verify = auth !== undefined && (options.verifyIdentities ?? options.authenticate === undefined);
@@ -101,6 +118,14 @@ export async function createServer(options) {
101
118
  if (config.public === true || config.auth === false)
102
119
  return;
103
120
  request.identity = await authenticate(request);
121
+ // Customers and vendors of the portal (RFC 0015 §5.1): the portal API and signing in, nothing else.
122
+ if (request.identity !== null &&
123
+ external.size > 0 &&
124
+ request.identity.groups.length > 0 &&
125
+ request.identity.groups.every((g) => external.has(g)) &&
126
+ !PORTAL_PATHS.some((p) => request.url.startsWith(p)) &&
127
+ request.url.startsWith('/api/'))
128
+ throw new HttpProblem(403, 'internal-only', 'For the company’s users only', 'Portal users reach the portal only.');
104
129
  });
105
130
  if (options.rateLimit?.max !== 0)
106
131
  await app.register(rateLimit, {
package/dist/ui/api.d.ts CHANGED
@@ -65,6 +65,8 @@ export interface CurrentUser {
65
65
  readonly name: string;
66
66
  readonly company: number | null;
67
67
  readonly companies: readonly number[];
68
+ /** Number grouping per company (RFC 0016); older servers send none. */
69
+ readonly groupings?: Readonly<Record<string, 'international' | 'lakh'>>;
68
70
  readonly twoFactorEnabled: boolean;
69
71
  readonly administrator: boolean;
70
72
  readonly administratorNeedsSecondFactor: boolean;
package/dist/ui/app.js CHANGED
@@ -177,7 +177,7 @@ function Shell() {
177
177
  const user = me.data;
178
178
  const current = company ?? user.company ?? user.companies[0] ?? 0;
179
179
  const app = apps.data?.find((a) => a.code === params.app);
180
- return (_jsx(KoshProvider, { api: api, registry: registry, uid: user.id, companyId: current, lang: lang, tz: Intl.DateTimeFormat().resolvedOptions().timeZone, ...(realtime === undefined ? {} : { realtime }), ...(offline === undefined ? {} : { offline }), children: _jsx(ToastProvider, { children: _jsxs(DesignProvider, { children: [_jsx(OfflineAgent, { uid: user.id }), _jsxs(PaletteContext.Provider, { value: openPalette, children: [_jsx("a", { href: "#content", className: "sr-only focus:not-sr-only focus:absolute focus:start-2 focus:top-2 focus:z-50 focus:rounded focus:bg-surface focus:px-3 focus:py-2", children: t('shell.skip') }), _jsx("header", { className: "sticky top-0 z-30 border-b bg-surface", children: _jsxs("div", { className: "flex h-12 items-center gap-2 px-3", children: [_jsxs(Menu, { children: [_jsx(MenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", "aria-label": t('shell.apps'), children: _jsx(Grid3x3, { "aria-hidden": true }) }) }), _jsxs(MenuContent, { align: "start", children: [_jsx(MenuItem, { onSelect: () => void navigate({ to: '/' }), children: t('shell.apps') }), _jsx(MenuSeparator, {}), (apps.data ?? []).map((a) => (_jsx(MenuItem, { onSelect: () => void navigate({ to: '/$app', params: { app: a.code } }), children: a.label }, a.code)))] })] }), _jsx(Link, { to: "/", className: "font-semibold", children: app?.label ?? t('app.name') }), _jsx("nav", { "aria-label": t('shell.menu'), className: "ms-2 flex-1", children: params.app === undefined ? null : (_jsx(MenuLinks, { app: params.app, menus: menus.data ?? [] })) }), _jsxs(Button, { variant: "secondary", size: "sm", className: "hidden h-8 w-56 justify-between text-muted-foreground sm:flex", onClick: () => setPalette(true), "aria-keyshortcuts": "Control+K Meta+K", children: [_jsxs("span", { className: "flex items-center gap-2", children: [_jsx(Search, { className: "size-4", "aria-hidden": true }), t('palette.button')] }), _jsx("kbd", { className: "rounded border bg-surface px-1.5 text-xs", children: "Ctrl K" })] }), _jsx(Button, { variant: "ghost", size: "icon", className: "sm:hidden", "aria-label": t('palette.button'), onClick: () => setPalette(true), children: _jsx(Search, { "aria-hidden": true }) }), _jsx(OfflineMenu, {}), registry.headerItems().map(({ name, component: Item }) => (_jsx(Item, { api: api }, name))), _jsx(CompanySwitcher, { me: user, company: current, onChange: (id) => {
180
+ return (_jsx(KoshProvider, { api: api, registry: registry, uid: user.id, companyId: current, lang: lang, tz: Intl.DateTimeFormat().resolvedOptions().timeZone, grouping: user.groupings?.[String(current)] ?? 'international', ...(realtime === undefined ? {} : { realtime }), ...(offline === undefined ? {} : { offline }), children: _jsx(ToastProvider, { children: _jsxs(DesignProvider, { children: [_jsx(OfflineAgent, { uid: user.id }), _jsxs(PaletteContext.Provider, { value: openPalette, children: [_jsx("a", { href: "#content", className: "sr-only focus:not-sr-only focus:absolute focus:start-2 focus:top-2 focus:z-50 focus:rounded focus:bg-surface focus:px-3 focus:py-2", children: t('shell.skip') }), _jsx("header", { className: "sticky top-0 z-30 border-b bg-surface", children: _jsxs("div", { className: "flex h-12 items-center gap-2 px-3", children: [_jsxs(Menu, { children: [_jsx(MenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", "aria-label": t('shell.apps'), children: _jsx(Grid3x3, { "aria-hidden": true }) }) }), _jsxs(MenuContent, { align: "start", children: [_jsx(MenuItem, { onSelect: () => void navigate({ to: '/' }), children: t('shell.apps') }), _jsx(MenuSeparator, {}), (apps.data ?? []).map((a) => (_jsx(MenuItem, { onSelect: () => void navigate({ to: '/$app', params: { app: a.code } }), children: a.label }, a.code)))] })] }), _jsx(Link, { to: "/", className: "font-semibold", children: app?.label ?? t('app.name') }), _jsx("nav", { "aria-label": t('shell.menu'), className: "ms-2 flex-1", children: params.app === undefined ? null : (_jsx(MenuLinks, { app: params.app, menus: menus.data ?? [] })) }), _jsxs(Button, { variant: "secondary", size: "sm", className: "hidden h-8 w-56 justify-between text-muted-foreground sm:flex", onClick: () => setPalette(true), "aria-keyshortcuts": "Control+K Meta+K", children: [_jsxs("span", { className: "flex items-center gap-2", children: [_jsx(Search, { className: "size-4", "aria-hidden": true }), t('palette.button')] }), _jsx("kbd", { className: "rounded border bg-surface px-1.5 text-xs", children: "Ctrl K" })] }), _jsx(Button, { variant: "ghost", size: "icon", className: "sm:hidden", "aria-label": t('palette.button'), onClick: () => setPalette(true), children: _jsx(Search, { "aria-hidden": true }) }), _jsx(OfflineMenu, {}), registry.headerItems().map(({ name, component: Item }) => (_jsx(Item, { api: api }, name))), _jsx(CompanySwitcher, { me: user, company: current, onChange: (id) => {
181
181
  api.setCompany(id);
182
182
  setCompany(id);
183
183
  void queryClient.invalidateQueries();
@@ -26,13 +26,15 @@ export interface KoshContextValue {
26
26
  export declare function useKosh(): KoshContextValue;
27
27
  /** Applies the theme to `<html>` (the `dark` class) and follows the system when asked to. */
28
28
  declare function useTheme(): [ThemeChoice, (t: ThemeChoice) => void];
29
- export declare function KoshProvider({ api, registry, uid, companyId, lang, tz, realtime, offline, children, }: {
29
+ export declare function KoshProvider({ api, registry, uid, companyId, lang, tz, grouping, realtime, offline, children, }: {
30
30
  readonly api: KoshApi;
31
31
  readonly registry: Registry;
32
32
  readonly uid: number;
33
33
  readonly companyId: number;
34
34
  readonly lang: string;
35
35
  readonly tz: string;
36
+ /** The company's number grouping (RFC 0016). */
37
+ readonly grouping?: 'international' | 'lakh';
36
38
  readonly realtime?: RealtimeClient;
37
39
  readonly offline?: OfflineQueue;
38
40
  readonly children: ReactNode;
@@ -47,9 +47,9 @@ function useTheme() {
47
47
  }, [theme]);
48
48
  return [theme, setTheme];
49
49
  }
50
- export function KoshProvider({ api, registry, uid, companyId, lang, tz, realtime, offline, children, }) {
50
+ export function KoshProvider({ api, registry, uid, companyId, lang, tz, grouping, realtime, offline, children, }) {
51
51
  const [theme, setTheme] = useTheme();
52
- const format = useMemo(() => createFormatter(lang, tz), [lang, tz]);
52
+ const format = useMemo(() => createFormatter(lang, tz, grouping === undefined ? {} : { grouping }), [lang, tz, grouping]);
53
53
  const value = useMemo(() => ({
54
54
  api,
55
55
  registry,
@@ -11,7 +11,7 @@ export { ApiError, type ApiOptions, type AppEntry, type CurrentUser, createKoshA
11
11
  export { KoshApp, type KoshAppProps } from './app.js';
12
12
  export { AppIcon } from './components/AppIcon.js';
13
13
  export * from './components/primitives.js';
14
- export { type KoshContextValue, KoshProvider, type ThemeChoice, useKosh } from './context.js';
14
+ export { type KoshContextValue, KoshProvider, type ThemeChoice, useKosh, useTheme, } from './context.js';
15
15
  export { type DesignContextValue, DesignProvider, type DesignSpot, useDesign, } from './design.js';
16
16
  export { addMessages, createI18n, directionOf, NAMESPACE } from './i18n/index.js';
17
17
  export { LANGUAGES } from './i18n/messages.js';
package/dist/ui/index.js CHANGED
@@ -15,7 +15,7 @@ export { ApiError, createKoshApi, NetworkError, } from './api.js';
15
15
  export { KoshApp } from './app.js';
16
16
  export { AppIcon } from './components/AppIcon.js';
17
17
  export * from './components/primitives.js';
18
- export { KoshProvider, useKosh } from './context.js';
18
+ export { KoshProvider, useKosh, useTheme, } from './context.js';
19
19
  export { DesignProvider, useDesign, } from './design.js';
20
20
  export { addMessages, createI18n, directionOf, NAMESPACE } from './i18n/index.js';
21
21
  export { LANGUAGES } from './i18n/messages.js';
@@ -14,4 +14,6 @@ export interface Formatter {
14
14
  }
15
15
  export declare function createFormatter(lang: string, tz: string, options?: {
16
16
  readonly nativeDigits?: boolean;
17
+ /** `lakh`: 12,34,567 (RFC 0016). Bangla groups so already; English takes the Indian English pattern. */
18
+ readonly grouping?: 'international' | 'lakh';
17
19
  }): Formatter;
@@ -11,7 +11,9 @@ export function createFormatter(lang, tz, options = {}) {
11
11
  // Bangla defaults to Bengali digits in ICU; RFC 0007 makes them a choice (Latin digits unless asked).
12
12
  const locale = base.startsWith('bn')
13
13
  ? `${base}-u-nu-${options.nativeDigits === true ? 'beng' : 'latn'}`
14
- : base;
14
+ : options.grouping === 'lakh' && base.startsWith('en')
15
+ ? 'en-IN'
16
+ : base;
15
17
  const integer = new Intl.NumberFormat(locale, { maximumFractionDigits: 0 });
16
18
  const digits = new Intl.NumberFormat(locale, { useGrouping: false });
17
19
  const separator = new Intl.NumberFormat(locale).formatToParts(1.5).find((p) => p.type === 'decimal')?.value ??
@@ -19,23 +19,36 @@ import { cn } from '../lib/cn.js';
19
19
  import { useLiveRecord } from '../live.js';
20
20
  import { useToast } from '../shell/toast.js';
21
21
  import { PrefetchedNames, useRowNames } from './widgets.js';
22
- function commandsOf(lines) {
22
+ /** A line's values as the server takes them: many2many as `{ set }` (a line's taxes). */
23
+ function lineValues(values, fields) {
24
+ const out = {};
25
+ for (const [key, value] of Object.entries(values))
26
+ out[key] = fields[key]?.kind === 'many2many' && Array.isArray(value) ? { set: value } : value;
27
+ return out;
28
+ }
29
+ function commandsOf(lines, fields) {
23
30
  return {
24
31
  ...(lines.rows.some((r) => r.id === undefined)
25
- ? { create: lines.rows.filter((r) => r.id === undefined).map((r) => r.values) }
32
+ ? {
33
+ create: lines.rows
34
+ .filter((r) => r.id === undefined)
35
+ .map((r) => lineValues(r.values, fields)),
36
+ }
26
37
  : {}),
27
38
  ...(lines.rows.some((r) => r.id !== undefined && r.dirty)
28
39
  ? {
29
40
  update: lines.rows
30
41
  .filter((r) => r.id !== undefined && r.dirty)
31
- .map((r) => ({ id: r.id, values: r.values })),
42
+ .map((r) => ({ id: r.id, values: lineValues(r.values, fields) })),
32
43
  }
33
44
  : {}),
34
45
  ...(lines.deleted.length > 0 ? { delete: lines.deleted } : {}),
35
46
  };
36
47
  }
37
48
  /** The payload of a save: changed values, many2many as `{ set }`. */
38
- function payloadOf(changes, fields, lines) {
49
+ function payloadOf(changes, fields, lines,
50
+ /** The fields of a one2many's lines. */
51
+ lineFields) {
39
52
  const out = {};
40
53
  for (const [key, value] of Object.entries(changes)) {
41
54
  if (fields[key]?.kind === 'many2many')
@@ -44,7 +57,7 @@ function payloadOf(changes, fields, lines) {
44
57
  out[key] = value;
45
58
  }
46
59
  for (const [key, state] of lines) {
47
- const commands = commandsOf(state);
60
+ const commands = commandsOf(state, lineFields(key));
48
61
  if (Object.keys(commands).length > 0)
49
62
  out[key] = commands;
50
63
  }
@@ -177,7 +190,7 @@ export function FormView({ served, recordId, defaults, onSaved, onBack, crumbs,
177
190
  };
178
191
  const save = useMutation({
179
192
  mutationFn: async () => {
180
- const payload = payloadOf(changes, fields, lines);
193
+ const payload = payloadOf(changes, fields, lines, (key) => served.fields[fields[key]?.target ?? ''] ?? {});
181
194
  const etag = loaded.data?.etag ?? null;
182
195
  try {
183
196
  return isNew
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shutro-erp",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Shutro ERP (Kosh): a modern ERP framework on TypeScript, PostgreSQL and the Shutro ORM — typed end to end, permissions enforced on the server, metadata-driven views, reporting, documents and a chatter on every record.",
5
5
  "keywords": [
6
6
  "erp",
@@ -137,6 +137,14 @@
137
137
  "types": "./dist/assistant/web/index.d.ts",
138
138
  "default": "./dist/assistant/web/index.js"
139
139
  },
140
+ "./import": {
141
+ "types": "./dist/import/index.d.ts",
142
+ "default": "./dist/import/index.js"
143
+ },
144
+ "./import/web": {
145
+ "types": "./dist/import/web/index.d.ts",
146
+ "default": "./dist/import/web/index.js"
147
+ },
140
148
  "./contacts": {
141
149
  "types": "./dist/contacts/index.d.ts",
142
150
  "default": "./dist/contacts/index.js"
@@ -177,6 +185,46 @@
177
185
  "types": "./dist/sale_project/index.d.ts",
178
186
  "default": "./dist/sale_project/index.js"
179
187
  },
188
+ "./portal": {
189
+ "types": "./dist/portal/index.d.ts",
190
+ "default": "./dist/portal/index.js"
191
+ },
192
+ "./portal/web": {
193
+ "types": "./dist/portal/web/index.d.ts",
194
+ "default": "./dist/portal/web/index.js"
195
+ },
196
+ "./sale_portal": {
197
+ "types": "./dist/sale_portal/index.d.ts",
198
+ "default": "./dist/sale_portal/index.js"
199
+ },
200
+ "./account_portal": {
201
+ "types": "./dist/account_portal/index.d.ts",
202
+ "default": "./dist/account_portal/index.js"
203
+ },
204
+ "./purchase_portal": {
205
+ "types": "./dist/purchase_portal/index.d.ts",
206
+ "default": "./dist/purchase_portal/index.js"
207
+ },
208
+ "./project_portal": {
209
+ "types": "./dist/project_portal/index.d.ts",
210
+ "default": "./dist/project_portal/index.js"
211
+ },
212
+ "./l10n_bd": {
213
+ "types": "./dist/l10n_bd/index.d.ts",
214
+ "default": "./dist/l10n_bd/index.js"
215
+ },
216
+ "./l10n_bd/web": {
217
+ "types": "./dist/l10n_bd/web/index.d.ts",
218
+ "default": "./dist/l10n_bd/web/index.js"
219
+ },
220
+ "./l10n_in": {
221
+ "types": "./dist/l10n_in/index.d.ts",
222
+ "default": "./dist/l10n_in/index.js"
223
+ },
224
+ "./l10n_in/web": {
225
+ "types": "./dist/l10n_in/web/index.d.ts",
226
+ "default": "./dist/l10n_in/web/index.js"
227
+ },
180
228
  "./ui": {
181
229
  "types": "./dist/ui/index.d.ts",
182
230
  "default": "./dist/ui/index.js"
@@ -229,7 +277,7 @@
229
277
  "postal-mime": "^4.1.4",
230
278
  "radix-ui": "^1.8.1",
231
279
  "sanitize-html": "^2.18.1",
232
- "shutro": "^0.3.1",
280
+ "shutro": "^0.3.2",
233
281
  "tailwind-merge": "^3.7.0",
234
282
  "web-push": "^3.6.7"
235
283
  },