shutro-erp 0.3.4 → 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 +176 -6
- package/dist/account_portal/module.d.ts +505 -6
- package/dist/accounting/.kosh/bindings.js +1 -1
- package/dist/accounting/.kosh/schema.d.ts +176 -6
- package/dist/accounting/index.d.ts +1 -1
- package/dist/accounting/index.js +344 -4
- package/dist/accounting/module.d.ts +513 -5
- package/dist/accounting/module.js +55 -3
- 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/calendar/.kosh/bindings.d.ts +22 -0
- package/dist/calendar/.kosh/bindings.js +15 -0
- package/dist/calendar/.kosh/schema.d.ts +795 -0
- package/dist/calendar/demo.d.ts +6 -0
- package/dist/calendar/demo.js +24 -0
- package/dist/calendar/index.d.ts +3 -0
- package/dist/calendar/index.js +291 -0
- package/dist/calendar/module.d.ts +1705 -0
- package/dist/calendar/module.js +180 -0
- package/dist/cli/init.js +80 -0
- package/dist/cli/templates.d.ts +1 -1
- package/dist/cli/templates.js +226 -0
- package/dist/contacts/.kosh/bindings.js +1 -1
- package/dist/contacts/.kosh/schema.d.ts +78 -3
- package/dist/contacts/demo.js +30 -1
- package/dist/contacts/index.d.ts +1 -1
- package/dist/contacts/index.js +155 -9
- package/dist/contacts/module.d.ts +338 -2
- package/dist/contacts/module.js +93 -5
- package/dist/crm/.kosh/bindings.d.ts +22 -0
- package/dist/crm/.kosh/bindings.js +15 -0
- package/dist/crm/.kosh/schema.d.ts +800 -0
- package/dist/crm/demo.d.ts +6 -0
- package/dist/crm/demo.js +45 -0
- package/dist/crm/index.d.ts +3 -0
- package/dist/crm/index.js +361 -0
- package/dist/crm/module.d.ts +1845 -0
- package/dist/crm/module.js +208 -0
- 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/.kosh/bindings.d.ts +22 -0
- package/dist/event/.kosh/bindings.js +15 -0
- package/dist/event/.kosh/schema.d.ts +843 -0
- package/dist/event/demo.d.ts +6 -0
- package/dist/event/demo.js +65 -0
- package/dist/event/index.d.ts +14 -0
- package/dist/event/index.js +333 -0
- package/dist/event/mails.d.ts +15 -0
- package/dist/event/mails.js +112 -0
- package/dist/event/module.d.ts +2800 -0
- package/dist/event/module.js +323 -0
- package/dist/event/web/CheckInBox.d.ts +9 -0
- package/dist/event/web/CheckInBox.js +87 -0
- package/dist/event/web/index.d.ts +11 -0
- package/dist/event/web/index.js +16 -0
- package/dist/event/web/messages.d.ts +2 -0
- package/dist/event/web/messages.js +23 -0
- package/dist/event_account/.kosh/bindings.d.ts +22 -0
- package/dist/event_account/.kosh/bindings.js +15 -0
- package/dist/event_account/.kosh/schema.d.ts +1758 -0
- package/dist/event_account/index.d.ts +3 -0
- package/dist/event_account/index.js +28 -0
- package/dist/event_account/module.d.ts +7114 -0
- package/dist/event_account/module.js +90 -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/.kosh/bindings.d.ts +22 -0
- package/dist/hr/.kosh/bindings.js +15 -0
- package/dist/hr/.kosh/schema.d.ts +878 -0
- package/dist/hr/demo.d.ts +6 -0
- package/dist/hr/demo.js +60 -0
- package/dist/hr/index.d.ts +3 -0
- package/dist/hr/index.js +483 -0
- package/dist/hr/module.d.ts +2611 -0
- package/dist/hr/module.js +350 -0
- package/dist/l10n_bd/.kosh/bindings.js +1 -1
- package/dist/l10n_bd/.kosh/schema.d.ts +176 -6
- package/dist/l10n_bd/module.d.ts +505 -6
- 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 +176 -6
- package/dist/l10n_in/module.d.ts +527 -7
- package/dist/mail/inbound/handle.js +6 -1
- package/dist/maintenance/.kosh/bindings.d.ts +22 -0
- package/dist/maintenance/.kosh/bindings.js +15 -0
- package/dist/maintenance/.kosh/schema.d.ts +911 -0
- package/dist/maintenance/demo.d.ts +6 -0
- package/dist/maintenance/demo.js +60 -0
- package/dist/maintenance/index.d.ts +3 -0
- package/dist/maintenance/index.js +422 -0
- package/dist/maintenance/module.d.ts +3545 -0
- package/dist/maintenance/module.js +446 -0
- package/dist/maintenance_stock/.kosh/bindings.d.ts +22 -0
- package/dist/maintenance_stock/.kosh/bindings.js +15 -0
- package/dist/maintenance_stock/.kosh/schema.d.ts +1446 -0
- package/dist/maintenance_stock/index.d.ts +9 -0
- package/dist/maintenance_stock/index.js +52 -0
- package/dist/maintenance_stock/module.d.ts +7675 -0
- package/dist/maintenance_stock/module.js +192 -0
- package/dist/modules-sdk/index.d.ts +1 -1
- package/dist/modules-sdk/manifest.d.ts +109 -2
- package/dist/modules-sdk/manifest.js +35 -1
- package/dist/mrp/.kosh/bindings.d.ts +22 -0
- package/dist/mrp/.kosh/bindings.js +15 -0
- package/dist/mrp/.kosh/schema.d.ts +1453 -0
- package/dist/mrp/demo.d.ts +7 -0
- package/dist/mrp/demo.js +42 -0
- package/dist/mrp/index.d.ts +3 -0
- package/dist/mrp/index.js +393 -0
- package/dist/mrp/module.d.ts +9335 -0
- package/dist/mrp/module.js +757 -0
- package/dist/mrp_account/.kosh/bindings.d.ts +22 -0
- package/dist/mrp_account/.kosh/bindings.js +15 -0
- package/dist/mrp_account/.kosh/schema.d.ts +2299 -0
- package/dist/mrp_account/index.d.ts +3 -0
- package/dist/mrp_account/index.js +22 -0
- package/dist/mrp_account/module.d.ts +23151 -0
- package/dist/mrp_account/module.js +123 -0
- package/dist/payment/.kosh/bindings.js +1 -1
- package/dist/payment/.kosh/schema.d.ts +178 -7
- package/dist/payment/index.d.ts +41 -0
- package/dist/payment/index.js +131 -74
- package/dist/payment/module.d.ts +569 -12
- 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/.kosh/bindings.js +1 -1
- package/dist/portal/.kosh/schema.d.ts +78 -3
- package/dist/portal/index.js +1 -1
- package/dist/portal/module.d.ts +166 -2
- package/dist/pos/.kosh/bindings.d.ts +22 -0
- package/dist/pos/.kosh/bindings.js +15 -0
- package/dist/pos/.kosh/schema.d.ts +1809 -0
- package/dist/pos/accounting.d.ts +10 -0
- package/dist/pos/accounting.js +149 -0
- package/dist/pos/demo.d.ts +6 -0
- package/dist/pos/demo.js +90 -0
- package/dist/pos/index.d.ts +9 -0
- package/dist/pos/index.js +353 -0
- package/dist/pos/module.d.ts +9783 -0
- package/dist/pos/module.js +423 -0
- package/dist/pos/routes.d.ts +39 -0
- package/dist/pos/routes.js +368 -0
- package/dist/pos/web/Register.d.ts +9 -0
- package/dist/pos/web/Register.js +252 -0
- package/dist/pos/web/index.d.ts +11 -0
- package/dist/pos/web/index.js +16 -0
- package/dist/pos/web/messages.d.ts +2 -0
- package/dist/pos/web/messages.js +87 -0
- package/dist/pos_stock/.kosh/bindings.d.ts +22 -0
- package/dist/pos_stock/.kosh/bindings.js +15 -0
- package/dist/pos_stock/.kosh/schema.d.ts +2208 -0
- package/dist/pos_stock/index.d.ts +9 -0
- package/dist/pos_stock/index.js +35 -0
- package/dist/pos_stock/module.d.ts +12803 -0
- package/dist/pos_stock/module.js +115 -0
- package/dist/product/.kosh/bindings.js +1 -1
- package/dist/product/.kosh/schema.d.ts +52 -1
- package/dist/product/index.js +94 -4
- package/dist/product/module.d.ts +45 -1
- package/dist/product/module.js +11 -3
- package/dist/project/.kosh/bindings.js +1 -1
- package/dist/project/.kosh/schema.d.ts +725 -3
- package/dist/project/index.d.ts +3 -1
- package/dist/project/index.js +1045 -18
- package/dist/project/module.d.ts +10168 -458
- package/dist/project/module.js +1578 -14
- 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 +725 -3
- package/dist/project_portal/module.d.ts +5137 -119
- 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 +150 -6
- package/dist/purchase/index.js +163 -3
- package/dist/purchase/module.d.ts +228 -5
- package/dist/purchase/module.js +30 -3
- package/dist/purchase_account/.kosh/bindings.js +1 -1
- package/dist/purchase_account/.kosh/schema.d.ts +180 -8
- package/dist/purchase_account/index.js +27 -1
- package/dist/purchase_account/module.d.ts +591 -30
- 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 +150 -6
- package/dist/purchase_portal/module.d.ts +372 -6
- package/dist/purchase_stock/.kosh/bindings.d.ts +22 -0
- package/dist/purchase_stock/.kosh/bindings.js +15 -0
- package/dist/purchase_stock/.kosh/schema.d.ts +1479 -0
- package/dist/purchase_stock/index.d.ts +7 -0
- package/dist/purchase_stock/index.js +98 -0
- package/dist/purchase_stock/module.d.ts +7535 -0
- package/dist/purchase_stock/module.js +380 -0
- package/dist/purchase_stock_account/.kosh/bindings.d.ts +22 -0
- package/dist/purchase_stock_account/.kosh/bindings.js +15 -0
- package/dist/purchase_stock_account/.kosh/schema.d.ts +2155 -0
- package/dist/purchase_stock_account/index.d.ts +3 -0
- package/dist/purchase_stock_account/index.js +24 -0
- package/dist/purchase_stock_account/module.d.ts +38015 -0
- package/dist/purchase_stock_account/module.js +168 -0
- package/dist/recruitment/.kosh/bindings.d.ts +22 -0
- package/dist/recruitment/.kosh/bindings.js +15 -0
- package/dist/recruitment/.kosh/schema.d.ts +991 -0
- package/dist/recruitment/demo.d.ts +6 -0
- package/dist/recruitment/demo.js +45 -0
- package/dist/recruitment/index.d.ts +3 -0
- package/dist/recruitment/index.js +385 -0
- package/dist/recruitment/module.d.ts +3249 -0
- package/dist/recruitment/module.js +194 -0
- package/dist/recruitment/web/HireButton.d.ts +9 -0
- package/dist/recruitment/web/HireButton.js +32 -0
- package/dist/recruitment/web/index.d.ts +11 -0
- package/dist/recruitment/web/index.js +16 -0
- package/dist/recruitment/web/messages.d.ts +2 -0
- package/dist/recruitment/web/messages.js +19 -0
- package/dist/reporting/documents.d.ts +5 -0
- package/dist/reporting/documents.js +28 -1
- package/dist/reporting/index.d.ts +1 -1
- package/dist/reporting/index.js +4 -2
- package/dist/reporting/models.js +9 -0
- package/dist/reporting/overviews.d.ts +16 -0
- package/dist/reporting/overviews.js +56 -0
- package/dist/reporting/routes-documents.js +1 -1
- package/dist/reporting/routes.js +138 -80
- package/dist/reporting/web/DashboardView.d.ts +11 -0
- package/dist/reporting/web/DashboardView.js +2 -2
- package/dist/reporting/web/DesignerView.js +5 -0
- package/dist/reporting/web/Overview.d.ts +9 -0
- package/dist/reporting/web/Overview.js +53 -0
- package/dist/reporting/web/index.d.ts +1 -0
- package/dist/reporting/web/index.js +4 -1
- package/dist/reporting/web/messages.js +9 -0
- package/dist/sale_account/.kosh/bindings.js +1 -1
- package/dist/sale_account/.kosh/schema.d.ts +181 -6
- package/dist/sale_account/index.js +22 -1
- package/dist/sale_account/module.d.ts +639 -38
- package/dist/sale_portal/.kosh/bindings.js +1 -1
- package/dist/sale_portal/.kosh/schema.d.ts +59 -3
- package/dist/sale_portal/module.d.ts +435 -5
- package/dist/sale_project/.kosh/bindings.js +1 -1
- package/dist/sale_project/.kosh/schema.d.ts +732 -3
- package/dist/sale_project/module.d.ts +5349 -208
- package/dist/sale_project/module.js +17 -1
- package/dist/sale_stock/.kosh/bindings.d.ts +22 -0
- package/dist/sale_stock/.kosh/bindings.js +15 -0
- package/dist/sale_stock/.kosh/schema.d.ts +1303 -0
- package/dist/sale_stock/index.d.ts +7 -0
- package/dist/sale_stock/index.js +84 -0
- package/dist/sale_stock/module.d.ts +7514 -0
- package/dist/sale_stock/module.js +146 -0
- package/dist/sales/.kosh/bindings.js +1 -1
- package/dist/sales/.kosh/schema.d.ts +59 -3
- package/dist/sales/index.js +195 -4
- package/dist/sales/module.d.ts +356 -3
- package/dist/sales/module.js +78 -3
- package/dist/server/api/metadata.d.ts +5 -0
- package/dist/server/api/metadata.js +5 -0
- package/dist/server/api/rest.js +64 -1
- package/dist/server/api/views.js +1 -0
- package/dist/server/app-settings.d.ts +7 -0
- package/dist/server/app-settings.js +10 -0
- package/dist/server/auth/base.js +9 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +1 -0
- package/dist/settings/app-settings.d.ts +2 -0
- package/dist/settings/app-settings.js +518 -0
- package/dist/settings/index.d.ts +1 -0
- package/dist/settings/index.js +15 -0
- package/dist/settings/people.js +29 -5
- package/dist/settings/web/AppSettingsPage.d.ts +11 -0
- package/dist/settings/web/AppSettingsPage.js +210 -0
- package/dist/settings/web/OverviewPage.js +16 -3
- package/dist/settings/web/PeoplePage.js +20 -6
- package/dist/settings/web/index.d.ts +1 -0
- package/dist/settings/web/index.js +3 -0
- package/dist/settings/web/messages.js +118 -2
- 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.d.ts +22 -0
- package/dist/stock/.kosh/bindings.js +15 -0
- package/dist/stock/.kosh/schema.d.ts +1222 -0
- package/dist/stock/demo.d.ts +8 -0
- package/dist/stock/demo.js +82 -0
- package/dist/stock/index.d.ts +4 -0
- package/dist/stock/index.js +903 -0
- package/dist/stock/module.d.ts +7856 -0
- package/dist/stock/module.js +1880 -0
- 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/ScanBox.d.ts +8 -0
- package/dist/stock/web/ScanBox.js +90 -0
- package/dist/stock/web/index.d.ts +13 -0
- package/dist/stock/web/index.js +20 -0
- package/dist/stock/web/messages.d.ts +2 -0
- package/dist/stock/web/messages.js +77 -0
- package/dist/stock_account/.kosh/bindings.d.ts +22 -0
- package/dist/stock_account/.kosh/bindings.js +15 -0
- package/dist/stock_account/.kosh/schema.d.ts +2068 -0
- package/dist/stock_account/index.d.ts +3 -0
- package/dist/stock_account/index.js +140 -0
- package/dist/stock_account/module.d.ts +11523 -0
- package/dist/stock_account/module.js +339 -0
- package/dist/storage/attachments.d.ts +5 -0
- package/dist/storage/attachments.js +12 -1
- package/dist/storage/models.js +3 -0
- package/dist/storage/routes.js +13 -2
- package/dist/todo/.kosh/bindings.d.ts +22 -0
- package/dist/todo/.kosh/bindings.js +15 -0
- package/dist/todo/.kosh/schema.d.ts +732 -0
- package/dist/todo/demo.d.ts +6 -0
- package/dist/todo/demo.js +35 -0
- package/dist/todo/index.d.ts +3 -0
- package/dist/todo/index.js +187 -0
- package/dist/todo/module.d.ts +1180 -0
- package/dist/todo/module.js +130 -0
- package/dist/trade_finance/.kosh/bindings.d.ts +22 -0
- package/dist/trade_finance/.kosh/bindings.js +15 -0
- package/dist/trade_finance/.kosh/schema.d.ts +884 -0
- package/dist/trade_finance/demo.d.ts +6 -0
- package/dist/trade_finance/demo.js +63 -0
- package/dist/trade_finance/index.d.ts +3 -0
- package/dist/trade_finance/index.js +415 -0
- package/dist/trade_finance/module.d.ts +3779 -0
- package/dist/trade_finance/module.js +507 -0
- package/dist/trade_finance_account/.kosh/bindings.d.ts +22 -0
- package/dist/trade_finance_account/.kosh/bindings.js +15 -0
- package/dist/trade_finance_account/.kosh/schema.d.ts +1830 -0
- package/dist/trade_finance_account/index.d.ts +3 -0
- package/dist/trade_finance_account/index.js +93 -0
- package/dist/trade_finance_account/module.d.ts +8074 -0
- package/dist/trade_finance_account/module.js +245 -0
- package/dist/trade_finance_purchase/.kosh/bindings.d.ts +22 -0
- package/dist/trade_finance_purchase/.kosh/bindings.js +15 -0
- package/dist/trade_finance_purchase/.kosh/schema.d.ts +1246 -0
- package/dist/trade_finance_purchase/index.d.ts +8 -0
- package/dist/trade_finance_purchase/index.js +50 -0
- package/dist/trade_finance_purchase/module.d.ts +4257 -0
- package/dist/trade_finance_purchase/module.js +44 -0
- package/dist/trade_finance_sale/.kosh/bindings.d.ts +22 -0
- package/dist/trade_finance_sale/.kosh/bindings.js +15 -0
- package/dist/trade_finance_sale/.kosh/schema.d.ts +1072 -0
- package/dist/trade_finance_sale/index.d.ts +8 -0
- package/dist/trade_finance_sale/index.js +50 -0
- package/dist/trade_finance_sale/module.d.ts +4281 -0
- package/dist/trade_finance_sale/module.js +44 -0
- package/dist/ui/api.d.ts +2 -1
- package/dist/ui/api.js +2 -1
- package/dist/ui/app.js +26 -13
- package/dist/ui/components/AppIcon.js +30 -1
- package/dist/ui/components/primitives.d.ts +2 -0
- package/dist/ui/components/primitives.js +7 -1
- package/dist/ui/i18n/messages.d.ts +27 -0
- package/dist/ui/i18n/messages.js +46 -0
- package/dist/ui/index.d.ts +1 -0
- package/dist/ui/index.js +1 -0
- package/dist/ui/offline/queue.d.ts +5 -1
- package/dist/ui/offline/queue.js +2 -0
- package/dist/ui/registry.d.ts +5 -0
- package/dist/ui/shell/Home.js +55 -4
- package/dist/ui/views/FormView.js +102 -19
- package/dist/ui/views/KanbanView.d.ts +2 -0
- package/dist/ui/views/KanbanView.js +40 -3
- package/dist/ui/views/ListView.js +110 -11
- package/dist/ui/views/StatsRow.d.ts +14 -0
- package/dist/ui/views/StatsRow.js +88 -0
- package/dist/ui/views/widgets.d.ts +2 -1
- package/dist/ui/views/widgets.js +200 -1
- package/dist/views/builders.d.ts +19 -2
- package/dist/views/builders.js +21 -1
- package/dist/views/condition.d.ts +6 -0
- package/dist/views/condition.js +20 -0
- package/dist/views/documents.d.ts +8 -0
- package/dist/views/extend.d.ts +2 -2
- package/dist/views/extend.js +11 -1
- package/dist/views/index.d.ts +2 -2
- package/dist/views/index.js +2 -2
- package/dist/views/types.d.ts +52 -5
- package/dist/views/validate.js +39 -1
- package/dist/website/.kosh/bindings.d.ts +22 -0
- package/dist/website/.kosh/bindings.js +15 -0
- package/dist/website/.kosh/schema.d.ts +881 -0
- package/dist/website/demo.d.ts +6 -0
- package/dist/website/demo.js +115 -0
- package/dist/website/index.d.ts +9 -0
- package/dist/website/index.js +251 -0
- package/dist/website/module.d.ts +2987 -0
- package/dist/website/module.js +282 -0
- package/dist/website/render.d.ts +45 -0
- package/dist/website/render.js +102 -0
- package/dist/website/routes.d.ts +12 -0
- package/dist/website/routes.js +246 -0
- package/dist/website/web/PagePreview.d.ts +7 -0
- package/dist/website/web/PagePreview.js +33 -0
- package/dist/website/web/index.d.ts +11 -0
- package/dist/website/web/index.js +16 -0
- package/dist/website/web/messages.d.ts +2 -0
- package/dist/website/web/messages.js +15 -0
- package/dist/website_crm/.kosh/bindings.d.ts +22 -0
- package/dist/website_crm/.kosh/bindings.js +15 -0
- package/dist/website_crm/.kosh/schema.d.ts +967 -0
- package/dist/website_crm/index.d.ts +3 -0
- package/dist/website_crm/index.js +39 -0
- package/dist/website_crm/module.d.ts +2956 -0
- package/dist/website_crm/module.js +44 -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 +200 -2
|
@@ -0,0 +1,1880 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The data layer of Stockroom (RFC 0021): warehouses, a tree of locations, what is in them (quants), and the
|
|
3
|
+
* **Transfer** — one document for receipts, deliveries and internal moves — with its lines and the reservations that
|
|
4
|
+
* hold stock for them. Quantities are exact: a line may be in any unit of its product's unit category and is converted
|
|
5
|
+
* with Shutro's `Quantity`; quants are kept in the product's unit. Stock never goes below zero: a transfer out of an
|
|
6
|
+
* internal location is validated only when all of it is reserved, and reservations take only what is available.
|
|
7
|
+
* Quants and reservations are written only here, in audited superuser scopes, never by users.
|
|
8
|
+
*/
|
|
9
|
+
import { contactsShutro } from '../contacts/index.js';
|
|
10
|
+
import { messagingShutro } from '../messaging/index.js';
|
|
11
|
+
import { productShutro } from '../product/index.js';
|
|
12
|
+
import { installationSetting } from '../server/index.js';
|
|
13
|
+
import { orm } from '../server/orm.js';
|
|
14
|
+
import { check, compute, defineModel, defineModule, extendModel, f, method, onchange, record, records, } from './.kosh/bindings.js';
|
|
15
|
+
const READ = ['read'];
|
|
16
|
+
const ALL = ['read', 'write', 'create', 'unlink'];
|
|
17
|
+
const zero = orm.toDecimal(0);
|
|
18
|
+
const asUom = (u) => orm.defineUom({
|
|
19
|
+
id: u.id,
|
|
20
|
+
name: typeof u.name === 'string' ? u.name : String(u.id),
|
|
21
|
+
category: u.category,
|
|
22
|
+
ratio: u.ratio,
|
|
23
|
+
rounding: u.rounding,
|
|
24
|
+
});
|
|
25
|
+
/**
|
|
26
|
+
* `quantity` of `unit` in `productUnit`, exactly (a dozen is 12 units). The same unit, or a product without a unit,
|
|
27
|
+
* keeps the quantity. @throws {IncompatibleUomCategoryError} across categories (refused earlier by the line's check).
|
|
28
|
+
*/
|
|
29
|
+
export function inProductUnit(quantity, unit, productUnit) {
|
|
30
|
+
if (unit === null || productUnit === null || unit.id === productUnit.id)
|
|
31
|
+
return quantity;
|
|
32
|
+
return orm.Quantity.of(quantity, asUom(unit)).to(asUom(productUnit)).value;
|
|
33
|
+
}
|
|
34
|
+
/** Whether transfers reserve when they are confirmed (App settings › Stockroom). */
|
|
35
|
+
export async function reserveOnConfirm(env) {
|
|
36
|
+
return (await installationSetting(env, 'stock.reserveOnConfirm', true)) !== false;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The default locations of a kind of transfer in the company's first warehouse: a receipt comes from the vendors
|
|
40
|
+
* into stock, a delivery goes from stock to the customers, an internal move stays in stock.
|
|
41
|
+
*/
|
|
42
|
+
export async function defaultLocations(env, kind, warehouse,
|
|
43
|
+
/** The document's company (an order's), else the user's current one. */
|
|
44
|
+
forCompany) {
|
|
45
|
+
const company = forCompany ?? env.companyId;
|
|
46
|
+
const wh = warehouse ??
|
|
47
|
+
(await env.model('stock.warehouse').search({ where: { company }, order: ['id asc'], limit: 1 }))
|
|
48
|
+
.ids[0] ??
|
|
49
|
+
null;
|
|
50
|
+
const stock = wh === null ? null : await stockRoot(env, wh);
|
|
51
|
+
const shared = async (usage) => (await env.model('stock.location').search({ where: { usage }, limit: 1 })).ids[0] ?? null;
|
|
52
|
+
// In a warehouse with steps, receipts go into Input and deliveries leave from Output (once they exist; confirming
|
|
53
|
+
// routes them there in any case).
|
|
54
|
+
const [steps] = wh === null ? [] : await env.model('stock.warehouse').browse([wh]).load({});
|
|
55
|
+
const step = async (name) => wh === null
|
|
56
|
+
? null
|
|
57
|
+
: ((await env
|
|
58
|
+
.model('stock.location')
|
|
59
|
+
.search({ where: { warehouse: wh, step: name }, limit: 1 })).ids[0] ?? null);
|
|
60
|
+
if (kind === 'receipt')
|
|
61
|
+
return {
|
|
62
|
+
warehouse: wh,
|
|
63
|
+
source: await shared('vendor'),
|
|
64
|
+
destination: (steps !== undefined && steps.receptionSteps !== 'one' ? await step('input') : null) ??
|
|
65
|
+
stock,
|
|
66
|
+
};
|
|
67
|
+
if (kind === 'delivery')
|
|
68
|
+
return {
|
|
69
|
+
warehouse: wh,
|
|
70
|
+
source: (steps !== undefined && steps.deliverySteps !== 'one' ? await step('output') : null) ??
|
|
71
|
+
stock,
|
|
72
|
+
destination: await shared('customer'),
|
|
73
|
+
};
|
|
74
|
+
return { warehouse: wh, source: stock, destination: stock };
|
|
75
|
+
}
|
|
76
|
+
export const StockWarehouse = defineModel('stock.warehouse', {
|
|
77
|
+
label: 'Warehouse',
|
|
78
|
+
recName: 'name',
|
|
79
|
+
order: ['name asc'],
|
|
80
|
+
archivable: true,
|
|
81
|
+
companyScoped: { required: true },
|
|
82
|
+
access: { 'stock.user': READ, 'stock.manager': [...ALL, 'export'] },
|
|
83
|
+
fields: {
|
|
84
|
+
name: f.char({ required: true }),
|
|
85
|
+
/** The short code that starts its locations' names (`WH/Stock`). */
|
|
86
|
+
code: f.char({ required: true, label: 'Short code' }),
|
|
87
|
+
locations: f.one2many('stock.location', 'warehouse'),
|
|
88
|
+
address: f.many2one('res.partner', { ondelete: 'set null' }),
|
|
89
|
+
/** Receipts in one step (into stock), two (Input, then stock) or three (Input, Quality control, then stock). */
|
|
90
|
+
receptionSteps: f.selection({ one: 'In one step', two: 'Input, then stock', three: 'Input, quality control, then stock' }, { required: true, default: 'one', label: 'Receipts' }),
|
|
91
|
+
/** Deliveries in one step, two (a pick into Output, then the delivery) or three (pick, pack, deliver). */
|
|
92
|
+
deliverySteps: f.selection({ one: 'In one step', two: 'Pick, then deliver', three: 'Pick, pack, then deliver' }, { required: true, default: 'one', label: 'Deliveries' }),
|
|
93
|
+
},
|
|
94
|
+
sqlConstraints: {
|
|
95
|
+
oneCode: { unique: ['company', 'code'], message: 'Another warehouse has this short code.' },
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
export const StockLocation = defineModel('stock.location', {
|
|
99
|
+
label: 'Location',
|
|
100
|
+
recName: 'fullName',
|
|
101
|
+
order: ['fullName asc'],
|
|
102
|
+
archivable: true,
|
|
103
|
+
// Vendors, customers and the adjustment location are shared by the companies.
|
|
104
|
+
companyScoped: { required: false },
|
|
105
|
+
hierarchy: { parent: 'parent', path: 'parentPath' },
|
|
106
|
+
access: { 'stock.user': READ, 'stock.manager': [...ALL, 'export', 'import'] },
|
|
107
|
+
fields: {
|
|
108
|
+
name: f.char({ required: true, translate: true }),
|
|
109
|
+
parent: f.many2one('stock.location', { ondelete: 'restrict', label: 'Inside' }),
|
|
110
|
+
parentPath: f.char({ readonly: true, index: true }),
|
|
111
|
+
children: f.one2many('stock.location', 'parent'),
|
|
112
|
+
warehouse: f.many2one('stock.warehouse', { ondelete: 'cascade' }),
|
|
113
|
+
usage: f.selection({
|
|
114
|
+
internal: 'Internal (holds stock)',
|
|
115
|
+
vendor: 'Vendors',
|
|
116
|
+
customer: 'Customers',
|
|
117
|
+
adjustment: 'Inventory adjustment',
|
|
118
|
+
production: 'Production',
|
|
119
|
+
transit: 'In transit',
|
|
120
|
+
}, { required: true, default: 'internal' }),
|
|
121
|
+
/** The step of the warehouse's routes this location is for (its Input, Quality control, Packing or Output). */
|
|
122
|
+
step: f.selection({ input: 'Input', quality: 'Quality control', packing: 'Packing', output: 'Output' }, { label: 'Step' }),
|
|
123
|
+
/**
|
|
124
|
+
* `WH/Stock/Shelf 1`: the warehouse code and the names from the top (four levels by name; deeper ones start
|
|
125
|
+
* with `…`). Shutro refuses a computed field that depends on itself, so it reads the stored names.
|
|
126
|
+
*/
|
|
127
|
+
fullName: f.char({
|
|
128
|
+
store: true,
|
|
129
|
+
label: 'Location',
|
|
130
|
+
...compute('stock.location', 'fullName', [
|
|
131
|
+
'name',
|
|
132
|
+
'warehouse.code',
|
|
133
|
+
'parent.name',
|
|
134
|
+
'parent.parent.name',
|
|
135
|
+
'parent.parent.parent.name',
|
|
136
|
+
'parent.parent.parent.parent',
|
|
137
|
+
], (locations) => {
|
|
138
|
+
for (const l of locations) {
|
|
139
|
+
const names = [l.name];
|
|
140
|
+
let up = l.parent;
|
|
141
|
+
for (let depth = 0; up !== null && depth < 3; depth += 1) {
|
|
142
|
+
names.unshift(up.name);
|
|
143
|
+
up = up.parent;
|
|
144
|
+
}
|
|
145
|
+
const top = up !== null ? '…' : l.warehouse !== null ? l.warehouse.code : null;
|
|
146
|
+
l.fullName = [...(top === null ? [] : [top]), ...names].join('/');
|
|
147
|
+
}
|
|
148
|
+
}),
|
|
149
|
+
}),
|
|
150
|
+
},
|
|
151
|
+
constraints: {
|
|
152
|
+
// Stock is held in a warehouse's internal locations; a child stays in its parent's warehouse.
|
|
153
|
+
internalInWarehouse: {
|
|
154
|
+
...check('stock.location', ['usage', 'warehouse', 'parent'], async (locations) => (await locations.load({ warehouse: true, parent: { warehouse: true } }))
|
|
155
|
+
.filter((l) => (l.usage === 'internal' && l.warehouse === null) ||
|
|
156
|
+
(l.parent !== null && l.parent.warehouse?.id !== l.warehouse?.id))
|
|
157
|
+
.map((l) => l.id)),
|
|
158
|
+
message: 'An internal location belongs to a warehouse, the same as the location it is inside.',
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
export const StockLot = defineModel('stock.lot', {
|
|
163
|
+
label: 'Lot / serial number',
|
|
164
|
+
archivable: true,
|
|
165
|
+
recName: 'name',
|
|
166
|
+
order: ['expiryDate asc', 'name asc'],
|
|
167
|
+
companyScoped: { required: true },
|
|
168
|
+
access: {
|
|
169
|
+
'stock.user': ['read', 'write', 'create'],
|
|
170
|
+
'stock.manager': [...ALL, 'export', 'import'],
|
|
171
|
+
},
|
|
172
|
+
fields: {
|
|
173
|
+
name: f.char({ required: true, label: 'Lot / serial number' }),
|
|
174
|
+
product: f.many2one('product.product', { required: true, ondelete: 'restrict' }),
|
|
175
|
+
/** Reservations take the lots that expire first. */
|
|
176
|
+
expiryDate: f.date({ label: 'Expires on' }),
|
|
177
|
+
quants: f.one2many('stock.quant', 'lot'),
|
|
178
|
+
qtyOnHand: f.decimal({
|
|
179
|
+
scale: 4,
|
|
180
|
+
store: true,
|
|
181
|
+
label: 'On hand',
|
|
182
|
+
...compute('stock.lot', 'qtyOnHand', ['quants.quantity'], (lots) => {
|
|
183
|
+
for (const l of lots)
|
|
184
|
+
l.qtyOnHand = [...l.quants].reduce((s, q) => s.plus(q.quantity), zero);
|
|
185
|
+
}),
|
|
186
|
+
}),
|
|
187
|
+
/** Every transfer that moved it: where it came from and where it went. */
|
|
188
|
+
history: f.one2many('stock.lot.line', 'lot'),
|
|
189
|
+
note: f.text(),
|
|
190
|
+
},
|
|
191
|
+
sqlConstraints: {
|
|
192
|
+
onePerName: {
|
|
193
|
+
unique: ['company', 'product', 'name'],
|
|
194
|
+
message: 'This product already has a lot or serial number with this name.',
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
export const StockQuant = defineModel('stock.quant', {
|
|
199
|
+
label: 'Stock',
|
|
200
|
+
order: ['id asc'],
|
|
201
|
+
companyScoped: { required: true },
|
|
202
|
+
// Read-only to everyone: quantities change only through validated transfers.
|
|
203
|
+
access: { 'stock.user': READ, 'stock.manager': ['read', 'export'] },
|
|
204
|
+
fields: {
|
|
205
|
+
product: f.many2one('product.product', { required: true, ondelete: 'restrict' }),
|
|
206
|
+
location: f.many2one('stock.location', { required: true, ondelete: 'restrict' }),
|
|
207
|
+
/** The lot or serial number, for a tracked product. */
|
|
208
|
+
lot: f.many2one('stock.lot', { ondelete: 'restrict' }),
|
|
209
|
+
/** On hand, in the product's unit. */
|
|
210
|
+
quantity: f.decimal({ scale: 4, required: true, default: '0', label: 'On hand' }),
|
|
211
|
+
reservations: f.one2many('stock.reservation', 'quant'),
|
|
212
|
+
reserved: f.decimal({
|
|
213
|
+
scale: 4,
|
|
214
|
+
store: true,
|
|
215
|
+
label: 'Reserved',
|
|
216
|
+
...compute('stock.quant', 'reserved', ['reservations.quantity'], (quants) => {
|
|
217
|
+
for (const q of quants)
|
|
218
|
+
q.reserved = [...q.reservations].reduce((s, r) => s.plus(r.quantity), zero);
|
|
219
|
+
}),
|
|
220
|
+
}),
|
|
221
|
+
available: f.decimal({
|
|
222
|
+
scale: 4,
|
|
223
|
+
store: true,
|
|
224
|
+
label: 'Available',
|
|
225
|
+
...compute('stock.quant', 'available', ['quantity', 'reserved'], (quants) => {
|
|
226
|
+
for (const q of quants)
|
|
227
|
+
q.available = q.quantity.minus(q.reserved ?? zero);
|
|
228
|
+
}),
|
|
229
|
+
}),
|
|
230
|
+
},
|
|
231
|
+
sqlConstraints: {
|
|
232
|
+
onePerPlace: {
|
|
233
|
+
unique: ['product', 'location'],
|
|
234
|
+
where: orm.sql `lot_id IS NULL`,
|
|
235
|
+
message: 'A product has one stock line per location.',
|
|
236
|
+
},
|
|
237
|
+
onePerLot: {
|
|
238
|
+
unique: ['product', 'location', 'lot'],
|
|
239
|
+
where: orm.sql `lot_id IS NOT NULL`,
|
|
240
|
+
message: 'A lot has one stock line per location.',
|
|
241
|
+
},
|
|
242
|
+
notNegative: {
|
|
243
|
+
check: orm.sql `quantity >= 0`,
|
|
244
|
+
message: 'Stock cannot go below zero.',
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
export const StockReservation = defineModel('stock.reservation', {
|
|
249
|
+
label: 'Reservation',
|
|
250
|
+
recName: 'label',
|
|
251
|
+
order: ['id asc'],
|
|
252
|
+
access: { 'stock.user': READ, 'stock.manager': READ },
|
|
253
|
+
fields: {
|
|
254
|
+
move: f.many2one('stock.move', { required: true, ondelete: 'cascade' }),
|
|
255
|
+
quant: f.many2one('stock.quant', { required: true, ondelete: 'cascade' }),
|
|
256
|
+
quantity: f.decimal({ scale: 4, required: true }),
|
|
257
|
+
/** "2 Units reserved for TR/2026/00042 (SO/2026/00017)"; "1 Units of SN-0042 reserved for …" with a lot. */
|
|
258
|
+
label: f.char({
|
|
259
|
+
store: true,
|
|
260
|
+
label: 'Reserved for',
|
|
261
|
+
...compute('stock.reservation', 'label', [
|
|
262
|
+
'quantity',
|
|
263
|
+
'move.picking.name',
|
|
264
|
+
'move.picking.origin',
|
|
265
|
+
'quant.product.uom.name',
|
|
266
|
+
'quant.lot.name',
|
|
267
|
+
], (reservations) => {
|
|
268
|
+
for (const r of reservations) {
|
|
269
|
+
const p = r.move.picking;
|
|
270
|
+
const unit = r.quant.product.uom?.name ?? '';
|
|
271
|
+
const lot = r.quant.lot?.name;
|
|
272
|
+
r.label = `${r.quantity.toString()}${unit === '' ? '' : ` ${unit}`}${lot ? ` of ${lot}` : ''} reserved for ${p.name}${p.origin ? ` (${p.origin})` : ''}`;
|
|
273
|
+
}
|
|
274
|
+
}),
|
|
275
|
+
}),
|
|
276
|
+
},
|
|
277
|
+
sqlConstraints: {
|
|
278
|
+
positive: { check: orm.sql `quantity > 0`, message: 'A reservation holds a positive quantity.' },
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
export const StockMove = defineModel('stock.move', {
|
|
282
|
+
label: 'Transfer line',
|
|
283
|
+
order: ['id asc'],
|
|
284
|
+
access: {
|
|
285
|
+
'stock.user': ['read', 'write', 'create', 'unlink'],
|
|
286
|
+
'stock.manager': [...ALL, 'export'],
|
|
287
|
+
},
|
|
288
|
+
fields: {
|
|
289
|
+
picking: f.many2one('stock.picking', {
|
|
290
|
+
required: true,
|
|
291
|
+
ondelete: 'cascade',
|
|
292
|
+
label: 'Transfer',
|
|
293
|
+
}),
|
|
294
|
+
product: f.many2one('product.product', { required: true, ondelete: 'restrict' }),
|
|
295
|
+
name: f.char({ label: 'Description' }),
|
|
296
|
+
quantity: f.decimal({ scale: 4, required: true, default: '1', label: 'Quantity' }),
|
|
297
|
+
uom: f.many2one('uom.uom', { label: 'Unit' }),
|
|
298
|
+
/** The quantity in the product's unit (what quants count). */
|
|
299
|
+
productQty: f.decimal({
|
|
300
|
+
scale: 4,
|
|
301
|
+
store: true,
|
|
302
|
+
label: 'In the product’s unit',
|
|
303
|
+
...compute('stock.move', 'productQty', [
|
|
304
|
+
'quantity',
|
|
305
|
+
'uom.name',
|
|
306
|
+
'uom.ratio',
|
|
307
|
+
'uom.category',
|
|
308
|
+
'uom.rounding',
|
|
309
|
+
'product.uom.name',
|
|
310
|
+
'product.uom.ratio',
|
|
311
|
+
'product.uom.category',
|
|
312
|
+
'product.uom.rounding',
|
|
313
|
+
], (moves) => {
|
|
314
|
+
for (const m of moves) {
|
|
315
|
+
const unit = m.uom ?? null;
|
|
316
|
+
const productUnit = m.product.uom ?? null;
|
|
317
|
+
m.productQty =
|
|
318
|
+
unit !== null && productUnit !== null && unit.category !== productUnit.category
|
|
319
|
+
? m.quantity // refused by `sameUnitCategory`
|
|
320
|
+
: inProductUnit(m.quantity, unit, productUnit);
|
|
321
|
+
}
|
|
322
|
+
}),
|
|
323
|
+
}),
|
|
324
|
+
/**
|
|
325
|
+
* What is actually moved, in the line's unit, set while the transfer waits (empty: all of it). Less than the
|
|
326
|
+
* quantity validates part of the line; the rest goes to a backorder.
|
|
327
|
+
*/
|
|
328
|
+
quantityDone: f.decimal({ scale: 4, label: 'Done' }),
|
|
329
|
+
/** The done quantity in the product's unit. */
|
|
330
|
+
doneQty: f.decimal({
|
|
331
|
+
scale: 4,
|
|
332
|
+
store: true,
|
|
333
|
+
label: 'Done in the product’s unit',
|
|
334
|
+
...compute('stock.move', 'doneQty', [
|
|
335
|
+
'quantity',
|
|
336
|
+
'quantityDone',
|
|
337
|
+
'uom.name',
|
|
338
|
+
'uom.ratio',
|
|
339
|
+
'uom.category',
|
|
340
|
+
'uom.rounding',
|
|
341
|
+
'product.uom.name',
|
|
342
|
+
'product.uom.ratio',
|
|
343
|
+
'product.uom.category',
|
|
344
|
+
'product.uom.rounding',
|
|
345
|
+
], (moves) => {
|
|
346
|
+
for (const m of moves) {
|
|
347
|
+
const done = m.quantityDone ?? m.quantity;
|
|
348
|
+
const unit = m.uom ?? null;
|
|
349
|
+
const productUnit = m.product.uom ?? null;
|
|
350
|
+
m.doneQty =
|
|
351
|
+
unit !== null && productUnit !== null && unit.category !== productUnit.category
|
|
352
|
+
? done
|
|
353
|
+
: inProductUnit(done, unit, productUnit);
|
|
354
|
+
}
|
|
355
|
+
}),
|
|
356
|
+
}),
|
|
357
|
+
/** Take this lot only (a customer's batch, a count's loss); otherwise the lots that expire first. */
|
|
358
|
+
restrictLot: f.many2one('stock.lot', { ondelete: 'set null', label: 'Lot to take' }),
|
|
359
|
+
/** The document ordered for this line (make to order: `PO/2026/00012`, `MO/2026/00007`, `TR/2026/00031`). */
|
|
360
|
+
procurement: f.char({ label: 'Ordered on' }),
|
|
361
|
+
/** Where this line is put away inside the transfer's destination (a putaway rule's, or chosen). */
|
|
362
|
+
destination: f.many2one('stock.location', { ondelete: 'set null', label: 'Put away in' }),
|
|
363
|
+
reservations: f.one2many('stock.reservation', 'move'),
|
|
364
|
+
reserved: f.decimal({
|
|
365
|
+
scale: 4,
|
|
366
|
+
store: true,
|
|
367
|
+
label: 'Reserved',
|
|
368
|
+
...compute('stock.move', 'reserved', ['reservations.quantity'], (moves) => {
|
|
369
|
+
for (const m of moves)
|
|
370
|
+
m.reserved = [...m.reservations].reduce((s, r) => s.plus(r.quantity), zero);
|
|
371
|
+
}),
|
|
372
|
+
}),
|
|
373
|
+
},
|
|
374
|
+
methods: {
|
|
375
|
+
/**
|
|
376
|
+
* What a backorder's line copies from each of these lines: the product, description and unit. A bridge adds
|
|
377
|
+
* its link by overriding it (`sale_stock`: the order line), so the rest of a line stays tied to its origin.
|
|
378
|
+
*/
|
|
379
|
+
backorderValues: method('stock.move', async function () {
|
|
380
|
+
const out = new Map();
|
|
381
|
+
for (const m of await this.load({}))
|
|
382
|
+
out.set(m.id, {
|
|
383
|
+
product: m.product.id,
|
|
384
|
+
name: m.name,
|
|
385
|
+
uom: m.uom?.id ?? null,
|
|
386
|
+
restrictLot: m.restrictLot?.id ?? null,
|
|
387
|
+
});
|
|
388
|
+
return out;
|
|
389
|
+
}),
|
|
390
|
+
},
|
|
391
|
+
onchange: {
|
|
392
|
+
// The product's unit and name by default.
|
|
393
|
+
product: onchange('stock.move', async (d) => {
|
|
394
|
+
const p = await d.record('product');
|
|
395
|
+
if (p === null || p === undefined)
|
|
396
|
+
return;
|
|
397
|
+
if (p.uom)
|
|
398
|
+
d.set('uom', p.uom.id);
|
|
399
|
+
if ((d.get('name') ?? '') === '')
|
|
400
|
+
d.set('name', p.name);
|
|
401
|
+
}),
|
|
402
|
+
},
|
|
403
|
+
constraints: {
|
|
404
|
+
positiveQuantity: {
|
|
405
|
+
...check('stock.move', ['quantity'], async (moves) => [...moves].filter((m) => m.quantity.lte(0)).map((m) => m.id)),
|
|
406
|
+
message: 'A line moves a positive quantity.',
|
|
407
|
+
},
|
|
408
|
+
doneNotNegative: {
|
|
409
|
+
...check('stock.move', ['quantityDone'], async (moves) => [...moves].filter((m) => m.quantityDone?.lt(0) === true).map((m) => m.id)),
|
|
410
|
+
message: 'A done quantity cannot be negative.',
|
|
411
|
+
},
|
|
412
|
+
goodsOnly: {
|
|
413
|
+
...check('stock.move', ['product'], async (moves) => (await moves.load({ product: true }))
|
|
414
|
+
.filter((m) => m.product.kind !== 'goods')
|
|
415
|
+
.map((m) => m.id)),
|
|
416
|
+
message: 'Services are not kept in stock: only goods are transferred.',
|
|
417
|
+
},
|
|
418
|
+
sameUnitCategory: {
|
|
419
|
+
...check('stock.move', ['uom', 'product'], async (moves) => (await moves.load({ uom: true, product: { uom: true } }))
|
|
420
|
+
.filter((m) => m.uom !== null && m.product.uom !== null && m.uom.category !== m.product.uom.category)
|
|
421
|
+
.map((m) => m.id)),
|
|
422
|
+
message: 'The line’s unit must measure the same thing as the product’s unit (a dozen of units, grams of kg).',
|
|
423
|
+
},
|
|
424
|
+
// Lines change while the transfer is a draft (reservations would no longer match).
|
|
425
|
+
draftOnly: {
|
|
426
|
+
...check('stock.move', ['product', 'quantity', 'uom', 'picking'], async (moves) => (await moves.load({ picking: true }))
|
|
427
|
+
.filter((m) => m.picking.state !== 'draft')
|
|
428
|
+
.map((m) => m.id)),
|
|
429
|
+
message: 'Lines change while the transfer is a draft: cancel it and set it back to draft first.',
|
|
430
|
+
},
|
|
431
|
+
},
|
|
432
|
+
});
|
|
433
|
+
/** Holds what is available in the transfers' source locations (and inside them) for their lines. */
|
|
434
|
+
async function reserve(ids, env) {
|
|
435
|
+
await env.asSuperuser({ reason: 'stock reservation' }, async (su) => {
|
|
436
|
+
const pickings = await su
|
|
437
|
+
.model('stock.picking')
|
|
438
|
+
.browse(ids)
|
|
439
|
+
.load({ source: true, moves: { product: true } });
|
|
440
|
+
const taken = new Map();
|
|
441
|
+
for (const p of pickings) {
|
|
442
|
+
if (p.source.usage !== 'internal')
|
|
443
|
+
continue;
|
|
444
|
+
for (const m of p.moves) {
|
|
445
|
+
let need = (m.productQty ?? zero).minus(m.reserved ?? zero);
|
|
446
|
+
if (need.lte(0))
|
|
447
|
+
continue;
|
|
448
|
+
const found = await (await su.model('stock.quant').search({
|
|
449
|
+
where: {
|
|
450
|
+
product: m.product.id,
|
|
451
|
+
location: { childOf: p.source.id },
|
|
452
|
+
...(m.restrictLot === null ? {} : { lot: m.restrictLot.id }),
|
|
453
|
+
},
|
|
454
|
+
})).load({ lot: true });
|
|
455
|
+
// First expiry first out; lots without a date last; then the oldest stock.
|
|
456
|
+
const expiry = (d) => d instanceof Date
|
|
457
|
+
? d.getTime()
|
|
458
|
+
: typeof d === 'string' && d !== ''
|
|
459
|
+
? Date.parse(d)
|
|
460
|
+
: Number.POSITIVE_INFINITY;
|
|
461
|
+
const quants = [...found].sort((a, b) => expiry(a.lot?.expiryDate) - expiry(b.lot?.expiryDate) || a.id - b.id);
|
|
462
|
+
for (const q of quants) {
|
|
463
|
+
const free = q.quantity.minus(q.reserved ?? zero).minus(taken.get(q.id) ?? zero);
|
|
464
|
+
if (free.lte(0))
|
|
465
|
+
continue;
|
|
466
|
+
const take = free.lt(need) ? free : need;
|
|
467
|
+
await su.model('stock.reservation').create([{ move: m.id, quant: q.id, quantity: take }]);
|
|
468
|
+
taken.set(q.id, (taken.get(q.id) ?? zero).plus(take));
|
|
469
|
+
need = need.minus(take);
|
|
470
|
+
if (need.lte(0))
|
|
471
|
+
break;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
/** Releases what the transfers hold. */
|
|
478
|
+
async function unreserve(ids, env) {
|
|
479
|
+
await env.asSuperuser({ reason: 'stock unreservation' }, async (su) => {
|
|
480
|
+
const moves = await su.model('stock.move').search({ where: { picking: { in: [...ids] } } });
|
|
481
|
+
if (moves.length === 0)
|
|
482
|
+
return;
|
|
483
|
+
const held = await su
|
|
484
|
+
.model('stock.reservation')
|
|
485
|
+
.search({ where: { move: { in: [...moves.ids] } } });
|
|
486
|
+
if (held.length > 0)
|
|
487
|
+
await held.unlink();
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Moves the stock of validated transfers: what each line has done (all of it unless less is set) leaves its
|
|
492
|
+
* reservations and enters the destination's quant; the reservations are released. Lots travel with the stock: out of
|
|
493
|
+
* stock, the reserved quants' lots move and are recorded on the transfer (what left, which lot); into stock from
|
|
494
|
+
* outside (receipts, adjustment gains), a tracked product's lots and serial numbers are listed on the transfer — as
|
|
495
|
+
* many as are done, one per serial number, a serial number never received twice — and new ones are made. Lines done
|
|
496
|
+
* in part leave the rest to a backorder — a new transfer with the same partner, places and source document, and every
|
|
497
|
+
* other field of its transfer and lines that `backorderValues` gives (a bridge's order and order lines too) —
|
|
498
|
+
* confirmed at once.
|
|
499
|
+
*/
|
|
500
|
+
async function applyMoves(ids, env) {
|
|
501
|
+
await env.asSuperuser({ reason: 'stock transfer validated' }, async (su) => {
|
|
502
|
+
const pickings = await su
|
|
503
|
+
.model('stock.picking')
|
|
504
|
+
.browse(ids)
|
|
505
|
+
.load({
|
|
506
|
+
source: true,
|
|
507
|
+
destination: true,
|
|
508
|
+
company: true,
|
|
509
|
+
moves: { product: true, destination: true, reservations: { quant: true } },
|
|
510
|
+
lotLines: { lot: true, product: true },
|
|
511
|
+
});
|
|
512
|
+
const out = new Map();
|
|
513
|
+
const into = new Map();
|
|
514
|
+
const add = (place) => {
|
|
515
|
+
const key = `${place.product}:${place.location}:${place.lot ?? ''}`;
|
|
516
|
+
into.set(key, { ...place, quantity: (into.get(key)?.quantity ?? zero).plus(place.quantity) });
|
|
517
|
+
};
|
|
518
|
+
const used = [];
|
|
519
|
+
const shipped = [];
|
|
520
|
+
const backorders = [];
|
|
521
|
+
const refuse = (problem, hint) => {
|
|
522
|
+
throw new orm.ValidationError('stock.picking', ['lotLines'], problem, hint);
|
|
523
|
+
};
|
|
524
|
+
for (const p of pickings) {
|
|
525
|
+
const rest = new Map();
|
|
526
|
+
const company = p.company.id;
|
|
527
|
+
if (p.source.usage === 'internal') {
|
|
528
|
+
for (const m of p.moves) {
|
|
529
|
+
let left = m.doneQty ?? zero;
|
|
530
|
+
for (const r of m.reservations) {
|
|
531
|
+
const take = left.lt(r.quantity) ? left : r.quantity;
|
|
532
|
+
used.push(r.id);
|
|
533
|
+
left = left.minus(take);
|
|
534
|
+
if (take.lte(0))
|
|
535
|
+
continue;
|
|
536
|
+
out.set(r.quant.id, (out.get(r.quant.id) ?? zero).plus(take));
|
|
537
|
+
const lot = r.quant.lot?.id ?? null;
|
|
538
|
+
if (p.destination.usage === 'internal')
|
|
539
|
+
add({
|
|
540
|
+
product: m.product.id,
|
|
541
|
+
location: m.destination?.id ?? p.destination.id,
|
|
542
|
+
company,
|
|
543
|
+
lot,
|
|
544
|
+
quantity: take,
|
|
545
|
+
});
|
|
546
|
+
if (lot !== null)
|
|
547
|
+
shipped.push({ picking: p.id, product: m.product.id, lot, quantity: take });
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
else if (p.destination.usage === 'internal') {
|
|
552
|
+
const tracked = new Map();
|
|
553
|
+
for (const m of p.moves) {
|
|
554
|
+
const done = m.doneQty ?? zero;
|
|
555
|
+
if (m.product.tracking === 'none' || m.product.tracking === null) {
|
|
556
|
+
if (done.gt(0))
|
|
557
|
+
add({
|
|
558
|
+
product: m.product.id,
|
|
559
|
+
location: m.destination?.id ?? p.destination.id,
|
|
560
|
+
company,
|
|
561
|
+
lot: null,
|
|
562
|
+
quantity: done,
|
|
563
|
+
});
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
const t = tracked.get(m.product.id);
|
|
567
|
+
tracked.set(m.product.id, {
|
|
568
|
+
name: m.product.name,
|
|
569
|
+
serial: m.product.tracking === 'serial',
|
|
570
|
+
done: (t?.done ?? zero).plus(done),
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
for (const [product, t] of tracked) {
|
|
574
|
+
const lines = [...p.lotLines].filter((l) => l.product.id === product);
|
|
575
|
+
const given = lines.reduce((s, l) => s.plus(l.quantity), zero);
|
|
576
|
+
if (!given.eq(t.done))
|
|
577
|
+
refuse(`${t.name}: ${given.toString()} of ${t.done.toString()} have a lot or serial number.`, 'List them in Lots and serial numbers: one line per lot, one per serial number.');
|
|
578
|
+
const names = new Set();
|
|
579
|
+
for (const l of lines) {
|
|
580
|
+
const name = (l.lot?.name ?? l.lotName ?? '').trim();
|
|
581
|
+
if (t.serial && !l.quantity.eq(1))
|
|
582
|
+
refuse(`${t.name} is tracked by serial number: one line per unit.`, `${name} has ${l.quantity.toString()}.`);
|
|
583
|
+
if (t.serial && names.has(name))
|
|
584
|
+
refuse(`${t.name}: the serial number ${name} is listed twice.`, 'Each unit has its own serial number.');
|
|
585
|
+
names.add(name);
|
|
586
|
+
let lot = l.lot?.id ?? null;
|
|
587
|
+
if (lot === null) {
|
|
588
|
+
const existing = await su.model('stock.lot').search({
|
|
589
|
+
where: { company, product: orm.asId(product, 'product.product'), name },
|
|
590
|
+
limit: 1,
|
|
591
|
+
});
|
|
592
|
+
lot =
|
|
593
|
+
existing.ids[0] ??
|
|
594
|
+
(await su
|
|
595
|
+
.model('stock.lot')
|
|
596
|
+
.create([{ company, product: orm.asId(product, 'product.product'), name }])).ids[0];
|
|
597
|
+
await l.write({ lot });
|
|
598
|
+
}
|
|
599
|
+
if (t.serial) {
|
|
600
|
+
const [held] = await su.model('stock.lot').browse([lot]).load(['qtyOnHand']);
|
|
601
|
+
if ((held?.qtyOnHand ?? zero).gt(0))
|
|
602
|
+
refuse(`${t.name}: the serial number ${name} is already in stock.`, 'A serial number is received once.');
|
|
603
|
+
}
|
|
604
|
+
add({
|
|
605
|
+
product: orm.asId(product, 'product.product'),
|
|
606
|
+
location: [...p.moves].find((m) => m.product.id === product)?.destination?.id ??
|
|
607
|
+
p.destination.id,
|
|
608
|
+
company,
|
|
609
|
+
lot,
|
|
610
|
+
quantity: l.quantity,
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
for (const m of p.moves) {
|
|
616
|
+
const lineDone = m.quantityDone ?? m.quantity;
|
|
617
|
+
if (lineDone.lt(m.quantity))
|
|
618
|
+
rest.set(m.id, m.quantity.minus(lineDone));
|
|
619
|
+
if (m.quantityDone === null)
|
|
620
|
+
await m.write({ quantityDone: m.quantity });
|
|
621
|
+
}
|
|
622
|
+
if (rest.size > 0)
|
|
623
|
+
backorders.push({ picking: p.id, rest });
|
|
624
|
+
}
|
|
625
|
+
if (used.length > 0)
|
|
626
|
+
await su.model('stock.reservation').browse(used).unlink();
|
|
627
|
+
for (const [quant, quantity] of out) {
|
|
628
|
+
const [q] = await su.model('stock.quant').browse([quant]).load(['quantity']);
|
|
629
|
+
if (q !== undefined)
|
|
630
|
+
await q.write({ quantity: q.quantity.minus(quantity) });
|
|
631
|
+
}
|
|
632
|
+
for (const { product, location, company, lot, quantity } of into.values()) {
|
|
633
|
+
const found = await su.model('stock.quant').search({
|
|
634
|
+
where: { product, location, lot: lot === null ? { isNull: true } : lot },
|
|
635
|
+
limit: 1,
|
|
636
|
+
});
|
|
637
|
+
const [q] = await found.load(['quantity']);
|
|
638
|
+
if (q !== undefined)
|
|
639
|
+
await q.write({ quantity: q.quantity.plus(quantity) });
|
|
640
|
+
else
|
|
641
|
+
await su.model('stock.quant').create([{ product, location, company, lot, quantity }]);
|
|
642
|
+
}
|
|
643
|
+
// What left, lot by lot (what was listed on an outgoing transfer is replaced by what actually moved).
|
|
644
|
+
const outgoing = [...pickings].filter((p) => p.source.usage === 'internal').map((p) => p.id);
|
|
645
|
+
if (outgoing.length > 0) {
|
|
646
|
+
const listed = await su
|
|
647
|
+
.model('stock.lot.line')
|
|
648
|
+
.search({ where: { picking: { in: outgoing } } });
|
|
649
|
+
if (listed.length > 0)
|
|
650
|
+
await listed.unlink();
|
|
651
|
+
}
|
|
652
|
+
if (shipped.length > 0)
|
|
653
|
+
await su.model('stock.lot.line').create(shipped);
|
|
654
|
+
await su.model('stock.picking').browse(ids).write({ doneDate: su.today() });
|
|
655
|
+
// What follows a validation in other apps (`stock_account` values the stock).
|
|
656
|
+
await su.model('stock.picking').browse(ids).validated();
|
|
657
|
+
for (const { picking, rest } of backorders) {
|
|
658
|
+
const from = (await su.model('stock.picking').browse([picking]).backorderValues()).get(picking);
|
|
659
|
+
const lines = await su
|
|
660
|
+
.model('stock.move')
|
|
661
|
+
.browse([...rest.keys()])
|
|
662
|
+
.backorderValues();
|
|
663
|
+
const made = await su.model('stock.picking').create([
|
|
664
|
+
{
|
|
665
|
+
...from,
|
|
666
|
+
backorderOf: picking,
|
|
667
|
+
moves: {
|
|
668
|
+
create: [...rest].map(([id, quantity]) => ({ ...lines.get(id), quantity })),
|
|
669
|
+
},
|
|
670
|
+
},
|
|
671
|
+
]);
|
|
672
|
+
await su
|
|
673
|
+
.model('stock.picking')
|
|
674
|
+
.browse(made.ids)
|
|
675
|
+
.confirm();
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
const STEP_NAMES = {
|
|
680
|
+
input: 'Input',
|
|
681
|
+
quality: 'Quality control',
|
|
682
|
+
packing: 'Packing',
|
|
683
|
+
output: 'Output',
|
|
684
|
+
};
|
|
685
|
+
/** The warehouse's location for a step of its routes, made beside its stock location the first time it is needed. */
|
|
686
|
+
export async function stepLocation(env, warehouse, step) {
|
|
687
|
+
return env.asSuperuser({ reason: 'warehouse step location' }, async (su) => {
|
|
688
|
+
const found = await su
|
|
689
|
+
.withFlags({ activeTest: false })
|
|
690
|
+
.model('stock.location')
|
|
691
|
+
.search({ where: { warehouse, step }, limit: 1 });
|
|
692
|
+
if (found.ids[0] !== undefined)
|
|
693
|
+
return found.ids[0];
|
|
694
|
+
const [wh] = await su.model('stock.warehouse').browse([warehouse]).load({});
|
|
695
|
+
const made = await su.model('stock.location').create([
|
|
696
|
+
{
|
|
697
|
+
name: STEP_NAMES[step],
|
|
698
|
+
usage: 'internal',
|
|
699
|
+
warehouse,
|
|
700
|
+
company: wh?.company.id ?? su.companyId,
|
|
701
|
+
step,
|
|
702
|
+
},
|
|
703
|
+
]);
|
|
704
|
+
await su.flush();
|
|
705
|
+
return made.ids[0];
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
/** A warehouse's stock location: its first top-level internal location that is not a step. */
|
|
709
|
+
async function stockRoot(env, warehouse) {
|
|
710
|
+
return ((await env.model('stock.location').search({
|
|
711
|
+
where: { warehouse, usage: 'internal', parent: { isNull: true }, step: { isNull: true } },
|
|
712
|
+
order: ['id asc'],
|
|
713
|
+
limit: 1,
|
|
714
|
+
})).ids[0] ?? null);
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Before transfers are confirmed: a receipt into a warehouse's stock location goes into its Input when the
|
|
718
|
+
* warehouse receives in two or three steps, a delivery out of it leaves from its Output when it delivers in two or
|
|
719
|
+
* three. A location chosen inside the stock location is kept.
|
|
720
|
+
*/
|
|
721
|
+
async function routeFirstStep(ids, env) {
|
|
722
|
+
const pickings = await env
|
|
723
|
+
.model('stock.picking')
|
|
724
|
+
.browse(ids)
|
|
725
|
+
.load({ source: { warehouse: true }, destination: { warehouse: true } });
|
|
726
|
+
for (const p of pickings) {
|
|
727
|
+
if (p.kind === 'receipt' && p.destination.warehouse !== null) {
|
|
728
|
+
const wh = p.destination.warehouse;
|
|
729
|
+
if (wh.receptionSteps === 'one' || p.destination.id !== (await stockRoot(env, wh.id)))
|
|
730
|
+
continue;
|
|
731
|
+
await env
|
|
732
|
+
.model('stock.picking')
|
|
733
|
+
.browse([p.id])
|
|
734
|
+
.write({ destination: await stepLocation(env, wh.id, 'input') });
|
|
735
|
+
}
|
|
736
|
+
else if (p.kind === 'delivery' && p.source.warehouse !== null) {
|
|
737
|
+
const wh = p.source.warehouse;
|
|
738
|
+
if (wh.deliverySteps === 'one' || p.source.id !== (await stockRoot(env, wh.id)))
|
|
739
|
+
continue;
|
|
740
|
+
await env
|
|
741
|
+
.model('stock.picking')
|
|
742
|
+
.browse([p.id])
|
|
743
|
+
.write({ source: await stepLocation(env, wh.id, 'output') });
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
await env.flush();
|
|
747
|
+
}
|
|
748
|
+
/** A chained step: an internal transfer of `lines` from `source` to `destination`, confirmed (reserving). */
|
|
749
|
+
async function stepTransfer(env, from, source, destination, lines, next) {
|
|
750
|
+
const made = await env.model('stock.picking').create([
|
|
751
|
+
{
|
|
752
|
+
kind: 'internal',
|
|
753
|
+
company: from.company,
|
|
754
|
+
warehouse: from.warehouse,
|
|
755
|
+
partner: from.partner,
|
|
756
|
+
source,
|
|
757
|
+
destination,
|
|
758
|
+
origin: from.origin,
|
|
759
|
+
next,
|
|
760
|
+
moves: { create: lines.map((l) => ({ ...l })) },
|
|
761
|
+
},
|
|
762
|
+
]);
|
|
763
|
+
await env
|
|
764
|
+
.model('stock.picking')
|
|
765
|
+
.browse(made.ids)
|
|
766
|
+
.confirm();
|
|
767
|
+
return made.ids[0];
|
|
768
|
+
}
|
|
769
|
+
/**
|
|
770
|
+
* After deliveries are confirmed: one leaving from its warehouse's Output gets the steps before it — a pick from
|
|
771
|
+
* stock into Output (two steps), or a pick into Packing and a pack into Output (three) — confirmed, the pick
|
|
772
|
+
* reserving; each step is validated on its own and brings the next one its stock.
|
|
773
|
+
*/
|
|
774
|
+
async function chainDeliveries(ids, env) {
|
|
775
|
+
await env.asSuperuser({ reason: 'delivery steps' }, async (su) => {
|
|
776
|
+
const pickings = await su
|
|
777
|
+
.model('stock.picking')
|
|
778
|
+
.browse(ids)
|
|
779
|
+
.load({ source: { warehouse: true }, moves: { product: true }, previousSteps: true });
|
|
780
|
+
for (const p of pickings) {
|
|
781
|
+
const wh = p.source.warehouse;
|
|
782
|
+
if (p.kind !== 'delivery' || p.source.step !== 'output' || wh === null)
|
|
783
|
+
continue;
|
|
784
|
+
if (p.previousSteps.length > 0 || wh.deliverySteps === 'one')
|
|
785
|
+
continue;
|
|
786
|
+
const stock = await stockRoot(su, wh.id);
|
|
787
|
+
if (stock === null)
|
|
788
|
+
continue;
|
|
789
|
+
const lines = p.moves.map((m) => ({
|
|
790
|
+
product: m.product.id,
|
|
791
|
+
name: m.name,
|
|
792
|
+
quantity: m.productQty ?? m.quantity,
|
|
793
|
+
procurement: m.procurement,
|
|
794
|
+
}));
|
|
795
|
+
const from = {
|
|
796
|
+
company: p.company.id,
|
|
797
|
+
warehouse: wh.id,
|
|
798
|
+
origin: p.origin ?? p.name,
|
|
799
|
+
partner: p.partner?.id ?? null,
|
|
800
|
+
};
|
|
801
|
+
if (wh.deliverySteps === 'three') {
|
|
802
|
+
const packing = await stepLocation(su, wh.id, 'packing');
|
|
803
|
+
const pack = await stepTransfer(su, from, packing, p.source.id, lines, p.id);
|
|
804
|
+
await stepTransfer(su, from, stock, packing, lines, pack);
|
|
805
|
+
}
|
|
806
|
+
else
|
|
807
|
+
await stepTransfer(su, from, stock, p.source.id, lines, p.id);
|
|
808
|
+
}
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* After transfers are validated: what came into a warehouse's Input goes on to Quality control (three steps) or to
|
|
813
|
+
* stock, and what passed Quality control to stock — each a new transfer of what was done, confirmed; a step with a
|
|
814
|
+
* next one (a pick before its pack or delivery) reserves that one, now that its stock is there.
|
|
815
|
+
*/
|
|
816
|
+
async function chainOnValidate(ids, env) {
|
|
817
|
+
await env.asSuperuser({ reason: 'transfer steps' }, async (su) => {
|
|
818
|
+
const pickings = await su
|
|
819
|
+
.model('stock.picking')
|
|
820
|
+
.browse(ids)
|
|
821
|
+
.load({ destination: { warehouse: true }, moves: { product: true }, next: true });
|
|
822
|
+
// What was ordered for a delivery has come: the transfers waiting for it reserve it.
|
|
823
|
+
const documents = [...new Set([...pickings].flatMap((p) => [p.name, p.origin ?? '']))].filter((d) => d !== '');
|
|
824
|
+
if (documents.length > 0) {
|
|
825
|
+
const waiting = await (await su.model('stock.move').search({
|
|
826
|
+
where: { procurement: { in: documents }, 'picking.state': 'confirmed' },
|
|
827
|
+
})).load({ picking: true });
|
|
828
|
+
const ids = [...new Set(waiting.map((m) => m.picking.id))];
|
|
829
|
+
if (ids.length > 0)
|
|
830
|
+
await reserve(ids, su);
|
|
831
|
+
}
|
|
832
|
+
for (const p of pickings) {
|
|
833
|
+
if (p.next !== null && p.next.state === 'confirmed')
|
|
834
|
+
await reserve([p.next.id], su);
|
|
835
|
+
const wh = p.destination.warehouse;
|
|
836
|
+
const step = p.destination.step;
|
|
837
|
+
if (wh === null || (step !== 'input' && step !== 'quality') || p.next !== null)
|
|
838
|
+
continue;
|
|
839
|
+
const stock = await stockRoot(su, wh.id);
|
|
840
|
+
if (stock === null)
|
|
841
|
+
continue;
|
|
842
|
+
const to = step === 'input' && wh.receptionSteps === 'three'
|
|
843
|
+
? await stepLocation(su, wh.id, 'quality')
|
|
844
|
+
: stock;
|
|
845
|
+
const lines = p.moves
|
|
846
|
+
.filter((m) => (m.doneQty ?? zero).gt(0))
|
|
847
|
+
.map((m) => ({ product: m.product.id, name: m.name, quantity: m.doneQty ?? zero }));
|
|
848
|
+
if (lines.length === 0)
|
|
849
|
+
continue;
|
|
850
|
+
const next = await stepTransfer(su, {
|
|
851
|
+
company: p.company.id,
|
|
852
|
+
warehouse: wh.id,
|
|
853
|
+
origin: p.origin ?? p.name,
|
|
854
|
+
partner: p.partner?.id ?? null,
|
|
855
|
+
}, p.destination.id, to, lines, null);
|
|
856
|
+
await su
|
|
857
|
+
.model('stock.picking')
|
|
858
|
+
.browse([p.id])
|
|
859
|
+
.write({ next: next });
|
|
860
|
+
}
|
|
861
|
+
await su.flush();
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* After deliveries are confirmed: each line of a product with a make-to-order rule in the warehouse it leaves from
|
|
866
|
+
* (other than dropshipping) gets what it needs ordered by that rule — on a transfer, a request for quotation or a
|
|
867
|
+
* production order — once; the line says on which document.
|
|
868
|
+
*/
|
|
869
|
+
async function orderOnDemand(ids, env) {
|
|
870
|
+
await env.asSuperuser({ reason: 'make to order' }, async (su) => {
|
|
871
|
+
const pickings = await su
|
|
872
|
+
.model('stock.picking')
|
|
873
|
+
.browse(ids)
|
|
874
|
+
.load({ source: { warehouse: true }, moves: { product: true } });
|
|
875
|
+
const wanted = new Map();
|
|
876
|
+
for (const p of pickings) {
|
|
877
|
+
const wh = p.source.warehouse;
|
|
878
|
+
if (p.kind !== 'delivery' || wh === null)
|
|
879
|
+
continue;
|
|
880
|
+
for (const m of p.moves) {
|
|
881
|
+
if (m.procurement !== null)
|
|
882
|
+
continue;
|
|
883
|
+
const [rule] = (await su.model('stock.reorder').search({
|
|
884
|
+
where: {
|
|
885
|
+
product: m.product.id,
|
|
886
|
+
warehouse: wh.id,
|
|
887
|
+
trigger: 'order',
|
|
888
|
+
method: { ne: 'dropship' },
|
|
889
|
+
},
|
|
890
|
+
limit: 1,
|
|
891
|
+
})).ids;
|
|
892
|
+
if (rule === undefined)
|
|
893
|
+
continue;
|
|
894
|
+
const w = wanted.get(rule) ?? { quantity: zero, moves: [] };
|
|
895
|
+
wanted.set(rule, {
|
|
896
|
+
quantity: w.quantity.plus(m.productQty ?? m.quantity),
|
|
897
|
+
moves: [...w.moves, m.id],
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
if (wanted.size === 0)
|
|
902
|
+
return;
|
|
903
|
+
const made = await su
|
|
904
|
+
.model('stock.reorder')
|
|
905
|
+
.browse([...wanted.keys()])
|
|
906
|
+
.procure(new Map([...wanted].map(([id, w]) => [id, w.quantity.toString()])));
|
|
907
|
+
for (const [rule, w] of wanted) {
|
|
908
|
+
const document = made.get(rule);
|
|
909
|
+
if (document === undefined)
|
|
910
|
+
continue;
|
|
911
|
+
await su
|
|
912
|
+
.model('stock.move')
|
|
913
|
+
.browse(w.moves)
|
|
914
|
+
.write({ procurement: document });
|
|
915
|
+
await su
|
|
916
|
+
.model('stock.reorder')
|
|
917
|
+
.browse([rule])
|
|
918
|
+
.write({ lastOrder: document });
|
|
919
|
+
}
|
|
920
|
+
await su.flush();
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
/** After transfers are cancelled: the steps before them that are not done are cancelled too. */
|
|
924
|
+
async function cancelPreviousSteps(ids, env) {
|
|
925
|
+
const before = await env.model('stock.picking').search({
|
|
926
|
+
where: { next: { in: [...ids] }, state: { in: ['draft', 'confirmed'] } },
|
|
927
|
+
});
|
|
928
|
+
if (before.length > 0)
|
|
929
|
+
await before.cancel();
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* Before transfers into stock are validated: each line without a place of its own is put away where the most
|
|
933
|
+
* specific putaway rule of its destination says — the product's rule first, then its category's (or a parent
|
|
934
|
+
* category's), the lowest sequence first.
|
|
935
|
+
*/
|
|
936
|
+
async function putAway(ids, env) {
|
|
937
|
+
await env.asSuperuser({ reason: 'putaway' }, async (su) => {
|
|
938
|
+
const pickings = await su
|
|
939
|
+
.model('stock.picking')
|
|
940
|
+
.browse(ids)
|
|
941
|
+
.load({ destination: true, moves: { product: { category: true } } });
|
|
942
|
+
for (const p of pickings) {
|
|
943
|
+
if (p.destination.usage !== 'internal')
|
|
944
|
+
continue;
|
|
945
|
+
const rules = [
|
|
946
|
+
...(await (await su.model('stock.putaway').search({
|
|
947
|
+
where: { location: p.destination.id },
|
|
948
|
+
order: ['sequence asc', 'id asc'],
|
|
949
|
+
})).load({ category: true })),
|
|
950
|
+
];
|
|
951
|
+
if (rules.length === 0)
|
|
952
|
+
continue;
|
|
953
|
+
for (const m of p.moves) {
|
|
954
|
+
if (m.destination !== null)
|
|
955
|
+
continue;
|
|
956
|
+
const categories = [];
|
|
957
|
+
let c = m.product.category;
|
|
958
|
+
while (c !== null && c !== undefined && categories.length < 10) {
|
|
959
|
+
categories.push(c.id);
|
|
960
|
+
const [up] = await su.model('product.category').browse([c.id]).load({ parent: true });
|
|
961
|
+
c = up?.parent ?? null;
|
|
962
|
+
}
|
|
963
|
+
const rule = rules.find((r) => r.product?.id === m.product.id) ??
|
|
964
|
+
categories
|
|
965
|
+
.map((id) => rules.find((r) => r.product === null && r.category?.id === id))
|
|
966
|
+
.find((r) => r !== undefined);
|
|
967
|
+
if (rule !== undefined)
|
|
968
|
+
await m.write({ destination: rule.destination.id });
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
await su.flush();
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
/**
|
|
975
|
+
* A putaway rule: what arrives in `location` — of `product`, or of `category` (and the categories inside it) — is
|
|
976
|
+
* put away in `destination`, a location inside it.
|
|
977
|
+
*/
|
|
978
|
+
export const StockPutaway = defineModel('stock.putaway', {
|
|
979
|
+
label: 'Putaway rule',
|
|
980
|
+
order: ['location asc', 'sequence asc', 'id asc'],
|
|
981
|
+
archivable: true,
|
|
982
|
+
companyScoped: { required: true },
|
|
983
|
+
access: { 'stock.user': READ, 'stock.manager': [...ALL, 'export', 'import'] },
|
|
984
|
+
fields: {
|
|
985
|
+
location: f.many2one('stock.location', {
|
|
986
|
+
required: true,
|
|
987
|
+
ondelete: 'cascade',
|
|
988
|
+
label: 'When it arrives in',
|
|
989
|
+
}),
|
|
990
|
+
product: f.many2one('product.product', { ondelete: 'cascade' }),
|
|
991
|
+
category: f.many2one('product.category', { ondelete: 'cascade', label: 'Product category' }),
|
|
992
|
+
destination: f.many2one('stock.location', {
|
|
993
|
+
required: true,
|
|
994
|
+
ondelete: 'cascade',
|
|
995
|
+
label: 'Put it away in',
|
|
996
|
+
}),
|
|
997
|
+
sequence: f.integer({ required: true, default: 10 }),
|
|
998
|
+
},
|
|
999
|
+
constraints: {
|
|
1000
|
+
inside: {
|
|
1001
|
+
...check('stock.putaway', ['location', 'destination', 'product', 'category'], async (rules) => (await rules.load({ location: true, destination: true, product: true, category: true }))
|
|
1002
|
+
.filter((r) => (r.product === null) === (r.category === null) ||
|
|
1003
|
+
r.destination.usage !== 'internal' ||
|
|
1004
|
+
r.destination.id === r.location.id ||
|
|
1005
|
+
!String(r.destination.parentPath ?? '').startsWith(String(r.location.parentPath ?? '\u0000')))
|
|
1006
|
+
.map((r) => r.id)),
|
|
1007
|
+
message: 'A putaway rule is for a product or a category (one of them), into a location inside the one it arrives in.',
|
|
1008
|
+
},
|
|
1009
|
+
},
|
|
1010
|
+
});
|
|
1011
|
+
/**
|
|
1012
|
+
* The lots and serial numbers of a transfer: listed by hand on what comes into stock (an existing lot, or the name of
|
|
1013
|
+
* a new one), recorded by the validation on what leaves it. The lot's history.
|
|
1014
|
+
*/
|
|
1015
|
+
export const StockLotLine = defineModel('stock.lot.line', {
|
|
1016
|
+
label: 'Lot line',
|
|
1017
|
+
order: ['id asc'],
|
|
1018
|
+
access: {
|
|
1019
|
+
'stock.user': ['read', 'write', 'create', 'unlink'],
|
|
1020
|
+
'stock.manager': [...ALL, 'export', 'import'],
|
|
1021
|
+
},
|
|
1022
|
+
fields: {
|
|
1023
|
+
picking: f.many2one('stock.picking', {
|
|
1024
|
+
required: true,
|
|
1025
|
+
ondelete: 'cascade',
|
|
1026
|
+
label: 'Transfer',
|
|
1027
|
+
}),
|
|
1028
|
+
product: f.many2one('product.product', { required: true, ondelete: 'restrict' }),
|
|
1029
|
+
lot: f.many2one('stock.lot', { ondelete: 'restrict', label: 'Lot / serial number' }),
|
|
1030
|
+
/** A new lot or serial number, made when the transfer is validated. */
|
|
1031
|
+
lotName: f.char({ label: 'New lot / serial number' }),
|
|
1032
|
+
quantity: f.decimal({ scale: 4, required: true, default: '1' }),
|
|
1033
|
+
},
|
|
1034
|
+
sqlConstraints: {
|
|
1035
|
+
positive: { check: orm.sql `quantity > 0`, message: 'A lot line has a positive quantity.' },
|
|
1036
|
+
},
|
|
1037
|
+
constraints: {
|
|
1038
|
+
lotGiven: {
|
|
1039
|
+
...check('stock.lot.line', ['lot', 'lotName'], async (lines) => [...lines]
|
|
1040
|
+
.filter((l) => l.lot === null && (l.lotName ?? '').trim() === '')
|
|
1041
|
+
.map((l) => l.id)),
|
|
1042
|
+
message: 'Give the lot or serial number: an existing one, or the name of a new one.',
|
|
1043
|
+
},
|
|
1044
|
+
sameProduct: {
|
|
1045
|
+
...check('stock.lot.line', ['lot', 'product'], async (lines) => (await lines.load({ lot: true }))
|
|
1046
|
+
.filter((l) => l.lot !== null && l.lot.product.id !== l.product.id)
|
|
1047
|
+
.map((l) => l.id)),
|
|
1048
|
+
message: 'This lot or serial number belongs to another product.',
|
|
1049
|
+
},
|
|
1050
|
+
},
|
|
1051
|
+
});
|
|
1052
|
+
export const StockPicking = defineModel('stock.picking', {
|
|
1053
|
+
label: 'Transfer',
|
|
1054
|
+
recName: 'name',
|
|
1055
|
+
order: ['scheduledDate desc', 'id desc'],
|
|
1056
|
+
companyScoped: { required: true },
|
|
1057
|
+
tracking: ['state', 'partner', 'scheduledDate'],
|
|
1058
|
+
access: {
|
|
1059
|
+
'stock.user': ['read', 'write', 'create'],
|
|
1060
|
+
'stock.manager': [...ALL, 'export', 'import'],
|
|
1061
|
+
},
|
|
1062
|
+
fields: {
|
|
1063
|
+
name: f.char({
|
|
1064
|
+
required: true,
|
|
1065
|
+
readonly: true,
|
|
1066
|
+
sequence: { code: 'stock.picking', dateField: 'scheduledDate' },
|
|
1067
|
+
label: 'Reference',
|
|
1068
|
+
}),
|
|
1069
|
+
kind: f.selection({
|
|
1070
|
+
receipt: 'Receipt',
|
|
1071
|
+
delivery: 'Delivery',
|
|
1072
|
+
internal: 'Internal move',
|
|
1073
|
+
adjustment: 'Inventory adjustment',
|
|
1074
|
+
production: 'Production',
|
|
1075
|
+
dropship: 'Dropship',
|
|
1076
|
+
}, { required: true, default: 'receipt', label: 'Type' }),
|
|
1077
|
+
partner: f.many2one('res.partner', { ondelete: 'set null', label: 'Contact' }),
|
|
1078
|
+
warehouse: f.many2one('stock.warehouse', { ondelete: 'restrict' }),
|
|
1079
|
+
source: f.many2one('stock.location', { required: true, ondelete: 'restrict', label: 'From' }),
|
|
1080
|
+
destination: f.many2one('stock.location', {
|
|
1081
|
+
required: true,
|
|
1082
|
+
ondelete: 'restrict',
|
|
1083
|
+
label: 'To',
|
|
1084
|
+
}),
|
|
1085
|
+
scheduledDate: f.date({
|
|
1086
|
+
required: true,
|
|
1087
|
+
default: (env) => env.today(),
|
|
1088
|
+
label: 'Scheduled',
|
|
1089
|
+
}),
|
|
1090
|
+
doneDate: f.date({ label: 'Done on' }),
|
|
1091
|
+
/** The document it comes from (`SO/2026/00017`). */
|
|
1092
|
+
origin: f.char({ label: 'Source document' }),
|
|
1093
|
+
note: f.text(),
|
|
1094
|
+
moves: f.one2many('stock.move', 'picking', { label: 'Lines' }),
|
|
1095
|
+
lotLines: f.one2many('stock.lot.line', 'picking', { label: 'Lots and serial numbers' }),
|
|
1096
|
+
/** The count this adjustment applies. */
|
|
1097
|
+
count: f.many2one('stock.count', { ondelete: 'set null', label: 'Count' }),
|
|
1098
|
+
/** The transfer this one takes the rest of. */
|
|
1099
|
+
backorderOf: f.many2one('stock.picking', { ondelete: 'set null', label: 'Backorder of' }),
|
|
1100
|
+
backorders: f.one2many('stock.picking', 'backorderOf'),
|
|
1101
|
+
/** The step this one brings its stock to (a pick's pack or delivery). */
|
|
1102
|
+
next: f.many2one('stock.picking', { ondelete: 'set null', label: 'Next step' }),
|
|
1103
|
+
previousSteps: f.one2many('stock.picking', 'next', { label: 'Steps before' }),
|
|
1104
|
+
/** How much of what leaves an internal location is reserved; receipts are always ready. */
|
|
1105
|
+
availability: f.selection({ none: 'Nothing reserved', partial: 'Partly reserved', full: 'Everything reserved' }, {
|
|
1106
|
+
store: true,
|
|
1107
|
+
label: 'Availability',
|
|
1108
|
+
...compute('stock.picking', 'availability', ['source.usage', 'moves.productQty', 'moves.reserved'], (pickings) => {
|
|
1109
|
+
for (const p of pickings) {
|
|
1110
|
+
const moves = [...p.moves];
|
|
1111
|
+
if (p.source.usage !== 'internal' || moves.length === 0) {
|
|
1112
|
+
p.availability = 'full';
|
|
1113
|
+
continue;
|
|
1114
|
+
}
|
|
1115
|
+
const full = moves.every((m) => (m.reserved ?? zero).gte(m.productQty ?? zero));
|
|
1116
|
+
const some = moves.some((m) => (m.reserved ?? zero).gt(0));
|
|
1117
|
+
p.availability = full ? 'full' : some ? 'partial' : 'none';
|
|
1118
|
+
}
|
|
1119
|
+
}),
|
|
1120
|
+
}),
|
|
1121
|
+
/** The one chip: Draft, Waiting, Ready, Done or Cancelled. */
|
|
1122
|
+
status: f.selection({ draft: 'Draft', waiting: 'Waiting', ready: 'Ready', done: 'Done', cancelled: 'Cancelled' }, {
|
|
1123
|
+
store: true,
|
|
1124
|
+
label: 'Status',
|
|
1125
|
+
...compute('stock.picking', 'status', ['state', 'availability'], (pickings) => {
|
|
1126
|
+
for (const p of pickings)
|
|
1127
|
+
p.status =
|
|
1128
|
+
p.state === 'confirmed'
|
|
1129
|
+
? p.availability === 'full'
|
|
1130
|
+
? 'ready'
|
|
1131
|
+
: 'waiting'
|
|
1132
|
+
: p.state === 'cancel'
|
|
1133
|
+
? 'cancelled'
|
|
1134
|
+
: p.state;
|
|
1135
|
+
}),
|
|
1136
|
+
}),
|
|
1137
|
+
state: f.state({
|
|
1138
|
+
states: { draft: 'Draft', confirmed: 'Confirmed', done: 'Done', cancel: 'Cancelled' },
|
|
1139
|
+
initial: 'draft',
|
|
1140
|
+
transitions: {
|
|
1141
|
+
confirm: {
|
|
1142
|
+
from: ['draft'],
|
|
1143
|
+
to: 'confirmed',
|
|
1144
|
+
guard: records('stock.picking', async (pickings) => {
|
|
1145
|
+
const loaded = await pickings.load({ moves: true });
|
|
1146
|
+
return loaded.filter((p) => p.moves.length === 0);
|
|
1147
|
+
}),
|
|
1148
|
+
message: 'Add at least one line before confirming the transfer.',
|
|
1149
|
+
// The lines' checks run while the transfer is still a draft (created and confirmed at once).
|
|
1150
|
+
before: records('stock.picking', async (pickings) => {
|
|
1151
|
+
await pickings.env.flush();
|
|
1152
|
+
await pickings.env
|
|
1153
|
+
.model('stock.picking')
|
|
1154
|
+
.browse(pickings.ids)
|
|
1155
|
+
.routing();
|
|
1156
|
+
await routeFirstStep(pickings.ids, pickings.env);
|
|
1157
|
+
}),
|
|
1158
|
+
after: records('stock.picking', async (pickings) => {
|
|
1159
|
+
if (await reserveOnConfirm(pickings.env))
|
|
1160
|
+
await reserve(pickings.ids, pickings.env);
|
|
1161
|
+
await orderOnDemand(pickings.ids, pickings.env);
|
|
1162
|
+
await chainDeliveries(pickings.ids, pickings.env);
|
|
1163
|
+
}),
|
|
1164
|
+
},
|
|
1165
|
+
/** "Check availability": reserve what has come in since. */
|
|
1166
|
+
reserve: {
|
|
1167
|
+
from: ['confirmed'],
|
|
1168
|
+
to: 'confirmed',
|
|
1169
|
+
after: records('stock.picking', async (pickings) => {
|
|
1170
|
+
await reserve(pickings.ids, pickings.env);
|
|
1171
|
+
}),
|
|
1172
|
+
},
|
|
1173
|
+
unreserve: {
|
|
1174
|
+
from: ['confirmed'],
|
|
1175
|
+
to: 'confirmed',
|
|
1176
|
+
after: records('stock.picking', async (pickings) => {
|
|
1177
|
+
await unreserve(pickings.ids, pickings.env);
|
|
1178
|
+
}),
|
|
1179
|
+
},
|
|
1180
|
+
validate: {
|
|
1181
|
+
from: ['confirmed'],
|
|
1182
|
+
to: 'done',
|
|
1183
|
+
// What is done must be reserved (out of stock), and something must be done.
|
|
1184
|
+
guard: records('stock.picking', async (pickings) => {
|
|
1185
|
+
const loaded = await pickings.load({ source: true, moves: true });
|
|
1186
|
+
return loaded.filter((p) => [...p.moves].every((m) => (m.doneQty ?? zero).lte(0)) ||
|
|
1187
|
+
(p.source.usage === 'internal' &&
|
|
1188
|
+
[...p.moves].some((m) => (m.reserved ?? zero).lt(m.doneQty ?? zero))));
|
|
1189
|
+
}),
|
|
1190
|
+
message: 'Not everything done is reserved: check availability, wait for the stock, or lower the done quantities.',
|
|
1191
|
+
before: records('stock.picking', async (pickings) => {
|
|
1192
|
+
await putAway(pickings.ids, pickings.env);
|
|
1193
|
+
await applyMoves(pickings.ids, pickings.env);
|
|
1194
|
+
}),
|
|
1195
|
+
after: records('stock.picking', async (pickings) => {
|
|
1196
|
+
await chainOnValidate(pickings.ids, pickings.env);
|
|
1197
|
+
}),
|
|
1198
|
+
},
|
|
1199
|
+
cancel: {
|
|
1200
|
+
from: ['draft', 'confirmed'],
|
|
1201
|
+
to: 'cancel',
|
|
1202
|
+
after: records('stock.picking', async (pickings) => {
|
|
1203
|
+
await unreserve(pickings.ids, pickings.env);
|
|
1204
|
+
await cancelPreviousSteps(pickings.ids, pickings.env);
|
|
1205
|
+
}),
|
|
1206
|
+
},
|
|
1207
|
+
redraft: { from: ['cancel'], to: 'draft' },
|
|
1208
|
+
},
|
|
1209
|
+
extensible: true,
|
|
1210
|
+
}),
|
|
1211
|
+
},
|
|
1212
|
+
methods: {
|
|
1213
|
+
/**
|
|
1214
|
+
* Runs once these transfers are validated — their stock moved, their done quantities written — in the
|
|
1215
|
+
* validation's audited superuser scope, before any backorder is made. Nothing here; a bridge overrides it
|
|
1216
|
+
* (`stock_account`: the stock's value posted to Accounting).
|
|
1217
|
+
*/
|
|
1218
|
+
validated: method('stock.picking', async () => undefined),
|
|
1219
|
+
/**
|
|
1220
|
+
* Runs before these draft transfers are confirmed, in the confirmation's scope: where their lines go. Nothing
|
|
1221
|
+
* here; a bridge overrides it (`purchase_stock`: lines of dropshipped products leave the delivery for a dropship
|
|
1222
|
+
* from the vendor, on a request for quotation).
|
|
1223
|
+
*/
|
|
1224
|
+
routing: method('stock.picking', async () => undefined),
|
|
1225
|
+
/**
|
|
1226
|
+
* What a backorder copies from each of these transfers: the type, contact, places, company, source document,
|
|
1227
|
+
* date and note. A bridge adds its link by overriding it (`sale_stock`: the sales order).
|
|
1228
|
+
*/
|
|
1229
|
+
backorderValues: method('stock.picking', async function () {
|
|
1230
|
+
const out = new Map();
|
|
1231
|
+
for (const p of await this.load({}))
|
|
1232
|
+
out.set(p.id, {
|
|
1233
|
+
kind: p.kind,
|
|
1234
|
+
partner: p.partner?.id ?? null,
|
|
1235
|
+
warehouse: p.warehouse?.id ?? null,
|
|
1236
|
+
source: p.source.id,
|
|
1237
|
+
destination: p.destination.id,
|
|
1238
|
+
company: p.company.id,
|
|
1239
|
+
origin: p.origin,
|
|
1240
|
+
scheduledDate: p.scheduledDate,
|
|
1241
|
+
note: p.note,
|
|
1242
|
+
next: p.next?.id ?? null,
|
|
1243
|
+
});
|
|
1244
|
+
return out;
|
|
1245
|
+
}),
|
|
1246
|
+
},
|
|
1247
|
+
onchange: {
|
|
1248
|
+
// A new type or warehouse proposes its locations.
|
|
1249
|
+
kind: onchange('stock.picking', async (d, env) => {
|
|
1250
|
+
const wh = d.get('warehouse');
|
|
1251
|
+
const l = await defaultLocations(env, String(d.get('kind')), wh ?? null);
|
|
1252
|
+
d.set('warehouse', l.warehouse);
|
|
1253
|
+
d.set('source', l.source);
|
|
1254
|
+
d.set('destination', l.destination);
|
|
1255
|
+
}),
|
|
1256
|
+
warehouse: onchange('stock.picking', async (d, env) => {
|
|
1257
|
+
const wh = d.get('warehouse');
|
|
1258
|
+
const l = await defaultLocations(env, String(d.get('kind')), wh ?? null);
|
|
1259
|
+
d.set('source', l.source);
|
|
1260
|
+
d.set('destination', l.destination);
|
|
1261
|
+
}),
|
|
1262
|
+
},
|
|
1263
|
+
constraints: {
|
|
1264
|
+
differentPlaces: {
|
|
1265
|
+
...check('stock.picking', ['source', 'destination', 'kind'], async (pickings) => (await pickings.load({ source: true, destination: true }))
|
|
1266
|
+
.filter((p) => p.source.id === p.destination.id ||
|
|
1267
|
+
(p.kind === 'receipt' && p.destination.usage !== 'internal') ||
|
|
1268
|
+
(p.kind === 'delivery' && p.source.usage !== 'internal') ||
|
|
1269
|
+
(p.kind === 'dropship' &&
|
|
1270
|
+
(p.source.usage !== 'vendor' || p.destination.usage !== 'customer')) ||
|
|
1271
|
+
(p.kind === 'internal' &&
|
|
1272
|
+
(p.source.usage !== 'internal' || p.destination.usage !== 'internal')) ||
|
|
1273
|
+
(p.kind === 'adjustment' &&
|
|
1274
|
+
!((p.source.usage === 'adjustment' && p.destination.usage === 'internal') ||
|
|
1275
|
+
(p.source.usage === 'internal' && p.destination.usage === 'adjustment'))) ||
|
|
1276
|
+
(p.kind === 'production' &&
|
|
1277
|
+
!((p.source.usage === 'production' && p.destination.usage === 'internal') ||
|
|
1278
|
+
(p.source.usage === 'internal' && p.destination.usage === 'production'))))
|
|
1279
|
+
.map((p) => p.id)),
|
|
1280
|
+
message: 'A transfer goes between two places: a receipt into stock, a delivery out of stock, an internal move between stock locations, an adjustment between stock and the adjustment location.',
|
|
1281
|
+
},
|
|
1282
|
+
},
|
|
1283
|
+
});
|
|
1284
|
+
/**
|
|
1285
|
+
* What `product` has in `location` and the locations inside it, in the product's unit: all of it, or that of `lot`
|
|
1286
|
+
* (`null`: the stock without a lot).
|
|
1287
|
+
*/
|
|
1288
|
+
export async function onHandIn(env, product, location, lot) {
|
|
1289
|
+
const quants = await (await env.model('stock.quant').search({
|
|
1290
|
+
where: {
|
|
1291
|
+
product,
|
|
1292
|
+
location: { childOf: location },
|
|
1293
|
+
...(lot === undefined ? {} : { lot: lot === null ? { isNull: true } : lot }),
|
|
1294
|
+
},
|
|
1295
|
+
})).load(['quantity']);
|
|
1296
|
+
return [...quants].reduce((s, q) => s.plus(q.quantity), zero);
|
|
1297
|
+
}
|
|
1298
|
+
export const StockCountLine = defineModel('stock.count.line', {
|
|
1299
|
+
label: 'Count line',
|
|
1300
|
+
order: ['id asc'],
|
|
1301
|
+
access: {
|
|
1302
|
+
'stock.user': ['read', 'write', 'create', 'unlink'],
|
|
1303
|
+
'stock.manager': [...ALL, 'export', 'import'],
|
|
1304
|
+
},
|
|
1305
|
+
fields: {
|
|
1306
|
+
count: f.many2one('stock.count', { required: true, ondelete: 'cascade' }),
|
|
1307
|
+
product: f.many2one('product.product', { required: true, ondelete: 'restrict' }),
|
|
1308
|
+
/** The lot or serial number counted, for a tracked product. */
|
|
1309
|
+
lot: f.many2one('stock.lot', { ondelete: 'restrict', label: 'Lot / serial number' }),
|
|
1310
|
+
/** On hand when the count started, in the product's unit. */
|
|
1311
|
+
expected: f.decimal({ scale: 4, label: 'Expected' }),
|
|
1312
|
+
/** What was found (empty: not counted yet, left as it is). */
|
|
1313
|
+
counted: f.decimal({ scale: 4, label: 'Counted' }),
|
|
1314
|
+
difference: f.decimal({
|
|
1315
|
+
scale: 4,
|
|
1316
|
+
store: true,
|
|
1317
|
+
label: 'Difference',
|
|
1318
|
+
...compute('stock.count.line', 'difference', ['expected', 'counted'], (lines) => {
|
|
1319
|
+
for (const l of lines)
|
|
1320
|
+
l.difference = l.counted === null ? null : l.counted.minus(l.expected ?? zero);
|
|
1321
|
+
}),
|
|
1322
|
+
}),
|
|
1323
|
+
},
|
|
1324
|
+
sqlConstraints: {
|
|
1325
|
+
onePerProduct: {
|
|
1326
|
+
unique: ['count', 'product'],
|
|
1327
|
+
where: orm.sql `lot_id IS NULL`,
|
|
1328
|
+
message: 'A product is counted once per count.',
|
|
1329
|
+
},
|
|
1330
|
+
onePerLot: {
|
|
1331
|
+
unique: ['count', 'product', 'lot'],
|
|
1332
|
+
where: orm.sql `lot_id IS NOT NULL`,
|
|
1333
|
+
message: 'A lot is counted once per count.',
|
|
1334
|
+
},
|
|
1335
|
+
notNegative: {
|
|
1336
|
+
check: orm.sql `counted IS NULL OR counted >= 0`,
|
|
1337
|
+
message: 'A counted quantity cannot be negative.',
|
|
1338
|
+
},
|
|
1339
|
+
},
|
|
1340
|
+
constraints: {
|
|
1341
|
+
goodsOnly: {
|
|
1342
|
+
...check('stock.count.line', ['product'], async (lines) => (await lines.load({ product: true }))
|
|
1343
|
+
.filter((l) => l.product.kind !== 'goods')
|
|
1344
|
+
.map((l) => l.id)),
|
|
1345
|
+
message: 'Services are not counted: only goods are kept in stock.',
|
|
1346
|
+
},
|
|
1347
|
+
},
|
|
1348
|
+
});
|
|
1349
|
+
/**
|
|
1350
|
+
* Applies counts: what was found against what is on hand now, in one adjustment transfer for the gains (from the
|
|
1351
|
+
* adjustment location into the counted location) and one for the losses (out of it, reserved first), both validated.
|
|
1352
|
+
* Refused when a loss is held by other transfers' reservations (they are named).
|
|
1353
|
+
*/
|
|
1354
|
+
async function applyCounts(ids, env) {
|
|
1355
|
+
await env.asSuperuser({ reason: 'stock count applied' }, async (su) => {
|
|
1356
|
+
const counts = await su
|
|
1357
|
+
.model('stock.count')
|
|
1358
|
+
.browse(ids)
|
|
1359
|
+
.load({ location: true, company: true, lines: { product: true } });
|
|
1360
|
+
const adjustment = (await su
|
|
1361
|
+
.model('stock.location')
|
|
1362
|
+
.search({ where: { usage: 'adjustment', name: 'Inventory adjustment' }, limit: 1 })).ids[0];
|
|
1363
|
+
if (adjustment === undefined)
|
|
1364
|
+
throw new orm.ValidationError('stock.count', ['state'], 'There is no inventory adjustment location.', 'Upgrade the Stockroom app to restore it.');
|
|
1365
|
+
for (const c of counts) {
|
|
1366
|
+
const gains = [];
|
|
1367
|
+
const losses = [];
|
|
1368
|
+
for (const l of c.lines) {
|
|
1369
|
+
if (l.counted === null)
|
|
1370
|
+
continue;
|
|
1371
|
+
const lot = l.lot?.id ?? null;
|
|
1372
|
+
const tracked = l.product.tracking !== 'none' && l.product.tracking !== null;
|
|
1373
|
+
const diff = l.counted.minus(await onHandIn(su, l.product.id, c.location.id, lot));
|
|
1374
|
+
if (diff.gt(0) && tracked && lot === null)
|
|
1375
|
+
throw new orm.ValidationError('stock.count', ['lines'], `${l.product.name} is tracked: the stock found needs its lot or serial number.`, 'Choose the lot on its line, or scan the lot or serial number.');
|
|
1376
|
+
if (diff.gt(0))
|
|
1377
|
+
gains.push({ product: l.product.id, lot, quantity: diff });
|
|
1378
|
+
if (diff.lt(0))
|
|
1379
|
+
losses.push({ product: l.product.id, lot, quantity: diff.negated() });
|
|
1380
|
+
}
|
|
1381
|
+
for (const [lines, source, destination] of [
|
|
1382
|
+
[gains, adjustment, c.location.id],
|
|
1383
|
+
[losses, c.location.id, adjustment],
|
|
1384
|
+
]) {
|
|
1385
|
+
if (lines.length === 0)
|
|
1386
|
+
continue;
|
|
1387
|
+
const made = await su.model('stock.picking').create([
|
|
1388
|
+
{
|
|
1389
|
+
kind: 'adjustment',
|
|
1390
|
+
company: c.company.id,
|
|
1391
|
+
source,
|
|
1392
|
+
destination,
|
|
1393
|
+
origin: c.name,
|
|
1394
|
+
count: c.id,
|
|
1395
|
+
moves: {
|
|
1396
|
+
create: lines.map((l) => ({
|
|
1397
|
+
product: l.product,
|
|
1398
|
+
quantity: l.quantity,
|
|
1399
|
+
restrictLot: source === adjustment ? null : l.lot,
|
|
1400
|
+
})),
|
|
1401
|
+
},
|
|
1402
|
+
// Found stock enters with its lot.
|
|
1403
|
+
lotLines: {
|
|
1404
|
+
create: source === adjustment
|
|
1405
|
+
? lines
|
|
1406
|
+
.filter((l) => l.lot !== null)
|
|
1407
|
+
.map((l) => ({ product: l.product, lot: l.lot, quantity: l.quantity }))
|
|
1408
|
+
: [],
|
|
1409
|
+
},
|
|
1410
|
+
},
|
|
1411
|
+
]);
|
|
1412
|
+
await made.confirm();
|
|
1413
|
+
if (source !== adjustment)
|
|
1414
|
+
await made.reserve();
|
|
1415
|
+
const [loaded] = await made.load(['availability']);
|
|
1416
|
+
if (loaded?.availability !== 'full') {
|
|
1417
|
+
const held = await (await su.model('stock.reservation').search({
|
|
1418
|
+
where: { 'quant.location': { childOf: c.location.id } },
|
|
1419
|
+
limit: 3,
|
|
1420
|
+
})).load(['label']);
|
|
1421
|
+
throw new orm.ValidationError('stock.count', ['state'], `Some of what is missing is reserved: ${[...held].map((r) => r.label).join('; ')}.`, 'Unreserve or cancel those transfers, then apply the count again.');
|
|
1422
|
+
}
|
|
1423
|
+
await made.validate();
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
});
|
|
1427
|
+
}
|
|
1428
|
+
export const StockCount = defineModel('stock.count', {
|
|
1429
|
+
label: 'Count',
|
|
1430
|
+
recName: 'name',
|
|
1431
|
+
order: ['date desc', 'id desc'],
|
|
1432
|
+
companyScoped: { required: true },
|
|
1433
|
+
tracking: ['state'],
|
|
1434
|
+
access: {
|
|
1435
|
+
'stock.user': ['read', 'write', 'create'],
|
|
1436
|
+
'stock.manager': [...ALL, 'export'],
|
|
1437
|
+
},
|
|
1438
|
+
fields: {
|
|
1439
|
+
name: f.char({
|
|
1440
|
+
required: true,
|
|
1441
|
+
readonly: true,
|
|
1442
|
+
sequence: { code: 'stock.count', dateField: 'date' },
|
|
1443
|
+
label: 'Reference',
|
|
1444
|
+
}),
|
|
1445
|
+
location: f.many2one('stock.location', { required: true, ondelete: 'restrict' }),
|
|
1446
|
+
date: f.date({ required: true, default: (env) => env.today() }),
|
|
1447
|
+
user: f.many2one('res.users', {
|
|
1448
|
+
default: (env) => env.userId,
|
|
1449
|
+
label: 'Counted by',
|
|
1450
|
+
}),
|
|
1451
|
+
lines: f.one2many('stock.count.line', 'count'),
|
|
1452
|
+
adjustments: f.one2many('stock.picking', 'count'),
|
|
1453
|
+
note: f.text(),
|
|
1454
|
+
state: f.state({
|
|
1455
|
+
states: { draft: 'Draft', counting: 'Counting', done: 'Applied', cancel: 'Cancelled' },
|
|
1456
|
+
initial: 'draft',
|
|
1457
|
+
transitions: {
|
|
1458
|
+
/** Lists what the location holds (and inside it), with what is expected, for counting. */
|
|
1459
|
+
start: {
|
|
1460
|
+
from: ['draft'],
|
|
1461
|
+
to: 'counting',
|
|
1462
|
+
after: records('stock.count', async (counts) => {
|
|
1463
|
+
const env = counts.env;
|
|
1464
|
+
await env.asSuperuser({ reason: 'stock count started' }, async (su) => {
|
|
1465
|
+
for (const c of await su
|
|
1466
|
+
.model('stock.count')
|
|
1467
|
+
.browse(counts.ids)
|
|
1468
|
+
.load({ lines: true })) {
|
|
1469
|
+
// By product and lot. Indexed first: creating a line unloads the count's lines.
|
|
1470
|
+
const key = (product, lot) => `${product}:${lot ?? ''}`;
|
|
1471
|
+
const existing = new Map([...c.lines].map((l) => [key(l.product.id, l.lot?.id ?? null), l]));
|
|
1472
|
+
const listed = new Map([...c.lines].map((l) => [
|
|
1473
|
+
key(l.product.id, l.lot?.id ?? null),
|
|
1474
|
+
{ product: l.product.id, lot: l.lot?.id ?? null },
|
|
1475
|
+
]));
|
|
1476
|
+
const quants = await (await su
|
|
1477
|
+
.model('stock.quant')
|
|
1478
|
+
.search({ where: { location: { childOf: c.location.id } } })).load({ product: true, lot: true });
|
|
1479
|
+
for (const q of quants)
|
|
1480
|
+
listed.set(key(q.product.id, q.lot?.id ?? null), {
|
|
1481
|
+
product: q.product.id,
|
|
1482
|
+
lot: q.lot?.id ?? null,
|
|
1483
|
+
});
|
|
1484
|
+
for (const [k, { product, lot }] of listed) {
|
|
1485
|
+
const expected = await onHandIn(su, product, c.location.id, lot);
|
|
1486
|
+
const line = existing.get(k);
|
|
1487
|
+
if (line !== undefined)
|
|
1488
|
+
await line.write({ expected });
|
|
1489
|
+
else
|
|
1490
|
+
await su
|
|
1491
|
+
.model('stock.count.line')
|
|
1492
|
+
.create([{ count: c.id, product, lot, expected }]);
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
});
|
|
1496
|
+
}),
|
|
1497
|
+
},
|
|
1498
|
+
apply: {
|
|
1499
|
+
from: ['counting'],
|
|
1500
|
+
to: 'done',
|
|
1501
|
+
requires: 'stock.count_apply',
|
|
1502
|
+
before: records('stock.count', async (counts) => {
|
|
1503
|
+
await applyCounts(counts.ids, counts.env);
|
|
1504
|
+
}),
|
|
1505
|
+
},
|
|
1506
|
+
cancel: { from: ['draft', 'counting'], to: 'cancel' },
|
|
1507
|
+
redraft: { from: ['cancel'], to: 'draft' },
|
|
1508
|
+
},
|
|
1509
|
+
}),
|
|
1510
|
+
},
|
|
1511
|
+
constraints: {
|
|
1512
|
+
internalLocation: {
|
|
1513
|
+
...check('stock.count', ['location'], async (counts) => (await counts.load({ location: true }))
|
|
1514
|
+
.filter((c) => c.location.usage !== 'internal')
|
|
1515
|
+
.map((c) => c.id)),
|
|
1516
|
+
message: 'Stock is counted in an internal location.',
|
|
1517
|
+
},
|
|
1518
|
+
},
|
|
1519
|
+
});
|
|
1520
|
+
export const SCRAP_REASONS = {
|
|
1521
|
+
damaged: 'Damaged',
|
|
1522
|
+
defect: 'Defective',
|
|
1523
|
+
expired: 'Expired',
|
|
1524
|
+
spill: 'Spilt or broken in handling',
|
|
1525
|
+
other: 'Other',
|
|
1526
|
+
};
|
|
1527
|
+
/**
|
|
1528
|
+
* Scrap: stock thrown away, with its reason. Validated, it leaves its location for the Scrap location in a validated
|
|
1529
|
+
* adjustment transfer (reserved first: what other transfers hold is not scrapped), so it is valued as a loss.
|
|
1530
|
+
*/
|
|
1531
|
+
export const StockScrap = defineModel('stock.scrap', {
|
|
1532
|
+
label: 'Scrap',
|
|
1533
|
+
recName: 'name',
|
|
1534
|
+
order: ['date desc', 'id desc'],
|
|
1535
|
+
companyScoped: { required: true },
|
|
1536
|
+
tracking: ['state', 'quantity', 'reason'],
|
|
1537
|
+
access: {
|
|
1538
|
+
'stock.user': ['read', 'write', 'create'],
|
|
1539
|
+
'stock.manager': [...ALL, 'export'],
|
|
1540
|
+
},
|
|
1541
|
+
fields: {
|
|
1542
|
+
name: f.char({
|
|
1543
|
+
required: true,
|
|
1544
|
+
readonly: true,
|
|
1545
|
+
sequence: { code: 'stock.scrap', dateField: 'date' },
|
|
1546
|
+
label: 'Reference',
|
|
1547
|
+
}),
|
|
1548
|
+
product: f.many2one('product.product', { required: true, ondelete: 'restrict' }),
|
|
1549
|
+
/** In the product's unit. */
|
|
1550
|
+
quantity: f.decimal({ scale: 4, required: true, default: '1' }),
|
|
1551
|
+
/** Where it is thrown away from (empty: the main warehouse's stock). */
|
|
1552
|
+
location: f.many2one('stock.location', { ondelete: 'restrict', label: 'From' }),
|
|
1553
|
+
lot: f.many2one('stock.lot', { ondelete: 'restrict', label: 'Lot / serial number' }),
|
|
1554
|
+
reason: f.selection(SCRAP_REASONS, { required: true, default: 'damaged' }),
|
|
1555
|
+
note: f.text(),
|
|
1556
|
+
date: f.date({ required: true, default: (env) => env.today() }),
|
|
1557
|
+
picking: f.many2one('stock.picking', { ondelete: 'set null', label: 'Transfer' }),
|
|
1558
|
+
state: f.state({
|
|
1559
|
+
states: { draft: 'Draft', done: 'Scrapped', cancel: 'Cancelled' },
|
|
1560
|
+
initial: 'draft',
|
|
1561
|
+
transitions: {
|
|
1562
|
+
validate: {
|
|
1563
|
+
from: ['draft'],
|
|
1564
|
+
to: 'done',
|
|
1565
|
+
before: records('stock.scrap', async (scraps) => {
|
|
1566
|
+
await scrapStock(scraps.ids, scraps.env);
|
|
1567
|
+
}),
|
|
1568
|
+
},
|
|
1569
|
+
cancel: { from: ['draft'], to: 'cancel' },
|
|
1570
|
+
},
|
|
1571
|
+
}),
|
|
1572
|
+
},
|
|
1573
|
+
sqlConstraints: {
|
|
1574
|
+
positive: { check: orm.sql `quantity > 0`, message: 'Scrap a positive quantity.' },
|
|
1575
|
+
},
|
|
1576
|
+
});
|
|
1577
|
+
/** Moves the scraps' stock to the Scrap location through validated adjustment transfers. */
|
|
1578
|
+
async function scrapStock(ids, env) {
|
|
1579
|
+
const scrapped = (await env
|
|
1580
|
+
.model('stock.location')
|
|
1581
|
+
.search({ where: { usage: 'adjustment', name: 'Scrap' }, limit: 1 })).ids[0];
|
|
1582
|
+
if (scrapped === undefined)
|
|
1583
|
+
throw new orm.ValidationError('stock.scrap', ['state'], 'There is no Scrap location.', 'Upgrade the Stockroom app to restore it.');
|
|
1584
|
+
for (const sc of await env.model('stock.scrap').browse(ids).load({ product: true })) {
|
|
1585
|
+
const from = sc.location?.id ?? (await defaultLocations(env, 'internal', null, sc.company.id)).source;
|
|
1586
|
+
if (from === null)
|
|
1587
|
+
throw new orm.ValidationError('stock.scrap', ['location'], 'There is no stock location.', 'Choose where from.');
|
|
1588
|
+
const made = await env.model('stock.picking').create([
|
|
1589
|
+
{
|
|
1590
|
+
kind: 'adjustment',
|
|
1591
|
+
company: sc.company.id,
|
|
1592
|
+
source: from,
|
|
1593
|
+
destination: scrapped,
|
|
1594
|
+
origin: sc.name,
|
|
1595
|
+
note: sc.note,
|
|
1596
|
+
moves: {
|
|
1597
|
+
create: [
|
|
1598
|
+
{ product: sc.product.id, quantity: sc.quantity, restrictLot: sc.lot?.id ?? null },
|
|
1599
|
+
],
|
|
1600
|
+
},
|
|
1601
|
+
},
|
|
1602
|
+
]);
|
|
1603
|
+
await made.confirm();
|
|
1604
|
+
await made.reserve();
|
|
1605
|
+
const [loaded] = await made.load(['availability']);
|
|
1606
|
+
if (loaded?.availability !== 'full')
|
|
1607
|
+
throw new orm.ValidationError('stock.scrap', ['quantity'], `Not ${sc.quantity.toString()} of ${sc.product.name} is available to scrap there.`, 'Scrap less, from another location, or unreserve what other transfers hold.');
|
|
1608
|
+
await made.validate();
|
|
1609
|
+
await env
|
|
1610
|
+
.model('stock.scrap')
|
|
1611
|
+
.browse([sc.id])
|
|
1612
|
+
.write({ picking: made.ids[0] ?? null });
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
/** What a product has in stock, across the internal locations. */
|
|
1616
|
+
export const ProductStock = extendModel('product.product', {
|
|
1617
|
+
fields: {
|
|
1618
|
+
/** By lot (a batch, an expiry date) or by serial number (one unit each); lots travel with the stock. */
|
|
1619
|
+
tracking: f.selection({ none: 'Not tracked', lot: 'By lot', serial: 'By serial number' }, { required: true, default: 'none', label: 'Tracking' }),
|
|
1620
|
+
lots: f.one2many('stock.lot', 'product'),
|
|
1621
|
+
quants: f.one2many('stock.quant', 'product'),
|
|
1622
|
+
qtyOnHand: f.decimal({
|
|
1623
|
+
scale: 4,
|
|
1624
|
+
store: true,
|
|
1625
|
+
label: 'On hand',
|
|
1626
|
+
...compute('product.product', 'qtyOnHand', ['quants.quantity'], (products) => {
|
|
1627
|
+
for (const p of products)
|
|
1628
|
+
p.qtyOnHand = [...p.quants].reduce((s, q) => s.plus(q.quantity), zero);
|
|
1629
|
+
}),
|
|
1630
|
+
}),
|
|
1631
|
+
qtyReserved: f.decimal({
|
|
1632
|
+
scale: 4,
|
|
1633
|
+
store: true,
|
|
1634
|
+
label: 'Reserved',
|
|
1635
|
+
...compute('product.product', 'qtyReserved', ['quants.reserved'], (products) => {
|
|
1636
|
+
for (const p of products)
|
|
1637
|
+
p.qtyReserved = [...p.quants].reduce((s, q) => s.plus(q.reserved ?? zero), zero);
|
|
1638
|
+
}),
|
|
1639
|
+
}),
|
|
1640
|
+
qtyAvailable: f.decimal({
|
|
1641
|
+
scale: 4,
|
|
1642
|
+
store: true,
|
|
1643
|
+
label: 'Available',
|
|
1644
|
+
...compute('product.product', 'qtyAvailable', ['qtyOnHand', 'qtyReserved'], (products) => {
|
|
1645
|
+
for (const p of products)
|
|
1646
|
+
p.qtyAvailable = (p.qtyOnHand ?? zero).minus(p.qtyReserved ?? zero);
|
|
1647
|
+
}),
|
|
1648
|
+
}),
|
|
1649
|
+
},
|
|
1650
|
+
});
|
|
1651
|
+
/**
|
|
1652
|
+
* A warehouse's stock location, and every internal location of the warehouse — the stock location and those inside
|
|
1653
|
+
* it, and the steps of its routes (Input, Quality control, Packing, Output): what is anywhere in them is the
|
|
1654
|
+
* warehouse's.
|
|
1655
|
+
*/
|
|
1656
|
+
export async function warehouseStock(env, warehouse, company) {
|
|
1657
|
+
const root = (await defaultLocations(env, 'internal', warehouse, company)).source;
|
|
1658
|
+
if (root === null)
|
|
1659
|
+
return { root, inside: new Set() };
|
|
1660
|
+
const inside = await env
|
|
1661
|
+
.model('stock.location')
|
|
1662
|
+
.search({ where: { warehouse, usage: 'internal' } });
|
|
1663
|
+
return { root, inside: new Set(inside.ids) };
|
|
1664
|
+
}
|
|
1665
|
+
/**
|
|
1666
|
+
* A reorder rule (RFC 0024 §2.6): keep `product` in `warehouse` between a minimum and a maximum. Its forecast — on
|
|
1667
|
+
* hand, less what is going out, plus what is coming in and what is on order — is refreshed hourly and on demand;
|
|
1668
|
+
* below the minimum it proposes enough to reach the maximum, in steps of `multiple`, with the reckoning spelled out.
|
|
1669
|
+
* How it is ordered is its method: **transfer** from another warehouse (here), **buy** from its vendor
|
|
1670
|
+
* (`purchase_stock`), **make** with the product's bill of materials (`mrp`).
|
|
1671
|
+
*/
|
|
1672
|
+
export const StockReorder = defineModel('stock.reorder', {
|
|
1673
|
+
label: 'Reorder rule',
|
|
1674
|
+
recName: 'name',
|
|
1675
|
+
order: ['warehouse asc', 'name asc'],
|
|
1676
|
+
archivable: true,
|
|
1677
|
+
companyScoped: { required: true },
|
|
1678
|
+
tracking: ['minimum', 'maximum', 'method', 'vendor'],
|
|
1679
|
+
access: { 'stock.user': READ, 'stock.manager': [...ALL, 'export', 'import'] },
|
|
1680
|
+
fields: {
|
|
1681
|
+
name: f.char({
|
|
1682
|
+
store: true,
|
|
1683
|
+
label: 'Rule',
|
|
1684
|
+
...compute('stock.reorder', 'name', ['product.name', 'warehouse.code'], (rules) => {
|
|
1685
|
+
for (const r of rules)
|
|
1686
|
+
r.name = `${r.product.name} (${r.warehouse.code})`;
|
|
1687
|
+
}),
|
|
1688
|
+
}),
|
|
1689
|
+
product: f.many2one('product.product', { required: true, ondelete: 'cascade' }),
|
|
1690
|
+
warehouse: f.many2one('stock.warehouse', { required: true, ondelete: 'cascade' }),
|
|
1691
|
+
minimum: f.decimal({ scale: 4, required: true, default: '0', label: 'Minimum' }),
|
|
1692
|
+
maximum: f.decimal({ scale: 4, required: true, default: '0', label: 'Maximum' }),
|
|
1693
|
+
/** Ordered in steps of this (a box of 12): the proposal is rounded up to it. */
|
|
1694
|
+
multiple: f.decimal({ scale: 4, required: true, default: '1', label: 'Order in steps of' }),
|
|
1695
|
+
/** Days from ordering to having it: the expected date of what is ordered. */
|
|
1696
|
+
leadDays: f.integer({ required: true, default: 0, label: 'Lead time (days)' }),
|
|
1697
|
+
method: f.selection({
|
|
1698
|
+
buy: 'Buy',
|
|
1699
|
+
make: 'Make',
|
|
1700
|
+
transfer: 'Transfer',
|
|
1701
|
+
dropship: 'Dropship (the vendor delivers to the customer)',
|
|
1702
|
+
}, { required: true, default: 'buy', label: 'Replenish by' }),
|
|
1703
|
+
/**
|
|
1704
|
+
* When it orders: when the forecast falls below the minimum, or for each delivery, what it needs (make to
|
|
1705
|
+
* order; dropshipping always is).
|
|
1706
|
+
*/
|
|
1707
|
+
trigger: f.selection({ minmax: 'Below the minimum', order: 'For each delivery (make to order)' }, { required: true, default: 'minmax', label: 'Orders' }),
|
|
1708
|
+
vendor: f.many2one('res.partner', { ondelete: 'set null', label: 'Vendor' }),
|
|
1709
|
+
sourceWarehouse: f.many2one('stock.warehouse', {
|
|
1710
|
+
ondelete: 'set null',
|
|
1711
|
+
label: 'From warehouse',
|
|
1712
|
+
}),
|
|
1713
|
+
/** Ordered by the server, hourly, whenever the forecast falls below the minimum. */
|
|
1714
|
+
auto: f.boolean({ default: false, label: 'Order automatically' }),
|
|
1715
|
+
snoozedUntil: f.date({ label: 'Snoozed until' }),
|
|
1716
|
+
onHand: f.decimal({ scale: 4, label: 'On hand' }),
|
|
1717
|
+
forecast: f.decimal({ scale: 4, label: 'Forecast' }),
|
|
1718
|
+
proposed: f.decimal({ scale: 4, label: 'To order' }),
|
|
1719
|
+
/** How the proposal was reckoned, in words. */
|
|
1720
|
+
explanation: f.text({ label: 'Why' }),
|
|
1721
|
+
refreshedAt: f.datetime({ label: 'Refreshed' }),
|
|
1722
|
+
/** The document the last order made (`P00012`, `TR/2026/00031`, `MO/2026/00007`). */
|
|
1723
|
+
lastOrder: f.char({ label: 'Last ordered' }),
|
|
1724
|
+
},
|
|
1725
|
+
methods: {
|
|
1726
|
+
/**
|
|
1727
|
+
* What is on order for these rules beyond the transfers (which the forecast reads itself), in the product's
|
|
1728
|
+
* unit (decimal strings), with the documents. Nothing here; a bridge adds its documents (`purchase_stock`: requests for quotation,
|
|
1729
|
+
* `mrp`: production orders).
|
|
1730
|
+
*/
|
|
1731
|
+
pending: method('stock.reorder', async function () {
|
|
1732
|
+
return new Map(this.ids.map((id) => [id, []]));
|
|
1733
|
+
}),
|
|
1734
|
+
/**
|
|
1735
|
+
* Orders `quantities` (decimal strings, in the product's unit) for these rules; returns the document each one made. Transfers
|
|
1736
|
+
* are made here, one per pair of warehouses, confirmed; a bridge orders its method (`purchase_stock`: buy,
|
|
1737
|
+
* `mrp`: make) after `next()`.
|
|
1738
|
+
*/
|
|
1739
|
+
procure: method('stock.reorder', async function (quantities) {
|
|
1740
|
+
const out = new Map();
|
|
1741
|
+
const groups = new Map();
|
|
1742
|
+
for (const r of await this.load({})) {
|
|
1743
|
+
if (r.method !== 'transfer' || !orm.toDecimal(quantities.get(r.id) ?? '0').gt(0))
|
|
1744
|
+
continue;
|
|
1745
|
+
if (r.sourceWarehouse === null)
|
|
1746
|
+
continue;
|
|
1747
|
+
const key = `${r.company.id}:${r.sourceWarehouse.id}:${r.warehouse.id}`;
|
|
1748
|
+
const g = groups.get(key) ?? {
|
|
1749
|
+
rules: [],
|
|
1750
|
+
from: r.sourceWarehouse.id,
|
|
1751
|
+
to: r.warehouse.id,
|
|
1752
|
+
company: r.company.id,
|
|
1753
|
+
};
|
|
1754
|
+
g.rules.push(r.id);
|
|
1755
|
+
groups.set(key, g);
|
|
1756
|
+
}
|
|
1757
|
+
for (const g of groups.values()) {
|
|
1758
|
+
const company = g.company;
|
|
1759
|
+
const from = await defaultLocations(this.env, 'internal', g.from, company);
|
|
1760
|
+
const to = await defaultLocations(this.env, 'internal', g.to, company);
|
|
1761
|
+
if (from.source === null || to.source === null)
|
|
1762
|
+
continue;
|
|
1763
|
+
const rules = await this.env
|
|
1764
|
+
.model('stock.reorder')
|
|
1765
|
+
.browse(g.rules)
|
|
1766
|
+
.load({ product: true });
|
|
1767
|
+
const made = await this.env.model('stock.picking').create([
|
|
1768
|
+
{
|
|
1769
|
+
kind: 'internal',
|
|
1770
|
+
company,
|
|
1771
|
+
warehouse: g.to,
|
|
1772
|
+
source: from.source,
|
|
1773
|
+
destination: to.source,
|
|
1774
|
+
origin: 'Replenishment',
|
|
1775
|
+
scheduledDate: this.env.today(),
|
|
1776
|
+
moves: {
|
|
1777
|
+
create: rules.map((r) => ({
|
|
1778
|
+
product: r.product.id,
|
|
1779
|
+
quantity: quantities.get(r.id) ?? '0',
|
|
1780
|
+
uom: r.product.uom?.id ?? null,
|
|
1781
|
+
})),
|
|
1782
|
+
},
|
|
1783
|
+
},
|
|
1784
|
+
]);
|
|
1785
|
+
await made.confirm();
|
|
1786
|
+
const [picking] = await made.load({});
|
|
1787
|
+
for (const r of rules)
|
|
1788
|
+
out.set(r.id, picking?.name ?? '');
|
|
1789
|
+
}
|
|
1790
|
+
return out;
|
|
1791
|
+
}),
|
|
1792
|
+
},
|
|
1793
|
+
sqlConstraints: {
|
|
1794
|
+
oneRule: {
|
|
1795
|
+
unique: ['product', 'warehouse'],
|
|
1796
|
+
message: 'This product already has a reorder rule in this warehouse.',
|
|
1797
|
+
},
|
|
1798
|
+
levels: {
|
|
1799
|
+
check: orm.sql `minimum >= 0 AND maximum >= minimum AND multiple > 0 AND lead_days >= 0`,
|
|
1800
|
+
message: 'The minimum is zero or more, the maximum at least the minimum, the step and the lead time positive.',
|
|
1801
|
+
},
|
|
1802
|
+
},
|
|
1803
|
+
constraints: {
|
|
1804
|
+
howToOrder: {
|
|
1805
|
+
...check('stock.reorder', ['method', 'vendor', 'sourceWarehouse', 'warehouse', 'product'], async (rules) => (await rules.load({
|
|
1806
|
+
product: true,
|
|
1807
|
+
warehouse: true,
|
|
1808
|
+
vendor: true,
|
|
1809
|
+
sourceWarehouse: true,
|
|
1810
|
+
}))
|
|
1811
|
+
.filter((r) => r.product.kind !== 'goods' ||
|
|
1812
|
+
((r.method === 'buy' || r.method === 'dropship') && r.vendor === null) ||
|
|
1813
|
+
(r.method === 'dropship' && r.trigger !== 'order') ||
|
|
1814
|
+
(r.method === 'transfer' &&
|
|
1815
|
+
(r.sourceWarehouse === null || r.sourceWarehouse.id === r.warehouse.id)))
|
|
1816
|
+
.map((r) => r.id)),
|
|
1817
|
+
message: 'A rule keeps goods in stock; buying and dropshipping need a vendor (dropshipping is for each delivery), a transfer another warehouse to take from.',
|
|
1818
|
+
},
|
|
1819
|
+
},
|
|
1820
|
+
});
|
|
1821
|
+
const shared = (id, name, usage) => record('stock.location', id, { name, usage, company: null }, { noupdate: true });
|
|
1822
|
+
export const stockShutro = defineModule({
|
|
1823
|
+
name: 'stock',
|
|
1824
|
+
version: '0.3.0',
|
|
1825
|
+
depends: [productShutro, contactsShutro, messagingShutro],
|
|
1826
|
+
groups: [
|
|
1827
|
+
orm.defineGroup({ code: 'stock.user', label: 'Stockroom user', implies: ['product.user'] }),
|
|
1828
|
+
orm.defineGroup({ code: 'stock.manager', label: 'Stockroom manager', implies: ['stock.user'] }),
|
|
1829
|
+
],
|
|
1830
|
+
sequences: [
|
|
1831
|
+
orm.defineSequence({
|
|
1832
|
+
code: 'stock.picking',
|
|
1833
|
+
prefix: 'TR/{year}/',
|
|
1834
|
+
padding: 5,
|
|
1835
|
+
reset: 'yearly',
|
|
1836
|
+
}),
|
|
1837
|
+
orm.defineSequence({
|
|
1838
|
+
code: 'stock.scrap',
|
|
1839
|
+
prefix: 'SC/{year}/',
|
|
1840
|
+
padding: 5,
|
|
1841
|
+
reset: 'yearly',
|
|
1842
|
+
}),
|
|
1843
|
+
orm.defineSequence({
|
|
1844
|
+
code: 'stock.count',
|
|
1845
|
+
prefix: 'CNT/{year}/',
|
|
1846
|
+
padding: 5,
|
|
1847
|
+
reset: 'yearly',
|
|
1848
|
+
}),
|
|
1849
|
+
],
|
|
1850
|
+
permissions: [
|
|
1851
|
+
orm.definePermission({
|
|
1852
|
+
code: 'stock.count_apply',
|
|
1853
|
+
label: 'Apply stock counts',
|
|
1854
|
+
groups: ['stock.manager'],
|
|
1855
|
+
}),
|
|
1856
|
+
],
|
|
1857
|
+
models: [
|
|
1858
|
+
StockWarehouse,
|
|
1859
|
+
StockLocation,
|
|
1860
|
+
StockLot,
|
|
1861
|
+
StockQuant,
|
|
1862
|
+
StockReservation,
|
|
1863
|
+
StockMove,
|
|
1864
|
+
StockPicking,
|
|
1865
|
+
StockLotLine,
|
|
1866
|
+
StockCount,
|
|
1867
|
+
StockCountLine,
|
|
1868
|
+
StockScrap,
|
|
1869
|
+
StockReorder,
|
|
1870
|
+
StockPutaway,
|
|
1871
|
+
],
|
|
1872
|
+
extensions: [ProductStock],
|
|
1873
|
+
data: [
|
|
1874
|
+
shared('location_vendors', 'Vendors', 'vendor'),
|
|
1875
|
+
shared('location_customers', 'Customers', 'customer'),
|
|
1876
|
+
shared('location_adjustment', 'Inventory adjustment', 'adjustment'),
|
|
1877
|
+
shared('location_production', 'Production', 'production'),
|
|
1878
|
+
shared('location_scrap', 'Scrap', 'adjustment'),
|
|
1879
|
+
],
|
|
1880
|
+
});
|