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.
- package/README.md +17 -10
- package/dist/account_portal/.kosh/bindings.d.ts +22 -0
- package/dist/account_portal/.kosh/bindings.js +15 -0
- package/dist/account_portal/.kosh/schema.d.ts +1350 -0
- package/dist/account_portal/index.d.ts +3 -0
- package/dist/account_portal/index.js +73 -0
- package/dist/account_portal/module.d.ts +5983 -0
- package/dist/account_portal/module.js +18 -0
- package/dist/accounting/.kosh/bindings.js +1 -1
- package/dist/accounting/.kosh/schema.d.ts +142 -9
- package/dist/accounting/chart.d.ts +40 -8
- package/dist/accounting/chart.js +124 -30
- package/dist/accounting/closing.d.ts +6 -1
- package/dist/accounting/closing.js +5 -1
- package/dist/accounting/demo.js +6 -5
- package/dist/accounting/index.d.ts +3 -3
- package/dist/accounting/index.js +200 -23
- package/dist/accounting/module.d.ts +3326 -1086
- package/dist/accounting/module.js +408 -66
- package/dist/accounting/payments.d.ts +25 -0
- package/dist/accounting/payments.js +96 -4
- package/dist/accounting/web/RegisterPayment.d.ts +3 -2
- package/dist/accounting/web/RegisterPayment.js +39 -6
- package/dist/accounting/web/SettingsPage.js +5 -1
- package/dist/accounting/web/messages.js +10 -0
- package/dist/cli/init.js +172 -48
- package/dist/cli/launch.d.ts +16 -0
- package/dist/cli/launch.js +138 -0
- package/dist/cli/prompt.d.ts +35 -0
- package/dist/cli/prompt.js +46 -0
- package/dist/cli/templates.d.ts +11 -3
- package/dist/cli/templates.js +32 -7
- package/dist/cli/tui.d.ts +7 -0
- package/dist/cli/tui.js +254 -0
- package/dist/cli/ui.d.ts +25 -5
- package/dist/cli/ui.js +49 -9
- package/dist/contacts/.kosh/bindings.js +1 -1
- package/dist/contacts/.kosh/schema.d.ts +3 -1
- package/dist/contacts/index.js +24 -1
- package/dist/contacts/module.d.ts +42 -0
- package/dist/contacts/module.js +3 -1
- package/dist/import/convert.d.ts +55 -0
- package/dist/import/convert.js +224 -0
- package/dist/import/csv.d.ts +28 -0
- package/dist/import/csv.js +115 -0
- package/dist/import/index.d.ts +33 -0
- package/dist/import/index.js +60 -0
- package/dist/import/mapping.d.ts +15 -0
- package/dist/import/mapping.js +33 -0
- package/dist/import/match.d.ts +24 -0
- package/dist/import/match.js +60 -0
- package/dist/import/models.d.ts +2 -0
- package/dist/import/models.js +102 -0
- package/dist/import/routes.d.ts +13 -0
- package/dist/import/routes.js +392 -0
- package/dist/import/run.d.ts +131 -0
- package/dist/import/run.js +568 -0
- package/dist/import/service.d.ts +153 -0
- package/dist/import/service.js +367 -0
- package/dist/import/web/ImportButton.d.ts +7 -0
- package/dist/import/web/ImportButton.js +31 -0
- package/dist/import/web/ImportWizard.d.ts +75 -0
- package/dist/import/web/ImportWizard.js +217 -0
- package/dist/import/web/index.d.ts +13 -0
- package/dist/import/web/index.js +18 -0
- package/dist/import/web/messages.d.ts +2 -0
- package/dist/import/web/messages.js +139 -0
- package/dist/import/worker.d.ts +3 -0
- package/dist/import/worker.js +52 -0
- package/dist/import/xlsx.d.ts +20 -0
- package/dist/import/xlsx.js +192 -0
- package/dist/l10n_bd/.kosh/bindings.d.ts +22 -0
- package/dist/l10n_bd/.kosh/bindings.js +15 -0
- package/dist/l10n_bd/.kosh/schema.d.ts +1281 -0
- package/dist/l10n_bd/documents.d.ts +8 -0
- package/dist/l10n_bd/documents.js +183 -0
- package/dist/l10n_bd/index.d.ts +8 -0
- package/dist/l10n_bd/index.js +156 -0
- package/dist/l10n_bd/module.d.ts +5705 -0
- package/dist/l10n_bd/module.js +163 -0
- package/dist/l10n_bd/mushak.d.ts +67 -0
- package/dist/l10n_bd/mushak.js +139 -0
- package/dist/l10n_bd/routes.d.ts +3 -0
- package/dist/l10n_bd/routes.js +205 -0
- package/dist/l10n_bd/web/MushakPage.d.ts +7 -0
- package/dist/l10n_bd/web/MushakPage.js +53 -0
- package/dist/l10n_bd/web/index.d.ts +13 -0
- package/dist/l10n_bd/web/index.js +18 -0
- package/dist/l10n_bd/web/messages.d.ts +2 -0
- package/dist/l10n_bd/web/messages.js +101 -0
- package/dist/l10n_in/.kosh/bindings.d.ts +22 -0
- package/dist/l10n_in/.kosh/bindings.js +15 -0
- package/dist/l10n_in/.kosh/schema.d.ts +1343 -0
- package/dist/l10n_in/documents.d.ts +8 -0
- package/dist/l10n_in/documents.js +114 -0
- package/dist/l10n_in/einvoice.d.ts +253 -0
- package/dist/l10n_in/einvoice.js +485 -0
- package/dist/l10n_in/gstr.d.ts +109 -0
- package/dist/l10n_in/gstr.js +430 -0
- package/dist/l10n_in/index.d.ts +20 -0
- package/dist/l10n_in/index.js +232 -0
- package/dist/l10n_in/module.d.ts +7823 -0
- package/dist/l10n_in/module.js +427 -0
- package/dist/l10n_in/routes-einvoice.d.ts +15 -0
- package/dist/l10n_in/routes-einvoice.js +124 -0
- package/dist/l10n_in/routes.d.ts +17 -0
- package/dist/l10n_in/routes.js +282 -0
- package/dist/l10n_in/web/EInvoicePanel.d.ts +8 -0
- package/dist/l10n_in/web/EInvoicePanel.js +63 -0
- package/dist/l10n_in/web/GstPage.d.ts +7 -0
- package/dist/l10n_in/web/GstPage.js +57 -0
- package/dist/l10n_in/web/GstSettings.d.ts +10 -0
- package/dist/l10n_in/web/GstSettings.js +36 -0
- package/dist/l10n_in/web/index.d.ts +15 -0
- package/dist/l10n_in/web/index.js +22 -0
- package/dist/l10n_in/web/messages.d.ts +2 -0
- package/dist/l10n_in/web/messages.js +166 -0
- package/dist/modules-sdk/demo.d.ts +2 -0
- package/dist/modules-sdk/demo.js +7 -3
- package/dist/modules-sdk/index.d.ts +2 -2
- package/dist/modules-sdk/index.js +1 -1
- package/dist/modules-sdk/manifest.d.ts +132 -0
- package/dist/modules-sdk/manifest.js +22 -0
- package/dist/notify/inbox.js +21 -0
- package/dist/notify/messages.js +4 -0
- package/dist/notify/models.d.ts +1 -1
- package/dist/notify/models.js +2 -0
- package/dist/notify/web/messages.js +4 -0
- package/dist/portal/.kosh/bindings.d.ts +22 -0
- package/dist/portal/.kosh/bindings.js +15 -0
- package/dist/portal/.kosh/schema.d.ts +346 -0
- package/dist/portal/announce.d.ts +16 -0
- package/dist/portal/announce.js +117 -0
- package/dist/portal/documents.d.ts +17 -0
- package/dist/portal/documents.js +61 -0
- package/dist/portal/index.d.ts +20 -0
- package/dist/portal/index.js +65 -0
- package/dist/portal/module.d.ts +1405 -0
- package/dist/portal/module.js +84 -0
- package/dist/portal/routes-documents.d.ts +2 -0
- package/dist/portal/routes-documents.js +391 -0
- package/dist/portal/routes.d.ts +13 -0
- package/dist/portal/routes.js +313 -0
- package/dist/portal/rules.d.ts +11 -0
- package/dist/portal/rules.js +25 -0
- package/dist/portal/web/PortalAccess.d.ts +8 -0
- package/dist/portal/web/PortalAccess.js +66 -0
- package/dist/portal/web/index.d.ts +12 -0
- package/dist/portal/web/index.js +17 -0
- package/dist/portal/web/messages.d.ts +2 -0
- package/dist/portal/web/messages.js +33 -0
- package/dist/product/.kosh/bindings.js +1 -1
- package/dist/product/.kosh/schema.d.ts +12 -3
- package/dist/product/demo.js +6 -5
- package/dist/product/index.d.ts +1 -0
- package/dist/product/index.js +17 -4
- package/dist/product/module.d.ts +346 -14
- package/dist/product/module.js +48 -18
- package/dist/product/taxes.d.ts +59 -0
- package/dist/product/taxes.js +61 -0
- package/dist/project/.kosh/bindings.js +1 -1
- package/dist/project/.kosh/schema.d.ts +3 -1
- package/dist/project/module.d.ts +20 -0
- package/dist/project_portal/.kosh/bindings.d.ts +22 -0
- package/dist/project_portal/.kosh/bindings.js +15 -0
- package/dist/project_portal/.kosh/schema.d.ts +671 -0
- package/dist/project_portal/index.d.ts +9 -0
- package/dist/project_portal/index.js +42 -0
- package/dist/project_portal/module.d.ts +2142 -0
- package/dist/project_portal/module.js +22 -0
- package/dist/purchase/.kosh/bindings.js +1 -1
- package/dist/purchase/.kosh/schema.d.ts +15 -4
- package/dist/purchase/demo.js +3 -3
- package/dist/purchase/index.js +1 -1
- package/dist/purchase/module.d.ts +232 -8
- package/dist/purchase/module.js +51 -9
- package/dist/purchase_account/.kosh/bindings.js +1 -1
- package/dist/purchase_account/.kosh/schema.d.ts +144 -10
- package/dist/purchase_account/module.d.ts +2008 -641
- package/dist/purchase_account/module.js +2 -2
- package/dist/purchase_portal/.kosh/bindings.d.ts +22 -0
- package/dist/purchase_portal/.kosh/bindings.js +15 -0
- package/dist/purchase_portal/.kosh/schema.d.ts +822 -0
- package/dist/purchase_portal/index.d.ts +9 -0
- package/dist/purchase_portal/index.js +53 -0
- package/dist/purchase_portal/module.d.ts +2758 -0
- package/dist/purchase_portal/module.js +18 -0
- package/dist/reporting/documents.js +20 -3
- package/dist/reporting/index.d.ts +1 -0
- package/dist/reporting/index.js +1 -0
- package/dist/reporting/routes-documents.js +9 -2
- package/dist/reporting/words.d.ts +11 -0
- package/dist/reporting/words.js +229 -0
- package/dist/sale_account/.kosh/bindings.js +1 -1
- package/dist/sale_account/.kosh/schema.d.ts +144 -10
- package/dist/sale_account/module.d.ts +2008 -641
- package/dist/sale_account/module.js +3 -3
- package/dist/sale_portal/.kosh/bindings.d.ts +22 -0
- package/dist/sale_portal/.kosh/bindings.js +15 -0
- package/dist/sale_portal/.kosh/schema.d.ts +736 -0
- package/dist/sale_portal/index.d.ts +9 -0
- package/dist/sale_portal/index.js +71 -0
- package/dist/sale_portal/module.d.ts +2718 -0
- package/dist/sale_portal/module.js +19 -0
- package/dist/sale_project/.kosh/bindings.js +1 -1
- package/dist/sale_project/.kosh/schema.d.ts +15 -4
- package/dist/sale_project/module.d.ts +390 -10
- package/dist/sales/.kosh/bindings.js +1 -1
- package/dist/sales/.kosh/schema.d.ts +15 -4
- package/dist/sales/demo.js +3 -3
- package/dist/sales/index.js +67 -1
- package/dist/sales/module.d.ts +232 -8
- package/dist/sales/module.js +51 -12
- package/dist/server/auth/base.js +2 -0
- package/dist/server/auth/service.d.ts +2 -0
- package/dist/server/auth/service.js +7 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/server.js +26 -1
- package/dist/ui/api.d.ts +2 -0
- package/dist/ui/app.js +1 -1
- package/dist/ui/context.d.ts +3 -1
- package/dist/ui/context.js +2 -2
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/index.js +1 -1
- package/dist/ui/lib/format.d.ts +2 -0
- package/dist/ui/lib/format.js +3 -1
- package/dist/ui/views/FormView.js +19 -6
- 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
|
|
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 ?
|
|
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
|
|
434
|
-
const
|
|
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
|
|
4
|
-
*
|
|
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
|
|
5
|
-
*
|
|
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
|
-
|
|
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: [
|
|
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
|
};
|