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
@@ -51,9 +51,34 @@ export interface RegisterInput {
51
51
  readonly moves: readonly number[];
52
52
  readonly journal: number;
53
53
  readonly date?: string | undefined;
54
+ /** The money paid; by default what is due less what is withheld. */
54
55
  readonly amount?: string | undefined;
55
56
  readonly memo?: string | undefined;
57
+ /** What to withhold (see {@link proposeWithholdings}); none by default. */
58
+ readonly withholdings?: readonly {
59
+ readonly withholding: number;
60
+ readonly amount: string;
61
+ }[] | undefined;
56
62
  }
63
+ export interface WithholdingProposal {
64
+ /** What the documents still owe. */
65
+ readonly residual: string;
66
+ readonly currency: string;
67
+ readonly withholdings: readonly {
68
+ readonly withholding: number;
69
+ readonly name: string;
70
+ readonly base: string;
71
+ readonly amount: string;
72
+ }[];
73
+ /** The money to pay: the residual less the withholdings. */
74
+ readonly amount: string;
75
+ }
76
+ /**
77
+ * The withholdings a payment of `moves` should keep back (RFC 0016 §5.1): those of the partner in the payment's
78
+ * direction (vendor bills: `vendor`; our invoices: `customer`), for documents whose total reaches their threshold,
79
+ * on the documents' untaxed amount, tax or total — prorated to what is still due — rounded to the currency.
80
+ */
81
+ export declare function proposeWithholdings(env: Env, moves: readonly number[]): Promise<WithholdingProposal>;
57
82
  /** A payment for posted documents of one partner and currency, with their residual as the default amount. */
