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.
- package/README.md +74 -67
- 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 +1380 -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 +172 -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/assistant/routes.js +1 -1
- package/dist/automation/engine.js +1 -1
- package/dist/automation/web/messages.js +2 -2
- package/dist/cli/apps.d.ts +1 -1
- package/dist/cli/apps.js +14 -14
- package/dist/cli/bin.js +3 -0
- package/dist/cli/index.js +11 -11
- package/dist/cli/init.d.ts +1 -1
- package/dist/cli/init.js +224 -78
- package/dist/cli/launch.d.ts +16 -0
- package/dist/cli/launch.js +138 -0
- package/dist/cli/project.d.ts +7 -7
- package/dist/cli/project.js +25 -25
- package/dist/cli/prompt.d.ts +35 -0
- package/dist/cli/prompt.js +48 -2
- package/dist/cli/templates.d.ts +14 -6
- package/dist/cli/templates.js +61 -34
- package/dist/cli/tui.d.ts +7 -0
- package/dist/cli/tui.js +254 -0
- package/dist/cli/ui.d.ts +21 -8
- package/dist/cli/ui.js +65 -19
- package/dist/contacts/.kosh/bindings.js +1 -1
- package/dist/contacts/.kosh/schema.d.ts +33 -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 +1311 -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 +1373 -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/mail/index.js +2 -2
- 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/loader.js +2 -2
- 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 +7 -3
- package/dist/notify/models.d.ts +1 -1
- package/dist/notify/models.js +2 -0
- package/dist/notify/routes.js +1 -1
- package/dist/notify/web/messages.js +5 -1
- package/dist/portal/.kosh/bindings.d.ts +22 -0
- package/dist/portal/.kosh/bindings.js +15 -0
- package/dist/portal/.kosh/schema.d.ts +376 -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 +42 -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 +33 -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 +701 -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 +45 -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 +174 -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 +852 -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 +174 -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 +766 -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 +45 -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 +45 -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/api/client.js +1 -1
- package/dist/server/api/openapi.js +17 -1
- package/dist/server/apps.js +5 -5
- package/dist/server/auth/api-keys.d.ts +13 -0
- package/dist/server/auth/api-keys.js +65 -0
- package/dist/server/auth/base.js +26 -0
- package/dist/server/auth/crypto.js +1 -1
- package/dist/server/auth/scopes.d.ts +12 -0
- package/dist/server/auth/scopes.js +51 -0
- package/dist/server/auth/service.d.ts +59 -1
- package/dist/server/auth/service.js +236 -6
- package/dist/server/demo.d.ts +1 -1
- package/dist/server/demo.js +1 -1
- package/dist/server/env-names.d.ts +2 -0
- package/dist/server/env-names.js +16 -0
- package/dist/server/host.js +5 -1
- package/dist/server/identity.d.ts +7 -0
- package/dist/server/index.d.ts +2 -1
- package/dist/server/index.js +2 -1
- package/dist/server/registry.js +1 -1
- package/dist/server/server.d.ts +5 -0
- package/dist/server/server.js +71 -6
- package/dist/settings/routes.js +1 -1
- package/dist/settings/web/ApiKeysPage.d.ts +8 -0
- package/dist/settings/web/ApiKeysPage.js +70 -0
- package/dist/settings/web/index.d.ts +1 -0
- package/dist/settings/web/index.js +4 -0
- package/dist/settings/web/messages.js +62 -0
- package/dist/storage/routes.js +1 -1
- package/dist/studio/build.js +1 -1
- package/dist/studio/generate.js +4 -4
- package/dist/studio/module.js +5 -5
- package/dist/studio/web/messages.js +2 -2
- package/dist/ui/api.d.ts +2 -0
- package/dist/ui/app.js +27 -4
- package/dist/ui/context.d.ts +3 -1
- package/dist/ui/context.js +2 -2
- package/dist/ui/i18n/messages.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/pwa/worker.js +2 -2
- package/dist/ui/registry.d.ts +15 -0
- package/dist/ui/registry.js +20 -0
- package/dist/ui/views/FormView.js +19 -6
- 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
|
|
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
|
};
|
package/dist/assistant/routes.js
CHANGED
|
@@ -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
|
|
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 (
|
|
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 (
|
|
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), ব্যর্থ হলে আবার চেষ্টা; হোস্ট অনুমোদিত তালিকায় থাকতে হবে (
|
|
172
|
+
webhookHint: 'স্বাক্ষরিত JSON POST (X-Kosh-Signature: নিয়মের ওয়েবহুক সিক্রেট দিয়ে sha256 HMAC), ব্যর্থ হলে আবার চেষ্টা; হোস্ট অনুমোদিত তালিকায় থাকতে হবে (SHUTRO_ERP_AUTOMATION_WEBHOOK_HOSTS)।',
|
|
173
173
|
condition: 'এবং রেকর্ড মেলে',
|
|
174
174
|
conditionJson: 'শর্ত (ডোমেইন)',
|
|
175
175
|
field: 'ফিল্ড',
|
package/dist/cli/apps.d.ts
CHANGED
|
@@ -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 `
|
|
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
|
-
* `
|
|
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:
|
|
16
|
+
export const APPS_USAGE = `Usage: shutro-erp apps [command] [app …] [options]
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
|
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}:
|
|
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 `
|
|
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:
|
|
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):
|
|
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
|
|
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: '
|
|
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:
|
|
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 (
|
|
28
|
-
apps The apps: installed, disabled, available (
|
|
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
|
|
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/
|
|
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('
|
|
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/
|
|
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
|
|
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(`
|
|
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(`
|
|
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
|
|
143
|
+
write(`Unknown command '${argv.join(' ')}'. Run shutro-erp --help.\n`);
|
|
144
144
|
return 2;
|
|
145
145
|
}
|
package/dist/cli/init.d.ts
CHANGED
|
@@ -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 `
|
|
17
|
+
/** Runs `shutro-erp init`. Returns the exit code. */
|
|
18
18
|
export declare function runInit(argv: readonly string[], io: InitIo): Promise<number>;
|