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
@@ -0,0 +1,346 @@
1
+ // GENERATED by `pnpm generate:models` (Shutro’s generator). Do not edit. definitions-hash: 435daa394f2704fc
2
+ // Schema view of module `portal` (RFC 0002 §7.7): the models it owns or depends on, with the extensions it can see.
3
+ import type { F } from '../../server/orm.js';
4
+
5
+ /** Model name → model type (RFC 0002 §7.7). */
6
+ export interface SchemaView {
7
+ readonly 'audit.event': AuditEvent;
8
+ readonly 'auth.credential': AuthCredential;
9
+ readonly 'auth.session': AuthSession;
10
+ readonly 'auth.user_grant': AuthUserGrant;
11
+ readonly 'auth.user_group': AuthUserGroup;
12
+ readonly 'kosh.saved_view': KoshSavedView;
13
+ readonly 'portal.announcement': PortalAnnouncement;
14
+ readonly 'portal.invitation': PortalInvitation;
15
+ readonly 'portal.signature': PortalSignature;
16
+ readonly 'res.company': ResCompany;
17
+ readonly 'res.currency': ResCurrency;
18
+ readonly 'res.partner': ResPartner;
19
+ readonly 'res.users': ResUsers;
20
+ }
21
+
22
+ /** `audit.event` (owner: base). */
23
+ export interface AuditEvent {
24
+ readonly name: 'audit.event';
25
+ readonly fields: {
26
+ readonly id: F.Id<'audit.event'>;
27
+ readonly createdAt: F.Datetime<{ readonly required: true; readonly readonly: true }>;
28
+ readonly updatedAt: F.Datetime<{ readonly required: true; readonly readonly: true }>;
29
+ readonly createdBy: F.Many2one<'res.users', { readonly readonly: true }>;
30
+ readonly updatedBy: F.Many2one<'res.users', { readonly readonly: true }>;
31
+ readonly displayName: F.Char<{ readonly required: true; readonly readonly: true; readonly compute: true }>;
32
+ readonly kind: F.Char<{ readonly required: true }>;
33
+ readonly actor: F.Many2one<'res.users'>;
34
+ readonly company: F.Many2one<'res.company'>;
35
+ readonly targetModel: F.Char;
36
+ readonly targetId: F.Integer;
37
+ readonly summary: F.Char<{ readonly required: true }>;
38
+ readonly details: F.Json;
39
+ readonly ip: F.Char;
40
+ };
41
+ readonly methods: AuditEventMethods;
42
+ }
43
+
44
+ /** Methods of `audit.event`: declared ones and transitions. */
45
+ export interface AuditEventMethods extends F.Methods<typeof import('../../server/index.js').baseShutro, 'audit.event'> {
46
+ }
47
+
48
+ /** `auth.credential` (owner: base). */
49
+ export interface AuthCredential {
50
+ readonly name: 'auth.credential';
51
+ readonly fields: {
52
+ readonly id: F.Id<'auth.credential'>;
53
+ readonly createdAt: F.Datetime<{ readonly required: true; readonly readonly: true }>;
54
+ readonly updatedAt: F.Datetime<{ readonly required: true; readonly readonly: true }>;
55
+ readonly createdBy: F.Many2one<'res.users', { readonly readonly: true }>;
56
+ readonly updatedBy: F.Many2one<'res.users', { readonly readonly: true }>;
57
+ readonly displayName: F.Char<{ readonly required: true; readonly readonly: true; readonly compute: true }>;
58
+ readonly user: F.Many2one<'res.users', { readonly required: true }>;
59
+ readonly passwordHash: F.Char;
60
+ readonly totpSecret: F.Char;
61
+ readonly totpEnabled: F.Bool<{ readonly required: true; readonly default: true }>;
62
+ readonly recoveryCodes: F.Json;
63
+ };
64
+ readonly methods: AuthCredentialMethods;
65
+ }
66
+
67
+ /** Methods of `auth.credential`: declared ones and transitions. */
68
+ export interface AuthCredentialMethods extends F.Methods<typeof import('../../server/index.js').baseShutro, 'auth.credential'> {
69
+ }
70
+
71
+ /** `auth.session` (owner: base). */
72
+ export interface AuthSession {
73
+ readonly name: 'auth.session';
74
+ readonly fields: {
75
+ readonly id: F.Id<'auth.session'>;
76
+ readonly createdAt: F.Datetime<{ readonly required: true; readonly readonly: true }>;
77
+ readonly updatedAt: F.Datetime<{ readonly required: true; readonly readonly: true }>;
78
+ readonly createdBy: F.Many2one<'res.users', { readonly readonly: true }>;
79
+ readonly updatedBy: F.Many2one<'res.users', { readonly readonly: true }>;
80
+ readonly displayName: F.Char<{ readonly required: true; readonly readonly: true; readonly compute: true }>;
81
+ readonly tokenHash: F.Char<{ readonly required: true }>;
82
+ readonly csrfHash: F.Char<{ readonly required: true }>;
83
+ readonly user: F.Many2one<'res.users', { readonly required: true }>;
84
+ readonly twoFactorVerified: F.Bool<{ readonly required: true; readonly default: true }>;
85
+ readonly secondFactor: F.Bool<{ readonly required: true; readonly default: true }>;
86
+ readonly expiresAt: F.Datetime<{ readonly required: true }>;
87
+ readonly lastSeenAt: F.Datetime<{ readonly required: true }>;
88
+ readonly revoked: F.Bool<{ readonly required: true; readonly default: true }>;
89
+ readonly ip: F.Char;
90
+ readonly userAgent: F.Char;
91
+ };
92
+ readonly methods: AuthSessionMethods;
93
+ }
94
+
95
+ /** Methods of `auth.session`: declared ones and transitions. */
96
+ export interface AuthSessionMethods extends F.Methods<typeof import('../../server/index.js').baseShutro, 'auth.session'> {
97
+ }
98
+
99
+ /** `auth.user_grant` (owner: base). */
100
+ export interface AuthUserGrant {
101
+ readonly name: 'auth.user_grant';
102
+ readonly fields: {
103
+ readonly id: F.Id<'auth.user_grant'>;
104
+ readonly createdAt: F.Datetime<{ readonly required: true; readonly readonly: true }>;
105
+ readonly updatedAt: F.Datetime<{ readonly required: true; readonly readonly: true }>;
106
+ readonly createdBy: F.Many2one<'res.users', { readonly readonly: true }>;
107
+ readonly updatedBy: F.Many2one<'res.users', { readonly readonly: true }>;
108
+ readonly displayName: F.Char<{ readonly required: true; readonly readonly: true; readonly compute: true }>;
109
+ readonly user: F.Many2one<'res.users', { readonly required: true }>;
110
+ readonly company: F.Many2one<'res.company'>;
111
+ readonly grant: F.Char<{ readonly required: true }>;
112
+ readonly expiresAt: F.Datetime;
113
+ readonly reason: F.Char<{ readonly required: true }>;
114
+ };
115
+ readonly methods: AuthUserGrantMethods;
116
+ }
117
+
118
+ /** Methods of `auth.user_grant`: declared ones and transitions. */
119
+ export interface AuthUserGrantMethods extends F.Methods<typeof import('../../server/index.js').baseShutro, 'auth.user_grant'> {
120
+ }
121
+
122
+ /** `auth.user_group` (owner: base). */
123
+ export interface AuthUserGroup {
124
+ readonly name: 'auth.user_group';
125
+ readonly fields: {
126
+ readonly id: F.Id<'auth.user_group'>;
127
+ readonly createdAt: F.Datetime<{ readonly required: true; readonly readonly: true }>;
128
+ readonly updatedAt: F.Datetime<{ readonly required: true; readonly readonly: true }>;
129
+ readonly createdBy: F.Many2one<'res.users', { readonly readonly: true }>;
130
+ readonly updatedBy: F.Many2one<'res.users', { readonly readonly: true }>;
131
+ readonly displayName: F.Char<{ readonly required: true; readonly readonly: true; readonly compute: true }>;
132
+ readonly user: F.Many2one<'res.users', { readonly required: true }>;
133
+ readonly company: F.Many2one<'res.company'>;
134
+ readonly group: F.Char<{ readonly required: true }>;
135
+ };
136
+ readonly methods: AuthUserGroupMethods;
137
+ }
138
+
139
+ /** Methods of `auth.user_group`: declared ones and transitions. */
140
+ export interface AuthUserGroupMethods extends F.Methods<typeof import('../../server/index.js').baseShutro, 'auth.user_group'> {
141
+ }
142
+
143
+ /** `kosh.saved_view` (owner: base). */
144
+ export interface KoshSavedView {
145
+ readonly name: 'kosh.saved_view';
146
+ readonly fields: {
147
+ readonly id: F.Id<'kosh.saved_view'>;
148
+ readonly createdAt: F.Datetime<{ readonly required: true; readonly readonly: true }>;
149
+ readonly updatedAt: F.Datetime<{ readonly required: true; readonly readonly: true }>;
150
+ readonly createdBy: F.Many2one<'res.users', { readonly readonly: true }>;
151
+ readonly updatedBy: F.Many2one<'res.users', { readonly readonly: true }>;
152
+ readonly displayName: F.Char<{ readonly required: true; readonly readonly: true; readonly compute: true }>;
153
+ readonly name: F.Char<{ readonly required: true }>;
154
+ readonly targetModel: F.Char<{ readonly required: true }>;
155
+ readonly viewType: F.Char<{ readonly required: true; readonly default: true }>;
156
+ readonly state: F.Json<unknown, { readonly required: true }>;
157
+ readonly shared: F.Bool<{ readonly required: true; readonly default: true }>;
158
+ readonly isDefault: F.Bool<{ readonly required: true; readonly default: true }>;
159
+ };
160
+ readonly methods: KoshSavedViewMethods;
161
+ }
162
+
163
+ /** Methods of `kosh.saved_view`: declared ones and transitions. */
164
+ export interface KoshSavedViewMethods extends F.Methods<typeof import('../../server/index.js').baseShutro, 'kosh.saved_view'> {
165
+ }
166
+
167
+ /** `portal.announcement` (owner: portal). */
168
+ export interface PortalAnnouncement {
169
+ readonly name: 'portal.announcement';
170
+ readonly fields: {
171
+ readonly id: F.Id<'portal.announcement'>;
172
+ readonly createdAt: F.Datetime<{ readonly required: true; readonly readonly: true }>;
173
+ readonly updatedAt: F.Datetime<{ readonly required: true; readonly readonly: true }>;
174
+ readonly createdBy: F.Many2one<'res.users', { readonly readonly: true }>;
175
+ readonly updatedBy: F.Many2one<'res.users', { readonly readonly: true }>;
176
+ readonly displayName: F.Char<{ readonly required: true; readonly readonly: true; readonly compute: true }>;
177
+ readonly resModel: F.Char<{ readonly required: true }>;
178
+ readonly resId: F.Integer<{ readonly required: true }>;
179
+ readonly document: F.Char<{ readonly required: true }>;
180
+ readonly recipients: F.Integer<{ readonly required: true; readonly default: true }>;
181
+ };
182
+ readonly methods: PortalAnnouncementMethods;
183
+ }
184
+
185
+ /** Methods of `portal.announcement`: declared ones and transitions. */
186
+ export interface PortalAnnouncementMethods extends F.Methods<typeof import('../module.js').portalShutro, 'portal.announcement'> {
187
+ }
188
+
189
+ /** `portal.invitation` (owner: portal). */
190
+ export interface PortalInvitation {
191
+ readonly name: 'portal.invitation';
192
+ readonly fields: {
193
+ readonly id: F.Id<'portal.invitation'>;
194
+ readonly createdAt: F.Datetime<{ readonly required: true; readonly readonly: true }>;
195
+ readonly updatedAt: F.Datetime<{ readonly required: true; readonly readonly: true }>;
196
+ readonly createdBy: F.Many2one<'res.users', { readonly readonly: true }>;
197
+ readonly updatedBy: F.Many2one<'res.users', { readonly readonly: true }>;
198
+ readonly displayName: F.Char<{ readonly required: true; readonly readonly: true; readonly compute: true }>;
199
+ readonly user: F.Many2one<'res.users', { readonly required: true }>;
200
+ readonly tokenHash: F.Char<{ readonly required: true }>;
201
+ readonly expiresAt: F.Datetime<{ readonly required: true }>;
202
+ readonly usedAt: F.Datetime;
203
+ readonly invitedBy: F.Many2one<'res.users'>;
204
+ };
205
+ readonly methods: PortalInvitationMethods;
206
+ }
207
+
208
+ /** Methods of `portal.invitation`: declared ones and transitions. */
209
+ export interface PortalInvitationMethods extends F.Methods<typeof import('../module.js').portalShutro, 'portal.invitation'> {
210
+ }
211
+
212
+ /** `portal.signature` (owner: portal). */
213
+ export interface PortalSignature {
214
+ readonly name: 'portal.signature';
215
+ readonly fields: {
216
+ readonly id: F.Id<'portal.signature'>;
217
+ readonly createdAt: F.Datetime<{ readonly required: true; readonly readonly: true }>;
218
+ readonly updatedAt: F.Datetime<{ readonly required: true; readonly readonly: true }>;
219
+ readonly createdBy: F.Many2one<'res.users', { readonly readonly: true }>;
220
+ readonly updatedBy: F.Many2one<'res.users', { readonly readonly: true }>;
221
+ readonly displayName: F.Char<{ readonly required: true; readonly readonly: true; readonly compute: true }>;
222
+ readonly company: F.Many2one<'res.company', { readonly required: true; readonly default: true }>;
223
+ readonly resModel: F.Char<{ readonly required: true }>;
224
+ readonly resId: F.Integer<{ readonly required: true }>;
225
+ readonly action: F.Char<{ readonly required: true }>;
226
+ readonly user: F.Many2one<'res.users', { readonly required: true }>;
227
+ readonly signedName: F.Char<{ readonly required: true }>;
228
+ readonly image: F.Binary;
229
+ readonly documentHash: F.Char<{ readonly required: true }>;
230
+ readonly ip: F.Char;
231
+ };
232
+ readonly methods: PortalSignatureMethods;
233
+ }
234
+
235
+ /** Methods of `portal.signature`: declared ones and transitions. */
236
+ export interface PortalSignatureMethods extends F.Methods<typeof import('../module.js').portalShutro, 'portal.signature'> {
237
+ }
238
+
239
+ /** `res.company` (owner: base). */
240
+ export interface ResCompany {
241
+ readonly name: 'res.company';
242
+ readonly fields: {
243
+ readonly id: F.Id<'res.company'>;
244
+ readonly createdAt: F.Datetime<{ readonly required: true; readonly readonly: true }>;
245
+ readonly updatedAt: F.Datetime<{ readonly required: true; readonly readonly: true }>;
246
+ readonly createdBy: F.Many2one<'res.users', { readonly readonly: true }>;
247
+ readonly updatedBy: F.Many2one<'res.users', { readonly readonly: true }>;
248
+ readonly displayName: F.Char<{ readonly required: true; readonly readonly: true; readonly compute: true }>;
249
+ readonly name: F.Char<{ readonly required: true }>;
250
+ readonly currency: F.Many2one<'res.currency', { readonly required: true }>;
251
+ readonly numberGrouping: F.Selection<'international' | 'lakh', { readonly required: true; readonly default: true }>;
252
+ };
253
+ readonly methods: ResCompanyMethods;
254
+ }
255
+
256
+ /** Methods of `res.company`: declared ones and transitions. */
257
+ export interface ResCompanyMethods extends F.Methods<typeof import('../../server/index.js').baseShutro, 'res.company'> {
258
+ }
259
+
260
+ /** `res.currency` (owner: base). */
261
+ export interface ResCurrency {
262
+ readonly name: 'res.currency';
263
+ readonly fields: {
264
+ readonly id: F.Id<'res.currency'>;
265
+ readonly createdAt: F.Datetime<{ readonly required: true; readonly readonly: true }>;
266
+ readonly updatedAt: F.Datetime<{ readonly required: true; readonly readonly: true }>;
267
+ readonly createdBy: F.Many2one<'res.users', { readonly readonly: true }>;
268
+ readonly updatedBy: F.Many2one<'res.users', { readonly readonly: true }>;
269
+ readonly displayName: F.Char<{ readonly required: true; readonly readonly: true; readonly compute: true }>;
270
+ readonly name: F.Char<{ readonly required: true }>;
271
+ readonly rounding: F.Decimal<{ readonly required: true; readonly default: true }>;
272
+ readonly decimalPlaces: F.Integer<{ readonly default: true }>;
273
+ };
274
+ readonly methods: ResCurrencyMethods;
275
+ }
276
+
277
+ /** Methods of `res.currency`: declared ones and transitions. */
278
+ export interface ResCurrencyMethods extends F.Methods<typeof import('../../server/index.js').baseShutro, 'res.currency'> {
279
+ }
280
+
281
+ /** `res.partner` (owner: contacts; extended by: portal). */
282
+ export interface ResPartner {
283
+ readonly name: 'res.partner';
284
+ readonly fields: {
285
+ readonly id: F.Id<'res.partner'>;
286
+ readonly createdAt: F.Datetime<{ readonly required: true; readonly readonly: true }>;
287
+ readonly updatedAt: F.Datetime<{ readonly required: true; readonly readonly: true }>;
288
+ readonly createdBy: F.Many2one<'res.users', { readonly readonly: true }>;
289
+ readonly updatedBy: F.Many2one<'res.users', { readonly readonly: true }>;
290
+ readonly displayName: F.Char<{ readonly required: true; readonly readonly: true; readonly compute: true }>;
291
+ readonly active: F.Bool<{ readonly required: true; readonly default: true }>;
292
+ readonly company: F.Many2one<'res.company', { readonly default: true }>;
293
+ readonly name: F.Char<{ readonly required: true }>;
294
+ readonly isCompany: F.Bool<{ readonly required: true; readonly default: true }>;
295
+ readonly parent: F.Many2one<'res.partner'>;
296
+ readonly contacts: F.One2many<'res.partner', { readonly required: true }>;
297
+ readonly email: F.Char;
298
+ readonly phone: F.Char;
299
+ readonly mobile: F.Char;
300
+ readonly website: F.Char;
301
+ readonly street: F.Char;
302
+ readonly city: F.Char;
303
+ readonly zip: F.Char;
304
+ readonly country: F.Char;
305
+ readonly state: F.Char;
306
+ readonly taxId: F.Char;
307
+ readonly isCustomer: F.Bool<{ readonly required: true; readonly default: true }>;
308
+ readonly isVendor: F.Bool<{ readonly required: true; readonly default: true }>;
309
+ readonly currency: F.Many2one<'res.currency'>;
310
+ readonly notes: F.Text;
311
+ readonly portalUsers: F.One2many<'res.users', { readonly required: true }>; // portal
312
+ };
313
+ readonly methods: ResPartnerMethods;
314
+ }
315
+
316
+ /** Methods of `res.partner`: declared ones and transitions. */
317
+ export interface ResPartnerMethods extends F.Methods<typeof import('../../contacts/index.js').contactsShutro, 'res.partner'>, F.Methods<typeof import('../module.js').portalShutro, 'res.partner'> {
318
+ }
319
+
320
+ /** `res.users` (owner: base; extended by: portal). */
321
+ export interface ResUsers {
322
+ readonly name: 'res.users';
323
+ readonly fields: {
324
+ readonly id: F.Id<'res.users'>;
325
+ readonly createdAt: F.Datetime<{ readonly required: true; readonly readonly: true }>;
326
+ readonly updatedAt: F.Datetime<{ readonly required: true; readonly readonly: true }>;
327
+ readonly createdBy: F.Many2one<'res.users', { readonly readonly: true }>;
328
+ readonly updatedBy: F.Many2one<'res.users', { readonly readonly: true }>;
329
+ readonly displayName: F.Char<{ readonly required: true; readonly readonly: true; readonly compute: true }>;
330
+ readonly active: F.Bool<{ readonly required: true; readonly default: true }>;
331
+ readonly login: F.Char<{ readonly required: true }>;
332
+ readonly name: F.Char<{ readonly required: true }>;
333
+ readonly email: F.Char;
334
+ readonly phone: F.Char;
335
+ readonly company: F.Many2one<'res.company'>;
336
+ readonly companies: F.Many2many<'res.company', { readonly required: true }>;
337
+ readonly lang: F.Char<{ readonly default: true }>;
338
+ readonly tz: F.Char<{ readonly default: true }>;
339
+ readonly portalPartner: F.Many2one<'res.partner'>; // portal
340
+ };
341
+ readonly methods: ResUsersMethods;
342
+ }
343
+
344
+ /** Methods of `res.users`: declared ones and transitions. */
345
+ export interface ResUsersMethods extends F.Methods<typeof import('../../server/index.js').baseShutro, 'res.users'>, F.Methods<typeof import('../module.js').portalShutro, 'res.users'> {
346
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Telling customers about new documents (RFC 0015 §5.7): when a record enters a declared document's `announce`
3
+ * domain (a quotation sent, an invoice posted), the portal users who can see it get an email with a link to it in
4
+ * the portal — once per record and document type (`portal.announcement`). It reads Shutro's `record.changed`
5
+ * events, so the models must be in a module's `changeEvents` (the bridges list theirs).
6
+ */
7
+ import { type Mailer } from '../mail/index.js';
8
+ import { type ModuleContext } from '../server/index.js';
9
+ import type { Worker } from '../server/orm.js';
10
+ /** The topic of Shutro's change events. */
11
+ export declare const CHANGE_TOPIC = "record.changed";
12
+ /** The subscriber that announces documents; `start()` it (the module does), tests `drain()` it. */
13
+ export declare function portalAnnouncer(context: ModuleContext, options: {
14
+ readonly portalUrl: string;
15
+ readonly mailer?: Mailer | undefined;
16
+ }): Worker;
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Telling customers about new documents (RFC 0015 §5.7): when a record enters a declared document's `announce`
3
+ * domain (a quotation sent, an invoice posted), the portal users who can see it get an email with a link to it in
4
+ * the portal — once per record and document type (`portal.announcement`). It reads Shutro's `record.changed`
5
+ * events, so the models must be in a module's `changeEvents` (the bridges list theirs).
6
+ */
7
+ import { layout } from '../mail/index.js';
8
+ import { publicApiModels } from '../server/index.js';
9
+ import { portalDocuments } from './documents.js';
10
+ const ref = (env, name) => env.model(name);
11
+ /** The topic of Shutro's change events. */
12
+ export const CHANGE_TOPIC = 'record.changed';
13
+ const TEXTS = {
14
+ en: {
15
+ intro: (label, name) => `A new document is ready for you: ${label} ${name}.`,
16
+ open: 'Open it in the portal',
17
+ },
18
+ bn: {
19
+ intro: (label, name) => `আপনার জন্য একটি নতুন নথি প্রস্তুত: ${label} ${name}।`,
20
+ open: 'পোর্টালে খুলুন',
21
+ },
22
+ };
23
+ /** The contact a record is for (following a path such as `project.partner`), or null. */
24
+ async function partnerOf(su, models, model, id, path) {
25
+ let currentModel = model;
26
+ let currentId = id;
27
+ for (const segment of path.split('.')) {
28
+ const loaded = await ref(su, currentModel).browse([currentId]).load([segment]);
29
+ const rows = loaded.read([segment]);
30
+ const next = rows[0]?.[segment];
31
+ const target = models.get(currentModel)?.byKey.get(segment)?.target ?? null;
32
+ if (typeof next !== 'number' || target === null)
33
+ return null;
34
+ currentModel = target;
35
+ currentId = next;
36
+ }
37
+ return currentId;
38
+ }
39
+ /** The active portal users who see documents of `partner`: theirs, their company's, their colleagues'. */
40
+ async function recipientsOf(su, partner) {
41
+ const [row] = (await ref(su, 'res.partner').browse([partner]).load(['parent'])).read(['parent']);
42
+ const company = typeof row?.['parent'] === 'number' ? row['parent'] : null;
43
+ const heads = company === null ? [partner] : [partner, company];
44
+ const people = await ref(su, 'res.partner').search({ where: { parent: { in: heads } } });
45
+ const partners = [...new Set([...heads, ...people.ids])];
46
+ const users = await ref(su, 'res.users').search({
47
+ where: { portalPartner: { in: partners }, active: true },
48
+ });
49
+ return (await users.load(['login', 'name', 'lang'])).read(['login', 'name', 'lang']);
50
+ }
51
+ /** The subscriber that announces documents; `start()` it (the module does), tests `drain()` it. */
52
+ export function portalAnnouncer(context, options) {
53
+ const models = publicApiModels(context.registry);
54
+ const announced = [...portalDocuments(context.registry).values()].filter((d) => d.spec.announce !== undefined);
55
+ return context.shutro.subscribe('portal.announce', CHANGE_TOPIC, async (event, env) => {
56
+ const payload = (event.payload ?? {});
57
+ const model = String(payload['model'] ?? '');
58
+ const ids = Array.isArray(payload['ids'])
59
+ ? payload['ids'].filter((i) => typeof i === 'number')
60
+ : [];
61
+ const documents = announced.filter((d) => d.model.name === model);
62
+ if (documents.length === 0 || ids.length === 0)
63
+ return;
64
+ await env.asSuperuser({ reason: 'portal announcement' }, async (su) => {
65
+ for (const d of documents) {
66
+ const found = await ref(su, model).search({
67
+ where: { and: [{ id: { in: ids } }, d.spec.domain ?? {}, d.spec.announce] },
68
+ });
69
+ for (const id of found.ids) {
70
+ const already = await ref(su, 'portal.announcement').search({
71
+ where: { resModel: model, resId: id, document: d.spec.key },
72
+ limit: 1,
73
+ });
74
+ if (already.length > 0)
75
+ continue;
76
+ const partner = await partnerOf(su, models, model, id, d.spec.partner);
77
+ const users = partner === null ? [] : await recipientsOf(su, partner);
78
+ await ref(su, 'portal.announcement').create([
79
+ { resModel: model, resId: id, document: d.spec.key, recipients: users.length },
80
+ ]);
81
+ const mailer = options.mailer;
82
+ if (mailer === undefined || users.length === 0)
83
+ continue;
84
+ const [record] = (await ref(su, model).browse([id]).load(['displayName'])).read([
85
+ 'displayName',
86
+ ]);
87
+ const [company] = (await ref(su, 'res.company').browse([env.companyId]).load(['name'])).read(['name']);
88
+ const name = String(record?.['displayName'] ?? `#${id}`);
89
+ const from = String(company?.['name'] ?? '');
90
+ const url = `${options.portalUrl.replace(/\/$/, '')}/d/${d.spec.key}/${id}`;
91
+ const mails = users.map((u) => {
92
+ const texts = String(u['lang'] ?? '').startsWith('bn') ? TEXTS.bn : TEXTS.en;
93
+ const subject = `${from}: ${d.spec.label} ${name}`;
94
+ return {
95
+ to: String(u['login']),
96
+ subject,
97
+ ...layout({
98
+ title: subject,
99
+ paragraphs: [texts.intro(d.spec.label, name)],
100
+ action: { label: texts.open, url },
101
+ lang: String(u['lang'] ?? 'en').slice(0, 2),
102
+ }),
103
+ };
104
+ });
105
+ // Sent once the announcement is committed; a failed send is logged, not retried.
106
+ env.afterCommit(async () => {
107
+ for (const mail of mails)
108
+ await mailer
109
+ .send(mail)
110
+ .catch((error) => context.app.log.error({ err: error }, 'portal announcement not sent'));
111
+ });
112
+ }
113
+ }
114
+ await su.flush();
115
+ });
116
+ }, { pollMs: 5000 });
117
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The portal's catalogue (RFC 0015 §5.3): the documents the installed modules declare, checked when the server
3
+ * starts. Every declared model, field, line field, transition and template must exist, and the model must give
4
+ * `portal.user` read access with a record rule, so a declaration can never show a document without the rule that
5
+ * keeps it to its customer.
6
+ */
7
+ import type { PortalDocumentSpec } from '../modules-sdk/index.js';
8
+ import { type ApiModel, type KoshRegistry } from '../server/index.js';
9
+ /** A declared document with the models it reads. */
10
+ export interface PortalDocument {
11
+ readonly spec: PortalDocumentSpec;
12
+ readonly model: ApiModel;
13
+ /** The lines' model, when the document has lines. */
14
+ readonly lineModel: ApiModel | null;
15
+ }
16
+ /** The documents of the loaded modules, by key. @throws {ServerConfigError} for a declaration that cannot work */
17
+ export declare function portalDocuments(registry: KoshRegistry): ReadonlyMap<string, PortalDocument>;
@@ -0,0 +1,61 @@
1
+ import { publicApiModels, ServerConfigError } from '../server/index.js';
2
+ const fail = (key, problem, hint) => {
3
+ throw new ServerConfigError(`Portal document '${key}': ${problem}`, hint);
4
+ };
5
+ /** The documents of the loaded modules, by key. @throws {ServerConfigError} for a declaration that cannot work */
6
+ export function portalDocuments(registry) {
7
+ const models = publicApiModels(registry);
8
+ const out = new Map();
9
+ const templates = new Set(registry.loaded.modules.flatMap((m) => (m.documents ?? []).map((d) => `${m.name}.${d.key}`)));
10
+ for (const module of registry.loaded.modules)
11
+ for (const spec of module.portal?.documents ?? []) {
12
+ const key = spec.key;
13
+ if (out.has(key))
14
+ fail(key, `declared twice (again by ${module.name}).`, 'Document keys are unique.');
15
+ const model = models.get(spec.model);
16
+ if (model === undefined)
17
+ fail(key, `no served model '${spec.model}'.`, 'Declare a model of an installed module.');
18
+ const m = model;
19
+ const definition = registry.registry.get(spec.model).model;
20
+ const access = definition.access['portal.user'];
21
+ const ruled = Object.values(definition.rules).some((r) => r.groups !== 'global' && r.groups.includes('portal.user') && r.perms.includes('read'));
22
+ if (access?.includes('read') !== true || !ruled)
23
+ fail(key, `${spec.model} gives portal.user no read access with a record rule.`, "Extend the model in the bridge: access { 'portal.user': ['read'] } and rules { portal: portalRule('partner') }.");
24
+ // The customer: a many2one to res.partner, directly or through many2ones (`project.partner`).
25
+ let hop = m;
26
+ let target = null;
27
+ for (const segment of spec.partner.split('.')) {
28
+ const field = hop?.byKey.get(segment);
29
+ if (field?.kind !== 'many2one' || field.target === null) {
30
+ target = null;
31
+ break;
32
+ }
33
+ target = field.target;
34
+ hop = models.get(field.target);
35
+ }
36
+ if (target !== 'res.partner')
37
+ fail(key, `'${spec.partner}' does not lead to res.partner.`, 'Name the customer field.');
38
+ for (const field of [...spec.list, ...spec.fields])
39
+ if (!m.byKey.has(field))
40
+ fail(key, `${spec.model} has no field '${field}'.`, 'Declare existing fields.');
41
+ let lineModel = null;
42
+ if (spec.lines !== undefined) {
43
+ const lines = m.byKey.get(spec.lines.field);
44
+ if (lines?.kind !== 'one2many' || lines.target === null)
45
+ fail(key, `'${spec.lines.field}' is not a one2many.`, 'Name the lines field.');
46
+ lineModel = models.get(lines?.target ?? '') ?? null;
47
+ if (lineModel === null)
48
+ fail(key, `the lines' model ${lines?.target} is not served.`, 'Use a served model.');
49
+ for (const field of spec.lines.fields)
50
+ if (!lineModel.byKey.has(field))
51
+ fail(key, `${lines?.target} has no field '${field}'.`, 'Declare existing line fields.');
52
+ }
53
+ for (const action of spec.actions ?? [])
54
+ if (!m.transitions.includes(action.transition))
55
+ fail(key, `${spec.model} has no transition '${action.transition}' (action '${action.key}').`, `Transitions: ${m.transitions.join(', ') || 'none'}.`);
56
+ if (spec.pdf !== undefined && !templates.has(spec.pdf))
57
+ fail(key, `no document template '${spec.pdf}'.`, 'Name a template a module ships (module.key).');
58
+ out.set(key, { spec, model: m, lineModel });
59
+ }
60
+ return out;
61
+ }
@@ -0,0 +1,20 @@
1
+ import { type KoshModule } from '../modules-sdk/index.js';
2
+ import { type PortalRoutesOptions } from './routes.js';
3
+ export { CHANGE_TOPIC, portalAnnouncer } from './announce.js';
4
+ export { type PortalDocument, portalDocuments } from './documents.js';
5
+ export { PartnerPortalUsers, PortalInvitation, PortalSignature, portalShutro, UserPortalPartner, } from './module.js';
6
+ export { INVITATION_DAYS, PORTAL_USER, type PortalRoutesOptions, registerPortalRoutes, } from './routes.js';
7
+ export { registerPortalDocumentRoutes } from './routes-documents.js';
8
+ export { portalRule } from './rules.js';
9
+ /** Package identity. */
10
+ export declare const PACKAGE: {
11
+ readonly name: string;
12
+ readonly rfc: string;
13
+ };
14
+ /** The portal module with explicit options (tests): its URL and mailer. */
15
+ export declare function createPortal(options?: Partial<PortalRoutesOptions> & {
16
+ /** Announce new documents in this process (default: unless `KOSH_SCHEDULES=off`). */
17
+ readonly workers?: boolean;
18
+ }): KoshModule;
19
+ /** The portal, configured from the environment (`KOSH_PORTAL_URL`, `SMTP_URL`). */
20
+ export declare const portal: KoshModule;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * @packageDocumentation
3
+ * `shutro-erp/portal` (RFC 0015): the customer and vendor portal. Step 1 is who a portal user is — a user of one contact,
4
+ * holding only `portal.user`, kept to the portal's API by the server — and how they get in: an invitation from a
5
+ * portal access manager, a one-time link to choose a password, and revocation.
6
+ */
7
+ import { contacts } from '../contacts/index.js';
8
+ import { mailerFromEnv } from '../mail/index.js';
9
+ import { messaging } from '../messaging/index.js';
10
+ import { defineModule } from '../modules-sdk/index.js';
11
+ import { reporting } from '../reporting/index.js';
12
+ import { base } from '../server/index.js';
13
+ import { portalAnnouncer } from './announce.js';
14
+ import { portalShutro } from './module.js';
15
+ import { registerPortalRoutes } from './routes.js';
16
+ import { registerPortalDocumentRoutes } from './routes-documents.js';
17
+ export { CHANGE_TOPIC, portalAnnouncer } from './announce.js';
18
+ export { portalDocuments } from './documents.js';
19
+ export { PartnerPortalUsers, PortalInvitation, PortalSignature, portalShutro, UserPortalPartner, } from './module.js';
20
+ export { INVITATION_DAYS, PORTAL_USER, registerPortalRoutes, } from './routes.js';
21
+ export { registerPortalDocumentRoutes } from './routes-documents.js';
22
+ export { portalRule } from './rules.js';
23
+ /** Package identity. */
24
+ export const PACKAGE = Object.freeze({
25
+ name: 'shutro-erp/portal',
26
+ rfc: 'RFC 0015',
27
+ });
28
+ /** The portal module with explicit options (tests): its URL and mailer. */
29
+ export function createPortal(options = {}) {
30
+ return defineModule({
31
+ shutro: portalShutro,
32
+ kosh: '^0.1.0',
33
+ depends: [base, contacts, messaging, reporting],
34
+ accessLevels: { manager: { label: 'Portal access', groups: ['portal.manager'] } },
35
+ externalGroups: ['portal.user'],
36
+ privateModels: ['portal.invitation', 'portal.signature'],
37
+ // "Portal access" on the contact form, under the ways to reach them.
38
+ viewExtensions: [
39
+ {
40
+ view: 'res.partner.form',
41
+ operations: [
42
+ {
43
+ insertAfter: 'reach',
44
+ nodes: [{ kind: 'component', id: 'portal_access', component: 'portal.Access' }],
45
+ },
46
+ ],
47
+ },
48
+ ],
49
+ routes: (context) => {
50
+ const delivery = {
51
+ portalUrl: options.portalUrl ?? process.env['KOSH_PORTAL_URL'] ?? 'http://localhost:5174',
52
+ mailer: 'mailer' in options ? options.mailer : mailerFromEnv(),
53
+ };
54
+ registerPortalRoutes(context, delivery);
55
+ registerPortalDocumentRoutes(context);
56
+ if (!(options.workers ?? process.env['KOSH_SCHEDULES'] !== 'off'))
57
+ return;
58
+ const announcer = portalAnnouncer(context, delivery);
59
+ announcer.start();
60
+ context.app.addHook('onClose', () => announcer.stop());
61
+ },
62
+ });
63
+ }
64
+ /** The portal, configured from the environment (`KOSH_PORTAL_URL`, `SMTP_URL`). */
65
+ export const portal = createPortal();