58
83
  export declare function registerPayment(env: Env, input: RegisterInput): Promise<{
59
84
  id: number;
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import { HttpProblem } from '../server/index.js';
11
11
  import { orm } from '../server/orm.js';
12
+ import { owedToUs } from './module.js';
12
13
  const fail = (model, fields, problem, hint) => new orm.ValidationError(model, fields, problem, hint);
13
14
  /** The open (not fully reconciled) lines of a reconcilable account, oldest due first. */
14
15
  export async function openLines(env, ids) {
@@ -331,6 +332,7 @@ export async function postPayments(payments) {
331
332
  currency: true,
332
333
  company: { currency: true, accountReceivable: true, accountPayable: true },
333
334
  documents: { lines: { account: true } },
335
+ withholdings: { withholding: { account: true } },
334
336
  });
335
337
  for (const p of loaded) {
336
338
  if (p.journal.type !== 'cash' && p.journal.type !== 'bank')
@@ -352,7 +354,7 @@ export async function postPayments(payments) {
352
354
  throw fail('account.payment', ['documents'], `${d.name} is not from ${p.partner.name}.`, "Pay one partner's documents at a time.");
353
355
  if (d.currency.id !== p.currency.id)
354
356
  throw fail('account.payment', ['documents'], `${d.name} is in another currency.`, "Pay in the document's currency.");
355
- const incoming = d.kind === 'out_invoice' || d.kind === 'in_refund';
357
+ const incoming = owedToUs(d.kind);
356
358
  if (incoming !== (p.kind === 'inbound'))
357
359
  throw fail('account.payment', ['documents'], `${d.name} is not ${p.kind === 'inbound' ? 'money to receive' : 'money to send'}.`, 'Check the direction.');
358
360
  for (const l of d.lines)
@@ -360,6 +362,25 @@ export async function postPayments(payments) {
360
362
  docLineIds.push(l.id);
361
363
  }
362
364
  const inbound = p.kind === 'inbound';
365
+ // Withheld (VDS, TDS): the documents are settled by the money plus what was kept back (RFC 0016).
366
+ let withheld = orm.toDecimal(0);
367
+ const withheldLines = [];
368
+ for (const w of p.withholdings) {
369
+ const kept = w.amount.amount;
370
+ if (kept.lte(0))
371
+ continue;
372
+ withheld = withheld.plus(kept);
373
+ withheldLines.push({
374
+ account: w.withholding.account.id,
375
+ partner: p.partner.id,
376
+ name: w.withholding.name,
377
+ // Paying a vendor: a liability to the state (credit). Paid by a customer: a receivable from it (debit).
378
+ amountCurrency: inbound ? kept : kept.neg(),
379
+ debit: '0',
380
+ credit: '0',
381
+ });
382
+ }
383
+ const settled = amount.plus(withheld);
363
384
  const created = await env.model('account.move').create([
364
385
  {
365
386
  journal: p.journal.id,
@@ -383,10 +404,11 @@ export async function postPayments(payments) {
383
404
  account: counterpart.id,
384
405
  partner: p.partner.id,
385
406
  name: p.memo ?? 'Payment',
386
- amountCurrency: inbound ? amount.neg() : amount,
407
+ amountCurrency: inbound ? settled.neg() : settled,
387
408
  debit: '0',
388
409
  credit: '0',
389
410
  },
411
+ ...withheldLines,
390
412
  ],
391
413
  },
392
414
  },
@@ -415,6 +437,68 @@ export async function postPayments(payments) {
415
437
  }
416
438
  }
417
439
  }
440
+ /**
441
+ * The withholdings a payment of `moves` should keep back (RFC 0016 §5.1): those of the partner in the payment's
442
+ * direction (vendor bills: `vendor`; our invoices: `customer`), for documents whose total reaches their threshold,
443
+ * on the documents' untaxed amount, tax or total — prorated to what is still due — rounded to the currency.
444
+ */
445
+ export async function proposeWithholdings(env, moves) {
446
+ const docs = await env
447
+ .model('account.move')
448
+ .browse(moves.map((id) => orm.asId(id, 'account.move')))
449
+ .load({ partner: { withholdings: true }, currency: true });
450
+ const first = docs.first();
451
+ if (docs.length === 0)
452
+ throw fail('account.payment', ['documents'], 'No documents to pay.', 'Choose a posted document.');
453
+ const inbound = owedToUs(first.kind);
454
+ const unit = first.currency.rounding;
455
+ const round = (d) => d.div(unit).toDecimalPlaces(0, orm.Decimal.ROUND_HALF_UP).times(unit);
456
+ let residual = orm.toDecimal(0);
457
+ const byWithholding = new Map();
458
+ for (const d of docs) {
459
+ const due = d.amountResidual?.amount ?? orm.toDecimal(0);
460
+ residual = residual.plus(due);
461
+ const total = d.amountTotal?.amount ?? orm.toDecimal(0);
462
+ if (total.isZero())
463
+ continue;
464
+ const share = due.div(total); // a partly paid document withholds on what is left
465
+ for (const w of d.partner?.withholdings ?? []) {
466
+ if (w.direction !== (inbound ? 'customer' : 'vendor') || total.lt(w.threshold))
467
+ continue;
468
+ const whole = w.base === 'tax'
469
+ ? (d.amountTax?.amount ?? orm.toDecimal(0))
470
+ : w.base === 'total'
471
+ ? total
472
+ : (d.amountUntaxed?.amount ?? orm.toDecimal(0));
473
+ const base = whole.times(share);
474
+ const entry = byWithholding.get(w.id) ?? {
475
+ name: w.name,
476
+ sequence: w.sequence,
477
+ base: orm.toDecimal(0),
478
+ amount: orm.toDecimal(0),
479
+ };
480
+ entry.base = entry.base.plus(base);
481
+ entry.amount = entry.amount.plus(base.times(w.rate));
482
+ byWithholding.set(w.id, entry);
483
+ }
484
+ }
485
+ // In their order (sequence, then as created).
486
+ const withholdings = [...byWithholding]
487
+ .sort(([a, x], [b, y]) => x.sequence - y.sequence || a - b)
488
+ .map(([withholding, w]) => ({
489
+ withholding,
490
+ name: w.name,
491
+ base: round(w.base).toFixed(),
492
+ amount: round(w.amount).toFixed(),
493
+ }));
494
+ const kept = withholdings.reduce((s, w) => s.plus(w.amount), orm.toDecimal(0));
495
+ return {
496
+ residual: residual.toFixed(),
497
+ currency: first.currency.name,
498
+ withholdings,
499
+ amount: residual.minus(kept).toFixed(),
500
+ };
501
+ }
418
502
  /** A payment for posted documents of one partner and currency, with their residual as the default amount. */
419
503
  export async function registerPayment(env, input) {
420
504
  const docs = await env
@@ -430,8 +514,10 @@ export async function registerPayment(env, input) {
430
514
  throw fail('account.payment', ['documents'], `${d.name} is a journal entry, not a document.`, 'Pay invoices, bills or credit notes.');
431
515
  residual = residual.plus(d.amountResidual?.amount ?? 0);
432
516
  }
433
- const kind = first.kind === 'out_invoice' || first.kind === 'in_refund' ? 'inbound' : 'outbound';
434
- const amount = input.amount ?? residual.toString();
517
+ const kind = owedToUs(first.kind) ? 'inbound' : 'outbound';
518
+ const withholdings = input.withholdings ?? [];
519
+ const kept = withholdings.reduce((s, w) => s.plus(orm.toDecimal(w.amount)), orm.toDecimal(0));
520
+ const amount = input.amount ?? residual.minus(kept).toString();
435
521
  const created = await env.model('account.payment').create([
436
522
  {
437
523
  kind,
@@ -443,6 +529,12 @@ export async function registerPayment(env, input) {
443
529
  memo: input.memo ?? null,
444
530
  company: first.company.id,
445
531
  documents: { set: docs.ids },
532
+ withholdings: {
533
+ create: withholdings.map((w) => ({
534
+ withholding: orm.asId(w.withholding, 'account.withholding'),
535
+ amount: w.amount,
536
+ })),
537
+ },
446
538
  },
447
539
  ]);
448
540
  await created.post();
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * "Register payment" on a posted document that is not fully paid (a node component of the invoice form): a
3
- * dialog with the journal, the date, the amount (the amount due by default) and a memo, posted through
4
- * `POST /api/v1/accounting/payments/register`; the form reloads afterwards.
3
+ * dialog with the journal, the date, the withholdings the partner's documents call for (VDS, TDS — proposed by the
4
+ * server, each amount editable), the amount (what is due less what is withheld, by default) and a memo, posted
5
+ * through `POST /api/v1/accounting/payments/register`; the form reloads afterwards.
5
6
  */
6
7
  import { type NodeComponentProps } from '../../ui/index.js';
7
8
  import { type ReactElement } from 'react';
@@ -1,21 +1,35 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /**
3
3
  * "Register payment" on a posted document that is not fully paid (a node component of the invoice form): a
4
- * dialog with the journal, the date, the amount (the amount due by default) and a memo, posted through
5
- * `POST /api/v1/accounting/payments/register`; the form reloads afterwards.
4
+ * dialog with the journal, the date, the withholdings the partner's documents call for (VDS, TDS — proposed by the
5
+ * server, each amount editable), the amount (what is due less what is withheld, by default) and a memo, posted
6
+ * through `POST /api/v1/accounting/payments/register`; the form reloads afterwards.
6
7
  */
7
- import { Alert, Button, Dialog, Input, Label, Select } from '../../ui/index.js';
8
+ import { Alert, Button, Dialog, Input, Label, Select, useKosh, } from '../../ui/index.js';
8
9
  import { useMutation, useQuery } from '@tanstack/react-query';
9
10
  import { useId, useState } from 'react';
10
11
  import { useTranslation } from 'react-i18next';
12
+ /** Exact decimal subtraction of decimal strings (no floats): `a - Σ b`, to the cent. */
13
+ function less(a, bs) {
14
+ const cents = (v) => {
15
+ const [w = '0', f = ''] = v.trim().split('.');
16
+ return BigInt(w === '' ? '0' : w) * 100n + BigInt(`${f}00`.slice(0, 2));
17
+ };
18
+ const r = bs.reduce((s, b) => s - (/^\d+(\.\d+)?$/.test(b) ? cents(b) : 0n), cents(a));
19
+ const sign = r < 0n ? '-' : '';
20
+ const abs = r < 0n ? -r : r;
21
+ return `${sign}${abs / 100n}.${String(abs % 100n).padStart(2, '0')}`;
22
+ }
11
23
  export function RegisterPayment({ api, record, reload }) {
12
24
  const { t } = useTranslation('accounting');
25
+ const { format } = useKosh();
13
26
  const id = useId();
14
27
  const [open, setOpen] = useState(false);
15
28
  const [journal, setJournal] = useState(null);
16
29
  const [date, setDate] = useState(new Date().toISOString().slice(0, 10));
17
30
  const [amount, setAmount] = useState(null);
18
31
  const [memo, setMemo] = useState('');
32
+ const [kept, setKept] = useState({});
19
33
  const [error, setError] = useState(null);
20
34
  const journals = useQuery({
21
35
  queryKey: ['accounting', 'journals', 'cash-bank'],
@@ -25,17 +39,34 @@ export function RegisterPayment({ api, record, reload }) {
25
39
  }),
26
40
  enabled: open,
27
41
  });
42
+ const proposal = useQuery({
43
+ queryKey: ['accounting', 'withholdings', record?.id],
44
+ queryFn: () => api.call('POST', '/api/v1/accounting/payments/propose', { moves: [record?.id] }),
45
+ enabled: open,
46
+ });
47
+ const withholdings = (proposal.data?.withholdings ?? []).map((w) => ({
48
+ ...w,
49
+ amount: kept[w.withholding] ?? w.amount,
50
+ }));
51
+ const due = proposal.data?.residual ??
52
+ record?.['amountResidual']?.amount ??
53
+ '';
54
+ const net = less(due, withholdings.map((w) => w.amount));
28
55
  const pay = useMutation({
29
56
  mutationFn: () => api.call('POST', '/api/v1/accounting/payments/register', {
30
57
  moves: [record?.id],
31
58
  journal: Number(journal),
32
59
  date,
33
- ...(amount === null ? {} : { amount }),
60
+ amount: amount ?? net,
61
+ withholdings: withholdings
62
+ .filter((w) => /^\d+(\.\d+)?$/.test(w.amount) && Number(w.amount) > 0)
63
+ .map((w) => ({ withholding: w.withholding, amount: w.amount })),
34
64
  ...(memo === '' ? {} : { memo }),
35
65
  }),
36
66
  onSuccess: () => {
37
67
  setOpen(false);
38
68
  setError(null);
69
+ setKept({});
39
70
  reload();
40
71
  },
41
72
  onError: (e) => setError(e.message),
@@ -44,9 +75,11 @@ export function RegisterPayment({ api, record, reload }) {
44
75
  return null;
45
76
  if (record['paymentState'] === 'paid')
46
77
  return null;
47
- const due = record['amountResidual']?.amount ?? '';
48
78
  return (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "secondary", onClick: () => setOpen(true), children: t('pay.button') }), _jsx(Dialog, { open: open, onOpenChange: setOpen, title: t('pay.title'), footer: _jsx(Button, { variant: "primary", busy: pay.isPending, disabled: journal === null, onClick: () => pay.mutate(), children: t('pay.confirm') }), children: _jsxs("div", { className: "grid gap-3", children: [error === null ? null : _jsx(Alert, { tone: "danger", children: error }), _jsx(Select, { id: `${id}-journal`, label: t('pay.journal'), value: journal, options: (journals.data?.items ?? []).map((j) => ({
49
79
  value: String(j.id),
50
80
  label: `${String(j['code'])} ${String(j['name'])}`,
51
- })), onChange: setJournal }), _jsxs("div", { className: "grid gap-1", children: [_jsx(Label, { htmlFor: `${id}-date`, children: t('pay.date') }), _jsx(Input, { id: `${id}-date`, type: "date", value: date, onChange: (e) => setDate(e.target.value) })] }), _jsxs("div", { className: "grid gap-1", children: [_jsx(Label, { htmlFor: `${id}-amount`, children: t('pay.amount') }), _jsx(Input, { id: `${id}-amount`, inputMode: "decimal", value: amount ?? due, onChange: (e) => setAmount(e.target.value) })] }), _jsxs("div", { className: "grid gap-1", children: [_jsx(Label, { htmlFor: `${id}-memo`, children: t('pay.memo') }), _jsx(Input, { id: `${id}-memo`, value: memo, onChange: (e) => setMemo(e.target.value) })] })] }) })] }));
81
+ })), onChange: setJournal }), _jsxs("div", { className: "grid gap-1", children: [_jsx(Label, { htmlFor: `${id}-date`, children: t('pay.date') }), _jsx(Input, { id: `${id}-date`, type: "date", value: date, onChange: (e) => setDate(e.target.value) })] }), withholdings.length === 0 ? null : (_jsxs("fieldset", { className: "grid gap-2 rounded-md border p-3", children: [_jsx("legend", { className: "px-1 text-sm font-medium", children: t('pay.withheld') }), withholdings.map((w) => (_jsxs("div", { className: "grid gap-1", children: [_jsxs(Label, { htmlFor: `${id}-w${w.withholding}`, children: [w.name, " (", format.decimal(w.base, 2), ")"] }), _jsx(Input, { id: `${id}-w${w.withholding}`, inputMode: "decimal", value: w.amount, onChange: (e) => setKept((k) => ({ ...k, [w.withholding]: e.target.value })) })] }, w.withholding)))] })), _jsxs("div", { className: "grid gap-1", children: [_jsx(Label, { htmlFor: `${id}-amount`, children: t('pay.amount') }), _jsx(Input, { id: `${id}-amount`, inputMode: "decimal", value: amount ?? net, onChange: (e) => setAmount(e.target.value) }), withholdings.length === 0 ? null : (_jsx("p", { className: "text-xs text-muted-foreground", children: t('pay.due', {
82
+ amount: format.decimal(due, 2),
83
+ withheld: format.decimal(less('0', withholdings.map((w) => w.amount)).replace('-', ''), 2),
84
+ }) }))] }), _jsxs("div", { className: "grid gap-1", children: [_jsx(Label, { htmlFor: `${id}-memo`, children: t('pay.memo') }), _jsx(Input, { id: `${id}-memo`, value: memo, onChange: (e) => setMemo(e.target.value) })] })] }) })] }));
52
85
  }
