shutro-erp 0.2.0 → 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.
Files changed (232) hide show
  1. package/README.md +17 -10
  2. package/dist/account_portal/.kosh/bindings.d.ts +22 -0
  3. package/dist/account_portal/.kosh/bindings.js +15 -0
  4. package/dist/account_portal/.kosh/schema.d.ts +1350 -0
  5. package/dist/account_portal/index.d.ts +3 -0
  6. package/dist/account_portal/index.js +73 -0
  7. package/dist/account_portal/module.d.ts +5983 -0
  8. package/dist/account_portal/module.js +18 -0
  9. package/dist/accounting/.kosh/bindings.js +1 -1
  10. package/dist/accounting/.kosh/schema.d.ts +142 -9
  11. package/dist/accounting/chart.d.ts +40 -8
  12. package/dist/accounting/chart.js +124 -30
  13. package/dist/accounting/closing.d.ts +6 -1
  14. package/dist/accounting/closing.js +5 -1
  15. package/dist/accounting/demo.js +6 -5
  16. package/dist/accounting/index.d.ts +3 -3
  17. package/dist/accounting/index.js +200 -23
  18. package/dist/accounting/module.d.ts +3326 -1086
  19. package/dist/accounting/module.js +408 -66
  20. package/dist/accounting/payments.d.ts +25 -0
  21. package/dist/accounting/payments.js +96 -4
  22. package/dist/accounting/web/RegisterPayment.d.ts +3 -2
  23. package/dist/accounting/web/RegisterPayment.js +39 -6
  24. package/dist/accounting/web/SettingsPage.js +5 -1
  25. package/dist/accounting/web/messages.js +10 -0
  26. package/dist/cli/index.d.ts +1 -1
  27. package/dist/cli/index.js +1 -1
  28. package/dist/cli/init.d.ts +5 -0
  29. package/dist/cli/init.js +195 -53
  30. package/dist/cli/launch.d.ts +16 -0
  31. package/dist/cli/launch.js +138 -0
  32. package/dist/cli/prompt.d.ts +35 -0
  33. package/dist/cli/prompt.js +46 -0
  34. package/dist/cli/templates.d.ts +11 -3
  35. package/dist/cli/templates.js +32 -7
  36. package/dist/cli/tui.d.ts +7 -0
  37. package/dist/cli/tui.js +254 -0
  38. package/dist/cli/ui.d.ts +25 -5
  39. package/dist/cli/ui.js +49 -9
  40. package/dist/contacts/.kosh/bindings.js +1 -1
  41. package/dist/contacts/.kosh/schema.d.ts +3 -1
  42. package/dist/contacts/index.js +24 -1
  43. package/dist/contacts/module.d.ts +42 -0
  44. package/dist/contacts/module.js +3 -1
  45. package/dist/import/convert.d.ts +55 -0
  46. package/dist/import/convert.js +224 -0
  47. package/dist/import/csv.d.ts +28 -0
  48. package/dist/import/csv.js +115 -0
  49. package/dist/import/index.d.ts +33 -0
  50. package/dist/import/index.js +60 -0
  51. package/dist/import/mapping.d.ts +15 -0
  52. package/dist/import/mapping.js +33 -0
  53. package/dist/import/match.d.ts +24 -0
  54. package/dist/import/match.js +60 -0
  55. package/dist/import/models.d.ts +2 -0
  56. package/dist/import/models.js +102 -0
  57. package/dist/import/routes.d.ts +13 -0
  58. package/dist/import/routes.js +392 -0
  59. package/dist/import/run.d.ts +131 -0
  60. package/dist/import/run.js +568 -0
  61. package/dist/import/service.d.ts +153 -0
  62. package/dist/import/service.js +367 -0
  63. package/dist/import/web/ImportButton.d.ts +7 -0
  64. package/dist/import/web/ImportButton.js +31 -0
  65. package/dist/import/web/ImportWizard.d.ts +75 -0
  66. package/dist/import/web/ImportWizard.js +217 -0
  67. package/dist/import/web/index.d.ts +13 -0
  68. package/dist/import/web/index.js +18 -0
  69. package/dist/import/web/messages.d.ts +2 -0
  70. package/dist/import/web/messages.js +139 -0
  71. package/dist/import/worker.d.ts +3 -0
  72. package/dist/import/worker.js +52 -0
  73. package/dist/import/xlsx.d.ts +20 -0
  74. package/dist/import/xlsx.js +192 -0
  75. package/dist/l10n_bd/.kosh/bindings.d.ts +22 -0
  76. package/dist/l10n_bd/.kosh/bindings.js +15 -0
  77. package/dist/l10n_bd/.kosh/schema.d.ts +1281 -0
  78. package/dist/l10n_bd/documents.d.ts +8 -0
  79. package/dist/l10n_bd/documents.js +183 -0
  80. package/dist/l10n_bd/index.d.ts +8 -0
  81. package/dist/l10n_bd/index.js +156 -0
  82. package/dist/l10n_bd/module.d.ts +5705 -0
  83. package/dist/l10n_bd/module.js +163 -0
  84. package/dist/l10n_bd/mushak.d.ts +67 -0
  85. package/dist/l10n_bd/mushak.js +139 -0
  86. package/dist/l10n_bd/routes.d.ts +3 -0
  87. package/dist/l10n_bd/routes.js +205 -0
  88. package/dist/l10n_bd/web/MushakPage.d.ts +7 -0
  89. package/dist/l10n_bd/web/MushakPage.js +53 -0
  90. package/dist/l10n_bd/web/index.d.ts +13 -0
  91. package/dist/l10n_bd/web/index.js +18 -0
  92. package/dist/l10n_bd/web/messages.d.ts +2 -0
  93. package/dist/l10n_bd/web/messages.js +101 -0
  94. package/dist/l10n_in/.kosh/bindings.d.ts +22 -0
  95. package/dist/l10n_in/.kosh/bindings.js +15 -0
  96. package/dist/l10n_in/.kosh/schema.d.ts +1343 -0
  97. package/dist/l10n_in/documents.d.ts +8 -0
  98. package/dist/l10n_in/documents.js +114 -0
  99. package/dist/l10n_in/einvoice.d.ts +253 -0
  100. package/dist/l10n_in/einvoice.js +485 -0
  101. package/dist/l10n_in/gstr.d.ts +109 -0
  102. package/dist/l10n_in/gstr.js +430 -0
  103. package/dist/l10n_in/index.d.ts +20 -0
  104. package/dist/l10n_in/index.js +232 -0
  105. package/dist/l10n_in/module.d.ts +7823 -0
  106. package/dist/l10n_in/module.js +427 -0
  107. package/dist/l10n_in/routes-einvoice.d.ts +15 -0
  108. package/dist/l10n_in/routes-einvoice.js +124 -0
  109. package/dist/l10n_in/routes.d.ts +17 -0
  110. package/dist/l10n_in/routes.js +282 -0
  111. package/dist/l10n_in/web/EInvoicePanel.d.ts +8 -0
  112. package/dist/l10n_in/web/EInvoicePanel.js +63 -0
  113. package/dist/l10n_in/web/GstPage.d.ts +7 -0
  114. package/dist/l10n_in/web/GstPage.js +57 -0
  115. package/dist/l10n_in/web/GstSettings.d.ts +10 -0
  116. package/dist/l10n_in/web/GstSettings.js +36 -0
  117. package/dist/l10n_in/web/index.d.ts +15 -0
  118. package/dist/l10n_in/web/index.js +22 -0
  119. package/dist/l10n_in/web/messages.d.ts +2 -0
  120. package/dist/l10n_in/web/messages.js +166 -0
  121. package/dist/modules-sdk/demo.d.ts +2 -0
  122. package/dist/modules-sdk/demo.js +7 -3
  123. package/dist/modules-sdk/index.d.ts +2 -2
  124. package/dist/modules-sdk/index.js +1 -1
  125. package/dist/modules-sdk/manifest.d.ts +132 -0
  126. package/dist/modules-sdk/manifest.js +22 -0
  127. package/dist/notify/inbox.js +21 -0
  128. package/dist/notify/messages.js +4 -0
  129. package/dist/notify/models.d.ts +1 -1
  130. package/dist/notify/models.js +2 -0
  131. package/dist/notify/web/messages.js +4 -0
  132. package/dist/portal/.kosh/bindings.d.ts +22 -0
  133. package/dist/portal/.kosh/bindings.js +15 -0
  134. package/dist/portal/.kosh/schema.d.ts +346 -0
  135. package/dist/portal/announce.d.ts +16 -0
  136. package/dist/portal/announce.js +117 -0
  137. package/dist/portal/documents.d.ts +17 -0
  138. package/dist/portal/documents.js +61 -0
  139. package/dist/portal/index.d.ts +20 -0
  140. package/dist/portal/index.js +65 -0
  141. package/dist/portal/module.d.ts +1405 -0
  142. package/dist/portal/module.js +84 -0
  143. package/dist/portal/routes-documents.d.ts +2 -0
  144. package/dist/portal/routes-documents.js +391 -0
  145. package/dist/portal/routes.d.ts +13 -0
  146. package/dist/portal/routes.js +313 -0
  147. package/dist/portal/rules.d.ts +11 -0
  148. package/dist/portal/rules.js +25 -0
  149. package/dist/portal/web/PortalAccess.d.ts +8 -0
  150. package/dist/portal/web/PortalAccess.js +66 -0
  151. package/dist/portal/web/index.d.ts +12 -0
  152. package/dist/portal/web/index.js +17 -0
  153. package/dist/portal/web/messages.d.ts +2 -0
  154. package/dist/portal/web/messages.js +33 -0
  155. package/dist/product/.kosh/bindings.js +1 -1
  156. package/dist/product/.kosh/schema.d.ts +12 -3
  157. package/dist/product/demo.js +6 -5
  158. package/dist/product/index.d.ts +1 -0
  159. package/dist/product/index.js +17 -4
  160. package/dist/product/module.d.ts +346 -14
  161. package/dist/product/module.js +48 -18
  162. package/dist/product/taxes.d.ts +59 -0
  163. package/dist/product/taxes.js +61 -0
  164. package/dist/project/.kosh/bindings.js +1 -1
  165. package/dist/project/.kosh/schema.d.ts +3 -1
  166. package/dist/project/module.d.ts +20 -0
  167. package/dist/project_portal/.kosh/bindings.d.ts +22 -0
  168. package/dist/project_portal/.kosh/bindings.js +15 -0
  169. package/dist/project_portal/.kosh/schema.d.ts +671 -0
  170. package/dist/project_portal/index.d.ts +9 -0
  171. package/dist/project_portal/index.js +42 -0
  172. package/dist/project_portal/module.d.ts +2142 -0
  173. package/dist/project_portal/module.js +22 -0
  174. package/dist/purchase/.kosh/bindings.js +1 -1
  175. package/dist/purchase/.kosh/schema.d.ts +15 -4
  176. package/dist/purchase/demo.js +3 -3
  177. package/dist/purchase/index.js +1 -1
  178. package/dist/purchase/module.d.ts +232 -8
  179. package/dist/purchase/module.js +51 -9
  180. package/dist/purchase_account/.kosh/bindings.js +1 -1
  181. package/dist/purchase_account/.kosh/schema.d.ts +144 -10
  182. package/dist/purchase_account/module.d.ts +2008 -641
  183. package/dist/purchase_account/module.js +2 -2
  184. package/dist/purchase_portal/.kosh/bindings.d.ts +22 -0
  185. package/dist/purchase_portal/.kosh/bindings.js +15 -0
  186. package/dist/purchase_portal/.kosh/schema.d.ts +822 -0
  187. package/dist/purchase_portal/index.d.ts +9 -0
  188. package/dist/purchase_portal/index.js +53 -0
  189. package/dist/purchase_portal/module.d.ts +2758 -0
  190. package/dist/purchase_portal/module.js +18 -0
  191. package/dist/reporting/documents.js +20 -3
  192. package/dist/reporting/index.d.ts +1 -0
  193. package/dist/reporting/index.js +1 -0
  194. package/dist/reporting/routes-documents.js +9 -2
  195. package/dist/reporting/words.d.ts +11 -0
  196. package/dist/reporting/words.js +229 -0
  197. package/dist/sale_account/.kosh/bindings.js +1 -1
  198. package/dist/sale_account/.kosh/schema.d.ts +144 -10
  199. package/dist/sale_account/module.d.ts +2008 -641
  200. package/dist/sale_account/module.js +3 -3
  201. package/dist/sale_portal/.kosh/bindings.d.ts +22 -0
  202. package/dist/sale_portal/.kosh/bindings.js +15 -0
  203. package/dist/sale_portal/.kosh/schema.d.ts +736 -0
  204. package/dist/sale_portal/index.d.ts +9 -0
  205. package/dist/sale_portal/index.js +71 -0
  206. package/dist/sale_portal/module.d.ts +2718 -0
  207. package/dist/sale_portal/module.js +19 -0
  208. package/dist/sale_project/.kosh/bindings.js +1 -1
  209. package/dist/sale_project/.kosh/schema.d.ts +15 -4
  210. package/dist/sale_project/module.d.ts +390 -10
  211. package/dist/sales/.kosh/bindings.js +1 -1
  212. package/dist/sales/.kosh/schema.d.ts +15 -4
  213. package/dist/sales/demo.js +3 -3
  214. package/dist/sales/index.js +67 -1
  215. package/dist/sales/module.d.ts +232 -8
  216. package/dist/sales/module.js +51 -12
  217. package/dist/server/auth/base.js +2 -0
  218. package/dist/server/auth/service.d.ts +2 -0
  219. package/dist/server/auth/service.js +7 -1
  220. package/dist/server/index.d.ts +1 -1
  221. package/dist/server/index.js +1 -1
  222. package/dist/server/server.js +26 -1
  223. package/dist/ui/api.d.ts +2 -0
  224. package/dist/ui/app.js +1 -1
  225. package/dist/ui/context.d.ts +3 -1
  226. package/dist/ui/context.js +2 -2
  227. package/dist/ui/index.d.ts +1 -1
  228. package/dist/ui/index.js +1 -1
  229. package/dist/ui/lib/format.d.ts +2 -0
  230. package/dist/ui/lib/format.js +3 -1
  231. package/dist/ui/views/FormView.js +19 -6
  232. package/package.json +50 -2
