shutro-erp 0.3.5 → 0.3.6
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.d.ts +22 -0
- package/dist/account_asset/.kosh/bindings.js +15 -0
- package/dist/account_asset/.kosh/schema.d.ts +1755 -0
- package/dist/account_asset/board.d.ts +61 -0
- package/dist/account_asset/board.js +86 -0
- package/dist/account_asset/books.d.ts +42 -0
- package/dist/account_asset/books.js +398 -0
- package/dist/account_asset/demo.d.ts +7 -0
- package/dist/account_asset/demo.js +72 -0
- package/dist/account_asset/index.d.ts +9 -0
- package/dist/account_asset/index.js +335 -0
- package/dist/account_asset/module.d.ts +7622 -0
- package/dist/account_asset/module.js +230 -0
- package/dist/account_budget/.kosh/bindings.d.ts +22 -0
- package/dist/account_budget/.kosh/bindings.js +15 -0
- package/dist/account_budget/.kosh/schema.d.ts +1767 -0
- package/dist/account_budget/budget.d.ts +5 -0
- package/dist/account_budget/budget.js +142 -0
- package/dist/account_budget/demo.d.ts +6 -0
- package/dist/account_budget/demo.js +30 -0
- package/dist/account_budget/index.d.ts +4 -0
- package/dist/account_budget/index.js +223 -0
- package/dist/account_budget/module.d.ts +13863 -0
- package/dist/account_budget/module.js +167 -0
- package/dist/account_portal/.kosh/bindings.js +1 -1
- package/dist/account_portal/.kosh/schema.d.ts +123 -4
- package/dist/account_portal/module.d.ts +146 -0
- package/dist/accounting/.kosh/bindings.js +1 -1
- package/dist/accounting/.kosh/schema.d.ts +123 -4
- package/dist/accounting/index.d.ts +1 -1
- package/dist/accounting/index.js +131 -3
- package/dist/accounting/module.d.ts +296 -0
- package/dist/accounting/module.js +32 -1
- package/dist/approvals/.kosh/bindings.d.ts +22 -0
- package/dist/approvals/.kosh/bindings.js +15 -0
- package/dist/approvals/.kosh/schema.d.ts +611 -0
- package/dist/approvals/index.d.ts +18 -0
- package/dist/approvals/index.js +302 -0
- package/dist/approvals/mails.d.ts +25 -0
- package/dist/approvals/mails.js +82 -0
- package/dist/approvals/module.d.ts +1 -0
- package/dist/approvals/module.js +12 -0
- package/dist/approvals/web/History.d.ts +4 -0
- package/dist/approvals/web/History.js +13 -0
- package/dist/approvals/web/Inbox.d.ts +9 -0
- package/dist/approvals/web/Inbox.js +114 -0
- package/dist/approvals/web/Mine.d.ts +7 -0
- package/dist/approvals/web/Mine.js +27 -0
- package/dist/approvals/web/index.d.ts +13 -0
- package/dist/approvals/web/index.js +22 -0
- package/dist/approvals/web/messages.d.ts +2 -0
- package/dist/approvals/web/messages.js +119 -0
- package/dist/approvals/web/shared.d.ts +59 -0
- package/dist/approvals/web/shared.js +46 -0
- package/dist/attendance/.kosh/bindings.d.ts +22 -0
- package/dist/attendance/.kosh/bindings.js +15 -0
- package/dist/attendance/.kosh/schema.d.ts +1048 -0
- package/dist/attendance/days.d.ts +96 -0
- package/dist/attendance/days.js +430 -0
- package/dist/attendance/demo.d.ts +7 -0
- package/dist/attendance/demo.js +58 -0
- package/dist/attendance/index.d.ts +4 -0
- package/dist/attendance/index.js +513 -0
- package/dist/attendance/module.d.ts +3969 -0
- package/dist/attendance/module.js +412 -0
- package/dist/attendance/routes.d.ts +16 -0
- package/dist/attendance/routes.js +266 -0
- package/dist/attendance/web/Check.d.ts +8 -0
- package/dist/attendance/web/Check.js +40 -0
- package/dist/attendance/web/Kiosk.d.ts +8 -0
- package/dist/attendance/web/Kiosk.js +62 -0
- package/dist/attendance/web/PinButton.d.ts +7 -0
- package/dist/attendance/web/PinButton.js +29 -0
- package/dist/attendance/web/index.d.ts +13 -0
- package/dist/attendance/web/index.js +22 -0
- package/dist/attendance/web/messages.d.ts +2 -0
- package/dist/attendance/web/messages.js +61 -0
- package/dist/cli/init.js +32 -0
- package/dist/cli/templates.d.ts +1 -1
- package/dist/cli/templates.js +89 -0
- package/dist/contacts/module.js +13 -2
- package/dist/crm/.kosh/bindings.js +1 -1
- package/dist/crm/.kosh/schema.d.ts +2 -1
- package/dist/crm/module.d.ts +2 -0
- package/dist/crm/module.js +2 -1
- package/dist/documents/.kosh/bindings.d.ts +22 -0
- package/dist/documents/.kosh/bindings.js +15 -0
- package/dist/documents/.kosh/schema.d.ts +862 -0
- package/dist/documents/demo.d.ts +6 -0
- package/dist/documents/demo.js +22 -0
- package/dist/documents/documents.d.ts +98 -0
- package/dist/documents/documents.js +318 -0
- package/dist/documents/index.d.ts +30 -0
- package/dist/documents/index.js +442 -0
- package/dist/documents/module.d.ts +2781 -0
- package/dist/documents/module.js +214 -0
- package/dist/documents/web/Versions.d.ts +7 -0
- package/dist/documents/web/Versions.js +45 -0
- package/dist/documents/web/index.d.ts +11 -0
- package/dist/documents/web/index.js +16 -0
- package/dist/documents/web/messages.d.ts +2 -0
- package/dist/documents/web/messages.js +21 -0
- package/dist/event_account/.kosh/bindings.js +1 -1
- package/dist/event_account/.kosh/schema.d.ts +123 -4
- package/dist/event_account/module.d.ts +146 -0
- package/dist/expense/.kosh/bindings.d.ts +22 -0
- package/dist/expense/.kosh/bindings.js +15 -0
- package/dist/expense/.kosh/schema.d.ts +1299 -0
- package/dist/expense/demo.d.ts +7 -0
- package/dist/expense/demo.js +69 -0
- package/dist/expense/index.d.ts +3 -0
- package/dist/expense/index.js +495 -0
- package/dist/expense/module.d.ts +4747 -0
- package/dist/expense/module.js +647 -0
- package/dist/expense_account/.kosh/bindings.d.ts +22 -0
- package/dist/expense_account/.kosh/bindings.js +15 -0
- package/dist/expense_account/.kosh/schema.d.ts +1936 -0
- package/dist/expense_account/index.d.ts +3 -0
- package/dist/expense_account/index.js +74 -0
- package/dist/expense_account/module.d.ts +9153 -0
- package/dist/expense_account/module.js +318 -0
- package/dist/helpdesk/.kosh/bindings.d.ts +22 -0
- package/dist/helpdesk/.kosh/bindings.js +15 -0
- package/dist/helpdesk/.kosh/schema.d.ts +885 -0
- package/dist/helpdesk/demo.d.ts +7 -0
- package/dist/helpdesk/demo.js +65 -0
- package/dist/helpdesk/hours.d.ts +17 -0
- package/dist/helpdesk/hours.js +94 -0
- package/dist/helpdesk/index.d.ts +10 -0
- package/dist/helpdesk/index.js +461 -0
- package/dist/helpdesk/module.d.ts +2940 -0
- package/dist/helpdesk/module.js +326 -0
- package/dist/helpdesk/routes.d.ts +12 -0
- package/dist/helpdesk/routes.js +132 -0
- package/dist/helpdesk/sla.d.ts +37 -0
- package/dist/helpdesk/sla.js +226 -0
- package/dist/hr/index.d.ts +1 -1
- package/dist/hr/index.js +1 -1
- package/dist/l10n_bd/.kosh/bindings.js +1 -1
- package/dist/l10n_bd/.kosh/schema.d.ts +123 -4
- package/dist/l10n_bd/module.d.ts +146 -0
- package/dist/l10n_bd_payroll/.kosh/bindings.d.ts +22 -0
- package/dist/l10n_bd_payroll/.kosh/bindings.js +15 -0
- package/dist/l10n_bd_payroll/.kosh/schema.d.ts +2312 -0
- package/dist/l10n_bd_payroll/data.d.ts +47 -0
- package/dist/l10n_bd_payroll/data.js +347 -0
- package/dist/l10n_bd_payroll/index.d.ts +4 -0
- package/dist/l10n_bd_payroll/index.js +24 -0
- package/dist/l10n_bd_payroll/module.d.ts +12932 -0
- package/dist/l10n_bd_payroll/module.js +86 -0
- package/dist/l10n_in/.kosh/bindings.js +1 -1
- package/dist/l10n_in/.kosh/schema.d.ts +123 -4
- package/dist/l10n_in/module.d.ts +147 -0
- package/dist/mail/inbound/handle.js +6 -1
- package/dist/maintenance_stock/.kosh/bindings.js +1 -1
- package/dist/maintenance_stock/.kosh/schema.d.ts +77 -2
- package/dist/maintenance_stock/module.d.ts +788 -66
- package/dist/modules-sdk/index.d.ts +1 -1
- package/dist/modules-sdk/manifest.d.ts +39 -2
- package/dist/mrp/.kosh/bindings.js +1 -1
- package/dist/mrp/.kosh/schema.d.ts +77 -2
- package/dist/mrp/index.d.ts +1 -1
- package/dist/mrp/index.js +2 -1
- package/dist/mrp/module.d.ts +781 -54
- package/dist/mrp/module.js +64 -2
- package/dist/mrp_account/.kosh/bindings.js +1 -1
- package/dist/mrp_account/.kosh/schema.d.ts +198 -5
- package/dist/mrp_account/module.d.ts +4852 -2541
- package/dist/payment/.kosh/bindings.js +1 -1
- package/dist/payment/.kosh/schema.d.ts +125 -5
- package/dist/payment/index.d.ts +41 -0
- package/dist/payment/index.js +131 -74
- package/dist/payment/module.d.ts +210 -6
- package/dist/payment/module.js +19 -3
- package/dist/payment/settle.js +10 -1
- package/dist/payroll/.kosh/bindings.d.ts +22 -0
- package/dist/payroll/.kosh/bindings.js +15 -0
- package/dist/payroll/.kosh/schema.d.ts +1390 -0
- package/dist/payroll/compute.d.ts +35 -0
- package/dist/payroll/compute.js +398 -0
- package/dist/payroll/demo.d.ts +55 -0
- package/dist/payroll/demo.js +121 -0
- package/dist/payroll/formula.d.ts +62 -0
- package/dist/payroll/formula.js +320 -0
- package/dist/payroll/index.d.ts +230 -0
- package/dist/payroll/index.js +713 -0
- package/dist/payroll/module.d.ts +9390 -0
- package/dist/payroll/module.js +606 -0
- package/dist/payroll/routes.d.ts +10 -0
- package/dist/payroll/routes.js +112 -0
- package/dist/payroll_account/.kosh/bindings.d.ts +22 -0
- package/dist/payroll_account/.kosh/bindings.js +15 -0
- package/dist/payroll_account/.kosh/schema.d.ts +2308 -0
- package/dist/payroll_account/index.d.ts +3 -0
- package/dist/payroll_account/index.js +40 -0
- package/dist/payroll_account/module.d.ts +12396 -0
- package/dist/payroll_account/module.js +256 -0
- package/dist/permissions/approvals.d.ts +131 -9
- package/dist/permissions/approvals.js +662 -57
- package/dist/permissions/index.d.ts +1 -1
- package/dist/permissions/index.js +2 -2
- package/dist/permissions/models.js +191 -12
- package/dist/permissions/routes.d.ts +6 -1
- package/dist/permissions/routes.js +37 -4
- package/dist/permissions/web/ApprovalPanel.d.ts +5 -4
- package/dist/permissions/web/ApprovalPanel.js +39 -13
- package/dist/permissions/web/index.d.ts +6 -4
- package/dist/permissions/web/index.js +13 -4
- package/dist/permissions/web/messages.d.ts +2 -0
- package/dist/permissions/web/messages.js +43 -0
- package/dist/portal/index.js +1 -1
- package/dist/pos/.kosh/bindings.js +1 -1
- package/dist/pos/.kosh/schema.d.ts +123 -4
- package/dist/pos/index.js +4 -0
- package/dist/pos/module.d.ts +147 -0
- package/dist/pos_stock/.kosh/bindings.js +1 -1
- package/dist/pos_stock/.kosh/schema.d.ts +198 -5
- package/dist/pos_stock/module.d.ts +914 -46
- package/dist/product/.kosh/bindings.js +1 -1
- package/dist/product/.kosh/schema.d.ts +2 -1
- package/dist/product/index.js +13 -0
- package/dist/product/module.d.ts +2 -0
- package/dist/product/module.js +2 -1
- package/dist/project/.kosh/bindings.js +1 -1
- package/dist/project/.kosh/schema.d.ts +672 -1
- package/dist/project/index.d.ts +3 -1
- package/dist/project/index.js +901 -14
- package/dist/project/module.d.ts +9892 -390
- package/dist/project/module.js +1530 -13
- package/dist/project/routes.d.ts +44 -0
- package/dist/project/routes.js +314 -0
- package/dist/project/schedule.d.ts +72 -0
- package/dist/project/schedule.js +192 -0
- package/dist/project/web/Refresh.d.ts +8 -0
- package/dist/project/web/Refresh.js +29 -0
- package/dist/project/web/Schedule.d.ts +9 -0
- package/dist/project/web/Schedule.js +93 -0
- package/dist/project/web/Structure.d.ts +8 -0
- package/dist/project/web/Structure.js +86 -0
- package/dist/project/web/Workload.d.ts +8 -0
- package/dist/project/web/Workload.js +43 -0
- package/dist/project/web/index.d.ts +14 -0
- package/dist/project/web/index.js +25 -0
- package/dist/project/web/messages.d.ts +2 -0
- package/dist/project/web/messages.js +85 -0
- package/dist/project_account/.kosh/bindings.d.ts +22 -0
- package/dist/project_account/.kosh/bindings.js +15 -0
- package/dist/project_account/.kosh/schema.d.ts +2241 -0
- package/dist/project_account/index.d.ts +3 -0
- package/dist/project_account/index.js +27 -0
- package/dist/project_account/module.d.ts +11539 -0
- package/dist/project_account/module.js +76 -0
- package/dist/project_portal/.kosh/bindings.js +1 -1
- package/dist/project_portal/.kosh/schema.d.ts +672 -1
- package/dist/project_portal/module.d.ts +4798 -129
- package/dist/project_purchase/.kosh/bindings.d.ts +22 -0
- package/dist/project_purchase/.kosh/bindings.js +15 -0
- package/dist/project_purchase/.kosh/schema.d.ts +2320 -0
- package/dist/project_purchase/index.d.ts +9 -0
- package/dist/project_purchase/index.js +22 -0
- package/dist/project_purchase/module.d.ts +25147 -0
- package/dist/project_purchase/module.js +56 -0
- package/dist/purchase/.kosh/bindings.js +1 -1
- package/dist/purchase/.kosh/schema.d.ts +97 -4
- package/dist/purchase/module.d.ts +13 -2
- package/dist/purchase/module.js +10 -1
- package/dist/purchase_account/.kosh/bindings.js +1 -1
- package/dist/purchase_account/.kosh/schema.d.ts +126 -6
- package/dist/purchase_account/index.js +1 -0
- package/dist/purchase_account/module.d.ts +176 -1
- package/dist/purchase_account/module.js +13 -2
- package/dist/purchase_portal/.kosh/bindings.js +1 -1
- package/dist/purchase_portal/.kosh/schema.d.ts +97 -4
- package/dist/purchase_portal/module.d.ts +6 -1
- package/dist/purchase_stock/.kosh/bindings.js +1 -1
- package/dist/purchase_stock/.kosh/schema.d.ts +174 -5
- package/dist/purchase_stock/index.d.ts +1 -1
- package/dist/purchase_stock/index.js +9 -2
- package/dist/purchase_stock/module.d.ts +840 -68
- package/dist/purchase_stock/module.js +242 -6
- package/dist/purchase_stock_account/.kosh/bindings.js +1 -1
- package/dist/purchase_stock_account/.kosh/schema.d.ts +203 -7
- package/dist/purchase_stock_account/module.d.ts +8163 -5514
- package/dist/purchase_stock_account/module.js +11 -5
- package/dist/recruitment/.kosh/bindings.js +1 -1
- package/dist/recruitment/.kosh/schema.d.ts +2 -1
- package/dist/recruitment/module.d.ts +2 -0
- package/dist/recruitment/module.js +2 -1
- package/dist/sale_account/.kosh/bindings.js +1 -1
- package/dist/sale_account/.kosh/schema.d.ts +123 -4
- package/dist/sale_account/module.d.ts +147 -0
- package/dist/sale_portal/.kosh/bindings.js +1 -1
- package/dist/sale_portal/.kosh/schema.d.ts +2 -1
- package/dist/sale_portal/module.d.ts +1 -0
- package/dist/sale_project/.kosh/bindings.js +1 -1
- package/dist/sale_project/.kosh/schema.d.ts +673 -1
- package/dist/sale_project/module.d.ts +4800 -129
- package/dist/sale_project/module.js +17 -1
- package/dist/sale_stock/.kosh/bindings.js +1 -1
- package/dist/sale_stock/.kosh/schema.d.ts +77 -2
- package/dist/sale_stock/module.d.ts +789 -66
- package/dist/sales/.kosh/bindings.js +1 -1
- package/dist/sales/.kosh/schema.d.ts +2 -1
- package/dist/sales/index.js +4 -0
- package/dist/sales/module.d.ts +1 -0
- package/dist/sign/.kosh/bindings.d.ts +22 -0
- package/dist/sign/.kosh/bindings.js +15 -0
- package/dist/sign/.kosh/schema.d.ts +990 -0
- package/dist/sign/index.d.ts +14 -0
- package/dist/sign/index.js +350 -0
- package/dist/sign/module.d.ts +4478 -0
- package/dist/sign/module.js +158 -0
- package/dist/sign/sign.d.ts +82 -0
- package/dist/sign/sign.js +474 -0
- package/dist/stock/.kosh/bindings.js +1 -1
- package/dist/stock/.kosh/schema.d.ts +77 -2
- package/dist/stock/demo.d.ts +2 -1
- package/dist/stock/demo.js +28 -1
- package/dist/stock/index.d.ts +2 -1
- package/dist/stock/index.js +233 -4
- package/dist/stock/module.d.ts +2363 -837
- package/dist/stock/module.js +569 -14
- package/dist/stock/replenish.d.ts +38 -0
- package/dist/stock/replenish.js +176 -0
- package/dist/stock/web/Replenishment.d.ts +8 -0
- package/dist/stock/web/Replenishment.js +84 -0
- package/dist/stock/web/index.d.ts +4 -2
- package/dist/stock/web/index.js +6 -2
- package/dist/stock/web/messages.js +52 -0
- package/dist/stock_account/.kosh/bindings.js +1 -1
- package/dist/stock_account/.kosh/schema.d.ts +198 -5
- package/dist/stock_account/index.js +10 -0
- package/dist/stock_account/module.d.ts +969 -101
- package/dist/trade_finance/index.js +2 -0
- package/dist/trade_finance/module.js +2 -0
- package/dist/trade_finance_account/.kosh/bindings.js +1 -1
- package/dist/trade_finance_account/.kosh/schema.d.ts +123 -4
- package/dist/trade_finance_account/index.js +1 -0
- package/dist/trade_finance_account/module.d.ts +146 -0
- 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 +97 -4
- package/dist/trade_finance_purchase/module.d.ts +6 -1
- package/dist/trade_finance_sale/.kosh/bindings.js +1 -1
- package/dist/trade_finance_sale/.kosh/schema.d.ts +2 -1
- package/dist/trade_finance_sale/module.d.ts +1 -0
- package/dist/ui/app.js +24 -12
- package/dist/ui/components/AppIcon.js +8 -1
- package/dist/ui/i18n/messages.d.ts +4 -0
- package/dist/ui/i18n/messages.js +8 -0
- package/dist/ui/views/FormView.js +4 -2
- package/dist/ui/views/KanbanView.js +9 -1
- package/dist/ui/views/ListView.js +4 -2
- package/dist/ui/views/widgets.js +7 -1
- package/dist/website/index.js +1 -0
- package/dist/website/module.js +1 -0
- package/dist/website_crm/.kosh/bindings.js +1 -1
- package/dist/website_crm/.kosh/schema.d.ts +2 -1
- package/dist/website_crm/module.d.ts +1 -0
- package/dist/website_sale/.kosh/bindings.d.ts +22 -0
- package/dist/website_sale/.kosh/bindings.js +15 -0
- package/dist/website_sale/.kosh/schema.d.ts +2284 -0
- package/dist/website_sale/couriers.d.ts +50 -0
- package/dist/website_sale/couriers.js +59 -0
- package/dist/website_sale/demo.d.ts +7 -0
- package/dist/website_sale/demo.js +90 -0
- package/dist/website_sale/growth.d.ts +31 -0
- package/dist/website_sale/growth.js +163 -0
- package/dist/website_sale/index.d.ts +49 -0
- package/dist/website_sale/index.js +538 -0
- package/dist/website_sale/module.d.ts +22027 -0
- package/dist/website_sale/module.js +397 -0
- package/dist/website_sale/shipments.d.ts +16 -0
- package/dist/website_sale/shipments.js +113 -0
- package/dist/website_sale/shop.d.ts +81 -0
- package/dist/website_sale/shop.js +649 -0
- package/dist/website_sale_stock/.kosh/bindings.d.ts +22 -0
- package/dist/website_sale_stock/.kosh/bindings.js +15 -0
- package/dist/website_sale_stock/.kosh/schema.d.ts +2681 -0
- package/dist/website_sale_stock/index.d.ts +9 -0
- package/dist/website_sale_stock/index.js +20 -0
- package/dist/website_sale_stock/module.d.ts +24998 -0
- package/dist/website_sale_stock/module.js +28 -0
- package/package.json +83 -2
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The data layer of Spend (RFC 0024 §2.1): expense categories with their **policy** (a limit per expense and per
|
|
3
|
+
* month, a receipt above an amount, a rate per day or per kilometre), expenses with their receipts, expense reports
|
|
4
|
+
* submitted together and decided through the approval engine, and **cash advances** that later reports settle.
|
|
5
|
+
*
|
|
6
|
+
* The server enforces the policy when a report is submitted: a missing receipt refuses it; an expense over a limit
|
|
7
|
+
* needs a reason and brings a further approval level (`overPolicy`); likely duplicates (the same amount, date and
|
|
8
|
+
* category, or the same receipt file) are flagged for the approvers. People file their own expenses; approvers see
|
|
9
|
+
* every report of their companies; Spend managers everything.
|
|
10
|
+
*/
|
|
11
|
+
import { hrShutro } from '../hr/index.js';
|
|
12
|
+
import { approvalGuard, permissionsShutro, requestApprovals } from '../permissions/index.js';
|
|
13
|
+
import { productShutro } from '../product/index.js';
|
|
14
|
+
import { orm } from '../server/orm.js';
|
|
15
|
+
import { check, compute, defineModel, defineModule, f, method, onchange, record, records, } from './.kosh/bindings.js';
|
|
16
|
+
const READ = ['read'];
|
|
17
|
+
const ALL = ['read', 'write', 'create', 'unlink'];
|
|
18
|
+
const me = { $env: 'uid' };
|
|
19
|
+
const ZERO = orm.toDecimal(0);
|
|
20
|
+
export const ExpenseCategory = defineModel('expense.category', {
|
|
21
|
+
label: 'Expense category',
|
|
22
|
+
recName: 'name',
|
|
23
|
+
order: ['name asc'],
|
|
24
|
+
archivable: true,
|
|
25
|
+
access: { 'expense.user': READ, 'expense.manager': [...ALL, 'export', 'import'] },
|
|
26
|
+
fields: {
|
|
27
|
+
name: f.char({ required: true, translate: true }),
|
|
28
|
+
kind: f.selection({ amount: 'An amount', per_diem: 'Per day', mileage: 'Per kilometre' }, {
|
|
29
|
+
required: true,
|
|
30
|
+
default: 'amount',
|
|
31
|
+
label: 'Claimed as',
|
|
32
|
+
help: 'An amount as on the receipt, or a quantity of days or kilometres at the category’s rate.',
|
|
33
|
+
}),
|
|
34
|
+
rate: f.decimal({ scale: 2, label: 'Rate', help: 'Per day or per kilometre.' }),
|
|
35
|
+
limitPerExpense: f.decimal({
|
|
36
|
+
scale: 2,
|
|
37
|
+
label: 'Limit per expense',
|
|
38
|
+
help: 'An expense above it needs a reason and a further approval.',
|
|
39
|
+
}),
|
|
40
|
+
limitPerMonth: f.decimal({
|
|
41
|
+
scale: 2,
|
|
42
|
+
label: 'Limit per person per month',
|
|
43
|
+
help: 'A person’s expenses of this category in a month, together.',
|
|
44
|
+
}),
|
|
45
|
+
receiptAbove: f.decimal({
|
|
46
|
+
scale: 2,
|
|
47
|
+
label: 'Receipt required above',
|
|
48
|
+
help: 'Empty: always; 0 or more: a receipt is attached to expenses above this amount.',
|
|
49
|
+
}),
|
|
50
|
+
tax: f.many2one('account.tax', {
|
|
51
|
+
ondelete: 'set null',
|
|
52
|
+
label: 'Tax included',
|
|
53
|
+
help: 'The purchase tax included in the amounts of this category.',
|
|
54
|
+
}),
|
|
55
|
+
note: f.text({ label: 'Guidance', translate: true }),
|
|
56
|
+
},
|
|
57
|
+
sqlConstraints: {
|
|
58
|
+
ratedHasRate: {
|
|
59
|
+
check: orm.sql `kind = 'amount' OR (rate IS NOT NULL AND rate > 0)`,
|
|
60
|
+
message: 'A category per day or per kilometre has a rate.',
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
/** The employee of the acting user, if they have one. */
|
|
65
|
+
async function ownEmployee(env) {
|
|
66
|
+
const found = await env.model('hr.employee').search({ where: { user: env.userId }, limit: 1 });
|
|
67
|
+
return found.ids[0] ?? null;
|
|
68
|
+
}
|
|
69
|
+
/** Fills an empty employee with the user's own, and a rated category's amount from its quantity. */
|
|
70
|
+
async function fillExpense(d, env) {
|
|
71
|
+
if ((d.get('employee') ?? null) === null) {
|
|
72
|
+
const mine = await ownEmployee(env);
|
|
73
|
+
if (mine !== null)
|
|
74
|
+
d.set('employee', mine);
|
|
75
|
+
}
|
|
76
|
+
const category = (await d.record('category'));
|
|
77
|
+
if (category === null)
|
|
78
|
+
return;
|
|
79
|
+
if ((d.get('tax') ?? null) === null && category.tax !== null)
|
|
80
|
+
d.set('tax', category.tax.id);
|
|
81
|
+
if (category.kind !== 'amount' && category.rate !== null) {
|
|
82
|
+
const quantity = orm.toDecimal(String(d.get('quantity') ?? '1'));
|
|
83
|
+
d.set('amount', category.rate.times(quantity).toDecimalPlaces(2).toString());
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
export const ExpenseExpense = defineModel('expense.expense', {
|
|
87
|
+
label: 'Expense',
|
|
88
|
+
order: ['date desc', 'id desc'],
|
|
89
|
+
companyScoped: { required: true },
|
|
90
|
+
tracking: ['amount', 'category', 'sheet'],
|
|
91
|
+
access: {
|
|
92
|
+
'expense.user': ALL,
|
|
93
|
+
'expense.approver': ALL,
|
|
94
|
+
'expense.manager': [...ALL, 'export', 'import'],
|
|
95
|
+
},
|
|
96
|
+
rules: {
|
|
97
|
+
// One's own expenses; changed while not yet submitted.
|
|
98
|
+
seeOwn: { groups: ['expense.user'], perms: ['read'], domain: { 'employee.user': me } },
|
|
99
|
+
draftOwn: {
|
|
100
|
+
groups: ['expense.user'],
|
|
101
|
+
perms: ['write', 'create', 'unlink'],
|
|
102
|
+
domain: {
|
|
103
|
+
'employee.user': me,
|
|
104
|
+
or: [{ sheet: { isNull: true } }, { 'sheet.state': { in: ['draft', 'refused'] } }],
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
approvers: { groups: ['expense.approver'], perms: ['read'], domain: {} },
|
|
108
|
+
all: { groups: ['expense.manager'], perms: [...ALL], domain: {} },
|
|
109
|
+
},
|
|
110
|
+
fields: {
|
|
111
|
+
name: f.char({ required: true, label: 'Description' }),
|
|
112
|
+
employee: f.many2one('hr.employee', { required: true, ondelete: 'restrict' }),
|
|
113
|
+
date: f.date({ required: true, label: 'Spent on' }),
|
|
114
|
+
category: f.many2one('expense.category', { required: true, ondelete: 'restrict' }),
|
|
115
|
+
quantity: f.decimal({
|
|
116
|
+
scale: 2,
|
|
117
|
+
required: true,
|
|
118
|
+
default: '1',
|
|
119
|
+
help: 'Days or kilometres for a category claimed per day or per kilometre; 1 otherwise.',
|
|
120
|
+
}),
|
|
121
|
+
amount: f.decimal({ scale: 2, required: true, label: 'Amount (tax included)' }),
|
|
122
|
+
tax: f.many2one('account.tax', { ondelete: 'set null', label: 'Tax included' }),
|
|
123
|
+
taxAmount: f.decimal({
|
|
124
|
+
scale: 2,
|
|
125
|
+
label: 'Of which tax',
|
|
126
|
+
help: 'The tax included in the amount, from the tax’s rate.',
|
|
127
|
+
...compute('expense.expense', 'taxAmount', ['amount', 'quantity', 'tax.amountType', 'tax.rate', 'tax.fixedAmount'], (expenses) => {
|
|
128
|
+
for (const e of expenses) {
|
|
129
|
+
const t = e.tax;
|
|
130
|
+
if (t === null)
|
|
131
|
+
e.taxAmount = ZERO;
|
|
132
|
+
else if (t.amountType === 'percent')
|
|
133
|
+
e.taxAmount = e.amount
|
|
134
|
+
.times(t.rate)
|
|
135
|
+
.div(orm.toDecimal(100).plus(t.rate))
|
|
136
|
+
.toDecimalPlaces(2);
|
|
137
|
+
else
|
|
138
|
+
e.taxAmount = t.fixedAmount.times(e.quantity).toDecimalPlaces(2);
|
|
139
|
+
}
|
|
140
|
+
}),
|
|
141
|
+
store: true,
|
|
142
|
+
}),
|
|
143
|
+
vendor: f.many2one('res.partner', { ondelete: 'set null', label: 'Paid to' }),
|
|
144
|
+
reference: f.char({ label: 'Receipt number' }),
|
|
145
|
+
paidBy: f.selection({ employee: 'The employee (to reimburse)', company: 'The company (company card)' }, {
|
|
146
|
+
required: true,
|
|
147
|
+
default: 'employee',
|
|
148
|
+
label: 'Paid by',
|
|
149
|
+
help: 'Expenses paid by the employee are reimbursed; company-card expenses only booked.',
|
|
150
|
+
}),
|
|
151
|
+
sheet: f.many2one('expense.sheet', { ondelete: 'set null', label: 'Report' }),
|
|
152
|
+
/** Why the expense goes beyond its category's policy (required then, at submission). */
|
|
153
|
+
policyReason: f.char({ size: 300, label: 'Reason beyond the policy' }),
|
|
154
|
+
overPolicy: f.boolean({
|
|
155
|
+
default: false,
|
|
156
|
+
label: 'Beyond the policy',
|
|
157
|
+
help: 'Set when the report is submitted: above a limit of its category.',
|
|
158
|
+
}),
|
|
159
|
+
duplicate: f.boolean({
|
|
160
|
+
default: false,
|
|
161
|
+
label: 'Possible duplicate',
|
|
162
|
+
help: 'Set when the report is submitted: the same amount, date and category, or the same receipt file, as another expense.',
|
|
163
|
+
}),
|
|
164
|
+
status: f.selection({
|
|
165
|
+
draft: 'Not reported',
|
|
166
|
+
reported: 'In a draft report',
|
|
167
|
+
submitted: 'Waiting for approval',
|
|
168
|
+
approved: 'Approved',
|
|
169
|
+
refused: 'Refused',
|
|
170
|
+
done: 'Done',
|
|
171
|
+
posted: 'Booked',
|
|
172
|
+
paid: 'Reimbursed',
|
|
173
|
+
}, {
|
|
174
|
+
label: 'Status',
|
|
175
|
+
help: 'Follows the report the expense is in.',
|
|
176
|
+
...compute('expense.expense', 'status', ['sheet', 'sheet.state'], (expenses) => {
|
|
177
|
+
for (const e of expenses) {
|
|
178
|
+
const s = e.sheet?.state ?? null;
|
|
179
|
+
e.status =
|
|
180
|
+
s === null
|
|
181
|
+
? 'draft'
|
|
182
|
+
: s === 'draft'
|
|
183
|
+
? 'reported'
|
|
184
|
+
: s === 'to_approve'
|
|
185
|
+
? 'submitted'
|
|
186
|
+
: s;
|
|
187
|
+
}
|
|
188
|
+
}),
|
|
189
|
+
store: true,
|
|
190
|
+
}),
|
|
191
|
+
},
|
|
192
|
+
sqlConstraints: {
|
|
193
|
+
positive: {
|
|
194
|
+
check: orm.sql `amount > 0 AND quantity > 0`,
|
|
195
|
+
message: 'Amounts and quantities are positive.',
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
constraints: {
|
|
199
|
+
// A category claimed per day or kilometre: the amount is its rate times the quantity.
|
|
200
|
+
rated: {
|
|
201
|
+
...check('expense.expense', ['category', 'quantity', 'amount'], async (expenses) => [...(await expenses.load({ category: true }))]
|
|
202
|
+
.filter((e) => e.category.kind !== 'amount' &&
|
|
203
|
+
e.category.rate !== null &&
|
|
204
|
+
!e.category.rate.times(e.quantity).toDecimalPlaces(2).eq(e.amount))
|
|
205
|
+
.map((e) => e.id)),
|
|
206
|
+
message: 'The amount is the category’s rate times the days or kilometres.',
|
|
207
|
+
},
|
|
208
|
+
sameEmployee: {
|
|
209
|
+
...check('expense.expense', ['sheet', 'employee'], async (expenses) => [...(await expenses.load({ sheet: true }))]
|
|
210
|
+
.filter((e) => e.sheet !== null && e.sheet.employee.id !== e.employee.id)
|
|
211
|
+
.map((e) => e.id)),
|
|
212
|
+
message: 'A report holds the expenses of its own employee.',
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
onchange: {
|
|
216
|
+
name: onchange('expense.expense', async (d, env) => fillExpense(d, env)),
|
|
217
|
+
category: onchange('expense.expense', async (d, env) => fillExpense(d, env)),
|
|
218
|
+
quantity: onchange('expense.expense', async (d, env) => fillExpense(d, env)),
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
/**
|
|
222
|
+
* The policy at submission: receipts where required (refused otherwise), limits (a reason then, and the flag that
|
|
223
|
+
* brings the further approval), and likely duplicates (flagged). Writes the flags and the advance used.
|
|
224
|
+
*/
|
|
225
|
+
async function checkPolicy(sheets) {
|
|
226
|
+
const { env, ids } = sheets;
|
|
227
|
+
await env.asSuperuser({ reason: 'expense policy' }, async (su) => {
|
|
228
|
+
for (const sheet of await su
|
|
229
|
+
.model('expense.sheet')
|
|
230
|
+
.browse([...ids])
|
|
231
|
+
.load({
|
|
232
|
+
expenses: { category: true },
|
|
233
|
+
advance: true,
|
|
234
|
+
})) {
|
|
235
|
+
let over = false;
|
|
236
|
+
for (const e of sheet.expenses) {
|
|
237
|
+
const c = e.category;
|
|
238
|
+
// A receipt: an attachment of the expense, when the category asks for one at this amount.
|
|
239
|
+
const attachments = await su.model('storage.attachment').search({
|
|
240
|
+
where: { resModel: 'expense.expense', resId: e.id, variantOf: { isNull: true } },
|
|
241
|
+
});
|
|
242
|
+
const needsReceipt = c.receiptAbove === null || e.amount.gt(c.receiptAbove);
|
|
243
|
+
if (needsReceipt && c.kind === 'amount' && attachments.length === 0)
|
|
244
|
+
throw new orm.ValidationError('expense.sheet', ['expenses'], `${e.name}: attach the receipt (${c.name} needs one${c.receiptAbove === null ? '' : ` above ${c.receiptAbove.toString()}`}).`, 'Open the expense and attach a photo or a PDF of the receipt.');
|
|
245
|
+
// The limits: per expense, and per person and month with the category's other expenses.
|
|
246
|
+
let beyond = c.limitPerExpense !== null && e.amount.gt(c.limitPerExpense);
|
|
247
|
+
if (!beyond && c.limitPerMonth !== null) {
|
|
248
|
+
const date = isoDate(e.date);
|
|
249
|
+
const month = date.slice(0, 7);
|
|
250
|
+
const others = await (await su.model('expense.expense').search({
|
|
251
|
+
where: {
|
|
252
|
+
employee: e.employee.id,
|
|
253
|
+
category: c.id,
|
|
254
|
+
id: { ne: e.id },
|
|
255
|
+
status: { notIn: ['refused', 'draft'] },
|
|
256
|
+
date: { gte: `${month}-01`, lte: `${month}-31` },
|
|
257
|
+
},
|
|
258
|
+
})).load(['amount']);
|
|
259
|
+
const total = [...others].reduce((s, x) => s.plus(x.amount), e.amount);
|
|
260
|
+
beyond = total.gt(c.limitPerMonth);
|
|
261
|
+
}
|
|
262
|
+
if (beyond && (e.policyReason === null || e.policyReason.trim() === ''))
|
|
263
|
+
throw new orm.ValidationError('expense.sheet', ['expenses'], `${e.name}: beyond the ${c.name} policy — say why.`, 'Write the reason on the expense; a Spend manager approves it as well.');
|
|
264
|
+
over ||= beyond;
|
|
265
|
+
// Likely duplicates: the same amount, date and category, or the same receipt file.
|
|
266
|
+
const twins = await su.model('expense.expense').search({
|
|
267
|
+
where: {
|
|
268
|
+
id: { ne: e.id },
|
|
269
|
+
employee: e.employee.id,
|
|
270
|
+
category: c.id,
|
|
271
|
+
amount: e.amount,
|
|
272
|
+
date: e.date,
|
|
273
|
+
status: { ne: 'refused' },
|
|
274
|
+
},
|
|
275
|
+
limit: 1,
|
|
276
|
+
});
|
|
277
|
+
let duplicate = twins.length > 0;
|
|
278
|
+
if (!duplicate && attachments.length > 0) {
|
|
279
|
+
const hashes = [...(await attachments.load(['sha256']))].map((a) => a.sha256);
|
|
280
|
+
const same = await su.model('storage.attachment').search({
|
|
281
|
+
where: {
|
|
282
|
+
resModel: 'expense.expense',
|
|
283
|
+
resId: { ne: e.id },
|
|
284
|
+
sha256: { in: hashes },
|
|
285
|
+
},
|
|
286
|
+
limit: 1,
|
|
287
|
+
});
|
|
288
|
+
duplicate = same.length > 0;
|
|
289
|
+
}
|
|
290
|
+
await su.model('expense.expense').browse([e.id]).write({ overPolicy: beyond, duplicate });
|
|
291
|
+
}
|
|
292
|
+
// The advance this report settles: what is left of it, up to the report's total.
|
|
293
|
+
let used = ZERO;
|
|
294
|
+
if (sheet.advance !== null) {
|
|
295
|
+
const left = await advanceLeft(su, sheet.advance.id, sheet.id);
|
|
296
|
+
used = left.lt(sheet.total ?? ZERO) ? left : (sheet.total ?? ZERO);
|
|
297
|
+
if (used.lt(0))
|
|
298
|
+
used = ZERO;
|
|
299
|
+
}
|
|
300
|
+
await su
|
|
301
|
+
.model('expense.sheet')
|
|
302
|
+
.browse([sheet.id])
|
|
303
|
+
.write({ overPolicy: over, advanceUsed: used });
|
|
304
|
+
}
|
|
305
|
+
await su.flush();
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
/** What is left of an advance, other reports' use deducted (refused and draft reports aside). */
|
|
309
|
+
export async function advanceLeft(env, advance, except) {
|
|
310
|
+
const [a] = await env
|
|
311
|
+
.model('expense.advance')
|
|
312
|
+
.browse([advance])
|
|
313
|
+
.load(['amount']);
|
|
314
|
+
if (a === undefined)
|
|
315
|
+
return ZERO;
|
|
316
|
+
const users = await (await env.model('expense.sheet').search({
|
|
317
|
+
where: {
|
|
318
|
+
advance: advance,
|
|
319
|
+
state: { notIn: ['draft', 'refused'] },
|
|
320
|
+
...(except === undefined ? {} : { id: { ne: except } }),
|
|
321
|
+
},
|
|
322
|
+
})).load(['advanceUsed']);
|
|
323
|
+
return [...users].reduce((s, x) => s.minus(x.advanceUsed), a.amount);
|
|
324
|
+
}
|
|
325
|
+
/** A date-only value as `YYYY-MM-DD` (PostgreSQL dates read as local midnights). */
|
|
326
|
+
export function isoDate(v) {
|
|
327
|
+
if (!(v instanceof Date))
|
|
328
|
+
return String(v ?? '').slice(0, 10);
|
|
329
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
330
|
+
return `${v.getFullYear()}-${pad(v.getMonth() + 1)}-${pad(v.getDate())}`;
|
|
331
|
+
}
|
|
332
|
+
const SHEET_STATES = {
|
|
333
|
+
draft: 'Draft',
|
|
334
|
+
to_approve: 'Waiting for approval',
|
|
335
|
+
approved: 'Approved',
|
|
336
|
+
done: 'Done',
|
|
337
|
+
refused: 'Refused',
|
|
338
|
+
};
|
|
339
|
+
export const ExpenseSheet = defineModel('expense.sheet', {
|
|
340
|
+
label: 'Expense report',
|
|
341
|
+
recName: 'name',
|
|
342
|
+
order: ['id desc'],
|
|
343
|
+
companyScoped: { required: true },
|
|
344
|
+
tracking: ['state', 'total', 'advance'],
|
|
345
|
+
access: {
|
|
346
|
+
'expense.user': ALL,
|
|
347
|
+
'expense.approver': ALL,
|
|
348
|
+
'expense.manager': [...ALL, 'export', 'import'],
|
|
349
|
+
},
|
|
350
|
+
rules: {
|
|
351
|
+
seeOwn: { groups: ['expense.user'], perms: ['read'], domain: { 'employee.user': me } },
|
|
352
|
+
draftOwn: {
|
|
353
|
+
groups: ['expense.user'],
|
|
354
|
+
perms: ['write', 'create', 'unlink'],
|
|
355
|
+
domain: { 'employee.user': me, state: { in: ['draft', 'refused'] } },
|
|
356
|
+
},
|
|
357
|
+
approvers: { groups: ['expense.approver'], perms: ['read'], domain: {} },
|
|
358
|
+
all: { groups: ['expense.manager'], perms: [...ALL], domain: {} },
|
|
359
|
+
},
|
|
360
|
+
fields: {
|
|
361
|
+
name: f.char({ required: true, sequence: 'expense.sheet', label: 'Reference' }),
|
|
362
|
+
title: f.char({ label: 'Purpose' }),
|
|
363
|
+
employee: f.many2one('hr.employee', { required: true, ondelete: 'restrict' }),
|
|
364
|
+
expenses: f.one2many('expense.expense', 'sheet'),
|
|
365
|
+
expenseCount: f.integer({
|
|
366
|
+
label: 'Expenses',
|
|
367
|
+
...compute('expense.sheet', 'expenseCount', ['expenses'], (sheets) => {
|
|
368
|
+
for (const s of sheets)
|
|
369
|
+
s.expenseCount = s.expenses.length;
|
|
370
|
+
}),
|
|
371
|
+
}),
|
|
372
|
+
total: f.decimal({
|
|
373
|
+
scale: 2,
|
|
374
|
+
label: 'Total',
|
|
375
|
+
help: 'The expenses of the report, tax included.',
|
|
376
|
+
...compute('expense.sheet', 'total', ['expenses.amount'], (sheets) => {
|
|
377
|
+
for (const s of sheets)
|
|
378
|
+
s.total = [...s.expenses].reduce((t, e) => t.plus(e.amount), ZERO);
|
|
379
|
+
}),
|
|
380
|
+
store: true,
|
|
381
|
+
}),
|
|
382
|
+
toReimburse: f.decimal({
|
|
383
|
+
scale: 2,
|
|
384
|
+
label: 'To reimburse',
|
|
385
|
+
help: 'Paid by the employee, less what an advance already covered.',
|
|
386
|
+
...compute('expense.sheet', 'toReimburse', ['expenses.amount', 'expenses.paidBy', 'advanceUsed'], (sheets) => {
|
|
387
|
+
for (const s of sheets) {
|
|
388
|
+
const own = [...s.expenses]
|
|
389
|
+
.filter((e) => e.paidBy === 'employee')
|
|
390
|
+
.reduce((t, e) => t.plus(e.amount), ZERO);
|
|
391
|
+
const left = own.minus(s.advanceUsed);
|
|
392
|
+
s.toReimburse = left.gt(0) ? left : ZERO;
|
|
393
|
+
}
|
|
394
|
+
}),
|
|
395
|
+
store: true,
|
|
396
|
+
}),
|
|
397
|
+
advance: f.many2one('expense.advance', {
|
|
398
|
+
ondelete: 'restrict',
|
|
399
|
+
label: 'Settles advance',
|
|
400
|
+
help: 'A cash advance paid to the employee that this report’s expenses settle.',
|
|
401
|
+
}),
|
|
402
|
+
advanceUsed: f.decimal({
|
|
403
|
+
scale: 2,
|
|
404
|
+
required: true,
|
|
405
|
+
default: '0',
|
|
406
|
+
label: 'From the advance',
|
|
407
|
+
help: 'Set when the report is submitted: what the advance covers.',
|
|
408
|
+
}),
|
|
409
|
+
overPolicy: f.boolean({
|
|
410
|
+
default: false,
|
|
411
|
+
label: 'Beyond the policy',
|
|
412
|
+
help: 'An expense of the report goes beyond its category’s policy: a Spend manager approves it too.',
|
|
413
|
+
}),
|
|
414
|
+
state: f.state({
|
|
415
|
+
states: SHEET_STATES,
|
|
416
|
+
initial: 'draft',
|
|
417
|
+
help: 'Draft, then submitted for approval; approved reports are booked and reimbursed (or settled outside the books).',
|
|
418
|
+
transitions: {
|
|
419
|
+
submit: {
|
|
420
|
+
from: ['draft', 'refused'],
|
|
421
|
+
to: 'to_approve',
|
|
422
|
+
guard: records('expense.sheet', async (sheets) => [...(await sheets.load({ expenses: true }))]
|
|
423
|
+
.filter((s) => s.expenses.length === 0)
|
|
424
|
+
.map((s) => s.id)),
|
|
425
|
+
message: 'Add the expenses to the report before submitting it.',
|
|
426
|
+
before: records('expense.sheet', (sheets) => checkPolicy(sheets)),
|
|
427
|
+
after: records('expense.sheet', async (sheets) => {
|
|
428
|
+
await requestApprovals(sheets);
|
|
429
|
+
for (const s of await sheets.load({}))
|
|
430
|
+
await sheets.env.emit('expense.sheet.submitted', { id: s.id });
|
|
431
|
+
}),
|
|
432
|
+
},
|
|
433
|
+
approve: {
|
|
434
|
+
from: ['to_approve'],
|
|
435
|
+
to: 'approved',
|
|
436
|
+
guard: records('expense.sheet', (sheets) => approvalGuard(sheets)),
|
|
437
|
+
message: 'Waiting for approvals.',
|
|
438
|
+
after: records('expense.sheet', async (sheets) => {
|
|
439
|
+
for (const s of await sheets.load({}))
|
|
440
|
+
await sheets.env.emit('expense.sheet.approved', { id: s.id });
|
|
441
|
+
}),
|
|
442
|
+
},
|
|
443
|
+
refuse: { from: ['to_approve'], to: 'refused' },
|
|
444
|
+
/** Settled outside the books (no accounting, or paid by other means). */
|
|
445
|
+
reimbursed: { from: ['approved'], to: 'done', requires: 'expense.settle' },
|
|
446
|
+
redraft: { from: ['refused'], to: 'draft' },
|
|
447
|
+
},
|
|
448
|
+
extensible: true, // expense_account books and reimburses
|
|
449
|
+
}),
|
|
450
|
+
},
|
|
451
|
+
constraints: {
|
|
452
|
+
// The advance is one of the employee's, paid, with something left.
|
|
453
|
+
advanceOfTheEmployee: {
|
|
454
|
+
...check('expense.sheet', ['advance', 'employee'], async (sheets) => [...(await sheets.load({ advance: true }))]
|
|
455
|
+
.filter((s) => s.advance !== null &&
|
|
456
|
+
(s.advance.employee.id !== s.employee.id ||
|
|
457
|
+
!['paid', 'settled'].includes(s.advance.state)))
|
|
458
|
+
.map((s) => s.id)),
|
|
459
|
+
message: 'A report settles a paid advance of its own employee.',
|
|
460
|
+
},
|
|
461
|
+
},
|
|
462
|
+
onchange: {
|
|
463
|
+
title: onchange('expense.sheet', async (d, env) => {
|
|
464
|
+
const draft = d;
|
|
465
|
+
if ((draft.get('employee') ?? null) === null) {
|
|
466
|
+
const mine = await ownEmployee(env);
|
|
467
|
+
if (mine !== null)
|
|
468
|
+
draft.set('employee', mine);
|
|
469
|
+
}
|
|
470
|
+
}),
|
|
471
|
+
},
|
|
472
|
+
});
|
|
473
|
+
export const ExpenseAdvance = defineModel('expense.advance', {
|
|
474
|
+
label: 'Cash advance',
|
|
475
|
+
recName: 'name',
|
|
476
|
+
order: ['id desc'],
|
|
477
|
+
companyScoped: { required: true },
|
|
478
|
+
tracking: ['state', 'amount'],
|
|
479
|
+
access: {
|
|
480
|
+
'expense.user': ALL,
|
|
481
|
+
'expense.approver': ALL,
|
|
482
|
+
'expense.manager': [...ALL, 'export', 'import'],
|
|
483
|
+
},
|
|
484
|
+
rules: {
|
|
485
|
+
seeOwn: { groups: ['expense.user'], perms: ['read'], domain: { 'employee.user': me } },
|
|
486
|
+
draftOwn: {
|
|
487
|
+
groups: ['expense.user'],
|
|
488
|
+
perms: ['write', 'create', 'unlink'],
|
|
489
|
+
domain: { 'employee.user': me, state: { in: ['draft', 'refused'] } },
|
|
490
|
+
},
|
|
491
|
+
approvers: { groups: ['expense.approver'], perms: ['read'], domain: {} },
|
|
492
|
+
all: { groups: ['expense.manager'], perms: [...ALL], domain: {} },
|
|
493
|
+
},
|
|
494
|
+
fields: {
|
|
495
|
+
name: f.char({ required: true, sequence: 'expense.advance', label: 'Reference' }),
|
|
496
|
+
employee: f.many2one('hr.employee', { required: true, ondelete: 'restrict' }),
|
|
497
|
+
purpose: f.char({ required: true }),
|
|
498
|
+
date: f.date({ required: true, label: 'Needed on' }),
|
|
499
|
+
amount: f.decimal({ scale: 2, required: true }),
|
|
500
|
+
sheets: f.one2many('expense.sheet', 'advance', { label: 'Settling reports' }),
|
|
501
|
+
settled: f.decimal({
|
|
502
|
+
scale: 2,
|
|
503
|
+
label: 'Settled',
|
|
504
|
+
help: 'Covered by the expenses of submitted and approved reports.',
|
|
505
|
+
...compute('expense.advance', 'settled', ['sheets.advanceUsed', 'sheets.state'], (advances) => {
|
|
506
|
+
for (const a of advances)
|
|
507
|
+
a.settled = [...a.sheets]
|
|
508
|
+
.filter((s) => s.state !== 'draft' && s.state !== 'refused')
|
|
509
|
+
.reduce((t, s) => t.plus(s.advanceUsed), ZERO);
|
|
510
|
+
}),
|
|
511
|
+
store: true,
|
|
512
|
+
}),
|
|
513
|
+
balance: f.decimal({
|
|
514
|
+
scale: 2,
|
|
515
|
+
label: 'Left to settle',
|
|
516
|
+
help: 'The advance less what reports settled: returned by the employee when nothing more is spent.',
|
|
517
|
+
...compute('expense.advance', 'balance', ['amount', 'settled'], (advances) => {
|
|
518
|
+
for (const a of advances)
|
|
519
|
+
a.balance = a.amount.minus(a.settled ?? ZERO);
|
|
520
|
+
}),
|
|
521
|
+
store: true,
|
|
522
|
+
}),
|
|
523
|
+
state: f.state({
|
|
524
|
+
states: {
|
|
525
|
+
draft: 'Draft',
|
|
526
|
+
to_approve: 'Waiting for approval',
|
|
527
|
+
approved: 'Approved',
|
|
528
|
+
paid: 'Paid out',
|
|
529
|
+
settled: 'Settled',
|
|
530
|
+
refused: 'Refused',
|
|
531
|
+
},
|
|
532
|
+
initial: 'draft',
|
|
533
|
+
help: 'Requested, approved, paid out, then settled by expense reports and the rest returned.',
|
|
534
|
+
transitions: {
|
|
535
|
+
submit: {
|
|
536
|
+
from: ['draft', 'refused'],
|
|
537
|
+
to: 'to_approve',
|
|
538
|
+
after: records('expense.advance', async (advances) => {
|
|
539
|
+
await requestApprovals(advances);
|
|
540
|
+
for (const a of await advances.load({}))
|
|
541
|
+
await advances.env.emit('expense.advance.submitted', { id: a.id });
|
|
542
|
+
}),
|
|
543
|
+
},
|
|
544
|
+
approve: {
|
|
545
|
+
from: ['to_approve'],
|
|
546
|
+
to: 'approved',
|
|
547
|
+
guard: records('expense.advance', (advances) => approvalGuard(advances)),
|
|
548
|
+
message: 'Waiting for approvals.',
|
|
549
|
+
},
|
|
550
|
+
refuse: { from: ['to_approve'], to: 'refused' },
|
|
551
|
+
pay: {
|
|
552
|
+
from: ['approved'],
|
|
553
|
+
to: 'paid',
|
|
554
|
+
requires: 'expense.settle',
|
|
555
|
+
// Accounting books the payout (expense_account overrides `paidOut`).
|
|
556
|
+
after: records('expense.advance', async (advances) => {
|
|
557
|
+
const env = advances.env;
|
|
558
|
+
await env.flush();
|
|
559
|
+
await env.model('expense.advance').browse(advances.ids).paidOut();
|
|
560
|
+
for (const id of advances.ids)
|
|
561
|
+
await env.emit('expense.advance.paid', { id });
|
|
562
|
+
}),
|
|
563
|
+
},
|
|
564
|
+
/** Spent in full by reports, or the rest returned. */
|
|
565
|
+
settle: {
|
|
566
|
+
from: ['paid'],
|
|
567
|
+
to: 'settled',
|
|
568
|
+
requires: 'expense.settle',
|
|
569
|
+
after: records('expense.advance', async (advances) => {
|
|
570
|
+
const env = advances.env;
|
|
571
|
+
await env.flush();
|
|
572
|
+
await env.model('expense.advance').browse(advances.ids).settledOut();
|
|
573
|
+
}),
|
|
574
|
+
},
|
|
575
|
+
redraft: { from: ['refused'], to: 'draft' },
|
|
576
|
+
},
|
|
577
|
+
extensible: true,
|
|
578
|
+
}),
|
|
579
|
+
},
|
|
580
|
+
sqlConstraints: {
|
|
581
|
+
positive: { check: orm.sql `amount > 0`, message: 'An advance is a positive amount.' },
|
|
582
|
+
},
|
|
583
|
+
methods: {
|
|
584
|
+
/** After the payout: nothing here; Accounting books it. */
|
|
585
|
+
paidOut: method('expense.advance', async () => undefined),
|
|
586
|
+
/** After the settlement: nothing here; Accounting books the cash returned. */
|
|
587
|
+
settledOut: method('expense.advance', async () => undefined),
|
|
588
|
+
},
|
|
589
|
+
});
|
|
590
|
+
export const expenseShutro = defineModule({
|
|
591
|
+
name: 'expense',
|
|
592
|
+
version: '0.1.0',
|
|
593
|
+
depends: [hrShutro, productShutro, permissionsShutro],
|
|
594
|
+
groups: [
|
|
595
|
+
orm.defineGroup({
|
|
596
|
+
code: 'expense.user',
|
|
597
|
+
label: 'Spend: own expenses',
|
|
598
|
+
// Receipts are attachments.
|
|
599
|
+
implies: ['hr.user', 'storage.user'],
|
|
600
|
+
}),
|
|
601
|
+
orm.defineGroup({
|
|
602
|
+
code: 'expense.approver',
|
|
603
|
+
label: 'Spend: approver',
|
|
604
|
+
implies: ['expense.user'],
|
|
605
|
+
}),
|
|
606
|
+
orm.defineGroup({
|
|
607
|
+
code: 'expense.manager',
|
|
608
|
+
label: 'Spend manager',
|
|
609
|
+
implies: ['expense.approver', 'product.user'],
|
|
610
|
+
}),
|
|
611
|
+
],
|
|
612
|
+
permissions: [
|
|
613
|
+
orm.definePermission({
|
|
614
|
+
code: 'expense.settle',
|
|
615
|
+
label: 'Pay out advances and settle reports',
|
|
616
|
+
groups: ['expense.manager'],
|
|
617
|
+
}),
|
|
618
|
+
],
|
|
619
|
+
sequences: [
|
|
620
|
+
orm.defineSequence({
|
|
621
|
+
code: 'expense.sheet',
|
|
622
|
+
prefix: 'EXP/{year}/',
|
|
623
|
+
padding: 5,
|
|
624
|
+
reset: 'yearly',
|
|
625
|
+
}),
|
|
626
|
+
orm.defineSequence({
|
|
627
|
+
code: 'expense.advance',
|
|
628
|
+
prefix: 'ADV/{year}/',
|
|
629
|
+
padding: 5,
|
|
630
|
+
reset: 'yearly',
|
|
631
|
+
}),
|
|
632
|
+
],
|
|
633
|
+
models: [ExpenseCategory, ExpenseExpense, ExpenseSheet, ExpenseAdvance],
|
|
634
|
+
data: [
|
|
635
|
+
record('expense.category', 'category_travel', { name: 'Travel', kind: 'amount', receiptAbove: '500' }, { noupdate: true }),
|
|
636
|
+
record('expense.category', 'category_meals', {
|
|
637
|
+
name: 'Meals',
|
|
638
|
+
kind: 'amount',
|
|
639
|
+
limitPerExpense: '1500',
|
|
640
|
+
limitPerMonth: '6000',
|
|
641
|
+
receiptAbove: '300',
|
|
642
|
+
}, { noupdate: true }),
|
|
643
|
+
record('expense.category', 'category_per_diem', { name: 'Daily allowance', kind: 'per_diem', rate: '800' }, { noupdate: true }),
|
|
644
|
+
record('expense.category', 'category_mileage', { name: 'Own vehicle', kind: 'mileage', rate: '25' }, { noupdate: true }),
|
|
645
|
+
record('expense.category', 'category_office', { name: 'Office supplies', kind: 'amount' }, { noupdate: true }),
|
|
646
|
+
],
|
|
647
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export { defineModel, defineModule, extendModel, f, replace } from '../../server/orm.js';
|
|
2
|
+
import { type DefinitionHelpers } from '../../server/orm.js';
|
|
3
|
+
import type { DefaultLoaded, Env as CoreEnv, RecordSet as CoreRecordSet, Shutro as CoreShutro } from '../../server/orm.js';
|
|
4
|
+
import type { SchemaView } from './schema.js';
|
|
5
|
+
export type { SchemaView };
|
|
6
|
+
/** Every model visible in this view. */
|
|
7
|
+
export type ModelName = keyof SchemaView & string;
|
|
8
|
+
/** `Env` typed to this view. */
|
|
9
|
+
export type Env = CoreEnv<SchemaView>;
|
|
10
|
+
/** `Shutro` typed to this view. */
|
|
11
|
+
export type Shutro = CoreShutro<SchemaView>;
|
|
12
|
+
/** A recordset of `M`, with the default loaded shape unless `L` is given. */
|
|
13
|
+
export type Records<M extends ModelName, L = DefaultLoaded<SchemaView, M>> = CoreRecordSet<SchemaView, M, L>;
|
|
14
|
+
export declare const compute: DefinitionHelpers<SchemaView>['compute'];
|
|
15
|
+
export declare const check: DefinitionHelpers<SchemaView>['check'];
|
|
16
|
+
export declare const onchange: DefinitionHelpers<SchemaView>['onchange'];
|
|
17
|
+
export declare const records: DefinitionHelpers<SchemaView>['records'];
|
|
18
|
+
export declare const method: DefinitionHelpers<SchemaView>['method'];
|
|
19
|
+
/** Module data (RFC 0017): records, overrides of dependencies' records, and references, typed like creates. */
|
|
20
|
+
export declare const record: DefinitionHelpers<SchemaView>['record'];
|
|
21
|
+
export declare const override: DefinitionHelpers<SchemaView>['override'];
|
|
22
|
+
export declare const ref: DefinitionHelpers<SchemaView>['ref'];
|