@@ -22,6 +22,8 @@ export function SettingsPage({ api }) {
22
22
  const [template, setTemplate] = useState(null);
23
23
  const [lockDate, setLockDate] = useState(null);
24
24
  const [taxLockDate, setTaxLockDate] = useState(null);
25
+ const [country, setCountry] = useState(null);
26
+ const [state, setState] = useState(null);
25
27
  const [message, setMessage] = useState(null);
26
28
  const [error, setError] = useState(null);
27
29
  const install = useMutation({
@@ -39,6 +41,8 @@ export function SettingsPage({ api }) {
39
41
  mutationFn: () => api.call('PUT', '/api/v1/accounting/settings', {
40
42
  lockDate: lockDate ?? settings.data?.lockDate ?? null,
41
43
  taxLockDate: taxLockDate ?? settings.data?.taxLockDate ?? null,
44
+ country: country ?? settings.data?.country ?? null,
45
+ state: state ?? settings.data?.state ?? null,
42
46
  }),
43
47
  onSuccess: async () => {
44
48
  setError(null);
@@ -54,5 +58,5 @@ export function SettingsPage({ api }) {
54
58
  }) })) : (_jsxs("div", { className: "flex flex-wrap items-end gap-2", children: [_jsx(Select, { id: `${id}-chart`, label: t('settings.choose'), className: "w-64", value: template, options: (charts.data?.templates ?? []).map((c) => ({
55
59
  value: c.code,
56
60
  label: c.label,
57
- })), onChange: setTemplate }), _jsx(Button, { variant: "primary", disabled: template === null, busy: install.isPending, onClick: () => install.mutate(), children: t('settings.install') })] }))] }), _jsxs("section", { className: "grid gap-3 rounded-lg border p-4", children: [_jsxs("div", { className: "grid gap-1", children: [_jsx(Label, { htmlFor: `${id}-lock`, children: t('settings.lockDate') }), _jsx(Input, { id: `${id}-lock`, type: "date", value: lockDate ?? current?.lockDate ?? '', onChange: (e) => setLockDate(e.target.value === '' ? null : e.target.value) }), _jsx("p", { className: "text-xs text-muted-foreground", children: t('settings.lockDateHint') })] }), _jsxs("div", { className: "grid gap-1", children: [_jsx(Label, { htmlFor: `${id}-taxlock`, children: t('settings.taxLockDate') }), _jsx(Input, { id: `${id}-taxlock`, type: "date", value: taxLockDate ?? current?.taxLockDate ?? '', onChange: (e) => setTaxLockDate(e.target.value === '' ? null : e.target.value) }), _jsx("p", { className: "text-xs text-muted-foreground", children: t('settings.taxLockDateHint') })] }), _jsx("div", { children: _jsx(Button, { variant: "primary", busy: save.isPending, onClick: () => save.mutate(), children: t('settings.save') }) })] })] }));
61
+ })), onChange: setTemplate }), _jsx(Button, { variant: "primary", disabled: template === null, busy: install.isPending, onClick: () => install.mutate(), children: t('settings.install') })] }))] }), _jsxs("section", { className: "grid gap-3 rounded-lg border p-4", children: [_jsxs("div", { className: "grid gap-1", children: [_jsx(Label, { htmlFor: `${id}-lock`, children: t('settings.lockDate') }), _jsx(Input, { id: `${id}-lock`, type: "date", value: lockDate ?? current?.lockDate ?? '', onChange: (e) => setLockDate(e.target.value === '' ? null : e.target.value) }), _jsx("p", { className: "text-xs text-muted-foreground", children: t('settings.lockDateHint') })] }), _jsxs("div", { className: "grid gap-1", children: [_jsx(Label, { htmlFor: `${id}-taxlock`, children: t('settings.taxLockDate') }), _jsx(Input, { id: `${id}-taxlock`, type: "date", value: taxLockDate ?? current?.taxLockDate ?? '', onChange: (e) => setTaxLockDate(e.target.value === '' ? null : e.target.value) }), _jsx("p", { className: "text-xs text-muted-foreground", children: t('settings.taxLockDateHint') })] }), _jsxs("div", { className: "grid gap-3 sm:grid-cols-2", children: [_jsxs("div", { className: "grid gap-1", children: [_jsx(Label, { htmlFor: `${id}-country`, children: t('settings.country') }), _jsx(Input, { id: `${id}-country`, maxLength: 2, value: country ?? current?.country ?? '', onChange: (e) => setCountry(e.target.value) })] }), _jsxs("div", { className: "grid gap-1", children: [_jsx(Label, { htmlFor: `${id}-state`, children: t('settings.state') }), _jsx(Input, { id: `${id}-state`, maxLength: 6, value: state ?? current?.state ?? '', onChange: (e) => setState(e.target.value) })] }), _jsx("p", { className: "text-xs text-muted-foreground sm:col-span-2", children: t('settings.placeHint') })] }), _jsx("div", { children: _jsx(Button, { variant: "primary", busy: save.isPending, onClick: () => save.mutate(), children: t('settings.save') }) })] })] }));
58
62
  }
