shutro-erp 0.3.6 → 0.3.7
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/dist/account_asset/.kosh/bindings.js +1 -1
- package/dist/account_asset/.kosh/schema.d.ts +530 -8
- package/dist/account_asset/i18n/bn.d.ts +2 -0
- package/dist/account_asset/i18n/bn.js +97 -0
- package/dist/account_asset/index.js +2 -0
- package/dist/account_asset/module.d.ts +4899 -1445
- package/dist/account_budget/.kosh/bindings.js +1 -1
- package/dist/account_budget/.kosh/schema.d.ts +1130 -56
- package/dist/account_budget/i18n/bn.d.ts +2 -0
- package/dist/account_budget/i18n/bn.js +63 -0
- package/dist/account_budget/index.js +14 -0
- package/dist/account_budget/module.d.ts +17958 -4532
- package/dist/account_budget/module.js +15 -0
- package/dist/account_portal/.kosh/bindings.js +1 -1
- package/dist/account_portal/.kosh/schema.d.ts +530 -8
- package/dist/account_portal/i18n/bn.d.ts +2 -0
- package/dist/account_portal/i18n/bn.js +6 -0
- package/dist/account_portal/index.js +2 -0
- package/dist/account_portal/module.d.ts +5167 -1693
- package/dist/accounting/.kosh/bindings.js +1 -1
- package/dist/accounting/.kosh/schema.d.ts +530 -8
- package/dist/accounting/closing.js +1 -0
- package/dist/accounting/fiscal.d.ts +578 -0
- package/dist/accounting/fiscal.js +401 -0
- package/dist/accounting/followups.d.ts +618 -0
- package/dist/accounting/followups.js +420 -0
- package/dist/accounting/i18n/bn.d.ts +2 -0
- package/dist/accounting/i18n/bn.js +425 -0
- package/dist/accounting/index.d.ts +2 -0
- package/dist/accounting/index.js +649 -2
- package/dist/accounting/module.d.ts +4998 -1451
- package/dist/accounting/module.js +124 -18
- package/dist/accounting/statements.d.ts +878 -0
- package/dist/accounting/statements.js +728 -0
- package/dist/accounting/web/BankReconcilePage.d.ts +9 -0
- package/dist/accounting/web/BankReconcilePage.js +138 -0
- package/dist/accounting/web/FollowupsPage.d.ts +8 -0
- package/dist/accounting/web/FollowupsPage.js +54 -0
- package/dist/accounting/web/PostingPreview.d.ts +8 -0
- package/dist/accounting/web/PostingPreview.js +26 -0
- package/dist/accounting/web/index.d.ts +3 -0
- package/dist/accounting/web/index.js +9 -0
- package/dist/accounting/web/messages.js +140 -0
- package/dist/approvals/.kosh/bindings.js +1 -1
- package/dist/approvals/.kosh/schema.d.ts +272 -4
- package/dist/approvals/demo.d.ts +7 -0
- package/dist/approvals/demo.js +41 -0
- package/dist/approvals/i18n/bn.d.ts +2 -0
- package/dist/approvals/i18n/bn.js +133 -0
- package/dist/approvals/index.d.ts +19 -1
- package/dist/approvals/index.js +477 -7
- package/dist/approvals/mails.d.ts +7 -2
- package/dist/approvals/mails.js +8 -3
- package/dist/approvals/module.d.ts +2930 -1
- package/dist/approvals/module.js +468 -7
- package/dist/approvals/web/Config.d.ts +12 -0
- package/dist/approvals/web/Config.js +60 -0
- package/dist/approvals/web/Matrix.d.ts +9 -0
- package/dist/approvals/web/Matrix.js +26 -0
- package/dist/approvals/web/index.d.ts +3 -1
- package/dist/approvals/web/index.js +8 -1
- package/dist/approvals/web/messages.js +92 -0
- package/dist/approvals_purchase/.kosh/bindings.js +15 -0
- package/dist/approvals_purchase/.kosh/schema.d.ts +1997 -0
- package/dist/approvals_purchase/i18n/bn.d.ts +2 -0
- package/dist/approvals_purchase/i18n/bn.js +5 -0
- package/dist/approvals_purchase/index.d.ts +3 -0
- package/dist/approvals_purchase/index.js +47 -0
- package/dist/approvals_purchase/module.d.ts +12670 -0
- package/dist/approvals_purchase/module.js +84 -0
- package/dist/attendance/.kosh/bindings.js +1 -1
- package/dist/attendance/.kosh/schema.d.ts +59 -1
- package/dist/attendance/i18n/bn.d.ts +2 -0
- package/dist/attendance/i18n/bn.js +147 -0
- package/dist/attendance/index.js +3 -1
- package/dist/attendance/module.d.ts +452 -8
- package/dist/automation/engine.js +1 -1
- package/dist/calendar/.kosh/bindings.js +1 -1
- package/dist/calendar/.kosh/schema.d.ts +2 -1
- package/dist/calendar/i18n/bn.d.ts +2 -0
- package/dist/calendar/i18n/bn.js +80 -0
- package/dist/calendar/index.js +77 -0
- package/dist/calendar/module.d.ts +26 -0
- package/dist/calendar/module.js +14 -4
- package/dist/cli/apps.js +25 -5
- package/dist/cli/init.js +14 -4
- package/dist/cli/templates.d.ts +1 -1
- package/dist/cli/templates.js +34 -5
- package/dist/contacts/.kosh/bindings.js +1 -1
- package/dist/contacts/.kosh/schema.d.ts +2 -1
- package/dist/contacts/i18n/bn.d.ts +2 -0
- package/dist/contacts/i18n/bn.js +67 -0
- package/dist/contacts/index.js +3 -0
- package/dist/contacts/module.d.ts +42 -0
- package/dist/contacts/module.js +6 -1
- package/dist/crm/.kosh/bindings.js +1 -1
- package/dist/crm/.kosh/schema.d.ts +2 -1
- package/dist/crm/i18n/bn.d.ts +2 -0
- package/dist/crm/i18n/bn.js +80 -0
- package/dist/crm/index.js +22 -0
- package/dist/crm/module.d.ts +20 -0
- package/dist/crm/module.js +4 -2
- package/dist/documents/.kosh/bindings.js +1 -1
- package/dist/documents/.kosh/schema.d.ts +2 -1
- package/dist/documents/i18n/bn.d.ts +2 -0
- package/dist/documents/i18n/bn.js +83 -0
- package/dist/documents/index.js +128 -1
- package/dist/documents/module.d.ts +20 -0
- package/dist/documents/module.js +1 -0
- package/dist/event/.kosh/bindings.js +1 -1
- package/dist/event/.kosh/schema.d.ts +2 -1
- package/dist/event/i18n/bn.d.ts +2 -0
- package/dist/event/i18n/bn.js +101 -0
- package/dist/event/index.js +66 -0
- package/dist/event/module.d.ts +20 -0
- package/dist/event/module.js +8 -1
- package/dist/event_account/.kosh/bindings.js +1 -1
- package/dist/event_account/.kosh/schema.d.ts +530 -8
- package/dist/event_account/i18n/bn.d.ts +2 -0
- package/dist/event_account/i18n/bn.js +5 -0
- package/dist/event_account/index.js +2 -0
- package/dist/event_account/module.d.ts +4905 -1431
- package/dist/expense/.kosh/bindings.js +1 -1
- package/dist/expense/.kosh/schema.d.ts +323 -2
- package/dist/expense/i18n/bn.d.ts +2 -0
- package/dist/expense/i18n/bn.js +155 -0
- package/dist/expense/index.js +8 -1
- package/dist/expense/module.d.ts +2715 -694
- package/dist/expense/module.js +3 -1
- package/dist/expense_account/.kosh/bindings.js +1 -1
- package/dist/expense_account/.kosh/schema.d.ts +587 -8
- package/dist/expense_account/i18n/bn.d.ts +2 -0
- package/dist/expense_account/i18n/bn.js +15 -0
- package/dist/expense_account/index.js +2 -0
- package/dist/expense_account/module.d.ts +8094 -2619
- package/dist/helpdesk/.kosh/bindings.js +1 -1
- package/dist/helpdesk/.kosh/schema.d.ts +2 -1
- package/dist/helpdesk/i18n/bn.d.ts +2 -0
- package/dist/helpdesk/i18n/bn.js +127 -0
- package/dist/helpdesk/index.js +2 -0
- package/dist/helpdesk/module.d.ts +20 -0
- package/dist/hr/.kosh/bindings.js +1 -1
- package/dist/hr/.kosh/schema.d.ts +59 -1
- package/dist/hr/demo.js +11 -1
- package/dist/hr/i18n/bn.d.ts +2 -0
- package/dist/hr/i18n/bn.js +126 -0
- package/dist/hr/index.d.ts +1 -1
- package/dist/hr/index.js +158 -12
- package/dist/hr/module.d.ts +932 -59
- package/dist/hr/module.js +114 -17
- package/dist/{mrp → indent}/.kosh/bindings.js +2 -2
- package/dist/indent/.kosh/schema.d.ts +2084 -0
- package/dist/indent/demo.d.ts +7 -0
- package/dist/indent/demo.js +48 -0
- package/dist/indent/i18n/bn.d.ts +2 -0
- package/dist/indent/i18n/bn.js +143 -0
- package/dist/indent/index.d.ts +3 -0
- package/dist/indent/index.js +450 -0
- package/dist/{mrp → indent}/module.d.ts +7164 -3075
- package/dist/indent/module.js +792 -0
- package/dist/indent_purchase/.kosh/bindings.d.ts +22 -0
- package/dist/indent_purchase/.kosh/bindings.js +15 -0
- package/dist/indent_purchase/.kosh/schema.d.ts +2710 -0
- package/dist/indent_purchase/i18n/bn.d.ts +2 -0
- package/dist/indent_purchase/i18n/bn.js +13 -0
- package/dist/indent_purchase/index.d.ts +3 -0
- package/dist/indent_purchase/index.js +61 -0
- package/dist/indent_purchase/module.d.ts +27950 -0
- package/dist/indent_purchase/module.js +163 -0
- package/dist/l10n_bd/.kosh/bindings.js +1 -1
- package/dist/l10n_bd/.kosh/schema.d.ts +530 -8
- package/dist/l10n_bd/i18n/bn.d.ts +2 -0
- package/dist/l10n_bd/i18n/bn.js +69 -0
- package/dist/l10n_bd/index.js +2 -0
- package/dist/l10n_bd/module.d.ts +5167 -1693
- package/dist/l10n_bd_payroll/.kosh/bindings.js +1 -1
- package/dist/l10n_bd_payroll/.kosh/schema.d.ts +587 -8
- package/dist/l10n_bd_payroll/i18n/bn.d.ts +2 -0
- package/dist/l10n_bd_payroll/i18n/bn.js +4 -0
- package/dist/l10n_bd_payroll/index.js +2 -0
- package/dist/l10n_bd_payroll/module.d.ts +5910 -1548
- package/dist/l10n_bd_pos/.kosh/bindings.d.ts +22 -0
- package/dist/{mrp_account → l10n_bd_pos}/.kosh/bindings.js +2 -2
- package/dist/{mrp_account → l10n_bd_pos}/.kosh/schema.d.ts +693 -651
- package/dist/l10n_bd_pos/i18n/bn.d.ts +2 -0
- package/dist/l10n_bd_pos/i18n/bn.js +32 -0
- package/dist/l10n_bd_pos/index.d.ts +3 -0
- package/dist/l10n_bd_pos/index.js +111 -0
- package/dist/{mrp_account → l10n_bd_pos}/module.d.ts +7402 -7647
- package/dist/l10n_bd_pos/module.js +43 -0
- package/dist/l10n_in/.kosh/bindings.js +1 -1
- package/dist/l10n_in/.kosh/schema.d.ts +530 -8
- package/dist/l10n_in/i18n/bn.d.ts +2 -0
- package/dist/l10n_in/i18n/bn.js +95 -0
- package/dist/l10n_in/index.js +2 -0
- package/dist/l10n_in/module.d.ts +6703 -1652
- package/dist/l10n_in/module.js +25 -5
- package/dist/l10n_in/web/GstSettings.js +2 -2
- package/dist/mail/inbound/handle.js +2 -4
- package/dist/mail/inbound/models.js +5 -3
- package/dist/mail/index.d.ts +12 -1
- package/dist/mail/index.js +139 -2
- package/dist/mail/send.d.ts +56 -0
- package/dist/mail/send.js +394 -0
- package/dist/mail/web/SendButton.d.ts +9 -0
- package/dist/mail/web/SendButton.js +71 -0
- package/dist/mail/web/SendSelected.d.ts +8 -0
- package/dist/mail/web/SendSelected.js +33 -0
- package/dist/mail/web/index.d.ts +2 -0
- package/dist/mail/web/index.js +6 -0
- package/dist/mail/web/messages.js +36 -0
- package/dist/maintenance/.kosh/bindings.js +1 -1
- package/dist/maintenance/.kosh/schema.d.ts +2 -1
- package/dist/maintenance/i18n/bn.d.ts +2 -0
- package/dist/maintenance/i18n/bn.js +132 -0
- package/dist/maintenance/index.js +85 -0
- package/dist/maintenance/module.d.ts +25 -0
- package/dist/maintenance/module.js +27 -9
- package/dist/maintenance_stock/.kosh/bindings.js +1 -1
- package/dist/maintenance_stock/.kosh/schema.d.ts +298 -2
- package/dist/maintenance_stock/i18n/bn.d.ts +2 -0
- package/dist/maintenance_stock/i18n/bn.js +21 -0
- package/dist/maintenance_stock/index.js +2 -0
- package/dist/maintenance_stock/module.d.ts +5302 -1256
- package/dist/maintenance_stock/module.js +1 -0
- package/dist/modules-sdk/apps.d.ts +10 -0
- package/dist/modules-sdk/apps.js +32 -0
- package/dist/modules-sdk/index.d.ts +1 -1
- package/dist/modules-sdk/manifest.d.ts +68 -2
- package/dist/payment/.kosh/bindings.js +1 -1
- package/dist/payment/.kosh/schema.d.ts +530 -8
- package/dist/payment/i18n/bn.d.ts +2 -0
- package/dist/payment/i18n/bn.js +59 -0
- package/dist/payment/index.js +85 -0
- package/dist/payment/module.d.ts +5134 -1660
- package/dist/payment/module.js +17 -3
- package/dist/payment/settle.js +1 -0
- package/dist/payroll/.kosh/bindings.js +1 -1
- package/dist/payroll/.kosh/schema.d.ts +59 -1
- package/dist/payroll/i18n/bn.d.ts +2 -0
- package/dist/payroll/i18n/bn.js +206 -0
- package/dist/payroll/index.js +2 -0
- package/dist/payroll/module.d.ts +936 -48
- package/dist/payroll_account/.kosh/bindings.js +1 -1
- package/dist/payroll_account/.kosh/schema.d.ts +587 -8
- package/dist/payroll_account/i18n/bn.d.ts +2 -0
- package/dist/payroll_account/i18n/bn.js +10 -0
- package/dist/payroll_account/index.js +2 -0
- package/dist/payroll_account/module.d.ts +7534 -3192
- package/dist/permissions/approvals.d.ts +97 -4
- package/dist/permissions/approvals.js +780 -31
- package/dist/permissions/index.d.ts +1 -1
- package/dist/permissions/index.js +2 -2
- package/dist/permissions/models.js +177 -5
- package/dist/permissions/routes.d.ts +3 -0
- package/dist/permissions/routes.js +36 -2
- package/dist/permissions/web/ApprovalPanel.js +43 -5
- package/dist/permissions/web/messages.js +52 -0
- package/dist/portal/.kosh/bindings.js +1 -1
- package/dist/portal/.kosh/schema.d.ts +2 -1
- package/dist/portal/documents.d.ts +3 -0
- package/dist/portal/documents.js +21 -1
- package/dist/portal/i18n/bn.d.ts +2 -0
- package/dist/portal/i18n/bn.js +33 -0
- package/dist/portal/index.js +2 -0
- package/dist/portal/module.d.ts +20 -0
- package/dist/portal/routes-documents.js +76 -0
- package/dist/pos/.kosh/bindings.js +1 -1
- package/dist/pos/.kosh/schema.d.ts +531 -8
- package/dist/pos/i18n/bn.d.ts +2 -0
- package/dist/pos/i18n/bn.js +133 -0
- package/dist/pos/index.js +83 -1
- package/dist/pos/module.d.ts +6888 -1777
- package/dist/pos/module.js +49 -5
- package/dist/pos/routes.js +80 -7
- package/dist/pos/web/Register.js +2 -5
- package/dist/pos_stock/.kosh/bindings.js +1 -1
- package/dist/pos_stock/.kosh/schema.d.ts +626 -9
- package/dist/pos_stock/i18n/bn.d.ts +2 -0
- package/dist/pos_stock/i18n/bn.js +6 -0
- package/dist/pos_stock/index.js +2 -0
- package/dist/pos_stock/module.d.ts +10798 -3276
- package/dist/product/.kosh/bindings.js +1 -1
- package/dist/product/.kosh/schema.d.ts +202 -1
- package/dist/product/i18n/bn.d.ts +2 -0
- package/dist/product/i18n/bn.js +175 -0
- package/dist/product/index.d.ts +1 -0
- package/dist/product/index.js +288 -1
- package/dist/product/module.d.ts +1725 -0
- package/dist/product/module.js +66 -6
- package/dist/product/pricelist.d.ts +575 -0
- package/dist/product/pricelist.js +257 -0
- package/dist/product/variants.d.ts +908 -0
- package/dist/product/variants.js +251 -0
- package/dist/production/.kosh/bindings.d.ts +22 -0
- package/dist/production/.kosh/bindings.js +15 -0
- package/dist/production/.kosh/schema.d.ts +2345 -0
- package/dist/{mrp → production}/demo.d.ts +1 -1
- package/dist/{mrp → production}/demo.js +5 -5
- package/dist/production/depth.d.ts +790 -0
- package/dist/production/depth.js +262 -0
- package/dist/production/i18n/bn.d.ts +2 -0
- package/dist/production/i18n/bn.js +350 -0
- package/dist/production/index.d.ts +6 -0
- package/dist/production/index.js +1256 -0
- package/dist/production/module.d.ts +17327 -0
- package/dist/production/module.js +1333 -0
- package/dist/production/plan.d.ts +921 -0
- package/dist/production/plan.js +511 -0
- package/dist/production/schedule.d.ts +656 -0
- package/dist/production/schedule.js +309 -0
- package/dist/production_account/.kosh/bindings.d.ts +22 -0
- package/dist/production_account/.kosh/bindings.js +15 -0
- package/dist/production_account/.kosh/schema.d.ts +3496 -0
- package/dist/production_account/i18n/bn.d.ts +2 -0
- package/dist/production_account/i18n/bn.js +93 -0
- package/dist/production_account/index.d.ts +4 -0
- package/dist/production_account/index.js +246 -0
- package/dist/production_account/module.d.ts +1192 -0
- package/dist/{mrp_account → production_account}/module.js +72 -18
- package/dist/production_account/overhead.d.ts +1195 -0
- package/dist/production_account/overhead.js +554 -0
- package/dist/project/.kosh/bindings.js +1 -1
- package/dist/project/.kosh/schema.d.ts +65 -2
- package/dist/project/i18n/bn.d.ts +2 -0
- package/dist/project/i18n/bn.js +383 -0
- package/dist/project/index.js +123 -0
- package/dist/project/module.d.ts +20 -0
- package/dist/project/module.js +73 -14
- package/dist/project_account/.kosh/bindings.js +1 -1
- package/dist/project_account/.kosh/schema.d.ts +530 -8
- package/dist/project_account/i18n/bn.d.ts +2 -0
- package/dist/project_account/i18n/bn.js +6 -0
- package/dist/project_account/index.js +2 -0
- package/dist/project_account/module.d.ts +4926 -1452
- package/dist/project_portal/.kosh/bindings.js +1 -1
- package/dist/project_portal/.kosh/schema.d.ts +65 -2
- package/dist/project_portal/i18n/bn.d.ts +2 -0
- package/dist/project_portal/i18n/bn.js +6 -0
- package/dist/project_portal/index.js +2 -0
- package/dist/project_portal/module.d.ts +40 -0
- package/dist/project_purchase/.kosh/bindings.js +1 -1
- package/dist/project_purchase/.kosh/schema.d.ts +1126 -53
- package/dist/project_purchase/i18n/bn.d.ts +2 -0
- package/dist/project_purchase/i18n/bn.js +4 -0
- package/dist/project_purchase/index.js +2 -0
- package/dist/project_purchase/module.d.ts +17912 -4446
- package/dist/purchase/.kosh/bindings.js +1 -1
- package/dist/purchase/.kosh/schema.d.ts +813 -3
- package/dist/purchase/comparison.d.ts +993 -0
- package/dist/purchase/comparison.js +415 -0
- package/dist/purchase/extras.d.ts +1509 -0
- package/dist/purchase/extras.js +445 -0
- package/dist/purchase/i18n/bn.d.ts +2 -0
- package/dist/purchase/i18n/bn.js +413 -0
- package/dist/purchase/index.d.ts +5 -0
- package/dist/purchase/index.js +1280 -4
- package/dist/purchase/module.d.ts +7139 -398
- package/dist/purchase/module.js +295 -11
- package/dist/purchase/request.d.ts +696 -0
- package/dist/purchase/request.js +446 -0
- package/dist/purchase/tender.d.ts +1208 -0
- package/dist/purchase/tender.js +507 -0
- package/dist/purchase/vendorprice.d.ts +340 -0
- package/dist/purchase/vendorprice.js +153 -0
- package/dist/purchase_account/.kosh/bindings.js +1 -1
- package/dist/purchase_account/.kosh/schema.d.ts +1130 -57
- package/dist/purchase_account/i18n/bn.d.ts +2 -0
- package/dist/purchase_account/i18n/bn.js +22 -0
- package/dist/purchase_account/index.js +19 -0
- package/dist/purchase_account/module.d.ts +12276 -2247
- package/dist/purchase_account/module.js +241 -9
- package/dist/purchase_portal/.kosh/bindings.js +1 -1
- package/dist/purchase_portal/.kosh/schema.d.ts +813 -3
- package/dist/purchase_portal/i18n/bn.d.ts +2 -0
- package/dist/purchase_portal/i18n/bn.js +5 -0
- package/dist/purchase_portal/index.js +2 -0
- package/dist/purchase_portal/module.d.ts +6849 -353
- package/dist/purchase_stock/.kosh/bindings.js +1 -1
- package/dist/purchase_stock/.kosh/schema.d.ts +910 -4
- package/dist/purchase_stock/i18n/bn.d.ts +2 -0
- package/dist/purchase_stock/i18n/bn.js +19 -0
- package/dist/purchase_stock/index.d.ts +1 -1
- package/dist/purchase_stock/index.js +29 -0
- package/dist/purchase_stock/module.d.ts +12410 -1864
- package/dist/purchase_stock/module.js +81 -9
- package/dist/purchase_stock/scorecard.d.ts +22 -0
- package/dist/purchase_stock/scorecard.js +77 -0
- package/dist/purchase_stock/web/Scorecard.d.ts +7 -0
- package/dist/purchase_stock/web/Scorecard.js +22 -0
- package/dist/purchase_stock/web/index.d.ts +11 -0
- package/dist/purchase_stock/web/index.js +16 -0
- package/dist/purchase_stock/web/messages.d.ts +2 -0
- package/dist/purchase_stock/web/messages.js +27 -0
- package/dist/purchase_stock_account/.kosh/bindings.js +1 -1
- package/dist/purchase_stock_account/.kosh/schema.d.ts +1286 -58
- package/dist/purchase_stock_account/i18n/bn.d.ts +2 -0
- package/dist/purchase_stock_account/i18n/bn.js +8 -0
- package/dist/purchase_stock_account/index.js +18 -0
- package/dist/purchase_stock_account/module.d.ts +7 -37995
- package/dist/purchase_stock_account/module.js +24 -3
- package/dist/recruitment/.kosh/bindings.js +1 -1
- package/dist/recruitment/.kosh/schema.d.ts +59 -1
- package/dist/recruitment/i18n/bn.d.ts +2 -0
- package/dist/recruitment/i18n/bn.js +93 -0
- package/dist/recruitment/index.js +64 -0
- package/dist/recruitment/module.d.ts +458 -8
- package/dist/recruitment/module.js +11 -3
- package/dist/reporting/documents.d.ts +5 -0
- package/dist/reporting/documents.js +11 -1
- package/dist/reporting/index.d.ts +1 -1
- package/dist/reporting/index.js +1 -1
- package/dist/reporting/web/CalendarView.d.ts +18 -5
- package/dist/reporting/web/CalendarView.js +163 -29
- package/dist/reporting/web/messages.js +18 -0
- package/dist/sale_account/.kosh/bindings.js +1 -1
- package/dist/sale_account/.kosh/schema.d.ts +642 -16
- package/dist/sale_account/i18n/bn.d.ts +2 -0
- package/dist/sale_account/i18n/bn.js +48 -0
- package/dist/sale_account/index.d.ts +1 -1
- package/dist/sale_account/index.js +102 -4
- package/dist/sale_account/module.d.ts +8254 -2212
- package/dist/sale_account/module.js +535 -46
- package/dist/sale_crm/.kosh/bindings.d.ts +22 -0
- package/dist/sale_crm/.kosh/bindings.js +15 -0
- package/dist/{mrp → sale_crm}/.kosh/schema.d.ts +389 -567
- package/dist/sale_crm/i18n/bn.d.ts +2 -0
- package/dist/sale_crm/i18n/bn.js +9 -0
- package/dist/sale_crm/index.d.ts +7 -0
- package/dist/sale_crm/index.js +104 -0
- package/dist/sale_crm/module.d.ts +5609 -0
- package/dist/sale_crm/module.js +73 -0
- package/dist/sale_portal/.kosh/bindings.js +1 -1
- package/dist/sale_portal/.kosh/schema.d.ts +291 -4
- package/dist/sale_portal/i18n/bn.d.ts +2 -0
- package/dist/sale_portal/i18n/bn.js +7 -0
- package/dist/sale_portal/index.js +10 -0
- package/dist/sale_portal/module.d.ts +2564 -294
- package/dist/sale_project/.kosh/bindings.js +1 -1
- package/dist/sale_project/.kosh/schema.d.ts +354 -5
- package/dist/sale_project/i18n/bn.d.ts +2 -0
- package/dist/sale_project/i18n/bn.js +12 -0
- package/dist/sale_project/index.js +2 -0
- package/dist/sale_project/module.d.ts +6249 -2402
- package/dist/sale_stock/.kosh/bindings.js +1 -1
- package/dist/sale_stock/.kosh/schema.d.ts +387 -5
- package/dist/sale_stock/i18n/bn.d.ts +2 -0
- package/dist/sale_stock/i18n/bn.js +15 -0
- package/dist/sale_stock/index.js +2 -0
- package/dist/sale_stock/module.d.ts +8057 -1759
- package/dist/sale_stock/module.js +20 -3
- package/dist/sales/.kosh/bindings.js +1 -1
- package/dist/sales/.kosh/schema.d.ts +291 -4
- package/dist/sales/i18n/bn.d.ts +2 -0
- package/dist/sales/i18n/bn.js +128 -0
- package/dist/sales/index.js +120 -5
- package/dist/sales/module.d.ts +3567 -633
- package/dist/sales/module.js +306 -11
- package/dist/server/api/duplicate.d.ts +14 -0
- package/dist/server/api/duplicate.js +75 -0
- package/dist/server/api/explain.d.ts +70 -0
- package/dist/server/api/explain.js +111 -0
- package/dist/server/api/metadata.d.ts +2 -0
- package/dist/server/api/metadata.js +9 -0
- package/dist/server/api/openapi.js +15 -0
- package/dist/server/api/rest.d.ts +1 -0
- package/dist/server/api/rest.js +76 -12
- package/dist/server/api/views.js +1 -1
- package/dist/server/apps.d.ts +16 -1
- package/dist/server/apps.js +164 -2
- package/dist/server/auth/base.js +2 -0
- package/dist/server/auth/service.d.ts +8 -0
- package/dist/server/auth/service.js +19 -0
- package/dist/server/events.d.ts +13 -0
- package/dist/server/events.js +22 -0
- package/dist/server/i18n.d.ts +27 -0
- package/dist/server/i18n.js +143 -0
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.js +2 -0
- package/dist/server/server.js +6 -0
- package/dist/settings/company.d.ts +3 -1
- package/dist/settings/company.js +26 -5
- package/dist/settings/routes.d.ts +10 -0
- package/dist/settings/routes.js +19 -3
- package/dist/settings/web/AppsPage.d.ts +3 -2
- package/dist/settings/web/AppsPage.js +26 -8
- package/dist/settings/web/CompaniesPage.js +91 -6
- package/dist/settings/web/messages.js +64 -0
- package/dist/sign/.kosh/bindings.js +1 -1
- package/dist/sign/.kosh/schema.d.ts +2 -1
- package/dist/sign/i18n/bn.d.ts +2 -0
- package/dist/sign/i18n/bn.js +97 -0
- package/dist/sign/index.d.ts +15 -0
- package/dist/sign/index.js +129 -1
- package/dist/sign/module.d.ts +40 -0
- package/dist/sign/module.js +11 -2
- package/dist/stock/.kosh/bindings.js +1 -1
- package/dist/stock/.kosh/schema.d.ts +298 -2
- package/dist/stock/i18n/bn.d.ts +2 -0
- package/dist/stock/i18n/bn.js +311 -0
- package/dist/stock/index.d.ts +3 -1
- package/dist/stock/index.js +442 -3
- package/dist/stock/ledger.d.ts +78 -0
- package/dist/stock/ledger.js +582 -0
- package/dist/stock/module.d.ts +5280 -2044
- package/dist/stock/module.js +222 -23
- package/dist/stock/replenish.js +16 -1
- package/dist/stock/returns.d.ts +157 -0
- package/dist/stock/returns.js +163 -0
- package/dist/stock/web/Ledger.d.ts +9 -0
- package/dist/stock/web/Ledger.js +108 -0
- package/dist/stock/web/Operations.d.ts +8 -0
- package/dist/stock/web/Operations.js +33 -0
- package/dist/stock/web/ReturnButton.d.ts +7 -0
- package/dist/stock/web/ReturnButton.js +62 -0
- package/dist/stock/web/index.d.ts +3 -0
- package/dist/stock/web/index.js +9 -0
- package/dist/stock/web/messages.js +154 -0
- package/dist/stock_account/.kosh/bindings.js +1 -1
- package/dist/stock_account/.kosh/schema.d.ts +682 -9
- package/dist/stock_account/i18n/bn.d.ts +2 -0
- package/dist/stock_account/i18n/bn.js +73 -0
- package/dist/stock_account/index.d.ts +1 -0
- package/dist/stock_account/index.js +131 -3
- package/dist/stock_account/landed.d.ts +429 -0
- package/dist/stock_account/landed.js +335 -0
- package/dist/stock_account/module.d.ts +11247 -3363
- package/dist/stock_account/module.js +135 -6
- package/dist/stock_account/web/LandedPreview.d.ts +7 -0
- package/dist/stock_account/web/LandedPreview.js +23 -0
- package/dist/stock_account/web/index.d.ts +11 -0
- package/dist/stock_account/web/index.js +16 -0
- package/dist/stock_account/web/messages.d.ts +2 -0
- package/dist/stock_account/web/messages.js +27 -0
- package/dist/todo/i18n/bn.d.ts +2 -0
- package/dist/todo/i18n/bn.js +66 -0
- package/dist/todo/index.js +59 -0
- package/dist/todo/module.d.ts +5 -0
- package/dist/todo/module.js +5 -3
- package/dist/trade_finance/.kosh/bindings.js +1 -1
- package/dist/trade_finance/.kosh/schema.d.ts +2 -1
- package/dist/trade_finance/i18n/bn.d.ts +2 -0
- package/dist/trade_finance/i18n/bn.js +162 -0
- package/dist/trade_finance/index.js +93 -0
- package/dist/trade_finance/module.d.ts +26 -0
- package/dist/trade_finance/module.js +27 -8
- package/dist/trade_finance_account/.kosh/bindings.js +1 -1
- package/dist/trade_finance_account/.kosh/schema.d.ts +530 -8
- package/dist/trade_finance_account/i18n/bn.d.ts +2 -0
- package/dist/trade_finance_account/i18n/bn.js +27 -0
- package/dist/trade_finance_account/index.js +2 -0
- package/dist/trade_finance_account/module.d.ts +4906 -1432
- package/dist/trade_finance_account/module.js +1 -0
- package/dist/trade_finance_purchase/.kosh/bindings.js +1 -1
- package/dist/trade_finance_purchase/.kosh/schema.d.ts +813 -3
- package/dist/trade_finance_purchase/i18n/bn.d.ts +2 -0
- package/dist/trade_finance_purchase/i18n/bn.js +6 -0
- package/dist/trade_finance_purchase/index.js +2 -0
- package/dist/trade_finance_purchase/module.d.ts +6909 -413
- package/dist/trade_finance_sale/.kosh/bindings.js +1 -1
- package/dist/trade_finance_sale/.kosh/schema.d.ts +291 -4
- package/dist/trade_finance_sale/i18n/bn.d.ts +2 -0
- package/dist/trade_finance_sale/i18n/bn.js +6 -0
- package/dist/trade_finance_sale/index.js +2 -0
- package/dist/trade_finance_sale/module.d.ts +2601 -331
- package/dist/ui/api.d.ts +2 -0
- package/dist/ui/app.d.ts +9 -0
- package/dist/ui/app.js +24 -4
- package/dist/ui/components/AppIcon.js +2 -2
- package/dist/ui/components/Explain.d.ts +69 -0
- package/dist/ui/components/Explain.js +62 -0
- package/dist/ui/context.d.ts +8 -1
- package/dist/ui/context.js +34 -1
- package/dist/ui/i18n/messages.d.ts +36 -0
- package/dist/ui/i18n/messages.js +72 -0
- package/dist/ui/index.d.ts +2 -1
- package/dist/ui/index.js +1 -0
- package/dist/ui/registry.d.ts +14 -0
- package/dist/ui/registry.js +10 -0
- package/dist/ui/views/FormView.d.ts +6 -0
- package/dist/ui/views/FormView.js +47 -10
- package/dist/ui/views/ListView.d.ts +8 -0
- package/dist/ui/views/ListView.js +10 -3
- package/dist/ui/views/StatsRow.js +2 -1
- package/dist/views/extend.js +12 -0
- package/dist/views/types.d.ts +13 -2
- package/dist/views/validate.js +8 -1
- package/dist/website/.kosh/bindings.js +1 -1
- package/dist/website/.kosh/schema.d.ts +2 -1
- package/dist/website/i18n/bn.d.ts +2 -0
- package/dist/website/i18n/bn.js +113 -0
- package/dist/website/index.js +36 -0
- package/dist/website/module.d.ts +20 -0
- package/dist/website/module.js +29 -5
- package/dist/website_crm/.kosh/bindings.js +1 -1
- package/dist/website_crm/.kosh/schema.d.ts +2 -1
- package/dist/website_crm/i18n/bn.d.ts +2 -0
- package/dist/website_crm/i18n/bn.js +5 -0
- package/dist/website_crm/index.js +2 -0
- package/dist/website_crm/module.d.ts +40 -0
- package/dist/website_sale/.kosh/bindings.js +1 -1
- package/dist/website_sale/.kosh/schema.d.ts +642 -17
- package/dist/website_sale/i18n/bn.d.ts +2 -0
- package/dist/website_sale/i18n/bn.js +132 -0
- package/dist/website_sale/index.js +83 -1
- package/dist/website_sale/module.d.ts +18705 -5550
- package/dist/website_sale/module.js +32 -10
- package/dist/website_sale/shop.js +22 -9
- package/dist/website_sale_stock/.kosh/bindings.js +1 -1
- package/dist/website_sale_stock/.kosh/schema.d.ts +737 -18
- package/dist/website_sale_stock/i18n/bn.d.ts +2 -0
- package/dist/website_sale_stock/i18n/bn.js +4 -0
- package/dist/website_sale_stock/index.js +2 -0
- package/dist/website_sale_stock/module.d.ts +10 -24997
- package/dist/website_sale_stock/module.js +7 -3
- package/package.json +36 -8
- package/dist/mrp/index.d.ts +0 -3
- package/dist/mrp/index.js +0 -393
- package/dist/mrp/module.js +0 -757
- package/dist/mrp_account/index.d.ts +0 -3
- package/dist/mrp_account/index.js +0 -22
- /package/dist/{mrp → approvals_purchase}/.kosh/bindings.d.ts +0 -0
- /package/dist/{mrp_account → indent}/.kosh/bindings.d.ts +0 -0
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { audit, HttpProblem } from '../server/index.js';
|
|
2
2
|
const ref = (env, name) => env.model(name);
|
|
3
3
|
/** Many-to-many fields read here: loaded by the object spec (defaults plus the relation), read as ids. */
|
|
4
|
-
const MANY = new Set([
|
|
4
|
+
const MANY = new Set([
|
|
5
|
+
'approvers',
|
|
6
|
+
'backups',
|
|
7
|
+
'approverUsers',
|
|
8
|
+
'extraUsers',
|
|
9
|
+
'backupUsers',
|
|
10
|
+
'companies',
|
|
11
|
+
]);
|
|
5
12
|
const specOf = (keys) => keys.some((k) => MANY.has(k))
|
|
6
13
|
? Object.fromEntries(keys.filter((k) => MANY.has(k)).map((k) => [k, true]))
|
|
7
14
|
: keys;
|
|
@@ -11,6 +18,9 @@ async function find(env, model, where, keys, order) {
|
|
|
11
18
|
}
|
|
12
19
|
const REQUEST_KEYS = [
|
|
13
20
|
'dueAt',
|
|
21
|
+
'backups',
|
|
22
|
+
'startedAt',
|
|
23
|
+
'explanation',
|
|
14
24
|
'escalatedTo',
|
|
15
25
|
'approvers',
|
|
16
26
|
'required',
|
|
@@ -40,6 +50,7 @@ function info(row) {
|
|
|
40
50
|
company: row['company'] ?? null,
|
|
41
51
|
approverRole: String(row['approverRole'] ?? ''),
|
|
42
52
|
approvers: Array.isArray(row['approvers']) ? row['approvers'] : [],
|
|
53
|
+
backups: Array.isArray(row['backups']) ? row['backups'] : [],
|
|
43
54
|
required: Number(row['required'] ?? 1),
|
|
44
55
|
approvals: Number(row['approvals'] ?? 0),
|
|
45
56
|
round: Number(row['round'] ?? 1),
|
|
@@ -51,6 +62,12 @@ function info(row) {
|
|
|
51
62
|
escalatedTo: typeof row['escalatedTo'] === 'string' && row['escalatedTo'] !== ''
|
|
52
63
|
? row['escalatedTo']
|
|
53
64
|
: null,
|
|
65
|
+
startedAt: row['startedAt'] instanceof Date
|
|
66
|
+
? row['startedAt'].toISOString()
|
|
67
|
+
: typeof row['startedAt'] === 'string'
|
|
68
|
+
? row['startedAt']
|
|
69
|
+
: null,
|
|
70
|
+
explanation: row['explanation'] ?? null,
|
|
54
71
|
state: row['state'],
|
|
55
72
|
requestedBy: row['requestedBy'] ?? null,
|
|
56
73
|
decidedBy: row['decidedBy'] ?? null,
|
|
@@ -112,36 +129,210 @@ async function userIn(su, model, id, field) {
|
|
|
112
129
|
}
|
|
113
130
|
}
|
|
114
131
|
/**
|
|
115
|
-
*
|
|
116
|
-
*
|
|
132
|
+
* The user in a field of the requester's employee: the field's employee's user (`unitHead`, `coach`), or the user
|
|
133
|
+
* itself (`expenseApprover`); null when there is none or Workforce is not installed.
|
|
134
|
+
*/
|
|
135
|
+
async function employeePerson(su, requester, field) {
|
|
136
|
+
let me;
|
|
137
|
+
try {
|
|
138
|
+
[me] = await find(su, 'hr.employee', { user: requester }, [field], ['id asc']);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
const value = me?.[field];
|
|
144
|
+
if (me === undefined || typeof value !== 'number')
|
|
145
|
+
return null;
|
|
146
|
+
try {
|
|
147
|
+
// A field to an employee: that employee's user.
|
|
148
|
+
const [row] = (await ref(su, 'hr.employee')
|
|
149
|
+
.browse([me['id']])
|
|
150
|
+
.load({ [field]: { user: true } }));
|
|
151
|
+
return row?.[field]?.user?.id ?? null;
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
// A field to a user: the user.
|
|
155
|
+
return value;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/** The requester's employee in Workforce, with its department (null when Workforce is not installed or none). */
|
|
159
|
+
async function employeeOf(su, requester) {
|
|
160
|
+
try {
|
|
161
|
+
const [me] = await find(su, 'hr.employee', { user: requester }, ['name', 'department'], ['id asc']);
|
|
162
|
+
if (me === undefined)
|
|
163
|
+
return null;
|
|
164
|
+
return {
|
|
165
|
+
name: String(me['name']),
|
|
166
|
+
department: typeof me['department'] === 'number' ? me['department'] : null,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/** The head of a department and its approvers, with its name. */
|
|
174
|
+
async function departmentOf(su, id) {
|
|
175
|
+
const set = await ref(su, 'hr.department').search({ where: { id } });
|
|
176
|
+
const [row] = set.length === 0
|
|
177
|
+
? []
|
|
178
|
+
: (await set.load({ approvalUsers: true })).read(['name', 'head', 'approvalUsers']);
|
|
179
|
+
const headEmployee = typeof row?.['head'] === 'number' ? row['head'] : null;
|
|
180
|
+
const [head] = headEmployee === null ? [] : await find(su, 'hr.employee', { id: headEmployee }, ['user']);
|
|
181
|
+
return {
|
|
182
|
+
name: String(row?.['name'] ?? id),
|
|
183
|
+
head: typeof head?.['user'] === 'number' ? head['user'] : null,
|
|
184
|
+
approvers: Array.isArray(row?.['approvalUsers']) ? row['approvalUsers'] : [],
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
/** The people the record names for a level (its model's `approvalUsers(key)`), or null when it names none. */
|
|
188
|
+
async function recordApprovers(su, model, id, key) {
|
|
189
|
+
const record = ref(su, model).browse([id]);
|
|
190
|
+
if (typeof record.approvalUsers !== 'function')
|
|
191
|
+
return null;
|
|
192
|
+
return [...((await record.approvalUsers(key)).get(id) ?? [])];
|
|
193
|
+
}
|
|
194
|
+
/** People's names, for explanations. */
|
|
195
|
+
async function namesOf(su, ids) {
|
|
196
|
+
if (ids.length === 0)
|
|
197
|
+
return 'nobody';
|
|
198
|
+
const rows = await find(su, 'res.users', { id: { in: [...ids] } }, ['name']);
|
|
199
|
+
const by = new Map(rows.map((r) => [r['id'], String(r['name'])]));
|
|
200
|
+
return ids.map((u) => by.get(u) ?? `#${u}`).join(', ');
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Who decides a level, how many approvals it needs and how they were found (RFC 0027 §2.1): named people, the
|
|
204
|
+
* requester's manager, their department's head or approvers, a field's person, the people the record names — plus
|
|
205
|
+
* the level's extra people —; a role's holders for a role, and when all or a number of them must approve. Never the
|
|
206
|
+
* requester. `nobody` when no one can decide (the policy's `whenNobody` decides what then).
|
|
117
207
|
*/
|
|
118
208
|
async function approversOf(su, level, target, requester) {
|
|
119
209
|
const role = typeof level['approverRole'] === 'string' ? level['approverRole'] : '';
|
|
210
|
+
const notes = [];
|
|
120
211
|
let people = [];
|
|
121
|
-
const kind = level['approverKind'] ?? 'role';
|
|
122
|
-
|
|
212
|
+
const kind = String(level['approverKind'] ?? 'role');
|
|
213
|
+
const who = await namesOf(su, [requester]);
|
|
214
|
+
if (kind === 'users') {
|
|
123
215
|
people = level['approverUsers'] ?? [];
|
|
216
|
+
notes.push(`Named people: ${await namesOf(su, people)}.`);
|
|
217
|
+
}
|
|
124
218
|
else if (kind === 'manager') {
|
|
125
219
|
const manager = await managerOf(su, requester);
|
|
126
220
|
if (manager !== null)
|
|
127
221
|
people = [manager];
|
|
222
|
+
notes.push(manager === null
|
|
223
|
+
? `${who} has no manager with a user in Workforce.`
|
|
224
|
+
: `${who}’s manager: ${await namesOf(su, people)}.`);
|
|
225
|
+
}
|
|
226
|
+
else if (kind === 'department_head' || kind === 'department_approvers') {
|
|
227
|
+
const employee = await employeeOf(su, requester);
|
|
228
|
+
if (employee === null)
|
|
229
|
+
notes.push(`${who} is not an employee in Workforce.`);
|
|
230
|
+
else if (employee.department === null)
|
|
231
|
+
notes.push(`${employee.name} has no department.`);
|
|
232
|
+
else {
|
|
233
|
+
const department = await departmentOf(su, employee.department);
|
|
234
|
+
if (kind === 'department_head') {
|
|
235
|
+
if (department.head !== null)
|
|
236
|
+
people = [department.head];
|
|
237
|
+
notes.push(department.head === null
|
|
238
|
+
? `${department.name} has no head with a user.`
|
|
239
|
+
: `${department.name}’s head: ${await namesOf(su, people)}.`);
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
people = department.approvers;
|
|
243
|
+
notes.push(`${department.name}’s approvers: ${await namesOf(su, people)}.`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
else if (kind === 'employee') {
|
|
248
|
+
// A person on the requester's employee record (RFC 0032 §3.2): their unit head, coach, expense approver…
|
|
249
|
+
const field = String(level['approverField'] ?? '');
|
|
250
|
+
const person = await employeePerson(su, requester, field);
|
|
251
|
+
if (person !== null)
|
|
252
|
+
people = [person];
|
|
253
|
+
notes.push(person === null
|
|
254
|
+
? `${who} has no ${field} with a user in Workforce.`
|
|
255
|
+
: `${who}’s ${field}: ${await namesOf(su, people)}.`);
|
|
128
256
|
}
|
|
129
257
|
else if (kind === 'field') {
|
|
130
|
-
const
|
|
258
|
+
const field = String(level['approverField'] ?? '');
|
|
259
|
+
const person = await userIn(su, target.model, target.id, field);
|
|
131
260
|
if (person !== null)
|
|
132
261
|
people = [person];
|
|
262
|
+
notes.push(`The record’s ${field}: ${await namesOf(su, people)}.`);
|
|
263
|
+
}
|
|
264
|
+
else if (kind === 'record') {
|
|
265
|
+
const named = await recordApprovers(su, target.model, target.id, String(level['approverField'] ?? ''));
|
|
266
|
+
people = named ?? [];
|
|
267
|
+
notes.push(named === null
|
|
268
|
+
? `${target.model} names no approvers.`
|
|
269
|
+
: `Named by the record: ${await namesOf(su, people)}.`);
|
|
133
270
|
}
|
|
271
|
+
// A role decides a role level, and stands in when nobody else was found.
|
|
272
|
+
const roleHolders = role === '' ? [] : (await holders(su, role, target.company)).filter((u) => u !== requester);
|
|
273
|
+
const extra = level['extraUsers'] ?? [];
|
|
274
|
+
if (extra.length > 0) {
|
|
275
|
+
// A role with extra people: its holders by name, beside them.
|
|
276
|
+
if (kind === 'role') {
|
|
277
|
+
people = roleHolders;
|
|
278
|
+
notes.push(`Role ${role}: ${await namesOf(su, roleHolders)}.`);
|
|
279
|
+
}
|
|
280
|
+
people = [...new Set([...people, ...extra])];
|
|
281
|
+
notes.push(`Also: ${await namesOf(su, extra)}.`);
|
|
282
|
+
}
|
|
283
|
+
if (people.includes(requester))
|
|
284
|
+
notes.push(`${who} asked, so does not decide.`);
|
|
134
285
|
people = people.filter((u) => u !== requester);
|
|
135
286
|
const quorum = level['quorum'] ?? 'any';
|
|
136
|
-
|
|
137
|
-
|
|
287
|
+
const byRole = people.length === 0 && role !== '';
|
|
288
|
+
if (byRole) {
|
|
289
|
+
notes.push(kind === 'role'
|
|
290
|
+
? `Role ${role}: ${roleHolders.length} ${roleHolders.length === 1 ? 'person' : 'people'} in the company.`
|
|
291
|
+
: `Instead, role ${role}: ${roleHolders.length} ${roleHolders.length === 1 ? 'person' : 'people'} in the company.`);
|
|
292
|
+
if (roleHolders.length === 0)
|
|
293
|
+
notes.push('Nobody holds it yet: administrators decide.');
|
|
294
|
+
if (quorum !== 'any')
|
|
295
|
+
people = roleHolders;
|
|
296
|
+
}
|
|
138
297
|
const count = Math.max(1, Number(level['quorumCount'] ?? 1));
|
|
139
298
|
const required = quorum === 'all'
|
|
140
299
|
? Math.max(1, people.length)
|
|
141
300
|
: quorum === 'count'
|
|
142
301
|
? Math.min(count, Math.max(1, people.length))
|
|
143
302
|
: 1;
|
|
144
|
-
return {
|
|
303
|
+
return {
|
|
304
|
+
role: byRole ? role : '',
|
|
305
|
+
approvers: people,
|
|
306
|
+
required,
|
|
307
|
+
explanation: notes,
|
|
308
|
+
// A role always has someone: administrators decide a role nobody holds yet.
|
|
309
|
+
nobody: people.length === 0 && !byRole,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
/** A company's segment (RFC 0027 §2.3), or null. */
|
|
313
|
+
async function segmentOf(su, company) {
|
|
314
|
+
if (company === null)
|
|
315
|
+
return null;
|
|
316
|
+
const segments = await find(su, 'res.segment', { active: true }, ['companies']);
|
|
317
|
+
const mine = segments.find((s) => (s['companies'] ?? []).includes(company));
|
|
318
|
+
return mine === undefined ? null : mine['id'];
|
|
319
|
+
}
|
|
320
|
+
/** Whether a policy covers a company: its company, its companies and its segment's together; none: every company. */
|
|
321
|
+
function covers(policy, company, segment) {
|
|
322
|
+
const one = typeof policy['company'] === 'number' ? policy['company'] : null;
|
|
323
|
+
const many = Array.isArray(policy['companies']) ? policy['companies'] : [];
|
|
324
|
+
const group = typeof policy['segment'] === 'number' ? policy['segment'] : null;
|
|
325
|
+
if (one === null && many.length === 0 && group === null)
|
|
326
|
+
return true;
|
|
327
|
+
if (company === null)
|
|
328
|
+
return false;
|
|
329
|
+
return one === company || many.includes(company) || (group !== null && group === segment);
|
|
330
|
+
}
|
|
331
|
+
/** The active policies of a model that cover a company, in order. */
|
|
332
|
+
async function policiesFor(su, model, company, keys) {
|
|
333
|
+
const segment = await segmentOf(su, company);
|
|
334
|
+
const rows = await find(su, 'approval.policy', { targetModel: model }, [...keys, 'company', 'companies', 'segment'], ['sequence asc', 'id asc']);
|
|
335
|
+
return rows.filter((p) => covers(p, company, segment));
|
|
145
336
|
}
|
|
146
337
|
/** The record's company, when the model has one. */
|
|
147
338
|
async function companyOf(su, model, id) {
|
|
@@ -158,6 +349,199 @@ async function companyOf(su, model, id) {
|
|
|
158
349
|
* and levels; earlier pending requests of the records are cancelled (a resubmission starts a new round). Returns
|
|
159
350
|
* the number of requests created.
|
|
160
351
|
*/
|
|
352
|
+
/** Whether record `id` of `model` meets `condition` (a Shutro domain; empty: always). */
|
|
353
|
+
async function matches(su, model, id, condition) {
|
|
354
|
+
if (condition === null || Object.keys(condition).length === 0)
|
|
355
|
+
return true;
|
|
356
|
+
return ((await ref(su, model).search({ where: { and: [{ id }, condition] }, limit: 1 })).length > 0);
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Which of the last `limit` records of the policy's model each of its levels would have asked about (RFC 0025 step
|
|
360
|
+
* 4: the editor's preview), as the system; the policy's own condition first.
|
|
361
|
+
*/
|
|
362
|
+
export async function previewPolicy(su, policyId, limit = 50) {
|
|
363
|
+
const [policy] = await find(su, 'approval.policy', { id: policyId }, [
|
|
364
|
+
'targetModel',
|
|
365
|
+
'condition',
|
|
366
|
+
'company',
|
|
367
|
+
'companies',
|
|
368
|
+
'segment',
|
|
369
|
+
]);
|
|
370
|
+
if (policy === undefined)
|
|
371
|
+
throw new HttpProblem(404, 'missing-record', 'Not found', 'No such policy.');
|
|
372
|
+
const model = String(policy['targetModel']);
|
|
373
|
+
const recent = await ref(su, model).search({
|
|
374
|
+
where: policy['condition'] ?? {},
|
|
375
|
+
order: ['id desc'],
|
|
376
|
+
limit,
|
|
377
|
+
});
|
|
378
|
+
const ids = recent.ids;
|
|
379
|
+
const levels = await find(su, 'approval.level', { policy: policyId }, ['name', 'condition'], ['sequence asc', 'id asc']);
|
|
380
|
+
const out = [];
|
|
381
|
+
for (const level of levels) {
|
|
382
|
+
const condition = level['condition'];
|
|
383
|
+
const matched = ids.length === 0
|
|
384
|
+
? []
|
|
385
|
+
: condition === null || Object.keys(condition).length === 0
|
|
386
|
+
? ids
|
|
387
|
+
: (await ref(su, model).search({ where: { and: [{ id: { in: ids } }, condition] } }))
|
|
388
|
+
.ids;
|
|
389
|
+
out.push({ id: level['id'], name: String(level['name']), matched });
|
|
390
|
+
}
|
|
391
|
+
// The companies it covers: its company, its companies and its segment's, or every company.
|
|
392
|
+
const ids2 = new Set(Array.isArray(policy['companies']) ? policy['companies'] : []);
|
|
393
|
+
if (typeof policy['company'] === 'number')
|
|
394
|
+
ids2.add(policy['company']);
|
|
395
|
+
if (typeof policy['segment'] === 'number')
|
|
396
|
+
for (const s of await find(su, 'res.segment', { id: policy['segment'] }, ['companies']))
|
|
397
|
+
for (const c of s['companies'] ?? [])
|
|
398
|
+
ids2.add(c);
|
|
399
|
+
const all = ids2.size === 0 &&
|
|
400
|
+
typeof policy['segment'] !== 'number' &&
|
|
401
|
+
typeof policy['company'] !== 'number';
|
|
402
|
+
const names = ids2.size === 0
|
|
403
|
+
? []
|
|
404
|
+
: (await find(su, 'res.company', { id: { in: [...ids2] } }, ['name'], ['name asc'])).map((c) => String(c['name']));
|
|
405
|
+
return { records: ids.length, levels: out, covers: { all, companies: names } };
|
|
406
|
+
}
|
|
407
|
+
/** Writes steps to the matrix log (RFC 0027 §2.5). */
|
|
408
|
+
async function logEvents(su, events) {
|
|
409
|
+
if (events.length === 0)
|
|
410
|
+
return;
|
|
411
|
+
const at = new Date();
|
|
412
|
+
await ref(su, 'approval.event').create(events.map((e) => ({
|
|
413
|
+
request: e.request,
|
|
414
|
+
kind: e.kind,
|
|
415
|
+
user: e.user ?? null,
|
|
416
|
+
note: e.note === undefined || e.note === '' ? null : e.note,
|
|
417
|
+
at: e.at ?? at,
|
|
418
|
+
})));
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Starts the levels whose turn it is and that have not started: `startedAt` now, the deadline counted from it
|
|
422
|
+
* (RFC 0027 §2.5), logged.
|
|
423
|
+
*/
|
|
424
|
+
async function startTurn(su, model, id, now) {
|
|
425
|
+
const turn = (await turnOf(su, model, id)).filter((r) => r.startedAt === null);
|
|
426
|
+
if (turn.length === 0)
|
|
427
|
+
return;
|
|
428
|
+
const rows = await find(su, 'approval.request', { id: { in: turn.map((r) => r.id) } }, ['level']);
|
|
429
|
+
const levelIds = rows.map((r) => r['level']).filter((l) => typeof l === 'number');
|
|
430
|
+
const levels = levelIds.length === 0
|
|
431
|
+
? []
|
|
432
|
+
: await find(su, 'approval.level', { id: { in: levelIds } }, ['deadlineHours']);
|
|
433
|
+
const hours = new Map(levels.map((l) => [l['id'], Number(l['deadlineHours'] ?? 0)]));
|
|
434
|
+
for (const row of rows) {
|
|
435
|
+
const h = typeof row['level'] === 'number' ? (hours.get(row['level']) ?? 0) : 0;
|
|
436
|
+
await ref(su, 'approval.request')
|
|
437
|
+
.browse([row['id']])
|
|
438
|
+
.write({
|
|
439
|
+
startedAt: now,
|
|
440
|
+
...(h > 0 ? { dueAt: new Date(now.getTime() + h * 3_600_000) } : {}),
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
await logEvents(su, turn.map((r) => ({ request: r.id, kind: 'started', at: now })));
|
|
444
|
+
await skipDuplicates(su, model, id, turn, now);
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Skip approvers who already approved (RFC 0027 §2.2): for the levels just started, under a policy that asks for it,
|
|
448
|
+
* the approval of someone who approved an earlier level of this round counts here too — a vote marked `auto` —;
|
|
449
|
+
* a level that reaches its quorum this way is approved and the next turn starts (the record approved when it was
|
|
450
|
+
* the last).
|
|
451
|
+
*/
|
|
452
|
+
async function skipDuplicates(su, model, id, started, now) {
|
|
453
|
+
const policyIds = [
|
|
454
|
+
...new Set(started.map((r) => r.policy).filter((p) => p !== null)),
|
|
455
|
+
];
|
|
456
|
+
if (policyIds.length === 0)
|
|
457
|
+
return;
|
|
458
|
+
const skipping = new Set((await find(su, 'approval.policy', { id: { in: policyIds }, skipDuplicates: true }, ['id'])).map((p) => p['id']));
|
|
459
|
+
if (skipping.size === 0)
|
|
460
|
+
return;
|
|
461
|
+
const round = await currentRequests(su, model, id);
|
|
462
|
+
const earlier = round.filter((r) => r.state === 'approved');
|
|
463
|
+
if (earlier.length === 0)
|
|
464
|
+
return;
|
|
465
|
+
const votes = await find(su, 'approval.vote', { request: { in: earlier.map((r) => r.id) }, decision: 'approve' }, ['request', 'user']);
|
|
466
|
+
let approvedAny = false;
|
|
467
|
+
for (const request of started) {
|
|
468
|
+
if (request.policy === null || !skipping.has(request.policy))
|
|
469
|
+
continue;
|
|
470
|
+
const already = new Set((await find(su, 'approval.vote', { request: request.id }, ['user'])).map((v) => v['user']));
|
|
471
|
+
const allowed = new Set([
|
|
472
|
+
...request.approvers,
|
|
473
|
+
...request.backups,
|
|
474
|
+
...(request.approvers.length === 0 && request.approverRole !== ''
|
|
475
|
+
? await holders(su, request.approverRole, request.company)
|
|
476
|
+
: []),
|
|
477
|
+
]);
|
|
478
|
+
let approvals = request.approvals;
|
|
479
|
+
let last = null;
|
|
480
|
+
for (const v of votes) {
|
|
481
|
+
const user = v['user'];
|
|
482
|
+
if (approvals >= request.required)
|
|
483
|
+
break;
|
|
484
|
+
if (user === request.requestedBy || already.has(user) || !allowed.has(user))
|
|
485
|
+
continue;
|
|
486
|
+
const level = earlier.find((r) => r.id === v['request'])?.levelName ?? '';
|
|
487
|
+
await ref(su, 'approval.vote').create([
|
|
488
|
+
{
|
|
489
|
+
request: request.id,
|
|
490
|
+
user,
|
|
491
|
+
decision: 'approve',
|
|
492
|
+
comment: `Approved already at ${level}.`,
|
|
493
|
+
at: now,
|
|
494
|
+
auto: true,
|
|
495
|
+
},
|
|
496
|
+
]);
|
|
497
|
+
await logEvents(su, [
|
|
498
|
+
{ request: request.id, kind: 'auto', user, note: `Approved already at ${level}.`, at: now },
|
|
499
|
+
]);
|
|
500
|
+
already.add(user);
|
|
501
|
+
approvals += 1;
|
|
502
|
+
last = user;
|
|
503
|
+
}
|
|
504
|
+
if (approvals >= request.required && last !== null) {
|
|
505
|
+
await ref(su, 'approval.request')
|
|
506
|
+
.browse([request.id])
|
|
507
|
+
.write({ state: 'approved', decidedBy: last, decidedAt: now });
|
|
508
|
+
approvedAny = true;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
if (!approvedAny)
|
|
512
|
+
return;
|
|
513
|
+
const after = await currentRequests(su, model, id);
|
|
514
|
+
if (after.every((r) => r.state === 'approved')) {
|
|
515
|
+
await su.emit('approval.decided', { model, id, state: 'approved', final: true });
|
|
516
|
+
for (const pid of new Set(after.map((r) => r.policy).filter((p) => p !== null))) {
|
|
517
|
+
const [policy] = await find(su, 'approval.policy', { id: pid }, ['onApproved']);
|
|
518
|
+
await runTransition(su, { model, id }, policy?.['onApproved']);
|
|
519
|
+
}
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
await su.emit('approval.decided', { model, id, state: 'approved', final: false });
|
|
523
|
+
await startTurn(su, model, id, now);
|
|
524
|
+
}
|
|
525
|
+
/** The matrix log of a record's requests, when `env` may read the record. */
|
|
526
|
+
export async function eventsOf(env, model, id) {
|
|
527
|
+
await ref(env, model).browse([id]).load(['id']); // as the caller: ACL and rules decide
|
|
528
|
+
return env.asSuperuser({ reason: 'approval log' }, async (su) => {
|
|
529
|
+
const requests = await find(su, 'approval.request', { targetModel: model, targetId: id }, [
|
|
530
|
+
'id',
|
|
531
|
+
]);
|
|
532
|
+
if (requests.length === 0)
|
|
533
|
+
return [];
|
|
534
|
+
const rows = await find(su, 'approval.event', { request: { in: requests.map((r) => r['id']) } }, ['request', 'at', 'user', 'kind', 'note'], ['at asc', 'id asc']);
|
|
535
|
+
return rows.map((r) => ({
|
|
536
|
+
id: r['id'],
|
|
537
|
+
request: r['request'],
|
|
538
|
+
at: r['at'] instanceof Date ? r['at'].toISOString() : String(r['at']),
|
|
539
|
+
user: r['user'] ?? null,
|
|
540
|
+
kind: String(r['kind']),
|
|
541
|
+
note: r['note'] ?? null,
|
|
542
|
+
}));
|
|
543
|
+
});
|
|
544
|
+
}
|
|
161
545
|
export async function requestApprovals(records) {
|
|
162
546
|
const { env, modelName, ids } = records;
|
|
163
547
|
const requester = env.uid;
|
|
@@ -167,14 +551,28 @@ export async function requestApprovals(records) {
|
|
|
167
551
|
const stale = await ref(su, 'approval.request').search({
|
|
168
552
|
where: { targetModel: modelName, targetId: id, state: 'pending' },
|
|
169
553
|
});
|
|
170
|
-
if (stale.length > 0)
|
|
554
|
+
if (stale.length > 0) {
|
|
171
555
|
await stale.write({ state: 'cancelled' });
|
|
556
|
+
await logEvents(su, stale.ids.map((request) => ({
|
|
557
|
+
request,
|
|
558
|
+
kind: 'cancelled',
|
|
559
|
+
user: requester,
|
|
560
|
+
note: 'Submitted again.',
|
|
561
|
+
})));
|
|
562
|
+
}
|
|
172
563
|
const [last] = await find(su, 'approval.request', { targetModel: modelName, targetId: id }, ['round'], ['round desc']);
|
|
173
564
|
const round = Number(last?.['round'] ?? 0) + 1;
|
|
174
565
|
const company = (await companyOf(su, modelName, id)) ?? env.companyId;
|
|
175
|
-
const policies = await
|
|
566
|
+
const policies = await policiesFor(su, modelName, company, [
|
|
567
|
+
'name',
|
|
568
|
+
'condition',
|
|
569
|
+
'whenNobody',
|
|
570
|
+
]);
|
|
176
571
|
const rows = [];
|
|
177
572
|
for (const policy of policies) {
|
|
573
|
+
// A policy for some records only (a request category's): the record must match its condition.
|
|
574
|
+
if (!(await matches(su, modelName, id, policy['condition'])))
|
|
575
|
+
continue;
|
|
178
576
|
const levels = await find(su, 'approval.level', { policy: policy['id'] }, [
|
|
179
577
|
'sequence',
|
|
180
578
|
'name',
|
|
@@ -182,20 +580,21 @@ export async function requestApprovals(records) {
|
|
|
182
580
|
'approverKind',
|
|
183
581
|
'approverRole',
|
|
184
582
|
'approverUsers',
|
|
583
|
+
'extraUsers',
|
|
584
|
+
'backupUsers',
|
|
185
585
|
'approverField',
|
|
186
586
|
'quorum',
|
|
187
587
|
'quorumCount',
|
|
188
588
|
'deadlineHours',
|
|
189
589
|
], ['sequence asc', 'id asc']);
|
|
190
590
|
for (const level of levels) {
|
|
191
|
-
|
|
192
|
-
const applies = condition === null ||
|
|
193
|
-
Object.keys(condition).length === 0 ||
|
|
194
|
-
(await ref(su, modelName).search({ where: { and: [{ id }, condition] }, limit: 1 }))
|
|
195
|
-
.length > 0;
|
|
196
|
-
if (!applies)
|
|
591
|
+
if (!(await matches(su, modelName, id, level['condition'])))
|
|
197
592
|
continue;
|
|
198
593
|
const who = await approversOf(su, level, { model: modelName, id, company }, requester);
|
|
594
|
+
const whenNobody = String(policy['whenNobody'] ?? 'refuse');
|
|
595
|
+
if (who.nobody && whenNobody === 'refuse')
|
|
596
|
+
refuse(422, 'no-approver', 'Nobody to approve', `Nobody can decide the level “${String(level['name'])}” of “${String(policy['name'])}”. ${who.explanation.join(' ')}`, 'Set who approves (Workforce: the manager or the department), or change the approval policy.');
|
|
597
|
+
const skipped = who.nobody && whenNobody === 'skip';
|
|
199
598
|
rows.push({
|
|
200
599
|
policy: policy['id'],
|
|
201
600
|
level: level['id'],
|
|
@@ -206,19 +605,44 @@ export async function requestApprovals(records) {
|
|
|
206
605
|
company,
|
|
207
606
|
approverRole: who.role,
|
|
208
607
|
approvers: { set: who.approvers },
|
|
608
|
+
backups: {
|
|
609
|
+
set: (level['backupUsers'] ?? []).filter((u) => u !== requester),
|
|
610
|
+
},
|
|
209
611
|
required: who.required,
|
|
210
612
|
round,
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
613
|
+
explanation: [
|
|
614
|
+
...who.explanation,
|
|
615
|
+
...((level['backupUsers'] ?? []).length > 0
|
|
616
|
+
? [
|
|
617
|
+
`Backups: ${await namesOf(su, level['backupUsers'].filter((u) => u !== requester))}.`,
|
|
618
|
+
]
|
|
619
|
+
: []),
|
|
620
|
+
...(skipped ? ['Nobody could decide: the level is skipped.'] : []),
|
|
621
|
+
].join(' '),
|
|
622
|
+
...(skipped ? { state: 'approved', decidedAt: new Date() } : {}),
|
|
214
623
|
requestedBy: requester,
|
|
215
624
|
});
|
|
216
625
|
}
|
|
217
626
|
}
|
|
218
627
|
if (rows.length === 0)
|
|
219
628
|
continue;
|
|
220
|
-
await ref(su, 'approval.request').create(rows);
|
|
629
|
+
const made = await ref(su, 'approval.request').create(rows);
|
|
221
630
|
created += rows.length;
|
|
631
|
+
const now = new Date();
|
|
632
|
+
await logEvents(su, made.ids.map((request, i) => ({
|
|
633
|
+
request,
|
|
634
|
+
kind: rows[i]?.['state'] === 'approved' ? 'skipped' : 'created',
|
|
635
|
+
user: requester,
|
|
636
|
+
note: String(rows[i]?.['explanation'] ?? ''),
|
|
637
|
+
at: now,
|
|
638
|
+
})));
|
|
639
|
+
await startTurn(su, modelName, id, now);
|
|
640
|
+
// Every level skipped (nobody to ask, the policy says skip): approved now.
|
|
641
|
+
if (rows.every((r) => r['state'] === 'approved'))
|
|
642
|
+
for (const pid of new Set(rows.map((r) => r['policy']))) {
|
|
643
|
+
const [policy] = await find(su, 'approval.policy', { id: pid }, ['onApproved']);
|
|
644
|
+
await runTransition(su, { model: modelName, id }, policy?.['onApproved']);
|
|
645
|
+
}
|
|
222
646
|
await audit(su, {
|
|
223
647
|
kind: 'approval.requested',
|
|
224
648
|
summary: `${rows.length} approval(s) requested for ${modelName} ${id}.`,
|
|
@@ -294,7 +718,7 @@ const holds = (roles, role, company) => role !== '' &&
|
|
|
294
718
|
* the escalation role once it is late —, or for someone away who delegated to them (`{ for: <user> }`); null when not.
|
|
295
719
|
*/
|
|
296
720
|
function approverFor(who, request) {
|
|
297
|
-
if (request.approvers.includes(who.uid))
|
|
721
|
+
if (request.approvers.includes(who.uid) || request.backups.includes(who.uid))
|
|
298
722
|
return { for: null };
|
|
299
723
|
if (request.approvers.length === 0 && (who.administrator || who.groups.has(request.approverRole)))
|
|
300
724
|
return { for: null };
|
|
@@ -303,7 +727,7 @@ function approverFor(who, request) {
|
|
|
303
727
|
for (const d of who.delegators) {
|
|
304
728
|
if (d.user === request.requestedBy)
|
|
305
729
|
continue;
|
|
306
|
-
if (request.approvers.includes(d.user))
|
|
730
|
+
if (request.approvers.includes(d.user) || request.backups.includes(d.user))
|
|
307
731
|
return { for: d.user };
|
|
308
732
|
if (request.approvers.length === 0 && holds(d.roles, request.approverRole, request.company))
|
|
309
733
|
return { for: d.user };
|
|
@@ -401,6 +825,9 @@ export async function decide(env, requestId, decision, comment, from) {
|
|
|
401
825
|
at: new Date(),
|
|
402
826
|
onBehalfOf,
|
|
403
827
|
ip: from?.ip ?? null,
|
|
828
|
+
asBackup: onBehalfOf === null &&
|
|
829
|
+
request.backups.includes(actor) &&
|
|
830
|
+
!request.approvers.includes(actor),
|
|
404
831
|
},
|
|
405
832
|
]);
|
|
406
833
|
const approvals = request.approvals + (decision === 'approve' ? 1 : 0);
|
|
@@ -420,12 +847,29 @@ export async function decide(env, requestId, decision, comment, from) {
|
|
|
420
847
|
? {}
|
|
421
848
|
: { decidedBy: actor, decidedAt: new Date(), comment: text === '' ? null : text }),
|
|
422
849
|
});
|
|
850
|
+
await logEvents(su, [
|
|
851
|
+
{
|
|
852
|
+
request: request.id,
|
|
853
|
+
kind: state === 'pending' ? 'voted' : state,
|
|
854
|
+
user: actor,
|
|
855
|
+
note: [onBehalfOf === null ? '' : `For user ${onBehalfOf}.`, text]
|
|
856
|
+
.filter((x) => x !== '')
|
|
857
|
+
.join(' ') || null,
|
|
858
|
+
},
|
|
859
|
+
]);
|
|
423
860
|
if (state === 'rejected' || state === 'sent_back') {
|
|
424
861
|
const open = round
|
|
425
862
|
.filter((r) => r.id !== request.id && r.state === 'pending')
|
|
426
863
|
.map((r) => r.id);
|
|
427
|
-
if (open.length > 0)
|
|
864
|
+
if (open.length > 0) {
|
|
428
865
|
await ref(su, 'approval.request').browse(open).write({ state: 'cancelled' });
|
|
866
|
+
await logEvents(su, open.map((id) => ({
|
|
867
|
+
request: id,
|
|
868
|
+
kind: 'cancelled',
|
|
869
|
+
user: actor,
|
|
870
|
+
note: `${request.levelName} ${state === 'rejected' ? 'rejected' : 'sent back'}.`,
|
|
871
|
+
})));
|
|
872
|
+
}
|
|
429
873
|
}
|
|
430
874
|
const done = state === 'rejected' ||
|
|
431
875
|
state === 'sent_back' ||
|
|
@@ -446,6 +890,8 @@ export async function decide(env, requestId, decision, comment, from) {
|
|
|
446
890
|
ip: from?.ip ?? null,
|
|
447
891
|
},
|
|
448
892
|
});
|
|
893
|
+
if (state === 'approved' && !done)
|
|
894
|
+
await startTurn(su, target.model, target.id, new Date());
|
|
449
895
|
await su.emit('approval.decided', { ...target, request: request.id, state, final: done });
|
|
450
896
|
if (done && policy !== undefined) {
|
|
451
897
|
const key = state === 'approved' ? 'onApproved' : state === 'rejected' ? 'onRejected' : 'onSentBack';
|
|
@@ -481,6 +927,7 @@ export async function withdraw(env, model, id) {
|
|
|
481
927
|
await ref(su, 'approval.request')
|
|
482
928
|
.browse(pending.map((r) => r.id))
|
|
483
929
|
.write({ state: 'cancelled' });
|
|
930
|
+
await logEvents(su, pending.map((r) => ({ request: r.id, kind: 'cancelled', user: actor, note: 'Withdrawn.' })));
|
|
484
931
|
await audit(su, {
|
|
485
932
|
kind: 'approval.withdrawn',
|
|
486
933
|
summary: `Approvals withdrawn for ${model} ${id}.`,
|
|
@@ -501,6 +948,179 @@ export async function withdraw(env, model, id) {
|
|
|
501
948
|
return pending.length;
|
|
502
949
|
});
|
|
503
950
|
}
|
|
951
|
+
const managesApprovals = (env) => env.administrator || env.groups.has('approvals.manager');
|
|
952
|
+
/**
|
|
953
|
+
* Reassigns a pending request (RFC 0027 §2.6), for Approvals managers: its approvers replaced by `users`, or `users`
|
|
954
|
+
* added to them (a role's holders named first); never the requester; a reason required; logged, audited and
|
|
955
|
+
* announced (`approval.reassigned`, with the people added). @throws {HttpProblem} 403 / 404 / 409 / 422
|
|
956
|
+
*/
|
|
957
|
+
export async function reassign(env, requestId, users, mode, reason) {
|
|
958
|
+
if (!managesApprovals(env))
|
|
959
|
+
refuse(403, 'access', 'Approvals managers', 'Reassigning needs the Approvals manager role.');
|
|
960
|
+
const text = reason.trim();
|
|
961
|
+
if (text === '')
|
|
962
|
+
refuse(422, 'reason-required', 'Say why', 'A reassignment says why.');
|
|
963
|
+
const actor = env.uid;
|
|
964
|
+
return env.asSuperuser({ reason: `reassign approval ${requestId}` }, async (su) => {
|
|
965
|
+
const [request] = await loadRequests(su, { id: requestId }, ['id asc']);
|
|
966
|
+
if (request === undefined)
|
|
967
|
+
refuse(404, 'missing-record', 'Not found', `No approval request ${requestId}.`);
|
|
968
|
+
if (request.state !== 'pending')
|
|
969
|
+
refuse(409, 'approval-decided', 'Already decided', `This request is ${request.state}.`);
|
|
970
|
+
const chosen = [...new Set(users)];
|
|
971
|
+
if (chosen.length === 0)
|
|
972
|
+
refuse(422, 'validation', 'Nobody chosen', 'Choose who decides.');
|
|
973
|
+
if (request.requestedBy !== null && chosen.includes(request.requestedBy))
|
|
974
|
+
refuse(422, 'separation-of-duties', 'Separation of duties', 'The requester cannot approve their own record.');
|
|
975
|
+
const found = await find(su, 'res.users', { id: { in: chosen } }, ['id']);
|
|
976
|
+
if (found.length !== chosen.length)
|
|
977
|
+
refuse(422, 'validation', 'Unknown people', 'Every person must be a user.');
|
|
978
|
+
const before = request.approvers.length > 0
|
|
979
|
+
? [...request.approvers]
|
|
980
|
+
: request.approverRole === ''
|
|
981
|
+
? []
|
|
982
|
+
: (await holders(su, request.approverRole, request.company)).filter((u) => u !== request.requestedBy);
|
|
983
|
+
const next = mode === 'replace' ? chosen : [...new Set([...before, ...chosen])];
|
|
984
|
+
const required = Math.min(request.required, Math.max(1, next.length));
|
|
985
|
+
await ref(su, 'approval.request')
|
|
986
|
+
.browse([request.id])
|
|
987
|
+
.write({ approvers: { set: next }, required });
|
|
988
|
+
const note = `${await namesOf(su, before)} → ${await namesOf(su, next)}. ${text}`;
|
|
989
|
+
await logEvents(su, [{ request: request.id, kind: 'reassigned', user: actor, note }]);
|
|
990
|
+
await audit(su, {
|
|
991
|
+
kind: 'approval.reassigned',
|
|
992
|
+
summary: `${request.levelName} reassigned for ${request.targetModel} ${request.targetId}.`,
|
|
993
|
+
actor,
|
|
994
|
+
company: request.company,
|
|
995
|
+
targetModel: request.targetModel,
|
|
996
|
+
targetId: request.targetId,
|
|
997
|
+
details: { request: request.id, before, after: next, reason: text },
|
|
998
|
+
});
|
|
999
|
+
const added = next.filter((u) => !before.includes(u));
|
|
1000
|
+
if (added.length > 0 && request.startedAt !== null)
|
|
1001
|
+
await su.emit('approval.reassigned', { request: request.id, users: added });
|
|
1002
|
+
const [after] = await loadRequests(su, { id: request.id }, ['id asc']);
|
|
1003
|
+
return after;
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Tells a current request's backups that it waits (RFC 0027 §2.6): for Approvals managers and the request's
|
|
1008
|
+
* approvers; logged and announced (`approval.backups`). @throws {HttpProblem} 403 / 404 / 409
|
|
1009
|
+
*/
|
|
1010
|
+
export async function notifyBackups(env, requestId) {
|
|
1011
|
+
const actor = env.uid;
|
|
1012
|
+
return env.asSuperuser({ reason: `notify backups of approval ${requestId}` }, async (su) => {
|
|
1013
|
+
const [request] = await loadRequests(su, { id: requestId }, ['id asc']);
|
|
1014
|
+
if (request === undefined)
|
|
1015
|
+
refuse(404, 'missing-record', 'Not found', `No approval request ${requestId}.`);
|
|
1016
|
+
if (!managesApprovals(env) && !request.approvers.includes(actor))
|
|
1017
|
+
refuse(403, 'access', 'Not an approver', 'Approvers and Approvals managers tell backups.');
|
|
1018
|
+
if (request.state !== 'pending' || request.startedAt === null)
|
|
1019
|
+
refuse(409, 'approval-not-current', 'Not waiting', 'Only a request whose turn it is.');
|
|
1020
|
+
if (request.backups.length === 0)
|
|
1021
|
+
refuse(409, 'no-backups', 'No backups', 'This level has no backups.');
|
|
1022
|
+
await logEvents(su, [
|
|
1023
|
+
{
|
|
1024
|
+
request: request.id,
|
|
1025
|
+
kind: 'notified',
|
|
1026
|
+
user: actor,
|
|
1027
|
+
note: await namesOf(su, request.backups),
|
|
1028
|
+
},
|
|
1029
|
+
]);
|
|
1030
|
+
await su.emit('approval.backups', { request: request.id, users: [...request.backups] });
|
|
1031
|
+
return request.backups.length;
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
/** The fields of a level that decide its approvers: a change to one is synced to its waiting requests. */
|
|
1035
|
+
export const APPROVER_KEYS = [
|
|
1036
|
+
'approverKind',
|
|
1037
|
+
'approverRole',
|
|
1038
|
+
'approverUsers',
|
|
1039
|
+
'extraUsers',
|
|
1040
|
+
'backupUsers',
|
|
1041
|
+
'approverField',
|
|
1042
|
+
'quorum',
|
|
1043
|
+
'quorumCount',
|
|
1044
|
+
];
|
|
1045
|
+
/**
|
|
1046
|
+
* Keeps waiting requests in step with their levels (RFC 0027 §2.6), as the system: each pending request of these
|
|
1047
|
+
* levels gets its approvers found again — added to the ones it has, or replacing them, as its policy says (`mode`
|
|
1048
|
+
* overrides it, for "Sync now") — logged when they change. Returns what changed, with the people added to requests
|
|
1049
|
+
* whose turn it is (to tell, when the policy says so).
|
|
1050
|
+
*/
|
|
1051
|
+
export async function syncLevels(su, levelIds, mode) {
|
|
1052
|
+
const out = [];
|
|
1053
|
+
if (levelIds.length === 0)
|
|
1054
|
+
return out;
|
|
1055
|
+
const levels = await find(su, 'approval.level', { id: { in: [...levelIds] } }, [
|
|
1056
|
+
'policy',
|
|
1057
|
+
'name',
|
|
1058
|
+
'approverKind',
|
|
1059
|
+
'approverRole',
|
|
1060
|
+
'approverUsers',
|
|
1061
|
+
'extraUsers',
|
|
1062
|
+
'backupUsers',
|
|
1063
|
+
'approverField',
|
|
1064
|
+
'quorum',
|
|
1065
|
+
'quorumCount',
|
|
1066
|
+
]);
|
|
1067
|
+
for (const level of levels) {
|
|
1068
|
+
const [policy] = await find(su, 'approval.policy', { id: level['policy'] }, [
|
|
1069
|
+
'syncPending',
|
|
1070
|
+
'notifyOnSync',
|
|
1071
|
+
]);
|
|
1072
|
+
const how = mode ?? String(policy?.['syncPending'] ?? 'add');
|
|
1073
|
+
if (how === 'off')
|
|
1074
|
+
continue;
|
|
1075
|
+
const pending = await loadRequests(su, { level: level['id'], state: 'pending' }, ['id asc']);
|
|
1076
|
+
for (const request of pending) {
|
|
1077
|
+
const requester = request.requestedBy ?? 0;
|
|
1078
|
+
const who = await approversOf(su, level, { model: request.targetModel, id: request.targetId, company: request.company }, requester);
|
|
1079
|
+
const levelBackups = (level['backupUsers'] ?? []).filter((u) => u !== requester);
|
|
1080
|
+
const approvers = how === 'add'
|
|
1081
|
+
? [...new Set([...request.approvers, ...who.approvers])]
|
|
1082
|
+
: who.approvers.length > 0 || who.role !== ''
|
|
1083
|
+
? who.approvers
|
|
1084
|
+
: request.approvers;
|
|
1085
|
+
const backups = how === 'add' ? [...new Set([...request.backups, ...levelBackups])] : levelBackups;
|
|
1086
|
+
const same = (a, b) => a.length === b.length && a.every((x) => b.includes(x));
|
|
1087
|
+
const role = how === 'replace' ? who.role : request.approverRole;
|
|
1088
|
+
if (same(approvers, request.approvers) &&
|
|
1089
|
+
same(backups, request.backups) &&
|
|
1090
|
+
role === request.approverRole)
|
|
1091
|
+
continue;
|
|
1092
|
+
await ref(su, 'approval.request')
|
|
1093
|
+
.browse([request.id])
|
|
1094
|
+
.write({
|
|
1095
|
+
approvers: { set: approvers },
|
|
1096
|
+
backups: { set: backups },
|
|
1097
|
+
approverRole: role,
|
|
1098
|
+
required: how === 'replace' ? who.required : request.required,
|
|
1099
|
+
explanation: who.explanation.join(' '),
|
|
1100
|
+
});
|
|
1101
|
+
await logEvents(su, [
|
|
1102
|
+
{
|
|
1103
|
+
request: request.id,
|
|
1104
|
+
kind: 'synced',
|
|
1105
|
+
note: `${how === 'add' ? 'Added' : 'Replaced'}: ${await namesOf(su, approvers)}${backups.length > 0 ? `; backups: ${await namesOf(su, backups)}` : ''}.`,
|
|
1106
|
+
},
|
|
1107
|
+
]);
|
|
1108
|
+
const [after] = await loadRequests(su, { id: request.id }, ['id asc']);
|
|
1109
|
+
if (after === undefined)
|
|
1110
|
+
continue;
|
|
1111
|
+
const added = [
|
|
1112
|
+
...approvers.filter((u) => !request.approvers.includes(u)),
|
|
1113
|
+
...backups.filter((u) => !request.backups.includes(u)),
|
|
1114
|
+
];
|
|
1115
|
+
out.push({
|
|
1116
|
+
request: after,
|
|
1117
|
+
added: request.startedAt === null ? [] : added,
|
|
1118
|
+
notify: policy?.['notifyOnSync'] !== false,
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
return out;
|
|
1123
|
+
}
|
|
504
1124
|
/** `purchase.order` as people say it: "Purchase order". */
|
|
505
1125
|
const labelOf = (model) => {
|
|
506
1126
|
const words = model.replace(/[._]/g, ' ');
|
|
@@ -698,10 +1318,122 @@ export async function approvalStats(env) {
|
|
|
698
1318
|
};
|
|
699
1319
|
});
|
|
700
1320
|
}
|
|
1321
|
+
/** The periods of the matrix dashboard, in days (`all`: no limit). */
|
|
1322
|
+
const MATRIX_DAYS = { '7d': 7, '30d': 30, '3m': 91, '6m': 182, '1y': 365, all: null };
|
|
1323
|
+
export const MATRIX_PERIODS = Object.keys(MATRIX_DAYS);
|
|
1324
|
+
/**
|
|
1325
|
+
* The matrix dashboard (RFC 0027 §2.7): the requests of the period the user decides — directly (named, or holding
|
|
1326
|
+
* the role) or delegated (as a backup or for someone away) — a card per policy with what waits, what was approved
|
|
1327
|
+
* and rejected; for Approvals managers also the hours each level takes to decide and the bottlenecks (who has the
|
|
1328
|
+
* most requests late).
|
|
1329
|
+
*/
|
|
1330
|
+
export async function approvalMatrix(env, period, mode) {
|
|
1331
|
+
const manages = managesApprovals(env);
|
|
1332
|
+
return env.asSuperuser({ reason: 'approval matrix' }, async (su) => {
|
|
1333
|
+
const who = await whoIs(su, env);
|
|
1334
|
+
const days = MATRIX_DAYS[period];
|
|
1335
|
+
const since = days === null ? null : new Date(Date.now() - days * 86_400_000);
|
|
1336
|
+
const rows = await loadRequests(su, {
|
|
1337
|
+
state: { ne: 'cancelled' },
|
|
1338
|
+
...(since === null ? {} : { createdAt: { gte: since } }),
|
|
1339
|
+
}, ['id asc']);
|
|
1340
|
+
const mine = (r) => r.approvers.includes(who.uid) ||
|
|
1341
|
+
(r.approvers.length === 0 &&
|
|
1342
|
+
r.approverRole !== '' &&
|
|
1343
|
+
(r.company === null || r.company === who.companyId) &&
|
|
1344
|
+
who.groups.has(r.approverRole));
|
|
1345
|
+
const forOthers = (r) => !mine(r) &&
|
|
1346
|
+
(r.backups.includes(who.uid) ||
|
|
1347
|
+
who.delegators.some((d) => d.user !== r.requestedBy &&
|
|
1348
|
+
(r.approvers.includes(d.user) ||
|
|
1349
|
+
(r.approvers.length === 0 && holds(d.roles, r.approverRole, r.company)))));
|
|
1350
|
+
const waitingNow = (r) => r.state === 'pending' && r.startedAt !== null;
|
|
1351
|
+
const scoped = rows.filter((r) => r.requestedBy !== who.uid);
|
|
1352
|
+
const direct = scoped.filter(mine);
|
|
1353
|
+
const delegated = scoped.filter(forOthers);
|
|
1354
|
+
const names = new Map((await find(
|
|
1355
|
+
// Archived policies keep their requests' history.
|
|
1356
|
+
su.withFlags({ activeTest: false }), 'approval.policy', {
|
|
1357
|
+
id: {
|
|
1358
|
+
in: [...new Set(rows.map((r) => r.policy).filter((p) => p !== null))],
|
|
1359
|
+
},
|
|
1360
|
+
}, ['name'])).map((p) => [p['id'], String(p['name'])]));
|
|
1361
|
+
const cards = new Map();
|
|
1362
|
+
for (const r of mode === 'direct' ? direct : delegated) {
|
|
1363
|
+
const key = String(r.policy ?? 0);
|
|
1364
|
+
const card = cards.get(key) ?? {
|
|
1365
|
+
policy: r.policy,
|
|
1366
|
+
title: (r.policy === null ? undefined : names.get(r.policy)) ?? r.targetModel,
|
|
1367
|
+
waiting: 0,
|
|
1368
|
+
approved: 0,
|
|
1369
|
+
rejected: 0,
|
|
1370
|
+
total: 0,
|
|
1371
|
+
};
|
|
1372
|
+
if (r.state === 'pending' && !waitingNow(r))
|
|
1373
|
+
continue; // a later level: not anyone's turn yet
|
|
1374
|
+
cards.set(key, {
|
|
1375
|
+
...card,
|
|
1376
|
+
waiting: card.waiting + (waitingNow(r) ? 1 : 0),
|
|
1377
|
+
approved: card.approved + (r.state === 'approved' ? 1 : 0),
|
|
1378
|
+
rejected: card.rejected + (r.state === 'rejected' || r.state === 'sent_back' ? 1 : 0),
|
|
1379
|
+
total: card.total + 1,
|
|
1380
|
+
});
|
|
1381
|
+
}
|
|
1382
|
+
let levels = null;
|
|
1383
|
+
let bottlenecks = null;
|
|
1384
|
+
if (manages) {
|
|
1385
|
+
const byLevel = new Map();
|
|
1386
|
+
for (const r of rows) {
|
|
1387
|
+
if (r.decidedAt === null || r.startedAt === null || r.state === 'pending')
|
|
1388
|
+
continue;
|
|
1389
|
+
const policy = (r.policy === null ? undefined : names.get(r.policy)) ?? r.targetModel;
|
|
1390
|
+
const key = `${policy}\u0000${r.levelName}`;
|
|
1391
|
+
const entry = byLevel.get(key) ?? { policy, level: r.levelName, hours: [] };
|
|
1392
|
+
entry.hours.push((Date.parse(r.decidedAt) - Date.parse(r.startedAt)) / 3_600_000);
|
|
1393
|
+
byLevel.set(key, entry);
|
|
1394
|
+
}
|
|
1395
|
+
levels = [...byLevel.values()]
|
|
1396
|
+
.map((e) => ({
|
|
1397
|
+
policy: e.policy,
|
|
1398
|
+
level: e.level,
|
|
1399
|
+
decided: e.hours.length,
|
|
1400
|
+
averageHours: Math.round((e.hours.reduce((a, b) => a + b, 0) / e.hours.length) * 10) / 10,
|
|
1401
|
+
}))
|
|
1402
|
+
.sort((a, b) => b.averageHours - a.averageHours);
|
|
1403
|
+
const load = new Map();
|
|
1404
|
+
const now = Date.now();
|
|
1405
|
+
for (const r of rows.filter(waitingNow))
|
|
1406
|
+
for (const u of r.approvers) {
|
|
1407
|
+
const entry = load.get(u) ?? { late: 0, waiting: 0 };
|
|
1408
|
+
entry.waiting += 1;
|
|
1409
|
+
if (r.dueAt !== null && Date.parse(r.dueAt) <= now)
|
|
1410
|
+
entry.late += 1;
|
|
1411
|
+
load.set(u, entry);
|
|
1412
|
+
}
|
|
1413
|
+
const people = new Map(load.size === 0
|
|
1414
|
+
? []
|
|
1415
|
+
: (await find(su, 'res.users', { id: { in: [...load.keys()] } }, ['name'])).map((u) => [
|
|
1416
|
+
u['id'],
|
|
1417
|
+
String(u['name']),
|
|
1418
|
+
]));
|
|
1419
|
+
bottlenecks = [...load.entries()]
|
|
1420
|
+
.map(([user, l]) => ({ user, name: people.get(user) ?? `#${user}`, ...l }))
|
|
1421
|
+
.sort((a, b) => b.late - a.late || b.waiting - a.waiting)
|
|
1422
|
+
.slice(0, 10);
|
|
1423
|
+
}
|
|
1424
|
+
return {
|
|
1425
|
+
cards: [...cards.values()].sort((a, b) => a.title.localeCompare(b.title)),
|
|
1426
|
+
direct: direct.filter(waitingNow).length,
|
|
1427
|
+
delegated: delegated.filter(waitingNow).length,
|
|
1428
|
+
levels,
|
|
1429
|
+
bottlenecks,
|
|
1430
|
+
};
|
|
1431
|
+
});
|
|
1432
|
+
}
|
|
701
1433
|
/** The people who may decide `request` now: its approvers (or its role's holders), the escalation role's once late,
|
|
702
1434
|
* and whoever they delegated to today; never the requester. */
|
|
703
1435
|
export async function decidersOf(su, request, today) {
|
|
704
|
-
const people = new Set(request.approvers);
|
|
1436
|
+
const people = new Set([...request.approvers, ...request.backups]);
|
|
705
1437
|
if (request.approvers.length === 0 && request.approverRole !== '')
|
|
706
1438
|
for (const u of await holders(su, request.approverRole, request.company))
|
|
707
1439
|
people.add(u);
|
|
@@ -733,11 +1465,12 @@ export async function sweepDeadlines(su, now = new Date()) {
|
|
|
733
1465
|
'remindedAt',
|
|
734
1466
|
'escalatedTo',
|
|
735
1467
|
'createdAt',
|
|
1468
|
+
'startedAt',
|
|
736
1469
|
'level',
|
|
737
1470
|
]);
|
|
738
1471
|
for (const row of rows) {
|
|
739
1472
|
const due = new Date(row['dueAt']).getTime();
|
|
740
|
-
const start = new Date(row['createdAt']).getTime();
|
|
1473
|
+
const start = new Date((row['startedAt'] ?? row['createdAt'])).getTime();
|
|
741
1474
|
const [request] = await loadRequests(su, { id: row['id'] }, ['id asc']);
|
|
742
1475
|
if (request === undefined)
|
|
743
1476
|
continue;
|
|
@@ -763,6 +1496,7 @@ export async function sweepDeadlines(su, now = new Date()) {
|
|
|
763
1496
|
targetId: request.targetId,
|
|
764
1497
|
details: { request: request.id, role },
|
|
765
1498
|
});
|
|
1499
|
+
await logEvents(su, [{ request: request.id, kind: 'escalated', note: role, at: now }]);
|
|
766
1500
|
await su.emit('approval.escalated', { request: request.id, role });
|
|
767
1501
|
out.push({ kind: 'escalated', request: { ...request, escalatedTo: role } });
|
|
768
1502
|
continue;
|
|
@@ -770,16 +1504,19 @@ export async function sweepDeadlines(su, now = new Date()) {
|
|
|
770
1504
|
}
|
|
771
1505
|
if ((row['remindedAt'] ?? null) === null && now.getTime() >= start + (due - start) / 2) {
|
|
772
1506
|
await ref(su, 'approval.request').browse([request.id]).write({ remindedAt: now });
|
|
1507
|
+
await logEvents(su, [{ request: request.id, kind: 'reminded', at: now }]);
|
|
773
1508
|
out.push({ kind: 'reminded', request });
|
|
774
1509
|
}
|
|
775
1510
|
}
|
|
776
1511
|
return out;
|
|
777
1512
|
}
|
|
778
1513
|
/** One-time links deciding `request` from an email, one per person who may decide it now (7 days). */
|
|
779
|
-
export async function issueLinks(su, request, today, newToken, digestOf) {
|
|
1514
|
+
export async function issueLinks(su, request, today, newToken, digestOf, only) {
|
|
780
1515
|
const out = [];
|
|
781
1516
|
const expiresAt = new Date(Date.now() + 7 * 86_400_000);
|
|
782
1517
|
for (const user of await decidersOf(su, request, today)) {
|
|
1518
|
+
if (only !== undefined && !only.includes(user))
|
|
1519
|
+
continue;
|
|
783
1520
|
const token = newToken();
|
|
784
1521
|
await ref(su, 'approval.link').create([
|
|
785
1522
|
{ request: request.id, user, tokenHash: digestOf(token), expiresAt },
|
|
@@ -859,12 +1596,24 @@ export async function installApprovalPolicies(env, modules) {
|
|
|
859
1596
|
name: level.name,
|
|
860
1597
|
condition: level.condition ?? null,
|
|
861
1598
|
approverRole: level.approverRole,
|
|
862
|
-
approverKind: level.approver === '
|
|
863
|
-
?
|
|
1599
|
+
approverKind: typeof level.approver === 'string'
|
|
1600
|
+
? level.approver
|
|
864
1601
|
: typeof level.approver === 'object'
|
|
865
|
-
? '
|
|
1602
|
+
? 'record' in level.approver
|
|
1603
|
+
? 'record'
|
|
1604
|
+
: 'employee' in level.approver
|
|
1605
|
+
? 'employee'
|
|
1606
|
+
: 'field'
|
|
866
1607
|
: 'role',
|
|
867
|
-
...(typeof level.approver === 'object'
|
|
1608
|
+
...(typeof level.approver === 'object'
|
|
1609
|
+
? {
|
|
1610
|
+
approverField: 'record' in level.approver
|
|
1611
|
+
? level.approver.record
|
|
1612
|
+
: 'employee' in level.approver
|
|
1613
|
+
? level.approver.employee
|
|
1614
|
+
: level.approver.field,
|
|
1615
|
+
}
|
|
1616
|
+
: {}),
|
|
868
1617
|
quorum: level.quorum === undefined || level.quorum === 'any'
|
|
869
1618
|
? 'any'
|
|
870
1619
|
: level.quorum === 'all'
|