shutro-erp 0.3.5 → 0.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/account_asset/.kosh/bindings.js +15 -0
- package/dist/{mrp_account → account_asset}/.kosh/schema.d.ts +785 -614
- 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/i18n/bn.d.ts +2 -0
- package/dist/account_asset/i18n/bn.js +97 -0
- package/dist/account_asset/index.d.ts +9 -0
- package/dist/account_asset/index.js +337 -0
- package/dist/account_asset/module.d.ts +11076 -0
- package/dist/account_asset/module.js +230 -0
- package/dist/account_budget/.kosh/bindings.js +15 -0
- package/dist/account_budget/.kosh/schema.d.ts +2841 -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/i18n/bn.d.ts +2 -0
- package/dist/account_budget/i18n/bn.js +63 -0
- package/dist/account_budget/index.d.ts +4 -0
- package/dist/account_budget/index.js +237 -0
- package/dist/account_budget/module.d.ts +27289 -0
- package/dist/account_budget/module.js +182 -0
- package/dist/account_portal/.kosh/bindings.js +1 -1
- package/dist/account_portal/.kosh/schema.d.ts +651 -10
- package/dist/account_portal/i18n/bn.d.ts +2 -0
- package/dist/account_portal/i18n/bn.js +6 -0
- package/dist/account_portal/index.js +2 -0
- package/dist/account_portal/module.d.ts +5212 -1592
- package/dist/accounting/.kosh/bindings.js +1 -1
- package/dist/accounting/.kosh/schema.d.ts +651 -10
- package/dist/accounting/closing.js +1 -0
- package/dist/accounting/fiscal.d.ts +578 -0
- package/dist/accounting/fiscal.js +401 -0
- package/dist/accounting/followups.d.ts +618 -0
- package/dist/accounting/followups.js +420 -0
- package/dist/accounting/i18n/bn.d.ts +2 -0
- package/dist/accounting/i18n/bn.js +425 -0
- package/dist/accounting/index.d.ts +3 -1
- package/dist/accounting/index.js +780 -5
- package/dist/accounting/module.d.ts +5386 -1543
- package/dist/accounting/module.js +155 -18
- package/dist/accounting/statements.d.ts +878 -0
- package/dist/accounting/statements.js +728 -0
- package/dist/accounting/web/BankReconcilePage.d.ts +9 -0
- package/dist/accounting/web/BankReconcilePage.js +138 -0
- package/dist/accounting/web/FollowupsPage.d.ts +8 -0
- package/dist/accounting/web/FollowupsPage.js +54 -0
- package/dist/accounting/web/PostingPreview.d.ts +8 -0
- package/dist/accounting/web/PostingPreview.js +26 -0
- package/dist/accounting/web/index.d.ts +3 -0
- package/dist/accounting/web/index.js +9 -0
- package/dist/accounting/web/messages.js +140 -0
- package/dist/approvals/.kosh/bindings.d.ts +22 -0
- package/dist/approvals/.kosh/bindings.js +15 -0
- package/dist/approvals/.kosh/schema.d.ts +879 -0
- package/dist/approvals/demo.d.ts +7 -0
- package/dist/approvals/demo.js +41 -0
- package/dist/approvals/i18n/bn.d.ts +2 -0
- package/dist/approvals/i18n/bn.js +133 -0
- package/dist/approvals/index.d.ts +36 -0
- package/dist/approvals/index.js +772 -0
- package/dist/approvals/mails.d.ts +30 -0
- package/dist/approvals/mails.js +87 -0
- package/dist/approvals/module.d.ts +2930 -0
- package/dist/approvals/module.js +473 -0
- package/dist/approvals/web/Config.d.ts +12 -0
- package/dist/approvals/web/Config.js +60 -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/Matrix.d.ts +9 -0
- package/dist/approvals/web/Matrix.js +26 -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 +15 -0
- package/dist/approvals/web/index.js +29 -0
- package/dist/approvals/web/messages.d.ts +2 -0
- package/dist/approvals/web/messages.js +211 -0
- package/dist/approvals/web/shared.d.ts +59 -0
- package/dist/approvals/web/shared.js +46 -0
- package/dist/approvals_purchase/.kosh/bindings.d.ts +22 -0
- package/dist/approvals_purchase/.kosh/bindings.js +15 -0
- package/dist/approvals_purchase/.kosh/schema.d.ts +1997 -0
- package/dist/approvals_purchase/i18n/bn.d.ts +2 -0
- package/dist/approvals_purchase/i18n/bn.js +5 -0
- package/dist/approvals_purchase/index.d.ts +3 -0
- package/dist/approvals_purchase/index.js +47 -0
- package/dist/approvals_purchase/module.d.ts +12670 -0
- package/dist/approvals_purchase/module.js +84 -0
- package/dist/attendance/.kosh/bindings.d.ts +22 -0
- package/dist/attendance/.kosh/bindings.js +15 -0
- package/dist/attendance/.kosh/schema.d.ts +1106 -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/i18n/bn.d.ts +2 -0
- package/dist/attendance/i18n/bn.js +147 -0
- package/dist/attendance/index.d.ts +4 -0
- package/dist/attendance/index.js +515 -0
- package/dist/attendance/module.d.ts +4413 -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/automation/engine.js +1 -1
- package/dist/calendar/.kosh/bindings.js +1 -1
- package/dist/calendar/.kosh/schema.d.ts +2 -1
- package/dist/calendar/i18n/bn.d.ts +2 -0
- package/dist/calendar/i18n/bn.js +80 -0
- package/dist/calendar/index.js +77 -0
- package/dist/calendar/module.d.ts +26 -0
- package/dist/calendar/module.js +14 -4
- package/dist/cli/apps.js +25 -5
- package/dist/cli/init.js +46 -4
- package/dist/cli/templates.d.ts +1 -1
- package/dist/cli/templates.js +123 -5
- package/dist/contacts/.kosh/bindings.js +1 -1
- package/dist/contacts/.kosh/schema.d.ts +2 -1
- package/dist/contacts/i18n/bn.d.ts +2 -0
- package/dist/contacts/i18n/bn.js +67 -0
- package/dist/contacts/index.js +3 -0
- package/dist/contacts/module.d.ts +42 -0
- package/dist/contacts/module.js +19 -3
- package/dist/crm/.kosh/bindings.js +1 -1
- package/dist/crm/.kosh/schema.d.ts +3 -1
- package/dist/crm/i18n/bn.d.ts +2 -0
- package/dist/crm/i18n/bn.js +80 -0
- package/dist/crm/index.js +22 -0
- package/dist/crm/module.d.ts +22 -0
- package/dist/crm/module.js +6 -3
- package/dist/documents/.kosh/bindings.d.ts +22 -0
- package/dist/documents/.kosh/bindings.js +15 -0
- package/dist/documents/.kosh/schema.d.ts +863 -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/i18n/bn.d.ts +2 -0
- package/dist/documents/i18n/bn.js +83 -0
- package/dist/documents/index.d.ts +30 -0
- package/dist/documents/index.js +569 -0
- package/dist/documents/module.d.ts +2801 -0
- package/dist/documents/module.js +215 -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.js +1 -1
- package/dist/event/.kosh/schema.d.ts +2 -1
- package/dist/event/i18n/bn.d.ts +2 -0
- package/dist/event/i18n/bn.js +101 -0
- package/dist/event/index.js +66 -0
- package/dist/event/module.d.ts +20 -0
- package/dist/event/module.js +8 -1
- package/dist/event_account/.kosh/bindings.js +1 -1
- package/dist/event_account/.kosh/schema.d.ts +651 -10
- package/dist/event_account/i18n/bn.d.ts +2 -0
- package/dist/event_account/i18n/bn.js +5 -0
- package/dist/event_account/index.js +2 -0
- package/dist/event_account/module.d.ts +5133 -1513
- package/dist/expense/.kosh/bindings.d.ts +22 -0
- package/dist/expense/.kosh/bindings.js +15 -0
- package/dist/expense/.kosh/schema.d.ts +1620 -0
- package/dist/expense/demo.d.ts +7 -0
- package/dist/expense/demo.js +69 -0
- package/dist/expense/i18n/bn.d.ts +2 -0
- package/dist/expense/i18n/bn.js +155 -0
- package/dist/expense/index.d.ts +3 -0
- package/dist/expense/index.js +502 -0
- package/dist/expense/module.d.ts +6768 -0
- package/dist/expense/module.js +649 -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 +2515 -0
- package/dist/expense_account/i18n/bn.d.ts +2 -0
- package/dist/expense_account/i18n/bn.js +15 -0
- package/dist/expense_account/index.d.ts +3 -0
- package/dist/expense_account/index.js +76 -0
- package/dist/expense_account/module.d.ts +14628 -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 +886 -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/i18n/bn.d.ts +2 -0
- package/dist/helpdesk/i18n/bn.js +127 -0
- package/dist/helpdesk/index.d.ts +10 -0
- package/dist/helpdesk/index.js +463 -0
- package/dist/helpdesk/module.d.ts +2960 -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.js +1 -1
- package/dist/hr/.kosh/schema.d.ts +59 -1
- package/dist/hr/demo.js +11 -1
- package/dist/hr/i18n/bn.d.ts +2 -0
- package/dist/hr/i18n/bn.js +126 -0
- package/dist/hr/index.d.ts +1 -1
- package/dist/hr/index.js +158 -12
- package/dist/hr/module.d.ts +932 -59
- package/dist/hr/module.js +114 -17
- package/dist/indent/.kosh/bindings.d.ts +22 -0
- package/dist/indent/.kosh/bindings.js +15 -0
- package/dist/indent/.kosh/schema.d.ts +2084 -0
- package/dist/indent/demo.d.ts +7 -0
- package/dist/indent/demo.js +48 -0
- package/dist/indent/i18n/bn.d.ts +2 -0
- package/dist/indent/i18n/bn.js +143 -0
- package/dist/indent/index.d.ts +3 -0
- package/dist/indent/index.js +450 -0
- package/dist/indent/module.d.ts +13424 -0
- package/dist/indent/module.js +792 -0
- package/dist/indent_purchase/.kosh/bindings.d.ts +22 -0
- package/dist/indent_purchase/.kosh/bindings.js +15 -0
- package/dist/indent_purchase/.kosh/schema.d.ts +2710 -0
- package/dist/indent_purchase/i18n/bn.d.ts +2 -0
- package/dist/indent_purchase/i18n/bn.js +13 -0
- package/dist/indent_purchase/index.d.ts +3 -0
- package/dist/indent_purchase/index.js +61 -0
- package/dist/indent_purchase/module.d.ts +27950 -0
- package/dist/indent_purchase/module.js +163 -0
- package/dist/l10n_bd/.kosh/bindings.js +1 -1
- package/dist/l10n_bd/.kosh/schema.d.ts +651 -10
- package/dist/l10n_bd/i18n/bn.d.ts +2 -0
- package/dist/l10n_bd/i18n/bn.js +69 -0
- package/dist/l10n_bd/index.js +2 -0
- package/dist/l10n_bd/module.d.ts +5212 -1592
- 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 +2891 -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/i18n/bn.d.ts +2 -0
- package/dist/l10n_bd_payroll/i18n/bn.js +4 -0
- package/dist/l10n_bd_payroll/index.d.ts +4 -0
- package/dist/l10n_bd_payroll/index.js +26 -0
- package/dist/l10n_bd_payroll/module.d.ts +17294 -0
- package/dist/l10n_bd_payroll/module.js +86 -0
- package/dist/l10n_bd_pos/.kosh/bindings.d.ts +22 -0
- package/dist/{mrp_account → l10n_bd_pos}/.kosh/bindings.js +2 -2
- package/dist/l10n_bd_pos/.kosh/schema.d.ts +2341 -0
- package/dist/l10n_bd_pos/i18n/bn.d.ts +2 -0
- package/dist/l10n_bd_pos/i18n/bn.js +32 -0
- package/dist/l10n_bd_pos/index.d.ts +3 -0
- package/dist/l10n_bd_pos/index.js +111 -0
- package/dist/{mrp_account → l10n_bd_pos}/module.d.ts +9047 -6981
- package/dist/l10n_bd_pos/module.js +43 -0
- package/dist/l10n_in/.kosh/bindings.js +1 -1
- package/dist/l10n_in/.kosh/schema.d.ts +651 -10
- package/dist/l10n_in/i18n/bn.d.ts +2 -0
- package/dist/l10n_in/i18n/bn.js +95 -0
- package/dist/l10n_in/index.js +2 -0
- package/dist/l10n_in/module.d.ts +6778 -1580
- package/dist/l10n_in/module.js +25 -5
- package/dist/l10n_in/web/GstSettings.js +2 -2
- package/dist/mail/inbound/handle.js +8 -5
- package/dist/mail/inbound/models.js +5 -3
- package/dist/mail/index.d.ts +12 -1
- package/dist/mail/index.js +139 -2
- package/dist/mail/send.d.ts +56 -0
- package/dist/mail/send.js +394 -0
- package/dist/mail/web/SendButton.d.ts +9 -0
- package/dist/mail/web/SendButton.js +71 -0
- package/dist/mail/web/SendSelected.d.ts +8 -0
- package/dist/mail/web/SendSelected.js +33 -0
- package/dist/mail/web/index.d.ts +2 -0
- package/dist/mail/web/index.js +6 -0
- package/dist/mail/web/messages.js +36 -0
- package/dist/maintenance/.kosh/bindings.js +1 -1
- package/dist/maintenance/.kosh/schema.d.ts +2 -1
- package/dist/maintenance/i18n/bn.d.ts +2 -0
- package/dist/maintenance/i18n/bn.js +132 -0
- package/dist/maintenance/index.js +85 -0
- package/dist/maintenance/module.d.ts +25 -0
- package/dist/maintenance/module.js +27 -9
- package/dist/maintenance_stock/.kosh/bindings.js +1 -1
- package/dist/maintenance_stock/.kosh/schema.d.ts +374 -3
- package/dist/maintenance_stock/i18n/bn.d.ts +2 -0
- package/dist/maintenance_stock/i18n/bn.js +21 -0
- package/dist/maintenance_stock/index.js +2 -0
- package/dist/maintenance_stock/module.d.ts +6095 -1327
- package/dist/maintenance_stock/module.js +1 -0
- package/dist/modules-sdk/apps.d.ts +10 -0
- package/dist/modules-sdk/apps.js +32 -0
- package/dist/modules-sdk/index.d.ts +1 -1
- package/dist/modules-sdk/manifest.d.ts +105 -2
- package/dist/payment/.kosh/bindings.js +1 -1
- package/dist/payment/.kosh/schema.d.ts +653 -11
- package/dist/payment/i18n/bn.d.ts +2 -0
- package/dist/payment/i18n/bn.js +59 -0
- package/dist/payment/index.d.ts +41 -0
- package/dist/payment/index.js +216 -74
- package/dist/payment/module.d.ts +5353 -1675
- package/dist/payment/module.js +36 -6
- package/dist/payment/settle.js +11 -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 +1448 -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/i18n/bn.d.ts +2 -0
- package/dist/payroll/i18n/bn.js +206 -0
- package/dist/payroll/index.d.ts +230 -0
- package/dist/payroll/index.js +715 -0
- package/dist/{mrp → payroll}/module.d.ts +4294 -2624
- 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 +2887 -0
- package/dist/payroll_account/i18n/bn.d.ts +2 -0
- package/dist/payroll_account/i18n/bn.js +10 -0
- package/dist/payroll_account/index.d.ts +3 -0
- package/dist/payroll_account/index.js +42 -0
- package/dist/payroll_account/module.d.ts +16738 -0
- package/dist/payroll_account/module.js +256 -0
- package/dist/permissions/approvals.d.ts +227 -12
- package/dist/permissions/approvals.js +1421 -67
- package/dist/permissions/index.d.ts +1 -1
- package/dist/permissions/index.js +2 -2
- package/dist/permissions/models.js +364 -13
- package/dist/permissions/routes.d.ts +9 -1
- package/dist/permissions/routes.js +72 -5
- package/dist/permissions/web/ApprovalPanel.d.ts +5 -4
- package/dist/permissions/web/ApprovalPanel.js +75 -11
- 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 +95 -0
- package/dist/portal/.kosh/bindings.js +1 -1
- package/dist/portal/.kosh/schema.d.ts +2 -1
- package/dist/portal/documents.d.ts +3 -0
- package/dist/portal/documents.js +21 -1
- package/dist/portal/i18n/bn.d.ts +2 -0
- package/dist/portal/i18n/bn.js +33 -0
- package/dist/portal/index.js +3 -1
- package/dist/portal/module.d.ts +20 -0
- package/dist/portal/routes-documents.js +76 -0
- package/dist/pos/.kosh/bindings.js +1 -1
- package/dist/pos/.kosh/schema.d.ts +652 -10
- package/dist/pos/i18n/bn.d.ts +2 -0
- package/dist/pos/i18n/bn.js +133 -0
- package/dist/pos/index.js +87 -1
- package/dist/pos/module.d.ts +6967 -1709
- package/dist/pos/module.js +49 -5
- package/dist/pos/routes.js +80 -7
- package/dist/pos/web/Register.js +2 -5
- package/dist/pos_stock/.kosh/bindings.js +1 -1
- package/dist/pos_stock/.kosh/schema.d.ts +822 -12
- package/dist/pos_stock/i18n/bn.d.ts +2 -0
- package/dist/pos_stock/i18n/bn.js +6 -0
- package/dist/pos_stock/index.js +2 -0
- package/dist/pos_stock/module.d.ts +11241 -2851
- package/dist/product/.kosh/bindings.js +1 -1
- package/dist/product/.kosh/schema.d.ts +203 -1
- package/dist/product/i18n/bn.d.ts +2 -0
- package/dist/product/i18n/bn.js +175 -0
- package/dist/product/index.d.ts +1 -0
- package/dist/product/index.js +301 -1
- package/dist/product/module.d.ts +1727 -0
- package/dist/product/module.js +67 -6
- package/dist/product/pricelist.d.ts +575 -0
- package/dist/product/pricelist.js +257 -0
- package/dist/product/variants.d.ts +908 -0
- package/dist/product/variants.js +251 -0
- package/dist/production/.kosh/bindings.d.ts +22 -0
- package/dist/production/.kosh/bindings.js +15 -0
- package/dist/production/.kosh/schema.d.ts +2345 -0
- package/dist/{mrp → production}/demo.d.ts +1 -1
- package/dist/{mrp → production}/demo.js +5 -5
- package/dist/production/depth.d.ts +790 -0
- package/dist/production/depth.js +262 -0
- package/dist/production/i18n/bn.d.ts +2 -0
- package/dist/production/i18n/bn.js +350 -0
- package/dist/production/index.d.ts +6 -0
- package/dist/production/index.js +1256 -0
- package/dist/production/module.d.ts +17327 -0
- package/dist/production/module.js +1333 -0
- package/dist/production/plan.d.ts +921 -0
- package/dist/production/plan.js +511 -0
- package/dist/production/schedule.d.ts +656 -0
- package/dist/production/schedule.js +309 -0
- package/dist/production_account/.kosh/bindings.d.ts +22 -0
- package/dist/production_account/.kosh/bindings.js +15 -0
- package/dist/production_account/.kosh/schema.d.ts +3496 -0
- package/dist/production_account/i18n/bn.d.ts +2 -0
- package/dist/production_account/i18n/bn.js +93 -0
- package/dist/production_account/index.d.ts +4 -0
- package/dist/production_account/index.js +246 -0
- package/dist/production_account/module.d.ts +1192 -0
- package/dist/{mrp_account → production_account}/module.js +72 -18
- package/dist/production_account/overhead.d.ts +1195 -0
- package/dist/production_account/overhead.js +554 -0
- package/dist/project/.kosh/bindings.js +1 -1
- package/dist/project/.kosh/schema.d.ts +735 -1
- package/dist/project/i18n/bn.d.ts +2 -0
- package/dist/project/i18n/bn.js +383 -0
- package/dist/project/index.d.ts +3 -1
- package/dist/project/index.js +1024 -14
- package/dist/project/module.d.ts +9912 -390
- package/dist/project/module.js +1590 -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 +2763 -0
- package/dist/project_account/i18n/bn.d.ts +2 -0
- package/dist/project_account/i18n/bn.js +6 -0
- package/dist/project_account/index.d.ts +3 -0
- package/dist/project_account/index.js +29 -0
- package/dist/project_account/module.d.ts +15013 -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 +735 -1
- package/dist/project_portal/i18n/bn.d.ts +2 -0
- package/dist/project_portal/i18n/bn.js +6 -0
- package/dist/project_portal/index.js +2 -0
- package/dist/project_portal/module.d.ts +4796 -87
- 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 +3393 -0
- package/dist/project_purchase/i18n/bn.d.ts +2 -0
- package/dist/project_purchase/i18n/bn.js +4 -0
- package/dist/project_purchase/index.d.ts +9 -0
- package/dist/project_purchase/index.js +24 -0
- package/dist/project_purchase/module.d.ts +38613 -0
- package/dist/project_purchase/module.js +56 -0
- package/dist/purchase/.kosh/bindings.js +1 -1
- package/dist/purchase/.kosh/schema.d.ts +908 -5
- package/dist/purchase/comparison.d.ts +993 -0
- package/dist/purchase/comparison.js +415 -0
- package/dist/purchase/extras.d.ts +1509 -0
- package/dist/purchase/extras.js +445 -0
- package/dist/purchase/i18n/bn.d.ts +2 -0
- package/dist/purchase/i18n/bn.js +413 -0
- package/dist/purchase/index.d.ts +5 -0
- package/dist/purchase/index.js +1280 -4
- package/dist/purchase/module.d.ts +7147 -395
- package/dist/purchase/module.js +303 -10
- package/dist/purchase/request.d.ts +696 -0
- package/dist/purchase/request.js +446 -0
- package/dist/purchase/tender.d.ts +1208 -0
- package/dist/purchase/tender.js +507 -0
- package/dist/purchase/vendorprice.d.ts +340 -0
- package/dist/purchase/vendorprice.js +153 -0
- package/dist/purchase_account/.kosh/bindings.js +1 -1
- package/dist/purchase_account/.kosh/schema.d.ts +1209 -16
- package/dist/purchase_account/i18n/bn.d.ts +2 -0
- package/dist/purchase_account/i18n/bn.js +22 -0
- package/dist/purchase_account/index.js +20 -0
- package/dist/purchase_account/module.d.ts +12232 -2028
- package/dist/purchase_account/module.js +252 -9
- package/dist/purchase_portal/.kosh/bindings.js +1 -1
- package/dist/purchase_portal/.kosh/schema.d.ts +908 -5
- package/dist/purchase_portal/i18n/bn.d.ts +2 -0
- package/dist/purchase_portal/i18n/bn.js +5 -0
- package/dist/purchase_portal/index.js +2 -0
- package/dist/purchase_portal/module.d.ts +6853 -352
- package/dist/purchase_stock/.kosh/bindings.js +1 -1
- package/dist/purchase_stock/.kosh/schema.d.ts +1096 -21
- package/dist/purchase_stock/i18n/bn.d.ts +2 -0
- package/dist/purchase_stock/i18n/bn.js +19 -0
- package/dist/purchase_stock/index.d.ts +2 -2
- package/dist/purchase_stock/index.js +38 -2
- package/dist/purchase_stock/module.d.ts +12930 -1612
- package/dist/purchase_stock/module.js +317 -9
- package/dist/purchase_stock/scorecard.d.ts +22 -0
- package/dist/purchase_stock/scorecard.js +77 -0
- package/dist/purchase_stock/web/Scorecard.d.ts +7 -0
- package/dist/purchase_stock/web/Scorecard.js +22 -0
- package/dist/purchase_stock/web/index.d.ts +11 -0
- package/dist/purchase_stock/web/index.js +16 -0
- package/dist/purchase_stock/web/messages.d.ts +2 -0
- package/dist/purchase_stock/web/messages.js +27 -0
- package/dist/purchase_stock_account/.kosh/bindings.js +1 -1
- package/dist/purchase_stock_account/.kosh/schema.d.ts +1444 -20
- package/dist/purchase_stock_account/i18n/bn.d.ts +2 -0
- package/dist/purchase_stock_account/i18n/bn.js +8 -0
- package/dist/purchase_stock_account/index.js +18 -0
- package/dist/purchase_stock_account/module.d.ts +7 -35346
- package/dist/purchase_stock_account/module.js +35 -8
- package/dist/recruitment/.kosh/bindings.js +1 -1
- package/dist/recruitment/.kosh/schema.d.ts +60 -1
- package/dist/recruitment/i18n/bn.d.ts +2 -0
- package/dist/recruitment/i18n/bn.js +93 -0
- package/dist/recruitment/index.js +64 -0
- package/dist/recruitment/module.d.ts +460 -8
- package/dist/recruitment/module.js +13 -4
- package/dist/reporting/documents.d.ts +5 -0
- package/dist/reporting/documents.js +11 -1
- package/dist/reporting/index.d.ts +1 -1
- package/dist/reporting/index.js +1 -1
- package/dist/reporting/web/CalendarView.d.ts +18 -5
- package/dist/reporting/web/CalendarView.js +163 -29
- package/dist/reporting/web/messages.js +18 -0
- package/dist/sale_account/.kosh/bindings.js +1 -1
- package/dist/sale_account/.kosh/schema.d.ts +763 -18
- package/dist/sale_account/i18n/bn.d.ts +2 -0
- package/dist/sale_account/i18n/bn.js +48 -0
- package/dist/sale_account/index.d.ts +1 -1
- package/dist/sale_account/index.js +102 -4
- package/dist/sale_account/module.d.ts +8241 -2052
- package/dist/sale_account/module.js +535 -46
- package/dist/sale_crm/.kosh/bindings.d.ts +22 -0
- package/dist/sale_crm/.kosh/bindings.js +15 -0
- package/dist/{mrp → sale_crm}/.kosh/schema.d.ts +391 -494
- package/dist/sale_crm/i18n/bn.d.ts +2 -0
- package/dist/sale_crm/i18n/bn.js +9 -0
- package/dist/sale_crm/index.d.ts +7 -0
- package/dist/sale_crm/index.js +104 -0
- package/dist/sale_crm/module.d.ts +5609 -0
- package/dist/sale_crm/module.js +73 -0
- package/dist/sale_portal/.kosh/bindings.js +1 -1
- package/dist/sale_portal/.kosh/schema.d.ts +292 -4
- package/dist/sale_portal/i18n/bn.d.ts +2 -0
- package/dist/sale_portal/i18n/bn.js +7 -0
- package/dist/sale_portal/index.js +10 -0
- package/dist/sale_portal/module.d.ts +2563 -292
- package/dist/sale_project/.kosh/bindings.js +1 -1
- package/dist/sale_project/.kosh/schema.d.ts +1025 -4
- package/dist/sale_project/i18n/bn.d.ts +2 -0
- package/dist/sale_project/i18n/bn.js +12 -0
- package/dist/sale_project/index.js +2 -0
- package/dist/sale_project/module.d.ts +9280 -762
- package/dist/sale_project/module.js +17 -1
- package/dist/sale_stock/.kosh/bindings.js +1 -1
- package/dist/sale_stock/.kosh/schema.d.ts +463 -6
- package/dist/sale_stock/i18n/bn.d.ts +2 -0
- package/dist/sale_stock/i18n/bn.js +15 -0
- package/dist/sale_stock/index.js +2 -0
- package/dist/sale_stock/module.d.ts +8355 -1334
- package/dist/sale_stock/module.js +20 -3
- package/dist/sales/.kosh/bindings.js +1 -1
- package/dist/sales/.kosh/schema.d.ts +292 -4
- package/dist/sales/i18n/bn.d.ts +2 -0
- package/dist/sales/i18n/bn.js +128 -0
- package/dist/sales/index.js +124 -5
- package/dist/sales/module.d.ts +3568 -633
- package/dist/sales/module.js +306 -11
- package/dist/server/api/duplicate.d.ts +14 -0
- package/dist/server/api/duplicate.js +75 -0
- package/dist/server/api/explain.d.ts +70 -0
- package/dist/server/api/explain.js +111 -0
- package/dist/server/api/metadata.d.ts +2 -0
- package/dist/server/api/metadata.js +9 -0
- package/dist/server/api/openapi.js +15 -0
- package/dist/server/api/rest.d.ts +1 -0
- package/dist/server/api/rest.js +76 -12
- package/dist/server/api/views.js +1 -1
- package/dist/server/apps.d.ts +16 -1
- package/dist/server/apps.js +164 -2
- package/dist/server/auth/base.js +2 -0
- package/dist/server/auth/service.d.ts +8 -0
- package/dist/server/auth/service.js +19 -0
- package/dist/server/events.d.ts +13 -0
- package/dist/server/events.js +22 -0
- package/dist/server/i18n.d.ts +27 -0
- package/dist/server/i18n.js +143 -0
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.js +2 -0
- package/dist/server/server.js +6 -0
- package/dist/settings/company.d.ts +3 -1
- package/dist/settings/company.js +26 -5
- package/dist/settings/routes.d.ts +10 -0
- package/dist/settings/routes.js +19 -3
- package/dist/settings/web/AppsPage.d.ts +3 -2
- package/dist/settings/web/AppsPage.js +26 -8
- package/dist/settings/web/CompaniesPage.js +91 -6
- package/dist/settings/web/messages.js +64 -0
- package/dist/sign/.kosh/bindings.d.ts +22 -0
- package/dist/{mrp → sign}/.kosh/bindings.js +2 -2
- package/dist/sign/.kosh/schema.d.ts +991 -0
- package/dist/sign/i18n/bn.d.ts +2 -0
- package/dist/sign/i18n/bn.js +97 -0
- package/dist/sign/index.d.ts +29 -0
- package/dist/sign/index.js +478 -0
- package/dist/sign/module.d.ts +4518 -0
- package/dist/sign/module.js +167 -0
- package/dist/sign/sign.d.ts +82 -0
- package/dist/sign/sign.js +474 -0
- package/dist/stock/.kosh/bindings.js +1 -1
- package/dist/stock/.kosh/schema.d.ts +374 -3
- package/dist/stock/demo.d.ts +2 -1
- package/dist/stock/demo.js +28 -1
- package/dist/stock/i18n/bn.d.ts +2 -0
- package/dist/stock/i18n/bn.js +311 -0
- package/dist/stock/index.d.ts +4 -1
- package/dist/stock/index.js +674 -6
- package/dist/stock/ledger.d.ts +78 -0
- package/dist/stock/ledger.js +582 -0
- package/dist/stock/module.d.ts +6142 -1380
- package/dist/stock/module.js +780 -26
- package/dist/stock/replenish.d.ts +38 -0
- package/dist/stock/replenish.js +191 -0
- package/dist/stock/returns.d.ts +157 -0
- package/dist/stock/returns.js +163 -0
- package/dist/stock/web/Ledger.d.ts +9 -0
- package/dist/stock/web/Ledger.js +108 -0
- package/dist/stock/web/Operations.d.ts +8 -0
- package/dist/stock/web/Operations.js +33 -0
- package/dist/stock/web/Replenishment.d.ts +8 -0
- package/dist/stock/web/Replenishment.js +84 -0
- package/dist/stock/web/ReturnButton.d.ts +7 -0
- package/dist/stock/web/ReturnButton.js +62 -0
- package/dist/stock/web/index.d.ts +7 -2
- package/dist/stock/web/index.js +15 -2
- package/dist/stock/web/messages.js +206 -0
- package/dist/stock_account/.kosh/bindings.js +1 -1
- package/dist/stock_account/.kosh/schema.d.ts +878 -12
- package/dist/stock_account/i18n/bn.d.ts +2 -0
- package/dist/stock_account/i18n/bn.js +73 -0
- package/dist/stock_account/index.d.ts +1 -0
- package/dist/stock_account/index.js +141 -3
- package/dist/stock_account/landed.d.ts +429 -0
- package/dist/stock_account/landed.js +335 -0
- package/dist/stock_account/module.d.ts +12143 -3391
- package/dist/stock_account/module.js +135 -6
- package/dist/stock_account/web/LandedPreview.d.ts +7 -0
- package/dist/stock_account/web/LandedPreview.js +23 -0
- package/dist/stock_account/web/index.d.ts +11 -0
- package/dist/stock_account/web/index.js +16 -0
- package/dist/stock_account/web/messages.d.ts +2 -0
- package/dist/stock_account/web/messages.js +27 -0
- package/dist/todo/i18n/bn.d.ts +2 -0
- package/dist/todo/i18n/bn.js +66 -0
- package/dist/todo/index.js +59 -0
- package/dist/todo/module.d.ts +5 -0
- package/dist/todo/module.js +5 -3
- package/dist/trade_finance/.kosh/bindings.js +1 -1
- package/dist/trade_finance/.kosh/schema.d.ts +2 -1
- package/dist/trade_finance/i18n/bn.d.ts +2 -0
- package/dist/trade_finance/i18n/bn.js +162 -0
- package/dist/trade_finance/index.js +95 -0
- package/dist/trade_finance/module.d.ts +26 -0
- package/dist/trade_finance/module.js +29 -8
- package/dist/trade_finance_account/.kosh/bindings.js +1 -1
- package/dist/trade_finance_account/.kosh/schema.d.ts +651 -10
- package/dist/trade_finance_account/i18n/bn.d.ts +2 -0
- package/dist/trade_finance_account/i18n/bn.js +27 -0
- package/dist/trade_finance_account/index.js +3 -0
- package/dist/trade_finance_account/module.d.ts +5129 -1509
- package/dist/trade_finance_account/module.js +2 -0
- package/dist/trade_finance_purchase/.kosh/bindings.js +1 -1
- package/dist/trade_finance_purchase/.kosh/schema.d.ts +908 -5
- package/dist/trade_finance_purchase/i18n/bn.d.ts +2 -0
- package/dist/trade_finance_purchase/i18n/bn.js +6 -0
- package/dist/trade_finance_purchase/index.js +2 -0
- package/dist/trade_finance_purchase/module.d.ts +6909 -408
- package/dist/trade_finance_sale/.kosh/bindings.js +1 -1
- package/dist/trade_finance_sale/.kosh/schema.d.ts +292 -4
- package/dist/trade_finance_sale/i18n/bn.d.ts +2 -0
- package/dist/trade_finance_sale/i18n/bn.js +6 -0
- package/dist/trade_finance_sale/index.js +2 -0
- package/dist/trade_finance_sale/module.d.ts +2602 -331
- package/dist/ui/api.d.ts +2 -0
- package/dist/ui/app.d.ts +9 -0
- package/dist/ui/app.js +47 -15
- package/dist/ui/components/AppIcon.js +9 -2
- package/dist/ui/components/Explain.d.ts +69 -0
- package/dist/ui/components/Explain.js +62 -0
- package/dist/ui/context.d.ts +8 -1
- package/dist/ui/context.js +34 -1
- package/dist/ui/i18n/messages.d.ts +40 -0
- package/dist/ui/i18n/messages.js +80 -0
- package/dist/ui/index.d.ts +2 -1
- package/dist/ui/index.js +1 -0
- package/dist/ui/registry.d.ts +14 -0
- package/dist/ui/registry.js +10 -0
- package/dist/ui/views/FormView.d.ts +6 -0
- package/dist/ui/views/FormView.js +51 -12
- package/dist/ui/views/KanbanView.js +9 -1
- package/dist/ui/views/ListView.d.ts +8 -0
- package/dist/ui/views/ListView.js +14 -5
- package/dist/ui/views/StatsRow.js +2 -1
- package/dist/ui/views/widgets.js +7 -1
- package/dist/views/extend.js +12 -0
- package/dist/views/types.d.ts +13 -2
- package/dist/views/validate.js +8 -1
- package/dist/website/.kosh/bindings.js +1 -1
- package/dist/website/.kosh/schema.d.ts +2 -1
- package/dist/website/i18n/bn.d.ts +2 -0
- package/dist/website/i18n/bn.js +113 -0
- package/dist/website/index.js +37 -0
- package/dist/website/module.d.ts +20 -0
- package/dist/website/module.js +30 -5
- package/dist/website_crm/.kosh/bindings.js +1 -1
- package/dist/website_crm/.kosh/schema.d.ts +3 -1
- package/dist/website_crm/i18n/bn.d.ts +2 -0
- package/dist/website_crm/i18n/bn.js +5 -0
- package/dist/website_crm/index.js +2 -0
- package/dist/website_crm/module.d.ts +41 -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 +2909 -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/i18n/bn.d.ts +2 -0
- package/dist/website_sale/i18n/bn.js +132 -0
- package/dist/website_sale/index.d.ts +49 -0
- package/dist/website_sale/index.js +620 -0
- package/dist/website_sale/module.d.ts +35182 -0
- package/dist/website_sale/module.js +419 -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 +662 -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 +3400 -0
- package/dist/website_sale_stock/i18n/bn.d.ts +2 -0
- package/dist/website_sale_stock/i18n/bn.js +4 -0
- package/dist/website_sale_stock/index.d.ts +9 -0
- package/dist/website_sale_stock/index.js +22 -0
- package/dist/website_sale_stock/module.d.ts +11 -0
- package/dist/website_sale_stock/module.js +32 -0
- package/package.json +117 -8
- package/dist/mrp/index.d.ts +0 -3
- package/dist/mrp/index.js +0 -392
- package/dist/mrp/module.js +0 -695
- package/dist/mrp_account/index.d.ts +0 -3
- package/dist/mrp_account/index.js +0 -22
- /package/dist/{mrp → account_asset}/.kosh/bindings.d.ts +0 -0
- /package/dist/{mrp_account → account_budget}/.kosh/bindings.d.ts +0 -0
|
@@ -0,0 +1,1333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The data layer of Production (RFC 0021): bills of materials with versions and effective dates, work centres with
|
|
3
|
+
* their hours and cost, and production orders that say what is required, available and short before they start and
|
|
4
|
+
* what they will cost. Orders move stock only through Stockroom: Confirm reserves the components in a consumption
|
|
5
|
+
* transfer (stock → Production), Produce validates it and the output transfer (Production → stock).
|
|
6
|
+
*/
|
|
7
|
+
import { contactsShutro } from '../contacts/index.js';
|
|
8
|
+
import { messagingShutro } from '../messaging/index.js';
|
|
9
|
+
import { approvalGuard, permissionsShutro, requestApprovals } from '../permissions/index.js';
|
|
10
|
+
import { productShutro } from '../product/index.js';
|
|
11
|
+
import { emitting } from '../server/index.js';
|
|
12
|
+
import { orm } from '../server/orm.js';
|
|
13
|
+
import { defaultLocations, inProductUnit, stockShutro } from '../stock/index.js';
|
|
14
|
+
import { check, compute, defineModel, defineModule, extendModel, f, onchange, records, } from './.kosh/bindings.js';
|
|
15
|
+
import { BOM_KINDS, bomsNeedApproval, CompanyProduction, ineligible, ProductionBomCost, ProductionBomEligibility, ProductionBomSpec, ProductionProductSpec, ProductionShift, ProductionSpecParameter, } from './depth.js';
|
|
16
|
+
import { ProductionPlan, ProductionPlanLine, ProductionPlanMaterial } from './plan.js';
|
|
17
|
+
import { followOrders, ProductionSchedule, ProductionScheduleComponent } from './schedule.js';
|
|
18
|
+
const READ = ['read'];
|
|
19
|
+
const ALL = ['read', 'write', 'create', 'unlink'];
|
|
20
|
+
const zero = orm.toDecimal(0);
|
|
21
|
+
const money = (d) => d.toDecimalPlaces(2);
|
|
22
|
+
/** A date as `YYYY-MM-DD` (date-only values from PostgreSQL are local midnights: their local parts). */
|
|
23
|
+
const iso = (v) => {
|
|
24
|
+
if (!(v instanceof Date))
|
|
25
|
+
return String(v ?? '');
|
|
26
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
27
|
+
return `${v.getFullYear()}-${pad(v.getMonth() + 1)}-${pad(v.getDate())}`;
|
|
28
|
+
};
|
|
29
|
+
/** The bill of `product` valid on `date`: the latest that starts on or before it and has not ended. */
|
|
30
|
+
export async function validBom(env, product, date) {
|
|
31
|
+
const bills = await (await env.model('production.bom').search({ where: { product } })).load([
|
|
32
|
+
'validFrom',
|
|
33
|
+
'validTo',
|
|
34
|
+
'state',
|
|
35
|
+
'company',
|
|
36
|
+
]);
|
|
37
|
+
// Active bills; drafts too when the company needs no approval (RFC 0031 §3.2).
|
|
38
|
+
const needs = new Map();
|
|
39
|
+
const usable = async (b) => {
|
|
40
|
+
if (b.state === 'active')
|
|
41
|
+
return true;
|
|
42
|
+
if (b.state !== 'draft')
|
|
43
|
+
return false;
|
|
44
|
+
let need = needs.get(b.company.id);
|
|
45
|
+
if (need === undefined) {
|
|
46
|
+
need = await bomsNeedApproval(env, b.company.id);
|
|
47
|
+
needs.set(b.company.id, need);
|
|
48
|
+
}
|
|
49
|
+
return !need;
|
|
50
|
+
};
|
|
51
|
+
const candidates = [];
|
|
52
|
+
for (const b of bills)
|
|
53
|
+
if (await usable(b))
|
|
54
|
+
candidates.push(b);
|
|
55
|
+
const valid = candidates
|
|
56
|
+
.filter((b) => (b.validFrom === null || iso(b.validFrom) <= date) &&
|
|
57
|
+
(b.validTo === null || iso(b.validTo) >= date))
|
|
58
|
+
.sort((a, b) => iso(b.validFrom).localeCompare(iso(a.validFrom)) || b.id - a.id);
|
|
59
|
+
return valid[0]?.id ?? null;
|
|
60
|
+
}
|
|
61
|
+
export const ProductionWorkcenter = defineModel('production.workcenter', {
|
|
62
|
+
label: 'Work centre',
|
|
63
|
+
recName: 'name',
|
|
64
|
+
order: ['name asc'],
|
|
65
|
+
archivable: true,
|
|
66
|
+
companyScoped: { required: true },
|
|
67
|
+
access: { 'production.user': READ, 'production.manager': [...ALL, 'export', 'import'] },
|
|
68
|
+
fields: {
|
|
69
|
+
name: f.char({ required: true }),
|
|
70
|
+
code: f.char(),
|
|
71
|
+
/** Hours it works a day (its capacity). */
|
|
72
|
+
hoursPerDay: f.decimal({ scale: 2, required: true, default: '8', label: 'Hours a day' }),
|
|
73
|
+
/** Labour and overhead per hour of work. */
|
|
74
|
+
costPerHour: f.decimal({ scale: 2, required: true, default: '0', label: 'Cost per hour' }),
|
|
75
|
+
batchMin: f.decimal({ scale: 4, label: 'Minimum batch', help: 'The smallest batch it runs.' }),
|
|
76
|
+
batchMax: f.decimal({
|
|
77
|
+
scale: 4,
|
|
78
|
+
label: 'Maximum batch',
|
|
79
|
+
help: 'The largest batch it runs at once.',
|
|
80
|
+
}),
|
|
81
|
+
oeeTarget: f.decimal({
|
|
82
|
+
scale: 2,
|
|
83
|
+
default: '85',
|
|
84
|
+
label: 'OEE target (%)',
|
|
85
|
+
help: 'The overall equipment effectiveness aimed at: availability × performance × quality.',
|
|
86
|
+
}),
|
|
87
|
+
setupMinutes: f.decimal({
|
|
88
|
+
scale: 2,
|
|
89
|
+
default: '0',
|
|
90
|
+
label: 'Setup (minutes)',
|
|
91
|
+
help: 'Before each batch.',
|
|
92
|
+
}),
|
|
93
|
+
cleanupMinutes: f.decimal({
|
|
94
|
+
scale: 2,
|
|
95
|
+
default: '0',
|
|
96
|
+
label: 'Cleanup (minutes)',
|
|
97
|
+
help: 'After each batch.',
|
|
98
|
+
}),
|
|
99
|
+
alternatives: f.many2many('production.workcenter', {
|
|
100
|
+
relation: 'production_wc_alternative',
|
|
101
|
+
label: 'Alternative work centres',
|
|
102
|
+
help: 'Where the same operation may run when this centre is full.',
|
|
103
|
+
}),
|
|
104
|
+
note: f.text(),
|
|
105
|
+
},
|
|
106
|
+
sqlConstraints: {
|
|
107
|
+
positive: {
|
|
108
|
+
check: orm.sql `hours_per_day > 0 AND cost_per_hour >= 0`,
|
|
109
|
+
message: 'A work centre works some hours a day, at a cost that is not negative.',
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
export const ProductionBom = defineModel('production.bom', {
|
|
114
|
+
label: 'Bill of materials',
|
|
115
|
+
recName: 'label',
|
|
116
|
+
order: ['product asc', 'validFrom desc', 'id desc'],
|
|
117
|
+
archivable: true,
|
|
118
|
+
companyScoped: { required: true },
|
|
119
|
+
tracking: ['quantity', 'validFrom', 'validTo', 'kind', 'state'],
|
|
120
|
+
access: { 'production.user': READ, 'production.manager': [...ALL, 'export', 'import'] },
|
|
121
|
+
fields: {
|
|
122
|
+
product: f.many2one('product.product', { required: true, ondelete: 'restrict' }),
|
|
123
|
+
/** How many of the product one bill makes, in the product's unit. */
|
|
124
|
+
quantity: f.decimal({ scale: 4, required: true, default: '1', label: 'Makes' }),
|
|
125
|
+
kind: f.selection(BOM_KINDS, {
|
|
126
|
+
required: true,
|
|
127
|
+
default: 'standard',
|
|
128
|
+
help: 'Standard: components in quantities. Formula: components as shares of the batch (ratios adding up to 100 %), scaled to any quantity. Packaging: a pack of the product made.',
|
|
129
|
+
}),
|
|
130
|
+
packSize: f.decimal({
|
|
131
|
+
scale: 4,
|
|
132
|
+
label: 'Pack size',
|
|
133
|
+
help: 'For a packaging bill: how much of the product one pack holds, in the product’s unit.',
|
|
134
|
+
}),
|
|
135
|
+
expectedLoss: f.decimal({
|
|
136
|
+
scale: 2,
|
|
137
|
+
default: '0',
|
|
138
|
+
label: 'Expected loss (%)',
|
|
139
|
+
help: 'What is lost making or packing a batch, as a share of it.',
|
|
140
|
+
}),
|
|
141
|
+
tolerance: f.decimal({
|
|
142
|
+
scale: 2,
|
|
143
|
+
default: '0',
|
|
144
|
+
label: 'Tolerance (±%)',
|
|
145
|
+
help: 'How far the quantity made may stray from the batch before it is questioned.',
|
|
146
|
+
}),
|
|
147
|
+
version: f.char({ required: true, default: 'v1' }),
|
|
148
|
+
/** From when this version is used (empty: always). */
|
|
149
|
+
validFrom: f.date({ label: 'Valid from' }),
|
|
150
|
+
validTo: f.date({ label: 'Valid until' }),
|
|
151
|
+
lines: f.one2many('production.bom.line', 'bom', { label: 'Components' }),
|
|
152
|
+
operations: f.one2many('production.bom.operation', 'bom'),
|
|
153
|
+
byproducts: f.one2many('production.bom.byproduct', 'bom', { label: 'By-products' }),
|
|
154
|
+
specs: f.one2many('production.bom.spec', 'bom', { label: 'Specifications' }),
|
|
155
|
+
costs: f.one2many('production.bom.cost', 'bom', { label: 'Costing history' }),
|
|
156
|
+
unitCost: f.decimal({
|
|
157
|
+
scale: 4,
|
|
158
|
+
store: true,
|
|
159
|
+
label: 'Unit cost',
|
|
160
|
+
help: 'The components (gross of their losses) at their cost and the operations at their work centres’ rates, per unit made.',
|
|
161
|
+
...compute('production.bom', 'unitCost', [
|
|
162
|
+
'quantity',
|
|
163
|
+
'lines.grossQuantity',
|
|
164
|
+
'lines.quantity',
|
|
165
|
+
'lines.uom',
|
|
166
|
+
'lines.product.standardPrice',
|
|
167
|
+
'lines.product.uom',
|
|
168
|
+
'operations.minutes',
|
|
169
|
+
'operations.workcenter.costPerHour',
|
|
170
|
+
], (bills) => {
|
|
171
|
+
for (const b of bills) {
|
|
172
|
+
let total = zero;
|
|
173
|
+
for (const l of b.lines) {
|
|
174
|
+
const qty = inProductUnit(l.grossQuantity ?? l.quantity, (l.uom ?? null), (l.product.uom ?? null));
|
|
175
|
+
total = total.plus(qty.times(l.product.standardPrice ?? zero));
|
|
176
|
+
}
|
|
177
|
+
for (const op of b.operations)
|
|
178
|
+
total = total.plus(op.minutes.div(60).times(op.workcenter.costPerHour));
|
|
179
|
+
b.unitCost = b.quantity.gt(0) ? total.div(b.quantity).toDecimalPlaces(4) : zero;
|
|
180
|
+
}
|
|
181
|
+
}),
|
|
182
|
+
}),
|
|
183
|
+
state: f.state({
|
|
184
|
+
states: { draft: 'Draft', review: 'Under review', active: 'Active', inactive: 'Inactive' },
|
|
185
|
+
initial: 'draft',
|
|
186
|
+
help: 'Drafted, submitted for review, approved into active (the version used), set inactive when superseded; a revision copies it to the next version. A company that needs no approval uses drafts as well.',
|
|
187
|
+
transitions: {
|
|
188
|
+
submit: {
|
|
189
|
+
from: ['draft'],
|
|
190
|
+
to: 'review',
|
|
191
|
+
guard: records('production.bom', async (bills) => {
|
|
192
|
+
const out = [];
|
|
193
|
+
for (const b of await bills.load({ lines: true, specs: true })) {
|
|
194
|
+
const lines = [...b.lines];
|
|
195
|
+
const ratios = lines.reduce((s, l) => s.plus(l.ratio ?? zero), zero);
|
|
196
|
+
if (lines.length === 0 ||
|
|
197
|
+
(b.kind === 'formula' && !ratios.eq(100)) ||
|
|
198
|
+
[...b.specs].some((s) => s.within === false))
|
|
199
|
+
out.push(b.id);
|
|
200
|
+
}
|
|
201
|
+
return out;
|
|
202
|
+
}),
|
|
203
|
+
message: 'A bill under review has components, formula ratios adding up to 100 % and every specification within range.',
|
|
204
|
+
after: records('production.bom', async (bills) => {
|
|
205
|
+
await requestApprovals(bills);
|
|
206
|
+
for (const b of await bills.load({}))
|
|
207
|
+
await bills.env.emit('production.bom.submitted', { id: b.id });
|
|
208
|
+
}),
|
|
209
|
+
},
|
|
210
|
+
approve: {
|
|
211
|
+
from: ['review'],
|
|
212
|
+
to: 'active',
|
|
213
|
+
guard: records('production.bom', (bills) => approvalGuard(bills)),
|
|
214
|
+
message: 'Waiting for approvals.',
|
|
215
|
+
after: records('production.bom', async (bills) => {
|
|
216
|
+
const env = bills.env;
|
|
217
|
+
for (const b of await bills.load({})) {
|
|
218
|
+
await env.asSuperuser({ reason: 'bill costing history' }, (su) => su.model('production.bom.cost').create([
|
|
219
|
+
{
|
|
220
|
+
bom: b.id,
|
|
221
|
+
date: env.today(),
|
|
222
|
+
unitCost: b.unitCost ?? zero,
|
|
223
|
+
note: 'Activated',
|
|
224
|
+
},
|
|
225
|
+
]));
|
|
226
|
+
await env.emit('production.bom.activated', { id: b.id });
|
|
227
|
+
}
|
|
228
|
+
}),
|
|
229
|
+
},
|
|
230
|
+
reject: { from: ['review'], to: 'draft' },
|
|
231
|
+
deactivate: { from: ['active'], to: 'inactive' },
|
|
232
|
+
reactivate: { from: ['inactive'], to: 'active' },
|
|
233
|
+
/** A copy as the next version, in draft, to change and submit; the cost of this version kept. */
|
|
234
|
+
revise: {
|
|
235
|
+
from: ['active'],
|
|
236
|
+
to: 'active',
|
|
237
|
+
after: records('production.bom', async (bills) => {
|
|
238
|
+
const env = bills.env;
|
|
239
|
+
for (const id of bills.ids)
|
|
240
|
+
await reviseBom(env, id);
|
|
241
|
+
}),
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
extensible: true,
|
|
245
|
+
}),
|
|
246
|
+
note: f.text(),
|
|
247
|
+
label: f.char({
|
|
248
|
+
store: true,
|
|
249
|
+
label: 'Bill of materials',
|
|
250
|
+
...compute('production.bom', 'label', ['product.name', 'version'], (bills) => {
|
|
251
|
+
for (const b of bills)
|
|
252
|
+
b.label = `${b.product.name} ${b.version}`;
|
|
253
|
+
}),
|
|
254
|
+
}),
|
|
255
|
+
},
|
|
256
|
+
sqlConstraints: {
|
|
257
|
+
positive: { check: orm.sql `quantity > 0`, message: 'A bill makes a positive quantity.' },
|
|
258
|
+
packaging: {
|
|
259
|
+
check: orm.sql `kind <> 'packaging' OR (pack_size IS NOT NULL AND pack_size > 0)`,
|
|
260
|
+
message: 'A packaging bill has a pack size.',
|
|
261
|
+
},
|
|
262
|
+
shares: {
|
|
263
|
+
check: orm.sql `expected_loss >= 0 AND expected_loss < 100 AND tolerance >= 0 AND tolerance <= 100`,
|
|
264
|
+
message: 'Loss and tolerance are percentages.',
|
|
265
|
+
},
|
|
266
|
+
period: {
|
|
267
|
+
check: orm.sql `valid_from IS NULL OR valid_to IS NULL OR valid_to >= valid_from`,
|
|
268
|
+
message: 'A bill ends on or after it starts.',
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
/** The next version's name: v1 → v2, 2026-A → 2026-A-2. */
|
|
273
|
+
function nextVersion(version) {
|
|
274
|
+
const m = /^(.*?)(\d+)$/.exec(version);
|
|
275
|
+
return m === null ? `${version}-2` : `${m[1]}${Number(m[2]) + 1}`;
|
|
276
|
+
}
|
|
277
|
+
/** A copy of a bill as its next version, in draft: lines, operations, by-products and specifications. */
|
|
278
|
+
export async function reviseBom(env, id) {
|
|
279
|
+
const [b] = await env
|
|
280
|
+
.model('production.bom')
|
|
281
|
+
.browse([id])
|
|
282
|
+
.load({ lines: true, operations: true, byproducts: true, specs: true, company: true });
|
|
283
|
+
if (b === undefined)
|
|
284
|
+
return null;
|
|
285
|
+
const made = await env.model('production.bom').create([
|
|
286
|
+
{
|
|
287
|
+
product: b.product.id,
|
|
288
|
+
company: b.company.id,
|
|
289
|
+
quantity: b.quantity,
|
|
290
|
+
kind: b.kind,
|
|
291
|
+
packSize: b.packSize,
|
|
292
|
+
expectedLoss: b.expectedLoss,
|
|
293
|
+
tolerance: b.tolerance,
|
|
294
|
+
version: nextVersion(b.version),
|
|
295
|
+
note: b.note,
|
|
296
|
+
lines: {
|
|
297
|
+
create: [...b.lines].map((l) => ({
|
|
298
|
+
product: l.product.id,
|
|
299
|
+
quantity: l.quantity,
|
|
300
|
+
uom: l.uom?.id ?? null,
|
|
301
|
+
ratio: l.ratio,
|
|
302
|
+
loss: l.loss,
|
|
303
|
+
})),
|
|
304
|
+
},
|
|
305
|
+
operations: {
|
|
306
|
+
create: [...b.operations].map((o) => ({
|
|
307
|
+
name: o.name,
|
|
308
|
+
workcenter: o.workcenter.id,
|
|
309
|
+
minutes: o.minutes,
|
|
310
|
+
})),
|
|
311
|
+
},
|
|
312
|
+
byproducts: {
|
|
313
|
+
create: [...b.byproducts].map((x) => ({
|
|
314
|
+
product: x.product.id,
|
|
315
|
+
quantity: x.quantity,
|
|
316
|
+
uom: x.uom?.id ?? null,
|
|
317
|
+
costShare: x.costShare,
|
|
318
|
+
})),
|
|
319
|
+
},
|
|
320
|
+
specs: {
|
|
321
|
+
create: [...b.specs].map((x) => ({
|
|
322
|
+
parameter: x.parameter.id,
|
|
323
|
+
minimum: x.minimum,
|
|
324
|
+
maximum: x.maximum,
|
|
325
|
+
})),
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
]);
|
|
329
|
+
await env.emit('production.bom.revised', { id: b.id });
|
|
330
|
+
return made.ids[0] ?? null;
|
|
331
|
+
}
|
|
332
|
+
export const ProductionBomLine = defineModel('production.bom.line', {
|
|
333
|
+
label: 'Component',
|
|
334
|
+
order: ['id asc'],
|
|
335
|
+
access: { 'production.user': READ, 'production.manager': ALL },
|
|
336
|
+
fields: {
|
|
337
|
+
bom: f.many2one('production.bom', { required: true, ondelete: 'cascade' }),
|
|
338
|
+
product: f.many2one('product.product', { required: true, ondelete: 'restrict' }),
|
|
339
|
+
quantity: f.decimal({ scale: 4, required: true, default: '1' }),
|
|
340
|
+
uom: f.many2one('uom.uom', { label: 'Unit' }),
|
|
341
|
+
ratio: f.decimal({
|
|
342
|
+
scale: 2,
|
|
343
|
+
label: 'Ratio (%)',
|
|
344
|
+
help: 'For a formula: the component’s share of the batch; the ratios add up to 100 %.',
|
|
345
|
+
}),
|
|
346
|
+
loss: f.decimal({
|
|
347
|
+
scale: 2,
|
|
348
|
+
default: '0',
|
|
349
|
+
label: 'Loss (%)',
|
|
350
|
+
help: 'What is lost of this component in making (spillage, moisture): the gross quantity covers it.',
|
|
351
|
+
}),
|
|
352
|
+
netQuantity: f.decimal({
|
|
353
|
+
scale: 4,
|
|
354
|
+
store: true,
|
|
355
|
+
label: 'Net quantity',
|
|
356
|
+
help: 'What goes into the product: the quantity, or the ratio of the batch for a formula.',
|
|
357
|
+
...compute('production.bom.line', 'netQuantity', ['quantity', 'ratio', 'bom.kind', 'bom.quantity'], (lines) => {
|
|
358
|
+
for (const l of lines)
|
|
359
|
+
l.netQuantity =
|
|
360
|
+
l.bom.kind === 'formula' && l.ratio !== null
|
|
361
|
+
? l.bom.quantity.times(l.ratio).div(100).toDecimalPlaces(4)
|
|
362
|
+
: l.quantity;
|
|
363
|
+
}),
|
|
364
|
+
}),
|
|
365
|
+
grossQuantity: f.decimal({
|
|
366
|
+
scale: 4,
|
|
367
|
+
store: true,
|
|
368
|
+
label: 'Gross quantity',
|
|
369
|
+
help: 'The net quantity plus the loss: what the order consumes.',
|
|
370
|
+
...compute('production.bom.line', 'grossQuantity', ['netQuantity', 'loss'], (lines) => {
|
|
371
|
+
for (const l of lines)
|
|
372
|
+
l.grossQuantity = (l.netQuantity ?? l.quantity)
|
|
373
|
+
.times(orm.toDecimal(100).plus(l.loss ?? zero))
|
|
374
|
+
.div(100)
|
|
375
|
+
.toDecimalPlaces(4);
|
|
376
|
+
}),
|
|
377
|
+
}),
|
|
378
|
+
},
|
|
379
|
+
onchange: {
|
|
380
|
+
product: onchange('production.bom.line', async (d) => {
|
|
381
|
+
const p = await d.record('product');
|
|
382
|
+
if (p?.uom)
|
|
383
|
+
d.set('uom', p.uom.id);
|
|
384
|
+
}),
|
|
385
|
+
},
|
|
386
|
+
sqlConstraints: {
|
|
387
|
+
positive: {
|
|
388
|
+
check: orm.sql `quantity > 0`,
|
|
389
|
+
message: 'A component is needed in a positive quantity.',
|
|
390
|
+
},
|
|
391
|
+
shares: {
|
|
392
|
+
check: orm.sql `(ratio IS NULL OR (ratio >= 0 AND ratio <= 100)) AND loss >= 0 AND loss < 100`,
|
|
393
|
+
message: 'Ratios and losses are percentages.',
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
constraints: {
|
|
397
|
+
// The company's eligibility rules for the bill's kind (RFC 0031 §3.2): refused lines say why.
|
|
398
|
+
eligible: {
|
|
399
|
+
...check('production.bom.line', ['product', 'bom'], async (lines) => {
|
|
400
|
+
const env = lines.env;
|
|
401
|
+
const failing = [];
|
|
402
|
+
for (const l of await lines.load({ bom: { company: true }, product: true })) {
|
|
403
|
+
const why = await ineligible(env, l.bom.company.id, l.bom.kind, l.product.id);
|
|
404
|
+
if (why !== null) {
|
|
405
|
+
failing.push(l.id);
|
|
406
|
+
throw new orm.ValidationError('production.bom.line', ['product'], why, 'Choose an allowed component, or change the eligibility rules (Production › Configuration).');
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return failing;
|
|
410
|
+
}),
|
|
411
|
+
message: 'This product is not an allowed component of the bill’s kind.',
|
|
412
|
+
},
|
|
413
|
+
notItself: {
|
|
414
|
+
...check('production.bom.line', ['product', 'bom'], async (lines) => (await lines.load({ bom: true }))
|
|
415
|
+
.filter((l) => l.bom.product.id === l.product.id)
|
|
416
|
+
.map((l) => l.id)),
|
|
417
|
+
message: 'A product is not a component of itself.',
|
|
418
|
+
},
|
|
419
|
+
},
|
|
420
|
+
});
|
|
421
|
+
/** What else one bill makes (sawdust, offcuts), and the share of the order's cost it carries. */
|
|
422
|
+
export const ProductionBomByproduct = defineModel('production.bom.byproduct', {
|
|
423
|
+
label: 'By-product',
|
|
424
|
+
order: ['id asc'],
|
|
425
|
+
access: { 'production.user': READ, 'production.manager': ALL },
|
|
426
|
+
fields: {
|
|
427
|
+
bom: f.many2one('production.bom', { required: true, ondelete: 'cascade' }),
|
|
428
|
+
product: f.many2one('product.product', { required: true, ondelete: 'restrict' }),
|
|
429
|
+
quantity: f.decimal({ scale: 4, required: true, default: '1' }),
|
|
430
|
+
uom: f.many2one('uom.uom', { label: 'Unit' }),
|
|
431
|
+
/** Percent of the order's cost (the main product carries the rest). */
|
|
432
|
+
costShare: f.decimal({
|
|
433
|
+
scale: 2,
|
|
434
|
+
required: true,
|
|
435
|
+
default: '0',
|
|
436
|
+
label: 'Share of the cost (%)',
|
|
437
|
+
}),
|
|
438
|
+
},
|
|
439
|
+
sqlConstraints: {
|
|
440
|
+
positive: {
|
|
441
|
+
check: orm.sql `quantity > 0 AND cost_share >= 0 AND cost_share < 100`,
|
|
442
|
+
message: 'A by-product is made in a positive quantity, with a share of the cost under 100 %.',
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
constraints: {
|
|
446
|
+
sharesUnderAll: {
|
|
447
|
+
...check('production.bom.byproduct', ['costShare', 'bom', 'product'], async (lines) => {
|
|
448
|
+
const failing = [];
|
|
449
|
+
for (const l of await lines.load({ bom: { byproducts: true } })) {
|
|
450
|
+
const total = [...l.bom.byproducts].reduce((s, b) => s.plus(b.costShare), zero);
|
|
451
|
+
if (total.gte(100) || l.product.id === l.bom.product.id)
|
|
452
|
+
failing.push(l.id);
|
|
453
|
+
}
|
|
454
|
+
return failing;
|
|
455
|
+
}),
|
|
456
|
+
message: 'By-products are other products than the one made, and their shares of the cost stay under 100 %.',
|
|
457
|
+
},
|
|
458
|
+
},
|
|
459
|
+
});
|
|
460
|
+
export const ProductionBomOperation = defineModel('production.bom.operation', {
|
|
461
|
+
label: 'Operation',
|
|
462
|
+
order: ['id asc'],
|
|
463
|
+
access: { 'production.user': READ, 'production.manager': ALL },
|
|
464
|
+
fields: {
|
|
465
|
+
bom: f.many2one('production.bom', { required: true, ondelete: 'cascade' }),
|
|
466
|
+
name: f.char({ required: true }),
|
|
467
|
+
workcenter: f.many2one('production.workcenter', { required: true, ondelete: 'restrict' }),
|
|
468
|
+
/** Minutes for what one bill makes. */
|
|
469
|
+
minutes: f.decimal({ scale: 2, required: true, default: '0' }),
|
|
470
|
+
},
|
|
471
|
+
});
|
|
472
|
+
export const ProductionOrderComponent = defineModel('production.order.component', {
|
|
473
|
+
label: 'Component need',
|
|
474
|
+
order: ['id asc'],
|
|
475
|
+
access: { 'production.user': READ, 'production.manager': READ },
|
|
476
|
+
fields: {
|
|
477
|
+
production: f.many2one('production.order', { required: true, ondelete: 'cascade' }),
|
|
478
|
+
product: f.many2one('product.product', { required: true, ondelete: 'restrict' }),
|
|
479
|
+
/** In the product's unit. */
|
|
480
|
+
required: f.decimal({ scale: 4, required: true, default: '0' }),
|
|
481
|
+
/** Available in the warehouse when the order was last checked. */
|
|
482
|
+
available: f.decimal({ scale: 4, required: true, default: '0' }),
|
|
483
|
+
short: f.decimal({
|
|
484
|
+
scale: 4,
|
|
485
|
+
store: true,
|
|
486
|
+
label: 'Short',
|
|
487
|
+
help: 'How much more is needed than the warehouse had available when the order was last checked.',
|
|
488
|
+
...compute('production.order.component', 'short', ['required', 'available'], (lines) => {
|
|
489
|
+
for (const l of lines) {
|
|
490
|
+
const missing = l.required.minus(l.available);
|
|
491
|
+
l.short = missing.gt(0) ? missing : zero;
|
|
492
|
+
}
|
|
493
|
+
}),
|
|
494
|
+
}),
|
|
495
|
+
cost: f.decimal({
|
|
496
|
+
scale: 2,
|
|
497
|
+
store: true,
|
|
498
|
+
label: 'Cost',
|
|
499
|
+
help: 'The quantity needed at the component’s cost.',
|
|
500
|
+
...compute('production.order.component', 'cost', ['required', 'product.standardPrice'], (lines) => {
|
|
501
|
+
for (const l of lines)
|
|
502
|
+
l.cost = money(l.required.times(l.product.standardPrice));
|
|
503
|
+
}),
|
|
504
|
+
}),
|
|
505
|
+
},
|
|
506
|
+
});
|
|
507
|
+
/**
|
|
508
|
+
* A bill's component needs for `quantity` of its product (in the components' own units, gross of losses) with what
|
|
509
|
+
* is available — on hand and not reserved — in the warehouse now.
|
|
510
|
+
*/
|
|
511
|
+
export async function componentNeeds(env, bom, quantity, warehouse, company) {
|
|
512
|
+
const [bill] = await env
|
|
513
|
+
.model('production.bom')
|
|
514
|
+
.browse([bom])
|
|
515
|
+
.load({ lines: { product: { uom: true }, uom: true } });
|
|
516
|
+
if (bill === undefined || bill.quantity.lte(0))
|
|
517
|
+
return [];
|
|
518
|
+
const factor = quantity.div(bill.quantity);
|
|
519
|
+
const needs = new Map();
|
|
520
|
+
for (const l of bill.lines) {
|
|
521
|
+
const inUnit = inProductUnit((l.grossQuantity ?? l.quantity).times(factor), l.uom ?? null, l.product.uom ?? null);
|
|
522
|
+
needs.set(l.product.id, (needs.get(l.product.id) ?? zero).plus(inUnit));
|
|
523
|
+
}
|
|
524
|
+
const places = await defaultLocations(env, 'internal', warehouse, company);
|
|
525
|
+
const out = [];
|
|
526
|
+
for (const [product, required] of needs) {
|
|
527
|
+
let available = zero;
|
|
528
|
+
if (places.source !== null) {
|
|
529
|
+
const quants = await (await env.model('stock.quant').search({
|
|
530
|
+
where: {
|
|
531
|
+
product: orm.asId(product, 'product.product'),
|
|
532
|
+
location: { childOf: places.source },
|
|
533
|
+
},
|
|
534
|
+
})).load(['quantity', 'reserved']);
|
|
535
|
+
available = [...quants].reduce((s, q) => s.plus(q.quantity).minus(q.reserved ?? zero), zero);
|
|
536
|
+
}
|
|
537
|
+
out.push({
|
|
538
|
+
product,
|
|
539
|
+
required: required.toDecimalPlaces(4),
|
|
540
|
+
available: available.gt(0) ? available.toDecimalPlaces(4) : zero,
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
return out;
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Lists the orders' component needs from their bills (in the product's unit, for the quantity ordered) with what is
|
|
547
|
+
* available in their warehouse now; as the system (component lines are not edited by hand).
|
|
548
|
+
*/
|
|
549
|
+
async function explode(ids, env) {
|
|
550
|
+
await env.asSuperuser({ reason: 'production components' }, async (su) => {
|
|
551
|
+
const orders = await su.model('production.order').browse(ids).load({ components: true });
|
|
552
|
+
for (const o of orders) {
|
|
553
|
+
if (o.components.length > 0)
|
|
554
|
+
await o.components.unlink();
|
|
555
|
+
if (o.bom === null)
|
|
556
|
+
continue;
|
|
557
|
+
for (const n of await componentNeeds(su, o.bom.id, o.quantity, o.warehouse?.id ?? null, o.company.id))
|
|
558
|
+
await su.model('production.order.component').create([
|
|
559
|
+
{
|
|
560
|
+
production: o.id,
|
|
561
|
+
product: orm.asId(n.product, 'product.product'),
|
|
562
|
+
required: n.required,
|
|
563
|
+
available: n.available,
|
|
564
|
+
},
|
|
565
|
+
]);
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
/** When a work centre's day starts (local time). */
|
|
570
|
+
export const DAY_STARTS_AT = 8;
|
|
571
|
+
/**
|
|
572
|
+
* Forward schedule of `minutes` of work at a centre working `hoursPerDay` from `DAY_STARTS_AT`, from `from`: what does
|
|
573
|
+
* not fit in a day continues the next day. Returns the start and the end.
|
|
574
|
+
*/
|
|
575
|
+
export function schedule(from, minutes, hoursPerDay) {
|
|
576
|
+
const dayStart = (d) => new Date(d.getFullYear(), d.getMonth(), d.getDate(), DAY_STARTS_AT);
|
|
577
|
+
const dayEnd = (d) => new Date(dayStart(d).getTime() + hoursPerDay * 3_600_000);
|
|
578
|
+
let at = new Date(Math.max(from.getTime(), dayStart(from).getTime()));
|
|
579
|
+
if (at >= dayEnd(at))
|
|
580
|
+
at = dayStart(new Date(at.getTime() + 86_400_000));
|
|
581
|
+
const start = at;
|
|
582
|
+
let left = minutes * 60_000;
|
|
583
|
+
for (;;) {
|
|
584
|
+
const room = dayEnd(at).getTime() - at.getTime();
|
|
585
|
+
if (left <= room)
|
|
586
|
+
return { start, stop: new Date(at.getTime() + left) };
|
|
587
|
+
left -= room;
|
|
588
|
+
at = dayStart(new Date(dayStart(at).getTime() + 86_400_000));
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* A step of a production order at a work centre: planned from the bill's operation (its minutes for the quantity
|
|
593
|
+
* ordered), scheduled after what the centre already has, then started and finished with the minutes it took.
|
|
594
|
+
*/
|
|
595
|
+
export const ProductionWorkorder = defineModel('production.workorder', {
|
|
596
|
+
label: 'Work order',
|
|
597
|
+
tracking: ['state', 'plannedStart', 'workcenter'],
|
|
598
|
+
recName: 'name',
|
|
599
|
+
order: ['plannedStart asc', 'id asc'],
|
|
600
|
+
companyScoped: { required: true },
|
|
601
|
+
access: {
|
|
602
|
+
'production.user': ['read', 'write'],
|
|
603
|
+
'production.manager': [...ALL, 'export'],
|
|
604
|
+
},
|
|
605
|
+
fields: {
|
|
606
|
+
production: f.many2one('production.order', { required: true, ondelete: 'cascade' }),
|
|
607
|
+
name: f.char({ required: true, label: 'Operation' }),
|
|
608
|
+
workcenter: f.many2one('production.workcenter', { required: true, ondelete: 'restrict' }),
|
|
609
|
+
plannedMinutes: f.decimal({ scale: 2, required: true, default: '0', label: 'Planned minutes' }),
|
|
610
|
+
plannedStart: f.datetime({ label: 'Planned start' }),
|
|
611
|
+
plannedStop: f.datetime({ label: 'Planned end' }),
|
|
612
|
+
startedAt: f.datetime({ label: 'Started' }),
|
|
613
|
+
finishedAt: f.datetime({ label: 'Finished' }),
|
|
614
|
+
/** The minutes it took (from start to finish). */
|
|
615
|
+
actualMinutes: f.decimal({ scale: 2, label: 'Minutes taken' }),
|
|
616
|
+
state: f.state({
|
|
617
|
+
states: { ready: 'Ready', progress: 'In progress', done: 'Done', cancel: 'Cancelled' },
|
|
618
|
+
initial: 'ready',
|
|
619
|
+
help: 'Started and finished at the work centre; the minutes between them are recorded.',
|
|
620
|
+
transitions: {
|
|
621
|
+
start: {
|
|
622
|
+
from: ['ready'],
|
|
623
|
+
to: 'progress',
|
|
624
|
+
after: records('production.workorder', async (orders) => {
|
|
625
|
+
await orders.write({ startedAt: new Date() });
|
|
626
|
+
}),
|
|
627
|
+
},
|
|
628
|
+
finish: {
|
|
629
|
+
from: ['progress'],
|
|
630
|
+
to: 'done',
|
|
631
|
+
after: records('production.workorder', async (orders) => {
|
|
632
|
+
const now = new Date();
|
|
633
|
+
for (const w of await orders.load({})) {
|
|
634
|
+
const began = w.startedAt instanceof Date ? w.startedAt : now;
|
|
635
|
+
await w.write({
|
|
636
|
+
finishedAt: now,
|
|
637
|
+
// Whole milliseconds, then minutes (Shutro takes no fractional JS numbers).
|
|
638
|
+
actualMinutes: orm
|
|
639
|
+
.toDecimal(now.getTime() - began.getTime())
|
|
640
|
+
.div(60_000)
|
|
641
|
+
.toDecimalPlaces(2),
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
}),
|
|
645
|
+
},
|
|
646
|
+
/** Done with its order (Produce), however far it got. */
|
|
647
|
+
close: { from: ['ready', 'progress'], to: 'done' },
|
|
648
|
+
cancel: { from: ['ready', 'progress'], to: 'cancel' },
|
|
649
|
+
},
|
|
650
|
+
}),
|
|
651
|
+
},
|
|
652
|
+
});
|
|
653
|
+
/**
|
|
654
|
+
* Makes the orders' work orders from their bills' operations and schedules them forward, step after step, each at
|
|
655
|
+
* its work centre after what the centre already has planned (from the order's date).
|
|
656
|
+
*/
|
|
657
|
+
async function planWork(ids, env) {
|
|
658
|
+
await env.asSuperuser({ reason: 'production schedule' }, async (su) => {
|
|
659
|
+
const orders = await su
|
|
660
|
+
.model('production.order')
|
|
661
|
+
.browse(ids)
|
|
662
|
+
.load({ bom: { operations: { workcenter: true } } });
|
|
663
|
+
for (const o of orders) {
|
|
664
|
+
if (o.bom === null)
|
|
665
|
+
continue;
|
|
666
|
+
const factor = o.quantity.div(o.bom.quantity);
|
|
667
|
+
// The order's day, local (a date is read as text or as a local midnight).
|
|
668
|
+
const day = new Date(`${iso(o.date)}T00:00:00`);
|
|
669
|
+
let after = new Date(day.getFullYear(), day.getMonth(), day.getDate(), DAY_STARTS_AT);
|
|
670
|
+
for (const op of o.bom.operations) {
|
|
671
|
+
const minutes = op.minutes.times(factor).toDecimalPlaces(2);
|
|
672
|
+
// The centre is free after the last step it has that is not done or cancelled.
|
|
673
|
+
const busy = await (await su.model('production.workorder').search({
|
|
674
|
+
where: { workcenter: op.workcenter.id, state: { in: ['ready', 'progress'] } },
|
|
675
|
+
order: ['plannedStop desc'],
|
|
676
|
+
limit: 1,
|
|
677
|
+
})).load(['plannedStop']);
|
|
678
|
+
const free = [...busy][0]?.plannedStop;
|
|
679
|
+
const from = free instanceof Date && free > after ? free : after;
|
|
680
|
+
const { start, stop } = schedule(from, Number(minutes), Number(op.workcenter.hoursPerDay));
|
|
681
|
+
await su.model('production.workorder').create([
|
|
682
|
+
{
|
|
683
|
+
production: o.id,
|
|
684
|
+
company: o.company.id,
|
|
685
|
+
name: op.name,
|
|
686
|
+
workcenter: op.workcenter.id,
|
|
687
|
+
plannedMinutes: minutes,
|
|
688
|
+
plannedStart: start,
|
|
689
|
+
plannedStop: stop,
|
|
690
|
+
},
|
|
691
|
+
]);
|
|
692
|
+
after = stop;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
/** The places of an order: its warehouse's stock, and the Production location. */
|
|
698
|
+
/** A date-only value as `YYYY-MM-DD`. */
|
|
699
|
+
function dayOf(v) {
|
|
700
|
+
if (!(v instanceof Date))
|
|
701
|
+
return String(v ?? '').slice(0, 10);
|
|
702
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
703
|
+
return `${v.getFullYear()}-${pad(v.getMonth() + 1)}-${pad(v.getDate())}`;
|
|
704
|
+
}
|
|
705
|
+
async function placesOf(env, warehouse, company) {
|
|
706
|
+
const stock = (await defaultLocations(env, 'internal', warehouse, company)).source;
|
|
707
|
+
const production = (await env.model('stock.location').search({ where: { usage: 'production' }, limit: 1 })).ids[0];
|
|
708
|
+
if (stock === null || production === undefined)
|
|
709
|
+
throw new orm.ValidationError('production.order', ['warehouse'], 'There is no stock location or Production location to work from.', 'Check the warehouse in Stockroom › Configuration.');
|
|
710
|
+
return { stock, production };
|
|
711
|
+
}
|
|
712
|
+
export const ProductionOrder = defineModel('production.order', {
|
|
713
|
+
label: 'Production order',
|
|
714
|
+
recName: 'name',
|
|
715
|
+
order: ['date desc', 'id desc'],
|
|
716
|
+
companyScoped: { required: true },
|
|
717
|
+
tracking: ['state', 'quantity', 'bom'],
|
|
718
|
+
access: {
|
|
719
|
+
'production.user': ['read', 'write', 'create'],
|
|
720
|
+
'production.manager': [...ALL, 'export', 'import'],
|
|
721
|
+
},
|
|
722
|
+
fields: {
|
|
723
|
+
name: f.char({
|
|
724
|
+
required: true,
|
|
725
|
+
readonly: true,
|
|
726
|
+
sequence: { code: 'production.order', dateField: 'date' },
|
|
727
|
+
label: 'Reference',
|
|
728
|
+
}),
|
|
729
|
+
product: f.many2one('product.product', { required: true, ondelete: 'restrict' }),
|
|
730
|
+
quantity: f.decimal({ scale: 4, required: true, default: '1' }),
|
|
731
|
+
bom: f.many2one('production.bom', { ondelete: 'restrict', label: 'Bill of materials' }),
|
|
732
|
+
plan: f.many2one('production.plan', { ondelete: 'set null', label: 'Production plan' }),
|
|
733
|
+
schedule: f.many2one('production.schedule', { ondelete: 'set null', label: 'Batch' }),
|
|
734
|
+
stage: f.selection({ make: 'Make', pack: 'Pack' }, {
|
|
735
|
+
required: true,
|
|
736
|
+
default: 'make',
|
|
737
|
+
help: 'Make: the product from its components. Pack: packs of a made product, taken from its lot.',
|
|
738
|
+
}),
|
|
739
|
+
batch: f.char({
|
|
740
|
+
label: 'Batch number',
|
|
741
|
+
help: 'The lot the batch is produced into, for a tracked product.',
|
|
742
|
+
}),
|
|
743
|
+
sourceOrder: f.many2one('production.order', {
|
|
744
|
+
ondelete: 'set null',
|
|
745
|
+
label: 'Source order',
|
|
746
|
+
help: 'The make order a pack order packs.',
|
|
747
|
+
}),
|
|
748
|
+
sourceLot: f.many2one('stock.lot', {
|
|
749
|
+
ondelete: 'set null',
|
|
750
|
+
label: 'Source lot',
|
|
751
|
+
help: 'The lot a pack order takes its bulk from: the make order’s, once produced.',
|
|
752
|
+
}),
|
|
753
|
+
lot: f.many2one('stock.lot', { ondelete: 'set null', label: 'Produced lot' }),
|
|
754
|
+
producedQty: f.decimal({
|
|
755
|
+
scale: 4,
|
|
756
|
+
label: 'Produced',
|
|
757
|
+
help: 'What came out, set before producing; the quantity ordered when empty.',
|
|
758
|
+
}),
|
|
759
|
+
consumedQty: f.decimal({
|
|
760
|
+
scale: 4,
|
|
761
|
+
store: true,
|
|
762
|
+
label: 'Consumed',
|
|
763
|
+
help: 'For a pack order the bulk it packs; else the components in the product’s own unit (a formula’s shares).',
|
|
764
|
+
...compute('production.order', 'consumedQty', [
|
|
765
|
+
'stage',
|
|
766
|
+
'components.required',
|
|
767
|
+
'components.product.uom',
|
|
768
|
+
'product.uom',
|
|
769
|
+
'sourceOrder.product',
|
|
770
|
+
], (orders) => {
|
|
771
|
+
for (const o of orders) {
|
|
772
|
+
const lines = [...o.components];
|
|
773
|
+
if (o.stage === 'pack') {
|
|
774
|
+
const bulk = lines.find((c) => c.product.id === (o.sourceOrder?.product.id ?? -1));
|
|
775
|
+
o.consumedQty = bulk?.required ?? zero;
|
|
776
|
+
continue;
|
|
777
|
+
}
|
|
778
|
+
const unit = o.product.uom?.id ?? null;
|
|
779
|
+
o.consumedQty = lines
|
|
780
|
+
.filter((c) => (c.product.uom?.id ?? null) === unit)
|
|
781
|
+
.reduce((s, c) => s.plus(c.required), zero);
|
|
782
|
+
}
|
|
783
|
+
}),
|
|
784
|
+
}),
|
|
785
|
+
lossQty: f.decimal({
|
|
786
|
+
scale: 4,
|
|
787
|
+
store: true,
|
|
788
|
+
label: 'Process loss',
|
|
789
|
+
help: 'Consumed less produced (packs × the pack size for a pack order), once done; negative: a gain.',
|
|
790
|
+
...compute('production.order', 'lossQty', ['state', 'stage', 'consumedQty', 'producedQty', 'quantity', 'bom.packSize'], (orders) => {
|
|
791
|
+
for (const o of orders) {
|
|
792
|
+
if (o.state !== 'done') {
|
|
793
|
+
o.lossQty = zero;
|
|
794
|
+
continue;
|
|
795
|
+
}
|
|
796
|
+
const out = o.producedQty ?? o.quantity;
|
|
797
|
+
const bulk = o.stage === 'pack' ? out.times(o.bom?.packSize ?? orm.toDecimal(1)) : out;
|
|
798
|
+
o.lossQty = (o.consumedQty ?? zero).minus(bulk);
|
|
799
|
+
}
|
|
800
|
+
}),
|
|
801
|
+
}),
|
|
802
|
+
lossPercent: f.decimal({
|
|
803
|
+
scale: 2,
|
|
804
|
+
store: true,
|
|
805
|
+
label: 'Loss %',
|
|
806
|
+
help: 'The process loss as a share of what was consumed.',
|
|
807
|
+
...compute('production.order', 'lossPercent', ['lossQty', 'consumedQty'], (orders) => {
|
|
808
|
+
for (const o of orders) {
|
|
809
|
+
const c = o.consumedQty ?? zero;
|
|
810
|
+
o.lossPercent = c.gt(0) ? (o.lossQty ?? zero).div(c).times(100).toDecimalPlaces(2) : zero;
|
|
811
|
+
}
|
|
812
|
+
}),
|
|
813
|
+
}),
|
|
814
|
+
yieldStatus: f.selection({ pending: 'Pending', within: 'Within tolerance', beyond: 'Beyond tolerance' }, {
|
|
815
|
+
store: true,
|
|
816
|
+
label: 'Yield',
|
|
817
|
+
help: 'Once done: the loss against the bill’s expected loss, within its tolerance or beyond it.',
|
|
818
|
+
...compute('production.order', 'yieldStatus', ['state', 'lossPercent', 'consumedQty', 'bom.expectedLoss', 'bom.tolerance'], (orders) => {
|
|
819
|
+
for (const o of orders) {
|
|
820
|
+
if (o.state !== 'done' || (o.consumedQty ?? zero).lte(0)) {
|
|
821
|
+
o.yieldStatus = 'pending';
|
|
822
|
+
continue;
|
|
823
|
+
}
|
|
824
|
+
const diff = (o.lossPercent ?? zero).minus(o.bom?.expectedLoss ?? zero);
|
|
825
|
+
const gap = diff.lt(0) ? zero.minus(diff) : diff;
|
|
826
|
+
o.yieldStatus = gap.lte(o.bom?.tolerance ?? zero) ? 'within' : 'beyond';
|
|
827
|
+
}
|
|
828
|
+
}),
|
|
829
|
+
}),
|
|
830
|
+
actualStart: f.datetime({ label: 'Started' }),
|
|
831
|
+
actualEnd: f.datetime({ label: 'Finished' }),
|
|
832
|
+
operator: f.many2one('res.users', { ondelete: 'set null' }),
|
|
833
|
+
shift: f.many2one('production.shift', { ondelete: 'set null' }),
|
|
834
|
+
manufacturingDate: f.date({ label: 'Manufactured on' }),
|
|
835
|
+
expiryDate: f.date({
|
|
836
|
+
label: 'Expires on',
|
|
837
|
+
help: 'From the product’s shelf life, on the batch’s lot.',
|
|
838
|
+
}),
|
|
839
|
+
date: f.date({ required: true, default: (env) => env.today(), label: 'Planned for' }),
|
|
840
|
+
warehouse: f.many2one('stock.warehouse', { ondelete: 'restrict' }),
|
|
841
|
+
components: f.one2many('production.order.component', 'production'),
|
|
842
|
+
/** Scrap recorded on the order, with its reasons (Stockroom's scrap). */
|
|
843
|
+
scraps: f.one2many('stock.scrap', 'production'),
|
|
844
|
+
workorders: f.one2many('production.workorder', 'production', { label: 'Work orders' }),
|
|
845
|
+
consumption: f.many2one('stock.picking', {
|
|
846
|
+
ondelete: 'set null',
|
|
847
|
+
label: 'Components transfer',
|
|
848
|
+
}),
|
|
849
|
+
output: f.many2one('stock.picking', { ondelete: 'set null', label: 'Output transfer' }),
|
|
850
|
+
/** The components at their cost. */
|
|
851
|
+
materialCost: f.decimal({
|
|
852
|
+
scale: 2,
|
|
853
|
+
store: true,
|
|
854
|
+
label: 'Materials',
|
|
855
|
+
help: 'The components needed at their cost.',
|
|
856
|
+
...compute('production.order', 'materialCost', ['components.cost'], (orders) => {
|
|
857
|
+
for (const o of orders)
|
|
858
|
+
o.materialCost = [...o.components].reduce((s, c) => s.plus(c.cost ?? zero), zero);
|
|
859
|
+
}),
|
|
860
|
+
}),
|
|
861
|
+
/** The operations at their work centres' cost per hour. */
|
|
862
|
+
operationCost: f.decimal({
|
|
863
|
+
scale: 2,
|
|
864
|
+
store: true,
|
|
865
|
+
label: 'Operations',
|
|
866
|
+
help: 'The operations’ minutes for the quantity ordered at their work centres’ cost per hour.',
|
|
867
|
+
...compute('production.order', 'operationCost', [
|
|
868
|
+
'quantity',
|
|
869
|
+
'bom.quantity',
|
|
870
|
+
'bom.operations.minutes',
|
|
871
|
+
'bom.operations.workcenter.costPerHour',
|
|
872
|
+
], (orders) => {
|
|
873
|
+
for (const o of orders) {
|
|
874
|
+
if (o.bom === null) {
|
|
875
|
+
o.operationCost = zero;
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
878
|
+
const factor = o.quantity.div(o.bom.quantity);
|
|
879
|
+
o.operationCost = money([...o.bom.operations].reduce((s, op) => s.plus(op.minutes.times(factor).div(60).times(op.workcenter.costPerHour)), zero));
|
|
880
|
+
}
|
|
881
|
+
}),
|
|
882
|
+
}),
|
|
883
|
+
totalCost: f.decimal({
|
|
884
|
+
scale: 2,
|
|
885
|
+
store: true,
|
|
886
|
+
label: 'Total cost',
|
|
887
|
+
help: 'The materials and the operations together.',
|
|
888
|
+
...compute('production.order', 'totalCost', ['materialCost', 'operationCost'], (orders) => {
|
|
889
|
+
for (const o of orders)
|
|
890
|
+
o.totalCost = (o.materialCost ?? zero).plus(o.operationCost ?? zero);
|
|
891
|
+
}),
|
|
892
|
+
}),
|
|
893
|
+
unitCost: f.decimal({
|
|
894
|
+
scale: 4,
|
|
895
|
+
store: true,
|
|
896
|
+
label: 'Unit cost',
|
|
897
|
+
help: 'The total cost divided by the quantity made.',
|
|
898
|
+
...compute('production.order', 'unitCost', ['totalCost', 'quantity'], (orders) => {
|
|
899
|
+
for (const o of orders)
|
|
900
|
+
o.unitCost = o.quantity.gt(0)
|
|
901
|
+
? (o.totalCost ?? zero).div(o.quantity).toDecimalPlaces(4)
|
|
902
|
+
: zero;
|
|
903
|
+
}),
|
|
904
|
+
}),
|
|
905
|
+
/** The one chip: Draft, Waiting (components short), Ready, Done, Cancelled. */
|
|
906
|
+
status: f.selection({
|
|
907
|
+
draft: 'Draft',
|
|
908
|
+
waiting: 'Waiting',
|
|
909
|
+
ready: 'Ready',
|
|
910
|
+
progress: 'In progress',
|
|
911
|
+
done: 'Done',
|
|
912
|
+
cancelled: 'Cancelled',
|
|
913
|
+
}, {
|
|
914
|
+
store: true,
|
|
915
|
+
label: 'Status',
|
|
916
|
+
help: 'A confirmed order is ready once all its components are reserved, otherwise it is waiting.',
|
|
917
|
+
...compute('production.order', 'status', ['state', 'consumption.availability'], (orders) => {
|
|
918
|
+
for (const o of orders)
|
|
919
|
+
o.status =
|
|
920
|
+
o.state === 'confirmed'
|
|
921
|
+
? o.consumption?.availability === 'full'
|
|
922
|
+
? 'ready'
|
|
923
|
+
: 'waiting'
|
|
924
|
+
: o.state === 'cancel'
|
|
925
|
+
? 'cancelled'
|
|
926
|
+
: o.state;
|
|
927
|
+
}),
|
|
928
|
+
}),
|
|
929
|
+
note: f.text(),
|
|
930
|
+
state: f.state({
|
|
931
|
+
states: {
|
|
932
|
+
draft: 'Draft',
|
|
933
|
+
confirmed: 'Confirmed',
|
|
934
|
+
progress: 'In progress',
|
|
935
|
+
done: 'Done',
|
|
936
|
+
cancel: 'Cancelled',
|
|
937
|
+
},
|
|
938
|
+
initial: 'draft',
|
|
939
|
+
help: 'Confirmed, the components are reserved and the work orders scheduled; done, the product is made.',
|
|
940
|
+
transitions: {
|
|
941
|
+
/** Lists the components with what is available and short, and the cost, before anything starts. */
|
|
942
|
+
check: {
|
|
943
|
+
from: ['draft', 'confirmed'],
|
|
944
|
+
to: 'draft',
|
|
945
|
+
guard: records('production.order', async (orders) => (await orders.load({})).filter((o) => o.state !== 'draft')),
|
|
946
|
+
message: 'A confirmed order is checked with Check availability.',
|
|
947
|
+
after: records('production.order', async (orders) => {
|
|
948
|
+
await explode(orders.ids, orders.env);
|
|
949
|
+
}),
|
|
950
|
+
},
|
|
951
|
+
confirm: emitting('production.order.confirmed', {
|
|
952
|
+
from: ['draft'],
|
|
953
|
+
to: 'confirmed',
|
|
954
|
+
guard: records('production.order', async (orders) => {
|
|
955
|
+
const env = orders.env;
|
|
956
|
+
const out = [];
|
|
957
|
+
for (const o of await orders.load({ bom: true, company: true })) {
|
|
958
|
+
if (o.bom === null || o.bom.state === 'inactive' || o.bom.state === 'review') {
|
|
959
|
+
out.push(o.id);
|
|
960
|
+
continue;
|
|
961
|
+
}
|
|
962
|
+
if (o.bom.state === 'draft' && (await bomsNeedApproval(env, o.company.id)))
|
|
963
|
+
out.push(o.id);
|
|
964
|
+
}
|
|
965
|
+
return out;
|
|
966
|
+
}),
|
|
967
|
+
message: 'Choose a bill of materials that may be used: an active one (or a draft, when bills need no approval).',
|
|
968
|
+
// The components, their transfer to Production (reserved), and the work orders, scheduled.
|
|
969
|
+
after: records('production.order', async (orders) => {
|
|
970
|
+
await explode(orders.ids, orders.env);
|
|
971
|
+
await planWork(orders.ids, orders.env);
|
|
972
|
+
const env = orders.env;
|
|
973
|
+
for (const o of await env
|
|
974
|
+
.model('production.order')
|
|
975
|
+
.browse(orders.ids)
|
|
976
|
+
.load({ components: true, sourceOrder: true })) {
|
|
977
|
+
// Nothing to consume (labour only): no components transfer.
|
|
978
|
+
if (o.components.length === 0)
|
|
979
|
+
continue;
|
|
980
|
+
// A pack order takes its bulk from the make order's lot.
|
|
981
|
+
const bulkProduct = o.sourceOrder?.product.id ?? null;
|
|
982
|
+
const { stock, production } = await placesOf(env, o.warehouse?.id ?? null, o.company.id);
|
|
983
|
+
const made = await env.model('stock.picking').create([
|
|
984
|
+
{
|
|
985
|
+
kind: 'production',
|
|
986
|
+
company: o.company.id,
|
|
987
|
+
warehouse: o.warehouse?.id ?? null,
|
|
988
|
+
source: stock,
|
|
989
|
+
destination: production,
|
|
990
|
+
origin: o.name,
|
|
991
|
+
scheduledDate: o.date,
|
|
992
|
+
moves: {
|
|
993
|
+
create: [...o.components].map((c) => ({
|
|
994
|
+
product: c.product.id,
|
|
995
|
+
quantity: c.required,
|
|
996
|
+
restrictLot: o.sourceLot !== null && c.product.id === bulkProduct
|
|
997
|
+
? o.sourceLot.id
|
|
998
|
+
: null,
|
|
999
|
+
})),
|
|
1000
|
+
},
|
|
1001
|
+
},
|
|
1002
|
+
]);
|
|
1003
|
+
await made.confirm();
|
|
1004
|
+
await env
|
|
1005
|
+
.model('production.order')
|
|
1006
|
+
.browse([o.id])
|
|
1007
|
+
.write({ consumption: made.ids[0] ?? null });
|
|
1008
|
+
}
|
|
1009
|
+
}),
|
|
1010
|
+
}),
|
|
1011
|
+
/** "Check availability": reserve the components that came in since. */
|
|
1012
|
+
reserve: {
|
|
1013
|
+
from: ['confirmed'],
|
|
1014
|
+
to: 'confirmed',
|
|
1015
|
+
after: records('production.order', async (orders) => {
|
|
1016
|
+
const env = orders.env;
|
|
1017
|
+
for (const o of await env.model('production.order').browse(orders.ids).load({}))
|
|
1018
|
+
if (o.consumption !== null)
|
|
1019
|
+
await env.model('stock.picking').browse([o.consumption.id]).reserve();
|
|
1020
|
+
}),
|
|
1021
|
+
},
|
|
1022
|
+
/** Work begins: the actual start; the batch in progress. */
|
|
1023
|
+
start: {
|
|
1024
|
+
from: ['confirmed'],
|
|
1025
|
+
to: 'progress',
|
|
1026
|
+
after: records('production.order', async (orders) => {
|
|
1027
|
+
const env = orders.env;
|
|
1028
|
+
await env
|
|
1029
|
+
.model('production.order')
|
|
1030
|
+
.browse(orders.ids)
|
|
1031
|
+
.write({ actualStart: env.now() });
|
|
1032
|
+
await env.flush();
|
|
1033
|
+
for (const o of await orders.load({}))
|
|
1034
|
+
if (o.schedule !== null)
|
|
1035
|
+
await followOrders(env, o.schedule.id);
|
|
1036
|
+
}),
|
|
1037
|
+
},
|
|
1038
|
+
produce: emitting('production.order.done', {
|
|
1039
|
+
from: ['confirmed', 'progress'],
|
|
1040
|
+
to: 'done',
|
|
1041
|
+
guard: records('production.order', async (orders) => (await orders.load({ consumption: true })).filter((o) => o.consumption !== null && o.consumption.availability !== 'full')),
|
|
1042
|
+
message: 'Components are short: check availability, or wait for them to come in.',
|
|
1043
|
+
before: records('production.order', async (orders) => {
|
|
1044
|
+
const env = orders.env;
|
|
1045
|
+
// What is left of the work is done with the order.
|
|
1046
|
+
const open = await env.model('production.workorder').search({
|
|
1047
|
+
where: { production: { in: [...orders.ids] }, state: { in: ['ready', 'progress'] } },
|
|
1048
|
+
});
|
|
1049
|
+
if (open.length > 0)
|
|
1050
|
+
await open.close();
|
|
1051
|
+
for (const o of await env
|
|
1052
|
+
.model('production.order')
|
|
1053
|
+
.browse(orders.ids)
|
|
1054
|
+
.load({ product: true })) {
|
|
1055
|
+
if (o.consumption !== null)
|
|
1056
|
+
await env.model('stock.picking').browse([o.consumption.id]).validate();
|
|
1057
|
+
const { stock, production } = await placesOf(env, o.warehouse?.id ?? null, o.company.id);
|
|
1058
|
+
const [bill] = o.bom === null
|
|
1059
|
+
? []
|
|
1060
|
+
: await env.model('production.bom').browse([o.bom.id]).load({ byproducts: true });
|
|
1061
|
+
const produced = o.producedQty ?? o.quantity;
|
|
1062
|
+
const factor = bill === undefined ? zero : produced.div(bill.quantity);
|
|
1063
|
+
// A tracked product is produced into the batch's lot (the order's reference without a batch).
|
|
1064
|
+
const tracking = o.product.tracking ?? 'none';
|
|
1065
|
+
const tracked = tracking !== 'none';
|
|
1066
|
+
const lotName = o.batch ?? o.name;
|
|
1067
|
+
const made = await env.model('stock.picking').create([
|
|
1068
|
+
{
|
|
1069
|
+
kind: 'production',
|
|
1070
|
+
company: o.company.id,
|
|
1071
|
+
warehouse: o.warehouse?.id ?? null,
|
|
1072
|
+
source: production,
|
|
1073
|
+
destination: stock,
|
|
1074
|
+
origin: o.name,
|
|
1075
|
+
moves: {
|
|
1076
|
+
create: [
|
|
1077
|
+
{ product: o.product.id, quantity: produced },
|
|
1078
|
+
...[...(bill?.byproducts ?? [])].map((b) => ({
|
|
1079
|
+
product: b.product.id,
|
|
1080
|
+
quantity: b.quantity.times(factor).toDecimalPlaces(4),
|
|
1081
|
+
uom: b.uom?.id ?? null,
|
|
1082
|
+
})),
|
|
1083
|
+
],
|
|
1084
|
+
},
|
|
1085
|
+
...(tracked
|
|
1086
|
+
? {
|
|
1087
|
+
lotLines: {
|
|
1088
|
+
create: [{ product: o.product.id, lotName, quantity: produced }],
|
|
1089
|
+
},
|
|
1090
|
+
}
|
|
1091
|
+
: {}),
|
|
1092
|
+
},
|
|
1093
|
+
]);
|
|
1094
|
+
// Linked before it is validated: what values it (production_account) finds its order.
|
|
1095
|
+
await env
|
|
1096
|
+
.model('production.order')
|
|
1097
|
+
.browse([o.id])
|
|
1098
|
+
.write({ output: made.ids[0] ?? null });
|
|
1099
|
+
await made.confirm();
|
|
1100
|
+
await made.validate();
|
|
1101
|
+
// The batch's lot with its expiry from the product's shelf life; the dates; the end of the work.
|
|
1102
|
+
const today = dayOf(env.today());
|
|
1103
|
+
let lot = null;
|
|
1104
|
+
let expiryDate = null;
|
|
1105
|
+
if (tracked) {
|
|
1106
|
+
lot =
|
|
1107
|
+
(await env
|
|
1108
|
+
.model('stock.lot')
|
|
1109
|
+
.search({ where: { product: o.product.id, name: lotName }, limit: 1 })).ids[0] ?? null;
|
|
1110
|
+
const days = o.product.shelfLifeDays ?? 0;
|
|
1111
|
+
if (lot !== null && days > 0) {
|
|
1112
|
+
const expiry = new Date(`${today}T00:00:00Z`);
|
|
1113
|
+
expiry.setUTCDate(expiry.getUTCDate() + days);
|
|
1114
|
+
expiryDate = expiry.toISOString().slice(0, 10);
|
|
1115
|
+
await env.model('stock.lot').browse([lot]).write({ expiryDate });
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
await env.model('production.order').browse([o.id]).write({
|
|
1119
|
+
lot,
|
|
1120
|
+
producedQty: produced,
|
|
1121
|
+
manufacturingDate: today,
|
|
1122
|
+
expiryDate,
|
|
1123
|
+
actualEnd: env.now(),
|
|
1124
|
+
});
|
|
1125
|
+
await env.flush();
|
|
1126
|
+
// The pack orders waiting for this batch: confirmed now, their bulk taken from its lot.
|
|
1127
|
+
const packs = await env
|
|
1128
|
+
.model('production.order')
|
|
1129
|
+
.search({ where: { sourceOrder: o.id, state: 'draft' } });
|
|
1130
|
+
if (packs.length > 0) {
|
|
1131
|
+
await env.model('production.order').browse(packs.ids).write({ sourceLot: lot });
|
|
1132
|
+
await env.flush();
|
|
1133
|
+
await packs.confirm();
|
|
1134
|
+
await env.flush();
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
}),
|
|
1138
|
+
after: records('production.order', async (orders) => {
|
|
1139
|
+
const env = orders.env;
|
|
1140
|
+
for (const o of await orders.load({}))
|
|
1141
|
+
if (o.schedule !== null)
|
|
1142
|
+
await followOrders(env, o.schedule.id);
|
|
1143
|
+
}),
|
|
1144
|
+
}),
|
|
1145
|
+
cancel: {
|
|
1146
|
+
from: ['draft', 'confirmed', 'progress'],
|
|
1147
|
+
to: 'cancel',
|
|
1148
|
+
after: records('production.order', async (orders) => {
|
|
1149
|
+
const env = orders.env;
|
|
1150
|
+
const open = await env.model('production.workorder').search({
|
|
1151
|
+
where: { production: { in: [...orders.ids] }, state: { in: ['ready', 'progress'] } },
|
|
1152
|
+
});
|
|
1153
|
+
for (const w of await open.load({}))
|
|
1154
|
+
await w.cancel();
|
|
1155
|
+
for (const o of await env
|
|
1156
|
+
.model('production.order')
|
|
1157
|
+
.browse(orders.ids)
|
|
1158
|
+
.load({ consumption: true }))
|
|
1159
|
+
if (o.consumption !== null &&
|
|
1160
|
+
o.consumption.state !== 'done' &&
|
|
1161
|
+
o.consumption.state !== 'cancel')
|
|
1162
|
+
await env.model('stock.picking').browse([o.consumption.id]).cancel();
|
|
1163
|
+
}),
|
|
1164
|
+
},
|
|
1165
|
+
redraft: { from: ['cancel'], to: 'draft' },
|
|
1166
|
+
},
|
|
1167
|
+
}),
|
|
1168
|
+
},
|
|
1169
|
+
onchange: {
|
|
1170
|
+
// The bill valid on the order's date, and the company's first warehouse.
|
|
1171
|
+
product: onchange('production.order', async (d, env) => {
|
|
1172
|
+
const product = d.get('product');
|
|
1173
|
+
if (product === null || product === undefined)
|
|
1174
|
+
return;
|
|
1175
|
+
d.set('bom', await validBom(env, product, String(d.get('date') ?? env.today())));
|
|
1176
|
+
if ((d.get('warehouse') ?? null) === null)
|
|
1177
|
+
d.set('warehouse', (await defaultLocations(env, 'internal')).warehouse);
|
|
1178
|
+
}),
|
|
1179
|
+
date: onchange('production.order', async (d, env) => {
|
|
1180
|
+
const product = d.get('product');
|
|
1181
|
+
if (product === null || product === undefined)
|
|
1182
|
+
return;
|
|
1183
|
+
d.set('bom', await validBom(env, product, String(d.get('date') ?? env.today())));
|
|
1184
|
+
}),
|
|
1185
|
+
},
|
|
1186
|
+
sqlConstraints: {
|
|
1187
|
+
positive: { check: orm.sql `quantity > 0`, message: 'An order makes a positive quantity.' },
|
|
1188
|
+
},
|
|
1189
|
+
constraints: {
|
|
1190
|
+
billOfTheProduct: {
|
|
1191
|
+
...check('production.order', ['bom', 'product'], async (orders) => (await orders.load({ bom: true }))
|
|
1192
|
+
.filter((o) => o.bom !== null && o.bom.product.id !== o.product.id)
|
|
1193
|
+
.map((o) => o.id)),
|
|
1194
|
+
message: 'The bill of materials is for another product.',
|
|
1195
|
+
},
|
|
1196
|
+
},
|
|
1197
|
+
});
|
|
1198
|
+
/** Scrap recorded on a production order (a broken component, a faulty unit made), with its reason. */
|
|
1199
|
+
/** Production's product fields: the shelf life of what is made, the retail price printed on its label. */
|
|
1200
|
+
export const ProductProduction = extendModel('product.product', {
|
|
1201
|
+
fields: {
|
|
1202
|
+
shelfLifeDays: f.integer({
|
|
1203
|
+
label: 'Shelf life (days)',
|
|
1204
|
+
help: 'A produced lot expires this many days after its manufacturing date; empty: no expiry.',
|
|
1205
|
+
}),
|
|
1206
|
+
retailPrice: f.decimal({
|
|
1207
|
+
scale: 2,
|
|
1208
|
+
label: 'Maximum retail price',
|
|
1209
|
+
help: 'Printed on the batch label (MRP).',
|
|
1210
|
+
}),
|
|
1211
|
+
},
|
|
1212
|
+
});
|
|
1213
|
+
export const ScrapProduction = extendModel('stock.scrap', {
|
|
1214
|
+
fields: {
|
|
1215
|
+
production: f.many2one('production.order', { ondelete: 'set null', label: 'Production order' }),
|
|
1216
|
+
},
|
|
1217
|
+
});
|
|
1218
|
+
/**
|
|
1219
|
+
* Reorder rules that make (RFC 0024 §2.6): production orders not produced yet are on order; ordering makes one
|
|
1220
|
+
* production order per rule with the bill valid on its date, confirmed (its components reserved, its work planned).
|
|
1221
|
+
*/
|
|
1222
|
+
export const ReorderMake = extendModel('stock.reorder', {
|
|
1223
|
+
overrides: {
|
|
1224
|
+
pending: async function (next) {
|
|
1225
|
+
const out = await next();
|
|
1226
|
+
const rules = this;
|
|
1227
|
+
const env = rules.env;
|
|
1228
|
+
for (const r of await rules.load({})) {
|
|
1229
|
+
const main = (await defaultLocations(env, 'internal', null, r.company.id))
|
|
1230
|
+
.warehouse;
|
|
1231
|
+
const orders = await (await env.model('production.order').search({
|
|
1232
|
+
where: {
|
|
1233
|
+
product: r.product.id,
|
|
1234
|
+
company: r.company.id,
|
|
1235
|
+
state: { in: ['draft', 'confirmed'] },
|
|
1236
|
+
},
|
|
1237
|
+
})).load({});
|
|
1238
|
+
const found = out.get(r.id) ?? [];
|
|
1239
|
+
for (const o of orders)
|
|
1240
|
+
if ((o.warehouse?.id ?? main) === r.warehouse.id)
|
|
1241
|
+
found.push({ quantity: o.quantity.toString(), document: o.name });
|
|
1242
|
+
out.set(r.id, found);
|
|
1243
|
+
}
|
|
1244
|
+
return out;
|
|
1245
|
+
},
|
|
1246
|
+
procure: async function (next, quantities) {
|
|
1247
|
+
const out = await next(quantities);
|
|
1248
|
+
const rules = this;
|
|
1249
|
+
const env = rules.env;
|
|
1250
|
+
for (const r of await rules.load({ product: true })) {
|
|
1251
|
+
const qty = quantities.get(r.id);
|
|
1252
|
+
if (r.method !== 'make' || qty === undefined)
|
|
1253
|
+
continue;
|
|
1254
|
+
const date = new Date(`${env.today()}T00:00:00Z`);
|
|
1255
|
+
date.setUTCDate(date.getUTCDate() + r.leadDays);
|
|
1256
|
+
const when = date.toISOString().slice(0, 10);
|
|
1257
|
+
const bom = await validBom(env, r.product.id, when);
|
|
1258
|
+
if (bom === null)
|
|
1259
|
+
throw new orm.ValidationError('stock.reorder', ['method'], `${r.product.name} has no bill of materials valid on ${when}.`, 'Add one in Production › Bills of materials, or replenish it another way.');
|
|
1260
|
+
const made = await env.model('production.order').create([
|
|
1261
|
+
{
|
|
1262
|
+
product: r.product.id,
|
|
1263
|
+
quantity: qty,
|
|
1264
|
+
bom,
|
|
1265
|
+
date: when,
|
|
1266
|
+
warehouse: r.warehouse.id,
|
|
1267
|
+
company: r.company.id,
|
|
1268
|
+
note: 'Replenishment',
|
|
1269
|
+
},
|
|
1270
|
+
]);
|
|
1271
|
+
await made.confirm();
|
|
1272
|
+
const [order] = await made.load({});
|
|
1273
|
+
out.set(r.id, order?.name ?? '');
|
|
1274
|
+
}
|
|
1275
|
+
return out;
|
|
1276
|
+
},
|
|
1277
|
+
},
|
|
1278
|
+
});
|
|
1279
|
+
export const productionShutro = defineModule({
|
|
1280
|
+
name: 'production',
|
|
1281
|
+
version: '0.3.0',
|
|
1282
|
+
depends: [productShutro, stockShutro, contactsShutro, messagingShutro, permissionsShutro],
|
|
1283
|
+
groups: [
|
|
1284
|
+
orm.defineGroup({ code: 'production.user', label: 'Production user', implies: ['stock.user'] }),
|
|
1285
|
+
orm.defineGroup({
|
|
1286
|
+
code: 'production.manager',
|
|
1287
|
+
label: 'Production manager',
|
|
1288
|
+
implies: ['production.user'],
|
|
1289
|
+
}),
|
|
1290
|
+
],
|
|
1291
|
+
sequences: [
|
|
1292
|
+
orm.defineSequence({
|
|
1293
|
+
code: 'production.order',
|
|
1294
|
+
prefix: 'MO/{year}/',
|
|
1295
|
+
padding: 5,
|
|
1296
|
+
reset: 'yearly',
|
|
1297
|
+
}),
|
|
1298
|
+
orm.defineSequence({
|
|
1299
|
+
code: 'production.plan',
|
|
1300
|
+
prefix: 'PLAN/{year}/',
|
|
1301
|
+
padding: 5,
|
|
1302
|
+
reset: 'yearly',
|
|
1303
|
+
}),
|
|
1304
|
+
orm.defineSequence({
|
|
1305
|
+
code: 'production.schedule',
|
|
1306
|
+
prefix: 'BATCH/{year}/',
|
|
1307
|
+
padding: 5,
|
|
1308
|
+
reset: 'yearly',
|
|
1309
|
+
}),
|
|
1310
|
+
],
|
|
1311
|
+
models: [
|
|
1312
|
+
ProductionWorkcenter,
|
|
1313
|
+
ProductionBom,
|
|
1314
|
+
ProductionBomLine,
|
|
1315
|
+
ProductionBomByproduct,
|
|
1316
|
+
ProductionBomOperation,
|
|
1317
|
+
ProductionOrder,
|
|
1318
|
+
ProductionOrderComponent,
|
|
1319
|
+
ProductionWorkorder,
|
|
1320
|
+
ProductionSpecParameter,
|
|
1321
|
+
ProductionProductSpec,
|
|
1322
|
+
ProductionBomSpec,
|
|
1323
|
+
ProductionBomCost,
|
|
1324
|
+
ProductionBomEligibility,
|
|
1325
|
+
ProductionShift,
|
|
1326
|
+
ProductionPlan,
|
|
1327
|
+
ProductionPlanLine,
|
|
1328
|
+
ProductionPlanMaterial,
|
|
1329
|
+
ProductionSchedule,
|
|
1330
|
+
ProductionScheduleComponent,
|
|
1331
|
+
],
|
|
1332
|
+
extensions: [ScrapProduction, ReorderMake, CompanyProduction, ProductProduction],
|
|
1333
|
+
});
|