@@ -12,6 +12,9 @@ export const messages = {
12
12
  lockDateHint: 'Nothing is posted, reversed or matched on or before this date.',
13
13
  taxLockDate: 'Tax lock date',
14
14
  taxLockDateHint: 'Tax returns are filed up to this date.',
15
+ country: 'Country (ISO code, e.g. IN)',
16
+ state: 'State (e.g. IN-MH)',
17
+ placeHint: 'Where the company is registered: a tax split by state (GST) posts its within-state parts when the place of supply is this state.',
15
18
  save: 'Save',
16
19
  saved: 'Settings saved.',
17
20
  },
@@ -55,6 +58,8 @@ export const messages = {
55
58
  memo: 'Memo',
56
59
  confirm: 'Pay',
57
60
  done: 'Payment {{name}} registered.',
61
+ withheld: 'Withheld at source',
62
+ due: 'Due {{amount}}; withheld {{withheld}}.',
58
63
  },
59
64
  },
60
65
  bn: {
@@ -69,6 +74,9 @@ export const messages = {
69
74
  lockDateHint: 'এই তারিখ বা তার আগে কিছু পোস্ট, রিভার্স বা মেলানো যাবে না।',
70
75
  taxLockDate: 'কর লক তারিখ',
71
76
  taxLockDateHint: 'এই তারিখ পর্যন্ত কর রিটার্ন দাখিল হয়েছে।',
77
+ country: 'দেশ (ISO কোড, যেমন IN)',
78
+ state: 'রাজ্য (যেমন IN-MH)',
79
+ placeHint: 'প্রতিষ্ঠান যেখানে নিবন্ধিত: রাজ্যভেদে ভাগ হওয়া কর (GST) সরবরাহের স্থান এই রাজ্য হলে রাজ্যের ভেতরের অংশগুলো পোস্ট করে।',
72
80
  save: 'সংরক্ষণ',
73
81
  saved: 'সেটিংস সংরক্ষিত।',
74
82
  },
@@ -112,6 +120,8 @@ export const messages = {
112
120
  memo: 'মন্তব্য',
113
121
  confirm: 'পরিশোধ',
114
122
  done: 'পেমেন্ট {{name}} নথিভুক্ত হয়েছে।',
123
+ withheld: 'উৎসে কর্তন',
124
+ due: 'বাকি {{amount}}; কর্তন {{withheld}}।',
115
125
  },
116
126
  },
