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
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The GST returns over HTTP (RFC 0016 step 5, part 3): `GET /api/v1/l10n_in/gstr/1?month=2026-10` and
|
|
3
|
+
* `/gstr/3b?month=…`, as JSON for the screen, or with `format=xlsx` / `format=pdf` as a file, and GSTR-1 with
|
|
4
|
+
* `format=portal` as the GST portal's offline JSON. As the user: reading needs the documents' read right, a file
|
|
5
|
+
* their export right, and every file is in the audit log.
|
|
6
|
+
*/
|
|
7
|
+
import { moneyFormat, renderPdf, workbook } from '../reporting/index.js';
|
|
8
|
+
import { audit, HttpProblem } from '../server/index.js';
|
|
9
|
+
import { orm } from '../server/orm.js';
|
|
10
|
+
import { gstr1, gstr1PortalJson, gstr3b, MonthError, monthRange, } from './gstr.js';
|
|
11
|
+
const isDecimal = (v) => typeof v !== 'string';
|
|
12
|
+
/** The JSON of a return: decimals as strings (amounts with two decimals). */
|
|
13
|
+
function plain(value, key = '') {
|
|
14
|
+
if (value instanceof orm.Decimal)
|
|
15
|
+
return key === 'rate' || key === 'quantity' ? value.toString() : value.toFixed(2);
|
|
16
|
+
if (Array.isArray(value))
|
|
17
|
+
return value.map((v) => plain(v));
|
|
18
|
+
if (value !== null && typeof value === 'object')
|
|
19
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, plain(v, k)]));
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
const HEADS = ['Taxable value', 'IGST', 'CGST', 'SGST', 'Cess'];
|
|
23
|
+
const heads = (h) => [h.txval, h.iamt, h.camt, h.samt, h.csamt];
|
|
24
|
+
const total = (docs) => docs.reduce((s, d) => d.items.reduce((t, i) => ({
|
|
25
|
+
txval: t.txval.plus(i.txval),
|
|
26
|
+
iamt: t.iamt.plus(i.iamt),
|
|
27
|
+
camt: t.camt.plus(i.camt),
|
|
28
|
+
samt: t.samt.plus(i.samt),
|
|
29
|
+
csamt: t.csamt.plus(i.csamt),
|
|
30
|
+
}), s), {
|
|
31
|
+
txval: orm.toDecimal(0),
|
|
32
|
+
iamt: orm.toDecimal(0),
|
|
33
|
+
camt: orm.toDecimal(0),
|
|
34
|
+
samt: orm.toDecimal(0),
|
|
35
|
+
csamt: orm.toDecimal(0),
|
|
36
|
+
});
|
|
37
|
+
const SECTIONS = {
|
|
38
|
+
b2b: '4A B2B invoices',
|
|
39
|
+
b2cl: '5 B2C large invoices',
|
|
40
|
+
exp: '6A Exports',
|
|
41
|
+
b2cs: '7 B2C others',
|
|
42
|
+
cdnr: '9B Credit / debit notes (registered)',
|
|
43
|
+
cdnur: '9B Credit / debit notes (unregistered)',
|
|
44
|
+
};
|
|
45
|
+
/** GSTR-1's sections: the documents (or B2C rows) of each, and their totals by head. */
|
|
46
|
+
export function gstr1Summary(r) {
|
|
47
|
+
const b2cs = r.b2cs.reduce((s, x) => ({
|
|
48
|
+
txval: s.txval.plus(x.txval),
|
|
49
|
+
iamt: s.iamt.plus(x.iamt),
|
|
50
|
+
camt: s.camt.plus(x.camt),
|
|
51
|
+
samt: s.samt.plus(x.samt),
|
|
52
|
+
csamt: s.csamt.plus(x.csamt),
|
|
53
|
+
}), total([]));
|
|
54
|
+
return [
|
|
55
|
+
{ section: 'b2b', documents: r.b2b.length, ...total(r.b2b) },
|
|
56
|
+
{ section: 'b2cl', documents: r.b2cl.length, ...total(r.b2cl) },
|
|
57
|
+
{ section: 'exp', documents: r.exp.length, ...total(r.exp) },
|
|
58
|
+
{ section: 'b2cs', documents: r.b2cs.length, ...b2cs },
|
|
59
|
+
{ section: 'cdnr', documents: r.cdnr.length, ...total(r.cdnr) },
|
|
60
|
+
{ section: 'cdnur', documents: r.cdnur.length, ...total(r.cdnur) },
|
|
61
|
+
];
|
|
62
|
+
}
|
|
63
|
+
/** GSTR-1's sections as tables (the workbook's sheets; the first is the summary). */
|
|
64
|
+
function gstr1Tables(r) {
|
|
65
|
+
const docRows = (docs, withGstin) => [
|
|
66
|
+
[
|
|
67
|
+
...(withGstin ? ['GSTIN of recipient'] : []),
|
|
68
|
+
'Name',
|
|
69
|
+
'Number',
|
|
70
|
+
'Date',
|
|
71
|
+
'Value',
|
|
72
|
+
'Place of supply',
|
|
73
|
+
'Reverse charge',
|
|
74
|
+
'Rate %',
|
|
75
|
+
...HEADS,
|
|
76
|
+
],
|
|
77
|
+
...docs.flatMap((d) => d.items.map((i) => [
|
|
78
|
+
...(withGstin ? [d.gstin ?? ''] : []),
|
|
79
|
+
d.partner,
|
|
80
|
+
`${d.noteType === undefined ? '' : `${d.noteType === 'C' ? 'Credit' : 'Debit'} note `}${d.number}`,
|
|
81
|
+
d.date,
|
|
82
|
+
d.value,
|
|
83
|
+
d.pos,
|
|
84
|
+
d.reverseCharge ? 'Y' : 'N',
|
|
85
|
+
i.rate.toString(),
|
|
86
|
+
...heads(i),
|
|
87
|
+
])),
|
|
88
|
+
];
|
|
89
|
+
return [
|
|
90
|
+
{
|
|
91
|
+
name: 'Summary',
|
|
92
|
+
rows: [
|
|
93
|
+
['Section', 'Documents', ...HEADS],
|
|
94
|
+
...gstr1Summary(r).map((s) => [SECTIONS[s.section], String(s.documents), ...heads(s)]),
|
|
95
|
+
],
|
|
96
|
+
},
|
|
97
|
+
{ name: 'b2b', rows: docRows(r.b2b, true) },
|
|
98
|
+
{ name: 'b2cl', rows: docRows(r.b2cl, false) },
|
|
99
|
+
{
|
|
100
|
+
name: 'b2cs',
|
|
101
|
+
rows: [
|
|
102
|
+
['Type', 'Place of supply', 'Rate %', ...HEADS],
|
|
103
|
+
...r.b2cs.map((s) => [s.supply, s.pos, s.rate.toString(), ...heads(s)]),
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
{ name: 'exp', rows: docRows(r.exp, false) },
|
|
107
|
+
{ name: 'cdnr', rows: docRows(r.cdnr, true) },
|
|
108
|
+
{ name: 'cdnur', rows: docRows(r.cdnur, false) },
|
|
109
|
+
{
|
|
110
|
+
name: 'nil',
|
|
111
|
+
rows: [
|
|
112
|
+
['Supply', 'Nil-rated', 'Exempt', 'Non-GST'],
|
|
113
|
+
...r.nil.map((n) => [n.supply, n.nil, n.exempt, n.nonGst]),
|
|
114
|
+
],
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: 'hsn',
|
|
118
|
+
rows: [
|
|
119
|
+
['HSN / SAC', 'Description', 'Quantity', 'Rate %', ...HEADS],
|
|
120
|
+
...r.hsn.map((h) => [
|
|
121
|
+
h.hsn,
|
|
122
|
+
h.description,
|
|
123
|
+
h.quantity.toString(),
|
|
124
|
+
h.rate.toString(),
|
|
125
|
+
...heads(h),
|
|
126
|
+
]),
|
|
127
|
+
],
|
|
128
|
+
},
|
|
129
|
+
];
|
|
130
|
+
}
|
|
131
|
+
function gstr3bTable(r) {
|
|
132
|
+
return [
|
|
133
|
+
['', ...HEADS],
|
|
134
|
+
['3.1 (a) Outward taxable supplies', ...heads(r.outwardTaxable)],
|
|
135
|
+
['3.1 (b) Outward zero-rated supplies', ...heads(r.outwardZeroRated)],
|
|
136
|
+
['3.1 (c) Other outward supplies (nil-rated, exempt)', ...heads(r.outwardNilExempt)],
|
|
137
|
+
['3.1 (d) Inward supplies liable to reverse charge', ...heads(r.inwardReverseCharge)],
|
|
138
|
+
['3.1 (e) Non-GST outward supplies', ...heads(r.outwardNonGst)],
|
|
139
|
+
['4 (A)(3) ITC: inward supplies liable to reverse charge', ...heads(r.itcReverseCharge)],
|
|
140
|
+
['4 (A)(5) ITC: all other', ...heads(r.itcOther)],
|
|
141
|
+
[
|
|
142
|
+
'5 Exempt, nil and non-GST inward supplies: from another state',
|
|
143
|
+
r.inwardExempt.inter,
|
|
144
|
+
'',
|
|
145
|
+
'',
|
|
146
|
+
'',
|
|
147
|
+
'',
|
|
148
|
+
],
|
|
149
|
+
[
|
|
150
|
+
'5 Exempt, nil and non-GST inward supplies: within the state',
|
|
151
|
+
r.inwardExempt.intra,
|
|
152
|
+
'',
|
|
153
|
+
'',
|
|
154
|
+
'',
|
|
155
|
+
'',
|
|
156
|
+
],
|
|
157
|
+
[
|
|
158
|
+
'6.1 Tax payable',
|
|
159
|
+
'',
|
|
160
|
+
r.payable.igst.tax,
|
|
161
|
+
r.payable.cgst.tax,
|
|
162
|
+
r.payable.sgst.tax,
|
|
163
|
+
r.payable.cess.tax,
|
|
164
|
+
],
|
|
165
|
+
[
|
|
166
|
+
'6.1 Input tax credit',
|
|
167
|
+
'',
|
|
168
|
+
r.payable.igst.credit,
|
|
169
|
+
r.payable.cgst.credit,
|
|
170
|
+
r.payable.sgst.credit,
|
|
171
|
+
r.payable.cess.credit,
|
|
172
|
+
],
|
|
173
|
+
[
|
|
174
|
+
'6.1 To pay (negative: carried forward)',
|
|
175
|
+
'',
|
|
176
|
+
r.payable.igst.net,
|
|
177
|
+
r.payable.cgst.net,
|
|
178
|
+
r.payable.sgst.net,
|
|
179
|
+
r.payable.cess.net,
|
|
180
|
+
],
|
|
181
|
+
];
|
|
182
|
+
}
|
|
183
|
+
const sheet = (name, rows) => {
|
|
184
|
+
const format = moneyFormat('INR');
|
|
185
|
+
return {
|
|
186
|
+
name,
|
|
187
|
+
rows: rows.map((row) => row.map((v) => (isDecimal(v) ? { decimal: v.toFixed(2), format } : v))),
|
|
188
|
+
};
|
|
189
|
+
};
|
|
190
|
+
const esc = (s) => s.replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c]);
|
|
191
|
+
function html(title, subtitle, tables) {
|
|
192
|
+
const number = new Intl.NumberFormat('en-IN', {
|
|
193
|
+
minimumFractionDigits: 2,
|
|
194
|
+
maximumFractionDigits: 2,
|
|
195
|
+
});
|
|
196
|
+
const cell = (v, tag) => isDecimal(v)
|
|
197
|
+
? `<${tag} class="n">${number.format(Number(v.toFixed(2)))}</${tag}>`
|
|
198
|
+
: `<${tag}>${esc(v)}</${tag}>`;
|
|
199
|
+
const body = tables
|
|
200
|
+
.filter((t) => t.rows.length > 1)
|
|
201
|
+
.map((t) => `<h2>${esc(t.name)}</h2><table><thead><tr>${(t.rows[0] ?? []).map((v) => cell(v, 'th')).join('')}</tr></thead><tbody>${t.rows
|
|
202
|
+
.slice(1)
|
|
203
|
+
.map((r) => `<tr>${r.map((v) => cell(v, 'td')).join('')}</tr>`)
|
|
204
|
+
.join('')}</tbody></table>`)
|
|
205
|
+
.join('');
|
|
206
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>${esc(title)}</title><style>
|
|
207
|
+
body { font: 8.5pt system-ui, sans-serif; color: #111; }
|
|
208
|
+
h1 { font-size: 14pt; margin: 0 0 2mm; } h2 { font-size: 10pt; margin: 5mm 0 1mm; } p { margin: 0 0 3mm; }
|
|
209
|
+
table { width: 100%; border-collapse: collapse; }
|
|
210
|
+
th, td { border-bottom: 1px solid #ccc; padding: 1mm 1.5mm; text-align: start; vertical-align: top; }
|
|
211
|
+
th { background: #f2f2f2; } .n { text-align: end; white-space: nowrap; }
|
|
212
|
+
</style></head><body><h1>${esc(title)}</h1><p>${esc(subtitle)}</p>${body}</body></html>`;
|
|
213
|
+
}
|
|
214
|
+
export function registerGstRoutes(context) {
|
|
215
|
+
context.app.get('/api/v1/l10n_in/gstr/:form', async (request, reply) => {
|
|
216
|
+
const form = request.params.form;
|
|
217
|
+
if (form !== '1' && form !== '3b')
|
|
218
|
+
throw new HttpProblem(404, 'unknown-form', 'Unknown return', 'The returns are 1 and 3b.');
|
|
219
|
+
const query = request.query;
|
|
220
|
+
const month = typeof query.month === 'string' ? query.month : '';
|
|
221
|
+
const format = query.format === 'xlsx' ||
|
|
222
|
+
query.format === 'pdf' ||
|
|
223
|
+
(query.format === 'portal' && form === '1')
|
|
224
|
+
? query.format
|
|
225
|
+
: 'json';
|
|
226
|
+
try {
|
|
227
|
+
monthRange(month);
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
if (error instanceof MonthError)
|
|
231
|
+
throw new HttpProblem(400, 'invalid-month', 'Invalid month', error.message);
|
|
232
|
+
throw error;
|
|
233
|
+
}
|
|
234
|
+
const data = await request.env(async (raw) => {
|
|
235
|
+
const env = raw;
|
|
236
|
+
env.access.check('account.move', format === 'json' ? 'read' : 'export');
|
|
237
|
+
const result = form === '1' ? await gstr1(env, month) : await gstr3b(env, month);
|
|
238
|
+
if (format !== 'json')
|
|
239
|
+
await audit(raw, {
|
|
240
|
+
kind: 'data.export',
|
|
241
|
+
summary: `Exported GSTR-${form.toUpperCase()} for ${month} as ${format}.`,
|
|
242
|
+
targetModel: 'account.move',
|
|
243
|
+
details: { form, month },
|
|
244
|
+
ip: request.ip,
|
|
245
|
+
});
|
|
246
|
+
return result;
|
|
247
|
+
},
|
|
248
|
+
// A file is an export: its audit event is written.
|
|
249
|
+
{ write: format !== 'json' });
|
|
250
|
+
if (format === 'json')
|
|
251
|
+
return form === '1'
|
|
252
|
+
? { ...plain(data), summary: plain(gstr1Summary(data)) }
|
|
253
|
+
: plain(data);
|
|
254
|
+
const title = `GSTR-${form.toUpperCase()}`;
|
|
255
|
+
const tables = form === '1'
|
|
256
|
+
? gstr1Tables(data)
|
|
257
|
+
: [{ name: 'GSTR-3B', rows: gstr3bTable(data) }];
|
|
258
|
+
const [y, m] = month.split('-');
|
|
259
|
+
const name = `${title}_${data.gstin ?? 'GSTIN'}_${m}${y}`;
|
|
260
|
+
if (format === 'portal') {
|
|
261
|
+
await reply
|
|
262
|
+
.type('application/json')
|
|
263
|
+
.header('content-disposition', `attachment; filename="${name}.json"`)
|
|
264
|
+
.send(JSON.stringify(gstr1PortalJson(data)));
|
|
265
|
+
return reply;
|
|
266
|
+
}
|
|
267
|
+
const content = format === 'xlsx'
|
|
268
|
+
? workbook(tables.map((t) => sheet(t.name, t.rows)))
|
|
269
|
+
: await renderPdf(html(title, `GSTIN ${data.gstin ?? '—'} · ${month}`, tables), {
|
|
270
|
+
paper: 'A4',
|
|
271
|
+
orientation: 'landscape',
|
|
272
|
+
blocks: [],
|
|
273
|
+
});
|
|
274
|
+
await reply
|
|
275
|
+
.type(format === 'xlsx'
|
|
276
|
+
? 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
|
277
|
+
: 'application/pdf')
|
|
278
|
+
.header('content-disposition', `attachment; filename="${name}.${format}"`)
|
|
279
|
+
.send(content);
|
|
280
|
+
return reply;
|
|
281
|
+
});
|
|
282
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* On a posted customer invoice or note (RFC 0016 step 5, part 4): its e-invoice (status, IRN, acknowledgement; register,
|
|
3
|
+
* cancel within 24 hours) and, on invoices, its e-way bill (the transport, then generate; cancel within 24 hours). The
|
|
4
|
+
* server checks and decides; the panel shows what it answers, refusals included.
|
|
5
|
+
*/
|
|
6
|
+
import { type NodeComponentProps } from '../../ui/index.js';
|
|
7
|
+
import { type ReactElement } from 'react';
|
|
8
|
+
export declare function EInvoicePanel({ api, record, version, reload, }: NodeComponentProps): ReactElement | null;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* On a posted customer invoice or note (RFC 0016 step 5, part 4): its e-invoice (status, IRN, acknowledgement; register,
|
|
4
|
+
* cancel within 24 hours) and, on invoices, its e-way bill (the transport, then generate; cancel within 24 hours). The
|
|
5
|
+
* server checks and decides; the panel shows what it answers, refusals included.
|
|
6
|
+
*/
|
|
7
|
+
import { Alert, ApiError, Button, Input, Label, Select } from '../../ui/index.js';
|
|
8
|
+
import { useMutation, useQuery } from '@tanstack/react-query';
|
|
9
|
+
import { useId, useState } from 'react';
|
|
10
|
+
import { useTranslation } from 'react-i18next';
|
|
11
|
+
const CUSTOMER_DOCUMENTS = ['out_invoice', 'out_refund', 'out_debit'];
|
|
12
|
+
export function EInvoicePanel({ api, record, version, reload, }) {
|
|
13
|
+
const { t } = useTranslation('l10n_in');
|
|
14
|
+
const id = useId();
|
|
15
|
+
const moveId = record?.['id'];
|
|
16
|
+
const shown = record !== null &&
|
|
17
|
+
record['state'] === 'posted' &&
|
|
18
|
+
CUSTOMER_DOCUMENTS.includes(String(record['kind']));
|
|
19
|
+
const state = useQuery({
|
|
20
|
+
queryKey: ['l10n_in', 'einvoice', moveId, version],
|
|
21
|
+
queryFn: () => api.call('GET', `/api/v1/l10n_in/einvoice/${moveId}`),
|
|
22
|
+
enabled: shown,
|
|
23
|
+
});
|
|
24
|
+
const [reason, setReason] = useState('2');
|
|
25
|
+
const [mode, setMode] = useState('road');
|
|
26
|
+
const [distance, setDistance] = useState('');
|
|
27
|
+
const [vehicle, setVehicle] = useState('');
|
|
28
|
+
const [transporter, setTransporter] = useState('');
|
|
29
|
+
const act = useMutation({
|
|
30
|
+
mutationFn: (request) => api.call('POST', `/api/v1/l10n_in/${request.path}`, request.body ?? {}),
|
|
31
|
+
onSettled: async () => {
|
|
32
|
+
await state.refetch();
|
|
33
|
+
reload();
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
if (!shown || state.data === undefined || !state.data.applicable)
|
|
37
|
+
return null;
|
|
38
|
+
const s = state.data;
|
|
39
|
+
const error = act.error instanceof ApiError
|
|
40
|
+
? act.error.message
|
|
41
|
+
: act.error === null
|
|
42
|
+
? null
|
|
43
|
+
: String(act.error);
|
|
44
|
+
return (_jsxs("section", { className: "grid gap-3 rounded-lg border p-4", "aria-labelledby": `${id}-title`, children: [_jsx("h2", { id: `${id}-title`, className: "font-medium", children: t('einvoice.title') }), s.provider === null ? (_jsx("p", { className: "text-sm text-muted-foreground", children: t('einvoice.noProvider') })) : null, error === null ? null : _jsx(Alert, { tone: "danger", children: error }), _jsxs("dl", { className: "grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 text-sm", children: [_jsx("dt", { className: "text-muted-foreground", children: t('einvoice.status') }), _jsx("dd", { children: t(`einvoice.states.${s.status ?? 'none'}`) }), s.irn === null ? null : (_jsxs(_Fragment, { children: [_jsx("dt", { className: "text-muted-foreground", children: "IRN" }), _jsx("dd", { className: "break-all font-mono text-xs", children: s.irn }), _jsx("dt", { className: "text-muted-foreground", children: t('einvoice.ack') }), _jsxs("dd", { children: [s.ackNo, " \u00B7 ", s.ackDate] })] }))] }), s.status === 'failed' && s.error !== null ? _jsx(Alert, { tone: "warning", children: s.error }) : null, s.provider === null ? null : (_jsxs("div", { className: "flex flex-wrap items-end gap-2", children: [s.status !== 'registered' && s.status !== 'cancelled' ? (_jsx(Button, { variant: "primary", busy: act.isPending, onClick: () => act.mutate({ path: `einvoice/${moveId}` }), children: t('einvoice.register') })) : null, s.status === 'registered' ? (_jsxs(_Fragment, { children: [_jsx(Select, { id: `${id}-reason`, label: t('einvoice.reason'), className: "w-56", value: reason, options: ['1', '2', '3', '4'].map((r) => ({
|
|
45
|
+
value: r,
|
|
46
|
+
label: t(`einvoice.reasons.${r}`),
|
|
47
|
+
})), onChange: setReason }), _jsx(Button, { variant: "secondary", busy: act.isPending, onClick: () => act.mutate({ path: `einvoice/${moveId}/cancel`, body: { reason } }), children: t('einvoice.cancel') })] })) : null] })), s.provider === null || record?.['kind'] !== 'out_invoice' ? null : (_jsxs("div", { className: "grid gap-2 border-t pt-3", children: [_jsx("h3", { className: "text-sm font-medium", children: t('einvoice.ewayBill') }), s.ewayBill.status === 'generated' ? (_jsxs("div", { className: "flex flex-wrap items-center gap-3 text-sm", children: [_jsx("span", { children: t('einvoice.ewbNumber', {
|
|
48
|
+
number: s.ewayBill.number,
|
|
49
|
+
until: s.ewayBill.validUntil,
|
|
50
|
+
vehicle: s.ewayBill.vehicleNo,
|
|
51
|
+
}) }), _jsx(Button, { variant: "secondary", busy: act.isPending, onClick: () => act.mutate({ path: `ewaybill/${moveId}/cancel`, body: { reason: '2' } }), children: t('einvoice.ewbCancel') })] })) : (_jsxs(_Fragment, { children: [s.ewayBill.status === 'failed' && s.ewayBill.error !== null ? (_jsx(Alert, { tone: "warning", children: s.ewayBill.error })) : null, s.ewayBill.status === 'cancelled' ? (_jsx("p", { className: "text-sm text-muted-foreground", children: t('einvoice.ewbCancelled') })) : null, _jsxs("div", { className: "flex flex-wrap items-end gap-2", children: [_jsx(Select, { id: `${id}-mode`, label: t('einvoice.mode'), className: "w-32", value: mode, options: ['road', 'rail', 'air', 'ship'].map((m) => ({
|
|
52
|
+
value: m,
|
|
53
|
+
label: t(`einvoice.modes.${m}`),
|
|
54
|
+
})), onChange: setMode }), _jsxs("div", { className: "grid gap-1", children: [_jsx(Label, { htmlFor: `${id}-distance`, children: t('einvoice.distance') }), _jsx(Input, { id: `${id}-distance`, inputMode: "numeric", className: "w-24", value: distance, onChange: (e) => setDistance(e.target.value) })] }), _jsxs("div", { className: "grid gap-1", children: [_jsx(Label, { htmlFor: `${id}-vehicle`, children: t('einvoice.vehicle') }), _jsx(Input, { id: `${id}-vehicle`, className: "w-36", value: vehicle, onChange: (e) => setVehicle(e.target.value) })] }), _jsxs("div", { className: "grid gap-1", children: [_jsx(Label, { htmlFor: `${id}-transporter`, children: t('einvoice.transporter') }), _jsx(Input, { id: `${id}-transporter`, className: "w-40", value: transporter, onChange: (e) => setTransporter(e.target.value) })] }), _jsx(Button, { variant: "primary", busy: act.isPending, onClick: () => act.mutate({
|
|
55
|
+
path: `ewaybill/${moveId}`,
|
|
56
|
+
body: {
|
|
57
|
+
mode,
|
|
58
|
+
distanceKm: Number(distance),
|
|
59
|
+
vehicleNo: vehicle,
|
|
60
|
+
transporterId: transporter,
|
|
61
|
+
},
|
|
62
|
+
}), children: t('einvoice.ewbGenerate') })] })] }))] }))] }));
|
|
63
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accounting › Reporting › GST returns (RFC 0016 step 5, part 3): GSTR-1 by section and GSTR-3B by table for a
|
|
3
|
+
* month, as the server computes them, with their XLSX and PDF files and GSTR-1's JSON for the GST portal.
|
|
4
|
+
*/
|
|
5
|
+
import { type PageProps } from '../../ui/index.js';
|
|
6
|
+
import { type ReactElement } from 'react';
|
|
7
|
+
export declare function GstPage({ api }: PageProps): ReactElement;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Accounting › Reporting › GST returns (RFC 0016 step 5, part 3): GSTR-1 by section and GSTR-3B by table for a
|
|
4
|
+
* month, as the server computes them, with their XLSX and PDF files and GSTR-1's JSON for the GST portal.
|
|
5
|
+
*/
|
|
6
|
+
import { ApiError, Button, Input, Label, useKosh } from '../../ui/index.js';
|
|
7
|
+
import { useMutation, useQuery } from '@tanstack/react-query';
|
|
8
|
+
import { useId, useState } from 'react';
|
|
9
|
+
import { useTranslation } from 'react-i18next';
|
|
10
|
+
import { GstSettings } from './GstSettings.js';
|
|
11
|
+
const HEADS = ['txval', 'iamt', 'camt', 'samt', 'csamt'];
|
|
12
|
+
const ROWS_3B = [
|
|
13
|
+
'outwardTaxable',
|
|
14
|
+
'outwardZeroRated',
|
|
15
|
+
'outwardNilExempt',
|
|
16
|
+
'inwardReverseCharge',
|
|
17
|
+
'outwardNonGst',
|
|
18
|
+
'itcReverseCharge',
|
|
19
|
+
'itcOther',
|
|
20
|
+
];
|
|
21
|
+
function saveFile(blob, filename) {
|
|
22
|
+
const url = URL.createObjectURL(blob);
|
|
23
|
+
const a = document.createElement('a');
|
|
24
|
+
a.href = url;
|
|
25
|
+
a.download = filename;
|
|
26
|
+
a.click();
|
|
27
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
28
|
+
}
|
|
29
|
+
const cell = 'px-2 py-1';
|
|
30
|
+
const amount = `${cell} text-end tabular-nums`;
|
|
31
|
+
export function GstPage({ api }) {
|
|
32
|
+
const { t } = useTranslation('l10n_in');
|
|
33
|
+
const { format } = useKosh();
|
|
34
|
+
const id = useId();
|
|
35
|
+
const [form, setForm] = useState('1');
|
|
36
|
+
const [month, setMonth] = useState(new Date().toISOString().slice(0, 7));
|
|
37
|
+
const path = `/api/v1/l10n_in/gstr/${form}?month=${month}`;
|
|
38
|
+
const data = useQuery({
|
|
39
|
+
queryKey: ['l10n_in', 'gstr', form, month],
|
|
40
|
+
queryFn: () => api.call('GET', path),
|
|
41
|
+
enabled: /^\d{4}-\d{2}$/.test(month),
|
|
42
|
+
});
|
|
43
|
+
const download = useMutation({
|
|
44
|
+
mutationFn: async (kind) => {
|
|
45
|
+
const { blob, filename } = await api.download(`${path}&format=${kind}`, undefined, 'GET');
|
|
46
|
+
saveFile(blob, filename);
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
const error = data.error ?? download.error;
|
|
50
|
+
const money = (v) => format.decimal(v, 2);
|
|
51
|
+
const headCells = (h) => HEADS.map((k) => (_jsx("td", { className: amount, children: money(h[k]) }, k)));
|
|
52
|
+
const header = (first) => (_jsx("thead", { className: "bg-muted", children: _jsxs("tr", { children: [_jsx("th", { className: `${cell} text-start`, children: first }), HEADS.map((k) => (_jsx("th", { className: `${cell} text-end`, children: t(`gst.heads.${k}`) }, k)))] }) }));
|
|
53
|
+
return (_jsxs("div", { className: "grid gap-4 p-4", children: [_jsx("h1", { className: "text-xl font-semibold", children: t('gst.title') }), _jsxs("div", { className: "flex flex-wrap items-end gap-2", children: [_jsxs("fieldset", { className: "flex gap-1", children: [_jsx("legend", { className: "sr-only", children: t('gst.form') }), ['1', '3b'].map((f) => (_jsx(Button, { variant: form === f ? 'primary' : 'secondary', "aria-pressed": form === f, onClick: () => setForm(f), children: t(`gst.forms.${f}`) }, f)))] }), _jsxs("div", { className: "grid gap-1", children: [_jsx(Label, { htmlFor: `${id}-month`, children: t('gst.month') }), _jsx(Input, { id: `${id}-month`, type: "month", value: month, onChange: (e) => setMonth(e.target.value) })] }), _jsx(Button, { variant: "secondary", disabled: download.isPending, onClick: () => download.mutate('xlsx'), children: t('gst.xlsx') }), _jsx(Button, { variant: "secondary", disabled: download.isPending, onClick: () => download.mutate('pdf'), children: t('gst.pdf') }), form === '1' ? (_jsx(Button, { variant: "secondary", disabled: download.isPending, onClick: () => download.mutate('portal'), children: t('gst.portal') })) : null] }), _jsx("p", { className: "text-sm text-muted-foreground", children: t('gst.hint', { gstin: data.data?.gstin ?? '—' }) }), error === null ? null : (_jsx("p", { role: "alert", className: "text-sm text-danger", children: error instanceof ApiError ? error.message : String(error) })), data.data === undefined ? null : 'summary' in data.data ? (_jsxs("div", { className: "grid gap-2 overflow-x-auto", children: [_jsxs("table", { className: "w-full text-sm", children: [header(t('gst.section')), _jsx("tbody", { children: data.data.summary.map((s) => (_jsxs("tr", { className: "border-b", children: [_jsxs("th", { scope: "row", className: `${cell} text-start font-normal`, children: [t(`gst.sections.${s.section}`), ' ', _jsxs("span", { className: "text-muted-foreground", children: ["(", s.documents, ")"] })] }), headCells(s)] }, s.section))) })] }), data.data.unclassified > 0 ? (_jsx("p", { role: "status", className: "text-sm text-warning", children: t('gst.unclassified', { count: data.data.unclassified }) })) : null] })) : (_jsx(Gstr3bTables, { data: data.data })), _jsx(GstSettings, { api: api })] }));
|
|
54
|
+
function Gstr3bTables({ data }) {
|
|
55
|
+
return (_jsxs("div", { className: "grid gap-4 overflow-x-auto", children: [_jsxs("table", { className: "w-full text-sm", children: [header(t('gst.table')), _jsxs("tbody", { children: [ROWS_3B.map((key) => (_jsxs("tr", { className: "border-b", children: [_jsx("th", { scope: "row", className: `${cell} text-start font-normal`, children: t(`gst.r3b.${key}`) }), headCells(data[key])] }, key))), _jsxs("tr", { className: "border-b", children: [_jsx("th", { scope: "row", className: `${cell} text-start font-normal`, children: t('gst.r3b.inwardExemptInter') }), _jsx("td", { className: amount, children: money(data.inwardExempt.inter) }), _jsx("td", { colSpan: 4 })] }), _jsxs("tr", { className: "border-b", children: [_jsx("th", { scope: "row", className: `${cell} text-start font-normal`, children: t('gst.r3b.inwardExemptIntra') }), _jsx("td", { className: amount, children: money(data.inwardExempt.intra) }), _jsx("td", { colSpan: 4 })] })] })] }), _jsxs("table", { className: "w-full max-w-2xl text-sm", children: [_jsx("thead", { className: "bg-muted", children: _jsxs("tr", { children: [_jsx("th", { className: `${cell} text-start`, children: t('gst.r3b.payable') }), ['igst', 'cgst', 'sgst', 'cess'].map((k) => (_jsx("th", { className: `${cell} text-end`, children: t(`gst.heads.${k === 'igst' ? 'iamt' : k === 'cgst' ? 'camt' : k === 'sgst' ? 'samt' : 'csamt'}`) }, k)))] }) }), _jsx("tbody", { children: ['tax', 'credit', 'net'].map((row) => (_jsxs("tr", { className: row === 'net' ? 'font-semibold' : 'border-b', children: [_jsx("th", { scope: "row", className: `${cell} text-start font-normal`, children: t(`gst.r3b.${row}`) }), ['igst', 'cgst', 'sgst', 'cess'].map((k) => (_jsx("td", { className: amount, children: money(data.payable[k][row]) }, k)))] }, row))) })] })] }));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The company's GST settings (RFC 0016 step 5, part 4), on the GST returns page: GSTIN, PAN, the address and PIN code
|
|
3
|
+
* the IRP needs, and e-invoicing on or off. Managers change them; the server checks the GSTIN against the company's
|
|
4
|
+
* state.
|
|
5
|
+
*/
|
|
6
|
+
import { type KoshApi } from '../../ui/index.js';
|
|
7
|
+
import { type ReactElement } from 'react';
|
|
8
|
+
export declare function GstSettings({ api }: {
|
|
9
|
+
readonly api: KoshApi;
|
|
10
|
+
}): ReactElement;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* The company's GST settings (RFC 0016 step 5, part 4), on the GST returns page: GSTIN, PAN, the address and PIN code
|
|
4
|
+
* the IRP needs, and e-invoicing on or off. Managers change them; the server checks the GSTIN against the company's
|
|
5
|
+
* state.
|
|
6
|
+
*/
|
|
7
|
+
import { Alert, ApiError, Button, Input, Label } from '../../ui/index.js';
|
|
8
|
+
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
9
|
+
import { useId, useState } from 'react';
|
|
10
|
+
import { useTranslation } from 'react-i18next';
|
|
11
|
+
const FIELDS = ['gstin', 'pan', 'street', 'city', 'zip'];
|
|
12
|
+
export function GstSettings({ api }) {
|
|
13
|
+
const { t } = useTranslation('l10n_in');
|
|
14
|
+
const id = useId();
|
|
15
|
+
const queryClient = useQueryClient();
|
|
16
|
+
const settings = useQuery({
|
|
17
|
+
queryKey: ['l10n_in', 'settings'],
|
|
18
|
+
queryFn: () => api.call('GET', '/api/v1/l10n_in/settings'),
|
|
19
|
+
});
|
|
20
|
+
const [edits, setEdits] = useState({});
|
|
21
|
+
const save = useMutation({
|
|
22
|
+
mutationFn: () => api.call('PUT', '/api/v1/l10n_in/settings', edits),
|
|
23
|
+
onSuccess: async () => {
|
|
24
|
+
setEdits({});
|
|
25
|
+
await queryClient.invalidateQueries({ queryKey: ['l10n_in'] });
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
const current = settings.data;
|
|
29
|
+
if (current === undefined)
|
|
30
|
+
return _jsx(_Fragment, {});
|
|
31
|
+
const value = (key) => String(edits[key] ?? current[key] ?? '');
|
|
32
|
+
const einvoice = Boolean(edits.einvoiceEnabled ?? current.einvoiceEnabled);
|
|
33
|
+
return (_jsxs("section", { className: "grid max-w-3xl gap-3 rounded-lg border p-4", "aria-labelledby": `${id}-title`, children: [_jsx("h2", { id: `${id}-title`, className: "font-medium", children: t('settings.title') }), _jsx("p", { className: "text-sm text-muted-foreground", children: current.provider === null
|
|
34
|
+
? t('settings.noProvider')
|
|
35
|
+
: t('settings.provider', { provider: current.provider }) }), save.error === null ? null : (_jsx(Alert, { tone: "danger", children: save.error instanceof ApiError ? save.error.message : String(save.error) })), save.isSuccess ? _jsx(Alert, { tone: "info", children: t('settings.saved') }) : null, _jsx("div", { className: "grid gap-3 sm:grid-cols-2", children: FIELDS.map((key) => (_jsxs("div", { className: "grid gap-1", children: [_jsx(Label, { htmlFor: `${id}-${key}`, children: t(`settings.${key}`) }), _jsx(Input, { id: `${id}-${key}`, value: value(key), onChange: (e) => setEdits({ ...edits, [key]: e.target.value }) })] }, key))) }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: einvoice, onChange: (e) => setEdits({ ...edits, einvoiceEnabled: e.target.checked }) }), t('settings.einvoice')] }), _jsx("div", { children: _jsx(Button, { variant: "primary", busy: save.isPending, onClick: () => save.mutate(), children: t('settings.save') }) })] }));
|
|
36
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
* The web contribution of `shutro-erp/l10n_in`: the GST returns page (GSTR-1, GSTR-3B, the company's GST settings) and
|
|
4
|
+
* the e-invoice panel on customer invoices.
|
|
5
|
+
*/
|
|
6
|
+
import { type Registry } from '../../ui/index.js';
|
|
7
|
+
import type { i18n } from 'i18next';
|
|
8
|
+
import { messages } from './messages.js';
|
|
9
|
+
export { EInvoicePanel } from './EInvoicePanel.js';
|
|
10
|
+
export { GstPage } from './GstPage.js';
|
|
11
|
+
export { GstSettings } from './GstSettings.js';
|
|
12
|
+
/** Registers the page (and the messages in `i18n`, when given). */
|
|
13
|
+
export declare function registerL10nInWeb(registry: Registry, i18n?: i18n): Registry;
|
|
14
|
+
/** The messages, named as the starter project's back-office imports them (`<app>Messages`). */
|
|
15
|
+
export { messages as l10n_inMessages };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
* The web contribution of `shutro-erp/l10n_in`: the GST returns page (GSTR-1, GSTR-3B, the company's GST settings) and
|
|
4
|
+
* the e-invoice panel on customer invoices.
|
|
5
|
+
*/
|
|
6
|
+
import { addMessages } from '../../ui/index.js';
|
|
7
|
+
import { EInvoicePanel } from './EInvoicePanel.js';
|
|
8
|
+
import { GstPage } from './GstPage.js';
|
|
9
|
+
import { messages } from './messages.js';
|
|
10
|
+
export { EInvoicePanel } from './EInvoicePanel.js';
|
|
11
|
+
export { GstPage } from './GstPage.js';
|
|
12
|
+
export { GstSettings } from './GstSettings.js';
|
|
13
|
+
/** Registers the page (and the messages in `i18n`, when given). */
|
|
14
|
+
export function registerL10nInWeb(registry, i18n) {
|
|
15
|
+
registry.registerPage('l10n_in.Gst', GstPage);
|
|
16
|
+
registry.registerComponent('l10n_in.EInvoice', EInvoicePanel);
|
|
17
|
+
if (i18n !== undefined)
|
|
18
|
+
addMessages(i18n, 'l10n_in', messages);
|
|
19
|
+
return registry;
|
|
20
|
+
}
|
|
21
|
+
/** The messages, named as the starter project's back-office imports them (`<app>Messages`). */
|
|
22
|
+
export { messages as l10n_inMessages };
|