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
|
@@ -16,10 +16,10 @@
|
|
|
16
16
|
import { contactsShutro } from '../contacts/index.js';
|
|
17
17
|
import { messagingShutro } from '../messaging/index.js';
|
|
18
18
|
import { approvalGuard, permissionsShutro, requestApprovals } from '../permissions/index.js';
|
|
19
|
-
import { productShutro } from '../product/index.js';
|
|
19
|
+
import { computeTaxes, productShutro, taxRule } from '../product/index.js';
|
|
20
20
|
import { reportingShutro } from '../reporting/index.js';
|
|
21
21
|
import { orm } from '../server/orm.js';
|
|
22
|
-
import { check, compute, defineModel, defineModule, extendModel, f, record, records, ref, } from './.kosh/bindings.js';
|
|
22
|
+
import { check, compute, defineModel, defineModule, extendModel, f, onchange, record, records, ref, } from './.kosh/bindings.js';
|
|
23
23
|
import { postPayments } from './payments.js';
|
|
24
24
|
const READ = ['read'];
|
|
25
25
|
const WRITE = ['read', 'write', 'create'];
|
|
@@ -57,7 +57,28 @@ export const MOVE_KINDS = {
|
|
|
57
57
|
out_refund: 'Customer credit note',
|
|
58
58
|
in_invoice: 'Vendor bill',
|
|
59
59
|
in_refund: 'Vendor credit note',
|
|
60
|
+
out_debit: 'Customer debit note',
|
|
61
|
+
in_debit: 'Vendor debit note',
|
|
60
62
|
};
|
|
63
|
+
/** Documents the partner owes the company for: customer invoices and debit notes, vendor credit notes. */
|
|
64
|
+
export function owedToUs(kind) {
|
|
65
|
+
return kind === 'out_invoice' || kind === 'out_debit' || kind === 'in_refund';
|
|
66
|
+
}
|
|
67
|
+
/** Credit notes (they post the other way round from the invoice they correct). */
|
|
68
|
+
export function isRefund(kind) {
|
|
69
|
+
return kind === 'out_refund' || kind === 'in_refund';
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The scope of a document: `inter` for a partner in another country, otherwise the place of supply against the
|
|
73
|
+
* company's state (`intra` when either is unknown: a country without states).
|
|
74
|
+
*/
|
|
75
|
+
export function supplyScope(company, partnerCountry, placeOfSupply) {
|
|
76
|
+
if (partnerCountry !== null && company.country !== null && partnerCountry !== company.country)
|
|
77
|
+
return 'inter';
|
|
78
|
+
if (placeOfSupply === null || company.state === null)
|
|
79
|
+
return 'intra';
|
|
80
|
+
return placeOfSupply === company.state ? 'intra' : 'inter';
|
|
81
|
+
}
|
|
61
82
|
/** The kind of the entry that reverses one of each kind. */
|
|
62
83
|
export const REVERSED_KIND = {
|
|
63
84
|
entry: 'entry',
|
|
@@ -65,6 +86,8 @@ export const REVERSED_KIND = {
|
|
|
65
86
|
out_refund: 'out_invoice',
|
|
66
87
|
in_invoice: 'in_refund',
|
|
67
88
|
in_refund: 'in_invoice',
|
|
89
|
+
out_debit: 'out_refund',
|
|
90
|
+
in_debit: 'in_refund',
|
|
68
91
|
};
|
|
69
92
|
/**
|
|
70
93
|
* The sequence field numbering each journal type (Shutro numbers one field per sequence code, and a sequence's
|
|
@@ -150,6 +173,28 @@ export const AccountJournal = defineModel('account.journal', {
|
|
|
150
173
|
},
|
|
151
174
|
},
|
|
152
175
|
});
|
|
176
|
+
/** A bank account of the company: shown on its invoices and in the portal, where customers pay (RFC 0016 §5.1). */
|
|
177
|
+
export const ResBankAccount = defineModel('res.bank.account', {
|
|
178
|
+
label: 'Bank account',
|
|
179
|
+
order: ['sequence asc', 'id asc'],
|
|
180
|
+
recName: 'number',
|
|
181
|
+
archivable: true,
|
|
182
|
+
companyScoped: { required: true },
|
|
183
|
+
// The company's payment details are not secret: every user (and the portal, through it) may read them.
|
|
184
|
+
access: { 'base.user': READ, 'account.manager': ALL },
|
|
185
|
+
fields: {
|
|
186
|
+
sequence: f.integer({ required: true, default: 10 }),
|
|
187
|
+
bank: f.char({ required: true, label: 'Bank' }),
|
|
188
|
+
branch: f.char(),
|
|
189
|
+
number: f.char({ required: true, label: 'Account number' }),
|
|
190
|
+
holder: f.char({ label: 'Account holder' }),
|
|
191
|
+
/** Routing number (Bangladesh), IFSC (India), sort code… */
|
|
192
|
+
routing: f.char({ label: 'Routing number' }),
|
|
193
|
+
swift: f.char({ label: 'SWIFT / BIC' }),
|
|
194
|
+
/** Shown on invoices in this currency; on every invoice when empty. */
|
|
195
|
+
currency: f.many2one('res.currency'),
|
|
196
|
+
},
|
|
197
|
+
});
|
|
153
198
|
export const AccountFiscalYear = defineModel('account.fiscal.year', {
|
|
154
199
|
label: 'Fiscal year',
|
|
155
200
|
order: ['dateFrom desc'],
|
|
@@ -166,6 +211,18 @@ export const AccountFiscalYear = defineModel('account.fiscal.year', {
|
|
|
166
211
|
closingMove: f.many2one('account.move', { label: 'Closing entry' }),
|
|
167
212
|
reopenReason: f.char({ label: 'Reopened because' }),
|
|
168
213
|
},
|
|
214
|
+
onchange: {
|
|
215
|
+
// A year that starts on a date ends the day before the same date a year later.
|
|
216
|
+
dateFrom: onchange('account.fiscal.year', (d) => {
|
|
217
|
+
const from = d.get('dateFrom');
|
|
218
|
+
if (typeof from !== 'string')
|
|
219
|
+
return;
|
|
220
|
+
const [y, m, day] = from.split('-').map(Number);
|
|
221
|
+
d.set('dateTo', new Date(Date.UTC(y + 1, m - 1, day - 1)).toISOString().slice(0, 10));
|
|
222
|
+
if (d.get('name') === null || d.get('name') === '')
|
|
223
|
+
d.set('name', m === 1 && day === 1 ? String(y) : `${y}–${String(y + 1).slice(2)}`);
|
|
224
|
+
}),
|
|
225
|
+
},
|
|
169
226
|
sqlConstraints: {
|
|
170
227
|
datesOrdered: {
|
|
171
228
|
check: orm.sql `date_to > date_from`,
|
|
@@ -228,10 +285,29 @@ export const AccountPaymentTermLine = defineModel('account.payment.term.line', {
|
|
|
228
285
|
daysPositive: { check: orm.sql `days >= 0`, message: 'Days are zero or more.' },
|
|
229
286
|
},
|
|
230
287
|
});
|
|
288
|
+
/** The fiscal year that `date` (ISO) falls in, for years starting in month `start` (1–12): its first and last day. */
|
|
289
|
+
export function fiscalYearOf(date, start) {
|
|
290
|
+
const [y, m] = date.split('-').map(Number);
|
|
291
|
+
const year = m >= start ? y : y - 1;
|
|
292
|
+
const from = new Date(Date.UTC(year, start - 1, 1));
|
|
293
|
+
const to = new Date(Date.UTC(year + 1, start - 1, 0)); // the day before the next year starts
|
|
294
|
+
return { dateFrom: from.toISOString().slice(0, 10), dateTo: to.toISOString().slice(0, 10) };
|
|
295
|
+
}
|
|
231
296
|
/** The company's accounting settings: lock dates and the accounts its documents post to. */
|
|
232
297
|
export const CompanyAccounting = extendModel('res.company', {
|
|
233
298
|
fields: {
|
|
234
|
-
|
|
299
|
+
/** The first month of its fiscal years (July in Bangladesh: July–June). */
|
|
300
|
+
fiscalYearStart: f.integer({
|
|
301
|
+
required: true,
|
|
302
|
+
default: 1,
|
|
303
|
+
label: 'Fiscal year starts in (month, 1–12)',
|
|
304
|
+
}),
|
|
305
|
+
/** The installed chart: `generic` or a localization's key (`bd`). */
|
|
306
|
+
chartTemplate: f.char({ size: 32, label: 'Chart of accounts' }),
|
|
307
|
+
/** Where the company is registered (ISO 3166): taxes split by place of supply compare against it. */
|
|
308
|
+
country: f.char({ size: 2, label: 'Country' }),
|
|
309
|
+
/** Its state or province (ISO 3166-2, `IN-MH`). */
|
|
310
|
+
state: f.char({ size: 6, label: 'State' }),
|
|
235
311
|
/** Nothing is posted, reversed or reconciled on or before it (everyone). */
|
|
236
312
|
lockDate: f.date({ label: 'Lock date' }),
|
|
237
313
|
/** Tax returns filed up to this date: tax lines cannot change on or before it. */
|
|
@@ -242,6 +318,12 @@ export const CompanyAccounting = extendModel('res.company', {
|
|
|
242
318
|
accountExchangeLoss: f.many2one('account.account', { label: 'Exchange loss account' }),
|
|
243
319
|
accountRetainedEarnings: f.many2one('account.account', { label: 'Retained earnings account' }),
|
|
244
320
|
},
|
|
321
|
+
sqlConstraints: {
|
|
322
|
+
fiscalYearMonth: {
|
|
323
|
+
check: orm.sql `fiscal_year_start BETWEEN 1 AND 12`,
|
|
324
|
+
message: 'A fiscal year starts in a month from 1 (January) to 12 (December).',
|
|
325
|
+
},
|
|
326
|
+
},
|
|
245
327
|
});
|
|
246
328
|
/** The accounts a tax posts to (the tax itself lives in the product module, shared by Sales and Purchase). */
|
|
247
329
|
export const TaxAccounts = extendModel('account.tax', {
|
|
@@ -249,8 +331,43 @@ export const TaxAccounts = extendModel('account.tax', {
|
|
|
249
331
|
account: f.many2one('account.account', { label: 'Tax account' }),
|
|
250
332
|
/** On credit notes, when different from the tax account. */
|
|
251
333
|
refundAccount: f.many2one('account.account', { label: 'Tax account on credit notes' }),
|
|
252
|
-
/**
|
|
253
|
-
|
|
334
|
+
/** Reverse charge: where the tax the company owes the state goes (the tax account takes what it may claim). */
|
|
335
|
+
reverseChargeAccount: f.many2one('account.account', {
|
|
336
|
+
label: 'Tax account for reverse charge (payable)',
|
|
337
|
+
}),
|
|
338
|
+
/** Its parts (CGST and SGST, or IGST): posted instead of the tax, by the document's scope. */
|
|
339
|
+
components: f.one2many('account.tax.component', 'tax'),
|
|
340
|
+
},
|
|
341
|
+
});
|
|
342
|
+
/**
|
|
343
|
+
* A part of a tax (RFC 0016 step 5): GST 18 % is CGST 9 % and SGST 9 % within a state, IGST 18 % between states.
|
|
344
|
+
* Lines and totals keep the tax; posting writes a tax line per component of the document's scope, each with its
|
|
345
|
+
* share of the amount, to its account. The shares of each scope add up to 1.
|
|
346
|
+
*/
|
|
347
|
+
export const AccountTaxComponent = defineModel('account.tax.component', {
|
|
348
|
+
label: 'Tax component',
|
|
349
|
+
order: ['sequence asc', 'id asc'],
|
|
350
|
+
access: { 'account.invoicing': READ, 'account.user': READ, 'account.manager': ALL },
|
|
351
|
+
fields: {
|
|
352
|
+
tax: f.many2one('account.tax', { required: true, ondelete: 'cascade' }),
|
|
353
|
+
sequence: f.integer({ required: true, default: 10 }),
|
|
354
|
+
name: f.char({ required: true, translate: true }),
|
|
355
|
+
/** Its part of the tax's amount: 0.5 for half. */
|
|
356
|
+
share: f.decimal({ scale: 4, required: true, default: '1' }),
|
|
357
|
+
scope: f.selection({ always: 'Always', intra: 'Within the state', inter: 'Between states or abroad' }, { required: true, default: 'always' }),
|
|
358
|
+
/** What the returns call it: CGST, SGST, IGST, CESS. */
|
|
359
|
+
code: f.char({ size: 16 }),
|
|
360
|
+
account: f.many2one('account.account'),
|
|
361
|
+
refundAccount: f.many2one('account.account', { label: 'Account on credit notes' }),
|
|
362
|
+
reverseChargeAccount: f.many2one('account.account', {
|
|
363
|
+
label: 'Account for reverse charge (payable)',
|
|
364
|
+
}),
|
|
365
|
+
},
|
|
366
|
+
constraints: {
|
|
367
|
+
shareRange: {
|
|
368
|
+
...check('account.tax.component', ['share'], (cs) => cs.filter((c) => c.share.lte(0) || c.share.gt(1))),
|
|
369
|
+
message: "A component's share is above 0 and at most 1 (0.5 for half).",
|
|
370
|
+
},
|
|
254
371
|
},
|
|
255
372
|
});
|
|
256
373
|
const moveTotals = compute('account.move', ['amountDebit', 'amountCredit'], ['lines.debit', 'lines.credit', 'lines.companyCurrency'], (moves) => {
|
|
@@ -289,7 +406,7 @@ const moveResidual = compute('account.move', ['amountResidual', 'paymentState'],
|
|
|
289
406
|
continue;
|
|
290
407
|
}
|
|
291
408
|
// The counterpart is a debit on outgoing documents (receivable) and a credit on incoming ones.
|
|
292
|
-
const sign = m.kind
|
|
409
|
+
const sign = owedToUs(m.kind) ? 1 : -1;
|
|
293
410
|
let residual = orm.toDecimal(0);
|
|
294
411
|
for (const l of m.lines)
|
|
295
412
|
if (l.lineKind === 'counterpart')
|
|
@@ -398,6 +515,37 @@ export const AccountMove = defineModel('account.move', {
|
|
|
398
515
|
}),
|
|
399
516
|
// Documents
|
|
400
517
|
dateDue: f.date({ label: 'Due date' }),
|
|
518
|
+
/** Where to pay a customer invoice: the company's bank accounts in its currency, and the reference to quote. */
|
|
519
|
+
paymentDetails: f.text({
|
|
520
|
+
label: 'Payment details',
|
|
521
|
+
...compute('account.move', 'paymentDetails', ['kind', 'name', 'currency', 'company'], async (moves) => {
|
|
522
|
+
for (const m of moves) {
|
|
523
|
+
if (m.kind !== 'out_invoice') {
|
|
524
|
+
m.paymentDetails = '';
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
const banks = await moves.env.model('res.bank.account').search({
|
|
528
|
+
where: {
|
|
529
|
+
company: m.company.id,
|
|
530
|
+
or: [{ currency: { isNull: true } }, { currency: m.currency.id }],
|
|
531
|
+
},
|
|
532
|
+
});
|
|
533
|
+
const rows = await banks.load({});
|
|
534
|
+
const lines = [...rows].map((b) => [
|
|
535
|
+
[b.bank, b.branch].filter((x) => x !== null && x !== '').join(', '),
|
|
536
|
+
`A/C ${b.number}${b.holder === null ? '' : ` (${b.holder})`}`,
|
|
537
|
+
b.routing === null ? null : `Routing ${b.routing}`,
|
|
538
|
+
b.swift === null ? null : `SWIFT ${b.swift}`,
|
|
539
|
+
]
|
|
540
|
+
.filter((x) => x !== null && x !== '')
|
|
541
|
+
.join(' · '));
|
|
542
|
+
m.paymentDetails =
|
|
543
|
+
lines.length === 0
|
|
544
|
+
? ''
|
|
545
|
+
: [...lines, ...(m.name === '/' ? [] : [`Reference: ${m.name}`])].join('\n');
|
|
546
|
+
}
|
|
547
|
+
}),
|
|
548
|
+
}),
|
|
401
549
|
paymentTerm: f.many2one('account.payment.term', { label: 'Payment terms' }),
|
|
402
550
|
invoiceOrigin: f.char({ label: 'Source document' }),
|
|
403
551
|
amountUntaxed: f.monetary({
|
|
@@ -461,6 +609,13 @@ export const AccountMove = defineModel('account.move', {
|
|
|
461
609
|
/** The database transaction that posted the entry: the immutability checks let that one finish its writes. */
|
|
462
610
|
postedTx: f.char({ size: 32 }),
|
|
463
611
|
reversalOf: f.many2one('account.move', { label: 'Reversal of' }),
|
|
612
|
+
/** A debit note: the invoice or bill it adds to. */
|
|
613
|
+
debitOf: f.many2one('account.move', { label: 'Adds to' }),
|
|
614
|
+
/**
|
|
615
|
+
* The state where the supply takes place (ISO 3166-2, `IN-KA`): from the partner's state when posted, editable.
|
|
616
|
+
* Decides whether a tax's within-state or between-states components post.
|
|
617
|
+
*/
|
|
618
|
+
placeOfSupply: f.char({ size: 6, label: 'Place of supply' }),
|
|
464
619
|
reversedBy: f.many2one('account.move', { label: 'Reversed by' }),
|
|
465
620
|
},
|
|
466
621
|
constraints: {
|
|
@@ -613,40 +768,62 @@ export const AccountMoveLine = defineModel('account.move.line', {
|
|
|
613
768
|
priceUnit: f.decimal({ scale: 2, default: '0', label: 'Unit price' }),
|
|
614
769
|
/** Percent off the unit price (0–100). */
|
|
615
770
|
discount: f.decimal({ scale: 2, default: '0', label: 'Discount %' }),
|
|
616
|
-
|
|
771
|
+
/** A product line's taxes, applied in order (RFC 0016): supplementary duty, then VAT on price + SD. */
|
|
772
|
+
taxes: f.many2many('account.tax'),
|
|
773
|
+
/** Before 0.2.0, one tax per line: copied into `taxes` by the migration; dropped later. */
|
|
774
|
+
tax: f.many2one('account.tax', { label: 'Tax (before 0.2.0)', readonly: true }),
|
|
617
775
|
/** Tax-excluded amount of a product line in the document currency. */
|
|
618
776
|
subtotal: f.monetary({
|
|
619
777
|
currency: 'currency',
|
|
620
778
|
store: true,
|
|
621
779
|
label: 'Subtotal',
|
|
622
|
-
...compute('account.move.line', 'subtotal', [
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
780
|
+
...compute('account.move.line', 'subtotal', [
|
|
781
|
+
'lineKind',
|
|
782
|
+
'quantity',
|
|
783
|
+
'priceUnit',
|
|
784
|
+
'discount',
|
|
785
|
+
'taxes.rate',
|
|
786
|
+
'taxes.sequence',
|
|
787
|
+
'taxes.amountType',
|
|
788
|
+
'taxes.fixedAmount',
|
|
789
|
+
'taxes.affectsBase',
|
|
790
|
+
'taxes.priceIncluded',
|
|
791
|
+
'taxes.reverseCharge',
|
|
792
|
+
], (lines) => {
|
|
793
|
+
for (const l of lines)
|
|
794
|
+
l.subtotal = l.lineKind === 'product' ? taxesOfLine(l).untaxed : null;
|
|
635
795
|
}),
|
|
636
796
|
}),
|
|
637
|
-
/** The
|
|
797
|
+
/** The taxes of a product line in the document currency (summed and rounded per tax at posting). */
|
|
638
798
|
taxAmount: f.monetary({
|
|
639
799
|
currency: 'currency',
|
|
640
800
|
store: true,
|
|
641
801
|
label: 'Tax',
|
|
642
|
-
...compute('account.move.line', 'taxAmount', [
|
|
802
|
+
...compute('account.move.line', 'taxAmount', [
|
|
803
|
+
'lineKind',
|
|
804
|
+
'quantity',
|
|
805
|
+
'priceUnit',
|
|
806
|
+
'discount',
|
|
807
|
+
'taxes.rate',
|
|
808
|
+
'taxes.sequence',
|
|
809
|
+
'taxes.amountType',
|
|
810
|
+
'taxes.fixedAmount',
|
|
811
|
+
'taxes.affectsBase',
|
|
812
|
+
'taxes.priceIncluded',
|
|
813
|
+
'taxes.reverseCharge',
|
|
814
|
+
], (lines) => {
|
|
643
815
|
for (const l of lines)
|
|
644
|
-
l.taxAmount =
|
|
645
|
-
l.subtotal === null ? null : l.subtotal.amount.times(l.tax?.rate ?? orm.toDecimal(0));
|
|
816
|
+
l.taxAmount = l.lineKind === 'product' ? taxesOfLine(l).taxTotal : null;
|
|
646
817
|
}),
|
|
647
818
|
}),
|
|
648
819
|
/** Set on the generated tax lines: the tax this line is the amount of. */
|
|
649
820
|
taxLineOf: f.many2one('account.tax', { label: 'Tax line for' }),
|
|
821
|
+
/** On the generated tax lines of a tax with components: which one (CGST, SGST, IGST). */
|
|
822
|
+
taxComponent: f.many2one('account.tax.component', { label: 'Tax component' }),
|
|
823
|
+
/** On the generated lines of a reverse-charge tax: claimable and payable, net zero. */
|
|
824
|
+
taxReverseCharge: f.boolean({ required: true, default: false, label: 'Reverse charge' }),
|
|
825
|
+
/** On the generated tax lines: what the tax was computed on, in the document currency (the tax reports). */
|
|
826
|
+
taxBase: f.monetary({ currency: 'currency', label: 'Tax base' }),
|
|
650
827
|
/** Debit minus credit, for the reports (trial balance, ledgers, P&L, balance sheet). */
|
|
651
828
|
balance: f.monetary({
|
|
652
829
|
currency: 'companyCurrency',
|
|
@@ -675,18 +852,13 @@ export const AccountMoveLine = defineModel('account.move.line', {
|
|
|
675
852
|
l.accountType = l.account?.type ?? null;
|
|
676
853
|
}),
|
|
677
854
|
}),
|
|
678
|
-
/** The tax a line reports under
|
|
855
|
+
/** The tax a generated tax line reports under (its amount, with its base in `taxBase`). */
|
|
679
856
|
taxReport: f.many2one('account.tax', {
|
|
680
857
|
store: true,
|
|
681
858
|
label: 'Tax (report)',
|
|
682
|
-
...compute('account.move.line', 'taxReport', ['
|
|
859
|
+
...compute('account.move.line', 'taxReport', ['taxLineOf', 'lineKind'], (lines) => {
|
|
683
860
|
for (const l of lines)
|
|
684
|
-
l.taxReport =
|
|
685
|
-
l.lineKind === 'tax'
|
|
686
|
-
? (l.taxLineOf?.id ?? null)
|
|
687
|
-
: l.lineKind === 'product'
|
|
688
|
-
? (l.tax?.id ?? null)
|
|
689
|
-
: null;
|
|
861
|
+
l.taxReport = l.lineKind === 'tax' ? (l.taxLineOf?.id ?? null) : null;
|
|
690
862
|
}),
|
|
691
863
|
}),
|
|
692
864
|
/** Partial matches this line takes part in, on its debit or its credit side (`payments.ts`). */
|
|
@@ -730,7 +902,7 @@ export const AccountMoveLine = defineModel('account.move.line', {
|
|
|
730
902
|
'quantity',
|
|
731
903
|
'priceUnit',
|
|
732
904
|
'discount',
|
|
733
|
-
'
|
|
905
|
+
'taxes',
|
|
734
906
|
], async (lines) => {
|
|
735
907
|
const loaded = await lines.load({ move: true });
|
|
736
908
|
const tx = await currentTx(lines.env);
|
|
@@ -740,6 +912,15 @@ export const AccountMoveLine = defineModel('account.move.line', {
|
|
|
740
912
|
},
|
|
741
913
|
},
|
|
742
914
|
});
|
|
915
|
+
/** A product line's taxes (RFC 0016): its amount after discount through the engine. */
|
|
916
|
+
function taxesOfLine(l) {
|
|
917
|
+
const quantity = l.quantity ?? orm.toDecimal(1);
|
|
918
|
+
const amount = quantity
|
|
919
|
+
.times(l.priceUnit ?? 0)
|
|
920
|
+
.times(orm.toDecimal(100).minus(l.discount ?? 0))
|
|
921
|
+
.div(100);
|
|
922
|
+
return computeTaxes(amount, quantity, [...l.taxes].map(taxRule));
|
|
923
|
+
}
|
|
743
924
|
/** The current database transaction's id (assigned on first use; posting writes anyway). */
|
|
744
925
|
async function currentTx(env) {
|
|
745
926
|
const [row] = await env.sql `SELECT pg_current_xact_id()::text AS tx`;
|
|
@@ -752,7 +933,8 @@ async function currentTx(env) {
|
|
|
752
933
|
async function postMoves(moves) {
|
|
753
934
|
const env = moves.env;
|
|
754
935
|
const loaded = await moves.load({
|
|
755
|
-
lines: {
|
|
936
|
+
lines: { taxes: { components: true } },
|
|
937
|
+
partner: true,
|
|
756
938
|
journal: true,
|
|
757
939
|
company: { currency: true },
|
|
758
940
|
currency: true,
|
|
@@ -788,12 +970,24 @@ async function postMoves(moves) {
|
|
|
788
970
|
throw fail(['lines'], 'The document has no product lines.', 'Add at least one line.');
|
|
789
971
|
if (m.partner === null)
|
|
790
972
|
throw fail(['partner'], 'The document has no partner.', 'Choose the customer or the vendor.');
|
|
791
|
-
const outgoing = m.kind
|
|
973
|
+
const outgoing = owedToUs(m.kind);
|
|
792
974
|
// Product and tax lines of an outgoing document are credits (income), the counterpart a debit.
|
|
793
975
|
const sign = outgoing ? -1 : 1;
|
|
794
|
-
const refund = m.kind
|
|
976
|
+
const refund = isRefund(m.kind);
|
|
977
|
+
const customer = m.kind.startsWith('out_');
|
|
795
978
|
const unit = m.currency.rounding;
|
|
796
979
|
const round = (d) => d.div(unit).toDecimalPlaces(0, orm.Decimal.ROUND_HALF_UP).times(unit);
|
|
980
|
+
const currency = orm.defineCurrency({
|
|
981
|
+
code: m.currency.name,
|
|
982
|
+
rounding: m.currency.rounding,
|
|
983
|
+
decimalPlaces: m.currency.decimalPlaces ?? 2,
|
|
984
|
+
id: m.currency.id,
|
|
985
|
+
});
|
|
986
|
+
// The place of supply: the partner's state unless set; it decides which components of a tax post.
|
|
987
|
+
const placeOfSupply = m.placeOfSupply ?? m.partner.state ?? null;
|
|
988
|
+
if (placeOfSupply !== m.placeOfSupply)
|
|
989
|
+
await one.write({ placeOfSupply });
|
|
990
|
+
const scope = supplyScope(m.company, m.partner.country ?? null, placeOfSupply);
|
|
797
991
|
let total = orm.toDecimal(0);
|
|
798
992
|
const byTax = new Map();
|
|
799
993
|
for (const l of products) {
|
|
@@ -809,42 +1003,106 @@ async function postMoves(moves) {
|
|
|
809
1003
|
debit: '0',
|
|
810
1004
|
credit: '0',
|
|
811
1005
|
});
|
|
812
|
-
|
|
813
|
-
|
|
1006
|
+
// Each tax of the line, in order, with its own base (SD in the VAT's base).
|
|
1007
|
+
for (const t of taxesOfLine(l).taxes) {
|
|
1008
|
+
const tax = [...l.taxes].find((x) => x.id === t.tax);
|
|
1009
|
+
if (tax === undefined)
|
|
1010
|
+
continue;
|
|
1011
|
+
const entry = byTax.get(t.tax) ?? {
|
|
814
1012
|
amount: orm.toDecimal(0),
|
|
815
|
-
|
|
816
|
-
|
|
1013
|
+
base: orm.toDecimal(0),
|
|
1014
|
+
tax,
|
|
817
1015
|
};
|
|
818
|
-
entry.amount = entry.amount.plus(
|
|
819
|
-
|
|
1016
|
+
entry.amount = entry.amount.plus(t.amount);
|
|
1017
|
+
entry.base = entry.base.plus(t.base);
|
|
1018
|
+
byTax.set(t.tax, entry);
|
|
820
1019
|
}
|
|
821
1020
|
}
|
|
822
|
-
for (const [taxId,
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
1021
|
+
for (const [taxId, { amount: exact, base, tax }] of byTax) {
|
|
1022
|
+
// A reverse-charge tax is the buyer's to pay to the state: the seller posts nothing.
|
|
1023
|
+
if (tax.reverseCharge && customer)
|
|
1024
|
+
continue;
|
|
1025
|
+
const amount = round(exact);
|
|
826
1026
|
if (amount.isZero())
|
|
827
1027
|
continue;
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
1028
|
+
// The tax itself, or its components of the document's scope, the amount allocated by share.
|
|
1029
|
+
const components = [...tax.components].filter((c) => c.scope === 'always' || c.scope === scope);
|
|
1030
|
+
const parts = components.length === 0
|
|
1031
|
+
? [
|
|
1032
|
+
{
|
|
1033
|
+
component: null,
|
|
1034
|
+
name: tax.name,
|
|
1035
|
+
account: (refund ? (tax.refundAccount?.id ?? tax.account?.id) : tax.account?.id) ?? null,
|
|
1036
|
+
payable: tax.reverseChargeAccount?.id ?? null,
|
|
1037
|
+
share: orm.toDecimal(1),
|
|
1038
|
+
},
|
|
1039
|
+
]
|
|
1040
|
+
: components.map((c) => ({
|
|
1041
|
+
component: c.id,
|
|
1042
|
+
name: c.name,
|
|
1043
|
+
account: (refund ? (c.refundAccount?.id ?? c.account?.id) : c.account?.id) ?? null,
|
|
1044
|
+
payable: c.reverseChargeAccount?.id ?? null,
|
|
1045
|
+
share: c.share,
|
|
1046
|
+
}));
|
|
1047
|
+
const shares = parts.reduce((sum, p) => sum.plus(p.share), orm.toDecimal(0));
|
|
1048
|
+
if (!shares.eq(1))
|
|
1049
|
+
throw fail(['lines'], `The components of ${tax.name} ${scope === 'intra' ? 'within the state' : 'between states'} add up to ${shares.toString()}, not 1.`, 'Fix their shares (Accounting › Configuration › Taxes).');
|
|
1050
|
+
const amounts = parts.length === 1
|
|
1051
|
+
? [amount]
|
|
1052
|
+
: orm.Money.of(amount, currency)
|
|
1053
|
+
.allocate(parts.map((p) => p.share))
|
|
1054
|
+
.map((money) => money.amount);
|
|
1055
|
+
for (const [i, part] of parts.entries()) {
|
|
1056
|
+
if (part.account === null)
|
|
1057
|
+
throw fail(['lines'], `Tax ${part.name} has no account.`, 'Set the tax account on the tax (Accounting › Configuration › Taxes).');
|
|
1058
|
+
const value = amounts[i];
|
|
1059
|
+
const line = {
|
|
831
1060
|
move: m.id,
|
|
832
1061
|
lineKind: 'tax',
|
|
833
|
-
account: orm.asId(t.account, 'account.account'),
|
|
834
1062
|
partner: m.partner.id,
|
|
835
|
-
name:
|
|
1063
|
+
name: part.name,
|
|
836
1064
|
taxLineOf: orm.asId(taxId, 'account.tax'),
|
|
837
|
-
|
|
1065
|
+
...(part.component === null
|
|
1066
|
+
? {}
|
|
1067
|
+
: { taxComponent: orm.asId(part.component, 'account.tax.component') }),
|
|
1068
|
+
taxBase: round(base),
|
|
838
1069
|
debit: '0',
|
|
839
1070
|
credit: '0',
|
|
840
|
-
}
|
|
841
|
-
|
|
1071
|
+
};
|
|
1072
|
+
if (!tax.reverseCharge) {
|
|
1073
|
+
total = total.plus(value);
|
|
1074
|
+
await linesOf.create([
|
|
1075
|
+
{
|
|
1076
|
+
...line,
|
|
1077
|
+
account: orm.asId(part.account, 'account.account'),
|
|
1078
|
+
amountCurrency: value.times(sign),
|
|
1079
|
+
},
|
|
1080
|
+
]);
|
|
1081
|
+
continue;
|
|
1082
|
+
}
|
|
1083
|
+
// Reverse charge: what the company may claim, and what it owes the state; not in the total.
|
|
1084
|
+
if (part.payable === null)
|
|
1085
|
+
throw fail(['lines'], `Tax ${part.name} is a reverse charge without a payable account.`, 'Set its reverse-charge account (Accounting › Configuration › Taxes).');
|
|
1086
|
+
await linesOf.create([
|
|
1087
|
+
{
|
|
1088
|
+
...line,
|
|
1089
|
+
taxReverseCharge: true,
|
|
1090
|
+
account: orm.asId(part.account, 'account.account'),
|
|
1091
|
+
amountCurrency: value.times(sign),
|
|
1092
|
+
},
|
|
1093
|
+
{
|
|
1094
|
+
...line,
|
|
1095
|
+
taxReverseCharge: true,
|
|
1096
|
+
account: orm.asId(part.payable, 'account.account'),
|
|
1097
|
+
amountCurrency: value.times(-sign),
|
|
1098
|
+
},
|
|
1099
|
+
]);
|
|
1100
|
+
}
|
|
842
1101
|
}
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
: m.company.accountPayable;
|
|
1102
|
+
// Customers' documents settle on the receivable account, vendors' on the payable one.
|
|
1103
|
+
const counterpart = customer ? m.company.accountReceivable : m.company.accountPayable;
|
|
846
1104
|
if (counterpart === null)
|
|
847
|
-
throw fail(['partner'], `The company has no ${
|
|
1105
|
+
throw fail(['partner'], `The company has no ${customer ? 'receivable' : 'payable'} account.`, 'Install a chart of accounts (Accounting › Configuration), or set the account on the company.');
|
|
848
1106
|
// One counterpart line per payment term part, the whole rounded total allocated between them.
|
|
849
1107
|
// Without payment terms, one part due on the document's due date (or its date).
|
|
850
1108
|
const parts = m.paymentTerm === null || m.paymentTerm.lines.length === 0
|
|
@@ -855,12 +1113,7 @@ async function postMoves(moves) {
|
|
|
855
1113
|
},
|
|
856
1114
|
]
|
|
857
1115
|
: m.paymentTerm.lines.map((pl) => ({ days: pl.days, percent: pl.percent }));
|
|
858
|
-
const totalMoney = orm.Money.of(total,
|
|
859
|
-
code: m.currency.name,
|
|
860
|
-
rounding: m.currency.rounding,
|
|
861
|
-
decimalPlaces: m.currency.decimalPlaces ?? 2,
|
|
862
|
-
id: m.currency.id,
|
|
863
|
-
}));
|
|
1116
|
+
const totalMoney = orm.Money.of(total, currency);
|
|
864
1117
|
const amounts = parts.length === 1 ? [totalMoney] : totalMoney.allocate(parts.map((pt) => pt.percent));
|
|
865
1118
|
let latest = m.date;
|
|
866
1119
|
for (const [i, part] of parts.entries()) {
|
|
@@ -946,6 +1199,8 @@ async function postMoves(moves) {
|
|
|
946
1199
|
throw fail(['lines'], `The entry does not balance: debit ${debit.toFixed(places)} and credit ${credit.toFixed(places)} differ by ${debit.minus(credit).abs().toFixed(places)}.`, 'Make the debits and the credits equal before posting.');
|
|
947
1200
|
await one.assignNumber(NUMBER_FIELD[m.journal.type], { date: m.date });
|
|
948
1201
|
await one.write({ postedAt: env.now(), postedTx: await currentTx(env) });
|
|
1202
|
+
// For whoever acts on posted documents (e-invoicing, automation), through the outbox: after the commit.
|
|
1203
|
+
await env.emit('account.move.posted', { id: m.id, kind: m.kind });
|
|
949
1204
|
}
|
|
950
1205
|
}
|
|
951
1206
|
/** Whole days from `from` to `to` (YYYY-MM-DD), negative when `to` is earlier. */
|
|
@@ -963,6 +1218,68 @@ export function addDays(date, days) {
|
|
|
963
1218
|
* and posts its entry (the journal's account against the receivable / payable) and matches the receivable /
|
|
964
1219
|
* payable line with the documents it pays (`payments.ts`).
|
|
965
1220
|
*/
|
|
1221
|
+
/**
|
|
1222
|
+
* A withholding at payment (RFC 0016 §5.1): VAT or income tax that the payer keeps back and pays to the state, such as
|
|
1223
|
+
* Bangladesh's VDS and TDS. `vendor`: we withhold when paying a vendor's bill (the account is a liability to the
|
|
1224
|
+
* state). `customer`: a customer withholds from us when paying our invoice (the account is a receivable from the
|
|
1225
|
+
* state). The base is the documents' untaxed amount, their tax or their total; it applies from a document total.
|
|
1226
|
+
*/
|
|
1227
|
+
export const AccountWithholding = defineModel('account.withholding', {
|
|
1228
|
+
label: 'Withholding',
|
|
1229
|
+
order: ['sequence asc', 'name asc'],
|
|
1230
|
+
archivable: true,
|
|
1231
|
+
companyScoped: { required: true },
|
|
1232
|
+
access: { 'account.invoicing': READ, 'account.user': READ, 'account.manager': ALL },
|
|
1233
|
+
fields: {
|
|
1234
|
+
name: f.char({ required: true, translate: true }),
|
|
1235
|
+
sequence: f.integer({ required: true, default: 10 }),
|
|
1236
|
+
direction: f.selection({ vendor: 'We withhold when paying vendors', customer: 'Customers withhold from us' }, { required: true, default: 'vendor' }),
|
|
1237
|
+
base: f.selection({ untaxed: 'Untaxed amount', tax: 'Tax amount', total: 'Total' }, { required: true, default: 'untaxed', label: 'Computed on' }),
|
|
1238
|
+
/** 0.05 for 5 %. */
|
|
1239
|
+
rate: f.decimal({ scale: 4, required: true, default: '0' }),
|
|
1240
|
+
/** A document whose total is below it is not withheld from. */
|
|
1241
|
+
threshold: f.decimal({
|
|
1242
|
+
scale: 2,
|
|
1243
|
+
required: true,
|
|
1244
|
+
default: '0',
|
|
1245
|
+
label: 'Applies from (document total)',
|
|
1246
|
+
}),
|
|
1247
|
+
account: f.many2one('account.account', { required: true, checkCompany: true }),
|
|
1248
|
+
},
|
|
1249
|
+
constraints: {
|
|
1250
|
+
rateRange: {
|
|
1251
|
+
...check('account.withholding', ['rate'], (ws) => ws.filter((w) => w.rate.lte(0) || w.rate.gt(1))),
|
|
1252
|
+
message: 'A withholding rate is above 0 and at most 1 (0.05 for 5 %).',
|
|
1253
|
+
},
|
|
1254
|
+
},
|
|
1255
|
+
});
|
|
1256
|
+
/** The withholdings that apply to a partner (vendor or customer): what a payment of their documents proposes. */
|
|
1257
|
+
export const PartnerWithholdings = extendModel('res.partner', {
|
|
1258
|
+
fields: {
|
|
1259
|
+
withholdings: f.many2many('account.withholding', {
|
|
1260
|
+
relation: 'res_partner_withholding_rel',
|
|
1261
|
+
label: 'Withholdings',
|
|
1262
|
+
}),
|
|
1263
|
+
},
|
|
1264
|
+
});
|
|
1265
|
+
/** What a payment withheld: the withholding, the base and the amount, in the payment's currency. */
|
|
1266
|
+
export const AccountPaymentWithholding = defineModel('account.payment.withholding', {
|
|
1267
|
+
label: 'Payment withholding',
|
|
1268
|
+
access: { 'account.invoicing': WRITE, 'account.user': ALL, 'account.manager': ALL },
|
|
1269
|
+
fields: {
|
|
1270
|
+
payment: f.many2one('account.payment', { required: true, ondelete: 'cascade' }),
|
|
1271
|
+
withholding: f.many2one('account.withholding', { required: true, ondelete: 'restrict' }),
|
|
1272
|
+
currency: f.many2one('res.currency', {
|
|
1273
|
+
store: true,
|
|
1274
|
+
...compute('account.payment.withholding', 'currency', ['payment.currency'], (lines) => {
|
|
1275
|
+
for (const l of lines)
|
|
1276
|
+
l.currency = l.payment.currency.id;
|
|
1277
|
+
}),
|
|
1278
|
+
}),
|
|
1279
|
+
base: f.monetary({ currency: 'currency', label: 'Base' }),
|
|
1280
|
+
amount: f.monetary({ currency: 'currency', required: true }),
|
|
1281
|
+
},
|
|
1282
|
+
});
|
|
966
1283
|
export const AccountPayment = defineModel('account.payment', {
|
|
967
1284
|
label: 'Payment',
|
|
968
1285
|
order: ['date desc', 'id desc'],
|
|
@@ -996,6 +1313,17 @@ export const AccountPayment = defineModel('account.payment', {
|
|
|
996
1313
|
memo: f.char(),
|
|
997
1314
|
/** The documents this payment settles (oldest due first); none: an open payment to match later. */
|
|
998
1315
|
documents: f.many2many('account.move', { label: 'Pays' }),
|
|
1316
|
+
/** Kept back from the documents (VDS, TDS): the documents are settled by `amount` plus these. */
|
|
1317
|
+
withholdings: f.one2many('account.payment.withholding', 'payment'),
|
|
1318
|
+
amountWithheld: f.monetary({
|
|
1319
|
+
currency: 'currency',
|
|
1320
|
+
store: true,
|
|
1321
|
+
label: 'Withheld',
|
|
1322
|
+
...compute('account.payment', 'amountWithheld', ['withholdings.amount'], (payments) => {
|
|
1323
|
+
for (const p of payments)
|
|
1324
|
+
p.amountWithheld = p.withholdings.sum('amount', p.currency);
|
|
1325
|
+
}),
|
|
1326
|
+
}),
|
|
999
1327
|
move: f.many2one('account.move', { label: 'Entry' }),
|
|
1000
1328
|
state: f.state({
|
|
1001
1329
|
states: { draft: 'Draft', posted: 'Posted', cancel: 'Cancelled' },
|
|
@@ -1025,7 +1353,17 @@ const sequence = (type, prefix) => orm.defineSequence({
|
|
|
1025
1353
|
});
|
|
1026
1354
|
export const accountingShutro = defineModule({
|
|
1027
1355
|
name: 'accounting',
|
|
1028
|
-
version: '0.
|
|
1356
|
+
version: '0.3.0',
|
|
1357
|
+
migrations: [
|
|
1358
|
+
{
|
|
1359
|
+
version: '0.2.0',
|
|
1360
|
+
description: 'Several taxes per line: the single tax of each line becomes its first',
|
|
1361
|
+
up: async (ctx) => {
|
|
1362
|
+
await ctx.sql `INSERT INTO account_move_line_account_tax_rel (account_move_line_id, account_tax_id)
|
|
1363
|
+
SELECT id, tax_id FROM account_move_line WHERE tax_id IS NOT NULL ON CONFLICT DO NOTHING`;
|
|
1364
|
+
},
|
|
1365
|
+
},
|
|
1366
|
+
],
|
|
1029
1367
|
depends: [contactsShutro, productShutro, messagingShutro, permissionsShutro, reportingShutro],
|
|
1030
1368
|
groups: [
|
|
1031
1369
|
orm.defineGroup({
|
|
@@ -1049,6 +1387,8 @@ export const accountingShutro = defineModule({
|
|
|
1049
1387
|
],
|
|
1050
1388
|
models: [
|
|
1051
1389
|
ResCurrencyRate,
|
|
1390
|
+
ResBankAccount,
|
|
1391
|
+
AccountWithholding,
|
|
1052
1392
|
AccountAccount,
|
|
1053
1393
|
AccountJournal,
|
|
1054
1394
|
AccountFiscalYear,
|
|
@@ -1059,6 +1399,8 @@ export const accountingShutro = defineModule({
|
|
|
1059
1399
|
AccountPartialReconcile,
|
|
1060
1400
|
AccountFullReconcile,
|
|
1061
1401
|
AccountPayment,
|
|
1402
|
+
AccountPaymentWithholding,
|
|
1403
|
+
AccountTaxComponent,
|
|
1062
1404
|
],
|
|
1063
1405
|
permissions: [
|
|
1064
1406
|
orm.definePermission({ code: 'account.post', label: 'Post entries', groups: ['account.user'] }),
|
|
@@ -1090,5 +1432,5 @@ export const accountingShutro = defineModule({
|
|
|
1090
1432
|
record('account.payment.term.line', 'term_half_now', { term: ref('account.payment.term', 'accounting.term_half'), days: 0, percent: '50' }, { noupdate: true }),
|
|
1091
1433
|
record('account.payment.term.line', 'term_half_later', { term: ref('account.payment.term', 'accounting.term_half'), days: 30, percent: '50' }, { noupdate: true }),
|
|
1092
1434
|
],
|
|
1093
|
-
extensions: [CompanyAccounting, TaxAccounts],
|
|
1435
|
+
extensions: [CompanyAccounting, TaxAccounts, PartnerWithholdings],
|
|
1094
1436
|
});
|