@@ -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
- chartTemplate: f.selection({ generic: 'Generic', bd: 'Bangladesh' }, { label: 'Chart of accounts' }),
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
- /** Taxes included in the unit price (the price is gross). */
253
- priceIncluded: f.boolean({ default: false, label: 'Included in price' }),
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 === 'out_invoice' || m.kind === 'in_refund' ? 1 : -1;
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
- tax: f.many2one('account.tax'),
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', ['lineKind', 'quantity', 'priceUnit', 'discount', 'tax.rate', 'tax.priceIncluded'], (lines) => {
623
- for (const l of lines) {
624
- if (l.lineKind !== 'product') {
625
- l.subtotal = null;
626
- continue;
627
- }
628
- const gross = (l.quantity ?? orm.toDecimal(1))
629
- .times(l.priceUnit ?? 0)
630
- .times(orm.toDecimal(100).minus(l.discount ?? 0))
631
- .div(100);
632
- const rate = l.tax?.rate ?? orm.toDecimal(0);
633
- l.subtotal = l.tax?.priceIncluded === true ? gross.div(rate.plus(1)) : gross;
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 tax of a product line in the document currency (summed and rounded per tax at posting). */
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', ['subtotal', 'tax.rate'], (lines) => {
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: a product line's tax (its base), a tax line's tax (its amount). */
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', ['tax', 'taxLineOf', 'lineKind'], (lines) => {
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
- 'tax',
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: { tax: true },
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 === 'out_invoice' || m.kind === 'in_refund';
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 === 'out_refund' || m.kind === 'in_refund';
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
- if (l.tax !== null) {
813
- const entry = byTax.get(l.tax.id) ?? {
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
- account: (refund ? (l.tax.refundAccount?.id ?? l.tax.account?.id) : l.tax.account?.id) ?? null,
816
- name: l.tax.name,
1013
+ base: orm.toDecimal(0),
1014
+ tax,
817
1015
  };
818
- entry.amount = entry.amount.plus(l.taxAmount?.amount ?? 0);
819
- byTax.set(l.tax.id, entry);
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, t] of byTax) {
823
- if (t.account === null)
824
- throw fail(['lines'], `Tax ${t.name} has no account.`, 'Set the tax account on the tax (Accounting › Configuration › Taxes).');
825
- const amount = round(t.amount);
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
- total = total.plus(amount);
829
- await linesOf.create([
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: t.name,
1063
+ name: part.name,
836
1064
  taxLineOf: orm.asId(taxId, 'account.tax'),
837
- amountCurrency: amount.times(sign),
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
- const counterpart = outgoing || m.kind === 'out_refund'
844
- ? m.company.accountReceivable
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 ${outgoing || m.kind === 'out_refund' ? 'receivable' : 'payable'} account.`, 'Install a chart of accounts (Accounting › Configuration), or set the account on the company.');
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, orm.defineCurrency({
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.1.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
  });