117
127
  };
@@ -209,7 +209,7 @@ export function registerAssistantRoutes(context, options) {
209
209
  ? 'No API key is configured on this server (ANTHROPIC_API_KEY).'
210
210
  : 'An administrator has not switched the assistant on for this company.');
211
211
  if ((await usedThisMonth(env.companyId)) >= options.monthlyTokens)
212
- throw new HttpProblem(429, 'assistant-budget', "The month's assistant budget is used up", `This company used its ${options.monthlyTokens} tokens of the month.`, 'An administrator can raise KOSH_ASSISTANT_MONTHLY_TOKENS.');
212
+ throw new HttpProblem(429, 'assistant-budget', "The month's assistant budget is used up", `This company used its ${options.monthlyTokens} tokens of the month.`, 'An administrator can raise SHUTRO_ERP_ASSISTANT_MONTHLY_TOKENS.');
213
213
  return client;
214
214
  };
215
215
  /** Logs a call in a transaction of its own (after the model answered: no transaction stays open meanwhile). */
@@ -832,7 +832,7 @@ export function webhookProblem(url, hosts = webhookHosts()) {
832
832
  return 'Webhooks call https URLs (http only for localhost).';
833
833
  const allowed = hosts.some((h) => (h.startsWith('*.') ? host.endsWith(h.slice(1)) : host === h));
834
834
  if (!allowed)
835
- return `The host ${host} is not on the webhook allow-list (KOSH_AUTOMATION_WEBHOOK_HOSTS).`;
835
+ return `The host ${host} is not on the webhook allow-list (SHUTRO_ERP_AUTOMATION_WEBHOOK_HOSTS).`;
836
836
  return null;
837
837
  }
