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,427 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The data layer of the India localization (RFC 0016 step 5): GST at its rates as one tax per rate whose components
|
|
3
|
+
* post CGST and SGST within a state or IGST between states and abroad (Accounting's tax components); compensation
|
|
4
|
+
* cess; nil-rated, exempt, non-GST and zero-rated (exports under LUT) supplies; purchase taxes under reverse charge.
|
|
5
|
+
* GSTIN and PAN on companies and contacts, checked (the GSTIN's check digit, its state against the contact's state,
|
|
6
|
+
* its PAN); HSN and SAC codes on products. Product lines store their CGST, SGST, IGST and cess for the tax invoice.
|
|
7
|
+
* The taxes are data (`noupdate`: the company's to change); their accounts are set when the localization installs.
|
|
8
|
+
*/
|
|
9
|
+
import { accountingShutro, supplyScope } from '../accounting/index.js';
|
|
10
|
+
import { contactsShutro } from '../contacts/index.js';
|
|
11
|
+
import { computeTaxes, productShutro, taxRule } from '../product/index.js';
|
|
12
|
+
import { orm } from '../server/orm.js';
|
|
13
|
+
import { check, compute, defineModel, defineModule, extendModel, f, record, ref, } from './.kosh/bindings.js';
|
|
14
|
+
const CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
|
15
|
+
const GSTIN = /^\d{2}[A-Z]{5}\d{4}[A-Z][1-9A-Z]Z[0-9A-Z]$/;
|
|
16
|
+
const PAN = /^[A-Z]{5}\d{4}[A-Z]$/;
|
|
17
|
+
/** The GST state codes (the first two digits of a GSTIN) of each state's ISO 3166-2 code (old and new codes). */
|
|
18
|
+
export const GST_STATE_CODES = {
|
|
19
|
+
'IN-JK': '01',
|
|
20
|
+
'IN-HP': '02',
|
|
21
|
+
'IN-PB': '03',
|
|
22
|
+
'IN-CH': '04',
|
|
23
|
+
'IN-UT': '05',
|
|
24
|
+
'IN-UK': '05',
|
|
25
|
+
'IN-HR': '06',
|
|
26
|
+
'IN-DL': '07',
|
|
27
|
+
'IN-RJ': '08',
|
|
28
|
+
'IN-UP': '09',
|
|
29
|
+
'IN-BR': '10',
|
|
30
|
+
'IN-SK': '11',
|
|
31
|
+
'IN-AR': '12',
|
|
32
|
+
'IN-NL': '13',
|
|
33
|
+
'IN-MN': '14',
|
|
34
|
+
'IN-MZ': '15',
|
|
35
|
+
'IN-TR': '16',
|
|
36
|
+
'IN-ML': '17',
|
|
37
|
+
'IN-AS': '18',
|
|
38
|
+
'IN-WB': '19',
|
|
39
|
+
'IN-JH': '20',
|
|
40
|
+
'IN-OR': '21',
|
|
41
|
+
'IN-OD': '21',
|
|
42
|
+
'IN-CT': '22',
|
|
43
|
+
'IN-CG': '22',
|
|
44
|
+
'IN-MP': '23',
|
|
45
|
+
'IN-GJ': '24',
|
|
46
|
+
'IN-DH': '26',
|
|
47
|
+
'IN-DN': '26',
|
|
48
|
+
'IN-DD': '26',
|
|
49
|
+
'IN-MH': '27',
|
|
50
|
+
'IN-KA': '29',
|
|
51
|
+
'IN-GA': '30',
|
|
52
|
+
'IN-LD': '31',
|
|
53
|
+
'IN-KL': '32',
|
|
54
|
+
'IN-TN': '33',
|
|
55
|
+
'IN-PY': '34',
|
|
56
|
+
'IN-AN': '35',
|
|
57
|
+
'IN-TG': '36',
|
|
58
|
+
'IN-TS': '36',
|
|
59
|
+
'IN-AP': '37',
|
|
60
|
+
'IN-LA': '38',
|
|
61
|
+
};
|
|
62
|
+
/** The check digit of a GSTIN's first 14 characters (Luhn mod 36). */
|
|
63
|
+
export function gstinCheckDigit(first14) {
|
|
64
|
+
let sum = 0;
|
|
65
|
+
for (let i = 0; i < 14; i++) {
|
|
66
|
+
const product = CHARS.indexOf(first14[i] ?? '') * (i % 2 === 0 ? 1 : 2);
|
|
67
|
+
sum += Math.floor(product / 36) + (product % 36);
|
|
68
|
+
}
|
|
69
|
+
return CHARS[(36 - (sum % 36)) % 36];
|
|
70
|
+
}
|
|
71
|
+
/** Why a GSTIN is wrong for a party in `state`, or null. */
|
|
72
|
+
export function gstinProblem(gstin, state) {
|
|
73
|
+
if (!GSTIN.test(gstin))
|
|
74
|
+
return 'A GSTIN has 15 characters: state code, PAN, entity number, Z, check digit.';
|
|
75
|
+
if (gstinCheckDigit(gstin.slice(0, 14)) !== gstin[14])
|
|
76
|
+
return "The GSTIN's check digit is wrong.";
|
|
77
|
+
const expected = state === null ? undefined : GST_STATE_CODES[state];
|
|
78
|
+
if (expected !== undefined && gstin.slice(0, 2) !== expected)
|
|
79
|
+
return `The GSTIN starts with ${gstin.slice(0, 2)}, but ${state} is state code ${expected}.`;
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
export const PartnerGst = extendModel('res.partner', {
|
|
83
|
+
fields: {
|
|
84
|
+
/** GST identification number (15 characters): on the tax invoice and in GSTR-1. */
|
|
85
|
+
gstin: f.char({ size: 15, label: 'GSTIN' }),
|
|
86
|
+
/** Permanent account number (income tax, 10 characters). */
|
|
87
|
+
pan: f.char({ size: 10, label: 'PAN' }),
|
|
88
|
+
},
|
|
89
|
+
constraints: {
|
|
90
|
+
gstinValid: {
|
|
91
|
+
...check('res.partner', ['gstin', 'state'], (ps) => ps.filter((p) => p.gstin !== null && gstinProblem(p.gstin, p.state) !== null)),
|
|
92
|
+
message: "The GSTIN is not valid (format, check digit, or a state other than the contact's).",
|
|
93
|
+
},
|
|
94
|
+
panFormat: {
|
|
95
|
+
...check('res.partner', ['pan'], (ps) => ps.filter((p) => p.pan !== null && !PAN.test(p.pan))),
|
|
96
|
+
message: 'A PAN is five letters, four digits and a letter (ABCDE1234F).',
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
export const CompanyGst = extendModel('res.company', {
|
|
101
|
+
fields: {
|
|
102
|
+
gstin: f.char({ size: 15, label: 'GSTIN' }),
|
|
103
|
+
pan: f.char({ size: 10, label: 'PAN' }),
|
|
104
|
+
/** Its principal place of business: the seller's address on e-invoices and e-way bills. */
|
|
105
|
+
street: f.char({ label: 'Address' }),
|
|
106
|
+
city: f.char({ label: 'City' }),
|
|
107
|
+
zip: f.char({ size: 6, label: 'PIN code' }),
|
|
108
|
+
/** Customer invoices and notes are registered with the IRP when posted (turnover above the threshold). */
|
|
109
|
+
einvoiceEnabled: f.boolean({ required: true, default: false, label: 'E-invoicing' }),
|
|
110
|
+
},
|
|
111
|
+
constraints: {
|
|
112
|
+
companyGstinValid: {
|
|
113
|
+
...check('res.company', ['gstin', 'state'], (cs) => cs.filter((c) => c.gstin !== null && gstinProblem(c.gstin, c.state) !== null)),
|
|
114
|
+
message: "The GSTIN is not valid (format, check digit, or a state other than the company's).",
|
|
115
|
+
},
|
|
116
|
+
companyPanFormat: {
|
|
117
|
+
...check('res.company', ['pan'], (cs) => cs.filter((c) => c.pan !== null && !PAN.test(c.pan))),
|
|
118
|
+
message: 'A PAN is five letters, four digits and a letter (ABCDE1234F).',
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
export const ProductHsn = extendModel('product.product', {
|
|
123
|
+
fields: {
|
|
124
|
+
/** HSN (goods: 4, 6 or 8 digits) or SAC (services: 6 digits from 99): on lines of the tax invoice and in GSTR-1. */
|
|
125
|
+
hsnCode: f.char({ size: 8, label: 'HSN / SAC' }),
|
|
126
|
+
},
|
|
127
|
+
constraints: {
|
|
128
|
+
hsnFormat: {
|
|
129
|
+
...check('product.product', ['hsnCode'], (ps) => ps.filter((p) => p.hsnCode !== null && !/^(\d{4}|\d{6}|\d{8})$/.test(p.hsnCode))),
|
|
130
|
+
message: 'An HSN code has 4, 6 or 8 digits; a SAC code 6 (from 99).',
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
/** How the returns treat a tax (GSTR-1, GSTR-3B). */
|
|
135
|
+
export const GST_TREATMENTS = {
|
|
136
|
+
taxable: 'GST (taxable)',
|
|
137
|
+
nil: 'Nil-rated',
|
|
138
|
+
exempt: 'Exempt',
|
|
139
|
+
non_gst: 'Non-GST',
|
|
140
|
+
zero_rated: 'Zero-rated (export under LUT)',
|
|
141
|
+
cess: 'Compensation cess',
|
|
142
|
+
};
|
|
143
|
+
export const TaxGst = extendModel('account.tax', {
|
|
144
|
+
fields: {
|
|
145
|
+
/** Where the returns put the tax; a tax without it is not in them. */
|
|
146
|
+
gstTreatment: f.selection(GST_TREATMENTS, { label: 'GST treatment' }),
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
const ZERO = orm.toDecimal(0);
|
|
150
|
+
const GST_DEPENDS = [
|
|
151
|
+
'lineKind',
|
|
152
|
+
'quantity',
|
|
153
|
+
'priceUnit',
|
|
154
|
+
'discount',
|
|
155
|
+
'taxes.rate',
|
|
156
|
+
'taxes.sequence',
|
|
157
|
+
'taxes.amountType',
|
|
158
|
+
'taxes.fixedAmount',
|
|
159
|
+
'taxes.affectsBase',
|
|
160
|
+
'taxes.priceIncluded',
|
|
161
|
+
'taxes.reverseCharge',
|
|
162
|
+
'taxes.gstTreatment',
|
|
163
|
+
'taxes.components.share',
|
|
164
|
+
'taxes.components.scope',
|
|
165
|
+
'taxes.components.code',
|
|
166
|
+
'move.placeOfSupply',
|
|
167
|
+
'move.partner.state',
|
|
168
|
+
'move.partner.country',
|
|
169
|
+
'move.company.state',
|
|
170
|
+
'move.company.country',
|
|
171
|
+
];
|
|
172
|
+
/** A product line's GST by head, split as posting splits it (by the document's place of supply). */
|
|
173
|
+
export function gstAmounts(l) {
|
|
174
|
+
if (l.lineKind !== 'product')
|
|
175
|
+
return null;
|
|
176
|
+
const taxes = [...l.taxes];
|
|
177
|
+
const quantity = l.quantity ?? orm.toDecimal(1);
|
|
178
|
+
const amount = quantity
|
|
179
|
+
.times(l.priceUnit ?? 0)
|
|
180
|
+
.times(orm.toDecimal(100).minus(l.discount ?? 0))
|
|
181
|
+
.div(100);
|
|
182
|
+
const result = computeTaxes(amount, quantity, taxes.map(taxRule));
|
|
183
|
+
const partner = l.move.partner;
|
|
184
|
+
const scope = supplyScope(l.move.company, partner?.country ?? null, l.move.placeOfSupply ?? partner?.state ?? null);
|
|
185
|
+
const heads = { cgst: ZERO, sgst: ZERO, igst: ZERO, cess: ZERO };
|
|
186
|
+
let rate = ZERO;
|
|
187
|
+
for (const t of result.taxes) {
|
|
188
|
+
const tax = taxes.find((x) => x.id === t.tax);
|
|
189
|
+
if (tax === undefined)
|
|
190
|
+
continue;
|
|
191
|
+
if (tax.gstTreatment === 'cess') {
|
|
192
|
+
heads.cess = heads.cess.plus(t.amount);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (tax.gstTreatment === 'taxable')
|
|
196
|
+
rate = rate.plus((tax.rate ?? ZERO).times(100));
|
|
197
|
+
for (const c of tax.components) {
|
|
198
|
+
if (c.scope !== 'always' && c.scope !== scope)
|
|
199
|
+
continue;
|
|
200
|
+
const head = (c.code ?? '').toLowerCase();
|
|
201
|
+
if (head === 'cgst' || head === 'sgst' || head === 'igst')
|
|
202
|
+
heads[head] = heads[head].plus(t.amount.times(c.share));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const round = (d) => d.toDecimalPlaces(2, orm.Decimal.ROUND_HALF_UP);
|
|
206
|
+
return {
|
|
207
|
+
cgst: round(heads.cgst),
|
|
208
|
+
sgst: round(heads.sgst),
|
|
209
|
+
igst: round(heads.igst),
|
|
210
|
+
cess: round(heads.cess),
|
|
211
|
+
rate,
|
|
212
|
+
total: round(result.total),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
const head = (field, label) => {
|
|
216
|
+
const name = {
|
|
217
|
+
cgst: 'cgstAmount',
|
|
218
|
+
sgst: 'sgstAmount',
|
|
219
|
+
igst: 'igstAmount',
|
|
220
|
+
cess: 'cessAmount',
|
|
221
|
+
total: 'totalWithGst',
|
|
222
|
+
}[field];
|
|
223
|
+
return f.monetary({
|
|
224
|
+
currency: 'currency',
|
|
225
|
+
store: true,
|
|
226
|
+
label,
|
|
227
|
+
...compute('account.move.line', name, [...GST_DEPENDS], (lines) => {
|
|
228
|
+
for (const l of lines)
|
|
229
|
+
l[name] = gstAmounts(l)?.[field] ?? null;
|
|
230
|
+
}),
|
|
231
|
+
});
|
|
232
|
+
};
|
|
233
|
+
export const LineGst = extendModel('account.move.line', {
|
|
234
|
+
fields: {
|
|
235
|
+
cgstAmount: head('cgst', 'CGST'),
|
|
236
|
+
sgstAmount: head('sgst', 'SGST'),
|
|
237
|
+
igstAmount: head('igst', 'IGST'),
|
|
238
|
+
cessAmount: head('cess', 'Cess'),
|
|
239
|
+
totalWithGst: head('total', 'Total with GST'),
|
|
240
|
+
/** The line's GST rate, in percent. */
|
|
241
|
+
gstRate: f.decimal({
|
|
242
|
+
scale: 2,
|
|
243
|
+
store: true,
|
|
244
|
+
label: 'GST %',
|
|
245
|
+
...compute('account.move.line', 'gstRate', [...GST_DEPENDS], (lines) => {
|
|
246
|
+
for (const l of lines)
|
|
247
|
+
l.gstRate = gstAmounts(l)?.rate ?? null;
|
|
248
|
+
}),
|
|
249
|
+
}),
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
const EINVOICE_STATES = {
|
|
253
|
+
registered: 'Registered (IRN)',
|
|
254
|
+
failed: 'Not registered: see the error',
|
|
255
|
+
cancelled: 'Cancelled at the IRP',
|
|
256
|
+
};
|
|
257
|
+
/**
|
|
258
|
+
* The IRP's and the e-way bill system's answers for a document (RFC 0016 step 5, part 4): written by the system only
|
|
259
|
+
* (users read them), shown on the document through its computed fields.
|
|
260
|
+
*/
|
|
261
|
+
export const GstEinvoice = defineModel('l10n_in.einvoice', {
|
|
262
|
+
label: 'E-invoice',
|
|
263
|
+
access: { 'account.invoicing': ['read'], 'account.user': ['read'], 'account.manager': ['read'] },
|
|
264
|
+
fields: {
|
|
265
|
+
move: f.many2one('account.move', { required: true, ondelete: 'cascade' }),
|
|
266
|
+
/** E-invoicing (RFC 0016 step 5, part 4): the IRP's answer, written only by the registration. */
|
|
267
|
+
einvoiceStatus: f.selection(EINVOICE_STATES, { label: 'E-invoice' }),
|
|
268
|
+
irn: f.char({ size: 64, label: 'IRN' }),
|
|
269
|
+
irnAckNo: f.char({ size: 20, label: 'Acknowledgement number' }),
|
|
270
|
+
irnAckDate: f.datetime({ label: 'Acknowledged at' }),
|
|
271
|
+
/** The IRP's signed QR code content (a JWT), printed as a QR code. */
|
|
272
|
+
irnSignedQr: f.text({ label: 'Signed QR code' }),
|
|
273
|
+
irnCancelledAt: f.datetime({ label: 'IRN cancelled at' }),
|
|
274
|
+
einvoiceError: f.text({ label: 'E-invoice error' }),
|
|
275
|
+
/** The e-way bill for moving the goods (above ₹50,000): part A from the invoice, part B the transport. */
|
|
276
|
+
ewbNo: f.char({ size: 12, label: 'E-way bill' }),
|
|
277
|
+
ewbDate: f.datetime({ label: 'E-way bill generated at' }),
|
|
278
|
+
ewbValidUntil: f.datetime({ label: 'E-way bill valid until' }),
|
|
279
|
+
ewbStatus: f.selection({ generated: 'Generated', failed: 'Not generated: see the error', cancelled: 'Cancelled' }, { label: 'E-way bill status' }),
|
|
280
|
+
ewbError: f.text({ label: 'E-way bill error' }),
|
|
281
|
+
ewbMode: f.selection({ road: 'Road', rail: 'Rail', air: 'Air', ship: 'Ship' }, { label: 'Transport mode' }),
|
|
282
|
+
ewbVehicleNo: f.char({ size: 15, label: 'Vehicle number' }),
|
|
283
|
+
ewbTransporterId: f.char({ size: 15, label: 'Transporter ID' }),
|
|
284
|
+
ewbDistanceKm: f.integer({ label: 'Distance (km)' }),
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
const EINVOICE_FIELDS = [
|
|
288
|
+
'einvoiceStatus',
|
|
289
|
+
'irn',
|
|
290
|
+
'irnAckNo',
|
|
291
|
+
'irnAckDate',
|
|
292
|
+
'irnSignedQr',
|
|
293
|
+
'irnCancelledAt',
|
|
294
|
+
'einvoiceError',
|
|
295
|
+
'ewbNo',
|
|
296
|
+
'ewbDate',
|
|
297
|
+
'ewbValidUntil',
|
|
298
|
+
'ewbStatus',
|
|
299
|
+
'ewbError',
|
|
300
|
+
'ewbMode',
|
|
301
|
+
'ewbVehicleNo',
|
|
302
|
+
'ewbTransporterId',
|
|
303
|
+
'ewbDistanceKm',
|
|
304
|
+
];
|
|
305
|
+
/** The document's view of its e-invoice: computed from its record, read-only to everyone. */
|
|
306
|
+
const fromEinvoice = compute('account.move', [...EINVOICE_FIELDS], [
|
|
307
|
+
'einvoices.einvoiceStatus',
|
|
308
|
+
'einvoices.irn',
|
|
309
|
+
'einvoices.irnAckNo',
|
|
310
|
+
'einvoices.irnAckDate',
|
|
311
|
+
'einvoices.irnSignedQr',
|
|
312
|
+
'einvoices.irnCancelledAt',
|
|
313
|
+
'einvoices.einvoiceError',
|
|
314
|
+
'einvoices.ewbNo',
|
|
315
|
+
'einvoices.ewbDate',
|
|
316
|
+
'einvoices.ewbValidUntil',
|
|
317
|
+
'einvoices.ewbStatus',
|
|
318
|
+
'einvoices.ewbError',
|
|
319
|
+
'einvoices.ewbMode',
|
|
320
|
+
'einvoices.ewbVehicleNo',
|
|
321
|
+
'einvoices.ewbTransporterId',
|
|
322
|
+
'einvoices.ewbDistanceKm',
|
|
323
|
+
], (moves) => {
|
|
324
|
+
for (const m of moves) {
|
|
325
|
+
const e = [...m.einvoices][0];
|
|
326
|
+
const target = m;
|
|
327
|
+
for (const k of EINVOICE_FIELDS)
|
|
328
|
+
target[k] = e === undefined ? null : e[k];
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
export const MoveGst = extendModel('account.move', {
|
|
332
|
+
fields: {
|
|
333
|
+
einvoices: f.one2many('l10n_in.einvoice', 'move'),
|
|
334
|
+
/** E-invoicing (RFC 0016 step 5, part 4): the IRP's answer (its record's). */
|
|
335
|
+
einvoiceStatus: f.selection(EINVOICE_STATES, {
|
|
336
|
+
label: 'E-invoice',
|
|
337
|
+
store: true,
|
|
338
|
+
...fromEinvoice,
|
|
339
|
+
}),
|
|
340
|
+
irn: f.char({ size: 64, label: 'IRN', store: true, ...fromEinvoice }),
|
|
341
|
+
irnAckNo: f.char({ size: 20, label: 'Acknowledgement number', store: true, ...fromEinvoice }),
|
|
342
|
+
irnAckDate: f.datetime({ label: 'Acknowledged at', store: true, ...fromEinvoice }),
|
|
343
|
+
/** The IRP's signed QR code content (a JWT), printed as a QR code. */
|
|
344
|
+
irnSignedQr: f.text({ label: 'Signed QR code', store: true, ...fromEinvoice }),
|
|
345
|
+
irnCancelledAt: f.datetime({ label: 'IRN cancelled at', store: true, ...fromEinvoice }),
|
|
346
|
+
einvoiceError: f.text({ label: 'E-invoice error', store: true, ...fromEinvoice }),
|
|
347
|
+
/** The e-way bill for moving the goods (above ₹50,000): part A from the invoice, part B the transport. */
|
|
348
|
+
ewbNo: f.char({ size: 12, label: 'E-way bill', store: true, ...fromEinvoice }),
|
|
349
|
+
ewbDate: f.datetime({ label: 'E-way bill generated at', store: true, ...fromEinvoice }),
|
|
350
|
+
ewbValidUntil: f.datetime({ label: 'E-way bill valid until', store: true, ...fromEinvoice }),
|
|
351
|
+
ewbStatus: f.selection({ generated: 'Generated', failed: 'Not generated: see the error', cancelled: 'Cancelled' }, { label: 'E-way bill status', store: true, ...fromEinvoice }),
|
|
352
|
+
ewbError: f.text({ label: 'E-way bill error', store: true, ...fromEinvoice }),
|
|
353
|
+
ewbMode: f.selection({ road: 'Road', rail: 'Rail', air: 'Air', ship: 'Ship' }, { label: 'Transport mode', store: true, ...fromEinvoice }),
|
|
354
|
+
ewbVehicleNo: f.char({ size: 15, label: 'Vehicle number', store: true, ...fromEinvoice }),
|
|
355
|
+
ewbTransporterId: f.char({ size: 15, label: 'Transporter ID', store: true, ...fromEinvoice }),
|
|
356
|
+
ewbDistanceKm: f.integer({ label: 'Distance (km)', store: true, ...fromEinvoice }),
|
|
357
|
+
/** A tax of the document is paid by the recipient (reverse charge): printed on the tax invoice. */
|
|
358
|
+
gstReverseCharge: f.boolean({
|
|
359
|
+
store: true,
|
|
360
|
+
label: 'Reverse charge',
|
|
361
|
+
...compute('account.move', 'gstReverseCharge', ['lines.lineKind', 'lines.taxes.reverseCharge'], (moves) => {
|
|
362
|
+
for (const m of moves)
|
|
363
|
+
m.gstReverseCharge = [...m.lines].some((l) => l.lineKind === 'product' && [...l.taxes].some((t) => t.reverseCharge));
|
|
364
|
+
}),
|
|
365
|
+
}),
|
|
366
|
+
},
|
|
367
|
+
});
|
|
368
|
+
/** The GST rates (percent) of India's slabs. */
|
|
369
|
+
export const GST_RATES = ['0.25', '3', '5', '12', '18', '28'];
|
|
370
|
+
/** Compensation cess rates (percent) commonly applied. */
|
|
371
|
+
export const CESS_RATES = ['1', '3', '12', '15', '22'];
|
|
372
|
+
const pct = (rate) => `${rate}%`;
|
|
373
|
+
const half = (rate) => orm.toDecimal(rate).div(2).toString();
|
|
374
|
+
const fraction = (rate) => orm.toDecimal(rate).div(100).toString();
|
|
375
|
+
const idOf = (rate) => rate.replace('.', '_');
|
|
376
|
+
/** One GST tax with its components: CGST and SGST within the state, IGST between states. */
|
|
377
|
+
function gst(use, rate, options = {}) {
|
|
378
|
+
const kind = options.reverseCharge === true ? 'rc' : use;
|
|
379
|
+
const id = `gst_${kind}_${idOf(rate)}`;
|
|
380
|
+
const suffix = options.reverseCharge === true ? ' (reverse charge)' : use === 'purchase' ? ' (purchases)' : '';
|
|
381
|
+
const tax = record('account.tax', id, {
|
|
382
|
+
name: `GST ${pct(rate)}${suffix}`,
|
|
383
|
+
use,
|
|
384
|
+
rate: fraction(rate),
|
|
385
|
+
gstTreatment: 'taxable',
|
|
386
|
+
reverseCharge: options.reverseCharge === true,
|
|
387
|
+
}, { noupdate: true });
|
|
388
|
+
const component = (code, share, scope) => record('account.tax.component', `${id}_${code.toLowerCase()}`, {
|
|
389
|
+
tax: ref('account.tax', `l10n_in.${id}`),
|
|
390
|
+
name: `${code} ${pct(code === 'IGST' ? rate : half(rate))}${suffix}`,
|
|
391
|
+
code,
|
|
392
|
+
share,
|
|
393
|
+
scope,
|
|
394
|
+
sequence: code === 'CGST' ? 1 : code === 'SGST' ? 2 : 3,
|
|
395
|
+
}, { noupdate: true });
|
|
396
|
+
return [
|
|
397
|
+
tax,
|
|
398
|
+
component('CGST', '0.5', 'intra'),
|
|
399
|
+
component('SGST', '0.5', 'intra'),
|
|
400
|
+
component('IGST', '1', 'inter'),
|
|
401
|
+
];
|
|
402
|
+
}
|
|
403
|
+
const plain = (id, name, use, treatment, rate = '0') => record('account.tax', id, { name, use, rate, gstTreatment: treatment, ...(treatment === 'cess' ? { sequence: 20 } : {}) }, { noupdate: true });
|
|
404
|
+
export const TAXES = [
|
|
405
|
+
...GST_RATES.flatMap((r) => [...gst('sale', r), ...gst('purchase', r)]),
|
|
406
|
+
// Purchases where the recipient pays the GST (services of advocates, goods transport, from unregistered suppliers…).
|
|
407
|
+
...['5', '12', '18', '28'].flatMap((r) => gst('purchase', r, { reverseCharge: true })),
|
|
408
|
+
...CESS_RATES.flatMap((r) => [
|
|
409
|
+
plain(`cess_sale_${idOf(r)}`, `Cess ${pct(r)}`, 'sale', 'cess', fraction(r)),
|
|
410
|
+
plain(`cess_purchase_${idOf(r)}`, `Cess ${pct(r)} (purchases)`, 'purchase', 'cess', fraction(r)),
|
|
411
|
+
]),
|
|
412
|
+
plain('gst_sale_nil', 'GST 0% (nil-rated)', 'sale', 'nil'),
|
|
413
|
+
plain('gst_sale_exempt', 'GST exempt', 'sale', 'exempt'),
|
|
414
|
+
plain('gst_sale_non_gst', 'Non-GST supply', 'sale', 'non_gst'),
|
|
415
|
+
plain('gst_sale_lut', 'IGST 0% (export under LUT)', 'sale', 'zero_rated'),
|
|
416
|
+
plain('gst_purchase_nil', 'GST 0% (nil-rated purchases)', 'purchase', 'nil'),
|
|
417
|
+
plain('gst_purchase_exempt', 'GST exempt (purchases)', 'purchase', 'exempt'),
|
|
418
|
+
plain('gst_purchase_non_gst', 'Non-GST purchase', 'purchase', 'non_gst'),
|
|
419
|
+
];
|
|
420
|
+
export const l10nInShutro = defineModule({
|
|
421
|
+
name: 'l10n_in',
|
|
422
|
+
version: '0.1.0',
|
|
423
|
+
depends: [accountingShutro, contactsShutro, productShutro],
|
|
424
|
+
models: [GstEinvoice],
|
|
425
|
+
extensions: [PartnerGst, CompanyGst, ProductHsn, TaxGst, LineGst, MoveGst],
|
|
426
|
+
data: TAXES,
|
|
427
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E-invoicing, e-way bills and the company's GST settings over HTTP (RFC 0016 step 5, part 4), and the subscriber
|
|
3
|
+
* that registers documents when they are posted:
|
|
4
|
+
*
|
|
5
|
+
* - `GET /api/v1/l10n_in/einvoice/:id` — a document's e-invoice and e-way bill;
|
|
6
|
+
* - `POST /api/v1/l10n_in/einvoice/:id` — register it with the IRP; `POST …/cancel { reason, remark }`;
|
|
7
|
+
* - `POST /api/v1/l10n_in/ewaybill/:id { mode, distanceKm, vehicleNo, transporterId? }`; `POST …/cancel`;
|
|
8
|
+
* - `GET` / `PUT /api/v1/l10n_in/settings` — the company's GSTIN, PAN, address and e-invoicing switch (managers).
|
|
9
|
+
*/
|
|
10
|
+
import { type ModuleContext } from '../server/index.js';
|
|
11
|
+
import type { Worker } from '../server/orm.js';
|
|
12
|
+
import { type GstProvider } from './einvoice.js';
|
|
13
|
+
export declare function registerEInvoiceRoutes(context: ModuleContext, provider: GstProvider | undefined): void;
|
|
14
|
+
/** The subscriber registering documents with the IRP when they are posted; tests `drain()` it, the server starts it. */
|
|
15
|
+
export declare function einvoiceWorker(context: Pick<ModuleContext, 'shutro'>, provider: GstProvider): Worker;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E-invoicing, e-way bills and the company's GST settings over HTTP (RFC 0016 step 5, part 4), and the subscriber
|
|
3
|
+
* that registers documents when they are posted:
|
|
4
|
+
*
|
|
5
|
+
* - `GET /api/v1/l10n_in/einvoice/:id` — a document's e-invoice and e-way bill;
|
|
6
|
+
* - `POST /api/v1/l10n_in/einvoice/:id` — register it with the IRP; `POST …/cancel { reason, remark }`;
|
|
7
|
+
* - `POST /api/v1/l10n_in/ewaybill/:id { mode, distanceKm, vehicleNo, transporterId? }`; `POST …/cancel`;
|
|
8
|
+
* - `GET` / `PUT /api/v1/l10n_in/settings` — the company's GSTIN, PAN, address and e-invoicing switch (managers).
|
|
9
|
+
*/
|
|
10
|
+
import { HttpProblem } from '../server/index.js';
|
|
11
|
+
import { cancelEInvoice, cancelEwayBill, einvoiceState, generateEwayBill, registerEInvoice, registerPosted, } from './einvoice.js';
|
|
12
|
+
import { gstinProblem } from './module.js';
|
|
13
|
+
const idOf = (params) => {
|
|
14
|
+
const id = Number(params.id);
|
|
15
|
+
if (!Number.isInteger(id) || id <= 0)
|
|
16
|
+
throw new HttpProblem(400, 'invalid-id', 'Invalid id', 'The document id is a positive integer.');
|
|
17
|
+
return id;
|
|
18
|
+
};
|
|
19
|
+
const text = (v) => (typeof v === 'string' ? v.trim() : '');
|
|
20
|
+
/** The company's GST settings. */
|
|
21
|
+
async function readSettings(env) {
|
|
22
|
+
const [c] = await env.model('res.company').search({ where: { id: env.companyRef }, limit: 1 });
|
|
23
|
+
return {
|
|
24
|
+
gstin: c?.gstin ?? null,
|
|
25
|
+
pan: c?.pan ?? null,
|
|
26
|
+
street: c?.street ?? null,
|
|
27
|
+
city: c?.city ?? null,
|
|
28
|
+
zip: c?.zip ?? null,
|
|
29
|
+
state: c?.state ?? null,
|
|
30
|
+
einvoiceEnabled: c?.einvoiceEnabled ?? false,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export function registerEInvoiceRoutes(context, provider) {
|
|
34
|
+
const { app } = context;
|
|
35
|
+
app.get('/api/v1/l10n_in/einvoice/:id', async (request) => {
|
|
36
|
+
const id = idOf(request.params);
|
|
37
|
+
return request.env(async (raw) => {
|
|
38
|
+
const env = raw;
|
|
39
|
+
const [company] = await env
|
|
40
|
+
.model('res.company')
|
|
41
|
+
.search({ where: { id: env.companyRef }, limit: 1 });
|
|
42
|
+
return {
|
|
43
|
+
// E-invoicing is Indian: other companies of an installation with l10n_in do not see it.
|
|
44
|
+
applicable: company?.country === 'IN',
|
|
45
|
+
provider: provider?.name ?? null,
|
|
46
|
+
...(await einvoiceState(env, id)),
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
app.post('/api/v1/l10n_in/einvoice/:id', async (request) => {
|
|
51
|
+
const id = idOf(request.params);
|
|
52
|
+
return request.env((env) => registerEInvoice(env, provider, id), {
|
|
53
|
+
write: true,
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
app.post('/api/v1/l10n_in/einvoice/:id/cancel', async (request) => {
|
|
57
|
+
const id = idOf(request.params);
|
|
58
|
+
const body = (request.body ?? {});
|
|
59
|
+
return request.env((env) => cancelEInvoice(env, provider, id, String(body.reason ?? ''), text(body.remark)), { write: true });
|
|
60
|
+
});
|
|
61
|
+
app.post('/api/v1/l10n_in/ewaybill/:id', async (request) => {
|
|
62
|
+
const id = idOf(request.params);
|
|
63
|
+
const body = (request.body ?? {});
|
|
64
|
+
const mode = body['mode'] ?? 'road';
|
|
65
|
+
if (mode !== 'road' && mode !== 'rail' && mode !== 'air' && mode !== 'ship')
|
|
66
|
+
throw new HttpProblem(400, 'invalid-body', 'Invalid request', 'mode is road, rail, air or ship.');
|
|
67
|
+
const transporterId = text(body['transporterId']);
|
|
68
|
+
return request.env((env) => generateEwayBill(env, provider, id, {
|
|
69
|
+
mode,
|
|
70
|
+
distanceKm: Number(body['distanceKm']),
|
|
71
|
+
vehicleNo: text(body['vehicleNo']).toUpperCase().replace(/[\s-]/g, ''),
|
|
72
|
+
...(transporterId === '' ? {} : { transporterId }),
|
|
73
|
+
}), { write: true });
|
|
74
|
+
});
|
|
75
|
+
app.post('/api/v1/l10n_in/ewaybill/:id/cancel', async (request) => {
|
|
76
|
+
const id = idOf(request.params);
|
|
77
|
+
const body = (request.body ?? {});
|
|
78
|
+
return request.env((env) => cancelEwayBill(env, provider, id, String(body.reason ?? '4'), text(body.remark)), { write: true });
|
|
79
|
+
});
|
|
80
|
+
app.get('/api/v1/l10n_in/settings', async (request) => request.env(async (env) => ({
|
|
81
|
+
provider: provider?.name ?? null,
|
|
82
|
+
...(await readSettings(env)),
|
|
83
|
+
})));
|
|
84
|
+
app.put('/api/v1/l10n_in/settings', async (request) => {
|
|
85
|
+
const body = (request.body ?? {});
|
|
86
|
+
const values = {};
|
|
87
|
+
for (const key of ['gstin', 'pan', 'street', 'city', 'zip'])
|
|
88
|
+
if (body[key] !== undefined) {
|
|
89
|
+
const v = text(body[key]);
|
|
90
|
+
values[key] = v === '' ? null : key === 'gstin' || key === 'pan' ? v.toUpperCase() : v;
|
|
91
|
+
}
|
|
92
|
+
if (body['einvoiceEnabled'] !== undefined)
|
|
93
|
+
values['einvoiceEnabled'] = body['einvoiceEnabled'] === true;
|
|
94
|
+
if (typeof values['zip'] === 'string' && !/^\d{6}$/.test(values['zip']))
|
|
95
|
+
throw new HttpProblem(400, 'invalid-body', 'Invalid request', 'A PIN code has 6 digits.');
|
|
96
|
+
return request.env(async (raw) => {
|
|
97
|
+
const env = raw;
|
|
98
|
+
if (!env.access.has('account.settings'))
|
|
99
|
+
throw new HttpProblem(403, 'access', 'Forbidden', 'Changing the GST settings needs the account.settings permission.', 'Ask for the Accounting manager access level.');
|
|
100
|
+
const current = await readSettings(env);
|
|
101
|
+
if (typeof values['gstin'] === 'string') {
|
|
102
|
+
const problem = gstinProblem(values['gstin'], current.state);
|
|
103
|
+
if (problem !== null)
|
|
104
|
+
throw new HttpProblem(422, 'invalid-gstin', 'Invalid GSTIN', problem);
|
|
105
|
+
}
|
|
106
|
+
await env.asSuperuser({ reason: 'GST settings' }, async (su) => {
|
|
107
|
+
await su
|
|
108
|
+
.model('res.company')
|
|
109
|
+
.browse([env.companyRef])
|
|
110
|
+
.write(values);
|
|
111
|
+
await su.flush();
|
|
112
|
+
});
|
|
113
|
+
return { provider: provider?.name ?? null, ...(await readSettings(env)) };
|
|
114
|
+
}, { write: true });
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
/** The subscriber registering documents with the IRP when they are posted; tests `drain()` it, the server starts it. */
|
|
118
|
+
export function einvoiceWorker(context, provider) {
|
|
119
|
+
return context.shutro.subscribe('l10n_in.einvoice', 'account.move.posted', async (event, env) => {
|
|
120
|
+
const id = Number(event.payload?.id);
|
|
121
|
+
if (Number.isInteger(id) && id > 0)
|
|
122
|
+
await registerPosted(env, provider, id);
|
|
123
|
+
}, { pollMs: 5000 });
|
|
124
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type ModuleContext } from '../server/index.js';
|
|
2
|
+
import { type Gstr1, type Heads } from './gstr.js';
|
|
3
|
+
declare const SECTIONS: {
|
|
4
|
+
readonly b2b: '4A B2B invoices';
|
|
5
|
+
readonly b2cl: '5 B2C large invoices';
|
|
6
|
+
readonly exp: '6A Exports';
|
|
7
|
+
readonly b2cs: '7 B2C others';
|
|
8
|
+
readonly cdnr: '9B Credit / debit notes (registered)';
|
|
9
|
+
readonly cdnur: '9B Credit / debit notes (unregistered)';
|
|
10
|
+
};
|
|
11
|
+
/** GSTR-1's sections: the documents (or B2C rows) of each, and their totals by head. */
|
|
12
|
+
export declare function gstr1Summary(r: Gstr1): (Heads & {
|
|
13
|
+
readonly section: keyof typeof SECTIONS;
|
|
14
|
+
readonly documents: number;
|
|
15
|
+
})[];
|
|
16
|
+
export declare function registerGstRoutes(context: ModuleContext): void;
|
|
17
|
+
export {};
|