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