838
838
  /** The signature of a webhook body: `sha256=<hex HMAC>` with the rule's secret. */
@@ -62,7 +62,7 @@ export const messages = {
62
62
  cronHint: 'Minute hour day month weekday: "0 2 * * *" is every night at 02:00; @daily, @weekly, @monthly too. Runs on the records the condition matches.',
63
63
  url: 'URL',
64
64
  webhookFields: 'Fields to send',
65
- webhookHint: "A signed JSON POST (X-Kosh-Signature: sha256 HMAC with the rule's webhook secret), retried on failure; the host must be on the allow-list (KOSH_AUTOMATION_WEBHOOK_HOSTS).",
65
+ webhookHint: "A signed JSON POST (X-Kosh-Signature: sha256 HMAC with the rule's webhook secret), retried on failure; the host must be on the allow-list (SHUTRO_ERP_AUTOMATION_WEBHOOK_HOSTS).",
66
66
  condition: 'And the record matches',
67
67
  conditionJson: 'Condition (domain)',
68
68
  field: 'Field',
@@ -169,7 +169,7 @@ export const messages = {
169
169
  cronHint: 'মিনিট ঘণ্টা দিন মাস সপ্তাহের দিন: "0 2 * * *" মানে প্রতি রাত ০২:০০; @daily, @weekly, @monthly-ও চলে। শর্তে মেলা রেকর্ডে চলে।',
170
170
  url: 'URL',
171
171
  webhookFields: 'যে ফিল্ড পাঠাবে',
172
- webhookHint: 'স্বাক্ষরিত JSON POST (X-Kosh-Signature: নিয়মের ওয়েবহুক সিক্রেট দিয়ে sha256 HMAC), ব্যর্থ হলে আবার চেষ্টা; হোস্ট অনুমোদিত তালিকায় থাকতে হবে (KOSH_AUTOMATION_WEBHOOK_HOSTS)।',
172
+ webhookHint: 'স্বাক্ষরিত JSON POST (X-Kosh-Signature: নিয়মের ওয়েবহুক সিক্রেট দিয়ে sha256 HMAC), ব্যর্থ হলে আবার চেষ্টা; হোস্ট অনুমোদিত তালিকায় থাকতে হবে (SHUTRO_ERP_AUTOMATION_WEBHOOK_HOSTS)।',
173
173
  condition: 'এবং রেকর্ড মেলে',
174
174
  conditionJson: 'শর্ত (ডোমেইন)',
175
175
  field: 'ফিল্ড',
@@ -8,5 +8,5 @@ export interface AppsIo {
8
8
  /** The confirmation question (tests script it). Default: the terminal. */
9
9
  readonly prompter?: Prompter;
10
10
  }
11
- /** Runs `kosh apps …`. Returns the exit code. */
11
+ /** Runs `shutro-erp apps …`. Returns the exit code. */
12
12
  export declare function runApps(argv: readonly string[], io: AppsIo): Promise<number>;
package/dist/cli/apps.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `kosh apps` (RFC 0008): the App manager in the terminal. Lists the catalogue with each app's state, and installs,
2
+ * `shutro-erp apps` (RFC 0008): the App manager in the terminal. Lists the catalogue with each app's state, and installs,
3
3
  * enables or disables apps: the plan is shown first (modules, dependencies, cascades, schema statements), then
4
4
  * applied in one transaction by `shutro-erp` (`prepareApps` / `applyApps`).
5
5
  */
@@ -13,14 +13,14 @@ import { PROTECTED_APPS } from '../settings/index.js';
13
13
  import { modulesOf, seedWith } from './project.js';
14
14
  import { defaultsPrompter, terminalPrompter } from './prompt.js';
15
15
  import { createUi, PLAIN_TERMINAL } from './ui.js';
16
- export const APPS_USAGE = `Usage: kosh apps [command] [app …] [options]
16
+ export const APPS_USAGE = `Usage: shutro-erp apps [command] [app …] [options]
17
17
 
18
- kosh apps the catalogue: installed, disabled and available apps
19
- kosh apps install <app …> install apps and what they need, in one transaction
20
- kosh apps enable <app …> serve disabled apps again (their data is still there)
21
- kosh apps disable <app …> switch apps off: menus, API and access go, tables and data stay
18
+ shutro-erp apps the catalogue: installed, disabled and available apps
19
+ shutro-erp apps install <app …> install apps and what they need, in one transaction
20
+ shutro-erp apps enable <app …> serve disabled apps again (their data is still there)
21
+ shutro-erp apps disable <app …> switch apps off: menus, API and access go, tables and data stay
22
22
  (apps that depend on them are disabled too)
23
- kosh apps upgrade [app …] move installed apps to their code's version: schema, data migrations,
23
+ shutro-erp apps upgrade [app …] move installed apps to their code's version: schema, data migrations,
24
24
  seed data (default: every app whose code is newer)
25
25
 
26
26
  Options:
@@ -32,7 +32,7 @@ Options:
32
32
 
33
33
  After a change, restart the API (npm start) so it serves the new set.
34
34
  `;
35
- /** Pictographs for the apps Kosh ships; other modules get a puzzle piece. */
35
+ /** Pictographs for the apps Shutro ERP ships; other modules get a puzzle piece. */
36
36
  const APP_ICONS = {
37
37
  base: '🏠',
38
38
  settings: '⚙️',
@@ -74,7 +74,7 @@ async function list(ui, driver, catalogue) {
74
74
  const label = m.app?.label ?? '';
75
75
  const why = row?.reason === 'dependency' ? ui.style('gray', ' (needed by another app)') : '';
76
76
  const behind = row !== undefined && row.version !== m.version
77
- ? ui.style('cyan', ` (installed ${row.version}: kosh apps upgrade ${m.name})`)
77
+ ? ui.style('cyan', ` (installed ${row.version}: shutro-erp apps upgrade ${m.name})`)
78
78
  : '';
79
79
  ui.write(` ${marks[s]} ${iconOf(ui, m.name)} ${ui.style('bold', m.name.padEnd(width))} ${ui.style('gray', m.version.padEnd(8))} ${s.padEnd(9)} ${label}${why}${behind}\n`);
80
80
  }
@@ -103,7 +103,7 @@ function showPlan(ui, prepared, catalogue) {
103
103
  if (plan.disable.length > 0)
104
104
  ui.detail('tables and data stay; enable brings everything back');
105
105
  }
106
- /** Runs `kosh apps …`. Returns the exit code. */
106
+ /** Runs `shutro-erp apps …`. Returns the exit code. */
107
107
  export async function runApps(argv, io) {
108
108
  const { values, positionals } = parseArgs({
109
109
  args: [...argv],
@@ -131,11 +131,11 @@ export async function runApps(argv, io) {
131
131
  }
132
132
  if (command === 'uninstall') {
133
133
  ui.warn('Removing an app and its data comes with a later step of RFC 0008.');
134
- ui.info(`Meanwhile: kosh apps disable ${names.join(' ') || '<app>'} (reversible, keeps the data).`);
134
+ ui.info(`Meanwhile: shutro-erp apps disable ${names.join(' ') || '<app>'} (reversible, keeps the data).`);
135
135
  return 2;
136
136
  }
137
137
  if (command !== 'list' && command !== 'upgrade' && names.length === 0) {
138
- ui.fail(`Name the app(s): kosh apps ${command} <app …>`);
138
+ ui.fail(`Name the app(s): shutro-erp apps ${command} <app …>`);
139
139
  return 2;
140
140
  }
141
141
  const envFile = join(io.cwd, '.env');
@@ -143,14 +143,14 @@ export async function runApps(argv, io) {
143
143
  process.loadEnvFile(envFile);
144
144
  const databaseUrl = process.env['DATABASE_URL'];
145
145
  if (databaseUrl === undefined || databaseUrl === '') {
146
- ui.fail('DATABASE_URL is not set: run kosh init, or add it to .env.');
146
+ ui.fail('DATABASE_URL is not set: run shutro-erp init, or add it to .env.');
147
147
  return 1;
148
148
  }
149
149
  const driver = pg.createDriver({
150
150
  url: databaseUrl,
151
151
  max: 2,
152
152
  parseNumeric: orm.toDecimal,
153
- applicationName: 'kosh apps',
153
+ applicationName: 'shutro-erp apps',
154
154
  });
155
155
  const prompter = io.prompter ??
156
156
  (values.yes === true || values.plan === true ? defaultsPrompter() : terminalPrompter());
package/dist/cli/bin.js CHANGED
@@ -1,6 +1,9 @@
1
1
  #!/usr/bin/env node
2
+ import { applyEnvironmentNames } from '../server/index.js';
2
3
  import { runCli } from './index.js';
3
4
  import { detectTerminal } from './ui.js';
5
+ // SHUTRO_ERP_* settings, read under their older KOSH_* names.
6
+ applyEnvironmentNames();
4
7
  process.exitCode = await runCli(process.argv.slice(2), (text) => process.stdout.write(text), {
5
8
  terminal: detectTerminal(process.stdout),
6
9
  });
package/dist/cli/index.js CHANGED
@@ -21,11 +21,11 @@ export const PACKAGE = Object.freeze({
21
21
  name: 'shutro-erp/cli',
22
22
  rfc: 'RFC 0001',
23
23
  });
24
- const USAGE = `Usage: kosh <command>
24
+ const USAGE = `Usage: shutro-erp <command>
25
25
 
26
26
  Commands (run in the installation's folder; they read .env and src/modules.ts):
27
- init [folder] Set up an installation: database, company, administrator, apps (kosh init --help)
28
- apps The apps: installed, disabled, available (kosh apps --help)
27
+ init [folder] Set up an installation: database, company, administrator, apps (shutro-erp init --help)
28
+ apps The apps: installed, disabled, available (shutro-erp apps --help)
29
29
  apps install <app …> Install apps and what they need, in one transaction
30
30
  apps disable <app …> Switch apps off (their data stays); apps enable <app …> brings them back
31
31
  migrate Plan schema changes into migrations/NNNN_*.sql, apply pending files, run the modules'
@@ -41,10 +41,10 @@ Commands (run in the installation's folder; they read .env and src/modules.ts):
41
41
  user create <login> Create a user: --name, --email, --roles reporting.user,messaging.user, --password
42
42
  user password <login> Set a new password
43
43
  generate client Typed API client from the module set or an OpenAPI document
44
- --modules <file|package> a module (or file) exporting Kosh modules (\`modules\`, default, or each export)
44
+ --modules <file|package> a module (or file) exporting Shutro ERP modules (\`modules\`, default, or each export)
45
45
  --from <file|url> an OpenAPI document instead (a URL needs --token)
46
46
  --token <token> Bearer token for --from <url>
47
- --out <file> where to write (default: src/api/kosh.gen.ts)
47
+ --out <file> where to write (default: src/api/shutro-erp.gen.ts)
48
48
  --check fail if the file is out of date (CI)
49
49
 
50
50
  Every command takes --modules <file> (default src/modules.ts).
@@ -74,21 +74,21 @@ async function generate(argv, write, options) {
74
74
  });
75
75
  const cwd = options.cwd ?? process.cwd();
76
76
  if ((values.modules === undefined) === (values.from === undefined)) {
77
- write('kosh generate client: pass exactly one of --modules or --from.\n');
77
+ write('shutro-erp generate client: pass exactly one of --modules or --from.\n');
78
78
  return 2;
79
79
  }
80
80
  const document = values.modules === undefined
81
81
  ? await documentFrom(values.from, values.token, cwd, options.fetch ?? globalThis.fetch)
82
82
  : openApiOf(await modulesOf(values.modules, cwd));
83
83
  const code = generateClient(document);
84
- const out = resolve(cwd, values.out ?? 'src/api/kosh.gen.ts');
84
+ const out = resolve(cwd, values.out ?? 'src/api/shutro-erp.gen.ts');
85
85
  if (values.check === true) {
86
86
  const current = await readFile(out, 'utf8').catch(() => '');
87
87
  if (current.replaceAll('\r\n', '\n') === code) {
88
88
  write(`${out} is up to date.\n`);
89
89
  return 0;
90
90
  }
91
- write(`${out} is out of date: run kosh generate client.\n`);
91
+ write(`${out} is out of date: run shutro-erp generate client.\n`);
92
92
  return 1;
93
93
  }
94
94
  await mkdir(dirname(out), { recursive: true });
@@ -128,7 +128,7 @@ export async function runCli(argv, write, options = {}) {
128
128
  return await command(argv.slice(1), write, cwd);
129
129
  }
130
130
  catch (error) {
131
- write(`kosh ${argv[0]}: ${error instanceof Error ? error.message : String(error)}\n`);
131
+ write(`shutro-erp ${argv[0]}: ${error instanceof Error ? error.message : String(error)}\n`);
132
132
  return 1;
133
133
  }
134
134
  if (argv[0] === 'generate' && argv[1] === 'client') {
@@ -136,10 +136,10 @@ export async function runCli(argv, write, options = {}) {
136
136
  return await generate(argv.slice(2), write, options);
137
137
  }
138
138
  catch (error) {
139
- write(`kosh generate client: ${error instanceof Error ? error.message : String(error)}\n`);
139
+ write(`shutro-erp generate client: ${error instanceof Error ? error.message : String(error)}\n`);
140
140
  return 1;
141
141
  }
142
142
  }
143
- write(`Unknown command '${argv.join(' ')}'. Run kosh --help.\n`);
143
+ write(`Unknown command '${argv.join(' ')}'. Run shutro-erp --help.\n`);
144
144
  return 2;
145
145
  }
@@ -14,5 +14,5 @@ export interface InitIo {
14
14
  /** What the output can show (colour, icons, spinners). Default: plain lines. */
15
15
  readonly terminal?: Terminal;
16
16
  }
17
- /** Runs `kosh init`. Returns the exit code. */
17
+ /** Runs `shutro-erp init`. Returns the exit code. */
18
18
  export declare function runInit(argv: readonly string[], io: InitIo): Promise<number>;