diff --git a/.copier-answers.yml b/.copier-answers.yml index 625e7e59390..d5e90db716f 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,6 +1,6 @@ # Do NOT update manually; changes here will be overwritten by Copier -_commit: v1.29 -_src_path: gh:oca/oca-addons-repo-template +_commit: v1.38 +_src_path: https://github.com/OCA/oca-addons-repo-template.git ci: GitHub convert_readme_fragments_to_markdown: false enable_checklog_odoo: false @@ -18,7 +18,7 @@ org_name: Odoo Community Association (OCA) org_slug: OCA rebel_module_groups: [] repo_description: WMS modules for Odoo -repo_name: wms +repo_name: Warehouse Management System (WMS) repo_slug: wms repo_website: https://github.com/OCA/wms use_pyproject_toml: false diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000000..e0d56685a95 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +test-requirements.txt merge=union diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index afd7524ef0d..5e6c3fb4395 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -17,6 +17,7 @@ jobs: - uses: actions/setup-python@v5 with: python-version: "3.11" + cache: 'pip' - name: Get python version run: echo "PY=$(python -VV | sha256sum | cut -d' ' -f1)" >> $GITHUB_ENV - uses: actions/cache@v4 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4ccc216aff6..743814303f6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -63,6 +63,13 @@ jobs: run: oca_init_test_database - name: Run tests run: oca_run_tests + - name: Upload screenshots from JS tests + uses: actions/upload-artifact@v4 + if: ${{ failure() }} + with: + name: Screenshots of failed JS tests - ${{ matrix.name }}${{ join(matrix.include) }} + path: /tmp/odoo_tests/${{ env.PGDATABASE }} + if-no-files-found: ignore - uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.oca/oca-port/blacklist/stock_available_to_promise_release.json b/.oca/oca-port/blacklist/stock_available_to_promise_release.json new file mode 100644 index 00000000000..842603423a1 --- /dev/null +++ b/.oca/oca-port/blacklist/stock_available_to_promise_release.json @@ -0,0 +1,9 @@ +{ + "pull_requests": { + "OCA/wms#556": "Already ported", + "OCA/wms#604": "v14 migration update, not relevant", + "OCA/wms#686": "Already ported", + "OCA/wms#810": "Already ported + not relevant", + "OCA/wms#1015": "14.0 backport from 16.0 (already there, false positive)" + } +} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5bb727f5655..3fa6f98534f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,7 +9,7 @@ exclude: | # Maybe reactivate this when all README files include prettier ignore tags? ^README\.md$| # Library files can have extraneous formatting (even minimized) - lib/| + /static/(src/)?lib/| # Repos using Sphinx to generate docs don't need prettying ^docs/_templates/.*\.html$| # Don't bother non-technical authors with formatting issues in docs @@ -39,7 +39,7 @@ repos: language: fail files: '[a-zA-Z0-9_]*/i18n/en\.po$' - repo: https://github.com/oca/maintainer-tools - rev: d5fab7ee87fceee858a3d01048c78a548974d935 + rev: f9b919b9868143135a9c9cb03021089cabba8223 hooks: # update the NOT INSTALLABLE ADDONS section above - id: oca-update-pre-commit-excluded-addons @@ -104,6 +104,7 @@ repos: additional_dependencies: - "eslint@8.24.0" - "eslint-plugin-jsdoc@" + - "globals@" # SPECIAL CASE: barcode scanner app use Vue.js + # advanced JS (modules, classes, etc) # which are not supported by OCA rules yet. @@ -144,7 +145,7 @@ repos: - --settings=. exclude: /__init__\.py$ - repo: https://github.com/acsone/setuptools-odoo - rev: 3.1.8 + rev: 3.3.2 hooks: - id: setuptools-odoo-make-default - id: setuptools-odoo-get-requirements diff --git a/.pylintrc b/.pylintrc index 554913276b4..0a521c31ffe 100644 --- a/.pylintrc +++ b/.pylintrc @@ -25,19 +25,25 @@ disable=all enable=anomalous-backslash-in-string, api-one-deprecated, api-one-multi-together, - assignment-from-none, - attribute-deprecated, class-camelcase, - dangerous-default-value, dangerous-view-replace-wo-priority, - development-status-allowed, duplicate-id-csv, - duplicate-key, duplicate-xml-fields, duplicate-xml-record-id, eval-referenced, - eval-used, incoherent-interpreter-exec-perm, + openerp-exception-warning, + redundant-modulename-xml, + relative-import, + rst-syntax-error, + wrong-tabs-instead-of-spaces, + xml-syntax-error, + assignment-from-none, + attribute-deprecated, + dangerous-default-value, + development-status-allowed, + duplicate-key, + eval-used, license-allowed, manifest-author-string, manifest-deprecated-key, @@ -48,73 +54,68 @@ enable=anomalous-backslash-in-string, method-inverse, method-required-super, method-search, - openerp-exception-warning, pointless-statement, pointless-string-statement, print-used, redundant-keyword-arg, - redundant-modulename-xml, reimported, - relative-import, return-in-init, - rst-syntax-error, sql-injection, too-few-format-args, translation-field, translation-required, unreachable, use-vim-comment, - wrong-tabs-instead-of-spaces, - xml-syntax-error, - attribute-string-redundant, character-not-valid-in-resource-link, - consider-merging-classes-inherited, - context-overridden, create-user-wo-reset-password, dangerous-filter-wo-user, dangerous-qweb-replace-wo-priority, deprecated-data-xml-node, deprecated-openerp-xml-node, duplicate-po-message-definition, - except-pass, file-not-used, + missing-newline-extrafiles, + old-api7-method-defined, + po-msgstr-variables, + po-syntax-error, + str-format-used, + unnecessary-utf8-coding-comment, + xml-attribute-translatable, + xml-deprecated-qweb-directive, + xml-deprecated-tree-attribute, + attribute-string-redundant, + consider-merging-classes-inherited, + context-overridden, + except-pass, invalid-commit, manifest-maintainers-list, - missing-newline-extrafiles, missing-readme, missing-return, odoo-addons-relative-import, - old-api7-method-defined, - po-msgstr-variables, - po-syntax-error, renamed-field-parameter, resource-not-exist, - str-format-used, test-folder-imported, translation-contains-variable, translation-positional-used, - unnecessary-utf8-coding-comment, website-manifest-key-not-valid-uri, - xml-attribute-translatable, - xml-deprecated-qweb-directive, - xml-deprecated-tree-attribute, external-request-timeout, - # messages that do not cause the lint step to fail - consider-merging-classes-inherited, + missing-manifest-dependency, + too-complex,, create-user-wo-reset-password, dangerous-filter-wo-user, - deprecated-module, file-not-used, - invalid-commit, - missing-manifest-dependency, missing-newline-extrafiles, - missing-readme, no-utf8-coding-comment, - odoo-addons-relative-import, old-api7-method-defined, + unnecessary-utf8-coding-comment, + # messages that do not cause the lint step to fail + consider-merging-classes-inherited, + deprecated-module, + invalid-commit, + missing-readme, + odoo-addons-relative-import, redefined-builtin, - too-complex, - unnecessary-utf8-coding-comment + manifest-external-assets [REPORTS] diff --git a/.pylintrc-mandatory b/.pylintrc-mandatory index 7a0cd4efefe..098393aadb8 100644 --- a/.pylintrc-mandatory +++ b/.pylintrc-mandatory @@ -17,19 +17,25 @@ disable=all enable=anomalous-backslash-in-string, api-one-deprecated, api-one-multi-together, - assignment-from-none, - attribute-deprecated, class-camelcase, - dangerous-default-value, dangerous-view-replace-wo-priority, - development-status-allowed, duplicate-id-csv, - duplicate-key, duplicate-xml-fields, duplicate-xml-record-id, eval-referenced, - eval-used, incoherent-interpreter-exec-perm, + openerp-exception-warning, + redundant-modulename-xml, + relative-import, + rst-syntax-error, + wrong-tabs-instead-of-spaces, + xml-syntax-error, + assignment-from-none, + attribute-deprecated, + dangerous-default-value, + development-status-allowed, + duplicate-key, + eval-used, license-allowed, manifest-author-string, manifest-deprecated-key, @@ -40,56 +46,50 @@ enable=anomalous-backslash-in-string, method-inverse, method-required-super, method-search, - openerp-exception-warning, pointless-statement, pointless-string-statement, print-used, redundant-keyword-arg, - redundant-modulename-xml, reimported, - relative-import, return-in-init, - rst-syntax-error, sql-injection, too-few-format-args, translation-field, translation-required, unreachable, use-vim-comment, - wrong-tabs-instead-of-spaces, - xml-syntax-error, - attribute-string-redundant, character-not-valid-in-resource-link, - consider-merging-classes-inherited, - context-overridden, create-user-wo-reset-password, dangerous-filter-wo-user, dangerous-qweb-replace-wo-priority, deprecated-data-xml-node, deprecated-openerp-xml-node, duplicate-po-message-definition, - except-pass, file-not-used, + missing-newline-extrafiles, + old-api7-method-defined, + po-msgstr-variables, + po-syntax-error, + str-format-used, + unnecessary-utf8-coding-comment, + xml-attribute-translatable, + xml-deprecated-qweb-directive, + xml-deprecated-tree-attribute, + attribute-string-redundant, + consider-merging-classes-inherited, + context-overridden, + except-pass, invalid-commit, manifest-maintainers-list, - missing-newline-extrafiles, missing-readme, missing-return, odoo-addons-relative-import, - old-api7-method-defined, - po-msgstr-variables, - po-syntax-error, renamed-field-parameter, resource-not-exist, - str-format-used, test-folder-imported, translation-contains-variable, translation-positional-used, - unnecessary-utf8-coding-comment, website-manifest-key-not-valid-uri, - xml-attribute-translatable, - xml-deprecated-qweb-directive, - xml-deprecated-tree-attribute, external-request-timeout [REPORTS] diff --git a/README.md b/README.md index ca20853bc1a..321bf111763 100644 --- a/README.md +++ b/README.md @@ -23,52 +23,64 @@ addon | version | maintainers | summary --- | --- | --- | --- [delivery_carrier_warehouse](delivery_carrier_warehouse/) | 16.0.1.0.1 | | Get delivery method used in sales orders from warehouse [sale_stock_available_to_promise_release](sale_stock_available_to_promise_release/) | 16.0.1.1.2 | | Integration between Sales and Available to Promise Release -[sale_stock_available_to_promise_release_block](sale_stock_available_to_promise_release_block/) | 16.0.1.0.0 | | Block release of deliveries from sales orders. -[sale_stock_release_channel_partner_by_date](sale_stock_release_channel_partner_by_date/) | 16.0.1.1.0 | [![sebalix](https://github.com/sebalix.png?size=30px)](https://github.com/sebalix) | Release channels integration with Sales -[sale_stock_release_channel_partner_by_date_delivery](sale_stock_release_channel_partner_by_date_delivery/) | 16.0.1.1.0 | [![sebalix](https://github.com/sebalix.png?size=30px)](https://github.com/sebalix) | Filters channels on sales based on selected carrier. -[shopfloor](shopfloor/) | 16.0.2.6.0 | [![guewen](https://github.com/guewen.png?size=30px)](https://github.com/guewen) [![simahawk](https://github.com/simahawk.png?size=30px)](https://github.com/simahawk) [![sebalix](https://github.com/sebalix.png?size=30px)](https://github.com/sebalix) | manage warehouse operations with barcode scanners -[shopfloor_base](shopfloor_base/) | 16.0.1.1.1 | [![guewen](https://github.com/guewen.png?size=30px)](https://github.com/guewen) [![simahawk](https://github.com/simahawk.png?size=30px)](https://github.com/simahawk) [![sebalix](https://github.com/sebalix.png?size=30px)](https://github.com/sebalix) | Core module for creating mobile apps -[shopfloor_batch_automatic_creation](shopfloor_batch_automatic_creation/) | 16.0.1.1.0 | [![guewen](https://github.com/guewen.png?size=30px)](https://github.com/guewen) | Create batch transfers for Cluster Picking -[shopfloor_mobile](shopfloor_mobile/) | 16.0.1.4.0 | [![simahawk](https://github.com/simahawk.png?size=30px)](https://github.com/simahawk) | Mobile frontend for WMS Shopfloor app -[shopfloor_mobile_base](shopfloor_mobile_base/) | 16.0.1.1.0 | [![simahawk](https://github.com/simahawk.png?size=30px)](https://github.com/simahawk) | Mobile frontend for WMS Shopfloor app +[sale_stock_available_to_promise_release_block](sale_stock_available_to_promise_release_block/) | 16.0.1.1.1 | | Block release of deliveries from sales orders. +[sale_stock_release_channel](sale_stock_release_channel/) | 16.0.1.0.0 | jbaudoux | Sales Stock Release Channel +[sale_stock_release_channel_delivery](sale_stock_release_channel_delivery/) | 16.0.1.0.0 | jbaudoux | Sales Stock Release Channel Delivery +[sale_stock_release_channel_delivery_date](sale_stock_release_channel_delivery_date/) | 16.0.1.1.2 | jbaudoux | Compute expected date based on available release channels +[sale_stock_release_channel_partner_by_date](sale_stock_release_channel_partner_by_date/) | 16.0.1.1.0 | sebalix | Release channels integration with Sales +[sale_stock_release_channel_partner_by_date_delivery](sale_stock_release_channel_partner_by_date_delivery/) | 16.0.1.1.1 | sebalix | Filters channels on sales based on selected carrier. +[shopfloor](shopfloor/) | 16.0.2.16.1 | guewen simahawk sebalix | manage warehouse operations with barcode scanners +[shopfloor_base](shopfloor_base/) | 16.0.1.2.0 | guewen simahawk sebalix | Core module for creating mobile apps +[shopfloor_batch_automatic_creation](shopfloor_batch_automatic_creation/) | 16.0.1.1.0 | guewen | Create batch transfers for Cluster Picking +[shopfloor_mobile](shopfloor_mobile/) | 16.0.1.4.1 | simahawk | Mobile frontend for WMS Shopfloor app +[shopfloor_mobile_base](shopfloor_mobile_base/) | 16.0.1.1.0 | simahawk | Mobile frontend for WMS Shopfloor app [shopfloor_mobile_base_auth_api_key](shopfloor_mobile_base_auth_api_key/) | 16.0.1.0.0 | | Provides authentication via API key to Shopfloor base mobile app -[shopfloor_reception](shopfloor_reception/) | 16.0.1.0.0 | [![mmequignon](https://github.com/mmequignon.png?size=30px)](https://github.com/mmequignon) [![JuMiSanAr](https://github.com/JuMiSanAr.png?size=30px)](https://github.com/JuMiSanAr) | Reception scenario for shopfloor -[shopfloor_reception_mobile](shopfloor_reception_mobile/) | 16.0.1.0.0 | [![JuMiSanAr](https://github.com/JuMiSanAr.png?size=30px)](https://github.com/JuMiSanAr) | Scenario for receiving products -[shopfloor_rest_log](shopfloor_rest_log/) | 16.0.1.0.0 | [![simahawk](https://github.com/simahawk.png?size=30px)](https://github.com/simahawk) | Integrate rest_log into Shopfloor app +[shopfloor_reception](shopfloor_reception/) | 16.0.1.6.6 | mmequignon JuMiSanAr | Reception scenario for shopfloor +[shopfloor_reception_mobile](shopfloor_reception_mobile/) | 16.0.1.1.2 | JuMiSanAr | Scenario for receiving products +[shopfloor_reception_refund_return](shopfloor_reception_refund_return/) | 16.0.1.0.0 | mmequignon | Mark created return as to refund +[shopfloor_rest_log](shopfloor_rest_log/) | 16.0.1.0.0 | simahawk | Integrate rest_log into Shopfloor app [shopfloor_workstation](shopfloor_workstation/) | 16.0.1.0.0 | | Manage warehouse workstation with barcode scanners [shopfloor_workstation_mobile](shopfloor_workstation_mobile/) | 16.0.1.0.0 | | Shopfloor mobile app integration for workstation -[stock_available_to_promise_release](stock_available_to_promise_release/) | 16.0.3.6.2 | | Release Operations based on available to promise +[stock_available_to_promise_release](stock_available_to_promise_release/) | 16.0.3.8.3 | | Release Operations based on available to promise [stock_available_to_promise_release_block](stock_available_to_promise_release_block/) | 16.0.1.1.1 | | Block Release of Operations +[stock_available_to_promise_release_dynamic_routing](stock_available_to_promise_release_dynamic_routing/) | 16.0.1.0.0 | jbaudoux | Glue between moves release and dynamic routing [stock_available_to_promise_release_exclude_location](stock_available_to_promise_release_exclude_location/) | 16.0.1.0.0 | | Exclude locations from available stock -[stock_dynamic_routing](stock_dynamic_routing/) | 16.0.1.0.2 | | Dynamic routing of stock moves -[stock_picking_batch_creation](stock_picking_batch_creation/) | 16.0.1.0.0 | [![lmignon](https://github.com/lmignon.png?size=30px)](https://github.com/lmignon) | Create a batch of pickings to be processed all together +[stock_dynamic_routing](stock_dynamic_routing/) | 16.0.1.0.4 | | Dynamic routing of stock moves +[stock_full_location_reservation](stock_full_location_reservation/) | 16.0.1.0.0 | mt-software-de | Extend reservation to full content of location +[stock_picking_batch_creation](stock_picking_batch_creation/) | 16.0.2.2.0 | lmignon jbaudoux | Create a batch of pickings to be processed all together [stock_picking_completion_info](stock_picking_completion_info/) | 16.0.1.0.1 | | Display on current document completion information according to next operations [stock_picking_type_shipping_policy](stock_picking_type_shipping_policy/) | 16.0.1.0.0 | | Define different shipping policies according to picking type -[stock_release_channel](stock_release_channel/) | 16.0.2.18.4 | [![sebalix](https://github.com/sebalix.png?size=30px)](https://github.com/sebalix) [![jbaudoux](https://github.com/jbaudoux.png?size=30px)](https://github.com/jbaudoux) [![mt-software-de](https://github.com/mt-software-de.png?size=30px)](https://github.com/mt-software-de) | Manage workload in WMS with release channels +[stock_release_channel](stock_release_channel/) | 16.0.3.1.1 | sebalix jbaudoux mt-software-de | Manage workload in WMS with release channels [stock_release_channel_auto_release](stock_release_channel_auto_release/) | 16.0.1.1.0 | | Add an automatic release mode to the release channel [stock_release_channel_batch_mode_commercial_partner](stock_release_channel_batch_mode_commercial_partner/) | 16.0.1.0.2 | | Release pickings into channels by batch of same commercial entity -[stock_release_channel_cutoff](stock_release_channel_cutoff/) | 16.0.1.0.2 | [![jbaudoux](https://github.com/jbaudoux.png?size=30px)](https://github.com/jbaudoux) | Add the cutoff time to the release channel -[stock_release_channel_delivery](stock_release_channel_delivery/) | 16.0.2.1.0 | | Add a carrier selection criteria on the release channel -[stock_release_channel_geoengine](stock_release_channel_geoengine/) | 16.0.1.1.0 | | Release channel based on geo-localization -[stock_release_channel_partner_by_date](stock_release_channel_partner_by_date/) | 16.0.1.1.0 | [![sebalix](https://github.com/sebalix.png?size=30px)](https://github.com/sebalix) [![jbaudoux](https://github.com/jbaudoux.png?size=30px)](https://github.com/jbaudoux) | Set release channels for specific delivery dates -[stock_release_channel_partner_delivery_window](stock_release_channel_partner_delivery_window/) | 16.0.1.0.1 | [![jbaudoux](https://github.com/jbaudoux.png?size=30px)](https://github.com/jbaudoux) | Allows to define an end date (and time) on a release channel and propagate it to the concerned pickings -[stock_release_channel_partner_public_holidays](stock_release_channel_partner_public_holidays/) | 16.0.1.0.0 | [![jbaudoux](https://github.com/jbaudoux.png?size=30px)](https://github.com/jbaudoux) | Add an option to exclude the public holidays when assigning th release channel -[stock_release_channel_plan](stock_release_channel_plan/) | 16.0.1.3.0 | [![jbaudoux](https://github.com/jbaudoux.png?size=30px)](https://github.com/jbaudoux) | Manage release channel preparation plan -[stock_release_channel_plan_process_end_time](stock_release_channel_plan_process_end_time/) | 16.0.1.1.0 | [![jbaudoux](https://github.com/jbaudoux.png?size=30px)](https://github.com/jbaudoux) | Glue module between release channel plan and process end time -[stock_release_channel_plan_shipment_lead_time](stock_release_channel_plan_shipment_lead_time/) | 16.0.1.0.0 | [![jbaudoux](https://github.com/jbaudoux.png?size=30px)](https://github.com/jbaudoux) | Stock release channel plan shipment lead time -[stock_release_channel_process_end_time](stock_release_channel_process_end_time/) | 16.0.1.7.0 | [![rousseldenis](https://github.com/rousseldenis.png?size=30px)](https://github.com/rousseldenis) [![jbaudoux](https://github.com/jbaudoux.png?size=30px)](https://github.com/jbaudoux) | Allows to define an end date (and time) on a release channel and propagate it to the concerned pickings +[stock_release_channel_cutoff](stock_release_channel_cutoff/) | 16.0.1.1.0 | jbaudoux | Add the cutoff time to the release channel +[stock_release_channel_delivery](stock_release_channel_delivery/) | 16.0.3.0.0 | | Add a carrier selection criteria on the release channel +[stock_release_channel_depot](stock_release_channel_depot/) | 16.0.1.0.0 | | This module allows users to add partner depot to stock release channel. +[stock_release_channel_geoengine](stock_release_channel_geoengine/) | 16.0.2.0.0 | | Release channel based on geo-localization +[stock_release_channel_partner_by_date](stock_release_channel_partner_by_date/) | 16.0.2.0.1 | sebalix jbaudoux | Set release channels for specific delivery dates +[stock_release_channel_partner_by_date_delivery_window](stock_release_channel_partner_by_date_delivery_window/) | 16.0.1.0.0 | jbaudoux | Glue Stock Release Channels for Delivery Dates and Delivery window +[stock_release_channel_partner_by_date_public_holidays](stock_release_channel_partner_by_date_public_holidays/) | 16.0.2.0.0 | jbaudoux | Glue Stock Release Channels for Delivery Dates and Public holidays +[stock_release_channel_partner_delivery_window](stock_release_channel_partner_delivery_window/) | 16.0.2.1.0 | jbaudoux | Allows to define an end date (and time) on a release channel and propagate it to the concerned pickings +[stock_release_channel_partner_public_holidays](stock_release_channel_partner_public_holidays/) | 16.0.2.1.0 | jbaudoux | Add an option to exclude the public holidays when assigning th release channel +[stock_release_channel_plan](stock_release_channel_plan/) | 16.0.1.3.0 | jbaudoux | Manage release channel preparation plan +[stock_release_channel_plan_depot](stock_release_channel_plan_depot/) | 16.0.1.0.0 | | This module allows users to set partner depot on stock release channel preparation plan. +[stock_release_channel_plan_process_end_time](stock_release_channel_plan_process_end_time/) | 16.0.1.1.0 | jbaudoux | Glue module between release channel plan and process end time +[stock_release_channel_plan_shipment_lead_time](stock_release_channel_plan_shipment_lead_time/) | 16.0.1.1.0 | jbaudoux | Stock release channel plan shipment lead time +[stock_release_channel_process_end_time](stock_release_channel_process_end_time/) | 16.0.1.7.0 | rousseldenis jbaudoux | Allows to define an end date (and time) on a release channel and propagate it to the concerned pickings [stock_release_channel_propagate_channel_picking](stock_release_channel_propagate_channel_picking/) | 16.0.1.2.0 | | Allows to propagate the channel to every picking that is created from the original one. -[stock_release_channel_shipment_advice](stock_release_channel_shipment_advice/) | 16.0.1.2.0 | [![jbaudoux](https://github.com/jbaudoux.png?size=30px)](https://github.com/jbaudoux) | Plan shipment advices for ready and released pickings -[stock_release_channel_shipment_advice_deliver](stock_release_channel_shipment_advice_deliver/) | 16.0.1.1.0 | | This module adds an action to the release channel to automate the delivery of its shippings. +[stock_release_channel_shipment_advice](stock_release_channel_shipment_advice/) | 16.0.1.2.0 | jbaudoux | Plan shipment advices for ready and released pickings +[stock_release_channel_shipment_advice_deliver](stock_release_channel_shipment_advice_deliver/) | 16.0.2.0.2 | | This module adds an action to the release channel to automate the delivery of its shippings. [stock_release_channel_shipment_advice_process_end_time](stock_release_channel_shipment_advice_process_end_time/) | 16.0.1.0.0 | | This module allows to set a delay time (in minutes) between the release channel process end time and the shipment advice arrival to the dock time. -[stock_release_channel_shipment_advice_toursolver](stock_release_channel_shipment_advice_toursolver/) | 16.0.1.0.1 | | Use TourSolver to plan shipment advices for ready and released pickings -[stock_release_channel_shipment_lead_time](stock_release_channel_shipment_lead_time/) | 16.0.1.3.0 | [![jbaudoux](https://github.com/jbaudoux.png?size=30px)](https://github.com/jbaudoux) | Release channel with shipment lead time +[stock_release_channel_shipment_advice_toursolver](stock_release_channel_shipment_advice_toursolver/) | 16.0.1.1.0 | | Use TourSolver to plan shipment advices for ready and released pickings +[stock_release_channel_shipment_lead_time](stock_release_channel_shipment_lead_time/) | 16.0.2.1.0 | jbaudoux | Release channel with shipment lead time [stock_release_channel_show_volume](stock_release_channel_show_volume/) | 16.0.1.1.0 | | Display volumes of stock release channels [stock_release_channel_show_weight](stock_release_channel_show_weight/) | 16.0.1.1.0 | | Display weights of stock release channels -[stock_storage_type](stock_storage_type/) | 16.0.1.1.0 | [![jbaudoux](https://github.com/jbaudoux.png?size=30px)](https://github.com/jbaudoux) [![rousseldenis](https://github.com/rousseldenis.png?size=30px)](https://github.com/rousseldenis) | Manage packages and locations storage types +[stock_release_channel_warehouse_calendar](stock_release_channel_warehouse_calendar/) | 16.0.1.0.0 | jbaudoux | Glue module between release channel and warehouse calendar +[stock_storage_type](stock_storage_type/) | 16.0.2.0.3 | jbaudoux rousseldenis | Manage packages and locations storage types [stock_storage_type_putaway_abc](stock_storage_type_putaway_abc/) | 16.0.1.0.0 | | Advanced storage strategy ABC for WMS -[stock_warehouse_flow](stock_warehouse_flow/) | 16.0.1.0.2 | | Configure routing flow for stock moves -[stock_warehouse_flow_release](stock_warehouse_flow_release/) | 16.0.1.0.0 | | Warehouse flows integrated with Operation Release +[stock_warehouse_flow](stock_warehouse_flow/) | 16.0.1.1.0 | | Configure routing flow for stock moves +[stock_warehouse_flow_delivery_refresh](stock_warehouse_flow_delivery_refresh/) | 16.0.1.0.0 | | Allow to refresh delivery flow when carrier changes +[stock_warehouse_flow_release](stock_warehouse_flow_release/) | 16.0.1.1.0 | | Warehouse flows integrated with Operation Release [//]: # (end addons) diff --git a/sale_stock_available_to_promise_release/i18n/it.po b/sale_stock_available_to_promise_release/i18n/it.po index 5514224458e..60fd8cc7b1e 100644 --- a/sale_stock_available_to_promise_release/i18n/it.po +++ b/sale_stock_available_to_promise_release/i18n/it.po @@ -7,7 +7,7 @@ msgstr "" "Project-Id-Version: Odoo Server 13.0+e\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-07-12 17:07+0000\n" -"PO-Revision-Date: 2024-06-24 13:35+0000\n" +"PO-Revision-Date: 2025-09-03 09:42+0000\n" "Last-Translator: mymage \n" "Language-Team: \n" "Language: it\n" @@ -15,7 +15,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.17\n" +"X-Generator: Weblate 5.10.4\n" #. module: sale_stock_available_to_promise_release #: model_terms:ir.ui.view,arch_db:sale_stock_available_to_promise_release.report_saleorder_document @@ -79,7 +79,7 @@ msgstr "Data di disponibilità prevista" #. module: sale_stock_available_to_promise_release #: model:ir.model.fields.selection,name:sale_stock_available_to_promise_release.selection__sale_order_line__availability_status__full msgid "Fully Available" -msgstr "Completamente Disponibile" +msgstr "Completamente disponibile" #. module: sale_stock_available_to_promise_release #: model_terms:ir.ui.view,arch_db:sale_stock_available_to_promise_release.view_move_release_search @@ -105,7 +105,7 @@ msgstr "No" #. module: sale_stock_available_to_promise_release #: model:ir.model.fields.selection,name:sale_stock_available_to_promise_release.selection__sale_order_line__availability_status__no msgid "Not available" -msgstr "No disponibile" +msgstr "Non disponibile" #. module: sale_stock_available_to_promise_release #: model:ir.model.fields.selection,name:sale_stock_available_to_promise_release.selection__sale_order_line__availability_status__on_order @@ -116,7 +116,7 @@ msgstr "Su ordine" #. module: sale_stock_available_to_promise_release #: model:ir.model.fields.selection,name:sale_stock_available_to_promise_release.selection__sale_order_line__availability_status__partial msgid "Partially Available" -msgstr "Parzialmente Disponibile" +msgstr "Parzialmente disponibile" #. module: sale_stock_available_to_promise_release #: model:ir.model,name:sale_stock_available_to_promise_release.model_procurement_group diff --git a/sale_stock_available_to_promise_release/tests/common.py b/sale_stock_available_to_promise_release/tests/common.py index c0c9c0e2691..7d70958ff91 100644 --- a/sale_stock_available_to_promise_release/tests/common.py +++ b/sale_stock_available_to_promise_release/tests/common.py @@ -21,12 +21,13 @@ def setUpClassProduct(cls): } ) + @classmethod + def _create_sale_order(cls): + return cls.env["sale.order"].create({"partner_id": cls.customer.id}) + @classmethod def setUpClassSale(cls): - customer = cls.env["res.partner"].create( - {"name": "Partner who loves storable products"} - ) - cls.sale = cls.env["sale.order"].create({"partner_id": customer.id}) + cls.sale = cls._create_sale_order() cls.line = cls.env["sale.order.line"].create( { "order_id": cls.sale.id, @@ -45,6 +46,9 @@ def setUpClassStock(cls): def setUpClass(cls): super().setUpClass() cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls.customer = cls.env["res.partner"].create( + {"name": "Partner who loves storable products"} + ) cls.setUpClassProduct() cls.setUpClassSale() cls.setUpClassStock() diff --git a/sale_stock_available_to_promise_release_block/README.rst b/sale_stock_available_to_promise_release_block/README.rst index 7d564cc2ede..c9b64a30916 100644 --- a/sale_stock_available_to_promise_release_block/README.rst +++ b/sale_stock_available_to_promise_release_block/README.rst @@ -1,3 +1,7 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + ===================================================== Stock Available to Promise Release - Block from Sales ===================================================== @@ -7,13 +11,13 @@ Stock Available to Promise Release - Block from Sales !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:a4fccfd767ff2327845ac9a41c82922424dd602e454da081ba2a9a565120f8ca + !! source digest: sha256:7034247fc3b671f076b8cd90e6ec65cd1641a1b03bc56cc2f776fe5ae2670347 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png :target: https://odoo-community.org/page/development-status :alt: Beta -.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png +.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fwms-lightgray.png?logo=github @@ -28,7 +32,18 @@ Stock Available to Promise Release - Block from Sales |badge1| |badge2| |badge3| |badge4| |badge5| -Block release of deliveries from sale orders. +Block and unblock release of deliveries from sale orders. + +Release of deliveries can be blocked right after the sale order confirmation. + +When encoding a new order sharing the same delivery address, the user can +list the existing blocked deliveries (backorders) and plan to unblock them +when this new order is confirmed, making the existing deliveries and the new +ones sharing the same scheduled dates and deadlines. + +As a side-effect, this will leverage the module +`stock_picking_group_by_partner_by_carrier_by_date` if this one is installed, +by grouping all delivery lines within the same delivery order. **Table of contents** diff --git a/sale_stock_available_to_promise_release_block/__init__.py b/sale_stock_available_to_promise_release_block/__init__.py index 0650744f6bc..a0f653930e7 100644 --- a/sale_stock_available_to_promise_release_block/__init__.py +++ b/sale_stock_available_to_promise_release_block/__init__.py @@ -1 +1,3 @@ from . import models +from . import wizards +from .hooks import post_init_hook diff --git a/sale_stock_available_to_promise_release_block/__manifest__.py b/sale_stock_available_to_promise_release_block/__manifest__.py index 1b3b34dfa2a..585b937cd6b 100644 --- a/sale_stock_available_to_promise_release_block/__manifest__.py +++ b/sale_stock_available_to_promise_release_block/__manifest__.py @@ -5,7 +5,7 @@ { "name": "Stock Available to Promise Release - Block from Sales", "summary": """Block release of deliveries from sales orders.""", - "version": "16.0.1.0.0", + "version": "16.0.1.1.1", "license": "AGPL-3", "author": "Camptcamp, ACSONE SA/NV, BCIM, Odoo Community Association (OCA)", "website": "https://github.com/OCA/wms", @@ -15,8 +15,12 @@ "stock_available_to_promise_release_block", ], "data": [ + "security/ir.model.access.csv", "views/sale_order.xml", "views/sale_order_line.xml", + "views/stock_move.xml", + "wizards/unblock_release.xml", ], "installable": True, + "post_init_hook": "post_init_hook", } diff --git a/sale_stock_available_to_promise_release_block/hooks.py b/sale_stock_available_to_promise_release_block/hooks.py new file mode 100644 index 00000000000..e976062791f --- /dev/null +++ b/sale_stock_available_to_promise_release_block/hooks.py @@ -0,0 +1,18 @@ +# Copyright 2024 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +import logging + +from odoo import SUPERUSER_ID, api + +_logger = logging.getLogger(__name__) + + +def post_init_hook(cr, registry): + _logger.info("Remove original 'Unblock Release' server action...") + env = api.Environment(cr, SUPERUSER_ID, {}) + action = env.ref( + "stock_available_to_promise_release_block.action_stock_move_unblock_release", + raise_if_not_found=False, + ) + action.unlink() diff --git a/sale_stock_available_to_promise_release_block/i18n/it.po b/sale_stock_available_to_promise_release_block/i18n/it.po index 919b8032ddf..8d040a8a443 100644 --- a/sale_stock_available_to_promise_release_block/i18n/it.po +++ b/sale_stock_available_to_promise_release_block/i18n/it.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0\n" "Report-Msgid-Bugs-To: \n" -"PO-Revision-Date: 2024-04-22 09:34+0000\n" +"PO-Revision-Date: 2025-05-22 07:57+0000\n" "Last-Translator: mymage \n" "Language-Team: none\n" "Language: it\n" @@ -14,7 +14,40 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.17\n" +"X-Generator: Weblate 5.10.4\n" + +#. module: sale_stock_available_to_promise_release_block +#: model:ir.model.fields,help:sale_stock_available_to_promise_release_block.field_unblock_release__option +msgid "" +"- Manual: schedule blocked deliveries at a given date;\n" +"- Automatic: schedule blocked deliveries as soon as possible;\n" +"- Based on current order: schedule blocked deliveries with the contextual " +"sale order." +msgstr "" +"- Manuale: pianifica le consegne bloccate a una data specifica;\n" +"- Automatico: pianifica le consegne bloccate il prima possibile;\n" +"- In base all'ordine corrente: pianifica le consegne bloccate con l'ordine " +"di vendita contestuale." + +#. module: sale_stock_available_to_promise_release_block +#: model:ir.model.fields,field_description:sale_stock_available_to_promise_release_block.field_sale_order__available_move_to_unblock_count +msgid "Available Move To Unblock Count" +msgstr "Conteggio movimenti da sbloccare disponibili" + +#. module: sale_stock_available_to_promise_release_block +#: model:ir.model.fields,field_description:sale_stock_available_to_promise_release_block.field_sale_order__available_move_to_unblock_ids +msgid "Available moves to unblock" +msgstr "Movimenti da sbloccare disponibili" + +#. module: sale_stock_available_to_promise_release_block +#: model:ir.model.fields,help:sale_stock_available_to_promise_release_block.field_sale_order__available_move_to_unblock_ids +msgid "Available moves to unblock for this order." +msgstr "Movimenti da sbloccare disponibili per questo ordine." + +#. module: sale_stock_available_to_promise_release_block +#: model_terms:ir.ui.view,arch_db:sale_stock_available_to_promise_release_block.view_order_form +msgid "Backorders" +msgstr "Ordini residui" #. module: sale_stock_available_to_promise_release_block #: model:ir.actions.server,name:sale_stock_available_to_promise_release_block.action_sale_order_line_block_release @@ -34,16 +67,106 @@ msgstr "Blocca il rilascio delle consegne generate alla conferma ordine." msgid "Blocked" msgstr "Bloccata" +#. module: sale_stock_available_to_promise_release_block +#: model_terms:ir.ui.view,arch_db:sale_stock_available_to_promise_release_block.unblock_release_view_form +msgid "Cancel" +msgstr "Annulla" + +#. module: sale_stock_available_to_promise_release_block +#: model:ir.model.fields,field_description:sale_stock_available_to_promise_release_block.field_unblock_release__create_uid +msgid "Created by" +msgstr "Creato da" + +#. module: sale_stock_available_to_promise_release_block +#: model:ir.model.fields,field_description:sale_stock_available_to_promise_release_block.field_unblock_release__create_date +msgid "Created on" +msgstr "Creato il" + +#. module: sale_stock_available_to_promise_release_block +#: model:ir.model.fields,field_description:sale_stock_available_to_promise_release_block.field_unblock_release__date_deadline +msgid "Date Deadline" +msgstr "Data scadenza" + +#. module: sale_stock_available_to_promise_release_block +#: model_terms:ir.ui.view,arch_db:sale_stock_available_to_promise_release_block.view_order_form +msgid "Deliveries that could be unblocked at order confirmation." +msgstr "Consegne che possono essere sbloccate alla conferma dell'ordine." + +#. module: sale_stock_available_to_promise_release_block +#: model_terms:ir.ui.view,arch_db:sale_stock_available_to_promise_release_block.view_order_form +msgid "Deliveries that will be unblocked at order confirmation." +msgstr "Consegne che verranno sbloccate alla conferma dell'ordine." + +#. module: sale_stock_available_to_promise_release_block +#: model:ir.model.fields,field_description:sale_stock_available_to_promise_release_block.field_unblock_release__move_ids +msgid "Delivery moves" +msgstr "Movimenti consegne" + +#. module: sale_stock_available_to_promise_release_block +#: model:ir.model.fields,field_description:sale_stock_available_to_promise_release_block.field_unblock_release__display_name +msgid "Display Name" +msgstr "Nome visualizzato" + #. module: sale_stock_available_to_promise_release_block #: model:ir.model.fields,field_description:sale_stock_available_to_promise_release_block.field_sale_order_line__is_release_blocked msgid "Has Blocked Delivery" msgstr "Ha consegne bloccate" +#. module: sale_stock_available_to_promise_release_block +#: model:ir.model.fields,field_description:sale_stock_available_to_promise_release_block.field_unblock_release__id +msgid "ID" +msgstr "ID" + #. module: sale_stock_available_to_promise_release_block #: model_terms:ir.ui.view,arch_db:sale_stock_available_to_promise_release_block.view_sales_order_line_filter msgid "Is Release Blocked" msgstr "Il rilascio è bloccato" +#. module: sale_stock_available_to_promise_release_block +#: model:ir.model.fields,field_description:sale_stock_available_to_promise_release_block.field_unblock_release____last_update +msgid "Last Modified on" +msgstr "Ultima modifica il" + +#. module: sale_stock_available_to_promise_release_block +#: model:ir.model.fields,field_description:sale_stock_available_to_promise_release_block.field_unblock_release__write_uid +msgid "Last Updated by" +msgstr "Ultimo aggiornamento di" + +#. module: sale_stock_available_to_promise_release_block +#: model:ir.model.fields,field_description:sale_stock_available_to_promise_release_block.field_unblock_release__write_date +msgid "Last Updated on" +msgstr "Ultimo aggiornamento il" + +#. module: sale_stock_available_to_promise_release_block +#: model:ir.model.fields,field_description:sale_stock_available_to_promise_release_block.field_sale_order__move_to_unblock_count +msgid "Move To Unblock Count" +msgstr "Conteggio movimenti da sbloccare" + +#. module: sale_stock_available_to_promise_release_block +#: model:ir.model.fields,field_description:sale_stock_available_to_promise_release_block.field_sale_order__move_to_unblock_ids +msgid "Moves To Unblock" +msgstr "Movimenti da sbloccare" + +#. module: sale_stock_available_to_promise_release_block +#: model:ir.model.fields,help:sale_stock_available_to_promise_release_block.field_sale_order__move_to_unblock_ids +msgid "Moves to unblock when the current order is confirmed." +msgstr "Movimenti da sbloccare alla conferma dell'ordine attuale." + +#. module: sale_stock_available_to_promise_release_block +#: model:ir.model.fields,field_description:sale_stock_available_to_promise_release_block.field_unblock_release__option +msgid "Option" +msgstr "Opzione" + +#. module: sale_stock_available_to_promise_release_block +#: model:ir.model.fields,field_description:sale_stock_available_to_promise_release_block.field_unblock_release__order_id +msgid "Order" +msgstr "Ordine" + +#. module: sale_stock_available_to_promise_release_block +#: model:ir.model.fields,field_description:sale_stock_available_to_promise_release_block.field_unblock_release__order_line_ids +msgid "Order Lines" +msgstr "Righe ordine" + #. module: sale_stock_available_to_promise_release_block #: model:ir.model.fields,field_description:sale_stock_available_to_promise_release_block.field_sale_order_line__release_blocked_label msgid "Release Blocked" @@ -64,6 +187,29 @@ msgstr "Riga ordine di vendita" msgid "Sales Order Lines having blocked deliveries" msgstr "Righe ordine di vendita con consegne bloccate" +#. module: sale_stock_available_to_promise_release_block +#: model_terms:ir.ui.view,arch_db:sale_stock_available_to_promise_release_block.unblock_release_view_form +msgid "Selected deliveries" +msgstr "Consegne selezionate" + +#. module: sale_stock_available_to_promise_release_block +#: model_terms:ir.ui.view,arch_db:sale_stock_available_to_promise_release_block.unblock_release_view_form +msgid "" +"Selected delivery moves will be unblocked when the above order is " +"confirmed.\n" +" Their delivery method, shipping policy and scheduled date will " +"be aligned with the deliveries of the confirmed order." +msgstr "" +"Le consegne selezionate verranno sbloccate alla conferma dell'ordine sopra " +"indicato. \n" +" Il metodo di consegna, la politica di spedizione e la data " +"prevista saranno allineati alle consegne dell'ordine confermato." + +#. module: sale_stock_available_to_promise_release_block +#: model:ir.model,name:sale_stock_available_to_promise_release_block.model_stock_move +msgid "Stock Move" +msgstr "Movimento di magazzino" + #. module: sale_stock_available_to_promise_release_block #: model:ir.model,name:sale_stock_available_to_promise_release_block.model_stock_rule msgid "Stock Rule" @@ -77,6 +223,25 @@ msgid "This operator is not supported" msgstr "Questo operatore non è supportato" #. module: sale_stock_available_to_promise_release_block -#: model:ir.actions.server,name:sale_stock_available_to_promise_release_block.action_sale_order_line_unblock_release +#: model:ir.actions.act_window,name:sale_stock_available_to_promise_release_block.action_sale_order_line_unblock_release +#: model:ir.actions.act_window,name:sale_stock_available_to_promise_release_block.action_stock_move_unblock_release +#: model:ir.model,name:sale_stock_available_to_promise_release_block.model_unblock_release msgid "Unblock Release" msgstr "Sblocco rilascio" + +#. module: sale_stock_available_to_promise_release_block +#: model:ir.model.fields,field_description:sale_stock_available_to_promise_release_block.field_stock_move__unblocked_by_order_id +msgid "Unblocked by order" +msgstr "Sbloccato da ordine" + +#. module: sale_stock_available_to_promise_release_block +#: model_terms:ir.ui.view,arch_db:sale_stock_available_to_promise_release_block.unblock_release_view_form +msgid "Validate" +msgstr "Valida" + +#. module: sale_stock_available_to_promise_release_block +#. odoo-python +#: code:addons/sale_stock_available_to_promise_release_block/wizards/unblock_release.py:0 +#, python-format +msgid "You cannot reschedule deliveries in the past." +msgstr "Non si possono rischedulare le consegne nel passato." diff --git a/sale_stock_available_to_promise_release_block/i18n/sale_stock_available_to_promise_release_block.pot b/sale_stock_available_to_promise_release_block/i18n/sale_stock_available_to_promise_release_block.pot index eba9fe1fc75..474e1afce15 100644 --- a/sale_stock_available_to_promise_release_block/i18n/sale_stock_available_to_promise_release_block.pot +++ b/sale_stock_available_to_promise_release_block/i18n/sale_stock_available_to_promise_release_block.pot @@ -13,6 +13,34 @@ msgstr "" "Content-Transfer-Encoding: \n" "Plural-Forms: \n" +#. module: sale_stock_available_to_promise_release_block +#: model:ir.model.fields,help:sale_stock_available_to_promise_release_block.field_unblock_release__option +msgid "" +"- Manual: schedule blocked deliveries at a given date;\n" +"- Automatic: schedule blocked deliveries as soon as possible;\n" +"- Based on current order: schedule blocked deliveries with the contextual sale order." +msgstr "" + +#. module: sale_stock_available_to_promise_release_block +#: model:ir.model.fields,field_description:sale_stock_available_to_promise_release_block.field_sale_order__available_move_to_unblock_count +msgid "Available Move To Unblock Count" +msgstr "" + +#. module: sale_stock_available_to_promise_release_block +#: model:ir.model.fields,field_description:sale_stock_available_to_promise_release_block.field_sale_order__available_move_to_unblock_ids +msgid "Available moves to unblock" +msgstr "" + +#. module: sale_stock_available_to_promise_release_block +#: model:ir.model.fields,help:sale_stock_available_to_promise_release_block.field_sale_order__available_move_to_unblock_ids +msgid "Available moves to unblock for this order." +msgstr "" + +#. module: sale_stock_available_to_promise_release_block +#: model_terms:ir.ui.view,arch_db:sale_stock_available_to_promise_release_block.view_order_form +msgid "Backorders" +msgstr "" + #. module: sale_stock_available_to_promise_release_block #: model:ir.actions.server,name:sale_stock_available_to_promise_release_block.action_sale_order_line_block_release #: model:ir.model.fields,field_description:sale_stock_available_to_promise_release_block.field_sale_order__block_release @@ -31,16 +59,106 @@ msgstr "" msgid "Blocked" msgstr "" +#. module: sale_stock_available_to_promise_release_block +#: model_terms:ir.ui.view,arch_db:sale_stock_available_to_promise_release_block.unblock_release_view_form +msgid "Cancel" +msgstr "" + +#. module: sale_stock_available_to_promise_release_block +#: model:ir.model.fields,field_description:sale_stock_available_to_promise_release_block.field_unblock_release__create_uid +msgid "Created by" +msgstr "" + +#. module: sale_stock_available_to_promise_release_block +#: model:ir.model.fields,field_description:sale_stock_available_to_promise_release_block.field_unblock_release__create_date +msgid "Created on" +msgstr "" + +#. module: sale_stock_available_to_promise_release_block +#: model:ir.model.fields,field_description:sale_stock_available_to_promise_release_block.field_unblock_release__date_deadline +msgid "Date Deadline" +msgstr "" + +#. module: sale_stock_available_to_promise_release_block +#: model_terms:ir.ui.view,arch_db:sale_stock_available_to_promise_release_block.view_order_form +msgid "Deliveries that could be unblocked at order confirmation." +msgstr "" + +#. module: sale_stock_available_to_promise_release_block +#: model_terms:ir.ui.view,arch_db:sale_stock_available_to_promise_release_block.view_order_form +msgid "Deliveries that will be unblocked at order confirmation." +msgstr "" + +#. module: sale_stock_available_to_promise_release_block +#: model:ir.model.fields,field_description:sale_stock_available_to_promise_release_block.field_unblock_release__move_ids +msgid "Delivery moves" +msgstr "" + +#. module: sale_stock_available_to_promise_release_block +#: model:ir.model.fields,field_description:sale_stock_available_to_promise_release_block.field_unblock_release__display_name +msgid "Display Name" +msgstr "" + #. module: sale_stock_available_to_promise_release_block #: model:ir.model.fields,field_description:sale_stock_available_to_promise_release_block.field_sale_order_line__is_release_blocked msgid "Has Blocked Delivery" msgstr "" +#. module: sale_stock_available_to_promise_release_block +#: model:ir.model.fields,field_description:sale_stock_available_to_promise_release_block.field_unblock_release__id +msgid "ID" +msgstr "" + #. module: sale_stock_available_to_promise_release_block #: model_terms:ir.ui.view,arch_db:sale_stock_available_to_promise_release_block.view_sales_order_line_filter msgid "Is Release Blocked" msgstr "" +#. module: sale_stock_available_to_promise_release_block +#: model:ir.model.fields,field_description:sale_stock_available_to_promise_release_block.field_unblock_release____last_update +msgid "Last Modified on" +msgstr "" + +#. module: sale_stock_available_to_promise_release_block +#: model:ir.model.fields,field_description:sale_stock_available_to_promise_release_block.field_unblock_release__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: sale_stock_available_to_promise_release_block +#: model:ir.model.fields,field_description:sale_stock_available_to_promise_release_block.field_unblock_release__write_date +msgid "Last Updated on" +msgstr "" + +#. module: sale_stock_available_to_promise_release_block +#: model:ir.model.fields,field_description:sale_stock_available_to_promise_release_block.field_sale_order__move_to_unblock_count +msgid "Move To Unblock Count" +msgstr "" + +#. module: sale_stock_available_to_promise_release_block +#: model:ir.model.fields,field_description:sale_stock_available_to_promise_release_block.field_sale_order__move_to_unblock_ids +msgid "Moves To Unblock" +msgstr "" + +#. module: sale_stock_available_to_promise_release_block +#: model:ir.model.fields,help:sale_stock_available_to_promise_release_block.field_sale_order__move_to_unblock_ids +msgid "Moves to unblock when the current order is confirmed." +msgstr "" + +#. module: sale_stock_available_to_promise_release_block +#: model:ir.model.fields,field_description:sale_stock_available_to_promise_release_block.field_unblock_release__option +msgid "Option" +msgstr "" + +#. module: sale_stock_available_to_promise_release_block +#: model:ir.model.fields,field_description:sale_stock_available_to_promise_release_block.field_unblock_release__order_id +msgid "Order" +msgstr "" + +#. module: sale_stock_available_to_promise_release_block +#: model:ir.model.fields,field_description:sale_stock_available_to_promise_release_block.field_unblock_release__order_line_ids +msgid "Order Lines" +msgstr "" + #. module: sale_stock_available_to_promise_release_block #: model:ir.model.fields,field_description:sale_stock_available_to_promise_release_block.field_sale_order_line__release_blocked_label msgid "Release Blocked" @@ -61,6 +179,23 @@ msgstr "" msgid "Sales Order Lines having blocked deliveries" msgstr "" +#. module: sale_stock_available_to_promise_release_block +#: model_terms:ir.ui.view,arch_db:sale_stock_available_to_promise_release_block.unblock_release_view_form +msgid "Selected deliveries" +msgstr "" + +#. module: sale_stock_available_to_promise_release_block +#: model_terms:ir.ui.view,arch_db:sale_stock_available_to_promise_release_block.unblock_release_view_form +msgid "" +"Selected delivery moves will be unblocked when the above order is confirmed.\n" +" Their delivery method, shipping policy and scheduled date will be aligned with the deliveries of the confirmed order." +msgstr "" + +#. module: sale_stock_available_to_promise_release_block +#: model:ir.model,name:sale_stock_available_to_promise_release_block.model_stock_move +msgid "Stock Move" +msgstr "" + #. module: sale_stock_available_to_promise_release_block #: model:ir.model,name:sale_stock_available_to_promise_release_block.model_stock_rule msgid "Stock Rule" @@ -74,6 +209,25 @@ msgid "This operator is not supported" msgstr "" #. module: sale_stock_available_to_promise_release_block -#: model:ir.actions.server,name:sale_stock_available_to_promise_release_block.action_sale_order_line_unblock_release +#: model:ir.actions.act_window,name:sale_stock_available_to_promise_release_block.action_sale_order_line_unblock_release +#: model:ir.actions.act_window,name:sale_stock_available_to_promise_release_block.action_stock_move_unblock_release +#: model:ir.model,name:sale_stock_available_to_promise_release_block.model_unblock_release msgid "Unblock Release" msgstr "" + +#. module: sale_stock_available_to_promise_release_block +#: model:ir.model.fields,field_description:sale_stock_available_to_promise_release_block.field_stock_move__unblocked_by_order_id +msgid "Unblocked by order" +msgstr "" + +#. module: sale_stock_available_to_promise_release_block +#: model_terms:ir.ui.view,arch_db:sale_stock_available_to_promise_release_block.unblock_release_view_form +msgid "Validate" +msgstr "" + +#. module: sale_stock_available_to_promise_release_block +#. odoo-python +#: code:addons/sale_stock_available_to_promise_release_block/wizards/unblock_release.py:0 +#, python-format +msgid "You cannot reschedule deliveries in the past." +msgstr "" diff --git a/sale_stock_available_to_promise_release_block/migrations/16.0.1.1.0/pre-migrate.py b/sale_stock_available_to_promise_release_block/migrations/16.0.1.1.0/pre-migrate.py new file mode 100644 index 00000000000..9ec4cdcd26e --- /dev/null +++ b/sale_stock_available_to_promise_release_block/migrations/16.0.1.1.0/pre-migrate.py @@ -0,0 +1,35 @@ +# Copyright 2024 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) +import logging + +_logger = logging.getLogger(__name__) + + +def migrate(cr, version): + if not version: + return + remove_unblock_release_ir_action_server(cr) + + +def remove_unblock_release_ir_action_server(cr): + # The same XML-ID will be used by a new window action to open a wizard + _logger.info("Remove action 'action_sale_order_line_unblock_release'") + queries = [ + """ + DELETE FROM ir_act_server + WHERE id IN ( + SELECT res_id + FROM ir_model_data + WHERE module='sale_stock_available_to_promise_release_block' + AND name='action_sale_order_line_unblock_release' + AND model='ir.actions.server' + ); + """, + """ + DELETE FROM ir_model_data + WHERE module='sale_stock_available_to_promise_release_block' + AND name='action_sale_order_line_unblock_release'; + """, + ] + for query in queries: + cr.execute(query) diff --git a/sale_stock_available_to_promise_release_block/models/__init__.py b/sale_stock_available_to_promise_release_block/models/__init__.py index 0d2d587c109..e9668470db4 100644 --- a/sale_stock_available_to_promise_release_block/models/__init__.py +++ b/sale_stock_available_to_promise_release_block/models/__init__.py @@ -1,3 +1,4 @@ +from . import stock_move from . import stock_rule from . import sale_order from . import sale_order_line diff --git a/sale_stock_available_to_promise_release_block/models/sale_order.py b/sale_stock_available_to_promise_release_block/models/sale_order.py index e9630f9dcac..1c81e774bb5 100644 --- a/sale_stock_available_to_promise_release_block/models/sale_order.py +++ b/sale_stock_available_to_promise_release_block/models/sale_order.py @@ -2,7 +2,7 @@ # Copyright 2024 Camptocamp # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -from odoo import fields, models +from odoo import api, fields, models class SaleOrder(models.Model): @@ -14,3 +14,93 @@ class SaleOrder(models.Model): states={"draft": [("readonly", False)]}, help="Block the release of the generated delivery at order confirmation.", ) + available_move_to_unblock_ids = fields.One2many( + comodel_name="stock.move", + compute="_compute_available_move_to_unblock_ids", + string="Available moves to unblock", + help="Available moves to unblock for this order.", + ) + available_move_to_unblock_count = fields.Integer( + compute="_compute_available_move_to_unblock_ids" + ) + move_to_unblock_ids = fields.One2many( + comodel_name="stock.move", + inverse_name="unblocked_by_order_id", + string="Moves To Unblock", + readonly=True, + help="Moves to unblock when the current order is confirmed.", + ) + move_to_unblock_count = fields.Integer(compute="_compute_move_to_unblock_count") + + def _domain_available_move_to_unblock(self): + self.ensure_one() + # Returns domain for moves: + # - of type delivery + # - sharing the same shipping address + # - not yet release and blocked + return [ + ("picking_type_id.code", "=", "outgoing"), + ("partner_id", "=", self.partner_shipping_id.id), + ("state", "=", "waiting"), + ("need_release", "=", True), + ("release_blocked", "=", True), + ("unblocked_by_order_id", "!=", self.id), + ] + + @api.depends("order_line.move_ids") + def _compute_available_move_to_unblock_ids(self): + for order in self: + moves = self.env["stock.move"].search( + order._domain_available_move_to_unblock() + ) + self.available_move_to_unblock_ids = moves + self.available_move_to_unblock_count = len(moves) + + @api.depends("move_to_unblock_ids") + def _compute_move_to_unblock_count(self): + for order in self: + order.move_to_unblock_count = len(order.move_to_unblock_ids) + + def action_open_move_need_release(self): + action = super().action_open_move_need_release() + if not action.get("context"): + action["context"] = {} + action["context"].update(from_sale_order_id=self.id) + return action + + def action_open_available_move_to_unblock(self): + self.ensure_one() + if not self.available_move_to_unblock_count: + return + xmlid = "stock_available_to_promise_release.stock_move_release_action" + action = self.env["ir.actions.act_window"]._for_xml_id(xmlid) + action["domain"] = [("id", "in", self.available_move_to_unblock_ids.ids)] + action["context"] = {"from_sale_order_id": self.id} + return action + + def action_open_move_to_unblock(self): + self.ensure_one() + if not self.move_to_unblock_count: + return + xmlid = "stock_available_to_promise_release.stock_move_release_action" + action = self.env["ir.actions.act_window"]._for_xml_id(xmlid) + action["domain"] = [("id", "in", self.move_to_unblock_ids.ids)] + action["context"] = {} + return action + + def action_confirm(self): + # Reschedule the blocked moves when confirming the order + # NOTE: If a module like 'stock_picking_group_by_partner_by_carrier_by_date' + # is installed, these moves + the new ones generated by the current order + # will all be grouped in the same delivery order as soon as they share + # the same grouping criteria (partner, date, carrier...). + for order in self: + if order.move_to_unblock_ids: + date_deadline = order.commitment_date or order.expected_date + self.env["unblock.release"]._reschedule_moves( + order.move_to_unblock_ids, date_deadline, from_order=order + ) + # Unblock the release + if not order.block_release: + order.move_to_unblock_ids.action_unblock_release() + return super().action_confirm() diff --git a/sale_stock_available_to_promise_release_block/models/sale_order_line.py b/sale_stock_available_to_promise_release_block/models/sale_order_line.py index ba6ebbc4349..5a704cde1f3 100644 --- a/sale_stock_available_to_promise_release_block/models/sale_order_line.py +++ b/sale_stock_available_to_promise_release_block/models/sale_order_line.py @@ -18,6 +18,7 @@ class SaleOrderLine(models.Model): compute="_compute_release_blocked_label", ) + @api.depends("move_ids.release_blocked") def _compute_is_release_blocked(self): for rec in self: rec.is_release_blocked = any(rec.move_ids.mapped("release_blocked")) @@ -30,7 +31,9 @@ def _search_blocked_delivery(self, operator, value): @api.depends("is_release_blocked") def _compute_release_blocked_label(self): for rec in self: - rec.release_blocked_label = _("Blocked") if rec.is_release_blocked else "" + rec.release_blocked_label = ( + _("Blocked") if rec.is_release_blocked else False + ) def _prepare_procurement_values(self, group_id=False): vals = super()._prepare_procurement_values(group_id=group_id) diff --git a/sale_stock_available_to_promise_release_block/models/stock_move.py b/sale_stock_available_to_promise_release_block/models/stock_move.py new file mode 100644 index 00000000000..75c88cd45b4 --- /dev/null +++ b/sale_stock_available_to_promise_release_block/models/stock_move.py @@ -0,0 +1,16 @@ +# Copyright 2024 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import fields, models + + +class StockMove(models.Model): + _inherit = "stock.move" + + unblocked_by_order_id = fields.Many2one( + comodel_name="sale.order", + ondelete="set null", + string="Unblocked by order", + readonly=True, + index=True, + ) diff --git a/sale_stock_available_to_promise_release_block/readme/DESCRIPTION.rst b/sale_stock_available_to_promise_release_block/readme/DESCRIPTION.rst index 4ab6b667f2d..0eac4ac4bd8 100644 --- a/sale_stock_available_to_promise_release_block/readme/DESCRIPTION.rst +++ b/sale_stock_available_to_promise_release_block/readme/DESCRIPTION.rst @@ -1 +1,12 @@ -Block release of deliveries from sale orders. +Block and unblock release of deliveries from sale orders. + +Release of deliveries can be blocked right after the sale order confirmation. + +When encoding a new order sharing the same delivery address, the user can +list the existing blocked deliveries (backorders) and plan to unblock them +when this new order is confirmed, making the existing deliveries and the new +ones sharing the same scheduled dates and deadlines. + +As a side-effect, this will leverage the module +`stock_picking_group_by_partner_by_carrier_by_date` if this one is installed, +by grouping all delivery lines within the same delivery order. diff --git a/sale_stock_available_to_promise_release_block/security/ir.model.access.csv b/sale_stock_available_to_promise_release_block/security/ir.model.access.csv new file mode 100644 index 00000000000..e4b605c8252 --- /dev/null +++ b/sale_stock_available_to_promise_release_block/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_unblock_release_sale,access.unblock.release,model_unblock_release,sales_team.group_sale_salesman,1,1,1,0 +access_unblock_release_stock,access.unblock.release,model_unblock_release,stock.group_stock_user,1,1,1,0 diff --git a/sale_stock_available_to_promise_release_block/static/description/index.html b/sale_stock_available_to_promise_release_block/static/description/index.html index a9c5d1ff93c..72f1d0d6aba 100644 --- a/sale_stock_available_to_promise_release_block/static/description/index.html +++ b/sale_stock_available_to_promise_release_block/static/description/index.html @@ -3,15 +3,16 @@ -Stock Available to Promise Release - Block from Sales +README.rst -
-

Stock Available to Promise Release - Block from Sales

+
+ + +Odoo Community Association + +
+

Stock Available to Promise Release - Block from Sales

-

Beta License: AGPL-3 OCA/wms Translate me on Weblate Try me on Runboat

-

Block release of deliveries from sale orders.

+

Beta License: AGPL-3 OCA/wms Translate me on Weblate Try me on Runboat

+

Block and unblock release of deliveries from sale orders.

+

Release of deliveries can be blocked right after the sale order confirmation.

+

When encoding a new order sharing the same delivery address, the user can +list the existing blocked deliveries (backorders) and plan to unblock them +when this new order is confirmed, making the existing deliveries and the new +ones sharing the same scheduled dates and deadlines.

+

As a side-effect, this will leverage the module +stock_picking_group_by_partner_by_carrier_by_date if this one is installed, +by grouping all delivery lines within the same delivery order.

Table of contents

    @@ -384,12 +398,12 @@

    Stock Available to Promise Release - Block from Sales

-

Usage

+

Usage

When the order is confirmed, the release of the generated delivery will be blocked if the option “Block Release” is enabled.

-

Bug Tracker

+

Bug Tracker

Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed @@ -397,9 +411,9 @@

Bug Tracker

Do not contact contributors directly about support or help with technical issues.

-

Credits

+

Credits

-

Authors

+

Authors

  • Camptcamp
  • ACSONE SA/NV
  • @@ -407,7 +421,7 @@

    Authors

-

Contributors

+

Contributors

-

Maintainers

+

Maintainers

This module is maintained by the OCA.

-Odoo Community Association + +Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use.

@@ -428,5 +444,6 @@

Maintainers

+
diff --git a/sale_stock_available_to_promise_release_block/tests/test_sale_block_release.py b/sale_stock_available_to_promise_release_block/tests/test_sale_block_release.py index 1ef44f80fae..c7edb88a538 100644 --- a/sale_stock_available_to_promise_release_block/tests/test_sale_block_release.py +++ b/sale_stock_available_to_promise_release_block/tests/test_sale_block_release.py @@ -1,10 +1,23 @@ # Copyright 2024 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) +from odoo import exceptions, fields +from odoo.tests.common import Form + from odoo.addons.sale_stock_available_to_promise_release.tests import common class TestSaleBlockRelease(common.Common): + @classmethod + def setUpClass(cls): + super().setUpClass() + # Ensure there is no security lead during tests + cls.env.company.security_lead = 0 + # Deliver in two steps to get a SHIP to release + cls.wh = cls.env.ref("stock.warehouse0") + cls.wh.delivery_steps = "pick_ship" + cls.wh.delivery_route_id.available_to_promise_defer_pull = True + def test_sale_release_not_blocked(self): self._set_stock(self.line.product_id, self.line.product_uom_qty) self.assertFalse(self.sale.block_release) @@ -16,3 +29,215 @@ def test_sale_release_blocked(self): self.sale.block_release = True self.sale.action_confirm() self.assertTrue(self.sale.picking_ids.release_blocked) + + def _create_unblock_release_wizard( + self, records=None, date_deadline=None, from_order=None, option="manual" + ): + wiz_form = Form( + self.env["unblock.release"].with_context( + from_sale_order_id=from_order and from_order.id, + active_model=records._name, + active_ids=records.ids, + default_option=option, + ) + ) + if date_deadline: + wiz_form.date_deadline = date_deadline + return wiz_form.save() + + def test_unblock_release_contextual(self): + self._set_stock(self.line.product_id, self.line.product_uom_qty) + self.sale.block_release = True + self.sale.action_confirm() + existing_moves = self.sale.order_line.move_ids + # Unblock deliveries through the wizard, opened from another SO + new_sale = self._create_sale_order() + self.env["sale.order.line"].create( + { + "order_id": new_sale.id, + "product_id": self.product.id, + "product_uom_qty": 50, + "product_uom": self.uom_unit.id, + } + ) + new_sale.commitment_date = fields.Datetime.add(fields.Datetime.now(), days=1) + self.assertIn(existing_moves, new_sale.available_move_to_unblock_ids) + wiz = self._create_unblock_release_wizard( + self.sale.order_line, from_order=new_sale + ) + self.assertEqual(wiz.option, "contextual") + self.assertEqual(wiz.order_id, new_sale) + self.assertEqual(wiz.date_deadline, new_sale.commitment_date) + self.assertNotEqual(wiz.order_line_ids.move_ids.date, new_sale.commitment_date) + old_picking = wiz.order_line_ids.move_ids.picking_id + wiz.validate() + # Deliveries will be unblocked when the new SO is confirmed + self.assertFalse(new_sale.available_move_to_unblock_ids) + self.assertEqual(new_sale.move_to_unblock_ids, existing_moves) + # Confirm the new SO: deliveries have been scheduled to the new date deadline + new_sale.action_confirm() + new_moves = new_sale.order_line.move_ids + new_picking = wiz.order_line_ids.move_ids.picking_id + self.assertNotEqual(old_picking, new_picking) + self.assertFalse(old_picking.exists()) + self.assertTrue( + all( + m.date == m.date_deadline == new_sale.commitment_date + for m in (existing_moves | new_moves) + ) + ) + self.assertTrue( + all(not m.release_blocked for m in (existing_moves | new_moves)) + ) + + def test_unblock_release_contextual_order_not_eligible(self): + self._set_stock(self.line.product_id, self.line.product_uom_qty) + self.sale.block_release = True + self.sale.action_confirm() + # Unblock deliveries through the wizard, opened from another SO + new_sale = self._create_sale_order() + self.env["sale.order.line"].create( + { + "order_id": new_sale.id, + "product_id": self.product.id, + "product_uom_qty": 50, + "product_uom": self.uom_unit.id, + } + ) + new_sale.action_cancel() + wiz = self._create_unblock_release_wizard( + self.sale.order_line, + from_order=new_sale, + date_deadline=fields.Datetime.now(), + ) + self.assertEqual(wiz.option, "manual") + + def test_unblock_release_contextual_different_shipping_policy(self): + self._set_stock(self.line.product_id, self.line.product_uom_qty) + self.sale.block_release = True + self.sale.action_confirm() + existing_moves = self.sale.order_line.move_ids + # Unblock deliveries through the wizard, opened from another SO with a + # different shipping_policy + new_sale = self._create_sale_order() + new_sale.picking_policy = "one" + self.env["sale.order.line"].create( + { + "order_id": new_sale.id, + "product_id": self.product.id, + "product_uom_qty": 50, + "product_uom": self.uom_unit.id, + } + ) + new_sale.commitment_date = fields.Datetime.add(fields.Datetime.now(), days=1) + self.assertIn(existing_moves, new_sale.available_move_to_unblock_ids) + wiz = self._create_unblock_release_wizard( + self.sale.order_line, from_order=new_sale + ) + self.assertEqual(wiz.option, "contextual") + self.assertEqual(wiz.order_id, new_sale) + self.assertEqual(wiz.date_deadline, new_sale.commitment_date) + self.assertNotEqual(wiz.order_line_ids.move_ids.date, new_sale.commitment_date) + self.assertNotEqual( + wiz.order_line_ids.move_ids.group_id.move_type, new_sale.picking_policy + ) + old_picking = wiz.order_line_ids.move_ids.picking_id + wiz.validate() + # Deliveries will be unblocked when the new SO is confirmed + self.assertFalse(new_sale.available_move_to_unblock_ids) + self.assertEqual(new_sale.move_to_unblock_ids, existing_moves) + # Confirm the new SO: deliveries have been scheduled to the new date deadline + # with the same shipping policy + new_sale.action_confirm() + new_moves = new_sale.order_line.move_ids + new_picking = wiz.order_line_ids.move_ids.picking_id + self.assertNotEqual(old_picking, new_picking) + self.assertFalse(old_picking.exists()) + self.assertTrue( + all( + m.date == m.date_deadline == new_sale.commitment_date + for m in (existing_moves | new_moves) + ) + ) + self.assertTrue( + all(not m.release_blocked for m in (existing_moves | new_moves)) + ) + self.assertEqual( + existing_moves.group_id.move_type, new_moves.group_id.move_type + ) + + def test_unblock_release_manual(self): + self._set_stock(self.line.product_id, self.line.product_uom_qty) + self.sale.block_release = True + self.sale.action_confirm() + # Unblock deliveries through the wizard + new_date_deadline = fields.Datetime.add(fields.Datetime.now(), days=1) + wiz = self._create_unblock_release_wizard( + self.sale.order_line, date_deadline=new_date_deadline + ) + self.assertEqual(wiz.option, "manual") + self.assertEqual(wiz.date_deadline, new_date_deadline) + self.assertNotEqual(wiz.order_line_ids.move_ids.date, new_date_deadline) + old_picking = wiz.order_line_ids.move_ids.picking_id + wiz.validate() + # Deliveries have been scheduled to the new date deadline + new_picking = wiz.order_line_ids.move_ids.picking_id + self.assertEqual(wiz.order_line_ids.move_ids.date, new_date_deadline) + self.assertNotEqual(old_picking, new_picking) + self.assertFalse(old_picking.exists()) + + def test_unblock_release_automatic(self): + # Start with a blocked SO having a commitment date in the past + self._set_stock(self.line.product_id, self.line.product_uom_qty) + self.sale.block_release = True + yesterday = fields.Datetime.subtract(fields.Datetime.now(), days=1) + self.sale.commitment_date = yesterday + self.sale.action_confirm() + # Unblock deliveries through the wizard + wiz = self._create_unblock_release_wizard( + self.sale.order_line, option="automatic" + ) + today = wiz.date_deadline + self.assertEqual(wiz.option, "automatic") + self.assertNotEqual(wiz.order_line_ids.move_ids.date, today) + old_picking = wiz.order_line_ids.move_ids.picking_id + wiz.validate() + # Deliveries have been scheduled for today + new_picking = wiz.order_line_ids.move_ids.picking_id + self.assertEqual(wiz.order_line_ids.move_ids.date, today) + self.assertNotEqual(old_picking, new_picking) + self.assertFalse(old_picking.exists()) + + def test_unblock_release_automatic_from_moves(self): + # Same test than above but running the wizard from moves. + # Start with a blocked SO having a commitment date in the past + self._set_stock(self.line.product_id, self.line.product_uom_qty) + self.sale.block_release = True + yesterday = fields.Datetime.subtract(fields.Datetime.now(), days=1) + self.sale.commitment_date = yesterday + self.sale.action_confirm() + # Unblock deliveries through the wizard + wiz = self._create_unblock_release_wizard( + self.sale.order_line.move_ids, option="automatic" + ) + today = wiz.date_deadline + self.assertNotEqual(wiz.move_ids.date, today) + old_picking = wiz.move_ids.picking_id + wiz.validate() + # Deliveries have been scheduled for today + new_picking = wiz.move_ids.picking_id + self.assertEqual(wiz.move_ids.date, today) + self.assertNotEqual(old_picking, new_picking) + self.assertFalse(old_picking.exists()) + + def test_unblock_release_past_date_deadline(self): + self._set_stock(self.line.product_id, self.line.product_uom_qty) + self.sale.block_release = True + self.sale.action_confirm() + # Try to unblock deliveries through the wizard with a scheduled date + # in the past + yesterday = fields.Datetime.subtract(fields.Datetime.now(), days=1) + with self.assertRaises(exceptions.ValidationError): + self._create_unblock_release_wizard( + self.sale.order_line, date_deadline=yesterday + ) diff --git a/sale_stock_available_to_promise_release_block/views/sale_order.xml b/sale_stock_available_to_promise_release_block/views/sale_order.xml index a96eb7c6a1a..9004a000a24 100644 --- a/sale_stock_available_to_promise_release_block/views/sale_order.xml +++ b/sale_stock_available_to_promise_release_block/views/sale_order.xml @@ -8,6 +8,40 @@ sale.order +
+ + +
diff --git a/sale_stock_available_to_promise_release_block/views/sale_order_line.xml b/sale_stock_available_to_promise_release_block/views/sale_order_line.xml index dc5bc18af71..0b018251a49 100644 --- a/sale_stock_available_to_promise_release_block/views/sale_order_line.xml +++ b/sale_stock_available_to_promise_release_block/views/sale_order_line.xml @@ -34,16 +34,13 @@ - + Unblock Release - - + unblock.release + form + new + list - code - - if records: - records.move_ids.action_unblock_release() - diff --git a/sale_stock_available_to_promise_release_block/views/stock_move.xml b/sale_stock_available_to_promise_release_block/views/stock_move.xml new file mode 100644 index 00000000000..0e5214d20ac --- /dev/null +++ b/sale_stock_available_to_promise_release_block/views/stock_move.xml @@ -0,0 +1,34 @@ + + + + + + stock.move + + + + + + + + + + + Unblock Release + unblock.release + form + new + + list + + + diff --git a/sale_stock_available_to_promise_release_block/wizards/__init__.py b/sale_stock_available_to_promise_release_block/wizards/__init__.py new file mode 100644 index 00000000000..77fc1c0e80b --- /dev/null +++ b/sale_stock_available_to_promise_release_block/wizards/__init__.py @@ -0,0 +1 @@ +from . import unblock_release diff --git a/sale_stock_available_to_promise_release_block/wizards/unblock_release.py b/sale_stock_available_to_promise_release_block/wizards/unblock_release.py new file mode 100644 index 00000000000..a1bc7232b21 --- /dev/null +++ b/sale_stock_available_to_promise_release_block/wizards/unblock_release.py @@ -0,0 +1,138 @@ +# Copyright 2024 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import _, api, exceptions, fields, models + + +class UnblockRelease(models.TransientModel): + _name = "unblock.release" + _description = "Unblock Release" + + order_line_ids = fields.Many2many( + comodel_name="sale.order.line", + string="Order Lines", + readonly=True, + ) + move_ids = fields.Many2many( + comodel_name="stock.move", + string="Delivery moves", + readonly=True, + ) + option = fields.Selection( + selection=lambda self: self._selection_option(), + default="automatic", + required=True, + help=( + "- Manual: schedule blocked deliveries at a given date;\n" + "- Automatic: schedule blocked deliveries as soon as possible;\n" + "- Based on current order: schedule blocked deliveries with the " + "contextual sale order." + ), + ) + order_id = fields.Many2one(comodel_name="sale.order", string="Order", readonly=True) + date_deadline = fields.Datetime( + compute="_compute_date_deadline", store=True, readonly=False, required=True + ) + + @api.constrains("date_deadline") + def _constrains_date_deadline(self): + today = fields.Date.today() + for rec in self: + if rec.date_deadline.date() < today: + raise exceptions.ValidationError( + _("You cannot reschedule deliveries in the past.") + ) + + def _get_contextual_order(self): + """Return the current and eligible sale order from the context.""" + from_sale_order_id = self.env.context.get("from_sale_order_id") + order = self.env["sale.order"].browse(from_sale_order_id).exists() + if order and order.state not in ("sale", "done", "cancel"): + return order + + def _selection_option(self): + options = [ + ("manual", "Manual"), + ("automatic", "Automatic / As soon as possible"), + ] + order = self._get_contextual_order() + if order: + options.append(("contextual", "Based on current order")) + return options + + @api.depends("option") + def _compute_date_deadline(self): + from_sale_order_id = self.env.context.get("from_sale_order_id") + order = self.env["sale.order"].browse(from_sale_order_id).exists() + for rec in self: + rec.date_deadline = False + if rec.option == "automatic": + rec.date_deadline = fields.Datetime.now() + elif rec.option == "contextual" and order: + rec.date_deadline = order.commitment_date or order.expected_date + + @api.model + def default_get(self, fields_list): + res = super().default_get(fields_list) + active_model = self.env.context.get("active_model") + active_ids = self.env.context.get("active_ids") + from_sale_order = self._get_contextual_order() + if from_sale_order: + res["order_id"] = from_sale_order.id + if active_model == "sale.order.line" and active_ids: + res["order_line_ids"] = [(6, 0, active_ids)] + if active_model == "stock.move" and active_ids: + res["move_ids"] = [(6, 0, active_ids)] + if from_sale_order: + res["option"] = "contextual" + return res + + def validate(self): + self.ensure_one() + moves = self._filter_moves(self.order_line_ids.move_ids or self.move_ids) + if self.option == "contextual": + self._plan_moves_for_current_order(moves) + else: + self._reschedule_moves(moves, self.date_deadline) + # Unblock release + moves.action_unblock_release() + + def _filter_moves(self, moves): + return moves.filtered_domain( + [("state", "=", "waiting"), ("release_blocked", "=", True)] + ) + + def _plan_moves_for_current_order(self, moves): + """Plan moves to be unblocked when the current order is confirmed.""" + self.order_id.move_to_unblock_ids = moves + + @api.model + def _reschedule_moves(self, moves, date_deadline, from_order=None): + """Reschedule the moves based on the deadline.""" + # Filter out moves that don't need to be released + moves = moves.filtered("need_release") + # Unset current deliveries (keep track of them to delete empty ones at the end) + pickings = moves.picking_id + moves.picking_id = False + # If the rescheduling is triggered from a sale order we set a dedicated + # procurement group on blocked moves. + # This has the side-effect to benefit from other modules like + # 'stock_picking_group_by_partner_by_carrier*' to get existing moves + # and new ones merged together if they share the same criteria + # (picking policy, carrier, scheduled date...). + if from_order: + group = self.env["procurement.group"].create( + fields.first(from_order.order_line)._prepare_procurement_group_vals() + ) + group.name += " BACKORDERS" + moves.group_id = group + # Update the scheduled date and date deadline + date_planned = fields.Datetime.subtract( + date_deadline, days=self.env.company.security_lead + ) + moves.date = date_planned + moves.date_deadline = date_deadline + # Re-assign deliveries + moves._assign_picking() + # Clean up empty deliveries + pickings.filtered(lambda o: not o.move_ids and not o.printed).unlink() diff --git a/sale_stock_available_to_promise_release_block/wizards/unblock_release.xml b/sale_stock_available_to_promise_release_block/wizards/unblock_release.xml new file mode 100644 index 00000000000..38503802e38 --- /dev/null +++ b/sale_stock_available_to_promise_release_block/wizards/unblock_release.xml @@ -0,0 +1,56 @@ + + + + + + unblock.release.form + unblock.release + +
+ + + + + + + + + + + + +
+
+
+
+
+ +
diff --git a/sale_stock_release_channel/README.rst b/sale_stock_release_channel/README.rst new file mode 100644 index 00000000000..f0bd1c0ba91 --- /dev/null +++ b/sale_stock_release_channel/README.rst @@ -0,0 +1,85 @@ +=========================== +Sales Stock Release Channel +=========================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:97ef566cdfcfcdfaeb0e5f518d8619e64beede29f9c4092b8d2e9fad3fe4a444 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fwms-lightgray.png?logo=github + :target: https://github.com/OCA/wms/tree/16.0/sale_stock_release_channel + :alt: OCA/wms +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/wms-16-0/wms-16-0-sale_stock_release_channel + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/wms&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Do not apply extra domains for finding release channel candidates when there is +an SO commitment date. + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* BCIM + +Contributors +~~~~~~~~~~~~ + +* Jacques-Etienne Baudoux (BCIM) + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-jbaudoux| image:: https://github.com/jbaudoux.png?size=40px + :target: https://github.com/jbaudoux + :alt: jbaudoux + +Current `maintainer `__: + +|maintainer-jbaudoux| + +This module is part of the `OCA/wms `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/sale_stock_release_channel/__init__.py b/sale_stock_release_channel/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/sale_stock_release_channel/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/sale_stock_release_channel/__manifest__.py b/sale_stock_release_channel/__manifest__.py new file mode 100644 index 00000000000..7c493fe467c --- /dev/null +++ b/sale_stock_release_channel/__manifest__.py @@ -0,0 +1,16 @@ +# Copyright 2025 Jacques-Etienne Baudoux (BCIM) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Sales Stock Release Channel", + "version": "16.0.1.0.0", + "license": "AGPL-3", + "maintainers": ["jbaudoux"], + "author": "BCIM, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/wms", + "depends": [ + "sale", + "stock_release_channel", + ], + "auto_install": True, +} diff --git a/sale_stock_release_channel/i18n/it.po b/sale_stock_release_channel/i18n/it.po new file mode 100644 index 00000000000..2ed9566beb6 --- /dev/null +++ b/sale_stock_release_channel/i18n/it.po @@ -0,0 +1,27 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * sale_stock_release_channel +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2025-05-29 11:25+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.10.4\n" + +#. module: sale_stock_release_channel +#: model:ir.model,name:sale_stock_release_channel.model_sale_order +msgid "Sales Order" +msgstr "Ordine di vendita" + +#. module: sale_stock_release_channel +#: model:ir.model,name:sale_stock_release_channel.model_stock_picking +msgid "Transfer" +msgstr "Trasferimento" diff --git a/sale_stock_release_channel/i18n/sale_stock_release_channel.pot b/sale_stock_release_channel/i18n/sale_stock_release_channel.pot new file mode 100644 index 00000000000..392cf066540 --- /dev/null +++ b/sale_stock_release_channel/i18n/sale_stock_release_channel.pot @@ -0,0 +1,24 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * sale_stock_release_channel +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: sale_stock_release_channel +#: model:ir.model,name:sale_stock_release_channel.model_sale_order +msgid "Sales Order" +msgstr "" + +#. module: sale_stock_release_channel +#: model:ir.model,name:sale_stock_release_channel.model_stock_picking +msgid "Transfer" +msgstr "" diff --git a/sale_stock_release_channel/models/__init__.py b/sale_stock_release_channel/models/__init__.py new file mode 100644 index 00000000000..c0b4257cf63 --- /dev/null +++ b/sale_stock_release_channel/models/__init__.py @@ -0,0 +1,2 @@ +from . import sale_order +from . import stock_picking diff --git a/sale_stock_release_channel/models/sale_order.py b/sale_stock_release_channel/models/sale_order.py new file mode 100644 index 00000000000..031544e2482 --- /dev/null +++ b/sale_stock_release_channel/models/sale_order.py @@ -0,0 +1,24 @@ +# Copyright 2025 Jacques-Etienne Baudoux (BCIM) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import models + + +class SaleOrder(models.Model): + + _inherit = "sale.order" + + @property + def _release_channel_possible_candidate_domain_base(self): + """Base domain for finding channel candidates based on picking. + + Mimick the domain defined on stock.picking + Do not check company, only warehouse + """ + # Nice to have: also check picking types contain one of the outgoing + # picking types of the warehouse. For now, we rely on the warehouse + # properly filled in in case of multi-warehouse setup. This is + # reasonable. + return [ + ("warehouse_id", "in", (False, self.warehouse_id.id)), + ] diff --git a/sale_stock_release_channel/models/stock_picking.py b/sale_stock_release_channel/models/stock_picking.py new file mode 100644 index 00000000000..febcec7bbdd --- /dev/null +++ b/sale_stock_release_channel/models/stock_picking.py @@ -0,0 +1,16 @@ +# Copyright 2025 Jacques-Etienne Baudoux (BCIM) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import models + + +class StockPicking(models.Model): + + _inherit = "stock.picking" + + @property + def _release_channel_possible_candidate_domain_apply_extras(self): + """Do not apply extra domains when the delivery date is forced on the SO""" + if self.sale_id.commitment_date: + return False + return super()._release_channel_possible_candidate_domain_apply_extras diff --git a/sale_stock_release_channel/readme/CONTRIBUTORS.rst b/sale_stock_release_channel/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000000..3c6c5c696a8 --- /dev/null +++ b/sale_stock_release_channel/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Jacques-Etienne Baudoux (BCIM) diff --git a/sale_stock_release_channel/readme/DESCRIPTION.rst b/sale_stock_release_channel/readme/DESCRIPTION.rst new file mode 100644 index 00000000000..3ee583f0f50 --- /dev/null +++ b/sale_stock_release_channel/readme/DESCRIPTION.rst @@ -0,0 +1,2 @@ +Do not apply extra domains for finding release channel candidates when there is +an SO commitment date. diff --git a/sale_stock_release_channel/static/description/icon.png b/sale_stock_release_channel/static/description/icon.png new file mode 100644 index 00000000000..1dcc49c24f3 Binary files /dev/null and b/sale_stock_release_channel/static/description/icon.png differ diff --git a/sale_stock_release_channel/static/description/index.html b/sale_stock_release_channel/static/description/index.html new file mode 100644 index 00000000000..f1a14134704 --- /dev/null +++ b/sale_stock_release_channel/static/description/index.html @@ -0,0 +1,426 @@ + + + + + +Sales Stock Release Channel + + + +
+

Sales Stock Release Channel

+ + +

Beta License: AGPL-3 OCA/wms Translate me on Weblate Try me on Runboat

+

Do not apply extra domains for finding release channel candidates when there is +an SO commitment date.

+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • BCIM
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainer:

+

jbaudoux

+

This module is part of the OCA/wms project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/sale_stock_release_channel/tests/__init__.py b/sale_stock_release_channel/tests/__init__.py new file mode 100644 index 00000000000..53d40210de1 --- /dev/null +++ b/sale_stock_release_channel/tests/__init__.py @@ -0,0 +1 @@ +from . import test_release_channel diff --git a/sale_stock_release_channel/tests/common.py b/sale_stock_release_channel/tests/common.py new file mode 100644 index 00000000000..d488274fedf --- /dev/null +++ b/sale_stock_release_channel/tests/common.py @@ -0,0 +1,14 @@ +# Copyright 2025 Jacques-Etienne Baudoux (BCIM) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo.tests.common import Form, TransactionCase + + +class SaleStockReleaseChannelCommon(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.customer = cls.env.ref("base.res_partner_1") + sale_form = Form(cls.env["sale.order"]) + sale_form.partner_id = cls.customer + cls.so = sale_form.save() diff --git a/sale_stock_release_channel/tests/test_release_channel.py b/sale_stock_release_channel/tests/test_release_channel.py new file mode 100644 index 00000000000..63b88cc710e --- /dev/null +++ b/sale_stock_release_channel/tests/test_release_channel.py @@ -0,0 +1,11 @@ +# Copyright 2025 Jacques-Etienne Baudoux (BCIM) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from .common import SaleStockReleaseChannelCommon + + +class TestSaleStockReleaseChannel(SaleStockReleaseChannelCommon): + def test_candidate_domain(self): + """Test domain returns a value""" + domain = self.so._release_channel_possible_candidate_domain_base + self.assertIsInstance(domain, list) diff --git a/sale_stock_release_channel_delivery/README.rst b/sale_stock_release_channel_delivery/README.rst new file mode 100644 index 00000000000..43d5cf2e24a --- /dev/null +++ b/sale_stock_release_channel_delivery/README.rst @@ -0,0 +1,85 @@ +==================================== +Sales Stock Release Channel Delivery +==================================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:548008b0efe45bac1cb9144d5bce8a3dfbd3d37fc8e3766c08c2bd27012437b4 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fwms-lightgray.png?logo=github + :target: https://github.com/OCA/wms/tree/16.0/sale_stock_release_channel_delivery + :alt: OCA/wms +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/wms-16-0/wms-16-0-sale_stock_release_channel_delivery + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/wms&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Do not apply extra domains for finding release channel candidates when there is +an SO commitment date. + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* BCIM + +Contributors +~~~~~~~~~~~~ + +* Jacques-Etienne Baudoux (BCIM) + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-jbaudoux| image:: https://github.com/jbaudoux.png?size=40px + :target: https://github.com/jbaudoux + :alt: jbaudoux + +Current `maintainer `__: + +|maintainer-jbaudoux| + +This module is part of the `OCA/wms `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/sale_stock_release_channel_delivery/__init__.py b/sale_stock_release_channel_delivery/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/sale_stock_release_channel_delivery/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/sale_stock_release_channel_delivery/__manifest__.py b/sale_stock_release_channel_delivery/__manifest__.py new file mode 100644 index 00000000000..40ca88ec676 --- /dev/null +++ b/sale_stock_release_channel_delivery/__manifest__.py @@ -0,0 +1,16 @@ +# Copyright 2025 Jacques-Etienne Baudoux (BCIM) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Sales Stock Release Channel Delivery", + "version": "16.0.1.0.0", + "license": "AGPL-3", + "maintainers": ["jbaudoux"], + "author": "BCIM, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/wms", + "depends": [ + "sale_stock_release_channel", + "stock_release_channel_delivery", + ], + "auto_install": True, +} diff --git a/sale_stock_release_channel_delivery/i18n/it.po b/sale_stock_release_channel_delivery/i18n/it.po new file mode 100644 index 00000000000..9f3e1f0469f --- /dev/null +++ b/sale_stock_release_channel_delivery/i18n/it.po @@ -0,0 +1,22 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * sale_stock_release_channel_delivery +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2025-05-29 11:25+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.10.4\n" + +#. module: sale_stock_release_channel_delivery +#: model:ir.model,name:sale_stock_release_channel_delivery.model_sale_order +msgid "Sales Order" +msgstr "Ordine di vendita" diff --git a/sale_stock_release_channel_delivery/i18n/sale_stock_release_channel_delivery.pot b/sale_stock_release_channel_delivery/i18n/sale_stock_release_channel_delivery.pot new file mode 100644 index 00000000000..4cb90004798 --- /dev/null +++ b/sale_stock_release_channel_delivery/i18n/sale_stock_release_channel_delivery.pot @@ -0,0 +1,19 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * sale_stock_release_channel_delivery +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: sale_stock_release_channel_delivery +#: model:ir.model,name:sale_stock_release_channel_delivery.model_sale_order +msgid "Sales Order" +msgstr "" diff --git a/sale_stock_release_channel_delivery/models/__init__.py b/sale_stock_release_channel_delivery/models/__init__.py new file mode 100644 index 00000000000..6aacb753131 --- /dev/null +++ b/sale_stock_release_channel_delivery/models/__init__.py @@ -0,0 +1 @@ +from . import sale_order diff --git a/sale_stock_release_channel_delivery/models/sale_order.py b/sale_stock_release_channel_delivery/models/sale_order.py new file mode 100644 index 00000000000..426cdce35bd --- /dev/null +++ b/sale_stock_release_channel_delivery/models/sale_order.py @@ -0,0 +1,31 @@ +# Copyright 2025 Jacques-Etienne Baudoux (BCIM) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import models +from odoo.osv import expression + + +class SaleOrder(models.Model): + + _inherit = "sale.order" + + @property + def _release_channel_carrier_id(self): + """Return the SO carrier or planned carrier at confirm""" + return self.carrier_id + + @property + def _release_channel_possible_candidate_domain_base(self): + # Mimick the domain extension defined on stock.picking in + # stock_release_channel_delivery + domain = super()._release_channel_possible_candidate_domain_base + if self._release_channel_carrier_id: + domain_carrier = [ + "|", + ("carrier_ids", "=", False), + ("carrier_ids", "in", self._release_channel_carrier_id.id), + ] + else: + domain_carrier = [("carrier_ids", "=", False)] + domain = expression.AND([domain, domain_carrier]) + return domain diff --git a/sale_stock_release_channel_delivery/readme/CONTRIBUTORS.rst b/sale_stock_release_channel_delivery/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000000..3c6c5c696a8 --- /dev/null +++ b/sale_stock_release_channel_delivery/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Jacques-Etienne Baudoux (BCIM) diff --git a/sale_stock_release_channel_delivery/readme/DESCRIPTION.rst b/sale_stock_release_channel_delivery/readme/DESCRIPTION.rst new file mode 100644 index 00000000000..3ee583f0f50 --- /dev/null +++ b/sale_stock_release_channel_delivery/readme/DESCRIPTION.rst @@ -0,0 +1,2 @@ +Do not apply extra domains for finding release channel candidates when there is +an SO commitment date. diff --git a/sale_stock_release_channel_delivery/static/description/icon.png b/sale_stock_release_channel_delivery/static/description/icon.png new file mode 100644 index 00000000000..1dcc49c24f3 Binary files /dev/null and b/sale_stock_release_channel_delivery/static/description/icon.png differ diff --git a/sale_stock_release_channel_delivery/static/description/index.html b/sale_stock_release_channel_delivery/static/description/index.html new file mode 100644 index 00000000000..78c63dd698a --- /dev/null +++ b/sale_stock_release_channel_delivery/static/description/index.html @@ -0,0 +1,426 @@ + + + + + +Sales Stock Release Channel Delivery + + + +
+

Sales Stock Release Channel Delivery

+ + +

Beta License: AGPL-3 OCA/wms Translate me on Weblate Try me on Runboat

+

Do not apply extra domains for finding release channel candidates when there is +an SO commitment date.

+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • BCIM
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainer:

+

jbaudoux

+

This module is part of the OCA/wms project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/sale_stock_release_channel_delivery/tests/__init__.py b/sale_stock_release_channel_delivery/tests/__init__.py new file mode 100644 index 00000000000..53d40210de1 --- /dev/null +++ b/sale_stock_release_channel_delivery/tests/__init__.py @@ -0,0 +1 @@ +from . import test_release_channel diff --git a/sale_stock_release_channel_delivery/tests/test_release_channel.py b/sale_stock_release_channel_delivery/tests/test_release_channel.py new file mode 100644 index 00000000000..a1ab9632508 --- /dev/null +++ b/sale_stock_release_channel_delivery/tests/test_release_channel.py @@ -0,0 +1,13 @@ +# Copyright 2025 Jacques-Etienne Baudoux (BCIM) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo.addons.sale_stock_release_channel.tests.common import ( + SaleStockReleaseChannelCommon, +) + + +class TestSaleStockReleaseChannel(SaleStockReleaseChannelCommon): + def test_candidate_domain(self): + """Test domain returns a value""" + domain = self.so._release_channel_possible_candidate_domain_base + self.assertIsInstance(domain, list) diff --git a/sale_stock_release_channel_delivery_date/README.rst b/sale_stock_release_channel_delivery_date/README.rst new file mode 100644 index 00000000000..d0f2a52408f --- /dev/null +++ b/sale_stock_release_channel_delivery_date/README.rst @@ -0,0 +1,91 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +======================================== +Sale Stock Release Channel Delivery Date +======================================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:2ae76b0bacde574bbfe8ddaa4d7f67bc83790798b2b7e5362c01e94440c5be25 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fwms-lightgray.png?logo=github + :target: https://github.com/OCA/wms/tree/16.0/sale_stock_release_channel_delivery_date + :alt: OCA/wms +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/wms-16-0/wms-16-0-sale_stock_release_channel_delivery_date + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/wms&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Provide the SO expected date according to possible channels respecting SO +warehouse, carrier and shipping partner. + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* BCIM +* Camptocamp + +Contributors +~~~~~~~~~~~~ + +* Jacques-Etienne Baudoux (BCIM) +* Akim Juillerat + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-jbaudoux| image:: https://github.com/jbaudoux.png?size=40px + :target: https://github.com/jbaudoux + :alt: jbaudoux + +Current `maintainer `__: + +|maintainer-jbaudoux| + +This module is part of the `OCA/wms `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/sale_stock_release_channel_delivery_date/__init__.py b/sale_stock_release_channel_delivery_date/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/sale_stock_release_channel_delivery_date/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/sale_stock_release_channel_delivery_date/__manifest__.py b/sale_stock_release_channel_delivery_date/__manifest__.py new file mode 100644 index 00000000000..538a159eda7 --- /dev/null +++ b/sale_stock_release_channel_delivery_date/__manifest__.py @@ -0,0 +1,16 @@ +# Copyright 2025 Jacques-Etienne Baudoux (BCIM) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Sale Stock Release Channel Delivery Date", + "summary": """ + Compute expected date based on available release channels """, + "version": "16.0.1.1.2", + "license": "AGPL-3", + "author": "BCIM,Camptocamp,Odoo Community Association (OCA)", + "maintainers": ["jbaudoux"], + "website": "https://github.com/OCA/wms", + "depends": [ + "sale_stock_release_channel_delivery", + ], +} diff --git a/sale_stock_release_channel_delivery_date/i18n/it.po b/sale_stock_release_channel_delivery_date/i18n/it.po new file mode 100644 index 00000000000..767caf7b4ae --- /dev/null +++ b/sale_stock_release_channel_delivery_date/i18n/it.po @@ -0,0 +1,27 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * sale_stock_release_channel_delivery_date +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2025-06-04 10:26+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.10.4\n" + +#. module: sale_stock_release_channel_delivery_date +#: model:ir.model,name:sale_stock_release_channel_delivery_date.model_sale_order +msgid "Sales Order" +msgstr "Ordine di vendita" + +#. module: sale_stock_release_channel_delivery_date +#: model:ir.model,name:sale_stock_release_channel_delivery_date.model_sale_order_line +msgid "Sales Order Line" +msgstr "Riga ordine di vendita" diff --git a/sale_stock_release_channel_delivery_date/i18n/sale_stock_release_channel_delivery_date.pot b/sale_stock_release_channel_delivery_date/i18n/sale_stock_release_channel_delivery_date.pot new file mode 100644 index 00000000000..07bd8148357 --- /dev/null +++ b/sale_stock_release_channel_delivery_date/i18n/sale_stock_release_channel_delivery_date.pot @@ -0,0 +1,24 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * sale_stock_release_channel_delivery_date +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: sale_stock_release_channel_delivery_date +#: model:ir.model,name:sale_stock_release_channel_delivery_date.model_sale_order +msgid "Sales Order" +msgstr "" + +#. module: sale_stock_release_channel_delivery_date +#: model:ir.model,name:sale_stock_release_channel_delivery_date.model_sale_order_line +msgid "Sales Order Line" +msgstr "" diff --git a/sale_stock_release_channel_delivery_date/models/__init__.py b/sale_stock_release_channel_delivery_date/models/__init__.py new file mode 100644 index 00000000000..e7e9273fcff --- /dev/null +++ b/sale_stock_release_channel_delivery_date/models/__init__.py @@ -0,0 +1,2 @@ +from . import sale_order_line +from . import sale_order diff --git a/sale_stock_release_channel_delivery_date/models/sale_order.py b/sale_stock_release_channel_delivery_date/models/sale_order.py new file mode 100644 index 00000000000..159299d16d8 --- /dev/null +++ b/sale_stock_release_channel_delivery_date/models/sale_order.py @@ -0,0 +1,84 @@ +# Copyright 2024 Camptocamp SA +# Copyright 2024 Jacques-Etienne Baudoux (BCIM) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +import logging + +from odoo import api, fields, models +from odoo.osv import expression +from odoo.tools import ormcache + +_logger = logging.getLogger(__name__) + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + @api.depends( + "order_line.customer_lead", + "date_order", + "partner_shipping_id", + "carrier_id", + "warehouse_id", + "picking_policy", + ) + def _compute_expected_date(self): + res = super()._compute_expected_date() + for order in self: + if order.order_line: + # will be managed at line level + continue + if order.state in ["sale", "done"] and order.date_order: + order_dt = order.date_order + else: + order_dt = fields.Datetime.now() + expected_date = order._get_release_channel_expected_date(order_dt) + if expected_date: + order.expected_date = expected_date + return res + + def _get_release_channel_expected_date(self, order_dt): + self.ensure_one() + if not self.partner_shipping_id: + # do not compute + return + # If the carrier is not set, assume the partner default carrier + carrier = ( + self.carrier_id or self.partner_shipping_id.property_delivery_carrier_id + ) + # We don't need that precision for the computation & cache + order_dt = order_dt.replace(second=0, microsecond=0) + expected_dt = self._cached_release_channel_expected_date(carrier, order_dt) + return expected_dt + + @ormcache( + "self.company_id.id", + "self.partner_shipping_id.id", + "self.warehouse_id.id", + "carrier.id", + "order_dt", + ) + def _cached_release_channel_expected_date(self, carrier, order_dt): + self.ensure_one() + _logger.debug(f"Computing expected date for {self} starting from {order_dt}") + + channels = self._get_partner_release_channels(carrier) + if not channels: + return False + dates = [ + channel._get_earliest_delivery_date(self.partner_shipping_id, order_dt) + for channel in channels + ] + return min(dates) + + @api.model + def _get_partner_release_channels(self, carrier): + domain_order = self._release_channel_possible_candidate_domain_base + domain_partner = ( + self.partner_shipping_id._release_channel_possible_candidate_domain + if self.partner_shipping_id + else [] + ) + domain_channel = [("is_manual_assignment", "=", False)] + domain = expression.AND([domain_order, domain_partner, domain_channel]) + return self.env["stock.release.channel"].search(domain) diff --git a/sale_stock_release_channel_delivery_date/models/sale_order_line.py b/sale_stock_release_channel_delivery_date/models/sale_order_line.py new file mode 100644 index 00000000000..6a5af641fb5 --- /dev/null +++ b/sale_stock_release_channel_delivery_date/models/sale_order_line.py @@ -0,0 +1,17 @@ +# Copyright 2025 Jacques-Etienne Baudoux (BCIM) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +from odoo import models + + +class SaleOrderLine(models.Model): + _inherit = "sale.order.line" + + def _expected_date(self): + expected_dt = super()._expected_date() + if self.product_id.type in ("product", "consu"): + # we keep the added customer lead, it is the computation start dt + channel_dt = self.order_id._get_release_channel_expected_date(expected_dt) + if channel_dt: + expected_dt = channel_dt + return expected_dt diff --git a/sale_stock_release_channel_delivery_date/readme/CONTRIBUTORS.rst b/sale_stock_release_channel_delivery_date/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000000..9c640f6e4f8 --- /dev/null +++ b/sale_stock_release_channel_delivery_date/readme/CONTRIBUTORS.rst @@ -0,0 +1,2 @@ +* Jacques-Etienne Baudoux (BCIM) +* Akim Juillerat diff --git a/sale_stock_release_channel_delivery_date/readme/DESCRIPTION.rst b/sale_stock_release_channel_delivery_date/readme/DESCRIPTION.rst new file mode 100644 index 00000000000..5b800306e09 --- /dev/null +++ b/sale_stock_release_channel_delivery_date/readme/DESCRIPTION.rst @@ -0,0 +1,2 @@ +Provide the SO expected date according to possible channels respecting SO +warehouse, carrier and shipping partner. diff --git a/sale_stock_release_channel_delivery_date/static/description/icon.png b/sale_stock_release_channel_delivery_date/static/description/icon.png new file mode 100644 index 00000000000..1dcc49c24f3 Binary files /dev/null and b/sale_stock_release_channel_delivery_date/static/description/icon.png differ diff --git a/sale_stock_release_channel_delivery_date/static/description/index.html b/sale_stock_release_channel_delivery_date/static/description/index.html new file mode 100644 index 00000000000..05eeab39651 --- /dev/null +++ b/sale_stock_release_channel_delivery_date/static/description/index.html @@ -0,0 +1,434 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

Sale Stock Release Channel Delivery Date

+ +

Beta License: AGPL-3 OCA/wms Translate me on Weblate Try me on Runboat

+

Provide the SO expected date according to possible channels respecting SO +warehouse, carrier and shipping partner.

+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • BCIM
  • +
  • Camptocamp
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainer:

+

jbaudoux

+

This module is part of the OCA/wms project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+
+ + diff --git a/sale_stock_release_channel_delivery_date/tests/__init__.py b/sale_stock_release_channel_delivery_date/tests/__init__.py new file mode 100644 index 00000000000..19d3fb4a8a3 --- /dev/null +++ b/sale_stock_release_channel_delivery_date/tests/__init__.py @@ -0,0 +1 @@ +from . import test_delivery_date diff --git a/sale_stock_release_channel_delivery_date/tests/test_delivery_date.py b/sale_stock_release_channel_delivery_date/tests/test_delivery_date.py new file mode 100644 index 00000000000..836821b77af --- /dev/null +++ b/sale_stock_release_channel_delivery_date/tests/test_delivery_date.py @@ -0,0 +1,106 @@ +# Copyright 2025 Jacques-Etienne Baudoux (BCIM) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +from datetime import timedelta + +from freezegun import freeze_time + +from odoo import fields +from odoo.fields import Command + +from odoo.addons.sale.tests.common import SaleCommon +from odoo.addons.stock_release_channel.tests.common import ( # noqa + StockReleaseChannelDeliveryDateCommon, +) + + +class TestSaleStockReleaseChannelDeliveryDate( + StockReleaseChannelDeliveryDateCommon, SaleCommon +): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.warehouse = cls.env.ref("stock.warehouse0") + cls.channel.warehouse_id = cls.warehouse + cls.channel.is_manual_assignment = False + + @freeze_time("2025-01-02 10:00:00") + def test_empty(self): + """Test empty SO + + Expected date is computed as if there will be stock lines""" + so = self.empty_order + dt = fields.Datetime.now() + timedelta(days=2) + # the order was created in setup outside freezegun + so.invalidate_recordset(["expected_date"]) + self.assertEqual(so.expected_date, dt) + + @freeze_time("2025-01-02 10:01:00") + def test_service(self): + """Test SO with service""" + so = self.env["sale.order"].create( + { + "partner_id": self.partner.id, + "order_line": [ + Command.create( + { + "product_id": self.service_product.id, + "product_uom_qty": 22, + } + ) + ], + } + ) + dt = fields.Datetime.now() + # the order was created in setup outside freezegun + so.invalidate_recordset(["expected_date"]) + self.assertEqual(so.expected_date, dt) + + @freeze_time("2025-01-02 10:02:00") + def test_product(self): + """Test SO with 2 consumables""" + so = self.env["sale.order"].create( + { + "partner_id": self.partner.id, + "order_line": [ + Command.create( + { + "product_id": self.consumable_product.id, + "product_uom_qty": 22, + } + ), + Command.create( + { + "product_id": self.consumable_product.id, + "product_uom_qty": 22, + } + ), + ], + } + ) + dt = fields.Datetime.now() + timedelta(days=2) + # the order was created in setup outside freezegun + so.invalidate_recordset(["expected_date"]) + self.assertEqual(so.expected_date, dt) + + @freeze_time("2025-01-02 10:03:00") + def test_product_customer_lead(self): + """Test SO with a customer lead time""" + so = self.env["sale.order"].create( + { + "partner_id": self.partner.id, + "order_line": [ + Command.create( + { + "product_id": self.consumable_product.id, + "product_uom_qty": 22, + "customer_lead": 5, + } + ) + ], + } + ) + dt = fields.Datetime.now() + timedelta(days=7) + # the order was created in setup outside freezegun + so.invalidate_recordset(["expected_date"]) + self.assertEqual(so.expected_date, dt) diff --git a/sale_stock_release_channel_partner_by_date_delivery/README.rst b/sale_stock_release_channel_partner_by_date_delivery/README.rst index 5a679ed68b8..dfcad771fac 100644 --- a/sale_stock_release_channel_partner_by_date_delivery/README.rst +++ b/sale_stock_release_channel_partner_by_date_delivery/README.rst @@ -7,7 +7,7 @@ Stock Release Channels with Sales - Delivery !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:45392975595cc6fc0286ea148c65402138e33ef305029439bdc7f2188b010083 + !! source digest: sha256:28e5a0fc4629136b4abefda9785a261bce11f7ae5713deb93f6eaeeda5116b0c !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png diff --git a/sale_stock_release_channel_partner_by_date_delivery/__manifest__.py b/sale_stock_release_channel_partner_by_date_delivery/__manifest__.py index e6c320e53c7..82c493603b7 100644 --- a/sale_stock_release_channel_partner_by_date_delivery/__manifest__.py +++ b/sale_stock_release_channel_partner_by_date_delivery/__manifest__.py @@ -3,7 +3,7 @@ { "name": "Stock Release Channels with Sales - Delivery", "summary": "Filters channels on sales based on selected carrier.", - "version": "16.0.1.1.0", + "version": "16.0.1.1.1", "development_status": "Beta", "license": "AGPL-3", "author": "Camptocamp, BCIM, Odoo Community Association (OCA)", diff --git a/sale_stock_release_channel_partner_by_date_delivery/static/description/index.html b/sale_stock_release_channel_partner_by_date_delivery/static/description/index.html index 2cc85316732..962e6ca18d6 100644 --- a/sale_stock_release_channel_partner_by_date_delivery/static/description/index.html +++ b/sale_stock_release_channel_partner_by_date_delivery/static/description/index.html @@ -367,7 +367,7 @@

Stock Release Channels with Sales - Delivery

!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:45392975595cc6fc0286ea148c65402138e33ef305029439bdc7f2188b010083 +!! source digest: sha256:28e5a0fc4629136b4abefda9785a261bce11f7ae5713deb93f6eaeeda5116b0c !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

Beta License: AGPL-3 OCA/wms Translate me on Weblate Try me on Runboat

This addon restricts the available release channels to set on a sale order based diff --git a/sale_stock_release_channel_partner_by_date_delivery/tests/test_sale_release_channel.py b/sale_stock_release_channel_partner_by_date_delivery/tests/test_sale_release_channel.py index f990010facd..0170124a500 100644 --- a/sale_stock_release_channel_partner_by_date_delivery/tests/test_sale_release_channel.py +++ b/sale_stock_release_channel_partner_by_date_delivery/tests/test_sale_release_channel.py @@ -12,12 +12,14 @@ class TestSaleReleaseChannel(SaleReleaseChannelCase): @classmethod def setUpClass(cls): super().setUpClass() + cls.wh = cls.env.ref("stock.warehouse0") cls.carrier = cls.env.ref("delivery.delivery_carrier") cls.carrier2 = cls.env.ref("delivery.delivery_local_delivery") cls.carrier_channel = cls.default_channel.copy( { "name": "Test with carrier", "sequence": 10, + "warehouse_id": cls.wh.id, "carrier_ids": [(6, 0, cls.carrier.ids)], } ) diff --git a/setup/_metapackage/VERSION.txt b/setup/_metapackage/VERSION.txt index 6c7082808aa..431a9eeea71 100644 --- a/setup/_metapackage/VERSION.txt +++ b/setup/_metapackage/VERSION.txt @@ -1 +1 @@ -16.0.20250423.0 \ No newline at end of file +16.0.20250707.0 \ No newline at end of file diff --git a/setup/_metapackage/setup.py b/setup/_metapackage/setup.py index 4a8e3c0d4e1..8e2d79ab95b 100644 --- a/setup/_metapackage/setup.py +++ b/setup/_metapackage/setup.py @@ -11,6 +11,9 @@ 'odoo-addon-delivery_carrier_warehouse>=16.0dev,<16.1dev', 'odoo-addon-sale_stock_available_to_promise_release>=16.0dev,<16.1dev', 'odoo-addon-sale_stock_available_to_promise_release_block>=16.0dev,<16.1dev', + 'odoo-addon-sale_stock_release_channel>=16.0dev,<16.1dev', + 'odoo-addon-sale_stock_release_channel_delivery>=16.0dev,<16.1dev', + 'odoo-addon-sale_stock_release_channel_delivery_date>=16.0dev,<16.1dev', 'odoo-addon-sale_stock_release_channel_partner_by_date>=16.0dev,<16.1dev', 'odoo-addon-sale_stock_release_channel_partner_by_date_delivery>=16.0dev,<16.1dev', 'odoo-addon-shopfloor>=16.0dev,<16.1dev', @@ -21,13 +24,16 @@ 'odoo-addon-shopfloor_mobile_base_auth_api_key>=16.0dev,<16.1dev', 'odoo-addon-shopfloor_reception>=16.0dev,<16.1dev', 'odoo-addon-shopfloor_reception_mobile>=16.0dev,<16.1dev', + 'odoo-addon-shopfloor_reception_refund_return>=16.0dev,<16.1dev', 'odoo-addon-shopfloor_rest_log>=16.0dev,<16.1dev', 'odoo-addon-shopfloor_workstation>=16.0dev,<16.1dev', 'odoo-addon-shopfloor_workstation_mobile>=16.0dev,<16.1dev', 'odoo-addon-stock_available_to_promise_release>=16.0dev,<16.1dev', 'odoo-addon-stock_available_to_promise_release_block>=16.0dev,<16.1dev', + 'odoo-addon-stock_available_to_promise_release_dynamic_routing>=16.0dev,<16.1dev', 'odoo-addon-stock_available_to_promise_release_exclude_location>=16.0dev,<16.1dev', 'odoo-addon-stock_dynamic_routing>=16.0dev,<16.1dev', + 'odoo-addon-stock_full_location_reservation>=16.0dev,<16.1dev', 'odoo-addon-stock_picking_batch_creation>=16.0dev,<16.1dev', 'odoo-addon-stock_picking_completion_info>=16.0dev,<16.1dev', 'odoo-addon-stock_picking_type_shipping_policy>=16.0dev,<16.1dev', @@ -36,11 +42,15 @@ 'odoo-addon-stock_release_channel_batch_mode_commercial_partner>=16.0dev,<16.1dev', 'odoo-addon-stock_release_channel_cutoff>=16.0dev,<16.1dev', 'odoo-addon-stock_release_channel_delivery>=16.0dev,<16.1dev', + 'odoo-addon-stock_release_channel_depot>=16.0dev,<16.1dev', 'odoo-addon-stock_release_channel_geoengine>=16.0dev,<16.1dev', 'odoo-addon-stock_release_channel_partner_by_date>=16.0dev,<16.1dev', + 'odoo-addon-stock_release_channel_partner_by_date_delivery_window>=16.0dev,<16.1dev', + 'odoo-addon-stock_release_channel_partner_by_date_public_holidays>=16.0dev,<16.1dev', 'odoo-addon-stock_release_channel_partner_delivery_window>=16.0dev,<16.1dev', 'odoo-addon-stock_release_channel_partner_public_holidays>=16.0dev,<16.1dev', 'odoo-addon-stock_release_channel_plan>=16.0dev,<16.1dev', + 'odoo-addon-stock_release_channel_plan_depot>=16.0dev,<16.1dev', 'odoo-addon-stock_release_channel_plan_process_end_time>=16.0dev,<16.1dev', 'odoo-addon-stock_release_channel_plan_shipment_lead_time>=16.0dev,<16.1dev', 'odoo-addon-stock_release_channel_process_end_time>=16.0dev,<16.1dev', @@ -52,9 +62,11 @@ 'odoo-addon-stock_release_channel_shipment_lead_time>=16.0dev,<16.1dev', 'odoo-addon-stock_release_channel_show_volume>=16.0dev,<16.1dev', 'odoo-addon-stock_release_channel_show_weight>=16.0dev,<16.1dev', + 'odoo-addon-stock_release_channel_warehouse_calendar>=16.0dev,<16.1dev', 'odoo-addon-stock_storage_type>=16.0dev,<16.1dev', 'odoo-addon-stock_storage_type_putaway_abc>=16.0dev,<16.1dev', 'odoo-addon-stock_warehouse_flow>=16.0dev,<16.1dev', + 'odoo-addon-stock_warehouse_flow_delivery_refresh>=16.0dev,<16.1dev', 'odoo-addon-stock_warehouse_flow_release>=16.0dev,<16.1dev', ], classifiers=[ diff --git a/setup/sale_stock_release_channel/odoo/addons/sale_stock_release_channel b/setup/sale_stock_release_channel/odoo/addons/sale_stock_release_channel new file mode 120000 index 00000000000..4ee365a5345 --- /dev/null +++ b/setup/sale_stock_release_channel/odoo/addons/sale_stock_release_channel @@ -0,0 +1 @@ +../../../../sale_stock_release_channel \ No newline at end of file diff --git a/setup/sale_stock_release_channel/setup.py b/setup/sale_stock_release_channel/setup.py new file mode 100644 index 00000000000..28c57bb6403 --- /dev/null +++ b/setup/sale_stock_release_channel/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/sale_stock_release_channel_delivery/odoo/addons/sale_stock_release_channel_delivery b/setup/sale_stock_release_channel_delivery/odoo/addons/sale_stock_release_channel_delivery new file mode 120000 index 00000000000..59d2936211d --- /dev/null +++ b/setup/sale_stock_release_channel_delivery/odoo/addons/sale_stock_release_channel_delivery @@ -0,0 +1 @@ +../../../../sale_stock_release_channel_delivery \ No newline at end of file diff --git a/setup/sale_stock_release_channel_delivery/setup.py b/setup/sale_stock_release_channel_delivery/setup.py new file mode 100644 index 00000000000..28c57bb6403 --- /dev/null +++ b/setup/sale_stock_release_channel_delivery/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/sale_stock_release_channel_delivery_date/odoo/addons/sale_stock_release_channel_delivery_date b/setup/sale_stock_release_channel_delivery_date/odoo/addons/sale_stock_release_channel_delivery_date new file mode 120000 index 00000000000..1e69c8df826 --- /dev/null +++ b/setup/sale_stock_release_channel_delivery_date/odoo/addons/sale_stock_release_channel_delivery_date @@ -0,0 +1 @@ +../../../../sale_stock_release_channel_delivery_date \ No newline at end of file diff --git a/setup/sale_stock_release_channel_delivery_date/setup.py b/setup/sale_stock_release_channel_delivery_date/setup.py new file mode 100644 index 00000000000..28c57bb6403 --- /dev/null +++ b/setup/sale_stock_release_channel_delivery_date/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/shopfloor_reception_refund_return/odoo/addons/shopfloor_reception_refund_return b/setup/shopfloor_reception_refund_return/odoo/addons/shopfloor_reception_refund_return new file mode 120000 index 00000000000..ea4784eccfc --- /dev/null +++ b/setup/shopfloor_reception_refund_return/odoo/addons/shopfloor_reception_refund_return @@ -0,0 +1 @@ +../../../../shopfloor_reception_refund_return \ No newline at end of file diff --git a/setup/shopfloor_reception_refund_return/setup.py b/setup/shopfloor_reception_refund_return/setup.py new file mode 100644 index 00000000000..28c57bb6403 --- /dev/null +++ b/setup/shopfloor_reception_refund_return/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/stock_available_to_promise_release_dynamic_routing/odoo/addons/stock_available_to_promise_release_dynamic_routing b/setup/stock_available_to_promise_release_dynamic_routing/odoo/addons/stock_available_to_promise_release_dynamic_routing new file mode 120000 index 00000000000..ff1bcd794ed --- /dev/null +++ b/setup/stock_available_to_promise_release_dynamic_routing/odoo/addons/stock_available_to_promise_release_dynamic_routing @@ -0,0 +1 @@ +../../../../stock_available_to_promise_release_dynamic_routing \ No newline at end of file diff --git a/setup/stock_available_to_promise_release_dynamic_routing/setup.py b/setup/stock_available_to_promise_release_dynamic_routing/setup.py new file mode 100644 index 00000000000..28c57bb6403 --- /dev/null +++ b/setup/stock_available_to_promise_release_dynamic_routing/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/stock_full_location_reservation/odoo/addons/stock_full_location_reservation b/setup/stock_full_location_reservation/odoo/addons/stock_full_location_reservation new file mode 120000 index 00000000000..649b3ae0a50 --- /dev/null +++ b/setup/stock_full_location_reservation/odoo/addons/stock_full_location_reservation @@ -0,0 +1 @@ +../../../../stock_full_location_reservation \ No newline at end of file diff --git a/setup/stock_full_location_reservation/setup.py b/setup/stock_full_location_reservation/setup.py new file mode 100644 index 00000000000..28c57bb6403 --- /dev/null +++ b/setup/stock_full_location_reservation/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/stock_release_channel_depot/odoo/addons/stock_release_channel_depot b/setup/stock_release_channel_depot/odoo/addons/stock_release_channel_depot new file mode 120000 index 00000000000..6aabdb91c33 --- /dev/null +++ b/setup/stock_release_channel_depot/odoo/addons/stock_release_channel_depot @@ -0,0 +1 @@ +../../../../stock_release_channel_depot \ No newline at end of file diff --git a/setup/stock_release_channel_depot/setup.py b/setup/stock_release_channel_depot/setup.py new file mode 100644 index 00000000000..28c57bb6403 --- /dev/null +++ b/setup/stock_release_channel_depot/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/stock_release_channel_partner_by_date_delivery_window/odoo/addons/stock_release_channel_partner_by_date_delivery_window b/setup/stock_release_channel_partner_by_date_delivery_window/odoo/addons/stock_release_channel_partner_by_date_delivery_window new file mode 120000 index 00000000000..aa9538eaf1e --- /dev/null +++ b/setup/stock_release_channel_partner_by_date_delivery_window/odoo/addons/stock_release_channel_partner_by_date_delivery_window @@ -0,0 +1 @@ +../../../../stock_release_channel_partner_by_date_delivery_window \ No newline at end of file diff --git a/setup/stock_release_channel_partner_by_date_delivery_window/setup.py b/setup/stock_release_channel_partner_by_date_delivery_window/setup.py new file mode 100644 index 00000000000..28c57bb6403 --- /dev/null +++ b/setup/stock_release_channel_partner_by_date_delivery_window/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/stock_release_channel_partner_by_date_public_holidays/odoo/addons/stock_release_channel_partner_by_date_public_holidays b/setup/stock_release_channel_partner_by_date_public_holidays/odoo/addons/stock_release_channel_partner_by_date_public_holidays new file mode 120000 index 00000000000..75273debec9 --- /dev/null +++ b/setup/stock_release_channel_partner_by_date_public_holidays/odoo/addons/stock_release_channel_partner_by_date_public_holidays @@ -0,0 +1 @@ +../../../../stock_release_channel_partner_by_date_public_holidays \ No newline at end of file diff --git a/setup/stock_release_channel_partner_by_date_public_holidays/setup.py b/setup/stock_release_channel_partner_by_date_public_holidays/setup.py new file mode 100644 index 00000000000..28c57bb6403 --- /dev/null +++ b/setup/stock_release_channel_partner_by_date_public_holidays/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/stock_release_channel_plan_depot/odoo/addons/stock_release_channel_plan_depot b/setup/stock_release_channel_plan_depot/odoo/addons/stock_release_channel_plan_depot new file mode 120000 index 00000000000..a30aae60461 --- /dev/null +++ b/setup/stock_release_channel_plan_depot/odoo/addons/stock_release_channel_plan_depot @@ -0,0 +1 @@ +../../../../stock_release_channel_plan_depot \ No newline at end of file diff --git a/setup/stock_release_channel_plan_depot/setup.py b/setup/stock_release_channel_plan_depot/setup.py new file mode 100644 index 00000000000..28c57bb6403 --- /dev/null +++ b/setup/stock_release_channel_plan_depot/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/stock_release_channel_warehouse_calendar/odoo/addons/stock_release_channel_warehouse_calendar b/setup/stock_release_channel_warehouse_calendar/odoo/addons/stock_release_channel_warehouse_calendar new file mode 120000 index 00000000000..24c66fce05c --- /dev/null +++ b/setup/stock_release_channel_warehouse_calendar/odoo/addons/stock_release_channel_warehouse_calendar @@ -0,0 +1 @@ +../../../../stock_release_channel_warehouse_calendar \ No newline at end of file diff --git a/setup/stock_release_channel_warehouse_calendar/setup.py b/setup/stock_release_channel_warehouse_calendar/setup.py new file mode 100644 index 00000000000..28c57bb6403 --- /dev/null +++ b/setup/stock_release_channel_warehouse_calendar/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/stock_warehouse_flow_delivery_refresh/odoo/addons/stock_warehouse_flow_delivery_refresh b/setup/stock_warehouse_flow_delivery_refresh/odoo/addons/stock_warehouse_flow_delivery_refresh new file mode 120000 index 00000000000..80e56b952b5 --- /dev/null +++ b/setup/stock_warehouse_flow_delivery_refresh/odoo/addons/stock_warehouse_flow_delivery_refresh @@ -0,0 +1 @@ +../../../../stock_warehouse_flow_delivery_refresh \ No newline at end of file diff --git a/setup/stock_warehouse_flow_delivery_refresh/setup.py b/setup/stock_warehouse_flow_delivery_refresh/setup.py new file mode 100644 index 00000000000..28c57bb6403 --- /dev/null +++ b/setup/stock_warehouse_flow_delivery_refresh/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/shopfloor/README.rst b/shopfloor/README.rst index 1b7ca9b1d6e..16df3b9d278 100644 --- a/shopfloor/README.rst +++ b/shopfloor/README.rst @@ -1,3 +1,7 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + ========= Shopfloor ========= @@ -7,13 +11,13 @@ Shopfloor !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:15590a835f281c13f0eae031c50d19d04ab77e94ee95b697d5eabd04373c21ce + !! source digest: sha256:9f90d6a5968a33846a7f00f6daac7c54a3097b54e13d308bdc964d8115207c58 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png :target: https://odoo-community.org/page/development-status :alt: Beta -.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png +.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fwms-lightgray.png?logo=github diff --git a/shopfloor/__manifest__.py b/shopfloor/__manifest__.py index de5ce464254..1aefbb909cb 100644 --- a/shopfloor/__manifest__.py +++ b/shopfloor/__manifest__.py @@ -6,7 +6,7 @@ { "name": "Shopfloor", "summary": "manage warehouse operations with barcode scanners", - "version": "16.0.2.6.0", + "version": "16.0.2.16.1", "development_status": "Beta", "category": "Inventory", "website": "https://github.com/OCA/wms", diff --git a/shopfloor/actions/__init__.py b/shopfloor/actions/__init__.py index 5fecbe84ced..7d705fca756 100644 --- a/shopfloor/actions/__init__.py +++ b/shopfloor/actions/__init__.py @@ -1,3 +1,4 @@ +from . import barcode_parser from . import change_package_lot from . import data from . import data_detail diff --git a/shopfloor/actions/barcode_parser.py b/shopfloor/actions/barcode_parser.py new file mode 100644 index 00000000000..349f23b996a --- /dev/null +++ b/shopfloor/actions/barcode_parser.py @@ -0,0 +1,44 @@ +# Copyright 2025 ACSONE SA/NV (https://www.acsone.eu) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo.addons.component.core import Component + +from ..actions.search import SearchAction + + +class BarcodeResult: + + __slots__ = ("type", "value", "raw") + + def __init__(self, **kw) -> None: + for k in self.__slots__: + setattr(self, k, kw.get(k)) + + +class BarcodeParser(Component): + """ + Some barcodes can have complex data structure + """ + + _name = "shopfloor.barcode.parser" + _inherit = "shopfloor.process.action" + _usage = "barcode" + + def __init__(self, search_action: SearchAction): + # Get search action keys + self.search_action = search_action + + @property + def _authorized_barcode_types(self): + return self.search_action._barcode_type_handler.keys() + + def parse(self, barcode, types) -> list[BarcodeResult]: + """ + This method will parse the barcode and return the + value with its type if determined. + + Override this to implement specific parsing + + """ + + return [BarcodeResult(type="unknown", value=barcode, raw=barcode)] diff --git a/shopfloor/actions/change_package_lot.py b/shopfloor/actions/change_package_lot.py index 07ea3621b4d..b1c78b01f73 100644 --- a/shopfloor/actions/change_package_lot.py +++ b/shopfloor/actions/change_package_lot.py @@ -171,5 +171,7 @@ def filter_lines_allowed_to_change_lot(self, move_lines, lot): restricts the reservation based on the previous move(s). """ return move_lines.filtered( - lambda l: (l.product_id == lot.product_id and not l.move_id.move_orig_ids) + lambda x, lot=lot: ( + x.product_id == lot.product_id and not x.move_id.move_orig_ids + ) ) diff --git a/shopfloor/actions/data.py b/shopfloor/actions/data.py index 06019118fdd..47e10453ba4 100644 --- a/shopfloor/actions/data.py +++ b/shopfloor/actions/data.py @@ -351,7 +351,8 @@ def _picking_type_parser(self): def _get_operation_progress(self, domain): lines = self.env["stock.move.line"].search(domain) - # operations_to_do = number of total operations that are pending for this location. + # operations_to_do = number of total operations + # that are pending for this location. # operations_done = number of operations already done. operations_to_do = 0 operations_done = 0 diff --git a/shopfloor/actions/data_detail.py b/shopfloor/actions/data_detail.py index 5c073138bd0..b7c838a34e5 100644 --- a/shopfloor/actions/data_detail.py +++ b/shopfloor/actions/data_detail.py @@ -212,7 +212,7 @@ def _locations_for_product(self, record): def _product_image_url(self, record, field_name): if not record[field_name]: return None - return "/web/image/product.product/{}/{}".format(record.id, field_name) + return f"/web/image/product.product/{record.id}/{field_name}" @property def _product_supplierinfo_parser(self): @@ -228,7 +228,7 @@ def packaging_detail(self, record, **kw): return self._jsonify( record.with_context(packaging=record.id), self._packaging_detail_parser, - **kw + **kw, ) @property diff --git a/shopfloor/actions/inventory.py b/shopfloor/actions/inventory.py index f3c831acad8..694803b9107 100644 --- a/shopfloor/actions/inventory.py +++ b/shopfloor/actions/inventory.py @@ -59,10 +59,11 @@ def _create_draft_inventory(self, location, product, package=None, lot=None): for quant in quants: if quant.inventory_quantity_set: continue - quants.write( + quant.write( { - # Set an inventory quantity to prevent the zero quant cleanup - "inventory_quantity": quant.inventory_quantity + 1, + # Set a user to prevent the zero quant cleanup + "user_id": self.env.user.id, + "inventory_quantity": 0, "inventory_date": fields.Date.today(), } ) diff --git a/shopfloor/actions/location_content_transfer_sorter.py b/shopfloor/actions/location_content_transfer_sorter.py index 1ad4aa1f8b1..7d9a5d85816 100644 --- a/shopfloor/actions/location_content_transfer_sorter.py +++ b/shopfloor/actions/location_content_transfer_sorter.py @@ -4,7 +4,6 @@ class LocationContentTransferSorter(Component): - _name = "shopfloor.location.content.transfer.sorter" _inherit = "shopfloor.process.action" _usage = "location_content_transfer.sorter" diff --git a/shopfloor/actions/message.py b/shopfloor/actions/message.py index aa63b36824c..e8962bf19bc 100644 --- a/shopfloor/actions/message.py +++ b/shopfloor/actions/message.py @@ -123,7 +123,8 @@ def package_not_available_in_picking(self, package, picking): return { "message_type": "warning", "body": _( - "Package %(package_name)s is not available in transfer %(picking_name)s.", + "Package %(package_name)s " + "is not available in transfer %(picking_name)s.", package_name=package.name, picking_name=picking.name, ), @@ -214,6 +215,15 @@ def confirm_pack_moved(self): def already_done(self): return {"message_type": "info", "body": _("Operation already processed.")} + def transfer_canceled(self): + return { + "message_type": "info", + "body": _( + "Transfer has been canceled. " + "This cannot be processed using this scenario" + ), + } + def move_already_done(self): return {"message_type": "warning", "body": _("Move already processed.")} @@ -305,7 +315,8 @@ def several_move_in_different_location(self): return { "message_type": "warning", "body": _( - "Several moves found on different locations, please scan first the location." + "Several moves found on different locations, " + "please scan first the location." ), } @@ -426,7 +437,8 @@ def source_document_multiple_pickings_scan_package(self): "message_type": "warning", "body": _( _( - "This source document is part of multiple transfers, please scan a package." + "This source document is part of multiple transfers, " + "please scan a package." ) ), } @@ -458,6 +470,34 @@ def product_not_found_in_pickings(self): "body": _("No transfer found for this product."), } + def transfer_not_found_for_barcode(self, barcode): + body = _("No transfer found for barcode %s", barcode) + return { + "message_type": "error", + "body": body, + } + + def transfer_not_found_for_record(self, record): + model_mapping = { + "product.product": "product", + "stock.picking": "transfer", + "stock.quant.package": "package", + "product.packaging": "packaging", + "stock.location": "location", + "stock.lot": "lot", + "stock.move": "move", + } + model_name = model_mapping.get(record._name) + body = _( + "No transfer found for %(model_name)s %(record_name)s", + model_name=model_name, + record_name=record.name, + ) + return { + "message_type": "error", + "body": body, + } + def product_not_found_in_location_or_transfer(self, product, location, picking): return { "message_type": "error", @@ -474,8 +514,10 @@ def x_not_found_or_already_in_dest_package(self, message_code): return { "message_type": "warning", "body": _( - "{message_code} not found in the current transfer or already in a package." - ).format(message_code=message_code), + "%(message_code)s not found in the current transfer " + "or already in a package.", + message_code=message_code, + ), } def packaging_not_found_in_picking(self): @@ -526,11 +568,9 @@ def place_in_location_ask_confirmation(self, location_name): "body": _("Place it in {}?").format(location_name), } - def product_not_found_in_current_picking(self): - return { - "message_type": "error", - "body": _("Product is not in the current transfer."), - } + def product_not_found_in_current_picking(self, product): + body = _("Product %s is not in the current transfer.", product.name) + return {"message_type": "error", "body": body} def lot_mixed_package_scan_package(self): return { @@ -597,10 +637,14 @@ def transfer_complete(self, picking): "body": _("Transfer {} complete").format(picking.name), } - def location_content_transfer_item_complete(self, location_dest): + def location_content_transfer_item_complete(self, location_src, location_dest): return { "message_type": "success", - "body": _("Content transfer to {} completed").format(location_dest.name), + "body": _( + "Content line transferred from %(location_name)s to %(location_dest_name)s", + location_name=location_src.name, + location_dest_name=location_dest.name, + ), } def location_content_transfer_complete(self, location_src, location_dest): @@ -722,7 +766,8 @@ def line_scanned_qty_done_higher_than_allowed(self): return { "message_type": "warning", "body": _( - "Please note that the scanned quantity is higher than the maximum allowed." + "Please note that the scanned quantity " + "is higher than the maximum allowed." ), } @@ -982,3 +1027,13 @@ def lot_change_no_line_found(self): "message_type": "error", "body": _("Unable to find a line with the same product but different lot."), } + + def reserved_for_other_picking_type(self, picking): + body = _("Reserved for %(picking_type)s %(picking_name)s") % { + "picking_type": picking.picking_type_id.name, + "picking_name": picking.name, + } + return { + "message_type": "error", + "body": body, + } diff --git a/shopfloor/actions/move_line_search.py b/shopfloor/actions/move_line_search.py index 18cdf128bdf..c1c248e0279 100644 --- a/shopfloor/actions/move_line_search.py +++ b/shopfloor/actions/move_line_search.py @@ -53,7 +53,9 @@ def _sort_key_custom_code_eval_context(self, line): "key": None, "get_sort_key_priority": self._sort_key_move_lines_priority, "get_sort_key_location": self._sort_key_move_lines_location, - "get_sort_key_assigned_to_current_user": self._sort_key_assigned_to_current_user, + "get_sort_key_assigned_to_current_user": ( + self._sort_key_assigned_to_current_user + ), } def _search_move_lines_domain( diff --git a/shopfloor/actions/savepoint.py b/shopfloor/actions/savepoint.py index 8aea40287de..246ab0eb4b7 100644 --- a/shopfloor/actions/savepoint.py +++ b/shopfloor/actions/savepoint.py @@ -18,7 +18,7 @@ def new(self): return Savepoint(self.env.cr) -class Savepoint(object): +class Savepoint: """Wrapper for SQL Savepoint Close to "cr.savepoint()" context manager but this class gives more control diff --git a/shopfloor/actions/schema.py b/shopfloor/actions/schema.py index e474fe35de2..7757a791dee 100644 --- a/shopfloor/actions/schema.py +++ b/shopfloor/actions/schema.py @@ -4,7 +4,6 @@ class ShopfloorSchemaAction(Component): - _inherit = "shopfloor.schema.action" def picking(self): diff --git a/shopfloor/actions/search.py b/shopfloor/actions/search.py index 34a640e45af..ff6e4350b98 100644 --- a/shopfloor/actions/search.py +++ b/shopfloor/actions/search.py @@ -9,7 +9,7 @@ class SearchResult: - __slots__ = ("record", "type", "code") + __slots__ = ("record", "type", "code", "parse_result") def __init__(self, **kw) -> None: for k in self.__slots__: @@ -31,7 +31,8 @@ def __eq__(self, other): @property def records(self): - """In some cases we expect more than one records (eg: location limit > 1) or lots""" + # In some cases we expect more than one records + # (eg: location limit > 1) or lots return self.record if len(self.record) > 1 else None @@ -44,6 +45,12 @@ class SearchAction(Component): _inherit = "shopfloor.search.action" + @property + def parser(self): + parser = self._actions_for("barcode") + parser.search_action = self + return parser + @property def _barcode_type_handler(self): return { @@ -57,6 +64,8 @@ def _barcode_type_handler(self): "packaging": self.packaging_from_scan, "delivery_packaging": self.delivery_packaging_from_scan, "origin_move": self.origin_move_from_scan, + # Extra data can be contained in barcodes + "expiration_date": self.expiration_date_from_scan, } def _make_search_result(self, **kwargs): @@ -85,11 +94,20 @@ def _find_record_by_type(self, barcode, btype, handler_kw=None): def generic_find(self, barcode, types=None, handler_kw=None): _types = types or self._barcode_type_handler.keys() # TODO: decide the best default order in case we don't pass `types` - for btype in _types: - record = self._find_record_by_type(barcode, btype, handler_kw) - if record: - return self._make_search_result(record=record, code=barcode, type=btype) - return self._make_search_result(type="none") + parse_results = self.parser.parse(barcode, types) + for parse_result in parse_results: + for btype in _types: + record = self._find_record_by_type( + parse_result.value, btype, handler_kw + ) + if record: + return self._make_search_result( + record=record, + code=barcode, + type=btype, + parse_result=parse_results, + ) + return self._make_search_result(type="none", parse_result=parse_results) def location_from_scan(self, barcode, limit=1): model = self.env["stock.location"] @@ -188,3 +206,10 @@ def origin_move_from_scan(self, barcode, extra_domain=None): if extra_domain: outgoing_move_domain = AND([outgoing_move_domain, extra_domain]) return model.search(outgoing_move_domain) + + def dummy_from_scan(self, barcode): + return None + + def expiration_date_from_scan(self, barcode): + # TODO + return None diff --git a/shopfloor/actions/stock.py b/shopfloor/actions/stock.py index ee4a810b6cb..6c661d84ea5 100644 --- a/shopfloor/actions/stock.py +++ b/shopfloor/actions/stock.py @@ -1,4 +1,5 @@ # Copyright 2020 Camptocamp SA (http://www.camptocamp.com) +# Copyright 2025 Michael Tietz (MT Software) # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). from odoo import _, fields from odoo.tools.float_utils import float_round @@ -108,7 +109,13 @@ def create_return_picking(self, picking, return_types, origin): return picking.copy(return_values) def mark_move_line_as_picked( - self, move_lines, quantity=None, package=None, user=None, check_user=False + self, + move_lines, + quantity=None, + package=None, + user=None, + check_user=False, + split=True, ): """Set the qty_done and extract lines in new order""" user = user or self.env.user @@ -121,7 +128,8 @@ def mark_move_line_as_picked( for line in move_lines: qty_done = quantity if quantity is not None else line.reserved_uom_qty line.qty_done = qty_done - line._split_partial_quantity() + if split: + line._split_partial_quantity() data = { "shopfloor_user_id": user.id, } @@ -129,13 +137,19 @@ def mark_move_line_as_picked( # destination package is set to the scanned one data["result_package_id"] = package.id line.write(data) + values_assigned = { + "user_id": user.id, + "printed": True, + } # Extract the picked quantity in a split order and set current user - move_lines._extract_in_split_order( - { - "user_id": user.id, - "printed": True, - } - ) + if split: + move_lines._extract_in_split_order(values_assigned) + else: + lines = move_lines.picking_id.filtered( + lambda picking: not picking.printed or not picking.user_id == user + ) + if lines: + lines.write(values_assigned) move_lines.picking_id.filtered(lambda p: p.user_id != user).user_id = user.id def unmark_move_line_as_picked(self, move_lines): @@ -150,7 +164,7 @@ def unmark_move_line_as_picked(self, move_lines): pickings = move_lines.picking_id for picking in pickings: lines_still_assigned = picking.move_line_ids.filtered( - lambda l: l.shopfloor_user_id + lambda x: x.shopfloor_user_id ) if lines_still_assigned: # Because there is other lines in the picking still assigned @@ -237,3 +251,24 @@ def no_putaway_available(self, picking_types, move_lines): # when no putaway is found, the move line destination stays the # default's of the picking type return any(line.location_dest_id in base_locations for line in move_lines) + + def _lock_lines(self, lines): + self._actions_for("lock").for_update(lines) + + def _set_destination_on_lines(self, lines, location_dest): + # when writing the destination on the package level, it writes + # on the moves and move lines + lines_with_package_level = lines.package_level_id.move_line_ids + lines_without_package_level = lines - lines_with_package_level + if lines_with_package_level: + lines_with_package_level.package_level_id.location_dest_id = location_dest + if lines_without_package_level: + lines_without_package_level.location_dest_id = location_dest + lines_without_package_level.move_id.location_dest_id = location_dest + + def unload_package(self, lines): + lines.result_package_id = False + + def set_destination_on_lines(self, lines, location_dest): + self._lock_lines(lines) + self._set_destination_on_lines(lines, location_dest) diff --git a/shopfloor/actions/stock_unreserve.py b/shopfloor/actions/stock_unreserve.py index b279a5d9082..a86c0c917f1 100644 --- a/shopfloor/actions/stock_unreserve.py +++ b/shopfloor/actions/stock_unreserve.py @@ -10,7 +10,9 @@ class StockUnreserve(Component): _inherit = "shopfloor.process.action" _usage = "stock.unreserve" - def check_unreserve(self, location, move_lines, product=None, lot=None): + def check_unreserve( + self, location, move_lines, product=None, lot=None, allowed_types=None + ): """Return a message if there is an ongoing operation in the location. It could be a move line with some qty already processed or another @@ -20,12 +22,17 @@ def check_unreserve(self, location, move_lines, product=None, lot=None): :param move_lines: move lines to unreserve :param product: optional product to limit the scope in the location """ + if not allowed_types: + allowed_types = self.env["stock.picking.type"] location_move_lines = self._find_location_all_move_lines(location, product, lot) extra_move_lines = location_move_lines - move_lines if extra_move_lines: - return self.msg_store.picking_already_started_in_location( - extra_move_lines.picking_id - ) + extra_pickings = extra_move_lines.picking_id + if allowed_types: + for picking in extra_pickings: + if picking.picking_type_id not in allowed_types: + return self.msg_store.reserved_for_other_picking_type(picking) + return self.msg_store.picking_already_started_in_location(extra_pickings) def unreserve_moves(self, move_lines, picking_types): """Unreserve moves from `move_lines'. diff --git a/shopfloor/demo/shopfloor_app_demo.xml b/shopfloor/demo/shopfloor_app_demo.xml index d5651d07531..706ae5e12ca 100644 --- a/shopfloor/demo/shopfloor_app_demo.xml +++ b/shopfloor/demo/shopfloor_app_demo.xml @@ -1,12 +1,12 @@ - - Shopfloor WMS (demo) - WMS (demo) - wms_demo - wms - + Shopfloor WMS (demo) + WMS (demo) + wms_demo + wms + - + diff --git a/shopfloor/i18n/ca.po b/shopfloor/i18n/ca.po index 71e40111cfd..a32d0cf2d55 100644 --- a/shopfloor/i18n/ca.po +++ b/shopfloor/i18n/ca.po @@ -91,6 +91,14 @@ msgid "" "Incompatible with: \"Pick and pack at the same time\"\n" msgstr "" +#. module: shopfloor +#. odoo-python +#: code:addons/shopfloor/actions/message.py:0 +#, python-format +msgid "" +"%(message_code)s not found in the current transfer or already in a package." +msgstr "" + #. module: shopfloor #. odoo-python #: code:addons/shopfloor/actions/message.py:0 @@ -347,8 +355,9 @@ msgstr "" #. odoo-python #: code:addons/shopfloor/actions/message.py:0 #, python-format -msgid "Content transfer to {} completed" -msgstr "Transferència de contingut a {} competada" +msgid "" +"Content line transferred from %(location_name)s to %(location_dest_name)s" +msgstr "" #. module: shopfloor #. odoo-python @@ -527,6 +536,11 @@ msgstr "" msgid "Inventory Locations" msgstr "" +#. module: shopfloor +#: model:ir.model.fields,field_description:shopfloor.field_stock_picking__is_shopfloor_created +msgid "Is Shopfloor Created" +msgstr "" + #. module: shopfloor #. odoo-python #: code:addons/shopfloor/actions/completion_info.py:0 @@ -847,6 +861,20 @@ msgstr "" msgid "No quantity has been processed, unable to complete the transfer." msgstr "" +#. module: shopfloor +#. odoo-python +#: code:addons/shopfloor/actions/message.py:0 +#, python-format +msgid "No transfer found for %(model_name)s %(record_name)s" +msgstr "" + +#. module: shopfloor +#. odoo-python +#: code:addons/shopfloor/actions/message.py:0 +#, python-format +msgid "No transfer found for barcode %s" +msgstr "" + #. module: shopfloor #. odoo-python #: code:addons/shopfloor/actions/message.py:0 @@ -1054,13 +1082,6 @@ msgstr "" msgid "Package {} is not empty." msgstr "" -#. module: shopfloor -#. odoo-python -#: code:addons/shopfloor/services/checkout.py:0 -#, python-format -msgid "Package {} is not in the current transfer." -msgstr "" - #. module: shopfloor #: model:ir.model,name:shopfloor.model_stock_quant_package msgid "Packages" @@ -1216,16 +1237,16 @@ msgid "" "%(picking_name)s." msgstr "" -#. module: shopfloor -#: model:ir.model,name:shopfloor.model_stock_move_line -msgid "Product Moves (Stock Move Line)" -msgstr "" - #. module: shopfloor #. odoo-python #: code:addons/shopfloor/actions/message.py:0 #, python-format -msgid "Product is not in the current transfer." +msgid "Product %s is not in the current transfer." +msgstr "" + +#. module: shopfloor +#: model:ir.model,name:shopfloor.model_stock_move_line +msgid "Product Moves (Stock Move Line)" msgstr "" #. module: shopfloor @@ -1296,6 +1317,13 @@ msgstr "" msgid "Reserved Move Line" msgstr "" +#. module: shopfloor +#. odoo-python +#: code:addons/shopfloor/actions/message.py:0 +#, python-format +msgid "Reserved for %(picking_type)s %(picking_name)s" +msgstr "" + #. module: shopfloor #. odoo-python #: code:addons/shopfloor/actions/message.py:0 @@ -1895,6 +1923,14 @@ msgstr "" msgid "Transfer" msgstr "" +#. module: shopfloor +#. odoo-python +#: code:addons/shopfloor/actions/message.py:0 +#, python-format +msgid "" +"Transfer has been canceled. This cannot be processed using this scenario" +msgstr "" + #. module: shopfloor #. odoo-python #: code:addons/shopfloor/actions/message.py:0 @@ -2155,16 +2191,12 @@ msgstr "" #. odoo-python #: code:addons/shopfloor/actions/message.py:0 #, python-format -msgid "" -"{message_code} not found in the current transfer or already in a package." +msgid "{} is not a valid destination package." msgstr "" -#. module: shopfloor -#. odoo-python -#: code:addons/shopfloor/actions/message.py:0 #, python-format -msgid "{} is not a valid destination package." -msgstr "" +#~ msgid "Content transfer to {} completed" +#~ msgstr "Transferència de contingut a {} competada" #, python-format #~ msgid "A draft inventory has been created for control." diff --git a/shopfloor/i18n/de.po b/shopfloor/i18n/de.po index 749de96c952..ce2aa53e01a 100644 --- a/shopfloor/i18n/de.po +++ b/shopfloor/i18n/de.po @@ -6,13 +6,15 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 14.0\n" "Report-Msgid-Bugs-To: \n" -"Last-Translator: Automatically generated\n" +"PO-Revision-Date: 2026-02-11 11:09+0000\n" +"Last-Translator: Artur Machura \n" "Language-Team: none\n" "Language: de\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" "Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.15.2\n" #. module: shopfloor #: model:ir.model.fields,help:shopfloor.field_shopfloor_menu__pick_pack_same_time @@ -89,6 +91,14 @@ msgid "" "Incompatible with: \"Pick and pack at the same time\"\n" msgstr "" +#. module: shopfloor +#. odoo-python +#: code:addons/shopfloor/actions/message.py:0 +#, python-format +msgid "" +"%(message_code)s not found in the current transfer or already in a package." +msgstr "" + #. module: shopfloor #. odoo-python #: code:addons/shopfloor/actions/message.py:0 @@ -263,26 +273,26 @@ msgstr "" #: code:addons/shopfloor/actions/message.py:0 #, python-format msgid "Barcode not found" -msgstr "" +msgstr "Strichcode nicht gefunden" #. module: shopfloor #: model:ir.model,name:shopfloor.model_stock_picking_batch msgid "Batch Transfer" -msgstr "" +msgstr "Stapeltransfer" #. module: shopfloor #. odoo-python #: code:addons/shopfloor/actions/message.py:0 #, python-format msgid "Batch Transfer complete" -msgstr "" +msgstr "Stapeltransfer fertiggestellt" #. module: shopfloor #. odoo-python #: code:addons/shopfloor/actions/message.py:0 #, python-format msgid "Batch Transfer line done" -msgstr "" +msgstr "Stapeltransferzeile erledigt" #. module: shopfloor #. odoo-python @@ -345,7 +355,8 @@ msgstr "" #. odoo-python #: code:addons/shopfloor/actions/message.py:0 #, python-format -msgid "Content transfer to {} completed" +msgid "" +"Content line transferred from %(location_name)s to %(location_dest_name)s" msgstr "" #. module: shopfloor @@ -523,6 +534,11 @@ msgstr "" msgid "Inventory Locations" msgstr "" +#. module: shopfloor +#: model:ir.model.fields,field_description:shopfloor.field_stock_picking__is_shopfloor_created +msgid "Is Shopfloor Created" +msgstr "" + #. module: shopfloor #. odoo-python #: code:addons/shopfloor/actions/completion_info.py:0 @@ -843,6 +859,20 @@ msgstr "" msgid "No quantity has been processed, unable to complete the transfer." msgstr "" +#. module: shopfloor +#. odoo-python +#: code:addons/shopfloor/actions/message.py:0 +#, python-format +msgid "No transfer found for %(model_name)s %(record_name)s" +msgstr "" + +#. module: shopfloor +#. odoo-python +#: code:addons/shopfloor/actions/message.py:0 +#, python-format +msgid "No transfer found for barcode %s" +msgstr "" + #. module: shopfloor #. odoo-python #: code:addons/shopfloor/actions/message.py:0 @@ -1050,13 +1080,6 @@ msgstr "" msgid "Package {} is not empty." msgstr "" -#. module: shopfloor -#. odoo-python -#: code:addons/shopfloor/services/checkout.py:0 -#, python-format -msgid "Package {} is not in the current transfer." -msgstr "" - #. module: shopfloor #: model:ir.model,name:shopfloor.model_stock_quant_package msgid "Packages" @@ -1212,16 +1235,16 @@ msgid "" "%(picking_name)s." msgstr "" -#. module: shopfloor -#: model:ir.model,name:shopfloor.model_stock_move_line -msgid "Product Moves (Stock Move Line)" -msgstr "" - #. module: shopfloor #. odoo-python #: code:addons/shopfloor/actions/message.py:0 #, python-format -msgid "Product is not in the current transfer." +msgid "Product %s is not in the current transfer." +msgstr "" + +#. module: shopfloor +#: model:ir.model,name:shopfloor.model_stock_move_line +msgid "Product Moves (Stock Move Line)" msgstr "" #. module: shopfloor @@ -1292,6 +1315,13 @@ msgstr "" msgid "Reserved Move Line" msgstr "" +#. module: shopfloor +#. odoo-python +#: code:addons/shopfloor/actions/message.py:0 +#, python-format +msgid "Reserved for %(picking_type)s %(picking_name)s" +msgstr "" + #. module: shopfloor #. odoo-python #: code:addons/shopfloor/actions/message.py:0 @@ -1891,6 +1921,14 @@ msgstr "" msgid "Transfer" msgstr "" +#. module: shopfloor +#. odoo-python +#: code:addons/shopfloor/actions/message.py:0 +#, python-format +msgid "" +"Transfer has been canceled. This cannot be processed using this scenario" +msgstr "" + #. module: shopfloor #. odoo-python #: code:addons/shopfloor/actions/message.py:0 @@ -2147,14 +2185,6 @@ msgstr "" msgid "Zone Picking" msgstr "" -#. module: shopfloor -#. odoo-python -#: code:addons/shopfloor/actions/message.py:0 -#, python-format -msgid "" -"{message_code} not found in the current transfer or already in a package." -msgstr "" - #. module: shopfloor #. odoo-python #: code:addons/shopfloor/actions/message.py:0 diff --git a/shopfloor/i18n/es_AR.po b/shopfloor/i18n/es_AR.po index 629a786662a..2c8125f8cf7 100644 --- a/shopfloor/i18n/es_AR.po +++ b/shopfloor/i18n/es_AR.po @@ -119,6 +119,14 @@ msgstr "" "\n" "Incompatible con: \"Recoger y empaquetar al mismo tiempo\"\n" +#. module: shopfloor +#. odoo-python +#: code:addons/shopfloor/actions/message.py:0 +#, python-format +msgid "" +"%(message_code)s not found in the current transfer or already in a package." +msgstr "" + #. module: shopfloor #. odoo-python #: code:addons/shopfloor/actions/message.py:0 @@ -379,8 +387,9 @@ msgstr "" #. odoo-python #: code:addons/shopfloor/actions/message.py:0 #, python-format -msgid "Content transfer to {} completed" -msgstr "Transferencia de contenido a {} completada" +msgid "" +"Content line transferred from %(location_name)s to %(location_dest_name)s" +msgstr "" #. module: shopfloor #. odoo-python @@ -569,6 +578,11 @@ msgstr "No se permite ignorar el almacenamiento no encontrado para el menú {}." msgid "Inventory Locations" msgstr "Ubicaciones de Inventario" +#. module: shopfloor +#: model:ir.model.fields,field_description:shopfloor.field_stock_picking__is_shopfloor_created +msgid "Is Shopfloor Created" +msgstr "" + #. module: shopfloor #. odoo-python #: code:addons/shopfloor/actions/completion_info.py:0 @@ -894,6 +908,20 @@ msgstr "" "No se ha procesado ninguna cantidad, no se ha podido completar la " "transferencia." +#. module: shopfloor +#. odoo-python +#: code:addons/shopfloor/actions/message.py:0 +#, python-format +msgid "No transfer found for %(model_name)s %(record_name)s" +msgstr "" + +#. module: shopfloor +#. odoo-python +#: code:addons/shopfloor/actions/message.py:0 +#, python-format +msgid "No transfer found for barcode %s" +msgstr "" + #. module: shopfloor #. odoo-python #: code:addons/shopfloor/actions/message.py:0 @@ -1111,13 +1139,6 @@ msgstr "El Paquete {} está usado." msgid "Package {} is not empty." msgstr "El Paquete {} no está vacío." -#. module: shopfloor -#. odoo-python -#: code:addons/shopfloor/services/checkout.py:0 -#, python-format -msgid "Package {} is not in the current transfer." -msgstr "El Paquete {} no está en la transferencia actual." - #. module: shopfloor #: model:ir.model,name:shopfloor.model_stock_quant_package msgid "Packages" @@ -1279,17 +1300,17 @@ msgid "" "%(picking_name)s." msgstr "" -#. module: shopfloor -#: model:ir.model,name:shopfloor.model_stock_move_line -msgid "Product Moves (Stock Move Line)" -msgstr "Movimientos de Producto (Stock Move Line)" - #. module: shopfloor #. odoo-python #: code:addons/shopfloor/actions/message.py:0 #, python-format -msgid "Product is not in the current transfer." -msgstr "El Producto no está en la transferencia actual." +msgid "Product %s is not in the current transfer." +msgstr "" + +#. module: shopfloor +#: model:ir.model,name:shopfloor.model_stock_move_line +msgid "Product Moves (Stock Move Line)" +msgstr "Movimientos de Producto (Stock Move Line)" #. module: shopfloor #. odoo-python @@ -1362,6 +1383,13 @@ msgstr "" msgid "Reserved Move Line" msgstr "Movimiento de Línea Reservado" +#. module: shopfloor +#. odoo-python +#: code:addons/shopfloor/actions/message.py:0 +#, python-format +msgid "Reserved for %(picking_type)s %(picking_name)s" +msgstr "" + #. module: shopfloor #. odoo-python #: code:addons/shopfloor/actions/message.py:0 @@ -1993,6 +2021,14 @@ msgstr "Peso Total" msgid "Transfer" msgstr "Transferencia" +#. module: shopfloor +#. odoo-python +#: code:addons/shopfloor/actions/message.py:0 +#, python-format +msgid "" +"Transfer has been canceled. This cannot be processed using this scenario" +msgstr "" + #. module: shopfloor #. odoo-python #: code:addons/shopfloor/actions/message.py:0 @@ -2257,14 +2293,6 @@ msgstr "" msgid "Zone Picking" msgstr "Zona de Entreda" -#. module: shopfloor -#. odoo-python -#: code:addons/shopfloor/actions/message.py:0 -#, python-format -msgid "" -"{message_code} not found in the current transfer or already in a package." -msgstr "" - #. module: shopfloor #. odoo-python #: code:addons/shopfloor/actions/message.py:0 @@ -2272,6 +2300,18 @@ msgstr "" msgid "{} is not a valid destination package." msgstr "{} no es un paquete de destino válido." +#, python-format +#~ msgid "Content transfer to {} completed" +#~ msgstr "Transferencia de contenido a {} completada" + +#, python-format +#~ msgid "Package {} is not in the current transfer." +#~ msgstr "El Paquete {} no está en la transferencia actual." + +#, python-format +#~ msgid "Product is not in the current transfer." +#~ msgstr "El Producto no está en la transferencia actual." + #, python-format #~ msgid "Lot is not in the current transfer." #~ msgstr "El Lote no está en la transferencia actual." diff --git a/shopfloor/i18n/it.po b/shopfloor/i18n/it.po index 2042831b499..5bab7c9d7b6 100644 --- a/shopfloor/i18n/it.po +++ b/shopfloor/i18n/it.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 14.0\n" "Report-Msgid-Bugs-To: \n" -"PO-Revision-Date: 2025-01-31 20:06+0000\n" +"PO-Revision-Date: 2026-01-08 17:42+0000\n" "Last-Translator: mymage \n" "Language-Team: none\n" "Language: it\n" @@ -14,7 +14,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 5.6.2\n" +"X-Generator: Weblate 5.10.4\n" #. module: shopfloor #: model:ir.model.fields,help:shopfloor.field_shopfloor_menu__pick_pack_same_time @@ -136,6 +136,15 @@ msgstr "" "\n" "Incompatibile con: \"Preleva e imballa contemporaneamente\"\n" +#. module: shopfloor +#. odoo-python +#: code:addons/shopfloor/actions/message.py:0 +#, python-format +msgid "" +"%(message_code)s not found in the current transfer or already in a package." +msgstr "" +"%(message_code)s non trovato nel trasferimento attuale o già nel collo." + #. module: shopfloor #. odoo-python #: code:addons/shopfloor/actions/message.py:0 @@ -408,8 +417,9 @@ msgstr "Conferma modifica ubicazione da %(location_from)s a %(location_to)s?" #. odoo-python #: code:addons/shopfloor/actions/message.py:0 #, python-format -msgid "Content transfer to {} completed" -msgstr "Tasferimento contenuto a {} completato" +msgid "" +"Content line transferred from %(location_name)s to %(location_dest_name)s" +msgstr "Riga contenuto trasferita da %(location_name)s a %(location_dest_name)s" #. module: shopfloor #. odoo-python @@ -604,6 +614,11 @@ msgstr "Ignorare inoltro non trovato non è consentito per il menu {}." msgid "Inventory Locations" msgstr "Ubicazioni di inventario" +#. module: shopfloor +#: model:ir.model.fields,field_description:shopfloor.field_stock_picking__is_shopfloor_created +msgid "Is Shopfloor Created" +msgstr "Il reparto è creato" + #. module: shopfloor #. odoo-python #: code:addons/shopfloor/actions/completion_info.py:0 @@ -929,6 +944,20 @@ msgstr "Nessuna destinazion di inoltro disponibile." msgid "No quantity has been processed, unable to complete the transfer." msgstr "Nessuna quantità elaborata, impossibile completare il trasferimento." +#. module: shopfloor +#. odoo-python +#: code:addons/shopfloor/actions/message.py:0 +#, python-format +msgid "No transfer found for %(model_name)s %(record_name)s" +msgstr "Nessun trasferimento trovato per %(model_name)s %(record_name)s" + +#. module: shopfloor +#. odoo-python +#: code:addons/shopfloor/actions/message.py:0 +#, python-format +msgid "No transfer found for barcode %s" +msgstr "Nessun trasferimento trovato per il codice a barre %s" + #. module: shopfloor #. odoo-python #: code:addons/shopfloor/actions/message.py:0 @@ -1152,13 +1181,6 @@ msgstr "Il collo {} è già in uso." msgid "Package {} is not empty." msgstr "Il collo {} non è vuoto." -#. module: shopfloor -#. odoo-python -#: code:addons/shopfloor/services/checkout.py:0 -#, python-format -msgid "Package {} is not in the current transfer." -msgstr "Il collo {} non è nel trasferimento attuale." - #. module: shopfloor #: model:ir.model,name:shopfloor.model_stock_quant_package msgid "Packages" @@ -1322,17 +1344,17 @@ msgstr "" "Prodotto %(product_name)s non trovato nell'ubicazione %(location_name)s o " "trasferimento %(picking_name)s." -#. module: shopfloor -#: model:ir.model,name:shopfloor.model_stock_move_line -msgid "Product Moves (Stock Move Line)" -msgstr "Movimenti prodotto (riga movimento di magazzino)" - #. module: shopfloor #. odoo-python #: code:addons/shopfloor/actions/message.py:0 #, python-format -msgid "Product is not in the current transfer." -msgstr "Il prodotto non è nel trasferimento attuale." +msgid "Product %s is not in the current transfer." +msgstr "Il prodotto %s non è nel trasferimento attuale." + +#. module: shopfloor +#: model:ir.model,name:shopfloor.model_stock_move_line +msgid "Product Moves (Stock Move Line)" +msgstr "Movimenti prodotto (riga movimento di magazzino)" #. module: shopfloor #. odoo-python @@ -1403,6 +1425,13 @@ msgstr "È possibile richiedere il collo di destinazione" msgid "Reserved Move Line" msgstr "Riga movimento prenotata" +#. module: shopfloor +#. odoo-python +#: code:addons/shopfloor/actions/message.py:0 +#, python-format +msgid "Reserved for %(picking_type)s %(picking_name)s" +msgstr "Prenotato per %(picking_type)s %(picking_name)s" + #. module: shopfloor #. odoo-python #: code:addons/shopfloor/actions/message.py:0 @@ -1872,7 +1901,7 @@ msgstr "Il record su cui si stava lavorando non esiste più." #: code:addons/shopfloor/models/stock_move.py:0 #, python-format msgid "The split order {} has been created." -msgstr "L'ordine sddiviso {} è stato creato." +msgstr "L'ordine suddiviso {} è stato creato." #. module: shopfloor #: model:ir.model.fields,help:shopfloor.field_stock_move_line__picking_id @@ -2039,6 +2068,16 @@ msgstr "Peso totale" msgid "Transfer" msgstr "Trasferimento" +#. module: shopfloor +#. odoo-python +#: code:addons/shopfloor/actions/message.py:0 +#, python-format +msgid "" +"Transfer has been canceled. This cannot be processed using this scenario" +msgstr "" +"Il trasferimento è stato annullato. Non può essere elaborato usando questo " +"scenario" + #. module: shopfloor #. odoo-python #: code:addons/shopfloor/actions/message.py:0 @@ -2319,16 +2358,6 @@ msgstr "" msgid "Zone Picking" msgstr "Prelievo per area" -#. module: shopfloor -#. odoo-python -#: code:addons/shopfloor/actions/message.py:0 -#, python-format -msgid "" -"{message_code} not found in the current transfer or already in a package." -msgstr "" -"{message_code} non trovato nel trasferimento attuale o già presente in un " -"collo." - #. module: shopfloor #. odoo-python #: code:addons/shopfloor/actions/message.py:0 @@ -2336,6 +2365,25 @@ msgstr "" msgid "{} is not a valid destination package." msgstr "{} non è un collo destinazione valido." +#, python-format +#~ msgid "Content transfer to {} completed" +#~ msgstr "Tasferimento contenuto a {} completato" + +#, python-format +#~ msgid "" +#~ "{message_code} not found in the current transfer or already in a package." +#~ msgstr "" +#~ "{message_code} non trovato nel trasferimento attuale o già presente in un " +#~ "collo." + +#, python-format +#~ msgid "Package {} is not in the current transfer." +#~ msgstr "Il collo {} non è nel trasferimento attuale." + +#, python-format +#~ msgid "Product is not in the current transfer." +#~ msgstr "Il prodotto non è nel trasferimento attuale." + #, python-format #~ msgid "Lot is not in the current transfer." #~ msgstr "Il lotto non è nel trasferimento attuale." diff --git a/shopfloor/i18n/pt_BR.po b/shopfloor/i18n/pt_BR.po index 9f928a07f3b..e9ffe5b9a58 100644 --- a/shopfloor/i18n/pt_BR.po +++ b/shopfloor/i18n/pt_BR.po @@ -89,6 +89,14 @@ msgid "" "Incompatible with: \"Pick and pack at the same time\"\n" msgstr "" +#. module: shopfloor +#. odoo-python +#: code:addons/shopfloor/actions/message.py:0 +#, python-format +msgid "" +"%(message_code)s not found in the current transfer or already in a package." +msgstr "" + #. module: shopfloor #. odoo-python #: code:addons/shopfloor/actions/message.py:0 @@ -345,7 +353,8 @@ msgstr "" #. odoo-python #: code:addons/shopfloor/actions/message.py:0 #, python-format -msgid "Content transfer to {} completed" +msgid "" +"Content line transferred from %(location_name)s to %(location_dest_name)s" msgstr "" #. module: shopfloor @@ -523,6 +532,11 @@ msgstr "" msgid "Inventory Locations" msgstr "" +#. module: shopfloor +#: model:ir.model.fields,field_description:shopfloor.field_stock_picking__is_shopfloor_created +msgid "Is Shopfloor Created" +msgstr "" + #. module: shopfloor #. odoo-python #: code:addons/shopfloor/actions/completion_info.py:0 @@ -843,6 +857,20 @@ msgstr "" msgid "No quantity has been processed, unable to complete the transfer." msgstr "" +#. module: shopfloor +#. odoo-python +#: code:addons/shopfloor/actions/message.py:0 +#, python-format +msgid "No transfer found for %(model_name)s %(record_name)s" +msgstr "" + +#. module: shopfloor +#. odoo-python +#: code:addons/shopfloor/actions/message.py:0 +#, python-format +msgid "No transfer found for barcode %s" +msgstr "" + #. module: shopfloor #. odoo-python #: code:addons/shopfloor/actions/message.py:0 @@ -1050,13 +1078,6 @@ msgstr "" msgid "Package {} is not empty." msgstr "" -#. module: shopfloor -#. odoo-python -#: code:addons/shopfloor/services/checkout.py:0 -#, python-format -msgid "Package {} is not in the current transfer." -msgstr "" - #. module: shopfloor #: model:ir.model,name:shopfloor.model_stock_quant_package msgid "Packages" @@ -1212,16 +1233,16 @@ msgid "" "%(picking_name)s." msgstr "" -#. module: shopfloor -#: model:ir.model,name:shopfloor.model_stock_move_line -msgid "Product Moves (Stock Move Line)" -msgstr "" - #. module: shopfloor #. odoo-python #: code:addons/shopfloor/actions/message.py:0 #, python-format -msgid "Product is not in the current transfer." +msgid "Product %s is not in the current transfer." +msgstr "" + +#. module: shopfloor +#: model:ir.model,name:shopfloor.model_stock_move_line +msgid "Product Moves (Stock Move Line)" msgstr "" #. module: shopfloor @@ -1292,6 +1313,13 @@ msgstr "" msgid "Reserved Move Line" msgstr "" +#. module: shopfloor +#. odoo-python +#: code:addons/shopfloor/actions/message.py:0 +#, python-format +msgid "Reserved for %(picking_type)s %(picking_name)s" +msgstr "" + #. module: shopfloor #. odoo-python #: code:addons/shopfloor/actions/message.py:0 @@ -1891,6 +1919,14 @@ msgstr "" msgid "Transfer" msgstr "" +#. module: shopfloor +#. odoo-python +#: code:addons/shopfloor/actions/message.py:0 +#, python-format +msgid "" +"Transfer has been canceled. This cannot be processed using this scenario" +msgstr "" + #. module: shopfloor #. odoo-python #: code:addons/shopfloor/actions/message.py:0 @@ -2147,14 +2183,6 @@ msgstr "" msgid "Zone Picking" msgstr "" -#. module: shopfloor -#. odoo-python -#: code:addons/shopfloor/actions/message.py:0 -#, python-format -msgid "" -"{message_code} not found in the current transfer or already in a package." -msgstr "" - #. module: shopfloor #. odoo-python #: code:addons/shopfloor/actions/message.py:0 diff --git a/shopfloor/i18n/shopfloor.pot b/shopfloor/i18n/shopfloor.pot index 2b73bb4472d..de47765895d 100644 --- a/shopfloor/i18n/shopfloor.pot +++ b/shopfloor/i18n/shopfloor.pot @@ -85,6 +85,14 @@ msgid "" "Incompatible with: \"Pick and pack at the same time\"\n" msgstr "" +#. module: shopfloor +#. odoo-python +#: code:addons/shopfloor/actions/message.py:0 +#, python-format +msgid "" +"%(message_code)s not found in the current transfer or already in a package." +msgstr "" + #. module: shopfloor #. odoo-python #: code:addons/shopfloor/actions/message.py:0 @@ -341,7 +349,8 @@ msgstr "" #. odoo-python #: code:addons/shopfloor/actions/message.py:0 #, python-format -msgid "Content transfer to {} completed" +msgid "" +"Content line transferred from %(location_name)s to %(location_dest_name)s" msgstr "" #. module: shopfloor @@ -519,6 +528,11 @@ msgstr "" msgid "Inventory Locations" msgstr "" +#. module: shopfloor +#: model:ir.model.fields,field_description:shopfloor.field_stock_picking__is_shopfloor_created +msgid "Is Shopfloor Created" +msgstr "" + #. module: shopfloor #. odoo-python #: code:addons/shopfloor/actions/completion_info.py:0 @@ -839,6 +853,20 @@ msgstr "" msgid "No quantity has been processed, unable to complete the transfer." msgstr "" +#. module: shopfloor +#. odoo-python +#: code:addons/shopfloor/actions/message.py:0 +#, python-format +msgid "No transfer found for %(model_name)s %(record_name)s" +msgstr "" + +#. module: shopfloor +#. odoo-python +#: code:addons/shopfloor/actions/message.py:0 +#, python-format +msgid "No transfer found for barcode %s" +msgstr "" + #. module: shopfloor #. odoo-python #: code:addons/shopfloor/actions/message.py:0 @@ -1047,13 +1075,6 @@ msgstr "" msgid "Package {} is not empty." msgstr "" -#. module: shopfloor -#. odoo-python -#: code:addons/shopfloor/services/checkout.py:0 -#, python-format -msgid "Package {} is not in the current transfer." -msgstr "" - #. module: shopfloor #: model:ir.model,name:shopfloor.model_stock_quant_package msgid "Packages" @@ -1209,16 +1230,16 @@ msgid "" " %(picking_name)s." msgstr "" -#. module: shopfloor -#: model:ir.model,name:shopfloor.model_stock_move_line -msgid "Product Moves (Stock Move Line)" -msgstr "" - #. module: shopfloor #. odoo-python #: code:addons/shopfloor/actions/message.py:0 #, python-format -msgid "Product is not in the current transfer." +msgid "Product %s is not in the current transfer." +msgstr "" + +#. module: shopfloor +#: model:ir.model,name:shopfloor.model_stock_move_line +msgid "Product Moves (Stock Move Line)" msgstr "" #. module: shopfloor @@ -1289,6 +1310,13 @@ msgstr "" msgid "Reserved Move Line" msgstr "" +#. module: shopfloor +#. odoo-python +#: code:addons/shopfloor/actions/message.py:0 +#, python-format +msgid "Reserved for %(picking_type)s %(picking_name)s" +msgstr "" + #. module: shopfloor #. odoo-python #: code:addons/shopfloor/actions/message.py:0 @@ -1890,6 +1918,14 @@ msgstr "" msgid "Transfer" msgstr "" +#. module: shopfloor +#. odoo-python +#: code:addons/shopfloor/actions/message.py:0 +#, python-format +msgid "" +"Transfer has been canceled. This cannot be processed using this scenario" +msgstr "" + #. module: shopfloor #. odoo-python #: code:addons/shopfloor/actions/message.py:0 @@ -2146,14 +2182,6 @@ msgstr "" msgid "Zone Picking" msgstr "" -#. module: shopfloor -#. odoo-python -#: code:addons/shopfloor/actions/message.py:0 -#, python-format -msgid "" -"{message_code} not found in the current transfer or already in a package." -msgstr "" - #. module: shopfloor #. odoo-python #: code:addons/shopfloor/actions/message.py:0 diff --git a/shopfloor/migrations/16.0.2.0.0/post-migration.py b/shopfloor/migrations/16.0.2.0.0/post-migration.py index 62567a7edba..d2224a71855 100644 --- a/shopfloor/migrations/16.0.2.0.0/post-migration.py +++ b/shopfloor/migrations/16.0.2.0.0/post-migration.py @@ -37,7 +37,5 @@ def _enable_option_in_menus(menus): for menu in menus: menu.allow_alternative_destination_package = True _logger.info( - "Option allow_alternative_destination_package enabled for menu {}".format( - menu.name - ) + f"Option allow_alternative_destination_package enabled for menu {menu.name}" ) diff --git a/shopfloor/models/shopfloor_menu.py b/shopfloor/models/shopfloor_menu.py index 5be9d8f1af4..b41787f73cd 100644 --- a/shopfloor/models/shopfloor_menu.py +++ b/shopfloor/models/shopfloor_menu.py @@ -253,7 +253,8 @@ class ShopfloorMenu(models.Model): string="Destination package required", default=True, help="If set, the user will have to scan only the source location " - "and the destination location to process a line. The unload step will be skipped.", + "and the destination location to process a line. " + "The unload step will be skipped.", ) require_destination_package_is_possible = fields.Boolean( @@ -548,7 +549,8 @@ def _check_move_line_search_sort_order_custom_code(self): ): raise exceptions.ValidationError( _( - "Custom sort key code is required when 'Custom code' is selected." + "Custom sort key code is required " + "when 'Custom code' is selected." ) ) if ( @@ -557,7 +559,7 @@ def _check_move_line_search_sort_order_custom_code(self): ): raise exceptions.ValidationError( _( - "Custom sort key code is only allowed when 'Custom code' is selected." + "Custom sort key code is only allowed when 'Custom code' is selected." # noqa ) ) code = ( diff --git a/shopfloor/models/stock_picking.py b/shopfloor/models/stock_picking.py index ea805dd9a33..6c2dbe96bef 100644 --- a/shopfloor/models/stock_picking.py +++ b/shopfloor/models/stock_picking.py @@ -20,8 +20,10 @@ class StockPicking(models.Model): ) bulk_line_count = fields.Integer( compute="_compute_picking_info", - help="Technical field. Indicates number of move lines without package included.", + help="Technical field. " + "Indicates number of move lines without package included.", ) + is_shopfloor_created = fields.Boolean() @api.depends( "move_line_ids", "move_line_ids.reserved_qty", "move_line_ids.product_id.weight" diff --git a/shopfloor/services/checkout.py b/shopfloor/services/checkout.py index 232e1e96613..b7b81048533 100644 --- a/shopfloor/services/checkout.py +++ b/shopfloor/services/checkout.py @@ -214,25 +214,29 @@ def scan_document(self, barcode): * summary: stock.picking is selected and all its lines have a destination pack set """ - search_result = self._scan_document_find(barcode) - result_handler = getattr(self, "_select_document_from_" + search_result.type) - return result_handler(search_result.record) + handlers = { + "picking": self._select_document_from_picking, + "location": self._select_document_from_location, + "package": self._select_document_from_package, + "packaging": self._select_document_from_packaging, + "product": self._select_document_from_product, + "none": self._select_document_from_none, + } + if self.work.menu.scan_location_or_pack_first: + handlers.pop("product") + search_result = self._scan_document_find(barcode, handlers.keys()) + # Keep track of what has been initially scan, and forward it through kwargs + kwargs = { + "barcode": barcode, + "current_state": "select_document", + "scanned_record": search_result.record, + } + handler = handlers.get(search_result.type, self._select_document_from_none) + return handler(search_result.record, **kwargs) - def _scan_document_find(self, barcode, search_types=None): + def _scan_document_find(self, barcode, search_types): search = self._actions_for("search") - search_types = ( - "picking", - "location", - "package", - "packaging", - ) + (("product",) if not self.work.menu.scan_location_or_pack_first else ()) - return search.find( - barcode, - types=search_types, - ) - - def _select_document_from_picking(self, picking, **kw): - return self._select_picking(picking, "select_document") + return search.find(barcode, types=search_types) def _select_document_from_location(self, location, **kw): if not self.is_src_location_valid(location): @@ -251,7 +255,9 @@ def _select_document_from_location(self, location, **kw): ), } ) - return self._select_picking(pickings, "select_document") + # Keep track of what has been initially scan, and forward it through kwargs + kwargs = {**kw, "current_state": "select_document"} + return self._select_document_from_picking(pickings, **kwargs) def _select_document_from_package(self, package, **kw): pickings = package.move_line_ids.filtered( @@ -260,14 +266,15 @@ def _select_document_from_package(self, package, **kw): if len(pickings) > 1: # Filter only if we find several pickings to narrow the # selection to one of the good type. If we have one picking - # of the wrong type, it will be caught in _select_picking + # of the wrong type, it will be caught in _select_document_from_picking # with the proper error message. # Side note: rather unlikely to have several transfers ready # and moving the same things pickings = pickings.filtered( lambda p: p.picking_type_id in self.picking_types ) - return self._select_picking(fields.first(pickings), "select_document") + kwargs = {**kw, "current_state": "select_document"} + return self._select_document_from_picking(fields.first(pickings), **kwargs) def _select_document_from_product(self, product, line_domain=None, **kw): line_domain = line_domain or [] @@ -287,7 +294,8 @@ def _select_document_from_product(self, product, line_domain=None, **kw): order="priority desc, scheduled_date asc, id desc", limit=1, ) - return self._select_picking(picking, "select_document") + kwargs = {**kw, "current_state": "select_document"} + return self._select_document_from_picking(picking, **kwargs) def _select_document_from_packaging(self, packaging, **kw): # And retrieve its product @@ -298,35 +306,33 @@ def _select_document_from_packaging(self, packaging, **kw): line_domain = [("reserved_uom_qty", ">=", packaging.qty)] return self._select_document_from_product(product, line_domain=line_domain) - def _select_document_from_none(self, picking, **kw): + def _select_document_from_none(self, *args, barcode=None, **kwargs): """Handle result when no record is found.""" - return self._select_picking(picking, "select_document") + return self._response_for_select_document( + message=self.msg_store.transfer_not_found_for_barcode(barcode) + ) - def _select_picking(self, picking, state_for_error): + def _select_document_from_picking( + self, picking, current_state=None, barcode=None, **kwargs + ): + # Get origin record to give more context to the user when raising an error + # as we got picking from product/package/packaging/... + scanned_record = kwargs.get("scanned_record") if not picking: - if state_for_error == "manual_selection": - return self._response_for_manual_selection( - message=self.msg_store.stock_picking_not_found() - ) - return self._response_for_select_document( - message=self.msg_store.barcode_not_found() - ) + message = self.msg_store.transfer_not_found_for_record(scanned_record) + if current_state == "manual_selection": + return self._response_for_manual_selection(message=message) + return self._response_for_select_document(message=message) if picking.picking_type_id not in self.picking_types: - if state_for_error == "manual_selection": - return self._response_for_manual_selection( - message=self.msg_store.cannot_move_something_in_picking_type() - ) - return self._response_for_select_document( - message=self.msg_store.cannot_move_something_in_picking_type() - ) + message = self.msg_store.reserved_for_other_picking_type(picking) + if current_state == "manual_selection": + return self._response_for_manual_selection(message=message) + return self._response_for_select_document(message=message) if picking.state != "assigned": - if state_for_error == "manual_selection": - return self._response_for_manual_selection( - message=self.msg_store.stock_picking_not_available(picking) - ) - return self._response_for_select_document( - message=self.msg_store.stock_picking_not_available(picking) - ) + message = self.msg_store.stock_picking_not_available(picking) + if current_state == "manual_selection": + return self._response_for_manual_selection(message=message) + return self._response_for_select_document(message=message) return self._response_for_select_line(picking) def _data_for_move_lines(self, lines, **kw): @@ -360,7 +366,7 @@ def _lines_to_pack(self, picking): return picking.move_line_ids.filtered(self._filter_lines_unpacked) def _lines_prepare(self, picking, selected_lines): - """Hook to manipulate lines' ordering or anything else before sending them back.""" + """Hook to manipulate lines' ordering or anything else.""" return selected_lines def _domain_for_list_stock_picking(self): @@ -402,10 +408,17 @@ def select(self, picking_id): lines """ picking = self.env["stock.picking"].browse(picking_id) - message = self._check_picking_status(picking) + message = self._check_picking_processible(picking) if message: return self._response_for_manual_selection(message=message) - return self._select_picking(picking, "manual_selection") + # Because _select_document_from_picking expects some context + # to give meaningful infos to the user, add some here. + kwargs = { + "current_state": "manual_selection", + "barcode": picking.name, + "scanned_record": picking, + } + return self._select_document_from_picking(picking, **kwargs) def _select_lines(self, lines, prefill_qty=0, related_lines=None): for i, line in enumerate(lines): @@ -426,7 +439,7 @@ def _select_lines(self, lines, prefill_qty=0, related_lines=None): return lines def _deselect_lines(self, lines): - lines.filtered(lambda l: not l.shopfloor_checkout_done).write( + lines.filtered(lambda x: not x.shopfloor_checkout_done).write( {"qty_done": 0, "shopfloor_user_id": False} ) @@ -451,7 +464,7 @@ def scan_line(self, picking_id, barcode, confirm_pack_all=False, confirm_lot=Non screen to change the qty done and destination pack if needed """ picking = self.env["stock.picking"].browse(picking_id) - message = self._check_picking_status(picking) + message = self._check_picking_processible(picking) if message: return self._response_for_select_document(message=message) @@ -460,21 +473,31 @@ def scan_line(self, picking_id, barcode, confirm_pack_all=False, confirm_lot=Non return self._response_for_summary(picking) # Search of the destination package - search_result = self._scan_line_find(picking, barcode) - result_handler = getattr(self, "_select_lines_from_" + search_result.type) - kw = {"confirm_pack_all": confirm_pack_all, "confirm_lot": confirm_lot} - return result_handler(picking, selection_lines, search_result.record, **kw) + handlers = { + "package": self._select_lines_from_package, + "product": self._select_lines_from_product, + "packaging": self._select_lines_from_packaging, + "lot": self._select_lines_from_lot, + "serial": self._select_lines_from_serial, + "delivery_packaging": self._select_lines_from_delivery_packaging, + "none": self._select_lines_from_none, + } + search_result = self._scan_line_find(picking, barcode, handlers.keys()) + # setting scanned record as kwarg in order to make better logs. + # The reason for this is that from a product we might select various records + # and lose track of what was initially scanned. This forces us to display + # standard messages that might have no meaning for the user. + kwargs = { + "confirm_pack_all": confirm_pack_all, + "confirm_lot": confirm_lot, + "scanned_record": search_result.record, + "barcode": barcode, + } + handler = handlers.get(search_result.type, self._select_lines_from_none) + return handler(picking, selection_lines, search_result.record, **kwargs) - def _scan_line_find(self, picking, barcode, search_types=None): + def _scan_line_find(self, picking, barcode, search_types): search = self._actions_for("search") - search_types = ( - "package", - "product", - "packaging", - "lot", - "serial", - "delivery_packaging", - ) return search.find( barcode, types=search_types, @@ -494,18 +517,17 @@ def _select_lines_from_package( self, picking, selection_lines, package, prefill_qty=0, **kw ): lines = selection_lines.filtered( - lambda l: l.package_id == package and not l.shopfloor_checkout_done + lambda x: x.package_id == package and not x.shopfloor_checkout_done ) if not lines: - return self._response_for_select_line( - picking, - message={ - "message_type": "error", - "body": _("Package {} is not in the current transfer.").format( - package.name - ), - }, - ) + # No line for scanned package in selected picking + # Check if there's any picking reserving this product. + return_picking = self._get_pickings_for_package(package, limit=1) + if return_picking: + message = self.msg_store.reserved_for_other_picking_type(return_picking) + else: + message = self.msg_store.package_not_found_in_picking(package, picking) + return self._response_for_select_line(picking, message=message) self._select_lines(lines, prefill_qty=prefill_qty) if self.work.menu.no_prefill_qty: lines = picking.move_line_ids @@ -520,11 +542,14 @@ def _select_lines_from_product( picking, message=self.msg_store.scan_lot_on_product_tracked_by_lot() ) - lines = selection_lines.filtered(lambda l: l.product_id == product) + lines = selection_lines.filtered(lambda x: x.product_id == product) if not lines: - return self._response_for_select_line( - picking, message=self.msg_store.product_not_found_in_current_picking() - ) + return_picking = self._get_pickings_for_product(product, limit=1) + if return_picking: + message = self.msg_store.reserved_for_other_picking_type(return_picking) + else: + message = self.msg_store.product_not_found_in_current_picking(product) + return self._response_for_select_line(picking, message=message) # When products are as units outside of packages, we can select them for # packing, but if they are in a package, we want the user to scan the packages. @@ -555,7 +580,7 @@ def _select_lines_from_product( # not in a package. But only the quantity on first selected lines # are updated. related_lines = selection_lines.filtered( - lambda l: not l.package_id and l.product_id != product + lambda x: not x.package_id and x.product_id != product ) lines = self._select_lines( @@ -605,7 +630,7 @@ def _select_lines_from_lot( # Change lot confirmed line = fields.first( selection_lines.filtered( - lambda l: l.product_id == lot.product_id and l.lot_id != lot + lambda x: x.product_id == lot.product_id and x.lot_id != lot ) ) if not line: @@ -664,7 +689,7 @@ def _select_lines_from_lot( def _picking_lines_by_lot(self, picking, selection_lines, lot): """Control filtering of selected lines by given lot.""" - return selection_lines.filtered(lambda l: l.lot_id == lot) + return selection_lines.filtered(lambda x: x.lot_id == lot) def _change_lot_response_handler_ok(self, move_line, message=None): return message @@ -673,7 +698,7 @@ def _change_lot_response_handler_error(self, move_line, message=None): return message def _select_lines_from_serial(self, picking, selection_lines, lot, **kw): - # Search for serial number is actually the same as searching for lot (as of v14...) + # Search for serial number is the same as searching for lot (as of v14) return self._select_lines_from_lot(picking, selection_lines, lot, **kw) # Handling of the destination package scanned @@ -750,7 +775,7 @@ def _select_line_move_line(self, picking, selection_lines, move_line): ) related_lines = selection_lines.filtered( - lambda l: not l.package_id and l.product_id != move_line.product_id + lambda x: not x.package_id and x.product_id != move_line.product_id ) lines = self._select_lines(move_line, related_lines=related_lines) return self._response_for_select_package(picking, lines) @@ -774,7 +799,7 @@ def select_line(self, picking_id, package_id=None, move_line_id=None): assert package_id or move_line_id picking = self.env["stock.picking"].browse(picking_id) - message = self._check_picking_status(picking) + message = self._check_picking_processible(picking) if message: return self._response_for_select_document(message=message) @@ -793,7 +818,7 @@ def _change_line_qty( self, picking_id, selected_line_ids, move_line_ids, quantity_func ): picking = self.env["stock.picking"].browse(picking_id) - message = self._check_picking_status(picking) + message = self._check_picking_processible(picking) if message: return self._response_for_select_document(message=message) @@ -859,7 +884,7 @@ def set_line_qty(self, picking_id, selected_line_ids, move_line_id): as selected """ return self._change_line_qty( - picking_id, selected_line_ids, [move_line_id], lambda l: l.reserved_uom_qty + picking_id, selected_line_ids, [move_line_id], lambda x: x.reserved_uom_qty ) def set_custom_qty(self, picking_id, selected_line_ids, move_line_id, qty_done): @@ -892,7 +917,7 @@ def _switch_line_qty_done(self, picking, selected_lines, switch_lines): picking.id, selected_lines.ids, switch_lines.ids, - lambda l: l.reserved_uom_qty, + lambda x: x.reserved_uom_qty, ) def _increment_custom_qty( @@ -1031,25 +1056,28 @@ def scan_package_action(self, picking_id, selected_line_ids, barcode): to close the stock picking """ picking = self.env["stock.picking"].browse(picking_id) - message = self._check_picking_status(picking) + message = self._check_picking_processible(picking) if message: return self._response_for_select_document(message=message) selected_lines = self.env["stock.move.line"].browse(selected_line_ids).exists() - search_result = self._scan_package_find(picking, barcode) - message = self._check_scan_package_find(picking, search_result) - if message: - return self._response_for_select_package( - picking, - selected_lines, - message=message, - ) - result_handler = getattr( - self, "_scan_package_action_from_" + search_result.type - ) - return result_handler(picking, selected_lines, search_result.record) + handlers = { + "package": self._scan_package_action_from_package, + "product": self._scan_package_action_from_product, + "packaging": self._scan_package_action_from_packaging, + "lot": self._scan_package_action_from_lot, + "serial": self._scan_package_action_from_serial, + "delivery_packaging": self._scan_package_action_from_delivery_packaging, + } + search_result = self._scan_package_find(picking, barcode, handlers.keys()) + handler = handlers.get(search_result.type, self._scan_package_action_from_none) + kwargs = { + "barcode": barcode, + "scanned_record": search_result.record, + } + return handler(picking, selected_lines, search_result.record, **kwargs) - def _scan_package_find(self, picking, barcode, search_types=None): + def _scan_package_find(self, picking, barcode, search_types): search = self._actions_for("search") search_types = ( "package", @@ -1068,10 +1096,6 @@ def _scan_package_find(self, picking, barcode, search_types=None): ), ) - def _check_scan_package_find(self, picking, search_result): - # Used by inheriting modules - return False - def _find_line_to_increment(self, product_lines): """Find which line should have its qty incremented. @@ -1093,7 +1117,7 @@ def _scan_package_action_from_product( selected_lines, message=self.msg_store.scan_lot_on_product_tracked_by_lot(), ) - product_lines = selected_lines.filtered(lambda l: l.product_id == product) + product_lines = selected_lines.filtered(lambda x: x.product_id == product) if self.work.menu.no_prefill_qty: quantity_increment = packaging.qty if packaging else 1 return self._increment_custom_qty( @@ -1112,7 +1136,7 @@ def _scan_package_action_from_packaging( ) def _scan_package_action_from_lot(self, picking, selected_lines, lot, **kw): - lot_lines = selected_lines.filtered(lambda l: l.lot_id == lot) + lot_lines = selected_lines.filtered(lambda x: x.lot_id == lot) if self.work.menu.no_prefill_qty: return self._increment_custom_qty( picking, selected_lines, self._find_line_to_increment(lot_lines), 1 @@ -1120,7 +1144,7 @@ def _scan_package_action_from_lot(self, picking, selected_lines, lot, **kw): return self._switch_line_qty_done(picking, selected_lines, lot_lines) def _scan_package_action_from_serial(self, picking, selection_lines, lot, **kw): - # Search for serial number is actually the same as searching for lot (as of v14...) + # Search serial number is actually the same as searching for lot (as of v14...) return self._scan_package_action_from_lot(picking, selection_lines, lot, **kw) def _scan_package_action_from_package(self, picking, selected_lines, package, **kw): @@ -1186,7 +1210,7 @@ def list_delivery_packaging(self, picking_id, selected_line_ids): * select_package: when no delivery packaging is available """ picking = self.env["stock.picking"].browse(picking_id) - message = self._check_picking_status(picking) + message = self._check_picking_processible(picking) if message: return self._response_for_select_document(message=message) selected_lines = self.env["stock.move.line"].browse(selected_line_ids).exists() @@ -1216,7 +1240,7 @@ def new_package(self, picking_id, selected_line_ids, package_type_id=None): * select_line: goes back to selection of lines to work on next lines """ picking = self.env["stock.picking"].browse(picking_id) - message = self._check_picking_status(picking) + message = self._check_picking_processible(picking) if message: return self._response_for_select_document(message=message) packaging = None @@ -1238,7 +1262,7 @@ def no_package(self, picking_id, selected_line_ids): if self.options.get("checkout__disable_no_package"): raise BadRequest("`checkout.no_package` endpoint is not enabled") picking = self.env["stock.picking"].browse(picking_id) - message = self._check_picking_status(picking) + message = self._check_picking_processible(picking) if message: return self._response_for_select_document(message=message) selected_lines = self.env["stock.move.line"].browse(selected_line_ids).exists() @@ -1270,7 +1294,7 @@ def list_dest_package(self, picking_id, selected_line_ids): * select_package: when no package is available """ picking = self.env["stock.picking"].browse(picking_id) - message = self._check_picking_status(picking) + message = self._check_picking_processible(picking) if message: return self._response_for_select_document(message=message) lines = self.env["stock.move.line"].browse(selected_line_ids).exists() @@ -1320,7 +1344,7 @@ def scan_dest_package(self, picking_id, selected_line_ids, barcode): * summary: all lines are put in packages """ picking = self.env["stock.picking"].browse(picking_id) - message = self._check_picking_status(picking) + message = self._check_picking_processible(picking) if message: return self._response_for_select_document(message=message) lines = self.env["stock.move.line"].browse(selected_line_ids).exists() @@ -1347,7 +1371,7 @@ def set_dest_package(self, picking_id, selected_line_ids, package_id): * summary: all lines are put in packages """ picking = self.env["stock.picking"].browse(picking_id) - message = self._check_picking_status(picking) + message = self._check_picking_processible(picking) if message: return self._response_for_select_document(message=message) lines = self.env["stock.move.line"].browse(selected_line_ids).exists() @@ -1374,7 +1398,7 @@ def summary(self, picking_id): * summary """ picking = self.env["stock.picking"].browse(picking_id) - message = self._check_picking_status(picking) + message = self._check_picking_processible(picking) if message: return self._response_for_select_document(message=message) return self._response_for_summary(picking) @@ -1393,7 +1417,7 @@ def list_packaging(self, picking_id, package_id): * summary: if the package_id no longer exists """ picking = self.env["stock.picking"].browse(picking_id) - message = self._check_picking_status(picking) + message = self._check_picking_processible(picking) if message: return self._response_for_select_document(message=message) package = self.env["stock.quant.package"].browse(package_id).exists() @@ -1408,7 +1432,7 @@ def set_packaging(self, picking_id, package_id, package_type_id): * summary """ picking = self.env["stock.picking"].browse(picking_id) - message = self._check_picking_status(picking) + message = self._check_picking_processible(picking) if message: return self._response_for_select_document(message=message) @@ -1445,7 +1469,7 @@ def cancel_line(self, picking_id, package_id=None, line_id=None): * select_line: when package or line has been canceled """ picking = self.env["stock.picking"].browse(picking_id) - message = self._check_picking_status(picking) + message = self._check_picking_processible(picking) if message: return self._response_for_select_document(message=message) @@ -1458,8 +1482,8 @@ def cancel_line(self, picking_id, package_id=None, line_id=None): if package: move_lines = picking.move_line_ids.filtered( - lambda l: self._filter_lines_checkout_done(l) - and l.result_package_id == package + lambda x: self._filter_lines_checkout_done(x) + and x.result_package_id == package ) for move_line in move_lines: move_line.write( @@ -1490,7 +1514,7 @@ def done(self, picking_id, confirmation=False): * select_child_location: there are child destination locations """ picking = self.env["stock.picking"].browse(picking_id) - message = self._check_picking_status(picking) + message = self._check_picking_processible(picking) if message: return self._response_for_select_document(message=message) lines = picking.move_line_ids @@ -1533,7 +1557,7 @@ def scan_dest_location(self, picking_id, barcode): * select_child_location: in case of error """ picking = self.env["stock.picking"].browse(picking_id) - message = self._check_picking_status(picking) + message = self._check_picking_processible(picking) if message: return self._response_for_select_document(message=message) search = self._actions_for("search") diff --git a/shopfloor/services/cluster_picking.py b/shopfloor/services/cluster_picking.py index f5568475311..7ec69baea24 100644 --- a/shopfloor/services/cluster_picking.py +++ b/shopfloor/services/cluster_picking.py @@ -97,7 +97,7 @@ def _response_for_start_line( self, move_line, message=None, popup=None, sublocation=None ): kw = {"sublocation": self.data.location(sublocation)} if sublocation else {} - data = self._data_move_line(move_line, **kw) + data = self._data_move_line(move_line, with_qty_available=False, **kw) return self._response( next_state="start_line", data=data, @@ -107,9 +107,14 @@ def _response_for_start_line( def _response_for_scan_destination(self, move_line, message=None, qty_done=None): if qty_done is None: - data = self._data_move_line(move_line) + data = self._data_move_line( + move_line, + with_qty_available=False, + ) else: - data = self._data_move_line(move_line, qty_done=qty_done) + data = self._data_move_line( + move_line, with_qty_available=False, qty_done=qty_done + ) last_picked_line = self._last_picked_line(move_line.picking_id) if last_picked_line: # suggest pack to be used for the next line @@ -364,13 +369,13 @@ def _lines_for_picking_batch(self, picking_batch, filter_func=lambda x: x): def _lines_to_pick(self, picking_batch): return self._lines_for_picking_batch( picking_batch, - filter_func=lambda l: ( - l.state in ("assigned", "partially_available") + filter_func=lambda x: ( + x.state in ("assigned", "partially_available") # On 'StockPicking.action_assign()', result_package_id is set to # the same package as 'package_id'. Here, we need to exclude lines # that were already put into a bin, i.e. the destination package # is different. - and (not l.result_package_id or l.result_package_id == l.package_id) + and (not x.result_package_id or x.result_package_id == x.package_id) ), ) @@ -378,11 +383,11 @@ def _last_picked_line(self, picking): """Get the last line picked and put in a pack for this picking""" return fields.first( picking.move_line_ids.filtered( - lambda l: l.qty_done > 0 - and l.result_package_id + lambda x: x.qty_done > 0 + and x.result_package_id # if we are moving the entire package, we shouldn't # add stuff inside it, it's not a new package - and l.package_id != l.result_package_id + and x.package_id != x.result_package_id ).sorted(key="write_date", reverse=True) ) @@ -393,7 +398,7 @@ def _next_line_for_pick(self, picking_batch): def _response_batch_does_not_exist(self): return self._response_for_start(message=self.msg_store.record_not_found()) - def _data_move_line(self, line, **kw): + def _data_move_line(self, line, with_qty_available=True, **kw): picking = line.picking_id batch = picking.batch_id product = line.product_id @@ -406,9 +411,10 @@ def _data_move_line(self, line, **kw): data["batch"] = self.data.picking_batch(batch) data["picking"] = self.data.picking(picking) data["postponed"] = line.shopfloor_postponed - data["product"]["qty_available"] = product.with_context( - location=line.location_id.id - ).qty_available + if with_qty_available: + data["product"]["qty_available"] = product.with_context( + location=line.location_id.id + ).qty_available data["scan_location_or_pack_first"] = self.work.menu.scan_location_or_pack_first data.update(kw) return data @@ -582,7 +588,7 @@ def _scan_line_by_product(self, picking, move_line, product, sublocation): def _scan_line_by_packaging(self, picking, move_line, packaging, sublocation): """Packaging scanned, check if we can work with it. - If the packaging related product is part of several packages in the same location, + If the packaging product is part of several packages in the same location, we can't be sure it's the correct one, in such case, ask to scan a package """ response = self._check_first_scan_location_or_pack_first(move_line, sublocation) @@ -595,7 +601,7 @@ def _scan_line_by_packaging(self, picking, move_line, packaging, sublocation): move_line, message=self.msg_store.scan_lot_on_product_tracked_by_lot() ) other_product_lines = picking.move_line_ids.filtered( - lambda l: l.product_id == product and l.location_id == move_line.location_id + lambda x: x.product_id == product and x.location_id == move_line.location_id ) packages = other_product_lines.mapped("package_id") # Do not use mapped here: we want to see if we have more than one package, @@ -1146,10 +1152,11 @@ def set_destination_all(self, picking_batch_id, barcode, confirmation=None): return self._unload_end(batch, completion_info_popup=completion_info_popup) def _unload_write_destination_on_lines(self, lines, location): - lines.write({"shopfloor_unloaded": True, "location_dest_id": location.id}) - lines.package_level_id.location_dest_id = location + stock = self._actions_for("stock") + stock.set_destination_on_lines(lines, location) + lines.write({"shopfloor_unloaded": True}) for picking in lines.batch_id.picking_ids: - picking_lines = lines.filtered(lambda l, p=picking: l.picking_id == p) + picking_lines = lines.filtered(lambda x, p=picking: x.picking_id == p) self._unload_set_picking_to_done(picking, picking_lines) def _unload_set_picking_to_done(self, picking, picking_lines): @@ -1274,7 +1281,7 @@ def unload_scan_destination( # we work only on the lines of the scanned package lines = self._lines_to_unload(batch).filtered( - lambda l: l.result_package_id == package + lambda x: x.result_package_id == package ) if not lines: return self._unload_end(batch) @@ -1283,15 +1290,11 @@ def unload_scan_destination( batch, package, lines, barcode, confirmation=confirmation ) - def _lock_lines(self, lines): - """Lock move lines""" - self._actions_for("lock").for_update(lines) - def _unload_scan_destination_lines( self, batch, package, lines, barcode, confirmation=None ): # Lock move lines that will be updated - self._lock_lines(lines) + self._actions_for("lock").for_update(lines) first_line = fields.first(lines) scanned_location = self._actions_for("search").location_from_scan(barcode) if not scanned_location: diff --git a/shopfloor/services/delivery.py b/shopfloor/services/delivery.py index 9de2aaa98c3..769affe9746 100644 --- a/shopfloor/services/delivery.py +++ b/shopfloor/services/delivery.py @@ -140,54 +140,61 @@ def scan_deliver(self, barcode, picking_id=None, location_id=None): barcode_valid = bool(picking) if picking: - message = self._check_picking_status(picking) + message = self._check_picking_processible(picking) if message: return self._response_for_deliver(location=location, message=message) if picking_id: picking = self.env["stock.picking"].browse(picking_id) - # Validate picking anyway if not barcode_valid: - package = search.package_from_scan(barcode) - if package: - return self._deliver_package(picking, package, location) - - if not barcode_valid: - product = search.product_from_scan(barcode) - if product: - return self._deliver_product( - picking, product, product_qty=1, location=location - ) - - if not barcode_valid: - packaging = search.packaging_from_scan(barcode) - if packaging: - # By scanning a packaging, we want to process - # the full quantity of the packaging - packaging_qty = packaging.product_uom_id._compute_quantity( - packaging.qty, packaging.product_id.uom_id - ) - return self._deliver_product( - picking, - packaging.product_id, - product_qty=packaging_qty, - location=location, - ) + handlers_by_type = { + "package": self._scan_deliver__by_package, + "product": self._scan_deliver__by_product, + "packaging": self._scan_deliver__by_packaging, + "lot": self._scan_deliver__by_lot, + "location": self._scan_deliver__by_location, + } + search_result = search.find(barcode, handlers_by_type.keys()) + handler = handlers_by_type.get(search_result.type) + if handler: + result = handler(search_result.record, picking, location) + if result: + return result + return self._scan_deliver__fallback(picking, location, barcode_valid) + + def _scan_deliver__by_package(self, package, picking, location): + return self._deliver_package(picking, package, location) + + def _scan_deliver__by_product(self, product, picking, location): + return self._deliver_product(picking, product, product_qty=1, location=location) + + def _scan_deliver__by_packaging(self, packaging, picking, location): + # By scanning a packaging, we want to process + # the full quantity of the packaging + packaging_qty = packaging.product_uom_id._compute_quantity( + packaging.qty, packaging.product_id.uom_id + ) + return self._deliver_product( + picking, + packaging.product_id, + product_qty=packaging_qty, + location=location, + ) - if not barcode_valid: - lot = search.lot_from_scan(barcode) - if lot: - return self._deliver_lot(picking, lot, product_qty=1, location=location) + def _scan_deliver__by_lot(self, lot, picking, location): + return self._deliver_lot(picking, lot, product_qty=1, location=location) - if not barcode_valid: - sublocation = search.location_from_scan(barcode) - if sublocation and sublocation.is_sublocation_of( - self.picking_types.mapped("default_location_src_id") - ): - message = self.msg_store.location_src_set_to_sublocation(sublocation) - return self._response_for_deliver(location=sublocation, message=message) + def _scan_deliver__by_location(self, scanned_location, picking, location): + if scanned_location.is_sublocation_of( + self.picking_types.mapped("default_location_src_id") + ): + message = self.msg_store.location_src_set_to_sublocation(scanned_location) + return self._response_for_deliver( + location=scanned_location, message=message + ) + def _scan_deliver__fallback(self, picking, location, barcode_valid): message = self.msg_store.barcode_not_found() if not barcode_valid else None return self._response_for_deliver( picking=picking, location=location, message=message @@ -226,8 +233,14 @@ def _reset_lines(self, lines): def _deliver_package(self, picking, package, location): lines = package.move_line_ids.filtered( - lambda l: l.state in ("assigned", "partially_available") + lambda x: x.state in ("assigned", "partially_available") ) + if not lines: + return self._response_for_deliver( + picking=picking, + location=location, + message=self.msg_store.cannot_move_something_in_picking_type(), + ) # State of the picking might change while we reach this point: check again! message = self._check_picking_status(lines.mapped("picking_id")) if message: @@ -240,12 +253,9 @@ def _deliver_package(self, picking, package, location): ] ) return self._response_for_deliver(location=location, message=message) - if not lines: - return self._response_for_deliver( - picking=picking, - location=location, - message=self.msg_store.cannot_move_something_in_picking_type(), - ) + message = self._check_picking_type(lines.mapped("picking_id")) + if message: + return self._response_for_deliver(location=location, message=message) # TODO add a message if any of the lines already had a qty_done > 0 new_picking = fields.first(lines.mapped("picking_id")) if self._set_lines_done(lines): @@ -255,12 +265,9 @@ def _deliver_package(self, picking, package, location): return self._response_for_deliver(picking=new_picking, location=location) def _lines_base_domain(self, no_qty_done=True): - domain = [ - # we added auto_join for this, otherwise, the ORM would search all pickings - # in the picking type, and then use IN (ids) - ("picking_id.picking_type_id", "in", self.picking_types.ids), - ("picking_id.state", "not in", ("done", "cancel")), - ] + # we added auto_join for this, otherwise, the ORM would search all pickings + # in the picking type, and then use IN (ids) + domain = [] if no_qty_done: domain.append(("qty_done", "=", 0)) return domain @@ -297,6 +304,16 @@ def _lines_from_product_domain( ) if location: domain.extend([("location_id", "=", location.id)]) + else: + domain.extend( + [ + ( + "location_id", + "child_of", + self.picking_types.default_location_src_id.ids, + ) + ] + ) if product_qty: domain.extend( [ @@ -351,6 +368,12 @@ def _deliver_product(self, picking, product, product_qty=None, location=None): message=self.msg_store.product_in_multiple_sublocation(product), ) + message = self._check_picking_type(lines.mapped("picking_id")) + if message: + return self._response_for_deliver(location=location, message=message) + lines = lines.filtered( + lambda x: x.move_id.picking_type_id in self.picking_types + ) # State of the picking might change while we reach this point: check again! message = self._check_picking_status(lines.mapped("picking_id")) if message: @@ -398,7 +421,7 @@ def _deliver_product(self, picking, product, product_qty=None, location=None): ) # We focus only on lines on which we can increase the 'qty_done' lines = lines.filtered( - lambda l: (l.qty_done + product_qty) <= l.reserved_uom_qty + lambda x: (x.qty_done + product_qty) <= x.reserved_uom_qty ) # Filter lines to keep only ones from one delivery operation # (we do not want to process lines of another delivery operation) @@ -413,15 +436,14 @@ def _deliver_product(self, picking, product, product_qty=None, location=None): return self._response_for_deliver(new_picking, location=location) def _deliver_lot(self, picking, lot, product_qty=None, location=None): - lines = self.env["stock.move.line"].search( - self._lines_from_lot_domain( - lot, - no_qty_done=False, - product_qty=product_qty, - location=location, - picking=picking, - ) + domain = self._lines_from_lot_domain( + lot, + no_qty_done=False, + product_qty=product_qty, + location=location, + picking=picking, ) + lines = self.env["stock.move.line"].search(domain) if not lines: return self._response_for_deliver( picking, @@ -439,6 +461,9 @@ def _deliver_lot(self, picking, lot, product_qty=None, location=None): message=self.msg_store.lot_in_multiple_sublocation(lot), ) + message = self._check_picking_type(lines.mapped("picking_id")) + if message: + return self._response_for_deliver(location=location, message=message) # State of the picking might change while we reach this point: check again! message = self._check_picking_status(lines.mapped("picking_id")) if message: @@ -549,7 +574,7 @@ def select(self, picking_id): * deliver: with information about the stock.picking """ picking = self.env["stock.picking"].browse(picking_id) - message = self._check_picking_status(picking) + message = self._check_picking_processible(picking) if message: return self.list_stock_picking(message=message) if picking: @@ -566,7 +591,7 @@ def set_qty_done_pack(self, picking_id, package_id, location_id=None): * deliver: always return here with updated data """ picking = self.env["stock.picking"].browse(picking_id) - message = self._check_picking_status(picking) + message = self._check_picking_processible(picking) if message: return self._response_for_deliver(message=message) package = self.env["stock.quant.package"].browse(package_id).exists() @@ -591,7 +616,7 @@ def set_qty_done_line(self, picking_id, move_line_id): * deliver: always return here with updated data """ picking = self.env["stock.picking"].browse(picking_id) - message = self._check_picking_status(picking) + message = self._check_picking_processible(picking) if message: return self._response_for_deliver(message=message) line = self.env["stock.move.line"].browse(move_line_id).exists() @@ -618,7 +643,7 @@ def reset_qty_done_pack(self, picking_id, package_id): * deliver: always return here with updated data """ picking = self.env["stock.picking"].browse(picking_id) - message = self._check_picking_status(picking) + message = self._check_picking_processible(picking) if message: return self._response_for_deliver(message=message) package = self.env["stock.quant.package"].browse(package_id).exists() @@ -651,7 +676,7 @@ def reset_qty_done_line(self, picking_id, move_line_id): * deliver: always return here with updated data """ picking = self.env["stock.picking"].browse(picking_id) - message = self._check_picking_status(picking) + message = self._check_picking_processible(picking) if message: return self._response_for_deliver(message=message) line = self.env["stock.move.line"].browse(move_line_id).exists() @@ -681,7 +706,7 @@ def done(self, picking_id, confirm=False): * confirm_done: when not all lines of the stock.picking are done """ picking = self.env["stock.picking"].browse(picking_id) - message = self._check_picking_status(picking) + message = self._check_picking_processible(picking) if message: return self._response_for_deliver(message=message) if self._action_picking_done(picking): diff --git a/shopfloor/services/location_content_transfer.py b/shopfloor/services/location_content_transfer.py index 9f5408bfde1..e68bdcc8b65 100644 --- a/shopfloor/services/location_content_transfer.py +++ b/shopfloor/services/location_content_transfer.py @@ -342,7 +342,9 @@ def scan_location(self, barcode): # noqa: C901 unreserved_moves = self.env["stock.move"].browse() if self.work.menu.allow_unreserve_other_moves: - message = unreserve.check_unreserve(location, move_lines) + message = unreserve.check_unreserve( + location, move_lines, allowed_types=self.picking_types + ) if message: return self._response_for_start(message=message) move_lines, unreserved_moves = unreserve.unreserve_moves( @@ -426,10 +428,9 @@ def _find_transfer_move_lines(self, location): ) return lines - # hook used in module shopfloor_checkout_sync def _write_destination_on_lines(self, lines, location): - lines.location_dest_id = location - lines.package_level_id.picking_id.location_dest_id = location + stock = self._actions_for("stock") + stock.set_destination_on_lines(lines, location) def _set_all_destination_lines_and_done(self, pickings, move_lines, dest_location): self._write_destination_on_lines(move_lines, dest_location) @@ -602,20 +603,35 @@ def scan_line(self, location_id, move_line_id, barcode): ) search = self._actions_for("search") + handlers = { + "package": self._scan_line__by_package, + "product": self._scan_line__by_product, + "packaging": self._scan_line__by_packaging, + "lot": self._scan_line__by_lot, + "none": self._scan_line__fallback, + } + search_result = search.find( + barcode, + types=handlers.keys(), + handler_kw=dict(lot=dict(products=move_line.product_id)), + ) + handler = handlers.get(search_result.type, self._scan_line__fallback) + # handler might've been called but returned no response. + # I.E. package is scanned but doesn't matches move_line's package. + # Call explicitely fallback in such case + response = handler(search_result.record, move_line, location) + return response or self._scan_line__fallback( + search_result.record, move_line, location + ) - package = search.package_from_scan(barcode) - if package and move_line.package_id == package: + def _scan_line__by_package(self, package, move_line, location): + if move_line.package_id == package: # In case we have a source package but no package level because if # we have a package level, we would use "scan_package". return self._response_for_scan_destination(location, move_line) - product = search.product_from_scan(barcode) - if not product: - packaging = search.packaging_from_scan(barcode) - if packaging: - product = packaging.product_id - - if product and product == move_line.product_id: + def _scan_line__by_product(self, product, move_line, location): + if product == move_line.product_id: if product.tracking in ("lot", "serial"): move_lines = self._find_transfer_move_lines(location) return self._response_for_start_single( @@ -625,18 +641,21 @@ def scan_line(self, location_id, move_line_id, barcode): else: return self._response_for_scan_destination(location, move_line) - lot = search.lot_from_scan(barcode, products=move_line.product_id) - if lot and lot == move_line.lot_id: + def _scan_line__by_packaging(self, packaging, move_line, location): + return self._scan_line__by_product(packaging.product_id, move_line, location) + + def _scan_line__by_lot(self, lot, move_line, location): + if lot == move_line.lot_id: return self._response_for_scan_destination(location, move_line) + def _scan_line__fallback(self, record, move_line, location): # Nothing matches what is expected from the move line. move_lines = self._find_transfer_move_lines(location) - for rec in (package, product, lot): - if rec: - return self._response_for_start_single( - move_lines.mapped("picking_id"), - message=self.msg_store.wrong_record(rec), - ) + if record: + return self._response_for_start_single( + move_lines.mapped("picking_id"), + message=self.msg_store.wrong_record(record), + ) return self._response_for_start_single( move_lines.mapped("picking_id"), message=self.msg_store.barcode_not_found() ) @@ -690,7 +709,7 @@ def set_destination_package( stock.validate_moves(package_moves) move_lines = self._find_transfer_move_lines(location) message = self.msg_store.location_content_transfer_item_complete( - scanned_location + location, scanned_location ) completion_info = self._actions_for("completion.info") completion_info_popup = completion_info.popup(package_moves.move_line_ids) @@ -757,7 +776,7 @@ def set_destination_line( else: move_lines = self._find_transfer_move_lines(move_line.location_id) message = self.msg_store.location_content_transfer_item_complete( - scanned_location + location, scanned_location ) completion_info = self._actions_for("completion.info") completion_info_popup = completion_info.popup(move_line) diff --git a/shopfloor/services/service.py b/shopfloor/services/service.py index 0c98e23ae03..09600a066ca 100644 --- a/shopfloor/services/service.py +++ b/shopfloor/services/service.py @@ -2,7 +2,8 @@ # Copyright 2020 Akretion (http://www.akretion.com) # Copyright 2020-2021 Jacques-Etienne Baudoux (BCIM) # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from odoo import _, exceptions +from odoo import _, exceptions, fields +from odoo.osv.expression import AND from odoo.addons.component.core import AbstractComponent @@ -27,13 +28,28 @@ def search_move_line(self): class BaseShopfloorProcess(AbstractComponent): - _inherit = "base.shopfloor.process" def _get_process_picking_types(self): """Return picking types for the menu""" return self.work.menu.picking_type_ids + def _get_pickings_base_domain(self): + return [ + ("state", "not in", ("done", "cancel")), + ("location_id", "child_of", self.picking_types.default_location_src_id.ids), + ] + + def _get_pickings_for_package(self, package, **kwargs): + domain = self._get_pickings_base_domain() + package_domain = [("move_line_ids.package_id", "=", package.id)] + return self.env["stock.picking"].search(AND([domain, package_domain]), **kwargs) + + def _get_pickings_for_product(self, product, **kwargs): + domain = self._get_pickings_base_domain() + product_domain = [("move_line_ids.product_id", "=", product.id)] + return self.env["stock.picking"].search(AND([domain, product_domain]), **kwargs) + @property def picking_types(self): if not hasattr(self.work, "picking_types"): @@ -72,21 +88,41 @@ def search_move_line(self): sort_order_custom_code=self.sort_order_custom_code, ) - def _check_picking_status(self, pickings, states=("assigned",)): - """Check if given pickings can be processed. + def _check_picking_consistency(self, pickings): + if not pickings.exists(): + return self.msg_store.stock_picking_not_found() + + def _check_picking_type(self, pickings): + """Check if the pickings have the right expected type.""" + if not any( + picking.picking_type_id in self.picking_types for picking in pickings + ): + return self.msg_store.reserved_for_other_picking_type( + fields.first(pickings) + ) - If the picking is already done, canceled or didn't belong to the - expected picking type, a message is returned. - """ - for picking in pickings: - if not picking.exists(): - return self.msg_store.stock_picking_not_found() - if picking.state == "done": - return self.msg_store.already_done() - if picking.state not in states: # the picking must be ready - return self.msg_store.stock_picking_not_available(picking) - if picking.picking_type_id not in self.picking_types: - return self.msg_store.cannot_move_something_in_picking_type() + def _check_picking_status(self, pickings, states=("assigned",)): + """Checks if the picking exists, is already done or canceled.""" + if not any(picking.state != "done" for picking in pickings): + return self.msg_store.already_done() + if not any(picking.state != "cancel" for picking in pickings): + return self.msg_store.transfer_canceled() + if not any( + picking.state in states for picking in pickings + ): # the picking must be ready + return self.msg_store.stock_picking_not_available(fields.first(pickings)) + + def _check_picking_processible(self, pickings, states=("assigned",)): + """Check if given pickings can be processed""" + message = self._check_picking_consistency(pickings) + if message: + return message + message = self._check_picking_type(pickings) + if message: + return message + message = self._check_picking_status(pickings, states=states) + if message: + return message def is_src_location_valid(self, location): """Check the source location is valid for given process. diff --git a/shopfloor/services/single_pack_transfer.py b/shopfloor/services/single_pack_transfer.py index e759f01788a..efe5844409c 100644 --- a/shopfloor/services/single_pack_transfer.py +++ b/shopfloor/services/single_pack_transfer.py @@ -1,6 +1,7 @@ # Copyright 2020-2021 Camptocamp SA (http://www.camptocamp.com) # Copyright 2020-2021 Jacques-Etienne Baudoux (BCIM) # Copyright 2020 Akretion (http://www.akretion.com) +# Copyright 2025 Michael Tietz (MT Software) # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). from odoo import fields @@ -126,7 +127,6 @@ def start(self, barcode, confirmation=None): message=self.msg_store.package_already_picked_by(package, picking) ) elif other_move_lines and self.work.menu.allow_unreserve_other_moves: - unreserved_moves = other_move_lines.move_id other_package_levels = other_move_lines.package_level_id other_package_levels.explode_package() @@ -192,7 +192,7 @@ def _create_package_level(self, package): "picking_id": picking.id, "package_id": package.id, "location_dest_id": picking.location_dest_id.id, - "company_id": self.env.company.id, + "company_id": picking.company_id.id, } ) picking.action_confirm() @@ -266,10 +266,8 @@ def _router_validate_success(self, package_level): return self._response_for_start(message=message, popup=completion_info_popup) def _set_destination_and_done(self, package_level, scanned_location): - # when writing the destination on the package level, it writes - # on the move lines - package_level.location_dest_id = scanned_location stock = self._actions_for("stock") + stock.set_destination_on_lines(package_level.move_line_ids, scanned_location) stock.put_package_level_in_move(package_level) stock.validate_moves(package_level.move_line_ids.move_id) diff --git a/shopfloor/services/zone_picking.py b/shopfloor/services/zone_picking.py index abf59873727..ff6512cfe2e 100644 --- a/shopfloor/services/zone_picking.py +++ b/shopfloor/services/zone_picking.py @@ -238,9 +238,11 @@ def _response_for_set_line_destination( data = self._data_for_move_line(move_line) data["move_line"].update(kw) data["confirmation_required"] = confirmation_required - data[ - "allow_alternative_destination_package" - ] = self.work.menu.allow_alternative_destination_package + # fmt: off + data["allow_alternative_destination_package"] = ( + self.work.menu.allow_alternative_destination_package + ) + # fmt: on data["handle_complete_mix_pack"] = self._handle_complete_mix_pack( move_line.package_id ) @@ -368,13 +370,15 @@ def _data_for_move_lines( data_move_line["handle_complete_mix_pack"] = handle_complete_mix_pack # `location_will_be_empty` flag states if, by processing this move line # and picking the product, the location will be emptied. - data_move_line[ - "location_will_be_empty" - ] = move_line.location_id.planned_qty_in_location_is_empty( - move_line.package_id.move_line_ids - if handle_complete_mix_pack - else move_line + # fmt: off + data_move_line["location_will_be_empty"] = ( + move_line.location_id.planned_qty_in_location_is_empty( + move_line.package_id.move_line_ids + if handle_complete_mix_pack + else move_line + ) ) + # fmt: on return data def _data_for_location(self, location, zone_location=None, picking_type=None): @@ -1039,7 +1043,7 @@ def _set_destination_package(self, move_line, quantity, package): ) return (package_changed, response) stock = self._actions_for("stock") - self._lock_lines(move_line) + stock._lock_lines(move_line) try: stock.mark_move_line_as_picked( move_line, quantity, package, check_user=True @@ -1140,8 +1144,8 @@ def set_destination( When the barcode is the product (or its packaging) or the lot on the line: * The done quantity is incremented by one or the packaging quantity. - The `handle_complete_mix_pack` option, when it is set to true. Will move all they - lines contained in the package of the move line passed in parameter. + The `handle_complete_mix_pack` option, when it is set to true, + will move all lines contained in the package of the move line given. Transitions: * select_line: destination has been set, showing the next lines to pick @@ -1214,7 +1218,6 @@ def set_destination( # When the barcode is a package package = search.package_from_scan(barcode) if package: - if not moving_full_quantity and move_line.package_id == package: # Check we're not using the source package as transfer package. message = self.msg_store.dest_package_not_valid(package) @@ -1230,7 +1233,7 @@ def set_destination( and move_line.result_package_id and move_line.result_package_id != package ): - # Check whether the user can move a whole package to a different package. + # Check whether the user can move a whole package to a different one. message = self.msg_store.package_transfer_not_allowed_scan_location() return self._response_for_set_line_destination( move_line, message=message, qty_done=quantity @@ -1582,11 +1585,10 @@ def set_destination_all(self, barcode, confirmation=None): return self._set_destination_all_response(buffer_lines, message=message) def _write_destination_on_lines(self, lines, location): - self._lock_lines(lines) - lines.location_dest_id = location - lines.package_level_id.location_dest_id = location + stock = self._actions_for("stock") + stock.set_destination_on_lines(lines, location) if self.work.menu.unload_package_at_destination: - lines.result_package_id = False + stock.unload_package(lines) def unload_split(self): """Indicates that now the buffer must be treated line per line @@ -1667,10 +1669,6 @@ def unload_scan_pack(self, package_id, barcode): unload_single_message=self.msg_store.barcode_no_match(package.name), ) - def _lock_lines(self, lines): - """Lock move lines""" - self._actions_for("lock").for_update(lines) - def unload_set_destination(self, package_id, barcode, confirmation=None): """Scan the final destination for move lines in the buffer with the destination package diff --git a/shopfloor/static/description/index.html b/shopfloor/static/description/index.html index a1160934b74..86a779984fb 100644 --- a/shopfloor/static/description/index.html +++ b/shopfloor/static/description/index.html @@ -3,7 +3,7 @@ -Shopfloor +README.rst -

-

Shopfloor

+
+ + +Odoo Community Association + +
+

Shopfloor

-

Beta License: AGPL-3 OCA/wms Translate me on Weblate Try me on Runboat

+

Beta License: AGPL-3 OCA/wms Translate me on Weblate Try me on Runboat

Shopfloor is a barcode scanner application for internal warehouse operations.

The application supports scenarios, to relate to Operation Types:

    @@ -408,7 +413,7 @@

    Shopfloor

-

Usage

+

Usage

An API key is created in the Demo data (for development), using the Demo user. The key to use in the HTTP header API-KEY is: 72B044F7AC780DAC

Curl example:

@@ -417,7 +422,7 @@

Usage

-

Known issues / Roadmap

+

Known issues / Roadmap

  • improve documentation
  • split out scenario components to their own modules
  • @@ -426,14 +431,14 @@

    Known issues / Roadmap

-

Changelog

+

Changelog

-

13.0.1.0.0

+

13.0.1.0.0

First official version.

-

Bug Tracker

+

Bug Tracker

Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed @@ -441,9 +446,9 @@

Bug Tracker

Do not contact contributors directly about support or help with technical issues.

-

Credits

+

Credits

-

Authors

+

Authors

  • Camptocamp
  • BCIM
  • @@ -451,7 +456,7 @@

    Authors

-

Contributors

+

Contributors

-

Design

+

Design

-

Other credits

+

Other credits

Financial support

-

Maintainers

+

Maintainers

This module is maintained by the OCA.

Odoo Community Association @@ -499,5 +504,6 @@

Maintainers

+
diff --git a/shopfloor/tests/common.py b/shopfloor/tests/common.py index 4197428c138..516869f9e35 100644 --- a/shopfloor/tests/common.py +++ b/shopfloor/tests/common.py @@ -268,7 +268,6 @@ def _create_lot(self, product): class PickingBatchMixin: - BatchProduct = namedtuple( "BatchProduct", # browse record of the product, diff --git a/shopfloor/tests/models.py b/shopfloor/tests/models.py index 63bbee06ec6..8531412abd2 100644 --- a/shopfloor/tests/models.py +++ b/shopfloor/tests/models.py @@ -7,6 +7,7 @@ Shopfloor do not depend on 'delivery_*' modules adding the different delivery types. """ + from odoo import fields, models diff --git a/shopfloor/tests/test_actions_change_package_lot.py b/shopfloor/tests/test_actions_change_package_lot.py index ebc4931f838..909492e1d74 100644 --- a/shopfloor/tests/test_actions_change_package_lot.py +++ b/shopfloor/tests/test_actions_change_package_lot.py @@ -716,9 +716,7 @@ def test_change_pack_different_location_reserved_package_qty_done(self): # still we have to handle it). Forbid to pick. expected_message = self.msg_store.package_change_error( new_package, - "Package {} has been partially picked in another location".format( - new_package.display_name - ), + f"Package {new_package.display_name} has been partially picked in another location", # noqa ) self.change_package_lot.change_package( line, @@ -921,6 +919,10 @@ def test_other_line_with_qty_done(self): line2 = picking2.move_line_ids line2.qty_done = 10 + expected_msg = ( + f"Package {package2.display_name} does not contain available product " + f"{line1.product_id.display_name}, cannot replace package." + ) self.change_package_lot.change_package( line1, package2, @@ -929,13 +931,7 @@ def test_other_line_with_qty_done(self): # failure callback lambda move_line, message=None: self.assertEqual( message, - self.msg_store.package_change_error( - package2, - "Package {} does not contain available product {}," - " cannot replace package.".format( - package2.display_name, line1.product_id.display_name - ), - ), + self.msg_store.package_change_error(package2, expected_msg), ), ) diff --git a/shopfloor/tests/test_actions_data.py b/shopfloor/tests/test_actions_data.py index b3979103635..22717b1d36a 100644 --- a/shopfloor/tests/test_actions_data.py +++ b/shopfloor/tests/test_actions_data.py @@ -153,7 +153,10 @@ def test_data_picking(self): "weight": 110.0, "partner": {"id": self.customer.id, "name": self.customer.name}, "carrier": {"id": carrier.id, "name": carrier.name}, - "ship_carrier": None, + "ship_carrier": { + "id": self.picking.ship_carrier_id.id, + "name": self.picking.ship_carrier_id.name, + }, "priority": "0", } self.assertEqual(data.pop("scheduled_date").split("T")[0], "2020-08-03") @@ -177,7 +180,10 @@ def test_data_picking_with_progress(self): "weight": 110.0, "partner": {"id": self.customer.id, "name": self.customer.name}, "carrier": {"id": carrier.id, "name": carrier.name}, - "ship_carrier": None, + "ship_carrier": { + "id": self.picking.ship_carrier_id.id, + "name": self.picking.ship_carrier_id.name, + }, "progress": 0.0, "priority": "0", } diff --git a/shopfloor/tests/test_actions_data_base.py b/shopfloor/tests/test_actions_data_base.py index 4b3e3aa66c2..49c8e0d3846 100644 --- a/shopfloor/tests/test_actions_data_base.py +++ b/shopfloor/tests/test_actions_data_base.py @@ -219,7 +219,7 @@ def _expected_location_detail(self, record, **kw): kw.get("move_lines", []) ), "products": self.data_detail._location_content(record), - } + }, ) def _expected_location_lot(self, record, **kw): @@ -228,7 +228,7 @@ def _expected_location_lot(self, record, **kw): **{ "removal_date": record.removal_date or None, "quantity": sum(record.quant_ids.mapped("quantity")), - } + }, ) def _expected_product_detail(self, record, **kw): @@ -262,7 +262,7 @@ def _expected_product_detail(self, record, **kw): if kw.get("full"): detail.update( { - "image": "/web/image/product.product/{}/image_128".format(record.id) + "image": f"/web/image/product.product/{record.id}/image_128" if record.image_128 else None, "locations": locations_info, @@ -296,5 +296,5 @@ def _expected_packaging_detail(self, record, **kw): "length_uom": record.length_uom_name, "weight_uom": record.weight_uom_name, "barcode": record.barcode, - } + }, ) diff --git a/shopfloor/tests/test_actions_data_detail.py b/shopfloor/tests/test_actions_data_detail.py index 9cda26bbd70..d149640a286 100644 --- a/shopfloor/tests/test_actions_data_detail.py +++ b/shopfloor/tests/test_actions_data_detail.py @@ -122,7 +122,10 @@ def test_data_picking(self): "name": picking.name, "note": Markup("

read me

"), "origin": "created by test", - "ship_carrier": None, + "ship_carrier": { + "id": picking.ship_carrier_id.id, + "name": picking.ship_carrier_id.name, + }, "weight": 110.0, "partner": {"id": self.customer.id, "name": self.customer.name}, "carrier": {"id": picking.carrier_id.id, "name": picking.carrier_id.name}, @@ -159,7 +162,10 @@ def test_data_picking_with_progress(self): "name": picking.name, "note": Markup("

read me

"), "origin": "created by test", - "ship_carrier": None, + "ship_carrier": { + "id": picking.ship_carrier_id.id, + "name": picking.ship_carrier_id.name, + }, "weight": 110.0, "partner": {"id": self.customer.id, "name": self.customer.name}, "carrier": {"id": picking.carrier_id.id, "name": picking.carrier_id.name}, diff --git a/shopfloor/tests/test_checkout_auto_post.py b/shopfloor/tests/test_checkout_auto_post.py index 79bb1473b87..323e39dccde 100644 --- a/shopfloor/tests/test_checkout_auto_post.py +++ b/shopfloor/tests/test_checkout_auto_post.py @@ -12,20 +12,21 @@ def test_auto_posting(self): self._fill_stock_for_moves(picking.move_ids) picking.action_assign() selected_move_line_a = picking.move_line_ids.filtered( - lambda l: l.product_id == self.product_a + lambda x: x.product_id == self.product_a ) selected_move_line_a.qty_done = 7 selected_move_line_b = picking.move_line_ids.filtered( - lambda l: l.product_id == self.product_b + lambda x: x.product_id == self.product_b ) selected_move_line_b.qty_done = 9 selected_move_line_c = picking.move_line_ids.filtered( - lambda l: l.product_id == self.product_c + lambda x: x.product_id == self.product_c ) # User has selected 7 units out of 10 for product_a, # and 9 units out of 20 for product_b. - # We would expect a split picking to be created with those two lines and qtys done. + # We would expect a split picking to be created + # with those two lines and qtys done. self.service.dispatch( "scan_package_action", params={ @@ -53,10 +54,10 @@ def test_auto_posting(self): # - the original line for product c, unchanged; # - two lines (products a and b) with the non-split qtys. line_a_in_original_picking = picking.move_line_ids.filtered( - lambda l: l.product_id == selected_move_line_a.product_id + lambda x: x.product_id == selected_move_line_a.product_id ) line_b_in_original_picking = picking.move_line_ids.filtered( - lambda l: l.product_id == selected_move_line_b.product_id + lambda x: x.product_id == selected_move_line_b.product_id ) self.assertEqual(line_a_in_original_picking.reserved_uom_qty, 3) self.assertEqual(line_b_in_original_picking.reserved_uom_qty, 11) diff --git a/shopfloor/tests/test_checkout_base.py b/shopfloor/tests/test_checkout_base.py index 65761028b43..b4601ce9633 100644 --- a/shopfloor/tests/test_checkout_base.py +++ b/shopfloor/tests/test_checkout_base.py @@ -91,11 +91,6 @@ def _assert_select_package_qty_above(self, response, picking): "message_type": "warning", "body": "The quantity scanned for one or more lines cannot be " "higher than the maximum allowed. " - "(%(product_name)s : %(quantity_done)s > %(quantity_reserved)s)" - % dict( - product_name=line.product_id.name, - quantity_done=str(line.qty_done), - quantity_reserved=str(line.reserved_uom_qty), - ), + f"({line.product_id.name} : {str(line.qty_done)} > {str(line.reserved_uom_qty)})", # noqa }, ) diff --git a/shopfloor/tests/test_checkout_change_packaging.py b/shopfloor/tests/test_checkout_change_packaging.py index 8d0bf907f08..f4d2ccc4f6d 100644 --- a/shopfloor/tests/test_checkout_change_packaging.py +++ b/shopfloor/tests/test_checkout_change_packaging.py @@ -135,7 +135,7 @@ def test_set_packaging_ok(self): }, message={ "message_type": "success", - "body": "Packaging changed on package {}".format(self.package.name), + "body": f"Packaging changed on package {self.package.name}", }, ) diff --git a/shopfloor/tests/test_checkout_done.py b/shopfloor/tests/test_checkout_done.py index 3e2e986b6f1..bf38c37ad91 100644 --- a/shopfloor/tests/test_checkout_done.py +++ b/shopfloor/tests/test_checkout_done.py @@ -20,7 +20,7 @@ def test_done_ok(self): next_state="select_document", message={ "message_type": "success", - "body": "Transfer {} done".format(picking.name), + "body": f"Transfer {picking.name} done", }, data={"restrict_scan_first": False}, ) diff --git a/shopfloor/tests/test_checkout_list_delivery_packaging.py b/shopfloor/tests/test_checkout_list_delivery_packaging.py index ad0b758dc38..832004de4e9 100644 --- a/shopfloor/tests/test_checkout_list_delivery_packaging.py +++ b/shopfloor/tests/test_checkout_list_delivery_packaging.py @@ -31,7 +31,7 @@ def _load_test_models(cls): @classmethod def tearDownClass(cls): cls.loader.restore_registry() - super(CheckoutListDeliveryPackagingCase, cls).tearDownClass() + super().tearDownClass() @classmethod def setUpClassBaseData(cls, *args, **kwargs): diff --git a/shopfloor/tests/test_checkout_scan.py b/shopfloor/tests/test_checkout_scan.py index c980814ca58..457ff82f59b 100644 --- a/shopfloor/tests/test_checkout_scan.py +++ b/shopfloor/tests/test_checkout_scan.py @@ -41,7 +41,10 @@ def test_scan_document_with_option_product_not_ok(self): self.assert_response( response, next_state="select_document", - message={"message_type": "error", "body": "Barcode not found"}, + message={ + "message_type": "error", + "body": "No transfer found for barcode A", + }, data={"restrict_scan_first": True}, ) @@ -56,7 +59,10 @@ def test_scan_document_error_not_found(self): self.assert_response( response, next_state="select_document", - message={"message_type": "error", "body": "Barcode not found"}, + message={ + "message_type": "error", + "body": "No transfer found for barcode NOPE", + }, data={"restrict_scan_first": False}, ) @@ -78,7 +84,7 @@ def _test_scan_document_error_not_available(self, barcode_func): next_state="select_document", message={ "message_type": "error", - "body": "Transfer {} is not available.".format(picking.name), + "body": f"Transfer {picking.name} is not available.", }, data={"restrict_scan_first": False}, ) @@ -117,12 +123,14 @@ def _test_scan_document_error_different_picking_type(self, barcode_func): picking.action_assign() barcode = barcode_func(picking) response = self.service.dispatch("scan_document", params={"barcode": barcode}) + picking_name = picking.name + type_name = picking.picking_type_id.name self.assert_response( response, next_state="select_document", message={ "message_type": "error", - "body": "You cannot move this using this menu.", + "body": f"Reserved for {type_name} {picking_name}", }, data={"restrict_scan_first": False}, ) diff --git a/shopfloor/tests/test_checkout_scan_line.py b/shopfloor/tests/test_checkout_scan_line.py index 42e4f67ab8f..0414062a67e 100644 --- a/shopfloor/tests/test_checkout_scan_line.py +++ b/shopfloor/tests/test_checkout_scan_line.py @@ -67,7 +67,7 @@ def test_scan_line_product_ok(self): picking.action_assign() # The product a is scanned, so selected and quantity updated line_a = picking.move_line_ids.filtered( - lambda l: l.product_id == self.product_a + lambda x: x.product_id == self.product_a ) # Because not part of a package other lines are selected also related_lines = picking.move_line_ids - line_a @@ -84,7 +84,7 @@ def test_scan_line_product_several_lines_ok(self): picking.action_assign() # The product a is scanned, so selected and quantity updated lines_a = picking.move_line_ids.filtered( - lambda l: l.product_id == self.product_a + lambda x: x.product_id == self.product_a ) # Because not part of a package other lines are selected also related_lines = picking.move_line_ids - lines_a @@ -100,7 +100,7 @@ def test_scan_line_product_packaging_ok(self): self._fill_stock_for_moves(picking.move_ids) picking.action_assign() lines_a = picking.move_line_ids.filtered( - lambda l: l.product_id == self.product_a + lambda x: x.product_id == self.product_a ) # when we scan the packaging of the product, we should select the # lines as if the product was scanned @@ -164,6 +164,24 @@ def test_scan_line_error_barcode_not_found(self): ) def test_scan_line_error_package_not_in_picking(self): + picking = self._create_picking(lines=[(self.product_a, 10)]) + self._fill_stock_for_moves(picking.move_ids, in_package=True) + picking.action_assign() + # Create a package for product_a + package = self._create_package_in_location( + picking.location_id, [(self.product_a, 10, None)] + ) + # we work with picking, but we scan another package (not in a pick) + self._test_scan_line_error( + picking, + package.name, + { + "message_type": "error", + "body": f"Package {package.name} not found in transfer {picking.name}", # noqa + }, + ) + + def test_scan_line_error_package_reserved_by_another_picking(self): picking = self._create_picking(lines=[(self.product_a, 10)]) self._fill_stock_for_moves(picking.move_ids, in_package=True) picking2 = self._create_picking(lines=[(self.product_a, 10)]) @@ -176,9 +194,7 @@ def test_scan_line_error_package_not_in_picking(self): package.name, { "message_type": "error", - "body": "Package {} is not in the current transfer.".format( - package.name - ), + "body": f"Reserved for Checkout {picking2.name}", }, ) @@ -247,7 +263,22 @@ def test_scan_line_error_product_not_in_picking(self): self.product_b.barcode, { "message_type": "error", - "body": "Product is not in the current transfer.", + "body": "Product Product B is not in the current transfer.", + }, + ) + + def test_scan_line_error_product_in_another_picking(self): + picking = self._create_picking(lines=[(self.product_a, 10)]) + self._fill_stock_for_moves(picking.move_ids, in_package=True) + picking2 = self._create_picking(lines=[(self.product_b, 10)]) + self._fill_stock_for_moves(picking2.move_ids, in_package=True) + (picking | picking2).action_assign() + self._test_scan_line_error( + picking, + self.product_b.barcode, + { + "message_type": "error", + "body": f"Reserved for Checkout {picking2.name}", }, ) diff --git a/shopfloor/tests/test_checkout_scan_line_no_prefill_qty.py b/shopfloor/tests/test_checkout_scan_line_no_prefill_qty.py index 629fe2c9d81..6bf1d744f9a 100644 --- a/shopfloor/tests/test_checkout_scan_line_no_prefill_qty.py +++ b/shopfloor/tests/test_checkout_scan_line_no_prefill_qty.py @@ -37,7 +37,7 @@ def _assert_quantity_done(self, barcode, selected_lines, qties): "scan_line", params={"picking_id": picking.id, "barcode": barcode} ) response_lines = response["data"]["select_package"]["selected_move_lines"] - for response_line, qty in zip(response_lines, qties): + for response_line, qty in zip(response_lines, qties, strict=False): self.assertEqual(response_line["qty_done"], qty) def test_scan_line_product_exist_in_two_lines(self): @@ -65,7 +65,7 @@ def test_scan_line_product_no_prefill_ok(self): self._fill_stock_for_moves(picking.move_ids) picking.action_assign() line_a = picking.move_line_ids.filtered( - lambda l: l.product_id == self.product_a + lambda x: x.product_id == self.product_a ) # When no_prefill_qty is enabled in the checkout menu, prefilled qty # should be 1.0 if a product is scanned @@ -79,7 +79,7 @@ def test_scan_line_product_packaging_no_prefill_ok(self): self._fill_stock_for_moves(picking.move_ids) picking.action_assign() lines_a = picking.move_line_ids.filtered( - lambda l: l.product_id == self.product_a + lambda x: x.product_id == self.product_a ) # When no_prefill_qty is enabled in the checkout menu, prefilled qty # should be the packaging qty, if a packaging is scanned diff --git a/shopfloor/tests/test_checkout_scan_package_action.py b/shopfloor/tests/test_checkout_scan_package_action.py index fa6cd404dd9..b28ebf8f65e 100644 --- a/shopfloor/tests/test_checkout_scan_package_action.py +++ b/shopfloor/tests/test_checkout_scan_package_action.py @@ -40,40 +40,40 @@ def _test_select_product( def test_scan_package_action_select_product(self): self._test_select_product( - lambda l: l.product_id.barcode, lambda l: l.reserved_uom_qty, lambda __: 0 + lambda x: x.product_id.barcode, lambda x: x.reserved_uom_qty, lambda __: 0 ) def test_scan_package_action_deselect_product(self): self._test_select_product( - lambda l: l.product_id.barcode, lambda __: 0, lambda l: l.reserved_uom_qty + lambda x: x.product_id.barcode, lambda __: 0, lambda x: x.reserved_uom_qty ) def test_scan_package_action_select_product_packaging(self): self._test_select_product( - lambda l: l.product_id.packaging_ids.barcode, - lambda l: l.reserved_uom_qty, + lambda x: x.product_id.packaging_ids.barcode, + lambda x: x.reserved_uom_qty, lambda __: 0, ) def test_scan_package_action_deselect_product_packaging(self): self._test_select_product( - lambda l: l.product_id.packaging_ids.barcode, + lambda x: x.product_id.packaging_ids.barcode, lambda __: 0, - lambda l: l.reserved_uom_qty, + lambda x: x.reserved_uom_qty, ) def test_scan_package_action_select_product_lot(self): self._test_select_product( - lambda l: l.lot_id.name, + lambda x: x.lot_id.name, lambda __: 0, - lambda l: l.reserved_uom_qty, + lambda x: x.reserved_uom_qty, in_lot=True, ) def test_scan_package_action_deselect_product_lot(self): self._test_select_product( - lambda l: l.lot_id.name, - lambda l: l.reserved_uom_qty, + lambda x: x.lot_id.name, + lambda x: x.reserved_uom_qty, lambda __: 0, in_lot=True, ) diff --git a/shopfloor/tests/test_checkout_select.py b/shopfloor/tests/test_checkout_select.py index 173b0aaf108..546da5e4a0a 100644 --- a/shopfloor/tests/test_checkout_select.py +++ b/shopfloor/tests/test_checkout_select.py @@ -68,7 +68,9 @@ def test_select_error_not_available(self): ) def test_select_error_not_allowed(self): + # Trying to pick a picking with wrong picking type picking = self._create_picking(picking_type=self.wh.pick_type_id) self._fill_stock_for_moves(picking.move_ids, in_package=True) picking.action_assign() - self._test_error(picking, "You cannot move this using this menu.") + expected_message = f"Reserved for {picking.picking_type_id.name} {picking.name}" + self._test_error(picking, expected_message) diff --git a/shopfloor/tests/test_cluster_picking_is_zero.py b/shopfloor/tests/test_cluster_picking_is_zero.py index 338dd4e6630..736bb47a509 100644 --- a/shopfloor/tests/test_cluster_picking_is_zero.py +++ b/shopfloor/tests/test_cluster_picking_is_zero.py @@ -57,11 +57,7 @@ def test_is_zero_is_empty(self): data=self._line_data(self.next_line), message={ "message_type": "success", - "body": "{} {} put in {}".format( - self.line.qty_done, - self.line.product_id.display_name, - self.bin1.name, - ), + "body": f"{self.line.qty_done} {self.line.product_id.display_name} put in {self.bin1.name}", # noqa }, ) @@ -89,10 +85,6 @@ def test_is_zero_is_not_empty(self): data=self._line_data(self.next_line), message={ "message_type": "success", - "body": "{} {} put in {}".format( - self.line.qty_done, - self.line.product_id.display_name, - self.bin1.name, - ), + "body": f"{self.line.qty_done} {self.line.product_id.display_name} put in {self.bin1.name}", # noqa }, ) diff --git a/shopfloor/tests/test_cluster_picking_scan_destination.py b/shopfloor/tests/test_cluster_picking_scan_destination.py index 18f9af22acf..a2869be4112 100644 --- a/shopfloor/tests/test_cluster_picking_scan_destination.py +++ b/shopfloor/tests/test_cluster_picking_scan_destination.py @@ -64,9 +64,7 @@ def test_scan_destination_pack_ok(self): data=self._line_data(next_line), message={ "message_type": "success", - "body": "{} {} put in {}".format( - line.qty_done, line.product_id.display_name, self.bin1.name - ), + "body": f"{line.qty_done} {line.product_id.display_name} put in {self.bin1.name}", # noqa }, ) @@ -149,8 +147,8 @@ def test_scan_destination_pack_not_empty_different_picking(self): data=self._line_data(line, qty_done=10.0), message={ "message_type": "error", - "body": "The destination bin {} is not empty," - " please take another.".format(self.bin1.name), + "body": f"The destination bin {self.bin1.name} is not empty, " + "please take another.", }, ) @@ -233,9 +231,7 @@ def test_scan_destination_pack_quantity_more(self): data=self._line_data(line, qty_done=11.0), message={ "message_type": "error", - "body": "You must not pick more than {} units.".format( - line.reserved_uom_qty - ), + "body": f"You must not pick more than {line.reserved_uom_qty} units.", }, ) @@ -271,9 +267,7 @@ def test_scan_destination_pack_quantity_less(self): data=self._line_data(new_line), message={ "message_type": "success", - "body": "{} {} put in {}".format( - line.qty_done, line.product_id.display_name, self.bin1.name - ), + "body": f"{line.qty_done} {line.product_id.display_name} put in {self.bin1.name}", # noqa }, ) @@ -369,8 +363,6 @@ def test_scan_destination_pack_zero_check_disabled(self): data=self._line_data(next_line), message={ "message_type": "success", - "body": "{} {} put in {}".format( - line.qty_done, line.product_id.display_name, self.bin1.name - ), + "body": f"{line.qty_done} {line.product_id.display_name} put in {self.bin1.name}", # noqa }, ) diff --git a/shopfloor/tests/test_cluster_picking_scan_line.py b/shopfloor/tests/test_cluster_picking_scan_line.py index be76f004260..151c2a10ff3 100644 --- a/shopfloor/tests/test_cluster_picking_scan_line.py +++ b/shopfloor/tests/test_cluster_picking_scan_line.py @@ -78,6 +78,12 @@ def test_scan_line_serial_ok(self): self.product_a.tracking = "serial" self._simulate_batch_selected(self.batch, in_lot=True) line = self.batch.picking_ids.move_line_ids + lot = line.lot_id + self.assertTrue(lot) + lot_with_same_name = line.lot_id.copy( + {"product_id": self.product_b.id, "name": line.lot_id.name} + ) + self.assertEqual(lot_with_same_name.name, lot.name) self._scan_line_ok(line, line.lot_id.name) def test_scan_line_error_product_tracked(self): diff --git a/shopfloor/tests/test_cluster_picking_scan_line_location_or_pack_first.py b/shopfloor/tests/test_cluster_picking_scan_line_location_or_pack_first.py index 31868a0f385..5ea650eed62 100644 --- a/shopfloor/tests/test_cluster_picking_scan_line_location_or_pack_first.py +++ b/shopfloor/tests/test_cluster_picking_scan_line_location_or_pack_first.py @@ -103,8 +103,7 @@ def test_scan_line_location_with_multiple_product(self): """Check scanning a location then a product without package. When there is multiple product in the location and the location is scanned, - The user needs to scan the product but the system does not remember the location ? - + User needs to scan the product but the system does not remember the location. """ self._simulate_batch_selected(self.batch, in_package=False, in_lot=False) line = self.batch.picking_ids.move_line_ids diff --git a/shopfloor/tests/test_cluster_picking_stock_issue.py b/shopfloor/tests/test_cluster_picking_stock_issue.py index d2af17e4b73..fcbdfd9e72a 100644 --- a/shopfloor/tests/test_cluster_picking_stock_issue.py +++ b/shopfloor/tests/test_cluster_picking_stock_issue.py @@ -125,7 +125,7 @@ def test_stock_issue_with_other_batch(self): + self.move2.product_uom_qty + sum( self.batch_other.picking_ids.move_line_ids.filtered( - lambda l: l.location_id == self.shelf2 + lambda x: x.location_id == self.shelf2 ).mapped("reserved_uom_qty") ) ) @@ -162,10 +162,10 @@ def test_stock_issue_several_move_lines(self): self.assertEqual(self.move5.move_line_ids.location_id, self.shelf2) line_shelf1 = self.move3.move_line_ids.filtered( - lambda l: l.location_id == self.shelf1 + lambda x: x.location_id == self.shelf1 ) line_shelf2 = self.move3.move_line_ids.filtered( - lambda l: l.location_id == self.shelf2 + lambda x: x.location_id == self.shelf2 ) # pick the first 2 moves diff --git a/shopfloor/tests/test_cluster_picking_unload.py b/shopfloor/tests/test_cluster_picking_unload.py index f4ea508f2f5..496468e433e 100644 --- a/shopfloor/tests/test_cluster_picking_unload.py +++ b/shopfloor/tests/test_cluster_picking_unload.py @@ -430,7 +430,7 @@ def test_set_destination_all_with_confirmation(self): ) def test_set_destination_all_check_confirmation(self): - """Endpoint called confirming with a different location, ask confirmation again""" + """Endpoint called confirming with a different location, ask confirmation""" move_lines = self.move_lines self._set_dest_package_and_done(move_lines, self.bin1) move_lines.write({"location_dest_id": self.packing_a_location.id}) @@ -933,8 +933,8 @@ def test_unload_scan_destination_completion_info(self): response, next_state="unload_single", popup={ - "body": "Last operation of transfer {}. Next operation " - "({}) is ready to proceed.".format(picking.name, next_picking.name) + "body": f"Last operation of transfer {picking.name}. Next operation " + f"({next_picking.name}) is ready to proceed." }, data=data, ) diff --git a/shopfloor/tests/test_delivery_scan_deliver.py b/shopfloor/tests/test_delivery_scan_deliver.py index 32c7976065e..95350ebdabe 100644 --- a/shopfloor/tests/test_delivery_scan_deliver.py +++ b/shopfloor/tests/test_delivery_scan_deliver.py @@ -12,6 +12,20 @@ class DeliveryScanDeliverCase(DeliveryCommonCase): @classmethod def setUpClassBaseData(cls): super().setUpClassBaseData() + cls.out_location = cls.env.ref("stock.stock_location_output") + cls.cleanup_type = ( + cls.env["stock.picking.type"] + .sudo() + .create( + { + "name": "Cancel Cleanup", + "default_location_src_id": cls.out_location.id, + "default_location_dest_id": cls.stock_location.id, + "sequence_code": "CCP", + "code": "internal", + } + ) + ) cls.product_e.tracking = "lot" cls.picking = picking = cls._create_picking( lines=[ @@ -447,6 +461,134 @@ def test_scan_deliver_scan_product_alone_in_package_qty_one(self): move_lines = pick.move_ids.mapped("move_line_ids") self._test_scan_set_done_ok(move_lines, self.product_c.barcode, [1]) + def test_scan_deliver_picking_canceled(self): + self.picking.action_cancel() + params = {"barcode": self.picking.name} + response = self.service.dispatch("scan_deliver", params=params) + self.assert_response_deliver( + response, + message=self.service.msg_store.transfer_canceled(), + ) + + def test_scan_deliver_return_partial_package(self): + move_a = self.picking.move_ids.filtered( + lambda m: m.product_id == self.product_a + ) + move_a._action_cancel() + cleanup_picking = self._create_picking( + picking_type=self.cleanup_type, lines=[(self.product_a, 1)] + ) + package_vals = [(self.product_a, 1, None)] + cleanup_package = self._create_package_in_location( + cleanup_picking.location_id, package_vals + ) + cleanup_package.name = "CLEANUP_PACKAGE" + cleanup_picking.action_assign() + cleanup_picking.move_line_ids.package_id = cleanup_package + params = {"barcode": "CLEANUP_PACKAGE"} + response = self.service.dispatch("scan_deliver", params=params) + type_name = cleanup_picking.picking_type_id.name + pick_name = cleanup_picking.name + expected_body = f"Reserved for {type_name} {pick_name}" + self.assertEqual(response.get("message").get("body"), expected_body) + + def test_scan_deliver_return_package(self): + self.picking.action_cancel() + cleanup_picking = self._create_picking( + picking_type=self.cleanup_type, lines=[(self.product_a, 1)] + ) + package_vals = [(self.product_a, 1, None)] + cleanup_package = self._create_package_in_location( + cleanup_picking.location_id, package_vals + ) + cleanup_package.name = "CLEANUP_PACKAGE" + cleanup_picking.action_assign() + cleanup_picking.move_line_ids.package_id = cleanup_package + params = {"barcode": "CLEANUP_PACKAGE"} + response = self.service.dispatch("scan_deliver", params=params) + type_name = cleanup_picking.picking_type_id.name + pick_name = cleanup_picking.name + expected_body = f"Reserved for {type_name} {pick_name}" + self.assertEqual(response.get("message").get("body"), expected_body) + + def test_scan_deliver_return_product(self): + self.picking.action_cancel() + cleanup_picking = self._create_picking( + picking_type=self.cleanup_type, lines=[(self.product_a, 1)] + ) + cleanup_picking.action_assign() + params = {"barcode": self.product_a.barcode} + response = self.service.dispatch("scan_deliver", params=params) + type_name = cleanup_picking.picking_type_id.name + pick_name = cleanup_picking.name + expected_body = f"Reserved for {type_name} {pick_name}" + self.assertEqual(response.get("message").get("body"), expected_body) + + def test_scan_deliver_return_packaging(self): + self.picking.action_cancel() + cleanup_picking = self._create_picking( + picking_type=self.cleanup_type, + lines=[(self.product_a, 1)], + ) + cleanup_picking.action_assign() + + packaging_model = self.env["product.packaging"].sudo() + packaging_model.create( + { + "name": "CLEANUP PACKAGING", + "product_id": self.product_a.id, + "qty": 1, + "product_uom_id": self.product_a.id, + "barcode": "CLEANUP_PACKAGING", + } + ) + params = {"barcode": "CLEANUP_PACKAGING"} + response = self.service.dispatch("scan_deliver", params=params) + type_name = cleanup_picking.picking_type_id.name + pick_name = cleanup_picking.name + expected_body = f"Reserved for {type_name} {pick_name}" + self.assertEqual(response.get("message").get("body"), expected_body) + + def test_scan_deliver_return_lot(self): + self.picking.action_cancel() + cleanup_picking = self._create_picking( + picking_type=self.cleanup_type, lines=[(self.product_a, 1)] + ) + # Create move lines and set lot + cleanup_picking.action_assign() + cleanup_lot = self.env["stock.lot"].create( + { + "product_id": self.product_a.id, + "company_id": self.env.company.id, + "name": "CLEANUP_LOT", + "ref": "CLEANUP_LOT", + } + ) + # Re-force qty to 1, as setting the lot resets qty to 0 + cleanup_picking.move_line_ids.lot_id = cleanup_lot + cleanup_picking.move_line_ids.reserved_uom_qty = 1.0 + params = {"barcode": "CLEANUP_LOT"} + response = self.service.dispatch("scan_deliver", params=params) + type_name = cleanup_picking.picking_type_id.name + pick_name = cleanup_picking.name + expected_body = f"Reserved for {type_name} {pick_name}" + self.assertEqual(response.get("message").get("body"), expected_body) + + def test_scan_delivery_return_picking(self): + self.picking.action_cancel() + cleanup_picking = self._create_picking( + picking_type=self.cleanup_type, lines=[(self.product_c, 1)] + ) + cleanup_picking.action_assign() + params = {"barcode": cleanup_picking.name} + response = self.service.dispatch("scan_deliver", params=params) + self.assert_response_deliver( + response, + message=self.service.msg_store.reserved_for_other_picking_type( + cleanup_picking + ), + ) + def test_scan_deliver_picking_done(self): # Set qty done for all lines (packages/raw product/lot...), picking is # automatically set to done when the last line is completed @@ -525,7 +667,7 @@ def test_scan_deliver_error_picking_wrong_type(self): response, message={ "message_type": "error", - "body": "You cannot move this using this menu.", + "body": f"Reserved for {picking.picking_type_id.name} {picking.name}", }, ) @@ -538,7 +680,7 @@ def test_scan_deliver_error_picking_unavailable(self): response, message={ "message_type": "error", - "body": "Transfer {} is not available.".format(picking.name), + "body": f"Transfer {picking.name} is not available.", }, ) diff --git a/shopfloor/tests/test_delivery_set_qty_done_line.py b/shopfloor/tests/test_delivery_set_qty_done_line.py index 56a38e1cf06..696edc2f070 100644 --- a/shopfloor/tests/test_delivery_set_qty_done_line.py +++ b/shopfloor/tests/test_delivery_set_qty_done_line.py @@ -52,7 +52,7 @@ def test_set_qty_done_line_picking_canceled(self): ) self.assert_response_deliver( response, - message=self.service.msg_store.stock_picking_not_available(self.picking), + message=self.service.msg_store.transfer_canceled(), ) def test_set_qty_done_line_line_not_found(self): diff --git a/shopfloor/tests/test_delivery_set_qty_done_pack.py b/shopfloor/tests/test_delivery_set_qty_done_pack.py index bd7df5ad3f5..366950d4c25 100644 --- a/shopfloor/tests/test_delivery_set_qty_done_pack.py +++ b/shopfloor/tests/test_delivery_set_qty_done_pack.py @@ -67,7 +67,7 @@ def test_set_qty_done_pack_picking_canceled(self): ) self.assert_response_deliver( response, - message=self.service.msg_store.stock_picking_not_available(self.picking), + message=self.service.msg_store.transfer_canceled(), ) def test_set_qty_done_pack_package_not_found(self): diff --git a/shopfloor/tests/test_location_content_transfer_get_work.py b/shopfloor/tests/test_location_content_transfer_get_work.py index f593ccd4627..83a1f909f67 100644 --- a/shopfloor/tests/test_location_content_transfer_get_work.py +++ b/shopfloor/tests/test_location_content_transfer_get_work.py @@ -123,11 +123,14 @@ def test_find_work_additional_domain(self): def test_find_work_custom_sort_key(self): # fmt: off + custom_code = ( + f"key = (" + f"-1 if line.location_id.id == {self.content_loc.id} else 10, )" + ) self.menu.sudo().write( { "move_line_search_sort_order": "custom_code", - "move_line_search_sort_order_custom_code": - f"key = (-1 if line.location_id.id == {self.content_loc.id} else 10, )", + "move_line_search_sort_order_custom_code": custom_code, } ) # fmt: on diff --git a/shopfloor/tests/test_location_content_transfer_mix.py b/shopfloor/tests/test_location_content_transfer_mix.py index 9d9b53f6f6c..2550f739d53 100644 --- a/shopfloor/tests/test_location_content_transfer_mix.py +++ b/shopfloor/tests/test_location_content_transfer_mix.py @@ -220,7 +220,7 @@ def test_with_zone_picking1(self): # done, the operator is currently moving the goods to the destination location) pack_move_line1 = pick_move_line1.move_id.move_dest_ids.filtered( lambda m: m.state not in ("cancel", "done") - ).move_line_ids.filtered(lambda l: not l.shopfloor_user_id) + ).move_line_ids.filtered(lambda x: not x.shopfloor_user_id) self._location_content_transfer_process_line(pack_move_line1) # Operator-1 process the second pallet with the "zone picking" scenario self._zone_picking_process_line(pick_move_line2) @@ -228,11 +228,12 @@ def test_with_zone_picking1(self): # the location where this second pallet is pack_move_line2 = pick_move_line2.move_id.move_dest_ids.filtered( lambda m: m.state not in ("cancel", "done") - ).move_line_ids.filtered(lambda l: not l.shopfloor_user_id) - assert ( - len(pack_move_line2) == 1 - ), "Operator-3 should end up with one move line taken from {}".format( - pack_move_line2.picking_id.name + ).move_line_ids.filtered(lambda x: not x.shopfloor_user_id) + self.assertEqual( + len(pack_move_line2), + 1, + f"Operator-3 should find only " + f"one move line in {pack_move_line2.location_id.name}", ) self._location_content_transfer_process_line(pack_move_line2) @@ -302,12 +303,12 @@ def test_with_zone_picking2(self): ) self.assertEqual(pack_move_a, self.pack_move_a) pack_first_pallet = pack_move_a.move_line_ids.filtered( - lambda l: not l.shopfloor_user_id and l.location_id == dest_location1 + lambda x: not x.shopfloor_user_id and x.location_id == dest_location1 ) self.assertEqual(pack_first_pallet.reserved_uom_qty, 6) self.assertEqual(pack_first_pallet.qty_done, 0) pack_second_pallet = pack_move_a.move_line_ids.filtered( - lambda l: not l.shopfloor_user_id and l.location_id == dest_location2 + lambda x: not x.shopfloor_user_id and x.location_id == dest_location2 ) self.assertEqual(pack_second_pallet.reserved_uom_qty, 4) self.assertEqual(pack_second_pallet.qty_done, 0) @@ -367,7 +368,7 @@ def test_with_zone_picking2(self): ) self.assertEqual(pack_move_a, self.pack_move_a) pack_second_pallet = pack_move_a.move_line_ids.filtered( - lambda l: not l.shopfloor_user_id and l.location_id == dest_location2 + lambda x: not x.shopfloor_user_id and x.location_id == dest_location2 ) picking_before = pack_second_pallet.picking_id move_lines = self.service.search_move_line.search_move_lines( @@ -443,7 +444,7 @@ def test_with_zone_picking3(self): ) self.assertEqual(pack_move_a1, self.pack_move_a) pack_first_pallet = pack_move_a1.move_line_ids.filtered( - lambda l: not l.shopfloor_user_id and l.location_id == dest_location1 + lambda x: not x.shopfloor_user_id and x.location_id == dest_location1 ) self.assertEqual(pack_first_pallet.reserved_uom_qty, 6) self.assertEqual(pack_first_pallet.qty_done, 0) @@ -469,7 +470,7 @@ def test_with_zone_picking3(self): lambda m: m.move_line_ids.package_id == self.package_2 ) pack_second_pallet = pack_move_a2.move_line_ids.filtered( - lambda l: not l.shopfloor_user_id and l.location_id == dest_location2 + lambda x: not x.shopfloor_user_id and x.location_id == dest_location2 ) self.assertEqual(pack_second_pallet.reserved_uom_qty, 4) self.assertEqual(pack_second_pallet.qty_done, 0) diff --git a/shopfloor/tests/test_location_content_transfer_set_destination_package_or_line.py b/shopfloor/tests/test_location_content_transfer_set_destination_package_or_line.py index 1fd599b260c..803d2cb7c32 100644 --- a/shopfloor/tests/test_location_content_transfer_set_destination_package_or_line.py +++ b/shopfloor/tests/test_location_content_transfer_set_destination_package_or_line.py @@ -187,7 +187,7 @@ def test_set_destination_package_dest_location_ok(self): response, move_lines.mapped("picking_id"), message=self.service.msg_store.location_content_transfer_item_complete( - self.dest_location + self.content_loc, self.dest_location ), ) for move in package_level.move_line_ids.mapped("move_id"): @@ -235,7 +235,7 @@ def test_set_destination_package_dest_location_ok_with_completion_info(self): response, move_lines.mapped("picking_id"), message=self.service.msg_store.location_content_transfer_item_complete( - self.dest_location + self.content_loc, self.dest_location ), popup=completion_info_popup, ) @@ -385,7 +385,7 @@ def test_set_destination_line_dest_location_ok(self): response, move_lines.mapped("picking_id"), message=self.service.msg_store.location_content_transfer_item_complete( - self.dest_location + self.content_loc, self.dest_location ), ) @@ -436,7 +436,7 @@ def test_set_destination_line_dest_location_ok_with_completion_info(self): response, move_lines.mapped("picking_id"), message=self.service.msg_store.location_content_transfer_item_complete( - self.dest_location + self.content_loc, self.dest_location ), popup=completion_info_popup, ) @@ -485,7 +485,7 @@ def test_set_destination_line_partial_qty(self): response, done_picking.backorder_ids, message=self.service.msg_store.location_content_transfer_item_complete( - self.dest_location + self.content_loc, self.dest_location ), ) self.assertEqual(move_line_c.move_id.state, "done") @@ -754,7 +754,7 @@ def test_set_destination_package_split_move(self): response, move_lines.mapped("picking_id"), message=self.service.msg_store.location_content_transfer_item_complete( - self.dest_location + self.content_loc, self.dest_location ), ) @@ -812,7 +812,7 @@ def test_set_destination_line_split_move(self): response, move_lines.mapped("picking_id"), message=self.service.msg_store.location_content_transfer_item_complete( - self.dest_location + self.content_loc, self.dest_location ), ) # Process the other move lines (lines w/o package + package levels) @@ -1047,7 +1047,7 @@ def test_set_destination_lines_partial_qty_next_line(self): response, backorder, message=self.service.msg_store.location_content_transfer_item_complete( - self.dest_location + self.content_loc, self.dest_location ), ) # check that the next operation has the appropriate attributes @@ -1071,6 +1071,6 @@ def test_set_destination_lines_partial_qty_next_line(self): response, self.picking, message=self.service.msg_store.location_content_transfer_item_complete( - self.dest_location + self.content_loc, self.dest_location ), ) diff --git a/shopfloor/tests/test_location_content_transfer_single.py b/shopfloor/tests/test_location_content_transfer_single.py index 3ef9e59384e..92a62006f62 100644 --- a/shopfloor/tests/test_location_content_transfer_single.py +++ b/shopfloor/tests/test_location_content_transfer_single.py @@ -573,7 +573,7 @@ def test_postpone_line_ok_with_two_lines_and_view(self): backorder = self.picking3.backorder_ids self.assertTrue(backorder) message = { - "body": "Content transfer to Shelf 1 completed", + "body": "Content line transferred from Content Location to Shelf 1", "message_type": "success", } diff --git a/shopfloor/tests/test_location_content_transfer_start.py b/shopfloor/tests/test_location_content_transfer_start.py index d51597ee981..62bc1fcd6a2 100644 --- a/shopfloor/tests/test_location_content_transfer_start.py +++ b/shopfloor/tests/test_location_content_transfer_start.py @@ -275,6 +275,29 @@ def test_scan_location_wrong_picking_type_allow_unreserve_empty(self): message=self.service.msg_store.location_empty(self.content_loc), ) + def test_scan_location_picking_already_started(self): + self.menu.sudo().allow_unreserve_other_moves = True + picking = self._create_picking( + picking_type=self.menu.picking_type_ids, + lines=[(self.product_a, 10), (self.product_b, 10)], + ) + self._fill_stock_for_moves( + picking.move_ids, in_package=True, location=self.content_loc + ) + picking.action_assign() + picking.move_line_ids[0].qty_done = 10 + response = self.service.dispatch( + "scan_location", params={"barcode": self.content_loc.barcode} + ) + self.assert_response_start( + response, + message=self.service.msg_store.picking_already_started_in_location(picking), + ) + # check that the original moves are still assigned + self.assertRecordValues( + picking.move_ids, [{"state": "assigned"}, {"state": "assigned"}] + ) + def test_scan_location_wrong_picking_type_allow_unreserve_error(self): """Content has different picking type than menu, option to unreserve @@ -298,7 +321,7 @@ def test_scan_location_wrong_picking_type_allow_unreserve_error(self): ) self.assert_response_start( response, - message=self.service.msg_store.picking_already_started_in_location(picking), + message=self.service.msg_store.reserved_for_other_picking_type(picking), ) # check that the original moves are still assigned self.assertRecordValues( diff --git a/shopfloor/tests/test_menu_contrains.py b/shopfloor/tests/test_menu_contrains.py index 56d71911c16..459999847b9 100644 --- a/shopfloor/tests/test_menu_contrains.py +++ b/shopfloor/tests/test_menu_contrains.py @@ -7,7 +7,6 @@ class TestMenuContrains(MenuCountersCommonCase): def test_move_line_search_sort_order_custom_code_invalid(self): - with self.assertRaises(exceptions.ValidationError): # wrong indentation in python code self.menu1.sudo().write( diff --git a/shopfloor/tests/test_single_pack_transfer.py b/shopfloor/tests/test_single_pack_transfer.py index decda5a2e84..3146c08afd5 100644 --- a/shopfloor/tests/test_single_pack_transfer.py +++ b/shopfloor/tests/test_single_pack_transfer.py @@ -154,7 +154,7 @@ def test_start_no_operation(self): next_state="start", message={ "message_type": "error", - "body": "No pending operation for package {}.".format(self.pack_a.name), + "body": f"No pending operation for package {self.pack_a.name}.", }, ) @@ -188,7 +188,8 @@ def test_start_no_operation_create(self): package_level = move_line.package_level_id self.assertTrue(package_level.is_done) - + self.assertEqual(move_line.location_id, self.pack_a.location_id) + self.assertEqual(move_line.move_id.location_id, self.pack_a.location_id) expected_data = { "id": package_level.id, "name": package_level.package_id.name, @@ -206,6 +207,60 @@ def test_start_no_operation_create(self): self.assert_response(response, next_state="scan_location", data=expected_data) + def test_start_validate_no_operation_create(self): + self.menu.sudo().allow_move_create = True + self.picking.do_unreserve() + barcode = self.pack_a.name + params = {"barcode": barcode} + + # Simulate the client scanning a package's barcode, which + # in turns should start the operation in odoo + response = self.service.dispatch("start", params=params) + + move_line = self.env["stock.move.line"].search( + [("package_id", "=", self.pack_a.id)] + ) + package_level = move_line.package_level_id + + response = self.service.dispatch( + "validate", + params={ + "package_level_id": package_level.id, + "location_barcode": self.shelf2.barcode, + }, + ) + + self.assert_response( + response, + next_state="start", + message={ + "message_type": "success", + "body": "The pack has been moved, you can scan a new pack.", + }, + ) + + self.assertRecordValues( + package_level.move_line_ids, + [ + { + "qty_done": 1.0, + "location_dest_id": self.shelf2.id, + "location_id": self.shelf1.id, + "state": "done", + } + ], + ) + self.assertRecordValues( + package_level.move_line_ids.move_id, + [ + { + "location_dest_id": self.shelf2.id, + "location_id": self.shelf1.id, + "state": "done", + } + ], + ) + def test_start_barcode_not_known(self): """Test /start when the barcode is unknown @@ -302,8 +357,7 @@ def test_start_pack_from_location_empty(self): next_state="start", message={ "message_type": "error", - "body": "Location %s doesn't contain any package." - % (self.shelf2.name,), + "body": f"Location {self.shelf2.name} doesn't contain any package.", }, ) @@ -340,8 +394,8 @@ def test_start_pack_from_location_several_packs(self): next_state="start", message={ "message_type": "warning", - "body": "Several packages found in %s, please scan a package." - % (self.shelf1.name,), + "body": f"Several packages found in {self.shelf1.name}, " + "please scan a package.", }, ) @@ -366,8 +420,9 @@ def test_start_pack_outside_of_location(self): next_state="start", message={ "message_type": "error", - "body": "You cannot work on a package (%s) outside of locations: %s" - % (self.pack_a.name, self.picking_type.default_location_src_id.name), + "body": f"You cannot work on a package ({self.pack_a.name}) " + "outside of locations: " + f"{self.picking_type.default_location_src_id.name}", }, ) @@ -451,11 +506,24 @@ def test_validate(self): self.assertRecordValues( package_level.move_line_ids, - [{"qty_done": 1.0, "location_dest_id": self.shelf2.id, "state": "done"}], + [ + { + "qty_done": 1.0, + "location_dest_id": self.shelf2.id, + "location_id": self.shelf1.id, + "state": "done", + } + ], ) self.assertRecordValues( package_level.move_line_ids.move_id, - [{"location_dest_id": self.shelf2.id, "state": "done"}], + [ + { + "location_dest_id": self.shelf2.id, + "location_id": self.shelf1.location_id.id, + "state": "done", + } + ], ) def test_validate_completion_info(self): @@ -533,8 +601,8 @@ def test_validate_completion_info(self): response, next_state="start", popup={ - "body": "Last operation of transfer {}. Next operation " - "({}) is ready to proceed.".format(self.picking.name, next_picking.name) + "body": f"Last operation of transfer {self.picking.name}. " + f"Next operation ({next_picking.name}) is ready to proceed." }, message={ "message_type": "success", diff --git a/shopfloor/tests/test_stock_split.py b/shopfloor/tests/test_stock_split.py index ebe60d91918..ceab8957793 100644 --- a/shopfloor/tests/test_stock_split.py +++ b/shopfloor/tests/test_stock_split.py @@ -10,7 +10,7 @@ class TestStockSplit(TransactionCase): @classmethod def setUpClass(cls): - super(TestStockSplit, cls).setUpClass() + super().setUpClass() cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) cls.warehouse = cls.env.ref("stock.warehouse0") cls.warehouse.delivery_steps = "pick_pack_ship" diff --git a/shopfloor/tests/test_zone_picking_base.py b/shopfloor/tests/test_zone_picking_base.py index bb8c3e193b6..5e882b586ba 100644 --- a/shopfloor/tests/test_zone_picking_base.py +++ b/shopfloor/tests/test_zone_picking_base.py @@ -343,12 +343,14 @@ def _assert_response_select_line( data["sublocation"] = self.data.location(sublocation) for data_move_line in data["move_lines"]: move_line = self.env["stock.move.line"].browse(data_move_line["id"]) - data_move_line[ - "location_will_be_empty" - ] = move_line.location_id.planned_qty_in_location_is_empty(move_line) - data_move_line[ - "handle_complete_mix_pack" - ] = self.service._handle_complete_mix_pack(move_line.package_id) + # fmt: off + data_move_line["location_will_be_empty"] = ( + move_line.location_id.planned_qty_in_location_is_empty(move_line) + ) + data_move_line["handle_complete_mix_pack"] = ( + self.service._handle_complete_mix_pack(move_line.package_id) + ) + # fmt: on self.assert_response( response, next_state=state, @@ -401,9 +403,6 @@ def _assert_response_set_line_destination( expected_move_line = self.data.move_line(move_line, with_picking=True) if qty_done is not None: expected_move_line["qty_done"] = qty_done - allow_alternative_destination_package = ( - self.menu.allow_alternative_destination_package - ) self.assert_response( response, next_state=state, @@ -412,7 +411,9 @@ def _assert_response_set_line_destination( "picking_type": self.data.picking_type(picking_type), "move_line": expected_move_line, "confirmation_required": confirmation_required, - "allow_alternative_destination_package": allow_alternative_destination_package, + "allow_alternative_destination_package": ( + self.menu.allow_alternative_destination_package + ), "handle_complete_mix_pack": handle_complete_mix_pack, }, message=message, diff --git a/shopfloor/tests/test_zone_picking_complete_mix_pack_flux.py b/shopfloor/tests/test_zone_picking_complete_mix_pack_flux.py index 85a7b05093c..8033417fee4 100644 --- a/shopfloor/tests/test_zone_picking_complete_mix_pack_flux.py +++ b/shopfloor/tests/test_zone_picking_complete_mix_pack_flux.py @@ -24,7 +24,7 @@ def test_scan_source_and_set_destination_on_mixed_package(self): move_lines = self.service._find_location_move_lines( package=package, ) - move_lines = move_lines.sorted(lambda l: l.move_id.priority, reverse=True) + move_lines = move_lines.sorted(lambda x: x.move_id.priority, reverse=True) move_line = move_lines[0] self.assert_response_set_line_destination( response, @@ -49,7 +49,7 @@ def test_scan_source_and_set_destination_on_mixed_package(self): ) # Check response move_lines = self.service._find_location_move_lines() - move_lines = move_lines.sorted(lambda l: l.move_id.priority, reverse=True) + move_lines = move_lines.sorted(lambda x: x.move_id.priority, reverse=True) self.assert_response_select_line( response, zone_location=self.zone_location, diff --git a/shopfloor/tests/test_zone_picking_require_destination_package.py b/shopfloor/tests/test_zone_picking_require_destination_package.py index 5f1681fbf73..9ca762ec1fd 100644 --- a/shopfloor/tests/test_zone_picking_require_destination_package.py +++ b/shopfloor/tests/test_zone_picking_require_destination_package.py @@ -52,7 +52,7 @@ def test_set_destination(self): }, ) move_lines = self.service._find_location_move_lines() - move_lines = move_lines.sorted(lambda l: l.move_id.priority, reverse=True) + move_lines = move_lines.sorted(lambda x: x.move_id.priority, reverse=True) self.assert_response_select_line( response, zone_location, diff --git a/shopfloor/tests/test_zone_picking_select_line.py b/shopfloor/tests/test_zone_picking_select_line.py index 49fbb9e7fc9..1d793945d2a 100644 --- a/shopfloor/tests/test_zone_picking_select_line.py +++ b/shopfloor/tests/test_zone_picking_select_line.py @@ -185,7 +185,7 @@ def test_scan_source_barcode_location_several_move_lines(self): params={"barcode": self.zone_sublocation2.barcode}, ) move_lines = self.pickings.move_line_ids.filtered( - lambda l: l.location_id == self.zone_sublocation2 + lambda x: x.location_id == self.zone_sublocation2 ).sorted( self.service.search_move_line._sort_key_move_lines(self.service.lines_order) ) @@ -213,7 +213,7 @@ def test_scan_source_barcode_package(self): move_lines = self.service._find_location_move_lines( package=package, ) - move_lines = move_lines.sorted(lambda l: l.move_id.priority, reverse=True) + move_lines = move_lines.sorted(lambda x: x.move_id.priority, reverse=True) move_line = move_lines[0] self.assert_response_set_line_destination( response, @@ -234,7 +234,7 @@ def test_scan_source_barcode_package_not_found(self): params={"barcode": pack_code}, ) move_lines = self.service._find_location_move_lines() - move_lines = move_lines.sorted(lambda l: l.move_id.priority, reverse=True) + move_lines = move_lines.sorted(lambda x: x.move_id.priority, reverse=True) self.assert_response_select_line( response, zone_location=self.zone_location, @@ -252,7 +252,7 @@ def test_scan_source_barcode_package_not_exist(self): params={"barcode": "P-Unknown"}, ) move_lines = self.service._find_location_move_lines() - move_lines = move_lines.sorted(lambda l: l.move_id.priority, reverse=True) + move_lines = move_lines.sorted(lambda x: x.move_id.priority, reverse=True) self.assert_response_select_line( response, zone_location=self.zone_location, @@ -278,7 +278,7 @@ def test_scan_source_package_many_products(self): move_lines = self.service._find_location_move_lines( locations=self.zone_sublocation1 ) - move_lines = move_lines.sorted(lambda l: l.move_id.priority, reverse=True) + move_lines = move_lines.sorted(lambda x: x.move_id.priority, reverse=True) self.assert_response_select_line( response, zone_location=self.zone_location, @@ -299,7 +299,7 @@ def test_scan_source_empty_package(self): move_lines = self.service._find_location_move_lines( locations=self.zone_location ) - move_lines = move_lines.sorted(lambda l: l.move_id.priority, reverse=True) + move_lines = move_lines.sorted(lambda x: x.move_id.priority, reverse=True) self.assert_response_select_line( response, zone_location=self.zone_location, @@ -334,7 +334,7 @@ def test_scan_source_barcode_package_can_replace_in_line(self): move_lines = self.service._find_location_move_lines( package=package1, ) - move_lines = move_lines.sorted(lambda l: l.move_id.priority, reverse=True) + move_lines = move_lines.sorted(lambda x: x.move_id.priority, reverse=True) self.assert_response_select_line( response, zone_location=self.zone_location, @@ -390,7 +390,7 @@ def test_scan_source_barcode_product_not_found(self): params={"barcode": self.free_product.barcode}, ) move_lines = self.service._find_location_move_lines() - move_lines = move_lines.sorted(lambda l: l.move_id.priority, reverse=True) + move_lines = move_lines.sorted(lambda x: x.move_id.priority, reverse=True) self.assert_response_select_line( response, zone_location=self.zone_location, @@ -414,7 +414,7 @@ def test_scan_source_barcode_product_multiple_moves_different_location(self): params={"barcode": self.product_e.barcode}, ) move_lines = self.service._find_location_move_lines(product=self.product_e) - move_lines = move_lines.sorted(lambda l: l.move_id.priority, reverse=True) + move_lines = move_lines.sorted(lambda x: x.move_id.priority, reverse=True) self.assert_response_select_line( response, zone_location=self.zone_location, @@ -459,7 +459,7 @@ def test_scan_source_barcode_location_multiple_moves_different_product(self): move_lines = self.service._find_location_move_lines( locations=self.zone_sublocation3 ) - move_lines = move_lines.sorted(lambda l: l.move_id.priority, reverse=True) + move_lines = move_lines.sorted(lambda x: x.move_id.priority, reverse=True) self.assert_response_select_line( response, zone_location=self.zone_location, @@ -507,7 +507,7 @@ def test_scan_source_barcode_product_with_multiple_lot(self): params={"barcode": self.product_c.barcode}, ) move_lines = self.service._find_location_move_lines(product=self.product_c) - move_lines = move_lines.sorted(lambda l: l.move_id.priority, reverse=True) + move_lines = move_lines.sorted(lambda x: x.move_id.priority, reverse=True) self.assert_response_select_line( response, zone_location=self.zone_location, @@ -534,7 +534,7 @@ def test_scan_source_barcode_lot(self): params={"barcode": lot.name}, ) move_lines = self.service._find_location_move_lines(lot=lot) - move_lines = move_lines.sorted(lambda l: l.move_id.priority, reverse=True) + move_lines = move_lines.sorted(lambda x: x.move_id.priority, reverse=True) move_line = move_lines[0] self.assert_response_set_line_destination( response, @@ -563,7 +563,7 @@ def test_scan_source_barcode_lot_in_multiple_location(self): # FIX ME: need to filter lines only on lot scanned !! move_lines = self.service._find_location_move_lines() # move_lines = self.service._find_location_move_lines(lot=lot) - move_lines = move_lines.sorted(lambda l: l.move_id.priority, reverse=True) + move_lines = move_lines.sorted(lambda x: x.move_id.priority, reverse=True) self.assert_response_select_line( response, zone_location=self.zone_location, @@ -581,7 +581,7 @@ def test_scan_source_barcode_lot_not_found(self): params={"barcode": self.free_lot.name}, ) move_lines = self.service._find_location_move_lines() - move_lines = move_lines.sorted(lambda l: l.move_id.priority, reverse=True) + move_lines = move_lines.sorted(lambda x: x.move_id.priority, reverse=True) self.assert_response_select_line( response, zone_location=self.zone_location, @@ -695,7 +695,7 @@ def test_prepare_unload_buffer_multi_line_same_destination(self): move_lines = self.picking5.move_line_ids self.assertEqual(move_lines.location_dest_id, self.packing_location) for move_line, package_dest in zip( - move_lines, self.free_package | self.another_package + move_lines, self.free_package | self.another_package, strict=False ): self.service.dispatch( "set_destination", diff --git a/shopfloor/tests/test_zone_picking_select_line_no_prefill_qty.py b/shopfloor/tests/test_zone_picking_select_line_no_prefill_qty.py index 426eaf01f4c..8d092804ddf 100644 --- a/shopfloor/tests/test_zone_picking_select_line_no_prefill_qty.py +++ b/shopfloor/tests/test_zone_picking_select_line_no_prefill_qty.py @@ -48,7 +48,7 @@ def test_scan_source_barcode_package_no_prefill(self): move_lines = self.service._find_location_move_lines( package=package, ) - move_lines = move_lines.sorted(lambda l: l.move_id.priority, reverse=True) + move_lines = move_lines.sorted(lambda x: x.move_id.priority, reverse=True) move_line = move_lines[0] self.assert_response_select_line( response, @@ -95,7 +95,7 @@ def test_scan_source_barcode_lot_no_prefill(self): params={"barcode": lot.name}, ) move_lines = self.service._find_location_move_lines(lot=lot) - move_lines = move_lines.sorted(lambda l: l.move_id.priority, reverse=True) + move_lines = move_lines.sorted(lambda x: x.move_id.priority, reverse=True) move_line = move_lines[0] self.assert_response_set_line_destination( response, diff --git a/shopfloor/tests/test_zone_picking_set_line_destination.py b/shopfloor/tests/test_zone_picking_set_line_destination.py index b3ce74c7851..f3094d258e7 100644 --- a/shopfloor/tests/test_zone_picking_set_line_destination.py +++ b/shopfloor/tests/test_zone_picking_set_line_destination.py @@ -94,7 +94,7 @@ def test_set_destination_location_confirm(self): ) # Check response move_lines = self.service._find_location_move_lines() - move_lines = move_lines.sorted(lambda l: l.move_id.priority, reverse=True) + move_lines = move_lines.sorted(lambda x: x.move_id.priority, reverse=True) self.assert_response_select_line( response, zone_location, @@ -168,7 +168,7 @@ def test_set_destination_location_no_other_move_line_full_qty(self): self.assertEqual(move_line.qty_done, 10) # Check response move_lines = self.service._find_location_move_lines() - move_lines = move_lines.sorted(lambda l: l.move_id.priority, reverse=True) + move_lines = move_lines.sorted(lambda x: x.move_id.priority, reverse=True) self.assert_response_select_line( response, zone_location, @@ -211,7 +211,7 @@ def test_set_destination_location_no_other_move_line_partial_qty(self): }, ) move_lines = self.service._find_location_move_lines() - move_lines = move_lines.sorted(lambda l: l.move_id.priority, reverse=True) + move_lines = move_lines.sorted(lambda x: x.move_id.priority, reverse=True) self.assert_response_select_line( response, zone_location, @@ -281,7 +281,7 @@ def test_set_destination_location_several_move_line_full_qty(self): self.assertNotEqual(move_line.move_id, other_move_line.move_id) # Check response move_lines = self.service._find_location_move_lines() - move_lines = move_lines.sorted(lambda l: l.move_id.priority, reverse=True) + move_lines = move_lines.sorted(lambda x: x.move_id.priority, reverse=True) self.assert_response_select_line( response, zone_location, @@ -327,7 +327,7 @@ def test_set_destination_location_several_move_line_partial_qty(self): ) # Check response move_lines = self.service._find_location_move_lines() - move_lines = move_lines.sorted(lambda l: l.move_id.priority, reverse=True) + move_lines = move_lines.sorted(lambda x: x.move_id.priority, reverse=True) self.assert_response_select_line( response, zone_location, @@ -414,7 +414,7 @@ def test_set_destination_package_full_qty(self): ) # Check response move_lines = self.service._find_location_move_lines() - move_lines = move_lines.sorted(lambda l: l.move_id.priority, reverse=True) + move_lines = move_lines.sorted(lambda x: x.move_id.priority, reverse=True) self.assert_response_select_line( response, zone_location, @@ -483,7 +483,7 @@ def test_set_destination_package_partial_qty(self): ) # Check response move_lines = self.service._find_location_move_lines() - move_lines = move_lines.sorted(lambda l: l.move_id.priority, reverse=True) + move_lines = move_lines.sorted(lambda x: x.move_id.priority, reverse=True) self.assert_response_select_line( response, zone_location, @@ -544,7 +544,7 @@ def test_set_same_destination_package_multiple_moves(self): self.assertTrue(location_is_empty()) # Check response move_lines = self.service._find_location_move_lines() - move_lines = move_lines.sorted(lambda l: l.move_id.priority, reverse=True) + move_lines = move_lines.sorted(lambda x: x.move_id.priority, reverse=True) self.assert_response_select_line( response, zone_location, @@ -588,7 +588,7 @@ def test_set_same_destination_package_multiple_moves(self): ) # We now have no error in the response move_lines = self.service._find_location_move_lines() - move_lines = move_lines.sorted(lambda l: l.move_id.priority, reverse=True) + move_lines = move_lines.sorted(lambda x: x.move_id.priority, reverse=True) self.assert_response_select_line( response, zone_location, diff --git a/shopfloor/tests/test_zone_picking_set_line_destination_no_prefill_qty.py b/shopfloor/tests/test_zone_picking_set_line_destination_no_prefill_qty.py index 5c4b1cb1e27..1731cea48c6 100644 --- a/shopfloor/tests/test_zone_picking_set_line_destination_no_prefill_qty.py +++ b/shopfloor/tests/test_zone_picking_set_line_destination_no_prefill_qty.py @@ -64,7 +64,7 @@ def test_set_destination_increment_with_wrong_package(self): move_line, qty_done=qty_done, message={ - "body": "Package {} is not empty.".format(wrong_package.name), + "body": f"Package {wrong_package.name} is not empty.", "message_type": "warning", }, ) diff --git a/shopfloor/tests/test_zone_picking_set_line_destination_package_not_allowed.py b/shopfloor/tests/test_zone_picking_set_line_destination_package_not_allowed.py index c9895d64eb1..17a952493a0 100644 --- a/shopfloor/tests/test_zone_picking_set_line_destination_package_not_allowed.py +++ b/shopfloor/tests/test_zone_picking_set_line_destination_package_not_allowed.py @@ -59,7 +59,7 @@ def test_set_destination_alternative_package_not_allowed_scan_package_partial_qt }, ) move_lines = self.service._find_location_move_lines() - move_lines = move_lines.sorted(lambda l: l.move_id.priority, reverse=True) + move_lines = move_lines.sorted(lambda x: x.move_id.priority, reverse=True) self.assert_response_set_line_destination( response, zone_location=self.zone_location, @@ -84,7 +84,7 @@ def test_set_destination_alternative_package_not_allowed_scan_location(self): }, ) move_lines = self.service._find_location_move_lines() - move_lines = move_lines.sorted(lambda l: l.move_id.priority, reverse=True) + move_lines = move_lines.sorted(lambda x: x.move_id.priority, reverse=True) self.assert_response_select_line( response, zone_location=self.zone_location, diff --git a/shopfloor/tests/test_zone_picking_set_line_destination_pick_pack.py b/shopfloor/tests/test_zone_picking_set_line_destination_pick_pack.py index 8608b14e1f0..66ce8727b6e 100644 --- a/shopfloor/tests/test_zone_picking_set_line_destination_pick_pack.py +++ b/shopfloor/tests/test_zone_picking_set_line_destination_pick_pack.py @@ -109,7 +109,7 @@ def test_set_destination_location_ok_carrier(self): ) # Check response move_lines = self.service._find_location_move_lines() - move_lines = move_lines.sorted(lambda l: l.move_id.priority, reverse=True) + move_lines = move_lines.sorted(lambda x: x.move_id.priority, reverse=True) delivery_pkg = move_line.result_package_id self.assertNotIn(delivery_pkg, existing_packages) self.assertEqual( @@ -227,7 +227,7 @@ def test_set_destination_package_full_qty_ok_carrier_ok_package(self): ) # Check response move_lines = self.service._find_location_move_lines() - move_lines = move_lines.sorted(lambda l: l.move_id.priority, reverse=True) + move_lines = move_lines.sorted(lambda x: x.move_id.priority, reverse=True) self.assert_response_select_line( response, zone_location, diff --git a/shopfloor/tests/test_zone_picking_unload_all.py b/shopfloor/tests/test_zone_picking_unload_all.py index 362b830a07a..852f2d2c0c9 100644 --- a/shopfloor/tests/test_zone_picking_unload_all.py +++ b/shopfloor/tests/test_zone_picking_unload_all.py @@ -329,7 +329,9 @@ def test_unload_split_buffer_multi_lines(self): {"name": "ANOTHER_PACKAGE"} ) for move_line, package_dest in zip( - self.picking5.move_line_ids, self.free_package | self.another_package + self.picking5.move_line_ids, + self.free_package | self.another_package, + strict=False, ): self.service._set_destination_package( move_line, diff --git a/shopfloor/tests/test_zone_picking_unload_set_destination.py b/shopfloor/tests/test_zone_picking_unload_set_destination.py index bb9d96fef5d..daed7ccae89 100644 --- a/shopfloor/tests/test_zone_picking_unload_set_destination.py +++ b/shopfloor/tests/test_zone_picking_unload_set_destination.py @@ -275,7 +275,7 @@ def test_unload_set_destination_ok_buffer_not_empty(self): ) move_lines = self.picking5.move_line_ids for move_line, package_dest in zip( - move_lines, self.free_package | self.another_package + move_lines, self.free_package | self.another_package, strict=False ): self.service._set_destination_package( move_line, @@ -283,7 +283,7 @@ def test_unload_set_destination_ok_buffer_not_empty(self): package_dest, ) free_package_line = move_lines.filtered( - lambda l: l.result_package_id == self.free_package + lambda x: x.result_package_id == self.free_package ) another_package_line = move_lines - free_package_line diff --git a/shopfloor/views/shopfloor_menu.xml b/shopfloor/views/shopfloor_menu.xml index e63a87ec264..4b9b2f1173c 100644 --- a/shopfloor/views/shopfloor_menu.xml +++ b/shopfloor/views/shopfloor_menu.xml @@ -5,34 +5,37 @@ - - - - - - + + + - - - - + + + - - - + + + - - + - - - - + + + - - - - + + + - - - - + + + - - - - + - - - - + + + - - + - - + - - - - + + + - - - - + + + - - - - + + - - - -
+ + +
When choosing an order to pack, do not allow to scan a product.
-
When selecting a move line, force the user to first scan a package or a location and not a product or a lot.
-
When selecting a move line, force the user to first scan a package or a location and not a product or a lot.
- - - + - - - - + + + @@ -161,15 +166,15 @@ invisible="1" /> - - + - - - - + + + @@ -183,56 +188,58 @@ name="search_move_line" attrs="{'invisible': [('move_line_search_sort_order_is_possible', '=', False), ('move_line_search_additional_domain_is_possible', '=', False)]}" > - - - - + -
- -

Three variables are available:

-
    -
  • Three variables are available:

    +
      +
    • line: The move line for with the key must be computed.
    • -
    • get_sort_key_priority: Return sort key to sort by priority. (return a tuple)
    • -
    • get_sort_key_location: Return sort key to sort by location. (return a tuple)
    • -
    • get_sort_key_assigned_to_current_user: Return sort key to get line assigned to current user first. (return a tuple)
    • -
    • To assign the key value: To assign the key value: key = get_sort_key_assigned_to_current_user(line) + (line.x, )
    • -
    - + -
- -
+
+
@@ -250,7 +257,7 @@ - \n" +"PO-Revision-Date: 2026-02-11 11:09+0000\n" +"Last-Translator: Artur Machura \n" "Language-Team: none\n" "Language: de\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 5.6.2\n" +"X-Generator: Weblate 5.15.2\n" #. module: shopfloor_base #: model:ir.model.fields,help:shopfloor_base.field_shopfloor_app__api_route @@ -24,45 +24,49 @@ msgid "" " internal controller-ready version.\n" " " msgstr "" +"\n" +" BBasisroute für Endpunkte, die dieser Anwendung zugeordnet sind,\n" +" interne, controller-fertige Version.\n" +" " #. module: shopfloor_base #. odoo-python #: code:addons/shopfloor_base/services/forms/form_mixin.py:0 #, python-format msgid "%s updated." -msgstr "" +msgstr "%s aktualisiert." #. module: shopfloor_base #: model:ir.model,name:shopfloor_base.model_shopfloor_app msgid "A Shopfloor application" -msgstr "" +msgstr "Eine Lagerverwaltungssystemanwendung" #. module: shopfloor_base #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_app__active #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_menu__active #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_profile__active msgid "Active" -msgstr "" +msgstr "Aktiv" #. module: shopfloor_base #: model_terms:ir.ui.view,arch_db:shopfloor_base.shopfloor_app_search_view msgid "All" -msgstr "" +msgstr "Alle" #. module: shopfloor_base #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_app__api_docs_url msgid "Api Docs Url" -msgstr "" +msgstr "API-Dokumentations-URL" #. module: shopfloor_base #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_app__api_route msgid "Api Route" -msgstr "" +msgstr "API-Route" #. module: shopfloor_base #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_app__app_version msgid "App Version" -msgstr "" +msgstr "App-Version" #. module: shopfloor_base #: model_terms:ir.ui.view,arch_db:shopfloor_base.shopfloor_app_form_view @@ -71,33 +75,33 @@ msgstr "" #: model_terms:ir.ui.view,arch_db:shopfloor_base.shopfloor_profile_form_view #: model_terms:ir.ui.view,arch_db:shopfloor_base.shopfloor_profile_search_view msgid "Archived" -msgstr "" +msgstr "Archiviert" #. module: shopfloor_base #: model_terms:ir.ui.view,arch_db:shopfloor_base.shopfloor_app_form_view msgid "Auth" -msgstr "" +msgstr "Authentifizierung" #. module: shopfloor_base #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_app__auth_type msgid "Auth Type" -msgstr "" +msgstr "Authentifizierungsart" #. module: shopfloor_base #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_app__lang_ids msgid "Available languages" -msgstr "" +msgstr "Verfügbare Sprachen" #. module: shopfloor_base #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_app__category #: model_terms:ir.ui.view,arch_db:shopfloor_base.shopfloor_app_search_view msgid "Category" -msgstr "" +msgstr "Kategorie" #. module: shopfloor_base #: model:ir.model.fields,help:shopfloor_base.field_shopfloor_scenario__options_edit msgid "Configure options via JSON" -msgstr "" +msgstr "Optionen über JSON konfigurieren" #. module: shopfloor_base #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_app__create_uid @@ -105,7 +109,7 @@ msgstr "" #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_profile__create_uid #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_scenario__create_uid msgid "Created by" -msgstr "" +msgstr "Erstellt von" #. module: shopfloor_base #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_app__create_date @@ -113,32 +117,32 @@ msgstr "" #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_profile__create_date #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_scenario__create_date msgid "Created on" -msgstr "" +msgstr "Erzeugt am" #. module: shopfloor_base #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_app__lang_id msgid "Default language" -msgstr "" +msgstr "Standardsprache" #. module: shopfloor_base #: model:shopfloor.profile,name:shopfloor_base.profile_demo_1 msgid "Demo Profile 1" -msgstr "" +msgstr "Demoprofil 1" #. module: shopfloor_base #: model:shopfloor.profile,name:shopfloor_base.profile_demo_2 msgid "Demo Profile 2" -msgstr "" +msgstr "Demoprofil 2" #. module: shopfloor_base #: model:shopfloor.scenario,name:shopfloor_base.shopfloor_scenario_demo_1 msgid "Demo scenario 1" -msgstr "" +msgstr "Demoszenario 1" #. module: shopfloor_base #: model_terms:ir.ui.view,arch_db:shopfloor_base.shopfloor_app_form_view msgid "Developer" -msgstr "" +msgstr "Entwickler" #. module: shopfloor_base #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_app__display_name @@ -146,18 +150,18 @@ msgstr "" #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_profile__display_name #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_scenario__display_name msgid "Display Name" -msgstr "" +msgstr "Anzeigname" #. module: shopfloor_base #: model_terms:ir.ui.view,arch_db:shopfloor_base.shopfloor_app_search_view #: model_terms:ir.ui.view,arch_db:shopfloor_base.shopfloor_menu_search_view msgid "Group By" -msgstr "" +msgstr "Gruppiere nach" #. module: shopfloor_base #: model:ir.model,name:shopfloor_base.model_ir_http msgid "HTTP Routing" -msgstr "" +msgstr "HTTP-Weiterleitung" #. module: shopfloor_base #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_app__id @@ -165,7 +169,7 @@ msgstr "" #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_profile__id #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_scenario__id msgid "ID" -msgstr "" +msgstr "ID" #. module: shopfloor_base #: model:ir.model.fields,help:shopfloor_base.field_shopfloor_menu__scenario_key @@ -178,18 +182,18 @@ msgstr "" #. module: shopfloor_base #: model:ir.model.fields,help:shopfloor_base.field_shopfloor_app__lang_id msgid "If set, the app will be first loaded with this lang." -msgstr "" +msgstr "Falls gesetzt, wird die App zunächst in dieser Sprache geladen." #. module: shopfloor_base #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_menu__scenario_key #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_scenario__key msgid "Key" -msgstr "" +msgstr "Schlüssel" #. module: shopfloor_base #: model_terms:ir.ui.view,arch_db:shopfloor_base.shopfloor_app_form_view msgid "Language" -msgstr "" +msgstr "Sprache" #. module: shopfloor_base #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_app____last_update @@ -197,7 +201,7 @@ msgstr "" #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_profile____last_update #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_scenario____last_update msgid "Last Modified on" -msgstr "" +msgstr "Zuletzt geändert am" #. module: shopfloor_base #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_app__write_uid @@ -205,7 +209,7 @@ msgstr "" #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_profile__write_uid #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_scenario__write_uid msgid "Last Updated by" -msgstr "" +msgstr "Zuletzt aktualisiert von" #. module: shopfloor_base #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_app__write_date @@ -213,7 +217,7 @@ msgstr "" #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_profile__write_date #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_scenario__write_date msgid "Last Updated on" -msgstr "" +msgstr "Zuletzt aktualisiert am" #. module: shopfloor_base #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_menu__scenario @@ -228,7 +232,7 @@ msgstr "" #. module: shopfloor_base #: model_terms:ir.ui.view,arch_db:shopfloor_base.shopfloor_menu_form_view msgid "Menu Options" -msgstr "" +msgstr "Menüoptionen" #. module: shopfloor_base #: model:ir.model,name:shopfloor_base.model_shopfloor_menu @@ -253,12 +257,12 @@ msgstr "Für dieses Profil sichtbare Menüs" #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_profile__name #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_scenario__name msgid "Name" -msgstr "" +msgstr "Name" #. module: shopfloor_base #: model:ir.model.fields,help:shopfloor_base.field_shopfloor_app__short_name msgid "Needed for app manifest" -msgstr "" +msgstr "Für das Manifest der App notwendig" #. module: shopfloor_base #: model:ir.model.fields.selection,name:shopfloor_base.selection__shopfloor_app__category__ @@ -277,23 +281,23 @@ msgstr "" #. module: shopfloor_base #: model_terms:ir.ui.view,arch_db:shopfloor_base.shopfloor_app_form_view msgid "Open app" -msgstr "" +msgstr "App öffnen" #. module: shopfloor_base #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_scenario__options msgid "Options" -msgstr "" +msgstr "Optionen" #. module: shopfloor_base #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_scenario__options_edit msgid "Options Edit" -msgstr "" +msgstr "Optionen editiern" #. module: shopfloor_base #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_menu__profile_id #: model_terms:ir.ui.view,arch_db:shopfloor_base.shopfloor_menu_search_view msgid "Profile" -msgstr "" +msgstr "Profil" #. module: shopfloor_base #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_app__profile_required @@ -323,7 +327,7 @@ msgstr "" #. module: shopfloor_base #: model:ir.model.fields,help:shopfloor_base.field_shopfloor_app__api_docs_url msgid "Public URL for api docs." -msgstr "" +msgstr "Öffentliche URL für Schnittstellendokumentation." #. module: shopfloor_base #: model:ir.model.fields,help:shopfloor_base.field_shopfloor_app__url @@ -335,7 +339,7 @@ msgstr "" #: code:addons/shopfloor_base/actions/message.py:0 #, python-format msgid "Record not found." -msgstr "" +msgstr "Datensatz nicht gefunden." #. module: shopfloor_base #. odoo-python @@ -349,7 +353,7 @@ msgstr "" #. module: shopfloor_base #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_app__registered_routes msgid "Registered Routes" -msgstr "" +msgstr "Registrierte Routen" #. module: shopfloor_base #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_app__registry_sync @@ -362,6 +366,8 @@ msgid "" "Registry out of sync. Likely the record has been modified but not sync'ed " "with the routing registry." msgstr "" +"Die Registry ist nicht synchron. Wahrscheinlich wurde der Datensatz " +"geändert, aber nicht mit der Routing-Registry synchronisiert." #. module: shopfloor_base #: model:ir.actions.act_window,name:shopfloor_base.action_shopfloor_scenario @@ -369,55 +375,55 @@ msgstr "" #: model:ir.ui.menu,name:shopfloor_base.menu_action_shopfloor_scenario #: model_terms:ir.ui.view,arch_db:shopfloor_base.shopfloor_menu_search_view msgid "Scenario" -msgstr "" +msgstr "Szenario" #. module: shopfloor_base #: model:ir.model.constraint,message:shopfloor_base.constraint_shopfloor_scenario_key msgid "Scenario key must be unique" -msgstr "" +msgstr "Der Szenarioschlüssel muss eindeutig sein" #. module: shopfloor_base #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_menu__sequence #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_profile__sequence msgid "Sequence" -msgstr "" +msgstr "Reihenfolge" #. module: shopfloor_base #: model:ir.module.category,name:shopfloor_base.module_category_shopfloor #: model:ir.ui.menu,name:shopfloor_base.menu_shopfloor_root msgid "Shopfloor" -msgstr "" +msgstr "Shopfloor" #. module: shopfloor_base #: model:res.groups,name:shopfloor_base.group_shopfloor_manager msgid "Shopfloor Manager" -msgstr "" +msgstr "Shopfloor Manager" #. module: shopfloor_base #: model:ir.model,name:shopfloor_base.model_shopfloor_scenario msgid "Shopfloor Scenario" -msgstr "" +msgstr "Shopfloor Szenario" #. module: shopfloor_base #: model:res.groups,name:shopfloor_base.group_shopfloor_user msgid "Shopfloor User" -msgstr "" +msgstr "Shopfloor Benutzer" #. module: shopfloor_base #: model:ir.actions.act_window,name:shopfloor_base.action_shopfloor_app #: model:ir.ui.menu,name:shopfloor_base.menu_action_shopfloor_app msgid "Shopfloor apps" -msgstr "" +msgstr "Shopfloor Apps" #. module: shopfloor_base #: model:ir.model,name:shopfloor_base.model_shopfloor_profile msgid "Shopfloor profile settings" -msgstr "" +msgstr "Shopfloor Profileinstellungen" #. module: shopfloor_base #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_app__short_name msgid "Short Name" -msgstr "" +msgstr "Kurzname" #. module: shopfloor_base #: model:shopfloor.menu,name:shopfloor_base.shopfloor_menu_demo_1 @@ -427,35 +433,37 @@ msgstr "" #. module: shopfloor_base #: model:ir.actions.server,name:shopfloor_base.server_action_registry_sync msgid "Sync registry" -msgstr "" +msgstr "Registry synchronisieren" #. module: shopfloor_base #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_app__tech_name msgid "Tech Name" -msgstr "" +msgstr "Technischer Name" #. module: shopfloor_base #: model:ir.model.fields,help:shopfloor_base.field_shopfloor_app__registered_routes msgid "" "Technical field to allow developers to check registered routes on the form" msgstr "" +"Technisches Feld, das es Entwicklern ermöglicht, registrierte Routen im " +"Formular zu überprüfen" #. module: shopfloor_base #. odoo-python #: code:addons/shopfloor_base/services/service.py:0 #, python-format msgid "The record {model} {_id} does not exist" -msgstr "" +msgstr "Der Datensatz {model} {_id} existiert nicht" #. module: shopfloor_base #: model_terms:ir.ui.view,arch_db:shopfloor_base.shopfloor_app_search_view msgid "To sync" -msgstr "" +msgstr "Zu synchronisieren" #. module: shopfloor_base #: model:ir.model.fields,field_description:shopfloor_base.field_shopfloor_app__url msgid "Url" -msgstr "" +msgstr "URL" #. module: shopfloor_base #: model_terms:ir.ui.view,arch_db:shopfloor_base.shopfloor_app_form_view @@ -463,16 +471,18 @@ msgid "" "Use the action \"Sync registry\" to make changes effective once you are done" " with edits and creates." msgstr "" +"Verwenden Sie die Aktion „Registry synchronisieren“, um Änderungen nach " +"Abschluss der Bearbeitungen und Erstellungen wirksam werden zu lassen." #. module: shopfloor_base #: model_terms:ir.ui.view,arch_db:shopfloor_base.shopfloor_app_form_view msgid "View api docs" -msgstr "" +msgstr "Schnittstellendokumentation ansehen" #. module: shopfloor_base #: model_terms:ir.ui.view,arch_db:shopfloor_base.shopfloor_app_form_view msgid "View menu items" -msgstr "" +msgstr "Menüeinträge ansehen" #. module: shopfloor_base #: model:ir.model.fields,help:shopfloor_base.field_shopfloor_menu__profile_id @@ -482,4 +492,4 @@ msgstr "" #. module: shopfloor_base #: model:ir.model.constraint,message:shopfloor_base.constraint_shopfloor_app_tech_name msgid "tech_name must be unique" -msgstr "" +msgstr "tech_name muss einzigartig sein" diff --git a/shopfloor_base/models/shopfloor_app.py b/shopfloor_base/models/shopfloor_app.py index 12142868c84..65bc40713f7 100644 --- a/shopfloor_base/models/shopfloor_app.py +++ b/shopfloor_base/models/shopfloor_app.py @@ -266,7 +266,7 @@ def _generate_endpoints_route(self, service, vals): def _prepare_endpoint_vals(self, service, method_name, route, routing_params): request_method = routing_params["methods"][0] - name = f"app#{self.id}::{service._name}/{method_name}__{request_method.lower()}" + name = f"app#{self.id}::{service._name}/{method_name}__{request_method.lower()}" # noqa endpoint_vals = dict( name=name, request_method=request_method, @@ -278,7 +278,7 @@ def _prepare_endpoint_vals(self, service, method_name, route, routing_params): return endpoint_vals def _route_group(self): - return f"{self._name}:{self.tech_name}" + return f"{self._name}:{self.tech_name}" # noqa def _is_component_registry_ready(self): comp_registry = _component_databases.get(self.env.cr.dbname) diff --git a/shopfloor_base/static/description/index.html b/shopfloor_base/static/description/index.html index c010669035a..1a88ab995eb 100644 --- a/shopfloor_base/static/description/index.html +++ b/shopfloor_base/static/description/index.html @@ -367,7 +367,7 @@

Shopfloor Base

!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:3fba5235262f88cb9db1cacc2c23d8c65b878fc34084712baf6bc83d201ab3e7 +!! source digest: sha256:20bc1f30d736fb59db86e1756b980c66dcc273077b510bd312ac1ff1e345a85d !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

Beta License: LGPL-3 OCA/wms Translate me on Weblate Try me on Runboat

Shopfloor is a barcode scanner application.

diff --git a/shopfloor_batch_automatic_creation/i18n/it.po b/shopfloor_batch_automatic_creation/i18n/it.po index c2d549ad374..ff8dca0c1ab 100644 --- a/shopfloor_batch_automatic_creation/i18n/it.po +++ b/shopfloor_batch_automatic_creation/i18n/it.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0\n" "Report-Msgid-Bugs-To: \n" -"PO-Revision-Date: 2024-10-05 17:06+0000\n" +"PO-Revision-Date: 2026-01-28 18:09+0000\n" "Last-Translator: mymage \n" "Language-Team: none\n" "Language: it\n" @@ -14,7 +14,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 5.6.2\n" +"X-Generator: Weblate 5.15.2\n" #. module: shopfloor_batch_automatic_creation #: model_terms:ir.ui.view,arch_db:shopfloor_batch_automatic_creation.stock_device_type_form_view @@ -103,7 +103,7 @@ msgstr "Metodo chiave di ordinamento della riga" #. module: shopfloor_batch_automatic_creation #: model:ir.model.fields,field_description:shopfloor_batch_automatic_creation.field_shopfloor_menu__batch_maximum_number_of_preparation_lines msgid "Maximum number of preparation lines for the batch" -msgstr "Numero massim di righe di preparazione per il gruppo" +msgstr "Numero massimo di righe di preparazione per il gruppo" #. module: shopfloor_batch_automatic_creation #: model:ir.model,name:shopfloor_batch_automatic_creation.model_shopfloor_menu diff --git a/shopfloor_mobile/README.rst b/shopfloor_mobile/README.rst index 988e1881b27..420cb24069e 100644 --- a/shopfloor_mobile/README.rst +++ b/shopfloor_mobile/README.rst @@ -1,3 +1,7 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + ================ Shopfloor mobile ================ @@ -7,13 +11,13 @@ Shopfloor mobile !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:e2e8c5dda324092d44eb40ecd4e2bfd44696c59cdc3af9ded861ef7401f7d971 + !! source digest: sha256:f07e5aafb87420fa19f88d904f3718cfd8877b4730a036b454a4feb9d70900a8 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png :target: https://odoo-community.org/page/development-status :alt: Beta -.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png +.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fwms-lightgray.png?logo=github diff --git a/shopfloor_mobile/__manifest__.py b/shopfloor_mobile/__manifest__.py index cce3b38b0a1..b7aaa54e73a 100644 --- a/shopfloor_mobile/__manifest__.py +++ b/shopfloor_mobile/__manifest__.py @@ -5,7 +5,7 @@ { "name": "Shopfloor mobile", "summary": "Mobile frontend for WMS Shopfloor app", - "version": "16.0.1.4.0", + "version": "16.0.1.4.1", "depends": ["shopfloor", "shopfloor_mobile_base"], "author": "Camptocamp, BCIM, Akretion, Odoo Community Association (OCA)", "maintainers": ["simahawk"], diff --git a/shopfloor_mobile/static/description/index.html b/shopfloor_mobile/static/description/index.html index f437eee30bc..3b1e1642ee8 100644 --- a/shopfloor_mobile/static/description/index.html +++ b/shopfloor_mobile/static/description/index.html @@ -3,7 +3,7 @@ -Shopfloor mobile +README.rst -
-

Shopfloor mobile

+
+ + +Odoo Community Association + +
+

Shopfloor mobile

-

Beta License: AGPL-3 OCA/wms Translate me on Weblate Try me on Runboat

+

Beta License: AGPL-3 OCA/wms Translate me on Weblate Try me on Runboat

Frontend for Shopfloor app.

The work is organized in scenario. A scenario represents a process in the warehouse (eg: receive, deliver). @@ -377,7 +382,7 @@

Shopfloor mobile

Each scenario is linked to a specific menu item which can be configured in the backend. Each scenario drives you through the work to do.

-

Tech details

+

Tech details

  • This frontend is built on top of VueJS and VuetifyJS and relies on shopfloor module that exposes REST API in Odoo @@ -404,18 +409,18 @@

    Tech details

-

Pre-requisites

+

Pre-requisites

  • Your Odoo instance is accessible via mobile device
  • You have an API Key configured
-

Start the app

+

Start the app

  • Go to “Inventory -> Configuration -> Shopfloor -> Shopfloor App”
  • In the login screen fill in your API key
  • @@ -423,7 +428,7 @@

    Start the app

-

Select a profile

+

Select a profile

Several profiles can be configured in the backend, you must choose one before starting.

    @@ -433,17 +438,17 @@

    Select a profile

    This will load all available menu items for the selected profile.

-

Change language

+

Change language

  • Go to “Settings -> Language”
  • Select a language
-

Customization

+

Customization

Please refer to shopfloor_mobile_custom_example.

-

Known issues / Roadmap

+

Known issues / Roadmap

  • Split module by scenario

  • @@ -483,14 +488,14 @@

    Known issues / Roadmap

-

Changelog

+

Changelog

-

13.0.1.0.0

+

13.0.1.0.0

First official version.

-

Bug Tracker

+

Bug Tracker

Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed @@ -498,11 +503,11 @@

Bug Tracker

Do not contact contributors directly about support or help with technical issues.

-

Credits

+

Credits

-

Authors

+

Authors

  • Camptocamp
  • BCIM
  • @@ -510,7 +515,7 @@

    Authors

-

Contributors

+

Contributors

-

Design

+

Design

-

Other credits

+

Other credits

Financial support

  • Cosanum
  • @@ -537,7 +542,7 @@

    Other credits

+
diff --git a/shopfloor_mobile/static/wms/src/components/detail/detail_product.js b/shopfloor_mobile/static/wms/src/components/detail/detail_product.js index fc3d1db0ca1..84736c8db47 100644 --- a/shopfloor_mobile/static/wms/src/components/detail/detail_product.js +++ b/shopfloor_mobile/static/wms/src/components/detail/detail_product.js @@ -136,9 +136,10 @@ Vue.component("detail-product", { - + Lots \n" +"Language-Team: none\n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.10.4\n" + +#. module: shopfloor_mobile_base +#: model_terms:ir.ui.view,arch_db:shopfloor_mobile_base.shopfloor_app_main +msgid "" +"\n" +" We're sorry but wms doesn't work properly without JavaScript enabled.\n" +" Please enable it to continue.\n" +" " +msgstr "" +"\n" +" Es tut uns leid, aber das Lagerverwaltungssystem funktioniert " +"nicht richtig, wenn JavaScript nicht aktiviert ist.\n" +" Bitte aktivieren Sie es, um fortzufahren.\n" +" " + +#. module: shopfloor_mobile_base +#: model_terms:ir.ui.view,arch_db:shopfloor_mobile_base.shopfloor_app_assets +msgid "Hook to this element to inject your own scenario" +msgstr "Hängen Sie sich in dieses Element mit Ihrem eigenen Szenario ein" diff --git a/shopfloor_mobile_base_auth_api_key/i18n/de.po b/shopfloor_mobile_base_auth_api_key/i18n/de.po new file mode 100644 index 00000000000..b09e8ad2021 --- /dev/null +++ b/shopfloor_mobile_base_auth_api_key/i18n/de.po @@ -0,0 +1,42 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * shopfloor_mobile_base_auth_api_key +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2026-02-11 11:09+0000\n" +"Last-Translator: Artur Machura \n" +"Language-Team: none\n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.15.2\n" + +#. module: shopfloor_mobile_base_auth_api_key +#: model:ir.model,name:shopfloor_mobile_base_auth_api_key.model_shopfloor_app +msgid "A Shopfloor application" +msgstr "Eine Lagerverwaltungssystemanwendung" + +#. module: shopfloor_mobile_base_auth_api_key +#: model:ir.model.fields,field_description:shopfloor_mobile_base_auth_api_key.field_shopfloor_app__auth_api_key_group_ids +msgid "Allowed API key groups" +msgstr "Erlaubte API Schlüsselgruppen" + +#. module: shopfloor_mobile_base_auth_api_key +#: model_terms:ir.ui.view,arch_db:shopfloor_mobile_base_auth_api_key.shopfloor_app_form_view +msgid "Manage groups" +msgstr "Gruppen verwalten" + +#. module: shopfloor_mobile_base_auth_api_key +#: model:shopfloor.app,name:shopfloor_mobile_base_auth_api_key.app_demo1 +msgid "Shopfloor Demo (api key auth)" +msgstr "Shopfloor Demo (Auth Schnittstellenschlüssel)" + +#. module: shopfloor_mobile_base_auth_api_key +#: model:shopfloor.app,short_name:shopfloor_mobile_base_auth_api_key.app_demo1 +msgid "demo api key" +msgstr "Demo Schnittstellenschlüssel" diff --git a/shopfloor_reception/README.rst b/shopfloor_reception/README.rst index 2bc3044e8fb..d6f1ddb0f96 100644 --- a/shopfloor_reception/README.rst +++ b/shopfloor_reception/README.rst @@ -1,3 +1,7 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + =================== Shopfloor Reception =================== @@ -7,13 +11,13 @@ Shopfloor Reception !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:41faa7df7bdb71ba8f5be9c536daff42f29bea13d2ae79892e084f5259c2d1ce + !! source digest: sha256:f8d87b75abf2f07115fe503fe887e2266295e7bc6e9e86e9818ff85ed88bff85 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png :target: https://odoo-community.org/page/development-status :alt: Beta -.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png +.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fwms-lightgray.png?logo=github diff --git a/shopfloor_reception/__init__.py b/shopfloor_reception/__init__.py index 2d9836a7424..b447b57f1b2 100644 --- a/shopfloor_reception/__init__.py +++ b/shopfloor_reception/__init__.py @@ -1,3 +1,2 @@ -from . import services -from . import models +from . import services, models from .hooks import post_init_hook, uninstall_hook diff --git a/shopfloor_reception/__manifest__.py b/shopfloor_reception/__manifest__.py index afbe051d4f4..3cd79d28e16 100644 --- a/shopfloor_reception/__manifest__.py +++ b/shopfloor_reception/__manifest__.py @@ -1,7 +1,7 @@ { "name": "Shopfloor Reception", "summary": "Reception scenario for shopfloor", - "version": "16.0.1.0.0", + "version": "16.0.1.6.6", "development_status": "Beta", "category": "Inventory", "website": "https://github.com/OCA/wms", @@ -10,8 +10,10 @@ "license": "AGPL-3", "installable": True, "depends": ["shopfloor"], + "external_dependencies": {"python": ["openupgradelib"]}, "data": [ "data/shopfloor_scenario_data.xml", + "views/shopfloor_menu.xml", ], "demo": [ "demo/stock_picking_type_demo.xml", diff --git a/shopfloor_reception/data/shopfloor_scenario_data.xml b/shopfloor_reception/data/shopfloor_scenario_data.xml index 67b04aa8233..fd86065f8e0 100644 --- a/shopfloor_reception/data/shopfloor_scenario_data.xml +++ b/shopfloor_reception/data/shopfloor_scenario_data.xml @@ -10,7 +10,8 @@ { "auto_post_line": true, "allow_return": true, - "scan_location_or_pack_first": true + "scan_location_or_pack_first": true, + "allow_filter_today_scheduled_pickings": true }
diff --git a/shopfloor_reception/i18n/it.po b/shopfloor_reception/i18n/it.po index 9fa89b51eeb..bc65cfa65bd 100644 --- a/shopfloor_reception/i18n/it.po +++ b/shopfloor_reception/i18n/it.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 14.0\n" "Report-Msgid-Bugs-To: \n" -"PO-Revision-Date: 2023-09-20 04:45+0000\n" +"PO-Revision-Date: 2025-08-27 15:25+0000\n" "Last-Translator: mymage \n" "Language-Team: none\n" "Language: it\n" @@ -14,12 +14,45 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.17\n" +"X-Generator: Weblate 5.10.4\n" #. module: shopfloor_reception -#: model:ir.model.fields,field_description:shopfloor_reception.field_stock_picking__is_shopfloor_created -msgid "Is Shopfloor Created" -msgstr "Il reparto è crato" +#: model:ir.model.fields,help:shopfloor_reception.field_shopfloor_menu__filter_today_scheduled_pickings +msgid "" +"\n" +"By default, at first step, filter the available\n" +"pickings with the ones that are scheduled for today.\n" +msgstr "" +"\n" +"Per impostazione predefinita, al primo passaggio, filtra \n" +"i prelievi disponibili con quelli programmati per oggi.\n" + +#. module: shopfloor_reception +#. odoo-python +#: code:addons/shopfloor_reception/models/shopfloor_menu.py:0 +#, python-format +msgid "Filter Today Pickings is not allowed for menu {}." +msgstr "Il filtro Prelievi odierni non è consentito per il menu {}." + +#. module: shopfloor_reception +#: model:ir.model.fields,field_description:shopfloor_reception.field_shopfloor_menu__filter_today_scheduled_pickings +msgid "Filter Today Scheduled Pickings" +msgstr "Filtro Prelievi schedulati oggi" + +#. module: shopfloor_reception +#: model:ir.model.fields,field_description:shopfloor_reception.field_shopfloor_menu__filter_today_scheduled_pickings_is_possible +msgid "Filter Today Scheduled Pickings Is Possible" +msgstr "Filtro Possibili prelievi schedulati oggi" + +#. module: shopfloor_reception +#: model:ir.model,name:shopfloor_reception.model_shopfloor_menu +msgid "Menu displayed in the scanner application" +msgstr "Menu visualizzato nell'applicazione di scansione" + +#. module: shopfloor_reception +#: model:ir.model,name:shopfloor_reception.model_stock_move_line +msgid "Product Moves (Stock Move Line)" +msgstr "Movimenti prodotto (riga movimento di magazzino)" #. module: shopfloor_reception #: model:shopfloor.menu,name:shopfloor_reception.shopfloor_menu_demo_reception @@ -28,10 +61,11 @@ msgstr "Il reparto è crato" msgid "Reception" msgstr "Ricezione" -#. module: shopfloor_reception -#: model:ir.model,name:shopfloor_reception.model_stock_picking -msgid "Transfer" -msgstr "Trasferimento" +#~ msgid "Is Shopfloor Created" +#~ msgstr "Il reparto è crato" + +#~ msgid "Transfer" +#~ msgstr "Trasferimento" #~ msgid "Display Name" #~ msgstr "Nome visualizzato" diff --git a/shopfloor_reception/i18n/shopfloor_reception.pot b/shopfloor_reception/i18n/shopfloor_reception.pot index 78dd7fc6792..419d7292ed6 100644 --- a/shopfloor_reception/i18n/shopfloor_reception.pot +++ b/shopfloor_reception/i18n/shopfloor_reception.pot @@ -14,8 +14,38 @@ msgstr "" "Plural-Forms: \n" #. module: shopfloor_reception -#: model:ir.model.fields,field_description:shopfloor_reception.field_stock_picking__is_shopfloor_created -msgid "Is Shopfloor Created" +#: model:ir.model.fields,help:shopfloor_reception.field_shopfloor_menu__filter_today_scheduled_pickings +msgid "" +"\n" +"By default, at first step, filter the available\n" +"pickings with the ones that are scheduled for today.\n" +msgstr "" + +#. module: shopfloor_reception +#. odoo-python +#: code:addons/shopfloor_reception/models/shopfloor_menu.py:0 +#, python-format +msgid "Filter Today Pickings is not allowed for menu {}." +msgstr "" + +#. module: shopfloor_reception +#: model:ir.model.fields,field_description:shopfloor_reception.field_shopfloor_menu__filter_today_scheduled_pickings +msgid "Filter Today Scheduled Pickings" +msgstr "" + +#. module: shopfloor_reception +#: model:ir.model.fields,field_description:shopfloor_reception.field_shopfloor_menu__filter_today_scheduled_pickings_is_possible +msgid "Filter Today Scheduled Pickings Is Possible" +msgstr "" + +#. module: shopfloor_reception +#: model:ir.model,name:shopfloor_reception.model_shopfloor_menu +msgid "Menu displayed in the scanner application" +msgstr "" + +#. module: shopfloor_reception +#: model:ir.model,name:shopfloor_reception.model_stock_move_line +msgid "Product Moves (Stock Move Line)" msgstr "" #. module: shopfloor_reception @@ -24,8 +54,3 @@ msgstr "" #: model:stock.picking.type,name:shopfloor_reception.picking_type_reception_demo msgid "Reception" msgstr "" - -#. module: shopfloor_reception -#: model:ir.model,name:shopfloor_reception.model_stock_picking -msgid "Transfer" -msgstr "" diff --git a/shopfloor_reception/migrations/16.0.1.2.0/pre-migration.py b/shopfloor_reception/migrations/16.0.1.2.0/pre-migration.py new file mode 100644 index 00000000000..edd8a66cda0 --- /dev/null +++ b/shopfloor_reception/migrations/16.0.1.2.0/pre-migration.py @@ -0,0 +1,21 @@ +# Copyright 2023 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + + +from openupgradelib import openupgrade + + +def move_fields_to_new_module(cr): + # is_shopfloor_created has been wrongly put in shopfloor_reception. + # this script moves it in shopfloor. + openupgrade.update_module_moved_fields( + cr, + "stock.picking", + ["is_shopfloor_created"], + "shopfloor_reception", + "shopfloor", + ) + + +def migrate(cr, version): + move_fields_to_new_module(cr) diff --git a/shopfloor_reception/migrations/16.0.1.3.0/post-migrate.py b/shopfloor_reception/migrations/16.0.1.3.0/post-migrate.py new file mode 100644 index 00000000000..be021b28919 --- /dev/null +++ b/shopfloor_reception/migrations/16.0.1.3.0/post-migrate.py @@ -0,0 +1,31 @@ +import json +import logging + +from odoo import SUPERUSER_ID, api + +_logger = logging.getLogger(__name__) + + +def migrate(cr, version): + if not version: + return + env = api.Environment(cr, SUPERUSER_ID, {}) + reception = env.ref("shopfloor_reception.scenario_reception") + _update_scenario_options( + reception, sort_order=False, additional_domain=True, today=True + ) + + +def _update_scenario_options( + scenario, sort_order=True, additional_domain=True, today=True +): + options = scenario.options + options["allow_move_line_search_sort_order"] = sort_order + options["allow_move_line_search_additional_domain"] = additional_domain + options["allow_filter_today_scheduled_pickings"] = today + options_edit = json.dumps(options or {}, indent=4, sort_keys=True) + scenario.write({"options_edit": options_edit}) + _logger.info( + "Option allow_move_line_search_additional_domain added to scenario %s", + scenario.name, + ) diff --git a/shopfloor_reception/models/__init__.py b/shopfloor_reception/models/__init__.py index ae4c27227f1..eacaa196020 100644 --- a/shopfloor_reception/models/__init__.py +++ b/shopfloor_reception/models/__init__.py @@ -1 +1,2 @@ -from . import stock_picking +from . import shopfloor_menu +from . import stock_move_line diff --git a/shopfloor_reception/models/shopfloor_menu.py b/shopfloor_reception/models/shopfloor_menu.py new file mode 100644 index 00000000000..c863b1b5cca --- /dev/null +++ b/shopfloor_reception/models/shopfloor_menu.py @@ -0,0 +1,46 @@ +# Copyright 2025 ACSONE SA/NV (https://www.acsone.eu) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo import _, api, exceptions, fields, models + +FILTER_TODAY_SCHEDULED_PICKINGS_HELP = """ +By default, at first step, filter the available +pickings with the ones that are scheduled for today. +""" + + +class ShopfloorMenu(models.Model): + _inherit = "shopfloor.menu" + + filter_today_scheduled_pickings_is_possible = fields.Boolean( + compute="_compute_filter_today_scheduled_pickings_is_possible" + ) + filter_today_scheduled_pickings = fields.Boolean( + default=False, + help=FILTER_TODAY_SCHEDULED_PICKINGS_HELP, + ) + + @api.depends("scenario_id") + def _compute_filter_today_scheduled_pickings_is_possible(self): + for menu in self: + menu.filter_today_scheduled_pickings_is_possible = bool( + menu.scenario_id.has_option("allow_filter_today_scheduled_pickings") + ) + + @api.onchange("filter_today_scheduled_pickings_is_possible") + def onchange_filter_today_scheduled_pickings_is_possible(self): + self.filter_today_scheduled_pickings = ( + self.filter_today_scheduled_pickings_is_possible + ) + + @api.constrains("scenario_id", "picking_type_ids", "allow_move_create") + def _check_filter_today_scheduled_pickings(self): + for menu in self: + if ( + menu.filter_today_scheduled_pickings + and not menu.filter_today_scheduled_pickings_is_possible + ): + raise exceptions.ValidationError( + _("Filter Today Pickings is not allowed for menu {}.").format( + menu.name + ) + ) diff --git a/shopfloor_reception/models/stock_move_line.py b/shopfloor_reception/models/stock_move_line.py new file mode 100644 index 00000000000..03bb9d6e2da --- /dev/null +++ b/shopfloor_reception/models/stock_move_line.py @@ -0,0 +1,19 @@ +# Copyright 2025 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +from odoo import models + + +class StockMoveLine(models.Model): + + _inherit = "stock.move.line" + + @property + def shopfloor_should_create_lot(self) -> bool: + """ + This will return True if the line should be used to create lots + """ + return bool( + (not self.lot_id and not self.lot_name) + and self.picking_type_use_create_lots + ) diff --git a/shopfloor_reception/models/stock_picking.py b/shopfloor_reception/models/stock_picking.py deleted file mode 100644 index bd69e105a04..00000000000 --- a/shopfloor_reception/models/stock_picking.py +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright 2023 Camptocamp SA -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) - -from odoo import fields, models - - -class StockPicking(models.Model): - _inherit = "stock.picking" - - is_shopfloor_created = fields.Boolean() diff --git a/shopfloor_reception/services/reception.py b/shopfloor_reception/services/reception.py index 212711d5e08..c52f778b948 100644 --- a/shopfloor_reception/services/reception.py +++ b/shopfloor_reception/services/reception.py @@ -4,13 +4,17 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) +from datetime import datetime, time + import pytz +from decorator import contextmanager from odoo import fields -from odoo.tools import float_compare +from odoo.tools import float_compare, float_is_zero from odoo.addons.base_rest.components.service import to_int from odoo.addons.component.core import Component +from odoo.addons.shopfloor.actions.search import SearchResult from odoo.addons.shopfloor.utils import to_float @@ -48,13 +52,26 @@ class Reception(Component): _usage = "reception" _description = __doc__ - def _check_picking_status(self, pickings): + search_result = SearchResult() + + @contextmanager + def with_search_result(self, search_result: SearchResult): + """ + Use this context manager if you want to include search result in + component behavior. + + """ + self.search_result = search_result + yield + self.search_result = SearchResult() + + def _check_picking_processible(self, pickings): # When returns are allowed, # the created picking might be empty and cannot be assigned. states = ["assigned"] if self.work.menu.allow_return: states.append("draft") - return super()._check_picking_status(pickings, states=states) + return super()._check_picking_processible(pickings, states=states) def _move_line_by_product(self, product): return self.env["stock.move.line"].search( @@ -71,22 +88,44 @@ def _move_line_by_lot(self, lot): def _scheduled_date_today_domain(self): domain = [] - today_start, today_end = self._get_today_start_end_datetime() + today_start, today_end = self._get_today_start_end_datetime_utc() domain.append(("scheduled_date", ">=", today_start)) domain.append(("scheduled_date", "<=", today_end)) return domain - def _get_today_start_end_datetime(self): + def _get_today_start_end_datetime_utc(self): + """ + Returns the start and end of the current day for the warehouse/company + timezone, converted to UTC naive datetimes. + """ + # TODO: Put warehouse tz retrieval in shopfloor module? company = self.env.company - tz = company.partner_id.tz or "UTC" - today = fields.Datetime.today() - today_start = fields.Datetime.start_of(today, "day") - today_end = fields.Datetime.end_of(today, "day") - today_start_localized = ( - pytz.timezone(tz).localize(today_start).astimezone(pytz.utc) + warehouse = self.picking_types.warehouse_id + + tz_name = ( + warehouse.partner_id.tz + if (len(warehouse) == 1 and warehouse.partner_id.tz) + else company.partner_id.tz or "UTC" ) - today_end_localized = pytz.timezone(tz).localize(today_end).astimezone(pytz.utc) - return (today_start_localized, today_end_localized) + tz = pytz.timezone(tz_name) + + now_local = pytz.utc.localize(datetime.now()).astimezone(tz) + + local_start = datetime.combine( + now_local.date(), time.min, tzinfo=now_local.tzinfo + ) + local_end = datetime.combine( + now_local.date(), time.max, tzinfo=now_local.tzinfo + ) + + utc_start = local_start.astimezone(pytz.utc).replace(tzinfo=None) + utc_end = local_end.astimezone(pytz.utc).replace(tzinfo=None) + + return utc_start, utc_end + + @property + def filter_today_scheduled_pickings(self): + return self.work.menu.filter_today_scheduled_pickings # DOMAIN METHODS @@ -193,9 +232,13 @@ def _select_document_from_product(self, product): ) pickings = move_lines.move_id.picking_id if pickings: + message = None + # Don't display error message if just one picking has been found + if len(pickings) > 1: + message = self.msg_store.multiple_picks_found_select_manually() return self._response_for_select_document( pickings=pickings, - message=self.msg_store.multiple_picks_found_select_manually(), + message=message, ) return self._response_for_select_document( pickings=pickings, @@ -253,17 +296,14 @@ def _scan_line__find_or_create_line(self, picking, move, qty_done=1): If none are found create a new line. """ - line = None unassigned_lines = self.env["stock.move.line"] - for move_line in move.move_line_ids: - if move_line.result_package_id: - continue - if move_line.shopfloor_user_id.id == self.env.uid: - line = move_line - break - elif not move_line.shopfloor_user_id: - unassigned_lines |= move_line - if not line and unassigned_lines: + for line in move.move_line_ids: + if line.shopfloor_user_id.id == self.env.uid: + return self._scan_line__recover(picking, line, qty_done) + elif not line.shopfloor_user_id: + unassigned_lines |= line + line = None + if unassigned_lines: lock = self._actions_for("lock") for move_line in unassigned_lines: if lock.for_update(move_line, skip_locked=True): @@ -274,10 +314,28 @@ def _scan_line__find_or_create_line(self, picking, move, qty_done=1): line = self.env["stock.move.line"].create(values) return self._scan_line__assign_user(picking, line, qty_done) + def _scan_line__recover(self, picking, line, default_qty): + product = line.product_id + message = self.msg_store.recovered_previous_session() + # Do not restore further than set_destination, because a destination location + # might be set by default, and we want the user to be allowed to change it. + if line.result_package_id: + # Destination package is set, go to set_destination + return self._response_for_set_destination(picking, line, message=message) + if product.tracking not in ("lot", "serial") or (line.lot_id or line.lot_name): + # If lot already set, go to set_quantity + rounding = line.product_uom_id.rounding + if float_is_zero(line.qty_done, precision_rounding=rounding): + # If no qty_done, set default qty_done + line.qty_done = default_qty + return self._before_state__set_quantity(picking, line, message=message) + # Otherwise go to select_lot + return self._response_for_set_lot(picking, line, message=message) + def _scan_line__assign_user(self, picking, line, qty_done): product = line.product_id - self._assign_user_to_line(line) - line.qty_done += qty_done + stock = self._actions_for("stock") + stock.mark_move_line_as_picked(line, quantity=qty_done, split=False) if product.tracking not in ("lot", "serial") or (line.lot_id or line.lot_name): return self._before_state__set_quantity(picking, line) return self._response_for_set_lot(picking, line) @@ -328,7 +386,7 @@ def _scan_document__by_picking(self, pickings, barcode): message=self.msg_store.cannot_move_something_in_picking_type() ) if reception_pickings: - message = self._check_picking_status(reception_pickings) + message = self._check_picking_processible(reception_pickings) if message: return self._response_for_select_document( pickings=reception_pickings, message=message @@ -337,11 +395,9 @@ def _scan_document__by_picking(self, pickings, barcode): # could return more than one picking. # If there's only one picking due today, we go to the next screen. # Otherwise, we ask the user to scan a package instead. - today_start, today_end = self._get_today_start_end_datetime() + today_start, today_end = self._get_today_start_end_datetime_utc() picking_filter_result_due_today = picking_filter_result.filtered( - lambda p: today_start - <= p.scheduled_date.astimezone(pytz.utc) - < today_end + lambda p: today_start <= p.scheduled_date < today_end ) if len(picking_filter_result_due_today) == 1: return self._select_picking(picking_filter_result_due_today) @@ -412,7 +468,7 @@ def _scan_line__by_product__return(self, picking, product): # If we have an origin picking but no origin move, then user # scanned a wrong product. Warn him about this. if origin_moves and not origin_moves_for_product: - message = self.msg_store.product_not_found_in_current_picking() + message = self.msg_store.product_not_found_in_current_picking(product) return self._response_for_select_move(picking, message=message) if origin_moves_for_product: return_move = self._scan_line__create_return_move( @@ -428,7 +484,13 @@ def _scan_line__by_product__return(self, picking, product): picking.action_assign() return self._scan_line__find_or_create_line(picking, return_move) + def _scan_line__dummy(self): + return + def _scan_line__by_product(self, picking, product): + """ + Try to find a move by product + """ moves = picking.move_ids.filtered(lambda m: m.product_id == product) # Only create a return if don't already have a maching reception move if not moves and self.work.menu.allow_return: @@ -488,6 +550,9 @@ def _scan_line__by_packaging(self, picking, packaging): return self._scan_line__find_or_create_line(picking, move) def _scan_line__by_lot(self, picking, lot): + """ + Try to find a move line by its lot (it should already be assigned) + """ lines = picking.move_line_ids.filtered( lambda l: ( lot == l.lot_id @@ -529,7 +594,11 @@ def _scan_line__fallback(self, picking, barcode): message=message, ) - def _check_move_available(self, move, message_code="product"): + def _check_move_available(self, move, message_code="product") -> bool: + """ + This will check if move is available to be selected by user + scan + """ if not move: message_code = message_code.capitalize() return self.msg_store.x_not_found_or_already_in_dest_package(message_code) @@ -538,6 +607,7 @@ def _check_move_available(self, move, message_code="product"): ) if move.product_uom_qty - move.quantity_done < 1 and not line_without_package: return self.msg_store.move_already_done() + return False def _set_quantity__check_quantity_done(self, selected_line): move = selected_line.move_id @@ -621,6 +691,14 @@ def _set_package_on_move_line(self, picking, line, package): """ pack_location = package.location_id + if not pack_location: + package_line = fields.first( + picking.move_line_ids.filtered( + lambda ml: ml.result_package_id == package + ) + ) + if package_line: + pack_location = package_line.location_dest_id if not pack_location: line.result_package_id = package return None @@ -727,9 +805,12 @@ def _data_for_moves(self, moves, **kw): def _response_for_select_document(self, pickings=None, message=None): if not pickings: - pickings = self.env["stock.picking"].search( - self._domain_stock_picking(today_only=True), - order=self._order_stock_picking(), + # We use the standard shopfloor + move_lines = self.search_move_line.search_move_lines(match_user=True) + pickings = move_lines.picking_id.filtered_domain( + self._domain_stock_picking( + today_only=self.filter_today_scheduled_pickings + ) ) else: # We sort by scheduled date first. However, there might be a case @@ -750,6 +831,7 @@ def _response_for_manual_selection(self): return self._response(next_state="manual_selection", data=data) def _response_for_set_lot(self, picking, line, message=None): + self._set_lot_from_parse(picking, line) return self._response( next_state="set_lot", data={ @@ -759,6 +841,38 @@ def _response_for_set_lot(self, picking, line, message=None): message=message, ) + def _set_lot_from_parse(self, picking, line): + """ + The lot has not been found in move lines before this call. + + Following the picking type configuration, set it: + + - on lot_id if record is found + - on lot_name if record is not found + - set expiration date if found in parse result + """ + if line.shopfloor_should_create_lot and self.search_result.parse_result: + expiration_date = None + lot_name = None + found = False + for result in self.search_result.parse_result: + if result.type == "lot": + if self.search_result.type == "lot" and self.search_result.record: + lot_id = self.search_result.record + lot_name = lot_id.name + found = True + else: + lot_name = result.value + found = True + if ( + result.type == "expiration_date" + and line.product_id.use_expiration_date + ): + expiration_date = result.value + + if found: + return self.set_lot(picking.id, line.id, lot_name, expiration_date) + def _align_display_product_uom_qty(self, line, response): # This method aligns product uom qties on move lines. # In the shopfloor context, we might have multiple users working at @@ -939,26 +1053,31 @@ def scan_line(self, picking_id, barcode): - set_quantity: Packaging / Product has been scanned. Not tracked product """ picking = self.env["stock.picking"].browse(picking_id) - message = self._check_picking_status(picking) + message = self._check_picking_processible(picking) if message: return self._response_for_select_move(picking, message=message) handlers_by_type = { "product": self._scan_line__by_product, "packaging": self._scan_line__by_packaging, "lot": self._scan_line__by_lot, + "expiration_date": self._scan_line__dummy, } search = self._actions_for("search") search_result = search.find(barcode, handlers_by_type.keys()) # Fallback handler, returns a barcode not found error handler = handlers_by_type.get(search_result.type) - if handler: - return handler(picking, search_result.record) - return self._scan_line__fallback(picking, barcode) + + # This could maybe be removed if we pass instead + # the search result through all calls + with self.with_search_result(search_result): + if handler: + return handler(picking, search_result.record) + return self._scan_line__fallback(picking, barcode) def manual_select_move(self, move_id): move = self.env["stock.move"].browse(move_id) picking = move.picking_id - return self._scan_line__find_or_create_line(picking, move) + return self._scan_line__find_or_create_line(picking, move, qty_done=0) def done_action(self, picking_id, confirmation=False): """Mark a picking as done @@ -973,7 +1092,7 @@ def done_action(self, picking_id, confirmation=False): - select_document: Mark as done """ picking = self.env["stock.picking"].browse(picking_id) - message = self._check_picking_status(picking) + message = self._check_picking_processible(picking) if message: return self._response_for_select_move(picking, message=message) if all(line.qty_done == 0 for line in picking.move_line_ids): @@ -1029,7 +1148,7 @@ def set_lot( """ picking = self.env["stock.picking"].browse(picking_id) selected_line = self.env["stock.move.line"].browse(selected_line_id) - message = self._check_picking_status(picking) + message = self._check_picking_processible(picking) if message: return self._response_for_set_lot(picking, selected_line, message=message) if not selected_line.exists(): @@ -1045,7 +1164,7 @@ def set_lot( ) selected_line.lot_id = lot.id selected_line._onchange_lot_id() - elif expiration_date: + if expiration_date: selected_line.write({"expiration_date": expiration_date}) selected_line.lot_id.write({"expiration_date": expiration_date}) return self._response_for_set_lot(picking, selected_line) @@ -1060,15 +1179,27 @@ def _create_lot_values(self, product, lot_name): def set_lot_confirm_action(self, picking_id, selected_line_id): picking = self.env["stock.picking"].browse(picking_id) - message = self._check_picking_status(picking) + message = self._check_picking_processible(picking) selected_line = self.env["stock.move.line"].browse(selected_line_id) if message: return self._response_for_set_lot(picking, selected_line, message=message) - message = self._check_expiry_date(selected_line) - if message: - return self._response_for_set_lot(picking, selected_line, message=message) + checks = [ + self._check_expiry_date, + self._check_lot, + ] + for check in checks: + message = check(selected_line) + if message: + return self._response_for_set_lot( + picking, selected_line, message=message + ) return self._before_state__set_quantity(picking, selected_line) + def _check_lot(self, line): + need_lot = line.product_id.tracking == "lot" + if need_lot and not line.lot_id: + return self.msg_store.scan_lot_on_product_tracked_by_lot() + def _check_expiry_date(self, line): use_expiration_date = ( line.product_id.use_expiration_date or line.lot_id.use_expiration_date @@ -1090,7 +1221,11 @@ def _set_quantity__by_barcode( ): handlers_by_type = self._set_quantity__get_handlers_by_type() search = self._actions_for("search") - search_result = search.find(barcode, handlers_by_type.keys()) + search_result = search.find( + barcode, + handlers_by_type.keys(), + handler_kw=dict(lot=dict(products=selected_line.product_id)), + ) handler = handlers_by_type.get(search_result.type) if handler: return handler(picking, selected_line, search_result.record) @@ -1145,7 +1280,7 @@ def set_quantity( """ picking = self.env["stock.picking"].browse(picking_id) selected_line = self.env["stock.move.line"].browse(selected_line_id) - message = self._check_picking_status(picking) + message = self._check_picking_processible(picking) if message: return self._response_for_set_quantity( picking, selected_line, message=message @@ -1174,7 +1309,7 @@ def set_quantity( def set_quantity__cancel_action(self, picking_id, selected_line_id): picking = self.env["stock.picking"].browse(picking_id) selected_line = self.env["stock.move.line"].browse(selected_line_id) - message = self._check_picking_status(picking) + message = self._check_picking_processible(picking) if message: return self._response_for_set_quantity( picking, selected_line, message=message @@ -1212,7 +1347,7 @@ def _set_quantity__process__set_qty_and_split(self, picking, line, quantity): def process_with_existing_pack(self, picking_id, selected_line_id, quantity): picking = self.env["stock.picking"].browse(picking_id) selected_line = self.env["stock.move.line"].browse(selected_line_id) - message = self._check_picking_status(picking) + message = self._check_picking_processible(picking) if message: return self._response_for_set_quantity( picking, selected_line, message=message @@ -1227,7 +1362,7 @@ def process_with_existing_pack(self, picking_id, selected_line_id, quantity): def process_with_new_pack(self, picking_id, selected_line_id, quantity): picking = self.env["stock.picking"].browse(picking_id) selected_line = self.env["stock.move.line"].browse(selected_line_id) - message = self._check_picking_status(picking) + message = self._check_picking_processible(picking) if message: return self._response_for_set_quantity( picking, selected_line, message=message @@ -1243,7 +1378,7 @@ def process_with_new_pack(self, picking_id, selected_line_id, quantity): def process_without_pack(self, picking_id, selected_line_id, quantity): picking = self.env["stock.picking"].browse(picking_id) selected_line = self.env["stock.move.line"].browse(selected_line_id) - message = self._check_picking_status(picking) + message = self._check_picking_processible(picking) if message: return self._response_for_set_quantity( picking, selected_line, message=message @@ -1283,17 +1418,11 @@ def _auto_post_line(self, selected_line): # In such case, we must ensure there's another move with the remaining # quantity to do, so selected_line is extracted in a new move as expected. - # Always keep the quantity todo at zero, the same is done - # in Odoo when move lines are created manually (setting) lines_with_qty_todo = selected_line.move_id.move_line_ids.filtered( lambda line: line.state not in ("cancel", "done") and line.reserved_uom_qty > 0 ) move = selected_line.move_id - lock = self._actions_for("lock") - lock.for_update(move) - if lines_with_qty_todo: - lines_with_qty_todo.reserved_uom_qty = 0 move_quantity = move.product_uom._compute_quantity( move.product_uom_qty, selected_line.product_uom_id @@ -1301,6 +1430,14 @@ def _auto_post_line(self, selected_line): if selected_line.qty_done == move_quantity: # In case of full quantity, post the initial move return selected_line.move_id.extract_and_action_done() + + # Always keep the quantity todo at zero, the same is done + # in Odoo when move lines are created manually (setting) + lock = self._actions_for("lock") + lock.for_update(move) + if lines_with_qty_todo: + lines_with_qty_todo.reserved_uom_qty = 0 + split_move_vals = move._split(selected_line.qty_done) new_move = move.create(split_move_vals) new_move.move_line_ids = selected_line @@ -1337,7 +1474,7 @@ def set_destination( """ picking = self.env["stock.picking"].browse(picking_id) selected_line = self.env["stock.move.line"].browse(selected_line_id) - message = self._check_picking_status(picking) + message = self._check_picking_processible(picking) if message: return self._response_for_set_destination( picking, selected_line, message=message @@ -1381,6 +1518,12 @@ def set_destination( response = self._post_line(selected_line) if response: return response + # After line is posted, if picking is done, + # return select document with success message + if picking.state == "done": + message = self.msg_store.transfer_done_success(picking) + return self._response_for_select_document(message=message) + # Else return select move return self._response_for_select_move(picking) def select_dest_package( @@ -1399,7 +1542,7 @@ def select_dest_package( """ picking = self.env["stock.picking"].browse(picking_id) selected_line = self.env["stock.move.line"].browse(selected_line_id) - message = self._check_picking_status(picking) + message = self._check_picking_processible(picking) if message: return self._response_for_select_dest_package( picking, selected_line, message=message @@ -1617,7 +1760,7 @@ def _list_stock_pickings_next_states(self): } def _scan_line_next_states(self): - return {"select_move", "set_lot", "set_quantity"} + return {"select_move", "set_lot", "set_quantity", "set_destination"} def _set_lot_next_states(self): return {"select_move", "set_lot", "set_quantity"} diff --git a/shopfloor_reception/static/description/index.html b/shopfloor_reception/static/description/index.html index 578483fd8d6..6a9251b2515 100644 --- a/shopfloor_reception/static/description/index.html +++ b/shopfloor_reception/static/description/index.html @@ -3,7 +3,7 @@ -Shopfloor Reception +README.rst -
-

Shopfloor Reception

+
+ + +Odoo Community Association + +
+

Shopfloor Reception

-

Beta License: AGPL-3 OCA/wms Translate me on Weblate Try me on Runboat

+

Beta License: AGPL-3 OCA/wms Translate me on Weblate Try me on Runboat

Shopfloor implementation of the reception scenario. Allows to receive products and create the proper packs for each logistic unit.

Table of contents

@@ -386,11 +391,11 @@

Shopfloor Reception

-

Known issues / Roadmap

+

Known issues / Roadmap

Implement methods in the backend to cancel lines (to be used by the frontend in select_line & set_quantity).

-

Bug Tracker

+

Bug Tracker

Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed @@ -398,15 +403,15 @@

Bug Tracker

Do not contact contributors directly about support or help with technical issues.

-

Credits

+

Credits

-

Authors

+

Authors

  • Camptocamp
-

Contributors

+

Contributors

-

Maintainers

+

Maintainers

This module is maintained by the OCA.

Odoo Community Association @@ -431,5 +436,6 @@

Maintainers

+
diff --git a/shopfloor_reception/tests/__init__.py b/shopfloor_reception/tests/__init__.py index a5861ce2928..7e192132e7e 100644 --- a/shopfloor_reception/tests/__init__.py +++ b/shopfloor_reception/tests/__init__.py @@ -13,3 +13,4 @@ from . import test_return_scan_line from . import test_return_set_quantity from . import test_return_reception_done +from . import test_recover diff --git a/shopfloor_reception/tests/common.py b/shopfloor_reception/tests/common.py index 90bd62e323c..38733a76893 100644 --- a/shopfloor_reception/tests/common.py +++ b/shopfloor_reception/tests/common.py @@ -1,7 +1,6 @@ # Copyright 2020 Camptocamp SA (http://www.camptocamp.com) # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). # pylint: disable=missing-return - from odoo import fields from odoo.addons.shopfloor.tests.common import CommonCase as BaseCommonCase @@ -46,6 +45,7 @@ def _add_package(cls, picking): def setUpClassVars(cls, *args, **kwargs): super().setUpClassVars(*args, **kwargs) cls.menu = cls.env.ref("shopfloor_reception.shopfloor_menu_demo_reception") + cls.menu.sudo().filter_today_scheduled_pickings = True cls.profile = cls.env.ref("shopfloor.profile_demo_1") cls.picking_type = cls.menu.picking_type_ids cls.wh = cls.picking_type.warehouse_id @@ -144,6 +144,9 @@ def assertMessage(self, response, expected_message): for key, value in expected_message.items(): self.assertEqual(message.get(key), value) + def assertNotMessage(self, response, expected_message): + self.assertMessage(response, expected_message) + @classmethod def _get_move_ids_from_response(cls, response): state = response.get("next_state") diff --git a/shopfloor_reception/tests/test_multi_barcode.py b/shopfloor_reception/tests/test_multi_barcode.py new file mode 100644 index 00000000000..82c9d40023a --- /dev/null +++ b/shopfloor_reception/tests/test_multi_barcode.py @@ -0,0 +1,59 @@ +# Copyright 2025 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +import mock + +from odoo import fields + +from odoo.addons.shopfloor.actions.barcode_parser import BarcodeParser, BarcodeResult + +from .common import CommonCase + + +class TestStructuredBarcode(CommonCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.product_a.tracking = "lot" + cls.product_a.use_expiration_date = True + cls.picking_type.sudo().use_create_lots = True + + def test_scan_multiple_attribute_barcode(self): + """ + Check that scanning a product with multi attribute barcode + will fill in the lot + """ + picking = self._create_picking() + lot = self._create_lot() + selected_move_line = picking.move_line_ids.filtered( + lambda l: l.product_id == self.product_a + ) + # selected_move_line.lot_id = lot + with mock.patch.object(BarcodeParser, "parse") as mock_parse: + mock_parse.return_value = [ + BarcodeResult(type="lot", value=lot.name, raw=lot.name), + BarcodeResult( + type="expiration_date", + value=fields.Date.to_date("2025-04-15"), + raw="250415", + ), + ] + response = self.service.dispatch( + "scan_line", + params={ + "picking_id": picking.id, + "barcode": lot.name, + }, + ) + data = self.data.picking(picking) + self.assert_response( + response, + next_state="set_lot", + data={ + "picking": data, + "selected_move_line": self.data.move_lines(selected_move_line), + }, + ) + self.assertEqual( + selected_move_line.expiration_date, + fields.Datetime.to_datetime("2025-04-15"), + ) diff --git a/shopfloor_reception/tests/test_recover.py b/shopfloor_reception/tests/test_recover.py new file mode 100644 index 00000000000..622366cc096 --- /dev/null +++ b/shopfloor_reception/tests/test_recover.py @@ -0,0 +1,176 @@ +# Copyright 2025 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +# Recover happens at line selection. +# If a line exists for current user + +from .common import CommonCase + + +class TestRecover(CommonCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.recover_msg = { + "message_type": "info", + "body": "Recovered previous session.", + } + + def test_recover(self): + # here, product isn't tracked by lot, but the move has a move + # line already assigned to the user. + # No quantity done, we should be redirected to set_quantity, + # with the default qty set + picking = self._create_picking() + # First time we select the line, no recover + response = self.service.dispatch( + "scan_line", + params={"picking_id": picking.id, "barcode": self.product_a.default_code}, + ) + selected_move_line = picking.move_line_ids.filtered( + lambda li: li.product_id == self.product_a + ) + self.assertEqual(selected_move_line.qty_done, 1) + self.assertEqual(selected_move_line.shopfloor_user_id.id, self.env.uid) + picking_data = self.data.picking(picking) + move_line_data = self.data.move_lines(selected_move_line) + self.assert_response( + response, + next_state="set_quantity", + data={ + "picking": picking_data, + "selected_move_line": move_line_data, + "confirmation_required": None, + }, + ) + # Now that there's a shopfloor_user_id, we should recover the session + # but since didn't change anything, nothing should change. + # Most importantly, the existing move_line should be reused throughout + # the whole process + response = self.service.dispatch( + "scan_line", + params={"picking_id": picking.id, "barcode": self.product_a.default_code}, + ) + # qty done is the same + self.assertEqual(selected_move_line.qty_done, 1) + self.assert_response( + response, + next_state="set_quantity", + data={ + "picking": picking_data, + "selected_move_line": move_line_data, + "confirmation_required": None, + }, + message=self.recover_msg, + ) + # Set qty_done to 5/10 on the move line, we should recover it + selected_move_line.qty_done = 5 + response = self.service.dispatch( + "scan_line", + params={"picking_id": picking.id, "barcode": self.product_a.default_code}, + ) + self.assertEqual(selected_move_line.qty_done, 5) + move_line_data = self.data.move_lines(selected_move_line) + self.assert_response( + response, + next_state="set_quantity", + data={ + "picking": picking_data, + "selected_move_line": move_line_data, + "confirmation_required": None, + }, + message=self.recover_msg, + ) + # If the goods were put in a pack, we move to set destination + response = self.service.dispatch( + "process_with_new_pack", + params={ + "picking_id": picking.id, + "selected_line_id": selected_move_line.id, + "quantity": selected_move_line.qty_done, + }, + ) + package = selected_move_line.result_package_id + self.assertTrue(package) + self.assertEqual(selected_move_line.qty_done, 5) + self.assertEqual(selected_move_line.reserved_uom_qty, 5) + picking_data = self.data.picking(picking) + move_line_data = self.data.move_lines(selected_move_line) + self.assert_response( + response, + next_state="set_destination", + data={ + "picking": picking_data, + "selected_move_line": move_line_data, + }, + ) + # Scan the line again, we should end up with the exact same result + # with the additionnal recover message + response = self.service.dispatch( + "scan_line", + params={"picking_id": picking.id, "barcode": self.product_a.default_code}, + ) + self.assert_response( + response, + next_state="set_destination", + data={ + "picking": picking_data, + "selected_move_line": move_line_data, + }, + message=self.recover_msg, + ) + + def test_recover_tracking_by_lot(self): + # exact same test, just showing that we skip the set lot when recovering + picking = self._create_picking() + self.product_a.tracking = "lot" + # First time we select the line, no recover + response = self.service.dispatch( + "scan_line", + params={"picking_id": picking.id, "barcode": self.product_a.default_code}, + ) + selected_move_line = picking.move_line_ids.filtered( + lambda li: li.product_id == self.product_a + ) + picking_data = self.data.picking(picking) + move_line_data = self.data.move_lines(selected_move_line) + self.assert_response( + response, + next_state="set_lot", + data={ + "picking": picking_data, + "selected_move_line": move_line_data, + }, + ) + # Scan the same line, we end up on the same screen, but with a recover msg + response = self.service.dispatch( + "scan_line", + params={"picking_id": picking.id, "barcode": self.product_a.default_code}, + ) + self.assert_response( + response, + next_state="set_lot", + data={ + "picking": picking_data, + "selected_move_line": move_line_data, + }, + message=self.recover_msg, + ) + # Set a lot to the move line, we recover again, but straight to set quantity. + selected_move_line.lot_id = self._create_lot() + response = self.service.dispatch( + "scan_line", + params={"picking_id": picking.id, "barcode": self.product_a.default_code}, + ) + move_line_data = self.data.move_lines(selected_move_line) + self.assert_response( + response, + next_state="set_quantity", + data={ + "picking": picking_data, + "selected_move_line": move_line_data, + "confirmation_required": None, + }, + message=self.recover_msg, + ) + # The rest is all the same as test_recover diff --git a/shopfloor_reception/tests/test_return_scan_line.py b/shopfloor_reception/tests/test_return_scan_line.py index 9575ebdbf1d..76aa9c538f6 100644 --- a/shopfloor_reception/tests/test_return_scan_line.py +++ b/shopfloor_reception/tests/test_return_scan_line.py @@ -22,7 +22,7 @@ def test_scan_product_not_in_delivery(self): data={"picking": self._data_for_picking_with_moves(return_picking)}, message={ "message_type": "error", - "body": "Product is not in the current transfer.", + "body": f"Product {wrong_product.name} is not in the current transfer.", }, ) diff --git a/shopfloor_reception/tests/test_select_document.py b/shopfloor_reception/tests/test_select_document.py index 8ab5386a0bb..d9a2aadb669 100644 --- a/shopfloor_reception/tests/test_select_document.py +++ b/shopfloor_reception/tests/test_select_document.py @@ -1,6 +1,8 @@ # Copyright 2022 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) # pylint: disable=missing-return +from datetime import datetime, timedelta + from freezegun import freeze_time from odoo import fields @@ -81,6 +83,33 @@ def test_scan_picking_origin_multiple_pickings_one_today(self): data=self._data_for_select_move(picking_today), ) + # Since we use "Europe/Brussels" tz and _TODAY is in winter + # the shift is UTC+1 + @freeze_time(_TODAY, tz_offset=-1) + def test_scan_picking_origin_multiple_pickings_one_today_tz(self): + # freezed today is UTC time, set warehouse user to Brussels + tz_name = "Europe/Brussels" + self.wh.partner_id.sudo().tz = tz_name + + # Create a picking with the UTC hour + picking_today = self._create_picking( + scheduled_date=datetime.now() + timedelta(hours=23, minutes=30) + ) + picking_tomorrow = self._create_picking( + scheduled_date=datetime.now() + timedelta(days=1), + ) + pickings = picking_today | picking_tomorrow + pickings = pickings.sorted(lambda p: (p.scheduled_date, p.id), reverse=False) + pickings.write({"origin": "Somewhere together"}) + response = self.service.dispatch( + "scan_document", params={"barcode": "Somewhere together"} + ) + self.assert_response( + response, + next_state="select_move", + data=self._data_for_select_move(picking_today), + ) + def test_scan_picking_origin_one_picking(self): # Only 1 picking with this origin is found. # Move to select_move. @@ -127,6 +156,19 @@ def test_scan_product_multiple_pickings(self): message={"message_type": "error", "body": body}, ) + def test_scan_product_one_picking(self): + # next step is select_document, with document filtered based on the product + p1 = self._create_picking() + response = self.service.dispatch( + "scan_document", params={"barcode": self.product_a.barcode} + ) + self.assert_response( + response, + next_state="select_document", + data={"pickings": self._data_for_pickings(p1)}, + message=None, + ) + def test_scan_product_no_picking(self): # next_step is select_document, with an error message picking = self._create_picking() diff --git a/shopfloor_reception/tests/test_select_move.py b/shopfloor_reception/tests/test_select_move.py index 5e9b9cf1534..a184443e0a4 100644 --- a/shopfloor_reception/tests/test_select_move.py +++ b/shopfloor_reception/tests/test_select_move.py @@ -26,6 +26,7 @@ def test_scan_barcode_not_found(self): def test_scan_product(self): picking = self._create_picking() + self.assertFalse(picking.printed) response = self.service.dispatch( "scan_line", params={"picking_id": picking.id, "barcode": self.product_a.barcode}, @@ -34,6 +35,7 @@ def test_scan_product(self): selected_move_line = picking.move_line_ids.filtered( lambda l: l.product_id == self.product_a ) + self.assertTrue(selected_move_line.picking_id.printed) self.assert_response( response, next_state="set_lot", diff --git a/shopfloor_reception/tests/test_set_destination.py b/shopfloor_reception/tests/test_set_destination.py index 2c730360ce1..d79a44681f3 100644 --- a/shopfloor_reception/tests/test_set_destination.py +++ b/shopfloor_reception/tests/test_set_destination.py @@ -107,9 +107,10 @@ def test_scan_location_not_child_of_dest_locations(self): message={"message_type": "error", "body": "You cannot place it here"}, ) - def test_auto_posting(self): + def test_auto_posting_partial(self): self.menu.sudo().auto_post_line = True - picking = self._create_picking() + # Creating a picking with a single move, with qty todo = 10 + picking = self._create_picking(lines=[(self.product_a, 10)]) selected_move_line = picking.move_line_ids.filtered( lambda l: l.product_id == self.product_a ) @@ -129,7 +130,7 @@ def test_auto_posting(self): # and dest package & dest location are set, # a line with 3 demand will be automatically extracted # in a new picking, which will be marked as done. - self.service.dispatch( + response = self.service.dispatch( "set_destination", params={ "picking_id": picking.id, @@ -137,6 +138,10 @@ def test_auto_posting(self): "location_name": self.dispatch_location.name, }, ) + # Next screen is select move, because picking is not done + self.assert_response( + response, next_state="select_move", data=self._data_for_select_move(picking) + ) # The line has been moved to a different picking. self.assertNotEqual(picking, selected_move_line.picking_id) # Its qty_done is 3. @@ -152,6 +157,74 @@ def test_auto_posting(self): self.assertEqual(line_in_picking.qty_done, 0) self.assertEqual(picking.state, "assigned") + def test_auto_posting_full_one_line(self): + self.menu.sudo().auto_post_line = True + # Create a picking with a single move with qty todo = 10 + picking = self._create_picking(lines=[(self.product_a, 10)]) + selected_move_line = picking.move_line_ids.filtered( + lambda li: li.product_id == self.product_a + ) + # User has previously scanned the full qty + # A new pack has been created and assigned to the line. + self.service.dispatch( + "process_with_new_pack", + params={ + "picking_id": picking.id, + "selected_line_id": selected_move_line.id, + "quantity": 10, + }, + ) + # Full qty processed, picking is done, and next screen is select document. + response = self.service.dispatch( + "set_destination", + params={ + "picking_id": picking.id, + "selected_line_id": selected_move_line.id, + "location_name": self.dispatch_location.name, + }, + ) + message = self.msg_store.transfer_done_success(picking) + self.assert_response( + response, next_state="select_document", data=self.ANY, message=message + ) + + def test_auto_posting_full_two_lines(self): + self.menu.sudo().auto_post_line = True + # Create a picking with a two moves with qty todo = 10 + picking = self._create_picking() + selected_move_line = picking.move_line_ids.filtered( + lambda li: li.product_id == self.product_a + ) + # User has previously scanned the full qty + # A new pack has been created and assigned to the line. + self.service.dispatch( + "process_with_new_pack", + params={ + "picking_id": picking.id, + "selected_line_id": selected_move_line.id, + "quantity": 10, + }, + ) + # Full qty processed, but one more line to process, + # next screen is select_move + response = self.service.dispatch( + "set_destination", + params={ + "picking_id": picking.id, + "selected_line_id": selected_move_line.id, + "location_name": self.dispatch_location.name, + }, + ) + self.assert_response( + response, next_state="select_move", data=self._data_for_select_move(picking) + ) + # Lines has been moved in another picking + self.assertNotEqual(picking, selected_move_line.picking_id) + # Fully processed + self.assertEqual(selected_move_line.picking_id.state, "done") + # One move remaining in the picking, for product b, still to be processed + self.assertEqual(picking.move_ids.product_id, self.product_b) + def test_auto_posting_concurent_work(self): """Check 2 users working on the same move. @@ -177,13 +250,12 @@ def test_auto_posting_concurent_work(self): # User 1 finishes his work move_line_data = res_u1["data"]["set_quantity"]["selected_move_line"][0] line_id_u1 = move_line_data["id"] - qty_done_u1 = move_line_data["qty_done"] res_u1 = service_u1.dispatch( "process_without_pack", params={ "picking_id": picking.id, "selected_line_id": line_id_u1, - "quantity": qty_done_u1, + "quantity": 1, }, ) res_u1 = service_u1.dispatch( diff --git a/shopfloor_reception/tests/test_set_lot_confirm.py b/shopfloor_reception/tests/test_set_lot_confirm.py index e8d4f6efe53..238f4546f66 100644 --- a/shopfloor_reception/tests/test_set_lot_confirm.py +++ b/shopfloor_reception/tests/test_set_lot_confirm.py @@ -8,7 +8,30 @@ class TestSetLotConfirm(CommonCase): @classmethod def setUpClassBaseData(cls): super().setUpClassBaseData() - cls.product_a.tracking = "lot" + + def test_ensure_lot(self): + picking = self._create_picking() + self.product_a.tracking = "lot" + selected_move_line = picking.move_line_ids.filtered( + lambda li: li.product_id == self.product_a + ) + response = self.service.dispatch( + "set_lot_confirm_action", + params={ + "picking_id": picking.id, + "selected_line_id": selected_move_line.id, + }, + ) + message = self.msg_store.scan_lot_on_product_tracked_by_lot() + self.assert_response( + response, + next_state="set_lot", + data={ + "picking": self.data.picking(picking), + "selected_move_line": self.data.move_lines(selected_move_line), + }, + message=message, + ) def test_ensure_expiry_date(self): picking = self._create_picking() diff --git a/shopfloor_reception/tests/test_set_quantity.py b/shopfloor_reception/tests/test_set_quantity.py index 8cd13962002..9b4da52973e 100644 --- a/shopfloor_reception/tests/test_set_quantity.py +++ b/shopfloor_reception/tests/test_set_quantity.py @@ -53,6 +53,47 @@ def test_set_quantity_scan_product(self): }, ) + def test_set_quantity_scan_wrong_lot(self): + # create lot "4" for product b + self.env["stock.lot"].create( + { + "name": "4", + "product_id": self.product_b.id, + } + ) + self.product_a.sudo().tracking = "lot" + + picking = self._create_picking() + selected_move_line = picking.move_line_ids.filtered( + lambda l: l.product_id == self.product_a + ) + selected_move_line.shopfloor_user_id = self.env.uid + response = self.service.dispatch( + "set_quantity", + params={ + "picking_id": picking.id, + "selected_line_id": selected_move_line.id, + "quantity": 10.0, + "barcode": "4", + }, + ) + self.assertEqual(selected_move_line.qty_done, 10.0) + data = self.data.picking(picking) + message = { + "message_type": "warning", + "body": "Create new PACK 4? Scan it again to confirm.", + } + self.assert_response( + response, + next_state="set_quantity", + data={ + "picking": data, + "selected_move_line": self.data.move_lines(selected_move_line), + "confirmation_required": "4", + }, + message=message, + ) + def test_set_quantity_scan_packaging(self): picking = self._create_picking() selected_move_line = picking.move_line_ids.filtered( diff --git a/shopfloor_reception/views/shopfloor_menu.xml b/shopfloor_reception/views/shopfloor_menu.xml new file mode 100644 index 00000000000..10f73482c0b --- /dev/null +++ b/shopfloor_reception/views/shopfloor_menu.xml @@ -0,0 +1,24 @@ + + + + + shopfloor.menu + + + + + + + + + + + + diff --git a/shopfloor_reception_mobile/README.rst b/shopfloor_reception_mobile/README.rst index 91ff4b9b8f2..08644d8a2b7 100644 --- a/shopfloor_reception_mobile/README.rst +++ b/shopfloor_reception_mobile/README.rst @@ -1,3 +1,7 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + ========================== Shopfloor reception mobile ========================== @@ -7,13 +11,13 @@ Shopfloor reception mobile !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:53481754aeb6f45f2bc05fa4eaf8aaf55552d1ada7b26c1e7f065d60faa2adbb + !! source digest: sha256:e6e5bdb9f20c104e11b2d9c6c67bdf6b418ca342dd36317955c679a68d278ca4 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png :target: https://odoo-community.org/page/development-status :alt: Beta -.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png +.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fwms-lightgray.png?logo=github diff --git a/shopfloor_reception_mobile/__manifest__.py b/shopfloor_reception_mobile/__manifest__.py index a4f9bda4503..88acd316c1d 100644 --- a/shopfloor_reception_mobile/__manifest__.py +++ b/shopfloor_reception_mobile/__manifest__.py @@ -3,9 +3,9 @@ { "name": "Shopfloor reception mobile", "summary": "Scenario for receiving products", - "version": "16.0.1.0.0", + "version": "16.0.1.1.2", "development_status": "Beta", - "depends": ["shopfloor_mobile_base", "shopfloor_reception"], + "depends": ["shopfloor_mobile_base", "shopfloor_mobile", "shopfloor_reception"], "author": "Camptocamp, Odoo Community Association (OCA)", "maintainers": ["JuMiSanAr"], "website": "https://github.com/OCA/wms", diff --git a/shopfloor_reception_mobile/static/description/index.html b/shopfloor_reception_mobile/static/description/index.html index 5839f8cd1c5..97f13521bbf 100644 --- a/shopfloor_reception_mobile/static/description/index.html +++ b/shopfloor_reception_mobile/static/description/index.html @@ -3,7 +3,7 @@ -Shopfloor reception mobile +README.rst -
-

Shopfloor reception mobile

+
+ + +Odoo Community Association + +
+

Shopfloor reception mobile

-

Beta License: AGPL-3 OCA/wms Translate me on Weblate Try me on Runboat

+

Beta License: AGPL-3 OCA/wms Translate me on Weblate Try me on Runboat

Frontend for the reception scenario in shopfloor. Allows to receive products and create the proper packs for each logistic unit.

Table of contents

@@ -385,7 +390,7 @@

Shopfloor reception mobile

-

Bug Tracker

+

Bug Tracker

Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed @@ -393,15 +398,15 @@

Bug Tracker

Do not contact contributors directly about support or help with technical issues.

-

Credits

+

Credits

-

Authors

+

Authors

  • Camptocamp
-

Contributors

+

Contributors

-

Maintainers

+

Maintainers

This module is maintained by the OCA.

Odoo Community Association @@ -424,5 +429,6 @@

Maintainers

+
diff --git a/shopfloor_reception_mobile/static/src/scenario/reception.js b/shopfloor_reception_mobile/static/src/scenario/reception.js index f921e21a358..ce2117091a5 100644 --- a/shopfloor_reception_mobile/static/src/scenario/reception.js +++ b/shopfloor_reception_mobile/static/src/scenario/reception.js @@ -302,6 +302,13 @@ const Reception = { picking_detail_options_for_set_lot: function () { return { key_title: "product.display_name", + title_action_field: { + action_val_path: function (record, field) { + return record.product.barcode + ? "product.barcode" + : "product.default_code"; + }, + }, fields: [ { path: "product.supplier_code", @@ -346,14 +353,18 @@ const Reception = { ], }; }, - picking_detail_options_for_select_move: function (move) { + picking_detail_options_for_select_move: function () { return { show_title: true, showActions: false, list_item_options: { loud_title: true, title_action_field: { - action_val_path: "name", + action_val_path: function (record, field) { + return record.product.barcode + ? "product.barcode" + : "product.default_code"; + }, }, list_item_klass_maker: this.move_card_color, key_title: "product.display_name", @@ -418,7 +429,7 @@ const Reception = { return values; }, select_dest_package_display_name: function (rec) { - var values = this.select_dest_package_display_name_values(); + var values = this.select_dest_package_display_name_values(rec); return values.join(" - "); }, picking_detail_options_for_select_dest_package: function () { diff --git a/shopfloor_reception_refund_return/README.rst b/shopfloor_reception_refund_return/README.rst new file mode 100644 index 00000000000..9e827232860 --- /dev/null +++ b/shopfloor_reception_refund_return/README.rst @@ -0,0 +1,93 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +================================= +Shopfloor Reception Refund Return +================================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:9042334ed3023cfdbad3925fdda86bfbc54efd128bba3ce83a1ca78b37eb14df + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fwms-lightgray.png?logo=github + :target: https://github.com/OCA/wms/tree/16.0/shopfloor_reception_refund_return + :alt: OCA/wms +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/wms-16-0/wms-16-0-shopfloor_reception_refund_return + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/wms&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Marks returns created by shopfloor as to refund. + +Sets `to_refund` to true when when creating a return move. + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Camptocamp +* BCIM + +Contributors +~~~~~~~~~~~~ + +* Matthieu Méquignon +* Jacques-Etienne Baudoux (BCIM) +* Michael Tietz (MT Software) + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-mmequignon| image:: https://github.com/mmequignon.png?size=40px + :target: https://github.com/mmequignon + :alt: mmequignon + +Current `maintainer `__: + +|maintainer-mmequignon| + +This module is part of the `OCA/wms `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/shopfloor_reception_refund_return/__init__.py b/shopfloor_reception_refund_return/__init__.py new file mode 100644 index 00000000000..f5fe63aaf72 --- /dev/null +++ b/shopfloor_reception_refund_return/__init__.py @@ -0,0 +1 @@ +from . import actions diff --git a/shopfloor_reception_refund_return/__manifest__.py b/shopfloor_reception_refund_return/__manifest__.py new file mode 100644 index 00000000000..1322b1b5495 --- /dev/null +++ b/shopfloor_reception_refund_return/__manifest__.py @@ -0,0 +1,16 @@ +# Copyright 2023 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +{ + "name": "Shopfloor Reception Refund Return", + "summary": "Mark created return as to refund", + "version": "16.0.1.0.0", + "category": "Inventory, Accounting", + "website": "https://github.com/OCA/wms", + "author": "Camptocamp, BCIM, Odoo Community Association (OCA)", + "maintainers": ["mmequignon"], + "license": "AGPL-3", + "installable": True, + "auto_install": False, + "depends": ["shopfloor", "stock_account"], +} diff --git a/shopfloor_reception_refund_return/actions/__init__.py b/shopfloor_reception_refund_return/actions/__init__.py new file mode 100644 index 00000000000..12bab770a38 --- /dev/null +++ b/shopfloor_reception_refund_return/actions/__init__.py @@ -0,0 +1 @@ +from . import stock diff --git a/shopfloor_reception_refund_return/actions/stock.py b/shopfloor_reception_refund_return/actions/stock.py new file mode 100644 index 00000000000..ca70e8e42a7 --- /dev/null +++ b/shopfloor_reception_refund_return/actions/stock.py @@ -0,0 +1,14 @@ +# Copyright 2023 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo.addons.component.core import Component + + +class StockAction(Component): + _inherit = "shopfloor.stock.action" + + def _create_return_move__get_vals(self, return_picking, origin_move): + res = super()._create_return_move__get_vals(return_picking, origin_move) + if return_picking.picking_type_code == "incoming": + res["to_refund"] = True + return res diff --git a/shopfloor_reception_refund_return/i18n/it.po b/shopfloor_reception_refund_return/i18n/it.po new file mode 100644 index 00000000000..73388557f6d --- /dev/null +++ b/shopfloor_reception_refund_return/i18n/it.po @@ -0,0 +1,14 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" diff --git a/shopfloor_reception_refund_return/i18n/shopfloor_reception_refund_return.pot b/shopfloor_reception_refund_return/i18n/shopfloor_reception_refund_return.pot new file mode 100644 index 00000000000..78d58d53fe0 --- /dev/null +++ b/shopfloor_reception_refund_return/i18n/shopfloor_reception_refund_return.pot @@ -0,0 +1,13 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" diff --git a/shopfloor_reception_refund_return/readme/CONTRIBUTORS.rst b/shopfloor_reception_refund_return/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000000..0c8ba3d75d4 --- /dev/null +++ b/shopfloor_reception_refund_return/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +* Matthieu Méquignon +* Jacques-Etienne Baudoux (BCIM) +* Michael Tietz (MT Software) diff --git a/shopfloor_reception_refund_return/readme/DESCRIPTION.rst b/shopfloor_reception_refund_return/readme/DESCRIPTION.rst new file mode 100644 index 00000000000..48e692b9861 --- /dev/null +++ b/shopfloor_reception_refund_return/readme/DESCRIPTION.rst @@ -0,0 +1,3 @@ +Marks returns created by shopfloor as to refund. + +Sets `to_refund` to true when when creating a return move. diff --git a/shopfloor_reception_refund_return/static/description/icon.png b/shopfloor_reception_refund_return/static/description/icon.png new file mode 100644 index 00000000000..1dcc49c24f3 Binary files /dev/null and b/shopfloor_reception_refund_return/static/description/icon.png differ diff --git a/shopfloor_reception_refund_return/static/description/index.html b/shopfloor_reception_refund_return/static/description/index.html new file mode 100644 index 00000000000..23a315d3812 --- /dev/null +++ b/shopfloor_reception_refund_return/static/description/index.html @@ -0,0 +1,435 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

Shopfloor Reception Refund Return

+ +

Beta License: AGPL-3 OCA/wms Translate me on Weblate Try me on Runboat

+

Marks returns created by shopfloor as to refund.

+

Sets to_refund to true when when creating a return move.

+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Camptocamp
  • +
  • BCIM
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainer:

+

mmequignon

+

This module is part of the OCA/wms project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+
+ + diff --git a/shopfloor_reception_refund_return/tests/__init__.py b/shopfloor_reception_refund_return/tests/__init__.py new file mode 100644 index 00000000000..c175a20bf4e --- /dev/null +++ b/shopfloor_reception_refund_return/tests/__init__.py @@ -0,0 +1 @@ +from . import test_action_stock diff --git a/shopfloor_reception_refund_return/tests/test_action_stock.py b/shopfloor_reception_refund_return/tests/test_action_stock.py new file mode 100644 index 00000000000..68a6b525136 --- /dev/null +++ b/shopfloor_reception_refund_return/tests/test_action_stock.py @@ -0,0 +1,54 @@ +# Copyright 2023 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) +# pylint: disable=missing-return +from odoo.addons.shopfloor.tests.common import CommonCase + + +class TestActionsStock(CommonCase): + @classmethod + def setUpClassVars(cls): + super().setUpClassVars() + cls.wh = cls.env.ref("stock.warehouse0") + cls.location_customers = cls.env.ref("stock.stock_location_customers") + cls.picking_type = cls.wh.out_type_id + cls.picking_type.sudo().write( + {"default_location_dest_id": cls.location_customers.id} + ) + cls.picking_type_in = cls.picking_type.return_picking_type_id + cls.picking_type_in.sudo().write( + {"default_location_src_id": cls.location_customers.id} + ) + + @classmethod + def setUpClass(cls): + super().setUpClass() + with cls.work_on_actions(cls) as work: + cls.stock = work.component(usage="stock") + cls.picking = cls._create_picking( + lines=[(cls.product_a, 10), (cls.product_b, 10)], confirm=True + ) + cls.move0 = cls.picking.move_ids[0] + cls.move1 = cls.picking.move_ids[1] + cls._fill_stock_for_moves(cls.move0) + cls._fill_stock_for_moves(cls.move1) + cls.picking.action_assign() + cls.picking._action_done() + + def test_create_return_move(self): + # For incoming returns, moves are set to `to_refund` + return_picking = self.stock.create_return_picking( + self.picking, self.picking_type_in, "potato" + ) + return_moves = self.stock.create_return_move( + return_picking, self.picking.move_ids + ) + self.assertTrue(all(move.to_refund for move in return_moves)) + return_picking.action_assign() + return_picking._action_done() + + # However, on outgoing returns, this field is False + outgoing_return = self.stock.create_return_picking( + return_picking, self.picking_type, "potato" + ) + outgoing_moves = self.stock.create_return_move(outgoing_return, return_moves) + self.assertTrue(all(not move.to_refund for move in outgoing_moves)) diff --git a/stock_available_to_promise_release/README.rst b/stock_available_to_promise_release/README.rst index bc3c5b06be5..0041f828642 100644 --- a/stock_available_to_promise_release/README.rst +++ b/stock_available_to_promise_release/README.rst @@ -1,3 +1,7 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + ================================== Stock Available to Promise Release ================================== @@ -7,13 +11,13 @@ Stock Available to Promise Release !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:7dbd35643eddc66766843961912a08ae586df66939dbdc9472d365d36da415b8 + !! source digest: sha256:543c66e12c249c9e0f83d9e4a5449c6915eea33005a226e38e78f54dedcda8da !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png :target: https://odoo-community.org/page/development-status :alt: Beta -.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png +.. |badge2| image:: https://img.shields.io/badge/license-LGPL--3-blue.png :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html :alt: License: LGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fwms-lightgray.png?logo=github diff --git a/stock_available_to_promise_release/__manifest__.py b/stock_available_to_promise_release/__manifest__.py index 9371b2e0cd0..7dc337724e0 100644 --- a/stock_available_to_promise_release/__manifest__.py +++ b/stock_available_to_promise_release/__manifest__.py @@ -3,7 +3,7 @@ { "name": "Stock Available to Promise Release", - "version": "16.0.3.6.2", + "version": "16.0.3.8.3", "summary": "Release Operations based on available to promise", "author": "Camptocamp, BCIM, Odoo Community Association (OCA)", "website": "https://github.com/OCA/wms", diff --git a/stock_available_to_promise_release/i18n/es.po b/stock_available_to_promise_release/i18n/es.po index a2309506b01..027efd538a7 100644 --- a/stock_available_to_promise_release/i18n/es.po +++ b/stock_available_to_promise_release/i18n/es.po @@ -175,6 +175,15 @@ msgstr "" "más movimientos. Por ejemplo, si se añaden líneas en el pedido de cliente de " "origen, los nuevos movimientos se añadirán a una nueva entrega." +#. module: stock_available_to_promise_release +#: model:ir.model.fields,help:stock_available_to_promise_release.field_stock_route__allow_unrelease_return_done_move +#: model:ir.model.fields,help:stock_available_to_promise_release.field_stock_rule__allow_unrelease_return_done_move +msgid "" +"If checked, unreleasing the delivery may create a new inverse internal " +"operation on the last done pulled transfer. Otherwise, you won't be able to " +"unrelease as soon as one of the pulled transfer is done" +msgstr "" + #. module: stock_available_to_promise_release #: model:ir.model.fields,help:stock_available_to_promise_release.field_stock_picking_type__unrelease_on_backorder msgid "" @@ -344,6 +353,12 @@ msgstr "Liberar asignaciones de transferencias" msgid "Release based on Available to Promise" msgstr "Lanzamiento basado en disponibilidad para prometer" +#. module: stock_available_to_promise_release +#: model:ir.model.fields,field_description:stock_available_to_promise_release.field_stock_route__allow_unrelease_return_done_move +#: model:ir.model.fields,field_description:stock_available_to_promise_release.field_stock_rule__allow_unrelease_return_done_move +msgid "Reverse done transfer on cancellation" +msgstr "" + #. module: stock_available_to_promise_release #: model:ir.model,name:stock_available_to_promise_release.model_stock_release #: model_terms:ir.ui.view,arch_db:stock_available_to_promise_release.view_stock_release_form @@ -399,6 +414,13 @@ msgstr "" "Los siguientes movimientos no se han publicado:\n" "%(move_names)s" +#. module: stock_available_to_promise_release +#. odoo-python +#: code:addons/stock_available_to_promise_release/models/stock_move.py:0 +#, python-format +msgid "The operation %(picking_names)s is done and cannot be returned" +msgstr "" + #. module: stock_available_to_promise_release #: model_terms:ir.ui.view,arch_db:stock_available_to_promise_release.view_stock_release_form msgid "The selected records will be released." diff --git a/stock_available_to_promise_release/i18n/es_AR.po b/stock_available_to_promise_release/i18n/es_AR.po index 7dfe4413025..a74583bdb21 100644 --- a/stock_available_to_promise_release/i18n/es_AR.po +++ b/stock_available_to_promise_release/i18n/es_AR.po @@ -174,6 +174,15 @@ msgid "" "new moves will be added to a new delivery." msgstr "" +#. module: stock_available_to_promise_release +#: model:ir.model.fields,help:stock_available_to_promise_release.field_stock_route__allow_unrelease_return_done_move +#: model:ir.model.fields,help:stock_available_to_promise_release.field_stock_rule__allow_unrelease_return_done_move +msgid "" +"If checked, unreleasing the delivery may create a new inverse internal " +"operation on the last done pulled transfer. Otherwise, you won't be able to " +"unrelease as soon as one of the pulled transfer is done" +msgstr "" + #. module: stock_available_to_promise_release #: model:ir.model.fields,help:stock_available_to_promise_release.field_stock_picking_type__unrelease_on_backorder msgid "" @@ -337,6 +346,12 @@ msgstr "Liberar Asignaciones de Transferencias" msgid "Release based on Available to Promise" msgstr "Liberación basada en Disponibilidad a Prometer" +#. module: stock_available_to_promise_release +#: model:ir.model.fields,field_description:stock_available_to_promise_release.field_stock_route__allow_unrelease_return_done_move +#: model:ir.model.fields,field_description:stock_available_to_promise_release.field_stock_rule__allow_unrelease_return_done_move +msgid "Reverse done transfer on cancellation" +msgstr "" + #. module: stock_available_to_promise_release #: model:ir.model,name:stock_available_to_promise_release.model_stock_release #: model_terms:ir.ui.view,arch_db:stock_available_to_promise_release.view_stock_release_form @@ -388,6 +403,13 @@ msgid "" "%(move_names)s" msgstr "" +#. module: stock_available_to_promise_release +#. odoo-python +#: code:addons/stock_available_to_promise_release/models/stock_move.py:0 +#, python-format +msgid "The operation %(picking_names)s is done and cannot be returned" +msgstr "" + #. module: stock_available_to_promise_release #: model_terms:ir.ui.view,arch_db:stock_available_to_promise_release.view_stock_release_form msgid "The selected records will be released." diff --git a/stock_available_to_promise_release/i18n/fr.po b/stock_available_to_promise_release/i18n/fr.po index f36bf4d5727..ec36be7baa5 100644 --- a/stock_available_to_promise_release/i18n/fr.po +++ b/stock_available_to_promise_release/i18n/fr.po @@ -175,6 +175,15 @@ msgstr "" "des lignes dans la commande de vente d'origine, les nouveaux mouvements " "seront ajoutés à une nouvelle livraison." +#. module: stock_available_to_promise_release +#: model:ir.model.fields,help:stock_available_to_promise_release.field_stock_route__allow_unrelease_return_done_move +#: model:ir.model.fields,help:stock_available_to_promise_release.field_stock_rule__allow_unrelease_return_done_move +msgid "" +"If checked, unreleasing the delivery may create a new inverse internal " +"operation on the last done pulled transfer. Otherwise, you won't be able to " +"unrelease as soon as one of the pulled transfer is done" +msgstr "" + #. module: stock_available_to_promise_release #: model:ir.model.fields,help:stock_available_to_promise_release.field_stock_picking_type__unrelease_on_backorder msgid "" @@ -343,6 +352,12 @@ msgstr "" msgid "Release based on Available to Promise" msgstr "" +#. module: stock_available_to_promise_release +#: model:ir.model.fields,field_description:stock_available_to_promise_release.field_stock_route__allow_unrelease_return_done_move +#: model:ir.model.fields,field_description:stock_available_to_promise_release.field_stock_rule__allow_unrelease_return_done_move +msgid "Reverse done transfer on cancellation" +msgstr "" + #. module: stock_available_to_promise_release #: model:ir.model,name:stock_available_to_promise_release.model_stock_release #: model_terms:ir.ui.view,arch_db:stock_available_to_promise_release.view_stock_release_form @@ -398,6 +413,13 @@ msgstr "" "Les mouvements suivants ont été remis en attente de libération : \n" "%(move_names)s" +#. module: stock_available_to_promise_release +#. odoo-python +#: code:addons/stock_available_to_promise_release/models/stock_move.py:0 +#, python-format +msgid "The operation %(picking_names)s is done and cannot be returned" +msgstr "" + #. module: stock_available_to_promise_release #: model_terms:ir.ui.view,arch_db:stock_available_to_promise_release.view_stock_release_form msgid "The selected records will be released." diff --git a/stock_available_to_promise_release/i18n/hr.po b/stock_available_to_promise_release/i18n/hr.po index 6f011d613ce..910342ec56f 100644 --- a/stock_available_to_promise_release/i18n/hr.po +++ b/stock_available_to_promise_release/i18n/hr.po @@ -166,6 +166,15 @@ msgid "" "new moves will be added to a new delivery." msgstr "" +#. module: stock_available_to_promise_release +#: model:ir.model.fields,help:stock_available_to_promise_release.field_stock_route__allow_unrelease_return_done_move +#: model:ir.model.fields,help:stock_available_to_promise_release.field_stock_rule__allow_unrelease_return_done_move +msgid "" +"If checked, unreleasing the delivery may create a new inverse internal " +"operation on the last done pulled transfer. Otherwise, you won't be able to " +"unrelease as soon as one of the pulled transfer is done" +msgstr "" + #. module: stock_available_to_promise_release #: model:ir.model.fields,help:stock_available_to_promise_release.field_stock_picking_type__unrelease_on_backorder msgid "" @@ -329,6 +338,12 @@ msgstr "Otpusti alokacije prijenosa" msgid "Release based on Available to Promise" msgstr "Otpusti bazirano na raspoloživom za obećati" +#. module: stock_available_to_promise_release +#: model:ir.model.fields,field_description:stock_available_to_promise_release.field_stock_route__allow_unrelease_return_done_move +#: model:ir.model.fields,field_description:stock_available_to_promise_release.field_stock_rule__allow_unrelease_return_done_move +msgid "Reverse done transfer on cancellation" +msgstr "" + #. module: stock_available_to_promise_release #: model:ir.model,name:stock_available_to_promise_release.model_stock_release #: model_terms:ir.ui.view,arch_db:stock_available_to_promise_release.view_stock_release_form @@ -382,6 +397,13 @@ msgid "" "%(move_names)s" msgstr "" +#. module: stock_available_to_promise_release +#. odoo-python +#: code:addons/stock_available_to_promise_release/models/stock_move.py:0 +#, python-format +msgid "The operation %(picking_names)s is done and cannot be returned" +msgstr "" + #. module: stock_available_to_promise_release #: model_terms:ir.ui.view,arch_db:stock_available_to_promise_release.view_stock_release_form msgid "The selected records will be released." diff --git a/stock_available_to_promise_release/i18n/it.po b/stock_available_to_promise_release/i18n/it.po index cff64aef092..0e57f9b86e2 100644 --- a/stock_available_to_promise_release/i18n/it.po +++ b/stock_available_to_promise_release/i18n/it.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0\n" "Report-Msgid-Bugs-To: \n" -"PO-Revision-Date: 2025-01-15 15:06+0000\n" +"PO-Revision-Date: 2025-04-29 16:26+0000\n" "Last-Translator: mymage \n" "Language-Team: none\n" "Language: it\n" @@ -14,7 +14,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 5.6.2\n" +"X-Generator: Weblate 5.10.4\n" #. module: stock_available_to_promise_release #: model:ir.model.fields,field_description:stock_available_to_promise_release.field_stock_picking__release_ready_count @@ -175,6 +175,19 @@ msgstr "" "nell'ordine di vendita originale, i nuovi movimenti verranno aggiunti a una " "nuova consegna." +#. module: stock_available_to_promise_release +#: model:ir.model.fields,help:stock_available_to_promise_release.field_stock_route__allow_unrelease_return_done_move +#: model:ir.model.fields,help:stock_available_to_promise_release.field_stock_rule__allow_unrelease_return_done_move +msgid "" +"If checked, unreleasing the delivery may create a new inverse internal " +"operation on the last done pulled transfer. Otherwise, you won't be able to " +"unrelease as soon as one of the pulled transfer is done" +msgstr "" +"Se selezionata, l'annullamento del rilascio della consegna potrebbe creare " +"una nuova operazione interna inversa sull'ultimo trasferimento prelevato " +"eseguito. In caso contrario, non sarà possibile annullare il rilascio non " +"appena uno dei trasferimenti prelevati viene completato" + #. module: stock_available_to_promise_release #: model:ir.model.fields,help:stock_available_to_promise_release.field_stock_picking_type__unrelease_on_backorder msgid "" @@ -345,6 +358,12 @@ msgstr "Rilascio assegnazioni trasferimenti" msgid "Release based on Available to Promise" msgstr "Rilascio in base alla disponibilità alle promesse" +#. module: stock_available_to_promise_release +#: model:ir.model.fields,field_description:stock_available_to_promise_release.field_stock_route__allow_unrelease_return_done_move +#: model:ir.model.fields,field_description:stock_available_to_promise_release.field_stock_rule__allow_unrelease_return_done_move +msgid "Reverse done transfer on cancellation" +msgstr "Inverti trasferimento eseguito all'annullamento" + #. module: stock_available_to_promise_release #: model:ir.model,name:stock_available_to_promise_release.model_stock_release #: model_terms:ir.ui.view,arch_db:stock_available_to_promise_release.view_stock_release_form @@ -400,6 +419,13 @@ msgstr "" "I segenti movimenti sono stati trattenuti: \n" "%(move_names)s" +#. module: stock_available_to_promise_release +#. odoo-python +#: code:addons/stock_available_to_promise_release/models/stock_move.py:0 +#, python-format +msgid "The operation %(picking_names)s is done and cannot be returned" +msgstr "L'operazione %(picking_names)s è conclusa e non può essere resa" + #. module: stock_available_to_promise_release #: model_terms:ir.ui.view,arch_db:stock_available_to_promise_release.view_stock_release_form msgid "The selected records will be released." diff --git a/stock_available_to_promise_release/i18n/stock_available_to_promise_release.pot b/stock_available_to_promise_release/i18n/stock_available_to_promise_release.pot index d94b4663f21..434de62722b 100644 --- a/stock_available_to_promise_release/i18n/stock_available_to_promise_release.pot +++ b/stock_available_to_promise_release/i18n/stock_available_to_promise_release.pot @@ -26,6 +26,7 @@ msgstr "" #. module: stock_available_to_promise_release #. odoo-python #: code:addons/stock_available_to_promise_release/models/stock_move.py:0 +#: code:addons/stock_available_to_promise_release/models/stock_move.py:0 #, python-format msgid "- blocking transfer(s): %(picking_names)s" msgstr "" @@ -154,6 +155,15 @@ msgid "" "new moves will be added to a new delivery." msgstr "" +#. module: stock_available_to_promise_release +#: model:ir.model.fields,help:stock_available_to_promise_release.field_stock_route__allow_unrelease_return_done_move +#: model:ir.model.fields,help:stock_available_to_promise_release.field_stock_rule__allow_unrelease_return_done_move +msgid "" +"If checked, unreleasing the delivery may create a new inverse internal " +"operation on the last done pulled transfer. Otherwise, you won't be able to " +"unrelease as soon as one of the pulled transfer is done" +msgstr "" + #. module: stock_available_to_promise_release #: model:ir.model.fields,help:stock_available_to_promise_release.field_stock_picking_type__unrelease_on_backorder msgid "" @@ -315,6 +325,12 @@ msgstr "" msgid "Release based on Available to Promise" msgstr "" +#. module: stock_available_to_promise_release +#: model:ir.model.fields,field_description:stock_available_to_promise_release.field_stock_route__allow_unrelease_return_done_move +#: model:ir.model.fields,field_description:stock_available_to_promise_release.field_stock_rule__allow_unrelease_return_done_move +msgid "Reverse done transfer on cancellation" +msgstr "" + #. module: stock_available_to_promise_release #: model:ir.model,name:stock_available_to_promise_release.model_stock_release #: model_terms:ir.ui.view,arch_db:stock_available_to_promise_release.view_stock_release_form @@ -351,6 +367,7 @@ msgstr "" #. module: stock_available_to_promise_release #. odoo-python #: code:addons/stock_available_to_promise_release/models/stock_picking.py:0 +#: code:addons/stock_available_to_promise_release/models/stock_picking.py:0 #, python-format msgid "" "The backorder # Copyright 2023 Michael Tietz (MT Software) +# Copyright 2025 Raumschmiede GmbH # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). import itertools import logging import operator as py_operator +from collections import defaultdict from odoo import _, api, fields, models from odoo.exceptions import UserError from odoo.osv import expression -from odoo.tools import date_utils, float_compare, float_round, groupby +from odoo.tools import date_utils, float_compare, float_is_zero, float_round, groupby from odoo.addons.stock.models.stock_move import StockMove as StockMoveBase @@ -87,35 +89,42 @@ def _is_unreleaseable(self): and self.rule_id.available_to_promise_defer_pull ) + def _has_unreleasable_state(self): + self.ensure_one() + if self.rule_id.allow_unrelease_return_done_move: + blocking_states = ("cancel",) + else: + blocking_states = ("done", "cancel") + return self.state not in blocking_states + def _in_progress_for_unrelease(self) -> StockMoveBase: """ - This method will return the moves not done or canceled that : + This method will return the moves with unreleasable state that : - have their picking printed - - have a quantity done != 0 - + - have a quantity done set if allow_unrelease_return_done_move """ - moves = self.filtered(lambda m: m.state not in ("done", "cancel")) - if not moves: - return moves - moves_printed = moves.filtered("picking_id.printed") - if moves_printed: - return moves_printed - moves_done = moves.filtered("quantity_done") - if moves_done: - return moves_done - return moves.browse() + unreleasable_moves = self.filtered(lambda m: m._has_unreleasable_state()) + if not unreleasable_moves: + return unreleasable_moves + printed_pickings = unreleasable_moves.filtered("picking_id.printed") + if printed_pickings: + return printed_pickings + return unreleasable_moves.filtered( + lambda m: not m.rule_id.allow_unrelease_return_done_move and m.quantity_done + ) def _is_unrelease_allowed_on_origin_moves(self, origin_moves): """We check that the origin moves are in a state that allows the unrelease of the current move. At this stage, a move can't be unreleased if - * a picking is already printed. (The work on the picking is planned and - we don't want to change it) - * a quantity done is recorded - * the processed origin moves is not consumed by the dest moves. + the processed origin moves is not consumed by the dest moves. """ self.ensure_one() origin_done_moves = origin_moves.filtered(lambda m: m.state == "done") + if self.rule_id.allow_unrelease_return_done_move: + origin_done_moves = origin_done_moves.filtered( + lambda m: not m.picking_type_id.return_picking_type_id + ) origin_qty_done = sum( m.product_uom._compute_quantity( m.quantity_done, @@ -498,7 +507,7 @@ def _promise_reservation_horizon_date(self): return None def release_available_to_promise(self): - self._run_stock_rule() + return self._run_stock_rule() def _prepare_move_split_vals(self, qty): vals = super()._prepare_move_split_vals(qty) @@ -577,10 +586,13 @@ def _run_stock_rule(self): ) self.env["procurement.group"].run_defer(procurement_requests) - released_moves._after_release_assign_moves() - released_moves._after_release_update_chain() + assigned_moves = released_moves._after_release_assign_moves() + assigned_moves._after_release_update_chain() + + # We could have discrepancies regarding released moves state, recompute it + released_moves._recompute_state() - return released_moves + return assigned_moves def _before_release(self): """Hook that aims to be overridden.""" @@ -630,6 +642,7 @@ def _after_release_assign_moves(self): ).ids moves = self.browse(move_ids) moves._action_assign() + return moves def _release_split(self, remaining_qty): """Split move and put remaining_qty to a backorder move.""" @@ -672,6 +685,94 @@ def _get_chained_moves_iterator(self, chain_field): visited_moves += moves moves = moves.mapped(chain_field) - visited_moves + def _return_quantity_in_stock(self, qty_to_return_per_move): + """Return a quantity from a list of moves. + + The quantity to return is in the product uom""" + moves_to_return = self.browse([m_id for m_id in qty_to_return_per_move.keys()]) + moves_per_type = groupby(moves_to_return, lambda m: m.picking_type_id) + for picking_type, moves_list in moves_per_type: + moves = self.browse().union(*moves_list) + pickings = moves.picking_id + if not pickings: + continue + return_type = picking_type.return_picking_type_id + wiz_values = { + "picking_id": fields.first(pickings).id, + "location_id": return_type.default_location_dest_id.id, + } + product_return_moves = [] + if not return_type: + message = _( + "The operation %(picking_names)s is done and cannot be returned", + picking_names=", ".join(pickings.mapped("name")), + ) + raise UserError(message) + for move in moves: + # Cannot return an unprocessed move + if move.state != "done": + continue + product = move.product_id + uom = product.uom_id + qty_to_return = qty_to_return_per_move.get(move.id, 0) + # Cannot return 0 qty + if float_is_zero(qty_to_return, precision_rounding=uom.rounding): + continue + return_move_vals = { + "product_id": product.id, + "quantity": qty_to_return, + "uom_id": uom.id, + "move_id": move.id, + } + product_return_moves.append((0, 0, return_move_vals)) + if product_return_moves: + wiz_values["product_return_moves"] = product_return_moves + return_wiz = self.env["stock.return.picking"].create(wiz_values) + action = return_wiz.create_returns() + + cancel_picking = self.picking_id.browse(action["res_id"]) + # Do not copy the responsible user from the source picking as somebody + # else could scan the new cancel picking + cancel_picking.user_id = False + + returned_moves = return_wiz.product_return_moves.move_id + pickings_to_assign = returned_moves.move_dest_ids.picking_id.filtered( + lambda picking: picking.id != cancel_picking.id + and picking.state == "confirmed" + ) + if pickings_to_assign: + pickings_to_assign.action_assign() + return True + + def _unrelease_set_returnable_qty_per_move( + self, qty_to_return, qty_to_return_per_move + ): + returnable_qty = 0 + for move in self: + rounding = move.product_id.uom_id.rounding + # As a move might have multiple dest ids, we might have + # already planned to return a few units already. + # Get it, and deduce it from the returnable qty + move_qty_planned = qty_to_return_per_move.get(move.id, 0) + # A move might already have return moves linked to it, deduce their quantity + move_returned_qty = sum( + move.returned_move_ids.filtered(lambda m: m.state != "cancel").mapped( + "product_qty" + ) + ) + move_returnable_qty = min( + qty_to_return, move.product_qty - move_returned_qty - move_qty_planned + ) + if float_is_zero(move_returnable_qty, precision_rounding=rounding): + continue + # Update the quantity + qty_to_return_per_move[move.id] += move_returnable_qty + qty_to_return -= move_returnable_qty + returnable_qty += move_returnable_qty + if float_is_zero(qty_to_return, precision_rounding=rounding): + break + return returnable_qty + def unrelease(self, safe_unrelease=False): """Unrelease unreleasable moves @@ -685,29 +786,83 @@ def unrelease(self, safe_unrelease=False): if forbidden_moves: forbidden_moves._unrelease_not_allowed_error() moves_to_unrelease.write({"need_release": True}) - impacted_picking_ids = set() + qty_to_return_per_move = defaultdict(float) for move in moves_to_unrelease: + rounding = move.product_id.uom_id.rounding + # When a move is returned, it is going straight to WH/Stock, + # skipping all intermediate zones (pick/pack). + # That is why we need to keep track of qty returned along the way. + # We do not want to return the same goods at each step. + # At a given step (pick/pack/ship), qty to return is + # move.product_uom_qty - cancelled_qty_at_step - already returned qties + qty_to_unrelease = move.product_qty + qty_returned_for_move = 0 iterator = move._get_chained_moves_iterator("move_orig_ids") - moves_to_cancel = self.env["stock.move"] + moves_to_cancel_for_move = self.env["stock.move"] # backup procure_method as when you don't propagate cancel, the # destination move is forced to make_to_stock procure_method = move.procure_method next(iterator) # skip the current move for origin_moves in iterator: - origin_moves = origin_moves.filtered( + qty_to_cancel = qty_to_unrelease - qty_returned_for_move + if float_is_zero(qty_to_cancel, precision_rounding=rounding): + break + todo_origin_moves = origin_moves.filtered( lambda m: m.state not in ("done", "cancel") ) - if origin_moves: - origin_moves = move._split_origins(origin_moves) - impacted_picking_ids.update(origin_moves.mapped("picking_id").ids) + qty_canceled = 0 + if todo_origin_moves: + moves_to_cancel = move._split_origins( + todo_origin_moves, qty=qty_to_cancel + ) # avoid to propagate cancel to the original move - origin_moves.write({"propagate_cancel": False}) - # origin_moves._action_cancel() - moves_to_cancel |= origin_moves - moves_to_cancel._action_cancel() + moves_to_cancel.write({"propagate_cancel": False}) + moves_to_cancel_for_move |= moves_to_cancel + qty_canceled = sum(moves_to_cancel.mapped("product_qty")) + # checking that for the current step (pick/pack/ship) + # move.product_uom_qty == step.cancelled_qty + move.returned_quanty + # If not the case, we have to move back goods in stock. + qty_to_return = qty_to_cancel - qty_canceled + done_moves = origin_moves.filtered(lambda m: m.state == "done") + # in case of canceled origin_moves, the quantity to return must + # be limited to the quantity not consumed + done_dest_moves = done_moves.move_dest_ids.filtered( + lambda m: m.state == "done" + ) + returnable_qty = sum(done_moves.mapped("product_qty")) - sum( + done_dest_moves.mapped("product_qty") + ) + qty_to_return = min(qty_to_return, returnable_qty) + if float_compare(qty_to_return, 0, precision_rounding=rounding) <= 0: + continue + if not move.rule_id.allow_unrelease_return_done_move: + # Without allow_unrelease_return_done_move enabled, + # only moves that aren't done can be unreleased. + msg_args = { + "move_name": move.name, + "done_move_names": ", ".join(done_moves.mapped("name")), + } + message = _( + ( + "You cannot unrelease the move %(move_name)s " + "because some origin moves %(done_move_names)s are done" + ), + **msg_args + ) + raise UserError(message) + # Multiple pickings can satisfy a move + # -> len(move.move_orig_ids.picking_id) > 1 + # Group done_moves per picking, and create returns + returnable_qty = done_moves._unrelease_set_returnable_qty_per_move( + qty_to_return, qty_to_return_per_move + ) + qty_returned_for_move += returnable_qty + + moves_to_cancel_for_move._action_cancel() # restore the procure_method overwritten by _action_cancel() move.procure_method = procure_method + self._return_quantity_in_stock(qty_to_return_per_move) moves_to_unrelease.write({"need_release": True}) for picking, moves in itertools.groupby( moves_to_unrelease, lambda m: m.picking_id @@ -720,15 +875,17 @@ def unrelease(self, safe_unrelease=False): move_names=move_names, ) picking.message_post(body=body) + picking.last_release_date = False - def _split_origins(self, origins): + def _split_origins(self, origins, qty=None): """Split the origins of the move according to the quantity into the move and the quantity in the origin moves. Return the origins for the move's quantity. """ self.ensure_one() - qty = self.product_qty + if not qty: + qty = self.product_qty # Unreserve goods before the split origins._do_unreserve() rounding = self.product_uom.rounding diff --git a/stock_available_to_promise_release/models/stock_route.py b/stock_available_to_promise_release/models/stock_route.py index 7c91e368b37..b7d32ea9e93 100644 --- a/stock_available_to_promise_release/models/stock_route.py +++ b/stock_available_to_promise_release/models/stock_route.py @@ -7,6 +7,16 @@ class StockRoute(models.Model): _inherit = "stock.route" + allow_unrelease_return_done_move = fields.Boolean( + string="Reverse done transfer on cancellation", + default=False, + help=( + "If checked, unreleasing the delivery may create a new inverse " + "internal operation on the last done pulled transfer. " + "Otherwise, you won't be able to unrelease as soon as one of " + "the pulled transfer is done" + ), + ) available_to_promise_defer_pull = fields.Boolean( string="Release based on Available to Promise", default=False, diff --git a/stock_available_to_promise_release/models/stock_rule.py b/stock_available_to_promise_release/models/stock_rule.py index 7c893df5eab..be7c2516761 100644 --- a/stock_available_to_promise_release/models/stock_rule.py +++ b/stock_available_to_promise_release/models/stock_rule.py @@ -14,6 +14,10 @@ class StockRule(models.Model): related="route_id.available_to_promise_defer_pull", store=True ) + allow_unrelease_return_done_move = fields.Boolean( + related="route_id.allow_unrelease_return_done_move", store=True + ) + no_backorder_at_release = fields.Boolean( related="route_id.no_backorder_at_release", store=True ) diff --git a/stock_available_to_promise_release/static/description/index.html b/stock_available_to_promise_release/static/description/index.html index 74ba04c6572..56ad583b27e 100644 --- a/stock_available_to_promise_release/static/description/index.html +++ b/stock_available_to_promise_release/static/description/index.html @@ -3,7 +3,7 @@ -Stock Available to Promise Release +README.rst -
-

Stock Available to Promise Release

+
+ + +Odoo Community Association + +
+

Stock Available to Promise Release

-

Beta License: LGPL-3 OCA/wms Translate me on Weblate Try me on Runboat

+

Beta License: LGPL-3 OCA/wms Translate me on Weblate Try me on Runboat

Currently the reservation is performed by adding reserved quantities on quants, which is fine as long as the reservation is made right after the order confirmation. This way, the first arrived, first served principle is always @@ -422,13 +427,13 @@

Stock Available to Promise Release

-

Configuration

+

Configuration

In Inventory > Configuration > Routes, activate the option “Release based on Available to Promise” on the routes where you want to use the feature.

To modify the horizon go to “Inventory > Settings” and change “Stock reservation horizon”.

-

Usage

+

Usage

When an outgoing transfer would generate chained moves, it will not. The chained moves need to be released manually. To do so, open “Inventory > Operations > Stock Allocation”, select the moves to release and use “action > Release @@ -452,7 +457,7 @@

Usage

on the stock picking type.

-

Bug Tracker

+

Bug Tracker

Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed @@ -460,16 +465,16 @@

Bug Tracker

Do not contact contributors directly about support or help with technical issues.

-

Credits

+

Credits

-

Authors

+

Authors

  • Camptocamp
  • BCIM
-

Contributors

+

Contributors

-

Other credits

+

Other credits

The development of this module has been financially supported by:

  • Camptocamp
-

Maintainers

+

Maintainers

This module is maintained by the OCA.

Odoo Community Association @@ -500,5 +505,6 @@

Maintainers

+
diff --git a/stock_available_to_promise_release/tests/__init__.py b/stock_available_to_promise_release/tests/__init__.py index 35a813d9a28..234658fbefc 100644 --- a/stock_available_to_promise_release/tests/__init__.py +++ b/stock_available_to_promise_release/tests/__init__.py @@ -4,3 +4,5 @@ from . import test_unrelease_2steps from . import test_unrelease_3steps from . import test_unrelease_merged_moves +from . import test_unrelease_cancel +from . import test_unrelease_cancel_fixed_pick diff --git a/stock_available_to_promise_release/tests/common.py b/stock_available_to_promise_release/tests/common.py index 6fd56df73af..3184f703e5b 100644 --- a/stock_available_to_promise_release/tests/common.py +++ b/stock_available_to_promise_release/tests/common.py @@ -105,8 +105,11 @@ def _create_picking_chain(cls, wh, products=None, date=None, move_type="direct") return pickings @classmethod - def _pickings_in_group(cls, group): - return cls.env["stock.picking"].search([("group_id", "=", group.id)]) + def _pickings_in_group(cls, group, include_cancel=True): + domain = [("group_id", "=", group.id)] + if not include_cancel: + domain.append(("state", "!=", "cancel")) + return cls.env["stock.picking"].search(domain) @classmethod def _update_qty_in_location(cls, location, product, quantity): @@ -132,8 +135,18 @@ def _out_picking(cls, pickings): return pickings.filtered(lambda r: r.picking_type_code == "outgoing") @classmethod - def _deliver(cls, picking): + def _get_backorder_for_pickings(cls, pickings): + return cls.env["stock.picking"].search([("backorder_id", "in", pickings.ids)]) + + @classmethod + def _deliver(cls, picking, product_qty=None): picking.action_assign() - for line in picking.mapped("move_ids.move_line_ids"): - line.qty_done = line.reserved_qty + if product_qty: + lines = picking.move_ids.move_line_ids + for product, qty in product_qty: + line = lines.filtered(lambda m: m.product_id == product) + line.qty_done = qty + else: + for line in picking.mapped("move_ids.move_line_ids"): + line.qty_done = line.reserved_qty picking._action_done() diff --git a/stock_available_to_promise_release/tests/test_reservation.py b/stock_available_to_promise_release/tests/test_reservation.py index 11891f61dba..a064b51b881 100644 --- a/stock_available_to_promise_release/tests/test_reservation.py +++ b/stock_available_to_promise_release/tests/test_reservation.py @@ -726,7 +726,10 @@ def test_defer_creation_no_backorder_partial_available(self): split_cust_picking = cust_picking.backorder_ids self.assertEqual(len(split_cust_picking), 0) - out_picking = self._pickings_in_group(pickings.group_id) - cust_picking + out_picking = ( + self._pickings_in_group(pickings.group_id, include_cancel=False) + - cust_picking + ) # the complete one is assigned and placed into stock output self.assertRecordValues( out_picking, @@ -779,7 +782,9 @@ def test_defer_creation_no_backorder_partial_available(self): self.assertRecordValues(cust_picking, [{"state": "done"}]) cust_backorder = ( - self._pickings_in_group(cust_picking.group_id) - cust_picking - out_picking + self._pickings_in_group(cust_picking.group_id, include_cancel=False) + - cust_picking + - out_picking ) self.assertEqual(len(cust_backorder), 1) @@ -791,9 +796,13 @@ def test_defer_creation_no_backorder_partial_available(self): ] ) # nothing happen, no stock - self.assertEqual(len(self._pickings_in_group(cust_picking.group_id)), 3) + self.assertEqual( + len(self._pickings_in_group(cust_picking.group_id, include_cancel=False)), 3 + ) cust_backorder.release_available_to_promise() - self.assertEqual(len(self._pickings_in_group(cust_picking.group_id)), 3) + self.assertEqual( + len(self._pickings_in_group(cust_picking.group_id, include_cancel=False)), 3 + ) self.env["stock.move"].invalidate_model( fnames=[ @@ -807,7 +816,7 @@ def test_defer_creation_no_backorder_partial_available(self): self._update_qty_in_location(self.loc_bin1, self.product1, 30) cust_backorder.release_available_to_promise() out_backorder = ( - self._pickings_in_group(cust_picking.group_id) + self._pickings_in_group(cust_picking.group_id, include_cancel=False) - cust_backorder - cust_picking - out_picking @@ -907,7 +916,10 @@ def test_defer_creation_no_backorder_not_available(self): ], ) self.assertRecordValues( - out_picking.move_ids, + sorted( + out_picking.move_ids.filtered(lambda m: m.state != "cancel"), + key=lambda m: m.product_id.id, + ), [ {"product_qty": 10.0, "product_id": self.product1.id}, {"product_qty": 10.0, "product_id": self.product2.id}, @@ -1131,13 +1143,14 @@ def test_mto_picking(self): # TODO: test w/ multiple orders by priority def test_picking_priority(self): - self.wh.delivery_steps = "pick_pack_ship" + self.wh.delivery_steps = "pick_ship" self.wh.delivery_route_id.write({"available_to_promise_defer_pull": True}) self._update_qty_in_location(self.loc_bin1, self.product1, 20.0) pick = self._create_picking_chain(self.wh, [(self.product1, 20)]) pick.priority = "1" pick.action_confirm() pick.release_available_to_promise() + self.assertEqual(pick.priority, "1") self.assertEqual(pick.move_ids.move_orig_ids.picking_id.priority, "1") # from here we simulate a special processing flow where a priority @@ -1148,23 +1161,20 @@ def test_picking_priority(self): # partially process the picking pick_pick = pick.move_ids.move_orig_ids.picking_id pick_pick.move_ids.quantity_done = 3.0 - pick_pick._action_done() + pick_pick.with_context( + skip_immediate=True, skip_backorder=True + ).button_validate() # process and validate the picking to create a backorder pick.move_ids.quantity_done = 3.0 - pick.unrelease() - pick._action_done() - - # force priority on the initial picks to simulate an inconsistency - # this case should not happen but we observe it in real life without - # knowing how it happens - pick.priority = "1" - pick_pick.priority = "1" + pick.picking_type_id.unrelease_on_backorder = True + pick.with_context(skip_immediate=True, skip_backorder=True).button_validate() backorder = pick.backorder_ids + self.assertEqual(backorder.move_ids.need_release, True) + # the backorder should have kept the initial priority + self.assertEqual(backorder.priority, "1") # change the priority on the backorder and release it backorder.priority = "0" - backorder.action_confirm() - # force need release to True for the test - backorder.move_ids.need_release = True + self.assertEqual(pick.move_ids.priority, "0") backorder.release_available_to_promise() # the backorder should keep the new priority self.assertEqual(backorder.priority, "0") diff --git a/stock_available_to_promise_release/tests/test_unrelease.py b/stock_available_to_promise_release/tests/test_unrelease.py index 2909d9288dd..e1634878889 100644 --- a/stock_available_to_promise_release/tests/test_unrelease.py +++ b/stock_available_to_promise_release/tests/test_unrelease.py @@ -55,13 +55,14 @@ def _assert_full_unreleased(self): ) self.assertEqual(self.picking.move_ids.state, "cancel") self.assertEqual(self.picking.state, "cancel") + self.assertFalse(self.picking.last_release_date) def test_unrelease_full(self): """Unrelease all moves of a released ship. The pick should be deleted and the moves should be mark as to release""" with self._assert_full_unreleased(): self.shipping.move_ids.unrelease() - + self.assertFalse(self.shipping.last_release_date) # I can release again the move and a new pick is created self.shipping.release_available_to_promise() new_picking = self._prev_picking(self.shipping) - self.picking diff --git a/stock_available_to_promise_release/tests/test_unrelease_cancel.py b/stock_available_to_promise_release/tests/test_unrelease_cancel.py new file mode 100644 index 00000000000..337c0b91f69 --- /dev/null +++ b/stock_available_to_promise_release/tests/test_unrelease_cancel.py @@ -0,0 +1,255 @@ +# Copyright 2025 Camptocamp SA +# Copyright 2025 Raumschmiede GmbH +# Copyright 2025 Michael Tietz (MT Software) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) +from datetime import datetime + +from .common import PromiseReleaseCommonCase + + +class TestAvailableToPromiseReleaseCancel(PromiseReleaseCommonCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + + cls.wh.delivery_steps = "pick_pack_ship" + cls._update_qty_in_location(cls.loc_bin1, cls.product1, 50.0) + cls._update_qty_in_location(cls.loc_bin1, cls.product2, 50.0) + + delivery_route = cls.wh.delivery_route_id + ship_rule = delivery_route.rule_ids.filtered( + lambda r: r.location_dest_id == cls.loc_customer + ) + cls.loc_output = ship_rule.location_src_id + pack_rule = delivery_route.rule_ids.filtered( + lambda r: r.location_dest_id == cls.loc_output + ) + cls.loc_pack = pack_rule.location_src_id + pick_rule = delivery_route.rule_ids.filtered( + lambda r: r.location_dest_id == cls.loc_pack + ) + cls.pick_type = pick_rule.picking_type_id + cls.pack_type = pack_rule.picking_type_id + + cls.picking_chain = cls._create_picking_chain( + cls.wh, [(cls.product1, 10)], date=datetime(2019, 9, 2, 16, 0) + ) + cls.ship_picking = cls._out_picking(cls.picking_chain) + cls.pack_picking = cls._prev_picking(cls.ship_picking) + cls.pick_picking = cls._prev_picking(cls.pack_picking) + + cls.demo_user = cls.env.ref("base.user_demo") + cls.ship_picking.user_id = cls.demo_user + cls.pack_picking.user_id = cls.demo_user + cls.pick_picking.user_id = cls.demo_user + + # Why is this not working when creating picking after enabling this setting? + delivery_route.write( + { + "available_to_promise_defer_pull": True, + "allow_unrelease_return_done_move": True, + } + ) + cls.ship_picking.release_available_to_promise() + cls.cleanup_type = cls.env["stock.picking.type"].create( + { + "name": "Cancel Cleanup", + "default_location_dest_id": cls.loc_stock.id, + "sequence_code": "CCP", + "code": "internal", + } + ) + cls.pick_type.return_picking_type_id = cls.cleanup_type + cls.pack_type.return_picking_type_id = cls.cleanup_type + + @classmethod + def _get_cleanup_picking(cls): + return cls.env["stock.picking"].search( + [("picking_type_id", "=", cls.cleanup_type.id)] + ) + + def test_unrelease_picked(self): + # In this case, we should get 1 return picking from + # WH/PACK to WH/STOCK + self._deliver(self.pick_picking) + self.ship_picking.unrelease() + self.assertTrue(self.ship_picking.need_release) + self.assertEqual(self.pack_picking.state, "cancel") + self.assertEqual(self.pick_picking.state, "done") + cancel_picking = self._get_cleanup_picking() + self.assertEqual(self.pick_picking.user_id, self.demo_user) + self.assertFalse(cancel_picking.user_id) + self.assertEqual(len(cancel_picking), 1) + self.assertEqual(cancel_picking.location_id, self.loc_pack) + self.assertEqual(cancel_picking.location_dest_id, self.loc_stock) + + def test_unrelease_packed(self): + # In this case, we should get 1 return picking from + # WH/OUT to WH/STOCK + self._deliver(self.pick_picking) + self._deliver(self.pack_picking) + self.ship_picking.unrelease() + self.assertTrue(self.ship_picking.need_release) + self.assertEqual(self.pack_picking.state, "done") + self.assertEqual(self.pick_picking.state, "done") + cancel_picking = self._get_cleanup_picking() + self.assertEqual(self.pack_picking.user_id, self.demo_user) + self.assertFalse(cancel_picking.user_id) + self.assertEqual(len(cancel_picking), 1) + self.assertEqual(cancel_picking.location_id, self.loc_output) + self.assertEqual(cancel_picking.location_dest_id, self.loc_stock) + + def test_unrelease_picked_partial(self): + qty_picked = [(self.product1, 5.0)] + self._deliver(self.pick_picking, product_qty=qty_picked) + pick_backorder = self._get_backorder_for_pickings(self.pick_picking) + self.assertTrue(pick_backorder) + self.ship_picking.unrelease() + self.assertTrue(self.ship_picking.need_release) + self.assertEqual(self.pack_picking.state, "cancel") + self.assertEqual(self.pick_picking.state, "done") + cancel_picking = self._get_cleanup_picking() + self.assertFalse(cancel_picking.user_id) + # In the end, we cancelled 5 units for the pick backorder, and returned + # 5 units from pack -> stock + self.assertEqual(pick_backorder.state, "cancel") + self.assertEqual(cancel_picking.location_id, self.loc_pack) + self.assertEqual(cancel_picking.location_dest_id, self.loc_stock) + self.assertEqual(cancel_picking.move_ids.product_uom_qty, 5.0) + + def test_unrelease_packed_partial(self): + self._deliver(self.pick_picking) + qty_packed = [(self.product1, 5.0)] + self._deliver(self.pack_picking, product_qty=qty_packed) + pack_backorder = self._get_backorder_for_pickings(self.pack_picking) + self.assertTrue(pack_backorder) + self.ship_picking.unrelease() + self.assertTrue(self.ship_picking.need_release) + self.assertEqual(self.pack_picking.state, "done") + self.assertEqual(self.pick_picking.state, "done") + cancel_pickings = self._get_cleanup_picking() + self.assertFalse(cancel_pickings.user_id) + self.assertEqual(len(cancel_pickings), 2) + # In the end, we cancelled 5 units for the pack backorder, returned + # 5 units from pack -> stock, and 5 units from output -> stock + pack_cancel = cancel_pickings.filtered(lambda p: p.location_id == self.loc_pack) + ship_cancel = cancel_pickings.filtered( + lambda p: p.location_id == self.loc_output + ) + self.assertEqual(pack_cancel.move_ids.product_uom_qty, 5.0) + self.assertEqual(ship_cancel.move_ids.product_uom_qty, 5.0) + + @classmethod + def put_in_pack(cls, move): + # is it necessary to create stock moves? + move._action_assign() + pack = cls.env["stock.quant.package"].create({"name": move.product_id.name}) + move.move_line_ids.result_package_id = pack + return pack + + def test_unrelease_multiple_moves_same_product(self): + # Create a picking with twice the same move + product_qty = [ + (self.product1, 20), + ] + picking_chain = self._create_picking_chain(self.wh, products=product_qty) + ship_picking = self._out_picking(picking_chain) + ship_picking.release_available_to_promise() + # Creating a second move. Both moves thave the same origin (pack.move_line) + split_move_vals = ship_picking.move_ids._split(4) + split_move_vals[0]["date_deadline"] = datetime.now() + split_move = self.env["stock.move"].create(split_move_vals) + split_move._action_confirm() + split_move._action_assign() + pack_picking = self._prev_picking(ship_picking) + pick_picking = self._prev_picking(pack_picking) + self._deliver(pick_picking) + self._deliver(pack_picking) + ship_picking.unrelease() + cancel_pickings = self._get_cleanup_picking() + self.assertEqual(cancel_pickings.move_ids.product_qty, 20) + + def test_unrelease_packed_multi(self): + # Pick and pack 2 pickings, unrelease both before shipping + # Both have same picking types, goods should be returned + # to stock in the same picking + ship_no_pack = self.ship_picking + pack_no_pack = self.pack_picking + pick_no_pack = self.pick_picking + # The new picking chain will have packages + product_qty = [(self.product1, 10), (self.product2, 10)] + picking_chain = self._create_picking_chain(self.wh, products=product_qty) + ship_with_pack = self._out_picking(picking_chain) + ship_with_pack.release_available_to_promise() + pack_with_pack = self._prev_picking(ship_with_pack) + pick_with_pack = self._prev_picking(pack_with_pack) + # Process pick pickings + self._deliver(pick_with_pack) + self._deliver(pick_no_pack) + # put pack moves in packages on pack_with_pack, + pack_moves = pack_with_pack.move_ids + pack_move1 = pack_moves.filtered(lambda m: m.product_id == self.product1) + pack_move2 = pack_moves.filtered(lambda m: m.product_id == self.product2) + self.put_in_pack(pack_move1) + self.put_in_pack(pack_move2) + # Process pack pickings + self._deliver(pack_with_pack) + self._deliver(pack_no_pack) + # unrelease both ship pickings at once + (ship_with_pack | ship_no_pack).unrelease() + cancel_pickings = self._get_cleanup_picking() + # We should have 1 return picking only + self.assertEqual(len(cancel_pickings), 1) + # We should have 3 moves + cancel_moves = cancel_pickings.move_ids + self.assertEqual(len(cancel_moves), 3) + # We should have: + # - 1 move for product1 without pack + # - 1 move for product1 with pack + # - 1 move for product2 with pack + cancel_move1_no_pack = cancel_moves.filtered( + lambda m: m.product_id == self.product1 and not m.move_line_ids.package_id + ) + cancel_move1_with_pack = cancel_moves.filtered( + lambda m: m.product_id == self.product1 and m.move_line_ids.package_id + ) + cancel_move2_with_pack = cancel_moves.filtered( + lambda m: m.product_id == self.product2 and m.move_line_ids.package_id + ) + self.assertTrue(cancel_move1_no_pack) + self.assertEqual(cancel_move1_no_pack.product_qty, 10) + self.assertTrue(cancel_move1_with_pack) + self.assertEqual(cancel_move1_with_pack.product_qty, 10) + self.assertTrue(cancel_move2_with_pack) + self.assertEqual(cancel_move2_with_pack.product_qty, 10) + + def test_return_quantity_in_stock(self): + move_model = self.env["stock.move"] + pack_move = self.pack_picking.move_ids + # process pick and pack, so pack is done and returnable + self._deliver(self.pick_picking) + self._deliver(self.pack_picking) + # Using empty_recordsets doesn't raises an exception and doesn't create + # a return picking + empty_args = {} + move_model._return_quantity_in_stock(empty_args) + self.assertFalse(self._get_cleanup_picking()) + # Adding a move with no quantity + empty_args = {pack_move.id: 0} + move_model._return_quantity_in_stock(empty_args) + self.assertFalse(self._get_cleanup_picking()) + # Adding a quantity should create a return picking + valid_args = {pack_move.id: 5} + move_model._return_quantity_in_stock(valid_args) + return_picking = self._get_cleanup_picking() + self.assertEqual(return_picking.move_ids.product_qty, 5) + + def test_unrelease_shipped(self): + self._deliver(self.pick_picking) + self._deliver(self.pack_picking) + self._deliver(self.ship_picking) + self.ship_picking.unrelease() + # Did nothing + self.assertEqual(self.ship_picking.state, "done") + self.assertEqual(self.pack_picking.state, "done") + self.assertEqual(self.pick_picking.state, "done") diff --git a/stock_available_to_promise_release/tests/test_unrelease_cancel_fixed_pick.py b/stock_available_to_promise_release/tests/test_unrelease_cancel_fixed_pick.py new file mode 100644 index 00000000000..67e3b367948 --- /dev/null +++ b/stock_available_to_promise_release/tests/test_unrelease_cancel_fixed_pick.py @@ -0,0 +1,87 @@ +# Copyright 2025 Michael Tietz (MT Software) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) +from datetime import datetime + +from .common import PromiseReleaseCommonCase + + +class TestAvailableToPromiseReleaseCancelFixedPick(PromiseReleaseCommonCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + + cls._update_qty_in_location(cls.loc_bin1, cls.product1, 50.0) + cls._update_qty_in_location(cls.loc_bin1, cls.product2, 50.0) + pick_rule = cls.wh.delivery_route_id.rule_ids.filtered( + lambda r: r.location_src_id == cls.loc_stock + ) + pick_rule.group_propagation_option = "fixed" + pick_rule.group_id = cls.env["procurement.group"].create({"name": "Fixed"}) + delivery_route = cls.wh.delivery_route_id + delivery_route.write( + { + "available_to_promise_defer_pull": True, + "allow_unrelease_return_done_move": True, + } + ) + cls.cleanup_type = cls.env["stock.picking.type"].create( + { + "name": "Cancel Cleanup", + "default_location_dest_id": cls.loc_stock.id, + "sequence_code": "CCP", + "code": "internal", + } + ) + cls.pick_type = pick_rule.picking_type_id + cls.pick_type.return_picking_type_id = cls.cleanup_type + + def test_full_cancel(self): + picking_chain1 = self._create_picking_chain( + self.wh, [(self.product1, 10)], date=datetime(2019, 9, 2, 16, 0) + ) + picking_chain2 = self._create_picking_chain( + self.wh, [(self.product1, 10)], date=datetime(2019, 9, 2, 16, 0) + ) + ship_picking1 = self._out_picking(picking_chain1) + ship_picking1.release_available_to_promise() + ship_picking2 = self._out_picking(picking_chain2) + ship_picking2.release_available_to_promise() + self.assertNotEqual(ship_picking1, ship_picking2) + pick1 = self._prev_picking(ship_picking1) + pick2 = self._prev_picking(ship_picking2) + self.assertEqual(pick1, pick2) + self._deliver(pick1) + self.assertEqual(ship_picking1.state, "assigned") + self.assertEqual(ship_picking2.state, "assigned") + ship_picking2.action_cancel() + self.assertEqual(ship_picking2.state, "cancel") + self.assertEqual(ship_picking1.state, "assigned") + + def test_partial_cancel(self): + picking_chain1 = self._create_picking_chain( + self.wh, + [(self.product1, 10), (self.product2, 10)], + date=datetime(2019, 9, 2, 16, 0), + ) + picking_chain2 = self._create_picking_chain( + self.wh, + [(self.product1, 10), (self.product2, 10)], + date=datetime(2019, 9, 2, 16, 0), + ) + ship_picking1 = self._out_picking(picking_chain1) + ship_picking1.release_available_to_promise() + ship_picking2 = self._out_picking(picking_chain2) + ship_picking2.release_available_to_promise() + self.assertNotEqual(ship_picking1, ship_picking2) + pick1 = self._prev_picking(ship_picking1) + pick2 = self._prev_picking(ship_picking2) + self.assertEqual(pick1, pick2) + self._deliver(pick1) + self.assertEqual(ship_picking1.state, "assigned") + self.assertEqual(ship_picking2.state, "assigned") + ship2_product2_move = ship_picking2.move_ids.filtered( + lambda m: m.product_id == self.product2 + ) + ship2_product2_move._action_cancel() + self.assertEqual(ship_picking2.state, "assigned") + self.assertEqual(ship_picking1.state, "assigned") diff --git a/stock_available_to_promise_release/views/stock_picking_type_views.xml b/stock_available_to_promise_release/views/stock_picking_type_views.xml index 7ddb53aa7d7..50cded2a034 100644 --- a/stock_available_to_promise_release/views/stock_picking_type_views.xml +++ b/stock_available_to_promise_release/views/stock_picking_type_views.xml @@ -13,13 +13,11 @@ position="before" >
-
+ -
- + Need Release
diff --git a/stock_available_to_promise_release/views/stock_route_views.xml b/stock_available_to_promise_release/views/stock_route_views.xml index 514953fee7f..8861bb75968 100644 --- a/stock_available_to_promise_release/views/stock_route_views.xml +++ b/stock_available_to_promise_release/views/stock_route_views.xml @@ -7,6 +7,7 @@ + diff --git a/stock_available_to_promise_release_block/i18n/it.po b/stock_available_to_promise_release_block/i18n/it.po index 34faea626cf..9f28d41a420 100644 --- a/stock_available_to_promise_release_block/i18n/it.po +++ b/stock_available_to_promise_release_block/i18n/it.po @@ -94,12 +94,10 @@ msgstr "" msgid "Transfer" msgstr "Trasferimento" -#. module: stock_available_to_promise_release_block -#: model:ir.actions.server,name:stock_available_to_promise_release_block.action_stock_move_unblock_release -msgid "Unblock Release" -msgstr "Sblocco rilascio" - #. module: stock_available_to_promise_release_block #: model_terms:ir.ui.view,arch_db:stock_available_to_promise_release_block.stock_picking_form_view msgid "Unblock release" msgstr "Sblocco rilascio" + +#~ msgid "Unblock Release" +#~ msgstr "Sblocco rilascio" diff --git a/stock_available_to_promise_release_block/i18n/stock_available_to_promise_release_block.pot b/stock_available_to_promise_release_block/i18n/stock_available_to_promise_release_block.pot index f3c80732f99..36e425f397a 100644 --- a/stock_available_to_promise_release_block/i18n/stock_available_to_promise_release_block.pot +++ b/stock_available_to_promise_release_block/i18n/stock_available_to_promise_release_block.pot @@ -88,11 +88,6 @@ msgstr "" msgid "Transfer" msgstr "" -#. module: stock_available_to_promise_release_block -#: model:ir.actions.server,name:stock_available_to_promise_release_block.action_stock_move_unblock_release -msgid "Unblock Release" -msgstr "" - #. module: stock_available_to_promise_release_block #: model_terms:ir.ui.view,arch_db:stock_available_to_promise_release_block.stock_picking_form_view msgid "Unblock release" diff --git a/stock_available_to_promise_release_dynamic_routing/README.rst b/stock_available_to_promise_release_dynamic_routing/README.rst new file mode 100644 index 00000000000..1a2be00c82f --- /dev/null +++ b/stock_available_to_promise_release_dynamic_routing/README.rst @@ -0,0 +1,107 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +============================================== +Available to Promise Release - Dynamic Routing +============================================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:bdf33aea95ece6f7256a05c18e81b8e35884cc2b71072b137285020a88295c4c + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png + :target: https://odoo-community.org/page/development-status + :alt: Alpha +.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fwms-lightgray.png?logo=github + :target: https://github.com/OCA/wms/tree/16.0/stock_available_to_promise_release_dynamic_routing + :alt: OCA/wms +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/wms-16-0/wms-16-0-stock_available_to_promise_release_dynamic_routing + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/wms&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Glue module between ``stock_available_to_promise_release`` and +``stock_dynamic_routing``. + +Currently, the module only contains tests to verify the compatibility +between these two modules, but compatibility code may be needed later. + +.. IMPORTANT:: + This is an alpha version, the data model and design can change at any time without warning. + Only for development or testing purpose, do not use in production. + `More details on development status `_ + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Camptocamp +* BCIM + +Contributors +~~~~~~~~~~~~ + +* Jacques-Etienne Baudoux (BCIM) +* Guewen Baconnier +* `Trobz `_: + * Dung Tran + * Khoi Vo + +Other credits +~~~~~~~~~~~~~ + +The migration of this module from 13.0 to 14.0 was financially supported by Camptocamp + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-jbaudoux| image:: https://github.com/jbaudoux.png?size=40px + :target: https://github.com/jbaudoux + :alt: jbaudoux + +Current `maintainer `__: + +|maintainer-jbaudoux| + +This module is part of the `OCA/wms `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/stock_available_to_promise_release_dynamic_routing/__init__.py b/stock_available_to_promise_release_dynamic_routing/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/stock_available_to_promise_release_dynamic_routing/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/stock_available_to_promise_release_dynamic_routing/__manifest__.py b/stock_available_to_promise_release_dynamic_routing/__manifest__.py new file mode 100644 index 00000000000..cd2dc6b3d89 --- /dev/null +++ b/stock_available_to_promise_release_dynamic_routing/__manifest__.py @@ -0,0 +1,18 @@ +# Copyright 2020 Camptocamp (https://www.camptocamp.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +{ + "name": "Available to Promise Release - Dynamic Routing", + "summary": "Glue between moves release and dynamic routing", + "author": "Camptocamp,BCIM,Odoo Community Association (OCA)", + "maintainers": ["jbaudoux"], + "website": "https://github.com/OCA/wms", + "category": "Warehouse Management", + "version": "16.0.1.0.0", + "license": "AGPL-3", + "depends": ["stock_available_to_promise_release", "stock_dynamic_routing"], + "demo": [], + "data": [], + "auto_install": True, + "installable": True, + "development_status": "Alpha", +} diff --git a/stock_available_to_promise_release_dynamic_routing/i18n/es_AR.po b/stock_available_to_promise_release_dynamic_routing/i18n/es_AR.po new file mode 100644 index 00000000000..2aa1ab18ce5 --- /dev/null +++ b/stock_available_to_promise_release_dynamic_routing/i18n/es_AR.po @@ -0,0 +1,19 @@ +#. module: stock_available_to_promise_release_dynamic_routing +#: model:ir.model.fields,field_description:stock_available_to_promise_release_dynamic_routing.field_stock_move__display_name +msgid "Display Name" +msgstr "" + +#. module: stock_available_to_promise_release_dynamic_routing +#: model:ir.model.fields,field_description:stock_available_to_promise_release_dynamic_routing.field_stock_move__id +msgid "ID" +msgstr "" + +#. module: stock_available_to_promise_release_dynamic_routing +#: model:ir.model.fields,field_description:stock_available_to_promise_release_dynamic_routing.field_stock_move____last_update +msgid "Last Modified on" +msgstr "" + +#. module: stock_available_to_promise_release_dynamic_routing +#: model:ir.model,name:stock_available_to_promise_release_dynamic_routing.model_stock_move +msgid "Stock Move" +msgstr "" diff --git a/stock_available_to_promise_release_dynamic_routing/i18n/it.po b/stock_available_to_promise_release_dynamic_routing/i18n/it.po new file mode 100644 index 00000000000..4280258e3b6 --- /dev/null +++ b/stock_available_to_promise_release_dynamic_routing/i18n/it.po @@ -0,0 +1,35 @@ +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-11-13 14:37+0000\n" +"PO-Revision-Date: 2023-11-13 14:37+0000\n" +"Last-Translator: mymage \n" +"Language-Team: LANGUAGE \n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: ENCODING\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.17\n" + +#. module: stock_available_to_promise_release_dynamic_routing +#: model:ir.model.fields,field_description:stock_available_to_promise_release_dynamic_routing.field_stock_move__display_name +msgid "Display Name" +msgstr "Nome visualizzato" + +#. module: stock_available_to_promise_release_dynamic_routing +#: model:ir.model.fields,field_description:stock_available_to_promise_release_dynamic_routing.field_stock_move__id +msgid "ID" +msgstr "ID" + +#. module: stock_available_to_promise_release_dynamic_routing +#: model:ir.model.fields,field_description:stock_available_to_promise_release_dynamic_routing.field_stock_move____last_update +msgid "Last Modified on" +msgstr "Ultima modifica il" + +#. module: stock_available_to_promise_release_dynamic_routing +#: model:ir.model,name:stock_available_to_promise_release_dynamic_routing.model_stock_move +msgid "Stock Move" +msgstr "Movimento di magazzino" diff --git a/stock_available_to_promise_release_dynamic_routing/i18n/pt_BR.po b/stock_available_to_promise_release_dynamic_routing/i18n/pt_BR.po new file mode 100644 index 00000000000..2aa1ab18ce5 --- /dev/null +++ b/stock_available_to_promise_release_dynamic_routing/i18n/pt_BR.po @@ -0,0 +1,19 @@ +#. module: stock_available_to_promise_release_dynamic_routing +#: model:ir.model.fields,field_description:stock_available_to_promise_release_dynamic_routing.field_stock_move__display_name +msgid "Display Name" +msgstr "" + +#. module: stock_available_to_promise_release_dynamic_routing +#: model:ir.model.fields,field_description:stock_available_to_promise_release_dynamic_routing.field_stock_move__id +msgid "ID" +msgstr "" + +#. module: stock_available_to_promise_release_dynamic_routing +#: model:ir.model.fields,field_description:stock_available_to_promise_release_dynamic_routing.field_stock_move____last_update +msgid "Last Modified on" +msgstr "" + +#. module: stock_available_to_promise_release_dynamic_routing +#: model:ir.model,name:stock_available_to_promise_release_dynamic_routing.model_stock_move +msgid "Stock Move" +msgstr "" diff --git a/stock_available_to_promise_release_dynamic_routing/i18n/stock_available_to_promise_release_dynamic_routing.pot b/stock_available_to_promise_release_dynamic_routing/i18n/stock_available_to_promise_release_dynamic_routing.pot new file mode 100644 index 00000000000..71a53c55808 --- /dev/null +++ b/stock_available_to_promise_release_dynamic_routing/i18n/stock_available_to_promise_release_dynamic_routing.pot @@ -0,0 +1,19 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * stock_available_to_promise_release_dynamic_routing +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: stock_available_to_promise_release_dynamic_routing +#: model:ir.model,name:stock_available_to_promise_release_dynamic_routing.model_stock_move +msgid "Stock Move" +msgstr "" diff --git a/stock_available_to_promise_release_dynamic_routing/models/__init__.py b/stock_available_to_promise_release_dynamic_routing/models/__init__.py new file mode 100644 index 00000000000..6bda2d2428e --- /dev/null +++ b/stock_available_to_promise_release_dynamic_routing/models/__init__.py @@ -0,0 +1 @@ +from . import stock_move diff --git a/stock_available_to_promise_release_dynamic_routing/models/stock_move.py b/stock_available_to_promise_release_dynamic_routing/models/stock_move.py new file mode 100644 index 00000000000..2e01f3c259e --- /dev/null +++ b/stock_available_to_promise_release_dynamic_routing/models/stock_move.py @@ -0,0 +1,38 @@ +# Copyright 2023 Jacques-Etienne Baudoux (BCIM) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +from itertools import groupby + +from odoo import models + + +class StockMove(models.Model): + _inherit = "stock.move" + + def _after_release_assign_moves(self): + # Trigger the dynamic routing + moves = super()._after_release_assign_moves() + if self.env.context.get("in_merge_mode"): + return moves + # Check if moves can be merged. We do this after the call to + # _action_assign in super as this could delete some records in self + sorted_moves_by_rule = sorted(moves, key=lambda m: m.picking_id.id) + moves_to_rereserve_ids = [] + new_moves = self.browse() + for _picking_id, move_list in groupby( + sorted_moves_by_rule, key=lambda m: m.picking_id.id + ): + moves = self.browse(m.id for m in move_list) + merged_moves = moves._merge_moves() + new_moves |= merged_moves + if moves != merged_moves: + for move in merged_moves: + if not move.quantity_done: + moves_to_rereserve_ids.append(move.id) + if moves_to_rereserve_ids: + moves_to_rereserve = self.browse(moves_to_rereserve_ids) + moves_to_rereserve._do_unreserve() + moves_to_rereserve.with_context( + exclude_apply_dynamic_routing=True + )._action_assign() + return new_moves diff --git a/stock_available_to_promise_release_dynamic_routing/readme/CONTRIBUTORS.rst b/stock_available_to_promise_release_dynamic_routing/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000000..ca56f7d4c64 --- /dev/null +++ b/stock_available_to_promise_release_dynamic_routing/readme/CONTRIBUTORS.rst @@ -0,0 +1,5 @@ +* Jacques-Etienne Baudoux (BCIM) +* Guewen Baconnier +* `Trobz `_: + * Dung Tran + * Khoi Vo diff --git a/stock_available_to_promise_release_dynamic_routing/readme/CREDITS.rst b/stock_available_to_promise_release_dynamic_routing/readme/CREDITS.rst new file mode 100644 index 00000000000..f37ebe75704 --- /dev/null +++ b/stock_available_to_promise_release_dynamic_routing/readme/CREDITS.rst @@ -0,0 +1 @@ +The migration of this module from 13.0 to 14.0 was financially supported by Camptocamp diff --git a/stock_available_to_promise_release_dynamic_routing/readme/DESCRIPTION.rst b/stock_available_to_promise_release_dynamic_routing/readme/DESCRIPTION.rst new file mode 100644 index 00000000000..4d1b651744f --- /dev/null +++ b/stock_available_to_promise_release_dynamic_routing/readme/DESCRIPTION.rst @@ -0,0 +1,5 @@ +Glue module between ``stock_available_to_promise_release`` and +``stock_dynamic_routing``. + +Currently, the module only contains tests to verify the compatibility +between these two modules, but compatibility code may be needed later. diff --git a/stock_available_to_promise_release_dynamic_routing/static/description/icon.png b/stock_available_to_promise_release_dynamic_routing/static/description/icon.png new file mode 100644 index 00000000000..3a0328b516c Binary files /dev/null and b/stock_available_to_promise_release_dynamic_routing/static/description/icon.png differ diff --git a/stock_available_to_promise_release_dynamic_routing/static/description/index.html b/stock_available_to_promise_release_dynamic_routing/static/description/index.html new file mode 100644 index 00000000000..818056a6025 --- /dev/null +++ b/stock_available_to_promise_release_dynamic_routing/static/description/index.html @@ -0,0 +1,450 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

Available to Promise Release - Dynamic Routing

+ +

Alpha License: AGPL-3 OCA/wms Translate me on Weblate Try me on Runboat

+

Glue module between stock_available_to_promise_release and +stock_dynamic_routing.

+

Currently, the module only contains tests to verify the compatibility +between these two modules, but compatibility code may be needed later.

+
+

Important

+

This is an alpha version, the data model and design can change at any time without warning. +Only for development or testing purpose, do not use in production. +More details on development status

+
+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Camptocamp
  • +
  • BCIM
  • +
+
+
+

Contributors

+ +
+
+

Other credits

+

The migration of this module from 13.0 to 14.0 was financially supported by Camptocamp

+
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainer:

+

jbaudoux

+

This module is part of the OCA/wms project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+
+ + diff --git a/stock_available_to_promise_release_dynamic_routing/tests/__init__.py b/stock_available_to_promise_release_dynamic_routing/tests/__init__.py new file mode 100644 index 00000000000..e2abf0474a7 --- /dev/null +++ b/stock_available_to_promise_release_dynamic_routing/tests/__init__.py @@ -0,0 +1 @@ +from . import test_release_dynamic_routing diff --git a/stock_available_to_promise_release_dynamic_routing/tests/test_release_dynamic_routing.py b/stock_available_to_promise_release_dynamic_routing/tests/test_release_dynamic_routing.py new file mode 100644 index 00000000000..db55e074882 --- /dev/null +++ b/stock_available_to_promise_release_dynamic_routing/tests/test_release_dynamic_routing.py @@ -0,0 +1,230 @@ +# Copyright 2020 Camptocamp (https://www.camptocamp.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +""" +When we "release" moves, we set the "printed" flag on the transfers, +because after a release, we shouldn't have any new move merged in a +"wave" of release. + +The stock_available_to_promise_release module adds the flag on all +the transfer chain (pick, pack, ship, ...), but as transfers created +for dynamic routing are created later, we have to ensure that transfers +for these new moves have the flag. These tests check this. +""" + +from odoo.addons.stock_available_to_promise_release.tests.common import ( + PromiseReleaseCommonCase, +) + + +class TestAvailableToPromiseReleaseDynamicRouting(PromiseReleaseCommonCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + + cls.location_hb = cls.env["stock.location"].create( + {"name": "Highbay", "location_id": cls.wh.lot_stock_id.id} + ) + cls.location_hb_1 = cls.env["stock.location"].create( + {"name": "Highbay Shelf 1", "location_id": cls.location_hb.id} + ) + cls.location_handover = cls.env["stock.location"].create( + {"name": "Handover", "location_id": cls.wh.lot_stock_id.id} + ) + + def test_dynamic_routing_pull_printed(self): + """Pull Dynamic routing applied after release get "printed" flag""" + pick_type_routing_op = self.env["stock.picking.type"].create( + { + "name": "Dynamic Routing", + "code": "internal", + "sequence_code": "WH/HO", + "warehouse_id": self.wh.id, + "use_create_lots": False, + "use_existing_lots": True, + "default_location_src_id": self.location_hb.id, + "default_location_dest_id": self.location_handover.id, + } + ) + self.env["stock.routing"].create( + { + "location_id": self.location_hb.id, + "picking_type_id": self.wh.pick_type_id.id, + "rule_ids": [ + ( + 0, + 0, + {"method": "pull", "picking_type_id": pick_type_routing_op.id}, + ) + ], + } + ) + self.wh.delivery_route_id.write({"available_to_promise_defer_pull": True}) + + self._update_qty_in_location(self.location_hb_1, self.product1, 20.0) + self._update_qty_in_location(self.location_hb_1, self.product2, 10.0) + + pickings = self._create_picking_chain( + self.wh, + [(self.product1, 20), (self.product2, 10)], + ) + self.assertEqual(len(pickings), 1, "expect only the last out->customer") + cust_picking = pickings + self.assertRecordValues( + cust_picking, + [ + { + "state": "waiting", + "location_id": self.wh.wh_output_stock_loc_id.id, + "location_dest_id": self.loc_customer.id, + } + ], + ) + cust_picking.release_available_to_promise() + + pick_moves = cust_picking.move_ids.move_orig_ids + self.assertEqual(len(pick_moves), 2) + # this picking is created by standard 2-step rules + pick_picking = pick_moves.picking_id + # this flag is set by stock_available_to_promise_release + self.assertTrue(pick_picking.last_release_date) + + routing_moves = pick_moves.move_orig_ids + # if we put "printed" after we assign the 1st move only, the 2nd + # move will not be grouped in the same picking + self.assertEqual(len(routing_moves), 2) + routing_picking = routing_moves.picking_id + self.assertEqual(routing_picking.picking_type_id, pick_type_routing_op) + + self.assertTrue(routing_picking.last_release_date) + + def test_dynamic_routing_change_picking_type_printed(self): + """Type Dynamic routing applied after release get "printed" flag""" + self.wh.delivery_route_id.write({"available_to_promise_defer_pull": True}) + + area1 = self.env["stock.location"].create( + {"location_id": self.wh.wh_output_stock_loc_id.id, "name": "Area1"} + ) + pick_loc = self.wh.pick_type_id.default_location_src_id + pick_type_routing_op = self.env["stock.picking.type"].create( + { + "name": "Dynamic Routing", + "code": "internal", + "sequence_code": "WH/PICK2", + "warehouse_id": self.wh.id, + "use_create_lots": False, + "use_existing_lots": True, + "default_location_src_id": pick_loc.id, + "default_location_dest_id": area1.id, + } + ) + self.env["stock.routing"].create( + { + "location_id": pick_loc.id, + "picking_type_id": self.wh.pick_type_id.id, + "rule_ids": [ + ( + 0, + 0, + {"method": "pull", "picking_type_id": pick_type_routing_op.id}, + ) + ], + } + ) + + self._update_qty_in_location(self.loc_bin1, self.product1, 20.0) + self._update_qty_in_location(self.loc_bin1, self.product2, 10.0) + + pickings = self._create_picking_chain( + self.wh, + [(self.product1, 20), (self.product2, 10)], + ) + self.assertEqual(len(pickings), 1, "expect only the last out->customer") + cust_picking = pickings + self.assertRecordValues( + cust_picking, + [ + { + "state": "waiting", + "location_id": self.wh.wh_output_stock_loc_id.id, + "location_dest_id": self.loc_customer.id, + } + ], + ) + cust_picking.release_available_to_promise() + + pick_moves = cust_picking.move_ids.move_orig_ids + self.assertEqual(len(pick_moves), 2) + # this picking has been created to change the picking type + pick_picking = pick_moves.picking_id + self.assertEqual(pick_picking.picking_type_id, pick_type_routing_op) + # this flag is set by stock_available_to_promise_release + self.assertTrue(pick_picking.last_release_date) + + def test_dynamic_routing_change_picking_type_out_printed(self): + """Type Dynamic routing (on OUT) applied after release get "printed" flag + + Ensure the "printed" flag is set even when we have no "move_dest_ids" + moves. + """ + self.wh.delivery_route_id.write({"available_to_promise_defer_pull": True}) + pick_type_routing = self.wh.pick_type_id.copy( + {"name": "PICKP Routing", "sequence_code": "WH/PICKP"} + ) + self.env["stock.routing"].create( + { + "location_id": pick_type_routing.default_location_src_id.id, + "picking_type_id": self.wh.pick_type_id.id, + "rule_ids": [ + ( + 0, + 0, + {"method": "pull", "picking_type_id": pick_type_routing.id}, + ) + ], + } + ) + out_type_routing = self.wh.out_type_id.copy( + {"name": "OUTP Routing", "sequence_code": "WH/OUTP"} + ) + self.env["stock.routing"].create( + { + "location_id": out_type_routing.default_location_src_id.id, + "picking_type_id": self.wh.out_type_id.id, + "rule_ids": [ + (0, 0, {"method": "pull", "picking_type_id": out_type_routing.id}) + ], + } + ) + + self._update_qty_in_location(self.loc_bin1, self.product1, 20.0) + self._update_qty_in_location(self.loc_bin1, self.product2, 10.0) + + pickings = self._create_picking_chain( + self.wh, + [(self.product1, 20), (self.product2, 10)], + ) + self.assertEqual(len(pickings), 1, "expect only the last out->customer") + cust_picking = pickings + cust_picking.release_available_to_promise() + + # the original cust_picking has been canceled, because it is replaced + # by picking with the "OUTP" picking type + self.assertRecordValues(cust_picking, [{"state": "cancel"}]) + + new_cust_picking = self.env["stock.picking"].search( + [("picking_type_id", "=", out_type_routing.id)] + ) + + self.assertEqual(len(new_cust_picking.move_ids), 2) + self.assertRecordValues( + new_cust_picking, + [ + { + "state": "waiting", + "location_id": self.wh.wh_output_stock_loc_id.id, + "location_dest_id": self.loc_customer.id, + "picking_type_id": out_type_routing.id, + } + ], + ) + self.assertTrue(new_cust_picking.last_release_date) diff --git a/stock_dynamic_routing/README.rst b/stock_dynamic_routing/README.rst index 10dcaab6434..68e498b415c 100644 --- a/stock_dynamic_routing/README.rst +++ b/stock_dynamic_routing/README.rst @@ -1,3 +1,7 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + ===================== Stock Dynamic Routing ===================== @@ -7,13 +11,13 @@ Stock Dynamic Routing !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:15e4cc4851a06363897abf22b9922d8521c0d0e6fd1bb1e765ac8689351e1b0c + !! source digest: sha256:2609c19524a5c26b26ebf02be6eaa998af4ed30d6847d0d57a0f352939133db3 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png :target: https://odoo-community.org/page/development-status :alt: Beta -.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png +.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fwms-lightgray.png?logo=github diff --git a/stock_dynamic_routing/__manifest__.py b/stock_dynamic_routing/__manifest__.py index 152a1dd194c..0aceee6b566 100644 --- a/stock_dynamic_routing/__manifest__.py +++ b/stock_dynamic_routing/__manifest__.py @@ -5,7 +5,7 @@ "author": "Camptocamp, Odoo Community Association (OCA)", "website": "https://github.com/OCA/wms", "category": "Warehouse Management", - "version": "16.0.1.0.2", + "version": "16.0.1.0.4", "license": "AGPL-3", "depends": ["stock", "stock_helper"], "demo": [ diff --git a/stock_dynamic_routing/models/stock_move.py b/stock_dynamic_routing/models/stock_move.py index e6f91731c5c..0bbfc63b467 100644 --- a/stock_dynamic_routing/models/stock_move.py +++ b/stock_dynamic_routing/models/stock_move.py @@ -505,7 +505,7 @@ def _insert_routing_moves(self, picking_type, location, destination): ) if dest_moves: dest_moves.write({"move_orig_ids": [(3, self.id), (4, routing_move.id)]}) - routing_move._action_confirm(merge=False) + routing_move = routing_move._action_confirm(merge=False) return routing_move def _prepare_routing_move_values(self, picking_type, source, destination): @@ -518,4 +518,6 @@ def _prepare_routing_move_values(self, picking_type, source, destination): # https://github.com/odoo/odoo/commit/ecf726ae # to be on the safe side, force it to False "package_level_id": False, + # A routing move is always in MTO + "procure_method": "make_to_order", } diff --git a/stock_dynamic_routing/static/description/index.html b/stock_dynamic_routing/static/description/index.html index 8bbe0b5447d..59267db92fa 100644 --- a/stock_dynamic_routing/static/description/index.html +++ b/stock_dynamic_routing/static/description/index.html @@ -1,18 +1,18 @@ - -Stock Dynamic Routing +README.rst -
-

Stock Dynamic Routing

+
+ + +Odoo Community Association + +
+

Stock Dynamic Routing

-

Beta License: AGPL-3 OCA/wms Translate me on Weblate Try me on Runboat

+

Beta License: AGPL-3 OCA/wms Translate me on Weblate Try me on Runboat

Standard Stock Routes explain the steps you want to produce whereas the “Dynamic Routing” defines how operations are grouped according to their final source and destination location.

@@ -417,7 +422,7 @@

Stock Dynamic Routing

-

Configuration

+

Configuration

In Inventory Settings, you must have:

-

Usage

+

Usage

-

Try on runbot

+

Try on runbot

  • In Inventory Settings, activate:
-

Bug Tracker

+

Bug Tracker

Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed @@ -497,15 +502,15 @@

Bug Tracker

Do not contact contributors directly about support or help with technical issues.

-

Credits

+

Credits

-

Authors

+

Authors

  • Camptocamp
-

Contributors

+

Contributors

-

Maintainers

+

Maintainers

This module is maintained by the OCA.

-Odoo Community Association + +Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use.

@@ -525,5 +532,6 @@

Maintainers

+
diff --git a/stock_dynamic_routing/tests/test_routing_pull.py b/stock_dynamic_routing/tests/test_routing_pull.py index fc76a6d8d7b..4f9bcbe95f6 100644 --- a/stock_dynamic_routing/tests/test_routing_pull.py +++ b/stock_dynamic_routing/tests/test_routing_pull.py @@ -1053,3 +1053,54 @@ def test_mix_routing_reservation_same_location(self): {"location_id": self.location_hb_1_2.id, "reserved_uom_qty": 7}, ], ) + + def test_route_waiting_moves(self): + """Routing of waiting moves. + + When the initial move is rerouted, the waiting moves in the chain + are also rerouted in cascade even if the destination location of the + initial move is not changed. + """ + # make a routing that does not change locations + self.pick_type_routing_op.write( + { + "default_location_src_id": self.wh.pick_type_id.default_location_src_id, + "default_location_dest_id": self.wh.pick_type_id.default_location_dest_id, + } + ) + self.routing.location_id = ( + self.pick_type_routing_op.default_location_src_id.id, + ) + out_type_routing = self.wh.out_type_id.copy( + {"name": "OUTP Routing", "sequence_code": "WH/OUTP"} + ) + self.env["stock.routing"].create( + { + "location_id": out_type_routing.default_location_src_id.id, + "picking_type_id": self.wh.out_type_id.id, + "rule_ids": [ + (0, 0, {"method": "pull", "picking_type_id": out_type_routing.id}) + ], + } + ) + pick_picking, customer_picking = self._create_pick_ship( + self.wh, [(self.product1, 10)] + ) + self._update_product_qty_in_location(self.location_shelf_1, self.product1, 20.0) + pick_picking.action_assign() + new_cust_picking = self.env["stock.picking"].search( + [("picking_type_id", "=", out_type_routing.id)] + ) + + self.assertEqual(len(new_cust_picking), 1) + self.assertRecordValues( + new_cust_picking, + [ + { + "state": "waiting", + "location_id": self.wh.wh_output_stock_loc_id.id, + "location_dest_id": self.customer_loc.id, + "picking_type_id": out_type_routing.id, + } + ], + ) diff --git a/stock_full_location_reservation/README.rst b/stock_full_location_reservation/README.rst new file mode 100644 index 00000000000..96b3fbe0d02 --- /dev/null +++ b/stock_full_location_reservation/README.rst @@ -0,0 +1,101 @@ +=============================== +Stock full location reservation +=============================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:a0ab49571364d4e982429166e3f0a9f7f961ebbd856f1c86fc93b67dbd95b1bb + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fwms-lightgray.png?logo=github + :target: https://github.com/OCA/wms/tree/16.0/stock_full_location_reservation + :alt: OCA/wms +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/wms-16-0/wms-16-0-stock_full_location_reservation + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/wms&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Allow to extend a reservation to the full content of the source location. + +For example, if you have a reassort move for 2 units but the source location +contains 20 units, this module allows to extend the reservation to the 20 units +to move the complete content of the source location. + +This can typically be used with shopfloor location content transfer scenario. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +On the operation type, configure if the full reservation button action is visible on related transfers + +You can also configure the option 'Merge Move For Full Location Reservation' if you +want only one move at the end. WARNING: The original move will be lost. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* MT Software +* BCIM + +Contributors +~~~~~~~~~~~~ + +* Michael Tietz (MT Software) +* Jacques-Etienne Baudoux (BCIM) +* Denis Roussel + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-mt-software-de| image:: https://github.com/mt-software-de.png?size=40px + :target: https://github.com/mt-software-de + :alt: mt-software-de + +Current `maintainer `__: + +|maintainer-mt-software-de| + +This module is part of the `OCA/wms `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/stock_full_location_reservation/__init__.py b/stock_full_location_reservation/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/stock_full_location_reservation/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/stock_full_location_reservation/__manifest__.py b/stock_full_location_reservation/__manifest__.py new file mode 100644 index 00000000000..18a569e4740 --- /dev/null +++ b/stock_full_location_reservation/__manifest__.py @@ -0,0 +1,17 @@ +# Copyright 2023 Michael Tietz (MT Software) +{ + "name": "Stock full location reservation", + "summary": "Extend reservation to full content of location", + "author": "MT Software, BCIM, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/wms", + "category": "Warehouse Management", + "version": "16.0.1.0.0", + "license": "AGPL-3", + "depends": ["stock"], + "data": [ + "security/groups.xml", + "views/stock_picking_views.xml", + "views/stock_picking_type_views.xml", + ], + "maintainers": ["mt-software-de"], +} diff --git a/stock_full_location_reservation/i18n/it.po b/stock_full_location_reservation/i18n/it.po new file mode 100644 index 00000000000..8fde52495d6 --- /dev/null +++ b/stock_full_location_reservation/i18n/it.po @@ -0,0 +1,94 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * stock_full_location_reservation +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2025-07-08 09:40+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.10.4\n" + +#. module: stock_full_location_reservation +#: model_terms:ir.ui.view,arch_db:stock_full_location_reservation.stock_picking_form_view +msgid "Do Full location reservation" +msgstr "Esegui prenotazione completa ubicazione" + +#. module: stock_full_location_reservation +#: model:res.groups,name:stock_full_location_reservation.group_user +msgid "Full location reservation" +msgstr "Prenotazione completa ubicazione" + +#. module: stock_full_location_reservation +#: model:ir.model.fields,field_description:stock_full_location_reservation.field_stock_move__is_full_location_reservation +msgid "Full location reservation move" +msgstr "Movimento prenotazione completa ubicazione" + +#. module: stock_full_location_reservation +#: model:ir.model.fields,field_description:stock_full_location_reservation.field_stock_picking__has_full_location_reservations +msgid "Has full location reservations" +msgstr "Ha prenotazione completa ubicazione" + +#. module: stock_full_location_reservation +#: model:ir.model.fields,help:stock_full_location_reservation.field_stock_picking__is_full_location_reservation_visible +#: model:ir.model.fields,help:stock_full_location_reservation.field_stock_picking_type__is_full_location_reservation_visible +msgid "If this is checked, the full reservation of a the picking is visible" +msgstr "Se è selezionata, la prenotazione completa è visibile al prelievo" + +#. module: stock_full_location_reservation +#: model:ir.model.fields,help:stock_full_location_reservation.field_stock_picking_type__merge_move_for_full_location_reservation +msgid "" +"If this is checked, the full reservation of a the picking will be done\n" +" resulting of only one move (original one + full reservation one).\n" +" WARNING: If checked, it will be impossible to get the original move back.\n" +" " +msgstr "" +"Se questa opzione è selezionata, verrà effettuata la prenotazione completa \n" +" del prelievo risultante da un solo movimento (quello originale + " +"quello con prenotazione completa).\n" +" ATTENZIONE: se questa opzione è selezionata, sarà impossibile " +"recuperare il movimento originale.\n" +" " + +#. module: stock_full_location_reservation +#: model:ir.model.fields,field_description:stock_full_location_reservation.field_stock_picking__is_full_location_reservation_visible +#: model:ir.model.fields,field_description:stock_full_location_reservation.field_stock_picking_type__is_full_location_reservation_visible +msgid "Is full location reservation visible" +msgstr "La prenotazione completa è visibile" + +#. module: stock_full_location_reservation +#: model:ir.model.fields,field_description:stock_full_location_reservation.field_stock_picking_type__merge_move_for_full_location_reservation +msgid "Merge Move For Full Location Reservation" +msgstr "Unione movimento per la prenotazione completa ubicazione" + +#. module: stock_full_location_reservation +#: model:ir.model,name:stock_full_location_reservation.model_stock_picking_type +msgid "Picking Type" +msgstr "Tipo prelievo" + +#. module: stock_full_location_reservation +#: model:ir.model,name:stock_full_location_reservation.model_stock_move_line +msgid "Product Moves (Stock Move Line)" +msgstr "Movimenti prodotto (riga movimento di magazzino)" + +#. module: stock_full_location_reservation +#: model:ir.model,name:stock_full_location_reservation.model_stock_move +msgid "Stock Move" +msgstr "Movimento di magazzino" + +#. module: stock_full_location_reservation +#: model:ir.model,name:stock_full_location_reservation.model_stock_picking +msgid "Transfer" +msgstr "Trasferimento" + +#. module: stock_full_location_reservation +#: model_terms:ir.ui.view,arch_db:stock_full_location_reservation.stock_picking_form_view +msgid "Undo Full location reservation" +msgstr "Annulla prenotazione completa ubicazione" diff --git a/stock_full_location_reservation/i18n/stock_full_location_reservation.pot b/stock_full_location_reservation/i18n/stock_full_location_reservation.pot new file mode 100644 index 00000000000..8de34cfc990 --- /dev/null +++ b/stock_full_location_reservation/i18n/stock_full_location_reservation.pot @@ -0,0 +1,85 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * stock_full_location_reservation +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: stock_full_location_reservation +#: model_terms:ir.ui.view,arch_db:stock_full_location_reservation.stock_picking_form_view +msgid "Do Full location reservation" +msgstr "" + +#. module: stock_full_location_reservation +#: model:res.groups,name:stock_full_location_reservation.group_user +msgid "Full location reservation" +msgstr "" + +#. module: stock_full_location_reservation +#: model:ir.model.fields,field_description:stock_full_location_reservation.field_stock_move__is_full_location_reservation +msgid "Full location reservation move" +msgstr "" + +#. module: stock_full_location_reservation +#: model:ir.model.fields,field_description:stock_full_location_reservation.field_stock_picking__has_full_location_reservations +msgid "Has full location reservations" +msgstr "" + +#. module: stock_full_location_reservation +#: model:ir.model.fields,help:stock_full_location_reservation.field_stock_picking__is_full_location_reservation_visible +#: model:ir.model.fields,help:stock_full_location_reservation.field_stock_picking_type__is_full_location_reservation_visible +msgid "If this is checked, the full reservation of a the picking is visible" +msgstr "" + +#. module: stock_full_location_reservation +#: model:ir.model.fields,help:stock_full_location_reservation.field_stock_picking_type__merge_move_for_full_location_reservation +msgid "" +"If this is checked, the full reservation of a the picking will be done\n" +" resulting of only one move (original one + full reservation one).\n" +" WARNING: If checked, it will be impossible to get the original move back.\n" +" " +msgstr "" + +#. module: stock_full_location_reservation +#: model:ir.model.fields,field_description:stock_full_location_reservation.field_stock_picking__is_full_location_reservation_visible +#: model:ir.model.fields,field_description:stock_full_location_reservation.field_stock_picking_type__is_full_location_reservation_visible +msgid "Is full location reservation visible" +msgstr "" + +#. module: stock_full_location_reservation +#: model:ir.model.fields,field_description:stock_full_location_reservation.field_stock_picking_type__merge_move_for_full_location_reservation +msgid "Merge Move For Full Location Reservation" +msgstr "" + +#. module: stock_full_location_reservation +#: model:ir.model,name:stock_full_location_reservation.model_stock_picking_type +msgid "Picking Type" +msgstr "" + +#. module: stock_full_location_reservation +#: model:ir.model,name:stock_full_location_reservation.model_stock_move_line +msgid "Product Moves (Stock Move Line)" +msgstr "" + +#. module: stock_full_location_reservation +#: model:ir.model,name:stock_full_location_reservation.model_stock_move +msgid "Stock Move" +msgstr "" + +#. module: stock_full_location_reservation +#: model:ir.model,name:stock_full_location_reservation.model_stock_picking +msgid "Transfer" +msgstr "" + +#. module: stock_full_location_reservation +#: model_terms:ir.ui.view,arch_db:stock_full_location_reservation.stock_picking_form_view +msgid "Undo Full location reservation" +msgstr "" diff --git a/stock_full_location_reservation/models/__init__.py b/stock_full_location_reservation/models/__init__.py new file mode 100644 index 00000000000..27972ac5f52 --- /dev/null +++ b/stock_full_location_reservation/models/__init__.py @@ -0,0 +1,4 @@ +from . import stock_move +from . import stock_move_line +from . import stock_picking +from . import stock_picking_type diff --git a/stock_full_location_reservation/models/stock_move.py b/stock_full_location_reservation/models/stock_move.py new file mode 100644 index 00000000000..cdc0fae7f7d --- /dev/null +++ b/stock_full_location_reservation/models/stock_move.py @@ -0,0 +1,90 @@ +# Copyright 2023 Michael Tietz (MT Software) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo import fields, models + + +class StockMove(models.Model): + _inherit = "stock.move" + + is_full_location_reservation = fields.Boolean( + "Full location reservation move", default=False + ) + + def _filter_full_location_reservation_moves(self): + return self.filtered(lambda m: m.is_full_location_reservation) + + def _do_unreserve(self): + if self.env.context.get("skip_undo_full_location_reservation"): + return super()._do_unreserve() + full_location_moves = self._filter_full_location_reservation_moves() + full_location_moves._undo_full_location_reservation() + return super(StockMove, (self - full_location_moves))._do_unreserve() + + def undo_full_location_reservation(self): + full_location_moves = self._filter_full_location_reservation_moves() + full_location_moves._undo_full_location_reservation() + + def _undo_full_location_reservation(self): + if not self.exists(): + return + self = self.with_context(skip_undo_full_location_reservation=True) + self._do_unreserve() + self._action_cancel() + self.unlink() + + def _prepare_full_location_reservation_package_level_vals(self, package): + return { + "package_id": package.id, + "company_id": self.company_id.id, + } + + def _full_location_reservation_create_package_level(self, package): + return self.env["stock.package_level"].create( + self._prepare_full_location_reservation_package_level_vals(package) + ) + + def _full_location_reservation_prepare_move_vals( + self, product, qty, location, package=None + ): + self.ensure_one() + package_level_id = False + if package: + package_level_id = self._full_location_reservation_create_package_level( + package + ).id + return { + "is_full_location_reservation": True, + "product_uom_qty": qty, + "name": product.name, + "product_uom": product.uom_id.id, + "product_id": product.id, + "location_id": location.id, + "location_dest_id": self.picking_id.location_dest_id.id, + "picking_id": self.picking_id.id, + "package_level_id": package_level_id, + } + + def _full_location_reservation_create_move( + self, product, qty, location, package=None + ): + new_move = self.copy( + default=self._full_location_reservation_prepare_move_vals( + product, qty, location, package + ) + ) + if self.picking_type_id.merge_move_for_full_location_reservation: + # To be able to be merged, the new move should use the same source location as + # the original one. + new_move.location_id = self.location_id + self._do_unreserve() + # Don't merge at confirm + new_move._action_confirm(merge=False) + new_move = ( + (new_move | self) + .with_context(skip_undo_full_location_reservation=True) + ._merge_moves(merge_into=self) + ) + return new_move + + def _full_location_reservation(self, package_only=None): + return self.move_line_ids._full_location_reservation(package_only) diff --git a/stock_full_location_reservation/models/stock_move_line.py b/stock_full_location_reservation/models/stock_move_line.py new file mode 100644 index 00000000000..aee2844f9bc --- /dev/null +++ b/stock_full_location_reservation/models/stock_move_line.py @@ -0,0 +1,66 @@ +# Copyright 2023 Michael Tietz (MT Software) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from collections import defaultdict + +from odoo import models +from odoo.osv import expression +from odoo.tools.float_utils import float_compare + + +class StockMoveLine(models.Model): + _inherit = "stock.move.line" + + def _prepare_full_location_reservation_quants_domain(self, package_only=None): + domains = [] + for line in self: + domain = [("location_id", "=", line.location_id.id)] + if package_only: + if line.package_id: + domain += [("package_id", "=", line.package_id.id)] + else: + continue + domains.append(domain) + return expression.OR(domains) + + def _get_full_location_reservation_quants(self, package_only=None): + domain = self._prepare_full_location_reservation_quants_domain(package_only) + return self.env["stock.quant"].search(domain) + + def _get_full_location_reservable_qties(self, package_only=None): + quants = self._get_full_location_reservation_quants(package_only) + res = defaultdict(lambda: defaultdict(lambda: defaultdict(lambda: 0))) + for quant in quants: + qty_available = quant.available_quantity + if ( + float_compare( + qty_available, 0, precision_rounding=quant.product_uom_id.rounding + ) + > 0 + ): + res[quant.location_id][quant.package_id][ + quant.product_id + ] += qty_available + return res + + def _full_location_reservation(self, package_only=None): + reservable_qties = self._get_full_location_reservable_qties(package_only) + moves_to_assign_ids = [] + for line in self.exists(): # Move line should have been deleted + # Copy location and package as move line could be deleted if merge occurs + location = line.location_id + package = line.package_id + qties = reservable_qties.get(location, {}).get(package, {}) + if not qties: + continue + for product, qty in qties.items(): + moves_to_assign_ids.append( + line.move_id._full_location_reservation_create_move( + product, qty, location, package + ).id + ) + reservable_qties[location].pop(package) + moves_to_assign = self.env["stock.move"].browse(moves_to_assign_ids) + if moves_to_assign: + moves_to_assign._action_confirm() + moves_to_assign._action_assign() + return moves_to_assign diff --git a/stock_full_location_reservation/models/stock_picking.py b/stock_full_location_reservation/models/stock_picking.py new file mode 100644 index 00000000000..1ba474df72d --- /dev/null +++ b/stock_full_location_reservation/models/stock_picking.py @@ -0,0 +1,32 @@ +# Copyright 2023 Michael Tietz (MT Software) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class StockPicking(models.Model): + _inherit = "stock.picking" + + is_full_location_reservation_visible = fields.Boolean( + "Is full location reservation visible", + related="picking_type_id.is_full_location_reservation_visible", + ) + has_full_location_reservations = fields.Boolean( + "Has full location reservations", + compute="_compute_has_full_location_reservations", + ) + + @api.depends("move_ids.is_full_location_reservation") + def _compute_has_full_location_reservations(self): + for rec in self: + rec.has_full_location_reservations = ( + rec.move_ids.filtered(lambda m: m.is_full_location_reservation) + and True + or False + ) + + def do_full_location_reservation(self): + self.move_ids._full_location_reservation() + + def undo_full_location_reservation(self): + self.move_ids.undo_full_location_reservation() diff --git a/stock_full_location_reservation/models/stock_picking_type.py b/stock_full_location_reservation/models/stock_picking_type.py new file mode 100644 index 00000000000..4667544d643 --- /dev/null +++ b/stock_full_location_reservation/models/stock_picking_type.py @@ -0,0 +1,19 @@ +# Copyright 2023 Michael Tietz (MT Software) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class StockPickingType(models.Model): + _inherit = "stock.picking.type" + + is_full_location_reservation_visible = fields.Boolean( + "Is full location reservation visible", + help="""If this is checked, the full reservation of a the picking is visible""", + ) + merge_move_for_full_location_reservation = fields.Boolean( + help="""If this is checked, the full reservation of a the picking will be done + resulting of only one move (original one + full reservation one). + WARNING: If checked, it will be impossible to get the original move back. + """, + ) diff --git a/stock_full_location_reservation/readme/CONFIGURE.rst b/stock_full_location_reservation/readme/CONFIGURE.rst new file mode 100644 index 00000000000..43ce3dac8a5 --- /dev/null +++ b/stock_full_location_reservation/readme/CONFIGURE.rst @@ -0,0 +1,4 @@ +On the operation type, configure if the full reservation button action is visible on related transfers + +You can also configure the option 'Merge Move For Full Location Reservation' if you +want only one move at the end. WARNING: The original move will be lost. diff --git a/stock_full_location_reservation/readme/CONTRIBUTORS.rst b/stock_full_location_reservation/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000000..866de0e85cb --- /dev/null +++ b/stock_full_location_reservation/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +* Michael Tietz (MT Software) +* Jacques-Etienne Baudoux (BCIM) +* Denis Roussel diff --git a/stock_full_location_reservation/readme/DESCRIPTION.rst b/stock_full_location_reservation/readme/DESCRIPTION.rst new file mode 100644 index 00000000000..145887b0d27 --- /dev/null +++ b/stock_full_location_reservation/readme/DESCRIPTION.rst @@ -0,0 +1,7 @@ +Allow to extend a reservation to the full content of the source location. + +For example, if you have a reassort move for 2 units but the source location +contains 20 units, this module allows to extend the reservation to the 20 units +to move the complete content of the source location. + +This can typically be used with shopfloor location content transfer scenario. diff --git a/stock_full_location_reservation/security/groups.xml b/stock_full_location_reservation/security/groups.xml new file mode 100644 index 00000000000..28430bf76f3 --- /dev/null +++ b/stock_full_location_reservation/security/groups.xml @@ -0,0 +1,6 @@ + + + Full location reservation + + + diff --git a/stock_full_location_reservation/static/description/icon.png b/stock_full_location_reservation/static/description/icon.png new file mode 100644 index 00000000000..1dcc49c24f3 Binary files /dev/null and b/stock_full_location_reservation/static/description/icon.png differ diff --git a/stock_full_location_reservation/static/description/index.html b/stock_full_location_reservation/static/description/index.html new file mode 100644 index 00000000000..c0ddf66dce5 --- /dev/null +++ b/stock_full_location_reservation/static/description/index.html @@ -0,0 +1,437 @@ + + + + + + +Stock full location reservation + + + +
+

Stock full location reservation

+ + +

Beta License: AGPL-3 OCA/wms Translate me on Weblate Try me on Runboat

+

Allow to extend a reservation to the full content of the source location.

+

For example, if you have a reassort move for 2 units but the source location +contains 20 units, this module allows to extend the reservation to the 20 units +to move the complete content of the source location.

+

This can typically be used with shopfloor location content transfer scenario.

+

Table of contents

+ +
+

Configuration

+

On the operation type, configure if the full reservation button action is visible on related transfers

+

You can also configure the option ‘Merge Move For Full Location Reservation’ if you +want only one move at the end. WARNING: The original move will be lost.

+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • MT Software
  • +
  • BCIM
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainer:

+

mt-software-de

+

This module is part of the OCA/wms project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/stock_full_location_reservation/tests/__init__.py b/stock_full_location_reservation/tests/__init__.py new file mode 100644 index 00000000000..773c467311a --- /dev/null +++ b/stock_full_location_reservation/tests/__init__.py @@ -0,0 +1,2 @@ +from . import common +from . import test_full_location_reservation diff --git a/stock_full_location_reservation/tests/common.py b/stock_full_location_reservation/tests/common.py new file mode 100644 index 00000000000..ede76445b32 --- /dev/null +++ b/stock_full_location_reservation/tests/common.py @@ -0,0 +1,81 @@ +# Copyright 2023 Michael Tietz (MT Software) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo.addons.stock.tests.common import TestStockCommon + + +class TestStockFullLocationReservationCommon(TestStockCommon): + @classmethod + def setUpClass(cls): + super(TestStockFullLocationReservationCommon, cls).setUpClass() + cls.picking_type = cls.env.ref("stock.picking_type_out") + cls.picking_type.is_full_location_reservation_visible = True + cls.location = cls.env.ref("stock.stock_location_stock") + cls.location_rack = cls.location.create( + {"name": "Rack", "location_id": cls.location.id} + ) + cls.location_rack_child = cls.location.create( + {"name": "Rack child", "location_id": cls.location_rack.id} + ) + cls.customer_location = cls.env.ref("stock.stock_location_customers") + + def _create_quant(self, product, location, qty, package=None): + package_id = package and package.id + return self.env["stock.quant"].create( + { + "product_id": product.id, + "location_id": location.id, + "quantity": qty, + "package_id": package_id, + } + ) + + def _create_quants(self, vals): + for val in vals: + self._create_quant(*val) + + def _create_move(self, picking, product, qty, package=None): + package_level_id = False + if package: + package_level_id = ( + self.env["stock.package_level"] + .create({"package_id": package.id, "company_id": picking.company_id.id}) + .id + ) + return self.MoveObj.create( + { + "name": product.name, + "product_id": product.id, + "product_uom_qty": qty, + "product_uom": product.uom_id.id, + "picking_id": picking.id, + "location_id": picking.location_id.id, + "location_dest_id": picking.location_dest_id.id, + "package_level_id": package_level_id, + } + ) + + def _create_picking(self, location, location_dest, picking_type, moves): + picking = self.PickingObj.create( + { + "picking_type_id": picking_type.id, + "location_id": location.id, + "location_dest_id": location_dest.id, + } + ) + for move in moves: + vals = [picking] + move + self._create_move(*vals) + return picking + + def _check_move_line_len(self, pick, length, filter_func=None): + moves = pick.move_ids + if filter_func: + moves = moves.filtered(filter_func) + + self.assertEqual( + len(moves), + length, + ) + + def _filter_func(self, m): + return m.is_full_location_reservation diff --git a/stock_full_location_reservation/tests/test_full_location_reservation.py b/stock_full_location_reservation/tests/test_full_location_reservation.py new file mode 100644 index 00000000000..8bb0267e375 --- /dev/null +++ b/stock_full_location_reservation/tests/test_full_location_reservation.py @@ -0,0 +1,145 @@ +# Copyright 2023 Michael Tietz (MT Software) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo.addons.stock_full_location_reservation.tests.common import ( + TestStockFullLocationReservationCommon, +) + + +class TestFullLocationReservation(TestStockFullLocationReservationCommon): + def test_full_location_reservation(self): + picking = self._create_picking( + self.location_rack, + self.customer_location, + self.picking_type, + [[self.productA, 5]], + ) + + picking.action_confirm() + self._check_move_line_len(picking, 1) + + picking.do_full_location_reservation() + self._check_move_line_len(picking, 1) + + self._create_quants( + [ + (self.productA, self.location_rack_child, 10.0), + (self.productB, self.location_rack_child, 10.0), + ] + ) + + picking.do_full_location_reservation() + self._check_move_line_len(picking, 1) + + picking.action_assign() + + picking.do_full_location_reservation() + + self._check_move_line_len(picking, 3) + self._check_move_line_len(picking, 2, self._filter_func) + + # repeat test to check undo in do + picking.do_full_location_reservation() + + self._check_move_line_len(picking, 3) + self._check_move_line_len(picking, 2, self._filter_func) + + moves = picking.move_ids.filtered(self._filter_func) + self.assertEqual(moves.location_id, self.location_rack_child) + + picking.undo_full_location_reservation() + + self._check_move_line_len(picking, 1) + self._check_move_line_len(picking, 0, self._filter_func) + + def test_multiple_pickings(self): + picking = self._create_picking( + self.location_rack, + self.customer_location, + self.picking_type, + [[self.productA, 1]], + ) + + picking2 = self._create_picking( + self.location_rack, + self.customer_location, + self.picking_type, + [[self.productA, 1]], + ) + + pickings = picking | picking2 + + self._create_quants( + [ + (self.productA, self.location_rack_child, 10.0), + (self.productB, self.location_rack_child, 10.0), + ] + ) + + pickings.action_confirm() + pickings.action_assign() + + pickings.do_full_location_reservation() + self._check_move_line_len(pickings, 4) + self._check_move_line_len(pickings, 2, self._filter_func) + + def test_package_only(self): + package = self.env["stock.quant.package"].create({"name": "test package"}) + self._create_quants( + [ + (self.productA, self.location_rack_child, 10.0), + (self.productA, self.location_rack_child, 10.0, package), + ] + ) + picking = self._create_picking( + self.location_rack, + self.customer_location, + self.picking_type, + [ + [self.productA, 1], + [self.productA, 1, package], + ], + ) + picking.action_confirm() + picking.action_assign() + + self.assertEqual(picking.move_line_ids.package_id, package) + self._check_move_line_len(picking, 2) + picking.move_ids._full_location_reservation(package_only=True) + self._check_move_line_len(picking, 3) + self.assertEqual(picking.move_line_ids.package_id, package) + self.assertEqual(sum(picking.move_line_ids.mapped("reserved_qty")), 11) + + def test_full_location_reservation_merge(self): + """ + Activate the merge for new quantity move. + Create a picking and confirm it (quantity: 5). + Set product A in rack location (qauntity : 10). + Confirm the picking. + Do the full reservation. + The whole quantity should be assigned in one move. + + """ + self.picking_type.merge_move_for_full_location_reservation = True + picking = self._create_picking( + self.location_rack, + self.customer_location, + self.picking_type, + [[self.productA, 5]], + ) + + picking.action_confirm() + self._check_move_line_len(picking, 1) + + self._create_quants( + [ + (self.productA, self.location_rack_child, 10.0), + ] + ) + + picking.action_assign() + + picking.do_full_location_reservation() + + self._check_move_line_len(picking, 1) + self.assertEqual(10.0, picking.move_ids.product_uom_qty) + self.assertEqual(10.0, picking.move_ids.reserved_availability) diff --git a/stock_full_location_reservation/views/stock_picking_type_views.xml b/stock_full_location_reservation/views/stock_picking_type_views.xml new file mode 100644 index 00000000000..bab73297dd5 --- /dev/null +++ b/stock_full_location_reservation/views/stock_picking_type_views.xml @@ -0,0 +1,15 @@ + + + Operation Types + stock.picking.type + + + + + + + + diff --git a/stock_full_location_reservation/views/stock_picking_views.xml b/stock_full_location_reservation/views/stock_picking_views.xml new file mode 100644 index 00000000000..31a1bd09c35 --- /dev/null +++ b/stock_full_location_reservation/views/stock_picking_views.xml @@ -0,0 +1,27 @@ + + + stock.picking full location reservation + stock.picking + + + + + + diff --git a/stock_picking_batch_creation/README.rst b/stock_picking_batch_creation/README.rst index 1f349d7e869..fde7ac36d13 100644 --- a/stock_picking_batch_creation/README.rst +++ b/stock_picking_batch_creation/README.rst @@ -1,3 +1,7 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + ============================ Stock Picking Batch Creation ============================ @@ -7,13 +11,13 @@ Stock Picking Batch Creation !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:557c220536f625bc1a34b247c50dd7af3d0700fc112ddf1e1bb9e92e7094be97 + !! source digest: sha256:5776c02d9d60b438c67755c970d68145bd2112f5378cf09f73ae26ec7f1efd72 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png :target: https://odoo-community.org/page/development-status :alt: Beta -.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png +.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fwms-lightgray.png?logo=github @@ -140,6 +144,23 @@ are for the same partner. When activated, the computation of the number of bins consumed by the picking into the batch will take into account the volume of the pickings for the same partners already. +Splitting picking if needed +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +You can also activate the option *Split picking exceeding the limits* on the +wizard. In this case, when the system select the first picking to add to the +batch, it will disable the criteria based on the volume, weight and number of +lines. If the picking is exceeding the limits, the system will then try to split +the picking so that the new picking fits the criteria and can be added to the +batch. If the picking can't be split, an exception will be raised. + +This option is useful to allow to create a batch picking with pickings that +are exceeding the limits defined in the wizard. It also ensures that the +processing of pickings is done in the order of the pickings. If the option is +not activated, the system will try to find a picking that fits the criteria +and will ignore those that are exceeding the limits even if they are to be +processed first. + Bug Tracker =========== @@ -157,6 +178,7 @@ Authors ~~~~~~~ * ACSONE SA/NV +* BCIM Contributors ~~~~~~~~~~~~ @@ -188,10 +210,13 @@ promote its widespread use. .. |maintainer-lmignon| image:: https://github.com/lmignon.png?size=40px :target: https://github.com/lmignon :alt: lmignon +.. |maintainer-jbaudoux| image:: https://github.com/jbaudoux.png?size=40px + :target: https://github.com/jbaudoux + :alt: jbaudoux -Current `maintainer `__: +Current `maintainers `__: -|maintainer-lmignon| +|maintainer-lmignon| |maintainer-jbaudoux| This module is part of the `OCA/wms `_ project on GitHub. diff --git a/stock_picking_batch_creation/__manifest__.py b/stock_picking_batch_creation/__manifest__.py index 8335457cc46..056b94f6e4a 100644 --- a/stock_picking_batch_creation/__manifest__.py +++ b/stock_picking_batch_creation/__manifest__.py @@ -5,9 +5,9 @@ "name": "Stock Picking Batch Creation", "summary": """ Create a batch of pickings to be processed all together""", - "version": "16.0.1.0.0", + "version": "16.0.2.2.0", "license": "AGPL-3", - "author": "ACSONE SA/NV,Odoo Community Association (OCA)", + "author": "ACSONE SA/NV,BCIM,Odoo Community Association (OCA)", "website": "https://github.com/OCA/wms", "category": "Warehouse Management", "application": False, @@ -16,6 +16,7 @@ "delivery", # weight on picking "stock_picking_batch", "stock_picking_volume", # OCA/stock-logistics-warehouse + "stock_split_picking_dimension", # OCA/stock-logistics-workflow ], "data": [ "views/stock_device_type.xml", @@ -25,6 +26,6 @@ "security/ir.model.access.csv", ], "development_status": "Beta", - "maintainers": ["lmignon"], + "maintainers": ["lmignon", "jbaudoux"], "pre_init_hook": "pre_init_hook", } diff --git a/stock_picking_batch_creation/exceptions.py b/stock_picking_batch_creation/exceptions.py index 430648b561e..0b6b8d15807 100644 --- a/stock_picking_batch_creation/exceptions.py +++ b/stock_picking_batch_creation/exceptions.py @@ -6,7 +6,8 @@ class NoPickingCandidateError(UserError): - def __init__(self): + def __init__(self, env): + self.env = env super(NoPickingCandidateError, self).__init__( _("no candidate pickings to batch") ) @@ -14,6 +15,7 @@ def __init__(self): class PickingCandidateNumberLineExceedError(UserError): def __init__(self, picking, max_line): + self.env = picking.env self.picking = picking super(PickingCandidateNumberLineExceedError, self).__init__( _( @@ -28,7 +30,8 @@ def __init__(self, picking, max_line): class NoSuitableDeviceError(UserError): - def __init__(self, pickings): + def __init__(self, env, pickings): + self.env = env self.pickings = pickings message = _("No device found for batch picking.") if pickings: @@ -37,3 +40,12 @@ def __init__(self, pickings): names=", ".join(self.pickings.mapped("name")), ) super(NoSuitableDeviceError, self).__init__(message) + + +class PickingSplitNotPossibleError(UserError): + def __init__(self, picking): + self.env = picking.env + self.picking = picking + super(PickingSplitNotPossibleError, self).__init_( + _("Picking %(name)s cannot be split", name=self.picking.name) + ) diff --git a/stock_picking_batch_creation/i18n/fr.po b/stock_picking_batch_creation/i18n/fr.po index 699210fc2ae..0d7c2ceab43 100644 --- a/stock_picking_batch_creation/i18n/fr.po +++ b/stock_picking_batch_creation/i18n/fr.po @@ -145,6 +145,11 @@ msgstr "Matériel de préparation pour le transfert" msgid "Diagnostic" msgstr "Diagnostic" +#. module: stock_picking_batch_creation +#: model:ir.model.fields.selection,name:stock_picking_batch_creation.selection__stock_device_type__split_mode__dimension +msgid "Dimension" +msgstr "" + #. module: stock_picking_batch_creation #: model:ir.model.fields,field_description:stock_picking_batch_creation.field_make_picking_batch__display_name #: model:ir.model.fields,field_description:stock_picking_batch_creation.field_stock_device_type__display_name @@ -163,15 +168,13 @@ msgid "ID" msgstr "" #. module: stock_picking_batch_creation -#: model:ir.model.fields,help:stock_picking_batch_creation.field_make_picking_batch__no_line_limit_if_no_candidate +#: model:ir.model.fields,help:stock_picking_batch_creation.field_make_picking_batch__split_picking_exceeding_limits msgid "" -"If checked, the maximum number of lines will not be applied if there is no " -"candidate to add to the batch with a number of lines less than the maximum " -"number of lines. This option is useful if you want relax the maximum number " -"of lines to allow to create a batch even if there is no candidate to add to " -"the batch at first. This will avoid to manually create a batch with a single " -"picking for the sole case where a device is suitable for the picking but the " -"picking has more lines than the maximum number of lines." +"If checked, the pickings exceeding the maximum number of lines, volume or " +"weight of available devices will be split into multiple pickings to respect " +"the limits. If unchecked, the pickings exceeding the limits will not be " +"added to the batch. The limits are defined by the limits of the last " +"available devices." msgstr "" #. module: stock_picking_batch_creation @@ -246,8 +249,8 @@ msgstr "Volume max par bac" #. module: stock_picking_batch_creation #: model:ir.model.fields,field_description:stock_picking_batch_creation.field_make_picking_batch__maximum_number_of_preparation_lines -msgid "Maximum number of preparation lines for the batch" -msgstr "Nombre maximum de lignes de préparation pour la vague" +msgid "Maximum number of preparation lines for the batch." +msgstr "" #. module: stock_picking_batch_creation #: model:ir.model.fields,field_description:stock_picking_batch_creation.field_stock_device_type__user_max_volume @@ -291,11 +294,6 @@ msgstr "Nom" msgid "No device found for batch picking." msgstr "Aucun matériel de préparation trouvé pour la vague de transferts." -#. module: stock_picking_batch_creation -#: model:ir.model.fields,field_description:stock_picking_batch_creation.field_make_picking_batch__no_line_limit_if_no_candidate -msgid "No line limit if no candidate" -msgstr "" - #. module: stock_picking_batch_creation #: model:ir.model.fields,field_description:stock_picking_batch_creation.field_stock_device_type__nbr_bins #: model:ir.model.fields,field_description:stock_picking_batch_creation.field_stock_picking_batch__batch_nbr_bins @@ -331,6 +329,13 @@ msgstr "" msgid "Packaging volume unit of measure" msgstr "Unité de mesure de volume" +#. module: stock_picking_batch_creation +#. odoo-python +#: code:addons/stock_picking_batch_creation/exceptions.py:0 +#, python-format +msgid "Picking %(name)s cannot be split" +msgstr "" + #. module: stock_picking_batch_creation #: model:ir.model.fields,field_description:stock_picking_batch_creation.field_make_picking_batch__picking_locking_mode msgid "Picking locking mode" @@ -361,6 +366,21 @@ msgstr "" msgid "SQL FOR UPDATE SKIP LOCKED" msgstr "SQL FOR UPDATE SKIP LOCKED" +#. module: stock_picking_batch_creation +#: model:ir.model.fields,help:stock_picking_batch_creation.field_make_picking_batch__maximum_number_of_preparation_lines +msgid "Set to 0 to disable." +msgstr "" + +#. module: stock_picking_batch_creation +#: model:ir.model.fields,field_description:stock_picking_batch_creation.field_stock_device_type__split_mode +msgid "Split Mode" +msgstr "" + +#. module: stock_picking_batch_creation +#: model:ir.model.fields,field_description:stock_picking_batch_creation.field_make_picking_batch__split_picking_exceeding_limits +msgid "Split pickings exceeding limits" +msgstr "" + #. module: stock_picking_batch_creation #: model:ir.actions.act_window,name:stock_picking_batch_creation.stock_device_type_act_window #: model:ir.model,name:stock_picking_batch_creation.model_stock_device_type @@ -401,8 +421,8 @@ msgstr "Unités de mesure de volume" #. module: stock_picking_batch_creation #: model:ir.model.fields,help:stock_picking_batch_creation.field_stock_device_type__max_volume #: model:ir.model.fields,help:stock_picking_batch_creation.field_stock_device_type__min_volume -msgid "Volume in default system volume unit of measure" -msgstr "Volume dans l'unité de mesure de volume par défaut du système" +msgid "Volume in default system volume unit of measure. Set to 0 to disable." +msgstr "" #. module: stock_picking_batch_creation #: model:ir.model.fields,help:stock_picking_batch_creation.field_stock_device_type__user_weight_uom_id @@ -416,8 +436,8 @@ msgstr "Unités de mesure de poids" #. module: stock_picking_batch_creation #: model:ir.model.fields,help:stock_picking_batch_creation.field_stock_device_type__max_weight -msgid "Weight in default system weight unit of measure" -msgstr "Poids dans l'unité de mesure de poids par défaut du système" +msgid "Weight in default system weight unit of measure. Set to 0 to disable." +msgstr "" #. module: stock_picking_batch_creation #: model:ir.model.fields,field_description:stock_picking_batch_creation.field_stock_device_type__user_weight_uom_name @@ -436,6 +456,15 @@ msgstr "ex: Transpalette" msgid "no candidate pickings to batch" msgstr "aucun transfert candidat à la création de la vague de préparation" +#~ msgid "Maximum number of preparation lines for the batch" +#~ msgstr "Nombre maximum de lignes de préparation pour la vague" + +#~ msgid "Volume in default system volume unit of measure" +#~ msgstr "Volume dans l'unité de mesure de volume par défaut du système" + +#~ msgid "Weight in default system weight unit of measure" +#~ msgstr "Poids dans l'unité de mesure de poids par défaut du système" + #~ msgid "Indicates the bins occupied by the picking on the device." #~ msgstr "" #~ "Indique le nombre de bacs occupées par le transfert sur le matériel de " diff --git a/stock_picking_batch_creation/i18n/it.po b/stock_picking_batch_creation/i18n/it.po index 9a79f3cbb8a..fa71045b768 100644 --- a/stock_picking_batch_creation/i18n/it.po +++ b/stock_picking_batch_creation/i18n/it.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0\n" "Report-Msgid-Bugs-To: \n" -"PO-Revision-Date: 2024-06-07 12:35+0000\n" +"PO-Revision-Date: 2026-03-31 10:07+0000\n" "Last-Translator: mymage \n" "Language-Team: none\n" "Language: it\n" @@ -14,7 +14,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.17\n" +"X-Generator: Weblate 5.15.2\n" #. module: stock_picking_batch_creation #. odoo-python @@ -103,8 +103,7 @@ msgstr "" #. module: stock_picking_batch_creation #: model:ir.model.fields,help:stock_picking_batch_creation.field_make_picking_batch__picking_type_ids -msgid "" -"Default list of eligible operation types when creating a batch transfer" +msgid "Default list of eligible operation types when creating a batch transfer" msgstr "" "Lista predefinita dei tipi operazione utilizzabili nella creazione di un " "trasferimento raggruppato" @@ -147,6 +146,11 @@ msgstr "Dispositivi per il prelievo" msgid "Diagnostic" msgstr "Diagnostica" +#. module: stock_picking_batch_creation +#: model:ir.model.fields.selection,name:stock_picking_batch_creation.selection__stock_device_type__split_mode__dimension +msgid "Dimension" +msgstr "Dimensione" + #. module: stock_picking_batch_creation #: model:ir.model.fields,field_description:stock_picking_batch_creation.field_make_picking_batch__display_name #: model:ir.model.fields,field_description:stock_picking_batch_creation.field_stock_device_type__display_name @@ -165,31 +169,26 @@ msgid "ID" msgstr "ID" #. module: stock_picking_batch_creation -#: model:ir.model.fields,help:stock_picking_batch_creation.field_make_picking_batch__no_line_limit_if_no_candidate +#: model:ir.model.fields,help:stock_picking_batch_creation.field_make_picking_batch__split_picking_exceeding_limits msgid "" -"If checked, the maximum number of lines will not be applied if there is no " -"candidate to add to the batch with a number of lines less than the maximum " -"number of lines. This option is useful if you want relax the maximum number " -"of lines to allow to create a batch even if there is no candidate to add to " -"the batch at first. This will avoid to manually create a batch with a single" -" picking for the sole case where a device is suitable for the picking but " -"the picking has more lines than the maximum number of lines." +"If checked, the pickings exceeding the maximum number of lines, volume or " +"weight of available devices will be split into multiple pickings to respect " +"the limits. If unchecked, the pickings exceeding the limits will not be " +"added to the batch. The limits are defined by the limits of the last " +"available devices." msgstr "" -"Se selezionate, il massimo nimero di righe non verrà applicato se non ci " -"sono candidati da aggiungere al gruppo con un numero di righe inferiore al " -"massimo numero di righe. L'opzione è utile se si vuolerilasciare il massimo " -"numero di righe per consentire di creare un gruppo anche se non ci sono " -"candidati da aggiungere al gruppo come primi. Questo eviterà di creare " -"manualmente un gruppo con un singolo prelievo per il solo caso in cui un " -"dispositivo sia disponibile per il prelievo ma il prelievo ha più righe del " -"massimo." +"Se selezionata, i prelievi che superano il numero massimo di righe, volume o " +"peso dei dispositivi disponibili verranno suddivisi in più prelievi per " +"rispettare i limiti. Se deselezionata, i prelievi che superano i limiti non " +"verranno aggiunti al lotto. I limiti sono definiti dai limiti degli ultimi " +"dispositivi disponibili." #. module: stock_picking_batch_creation #: model:ir.model.fields,help:stock_picking_batch_creation.field_make_picking_batch__add_picking_list_in_error msgid "" "If not suitable device is provided for the pickings candidates, the error " -"message will contain the list of the pickings names. In somecases, this list" -" can be very long. That's why this option is uncheckedby default." +"message will contain the list of the pickings names. In somecases, this list " +"can be very long. That's why this option is uncheckedby default." msgstr "" "Se non viene fornito un dispositivo idoneo per i prelievi candidati, il " "messaggio di errore conterrà la lista dei nomi dei prelievi. In alcunui " @@ -254,13 +253,13 @@ msgstr "Volume massimo per contenitore" #. module: stock_picking_batch_creation #: model:ir.model.fields,field_description:stock_picking_batch_creation.field_make_picking_batch__maximum_number_of_preparation_lines -msgid "Maximum number of preparation lines for the batch" -msgstr "Numero massim di righe di preparazione per il gruppo" +msgid "Maximum number of preparation lines for the batch." +msgstr "Numero massimo di righe di preparazione per il gruppo." #. module: stock_picking_batch_creation #: model:ir.model.fields,field_description:stock_picking_batch_creation.field_stock_device_type__user_max_volume msgid "Maximum total net volume for electing this device" -msgstr "Massimo voume totale netto per selezionare questo dispositivo" +msgstr "Massimo volume totale netto per selezionare questo dispositivo" #. module: stock_picking_batch_creation #: model:ir.model.fields,field_description:stock_picking_batch_creation.field_stock_device_type__max_volume @@ -299,11 +298,6 @@ msgstr "Nome" msgid "No device found for batch picking." msgstr "Nessun dispositivo trovato per il gruppo prelievo." -#. module: stock_picking_batch_creation -#: model:ir.model.fields,field_description:stock_picking_batch_creation.field_make_picking_batch__no_line_limit_if_no_candidate -msgid "No line limit if no candidate" -msgstr "Nessun limite riga se non c'è il candidato" - #. module: stock_picking_batch_creation #: model:ir.model.fields,field_description:stock_picking_batch_creation.field_stock_device_type__nbr_bins #: model:ir.model.fields,field_description:stock_picking_batch_creation.field_stock_picking_batch__batch_nbr_bins @@ -340,6 +334,13 @@ msgstr "" msgid "Packaging volume unit of measure" msgstr "Unità di misura per il volume dell'imballaggio" +#. module: stock_picking_batch_creation +#. odoo-python +#: code:addons/stock_picking_batch_creation/exceptions.py:0 +#, python-format +msgid "Picking %(name)s cannot be split" +msgstr "Il prelievo %(name)s non può essere suddiviso" + #. module: stock_picking_batch_creation #: model:ir.model.fields,field_description:stock_picking_batch_creation.field_make_picking_batch__picking_locking_mode msgid "Picking locking mode" @@ -370,6 +371,21 @@ msgstr "Restringi alla stessa priorità" msgid "SQL FOR UPDATE SKIP LOCKED" msgstr "SQL PER SALTO AGGIORNAMENTO BLOCCATO" +#. module: stock_picking_batch_creation +#: model:ir.model.fields,help:stock_picking_batch_creation.field_make_picking_batch__maximum_number_of_preparation_lines +msgid "Set to 0 to disable." +msgstr "Impostare a 0 per disabilitare." + +#. module: stock_picking_batch_creation +#: model:ir.model.fields,field_description:stock_picking_batch_creation.field_stock_device_type__split_mode +msgid "Split Mode" +msgstr "Metodo di divisione" + +#. module: stock_picking_batch_creation +#: model:ir.model.fields,field_description:stock_picking_batch_creation.field_make_picking_batch__split_picking_exceeding_limits +msgid "Split pickings exceeding limits" +msgstr "Prelievi suddivisi che ecceedono i limiti" + #. module: stock_picking_batch_creation #: model:ir.actions.act_window,name:stock_picking_batch_creation.stock_device_type_act_window #: model:ir.model,name:stock_picking_batch_creation.model_stock_device_type @@ -410,8 +426,10 @@ msgstr "Unità di misura del volume" #. module: stock_picking_batch_creation #: model:ir.model.fields,help:stock_picking_batch_creation.field_stock_device_type__max_volume #: model:ir.model.fields,help:stock_picking_batch_creation.field_stock_device_type__min_volume -msgid "Volume in default system volume unit of measure" -msgstr "Volume nell'unità di misura del volume predefinita del sistema" +msgid "Volume in default system volume unit of measure. Set to 0 to disable." +msgstr "" +"Volume nell'unità di misura predefinita del volume del sistema. Impostare a " +"zero per disabilitare." #. module: stock_picking_batch_creation #: model:ir.model.fields,help:stock_picking_batch_creation.field_stock_device_type__user_weight_uom_id @@ -425,8 +443,10 @@ msgstr "Unità di misura del peso" #. module: stock_picking_batch_creation #: model:ir.model.fields,help:stock_picking_batch_creation.field_stock_device_type__max_weight -msgid "Weight in default system weight unit of measure" -msgstr "Peso nell'unità di misura del peso predefinita del sistema" +msgid "Weight in default system weight unit of measure. Set to 0 to disable." +msgstr "" +"Peso nell'unità di misura predefinita del peso del sistema. Impostare a zero " +"per disabilitare." #. module: stock_picking_batch_creation #: model:ir.model.fields,field_description:stock_picking_batch_creation.field_stock_device_type__user_weight_uom_name @@ -444,3 +464,34 @@ msgstr "es. muletto" #, python-format msgid "no candidate pickings to batch" msgstr "nessun prelievo candidato da raggruppare" + +#~ msgid "Maximum number of preparation lines for the batch" +#~ msgstr "Numero massimo di righe di preparazione per il gruppo" + +#~ msgid "Volume in default system volume unit of measure" +#~ msgstr "Volume nell'unità di misura del volume predefinita del sistema" + +#~ msgid "Weight in default system weight unit of measure" +#~ msgstr "Peso nell'unità di misura del peso predefinita del sistema" + +#~ msgid "" +#~ "If checked, the maximum number of lines will not be applied if there is " +#~ "no candidate to add to the batch with a number of lines less than the " +#~ "maximum number of lines. This option is useful if you want relax the " +#~ "maximum number of lines to allow to create a batch even if there is no " +#~ "candidate to add to the batch at first. This will avoid to manually " +#~ "create a batch with a single picking for the sole case where a device is " +#~ "suitable for the picking but the picking has more lines than the maximum " +#~ "number of lines." +#~ msgstr "" +#~ "Se selezionate, il massimo nimero di righe non verrà applicato se non ci " +#~ "sono candidati da aggiungere al gruppo con un numero di righe inferiore " +#~ "al massimo numero di righe. L'opzione è utile se si vuolerilasciare il " +#~ "massimo numero di righe per consentire di creare un gruppo anche se non " +#~ "ci sono candidati da aggiungere al gruppo come primi. Questo eviterà di " +#~ "creare manualmente un gruppo con un singolo prelievo per il solo caso in " +#~ "cui un dispositivo sia disponibile per il prelievo ma il prelievo ha più " +#~ "righe del massimo." + +#~ msgid "No line limit if no candidate" +#~ msgstr "Nessun limite riga se non c'è il candidato" diff --git a/stock_picking_batch_creation/i18n/stock_picking_batch_creation.pot b/stock_picking_batch_creation/i18n/stock_picking_batch_creation.pot index feaebf25ac4..f2c960d5e0b 100644 --- a/stock_picking_batch_creation/i18n/stock_picking_batch_creation.pot +++ b/stock_picking_batch_creation/i18n/stock_picking_batch_creation.pot @@ -132,6 +132,11 @@ msgstr "" msgid "Diagnostic" msgstr "" +#. module: stock_picking_batch_creation +#: model:ir.model.fields.selection,name:stock_picking_batch_creation.selection__stock_device_type__split_mode__dimension +msgid "Dimension" +msgstr "" + #. module: stock_picking_batch_creation #: model:ir.model.fields,field_description:stock_picking_batch_creation.field_make_picking_batch__display_name #: model:ir.model.fields,field_description:stock_picking_batch_creation.field_stock_device_type__display_name @@ -150,15 +155,13 @@ msgid "ID" msgstr "" #. module: stock_picking_batch_creation -#: model:ir.model.fields,help:stock_picking_batch_creation.field_make_picking_batch__no_line_limit_if_no_candidate +#: model:ir.model.fields,help:stock_picking_batch_creation.field_make_picking_batch__split_picking_exceeding_limits msgid "" -"If checked, the maximum number of lines will not be applied if there is no " -"candidate to add to the batch with a number of lines less than the maximum " -"number of lines. This option is useful if you want relax the maximum number " -"of lines to allow to create a batch even if there is no candidate to add to " -"the batch at first. This will avoid to manually create a batch with a single" -" picking for the sole case where a device is suitable for the picking but " -"the picking has more lines than the maximum number of lines." +"If checked, the pickings exceeding the maximum number of lines, volume or " +"weight of available devices will be split into multiple pickings to respect " +"the limits. If unchecked, the pickings exceeding the limits will not be " +"added to the batch. The limits are defined by the limits of the last " +"available devices." msgstr "" #. module: stock_picking_batch_creation @@ -227,7 +230,7 @@ msgstr "" #. module: stock_picking_batch_creation #: model:ir.model.fields,field_description:stock_picking_batch_creation.field_make_picking_batch__maximum_number_of_preparation_lines -msgid "Maximum number of preparation lines for the batch" +msgid "Maximum number of preparation lines for the batch." msgstr "" #. module: stock_picking_batch_creation @@ -272,11 +275,6 @@ msgstr "" msgid "No device found for batch picking." msgstr "" -#. module: stock_picking_batch_creation -#: model:ir.model.fields,field_description:stock_picking_batch_creation.field_make_picking_batch__no_line_limit_if_no_candidate -msgid "No line limit if no candidate" -msgstr "" - #. module: stock_picking_batch_creation #: model:ir.model.fields,field_description:stock_picking_batch_creation.field_stock_device_type__nbr_bins #: model:ir.model.fields,field_description:stock_picking_batch_creation.field_stock_picking_batch__batch_nbr_bins @@ -310,6 +308,13 @@ msgstr "" msgid "Packaging volume unit of measure" msgstr "" +#. module: stock_picking_batch_creation +#. odoo-python +#: code:addons/stock_picking_batch_creation/exceptions.py:0 +#, python-format +msgid "Picking %(name)s cannot be split" +msgstr "" + #. module: stock_picking_batch_creation #: model:ir.model.fields,field_description:stock_picking_batch_creation.field_make_picking_batch__picking_locking_mode msgid "Picking locking mode" @@ -340,6 +345,21 @@ msgstr "" msgid "SQL FOR UPDATE SKIP LOCKED" msgstr "" +#. module: stock_picking_batch_creation +#: model:ir.model.fields,help:stock_picking_batch_creation.field_make_picking_batch__maximum_number_of_preparation_lines +msgid "Set to 0 to disable." +msgstr "" + +#. module: stock_picking_batch_creation +#: model:ir.model.fields,field_description:stock_picking_batch_creation.field_stock_device_type__split_mode +msgid "Split Mode" +msgstr "" + +#. module: stock_picking_batch_creation +#: model:ir.model.fields,field_description:stock_picking_batch_creation.field_make_picking_batch__split_picking_exceeding_limits +msgid "Split pickings exceeding limits" +msgstr "" + #. module: stock_picking_batch_creation #: model:ir.actions.act_window,name:stock_picking_batch_creation.stock_device_type_act_window #: model:ir.model,name:stock_picking_batch_creation.model_stock_device_type @@ -380,7 +400,7 @@ msgstr "" #. module: stock_picking_batch_creation #: model:ir.model.fields,help:stock_picking_batch_creation.field_stock_device_type__max_volume #: model:ir.model.fields,help:stock_picking_batch_creation.field_stock_device_type__min_volume -msgid "Volume in default system volume unit of measure" +msgid "Volume in default system volume unit of measure. Set to 0 to disable." msgstr "" #. module: stock_picking_batch_creation @@ -395,7 +415,7 @@ msgstr "" #. module: stock_picking_batch_creation #: model:ir.model.fields,help:stock_picking_batch_creation.field_stock_device_type__max_weight -msgid "Weight in default system weight unit of measure" +msgid "Weight in default system weight unit of measure. Set to 0 to disable." msgstr "" #. module: stock_picking_batch_creation diff --git a/stock_picking_batch_creation/models/stock_device_type.py b/stock_picking_batch_creation/models/stock_device_type.py index dfe870544a7..9fff3aec4a9 100644 --- a/stock_picking_batch_creation/models/stock_device_type.py +++ b/stock_picking_batch_creation/models/stock_device_type.py @@ -13,15 +13,15 @@ class StockDeviceType(models.Model): name = fields.Char(required=True) min_volume = fields.Float( string="Minimum total net volume for this device", - help="Volume in default system volume unit of measure", + help="Volume in default system volume unit of measure. Set to 0 to disable.", ) max_volume = fields.Float( string="Maximum total net volume for this device", - help="Volume in default system volume unit of measure", + help="Volume in default system volume unit of measure. Set to 0 to disable.", ) max_weight = fields.Float( string="Maximum total net weight for this device", - help="Weight in default system weight unit of measure", + help="Weight in default system weight unit of measure. Set to 0 to disable.", ) user_min_volume = fields.Float( string="Minimum total net volume for electing this device", @@ -42,6 +42,11 @@ class StockDeviceType(models.Model): readonly=False, ) nbr_bins = fields.Integer(string="Number of compartments") + split_mode = fields.Selection( + selection=[("dimension", "Dimension")], + default="dimension", + required=True, + ) volume_per_bin = fields.Float( string="Max volume per bin", compute="_compute_volume_per_bin" diff --git a/stock_picking_batch_creation/models/stock_picking.py b/stock_picking_batch_creation/models/stock_picking.py index 181e01dcf9a..f22a8ec2b80 100644 --- a/stock_picking_batch_creation/models/stock_picking.py +++ b/stock_picking_batch_creation/models/stock_picking.py @@ -9,6 +9,7 @@ class StockPicking(models.Model): _inherit = "stock.picking" + picking_device_id = fields.Many2one( "stock.device.type", string="Device for the picking", @@ -26,7 +27,7 @@ def _get_nbr_bins_for_device(self, device): self.ensure_one() if not device: return 0 - if not self.volume: + if not self.volume or not device.volume_per_bin: return 1 return math.ceil(self.volume / device.volume_per_bin) diff --git a/stock_picking_batch_creation/readme/USAGE.rst b/stock_picking_batch_creation/readme/USAGE.rst index 3c67061e1fe..c27f24e1703 100644 --- a/stock_picking_batch_creation/readme/USAGE.rst +++ b/stock_picking_batch_creation/readme/USAGE.rst @@ -66,3 +66,20 @@ will prevent to consume at least one bin for each picking if pickings are for the same partner. When activated, the computation of the number of bins consumed by the picking into the batch will take into account the volume of the pickings for the same partners already. + +Splitting picking if needed +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +You can also activate the option *Split picking exceeding the limits* on the +wizard. In this case, when the system select the first picking to add to the +batch, it will disable the criteria based on the volume, weight and number of +lines. If the picking is exceeding the limits, the system will then try to split +the picking so that the new picking fits the criteria and can be added to the +batch. If the picking can't be split, an exception will be raised. + +This option is useful to allow to create a batch picking with pickings that +are exceeding the limits defined in the wizard. It also ensures that the +processing of pickings is done in the order of the pickings. If the option is +not activated, the system will try to find a picking that fits the criteria +and will ignore those that are exceeding the limits even if they are to be +processed first. \ No newline at end of file diff --git a/stock_picking_batch_creation/static/description/index.html b/stock_picking_batch_creation/static/description/index.html index c42de5c9b97..441ae9a6716 100644 --- a/stock_picking_batch_creation/static/description/index.html +++ b/stock_picking_batch_creation/static/description/index.html @@ -1,18 +1,18 @@ - -Stock Picking Batch Creation +README.rst -
-

Stock Picking Batch Creation

+
+ + +Odoo Community Association + +
+

Stock Picking Batch Creation

-

Beta License: AGPL-3 OCA/wms Translate me on Weblate Try me on Runboat

+

Beta License: AGPL-3 OCA/wms Translate me on Weblate Try me on Runboat

Odoo allows you to create batches of pickings by hand or automatically by specifying some criteria on the picking type definition.

The approach in this addon is slightly different. It doesn’t depend on the @@ -382,7 +387,7 @@

Stock Picking Batch Creation

  • A maximum number of lines in the batch
  • -

    Stock device types

    +

    Stock device types

    A stock device type is a new concept that allows to define a type of device that can be used by an operator to process operations into the warehouse (like a forklift, a compartmentalized trolleys, …).

    @@ -409,22 +414,23 @@

    Stock device types

  • Advanced configuration
  • -
  • Bug Tracker
  • -
  • Credits
  • -

    Usage

    +

    Usage

    First of all, you need to create your stock device type. To do so, go to the Inventory -> Configuration -> Stock Device Types menu.

    Once it’s done, you can start creating your picking batches.

    @@ -435,7 +441,7 @@

    Usage

    Create Batch Picking and lets the magic happen. A new window will open with the created batch picking.

    -

    Behind the scene

    +

    Behind the scene

    The creation of the picking batch is done in 3 steps:

    1. We search for a picking ready to be processed and that fits the @@ -465,9 +471,9 @@

      Behind the scene

      the updated criteria and update the criteria at each loop iteration.

    -

    Advanced configuration

    +

    Advanced configuration

    -

    Locking

    +
    Locking

    You can choice on the wizard to apply a LOCK into the database for each picking added to the batch. This is useful in a multi-user environment with a lot of users that can trigger the creation of bach picking to avoid @@ -475,7 +481,7 @@

    Locking

    process will be skipped.

    -

    Grouping by partner

    +
    Grouping by partner

    If you want to allow to group pickings of the same partner into the same bins, you can activate the option Group by partner on the wizard. This will prevent to consume at least one bin for each picking if pickings @@ -483,10 +489,25 @@

    Grouping by partner

    number of bins consumed by the picking into the batch will take into account the volume of the pickings for the same partners already.

    +
    +
    Splitting picking if needed
    +

    You can also activate the option Split picking exceeding the limits on the +wizard. In this case, when the system select the first picking to add to the +batch, it will disable the criteria based on the volume, weight and number of +lines. If the picking is exceeding the limits, the system will then try to split +the picking so that the new picking fits the criteria and can be added to the +batch. If the picking can’t be split, an exception will be raised.

    +

    This option is useful to allow to create a batch picking with pickings that +are exceeding the limits defined in the wizard. It also ensures that the +processing of pickings is done in the order of the pickings. If the option is +not activated, the system will try to find a picking that fits the criteria +and will ignore those that are exceeding the limits even if they are to be +processed first.

    +
    -

    Bug Tracker

    +

    Bug Tracker

    Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed @@ -494,22 +515,23 @@

    Bug Tracker

    Do not contact contributors directly about support or help with technical issues.

    -

    Credits

    +

    Credits

    -

    Authors

    +

    Authors

    • ACSONE SA/NV
    • +
    • BCIM
    -

    Other credits

    +

    Other credits

    The development of this module has been financially supported by:

    -

    Maintainers

    +

    Maintainers

    This module is maintained by the OCA.

    -Odoo Community Association + +Odoo Community Association +

    OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use.

    -

    Current maintainer:

    -

    lmignon

    +

    Current maintainers:

    +

    lmignon jbaudoux

    This module is part of the OCA/wms project on GitHub.

    You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

    +
    diff --git a/stock_picking_batch_creation/tests/__init__.py b/stock_picking_batch_creation/tests/__init__.py index 92afefe8492..ddf283a47cd 100644 --- a/stock_picking_batch_creation/tests/__init__.py +++ b/stock_picking_batch_creation/tests/__init__.py @@ -2,3 +2,4 @@ from . import test_picking_lock from . import test_get_device_to_use from . import test_clustering_conditions +from . import test_batch_creation_splitting diff --git a/stock_picking_batch_creation/tests/common.py b/stock_picking_batch_creation/tests/common.py index d5bdca02cb4..8e31db951e4 100644 --- a/stock_picking_batch_creation/tests/common.py +++ b/stock_picking_batch_creation/tests/common.py @@ -145,27 +145,23 @@ def _create_partner(cls, name, ref): return cls.env["res.partner"].create({"name": name, "ref": ref}) @classmethod - def _create_product( - cls, name, weight, length, height, width, uom_id=None, product_type=None - ): - if not uom_id: - uom_id = cls.uom_id - if not product_type: - product_type = "product" - volume = length * height * width - return cls.env["product.product"].create( - { - "name": name, - "uom_id": uom_id, - "type": product_type, - "weight": weight, - "product_length": length, - "product_height": height, - "product_width": width, - "volume": volume, - "dimensional_uom_id": cls.uom_m.id, - } - ) + def _create_product(cls, name, weight, length, height, width, **kwargs): + vals = { + "name": name, + "weight": weight, + "product_length": length, + "product_height": height, + "product_width": width, + "dimensional_uom_id": cls.uom_m.id, + **kwargs, + } + if not vals.get("type"): + vals["type"] = "product" + if "uom_id" not in vals: + vals["uom_id"] = cls.uom_id + if "volume" not in vals: + vals["volume"] = length * height * width + return cls.env["product.product"].create(vals) @classmethod def _create_device( @@ -212,9 +208,10 @@ def _create_picking_pick_and_assign( "product_uom": p.uom_id.id, "location_id": cls.env.ref("stock.stock_location_stock").id, "location_dest_id": warehouse.wh_output_stock_loc_id.id, + "sequence": sequence, }, ) - for p in products + for sequence, p in enumerate(products) ], } picking = cls.env["stock.picking"].create(picking_values) diff --git a/stock_picking_batch_creation/tests/test_batch_creation_splitting.py b/stock_picking_batch_creation/tests/test_batch_creation_splitting.py new file mode 100644 index 00000000000..7622ebb64a5 --- /dev/null +++ b/stock_picking_batch_creation/tests/test_batch_creation_splitting.py @@ -0,0 +1,90 @@ +# Copyright 2025 Camptocamp SA +# Copyright 2026 Jacques-Etienne Baudoux (BCIM) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +from .common import ClusterPickingCommonFeatures + + +class TestBatchCreationSplitting(ClusterPickingCommonFeatures): + @classmethod + def setUpClass(cls): + super().setUpClass() + + def test_batch_creation_one_pick_move_over_the_limit(self): + """Test splitting a picking when the first move exceeds a limit.""" + self.pick2.action_cancel() + self.pick3.action_cancel() + # Keep the picking 1 with one line and product 1 + move = self.pick1.move_ids + # And the weight of the move can not be accepted by the device + max_weight = 9 + move_weight = move.product_id.weight * move.product_qty + self.assertTrue(move_weight > max_weight) + self.make_picking_batch.write( + { + "maximum_number_of_preparation_lines": 2, + "split_picking_exceeding_limits": True, + } + ) + device = self._create_device( + "Device-A", + min_volume=0, + max_volume=0, + max_weight=max_weight, + nbr_bins=6, + sequence=1, + ) + self.make_picking_batch.stock_device_type_ids = device + batch = self.make_picking_batch._create_batch() + self.assertTrue(batch, "There should always be a batch") + + def test_batch_creation_move_over_the_limit_take_2nd_picking(self): + """Splitting is disabled, the first picking exceeding a limit is excluded.""" + self.pick3.action_cancel() + # Keep the picking 1 with one line and product 1 + move = self.pick1.move_ids + # And the weight of the move can not be accepted by the device + max_weight = 9 + move_weight = move.product_id.weight * move.product_qty + self.assertTrue(move_weight > max_weight) + # Get the picking to fit the limit + self.pick2.move_ids.product_id.weight = 0.2 + # Force recomputation after changing the product weight + self.pick2.move_ids._cal_move_weight() + self.make_picking_batch.write( + { + "maximum_number_of_preparation_lines": 2, + "split_picking_exceeding_limits": False, + } + ) + device = self._create_device( + "Device-A", + min_volume=0, + max_volume=0, + max_weight=max_weight, + nbr_bins=6, + sequence=1, + ) + self.make_picking_batch.stock_device_type_ids = device + batch = self.make_picking_batch._create_batch() + self.assertTrue(batch, "We should have a batch") + self.assertTrue(self.pick2 in batch.picking_ids) + self.assertTrue(batch.picking_ids, "We should have a picking in the batch") + + def test_batch_creation_splitting_by_number_of_lines(self): + """Test splitting a picking when the number of lines exceed the limit.""" + self.pick1.action_cancel() + self.pick2.action_cancel() + # Keep the picking 3 + self._add_product_to_picking(self.pick3, self.p3) + self._add_product_to_picking(self.pick3, self.p4) + # And now it has 4 lines + self.make_picking_batch.write({"maximum_number_of_preparation_lines": 2}) + self.make_picking_batch.write({"split_picking_exceeding_limits": True}) + device = self._create_device("Test", 0.0, 0.0, 0.0, 20, 1) + self.make_picking_batch.stock_device_type_ids = device + batch = self.make_picking_batch._create_batch() + self.assertFalse(self.pick3 in batch.picking_ids) + self.assertTrue(batch, "We should have a batch") + self.assertTrue(batch.picking_ids, "We should have a picking in the batch") + self.assertEqual(len(batch.picking_ids.move_line_ids), 2) diff --git a/stock_picking_batch_creation/tests/test_clustering_conditions.py b/stock_picking_batch_creation/tests/test_clustering_conditions.py index 73d2313e14e..cc4c0e293cf 100644 --- a/stock_picking_batch_creation/tests/test_clustering_conditions.py +++ b/stock_picking_batch_creation/tests/test_clustering_conditions.py @@ -1,7 +1,14 @@ # Copyright 2021 ACSONE SA/NV +# Copyright 2026 Jacques-Etienne Baudoux (BCIM) # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from ..exceptions import NoSuitableDeviceError, PickingCandidateNumberLineExceedError +from odoo.tests import Form, RecordCapturer + +from ..exceptions import ( + NoPickingCandidateError, + NoSuitableDeviceError, + PickingCandidateNumberLineExceedError, +) from .common import ClusterPickingCommonFeatures @@ -25,7 +32,74 @@ def test_device_with_one_bin(self): self.assertEqual(self.device3, batch.picking_device_id) self.assertEqual(self.pick3, batch.picking_ids) - def test_put_3_pickings_in_one_cluster(self): + def test_device_with_several_bins_volume_0_group_per_partner(self): + """ + Got a selected picking with volume == 0 + Enable grouping per partner + + Ensure we can create batch + """ + product = self._create_product("Without volume", 0.0, 0.0, 0.0, 0.0) + self._set_quantity_in_stock(self.stock_location, product) + self._create_picking_pick_and_assign(self.picking_type_1.id, 0, product) + device = self._create_device("Test", 0.0, 0.0, 200.0, 20, 1) + self.make_picking_batch.stock_device_type_ids = device + self.make_picking_batch.group_pickings_by_partner = True + self.make_picking_batch._create_batch() + + def test_put_3_pickings_in_one_cluster_no_limit(self): + """ + Data: 3 picks of type 1, total of 4 products for a volume of 60m3 + pick1: 1 line + pick2: 1 line + pick3: 3 lines + Test case: + Disable the maximum number of preparation lines + Create a device without any limit + Expected result: + The device without limit is used + The batch should contain pick3, pick2 and pick1 + """ + self._set_quantity_in_stock(self.stock_location, self.p5) + self.p1.write( + { + "volume": 5.0, + "product_length": 5, + "product_height": 1, + "product_width": 1, + "weight": 1, + } + ) + self.p2.write( + { + "volume": 5.0, + "product_length": 5, + "product_height": 1, + "product_width": 1, + "weight": 1, + } + ) + device = self.env["stock.device.type"].create( + { + "name": "test no limit device", + "min_volume": 0, + "max_volume": 0, + "max_weight": 0, + "nbr_bins": 6, + "sequence": 10, + } + ) + self.make_picking_batch.write( + { + "maximum_number_of_preparation_lines": 0, + "stock_device_type_ids": [(4, device.id)], + } + ) + batch = self.make_picking_batch._create_batch() + self.assertEqual(device, batch.picking_device_id) + self.assertEqual(self.pick3 | self.pick2 | self.pick1, batch.picking_ids) + + def test_put_3_pickings_in_one_cluster_max_lines(self): """ Data: 3 picks of type 1, total of 4 products for a volume of 60m3 pick1: 1 line @@ -432,7 +506,7 @@ def test_pickings_with_different_partners(self): batch2 = self.make_picking_batch._create_batch() self.assertEqual(self.pick1 | self.pick2, batch2.picking_ids) - def test_picking_with_maximum_number_of_lines_exceed(self): + def test_picking_split_with_maximum_number_of_lines_exceed(self): # pick 3 has 2 lines # create a batch picking with maximum number of lines = 1 self.pick1.action_cancel() @@ -441,12 +515,279 @@ def test_picking_with_maximum_number_of_lines_exceed(self): self.make_picking_batch.write( { "maximum_number_of_preparation_lines": 1, - "no_line_limit_if_no_candidate": False, + "split_picking_exceeding_limits": False, } ) with self.assertRaises(PickingCandidateNumberLineExceedError): self.make_picking_batch._create_batch(raise_if_not_possible=True) - self.make_picking_batch.no_line_limit_if_no_candidate = True - batch = self.make_picking_batch._create_batch() + self.make_picking_batch.split_picking_exceeding_limits = True + with RecordCapturer(self.env["stock.picking"], []) as rc: + batch = self.make_picking_batch._create_batch() + new_pickings = rc.records + self.assertEqual(new_pickings, batch.picking_ids) + self.assertEqual(len(batch.move_line_ids), 1) + + def test_device_with_one_bin_create_action(self): + """ + Data: 3 picks of type 1, total of 4 products for a volume of 60m3 + Test case: We have 3 devices possibles (device1, device2, device3), + ordered following sequence: device3, device2, device1. + The first picking will be pick3 (higher priority) and its volume is + is 30m3. -> device3 is the device to use (min 30m3, max 100m3) + + Device3 has 1 bin -> the batch should only contain pick3 + """ + batch_action = self.make_picking_batch.create_batch() + batch = self.env["stock.picking.batch"].browse(batch_action.get("res_id")) + self.assertEqual(self.device3, batch.picking_device_id) self.assertEqual(self.pick3, batch.picking_ids) - self.assertEqual(len(batch.move_line_ids), 2) + + def test_device_with_one_bin_create_action_no_picking(self): + """ + Cancel all pickings + + No picking candidate error should be raised + """ + self.picks.action_cancel() + with self.assertRaises(NoPickingCandidateError): + self.make_picking_batch._create_batch(raise_if_not_possible=True) + + def test_put_2_pickings_with_volume_in_one_cluster(self): + """2 products have a volume : + they should still occupy at least one bin each""" + device = self.env["stock.device.type"].create( + { + "name": "test volume devices", + "min_volume": 0, + "max_volume": 200, + "max_weight": 200, + "nbr_bins": 6, + "sequence": 50, + } + ) + make_picking_batch_volume_zero = self.makePickingBatch.create( + { + "user_id": self.env.user.id, + "picking_type_ids": [(4, self.picking_type_1.id)], + "stock_device_type_ids": [(4, device.id)], + "maximum_number_of_preparation_lines": 6, + } + ) + self.p1.write( + { + "product_length": 1, + "product_height": 1, + "product_width": 1, + "weight": 1, + } + ) + self.p2.write( + { + "product_length": 1, + "product_height": 2, + "product_width": 3, + "weight": 1, + } + ) + self.picks.mapped("move_ids")._compute_volume() + self.assertEqual(1.0, self.pick1.volume) + self.assertEqual(6.0, self.pick2.volume) + self.assertEqual(7.0, self.pick3.volume) + batch = make_picking_batch_volume_zero._create_batch() + self.assertEqual(device, batch.picking_device_id) + self.assertEqual(self.pick3 | self.pick2 | self.pick1, batch.picking_ids) + + # All picks have a volume of 0 : they should each occupy one bin + self.assertEqual(batch.batch_nbr_bins, 3) + + def test_changing_device_constraints(self): + device = self.env["stock.device.type"].create( + { + "name": "test volume devices", + "min_volume": 0, + "max_volume": 200, + "max_weight": 200, + "nbr_bins": 6, + "sequence": 50, + } + ) + self.assertEqual(device.user_max_volume, 200.0) + self.assertEqual(device.user_min_volume, 0.0) + self.assertEqual(device.user_max_weight, 200.0) + + device.write( + { + "min_volume": 100.0, + } + ) + self.assertEqual(device.user_min_volume, 100.0) + self.assertAlmostEqual(device.volume_per_bin, 33.33, places=2) + + # Test user interface + with Form(device) as device_form: + device_form.user_min_volume = 0.0 + device = device_form.save() + self.assertEqual(device.min_volume, 0.0) + + with Form(device) as device_form: + device_form.user_min_volume = 10.0 + device = device_form.save() + self.assertEqual(device.min_volume, 10.0) + + with Form(device) as device_form: + device_form.user_max_volume = 300.0 + device = device_form.save() + self.assertEqual(device.max_volume, 300.0) + + with Form(device) as device_form: + device_form.user_max_volume = 0.0 + device = device_form.save() + self.assertEqual(device.max_volume, 0.0) + + with Form(device) as device_form: + device_form.user_max_weight = 0.0 + device = device_form.save() + self.assertEqual(device.max_weight, 0.0) + + with Form(device) as device_form: + device_form.user_max_weight = 100.0 + device = device_form.save() + self.assertEqual(device.max_weight, 100.0) + + def test_picking_split_with_weight_exceed(self): + # pick 3 has 2 lines + # we will set a weight by line under the maximum weight of the device + # but the total weight of the picking will exceed the maximum weight of the device + # when the batch is created, the picking 3 should be split and the batch + # should contain only pick3 with 1 line + + self.pick1.action_cancel() + self.pick2.action_cancel() + self.assertEqual(len(self.pick3.move_line_ids), 2) + max_weight = 200 + device = self.env["stock.device.type"].create( + { + "name": "test volume null devices and one bin", + "min_volume": 0, + "max_volume": 200, + "max_weight": max_weight, + "nbr_bins": 1, + "sequence": 50, + } + ) + + self.make_picking_batch.write( + { + "split_picking_exceeding_limits": False, + "stock_device_type_ids": [(6, 0, [device.id])], + } + ) + self.pick3.move_ids.product_id.weight = max_weight - 1 + self.pick3.move_ids._cal_move_weight() + with self.assertRaises(NoSuitableDeviceError): + self.make_picking_batch._create_batch(raise_if_not_possible=True) + self.make_picking_batch.split_picking_exceeding_limits = True + with RecordCapturer(self.env["stock.picking"], []) as rc: + batch = self.make_picking_batch._create_batch() + new_pickings = rc.records + self.assertEqual(new_pickings, batch.picking_ids) + self.assertEqual(len(batch.move_line_ids), 1) + + def test_picking_split_with_volume_exceed(self): + # pick 3 has 2 lines + # we will set a volume by line under the maximum volume of the device + # but the total volume of the picking will exceed the maximum volume of the device + # when the batch is created, the picking 3 should be split and the batch + # should contain only pick3 with 1 line + + self.pick1.action_cancel() + self.pick2.action_cancel() + self.assertEqual(len(self.pick3.move_line_ids), 2) + + max_volume = 200 + device = self.env["stock.device.type"].create( + { + "name": "test volume null devices and one bin", + "min_volume": 0, + "max_volume": max_volume, + "max_weight": 300, + "nbr_bins": 1, + "sequence": 50, + } + ) + + self.make_picking_batch.write( + { + "split_picking_exceeding_limits": False, + "stock_device_type_ids": [(6, 0, [device.id])], + } + ) + # each product has a volume of 120 + self.pick3.move_ids.product_id.write( + { + "product_length": 12, + "product_height": 5, + "product_width": 2, + } + ) + self.pick3.move_ids._compute_volume() + with self.assertRaises(NoSuitableDeviceError): + self.make_picking_batch._create_batch(raise_if_not_possible=True) + self.make_picking_batch.split_picking_exceeding_limits = True + with RecordCapturer(self.env["stock.picking"], []) as rc: + batch = self.make_picking_batch._create_batch() + new_pickings = rc.records + self.assertEqual(new_pickings, batch.picking_ids) + self.assertEqual(len(batch.move_line_ids), 1) + + def test_picking_split_priority(self): + # We ensure than even if a picking with a higher priority has a volume + # exceeding the device capacity, it will be split and processed first + # if the split_picking_exceeding_limits is set to True + # the processing order for picks of type 1 will be: + # pick3 (priority), pick1 (lower id), pick2 + self.assertEqual(len(self.pick3.move_line_ids), 2) + + max_volume = 200 + device = self.env["stock.device.type"].create( + { + "name": "test volume null devices and one bin", + "min_volume": 0, + "max_volume": max_volume, + "max_weight": 300, + "nbr_bins": 1, + "sequence": 50, + } + ) + + self.make_picking_batch.write( + { + "split_picking_exceeding_limits": False, + "stock_device_type_ids": [(6, 0, [device.id])], + } + ) + # each product has a volume of 120 + self.pick3.move_ids.product_id.write( + { + "product_length": 12, + "product_height": 5, + "product_width": 2, + } + ) + self.pick3.move_ids._compute_volume() + + # since pick3 exceeds the device capacity and + # the split_picking_exceeding_limits is set to False + # the next picking to process should be pick1 + batch = self.make_picking_batch._create_batch() + self.assertEqual(self.pick1, batch.picking_ids) + + batch.unlink() + + # if the split_picking_exceeding_limits is set to True. + # then pick3 should be split and processed first + self.make_picking_batch.split_picking_exceeding_limits = True + with RecordCapturer(self.env["stock.picking"], []) as rc: + batch = self.make_picking_batch._create_batch() + new_pickings = rc.records + self.assertEqual(new_pickings, batch.picking_ids) diff --git a/stock_picking_batch_creation/tests/test_get_device_to_use.py b/stock_picking_batch_creation/tests/test_get_device_to_use.py index de18fc1c61e..8f0143b5fa6 100644 --- a/stock_picking_batch_creation/tests/test_get_device_to_use.py +++ b/stock_picking_batch_creation/tests/test_get_device_to_use.py @@ -1,6 +1,8 @@ # Copyright 2021 ACSONE SA/NV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import Command + from .common import ClusterPickingCommonFeatures @@ -65,6 +67,123 @@ def test_get_device_to_use_01(self): device = make_picking_batch._compute_device_to_use(first_picking) self.assertEqual(device, self.device2) + def test_get_device_to_use_no_matching_device(self): + """Use case: There's no device that can handle the picking + + Data: + We create a new product with a really big and heavy product in zone 1. + + Test case: + We have 3 devices possibles (device1, device2, device3), ordered following + sequence: device3, device2, device1. + + Expected Result: + No device can be used to handle it, so another picking is selected. + """ + product_big_1 = self._create_product("Unittest P1 big & heavy", 800, 80, 80, 80) + self._set_quantity_in_stock(self.stock_location, product_big_1) + self._add_product_to_picking(self.pick3, product_big_1) + + make_picking_batch = self.make_picking_batch.create( + { + "user_id": self.env.user.id, + "picking_type_ids": [ + Command.set(self.picking_type_1.ids), + ], + "stock_device_type_ids": [ + Command.link(self.device1.id), + Command.link(self.device2.id), + Command.link(self.device3.id), + ], + } + ) + + self.assertFalse( + make_picking_batch._compute_device_to_use(self.pick3), + "No device can hold this picking", + ) + + first_picking = make_picking_batch._get_first_picking() + self.assertEqual(first_picking, self.pick1) + + def test_get_device_to_use_without_max_volume(self): + """Use case: There's no max volume set, so it's unlimited + + Data: + We create a new product with a really big volume in zone 1. + We remove the max volume from device1. + + Test case: + We have 3 devices possibles (device1, device2, device3), ordered following + sequence: device3, device2, device1. + + Expected Result: + device1 should be the device to use since it's the one with no max volume, + effectively unlimited. + """ + self.device1.max_volume = 0 # Effectively unlimited + product_big_1 = self._create_product("Unittest P1 voluminous", 10, 800, 1, 1) + self._set_quantity_in_stock(self.stock_location, product_big_1) + self._add_product_to_picking(self.pick3, product_big_1) + + make_picking_batch = self.make_picking_batch.create( + { + "user_id": self.env.user.id, + "picking_type_ids": [ + Command.set(self.picking_type_1.ids), + ], + "stock_device_type_ids": [ + Command.link(self.device1.id), + Command.link(self.device2.id), + Command.link(self.device3.id), + ], + } + ) + first_picking = make_picking_batch._get_first_picking() + self.assertEqual(first_picking, self.pick3) + device = make_picking_batch._compute_device_to_use(first_picking) + self.assertEqual(device, self.device1) + + def test_get_device_to_use_without_max_weight(self): + """Use case: There's no max weight set, so it's unlimited + + Data: + We create a new product with a really big weight in zone 1. + We remove the max weight from device2. + We remove the min volume from device2 to not interfere (previously 70). + + Test case: + We have 3 devices possibles (device1, device2, device3), ordered following + sequence: device3, device2, device1. + + Expected Result: + device2 should be the device to use since it's the one with no max weight, + effectively unlimited. + """ + self.device2.max_weight = 0 # Effectively unlimited + self.device2.min_volume = 0 # No min_volume to not interfere + product_big_1 = self._create_product("Unittest P1 heavy", 800, 1, 1, 1) + self._set_quantity_in_stock(self.stock_location, product_big_1) + self._add_product_to_picking(self.pick3, product_big_1) + + make_picking_batch = self.make_picking_batch.create( + { + "user_id": self.env.user.id, + "picking_type_ids": [ + Command.set(self.picking_type_1.ids), + ], + "stock_device_type_ids": [ + Command.link(self.device1.id), + Command.link(self.device2.id), + Command.link(self.device3.id), + ], + } + ) + first_picking = make_picking_batch._get_first_picking() + self.assertEqual(first_picking, self.pick3) + device = make_picking_batch._compute_device_to_use(first_picking) + self.assertEqual(device, self.device2) + def test_get_device_to_use_filter_pickings(self): """ Data: we create 1 new product with big volume in zone 1 @@ -94,3 +213,67 @@ def test_get_device_to_use_filter_pickings(self): self.assertEqual(first_picking, self.pick1) device = make_picking_batch._compute_device_to_use(first_picking) self.assertEqual(device, self.device1) + + def test_device_used_for_first_picking_splitting_00(self): + """Check the last device is used for splitting the first picking. + + Default order is :: device3, device2, device1. + The last device (device1) can not manage the only picking. + So the picking will be split. + + """ + # Keep only one picking and a heavy one + self.pick1.action_cancel() + self.pick2.action_cancel() + product_big_1 = self._create_product("Unittest P1 voluminous", 10, 100, 1, 1) + self._set_quantity_in_stock(self.stock_location, product_big_1) + self._add_product_to_picking(self.pick3, product_big_1) + make_picking_batch = self.make_picking_batch.create( + { + "user_id": self.env.user.id, + "picking_type_ids": [(4, self.picking_type_1.id)], + "split_picking_exceeding_limits": True, + # Add the device not in their default sort order + "stock_device_type_ids": [ + (4, self.device1.id), + (4, self.device2.id), + (4, self.device3.id), + ], + } + ) + first_picking = make_picking_batch._get_first_picking() + # A split picking has been created + self.assertTrue(first_picking != self.pick3) + + def test_device_used_for_first_picking_splitting_01(self): + """Check the last device is used for splitting the first picking. + + The last device (device2) can manage the only picking. + So picking will not be split. + + """ + # Keep only one picking and a heavy one. + self.pick1.action_cancel() + self.pick2.action_cancel() + product_big_1 = self._create_product("Unittest P1 voluminous", 10, 100, 1, 1) + self._set_quantity_in_stock(self.stock_location, product_big_1) + self._add_product_to_picking(self.pick3, product_big_1) + # Set the device order + self.device1.sequence = 10 + self.device3.sequence = 20 + self.device2.sequence = 30 + make_picking_batch = self.make_picking_batch.create( + { + "user_id": self.env.user.id, + "picking_type_ids": [(4, self.picking_type_1.id)], + "split_picking_exceeding_limits": True, + # Add the device not in their default sort order + "stock_device_type_ids": [ + (4, self.device1.id), + (4, self.device2.id), + (4, self.device3.id), + ], + } + ) + first_picking = make_picking_batch._get_first_picking() + self.assertEqual(first_picking, self.pick3) diff --git a/stock_picking_batch_creation/views/stock_device_type.xml b/stock_picking_batch_creation/views/stock_device_type.xml index a3207f9dd59..c53c8765e98 100644 --- a/stock_picking_batch_creation/views/stock_device_type.xml +++ b/stock_picking_batch_creation/views/stock_device_type.xml @@ -15,6 +15,7 @@ + @@ -55,6 +56,7 @@
    + diff --git a/stock_picking_batch_creation/views/stock_picking.xml b/stock_picking_batch_creation/views/stock_picking.xml index aa2bf1711ea..19931cb7474 100644 --- a/stock_picking_batch_creation/views/stock_picking.xml +++ b/stock_picking_batch_creation/views/stock_picking.xml @@ -24,8 +24,8 @@ - - + + diff --git a/stock_picking_batch_creation/wizards/make_picking_batch.py b/stock_picking_batch_creation/wizards/make_picking_batch.py index 9c4e4455589..b429203997e 100644 --- a/stock_picking_batch_creation/wizards/make_picking_batch.py +++ b/stock_picking_batch_creation/wizards/make_picking_batch.py @@ -1,6 +1,8 @@ # Copyright 2021 ACSONE SA/NV +# Copyright 2026 Jacques-Etienne Baudoux (BCIM) # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import logging import math import threading from collections import defaultdict @@ -15,6 +17,8 @@ PickingCandidateNumberLineExceedError, ) +_logger = logging.getLogger(__name__) + class MakePickingBatch(models.TransientModel): @@ -42,7 +46,8 @@ class MakePickingBatch(models.TransientModel): ) maximum_number_of_preparation_lines = fields.Integer( default=20, - string="Maximum number of preparation lines for the batch", + string="Maximum number of preparation lines for the batch.", + help="Set to 0 to disable.", required=True, ) group_pickings_by_partner = fields.Boolean( @@ -53,7 +58,8 @@ class MakePickingBatch(models.TransientModel): restrict_to_same_priority = fields.Boolean( default=False, string="Restrict to the same priority", - help="Only the pickings with the same priority will be selected for this batch.", + help="Only the pickings with the same priority will be selected " + "for this batch.", ) restrict_to_same_partner = fields.Boolean( default=False, @@ -80,16 +86,14 @@ class MakePickingBatch(models.TransientModel): "by default.", ) - no_line_limit_if_no_candidate = fields.Boolean( - default=True, - string="No line limit if no candidate", - help="If checked, the maximum number of lines will not be applied if there is " - "no candidate to add to the batch with a number of lines less than the maximum " - "number of lines. This option is useful if you want relax the maximum number " - "of lines to allow to create a batch even if there is no candidate to add to " - "the batch at first. This will avoid to manually create a batch with a single " - "picking for the sole case where a device is suitable for the picking but the " - "picking has more lines than the maximum number of lines.", + split_picking_exceeding_limits = fields.Boolean( + default=False, + string="Split pickings exceeding limits", + help="If checked, the pickings exceeding the maximum number of lines, " + "volume or weight of available devices will be split into multiple pickings " + "to respect the limits. If unchecked, the pickings exceeding the limits will not " + "be added to the batch. The limits are defined by the limits of the last available " + "devices.", ) __slots__ = ( @@ -117,11 +121,17 @@ def _get_default_picking_locking_mode(self): return None return "sql_for_update_skip_locked" - def create_batch(self): + def create_batch(self) -> dict: self.ensure_one() try: batch = self._create_batch(raise_if_not_possible=True) - except (NoPickingCandidateError, NoSuitableDeviceError) as error: + except UserError as error: + # We catch specific batch picking creation errors to display + # them as user errors in the UI. They are declared as + # subclass of UserError to be able to catch them + # as UserError and display the message in the UI + # but also to be able to catch them specifically into + # the tests raise UserError(error.name) from error action = { "type": "ir.actions.act_window", @@ -136,8 +146,8 @@ def create_batch(self): def _reset_counters(self): self._volume_by_partners = defaultdict(lambda: 0) self._device = None - self._remaining_weight = 0 - self._remaining_nbr_picking_lines = 0 + self._remaining_weight = None # None means unlimited + self._remaining_nbr_picking_lines = None # None means unlimited self._selected_picking_ids = [] self._first_picking = None self._remaining_nbr_bins = None @@ -152,7 +162,10 @@ def _get_picking_order_by(self): # https://www.postgresql.org/docs/current/queries-order.html # so we need to sort user_id asc to have NULLS LAST if self.group_pickings_by_partner: - return "user_id asc, priority desc, scheduled_date asc, partner_id desc, id asc" + return ( + "user_id asc, priority desc, scheduled_date asc, " + "partner_id desc, id asc" + ) return "user_id asc, priority desc, scheduled_date asc, id asc" def _get_picking_domain_common(self): @@ -169,7 +182,7 @@ def _get_picking_domain_for_first(self, no_nbr_lines_limit=False): picking_domain_first = [ ("picking_type_id", "in", self.picking_type_ids.ids), ] - if apply_limit_on_nbr_lines: + if apply_limit_on_nbr_lines and self.maximum_number_of_preparation_lines: picking_domain_first.append( ( "nbr_picking_lines", @@ -180,11 +193,14 @@ def _get_picking_domain_for_first(self, no_nbr_lines_limit=False): return AND([picking_domain_common, picking_domain_first]) def _get_picking_domain_for_device(self, device): - return [ - ("volume", ">=", device.min_volume), - ("volume", "<=", device.max_volume), - ("weight", "<=", device.max_weight), - ] + domain = [] + if device.min_volume: + domain.append(("volume", ">=", device.min_volume)) + if device.max_volume: + domain.append(("volume", "<=", device.max_volume)) + if device.max_weight: + domain.append(("weight", "<=", device.max_weight)) + return domain def _get_picking_domain_for_additional(self): """Provides the domain expressing the additional constraints to apply to @@ -192,44 +208,47 @@ def _get_picking_domain_for_additional(self): """ excluded_ids = self._selected_picking_ids domain = [ - ( - "nbr_picking_lines", - "<=", - self._remaining_nbr_picking_lines, - ), ("id", "not in", excluded_ids), - ("weight", "<=", self._remaining_weight), ("picking_type_id", "=", self._first_picking.picking_type_id.id), ] + if self._remaining_nbr_picking_lines is not None: # None means unlimited + domain.append( + ("nbr_picking_lines", "<=", self._remaining_nbr_picking_lines) + ) + if self._remaining_weight is not None: # None means unlimited + domain.append(("weight", "<=", self._remaining_weight)) previous_picking = self._previous_selected_picking if self.restrict_to_same_priority: domain.append(("priority", "=", previous_picking.priority)) if self.restrict_to_same_partner: domain.append(("partner_id", "=", previous_picking.partner_id.id)) - volume_domains = [ - [ - ("volume", "<=", self._get_remaining_volume()), - ] - ] - if self.group_pickings_by_partner: - # in case of grouping by partner, we allow to group picking into - # the same bins. That means that the volume available for the - # partner does not depend on the volume of remaining bins only - # but also on the remaining volume into the bins already used by - # the partner. Since results are sorted by partner, the search - # takes as partner the partner of the previous picking. - previous_partner = previous_picking.partner_id - volume_domains.append( + remaining_volume = self._get_remaining_volume() + if remaining_volume is not None: # None means unlimited + volume_domains = [ [ - ("partner_id", "=", previous_partner.id), - ( - "volume", - "<=", - self._get_remaining_volume(previous_partner), - ), + ("volume", "<=", remaining_volume), ] - ) - return AND([domain, OR(volume_domains)]) + ] + if self.group_pickings_by_partner: + # in case of grouping by partner, we allow to group picking into + # the same bins. That means that the volume available for the + # partner does not depend on the volume of remaining bins only + # but also on the remaining volume into the bins already used by + # the partner. Since results are sorted by partner, the search + # takes as partner the partner of the previous picking. + previous_partner = previous_picking.partner_id + volume_domains.append( + [ + ("partner_id", "=", previous_partner.id), + ( + "volume", + "<=", + self._get_remaining_volume(previous_partner), + ), + ] + ) + domain = AND([domain, OR(volume_domains)]) + return domain def _execute_search_pickings(self, domain, limit=None): """Hook to allow to override the search of pickings @@ -260,15 +279,82 @@ def _execute_search_pickings(self, domain, limit=None): domain, order=self._get_picking_order_by(), limit=limit ) - def _get_first_picking(self, no_nbr_lines_limit=False): - domain = self._get_picking_domain_for_first( - no_nbr_lines_limit=no_nbr_lines_limit - ) - device_domains = [] - for device in self.stock_device_type_ids: - device_domains.append(self._get_picking_domain_for_device(device)) - domain = AND([domain, OR(device_domains)]) - return self._execute_search_pickings(domain, limit=1) + def _split_first_picking_for_limit(self, picking): + last_device = self._get_sorted_devices()[-1] + if last_device.split_mode == "dimension": + return ( + self.env["stock.split.picking"] + .with_context(active_ids=picking.ids) + .create( + { + "mode": "dimensions", + "max_nbr_lines": self.maximum_number_of_preparation_lines, + "max_volume": last_device.max_volume, + "max_weight": last_device.max_weight, + } + ) + ._action_apply() + ) + + def _is_picking_exceeding_limits(self, picking): + """Check if the picking exceeds the limits of the available devices. + + :param picking: the picking to check + """ + # First check the number of lines + if ( + self.maximum_number_of_preparation_lines + and picking.nbr_picking_lines > self.maximum_number_of_preparation_lines + ): + return True + # Then, check the device limits + last_device = self._get_sorted_devices()[-1] + if last_device.split_mode == "dimension": + if last_device.max_volume and picking.volume > last_device.max_volume: + return True + if last_device.max_weight and picking.weight > last_device.max_weight: + return True + return False + + def _get_first_picking(self, raise_if_not_found=False): + """Get the first picking to add to the batch. + + If the split_picking_exceeding_limits is set, we try to find the first picking + without taking into account the limit on the number of lines and we split it + if it exceeds the limits. If the split is not possible, we raise an error. + + Otherwise, we try to find the first picking taking into account the limit on the + number of lines. + """ + no_limit = self.split_picking_exceeding_limits + domain = self._get_picking_domain_for_first(no_nbr_lines_limit=no_limit) + if not no_limit: + device_domains = [] + for device in self.stock_device_type_ids: + device_domains.append(self._get_picking_domain_for_device(device)) + domain = AND([domain, OR(device_domains)]) + picking = self._execute_search_pickings(domain, limit=1) + if not picking and raise_if_not_found: + self._raise_create_batch_not_possible() + return picking + pickings = self._execute_search_pickings(domain) + # at this stage we have the first picking to add to the batch but it could + # exceed the limits of the available devices. In this case we split the + # picking and return the picking to add to the batch. The split is done only + # if the split_picking_exceeding_limits is set to True. + selected_picking = self.env["stock.picking"] + for picking in pickings: + if self._is_picking_exceeding_limits(picking): + split_picking = self._split_first_picking_for_limit(picking) + if not split_picking: + # If the picking has only one move, it won't be split + selected_picking = picking + else: + selected_picking = split_picking + else: + selected_picking = picking + break + return selected_picking def _get_additional_picking(self): """Get the next picking to add to the batch.""" @@ -283,6 +369,8 @@ def _get_remaining_volume(self, partner=False): :param partner: if set, the remaining volume will add to the volume available if free bins the volume remaining in the bins already used by the partner """ + if not self._device.volume_per_bin: + return None remaining_volume = self._remaining_nbr_bins * self._device.volume_per_bin if partner: # for a partner we must take into account the remaining volume in @@ -299,23 +387,17 @@ def _get_remaining_volume(self, partner=False): return remaining_volume def _compute_device_to_use(self, picking): - available_devices = self.stock_device_type_ids.sorted(lambda d: d.sequence) - for device in available_devices: - if ( - self._volume_condition_for_device_choice( - device.min_volume, - picking.volume, - device.max_volume, - ) - and tools.float_compare( - device.max_weight, - picking.weight, - precision_digits=self._precision_volume(), - ) - > 0 - ): + for device in self._get_sorted_devices(): + if picking.filtered_domain(self._get_picking_domain_for_device(device)): return device - return None + return self.env["stock.device.type"] + + def _get_sorted_devices(self): + """Return the devices sorted in their default order. + + Because it will not be done by default with the Many2many + """ + return self.stock_device_type_ids.sorted() def _volume_condition_for_device_choice( self, min_volume, picking_volume, max_volume @@ -334,13 +416,17 @@ def _raise_create_batch_not_possible(self): # constrains. If not, we raise an error to inform the user that there # is no picking to process otherwise we raise an error to inform the # user that there is not suitable device to process the pickings. - if not self.no_line_limit_if_no_candidate: - domain = self._get_picking_domain_for_first(no_nbr_lines_limit=True) - candidates = self.env["stock.picking"].search(domain, limit=1) - if candidates: - raise PickingCandidateNumberLineExceedError( - candidates, self.maximum_number_of_preparation_lines - ) + domain = self._get_picking_domain_for_first(no_nbr_lines_limit=True) + device_domains = [] + for device in self.stock_device_type_ids: + device_domains.append(self._get_picking_domain_for_device(device)) + domain = AND([domain, OR(device_domains)]) + candidates = self.env["stock.picking"].search(domain, limit=1) + if candidates: + raise PickingCandidateNumberLineExceedError( + candidates, self.maximum_number_of_preparation_lines + ) + domain = self._get_picking_domain_for_first() limit = 1 if self.add_picking_list_in_error: @@ -348,33 +434,31 @@ def _raise_create_batch_not_possible(self): candidates = self.env["stock.picking"].search(domain, limit=limit) if candidates: pickings = candidates if self.add_picking_list_in_error else None - raise NoSuitableDeviceError(pickings=pickings) - raise NoPickingCandidateError() + raise NoSuitableDeviceError(self.env, pickings=pickings) + raise NoPickingCandidateError(self.env) def _create_batch(self, raise_if_not_possible=False): """Create a batch transfer.""" self._reset_counters() # first we try to get the first picking for the user - first_picking = self._get_first_picking() - if not first_picking and self.no_line_limit_if_no_candidate: - first_picking = self._get_first_picking(no_nbr_lines_limit=True) + first_picking = self._get_first_picking( + raise_if_not_found=raise_if_not_possible + ) if not first_picking: - if raise_if_not_possible: - self._raise_create_batch_not_possible() return self.env["stock.picking.batch"].browse() device = self._compute_device_to_use(first_picking) + if not device: + # A picking has been elected. If no device is suitable, use the + # last device. This can happen when the picking still exceeds the + # limits. Then the best device to use is the last done (that should + # be the biggest one). + device = self._get_sorted_devices()[-1] self._init_counters(first_picking, device) self._apply_limits() vals = self._create_batch_values() batch = self.env["stock.picking.batch"].create(vals) return batch - def _precision_volume(self): - return max( - 6, - self.env["decimal.precision"].precision_get("Product Unit of Measure") * 2, - ) - def _init_counters(self, first_picking, device): """Initialize the counters used to compute the batch. This method is called at the beginning of the batch creation. It allows @@ -385,9 +469,19 @@ def _init_counters(self, first_picking, device): :param device: the device to use to prepare the batch """ self._device = device - self._remaining_weight = device.max_weight - first_picking.weight + self._remaining_weight = ( + max(device.max_weight - first_picking.weight, 0) + if device.max_weight + else None # None means unlimited + ) self._remaining_nbr_picking_lines = ( - self.maximum_number_of_preparation_lines - first_picking.nbr_picking_lines + max( + self.maximum_number_of_preparation_lines + - first_picking.nbr_picking_lines, + 0, + ) + if self.maximum_number_of_preparation_lines + else None # None means unlimited ) self._selected_picking_ids = [first_picking.id] self._first_picking = first_picking @@ -417,6 +511,9 @@ def _get_nbr_bins_for_picking(self, picking): # of the device by convention a picking without volume fill a complete # bin picking_volume = self._device.volume_per_bin + if not self._device.volume_per_bin: + # We should return current result to avoid division per 0 + return nbr_bins old_volume = self._volume_by_partners[picking.partner_id] new_volume = picking_volume + old_volume nbr_bins = math.ceil(new_volume / self._device.volume_per_bin) - math.ceil( @@ -433,8 +530,10 @@ def _add_picking(self, picking): :param picking: picking to add to the batch """ self._selected_picking_ids.append(picking.id) - self._remaining_weight -= picking.weight - self._remaining_nbr_picking_lines -= picking.nbr_picking_lines + if self._remaining_weight is not None: + self._remaining_weight -= picking.weight + if self._remaining_nbr_picking_lines is not None: + self._remaining_nbr_picking_lines -= picking.nbr_picking_lines nbr_bins = self._get_nbr_bins_for_picking(picking) self._remaining_nbr_bins -= nbr_bins self._previous_selected_picking = picking diff --git a/stock_picking_batch_creation/wizards/make_picking_batch.xml b/stock_picking_batch_creation/wizards/make_picking_batch.xml index c28c1fb1977..6cd89624065 100644 --- a/stock_picking_batch_creation/wizards/make_picking_batch.xml +++ b/stock_picking_batch_creation/wizards/make_picking_batch.xml @@ -10,11 +10,10 @@
    - + - diff --git a/stock_picking_completion_info/i18n/de.po b/stock_picking_completion_info/i18n/de.po new file mode 100644 index 00000000000..c3d4afbe803 --- /dev/null +++ b/stock_picking_completion_info/i18n/de.po @@ -0,0 +1,70 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * stock_picking_completion_info +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#. module: stock_picking_completion_info +#: model:ir.model.fields,field_description:stock_picking_completion_info.field_stock_picking__completion_info +msgid "Completion Info" +msgstr "" + +#. module: stock_picking_completion_info +#: model:ir.model.fields,field_description:stock_picking_completion_info.field_stock_picking_type__display_completion_info +msgid "Display Completion Info" +msgstr "" + +#. module: stock_picking_completion_info +#: model:ir.model.fields.selection,name:stock_picking_completion_info.selection__stock_picking__completion_info__full_order_picking +msgid "" +"Full order picking: You are processing a full order picking that will allow " +"next operation to be processed" +msgstr "" + +#. module: stock_picking_completion_info +#: model:ir.model.fields,help:stock_picking_completion_info.field_stock_picking_type__display_completion_info +msgid "" +"Inform operator of a completed operation at processing and at completion" +msgstr "" + +#. module: stock_picking_completion_info +#: model:ir.model.fields.selection,name:stock_picking_completion_info.selection__stock_picking__completion_info__last_picking +msgid "" +"Last picking: Completion of this operation allows next operations to be " +"processed." +msgstr "" + +#. module: stock_picking_completion_info +#: model:ir.model.fields.selection,name:stock_picking_completion_info.selection__stock_picking__completion_info__next_picking_ready +msgid "Next operations are ready to be processed." +msgstr "" + +#. module: stock_picking_completion_info +#: model:ir.model.fields.selection,name:stock_picking_completion_info.selection__stock_picking__completion_info__no +msgid "No" +msgstr "" + +#. module: stock_picking_completion_info +#: model:ir.model,name:stock_picking_completion_info.model_stock_picking_type +msgid "Picking Type" +msgstr "" + +#. module: stock_picking_completion_info +#: model:ir.model,name:stock_picking_completion_info.model_stock_move +msgid "Stock Move" +msgstr "" + +#. module: stock_picking_completion_info +#: model:ir.model,name:stock_picking_completion_info.model_stock_picking +msgid "Transfer" +msgstr "" diff --git a/stock_release_channel/README.rst b/stock_release_channel/README.rst index 26a753f6929..4af06749de6 100644 --- a/stock_release_channel/README.rst +++ b/stock_release_channel/README.rst @@ -1,3 +1,7 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + ====================== Stock Release Channels ====================== @@ -7,13 +11,13 @@ Stock Release Channels !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:ac44a0ebd239ea0288201d0ee262ca120ffd246b3ac82a260ad0a0d6d18a9edc + !! source digest: sha256:1f36dd380861d1ccbe9a15b44cc304825d014e2aa40b38296ed78b623940726e !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png :target: https://odoo-community.org/page/development-status :alt: Beta -.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png +.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fwms-lightgray.png?logo=github diff --git a/stock_release_channel/__manifest__.py b/stock_release_channel/__manifest__.py index 550385da1f1..9ef5c166d5b 100644 --- a/stock_release_channel/__manifest__.py +++ b/stock_release_channel/__manifest__.py @@ -5,7 +5,7 @@ { "name": "Stock Release Channels", "summary": "Manage workload in WMS with release channels", - "version": "16.0.2.18.4", + "version": "16.0.3.1.1", "development_status": "Beta", "license": "AGPL-3", "author": "Camptocamp, BCIM, ACSONE SA/NV, Odoo Community Association (OCA)", @@ -17,13 +17,13 @@ "queue_job", # OCA/queue ], "data": [ + "security/stock_release_channel.xml", "views/res_partner.xml", "views/stock_release_channel_views.xml", "views/stock_picking_views.xml", "views/res_config_settings.xml", "data/queue_job_data.xml", "data/ir_cron_data.xml", - "security/stock_release_channel.xml", ], "demo": [ "demo/stock_release_channel.xml", diff --git a/stock_release_channel/models/res_partner.py b/stock_release_channel/models/res_partner.py index cd3f114ed6b..e7bd444c1e7 100644 --- a/stock_release_channel/models/res_partner.py +++ b/stock_release_channel/models/res_partner.py @@ -1,4 +1,5 @@ # Copyright 2023 ACSONE SA/NV +# Copyright 2025 Jacques-Etienne Baudoux (BCIM) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from odoo import fields, models @@ -16,3 +17,13 @@ class ResPartner(models.Model): string="Release Channels", domain="company_id and [('company_id', '=', company_id)] or []", ) + + @property + def _release_channel_possible_candidate_domain(self): + """Domain fo finding channel candidates based on partner""" + self.ensure_one() + return [ + "|", + ("partner_ids", "=", False), + ("partner_ids", "in", self.id), + ] diff --git a/stock_release_channel/models/stock_picking.py b/stock_release_channel/models/stock_picking.py index 516c99b9546..b774533e6e2 100644 --- a/stock_release_channel/models/stock_picking.py +++ b/stock_release_channel/models/stock_picking.py @@ -1,4 +1,5 @@ # Copyright 2020 Camptocamp +# Copyright 2024 Jacques-Etienne Baudoux (BCIM) # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) from odoo import _, exceptions, fields, models @@ -103,48 +104,73 @@ def _find_release_channel_possible_candidate(self): self.ensure_one() return ( self.env["stock.release.channel"] - .search(self._get_release_channel_possible_candidate_domain()) + .search(self._release_channel_possible_candidate_domain) .sorted(key=lambda r: (not bool(r.partner_ids), r.sequence)) ) - def _get_release_channel_possible_candidate_domain_channel(self): - return [ + @property + def _release_channel_possible_candidate_domain(self): + """Domain for finding channel candidates""" + self.ensure_one() + domain_base = self._release_channel_possible_candidate_domain_base + domain = [ ("is_manual_assignment", "=", False), - ("state", "!=", "asleep"), + ("state", "in", ("open", "locked")), + "|", + ("picking_type_ids", "=", False), + ("picking_type_ids", "in", self.picking_type_id.ids), ] + domain_partner = ( + self.partner_id._release_channel_possible_candidate_domain + if self.partner_id + else [] + ) + domain_extras = [] + if self._release_channel_possible_candidate_domain_apply_extras: + domain_extras = self._release_channel_possible_candidate_domain_extras + domain = expression.AND([domain, domain_base, domain_partner] + domain_extras) + return domain + + @property + def _release_channel_possible_candidate_domain_base(self): + """Base domain for finding channel candidates based on picking. + + This is the base domain we always want to apply. - def _get_release_channel_possible_candidate_domain_picking(self): + This is used by stock_release_channel_partner_by_date where you + can force a channel for a partner on a specific day. This domain is + used to check if there is a specific channel defined for the warehouse + (and carrier with delivery module). + """ + # when a warehouse is defined on the channel, it must always match + # otherwise fallback on the picking type return [ ("company_id", "=", self.company_id.id), "|", + "&", + ("warehouse_id", "=", False), + "|", ("picking_type_ids", "=", False), ("picking_type_ids", "in", self.picking_type_id.ids), - "|", - ("warehouse_id", "=", False), ("warehouse_id", "=", self.picking_type_id.warehouse_id.id), ] - def _get_release_channel_possible_candidate_domain_partner(self): - return [ - "|", - ("partner_ids", "=", False), - ("partner_ids", "in", self.partner_id.ids), - ] + @property + def _release_channel_possible_candidate_domain_extras(self): + """Additional domains for finding channel candidates based on picking. - def _inject_possible_candidate_domain_partner(self): - """Hooks that could be overridden. + Allow extension modules to add domain rules. Each module can add a + domain to the list. - Return a boolean. + Those domains won't be used by stock_release_channel_partner_by_date + where you can force a channel for a partner on a specific day. """ - return True + return [] - def _get_release_channel_possible_candidate_domain(self): - self.ensure_one() - domain_channel = self._get_release_channel_possible_candidate_domain_channel() - domain_picking = self._get_release_channel_possible_candidate_domain_picking() - domain = expression.AND([domain_channel, domain_picking]) - if self._inject_possible_candidate_domain_partner(): - domain = expression.AND( - [domain, self._get_release_channel_possible_candidate_domain_partner()] - ) - return domain + @property + def _release_channel_possible_candidate_domain_apply_extras(self): + """Extra domains can be discarded. + + For example, when there is an SO commitment date. + """ + return True diff --git a/stock_release_channel/models/stock_release_channel.py b/stock_release_channel/models/stock_release_channel.py index ea932fc5082..938db4d9187 100644 --- a/stock_release_channel/models/stock_release_channel.py +++ b/stock_release_channel/models/stock_release_channel.py @@ -8,7 +8,7 @@ from copy import deepcopy from operator import itemgetter -from pytz import timezone +import pytz from odoo import _, api, exceptions, fields, models from odoo.osv.expression import NEGATIVE_TERM_OPERATORS @@ -617,7 +617,7 @@ def _eval_context(self, pickings): "time": safe_time, "datetime": safe_datetime, "dateutil": safe_dateutil, - "timezone": timezone, + "timezone": pytz.timezone, # orm "env": self.env, # record @@ -900,3 +900,82 @@ def _get_expected_date(self): """Return the new date to set on move chain""" self.ensure_one() return False + + def _localize(self, dt, tz=None): + """Localize a datetime + + Use the given tz or use the tz of the warehouse + """ + wh_tz = pytz.timezone(tz or self.warehouse_id.partner_id.tz or "UTC") + dt_tz = dt.astimezone(pytz.utc).astimezone(wh_tz) + return dt_tz + + @api.model + def _naive(self, dt_tz, reset_time=False): + """Convert a datetime as naive datetime + + Allow to reset time + """ + if reset_time: + dt_tz = dt_tz.replace(hour=0, minute=0, second=0, microsecond=0) + dt = dt_tz.astimezone(pytz.utc).replace(tzinfo=None) + return dt + + @property + def _delivery_date_steps(self): + """Returns the steps to compute the delivery date.""" + return ["preparation", "delivery", "customer"] + + @property + def _delivery_date_generators(self): + """Returns generators to compute delivery date. + + Meant to be extended by modules to register a generator. + Returns a dict where: + - the key must be part of _delivery_date_steps. + - the value is a list of generators + """ + return defaultdict(list) + + def _get_earliest_delivery_date(self, partner, order_dt): + """Compute the earliest delivery date for this channel + + Go through each steps. All generators of a step must agree on a date. + Initialize them with the provided start date for the first step and + then with the agreed date from the previous step. If a generator + provides a later date, send that date to the other generators to + request agreement or a new later date. + This algorithm performs a quick convergence to a date. + """ + self.ensure_one() + best_dt = order_dt + for step in self._delivery_date_steps: + funcs = self._delivery_date_generators.get(step) + if not funcs: + continue + generators = [] + best_generators = [] + start_dt = best_dt + for func in funcs: + # initialize generators with the start date + gen = func(start_dt, partner) + generators.append(gen) + new_dt = next(gen) + if new_dt > best_dt: + best_dt = new_dt + best_generators = [gen] + elif new_dt == best_dt: + best_generators.append(gen) + # loop until all generators return the same last date + while len(generators) != len(best_generators): + for gen in generators: + if gen in best_generators: + continue + best_dt = gen.send(previous_dt := best_dt) + if best_dt != previous_dt: + best_generators = [gen] + else: + best_generators.append(gen) + for gen in generators: + gen.close() + return best_dt diff --git a/stock_release_channel/security/stock_release_channel.xml b/stock_release_channel/security/stock_release_channel.xml index 991948df60b..86dc13b2ca9 100644 --- a/stock_release_channel/security/stock_release_channel.xml +++ b/stock_release_channel/security/stock_release_channel.xml @@ -31,4 +31,10 @@ + + Stock Release Channel multi-company + + [('company_id', 'in', company_ids)] + + diff --git a/stock_release_channel/static/description/index.html b/stock_release_channel/static/description/index.html index 8aaa6ce21d4..a524dc9d5c1 100644 --- a/stock_release_channel/static/description/index.html +++ b/stock_release_channel/static/description/index.html @@ -3,7 +3,7 @@ -Stock Release Channels +README.rst -
    -

    Stock Release Channels

    +
    + + +Odoo Community Association + +
    +

    Stock Release Channels

    -

    Beta License: AGPL-3 OCA/wms Translate me on Weblate Try me on Runboat

    +

    Beta License: AGPL-3 OCA/wms Translate me on Weblate Try me on Runboat

    Release channels are:

    • Release channels are created by stock managers (only pallets, only parcels, …)
    • @@ -403,12 +408,12 @@

      Stock Release Channels

    -

    Configuration

    +

    Configuration

    In Inventory > Configuration > Release Channels. Only Stock Managers have write permissions.

    -

    Usage

    +

    Usage

    Use Inventory > Operations > Release Channels to access to the dashboard.

    Each channel has a dashboard with statistics about the number of transfers to release and of the progress of the released transfers.

    @@ -438,7 +443,7 @@

    Usage

    “Open” or “Locked” state.

    -

    Bug Tracker

    +

    Bug Tracker

    Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed @@ -446,9 +451,9 @@

    Bug Tracker

    Do not contact contributors directly about support or help with technical issues.

    -

    Credits

    +

    Credits

    -

    Authors

    +

    Authors

    • Camptocamp
    • BCIM
    • @@ -456,7 +461,7 @@

      Authors

    -

    Contributors

    +

    Contributors

    -

    Design

    +

    Design

    -

    Other credits

    +

    Other credits

    Financial support

    -

    Maintainers

    +

    Maintainers

    This module is maintained by the OCA.

    Odoo Community Association @@ -497,5 +502,6 @@

    Maintainers

    +
    diff --git a/stock_release_channel/tests/__init__.py b/stock_release_channel/tests/__init__.py index ec79759c330..087001bae25 100644 --- a/stock_release_channel/tests/__init__.py +++ b/stock_release_channel/tests/__init__.py @@ -6,4 +6,5 @@ test_release_channel, test_release_channel_lifecycle, test_release_channel_partner, + test_release_channel_delivery_date, ) diff --git a/stock_release_channel/tests/common.py b/stock_release_channel/tests/common.py index 3f74ad743e9..61ff337b039 100644 --- a/stock_release_channel/tests/common.py +++ b/stock_release_channel/tests/common.py @@ -1,10 +1,14 @@ # Copyright 2020 Camptocamp (https://www.camptocamp.com) +# Copyright 2025 Jacques-Etienne Baudoux (BCIM) # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) import logging +from odoo_test_helper import FakeModelLoader + from odoo import _, fields from odoo.tests import common +from odoo.tests.common import TransactionCase from odoo.addons.stock_available_to_promise_release.tests.common import ( PromiseReleaseCommonCase, @@ -225,3 +229,22 @@ def _assert_action_nothing_in_the_queue(self, action): } }, ) + + +class StockReleaseChannelDeliveryDateCommon(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.loader = FakeModelLoader(cls.env, cls.__module__) + cls.loader.backup_registry() + from .models.generator_test import StockReleaseChannel + + cls.loader.update_registry((StockReleaseChannel,)) + + cls.partner = cls.env.ref("base.main_partner") + cls.channel = cls.env.ref("stock_release_channel.stock_release_channel_default") + + @classmethod + def tearDownClass(cls): + cls.loader.restore_registry() + return super().tearDownClass() diff --git a/stock_release_channel/tests/models/__init__.py b/stock_release_channel/tests/models/__init__.py new file mode 100644 index 00000000000..9540f4f0990 --- /dev/null +++ b/stock_release_channel/tests/models/__init__.py @@ -0,0 +1 @@ +from . import generator_test diff --git a/stock_release_channel/tests/models/generator_test.py b/stock_release_channel/tests/models/generator_test.py new file mode 100644 index 00000000000..bb2f1221cc3 --- /dev/null +++ b/stock_release_channel/tests/models/generator_test.py @@ -0,0 +1,35 @@ +# Copyright 2025 Jacques-Etienne Baudoux (BCIM) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +from datetime import timedelta + +from odoo import models + + +class StockReleaseChannel(models.Model): + _inherit = "stock.release.channel" + + @property + def _delivery_date_generators(self): + d = {} + d["preparation"] = [ + self._next_delivery_date_one_day, + self._next_delivery_date_two_days, + ] + return d + + def _next_delivery_date_one_day(self, delivery_date, partner=None): + """Get the next valid delivery date respecting transport lead time. + The delivery date must be postponed at least by the shipment lead time. + """ + later = delivery_date + timedelta(days=1) + while True: + delivery_date = yield max(delivery_date, later) + + def _next_delivery_date_two_days(self, delivery_date, partner=None): + """Get the next valid delivery date respecting transport lead time. + The delivery date must be postponed at least by the shipment lead time. + """ + later = delivery_date + timedelta(days=2) + while True: + delivery_date = yield max(delivery_date, later) diff --git a/stock_release_channel/tests/test_release_channel_delivery_date.py b/stock_release_channel/tests/test_release_channel_delivery_date.py new file mode 100644 index 00000000000..48c527341ea --- /dev/null +++ b/stock_release_channel/tests/test_release_channel_delivery_date.py @@ -0,0 +1,30 @@ +# Copyright 2025 Jacques-Etienne Baudoux (BCIM) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +from freezegun import freeze_time + +from odoo import fields + +from .common import ReleaseChannelCase, StockReleaseChannelDeliveryDateCommon + +to_datetime = fields.Datetime.to_datetime + + +class TestReleaseChannelDeliveryDateFake(StockReleaseChannelDeliveryDateCommon): + @freeze_time("2025-01-02 10:00:00") + def test_delivery_date(self): + """Test generator on channel object""" + now = fields.Datetime.now() + dt = self.channel._get_earliest_delivery_date(self.partner, now) + self.assertEqual(dt, to_datetime("2025-01-04 10:00:00")) + + +class TestReleaseChannelDeliveryDate(ReleaseChannelCase): + def test_compute_delivery_date(self): + """Test delivery date computes with registered generators + + This test will run with other modules loaded. + """ + now = fields.Datetime.now() + partner = self.env.ref("base.main_partner") + self.default_channel._get_earliest_delivery_date(partner, now) diff --git a/stock_release_channel_cutoff/README.rst b/stock_release_channel_cutoff/README.rst index c793f979f15..7932ab7e664 100644 --- a/stock_release_channel_cutoff/README.rst +++ b/stock_release_channel_cutoff/README.rst @@ -1,3 +1,7 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + ============================= Stock Release Channels Cutoff ============================= @@ -7,13 +11,13 @@ Stock Release Channels Cutoff !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:b9a622b5638dc89f2fe5ef7bb17be429c4b646474cf18c9c81cdea6dd3d9e007 + !! source digest: sha256:1990d9256b77b09bfb544585c91574bd064458fe5972b163e3843d6cd6d176da !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png :target: https://odoo-community.org/page/development-status :alt: Beta -.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png +.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fwms-lightgray.png?logo=github diff --git a/stock_release_channel_cutoff/__manifest__.py b/stock_release_channel_cutoff/__manifest__.py index 97a2927ccdd..6197f49cdee 100644 --- a/stock_release_channel_cutoff/__manifest__.py +++ b/stock_release_channel_cutoff/__manifest__.py @@ -3,7 +3,7 @@ { "name": "Stock Release Channels Cutoff", "summary": "Add the cutoff time to the release channel", - "version": "16.0.1.0.2", + "version": "16.0.1.1.0", "license": "AGPL-3", "author": "Camptocamp, BCIM, Odoo Community Association (OCA)", "maintainers": ["jbaudoux"], diff --git a/stock_release_channel_cutoff/models/stock_release_channel.py b/stock_release_channel_cutoff/models/stock_release_channel.py index dc652650939..9f6dcd64e35 100644 --- a/stock_release_channel_cutoff/models/stock_release_channel.py +++ b/stock_release_channel_cutoff/models/stock_release_channel.py @@ -2,6 +2,10 @@ # Copyright 2024 Jacques-Etienne Baudoux (BCIM) # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) +from datetime import datetime + +import pytz + from odoo import api, fields, models from odoo.addons.stock_release_channel_process_end_time.utils import ( @@ -20,18 +24,58 @@ class StockReleaseChannel(models.Model): # Technical field for warning on kanban view cutoff_warning = fields.Boolean(compute="_compute_cutoff_warning") + @property + def cutoff_datetime(self): + self.ensure_one() + if not self.cutoff_time: + return False + now = self.process_end_date if self.state == "open" else fields.Datetime.now() + return time_to_datetime( + float_to_time( + self.cutoff_time, + ), + now=now, + tz=self.process_end_time_tz, + ) + @api.depends("cutoff_time", "state", "process_end_date", "process_end_time_tz") def _compute_cutoff_warning(self): now = fields.Datetime.now() for channel in self: cutoff_warning = False if channel.state == "open" and channel.cutoff_time: - cutoff = time_to_datetime( - float_to_time( - channel.cutoff_time, - ), - now=channel.process_end_date, - tz=channel.process_end_time_tz, - ) - cutoff_warning = cutoff < now + cutoff_warning = channel.cutoff_datetime < now channel.cutoff_warning = cutoff_warning + + @property + def _delivery_date_generators(self): + d = super()._delivery_date_generators + d["preparation"].append(self._next_delivery_date_cutoff) + return d + + def _next_delivery_date_cutoff(self, delivery_date, partner=None): + """Get the next valid delivery date respecting cutoff. + + The preparation date must be before the cutoff time otherwise it is + postponed to next day. + + A delivery date generator needs to provide the earliest valid date + starting from the received date. It can be called multiple times with a + new date to validate. + """ + self.ensure_one() + cutoff = self.cutoff_datetime + if not cutoff: + # any date is valid + while True: + delivery_date = yield delivery_date + wh_tz = pytz.timezone(self.process_end_time_tz) + next_day = time_to_datetime( + datetime.min.time(), + now=fields.Datetime.add(cutoff, days=1), + tz=wh_tz, + ) + while True: + while delivery_date <= cutoff: + delivery_date = yield delivery_date + delivery_date = yield max(delivery_date, next_day) diff --git a/stock_release_channel_cutoff/static/description/index.html b/stock_release_channel_cutoff/static/description/index.html index 08637543308..b3af95487b9 100644 --- a/stock_release_channel_cutoff/static/description/index.html +++ b/stock_release_channel_cutoff/static/description/index.html @@ -3,15 +3,16 @@ -Stock Release Channels Cutoff +README.rst -
    -

    Stock Release Channels Cutoff

    +
    + + +Odoo Community Association + +
    +

    Stock Release Channels Cutoff

    -

    Beta License: AGPL-3 OCA/wms Translate me on Weblate Try me on Runboat

    +

    Beta License: AGPL-3 OCA/wms Translate me on Weblate Try me on Runboat

    This module lets users select a release channel and set a specific cutoff time for it.

    When the current time (now) becomes later than the cutoff time and the channel is still open, the kanban view for that channel will display the cutoff time in red as a warning.

    @@ -386,7 +392,7 @@

    Stock Release Channels Cutoff

    -

    Bug Tracker

    +

    Bug Tracker

    Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed @@ -394,32 +400,34 @@

    Bug Tracker

    Do not contact contributors directly about support or help with technical issues.

    -

    Credits

    +

    Credits

    -

    Authors

    +

    Authors

    • Camptocamp
    • BCIM
    -

    Contributors

    +

    Contributors

    • Jacques-Etienne Baudoux <je@bcim.be>
    • Duong (Tran Quoc) <trobz.com>
    -

    Other credits

    +

    Other credits

    The development of this module has been financially supported by:

    • Camptocamp
    -

    Maintainers

    +

    Maintainers

    This module is maintained by the OCA.

    -Odoo Community Association + +Odoo Community Association +

    OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use.

    @@ -430,5 +438,6 @@

    Maintainers

    +
    diff --git a/stock_release_channel_cutoff/tests/test_compute_cutoff_time.py b/stock_release_channel_cutoff/tests/test_compute_cutoff_time.py index 64663661fc3..8a5dcb246b8 100644 --- a/stock_release_channel_cutoff/tests/test_compute_cutoff_time.py +++ b/stock_release_channel_cutoff/tests/test_compute_cutoff_time.py @@ -6,8 +6,11 @@ from freezegun import freeze_time +from odoo import fields from odoo.tests.common import TransactionCase +to_datetime = fields.Datetime.to_datetime + class TestStockReleaseChannelCutoff(TransactionCase): @classmethod @@ -66,3 +69,38 @@ def test_cutoff_warning_with_process_end_date_yesterday(self): self.channel.cutoff_time = 8.0 self.assertTrue(self.channel.cutoff_warning) + + @freeze_time("2023-02-01 09:00:00") + def test_delivery_date_no_cutoff(self): + self.channel.state = "asleep" + self.env.company.partner_id.tz = "Europe/Brussels" + self.channel.cutoff_time = 0 + dt = to_datetime("2023-02-01 08:00:00") + gen = self.channel._next_delivery_date_cutoff(dt) + result = next(gen) + self.assertEqual(result, dt) + result = gen.send(result) + self.assertEqual(result, dt) + + @freeze_time("2023-02-01 09:00:00") + def test_delivery_date_cutoff(self): + self.channel.state = "asleep" + self.env.company.partner_id.tz = "Europe/Brussels" + self.channel.cutoff_time = 9.5 # = 8.5 in UTC + # before cutoff + dt = to_datetime("2023-02-01 08:00:00") + gen = self.channel._next_delivery_date_cutoff(dt) + result = next(gen) + self.assertEqual(result, dt) + result = gen.send(result) + self.assertEqual(result, dt) + # after cutoff + dt = to_datetime("2023-02-01 09:00:00") + result = gen.send(dt) + next_day = to_datetime("2023-02-01 23:00:00") + self.assertEqual(result, next_day) + result = gen.send(result) + self.assertEqual(result, next_day) + dt = to_datetime("2023-02-02 09:00:00") + result = gen.send(dt) + self.assertEqual(result, dt) diff --git a/stock_release_channel_delivery/README.rst b/stock_release_channel_delivery/README.rst index 1869db3dc1c..f7536e7f17c 100644 --- a/stock_release_channel_delivery/README.rst +++ b/stock_release_channel_delivery/README.rst @@ -7,7 +7,7 @@ Stock Release Channel Delivery !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:cc77481670c48e1405bcdfe6831bc807e6f5e989075fbd6195a43e9a6992330c + !! source digest: sha256:d86dc233e0fdccccf51aee7ec5f966b26b901a04c888531fb06aea98074dcf62 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png diff --git a/stock_release_channel_delivery/__manifest__.py b/stock_release_channel_delivery/__manifest__.py index caf7ef53af3..fc733129860 100644 --- a/stock_release_channel_delivery/__manifest__.py +++ b/stock_release_channel_delivery/__manifest__.py @@ -5,7 +5,7 @@ "name": "Stock Release Channel Delivery", "summary": """ Add a carrier selection criteria on the release channel """, - "version": "16.0.2.1.0", + "version": "16.0.3.0.0", "license": "AGPL-3", "author": "ACSONE SA/NV,BCIM,Odoo Community Association (OCA)", "website": "https://github.com/OCA/wms", diff --git a/stock_release_channel_delivery/models/stock_picking.py b/stock_release_channel_delivery/models/stock_picking.py index 62daa10b975..069811b4343 100644 --- a/stock_release_channel_delivery/models/stock_picking.py +++ b/stock_release_channel_delivery/models/stock_picking.py @@ -1,23 +1,25 @@ # Copyright 2023 ACSONE SA/NV +# Copyright 2025 Jacques-Etienne Baudoux (BCIM) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from odoo import models +from odoo.osv import expression class StockPicking(models.Model): _inherit = "stock.picking" - def _get_release_channel_possible_candidate_domain_picking(self): - domain = super()._get_release_channel_possible_candidate_domain_picking() + @property + def _release_channel_possible_candidate_domain_base(self): + domain = super()._release_channel_possible_candidate_domain_base if self.carrier_id: - domain.extend( - [ - "|", - ("carrier_ids", "=", False), - ("carrier_ids", "in", self.carrier_id.ids), - ] - ) + domain_carrier = [ + "|", + ("carrier_ids", "=", False), + ("carrier_ids", "in", self.carrier_id.ids), + ] else: - domain.extend([("carrier_ids", "=", False)]) + domain_carrier = [("carrier_ids", "=", False)] + domain = expression.AND([domain, domain_carrier]) return domain diff --git a/stock_release_channel_delivery/static/description/index.html b/stock_release_channel_delivery/static/description/index.html index 7404f31d78e..03ceb942d9d 100644 --- a/stock_release_channel_delivery/static/description/index.html +++ b/stock_release_channel_delivery/static/description/index.html @@ -367,7 +367,7 @@

    Stock Release Channel Delivery

    !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:cc77481670c48e1405bcdfe6831bc807e6f5e989075fbd6195a43e9a6992330c +!! source digest: sha256:d86dc233e0fdccccf51aee7ec5f966b26b901a04c888531fb06aea98074dcf62 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

    Beta License: AGPL-3 OCA/wms Translate me on Weblate Try me on Runboat

    This addon adds a selection criteria on the release channel based on the shipping diff --git a/stock_release_channel_depot/README.rst b/stock_release_channel_depot/README.rst new file mode 100644 index 00000000000..2592c70064f --- /dev/null +++ b/stock_release_channel_depot/README.rst @@ -0,0 +1,77 @@ +=========================== +Stock Release Channel Depot +=========================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:0a56a1bb41eebdd7b8566fadc36e5057eccab67da1131086b4c42c193b330523 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fwms-lightgray.png?logo=github + :target: https://github.com/OCA/wms/tree/16.0/stock_release_channel_depot + :alt: OCA/wms +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/wms-16-0/wms-16-0-stock_release_channel_depot + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/wms&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Add partner depot to stock release channel + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Camptocamp + +Contributors +~~~~~~~~~~~~ + +* Telmo Santos +* Jacques-Etienne Baudoux + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/wms `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/stock_release_channel_depot/__init__.py b/stock_release_channel_depot/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/stock_release_channel_depot/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/stock_release_channel_depot/__manifest__.py b/stock_release_channel_depot/__manifest__.py new file mode 100644 index 00000000000..3f62523fd02 --- /dev/null +++ b/stock_release_channel_depot/__manifest__.py @@ -0,0 +1,16 @@ +# Copyright 2024 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +{ + "name": "Stock Release Channel Depot", + "summary": """This module allows users to add partner depot to stock release channel.""", + "version": "16.0.1.0.0", + "license": "AGPL-3", + "author": "Camptocamp, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/wms", + "depends": ["stock_depot", "stock_release_channel"], + "data": [ + "views/stock_release_channel_views.xml", + "views/stock_picking.xml", + ], +} diff --git a/stock_release_channel_depot/i18n/it.po b/stock_release_channel_depot/i18n/it.po new file mode 100644 index 00000000000..3eac50c13d6 --- /dev/null +++ b/stock_release_channel_depot/i18n/it.po @@ -0,0 +1,33 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * stock_release_channel_depot +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2025-05-07 13:23+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.10.4\n" + +#. module: stock_release_channel_depot +#: model:ir.model.fields,field_description:stock_release_channel_depot.field_stock_picking__depot_id +#: model:ir.model.fields,field_description:stock_release_channel_depot.field_stock_release_channel__depot_id +msgid "Depot" +msgstr "Deposito" + +#. module: stock_release_channel_depot +#: model:ir.model,name:stock_release_channel_depot.model_stock_release_channel +msgid "Stock Release Channels" +msgstr "Canali rilascio magazzino" + +#. module: stock_release_channel_depot +#: model:ir.model,name:stock_release_channel_depot.model_stock_picking +msgid "Transfer" +msgstr "Trasferimento" diff --git a/stock_release_channel_depot/i18n/stock_release_channel_depot.pot b/stock_release_channel_depot/i18n/stock_release_channel_depot.pot new file mode 100644 index 00000000000..b7e1546fbc0 --- /dev/null +++ b/stock_release_channel_depot/i18n/stock_release_channel_depot.pot @@ -0,0 +1,30 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * stock_release_channel_depot +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: stock_release_channel_depot +#: model:ir.model.fields,field_description:stock_release_channel_depot.field_stock_picking__depot_id +#: model:ir.model.fields,field_description:stock_release_channel_depot.field_stock_release_channel__depot_id +msgid "Depot" +msgstr "" + +#. module: stock_release_channel_depot +#: model:ir.model,name:stock_release_channel_depot.model_stock_release_channel +msgid "Stock Release Channels" +msgstr "" + +#. module: stock_release_channel_depot +#: model:ir.model,name:stock_release_channel_depot.model_stock_picking +msgid "Transfer" +msgstr "" diff --git a/stock_release_channel_depot/l18n/fr.po b/stock_release_channel_depot/l18n/fr.po new file mode 100644 index 00000000000..540913f044e --- /dev/null +++ b/stock_release_channel_depot/l18n/fr.po @@ -0,0 +1,22 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * stock_release_channel_depot +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-07-04 06:13+0000\n" +"PO-Revision-Date: 2024-07-04 06:13+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: stock_release_channel_depot +#: model:ir.model.fields,field_description:stock_release_channel_depot.field_stock_picking__depot_id +#: model:ir.model.fields,field_description:stock_release_channel_depot.field_stock_release_channel__depot_id +msgid "Depot" +msgstr "Dépôt" diff --git a/stock_release_channel_depot/l18n/stock_release_channel_depot.pot b/stock_release_channel_depot/l18n/stock_release_channel_depot.pot new file mode 100644 index 00000000000..e3f20cbb747 --- /dev/null +++ b/stock_release_channel_depot/l18n/stock_release_channel_depot.pot @@ -0,0 +1,32 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * stock_release_channel_depot +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-07-04 06:13+0000\n" +"PO-Revision-Date: 2024-07-04 06:13+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: stock_release_channel_depot +#: model:ir.model.fields,field_description:stock_release_channel_depot.field_stock_picking__depot_id +#: model:ir.model.fields,field_description:stock_release_channel_depot.field_stock_release_channel__depot_id +msgid "Depot" +msgstr "" + +#. module: stock_release_channel_depot +#: model:ir.model,name:stock_release_channel_depot.model_stock_picking +msgid "Stock Picking" +msgstr "" + +#. module: stock_release_channel_depot +#: model:ir.model,name:stock_release_channel_depot.model_stock_release_channel +msgid "Stock Release Channels" +msgstr "" diff --git a/stock_release_channel_depot/models/__init__.py b/stock_release_channel_depot/models/__init__.py new file mode 100644 index 00000000000..56916daf065 --- /dev/null +++ b/stock_release_channel_depot/models/__init__.py @@ -0,0 +1,2 @@ +from . import release_channel +from . import stock_picking diff --git a/stock_release_channel_depot/models/release_channel.py b/stock_release_channel_depot/models/release_channel.py new file mode 100644 index 00000000000..a04bbd67d3b --- /dev/null +++ b/stock_release_channel_depot/models/release_channel.py @@ -0,0 +1,10 @@ +# Copyright 2024 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models + + +class StockReleaseChannel(models.Model): + _inherit = "stock.release.channel" + + depot_id = fields.Many2one("stock.depot") diff --git a/stock_release_channel_depot/models/stock_picking.py b/stock_release_channel_depot/models/stock_picking.py new file mode 100644 index 00000000000..71633832c8b --- /dev/null +++ b/stock_release_channel_depot/models/stock_picking.py @@ -0,0 +1,10 @@ +# Copyright 2024 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models + + +class StockPicking(models.Model): + _inherit = "stock.picking" + + depot_id = fields.Many2one(related="release_channel_id.depot_id", store=True) diff --git a/stock_release_channel_depot/readme/CONTRIBUTORS.rst b/stock_release_channel_depot/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000000..22612a25009 --- /dev/null +++ b/stock_release_channel_depot/readme/CONTRIBUTORS.rst @@ -0,0 +1,2 @@ +* Telmo Santos +* Jacques-Etienne Baudoux diff --git a/stock_release_channel_depot/readme/DESCRIPTION.rst b/stock_release_channel_depot/readme/DESCRIPTION.rst new file mode 100644 index 00000000000..1e578ec7e02 --- /dev/null +++ b/stock_release_channel_depot/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +Add partner depot to stock release channel diff --git a/stock_release_channel_depot/static/description/icon.png b/stock_release_channel_depot/static/description/icon.png new file mode 100644 index 00000000000..1dcc49c24f3 Binary files /dev/null and b/stock_release_channel_depot/static/description/icon.png differ diff --git a/stock_release_channel_depot/static/description/index.html b/stock_release_channel_depot/static/description/index.html new file mode 100644 index 00000000000..6f9a773054d --- /dev/null +++ b/stock_release_channel_depot/static/description/index.html @@ -0,0 +1,424 @@ + + + + + +Stock Release Channel Depot + + + +

    +

    Stock Release Channel Depot

    + + +

    Beta License: AGPL-3 OCA/wms Translate me on Weblate Try me on Runboat

    +

    Add partner depot to stock release channel

    +

    Table of contents

    + +
    +

    Bug Tracker

    +

    Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

    +

    Do not contact contributors directly about support or help with technical issues.

    +
    +
    +

    Credits

    +
    +

    Authors

    +
      +
    • Camptocamp
    • +
    +
    +
    +

    Contributors

    + +
    +
    +

    Maintainers

    +

    This module is maintained by the OCA.

    + +Odoo Community Association + +

    OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

    +

    This module is part of the OCA/wms project on GitHub.

    +

    You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

    +
    +
    +
    + + diff --git a/stock_release_channel_depot/views/stock_picking.xml b/stock_release_channel_depot/views/stock_picking.xml new file mode 100644 index 00000000000..8f632c18c21 --- /dev/null +++ b/stock_release_channel_depot/views/stock_picking.xml @@ -0,0 +1,24 @@ + + + + stock.picking.form (in stock_release_channel_depot) + stock.picking + + + + + + + + + + stock.picking.delivery.tree + stock.picking + + + + + + + + diff --git a/stock_release_channel_depot/views/stock_release_channel_views.xml b/stock_release_channel_depot/views/stock_release_channel_views.xml new file mode 100644 index 00000000000..3d9086f244e --- /dev/null +++ b/stock_release_channel_depot/views/stock_release_channel_views.xml @@ -0,0 +1,22 @@ + + + + + stock.release.channel.form (in stock_release_channel_depot) + stock.release.channel + + + + + + + + + + + diff --git a/stock_release_channel_geoengine/README.rst b/stock_release_channel_geoengine/README.rst index 621e5ea25b2..21ccc700371 100644 --- a/stock_release_channel_geoengine/README.rst +++ b/stock_release_channel_geoengine/README.rst @@ -7,7 +7,7 @@ Stock Release Channel Geoengine !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:4e6a0c62f90513e2c1a31fa1b4e5780862cb8e5182ee0dfa16b63fddca7e28f1 + !! source digest: sha256:f710239ba0afe50575605fba54d5b1041491ffb4b66caed10ca5027d745651eb !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png diff --git a/stock_release_channel_geoengine/__manifest__.py b/stock_release_channel_geoengine/__manifest__.py index da2ada5e04c..5d0c61218b0 100644 --- a/stock_release_channel_geoengine/__manifest__.py +++ b/stock_release_channel_geoengine/__manifest__.py @@ -4,7 +4,7 @@ { "name": "Stock Release Channel Geoengine", "summary": """Release channel based on geo-localization""", - "version": "16.0.1.1.0", + "version": "16.0.2.0.0", "license": "AGPL-3", "author": "ACSONE SA/NV,Odoo Community Association (OCA)", "website": "https://github.com/OCA/wms", diff --git a/stock_release_channel_geoengine/i18n/it.po b/stock_release_channel_geoengine/i18n/it.po index 1e33d12dfb0..3e0f39f33ed 100644 --- a/stock_release_channel_geoengine/i18n/it.po +++ b/stock_release_channel_geoengine/i18n/it.po @@ -100,11 +100,6 @@ msgstr "Da fare:" msgid "To Release:" msgstr "Da rilasciare:" -#. module: stock_release_channel_geoengine -#: model:ir.model,name:stock_release_channel_geoengine.model_stock_picking -msgid "Transfer" -msgstr "Trasferimento" - #. module: stock_release_channel_geoengine #: model_terms:ir.ui.view,arch_db:stock_release_channel_geoengine.stock_release_channel_geoengine_view msgid "Waiting:" @@ -114,3 +109,6 @@ msgstr "In attesa:" #: model_terms:ir.ui.view,arch_db:stock_release_channel_geoengine.stock_release_channel_geoengine_view msgid "Warehouse:" msgstr "Magazzino:" + +#~ msgid "Transfer" +#~ msgstr "Trasferimento" diff --git a/stock_release_channel_geoengine/i18n/stock_release_channel_geoengine.pot b/stock_release_channel_geoengine/i18n/stock_release_channel_geoengine.pot index 76f51a4e088..8a91acf7ca3 100644 --- a/stock_release_channel_geoengine/i18n/stock_release_channel_geoengine.pot +++ b/stock_release_channel_geoengine/i18n/stock_release_channel_geoengine.pot @@ -97,11 +97,6 @@ msgstr "" msgid "To Release:" msgstr "" -#. module: stock_release_channel_geoengine -#: model:ir.model,name:stock_release_channel_geoengine.model_stock_picking -msgid "Transfer" -msgstr "" - #. module: stock_release_channel_geoengine #: model_terms:ir.ui.view,arch_db:stock_release_channel_geoengine.stock_release_channel_geoengine_view msgid "Waiting:" diff --git a/stock_release_channel_geoengine/models/__init__.py b/stock_release_channel_geoengine/models/__init__.py index f561e6c4334..27796b2de8e 100644 --- a/stock_release_channel_geoengine/models/__init__.py +++ b/stock_release_channel_geoengine/models/__init__.py @@ -1,3 +1,2 @@ from . import stock_release_channel from . import res_partner -from . import stock_picking diff --git a/stock_release_channel_geoengine/models/res_partner.py b/stock_release_channel_geoengine/models/res_partner.py index 3f0329c7e67..58aab494dbd 100644 --- a/stock_release_channel_geoengine/models/res_partner.py +++ b/stock_release_channel_geoengine/models/res_partner.py @@ -1,7 +1,9 @@ # Copyright 2023 ACSONE SA/NV +# Copyright 2025 Jacques-Etienne Baudoux (BCIM) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from odoo import api, fields, models +from odoo.osv import expression class ResPartner(models.Model): @@ -31,3 +33,17 @@ def _compute_located_in_stock_release_channel_ids(self): [("delivery_zone", "geo_intersect", rec.geo_point)] ) ) + + @property + def _release_channel_possible_candidate_domain(self): + domain = super()._release_channel_possible_candidate_domain + if self.in_geo_release_channel: + domain_geoengine = [ + "|", + ("restrict_to_delivery_zone", "=", False), + ("delivery_zone", "geo_intersect", self.geo_point), + ] + else: + domain_geoengine = [("restrict_to_delivery_zone", "=", False)] + domain = expression.AND([domain, domain_geoengine]) + return domain diff --git a/stock_release_channel_geoengine/models/stock_picking.py b/stock_release_channel_geoengine/models/stock_picking.py deleted file mode 100644 index 63d77174b5b..00000000000 --- a/stock_release_channel_geoengine/models/stock_picking.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright 2023 ACSONE SA/NV -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). - -from odoo import models - - -class StockPicking(models.Model): - - _inherit = "stock.picking" - - def _get_release_channel_possible_candidate_domain_partner(self): - self.ensure_one() - domain = super()._get_release_channel_possible_candidate_domain_partner() - if self.partner_id.in_geo_release_channel: - domain += [ - "|", - ("restrict_to_delivery_zone", "=", False), - ("delivery_zone", "geo_intersect", self.partner_id.geo_point), - ] - else: - domain += [("restrict_to_delivery_zone", "=", False)] - return domain diff --git a/stock_release_channel_geoengine/static/description/index.html b/stock_release_channel_geoengine/static/description/index.html index d3438a4a9c8..da07aeb8c00 100644 --- a/stock_release_channel_geoengine/static/description/index.html +++ b/stock_release_channel_geoengine/static/description/index.html @@ -367,7 +367,7 @@

    Stock Release Channel Geoengine

    !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:4e6a0c62f90513e2c1a31fa1b4e5780862cb8e5182ee0dfa16b63fddca7e28f1 +!! source digest: sha256:f710239ba0afe50575605fba54d5b1041491ffb4b66caed10ca5027d745651eb !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

    Beta License: AGPL-3 OCA/wms Translate me on Weblate Try me on Runboat

    This module enhance release channels with the addition of diff --git a/stock_release_channel_partner_by_date/README.rst b/stock_release_channel_partner_by_date/README.rst index c93226c1e12..8e110aa7150 100644 --- a/stock_release_channel_partner_by_date/README.rst +++ b/stock_release_channel_partner_by_date/README.rst @@ -7,7 +7,7 @@ Stock Release Channels for Delivery Dates !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:4fe43c888e6b54ec55a6bb5e690de63ba9cd2a27d726808eb1402a2918d10764 + !! source digest: sha256:0ca449845c7440346880f1d856d8144be516dbeeb9e0376250d9d714926d60f1 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png diff --git a/stock_release_channel_partner_by_date/__manifest__.py b/stock_release_channel_partner_by_date/__manifest__.py index 6a4fb8a5c41..856c29c9e30 100644 --- a/stock_release_channel_partner_by_date/__manifest__.py +++ b/stock_release_channel_partner_by_date/__manifest__.py @@ -5,7 +5,7 @@ { "name": "Stock Release Channels for Delivery Dates", "summary": "Set release channels for specific delivery dates", - "version": "16.0.1.1.0", + "version": "16.0.2.0.1", "development_status": "Beta", "license": "AGPL-3", "author": "Camptocamp, BCIM, Odoo Community Association (OCA)", diff --git a/stock_release_channel_partner_by_date/models/res_partner.py b/stock_release_channel_partner_by_date/models/res_partner.py index f8054392211..067539d86e5 100644 --- a/stock_release_channel_partner_by_date/models/res_partner.py +++ b/stock_release_channel_partner_by_date/models/res_partner.py @@ -13,4 +13,5 @@ class ResPartner(models.Model): inverse_name="partner_id", string="Additional Release Channels", help="Additional release channels for a specific delivery date.", + context={"active_test": False}, ) diff --git a/stock_release_channel_partner_by_date/models/stock_picking.py b/stock_release_channel_partner_by_date/models/stock_picking.py index a6d41af0a06..d41151f100e 100644 --- a/stock_release_channel_partner_by_date/models/stock_picking.py +++ b/stock_release_channel_partner_by_date/models/stock_picking.py @@ -3,13 +3,13 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) from odoo import fields, models -from odoo.osv import expression class StockPicking(models.Model): _inherit = "stock.picking" - def _get_release_channel_partner_date_domain(self): + @property + def _release_channel_partner_date_domain(self): assert self.scheduled_date scheduled_date = max(self.scheduled_date, fields.Datetime.now()) tz = ( @@ -30,30 +30,22 @@ def _get_release_channel_partner_dates(self): """Return specific channel entries corresponding to the transfer.""" return ( self.env["stock.release.channel.partner.date"] - .search(self._get_release_channel_partner_date_domain()) + .search(self._release_channel_partner_date_domain) .filtered( lambda o: o.release_channel_id.filtered_domain( - self._get_release_channel_possible_candidate_domain_picking() + self._release_channel_possible_candidate_domain_base ) ) ) - def _inject_possible_candidate_domain_partner(self): - # Do not inject partners domain if there are channels for this specific - # delivery address and date - specific_rcs = self._get_release_channel_partner_dates() - if specific_rcs: - return False - return super()._inject_possible_candidate_domain_partner() - - def _get_release_channel_possible_candidate_domain(self): - domain = super()._get_release_channel_possible_candidate_domain() + @property + def _release_channel_possible_candidate_domain(self): # Look for a specific release channel at first - specific_rc_domain = None if self.scheduled_date: specific_rcs = self._get_release_channel_partner_dates() if specific_rcs: - specific_rc_domain = [("id", "in", specific_rcs.release_channel_id.ids)] - if specific_rc_domain: - domain = expression.AND([domain, specific_rc_domain]) - return domain + return [ + ("state", "in", ("open", "locked")), + ("id", "in", specific_rcs.release_channel_id.ids), + ] + return super()._release_channel_possible_candidate_domain diff --git a/stock_release_channel_partner_by_date/static/description/index.html b/stock_release_channel_partner_by_date/static/description/index.html index 848cc27af10..fe722c73475 100644 --- a/stock_release_channel_partner_by_date/static/description/index.html +++ b/stock_release_channel_partner_by_date/static/description/index.html @@ -367,7 +367,7 @@

    Stock Release Channels for Delivery Dates

    !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:4fe43c888e6b54ec55a6bb5e690de63ba9cd2a27d726808eb1402a2918d10764 +!! source digest: sha256:0ca449845c7440346880f1d856d8144be516dbeeb9e0376250d9d714926d60f1 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

    Beta License: AGPL-3 OCA/wms Translate me on Weblate Try me on Runboat

    Set release channels for specific delivery addresses and dates.

    diff --git a/stock_release_channel_partner_by_date/tests/common.py b/stock_release_channel_partner_by_date/tests/common.py new file mode 100644 index 00000000000..c4409a86131 --- /dev/null +++ b/stock_release_channel_partner_by_date/tests/common.py @@ -0,0 +1,31 @@ +# Copyright 2024 Camptocamp SA +# Copyright 2024 Jacques-Etienne Baudoux (BCIM) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + + +from odoo.addons.stock_release_channel.tests.test_release_channel_partner import ( + ReleaseChannelPartnerCommon, +) + + +class ReleaseChannelPartnerDateCommon(ReleaseChannelPartnerCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.delivery_date_channel = cls.partner_channel.copy( + { + "name": "Specific Date Channel", + "warehouse_id": cls.wh.id, + } + ) + + @classmethod + def _create_channel_partner_date(cls, channel, partner, date): + rc_date_model = cls.env["stock.release.channel.partner.date"] + return rc_date_model.create( + { + "partner_id": partner.id, + "release_channel_id": channel.id, + "date": date, + } + ) diff --git a/stock_release_channel_partner_by_date/tests/test_release_channel_partner_date.py b/stock_release_channel_partner_by_date/tests/test_release_channel_partner_date.py index f55eee019b7..00020e4bc20 100644 --- a/stock_release_channel_partner_by_date/tests/test_release_channel_partner_date.py +++ b/stock_release_channel_partner_by_date/tests/test_release_channel_partner_date.py @@ -4,29 +4,10 @@ from odoo import fields -from odoo.addons.stock_release_channel.tests.test_release_channel_partner import ( - ReleaseChannelPartnerCommon, -) +from .common import ReleaseChannelPartnerDateCommon -class TestReleaseChannelPartnerDate(ReleaseChannelPartnerCommon): - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.delivery_date_channel = cls.partner_channel.copy( - {"name": "Specific Date Channel"} - ) - - def _create_channel_partner_date(self, channel, partner, date): - rc_date_model = self.env["stock.release.channel.partner.date"] - return rc_date_model.create( - { - "partner_id": partner.id, - "release_channel_id": channel.id, - "date": date, - } - ) - +class TestReleaseChannelPartnerDate(ReleaseChannelPartnerDateCommon): def test_release_channel_on_specific_date(self): """partner specific date release channel is higher priority than other channels""" self.delivery_date_channel.action_wake_up() @@ -53,3 +34,21 @@ def test_release_channel_sleep_archive_specific_date(self): self.assertTrue(channel_date.active) self.delivery_date_channel.action_sleep() self.assertFalse(channel_date.active) + + def test_release_channel_on_specific_date_not_available(self): + """Test that when no release channel is available to satisfy + a specific partner date,no fallback release channel is + proposed.""" + # Exclude delivery channel from possible candidates + self.delivery_date_channel.picking_type_ids = self.env[ + "stock.picking.type" + ].search([("id", "!=", self.move.picking_id.picking_type_id.id)], limit=1) + scheduled_date = fields.Datetime.now() + self._create_channel_partner_date( + self.delivery_date_channel, + self.partner, + scheduled_date, + ) + self.move.picking_id.scheduled_date = scheduled_date + self.move.picking_id.assign_release_channel() + self.assertFalse(self.move.picking_id.release_channel_id) diff --git a/stock_release_channel_partner_by_date/views/res_partner.xml b/stock_release_channel_partner_by_date/views/res_partner.xml index 529bff9a607..c31d0e726b0 100644 --- a/stock_release_channel_partner_by_date/views/res_partner.xml +++ b/stock_release_channel_partner_by_date/views/res_partner.xml @@ -15,7 +15,8 @@ colspan="2" /> - + + diff --git a/stock_release_channel_partner_by_date_delivery_window/README.rst b/stock_release_channel_partner_by_date_delivery_window/README.rst new file mode 100644 index 00000000000..e19c4125a8f --- /dev/null +++ b/stock_release_channel_partner_by_date_delivery_window/README.rst @@ -0,0 +1,84 @@ +================================================================== +Glue Stock Release Channels for Delivery Dates and Delivery window +================================================================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:41a3baa296f8da7e0b152b188a758550e7a15aaf22245e6263c48e009dbb82d6 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fwms-lightgray.png?logo=github + :target: https://github.com/OCA/wms/tree/16.0/stock_release_channel_partner_by_date_delivery_window + :alt: OCA/wms +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/wms-16-0/wms-16-0-stock_release_channel_partner_by_date_delivery_window + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/wms&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Test module between Stock Release Channels for Delivery Dates and Delivery window + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* BCIM + +Contributors +~~~~~~~~~~~~ + +* Jacques-Etienne Baudoux (BCIM) + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-jbaudoux| image:: https://github.com/jbaudoux.png?size=40px + :target: https://github.com/jbaudoux + :alt: jbaudoux + +Current `maintainer `__: + +|maintainer-jbaudoux| + +This module is part of the `OCA/wms `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/stock_release_channel_partner_delivery_window/readme/USAGE.rst b/stock_release_channel_partner_by_date_delivery_window/__init__.py similarity index 100% rename from stock_release_channel_partner_delivery_window/readme/USAGE.rst rename to stock_release_channel_partner_by_date_delivery_window/__init__.py diff --git a/stock_release_channel_partner_by_date_delivery_window/__manifest__.py b/stock_release_channel_partner_by_date_delivery_window/__manifest__.py new file mode 100644 index 00000000000..c44f11c7d7a --- /dev/null +++ b/stock_release_channel_partner_by_date_delivery_window/__manifest__.py @@ -0,0 +1,17 @@ +# Copyright 2025 Jacques-Etienne Baudoux (BCIM) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +{ + "name": "Glue Stock Release Channels for Delivery Dates and Delivery window", + "version": "16.0.1.0.0", + "development_status": "Beta", + "license": "AGPL-3", + "author": "BCIM, Odoo Community Association (OCA)", + "maintainers": ["jbaudoux"], + "website": "https://github.com/OCA/wms", + "depends": [ + "stock_release_channel_partner_by_date", + "stock_release_channel_partner_delivery_window", + ], + "auto_install": True, +} diff --git a/stock_release_channel_partner_by_date_delivery_window/i18n/it.po b/stock_release_channel_partner_by_date_delivery_window/i18n/it.po new file mode 100644 index 00000000000..73388557f6d --- /dev/null +++ b/stock_release_channel_partner_by_date_delivery_window/i18n/it.po @@ -0,0 +1,14 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" diff --git a/stock_release_channel_partner_by_date_delivery_window/i18n/stock_release_channel_partner_by_date_delivery_window.pot b/stock_release_channel_partner_by_date_delivery_window/i18n/stock_release_channel_partner_by_date_delivery_window.pot new file mode 100644 index 00000000000..78d58d53fe0 --- /dev/null +++ b/stock_release_channel_partner_by_date_delivery_window/i18n/stock_release_channel_partner_by_date_delivery_window.pot @@ -0,0 +1,13 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" diff --git a/stock_release_channel_partner_by_date_delivery_window/readme/CONTRIBUTORS.rst b/stock_release_channel_partner_by_date_delivery_window/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000000..3c6c5c696a8 --- /dev/null +++ b/stock_release_channel_partner_by_date_delivery_window/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Jacques-Etienne Baudoux (BCIM) diff --git a/stock_release_channel_partner_by_date_delivery_window/readme/DESCRIPTION.rst b/stock_release_channel_partner_by_date_delivery_window/readme/DESCRIPTION.rst new file mode 100644 index 00000000000..bcfa4a7f3c1 --- /dev/null +++ b/stock_release_channel_partner_by_date_delivery_window/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +Test module between Stock Release Channels for Delivery Dates and Delivery window diff --git a/stock_release_channel_partner_by_date_delivery_window/static/description/icon.png b/stock_release_channel_partner_by_date_delivery_window/static/description/icon.png new file mode 100644 index 00000000000..1dcc49c24f3 Binary files /dev/null and b/stock_release_channel_partner_by_date_delivery_window/static/description/icon.png differ diff --git a/stock_release_channel_partner_by_date_delivery_window/static/description/index.html b/stock_release_channel_partner_by_date_delivery_window/static/description/index.html new file mode 100644 index 00000000000..32bdb5257f4 --- /dev/null +++ b/stock_release_channel_partner_by_date_delivery_window/static/description/index.html @@ -0,0 +1,425 @@ + + + + + +Glue Stock Release Channels for Delivery Dates and Delivery window + + + +
    +

    Glue Stock Release Channels for Delivery Dates and Delivery window

    + + +

    Beta License: AGPL-3 OCA/wms Translate me on Weblate Try me on Runboat

    +

    Test module between Stock Release Channels for Delivery Dates and Delivery window

    +

    Table of contents

    + +
    +

    Bug Tracker

    +

    Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

    +

    Do not contact contributors directly about support or help with technical issues.

    +
    +
    +

    Credits

    +
    +

    Authors

    +
      +
    • BCIM
    • +
    +
    +
    +

    Contributors

    + +
    +
    +

    Maintainers

    +

    This module is maintained by the OCA.

    + +Odoo Community Association + +

    OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

    +

    Current maintainer:

    +

    jbaudoux

    +

    This module is part of the OCA/wms project on GitHub.

    +

    You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

    +
    +
    +
    + + diff --git a/stock_release_channel_partner_by_date_delivery_window/tests/__init__.py b/stock_release_channel_partner_by_date_delivery_window/tests/__init__.py new file mode 100644 index 00000000000..3e28dc125ca --- /dev/null +++ b/stock_release_channel_partner_by_date_delivery_window/tests/__init__.py @@ -0,0 +1 @@ +from . import test_release_channel_partner_date diff --git a/stock_release_channel_partner_by_date_delivery_window/tests/test_release_channel_partner_date.py b/stock_release_channel_partner_by_date_delivery_window/tests/test_release_channel_partner_date.py new file mode 100644 index 00000000000..dfb9bc6ee8f --- /dev/null +++ b/stock_release_channel_partner_by_date_delivery_window/tests/test_release_channel_partner_date.py @@ -0,0 +1,67 @@ +# Copyright 2025 Jacques-Etienne Baudoux (BCIM) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import fields +from odoo.fields import Command + +from odoo.addons.stock_release_channel_partner_by_date.tests.common import ( + ReleaseChannelPartnerDateCommon, +) + + +class TestReleaseChannelPartnerDate(ReleaseChannelPartnerDateCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + + cls.scheduled_date = fields.Datetime.now() + cls.move.picking_id.scheduled_date = cls.scheduled_date + cls.move.picking_id.date_deadline = cls.scheduled_date + + # Create partner delivery window on a date different from scheduled + weekday = (cls.scheduled_date.weekday() + 3) % 6 + time_weekday = cls.env["time.weekday"].search([("name", "=", str(weekday))]) + cls.partner.write( + { + "delivery_time_preference": "time_windows", + "delivery_time_window_ids": [ + Command.create( + { + "time_window_start": 8.00, + "time_window_end": 18.50, + "time_window_weekday_ids": [Command.link(time_weekday.id)], + } + ) + ], + } + ) + + # Create specific date channel for partner + cls._create_channel_partner_date( + cls.delivery_date_channel, + cls.partner, + cls.scheduled_date, + ) + + def test_release_channel_on_specific_date_available(self): + """Test when channel is open. + + Test that when the specific channel is available, it is assigned even + if it is not in the delivery window. + """ + self.delivery_date_channel.action_wake_up() + self.delivery_date_channel.shipment_date = self.scheduled_date + self.move.picking_id.assign_release_channel() + self.assertEqual( + self.move.picking_id.release_channel_id, self.delivery_date_channel + ) + + def test_release_channel_on_specific_date_not_available(self): + """Test when channel is asleep. + + Test that when no release channel is available to satisfy + a specific partner date, no fallback release channel is + proposed. + """ + self.move.picking_id.assign_release_channel() + self.assertFalse(self.move.picking_id.release_channel_id) diff --git a/stock_release_channel_partner_by_date_public_holidays/README.rst b/stock_release_channel_partner_by_date_public_holidays/README.rst new file mode 100644 index 00000000000..22034ae07ff --- /dev/null +++ b/stock_release_channel_partner_by_date_public_holidays/README.rst @@ -0,0 +1,84 @@ +================================================================== +Glue Stock Release Channels for Delivery Dates and Public holidays +================================================================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:d97b4b0cd28c4b51d8d7d08d832b0caf31c2e966e3f4900ccd448a69c7969d4e + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fwms-lightgray.png?logo=github + :target: https://github.com/OCA/wms/tree/16.0/stock_release_channel_partner_by_date_public_holidays + :alt: OCA/wms +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/wms-16-0/wms-16-0-stock_release_channel_partner_by_date_public_holidays + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/wms&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Test module between Stock Release Channels for Delivery Dates and Public holiday + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* BCIM + +Contributors +~~~~~~~~~~~~ + +* Jacques-Etienne Baudoux (BCIM) + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-jbaudoux| image:: https://github.com/jbaudoux.png?size=40px + :target: https://github.com/jbaudoux + :alt: jbaudoux + +Current `maintainer `__: + +|maintainer-jbaudoux| + +This module is part of the `OCA/wms `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/stock_release_channel_partner_by_date_public_holidays/__init__.py b/stock_release_channel_partner_by_date_public_holidays/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/stock_release_channel_partner_by_date_public_holidays/__manifest__.py b/stock_release_channel_partner_by_date_public_holidays/__manifest__.py new file mode 100644 index 00000000000..4408ece199d --- /dev/null +++ b/stock_release_channel_partner_by_date_public_holidays/__manifest__.py @@ -0,0 +1,17 @@ +# Copyright 2025 Jacques-Etienne Baudoux (BCIM) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +{ + "name": "Glue Stock Release Channels for Delivery Dates and Public holidays", + "version": "16.0.2.0.0", + "development_status": "Beta", + "license": "AGPL-3", + "author": "BCIM, Odoo Community Association (OCA)", + "maintainers": ["jbaudoux"], + "website": "https://github.com/OCA/wms", + "depends": [ + "stock_release_channel_partner_by_date", + "stock_release_channel_partner_public_holidays", + ], + "auto_install": True, +} diff --git a/stock_release_channel_partner_by_date_public_holidays/i18n/it.po b/stock_release_channel_partner_by_date_public_holidays/i18n/it.po new file mode 100644 index 00000000000..73388557f6d --- /dev/null +++ b/stock_release_channel_partner_by_date_public_holidays/i18n/it.po @@ -0,0 +1,14 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" diff --git a/stock_release_channel_partner_by_date_public_holidays/i18n/stock_release_channel_partner_by_date_public_holidays.pot b/stock_release_channel_partner_by_date_public_holidays/i18n/stock_release_channel_partner_by_date_public_holidays.pot new file mode 100644 index 00000000000..78d58d53fe0 --- /dev/null +++ b/stock_release_channel_partner_by_date_public_holidays/i18n/stock_release_channel_partner_by_date_public_holidays.pot @@ -0,0 +1,13 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" diff --git a/stock_release_channel_partner_by_date_public_holidays/readme/CONTRIBUTORS.rst b/stock_release_channel_partner_by_date_public_holidays/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000000..3c6c5c696a8 --- /dev/null +++ b/stock_release_channel_partner_by_date_public_holidays/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Jacques-Etienne Baudoux (BCIM) diff --git a/stock_release_channel_partner_by_date_public_holidays/readme/DESCRIPTION.rst b/stock_release_channel_partner_by_date_public_holidays/readme/DESCRIPTION.rst new file mode 100644 index 00000000000..998fa9b1a37 --- /dev/null +++ b/stock_release_channel_partner_by_date_public_holidays/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +Test module between Stock Release Channels for Delivery Dates and Public holiday diff --git a/stock_release_channel_partner_by_date_public_holidays/static/description/icon.png b/stock_release_channel_partner_by_date_public_holidays/static/description/icon.png new file mode 100644 index 00000000000..1dcc49c24f3 Binary files /dev/null and b/stock_release_channel_partner_by_date_public_holidays/static/description/icon.png differ diff --git a/stock_release_channel_partner_by_date_public_holidays/static/description/index.html b/stock_release_channel_partner_by_date_public_holidays/static/description/index.html new file mode 100644 index 00000000000..24e499ed504 --- /dev/null +++ b/stock_release_channel_partner_by_date_public_holidays/static/description/index.html @@ -0,0 +1,425 @@ + + + + + +Glue Stock Release Channels for Delivery Dates and Public holidays + + + +
    +

    Glue Stock Release Channels for Delivery Dates and Public holidays

    + + +

    Beta License: AGPL-3 OCA/wms Translate me on Weblate Try me on Runboat

    +

    Test module between Stock Release Channels for Delivery Dates and Public holiday

    +

    Table of contents

    + +
    +

    Bug Tracker

    +

    Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

    +

    Do not contact contributors directly about support or help with technical issues.

    +
    +
    +

    Credits

    +
    +

    Authors

    +
      +
    • BCIM
    • +
    +
    +
    +

    Contributors

    + +
    +
    +

    Maintainers

    +

    This module is maintained by the OCA.

    + +Odoo Community Association + +

    OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

    +

    Current maintainer:

    +

    jbaudoux

    +

    This module is part of the OCA/wms project on GitHub.

    +

    You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

    +
    +
    +
    + + diff --git a/stock_release_channel_partner_by_date_public_holidays/tests/__init__.py b/stock_release_channel_partner_by_date_public_holidays/tests/__init__.py new file mode 100644 index 00000000000..3e28dc125ca --- /dev/null +++ b/stock_release_channel_partner_by_date_public_holidays/tests/__init__.py @@ -0,0 +1 @@ +from . import test_release_channel_partner_date diff --git a/stock_release_channel_partner_by_date_public_holidays/tests/test_release_channel_partner_date.py b/stock_release_channel_partner_by_date_public_holidays/tests/test_release_channel_partner_date.py new file mode 100644 index 00000000000..bfa1129c92e --- /dev/null +++ b/stock_release_channel_partner_by_date_public_holidays/tests/test_release_channel_partner_date.py @@ -0,0 +1,60 @@ +# Copyright 2025 Jacques-Etienne Baudoux (BCIM) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import fields + +from odoo.addons.stock_release_channel_partner_by_date.tests.common import ( + ReleaseChannelPartnerDateCommon, +) + + +class TestReleaseChannelPartnerDate(ReleaseChannelPartnerDateCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + + cls.scheduled_date = fields.Datetime.now() + cls.move.picking_id.scheduled_date = cls.scheduled_date + cls.move.picking_id.date_deadline = cls.scheduled_date + + # Create holiday on scheduled date + this_year = cls.scheduled_date.year + holiday_year = cls.env["hr.holidays.public"].create({"year": this_year}) + cls.env["hr.holidays.public.line"].create( + { + "name": "holiday 1", + "date": cls.scheduled_date, + "year_id": holiday_year.id, + } + ) + cls.delivery_date_channel.exclude_public_holidays = True + + # Create specific date channel for partner + cls._create_channel_partner_date( + cls.delivery_date_channel, + cls.partner, + cls.scheduled_date, + ) + + def test_release_channel_on_specific_date_available(self): + """Test when channel is open. + + Test that when the specific channel is available, it is assigned even + if it is a public holiday. + """ + self.delivery_date_channel.action_wake_up() + self.delivery_date_channel.shipment_date = self.scheduled_date + self.move.picking_id.assign_release_channel() + self.assertEqual( + self.move.picking_id.release_channel_id, self.delivery_date_channel + ) + + def test_release_channel_on_specific_date_not_available(self): + """Test when channel is asleep. + + Test that when no release channel is available to satisfy + a specific partner date, no fallback release channel is + proposed. + """ + self.move.picking_id.assign_release_channel() + self.assertFalse(self.move.picking_id.release_channel_id) diff --git a/stock_release_channel_partner_delivery_window/README.rst b/stock_release_channel_partner_delivery_window/README.rst index 3c5e8f59d1f..db66249b9b9 100644 --- a/stock_release_channel_partner_delivery_window/README.rst +++ b/stock_release_channel_partner_delivery_window/README.rst @@ -1,3 +1,7 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + ============================================= Stock Release Channel Partner Delivery Window ============================================= @@ -7,13 +11,13 @@ Stock Release Channel Partner Delivery Window !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:fc7d4d8a92bafb5420e2ddfc0c6ca34722c2bf02bdcfbc74faf6012cf885fcf4 + !! source digest: sha256:f546d82efdafe203b5da0321d93e8a6b764c8baf232b7999ccfc32ff82bdefde !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png :target: https://odoo-community.org/page/development-status :alt: Beta -.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png +.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fwms-lightgray.png?logo=github diff --git a/stock_release_channel_partner_delivery_window/__manifest__.py b/stock_release_channel_partner_delivery_window/__manifest__.py index 9d73aa55803..a72cd7e6f66 100644 --- a/stock_release_channel_partner_delivery_window/__manifest__.py +++ b/stock_release_channel_partner_delivery_window/__manifest__.py @@ -1,3 +1,4 @@ +# Copyright 2025 Jacques-Etienne Baudoux (BCIM) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). { @@ -5,7 +6,7 @@ "summary": """ Allows to define an end date (and time) on a release channel and propagate it to the concerned pickings""", - "version": "16.0.1.0.1", + "version": "16.0.2.1.0", "license": "AGPL-3", "maintainers": ["jbaudoux"], "author": "Camptocamp, BCIM, Odoo Community Association (OCA)", diff --git a/stock_release_channel_partner_delivery_window/i18n/fr.po b/stock_release_channel_partner_delivery_window/i18n/fr.po index 5324fc8ef95..a05e259483a 100644 --- a/stock_release_channel_partner_delivery_window/i18n/fr.po +++ b/stock_release_channel_partner_delivery_window/i18n/fr.po @@ -16,12 +16,17 @@ msgstr "" "Plural-Forms: nplurals=2; plural=n > 1;\n" "X-Generator: Weblate 5.6.2\n" +#. module: stock_release_channel_partner_delivery_window +#: model:ir.model.fields,field_description:stock_release_channel_partner_delivery_window.field_stock_release_channel__delivery_date_weekday +msgid "Delivery Date Weekday" +msgstr "" + #. module: stock_release_channel_partner_delivery_window #: model:ir.model.fields,help:stock_release_channel_partner_delivery_window.field_stock_release_channel__respect_partner_delivery_time_windows msgid "" "If the delivery has moves linked to SO lines linked to SO that has a " -"commitment_date, then we never respect the partner time window (it is not an" -" exclusion selection criteria anymore)" +"commitment_date, then we never respect the partner time window (it is not an " +"exclusion selection criteria anymore)" msgstr "" #. module: stock_release_channel_partner_delivery_window diff --git a/stock_release_channel_partner_delivery_window/i18n/it.po b/stock_release_channel_partner_delivery_window/i18n/it.po index 9da4bf3cdba..688083af387 100644 --- a/stock_release_channel_partner_delivery_window/i18n/it.po +++ b/stock_release_channel_partner_delivery_window/i18n/it.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0\n" "Report-Msgid-Bugs-To: \n" -"PO-Revision-Date: 2023-11-24 09:36+0000\n" +"PO-Revision-Date: 2025-05-22 07:57+0000\n" "Last-Translator: mymage \n" "Language-Team: none\n" "Language: it\n" @@ -14,14 +14,19 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.17\n" +"X-Generator: Weblate 5.10.4\n" + +#. module: stock_release_channel_partner_delivery_window +#: model:ir.model.fields,field_description:stock_release_channel_partner_delivery_window.field_stock_release_channel__delivery_date_weekday +msgid "Delivery Date Weekday" +msgstr "Giorno della settimana della data di consegna" #. module: stock_release_channel_partner_delivery_window #: model:ir.model.fields,help:stock_release_channel_partner_delivery_window.field_stock_release_channel__respect_partner_delivery_time_windows msgid "" "If the delivery has moves linked to SO lines linked to SO that has a " -"commitment_date, then we never respect the partner time window (it is not an" -" exclusion selection criteria anymore)" +"commitment_date, then we never respect the partner time window (it is not an " +"exclusion selection criteria anymore)" msgstr "" "Se la consegna ha movimenti collegati a righe dell'OV che ha una data " "impegno, allora non si rispetta mai l'intervallo tempoale del partner (non è " diff --git a/stock_release_channel_partner_delivery_window/i18n/stock_release_channel_partner_delivery_window.pot b/stock_release_channel_partner_delivery_window/i18n/stock_release_channel_partner_delivery_window.pot index 305c4552c35..a22ca7cca9d 100644 --- a/stock_release_channel_partner_delivery_window/i18n/stock_release_channel_partner_delivery_window.pot +++ b/stock_release_channel_partner_delivery_window/i18n/stock_release_channel_partner_delivery_window.pot @@ -13,6 +13,11 @@ msgstr "" "Content-Transfer-Encoding: \n" "Plural-Forms: \n" +#. module: stock_release_channel_partner_delivery_window +#: model:ir.model.fields,field_description:stock_release_channel_partner_delivery_window.field_stock_release_channel__delivery_date_weekday +msgid "Delivery Date Weekday" +msgstr "" + #. module: stock_release_channel_partner_delivery_window #: model:ir.model.fields,help:stock_release_channel_partner_delivery_window.field_stock_release_channel__respect_partner_delivery_time_windows msgid "" diff --git a/stock_release_channel_partner_delivery_window/models/stock_picking.py b/stock_release_channel_partner_delivery_window/models/stock_picking.py index 9aad3829211..7c6f3e53f9a 100644 --- a/stock_release_channel_partner_delivery_window/models/stock_picking.py +++ b/stock_release_channel_partner_delivery_window/models/stock_picking.py @@ -1,4 +1,6 @@ +# Copyright 2025 Jacques-Etienne Baudoux (BCIM) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + from odoo import models @@ -6,10 +8,23 @@ class StockPicking(models.Model): _inherit = "stock.picking" - def _find_release_channel_possible_candidate(self): - """Filter channels: make sure shipment date is in Partner window - :return: release channels - """ - channels = super()._find_release_channel_possible_candidate() - channels = channels.filter_release_channel_partner_window(self, self.partner_id) - return channels + @property + def _release_channel_possible_candidate_domain_partner_delivery_window(self): + """The delivery date must be on a partner open day""" + return [ + "|", + ("respect_partner_delivery_time_windows", "=", False), + ( + "delivery_date_weekday", + "in", + list(self.partner_id.delivery_time_weekdays), + ), + ] + + @property + def _release_channel_possible_candidate_domain_extras(self): + domains = super()._release_channel_possible_candidate_domain_extras + domains.append( + self._release_channel_possible_candidate_domain_partner_delivery_window + ) + return domains diff --git a/stock_release_channel_partner_delivery_window/models/stock_release_channel.py b/stock_release_channel_partner_delivery_window/models/stock_release_channel.py index 33ed84b3e19..7e37d953828 100644 --- a/stock_release_channel_partner_delivery_window/models/stock_release_channel.py +++ b/stock_release_channel_partner_delivery_window/models/stock_release_channel.py @@ -1,5 +1,9 @@ +# Copyright 2025 Jacques-Etienne Baudoux (BCIM) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -from odoo import fields, models + +from datetime import datetime + +from odoo import api, fields, models class StockReleaseChannel(models.Model): @@ -16,27 +20,95 @@ class StockReleaseChannel(models.Model): ), ) - def filter_release_channel_partner_window(self, picking, partner): - channels = self - if ( - not partner.delivery_time_preference - or partner.delivery_time_preference == "anytime" - ): - return channels + delivery_date_weekday = fields.Integer( + compute="_compute_delivery_date_weekday", + store=True, + ) + # Migration note: shipment_date will be renamed to delivery_date + @api.depends( + "shipment_date", + ) + def _compute_delivery_date_weekday(self): for channel in self: - if not channel.shipment_date: - continue - shipment_datetime = fields.Datetime.to_datetime(channel.shipment_date) - if channel.process_end_date: - shipment_datetime = shipment_datetime.replace( - hour=channel.process_end_date.hour, - minute=channel.process_end_date.minute, + if channel.shipment_date: + channel.delivery_date_weekday = channel.shipment_date.weekday() + else: + channel.delivery_date_weekday = -1 + + @property + def _delivery_date_generators(self): + d = super()._delivery_date_generators + d["customer"].append(self._next_delivery_date_partner_delivery_window) + return d + + def _next_delivery_date_partner_delivery_window(self, delivery_date, partner): + """Get the next valid delivery date respecting customer delivery window. + + The delivery date must be when the customer is open. + From the initial delivery_date, if the customer is not open on that + date and time, postpone to the start of the next open window. + + A delivery date generator needs to provide the earliest valid date + starting from the received date. It can be called multiple times with a + new date to validate. + """ + self.ensure_one() + partner.ensure_one() + if not self.respect_partner_delivery_time_windows: + while True: + delivery_date = yield delivery_date + + if partner.delivery_time_preference == "anytime": + # no constrain, any date is valid + while True: + delivery_date = yield delivery_date + + tz = partner.tz + if partner.delivery_time_preference == "workdays": + # postpone to Monday when date is on a week-end + while True: + delivery_date_tz = self._localize(delivery_date, tz=tz) + # postpone on Monday if Sat or Sun + if delivery_date_tz.weekday() < 5: # Mon-Fri + delivery_date = yield delivery_date + continue + days = 0 + if delivery_date_tz.weekday() == 5: # Sat + days = 1 + elif delivery_date_tz.weekday() == 6: # Sun + days = 2 + delivery_date_tz = fields.Datetime.add(delivery_date_tz, days=days) + delivery_date = self._naive(delivery_date_tz, reset_time=days) + delivery_date = yield delivery_date + + while True: + # yield first delivery window + delivery_date_tz = self._localize(delivery_date, tz=tz) + weekday = delivery_date_tz.weekday() + for inc in range(8): + # Each weekday is tested to find a window. + # On the first day, we need a window that ends after current + # delivery time. Afterwards, we just need a window. + windows = partner.delivery_time_window_ids.filtered( + lambda w: str(weekday + inc) + in w.time_window_weekday_ids.mapped("name") + and (inc or w.get_time_window_end_time() >= delivery_date_tz.time()) ) - if ( - channel.respect_partner_delivery_time_windows - and not picking.sale_id.commitment_date - and not partner.is_in_delivery_window(shipment_datetime) - ): - channels -= channel - return channels + if windows: + w = windows[0] + break + else: + # There is no time window, we consider any date valid + while True: + delivery_date = yield delivery_date + # Postpone the delivery date to that found window + delivery_date_tz = datetime.combine( + (fields.Datetime.add(delivery_date_tz, days=inc)).date(), + max(w.get_time_window_start_time(), delivery_date_tz.time()) + if not inc + else w.get_time_window_start_time(), + tzinfo=delivery_date_tz.tzinfo, + ) + delivery_date = self._naive(delivery_date_tz) + delivery_date = yield delivery_date diff --git a/stock_release_channel_partner_delivery_window/static/description/index.html b/stock_release_channel_partner_delivery_window/static/description/index.html index 582c76d0473..bbe8b3ee0bf 100644 --- a/stock_release_channel_partner_delivery_window/static/description/index.html +++ b/stock_release_channel_partner_delivery_window/static/description/index.html @@ -1,18 +1,18 @@ - -Stock Release Channel Partner Delivery Window +README.rst -
    -

    Stock Release Channel Partner Delivery Window

    +
    + + +Odoo Community Association + +
    +

    Stock Release Channel Partner Delivery Window

    -

    Beta License: AGPL-3 OCA/wms Translate me on Weblate Try me on Runboat

    +

    Beta License: AGPL-3 OCA/wms Translate me on Weblate Try me on Runboat

    This module excludes the channel when its shipment date is not in Partner delivery window

    Table of contents

    @@ -385,7 +390,7 @@

    Stock Release Channel Partner Delivery Window

    -

    Bug Tracker

    +

    Bug Tracker

    Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed @@ -393,29 +398,31 @@

    Bug Tracker

    Do not contact contributors directly about support or help with technical issues.

    -

    Credits

    +

    Credits

    -

    Authors

    +

    Authors

    • Camptocamp
    • BCIM
    -

    Contributors

    +

    Contributors

    -

    Other credits

    +

    Other credits

    The development of this module has been financially supported by Camptocamp

    -

    Maintainers

    +

    Maintainers

    This module is maintained by the OCA.

    -Odoo Community Association + +Odoo Community Association +

    OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use.

    @@ -426,5 +433,6 @@

    Maintainers

    +
    diff --git a/stock_release_channel_partner_delivery_window/tests/test_release_window.py b/stock_release_channel_partner_delivery_window/tests/test_release_window.py index a06a73fb78e..383bbfcc5ba 100644 --- a/stock_release_channel_partner_delivery_window/tests/test_release_window.py +++ b/stock_release_channel_partner_delivery_window/tests/test_release_window.py @@ -1,3 +1,4 @@ +# Copyright 2025 Jacques-Etienne Baudoux (BCIM) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from datetime import timedelta @@ -7,6 +8,8 @@ from odoo.addons.stock_release_channel.tests.common import ChannelReleaseCase +to_datetime = fields.Datetime.to_datetime + class ReleaseChannelEndDateCase(ChannelReleaseCase): @classmethod @@ -28,8 +31,8 @@ def setUpClass(cls): 0, 0, { - "time_window_start": 0.00, - "time_window_end": 23.99, + "time_window_start": 8.00, + "time_window_end": 18.50, "time_window_weekday_ids": [ ( 6, @@ -103,3 +106,28 @@ def test_assign_channel_no_respect_delivery_time_window(self): self.picking.partner_id = self.customer_time_window self._assign_picking(self.picking) self.assertEqual(self.channel, self.picking.release_channel_id) + + def test_delivery_date_partner_time_window_workdays(self): + # before opening + dt = to_datetime("2025-01-01 05:00:00") # Wed + gen = self.channel._next_delivery_date_partner_delivery_window( + dt, self.customer_time_window + ) + result = next(gen) + opening = to_datetime("2025-01-02 08:00:00") # Thu + self.assertEqual(result, opening) + result = gen.send(result) + self.assertEqual(result, opening) + # during opening + dt = to_datetime("2025-01-02 09:00:00") + result = gen.send(dt) + self.assertEqual(result, dt) + result = gen.send(result) + self.assertEqual(result, dt) + # after opening + dt = to_datetime("2025-01-02 19:00:00") + result = gen.send(dt) + next_opening = to_datetime("2025-01-04 08:00:00") # Sat + self.assertEqual(result, next_opening) + result = gen.send(result) + self.assertEqual(result, next_opening) diff --git a/stock_release_channel_partner_public_holidays/README.rst b/stock_release_channel_partner_public_holidays/README.rst index fbfcf24e26d..a98d983e2ae 100644 --- a/stock_release_channel_partner_public_holidays/README.rst +++ b/stock_release_channel_partner_public_holidays/README.rst @@ -1,3 +1,7 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + ============================================= Stock Release Channel Partner Public Holidays ============================================= @@ -7,13 +11,13 @@ Stock Release Channel Partner Public Holidays !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:7da4c7a1a2899c4f5465d2194105ab0242c4ea9ed32212592dfbb9aed03aebbe + !! source digest: sha256:477d3e7303508b90a197c9964ea9e067b6597e8504d6d8811f114cdd80ecfd98 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png :target: https://odoo-community.org/page/development-status :alt: Beta -.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png +.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fwms-lightgray.png?logo=github @@ -63,6 +67,7 @@ Authors Contributors ~~~~~~~~~~~~ +* Jacques-Etienne Baudoux (BCIM) * Nguyen Minh Chien Maintainers diff --git a/stock_release_channel_partner_public_holidays/__manifest__.py b/stock_release_channel_partner_public_holidays/__manifest__.py index 3a88823f9df..c4689a133c1 100644 --- a/stock_release_channel_partner_public_holidays/__manifest__.py +++ b/stock_release_channel_partner_public_holidays/__manifest__.py @@ -4,7 +4,7 @@ "name": "Stock Release Channel Partner Public Holidays", "summary": """ Add an option to exclude the public holidays when assigning th release channel""", - "version": "16.0.1.0.0", + "version": "16.0.2.1.0", "license": "AGPL-3", "maintainers": ["jbaudoux"], "author": "Camptocamp, BCIM, Odoo Community Association (OCA)", diff --git a/stock_release_channel_partner_public_holidays/models/stock_picking.py b/stock_release_channel_partner_public_holidays/models/stock_picking.py index 071dc674a77..b51fd83bd1a 100644 --- a/stock_release_channel_partner_public_holidays/models/stock_picking.py +++ b/stock_release_channel_partner_public_holidays/models/stock_picking.py @@ -1,15 +1,35 @@ +# Copyright 2025 Jacques-Etienne Baudoux (BCIM) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -from odoo import models + +from datetime import timedelta + +from odoo import fields, models class StockPicking(models.Model): _inherit = "stock.picking" - def _find_release_channel_possible_candidate(self): - """Filter channels: make sure shipment date is not a public holiday of Partner - :return: release channels - """ - channels = super()._find_release_channel_possible_candidate() - channels = channels.filter_release_channel(self.partner_id) - return channels + @property + def _release_channel_possible_candidate_domain_partner_public_holidays(self): + now = fields.Datetime.now() + all_holidays = self.env["hr.holidays.public"].get_holidays_list( + start_dt=now, + end_dt=now + timedelta(365), + partner_id=self.partner_id.id, + ) + if not all_holidays: + return [] + return [ + "|", + ("exclude_public_holidays", "=", False), + ("shipment_date", "not in", all_holidays.mapped("date")), + ] + + @property + def _release_channel_possible_candidate_domain_extras(self): + domains = super()._release_channel_possible_candidate_domain_extras + domains.append( + self._release_channel_possible_candidate_domain_partner_public_holidays + ) + return domains diff --git a/stock_release_channel_partner_public_holidays/models/stock_release_channel.py b/stock_release_channel_partner_public_holidays/models/stock_release_channel.py index 99fecdefc51..a64f5e235ba 100644 --- a/stock_release_channel_partner_public_holidays/models/stock_release_channel.py +++ b/stock_release_channel_partner_public_holidays/models/stock_release_channel.py @@ -1,4 +1,8 @@ +# Copyright 2025 Jacques-Etienne Baudoux (BCIM) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from datetime import timedelta + from odoo import fields, models @@ -8,32 +12,42 @@ class StockReleaseChannel(models.Model): exclude_public_holidays = fields.Boolean() - def filter_release_channel(self, partner): - channels = self - for channel in self: - if not channel.exclude_public_holidays: - continue - if channel._is_shipment_date_a_public_holiday(partner): - channels -= channel - return channels + @property + def _delivery_date_generators(self): + d = super()._delivery_date_generators + d["customer"].append(self._next_delivery_date_partner_public_holiday) + return d - def _is_shipment_date_a_public_holiday(self, partner): - """ - Returns True if shipment_date is a public holiday - :return: bool + def _next_delivery_date_partner_public_holiday(self, delivery_date, partner): + """Get the next valid delivery date respecting cutoff. + + The delivery date must not be a public holiday otherwise it is + postponed to next open day. + + A delivery date generator needs to provide the earliest valid date + starting from the received date. It can be called multiple times with a + new date to validate. """ self.ensure_one() - res = False - shipment_date = self.shipment_date - if not shipment_date: - return res - domain = [ - ("year_id.country_id", "in", (False, partner.country_id.id)), - "|", - ("state_ids", "=", False), - ("state_ids", "=", partner.state_id.id), - ("date", "=", shipment_date), - ] - hhplo = self.env["hr.holidays.public.line"] - holidays_line = hhplo.search(domain, limit=1, order="id") - return bool(holidays_line) + partner.ensure_one() + + if not self.exclude_public_holidays: + while True: + delivery_date = yield delivery_date + + batch_delta = timedelta(days=61) + delivery_date_tz = self._localize(delivery_date, tz=partner.tz) + while True: + end_dt_tz = delivery_date_tz + batch_delta + all_holidays = self.env["hr.holidays.public"].get_holidays_list( + start_dt=delivery_date_tz, end_dt=end_dt_tz, partner_id=partner.id + ) + while delivery_date_tz <= end_dt_tz: + if delivery_date_tz.date() not in all_holidays.mapped("date"): + delivery_date = yield self._naive(delivery_date_tz) + delivery_date_tz = self._localize(delivery_date, tz=partner.tz) + else: + delivery_date_tz += timedelta(days=1) + delivery_date_tz = delivery_date_tz.replace( + hour=0, minute=0, second=0, microsecond=0 + ) diff --git a/stock_release_channel_partner_public_holidays/readme/CONTRIBUTORS.rst b/stock_release_channel_partner_public_holidays/readme/CONTRIBUTORS.rst index 9873004f883..2ac0f1f7ff3 100644 --- a/stock_release_channel_partner_public_holidays/readme/CONTRIBUTORS.rst +++ b/stock_release_channel_partner_public_holidays/readme/CONTRIBUTORS.rst @@ -1 +1,2 @@ +* Jacques-Etienne Baudoux (BCIM) * Nguyen Minh Chien diff --git a/stock_release_channel_partner_public_holidays/static/description/index.html b/stock_release_channel_partner_public_holidays/static/description/index.html index fc747ca0f12..d23603d17e3 100644 --- a/stock_release_channel_partner_public_holidays/static/description/index.html +++ b/stock_release_channel_partner_public_holidays/static/description/index.html @@ -1,20 +1,20 @@ - - -Stock Release Channel Partner Public Holidays + +README.rst -
    -

    Stock Release Channel Partner Public Holidays

    +
    + + +Odoo Community Association + +
    +

    Stock Release Channel Partner Public Holidays

    -

    Beta License: AGPL-3 OCA/wms Translate me on Weblate Try me on Runboat

    +

    Beta License: AGPL-3 OCA/wms Translate me on Weblate Try me on Runboat

    This module adds an option to exclude the channel when its shipment date is a public holiday

    Table of contents

    -

    Usage

    +

    Usage

    1. Go To Release Channels
    2. on Tab Selection criteria: Tick on the option Exclude Public Holidays?
    -

    Bug Tracker

    +

    Bug Tracker

    Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed @@ -400,33 +405,37 @@

    Bug Tracker

    Do not contact contributors directly about support or help with technical issues.

    -

    Credits

    +

    Credits

    -

    Authors

    +

    Authors

    • Camptocamp
    • BCIM
    -

    Contributors

    +

    Contributors

    -

    Maintainers

    +

    Maintainers

    This module is maintained by the OCA.

    -Odoo Community Association + +Odoo Community Association +

    OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use.

    Current maintainer:

    -

    jbaudoux

    +

    jbaudoux

    This module is part of the OCA/wms project on GitHub.

    You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

    +
    diff --git a/stock_release_channel_partner_public_holidays/tests/test_release_on_holiday.py b/stock_release_channel_partner_public_holidays/tests/test_release_on_holiday.py index c4dcdff36de..d8aebf49b3f 100644 --- a/stock_release_channel_partner_public_holidays/tests/test_release_on_holiday.py +++ b/stock_release_channel_partner_public_holidays/tests/test_release_on_holiday.py @@ -1,3 +1,4 @@ +# Copyright 2025 Jacques-Etienne Baudoux (BCIM) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from datetime import timedelta @@ -7,6 +8,8 @@ from odoo.addons.stock_release_channel.tests.common import ChannelReleaseCase +to_datetime = fields.Datetime.to_datetime + class ReleaseChannelEndDateCase(ChannelReleaseCase): @classmethod @@ -58,3 +61,23 @@ def test_assign_channel(self): self.picking.partner_id.state_id = self.env.ref("base.state_us_35") self._assign_picking(self.picking) self.assertNotEqual(self.channel, self.picking.release_channel_id) + + @freeze_time("2023-09-01") + def test_delivery_date_public_holiday(self): + partner = self.picking.partner_id + self.channel.exclude_public_holidays = True + partner.tz = "Europe/Brussels" + dt = to_datetime("2023-09-17 08:00:00") + gen = self.channel._next_delivery_date_partner_public_holiday(dt, partner) + # not an holiday + result = next(gen) + self.assertEqual(result, dt) + result = gen.send(dt) + self.assertEqual(result, dt) + # an holiday + dt = to_datetime("2023-09-18 08:00:00") + result = gen.send(dt) + next_day = to_datetime("2023-09-18 22:00:00") + self.assertEqual(result, next_day) + result = gen.send(dt) + self.assertEqual(result, next_day) diff --git a/stock_release_channel_plan_depot/README.rst b/stock_release_channel_plan_depot/README.rst new file mode 100644 index 00000000000..ebd895a390c --- /dev/null +++ b/stock_release_channel_plan_depot/README.rst @@ -0,0 +1,77 @@ +================================ +Stock Release Channel Plan Depot +================================ + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:0f2f6cca378fe4e53df17bcfe21853e2fc2baf2bf6b0fccdab9f0d9ed54c92bf + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fwms-lightgray.png?logo=github + :target: https://github.com/OCA/wms/tree/16.0/stock_release_channel_plan_depot + :alt: OCA/wms +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/wms-16-0/wms-16-0-stock_release_channel_plan_depot + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/wms&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Add partner depot to stock release channel preparation plan + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Camptocamp + +Contributors +~~~~~~~~~~~~ + +* Telmo Santos +* Jacques-Etienne Baudoux + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/wms `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/stock_release_channel_plan_depot/__init__.py b/stock_release_channel_plan_depot/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/stock_release_channel_plan_depot/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/stock_release_channel_plan_depot/__manifest__.py b/stock_release_channel_plan_depot/__manifest__.py new file mode 100644 index 00000000000..12945c0a1c2 --- /dev/null +++ b/stock_release_channel_plan_depot/__manifest__.py @@ -0,0 +1,16 @@ +# Copyright 2024 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +{ + "name": "Stock Release Channel Plan Depot", + "summary": """This module allows users to set partner depot on + stock release channel preparation plan.""", + "version": "16.0.1.0.0", + "license": "AGPL-3", + "author": "Camptocamp, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/wms", + "depends": ["stock_depot", "stock_release_channel_plan"], + "data": [ + "views/stock_release_channel_preparation_plan.xml", + ], +} diff --git a/stock_release_channel_plan_depot/i18n/fr.po b/stock_release_channel_plan_depot/i18n/fr.po new file mode 100644 index 00000000000..9eabceef487 --- /dev/null +++ b/stock_release_channel_plan_depot/i18n/fr.po @@ -0,0 +1,21 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * stock_release_channel_plan_depot +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-07-04 06:21+0000\n" +"PO-Revision-Date: 2024-07-04 06:21+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: stock_release_channel_plan_depot +#: model:ir.model.fields,field_description:stock_release_channel_plan_depot.field_stock_release_channel_preparation_plan__depot_id +msgid "Depot" +msgstr "Dépôt" diff --git a/stock_release_channel_plan_depot/i18n/it.po b/stock_release_channel_plan_depot/i18n/it.po new file mode 100644 index 00000000000..022c06e6973 --- /dev/null +++ b/stock_release_channel_plan_depot/i18n/it.po @@ -0,0 +1,27 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * stock_release_channel_plan_depot +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2025-05-22 10:26+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.10.4\n" + +#. module: stock_release_channel_plan_depot +#: model:ir.model.fields,field_description:stock_release_channel_plan_depot.field_stock_release_channel_preparation_plan__depot_id +msgid "Depot" +msgstr "Deposito" + +#. module: stock_release_channel_plan_depot +#: model:ir.model,name:stock_release_channel_plan_depot.model_stock_release_channel_preparation_plan +msgid "Stock Release Channel Preparation Plan" +msgstr "Piano preparazione canale rilascio magazzino" diff --git a/stock_release_channel_plan_depot/i18n/stock_release_channel_plan_depot.pot b/stock_release_channel_plan_depot/i18n/stock_release_channel_plan_depot.pot new file mode 100644 index 00000000000..0e7b8a7e985 --- /dev/null +++ b/stock_release_channel_plan_depot/i18n/stock_release_channel_plan_depot.pot @@ -0,0 +1,24 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * stock_release_channel_plan_depot +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: stock_release_channel_plan_depot +#: model:ir.model.fields,field_description:stock_release_channel_plan_depot.field_stock_release_channel_preparation_plan__depot_id +msgid "Depot" +msgstr "" + +#. module: stock_release_channel_plan_depot +#: model:ir.model,name:stock_release_channel_plan_depot.model_stock_release_channel_preparation_plan +msgid "Stock Release Channel Preparation Plan" +msgstr "" diff --git a/stock_release_channel_plan_depot/models/__init__.py b/stock_release_channel_plan_depot/models/__init__.py new file mode 100644 index 00000000000..57926ea54ca --- /dev/null +++ b/stock_release_channel_plan_depot/models/__init__.py @@ -0,0 +1 @@ +from . import stock_release_channel_preparation_plan diff --git a/stock_release_channel_plan_depot/models/stock_release_channel_preparation_plan.py b/stock_release_channel_plan_depot/models/stock_release_channel_preparation_plan.py new file mode 100644 index 00000000000..9a4e30b5fb7 --- /dev/null +++ b/stock_release_channel_plan_depot/models/stock_release_channel_preparation_plan.py @@ -0,0 +1,10 @@ +# Copyright 2024 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models + + +class StockReleaseChannelPreparationPlan(models.Model): + _inherit = "stock.release.channel.preparation.plan" + + depot_id = fields.Many2one("stock.depot") diff --git a/stock_release_channel_plan_depot/readme/CONTRIBUTORS.rst b/stock_release_channel_plan_depot/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000000..22612a25009 --- /dev/null +++ b/stock_release_channel_plan_depot/readme/CONTRIBUTORS.rst @@ -0,0 +1,2 @@ +* Telmo Santos +* Jacques-Etienne Baudoux diff --git a/stock_release_channel_plan_depot/readme/DESCRIPTION.rst b/stock_release_channel_plan_depot/readme/DESCRIPTION.rst new file mode 100644 index 00000000000..99ae5360d35 --- /dev/null +++ b/stock_release_channel_plan_depot/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +Add partner depot to stock release channel preparation plan diff --git a/stock_release_channel_plan_depot/static/description/icon.png b/stock_release_channel_plan_depot/static/description/icon.png new file mode 100644 index 00000000000..1dcc49c24f3 Binary files /dev/null and b/stock_release_channel_plan_depot/static/description/icon.png differ diff --git a/stock_release_channel_plan_depot/static/description/index.html b/stock_release_channel_plan_depot/static/description/index.html new file mode 100644 index 00000000000..48c6163c4f1 --- /dev/null +++ b/stock_release_channel_plan_depot/static/description/index.html @@ -0,0 +1,424 @@ + + + + + +Stock Release Channel Plan Depot + + + +
    +

    Stock Release Channel Plan Depot

    + + +

    Beta License: AGPL-3 OCA/wms Translate me on Weblate Try me on Runboat

    +

    Add partner depot to stock release channel preparation plan

    +

    Table of contents

    + +
    +

    Bug Tracker

    +

    Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

    +

    Do not contact contributors directly about support or help with technical issues.

    +
    +
    +

    Credits

    +
    +

    Authors

    +
      +
    • Camptocamp
    • +
    +
    +
    +

    Contributors

    + +
    +
    +

    Maintainers

    +

    This module is maintained by the OCA.

    + +Odoo Community Association + +

    OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

    +

    This module is part of the OCA/wms project on GitHub.

    +

    You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

    +
    +
    +
    + + diff --git a/stock_release_channel_plan_depot/views/stock_release_channel_preparation_plan.xml b/stock_release_channel_plan_depot/views/stock_release_channel_preparation_plan.xml new file mode 100644 index 00000000000..d0133fee2f5 --- /dev/null +++ b/stock_release_channel_plan_depot/views/stock_release_channel_preparation_plan.xml @@ -0,0 +1,44 @@ + + + + + stock.release.channel.preparation.plan.form (in stock_release_channel_plan_depot) + stock.release.channel.preparation.plan + + + + + + + + + + + + + + + stock.release.channel.preparation.plan.tree (in stock_release_channel_plan_depot) + stock.release.channel.preparation.plan + + + + + + + + + diff --git a/stock_release_channel_plan_shipment_lead_time/README.rst b/stock_release_channel_plan_shipment_lead_time/README.rst index ce2ce557635..3de5c539b75 100644 --- a/stock_release_channel_plan_shipment_lead_time/README.rst +++ b/stock_release_channel_plan_shipment_lead_time/README.rst @@ -1,3 +1,7 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + ============================================= Stock release channel plan shipment lead time ============================================= @@ -7,13 +11,13 @@ Stock release channel plan shipment lead time !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:e0e7fe225eb4c28b94ac0f956eb4cb837566524e04a5766633bb5d5eea00f87c + !! source digest: sha256:c0bbd5b6dd47084d43073a117ef85911df2c73869a7f01e53b6da789c0fd48f9 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png :target: https://odoo-community.org/page/development-status :alt: Beta -.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png +.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fwms-lightgray.png?logo=github diff --git a/stock_release_channel_plan_shipment_lead_time/__manifest__.py b/stock_release_channel_plan_shipment_lead_time/__manifest__.py index bcedbc28820..68f54601ae2 100644 --- a/stock_release_channel_plan_shipment_lead_time/__manifest__.py +++ b/stock_release_channel_plan_shipment_lead_time/__manifest__.py @@ -3,7 +3,7 @@ { "name": "Stock release channel plan shipment lead time", "summary": "Stock release channel plan shipment lead time", - "version": "16.0.1.0.0", + "version": "16.0.1.1.0", "development_status": "Beta", "category": "Uncategorized", "website": "https://github.com/OCA/wms", diff --git a/stock_release_channel_plan_shipment_lead_time/models/stock_release_channel.py b/stock_release_channel_plan_shipment_lead_time/models/stock_release_channel.py index 67d63fca9d8..780f5c7af6f 100644 --- a/stock_release_channel_plan_shipment_lead_time/models/stock_release_channel.py +++ b/stock_release_channel_plan_shipment_lead_time/models/stock_release_channel.py @@ -1,6 +1,9 @@ # Copyright 2024 Camptocamp SA +# Copyright 2025 Jacques-Etienne Baudoux (BCIM) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +from datetime import datetime, timedelta + from odoo import api, fields, models @@ -52,3 +55,41 @@ def _compute_preparation_weekday_ids(self): channel.preparation_weekday_ids = self.env["time.weekday"].search( [("name", "in", weekday_names)] ) + + @property + def _delivery_date_generators(self): + d = super()._delivery_date_generators + d["preparation"].append(self._next_delivery_date_plan_weekdays) + return d + + def _next_delivery_date_plan_weekdays(self, delivery_date, partner=None): + """Get the next valid delivery date respecting plan weekdays. + + The preparation date must be a plan preparation weekday. + We do not consider the delivery weekday as it could be postponed with + leaves. + + A delivery date generator needs to provide the earliest valid date + starting from the received date. It can be called multiple times with a + new date to validate. + """ + self.ensure_one() + if not self.preparation_weekday_ids: + while True: + delivery_date = yield delivery_date + while True: + delivery_date_tz = self._localize(delivery_date) + weekday = delivery_date_tz.weekday() + for inc in range(8): + inc_weekday = (inc + weekday) % 7 + if str(inc_weekday) in self.preparation_weekday_ids.mapped("name"): + break + else: + raise Exception("delivery date plan weekdays internal error") + delivery_date_tz = datetime.combine( + (delivery_date_tz + timedelta(days=inc)).date(), + delivery_date_tz.time() if not inc else datetime.min.time(), + tzinfo=delivery_date_tz.tzinfo, + ) + delivery_date = self._naive(delivery_date_tz) + delivery_date = yield delivery_date diff --git a/stock_release_channel_plan_shipment_lead_time/static/description/index.html b/stock_release_channel_plan_shipment_lead_time/static/description/index.html index de88b873543..0fc8d468253 100644 --- a/stock_release_channel_plan_shipment_lead_time/static/description/index.html +++ b/stock_release_channel_plan_shipment_lead_time/static/description/index.html @@ -3,7 +3,7 @@ -Stock release channel plan shipment lead time +README.rst -
    -

    Stock release channel plan shipment lead time

    +
    + + +Odoo Community Association + +
    +

    Stock release channel plan shipment lead time

    -

    Beta License: AGPL-3 OCA/wms Translate me on Weblate Try me on Runboat

    +

    Beta License: AGPL-3 OCA/wms Translate me on Weblate Try me on Runboat

    When the release channel has a delivery lead time, visualize the preparation days based on the delivery days and the lead time. When creating your preparation plan, you can see which channel have to be prepared on which day.

    @@ -386,7 +391,7 @@

    Stock release channel plan shipment lead time

    -

    Bug Tracker

    +

    Bug Tracker

    Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed @@ -394,23 +399,23 @@

    Bug Tracker

    Do not contact contributors directly about support or help with technical issues.

    -

    Credits

    +

    Credits

    -

    Authors

    +

    Authors

    • Camptocamp
    • BCIM
    -

    Contributors

    +

    Contributors

    -

    Maintainers

    +

    Maintainers

    This module is maintained by the OCA.

    Odoo Community Association @@ -425,5 +430,6 @@

    Maintainers

    +
    diff --git a/stock_release_channel_plan_shipment_lead_time/tests/test_lead_time_weekday.py b/stock_release_channel_plan_shipment_lead_time/tests/test_lead_time_weekday.py index d8c37e6b92a..ce1171d9330 100644 --- a/stock_release_channel_plan_shipment_lead_time/tests/test_lead_time_weekday.py +++ b/stock_release_channel_plan_shipment_lead_time/tests/test_lead_time_weekday.py @@ -2,10 +2,13 @@ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) from freezegun import freeze_time +from odoo import fields from odoo.fields import Command from odoo.addons.stock_release_channel.tests.common import ReleaseChannelCase +to_datetime = fields.Datetime.to_datetime + class TestReleaseChannelLeadTimeWeekday(ReleaseChannelCase): @classmethod @@ -153,3 +156,22 @@ def test_preparation_weekday_without_calendar(self): {"delivery_weekday_ids": [Command.link(self.thursday.id)]} ) self.assertIn(self.monday, self.default_channel.preparation_weekday_ids) + + @freeze_time("2025-01-02") + def test_delivery_date_plan_weekdays(self): + self.default_channel.write({"delivery_weekday_ids": [Command.clear()]}) + self.default_channel.shipment_lead_time = 2 + self.default_channel.write( + {"delivery_weekday_ids": [Command.link(self.wednesday.id)]} + ) + dt = fields.Datetime.now() # Thursday + gen = self.default_channel._next_delivery_date_plan_weekdays(dt) + # next preparation date is on next Monday (Wed -2d lead time) + result = next(gen) + next_mon = to_datetime("2025-01-06 00:00:00") + self.assertEqual(result, next_mon) + result = gen.send(next_mon) + self.assertEqual(result, next_mon) + # if we add 1 day, the next preparation date is 1 week later + result = gen.send(fields.Datetime.add(next_mon, days=1)) + self.assertEqual(result, fields.Datetime.add(next_mon, weeks=1)) diff --git a/stock_release_channel_shipment_advice_deliver/README.rst b/stock_release_channel_shipment_advice_deliver/README.rst index 986a237056e..9d57b02e352 100644 --- a/stock_release_channel_shipment_advice_deliver/README.rst +++ b/stock_release_channel_shipment_advice_deliver/README.rst @@ -1,3 +1,7 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + ============================================= Stock Release Channel Shipment Advice Deliver ============================================= @@ -7,13 +11,13 @@ Stock Release Channel Shipment Advice Deliver !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:dc1500cd3d3aeb4cc935b248aebbb30988b012245111ed6a0685e9bce156503d + !! source digest: sha256:f1e4d9a35298da170efb90de6c3d7779de88b1962bf059970e7a4d9ec4066ea6 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png :target: https://odoo-community.org/page/development-status :alt: Beta -.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png +.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fwms-lightgray.png?logo=github diff --git a/stock_release_channel_shipment_advice_deliver/__manifest__.py b/stock_release_channel_shipment_advice_deliver/__manifest__.py index 074ba3c2bb9..b0393fafcda 100644 --- a/stock_release_channel_shipment_advice_deliver/__manifest__.py +++ b/stock_release_channel_shipment_advice_deliver/__manifest__.py @@ -8,7 +8,7 @@ "author": "ACSONE SA/NV, BCIM, Odoo Community Association (OCA)", "website": "https://github.com/OCA/wms", "category": "Warehouse Management", - "version": "16.0.1.1.0", + "version": "16.0.2.0.2", "license": "AGPL-3", "depends": [ "stock_release_channel", diff --git a/stock_release_channel_shipment_advice_deliver/data/queue_job_function.xml b/stock_release_channel_shipment_advice_deliver/data/queue_job_function.xml index 65724046a64..722994cb509 100644 --- a/stock_release_channel_shipment_advice_deliver/data/queue_job_function.xml +++ b/stock_release_channel_shipment_advice_deliver/data/queue_job_function.xml @@ -2,14 +2,14 @@ - _action_deliver + _process_shipments diff --git a/stock_release_channel_shipment_advice_deliver/i18n/fr.po b/stock_release_channel_shipment_advice_deliver/i18n/fr.po index 2a3bbf39c81..9f6cb8a76f7 100644 --- a/stock_release_channel_shipment_advice_deliver/i18n/fr.po +++ b/stock_release_channel_shipment_advice_deliver/i18n/fr.po @@ -303,12 +303,10 @@ msgstr "" "remises en non libérées.
    \n" " Êtes-vous sûr de vouloir procéder à la livraison ?" -#. module: stock_release_channel_shipment_advice_deliver -#: model:ir.model,name:stock_release_channel_shipment_advice_deliver.model_stock_picking -msgid "Transfer" -msgstr "Transfert" - #. module: stock_release_channel_shipment_advice_deliver #: model:ir.model,name:stock_release_channel_shipment_advice_deliver.model_stock_release_channel_deliver_check_wizard msgid "stock release channel deliver check wizard" msgstr "" + +#~ msgid "Transfer" +#~ msgstr "Transfert" diff --git a/stock_release_channel_shipment_advice_deliver/i18n/it.po b/stock_release_channel_shipment_advice_deliver/i18n/it.po index 3e28f336306..98f67989500 100644 --- a/stock_release_channel_shipment_advice_deliver/i18n/it.po +++ b/stock_release_channel_shipment_advice_deliver/i18n/it.po @@ -195,7 +195,8 @@ msgstr "Nessun prelievo da consegnare per il canale %(name)s." #, python-format msgid "" "One of the delivery for channel %(name)s is waiting on another transfer. \n" -"Please finish it manually or cancel its start and done quantities to be able to deliver.\n" +"Please finish it manually or cancel its start and done quantities to be able " +"to deliver.\n" "%(pickings)s" msgstr "" "Una delle consegne per il canale %(name)s è in attesa di un altro " @@ -263,12 +264,16 @@ msgstr "" #: model:ir.model.fields,help:stock_release_channel_shipment_advice_deliver.field_stock_release_channel__state msgid "" "The state allows you to control the availability of the release channel.\n" -"* Open: Manual and automatic picking assignment to the release is effective and release operations are allowed.\n" -" * Locked: Release operations are forbidden. (Assignement processes are still working)\n" -"* Delivering: A background task is running to automatically deliver ready shipments\n" +"* Open: Manual and automatic picking assignment to the release is effective " +"and release operations are allowed.\n" +" * Locked: Release operations are forbidden. (Assignement processes are " +"still working)\n" +"* Delivering: A background task is running to automatically deliver ready " +"shipments\n" "* Delivering Error: An error occurred in the delivery background task\n" "* Delivered: Ready transfers are delivered\n" -"* Asleep: Assigned pickings not processed are unassigned from the release channel.\n" +"* Asleep: Assigned pickings not processed are unassigned from the release " +"channel.\n" msgstr "" "Lo stato consente di controllare la disponibilità del canale di rilascio.\n" "* Apri: l'assegnazione manuale e automatica di raccolta al rilascio è " @@ -287,7 +292,8 @@ msgstr "" #: model_terms:ir.ui.view,arch_db:stock_release_channel_shipment_advice_deliver.stock_release_channel_deliver_check_wizard_form_view msgid "" "There are some preparations that have not been completed.\n" -" If you choose to proceed, these preparations will be unreleased.
    \n" +" If you choose to proceed, these preparations will be " +"unreleased.
    \n" " Are you sure you want to proceed with the delivery?" msgstr "" "Ci sono alcuni preparativi che non sono stati completati.\n" @@ -295,12 +301,10 @@ msgstr "" "saranno pubblicati.
    \n" " Si è sicuri di voler procedere con la consegna?" -#. module: stock_release_channel_shipment_advice_deliver -#: model:ir.model,name:stock_release_channel_shipment_advice_deliver.model_stock_picking -msgid "Transfer" -msgstr "Trasferimento" - #. module: stock_release_channel_shipment_advice_deliver #: model:ir.model,name:stock_release_channel_shipment_advice_deliver.model_stock_release_channel_deliver_check_wizard msgid "stock release channel deliver check wizard" msgstr "procedura guidata controllo consegna canale rilascio magazzino" + +#~ msgid "Transfer" +#~ msgstr "Trasferimento" diff --git a/stock_release_channel_shipment_advice_deliver/i18n/stock_release_channel_shipment_advice_deliver.pot b/stock_release_channel_shipment_advice_deliver/i18n/stock_release_channel_shipment_advice_deliver.pot index d3f3034a8ef..92a3a8cfc95 100644 --- a/stock_release_channel_shipment_advice_deliver/i18n/stock_release_channel_shipment_advice_deliver.pot +++ b/stock_release_channel_shipment_advice_deliver/i18n/stock_release_channel_shipment_advice_deliver.pot @@ -261,11 +261,6 @@ msgid "" " Are you sure you want to proceed with the delivery?" msgstr "" -#. module: stock_release_channel_shipment_advice_deliver -#: model:ir.model,name:stock_release_channel_shipment_advice_deliver.model_stock_picking -msgid "Transfer" -msgstr "" - #. module: stock_release_channel_shipment_advice_deliver #: model:ir.model,name:stock_release_channel_shipment_advice_deliver.model_stock_release_channel_deliver_check_wizard msgid "stock release channel deliver check wizard" diff --git a/stock_release_channel_shipment_advice_deliver/models/__init__.py b/stock_release_channel_shipment_advice_deliver/models/__init__.py index de94e10bf67..187635e70de 100644 --- a/stock_release_channel_shipment_advice_deliver/models/__init__.py +++ b/stock_release_channel_shipment_advice_deliver/models/__init__.py @@ -1,3 +1,2 @@ from . import stock_release_channel from . import shipment_advice -from . import stock_picking diff --git a/stock_release_channel_shipment_advice_deliver/models/shipment_advice.py b/stock_release_channel_shipment_advice_deliver/models/shipment_advice.py index 213da9c246c..648cda1845d 100644 --- a/stock_release_channel_shipment_advice_deliver/models/shipment_advice.py +++ b/stock_release_channel_shipment_advice_deliver/models/shipment_advice.py @@ -76,8 +76,8 @@ def _auto_process(self): ) return True - def _postprocess_action_done(self): - res = super()._postprocess_action_done() + def _postprocess_action_done(self, backorder_policy): + res = super()._postprocess_action_done(backorder_policy) if not self.release_channel_id: return res if self.state == "error": diff --git a/stock_release_channel_shipment_advice_deliver/models/stock_picking.py b/stock_release_channel_shipment_advice_deliver/models/stock_picking.py deleted file mode 100644 index 6d72d492d60..00000000000 --- a/stock_release_channel_shipment_advice_deliver/models/stock_picking.py +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright 2023 ACSONE SA/NV -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). - -from odoo import models -from odoo.osv.expression import AND - - -class StockPicking(models.Model): - _inherit = "stock.picking" - - def _get_release_channel_possible_candidate_domain(self): - self.ensure_one() - domain = [("state", "not in", ("delivering", "delivering_error", "delivered"))] - return AND([super()._get_release_channel_possible_candidate_domain(), domain]) diff --git a/stock_release_channel_shipment_advice_deliver/static/description/index.html b/stock_release_channel_shipment_advice_deliver/static/description/index.html index 21dbe7e8e90..5ea0e1f9574 100644 --- a/stock_release_channel_shipment_advice_deliver/static/description/index.html +++ b/stock_release_channel_shipment_advice_deliver/static/description/index.html @@ -3,7 +3,7 @@ -Stock Release Channel Shipment Advice Deliver +README.rst -
    -

    Stock Release Channel Shipment Advice Deliver

    +
    + + +Odoo Community Association + +
    +

    Stock Release Channel Shipment Advice Deliver

    -

    Beta License: AGPL-3 OCA/wms Translate me on Weblate Try me on Runboat

    +

    Beta License: AGPL-3 OCA/wms Translate me on Weblate Try me on Runboat

    This module adds an action to the release channel to automate the delivery of its shippings through shipment advices.

    Table of contents

    @@ -385,7 +390,7 @@

    Stock Release Channel Shipment Advice Deliver

    -

    Usage

    +

    Usage

    A “Deliver” button for locked release channels is added.

    When this new button is pressed:
    @@ -413,7 +418,7 @@

    Usage

    -

    Bug Tracker

    +

    Bug Tracker

    Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed @@ -421,16 +426,16 @@

    Bug Tracker

    Do not contact contributors directly about support or help with technical issues.

    -

    Credits

    +

    Credits

    -

    Authors

    +

    Authors

    • ACSONE SA/NV
    • BCIM
    -

    Maintainers

    +

    Maintainers

    This module is maintained by the OCA.

    Odoo Community Association @@ -443,5 +448,6 @@

    Maintainers

    +
    diff --git a/stock_release_channel_shipment_advice_toursolver/README.rst b/stock_release_channel_shipment_advice_toursolver/README.rst index 0c6c7086ab5..cd7a54dbfae 100644 --- a/stock_release_channel_shipment_advice_toursolver/README.rst +++ b/stock_release_channel_shipment_advice_toursolver/README.rst @@ -1,3 +1,7 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + ================================================ Stock Release Channel Shipment Advice Toursolver ================================================ @@ -7,13 +11,13 @@ Stock Release Channel Shipment Advice Toursolver !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:cf692d6f796a74eace6fbaf8a76c1be68c27c5a0f90740956aaba1d4c807d669 + !! source digest: sha256:5ac3471d64445f78449a4a30d12717b0314774e114b146bcf9fe7a5b4183f680 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png :target: https://odoo-community.org/page/development-status :alt: Beta -.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png +.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fwms-lightgray.png?logo=github diff --git a/stock_release_channel_shipment_advice_toursolver/__manifest__.py b/stock_release_channel_shipment_advice_toursolver/__manifest__.py index bd6261e3cb4..bd38eb3be35 100644 --- a/stock_release_channel_shipment_advice_toursolver/__manifest__.py +++ b/stock_release_channel_shipment_advice_toursolver/__manifest__.py @@ -5,7 +5,7 @@ "name": "Stock Release Channel Shipment Advice Toursolver", "summary": """ Use TourSolver to plan shipment advices for ready and released pickings""", - "version": "16.0.1.0.1", + "version": "16.0.1.1.0", "license": "AGPL-3", "author": "ACSONE SA/NV,Odoo Community Association (OCA)", "website": "https://github.com/OCA/wms", diff --git a/stock_release_channel_shipment_advice_toursolver/i18n/it.po b/stock_release_channel_shipment_advice_toursolver/i18n/it.po index 7fb07878523..0c1b4998818 100644 --- a/stock_release_channel_shipment_advice_toursolver/i18n/it.po +++ b/stock_release_channel_shipment_advice_toursolver/i18n/it.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0\n" "Report-Msgid-Bugs-To: \n" -"PO-Revision-Date: 2024-04-22 09:34+0000\n" +"PO-Revision-Date: 2025-07-01 12:25+0000\n" "Last-Translator: mymage \n" "Language-Team: none\n" "Language: it\n" @@ -14,7 +14,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.17\n" +"X-Generator: Weblate 5.10.4\n" #. module: stock_release_channel_shipment_advice_toursolver #: model:ir.model.fields,field_description:stock_release_channel_shipment_advice_toursolver.field_stock_release_channel__delivery_resource_ids @@ -26,6 +26,11 @@ msgstr "Risorse consegna" msgid "Release Channel" msgstr "Canale rilascio" +#. module: stock_release_channel_shipment_advice_toursolver +#: model:ir.model,name:stock_release_channel_shipment_advice_toursolver.model_shipment_advice +msgid "Shipment Advice" +msgstr "Avviso spedizione" + #. module: stock_release_channel_shipment_advice_toursolver #: model:ir.model,name:stock_release_channel_shipment_advice_toursolver.model_shipment_advice_planner msgid "Shipment Advice Planner" diff --git a/stock_release_channel_shipment_advice_toursolver/i18n/stock_release_channel_shipment_advice_toursolver.pot b/stock_release_channel_shipment_advice_toursolver/i18n/stock_release_channel_shipment_advice_toursolver.pot index e4fcafe313d..13e05cbcfc8 100644 --- a/stock_release_channel_shipment_advice_toursolver/i18n/stock_release_channel_shipment_advice_toursolver.pot +++ b/stock_release_channel_shipment_advice_toursolver/i18n/stock_release_channel_shipment_advice_toursolver.pot @@ -23,6 +23,11 @@ msgstr "" msgid "Release Channel" msgstr "" +#. module: stock_release_channel_shipment_advice_toursolver +#: model:ir.model,name:stock_release_channel_shipment_advice_toursolver.model_shipment_advice +msgid "Shipment Advice" +msgstr "" + #. module: stock_release_channel_shipment_advice_toursolver #: model:ir.model,name:stock_release_channel_shipment_advice_toursolver.model_shipment_advice_planner msgid "Shipment Advice Planner" diff --git a/stock_release_channel_shipment_advice_toursolver/models/__init__.py b/stock_release_channel_shipment_advice_toursolver/models/__init__.py index 5943f65274a..24db19cf91c 100644 --- a/stock_release_channel_shipment_advice_toursolver/models/__init__.py +++ b/stock_release_channel_shipment_advice_toursolver/models/__init__.py @@ -1,2 +1,3 @@ from . import stock_release_channel from . import toursolver_task +from . import shipment_advice diff --git a/stock_release_channel_shipment_advice_toursolver/models/shipment_advice.py b/stock_release_channel_shipment_advice_toursolver/models/shipment_advice.py new file mode 100644 index 00000000000..0cb2ece2051 --- /dev/null +++ b/stock_release_channel_shipment_advice_toursolver/models/shipment_advice.py @@ -0,0 +1,18 @@ +# Copyright 2025 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import models + + +class ShipmentAdvice(models.Model): + + _inherit = "shipment.advice" + + def create_toursolver_task(self): + self.ensure_one() + res = super().create_toursolver_task() + task = self.toursolver_task_id + rc = self.release_channel_id + task.release_channel_id = rc + task.name = f"{task.name} {rc.name}" + return res diff --git a/stock_release_channel_shipment_advice_toursolver/static/description/index.html b/stock_release_channel_shipment_advice_toursolver/static/description/index.html index c1c88f40d04..d7bf61e8c04 100644 --- a/stock_release_channel_shipment_advice_toursolver/static/description/index.html +++ b/stock_release_channel_shipment_advice_toursolver/static/description/index.html @@ -3,7 +3,7 @@ -Stock Release Channel Shipment Advice Toursolver +README.rst -
    -

    Stock Release Channel Shipment Advice Toursolver

    +
    + + +Odoo Community Association + +
    +

    Stock Release Channel Shipment Advice Toursolver

    -

    Beta License: AGPL-3 OCA/wms Translate me on Weblate Try me on Runboat

    +

    Beta License: AGPL-3 OCA/wms Translate me on Weblate Try me on Runboat

    Use TourSolver to plan shipment advices for ready and released pickings.

    Table of contents

    @@ -384,7 +389,7 @@

    Stock Release Channel Shipment Advice Toursolver

    -

    Bug Tracker

    +

    Bug Tracker

    Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed @@ -392,22 +397,22 @@

    Bug Tracker

    Do not contact contributors directly about support or help with technical issues.

    -

    Credits

    +

    Credits

    -

    Authors

    +

    Authors

    • ACSONE SA/NV
    -

    Maintainers

    +

    Maintainers

    This module is maintained by the OCA.

    Odoo Community Association @@ -420,5 +425,6 @@

    Maintainers

    +
    diff --git a/stock_release_channel_shipment_advice_toursolver/tests/test_shipment_advice_planner_toursolver.py b/stock_release_channel_shipment_advice_toursolver/tests/test_shipment_advice_planner_toursolver.py index c6effc6d438..8205db3a2f5 100644 --- a/stock_release_channel_shipment_advice_toursolver/tests/test_shipment_advice_planner_toursolver.py +++ b/stock_release_channel_shipment_advice_toursolver/tests/test_shipment_advice_planner_toursolver.py @@ -45,3 +45,4 @@ def test_plan_shipment_toursolver(self): ) self.assertTrue(self.channel.shipment_advice_ids) self.assertEqual(self.channel.shipment_advice_ids, task.shipment_advice_ids) + self.assertEqual(task.release_channel_id, self.channel) diff --git a/stock_release_channel_shipment_lead_time/README.rst b/stock_release_channel_shipment_lead_time/README.rst index a4f41140c63..dbfe60bf1db 100644 --- a/stock_release_channel_shipment_lead_time/README.rst +++ b/stock_release_channel_shipment_lead_time/README.rst @@ -7,7 +7,7 @@ Release channel shipment lead time !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:2d2ed3ca42130e4f6d1ad0bbaeb1c7b4bdc2203134f8b60b47c1a4816fc80d68 + !! source digest: sha256:307b41b3025c23a659098c0fb9e5cca2998d7c2dc745ca6f02497a4597b14319 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png @@ -48,6 +48,11 @@ Add the delivery date on the shipment advice. .. contents:: :local: +Known issues / Roadmap +====================== + +Rename "Shipment date" to "Delivery date" as it is misleading + Bug Tracker =========== diff --git a/stock_release_channel_shipment_lead_time/__manifest__.py b/stock_release_channel_shipment_lead_time/__manifest__.py index d4d09cefc8a..8c3728d6de2 100644 --- a/stock_release_channel_shipment_lead_time/__manifest__.py +++ b/stock_release_channel_shipment_lead_time/__manifest__.py @@ -5,7 +5,7 @@ { "name": "Release channel shipment lead time", "summary": "Release channel with shipment lead time", - "version": "16.0.1.3.0", + "version": "16.0.2.1.0", "development_status": "Beta", "license": "AGPL-3", "author": "Camptocamp, BCIM, Odoo Community Association (OCA)", diff --git a/stock_release_channel_shipment_lead_time/i18n/fr.po b/stock_release_channel_shipment_lead_time/i18n/fr.po index 45844b84866..348f008dd9d 100644 --- a/stock_release_channel_shipment_lead_time/i18n/fr.po +++ b/stock_release_channel_shipment_lead_time/i18n/fr.po @@ -16,6 +16,11 @@ msgstr "" "Plural-Forms: nplurals=2; plural=n > 1;\n" "X-Generator: Weblate 5.6.2\n" +#. module: stock_release_channel_shipment_lead_time +#: model:ir.model.fields,field_description:stock_release_channel_shipment_lead_time.field_stock_release_channel__delivery_calendar_id +msgid "Delivery Calendar" +msgstr "" + #. module: stock_release_channel_shipment_lead_time #: model:ir.model.fields,field_description:stock_release_channel_shipment_lead_time.field_shipment_advice__delivery_date msgid "Delivery Date" @@ -46,6 +51,13 @@ msgstr "" msgid "Shipment Lead Time (days)" msgstr "" +#. module: stock_release_channel_shipment_lead_time +#: model:ir.model.fields,help:stock_release_channel_shipment_lead_time.field_stock_release_channel__delivery_calendar_id +msgid "" +"Shipment Working Hours. Defaults tot warehouse calendar for simplicity but " +"another calendar can be used." +msgstr "" + #. module: stock_release_channel_shipment_lead_time #: model:ir.model,name:stock_release_channel_shipment_lead_time.model_stock_release_channel msgid "Stock Release Channels" @@ -60,8 +72,8 @@ msgstr "Transfert" #: model:ir.model.fields,help:stock_release_channel_shipment_lead_time.field_stock_release_channel__shipment_date msgid "" "if no warehouse or no calendar on the warehouse:process end date + shipment " -"lead time.Otherwise, it's counted by calendar included leaves:number of days" -" = lead time + 1" +"lead time.Otherwise, it's counted by calendar included leaves:number of days " +"= lead time + 1" msgstr "" #. module: stock_release_channel_shipment_lead_time diff --git a/stock_release_channel_shipment_lead_time/i18n/it.po b/stock_release_channel_shipment_lead_time/i18n/it.po index c79a0908326..b4f80a59139 100644 --- a/stock_release_channel_shipment_lead_time/i18n/it.po +++ b/stock_release_channel_shipment_lead_time/i18n/it.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0\n" "Report-Msgid-Bugs-To: \n" -"PO-Revision-Date: 2023-11-23 10:35+0000\n" +"PO-Revision-Date: 2025-06-04 10:26+0000\n" "Last-Translator: mymage \n" "Language-Team: none\n" "Language: it\n" @@ -14,7 +14,12 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.17\n" +"X-Generator: Weblate 5.10.4\n" + +#. module: stock_release_channel_shipment_lead_time +#: model:ir.model.fields,field_description:stock_release_channel_shipment_lead_time.field_stock_release_channel__delivery_calendar_id +msgid "Delivery Calendar" +msgstr "Calendario consegna" #. module: stock_release_channel_shipment_lead_time #: model:ir.model.fields,field_description:stock_release_channel_shipment_lead_time.field_shipment_advice__delivery_date @@ -46,6 +51,15 @@ msgstr "Durata spedizione" msgid "Shipment Lead Time (days)" msgstr "Durata spedizione (giorni)" +#. module: stock_release_channel_shipment_lead_time +#: model:ir.model.fields,help:stock_release_channel_shipment_lead_time.field_stock_release_channel__delivery_calendar_id +msgid "" +"Shipment Working Hours. Defaults tot warehouse calendar for simplicity but " +"another calendar can be used." +msgstr "" +"Ore lavoro spedizioni. Per semplicità predefinite come il calendario del " +"magazzino ma può essere usato un altro calendario." + #. module: stock_release_channel_shipment_lead_time #: model:ir.model,name:stock_release_channel_shipment_lead_time.model_stock_release_channel msgid "Stock Release Channels" @@ -60,8 +74,8 @@ msgstr "Trasferimento" #: model:ir.model.fields,help:stock_release_channel_shipment_lead_time.field_stock_release_channel__shipment_date msgid "" "if no warehouse or no calendar on the warehouse:process end date + shipment " -"lead time.Otherwise, it's counted by calendar included leaves:number of days" -" = lead time + 1" +"lead time.Otherwise, it's counted by calendar included leaves:number of days " +"= lead time + 1" msgstr "" "se non c'è il magazzino o il calendario del magazzino: data fine processo + " "durata spedizione. Altrimenti, è contata dal calendario dei livelli inclusi: " diff --git a/stock_release_channel_shipment_lead_time/i18n/stock_release_channel_shipment_lead_time.pot b/stock_release_channel_shipment_lead_time/i18n/stock_release_channel_shipment_lead_time.pot index f954ef3a6b7..06e3110826c 100644 --- a/stock_release_channel_shipment_lead_time/i18n/stock_release_channel_shipment_lead_time.pot +++ b/stock_release_channel_shipment_lead_time/i18n/stock_release_channel_shipment_lead_time.pot @@ -13,6 +13,11 @@ msgstr "" "Content-Transfer-Encoding: \n" "Plural-Forms: \n" +#. module: stock_release_channel_shipment_lead_time +#: model:ir.model.fields,field_description:stock_release_channel_shipment_lead_time.field_stock_release_channel__delivery_calendar_id +msgid "Delivery Calendar" +msgstr "" + #. module: stock_release_channel_shipment_lead_time #: model:ir.model.fields,field_description:stock_release_channel_shipment_lead_time.field_shipment_advice__delivery_date msgid "Delivery Date" @@ -43,6 +48,13 @@ msgstr "" msgid "Shipment Lead Time (days)" msgstr "" +#. module: stock_release_channel_shipment_lead_time +#: model:ir.model.fields,help:stock_release_channel_shipment_lead_time.field_stock_release_channel__delivery_calendar_id +msgid "" +"Shipment Working Hours. Defaults tot warehouse calendar for simplicity but " +"another calendar can be used." +msgstr "" + #. module: stock_release_channel_shipment_lead_time #: model:ir.model,name:stock_release_channel_shipment_lead_time.model_stock_release_channel msgid "Stock Release Channels" diff --git a/stock_release_channel_shipment_lead_time/models/stock_picking.py b/stock_release_channel_shipment_lead_time/models/stock_picking.py index bbda2f36ac5..3a5f29fab5c 100644 --- a/stock_release_channel_shipment_lead_time/models/stock_picking.py +++ b/stock_release_channel_shipment_lead_time/models/stock_picking.py @@ -7,9 +7,10 @@ class StockPicking(models.Model): _inherit = "stock.picking" - def _get_release_channel_possible_candidate_domain_picking(self): + @property + def _release_channel_possible_candidate_domain_extras(self): # Exclude deliveries (OUT pickings) when the date_deadline is after the shipment date - domain = super()._get_release_channel_possible_candidate_domain_picking() + domains = super()._release_channel_possible_candidate_domain_extras date = self.date_deadline if date: @@ -23,14 +24,13 @@ def _get_release_channel_possible_candidate_domain_picking(self): self.with_context(tz=tz), date ).date() - domain.extend( - [ - "|", - ("shipment_date", "=", False), - ("shipment_date", ">=", date), - ] - ) - return domain + domain_shipment = [ + "|", + ("shipment_date", "=", False), + ("shipment_date", ">=", date), + ] + domains.append(domain_shipment) + return domains @api.model def _search_scheduled_date_prior_to_channel_end_date_condition(self): diff --git a/stock_release_channel_shipment_lead_time/models/stock_release_channel.py b/stock_release_channel_shipment_lead_time/models/stock_release_channel.py index e113394402a..7110ea7795a 100644 --- a/stock_release_channel_shipment_lead_time/models/stock_release_channel.py +++ b/stock_release_channel_shipment_lead_time/models/stock_release_channel.py @@ -1,6 +1,6 @@ # Copyright 2023 Camptocamp +# Copyright 2025 Jacques-Etienne Baudoux (BCIM) # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) -from datetime import timedelta from odoo import api, fields, models @@ -9,6 +9,7 @@ class StockReleaseChannel(models.Model): _inherit = "stock.release.channel" shipment_lead_time = fields.Integer(help="Shipment Lead Time (days)") + # Migration note: rename shipment_date to delivery_date shipment_date = fields.Date( compute="_compute_shipment_date", store=True, @@ -19,29 +20,72 @@ class StockReleaseChannel(models.Model): "number of days = lead time + 1" ), ) + delivery_calendar_id = fields.Many2one( + comodel_name="resource.calendar", + compute="_compute_delivery_calendar_id", + store=True, + help=( + "Shipment Working Hours. Defaults tot warehouse calendar " + "for simplicity but another calendar can be used." + ), + ) + + @api.depends("warehouse_id.calendar_id") + def _compute_delivery_calendar_id(self): + for channel in self: + channel.delivery_calendar_id = channel.warehouse_id.calendar_id + + def _add_shipment_lead_time(self, dt): + """Add lead time to given datetime. + + If no calendar: dt + lead time + else: use calendar.plan_days(days, date_from, compute_leaves=True) + where days is amount of required open days (= lead time + 1) + """ + self.ensure_one() + if not self.shipment_lead_time: + return dt + dt_tz = self._localize(dt) + if not self.delivery_calendar_id: + shipment_tz = fields.Datetime.add(dt_tz, days=self.shipment_lead_time) + else: + days = self.shipment_lead_time + 1 + shipment_tz = self.delivery_calendar_id.plan_days( + days, dt_tz, compute_leaves=True + ) + shipment_dt = self._naive(shipment_tz, reset_time=True) + return shipment_dt + # Migration note: rename _compute_shipment_date to _compute_delivery_date @api.depends( "process_end_date", "shipment_lead_time", - "warehouse_id", - "warehouse_id.calendar_id", + "delivery_calendar_id", ) def _compute_shipment_date(self): - """ - if no warehouse or no calendar on the warehouse: - process end date + lead time - else: use calendar.plan_days(days, date_from, compute_leaves=True) - where days is amount of required open days (= lead time + 1) - """ for channel in self: shipment_date = False if channel.process_end_date: - shipment_date = channel.process_end_date + timedelta( - days=channel.shipment_lead_time + shipment_date = channel._add_shipment_lead_time( + channel.process_end_date ) - if channel.warehouse_id.calendar_id: - days = channel.shipment_lead_time + 1 - shipment_date = channel.warehouse_id.calendar_id.plan_days( - days, channel.process_end_date, compute_leaves=True - ) channel.shipment_date = shipment_date + + @property + def _delivery_date_generators(self): + d = super()._delivery_date_generators + d["delivery"].append(self._next_delivery_date_shipment_lead_time) + return d + + def _next_delivery_date_shipment_lead_time(self, delivery_date, partner=None): + """Get the next valid delivery date respecting transport lead time. + + The delivery date must be postponed at least by the shipment lead time. + + A delivery date generator needs to provide the earliest valid date + starting from the received date. It can be called multiple times with a + new date to validate. + """ + arrival_date = self._add_shipment_lead_time(delivery_date) + while True: + delivery_date = yield max(delivery_date, arrival_date) diff --git a/stock_release_channel_shipment_lead_time/readme/ROADMAP.rst b/stock_release_channel_shipment_lead_time/readme/ROADMAP.rst new file mode 100644 index 00000000000..29c8317207b --- /dev/null +++ b/stock_release_channel_shipment_lead_time/readme/ROADMAP.rst @@ -0,0 +1 @@ +Rename "Shipment date" to "Delivery date" as it is misleading diff --git a/stock_release_channel_shipment_lead_time/static/description/index.html b/stock_release_channel_shipment_lead_time/static/description/index.html index bf553711011..a965b252c02 100644 --- a/stock_release_channel_shipment_lead_time/static/description/index.html +++ b/stock_release_channel_shipment_lead_time/static/description/index.html @@ -367,7 +367,7 @@

    Release channel shipment lead time

    !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:2d2ed3ca42130e4f6d1ad0bbaeb1c7b4bdc2203134f8b60b47c1a4816fc80d68 +!! source digest: sha256:307b41b3025c23a659098c0fb9e5cca2998d7c2dc745ca6f02497a4597b14319 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

    Beta License: AGPL-3 OCA/wms Translate me on Weblate Try me on Runboat

    Manage shipment date and delivery lead time on release channel. @@ -384,18 +384,23 @@

    Release channel shipment lead time

    Table of contents

    +
    +

    Known issues / Roadmap

    +

    Rename “Shipment date” to “Delivery date” as it is misleading

    +
    -

    Bug Tracker

    +

    Bug Tracker

    Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed @@ -403,16 +408,16 @@

    Bug Tracker

    Do not contact contributors directly about support or help with technical issues.

    -

    Credits

    +

    Credits

    -

    Authors

    +

    Authors

    • Camptocamp
    • BCIM
    -

    Contributors

    +

    Contributors

    -

    Other credits

    +

    Other credits

    The development of this module has been financially supported by Camptocamp

    -

    Maintainers

    +

    Maintainers

    This module is maintained by the OCA.

    Odoo Community Association diff --git a/stock_release_channel_shipment_lead_time/tests/test_release_shipment_date.py b/stock_release_channel_shipment_lead_time/tests/test_release_shipment_date.py index a1755ea2c6e..994b713d9e9 100644 --- a/stock_release_channel_shipment_lead_time/tests/test_release_shipment_date.py +++ b/stock_release_channel_shipment_lead_time/tests/test_release_shipment_date.py @@ -1,9 +1,13 @@ # Copyright 2023 Camptocamp +# Copyright 2025 Jacques-Etienne Baudoux (BCIM) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + from odoo import fields from odoo.addons.stock_release_channel.tests.common import ChannelReleaseCase +to_datetime = fields.Datetime.to_datetime + class TestChannelReleaseShipmentLeadTime(ChannelReleaseCase): def test_shipment_date(self): @@ -28,3 +32,26 @@ def test_shipment_date_with_calendar(self): "2023-07-10", fields.Date.to_string(self.channel.shipment_date), ) + + def test_delivery_date_shipment_lead_time(self): + self.channel.warehouse_id = self.wh + self.channel.warehouse_id.calendar_id = self.env.ref( + "resource.resource_calendar_std" + ) + self.channel.warehouse_id.partner_id.tz = "Europe/Brussels" + self.channel.shipment_lead_time = 1 + dt = to_datetime("2025-01-02 08:00:00") # Thursday + gen = self.channel._next_delivery_date_shipment_lead_time(dt) + result = next(gen) + next_day = to_datetime("2025-01-02 23:00:00") # Friday + self.assertEqual(result, next_day) + result = gen.send(dt) + self.assertEqual(result, next_day) + # around week-end + dt = to_datetime("2025-01-03 08:00:00") # Friday + gen = self.channel._next_delivery_date_shipment_lead_time(dt) + result = next(gen) + next_day = to_datetime("2025-01-05 23:00:00") # Monday + self.assertEqual(result, next_day) + result = gen.send(dt) + self.assertEqual(result, next_day) diff --git a/stock_release_channel_warehouse_calendar/README.rst b/stock_release_channel_warehouse_calendar/README.rst new file mode 100644 index 00000000000..c6e52223951 --- /dev/null +++ b/stock_release_channel_warehouse_calendar/README.rst @@ -0,0 +1,85 @@ +========================================= +Stock Release Channels Warehouse Calendar +========================================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:cb4a89c714e1c59bee2952cc7c607482defba205f0b5bf3ae01d1ad31ee2f78e + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fwms-lightgray.png?logo=github + :target: https://github.com/OCA/wms/tree/16.0/stock_release_channel_warehouse_calendar + :alt: OCA/wms +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/wms-16-0/wms-16-0-stock_release_channel_warehouse_calendar + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/wms&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Glue module when a calendar can be set on the warehouse. + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* BCIM +* Camptocamp + +Contributors +~~~~~~~~~~~~ + +* Jacques-Etienne Baudoux (BCIM) + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-jbaudoux| image:: https://github.com/jbaudoux.png?size=40px + :target: https://github.com/jbaudoux + :alt: jbaudoux + +Current `maintainer `__: + +|maintainer-jbaudoux| + +This module is part of the `OCA/wms `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/stock_release_channel_warehouse_calendar/__init__.py b/stock_release_channel_warehouse_calendar/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/stock_release_channel_warehouse_calendar/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/stock_release_channel_warehouse_calendar/__manifest__.py b/stock_release_channel_warehouse_calendar/__manifest__.py new file mode 100644 index 00000000000..40d776458fa --- /dev/null +++ b/stock_release_channel_warehouse_calendar/__manifest__.py @@ -0,0 +1,16 @@ +# Copyright 2025 Jacques-Etienne Baudoux (BCIM) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) +{ + "name": "Stock Release Channels Warehouse Calendar", + "summary": "Glue module between release channel and warehouse calendar", + "version": "16.0.1.0.0", + "license": "AGPL-3", + "author": "BCIM, Camptocamp, Odoo Community Association (OCA)", + "maintainers": ["jbaudoux"], + "website": "https://github.com/OCA/wms", + "depends": [ + "stock_release_channel", + "stock_warehouse_calendar", # OCA/stock-logistics-warehouse + ], + "auto_install": True, +} diff --git a/stock_release_channel_warehouse_calendar/i18n/it.po b/stock_release_channel_warehouse_calendar/i18n/it.po new file mode 100644 index 00000000000..b02eaae39f7 --- /dev/null +++ b/stock_release_channel_warehouse_calendar/i18n/it.po @@ -0,0 +1,22 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * stock_release_channel_warehouse_calendar +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2025-06-04 14:26+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.10.4\n" + +#. module: stock_release_channel_warehouse_calendar +#: model:ir.model,name:stock_release_channel_warehouse_calendar.model_stock_release_channel +msgid "Stock Release Channels" +msgstr "Canali rilascio magazzino" diff --git a/stock_release_channel_warehouse_calendar/i18n/stock_release_channel_warehouse_calendar.pot b/stock_release_channel_warehouse_calendar/i18n/stock_release_channel_warehouse_calendar.pot new file mode 100644 index 00000000000..20ff234b523 --- /dev/null +++ b/stock_release_channel_warehouse_calendar/i18n/stock_release_channel_warehouse_calendar.pot @@ -0,0 +1,19 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * stock_release_channel_warehouse_calendar +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: stock_release_channel_warehouse_calendar +#: model:ir.model,name:stock_release_channel_warehouse_calendar.model_stock_release_channel +msgid "Stock Release Channels" +msgstr "" diff --git a/stock_release_channel_warehouse_calendar/models/__init__.py b/stock_release_channel_warehouse_calendar/models/__init__.py new file mode 100644 index 00000000000..a0504e84078 --- /dev/null +++ b/stock_release_channel_warehouse_calendar/models/__init__.py @@ -0,0 +1 @@ +from . import stock_release_channel diff --git a/stock_release_channel_warehouse_calendar/models/stock_release_channel.py b/stock_release_channel_warehouse_calendar/models/stock_release_channel.py new file mode 100644 index 00000000000..38aeb64bf80 --- /dev/null +++ b/stock_release_channel_warehouse_calendar/models/stock_release_channel.py @@ -0,0 +1,48 @@ +# Copyright 2025 Jacques-Etienne Baudoux (BCIM) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +from datetime import timedelta + +import pytz + +from odoo import models + + +class StockReleaseChannel(models.Model): + _inherit = "stock.release.channel" + + @property + def _delivery_date_generators(self): + d = super()._delivery_date_generators + d["preparation"].append(self._next_delivery_date_warehouse_calendar) + return d + + def _next_delivery_date_warehouse_calendar(self, delivery_date, partner=None): + """Get the next valid delivery date respecting warehouse calendar + + The preparation date must be during warehouse working hours given by + the calendar on the warehouse. + + A delivery date generator needs to provide the earliest valid date + starting from the received date. It can be called multiple times with a + new date to validate. + """ + calendar = self.warehouse_id.calendar_id + if not calendar: + while True: + delivery_date = yield delivery_date + wh_tz = pytz.timezone(self.warehouse_id.partner_id.tz or "UTC") + batch_delta = timedelta(days=61) + while True: + delivery_date = delivery_date.astimezone(pytz.utc) + work_intervals = calendar._work_intervals_batch( + delivery_date, delivery_date + batch_delta, tz=wh_tz + )[False] + for begin_dt_tz, end_dt_tz, _attendance in work_intervals: + while delivery_date <= end_dt_tz: + if delivery_date < begin_dt_tz: + delivery_date = begin_dt_tz + delivery_date = yield delivery_date.astimezone(pytz.utc).replace( + tzinfo=None + ) + delivery_date = delivery_date.astimezone(pytz.utc) diff --git a/stock_release_channel_warehouse_calendar/readme/CONTRIBUTORS.rst b/stock_release_channel_warehouse_calendar/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000000..3c6c5c696a8 --- /dev/null +++ b/stock_release_channel_warehouse_calendar/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Jacques-Etienne Baudoux (BCIM) diff --git a/stock_release_channel_warehouse_calendar/readme/DESCRIPTION.rst b/stock_release_channel_warehouse_calendar/readme/DESCRIPTION.rst new file mode 100644 index 00000000000..4ba96149a1c --- /dev/null +++ b/stock_release_channel_warehouse_calendar/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +Glue module when a calendar can be set on the warehouse. diff --git a/stock_release_channel_warehouse_calendar/static/description/icon.png b/stock_release_channel_warehouse_calendar/static/description/icon.png new file mode 100644 index 00000000000..1dcc49c24f3 Binary files /dev/null and b/stock_release_channel_warehouse_calendar/static/description/icon.png differ diff --git a/stock_release_channel_warehouse_calendar/static/description/index.html b/stock_release_channel_warehouse_calendar/static/description/index.html new file mode 100644 index 00000000000..c9c71369182 --- /dev/null +++ b/stock_release_channel_warehouse_calendar/static/description/index.html @@ -0,0 +1,426 @@ + + + + + +Stock Release Channels Warehouse Calendar + + + +
    +

    Stock Release Channels Warehouse Calendar

    + + +

    Beta License: AGPL-3 OCA/wms Translate me on Weblate Try me on Runboat

    +

    Glue module when a calendar can be set on the warehouse.

    +

    Table of contents

    + +
    +

    Bug Tracker

    +

    Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

    +

    Do not contact contributors directly about support or help with technical issues.

    +
    +
    +

    Credits

    +
    +

    Authors

    +
      +
    • BCIM
    • +
    • Camptocamp
    • +
    +
    +
    +

    Contributors

    + +
    +
    +

    Maintainers

    +

    This module is maintained by the OCA.

    + +Odoo Community Association + +

    OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

    +

    Current maintainer:

    +

    jbaudoux

    +

    This module is part of the OCA/wms project on GitHub.

    +

    You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

    +
    +
    +
    + + diff --git a/stock_release_channel_warehouse_calendar/tests/__init__.py b/stock_release_channel_warehouse_calendar/tests/__init__.py new file mode 100644 index 00000000000..52a6a32ce1c --- /dev/null +++ b/stock_release_channel_warehouse_calendar/tests/__init__.py @@ -0,0 +1 @@ +from . import test_channel_delivery_date diff --git a/stock_release_channel_warehouse_calendar/tests/test_channel_delivery_date.py b/stock_release_channel_warehouse_calendar/tests/test_channel_delivery_date.py new file mode 100644 index 00000000000..e18774a018e --- /dev/null +++ b/stock_release_channel_warehouse_calendar/tests/test_channel_delivery_date.py @@ -0,0 +1,69 @@ +# Copyright 2024 Jacques-Etienne Baudoux (BCIM) +# Copyright 2024 Camptocamp (https://www.camptocamp.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + + +from odoo import fields + +from odoo.addons.stock_release_channel.tests.common import ReleaseChannelCase + +to_datetime = fields.Datetime.to_datetime + + +class TestChannelDeliveryDate(ReleaseChannelCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + # Mon-Fri 8:00-12:00 13:00-17:00 + cls.calendar = cls.env.ref("resource.resource_calendar_std") + cls.wh.partner_id.tz = "Europe/Brussels" + cls.channel = cls._create_channel( + name="partner channel", + warehouse_id=cls.wh.id, + ) + + def test_warehouse_calendar_no_calendar(self): + dt = to_datetime("2023-02-01 08:00:00") + gen = self.channel._next_delivery_date_warehouse_calendar(dt) + result = next(gen) + self.assertEqual(result, dt) + result = gen.send(dt) + self.assertEqual(result, dt) + + def test_warehouse_calendar_time_before_opening(self): + self.wh.calendar_id = self.calendar + dt = to_datetime("2025-01-06 06:30:00") # Monday 07:30 + gen = self.channel._next_delivery_date_warehouse_calendar(dt) + result = next(gen) + opening = to_datetime("2025-01-06 07:00:00") # Monday 08:00 + self.assertEqual(result, opening) + result = gen.send(dt) + self.assertEqual(result, opening) + + def test_warehouse_calendar_time_during_opening(self): + self.wh.calendar_id = self.calendar + dt = to_datetime("2025-01-06 07:30:00") # Monday 08:30 + gen = self.channel._next_delivery_date_warehouse_calendar(dt) + result = next(gen) + self.assertEqual(result, dt) + dt = to_datetime("2025-01-06 15:30:00") # Monday 16:30 + result = gen.send(dt) + self.assertEqual(result, dt) + + def test_warehouse_calendar_time_after_opening(self): + self.wh.calendar_id = self.calendar + dt = to_datetime("2025-01-06 16:30:00") # Monday 17:30 + gen = self.channel._next_delivery_date_warehouse_calendar(dt) + result = next(gen) + next_day = to_datetime("2025-01-07 07:00:00") # Tuesday 08:00 + self.assertEqual(result, next_day) + + def test_warehouse_calendar_day_before_opening(self): + self.wh.calendar_id = self.calendar + dt = to_datetime("2025-01-05 14:30:00") # Sunday 15:30 + gen = self.channel._next_delivery_date_warehouse_calendar(dt) + result = next(gen) + opening = to_datetime("2025-01-06 07:00:00") # Monday 08:00 + self.assertEqual(result, opening) + result = gen.send(dt) + self.assertEqual(result, opening) diff --git a/stock_storage_type/README.rst b/stock_storage_type/README.rst index 25b50ea3278..e679aad239b 100644 --- a/stock_storage_type/README.rst +++ b/stock_storage_type/README.rst @@ -1,3 +1,7 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + ================== Stock Storage Type ================== @@ -7,13 +11,13 @@ Stock Storage Type !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:0e700eefd6bcaa1449f71d59b90a1e4a57c0f749f2d282af71ca5cdd7c0e5c42 + !! source digest: sha256:c46ce97684eda7baf75edac00f005ea18baac91d81a4f0df32ba6f49ce1b34ee !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png :target: https://odoo-community.org/page/development-status :alt: Beta -.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png +.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fwms-lightgray.png?logo=github @@ -28,56 +32,38 @@ Stock Storage Type |badge1| |badge2| |badge3| |badge4| |badge5| -This module introduces two new models in order to manage stock moves with - packages according to the packaging and stock location properties. - -* Stock package storage type (`stock.package.storage.type`) - - This model is linked to product.packaging and defines the type of storage - related to a specific packaging. - -* Stock location storage type (`stock.location.storage.type`) - - This models is linked to stock.location and defines the types of storage - that are allowed for a specific location. - -Therefore a Stock location storage type can include different Stock package -storage type in order to validate the destination of a move with package into a -stock location. -Moreover Stock location storage type can include product, size or lot -restrictions for the stock locations it's defined on, so that a move with -package will only be allowed if it doesn't violate the restrictions defined -(cf stock_location_storage_type_strategy). +This module extends package types Odoo feature in order to better manage stock +moves with packages according to the packaging and stock location properties +(like height, weight or any customized conditions). -Moreover, this module implements "storage type put-away strategy" in order to compute a -put-away location using storage types. +Moreover, this module implements "package type put-away strategy" in order to +compute a put-away location using package types. -The standard put-away strategy is applied *before* the storage type put-away +The standard put-away strategy is applied *before* the package type put-away strategy as the former relies on product or product category and the latter relies on stock packages. -In other words, when a move is assigned, Odoo standard put-away strategy will be +In other words, when a move is reserved, Odoo standard put-away strategy will be applied to compute a new destination on the stock move lines, according to the product. -After this first "put-away computation", the "storage type" put-away strategy -is applied, if the reserved quant is linked to a package defining a package -storage type. +After this first "put-away computation", the "package type" put-away strategy +is applied, if the reserved quant is linked to a package defining a package type. -Storage locations linked to the package storage are processed sequentially, if +Storage locations linked to the package type are processed sequentially, if said storage location is a child of the move line's destination location (i.e either the put-away location or the move's destination location). -For each location, their packs storage strategy is applied as well as the -restrictions defined on the stock location storage types. +For each location, their package type strategy is applied as well as the +restrictions defined on the storage category. If no suitable location is found, the next location in the sequence will be searched and so on. -For the packs putaway strategy "none", the location is considered as is. For +For the package type putaway strategy "None", the location is considered as is. For the "ordered children" strategy, children locations are sorted by first by max height which is a physical constraint to respect, then pack putaway sequence which allow to favor for example some level or corridor, and finally by name. At the end, if found location is not the same as the original destination location, -the putaway strategies are applied (e.g.: A "none" pack putaway strategy is set on +the putaway strategies are applied (e.g.: A "None" pack putaway strategy is set on computed location and a putaway rule exists on that one). **Table of contents** diff --git a/stock_storage_type/__manifest__.py b/stock_storage_type/__manifest__.py index 48b4a0e18ae..03e0ce4b10f 100644 --- a/stock_storage_type/__manifest__.py +++ b/stock_storage_type/__manifest__.py @@ -4,7 +4,7 @@ { "name": "Stock Storage Type", "summary": "Manage packages and locations storage types", - "version": "16.0.1.1.0", + "version": "16.0.2.0.3", "development_status": "Beta", "category": "Warehouse Management", "website": "https://github.com/OCA/wms", @@ -24,7 +24,7 @@ "views/product_template.xml", "views/stock_location.xml", "views/stock_storage_category.xml", - "views/stock_storage_category_capacity.xml", + "views/stock_storage_category_allow_new_product_cond.xml", "views/stock_package_level.xml", "views/stock_package_type.xml", "views/stock_storage_location_sequence.xml", @@ -33,6 +33,7 @@ ], "demo": [ "demo/stock_package_type.xml", + "demo/stock_storage_category_allow_new_product_cond.xml", "demo/stock_storage_category.xml", "demo/stock_storage_category_capacity.xml", "demo/product_packaging.xml", diff --git a/stock_storage_type/demo/stock_storage_category.xml b/stock_storage_type/demo/stock_storage_category.xml index 174b1166c17..d8b84b3a0b2 100644 --- a/stock_storage_type/demo/stock_storage_category.xml +++ b/stock_storage_type/demo/stock_storage_category.xml @@ -5,6 +5,30 @@ Pallets + + + + + empty + + + + + + empty + Cardboxes diff --git a/stock_storage_type/demo/stock_storage_category_allow_new_product_cond.xml b/stock_storage_type/demo/stock_storage_category_allow_new_product_cond.xml new file mode 100644 index 00000000000..da2729548d1 --- /dev/null +++ b/stock_storage_type/demo/stock_storage_category_allow_new_product_cond.xml @@ -0,0 +1,35 @@ + + + + + Package type is 'Pallets' + + + + + + + Package type is 'Pallets UK' + + + + + + diff --git a/stock_storage_type/demo/stock_storage_category_capacity.xml b/stock_storage_type/demo/stock_storage_category_capacity.xml index 5a3a536582f..f2fdaeee0b5 100644 --- a/stock_storage_type/demo/stock_storage_category_capacity.xml +++ b/stock_storage_type/demo/stock_storage_category_capacity.xml @@ -4,7 +4,6 @@ - empty \n" "Language-Team: none\n" "Language: de\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" "Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.10.4\n" #. module: stock_storage_type #. odoo-python @@ -20,7 +22,7 @@ msgstr "" #, python-format msgid "" " * {location} (WARNING: restrictions are " -"active on location storage types matching this package storage type)" +"active on storage categories matching this package type)" msgstr "" #. module: stock_storage_type @@ -29,7 +31,7 @@ msgstr "" #, python-format msgid "" " * {location} (WARNING: no suitable location " -"matching storage type)" +"matching package type)" msgstr "" #. module: stock_storage_type @@ -38,39 +40,49 @@ msgstr "" #, python-format msgid "" "The \"Put-Away sequence\" must be defined in " -"order to put away packages using this package storage type " -"({storage})." +"order to put away packages using this package type ({storage})." msgstr "" #. module: stock_storage_type #: model:ir.model.fields,field_description:stock_storage_type.field_stock_package_type__active +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__active +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_condition_mixin__active #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__active msgid "Active" msgstr "" #. module: stock_storage_type -#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category__allow_new_product -#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_capacity__allow_new_product -msgid "Allow New Product" +#: model_terms:ir.ui.view,arch_db:stock_storage_type.stock_storage_category_form +msgid "Advanced" msgstr "" #. module: stock_storage_type -#. odoo-python -#: code:addons/stock_storage_type/models/stock_storage_category_capacity.py:0 -#, python-format -msgid "Allow New Product: " +#: model:ir.model.fields,help:stock_storage_type.field_stock_storage_category_allow_new_product__condition_ids +msgid "All conditions have to match to apply the Allow New Product policy." msgstr "" #. module: stock_storage_type -#: model:ir.model.fields.selection,name:stock_storage_type.selection__stock_storage_category_capacity__allow_new_product__mixed -msgid "Allow mixed products" +#: model:ir.model.fields,help:stock_storage_type.field_stock_storage_location_sequence__location_sequence_cond_ids +msgid "All conditions have to match to apply the put-away strategy." +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category__allow_new_product +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__allow_new_product +msgid "Allow New Product" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category__allow_new_product_ids +#: model_terms:ir.ui.view,arch_db:stock_storage_type.stock_storage_category_form +msgid "Allow New Product Rules" msgstr "" #. module: stock_storage_type #: model:ir.model.fields,help:stock_storage_type.field_stock_location__package_type_putaway_sequence msgid "" -"Allow to sort the valid locations by sequence for the storage strategy based" -" on package type" +"Allow to sort the valid locations by sequence for the storage strategy based " +"on package type" msgstr "" #. module: stock_storage_type @@ -79,6 +91,7 @@ msgid "Allowed Destinations" msgstr "" #. module: stock_storage_type +#: model_terms:ir.ui.view,arch_db:stock_storage_type.stock_storage_category_allow_new_product_cond_view_form #: model_terms:ir.ui.view,arch_db:stock_storage_type.stock_storage_location_sequence_cond_form_view msgid "Archived" msgstr "" @@ -94,18 +107,21 @@ msgid "Capacity" msgstr "" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__code_snippet +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_condition_mixin__code_snippet #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__code_snippet msgid "Code Snippet" msgstr "" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__code_snippet_docs +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_condition_mixin__code_snippet_docs #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__code_snippet_docs msgid "Code Snippet Docs" msgstr "" #. module: stock_storage_type #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category__computed_location_ids -#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_capacity__computed_location_ids msgid "Computed Location" msgstr "" @@ -115,29 +131,36 @@ msgid "Computed Storage Category" msgstr "" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__condition_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_condition_mixin__condition_type #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__condition_type msgid "Condition Type" msgstr "" #. module: stock_storage_type #. odoo-python -#: code:addons/stock_storage_type/models/stock_storage_location_sequence_cond.py:0 +#: code:addons/stock_storage_type/models/stock_storage_condition_mixin.py:0 #, python-format msgid "Condition type is set to `Code`: you must provide a piece of code" msgstr "" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__condition_ids #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__location_sequence_cond_ids msgid "Conditions" -msgstr "" +msgstr "Bedingungen" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__create_uid +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__create_uid #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__create_uid #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__create_uid msgid "Created by" msgstr "" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__create_date +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__create_date #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__create_date #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__create_date msgid "Created on" @@ -148,8 +171,8 @@ msgstr "" #: model:ir.model.fields,help:stock_storage_type.field_product_template__package_type_id msgid "" "Defines a 'default' package type for this product to be applied on packages " -"without product packagings and on put-away computation based on package type" -" for product not in a package" +"without product packagings and on put-away computation based on package type " +"for product not in a package" msgstr "" #. module: stock_storage_type @@ -158,6 +181,8 @@ msgid "Dimensions Units of Measure" msgstr "" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__display_name +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__display_name #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__display_name #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__display_name msgid "Display Name" @@ -174,12 +199,14 @@ msgid "Do Not Mix Products" msgstr "" #. module: stock_storage_type +#: model:ir.model.fields.selection,name:stock_storage_type.selection__stock_storage_category_allow_new_product_cond__condition_type__code +#: model:ir.model.fields.selection,name:stock_storage_type.selection__stock_storage_condition_mixin__condition_type__code #: model:ir.model.fields.selection,name:stock_storage_type.selection__stock_storage_location_sequence_cond__condition_type__code msgid "Execute code" msgstr "" #. module: stock_storage_type -#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_capacity__has_restrictions +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category__has_restrictions msgid "Has Restrictions" msgstr "" @@ -190,7 +217,7 @@ msgstr "" #. module: stock_storage_type #: model:ir.model.fields,help:stock_storage_type.field_stock_package_type__height_required -msgid "Height is mandatory for packages configured with this storage type." +msgid "Height is mandatory for packages configured with this package type." msgstr "" #. module: stock_storage_type @@ -199,31 +226,18 @@ msgid "Height required for packages" msgstr "" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__id +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__id #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__id #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__id msgid "ID" msgstr "" -#. module: stock_storage_type -#: model:ir.model.fields.selection,name:stock_storage_type.selection__stock_storage_category_capacity__allow_new_product__same_lot -msgid "If all lots are the same" -msgstr "" - -#. module: stock_storage_type -#: model:ir.model.fields.selection,name:stock_storage_type.selection__stock_storage_category_capacity__allow_new_product__same -msgid "If all products are same" -msgstr "" - #. module: stock_storage_type #: model:ir.model.fields.selection,name:stock_storage_type.selection__stock_storage_category__allow_new_product__same_lot msgid "If lots are all the same" msgstr "" -#. module: stock_storage_type -#: model:ir.model.fields.selection,name:stock_storage_type.selection__stock_storage_category_capacity__allow_new_product__empty -msgid "If the location is empty" -msgstr "" - #. module: stock_storage_type #: model:ir.model.fields,field_description:stock_storage_type.field_stock_location__in_move_ids msgid "In Move" @@ -240,18 +254,24 @@ msgid "Inventory Locations" msgstr "" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product____last_update +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond____last_update #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence____last_update #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond____last_update msgid "Last Modified on" msgstr "" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__write_uid +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__write_uid #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__write_uid #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__write_uid msgid "Last Updated by" msgstr "" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__write_date +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__write_date #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__write_date #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__write_date msgid "Last Updated on" @@ -310,6 +330,13 @@ msgid "Max height should be a positive number." msgstr "" #. module: stock_storage_type +#: model:ir.model,name:stock_storage_type.model_stock_storage_condition_mixin +msgid "Mixin to implement storage condition." +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__name +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_condition_mixin__name #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__name msgid "Name" msgstr "" @@ -362,9 +389,10 @@ msgstr "" #: code:addons/stock_storage_type/models/stock_quant.py:0 #, python-format msgid "" -"Package {package} is not allowed into location {location}, because there isn't any storage capacity that allows package type {type} into it:\n" +"Package {package} is not allowed into location {location}, because there " +"isn't any rules that allows package type {type} into it:\n" "\n" -"{fails}" +"{error}" msgstr "" #. module: stock_storage_type @@ -417,13 +445,14 @@ msgid "Recompute Putaway" msgstr "" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__sequence #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__sequence msgid "Sequence" msgstr "" #. module: stock_storage_type #: model:ir.model,name:stock_storage_type.model_stock_storage_location_sequence -msgid "Sequence of locations to put-away the package storage type" +msgid "Sequence of locations to put-away the package type" msgstr "" #. module: stock_storage_type @@ -436,6 +465,11 @@ msgstr "" msgid "Stock Package Level" msgstr "" +#. module: stock_storage_type +#: model:ir.model,name:stock_storage_type.model_stock_storage_category_allow_new_product_cond +msgid "Stock Storage Category Allow New Product Condition" +msgstr "" + #. module: stock_storage_type #: model:ir.model,name:stock_storage_type.model_stock_storage_location_sequence_cond msgid "Stock Storage Location Sequence Condition" @@ -456,13 +490,35 @@ msgstr "" msgid "Stock storage location sequence condition name must be unique" msgstr "" +#. module: stock_storage_type +#: model:ir.model,name:stock_storage_type.model_stock_storage_category +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__storage_category_id +msgid "Storage Category" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.constraint,message:stock_storage_type.constraint_stock_storage_category_allow_new_product_cond_name +msgid "Storage Category Allow New Product Condition name must be unique" +msgstr "" + +#. module: stock_storage_type +#: model:ir.actions.act_window,name:stock_storage_type.stock_storage_category_allow_new_product_cond_act_window +#: model:ir.ui.menu,name:stock_storage_type.stock_storage_category_allow_new_product_cond_menu +msgid "Storage Category Allow New Product Conditions" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model,name:stock_storage_type.model_stock_storage_category_allow_new_product +msgid "Storage Category Allow New Product Rule" +msgstr "" + #. module: stock_storage_type #. odoo-python #: code:addons/stock_storage_type/models/stock_quant.py:0 #, python-format msgid "" -"Storage Capacity {storage_capacity} is flagged 'do not mix lots' but there " -"are other lots in location." +"Storage Category {category} defines max height of {max_h} but the package is " +"bigger: {height}." msgstr "" #. module: stock_storage_type @@ -470,8 +526,8 @@ msgstr "" #: code:addons/stock_storage_type/models/stock_quant.py:0 #, python-format msgid "" -"Storage Capacity {storage_capacity} is flagged 'do not mix products' but " -"there are other products in location." +"Storage Category {category} defines max weight of {max_w} but the package is " +"heavier: {weight_kg}." msgstr "" #. module: stock_storage_type @@ -479,18 +535,8 @@ msgstr "" #: code:addons/stock_storage_type/models/stock_quant.py:0 #, python-format msgid "" -"Storage Capacity {storage_capacity} is flagged 'only empty' with other " -"quants in location." -msgstr "" - -#. module: stock_storage_type -#: model:ir.model,name:stock_storage_type.model_stock_storage_category -msgid "Storage Category" -msgstr "" - -#. module: stock_storage_type -#: model:ir.model,name:stock_storage_type.model_stock_storage_category_capacity -msgid "Storage Category Capacity" +"Storage Category {category} is flagged 'do not mix lots' but there are other " +"lots in location." msgstr "" #. module: stock_storage_type @@ -498,8 +544,8 @@ msgstr "" #: code:addons/stock_storage_type/models/stock_quant.py:0 #, python-format msgid "" -"Storage Category {storage_category} defines max height of {max_h} but the " -"package is bigger: {height}." +"Storage Category {category} is flagged 'do not mix products' but there are " +"other products in location." msgstr "" #. module: stock_storage_type @@ -507,8 +553,8 @@ msgstr "" #: code:addons/stock_storage_type/models/stock_quant.py:0 #, python-format msgid "" -"Storage Category {storage_category} defines max weight of {max_w} but the " -"package is heavier: {weight_kg}." +"Storage Category {category} is flagged 'only empty' with other quants in " +"location." msgstr "" #. module: stock_storage_type @@ -541,7 +587,7 @@ msgid "Technical field, to speed up comparaisons" msgstr "" #. module: stock_storage_type -#: model:ir.model.fields,help:stock_storage_type.field_stock_storage_category_capacity__has_restrictions +#: model:ir.model.fields,help:stock_storage_type.field_stock_storage_category__has_restrictions msgid "Technical: This is used to check if we need to display warning message" msgstr "" @@ -562,16 +608,19 @@ msgstr "" #: model:ir.model.fields,help:stock_storage_type.field_stock_location__pack_putaway_strategy #: model:ir.model.fields,help:stock_storage_type.field_stock_storage_location_sequence__location_putaway_strategy msgid "" -"This defines the storage strategy based on package type to use when a product or package is put away in this location.\n" +"This defines the storage strategy based on package type to use when a " +"product or package is put away in this location.\n" "None: when moved to this location, it will not be put away any further.\n" -"Ordered Children Locations: when moved to this location, a suitable location will be searched in its children locations according to the restrictions defined on their respective location storage types." +"Ordered Children Locations: when moved to this location, a suitable location " +"will be searched in its children locations according to the restrictions " +"defined on their respective storage category." msgstr "" #. module: stock_storage_type #: model:ir.model.fields,help:stock_storage_type.field_stock_location__computed_storage_category_id msgid "" -"This represents the Storage Category that will be used. It depends either on" -" the category set on the location or on one of its parent." +"This represents the Storage Category that will be used. It depends either on " +"the category set on the location or on one of its parent." msgstr "" #. module: stock_storage_type @@ -599,16 +648,17 @@ msgstr "" #: code:addons/stock_storage_type/models/stock_package_type.py:0 #, python-format msgid "" -"When a package with storage type {name} is put away, the strategy will look " -"for an allowed location in the following locations:

    {message} " -"

    Note: this happens as long as these locations are " -"children of the stock move destination location or as long as these " -"locations are children of the destination location after the (product or " -"category) put-away is applied." +"When a package with type {name} is put away, the strategy will look for an " +"allowed location in the following locations:

    {message}

    Note: this happens as long as these locations are children of the " +"stock move destination location or as long as these locations are " +"children of the destination location after the (product or category) put-" +"away is applied." msgstr "" #. module: stock_storage_type #. odoo-python +#: code:addons/stock_storage_type/models/stock_storage_category_allow_new_product_cond.py:0 #: code:addons/stock_storage_type/models/stock_storage_location_sequence_cond.py:0 #, python-format msgid "code_snippet should return boolean value into `result` variable." @@ -619,7 +669,7 @@ msgstr "" msgid "" "technical field: True if the location is empty and there is no pending " "incoming products in the location. Computed only if the location needs to " -"check for emptiness (has an \"only empty\" location storage type)." +"check for emptiness (has an \"only empty\" policy)." msgstr "" #. module: stock_storage_type diff --git a/stock_storage_type/i18n/es.po b/stock_storage_type/i18n/es.po index 3b36874fcc6..de3769f093e 100644 --- a/stock_storage_type/i18n/es.po +++ b/stock_storage_type/i18n/es.po @@ -22,11 +22,8 @@ msgstr "" #, python-format msgid "" " * {location} (WARNING: restrictions are " -"active on location storage types matching this package storage type)" +"active on storage categories matching this package type)" msgstr "" -" * {location} (ADVERTENCIA: hay restricciones " -"activas en los tipos de ubicación de almacenamiento que coinciden con este " -"tipo de almacenamiento de paquete)" #. module: stock_storage_type #. odoo-python @@ -34,10 +31,8 @@ msgstr "" #, python-format msgid "" " * {location} (WARNING: no suitable location " -"matching storage type)" +"matching package type)" msgstr "" -" * {location} (ADVERTENCIA: no hay una ubicación " -"adecuada que coincida con el tipo de almacenamiento)" #. module: stock_storage_type #. odoo-python @@ -45,36 +40,43 @@ msgstr "" #, python-format msgid "" "The \"Put-Away sequence\" must be defined in " -"order to put away packages using this package storage type ({storage})." +"order to put away packages using this package type ({storage})." msgstr "" -"La \"secuencia de almacenamiento\" debe " -"definirse para poder almacenar paquetes utilizando este tipo de " -"almacenamiento de paquetes ({storage})." #. module: stock_storage_type #: model:ir.model.fields,field_description:stock_storage_type.field_stock_package_type__active +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__active +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_condition_mixin__active #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__active msgid "Active" msgstr "Activo" +#. module: stock_storage_type +#: model_terms:ir.ui.view,arch_db:stock_storage_type.stock_storage_category_form +msgid "Advanced" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,help:stock_storage_type.field_stock_storage_category_allow_new_product__condition_ids +msgid "All conditions have to match to apply the Allow New Product policy." +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,help:stock_storage_type.field_stock_storage_location_sequence__location_sequence_cond_ids +msgid "All conditions have to match to apply the put-away strategy." +msgstr "" + #. module: stock_storage_type #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category__allow_new_product -#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_capacity__allow_new_product +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__allow_new_product msgid "Allow New Product" msgstr "Nuevo Producto Permitido" #. module: stock_storage_type -#. odoo-python -#: code:addons/stock_storage_type/models/stock_storage_category_capacity.py:0 -#, python-format -msgid "Allow New Product: " -msgstr "Nuevo Producto Permitido " - -#. module: stock_storage_type -#: model:ir.model.fields.selection,name:stock_storage_type.selection__stock_storage_category_capacity__allow_new_product__mixed -msgid "Allow mixed products" -msgstr "Permitir productos mixtos" +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category__allow_new_product_ids +#: model_terms:ir.ui.view,arch_db:stock_storage_type.stock_storage_category_form +msgid "Allow New Product Rules" +msgstr "" #. module: stock_storage_type #: model:ir.model.fields,help:stock_storage_type.field_stock_location__package_type_putaway_sequence @@ -91,6 +93,7 @@ msgid "Allowed Destinations" msgstr "Destinos permitidos" #. module: stock_storage_type +#: model_terms:ir.ui.view,arch_db:stock_storage_type.stock_storage_category_allow_new_product_cond_view_form #: model_terms:ir.ui.view,arch_db:stock_storage_type.stock_storage_location_sequence_cond_form_view msgid "Archived" msgstr "Archivado" @@ -106,18 +109,21 @@ msgid "Capacity" msgstr "Capacidad" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__code_snippet +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_condition_mixin__code_snippet #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__code_snippet msgid "Code Snippet" msgstr "Fragmento de código" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__code_snippet_docs +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_condition_mixin__code_snippet_docs #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__code_snippet_docs msgid "Code Snippet Docs" msgstr "Documentos de fragmentos de código" #. module: stock_storage_type #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category__computed_location_ids -#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_capacity__computed_location_ids msgid "Computed Location" msgstr "Computar Localización" @@ -127,30 +133,37 @@ msgid "Computed Storage Category" msgstr "Categoría de almacenamiento computado" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__condition_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_condition_mixin__condition_type #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__condition_type msgid "Condition Type" msgstr "Tipo de condición" #. module: stock_storage_type #. odoo-python -#: code:addons/stock_storage_type/models/stock_storage_location_sequence_cond.py:0 +#: code:addons/stock_storage_type/models/stock_storage_condition_mixin.py:0 #, python-format msgid "Condition type is set to `Code`: you must provide a piece of code" msgstr "" "El tipo de condición es `Código`: debe proporcionar un fragmento de código" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__condition_ids #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__location_sequence_cond_ids msgid "Conditions" msgstr "Condiciones" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__create_uid +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__create_uid #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__create_uid #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__create_uid msgid "Created by" msgstr "Creado por" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__create_date +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__create_date #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__create_date #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__create_date msgid "Created on" @@ -174,6 +187,8 @@ msgid "Dimensions Units of Measure" msgstr "Dimensiones Unidades de medida" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__display_name +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__display_name #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__display_name #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__display_name msgid "Display Name" @@ -190,12 +205,14 @@ msgid "Do Not Mix Products" msgstr "No Mezclar Productos" #. module: stock_storage_type +#: model:ir.model.fields.selection,name:stock_storage_type.selection__stock_storage_category_allow_new_product_cond__condition_type__code +#: model:ir.model.fields.selection,name:stock_storage_type.selection__stock_storage_condition_mixin__condition_type__code #: model:ir.model.fields.selection,name:stock_storage_type.selection__stock_storage_location_sequence_cond__condition_type__code msgid "Execute code" msgstr "Ejecutar código" #. module: stock_storage_type -#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_capacity__has_restrictions +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category__has_restrictions msgid "Has Restrictions" msgstr "Tiene restricciones" @@ -206,10 +223,8 @@ msgstr "Altura En M" #. module: stock_storage_type #: model:ir.model.fields,help:stock_storage_type.field_stock_package_type__height_required -msgid "Height is mandatory for packages configured with this storage type." +msgid "Height is mandatory for packages configured with this package type." msgstr "" -"La altura es obligatoria para los paquetes configurados con este tipo de " -"almacenamiento." #. module: stock_storage_type #: model:ir.model.fields,field_description:stock_storage_type.field_stock_package_type__height_required @@ -217,31 +232,18 @@ msgid "Height required for packages" msgstr "Altura necesaria para los paquetes" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__id +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__id #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__id #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__id msgid "ID" msgstr "ID (identificación)" -#. module: stock_storage_type -#: model:ir.model.fields.selection,name:stock_storage_type.selection__stock_storage_category_capacity__allow_new_product__same_lot -msgid "If all lots are the same" -msgstr "Si todos los lotes son iguales" - -#. module: stock_storage_type -#: model:ir.model.fields.selection,name:stock_storage_type.selection__stock_storage_category_capacity__allow_new_product__same -msgid "If all products are same" -msgstr "Si todos los productos son iguales" - #. module: stock_storage_type #: model:ir.model.fields.selection,name:stock_storage_type.selection__stock_storage_category__allow_new_product__same_lot msgid "If lots are all the same" msgstr "Si los lotes son todos iguales" -#. module: stock_storage_type -#: model:ir.model.fields.selection,name:stock_storage_type.selection__stock_storage_category_capacity__allow_new_product__empty -msgid "If the location is empty" -msgstr "Si la localización está vacía" - #. module: stock_storage_type #: model:ir.model.fields,field_description:stock_storage_type.field_stock_location__in_move_ids msgid "In Move" @@ -258,18 +260,24 @@ msgid "Inventory Locations" msgstr "Localizaciones de Inventario" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product____last_update +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond____last_update #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence____last_update #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond____last_update msgid "Last Modified on" msgstr "Última Modificación el" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__write_uid +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__write_uid #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__write_uid #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__write_uid msgid "Last Updated by" msgstr "Última Actualización por" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__write_date +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__write_date #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__write_date #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__write_date msgid "Last Updated on" @@ -328,6 +336,13 @@ msgid "Max height should be a positive number." msgstr "La altura máxima debe ser un número positivo." #. module: stock_storage_type +#: model:ir.model,name:stock_storage_type.model_stock_storage_condition_mixin +msgid "Mixin to implement storage condition." +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__name +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_condition_mixin__name #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__name msgid "Name" msgstr "Nombre" @@ -382,15 +397,10 @@ msgstr "" #, python-format msgid "" "Package {package} is not allowed into location {location}, because there " -"isn't any storage capacity that allows package type {type} into it:\n" +"isn't any rules that allows package type {type} into it:\n" "\n" -"{fails}" +"{error}" msgstr "" -"El paquete {package} no está permitido en la localización {location}, porque " -"no hay ninguna capacidad de almacenamiento que permita el tipo de paquete " -"{type} en ella:\n" -"\n" -"{fails}" #. module: stock_storage_type #: model:ir.model,name:stock_storage_type.model_stock_quant_package @@ -442,14 +452,15 @@ msgid "Recompute Putaway" msgstr "Recálculo de salida" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__sequence #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__sequence msgid "Sequence" msgstr "Secuencia" #. module: stock_storage_type #: model:ir.model,name:stock_storage_type.model_stock_storage_location_sequence -msgid "Sequence of locations to put-away the package storage type" -msgstr "Secuencia de ubicaciones para guardar el tipo de almacén de paquetes" +msgid "Sequence of locations to put-away the package type" +msgstr "" #. module: stock_storage_type #: model_terms:ir.ui.view,arch_db:stock_storage_type.package_storage_location_tree_view @@ -461,6 +472,11 @@ msgstr "Mostrar localizaciones" msgid "Stock Package Level" msgstr "Nivel del paquete de existencias" +#. module: stock_storage_type +#: model:ir.model,name:stock_storage_type.model_stock_storage_category_allow_new_product_cond +msgid "Stock Storage Category Allow New Product Condition" +msgstr "" + #. module: stock_storage_type #: model:ir.model,name:stock_storage_type.model_stock_storage_location_sequence_cond msgid "Stock Storage Location Sequence Condition" @@ -481,70 +497,72 @@ msgstr "Tipo de empaquetado de existencias" msgid "Stock storage location sequence condition name must be unique" msgstr "El nombre de la condición de secuencia del almacén debe ser único" +#. module: stock_storage_type +#: model:ir.model,name:stock_storage_type.model_stock_storage_category +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__storage_category_id +msgid "Storage Category" +msgstr "Categoría Almacenamiento" + +#. module: stock_storage_type +#: model:ir.model.constraint,message:stock_storage_type.constraint_stock_storage_category_allow_new_product_cond_name +msgid "Storage Category Allow New Product Condition name must be unique" +msgstr "" + +#. module: stock_storage_type +#: model:ir.actions.act_window,name:stock_storage_type.stock_storage_category_allow_new_product_cond_act_window +#: model:ir.ui.menu,name:stock_storage_type.stock_storage_category_allow_new_product_cond_menu +msgid "Storage Category Allow New Product Conditions" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model,name:stock_storage_type.model_stock_storage_category_allow_new_product +msgid "Storage Category Allow New Product Rule" +msgstr "" + #. module: stock_storage_type #. odoo-python #: code:addons/stock_storage_type/models/stock_quant.py:0 #, python-format msgid "" -"Storage Capacity {storage_capacity} is flagged 'do not mix lots' but there " -"are other lots in location." +"Storage Category {category} defines max height of {max_h} but the package is " +"bigger: {height}." msgstr "" -"Capacidad de almacenamiento {storage_capacity} está marcada como 'no mezclar " -"lotes' pero hay otros lotes en la ubicación." #. module: stock_storage_type #. odoo-python #: code:addons/stock_storage_type/models/stock_quant.py:0 #, python-format msgid "" -"Storage Capacity {storage_capacity} is flagged 'do not mix products' but " -"there are other products in location." +"Storage Category {category} defines max weight of {max_w} but the package is " +"heavier: {weight_kg}." msgstr "" -"Capacidad de almacenamiento {storage_capacity} está marcada como 'no mezclar " -"productos' pero hay otros productos en la ubicación." #. module: stock_storage_type #. odoo-python #: code:addons/stock_storage_type/models/stock_quant.py:0 #, python-format msgid "" -"Storage Capacity {storage_capacity} is flagged 'only empty' with other " -"quants in location." +"Storage Category {category} is flagged 'do not mix lots' but there are other " +"lots in location." msgstr "" -"La capacidad de almacenamiento {storage_capacity} está marcada como \"sólo " -"vacía\" con otros cuantos en la ubicación." - -#. module: stock_storage_type -#: model:ir.model,name:stock_storage_type.model_stock_storage_category -msgid "Storage Category" -msgstr "Categoría Almacenamiento" - -#. module: stock_storage_type -#: model:ir.model,name:stock_storage_type.model_stock_storage_category_capacity -msgid "Storage Category Capacity" -msgstr "Capacidad de la categoría de almacenamiento" #. module: stock_storage_type #. odoo-python #: code:addons/stock_storage_type/models/stock_quant.py:0 #, python-format msgid "" -"Storage Category {storage_category} defines max height of {max_h} but the " -"package is bigger: {height}." +"Storage Category {category} is flagged 'do not mix products' but there are " +"other products in location." msgstr "" -"La categoría de almacenamiento {storage_category} define una altura máxima " -"de {max_h} pero el paquete es mayor: {height}." #. module: stock_storage_type #. odoo-python #: code:addons/stock_storage_type/models/stock_quant.py:0 #, python-format msgid "" -"Storage Category {storage_category} defines max weight of {max_w} but the " -"package is heavier: {weight_kg}." +"Storage Category {category} is flagged 'only empty' with other quants in " +"location." msgstr "" -"La categoría de almacenamiento {storage_category} define un peso máximo de " -"{max_w} pero el paquete es más pesado: {weight_kg}." #. module: stock_storage_type #: model:ir.ui.menu,name:stock_storage_type.stock_storage_location_sequence_cond_menu @@ -576,7 +594,7 @@ msgid "Technical field, to speed up comparaisons" msgstr "Ámbito técnico, para agilizar las comparaciones" #. module: stock_storage_type -#: model:ir.model.fields,help:stock_storage_type.field_stock_storage_category_capacity__has_restrictions +#: model:ir.model.fields,help:stock_storage_type.field_stock_storage_category__has_restrictions msgid "Technical: This is used to check if we need to display warning message" msgstr "" "Técnico: Se utiliza para comprobar si es necesario mostrar un mensaje de " @@ -604,14 +622,8 @@ msgid "" "None: when moved to this location, it will not be put away any further.\n" "Ordered Children Locations: when moved to this location, a suitable location " "will be searched in its children locations according to the restrictions " -"defined on their respective location storage types." +"defined on their respective storage category." msgstr "" -"Define la estrategia de almacenamiento basada en el tipo de paquete que se " -"utilizará cuando se guarde un producto o paquete en esta ubicación.\n" -"Ninguno: cuando se mueve a esta ubicación, no se guardará más.\n" -"Ubicaciones hijas ordenadas: cuando se mueve a esta ubicación, se buscará " -"una ubicación adecuada en sus ubicaciones hijas según las restricciones " -"definidas en sus respectivos tipos de almacenamiento de ubicación." #. module: stock_storage_type #: model:ir.model.fields,help:stock_storage_type.field_stock_location__computed_storage_category_id @@ -647,23 +659,17 @@ msgstr "Etiqueta de unidad de medida de peso" #: code:addons/stock_storage_type/models/stock_package_type.py:0 #, python-format msgid "" -"When a package with storage type {name} is put away, the strategy will look " -"for an allowed location in the following locations:

    {message}

    Note: this happens as long as these locations are children " -"of the stock move destination location or as long as these locations are " +"When a package with type {name} is put away, the strategy will look for an " +"allowed location in the following locations:

    {message}

    Note: this happens as long as these locations are children of the " +"stock move destination location or as long as these locations are " "children of the destination location after the (product or category) put-" "away is applied." msgstr "" -"Cuando se guarda un paquete con tipo de almacenamiento {name}, la estrategia " -"buscará una ubicación permitida en las siguientes ubicaciones:

    {message}

    Nota: esto sucede siempre que estas ubicaciones " -"sean secundarias de la ubicación de destino del movimiento de " -"existencias o siempre que estas ubicaciones sean secundarias de la " -"ubicación de destino después de que se aplique la ubicación (de producto o " -"categoría) ." #. module: stock_storage_type #. odoo-python +#: code:addons/stock_storage_type/models/stock_storage_category_allow_new_product_cond.py:0 #: code:addons/stock_storage_type/models/stock_storage_location_sequence_cond.py:0 #, python-format msgid "code_snippet should return boolean value into `result` variable." @@ -674,12 +680,8 @@ msgstr "code_snippet debe devolver un valor booleano en la variable `result`." msgid "" "technical field: True if the location is empty and there is no pending " "incoming products in the location. Computed only if the location needs to " -"check for emptiness (has an \"only empty\" location storage type)." +"check for emptiness (has an \"only empty\" policy)." msgstr "" -"campo técnico: Verdadero si la ubicación está vacía y no hay productos " -"entrantes pendientes en la ubicación. Sólo se calcula si la ubicación " -"necesita comprobar si está vacía (tiene un tipo de almacenamiento de " -"ubicación \"sólo vacía\")." #. module: stock_storage_type #: model:ir.model.fields,help:stock_storage_type.field_stock_location__leaf_location_ids @@ -725,3 +727,154 @@ msgstr "" #: model:ir.model.fields,help:stock_storage_type.field_stock_location__out_move_line_ids msgid "technical field: the pending outgoing stock.move.lines in the location" msgstr "campo técnico: las líneas de salida pendientes en la ubicación" + +#, python-format +#~ msgid "" +#~ " * {location} (WARNING: restrictions are " +#~ "active on location storage types matching this package storage type)" +#~ msgstr "" +#~ " * {location} (ADVERTENCIA: hay " +#~ "restricciones activas en los tipos de ubicación de almacenamiento que " +#~ "coinciden con este tipo de almacenamiento de paquete)" + +#, python-format +#~ msgid "" +#~ " * {location} (WARNING: no suitable location " +#~ "matching storage type)" +#~ msgstr "" +#~ " * {location} (ADVERTENCIA: no hay una " +#~ "ubicación adecuada que coincida con el tipo de almacenamiento)" + +#, python-format +#~ msgid "" +#~ "The \"Put-Away sequence\" must be defined in " +#~ "order to put away packages using this package storage type ({storage})." +#~ msgstr "" +#~ "La \"secuencia de almacenamiento\" debe " +#~ "definirse para poder almacenar paquetes utilizando este tipo de " +#~ "almacenamiento de paquetes ({storage})." + +#, python-format +#~ msgid "Allow New Product: " +#~ msgstr "Nuevo Producto Permitido " + +#~ msgid "Allow mixed products" +#~ msgstr "Permitir productos mixtos" + +#~ msgid "Height is mandatory for packages configured with this storage type." +#~ msgstr "" +#~ "La altura es obligatoria para los paquetes configurados con este tipo de " +#~ "almacenamiento." + +#~ msgid "If all lots are the same" +#~ msgstr "Si todos los lotes son iguales" + +#~ msgid "If all products are same" +#~ msgstr "Si todos los productos son iguales" + +#~ msgid "If the location is empty" +#~ msgstr "Si la localización está vacía" + +#, python-format +#~ msgid "" +#~ "Package {package} is not allowed into location {location}, because there " +#~ "isn't any storage capacity that allows package type {type} into it:\n" +#~ "\n" +#~ "{fails}" +#~ msgstr "" +#~ "El paquete {package} no está permitido en la localización {location}, " +#~ "porque no hay ninguna capacidad de almacenamiento que permita el tipo de " +#~ "paquete {type} en ella:\n" +#~ "\n" +#~ "{fails}" + +#~ msgid "Sequence of locations to put-away the package storage type" +#~ msgstr "" +#~ "Secuencia de ubicaciones para guardar el tipo de almacén de paquetes" + +#, python-format +#~ msgid "" +#~ "Storage Capacity {storage_capacity} is flagged 'do not mix lots' but " +#~ "there are other lots in location." +#~ msgstr "" +#~ "Capacidad de almacenamiento {storage_capacity} está marcada como 'no " +#~ "mezclar lotes' pero hay otros lotes en la ubicación." + +#, python-format +#~ msgid "" +#~ "Storage Capacity {storage_capacity} is flagged 'do not mix products' but " +#~ "there are other products in location." +#~ msgstr "" +#~ "Capacidad de almacenamiento {storage_capacity} está marcada como 'no " +#~ "mezclar productos' pero hay otros productos en la ubicación." + +#, python-format +#~ msgid "" +#~ "Storage Capacity {storage_capacity} is flagged 'only empty' with other " +#~ "quants in location." +#~ msgstr "" +#~ "La capacidad de almacenamiento {storage_capacity} está marcada como " +#~ "\"sólo vacía\" con otros cuantos en la ubicación." + +#~ msgid "Storage Category Capacity" +#~ msgstr "Capacidad de la categoría de almacenamiento" + +#, python-format +#~ msgid "" +#~ "Storage Category {storage_category} defines max height of {max_h} but the " +#~ "package is bigger: {height}." +#~ msgstr "" +#~ "La categoría de almacenamiento {storage_category} define una altura " +#~ "máxima de {max_h} pero el paquete es mayor: {height}." + +#, python-format +#~ msgid "" +#~ "Storage Category {storage_category} defines max weight of {max_w} but the " +#~ "package is heavier: {weight_kg}." +#~ msgstr "" +#~ "La categoría de almacenamiento {storage_category} define un peso máximo " +#~ "de {max_w} pero el paquete es más pesado: {weight_kg}." + +#~ msgid "" +#~ "This defines the storage strategy based on package type to use when a " +#~ "product or package is put away in this location.\n" +#~ "None: when moved to this location, it will not be put away any further.\n" +#~ "Ordered Children Locations: when moved to this location, a suitable " +#~ "location will be searched in its children locations according to the " +#~ "restrictions defined on their respective location storage types." +#~ msgstr "" +#~ "Define la estrategia de almacenamiento basada en el tipo de paquete que " +#~ "se utilizará cuando se guarde un producto o paquete en esta ubicación.\n" +#~ "Ninguno: cuando se mueve a esta ubicación, no se guardará más.\n" +#~ "Ubicaciones hijas ordenadas: cuando se mueve a esta ubicación, se buscará " +#~ "una ubicación adecuada en sus ubicaciones hijas según las restricciones " +#~ "definidas en sus respectivos tipos de almacenamiento de ubicación." + +#, python-format +#~ msgid "" +#~ "When a package with storage type {name} is put away, the strategy will " +#~ "look for an allowed location in the following locations:

    {message}

    Note: this happens as long as these locations " +#~ "are children of the stock move destination location or as long as " +#~ "these locations are children of the destination location after the " +#~ "(product or category) put-away is applied." +#~ msgstr "" +#~ "Cuando se guarda un paquete con tipo de almacenamiento {name}, la " +#~ "estrategia buscará una ubicación permitida en las siguientes ubicaciones: " +#~ "

    {message}

    Nota: esto sucede siempre que estas " +#~ "ubicaciones sean secundarias de la ubicación de destino del movimiento " +#~ "de existencias o siempre que estas ubicaciones sean secundarias de la " +#~ "ubicación de destino después de que se aplique la ubicación (de producto " +#~ "o categoría) ." + +#~ msgid "" +#~ "technical field: True if the location is empty and there is no pending " +#~ "incoming products in the location. Computed only if the location needs " +#~ "to check for emptiness (has an \"only empty\" location storage type)." +#~ msgstr "" +#~ "campo técnico: Verdadero si la ubicación está vacía y no hay productos " +#~ "entrantes pendientes en la ubicación. Sólo se calcula si la ubicación " +#~ "necesita comprobar si está vacía (tiene un tipo de almacenamiento de " +#~ "ubicación \"sólo vacía\")." diff --git a/stock_storage_type/i18n/es_AR.po b/stock_storage_type/i18n/es_AR.po index 2b2617198f6..b9926cb8cc5 100644 --- a/stock_storage_type/i18n/es_AR.po +++ b/stock_storage_type/i18n/es_AR.po @@ -22,7 +22,7 @@ msgstr "" #, python-format msgid "" " * {location} (WARNING: restrictions are " -"active on location storage types matching this package storage type)" +"active on storage categories matching this package type)
    " msgstr "" #. module: stock_storage_type @@ -31,7 +31,7 @@ msgstr "" #, python-format msgid "" " * {location} (WARNING: no suitable location " -"matching storage type)" +"matching package type)
    " msgstr "" #. module: stock_storage_type @@ -40,32 +40,42 @@ msgstr "" #, python-format msgid "" "The \"Put-Away sequence\" must be defined in " -"order to put away packages using this package storage type ({storage})." +"order to put away packages using this package type ({storage})." msgstr "" #. module: stock_storage_type #: model:ir.model.fields,field_description:stock_storage_type.field_stock_package_type__active +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__active +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_condition_mixin__active #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__active msgid "Active" msgstr "Activo" #. module: stock_storage_type -#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category__allow_new_product -#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_capacity__allow_new_product -msgid "Allow New Product" +#: model_terms:ir.ui.view,arch_db:stock_storage_type.stock_storage_category_form +msgid "Advanced" msgstr "" #. module: stock_storage_type -#. odoo-python -#: code:addons/stock_storage_type/models/stock_storage_category_capacity.py:0 -#, python-format -msgid "Allow New Product: " +#: model:ir.model.fields,help:stock_storage_type.field_stock_storage_category_allow_new_product__condition_ids +msgid "All conditions have to match to apply the Allow New Product policy." msgstr "" #. module: stock_storage_type -#: model:ir.model.fields.selection,name:stock_storage_type.selection__stock_storage_category_capacity__allow_new_product__mixed -msgid "Allow mixed products" +#: model:ir.model.fields,help:stock_storage_type.field_stock_storage_location_sequence__location_sequence_cond_ids +msgid "All conditions have to match to apply the put-away strategy." +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category__allow_new_product +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__allow_new_product +msgid "Allow New Product" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category__allow_new_product_ids +#: model_terms:ir.ui.view,arch_db:stock_storage_type.stock_storage_category_form +msgid "Allow New Product Rules" msgstr "" #. module: stock_storage_type @@ -81,6 +91,7 @@ msgid "Allowed Destinations" msgstr "" #. module: stock_storage_type +#: model_terms:ir.ui.view,arch_db:stock_storage_type.stock_storage_category_allow_new_product_cond_view_form #: model_terms:ir.ui.view,arch_db:stock_storage_type.stock_storage_location_sequence_cond_form_view msgid "Archived" msgstr "Archivado" @@ -96,18 +107,21 @@ msgid "Capacity" msgstr "" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__code_snippet +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_condition_mixin__code_snippet #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__code_snippet msgid "Code Snippet" msgstr "Snippet de Código" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__code_snippet_docs +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_condition_mixin__code_snippet_docs #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__code_snippet_docs msgid "Code Snippet Docs" msgstr "Documentación de Snippet de Código" #. module: stock_storage_type #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category__computed_location_ids -#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_capacity__computed_location_ids msgid "Computed Location" msgstr "" @@ -117,13 +131,15 @@ msgid "Computed Storage Category" msgstr "" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__condition_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_condition_mixin__condition_type #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__condition_type msgid "Condition Type" msgstr "Tipo de Condición" #. module: stock_storage_type #. odoo-python -#: code:addons/stock_storage_type/models/stock_storage_location_sequence_cond.py:0 +#: code:addons/stock_storage_type/models/stock_storage_condition_mixin.py:0 #, python-format msgid "Condition type is set to `Code`: you must provide a piece of code" msgstr "" @@ -131,17 +147,22 @@ msgstr "" "código" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__condition_ids #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__location_sequence_cond_ids msgid "Conditions" msgstr "Condiciones" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__create_uid +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__create_uid #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__create_uid #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__create_uid msgid "Created by" msgstr "Creado por" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__create_date +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__create_date #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__create_date #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__create_date msgid "Created on" @@ -162,6 +183,8 @@ msgid "Dimensions Units of Measure" msgstr "Dimensiones Unidades de Medida" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__display_name +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__display_name #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__display_name #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__display_name msgid "Display Name" @@ -178,12 +201,14 @@ msgid "Do Not Mix Products" msgstr "No Mezcla Productos" #. module: stock_storage_type +#: model:ir.model.fields.selection,name:stock_storage_type.selection__stock_storage_category_allow_new_product_cond__condition_type__code +#: model:ir.model.fields.selection,name:stock_storage_type.selection__stock_storage_condition_mixin__condition_type__code #: model:ir.model.fields.selection,name:stock_storage_type.selection__stock_storage_location_sequence_cond__condition_type__code msgid "Execute code" msgstr "Ejecutar código" #. module: stock_storage_type -#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_capacity__has_restrictions +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category__has_restrictions msgid "Has Restrictions" msgstr "Tiene Restricciones" @@ -194,10 +219,8 @@ msgstr "" #. module: stock_storage_type #: model:ir.model.fields,help:stock_storage_type.field_stock_package_type__height_required -msgid "Height is mandatory for packages configured with this storage type." +msgid "Height is mandatory for packages configured with this package type." msgstr "" -"La Altura es obligatoria para paquetes configurados con este tipo de " -"almacenamiento." #. module: stock_storage_type #: model:ir.model.fields,field_description:stock_storage_type.field_stock_package_type__height_required @@ -205,31 +228,18 @@ msgid "Height required for packages" msgstr "Altura requerida para paquetes" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__id +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__id #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__id #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__id msgid "ID" msgstr "ID" -#. module: stock_storage_type -#: model:ir.model.fields.selection,name:stock_storage_type.selection__stock_storage_category_capacity__allow_new_product__same_lot -msgid "If all lots are the same" -msgstr "" - -#. module: stock_storage_type -#: model:ir.model.fields.selection,name:stock_storage_type.selection__stock_storage_category_capacity__allow_new_product__same -msgid "If all products are same" -msgstr "" - #. module: stock_storage_type #: model:ir.model.fields.selection,name:stock_storage_type.selection__stock_storage_category__allow_new_product__same_lot msgid "If lots are all the same" msgstr "" -#. module: stock_storage_type -#: model:ir.model.fields.selection,name:stock_storage_type.selection__stock_storage_category_capacity__allow_new_product__empty -msgid "If the location is empty" -msgstr "" - #. module: stock_storage_type #: model:ir.model.fields,field_description:stock_storage_type.field_stock_location__in_move_ids msgid "In Move" @@ -246,18 +256,24 @@ msgid "Inventory Locations" msgstr "Ubicaciones de Inventario" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product____last_update +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond____last_update #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence____last_update #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond____last_update msgid "Last Modified on" msgstr "Última Modificación el" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__write_uid +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__write_uid #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__write_uid #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__write_uid msgid "Last Updated by" msgstr "Última Actualización por" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__write_date +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__write_date #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__write_date #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__write_date msgid "Last Updated on" @@ -316,6 +332,13 @@ msgid "Max height should be a positive number." msgstr "" #. module: stock_storage_type +#: model:ir.model,name:stock_storage_type.model_stock_storage_condition_mixin +msgid "Mixin to implement storage condition." +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__name +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_condition_mixin__name #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__name msgid "Name" msgstr "Nombre" @@ -369,9 +392,9 @@ msgstr "" #, python-format msgid "" "Package {package} is not allowed into location {location}, because there " -"isn't any storage capacity that allows package type {type} into it:\n" +"isn't any rules that allows package type {type} into it:\n" "\n" -"{fails}" +"{error}" msgstr "" #. module: stock_storage_type @@ -424,15 +447,15 @@ msgid "Recompute Putaway" msgstr "Recomputar Colocación" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__sequence #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__sequence msgid "Sequence" msgstr "Secuencia" #. module: stock_storage_type #: model:ir.model,name:stock_storage_type.model_stock_storage_location_sequence -msgid "Sequence of locations to put-away the package storage type" +msgid "Sequence of locations to put-away the package type" msgstr "" -"Secuencia de ubicaciones para guardar el tipo del paquete de almacenamiento" #. module: stock_storage_type #: model_terms:ir.ui.view,arch_db:stock_storage_type.package_storage_location_tree_view @@ -444,6 +467,11 @@ msgstr "Mostrar ubicaciones" msgid "Stock Package Level" msgstr "Nivel de Paquete de Existencias" +#. module: stock_storage_type +#: model:ir.model,name:stock_storage_type.model_stock_storage_category_allow_new_product_cond +msgid "Stock Storage Category Allow New Product Condition" +msgstr "" + #. module: stock_storage_type #: model:ir.model,name:stock_storage_type.model_stock_storage_location_sequence_cond msgid "Stock Storage Location Sequence Condition" @@ -466,13 +494,35 @@ msgstr "" "El nombre de la condición de secuencia de ubicación de almacenamiento de " "stock debe ser único" +#. module: stock_storage_type +#: model:ir.model,name:stock_storage_type.model_stock_storage_category +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__storage_category_id +msgid "Storage Category" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.constraint,message:stock_storage_type.constraint_stock_storage_category_allow_new_product_cond_name +msgid "Storage Category Allow New Product Condition name must be unique" +msgstr "" + +#. module: stock_storage_type +#: model:ir.actions.act_window,name:stock_storage_type.stock_storage_category_allow_new_product_cond_act_window +#: model:ir.ui.menu,name:stock_storage_type.stock_storage_category_allow_new_product_cond_menu +msgid "Storage Category Allow New Product Conditions" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model,name:stock_storage_type.model_stock_storage_category_allow_new_product +msgid "Storage Category Allow New Product Rule" +msgstr "" + #. module: stock_storage_type #. odoo-python #: code:addons/stock_storage_type/models/stock_quant.py:0 #, python-format msgid "" -"Storage Capacity {storage_capacity} is flagged 'do not mix lots' but there " -"are other lots in location." +"Storage Category {category} defines max height of {max_h} but the package is " +"bigger: {height}." msgstr "" #. module: stock_storage_type @@ -480,8 +530,8 @@ msgstr "" #: code:addons/stock_storage_type/models/stock_quant.py:0 #, python-format msgid "" -"Storage Capacity {storage_capacity} is flagged 'do not mix products' but " -"there are other products in location." +"Storage Category {category} defines max weight of {max_w} but the package is " +"heavier: {weight_kg}." msgstr "" #. module: stock_storage_type @@ -489,18 +539,8 @@ msgstr "" #: code:addons/stock_storage_type/models/stock_quant.py:0 #, python-format msgid "" -"Storage Capacity {storage_capacity} is flagged 'only empty' with other " -"quants in location." -msgstr "" - -#. module: stock_storage_type -#: model:ir.model,name:stock_storage_type.model_stock_storage_category -msgid "Storage Category" -msgstr "" - -#. module: stock_storage_type -#: model:ir.model,name:stock_storage_type.model_stock_storage_category_capacity -msgid "Storage Category Capacity" +"Storage Category {category} is flagged 'do not mix lots' but there are other " +"lots in location." msgstr "" #. module: stock_storage_type @@ -508,8 +548,8 @@ msgstr "" #: code:addons/stock_storage_type/models/stock_quant.py:0 #, python-format msgid "" -"Storage Category {storage_category} defines max height of {max_h} but the " -"package is bigger: {height}." +"Storage Category {category} is flagged 'do not mix products' but there are " +"other products in location." msgstr "" #. module: stock_storage_type @@ -517,8 +557,8 @@ msgstr "" #: code:addons/stock_storage_type/models/stock_quant.py:0 #, python-format msgid "" -"Storage Category {storage_category} defines max weight of {max_w} but the " -"package is heavier: {weight_kg}." +"Storage Category {category} is flagged 'only empty' with other quants in " +"location." msgstr "" #. module: stock_storage_type @@ -551,7 +591,7 @@ msgid "Technical field, to speed up comparaisons" msgstr "Campo técnico, para acelerar comparaciones" #. module: stock_storage_type -#: model:ir.model.fields,help:stock_storage_type.field_stock_storage_category_capacity__has_restrictions +#: model:ir.model.fields,help:stock_storage_type.field_stock_storage_category__has_restrictions msgid "Technical: This is used to check if we need to display warning message" msgstr "" @@ -577,7 +617,7 @@ msgid "" "None: when moved to this location, it will not be put away any further.\n" "Ordered Children Locations: when moved to this location, a suitable location " "will be searched in its children locations according to the restrictions " -"defined on their respective location storage types." +"defined on their respective storage category." msgstr "" #. module: stock_storage_type @@ -612,16 +652,17 @@ msgstr "Etiqueta de la unidad de medida del peso" #: code:addons/stock_storage_type/models/stock_package_type.py:0 #, python-format msgid "" -"When a package with storage type {name} is put away, the strategy will look " -"for an allowed location in the following locations:

    {message}

    Note: this happens as long as these locations are children " -"of the stock move destination location or as long as these locations are " +"When a package with type {name} is put away, the strategy will look for an " +"allowed location in the following locations:

    {message}

    Note: this happens as long as these locations are children of the " +"stock move destination location or as long as these locations are " "children of the destination location after the (product or category) put-" "away is applied." msgstr "" #. module: stock_storage_type #. odoo-python +#: code:addons/stock_storage_type/models/stock_storage_category_allow_new_product_cond.py:0 #: code:addons/stock_storage_type/models/stock_storage_location_sequence_cond.py:0 #, python-format msgid "code_snippet should return boolean value into `result` variable." @@ -632,12 +673,8 @@ msgstr "code_snippet debe devolver un valor booleano en la variable `result`." msgid "" "technical field: True if the location is empty and there is no pending " "incoming products in the location. Computed only if the location needs to " -"check for emptiness (has an \"only empty\" location storage type)." +"check for emptiness (has an \"only empty\" policy)." msgstr "" -"campo técnico: Es Verdadero si la ubicación está vacía y no hay productos " -"entrantes pendientes en la ubicación. Se calcula solo si la ubicación " -"necesita verificar si está vacía (tiene un tipo de ubicación de " -"alamacenamiento como \"solo vacío\")." #. module: stock_storage_type #: model:ir.model.fields,help:stock_storage_type.field_stock_location__leaf_location_ids @@ -680,6 +717,26 @@ msgstr "campo técnico: stock.moves de entrada pendientes en la ubicación" msgid "technical field: the pending outgoing stock.move.lines in the location" msgstr "campo técnico: stock.move.lines de salida pendientes en la ubicación" +#~ msgid "Height is mandatory for packages configured with this storage type." +#~ msgstr "" +#~ "La Altura es obligatoria para paquetes configurados con este tipo de " +#~ "almacenamiento." + +#~ msgid "Sequence of locations to put-away the package storage type" +#~ msgstr "" +#~ "Secuencia de ubicaciones para guardar el tipo del paquete de " +#~ "almacenamiento" + +#~ msgid "" +#~ "technical field: True if the location is empty and there is no pending " +#~ "incoming products in the location. Computed only if the location needs " +#~ "to check for emptiness (has an \"only empty\" location storage type)." +#~ msgstr "" +#~ "campo técnico: Es Verdadero si la ubicación está vacía y no hay productos " +#~ "entrantes pendientes en la ubicación. Se calcula solo si la ubicación " +#~ "necesita verificar si está vacía (tiene un tipo de ubicación de " +#~ "alamacenamiento como \"solo vacío\")." + #, python-format #~ msgid "" #~ " * %s (WARNING: restrictions are active on " diff --git a/stock_storage_type/i18n/fr.po b/stock_storage_type/i18n/fr.po index 133a7567963..e962c88b90c 100644 --- a/stock_storage_type/i18n/fr.po +++ b/stock_storage_type/i18n/fr.po @@ -22,7 +22,7 @@ msgstr "" #, python-format msgid "" " * {location} (WARNING: restrictions are " -"active on location storage types matching this package storage type)" +"active on storage categories matching this package type)" msgstr "" #. module: stock_storage_type @@ -31,7 +31,7 @@ msgstr "" #, python-format msgid "" " * {location} (WARNING: no suitable location " -"matching storage type)" +"matching package type)" msgstr "" #. module: stock_storage_type @@ -40,39 +40,49 @@ msgstr "" #, python-format msgid "" "The \"Put-Away sequence\" must be defined in " -"order to put away packages using this package storage type " -"({storage})." +"order to put away packages using this package type ({storage})." msgstr "" #. module: stock_storage_type #: model:ir.model.fields,field_description:stock_storage_type.field_stock_package_type__active +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__active +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_condition_mixin__active #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__active msgid "Active" msgstr "Actif" +#. module: stock_storage_type +#: model_terms:ir.ui.view,arch_db:stock_storage_type.stock_storage_category_form +msgid "Advanced" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,help:stock_storage_type.field_stock_storage_category_allow_new_product__condition_ids +msgid "All conditions have to match to apply the Allow New Product policy." +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,help:stock_storage_type.field_stock_storage_location_sequence__location_sequence_cond_ids +msgid "All conditions have to match to apply the put-away strategy." +msgstr "" + #. module: stock_storage_type #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category__allow_new_product -#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_capacity__allow_new_product +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__allow_new_product msgid "Allow New Product" msgstr "Autoriser nouveau produit" #. module: stock_storage_type -#. odoo-python -#: code:addons/stock_storage_type/models/stock_storage_category_capacity.py:0 -#, python-format -msgid "Allow New Product: " -msgstr "Autoriser nouveau produit : " - -#. module: stock_storage_type -#: model:ir.model.fields.selection,name:stock_storage_type.selection__stock_storage_category_capacity__allow_new_product__mixed -msgid "Allow mixed products" -msgstr "Autoriser un mix de produits" +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category__allow_new_product_ids +#: model_terms:ir.ui.view,arch_db:stock_storage_type.stock_storage_category_form +msgid "Allow New Product Rules" +msgstr "" #. module: stock_storage_type #: model:ir.model.fields,help:stock_storage_type.field_stock_location__package_type_putaway_sequence msgid "" -"Allow to sort the valid locations by sequence for the storage strategy based" -" on package type" +"Allow to sort the valid locations by sequence for the storage strategy based " +"on package type" msgstr "" "Permet de trier les emplacements valides par séquence pour la stratégie de " "stockage en fonction du type de conditionnement" @@ -83,6 +93,7 @@ msgid "Allowed Destinations" msgstr "Destinations autorisées" #. module: stock_storage_type +#: model_terms:ir.ui.view,arch_db:stock_storage_type.stock_storage_category_allow_new_product_cond_view_form #: model_terms:ir.ui.view,arch_db:stock_storage_type.stock_storage_location_sequence_cond_form_view msgid "Archived" msgstr "Archivé" @@ -98,18 +109,21 @@ msgid "Capacity" msgstr "Capacité" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__code_snippet +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_condition_mixin__code_snippet #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__code_snippet msgid "Code Snippet" msgstr "Code Snippet" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__code_snippet_docs +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_condition_mixin__code_snippet_docs #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__code_snippet_docs msgid "Code Snippet Docs" msgstr "Code Snippet Docs" #. module: stock_storage_type #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category__computed_location_ids -#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_capacity__computed_location_ids msgid "Computed Location" msgstr "" @@ -119,29 +133,36 @@ msgid "Computed Storage Category" msgstr "" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__condition_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_condition_mixin__condition_type #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__condition_type msgid "Condition Type" msgstr "" #. module: stock_storage_type #. odoo-python -#: code:addons/stock_storage_type/models/stock_storage_location_sequence_cond.py:0 +#: code:addons/stock_storage_type/models/stock_storage_condition_mixin.py:0 #, python-format msgid "Condition type is set to `Code`: you must provide a piece of code" msgstr "" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__condition_ids #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__location_sequence_cond_ids msgid "Conditions" msgstr "" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__create_uid +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__create_uid #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__create_uid #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__create_uid msgid "Created by" msgstr "" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__create_date +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__create_date #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__create_date #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__create_date msgid "Created on" @@ -152,8 +173,8 @@ msgstr "" #: model:ir.model.fields,help:stock_storage_type.field_product_template__package_type_id msgid "" "Defines a 'default' package type for this product to be applied on packages " -"without product packagings and on put-away computation based on package type" -" for product not in a package" +"without product packagings and on put-away computation based on package type " +"for product not in a package" msgstr "" #. module: stock_storage_type @@ -162,6 +183,8 @@ msgid "Dimensions Units of Measure" msgstr "" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__display_name +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__display_name #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__display_name #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__display_name msgid "Display Name" @@ -178,12 +201,14 @@ msgid "Do Not Mix Products" msgstr "" #. module: stock_storage_type +#: model:ir.model.fields.selection,name:stock_storage_type.selection__stock_storage_category_allow_new_product_cond__condition_type__code +#: model:ir.model.fields.selection,name:stock_storage_type.selection__stock_storage_condition_mixin__condition_type__code #: model:ir.model.fields.selection,name:stock_storage_type.selection__stock_storage_location_sequence_cond__condition_type__code msgid "Execute code" msgstr "" #. module: stock_storage_type -#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_capacity__has_restrictions +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category__has_restrictions msgid "Has Restrictions" msgstr "" @@ -194,7 +219,7 @@ msgstr "" #. module: stock_storage_type #: model:ir.model.fields,help:stock_storage_type.field_stock_package_type__height_required -msgid "Height is mandatory for packages configured with this storage type." +msgid "Height is mandatory for packages configured with this package type." msgstr "" #. module: stock_storage_type @@ -203,31 +228,18 @@ msgid "Height required for packages" msgstr "" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__id +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__id #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__id #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__id msgid "ID" msgstr "" -#. module: stock_storage_type -#: model:ir.model.fields.selection,name:stock_storage_type.selection__stock_storage_category_capacity__allow_new_product__same_lot -msgid "If all lots are the same" -msgstr "" - -#. module: stock_storage_type -#: model:ir.model.fields.selection,name:stock_storage_type.selection__stock_storage_category_capacity__allow_new_product__same -msgid "If all products are same" -msgstr "" - #. module: stock_storage_type #: model:ir.model.fields.selection,name:stock_storage_type.selection__stock_storage_category__allow_new_product__same_lot msgid "If lots are all the same" msgstr "" -#. module: stock_storage_type -#: model:ir.model.fields.selection,name:stock_storage_type.selection__stock_storage_category_capacity__allow_new_product__empty -msgid "If the location is empty" -msgstr "" - #. module: stock_storage_type #: model:ir.model.fields,field_description:stock_storage_type.field_stock_location__in_move_ids msgid "In Move" @@ -244,18 +256,24 @@ msgid "Inventory Locations" msgstr "" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product____last_update +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond____last_update #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence____last_update #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond____last_update msgid "Last Modified on" msgstr "" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__write_uid +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__write_uid #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__write_uid #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__write_uid msgid "Last Updated by" msgstr "" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__write_date +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__write_date #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__write_date #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__write_date msgid "Last Updated on" @@ -314,6 +332,13 @@ msgid "Max height should be a positive number." msgstr "" #. module: stock_storage_type +#: model:ir.model,name:stock_storage_type.model_stock_storage_condition_mixin +msgid "Mixin to implement storage condition." +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__name +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_condition_mixin__name #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__name msgid "Name" msgstr "" @@ -366,9 +391,10 @@ msgstr "" #: code:addons/stock_storage_type/models/stock_quant.py:0 #, python-format msgid "" -"Package {package} is not allowed into location {location}, because there isn't any storage capacity that allows package type {type} into it:\n" +"Package {package} is not allowed into location {location}, because there " +"isn't any rules that allows package type {type} into it:\n" "\n" -"{fails}" +"{error}" msgstr "" #. module: stock_storage_type @@ -421,13 +447,14 @@ msgid "Recompute Putaway" msgstr "" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__sequence #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__sequence msgid "Sequence" msgstr "" #. module: stock_storage_type #: model:ir.model,name:stock_storage_type.model_stock_storage_location_sequence -msgid "Sequence of locations to put-away the package storage type" +msgid "Sequence of locations to put-away the package type" msgstr "" #. module: stock_storage_type @@ -440,6 +467,11 @@ msgstr "" msgid "Stock Package Level" msgstr "" +#. module: stock_storage_type +#: model:ir.model,name:stock_storage_type.model_stock_storage_category_allow_new_product_cond +msgid "Stock Storage Category Allow New Product Condition" +msgstr "" + #. module: stock_storage_type #: model:ir.model,name:stock_storage_type.model_stock_storage_location_sequence_cond msgid "Stock Storage Location Sequence Condition" @@ -460,13 +492,35 @@ msgstr "" msgid "Stock storage location sequence condition name must be unique" msgstr "" +#. module: stock_storage_type +#: model:ir.model,name:stock_storage_type.model_stock_storage_category +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__storage_category_id +msgid "Storage Category" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.constraint,message:stock_storage_type.constraint_stock_storage_category_allow_new_product_cond_name +msgid "Storage Category Allow New Product Condition name must be unique" +msgstr "" + +#. module: stock_storage_type +#: model:ir.actions.act_window,name:stock_storage_type.stock_storage_category_allow_new_product_cond_act_window +#: model:ir.ui.menu,name:stock_storage_type.stock_storage_category_allow_new_product_cond_menu +msgid "Storage Category Allow New Product Conditions" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model,name:stock_storage_type.model_stock_storage_category_allow_new_product +msgid "Storage Category Allow New Product Rule" +msgstr "" + #. module: stock_storage_type #. odoo-python #: code:addons/stock_storage_type/models/stock_quant.py:0 #, python-format msgid "" -"Storage Capacity {storage_capacity} is flagged 'do not mix lots' but there " -"are other lots in location." +"Storage Category {category} defines max height of {max_h} but the package is " +"bigger: {height}." msgstr "" #. module: stock_storage_type @@ -474,8 +528,8 @@ msgstr "" #: code:addons/stock_storage_type/models/stock_quant.py:0 #, python-format msgid "" -"Storage Capacity {storage_capacity} is flagged 'do not mix products' but " -"there are other products in location." +"Storage Category {category} defines max weight of {max_w} but the package is " +"heavier: {weight_kg}." msgstr "" #. module: stock_storage_type @@ -483,18 +537,8 @@ msgstr "" #: code:addons/stock_storage_type/models/stock_quant.py:0 #, python-format msgid "" -"Storage Capacity {storage_capacity} is flagged 'only empty' with other " -"quants in location." -msgstr "" - -#. module: stock_storage_type -#: model:ir.model,name:stock_storage_type.model_stock_storage_category -msgid "Storage Category" -msgstr "" - -#. module: stock_storage_type -#: model:ir.model,name:stock_storage_type.model_stock_storage_category_capacity -msgid "Storage Category Capacity" +"Storage Category {category} is flagged 'do not mix lots' but there are other " +"lots in location." msgstr "" #. module: stock_storage_type @@ -502,8 +546,8 @@ msgstr "" #: code:addons/stock_storage_type/models/stock_quant.py:0 #, python-format msgid "" -"Storage Category {storage_category} defines max height of {max_h} but the " -"package is bigger: {height}." +"Storage Category {category} is flagged 'do not mix products' but there are " +"other products in location." msgstr "" #. module: stock_storage_type @@ -511,8 +555,8 @@ msgstr "" #: code:addons/stock_storage_type/models/stock_quant.py:0 #, python-format msgid "" -"Storage Category {storage_category} defines max weight of {max_w} but the " -"package is heavier: {weight_kg}." +"Storage Category {category} is flagged 'only empty' with other quants in " +"location." msgstr "" #. module: stock_storage_type @@ -545,7 +589,7 @@ msgid "Technical field, to speed up comparaisons" msgstr "" #. module: stock_storage_type -#: model:ir.model.fields,help:stock_storage_type.field_stock_storage_category_capacity__has_restrictions +#: model:ir.model.fields,help:stock_storage_type.field_stock_storage_category__has_restrictions msgid "Technical: This is used to check if we need to display warning message" msgstr "" @@ -566,16 +610,19 @@ msgstr "" #: model:ir.model.fields,help:stock_storage_type.field_stock_location__pack_putaway_strategy #: model:ir.model.fields,help:stock_storage_type.field_stock_storage_location_sequence__location_putaway_strategy msgid "" -"This defines the storage strategy based on package type to use when a product or package is put away in this location.\n" +"This defines the storage strategy based on package type to use when a " +"product or package is put away in this location.\n" "None: when moved to this location, it will not be put away any further.\n" -"Ordered Children Locations: when moved to this location, a suitable location will be searched in its children locations according to the restrictions defined on their respective location storage types." +"Ordered Children Locations: when moved to this location, a suitable location " +"will be searched in its children locations according to the restrictions " +"defined on their respective storage category." msgstr "" #. module: stock_storage_type #: model:ir.model.fields,help:stock_storage_type.field_stock_location__computed_storage_category_id msgid "" -"This represents the Storage Category that will be used. It depends either on" -" the category set on the location or on one of its parent." +"This represents the Storage Category that will be used. It depends either on " +"the category set on the location or on one of its parent." msgstr "" #. module: stock_storage_type @@ -603,16 +650,17 @@ msgstr "" #: code:addons/stock_storage_type/models/stock_package_type.py:0 #, python-format msgid "" -"When a package with storage type {name} is put away, the strategy will look " -"for an allowed location in the following locations:

    {message} " -"

    Note: this happens as long as these locations are " -"children of the stock move destination location or as long as these " -"locations are children of the destination location after the (product or " -"category) put-away is applied." +"When a package with type {name} is put away, the strategy will look for an " +"allowed location in the following locations:

    {message}

    Note: this happens as long as these locations are children of the " +"stock move destination location or as long as these locations are " +"children of the destination location after the (product or category) put-" +"away is applied." msgstr "" #. module: stock_storage_type #. odoo-python +#: code:addons/stock_storage_type/models/stock_storage_category_allow_new_product_cond.py:0 #: code:addons/stock_storage_type/models/stock_storage_location_sequence_cond.py:0 #, python-format msgid "code_snippet should return boolean value into `result` variable." @@ -623,7 +671,7 @@ msgstr "" msgid "" "technical field: True if the location is empty and there is no pending " "incoming products in the location. Computed only if the location needs to " -"check for emptiness (has an \"only empty\" location storage type)." +"check for emptiness (has an \"only empty\" policy)." msgstr "" #. module: stock_storage_type @@ -664,3 +712,10 @@ msgstr "" #: model:ir.model.fields,help:stock_storage_type.field_stock_location__out_move_line_ids msgid "technical field: the pending outgoing stock.move.lines in the location" msgstr "" + +#, python-format +#~ msgid "Allow New Product: " +#~ msgstr "Autoriser nouveau produit : " + +#~ msgid "Allow mixed products" +#~ msgstr "Autoriser un mix de produits" diff --git a/stock_storage_type/i18n/it.po b/stock_storage_type/i18n/it.po index 40c7092404d..2d7e271d093 100644 --- a/stock_storage_type/i18n/it.po +++ b/stock_storage_type/i18n/it.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0\n" "Report-Msgid-Bugs-To: \n" -"PO-Revision-Date: 2024-06-07 12:35+0000\n" +"PO-Revision-Date: 2025-10-07 10:43+0000\n" "Last-Translator: mymage \n" "Language-Team: none\n" "Language: it\n" @@ -14,7 +14,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.17\n" +"X-Generator: Weblate 5.10.4\n" #. module: stock_storage_type #. odoo-python @@ -22,11 +22,11 @@ msgstr "" #, python-format msgid "" " * {location} (WARNING: restrictions are " -"active on location storage types matching this package storage type)" +"active on storage categories matching this package type)" msgstr "" -" * {location} (ATTENZIONE: ci sono " -"restrizioni attive nei tipi stoccaggio ubicazione corrispondenti a questo " -"tipo stoccaggio collo)" +" * {location} (ATTENZIONE: sono attive " +"restrizioni sulle categorie di stoccaggio corrispondenti a questo tipo di " +"collo)" #. module: stock_storage_type #. odoo-python @@ -34,10 +34,10 @@ msgstr "" #, python-format msgid "" " * {location} (WARNING: no suitable location " -"matching storage type)" +"matching package type)" msgstr "" " * {location} (ATTENZIONE: nessuna ubicazione " -"disponibile corrispondente al tipo stoccaggio)" +"adatta corrisponde al tipo di collo)" #. module: stock_storage_type #. odoo-python @@ -45,36 +45,49 @@ msgstr "" #, python-format msgid "" "The \"Put-Away sequence\" must be defined in " -"order to put away packages using this package storage type ({storage})." +"order to put away packages using this package type ({storage})." msgstr "" "La \"Sequenza deposito\" deve essere definita " -"per depositare i colli utilizzando questo tipo stoccaggio collo " -"({storage})." +"per depositare i colli utilizzando questo tipo di collo ({storage})." #. module: stock_storage_type #: model:ir.model.fields,field_description:stock_storage_type.field_stock_package_type__active +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__active +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_condition_mixin__active #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__active msgid "Active" msgstr "Attiva" +#. module: stock_storage_type +#: model_terms:ir.ui.view,arch_db:stock_storage_type.stock_storage_category_form +msgid "Advanced" +msgstr "Avanzato" + +#. module: stock_storage_type +#: model:ir.model.fields,help:stock_storage_type.field_stock_storage_category_allow_new_product__condition_ids +msgid "All conditions have to match to apply the Allow New Product policy." +msgstr "" +"Per applicare la politica Consenti nuovi prodotti, tutte le condizioni " +"devono essere soddisfatte." + +#. module: stock_storage_type +#: model:ir.model.fields,help:stock_storage_type.field_stock_storage_location_sequence__location_sequence_cond_ids +msgid "All conditions have to match to apply the put-away strategy." +msgstr "" +"Per applicare la strategia di stoccaggio, devono essere soddisfatte tutte le " +"condizioni." + #. module: stock_storage_type #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category__allow_new_product -#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_capacity__allow_new_product +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__allow_new_product msgid "Allow New Product" msgstr "Consente nuovo prodotto" #. module: stock_storage_type -#. odoo-python -#: code:addons/stock_storage_type/models/stock_storage_category_capacity.py:0 -#, python-format -msgid "Allow New Product: " -msgstr "Consente nuovo prodotto: " - -#. module: stock_storage_type -#: model:ir.model.fields.selection,name:stock_storage_type.selection__stock_storage_category_capacity__allow_new_product__mixed -msgid "Allow mixed products" -msgstr "Consente prodotti misti" +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category__allow_new_product_ids +#: model_terms:ir.ui.view,arch_db:stock_storage_type.stock_storage_category_form +msgid "Allow New Product Rules" +msgstr "Consenti nuove regole prodotto" #. module: stock_storage_type #: model:ir.model.fields,help:stock_storage_type.field_stock_location__package_type_putaway_sequence @@ -91,6 +104,7 @@ msgid "Allowed Destinations" msgstr "Destinazioni consentite" #. module: stock_storage_type +#: model_terms:ir.ui.view,arch_db:stock_storage_type.stock_storage_category_allow_new_product_cond_view_form #: model_terms:ir.ui.view,arch_db:stock_storage_type.stock_storage_location_sequence_cond_form_view msgid "Archived" msgstr "In archivio" @@ -106,18 +120,21 @@ msgid "Capacity" msgstr "Capacità" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__code_snippet +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_condition_mixin__code_snippet #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__code_snippet msgid "Code Snippet" msgstr "Esempio codice" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__code_snippet_docs +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_condition_mixin__code_snippet_docs #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__code_snippet_docs msgid "Code Snippet Docs" msgstr "Documenti esempio codice" #. module: stock_storage_type #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category__computed_location_ids -#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_capacity__computed_location_ids msgid "Computed Location" msgstr "Calcola ubicazione" @@ -127,29 +144,36 @@ msgid "Computed Storage Category" msgstr "Calcola categoria stoccaggio" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__condition_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_condition_mixin__condition_type #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__condition_type msgid "Condition Type" msgstr "Tipo condizione" #. module: stock_storage_type #. odoo-python -#: code:addons/stock_storage_type/models/stock_storage_location_sequence_cond.py:0 +#: code:addons/stock_storage_type/models/stock_storage_condition_mixin.py:0 #, python-format msgid "Condition type is set to `Code`: you must provide a piece of code" msgstr "Il tipo condizione è impostato a `Codice`: fornire parte del codice" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__condition_ids #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__location_sequence_cond_ids msgid "Conditions" msgstr "Condizioni" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__create_uid +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__create_uid #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__create_uid #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__create_uid msgid "Created by" msgstr "Creato da" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__create_date +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__create_date #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__create_date #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__create_date msgid "Created on" @@ -164,7 +188,7 @@ msgid "" "for product not in a package" msgstr "" "Definisce un tipo collo 'predefinito' per questo prodotto da utilizzare nei " -"colli senza imballaggio prdotto e nel calcolo del deposito in base al tipo " +"colli senza imballaggio prodotto e nel calcolo del deposito in base al tipo " "collo per prodotto non in un collo" #. module: stock_storage_type @@ -173,6 +197,8 @@ msgid "Dimensions Units of Measure" msgstr "Unità di misura per le dimensioni" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__display_name +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__display_name #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__display_name #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__display_name msgid "Display Name" @@ -189,12 +215,14 @@ msgid "Do Not Mix Products" msgstr "Non mescolare prodotti" #. module: stock_storage_type +#: model:ir.model.fields.selection,name:stock_storage_type.selection__stock_storage_category_allow_new_product_cond__condition_type__code +#: model:ir.model.fields.selection,name:stock_storage_type.selection__stock_storage_condition_mixin__condition_type__code #: model:ir.model.fields.selection,name:stock_storage_type.selection__stock_storage_location_sequence_cond__condition_type__code msgid "Execute code" msgstr "Esegui codice" #. module: stock_storage_type -#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_capacity__has_restrictions +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category__has_restrictions msgid "Has Restrictions" msgstr "Ha restrizioni" @@ -205,9 +233,9 @@ msgstr "Altezza in metri" #. module: stock_storage_type #: model:ir.model.fields,help:stock_storage_type.field_stock_package_type__height_required -msgid "Height is mandatory for packages configured with this storage type." +msgid "Height is mandatory for packages configured with this package type." msgstr "" -"L'altezza è obbligatoria per colli configurati con questo tipo stoccaggio." +"L'altezza è obbligatoria per i colli configurati con questo tipo di collo." #. module: stock_storage_type #: model:ir.model.fields,field_description:stock_storage_type.field_stock_package_type__height_required @@ -215,31 +243,18 @@ msgid "Height required for packages" msgstr "Altezza richiesta per i colli" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__id +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__id #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__id #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__id msgid "ID" msgstr "ID" -#. module: stock_storage_type -#: model:ir.model.fields.selection,name:stock_storage_type.selection__stock_storage_category_capacity__allow_new_product__same_lot -msgid "If all lots are the same" -msgstr "Se tutti i lotti sono lo stesso" - -#. module: stock_storage_type -#: model:ir.model.fields.selection,name:stock_storage_type.selection__stock_storage_category_capacity__allow_new_product__same -msgid "If all products are same" -msgstr "Se tutti i prodotti sono lo stesso" - #. module: stock_storage_type #: model:ir.model.fields.selection,name:stock_storage_type.selection__stock_storage_category__allow_new_product__same_lot msgid "If lots are all the same" msgstr "Se i lotti sono uguali" -#. module: stock_storage_type -#: model:ir.model.fields.selection,name:stock_storage_type.selection__stock_storage_category_capacity__allow_new_product__empty -msgid "If the location is empty" -msgstr "Se l'ubicazione è vuota" - #. module: stock_storage_type #: model:ir.model.fields,field_description:stock_storage_type.field_stock_location__in_move_ids msgid "In Move" @@ -256,18 +271,24 @@ msgid "Inventory Locations" msgstr "Ubicazioni di inventario" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product____last_update +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond____last_update #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence____last_update #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond____last_update msgid "Last Modified on" msgstr "Ultima modifica il" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__write_uid +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__write_uid #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__write_uid #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__write_uid msgid "Last Updated by" msgstr "Ultimo aggiornamento di" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__write_date +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__write_date #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__write_date #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__write_date msgid "Last Updated on" @@ -326,6 +347,13 @@ msgid "Max height should be a positive number." msgstr "L'altezza massima deve essere un numero positivo." #. module: stock_storage_type +#: model:ir.model,name:stock_storage_type.model_stock_storage_condition_mixin +msgid "Mixin to implement storage condition." +msgstr "Mixin per implementare le condizioni di stoccaggio." + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__name +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_condition_mixin__name #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__name msgid "Name" msgstr "Nome" @@ -379,14 +407,14 @@ msgstr "Il tipo collo {storage} non è consentito nell'ubicazione {location}" #, python-format msgid "" "Package {package} is not allowed into location {location}, because there " -"isn't any storage capacity that allows package type {type} into it:\n" +"isn't any rules that allows package type {type} into it:\n" "\n" -"{fails}" +"{error}" msgstr "" -"Il collo {package} non è accettatto dall'ubicazione {location}, perché non " -"c'è capactà di stoccaggio che accetti il tipo collo {type}:\n" +"Il collo {package} non è consentito nell'ubicazione {location}, perché non " +"ci sono regole che consentano l'accesso al tipo di collo {type}:\n" "\n" -"{fails}" +"{error}" #. module: stock_storage_type #: model:ir.model,name:stock_storage_type.model_stock_quant_package @@ -438,14 +466,15 @@ msgid "Recompute Putaway" msgstr "Ricalcola deposito" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__sequence #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__sequence msgid "Sequence" msgstr "Sequenza" #. module: stock_storage_type #: model:ir.model,name:stock_storage_type.model_stock_storage_location_sequence -msgid "Sequence of locations to put-away the package storage type" -msgstr "Sequenza ubicazioni per deposito del tipo stoccaggio collo" +msgid "Sequence of locations to put-away the package type" +msgstr "Sequenza delle ubicazioni per stoccare il tipo di collo" #. module: stock_storage_type #: model_terms:ir.ui.view,arch_db:stock_storage_type.package_storage_location_tree_view @@ -457,6 +486,12 @@ msgstr "Visualizza ubicazioni" msgid "Stock Package Level" msgstr "Livello collo magazzino" +#. module: stock_storage_type +#: model:ir.model,name:stock_storage_type.model_stock_storage_category_allow_new_product_cond +msgid "Stock Storage Category Allow New Product Condition" +msgstr "" +"Categoria di stoccaggio delle giacenze Consenti nuova condizione del prodotto" + #. module: stock_storage_type #: model:ir.model,name:stock_storage_type.model_stock_storage_location_sequence_cond msgid "Stock Storage Location Sequence Condition" @@ -479,70 +514,84 @@ msgstr "" "Il nome della seqenza condizione ubicazione stoccaggio magazzino deve essere " "univoco" +#. module: stock_storage_type +#: model:ir.model,name:stock_storage_type.model_stock_storage_category +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__storage_category_id +msgid "Storage Category" +msgstr "Categoria stoccaggio" + +#. module: stock_storage_type +#: model:ir.model.constraint,message:stock_storage_type.constraint_stock_storage_category_allow_new_product_cond_name +msgid "Storage Category Allow New Product Condition name must be unique" +msgstr "" +"Il nome della categoria di stoccaggio Consenti nuova condizione prodotto " +"deve essere univoco" + +#. module: stock_storage_type +#: model:ir.actions.act_window,name:stock_storage_type.stock_storage_category_allow_new_product_cond_act_window +#: model:ir.ui.menu,name:stock_storage_type.stock_storage_category_allow_new_product_cond_menu +msgid "Storage Category Allow New Product Conditions" +msgstr "Categoria stoccaggio Consenti nuova categoria prodotto" + +#. module: stock_storage_type +#: model:ir.model,name:stock_storage_type.model_stock_storage_category_allow_new_product +msgid "Storage Category Allow New Product Rule" +msgstr "Categoria stoccaggio Consenti nuova regola prodotto" + #. module: stock_storage_type #. odoo-python #: code:addons/stock_storage_type/models/stock_quant.py:0 #, python-format msgid "" -"Storage Capacity {storage_capacity} is flagged 'do not mix lots' but there " -"are other lots in location." +"Storage Category {category} defines max height of {max_h} but the package is " +"bigger: {height}." msgstr "" -"La capacità stoccaggio {storage_capacity} è settata 'non mescolare lotti' ma " -"ci sono lotti diversi nell'ubicazione." +"La categoria stoccaggio {category} ha una altezza massima di {max_h} ma il " +"collo è più grande: {height}." #. module: stock_storage_type #. odoo-python #: code:addons/stock_storage_type/models/stock_quant.py:0 #, python-format msgid "" -"Storage Capacity {storage_capacity} is flagged 'do not mix products' but " -"there are other products in location." +"Storage Category {category} defines max weight of {max_w} but the package is " +"heavier: {weight_kg}." msgstr "" -"La capacità stoccaggio {storage_capacity} è settata 'non mescolare prdotti' " -"ma ci sono prodotti diversi nell'ubicazione." +"La categoria stoccaggio {category} ha un peso massimo di {max_w} ma il collo " +"è più pesante: {weight_kg}." #. module: stock_storage_type #. odoo-python #: code:addons/stock_storage_type/models/stock_quant.py:0 #, python-format msgid "" -"Storage Capacity {storage_capacity} is flagged 'only empty' with other " -"quants in location." +"Storage Category {category} is flagged 'do not mix lots' but there are other " +"lots in location." msgstr "" -"La capacità stoccaggio {storage_capacity} è settata 'solo vuoti' ma ci sono " -"altre quantità nell'ubicazione." - -#. module: stock_storage_type -#: model:ir.model,name:stock_storage_type.model_stock_storage_category -msgid "Storage Category" -msgstr "Categoria stoccaggio" - -#. module: stock_storage_type -#: model:ir.model,name:stock_storage_type.model_stock_storage_category_capacity -msgid "Storage Category Capacity" -msgstr "Capacità categoria stoccaggio" +"La categoria stoccaggio {category} è impostata a 'non mescolare i lotti' ma " +"ci sono altri lotti nell'ubicazione." #. module: stock_storage_type #. odoo-python #: code:addons/stock_storage_type/models/stock_quant.py:0 #, python-format msgid "" -"Storage Category {storage_category} defines max height of {max_h} but the " -"package is bigger: {height}." +"Storage Category {category} is flagged 'do not mix products' but there are " +"other products in location." msgstr "" -"La categoria stoccaggio {storage_category} ha una altezza massima di {max_h} " -"ma il collo è più grande: {height}." +"La categoria stoccaggio {category} è impostata a 'non mescolare i prodotti' " +"ma ci sono altri prodotti nell'ubicazione." #. module: stock_storage_type #. odoo-python #: code:addons/stock_storage_type/models/stock_quant.py:0 #, python-format msgid "" -"Storage Category {storage_category} defines max weight of {max_w} but the " -"package is heavier: {weight_kg}." +"Storage Category {category} is flagged 'only empty' with other quants in " +"location." msgstr "" -"La categoria stoccaggio {storage_category} ha un peso massimo di {max_w} ma " -"il collo è più pesante: {weight_kg}." +"La categoria stoccaggio {category} è impostata a 'solo vuote' con altri " +"quanti nell'ubicazione." #. module: stock_storage_type #: model:ir.ui.menu,name:stock_storage_type.stock_storage_location_sequence_cond_menu @@ -574,7 +623,7 @@ msgid "Technical field, to speed up comparaisons" msgstr "Campo tecnico, per velocizzare i confronti" #. module: stock_storage_type -#: model:ir.model.fields,help:stock_storage_type.field_stock_storage_category_capacity__has_restrictions +#: model:ir.model.fields,help:stock_storage_type.field_stock_storage_category__has_restrictions msgid "Technical: This is used to check if we need to display warning message" msgstr "" "Tecnico: viene utilizzato per controllare se serve visualizzare il messaggio " @@ -602,15 +651,15 @@ msgid "" "None: when moved to this location, it will not be put away any further.\n" "Ordered Children Locations: when moved to this location, a suitable location " "will be searched in its children locations according to the restrictions " -"defined on their respective location storage types." +"defined on their respective storage category." msgstr "" "Questo definisce la strategia di stoccaggio in base al tipo di collo da " -"utilizzare quanto un prodotto o un collo è depositato in questa ubicazione.\n" -"Nessuna: quanto movimentato in questa ubicazione, non verrà movimentato " +"utilizzare quando un prodotto o un collo è depositato in questa ubicazione.\n" +"Nessuna: quando movimentato in questa ubicazione, non verrà movimentato " "ulteriormente.\n" -"Ordinamento per ubicazioni figlie: quanto movimentato in questa ubicazione, " +"Ordinamento per ubicazioni figlie: quando movimentato in questa ubicazione, " "verrà cercata una ubicazione idonea nelle ubicazioni figlie in accordo con " -"le restrizioni definite nei rispettivi tipi stoccaggio ubicazione." +"le restrizioni definite nelle rispettive categorie stoccaggio." #. module: stock_storage_type #: model:ir.model.fields,help:stock_storage_type.field_stock_location__computed_storage_category_id @@ -619,7 +668,7 @@ msgid "" "the category set on the location or on one of its parent." msgstr "" "Questo rappresenta la categoria stoccaggio che verrà utilizzata. Dipende " -"dalla categoria mpostata nell'ubicazione o in uno dei suoi padri." +"dalla categoria impostata nell'ubicazione o in uno dei suoi padri." #. module: stock_storage_type #: model:ir.model.fields,help:stock_storage_type.field_stock_storage_category__length_uom_id @@ -646,22 +695,23 @@ msgstr "Etichetta unità di misura del peso" #: code:addons/stock_storage_type/models/stock_package_type.py:0 #, python-format msgid "" -"When a package with storage type {name} is put away, the strategy will look " -"for an allowed location in the following locations:

    {message}

    Note: this happens as long as these locations are children " -"of the stock move destination location or as long as these locations are " +"When a package with type {name} is put away, the strategy will look for an " +"allowed location in the following locations:

    {message}

    Note: this happens as long as these locations are children of the " +"stock move destination location or as long as these locations are " "children of the destination location after the (product or category) put-" "away is applied." msgstr "" -"Quando i colli con tipo stoccaggio {name} sono depositati, la strategia " -"cerca una ubicazione disponibile nelle segenti ubicazoni:

    {message}" -"

    Nota: questo accade finché queste ubicazioni sono " -"figlie della ubicazione destinazione del movimento di magazzino o finché " -"queste ubicazioni sono figlie dell'ubicazione di destinazione dopo che è " -"stato eseguito il deposito (prodotto o categoria)." +"Quando i colli con tipo {name} sono depositati, la strategia cerca una " +"ubicazione disponibile nelle seguenti ubicazioni:

    {message}
    <" +"br/>Nota: questo accade finché queste ubicazioni sono figlie della " +"ubicazione destinazione del movimento di magazzino o finché queste " +"ubicazioni sono figlie dell'ubicazione di destinazione dopo che è stato " +"eseguito il deposito (prodotto o categoria)." #. module: stock_storage_type #. odoo-python +#: code:addons/stock_storage_type/models/stock_storage_category_allow_new_product_cond.py:0 #: code:addons/stock_storage_type/models/stock_storage_location_sequence_cond.py:0 #, python-format msgid "code_snippet should return boolean value into `result` variable." @@ -672,12 +722,11 @@ msgstr "code_snippet deve restituire un buleano nella variabile `result`." msgid "" "technical field: True if the location is empty and there is no pending " "incoming products in the location. Computed only if the location needs to " -"check for emptiness (has an \"only empty\" location storage type)." +"check for emptiness (has an \"only empty\" policy)." msgstr "" -"campo tecnico: vero se l'ubicazione è vuta e non ci sono in sospeso ingressi " -"di prodotti nell'ubicazIone. Calcolato solo se l'ubicazione deve essere " -"controllata per rienpimento (ha un tipo stoccaggio ubicazione \"solo vuota" -"\")." +"Campo tecnico: Vero se l'ubicazione è vuota e non ci sono prodotti in arrivo " +"in sospeso. Calcolato solo se l'ubicazione deve verificare se vuota (ha una " +"politica \"solo vuote\")." #. module: stock_storage_type #: model:ir.model.fields,help:stock_storage_type.field_stock_location__leaf_location_ids @@ -722,3 +771,153 @@ msgstr "campo tecnico: i stock.moves in attesa nell'ubicazione" #: model:ir.model.fields,help:stock_storage_type.field_stock_location__out_move_line_ids msgid "technical field: the pending outgoing stock.move.lines in the location" msgstr "campo tecnico: le stock.move.lines in uscita in atesa nell'ubicazione" + +#, python-format +#~ msgid "" +#~ " * {location} (WARNING: restrictions are " +#~ "active on location storage types matching this package storage type)" +#~ msgstr "" +#~ " * {location} (ATTENZIONE: ci sono " +#~ "restrizioni attive nei tipi stoccaggio ubicazione corrispondenti a questo " +#~ "tipo stoccaggio collo)" + +#, python-format +#~ msgid "" +#~ " * {location} (WARNING: no suitable location " +#~ "matching storage type)" +#~ msgstr "" +#~ " * {location} (ATTENZIONE: nessuna ubicazione " +#~ "disponibile corrispondente al tipo stoccaggio)" + +#, python-format +#~ msgid "" +#~ "The \"Put-Away sequence\" must be defined in " +#~ "order to put away packages using this package storage type ({storage})." +#~ msgstr "" +#~ "La \"Sequenza deposito\" deve essere definita " +#~ "per depositare i colli utilizzando questo tipo stoccaggio collo " +#~ "({storage})." + +#, python-format +#~ msgid "Allow New Product: " +#~ msgstr "Consente nuovo prodotto: " + +#~ msgid "Allow mixed products" +#~ msgstr "Consente prodotti misti" + +#~ msgid "Height is mandatory for packages configured with this storage type." +#~ msgstr "" +#~ "L'altezza è obbligatoria per colli configurati con questo tipo stoccaggio." + +#~ msgid "If all lots are the same" +#~ msgstr "Se tutti i lotti sono lo stesso" + +#~ msgid "If all products are same" +#~ msgstr "Se tutti i prodotti sono lo stesso" + +#~ msgid "If the location is empty" +#~ msgstr "Se l'ubicazione è vuota" + +#, python-format +#~ msgid "" +#~ "Package {package} is not allowed into location {location}, because there " +#~ "isn't any storage capacity that allows package type {type} into it:\n" +#~ "\n" +#~ "{fails}" +#~ msgstr "" +#~ "Il collo {package} non è accettatto dall'ubicazione {location}, perché " +#~ "non c'è capactà di stoccaggio che accetti il tipo collo {type}:\n" +#~ "\n" +#~ "{fails}" + +#~ msgid "Sequence of locations to put-away the package storage type" +#~ msgstr "Sequenza ubicazioni per deposito del tipo stoccaggio collo" + +#, python-format +#~ msgid "" +#~ "Storage Capacity {storage_capacity} is flagged 'do not mix lots' but " +#~ "there are other lots in location." +#~ msgstr "" +#~ "La capacità stoccaggio {storage_capacity} è settata 'non mescolare lotti' " +#~ "ma ci sono lotti diversi nell'ubicazione." + +#, python-format +#~ msgid "" +#~ "Storage Capacity {storage_capacity} is flagged 'do not mix products' but " +#~ "there are other products in location." +#~ msgstr "" +#~ "La capacità stoccaggio {storage_capacity} è settata 'non mescolare " +#~ "prdotti' ma ci sono prodotti diversi nell'ubicazione." + +#, python-format +#~ msgid "" +#~ "Storage Capacity {storage_capacity} is flagged 'only empty' with other " +#~ "quants in location." +#~ msgstr "" +#~ "La capacità stoccaggio {storage_capacity} è settata 'solo vuoti' ma ci " +#~ "sono altre quantità nell'ubicazione." + +#~ msgid "Storage Category Capacity" +#~ msgstr "Capacità categoria stoccaggio" + +#, python-format +#~ msgid "" +#~ "Storage Category {storage_category} defines max height of {max_h} but the " +#~ "package is bigger: {height}." +#~ msgstr "" +#~ "La categoria stoccaggio {storage_category} ha una altezza massima di " +#~ "{max_h} ma il collo è più grande: {height}." + +#, python-format +#~ msgid "" +#~ "Storage Category {storage_category} defines max weight of {max_w} but the " +#~ "package is heavier: {weight_kg}." +#~ msgstr "" +#~ "La categoria stoccaggio {storage_category} ha un peso massimo di {max_w} " +#~ "ma il collo è più pesante: {weight_kg}." + +#~ msgid "" +#~ "This defines the storage strategy based on package type to use when a " +#~ "product or package is put away in this location.\n" +#~ "None: when moved to this location, it will not be put away any further.\n" +#~ "Ordered Children Locations: when moved to this location, a suitable " +#~ "location will be searched in its children locations according to the " +#~ "restrictions defined on their respective location storage types." +#~ msgstr "" +#~ "Questo definisce la strategia di stoccaggio in base al tipo di collo da " +#~ "utilizzare quanto un prodotto o un collo è depositato in questa " +#~ "ubicazione.\n" +#~ "Nessuna: quanto movimentato in questa ubicazione, non verrà movimentato " +#~ "ulteriormente.\n" +#~ "Ordinamento per ubicazioni figlie: quanto movimentato in questa " +#~ "ubicazione, verrà cercata una ubicazione idonea nelle ubicazioni figlie " +#~ "in accordo con le restrizioni definite nei rispettivi tipi stoccaggio " +#~ "ubicazione." + +#, python-format +#~ msgid "" +#~ "When a package with storage type {name} is put away, the strategy will " +#~ "look for an allowed location in the following locations:

    {message}

    Note: this happens as long as these locations " +#~ "are children of the stock move destination location or as long as " +#~ "these locations are children of the destination location after the " +#~ "(product or category) put-away is applied." +#~ msgstr "" +#~ "Quando i colli con tipo stoccaggio {name} sono depositati, la strategia " +#~ "cerca una ubicazione disponibile nelle segenti ubicazoni:

    {message}

    Nota: questo accade finché queste ubicazioni " +#~ "sono figlie della ubicazione destinazione del movimento di magazzino o finché queste ubicazioni sono figlie dell'ubicazione di destinazione " +#~ "dopo che è stato eseguito il deposito (prodotto o categoria)." + +#~ msgid "" +#~ "technical field: True if the location is empty and there is no pending " +#~ "incoming products in the location. Computed only if the location needs " +#~ "to check for emptiness (has an \"only empty\" location storage type)." +#~ msgstr "" +#~ "campo tecnico: vero se l'ubicazione è vuta e non ci sono in sospeso " +#~ "ingressi di prodotti nell'ubicazIone. Calcolato solo se l'ubicazione deve " +#~ "essere controllata per rienpimento (ha un tipo stoccaggio ubicazione " +#~ "\"solo vuota\")." diff --git a/stock_storage_type/i18n/stock_storage_type.pot b/stock_storage_type/i18n/stock_storage_type.pot index 33290bbb286..09502e30c7a 100644 --- a/stock_storage_type/i18n/stock_storage_type.pot +++ b/stock_storage_type/i18n/stock_storage_type.pot @@ -19,7 +19,7 @@ msgstr "" #, python-format msgid "" " * {location} (WARNING: restrictions are " -"active on location storage types matching this package storage type)" +"active on storage categories matching this package type)
    " msgstr "" #. module: stock_storage_type @@ -28,7 +28,7 @@ msgstr "" #, python-format msgid "" " * {location} (WARNING: no suitable location " -"matching storage type)" +"matching package type)
    " msgstr "" #. module: stock_storage_type @@ -37,32 +37,42 @@ msgstr "" #, python-format msgid "" "The \"Put-Away sequence\" must be defined in " -"order to put away packages using this package storage type " -"({storage})." +"order to put away packages using this package type ({storage})." msgstr "" #. module: stock_storage_type #: model:ir.model.fields,field_description:stock_storage_type.field_stock_package_type__active +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__active +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_condition_mixin__active #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__active msgid "Active" msgstr "" #. module: stock_storage_type -#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category__allow_new_product -#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_capacity__allow_new_product -msgid "Allow New Product" +#: model_terms:ir.ui.view,arch_db:stock_storage_type.stock_storage_category_form +msgid "Advanced" msgstr "" #. module: stock_storage_type -#. odoo-python -#: code:addons/stock_storage_type/models/stock_storage_category_capacity.py:0 -#, python-format -msgid "Allow New Product: " +#: model:ir.model.fields,help:stock_storage_type.field_stock_storage_category_allow_new_product__condition_ids +msgid "All conditions have to match to apply the Allow New Product policy." msgstr "" #. module: stock_storage_type -#: model:ir.model.fields.selection,name:stock_storage_type.selection__stock_storage_category_capacity__allow_new_product__mixed -msgid "Allow mixed products" +#: model:ir.model.fields,help:stock_storage_type.field_stock_storage_location_sequence__location_sequence_cond_ids +msgid "All conditions have to match to apply the put-away strategy." +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category__allow_new_product +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__allow_new_product +msgid "Allow New Product" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category__allow_new_product_ids +#: model_terms:ir.ui.view,arch_db:stock_storage_type.stock_storage_category_form +msgid "Allow New Product Rules" msgstr "" #. module: stock_storage_type @@ -78,6 +88,7 @@ msgid "Allowed Destinations" msgstr "" #. module: stock_storage_type +#: model_terms:ir.ui.view,arch_db:stock_storage_type.stock_storage_category_allow_new_product_cond_view_form #: model_terms:ir.ui.view,arch_db:stock_storage_type.stock_storage_location_sequence_cond_form_view msgid "Archived" msgstr "" @@ -93,18 +104,21 @@ msgid "Capacity" msgstr "" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__code_snippet +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_condition_mixin__code_snippet #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__code_snippet msgid "Code Snippet" msgstr "" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__code_snippet_docs +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_condition_mixin__code_snippet_docs #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__code_snippet_docs msgid "Code Snippet Docs" msgstr "" #. module: stock_storage_type #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category__computed_location_ids -#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_capacity__computed_location_ids msgid "Computed Location" msgstr "" @@ -114,29 +128,36 @@ msgid "Computed Storage Category" msgstr "" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__condition_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_condition_mixin__condition_type #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__condition_type msgid "Condition Type" msgstr "" #. module: stock_storage_type #. odoo-python -#: code:addons/stock_storage_type/models/stock_storage_location_sequence_cond.py:0 +#: code:addons/stock_storage_type/models/stock_storage_condition_mixin.py:0 #, python-format msgid "Condition type is set to `Code`: you must provide a piece of code" msgstr "" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__condition_ids #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__location_sequence_cond_ids msgid "Conditions" msgstr "" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__create_uid +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__create_uid #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__create_uid #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__create_uid msgid "Created by" msgstr "" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__create_date +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__create_date #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__create_date #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__create_date msgid "Created on" @@ -157,6 +178,8 @@ msgid "Dimensions Units of Measure" msgstr "" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__display_name +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__display_name #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__display_name #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__display_name msgid "Display Name" @@ -173,12 +196,14 @@ msgid "Do Not Mix Products" msgstr "" #. module: stock_storage_type +#: model:ir.model.fields.selection,name:stock_storage_type.selection__stock_storage_category_allow_new_product_cond__condition_type__code +#: model:ir.model.fields.selection,name:stock_storage_type.selection__stock_storage_condition_mixin__condition_type__code #: model:ir.model.fields.selection,name:stock_storage_type.selection__stock_storage_location_sequence_cond__condition_type__code msgid "Execute code" msgstr "" #. module: stock_storage_type -#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_capacity__has_restrictions +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category__has_restrictions msgid "Has Restrictions" msgstr "" @@ -189,7 +214,7 @@ msgstr "" #. module: stock_storage_type #: model:ir.model.fields,help:stock_storage_type.field_stock_package_type__height_required -msgid "Height is mandatory for packages configured with this storage type." +msgid "Height is mandatory for packages configured with this package type." msgstr "" #. module: stock_storage_type @@ -198,31 +223,18 @@ msgid "Height required for packages" msgstr "" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__id +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__id #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__id #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__id msgid "ID" msgstr "" -#. module: stock_storage_type -#: model:ir.model.fields.selection,name:stock_storage_type.selection__stock_storage_category_capacity__allow_new_product__same_lot -msgid "If all lots are the same" -msgstr "" - -#. module: stock_storage_type -#: model:ir.model.fields.selection,name:stock_storage_type.selection__stock_storage_category_capacity__allow_new_product__same -msgid "If all products are same" -msgstr "" - #. module: stock_storage_type #: model:ir.model.fields.selection,name:stock_storage_type.selection__stock_storage_category__allow_new_product__same_lot msgid "If lots are all the same" msgstr "" -#. module: stock_storage_type -#: model:ir.model.fields.selection,name:stock_storage_type.selection__stock_storage_category_capacity__allow_new_product__empty -msgid "If the location is empty" -msgstr "" - #. module: stock_storage_type #: model:ir.model.fields,field_description:stock_storage_type.field_stock_location__in_move_ids msgid "In Move" @@ -239,18 +251,24 @@ msgid "Inventory Locations" msgstr "" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product____last_update +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond____last_update #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence____last_update #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond____last_update msgid "Last Modified on" msgstr "" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__write_uid +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__write_uid #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__write_uid #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__write_uid msgid "Last Updated by" msgstr "" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__write_date +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__write_date #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__write_date #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__write_date msgid "Last Updated on" @@ -309,6 +327,13 @@ msgid "Max height should be a positive number." msgstr "" #. module: stock_storage_type +#: model:ir.model,name:stock_storage_type.model_stock_storage_condition_mixin +msgid "Mixin to implement storage condition." +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product_cond__name +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_condition_mixin__name #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__name msgid "Name" msgstr "" @@ -361,9 +386,9 @@ msgstr "" #: code:addons/stock_storage_type/models/stock_quant.py:0 #, python-format msgid "" -"Package {package} is not allowed into location {location}, because there isn't any storage capacity that allows package type {type} into it:\n" +"Package {package} is not allowed into location {location}, because there isn't any rules that allows package type {type} into it:\n" "\n" -"{fails}" +"{error}" msgstr "" #. module: stock_storage_type @@ -416,13 +441,14 @@ msgid "Recompute Putaway" msgstr "" #. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__sequence #: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__sequence msgid "Sequence" msgstr "" #. module: stock_storage_type #: model:ir.model,name:stock_storage_type.model_stock_storage_location_sequence -msgid "Sequence of locations to put-away the package storage type" +msgid "Sequence of locations to put-away the package type" msgstr "" #. module: stock_storage_type @@ -435,6 +461,11 @@ msgstr "" msgid "Stock Package Level" msgstr "" +#. module: stock_storage_type +#: model:ir.model,name:stock_storage_type.model_stock_storage_category_allow_new_product_cond +msgid "Stock Storage Category Allow New Product Condition" +msgstr "" + #. module: stock_storage_type #: model:ir.model,name:stock_storage_type.model_stock_storage_location_sequence_cond msgid "Stock Storage Location Sequence Condition" @@ -455,13 +486,35 @@ msgstr "" msgid "Stock storage location sequence condition name must be unique" msgstr "" +#. module: stock_storage_type +#: model:ir.model,name:stock_storage_type.model_stock_storage_category +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_category_allow_new_product__storage_category_id +msgid "Storage Category" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.constraint,message:stock_storage_type.constraint_stock_storage_category_allow_new_product_cond_name +msgid "Storage Category Allow New Product Condition name must be unique" +msgstr "" + +#. module: stock_storage_type +#: model:ir.actions.act_window,name:stock_storage_type.stock_storage_category_allow_new_product_cond_act_window +#: model:ir.ui.menu,name:stock_storage_type.stock_storage_category_allow_new_product_cond_menu +msgid "Storage Category Allow New Product Conditions" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model,name:stock_storage_type.model_stock_storage_category_allow_new_product +msgid "Storage Category Allow New Product Rule" +msgstr "" + #. module: stock_storage_type #. odoo-python #: code:addons/stock_storage_type/models/stock_quant.py:0 #, python-format msgid "" -"Storage Capacity {storage_capacity} is flagged 'do not mix lots' but there " -"are other lots in location." +"Storage Category {category} defines max height of {max_h} but the package is" +" bigger: {height}." msgstr "" #. module: stock_storage_type @@ -469,8 +522,8 @@ msgstr "" #: code:addons/stock_storage_type/models/stock_quant.py:0 #, python-format msgid "" -"Storage Capacity {storage_capacity} is flagged 'do not mix products' but " -"there are other products in location." +"Storage Category {category} defines max weight of {max_w} but the package is" +" heavier: {weight_kg}." msgstr "" #. module: stock_storage_type @@ -478,18 +531,8 @@ msgstr "" #: code:addons/stock_storage_type/models/stock_quant.py:0 #, python-format msgid "" -"Storage Capacity {storage_capacity} is flagged 'only empty' with other " -"quants in location." -msgstr "" - -#. module: stock_storage_type -#: model:ir.model,name:stock_storage_type.model_stock_storage_category -msgid "Storage Category" -msgstr "" - -#. module: stock_storage_type -#: model:ir.model,name:stock_storage_type.model_stock_storage_category_capacity -msgid "Storage Category Capacity" +"Storage Category {category} is flagged 'do not mix lots' but there are other" +" lots in location." msgstr "" #. module: stock_storage_type @@ -497,8 +540,8 @@ msgstr "" #: code:addons/stock_storage_type/models/stock_quant.py:0 #, python-format msgid "" -"Storage Category {storage_category} defines max height of {max_h} but the " -"package is bigger: {height}." +"Storage Category {category} is flagged 'do not mix products' but there are " +"other products in location." msgstr "" #. module: stock_storage_type @@ -506,8 +549,8 @@ msgstr "" #: code:addons/stock_storage_type/models/stock_quant.py:0 #, python-format msgid "" -"Storage Category {storage_category} defines max weight of {max_w} but the " -"package is heavier: {weight_kg}." +"Storage Category {category} is flagged 'only empty' with other quants in " +"location." msgstr "" #. module: stock_storage_type @@ -540,7 +583,7 @@ msgid "Technical field, to speed up comparaisons" msgstr "" #. module: stock_storage_type -#: model:ir.model.fields,help:stock_storage_type.field_stock_storage_category_capacity__has_restrictions +#: model:ir.model.fields,help:stock_storage_type.field_stock_storage_category__has_restrictions msgid "Technical: This is used to check if we need to display warning message" msgstr "" @@ -563,7 +606,7 @@ msgstr "" msgid "" "This defines the storage strategy based on package type to use when a product or package is put away in this location.\n" "None: when moved to this location, it will not be put away any further.\n" -"Ordered Children Locations: when moved to this location, a suitable location will be searched in its children locations according to the restrictions defined on their respective location storage types." +"Ordered Children Locations: when moved to this location, a suitable location will be searched in its children locations according to the restrictions defined on their respective storage category." msgstr "" #. module: stock_storage_type @@ -598,8 +641,8 @@ msgstr "" #: code:addons/stock_storage_type/models/stock_package_type.py:0 #, python-format msgid "" -"When a package with storage type {name} is put away, the strategy will look " -"for an allowed location in the following locations:

    {message} " +"When a package with type {name} is put away, the strategy will look for an " +"allowed location in the following locations:

    {message} " "

    Note: this happens as long as these locations are " "children of the stock move destination location or as long as these " "locations are children of the destination location after the (product or " @@ -608,6 +651,7 @@ msgstr "" #. module: stock_storage_type #. odoo-python +#: code:addons/stock_storage_type/models/stock_storage_category_allow_new_product_cond.py:0 #: code:addons/stock_storage_type/models/stock_storage_location_sequence_cond.py:0 #, python-format msgid "code_snippet should return boolean value into `result` variable." @@ -618,7 +662,7 @@ msgstr "" msgid "" "technical field: True if the location is empty and there is no pending " "incoming products in the location. Computed only if the location needs to " -"check for emptiness (has an \"only empty\" location storage type)." +"check for emptiness (has an \"only empty\" policy)." msgstr "" #. module: stock_storage_type diff --git a/stock_storage_type/migrations/16.0.1.0.1/post-migrate.py b/stock_storage_type/migrations/16.0.1.0.1/post-migrate.py index 532acba534a..8ad4f5c6f46 100644 --- a/stock_storage_type/migrations/16.0.1.0.1/post-migrate.py +++ b/stock_storage_type/migrations/16.0.1.0.1/post-migrate.py @@ -61,6 +61,16 @@ def _move_location_storage_type(env): WHERE slst.id = sscc.old_location_storage_type_id """ openupgrade.logged_query(env.cr, query) + query = """ + UPDATE stock_storage_category ssc + SET max_height = slst.max_height + FROM stock_location_storage_type slst, + stock_storage_category_capacity sscc + WHERE slst.id = sscc.old_location_storage_type_id + AND sscc.storage_category_id = ssc.id + AND slst.max_height > 0 + """ + openupgrade.logged_query(env.cr, query) def _update_location_sequence(env): diff --git a/stock_storage_type/migrations/16.0.2.0.0/post-migrate.py b/stock_storage_type/migrations/16.0.2.0.0/post-migrate.py new file mode 100644 index 00000000000..0c1c2d2f7e0 --- /dev/null +++ b/stock_storage_type/migrations/16.0.2.0.0/post-migrate.py @@ -0,0 +1,63 @@ +# Copyright 2025 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +import logging + +from odoo import SUPERUSER_ID, api, fields + +_logger = logging.getLogger(__name__) + + +def migrate(cr, version): + if not version: + return + env = api.Environment(cr, SUPERUSER_ID, {}) + migrate_capacity_allow_new_product_to_category_allow_new_product_rules(env) + + +def migrate_capacity_allow_new_product_to_category_allow_new_product_rules(env): + _logger.info( + "Migrate '.allow_new_product' values to " + "category allow_new_product rules..." + ) + query = """ + SELECT sscc.id, storage_category_id, sscc.allow_new_product, package_type_id + FROM stock_storage_category_capacity sscc + LEFT JOIN stock_storage_category ssc + ON sscc.storage_category_id = ssc.id + WHERE sscc.allow_new_product != ssc.allow_new_product; + """ + env.cr.execute(query) + capacities = env.cr.dictfetchall() + condition_model = env["stock.storage.category.allow_new_product.cond"] + for capacity in capacities: + _logger.info("row = %s", capacity) + package_type = env["stock.package.type"].browse(capacity["package_type_id"]) + package_type_name = package_type.name if package_type else "Any package type" + condition_name = f"[MIG] {package_type_name}" + condition = condition_model.search([("name", "=", condition_name)], limit=1) + if not condition: + if package_type: + code_snippet = f""" +result = False +if package_type and package_type.id == {package_type.id}: + result = True + """ + else: + code_snippet = """ +result = False +if not package_type: + result = True + """ + vals = { + "name": condition_name, + "code_snippet": code_snippet, + } + condition = condition_model.create(vals) + # Bind the condition with the category through a allow_new_product rule + category = env["stock.storage.category"].browse(capacity["storage_category_id"]) + vals = { + "allow_new_product": capacity["allow_new_product"], + "condition_ids": [fields.Command.link(condition.id)], + } + category.write({"allow_new_product_ids": [fields.Command.create(vals)]}) diff --git a/stock_storage_type/models/__init__.py b/stock_storage_type/models/__init__.py index ef4b4162a17..2c5f31919dd 100644 --- a/stock_storage_type/models/__init__.py +++ b/stock_storage_type/models/__init__.py @@ -5,8 +5,10 @@ stock_package_type, stock_quant, stock_quant_package, + stock_storage_condition_mixin, stock_storage_category, - stock_storage_category_capacity, + stock_storage_category_allow_new_product, + stock_storage_category_allow_new_product_cond, stock_storage_location_sequence, stock_storage_location_sequence_cond, ) diff --git a/stock_storage_type/models/stock_location.py b/stock_storage_type/models/stock_location.py index 9672dcce608..72d2de071cd 100644 --- a/stock_storage_type/models/stock_location.py +++ b/stock_storage_type/models/stock_location.py @@ -46,7 +46,7 @@ class StockLocation(models.Model): "Ordered Children Locations: when moved to this " "location, a suitable location will be searched in its children " "locations according to the restrictions defined on their " - "respective location storage types.", + "respective storage category.", ) package_type_putaway_sequence = fields.Integer( string="Putaway Sequence", @@ -64,7 +64,7 @@ class StockLocation(models.Model): help="technical field: True if the location is empty " "and there is no pending incoming products in the location. " " Computed only if the location needs to check for emptiness " - '(has an "only empty" location storage type).', + '(has an "only empty" policy).', recursive=True, ) # TODO: Maybe renaming these fields as there are already such fields @@ -158,7 +158,7 @@ def init(self): # pylint: disable=missing-return @api.depends( "usage", "computed_storage_category_id.allow_new_product", - "computed_storage_category_id.capacity_ids.allow_new_product", + "computed_storage_category_id.allow_new_product_ids.allow_new_product", ) def _compute_do_not_mix_lots(self): """ @@ -167,18 +167,16 @@ def _compute_do_not_mix_lots(self): - one of its Storage Capacities value """ for rec in self: + rules = rec.computed_storage_category_id.allow_new_product_ids rec.do_not_mix_lots = rec.usage == "internal" and ( - any( - storage_type.allow_new_product == "same_lot" - for storage_type in rec.computed_storage_category_id.capacity_ids - ) + any(rule.allow_new_product == "same_lot" for rule in rules) or rec.computed_storage_category_id.allow_new_product == "same_lot" ) @api.depends( "usage", "computed_storage_category_id.allow_new_product", - "computed_storage_category_id.capacity_ids.allow_new_product", + "computed_storage_category_id.allow_new_product_ids.allow_new_product", ) def _compute_only_empty(self): """ @@ -187,18 +185,16 @@ def _compute_only_empty(self): - one of its Storage Capacities value """ for rec in self: + rules = rec.computed_storage_category_id.allow_new_product_ids rec.only_empty = rec.usage == "internal" and ( - any( - storage_type.allow_new_product == "empty" - for storage_type in rec.computed_storage_category_id.capacity_ids - ) + any(rule.allow_new_product == "empty" for rule in rules) or rec.computed_storage_category_id.allow_new_product == "empty" ) @api.depends( "usage", "computed_storage_category_id.allow_new_product", - "computed_storage_category_id.capacity_ids.allow_new_product", + "computed_storage_category_id.allow_new_product_ids.allow_new_product", ) def _compute_do_not_mix_products(self): """ @@ -207,11 +203,9 @@ def _compute_do_not_mix_products(self): - one of its Storage Capacities value """ for rec in self: + rules = rec.computed_storage_category_id.allow_new_product_ids rec.do_not_mix_products = rec.usage == "internal" and ( - any( - storage_type.allow_new_product in ("same", "same_lot") - for storage_type in rec.computed_storage_category_id.capacity_ids - ) + any(rule.allow_new_product in ("same", "same_lot") for rule in rules) or rec.computed_storage_category_id.allow_new_product in ("same", "same_lot") ) @@ -513,30 +507,29 @@ def select_first_allowed_location(self, package_type, quants, products): allowed = self.select_allowed_locations(package_type, quants, products, limit=1) return allowed - def _domain_location_storage_type_constraints(self, package_type, quants, products): - """Compute the domain for the location storage type which match the package - storage type + def _domain_storage_category_constraints(self, package_type, quants, products): + """Compute the domain for the storage category which matches the package type. - This method also checks the "capacity" constraints (height and weight) + This method also checks the category constraints (height and weight) """ - # There can be multiple location storage types for a given + # There can be multiple storage capacities for a given # location, so we need to filter on the ones relative to the package # we consider. - Capacity = self.env["stock.storage.category.capacity"] - compatible_location_storage_types = Capacity.search( + Category = self.env["stock.storage.category"] + compatible_categories = Category.search( [("computed_location_ids", "in", self.ids)] ) - pertinent_loc_storagetype_domain = [ - ("id", "in", compatible_location_storage_types.ids), - ("package_type_id", "=", package_type.id), + pertinent_category_domain = [ + ("id", "in", compatible_categories.ids), + ("capacity_ids.package_type_id", "=", package_type.id), ] if quants.package_id.height: - pertinent_loc_storagetype_domain += [ + pertinent_category_domain += [ "|", - ("storage_category_id.max_height_in_m", "=", 0), + ("max_height_in_m", "=", 0), ( - "storage_category_id.max_height_in_m", + "max_height_in_m", ">=", quants.package_id.height_in_m, ), @@ -546,23 +539,23 @@ def _domain_location_storage_type_constraints(self, package_type, quants, produc or quants.package_id.estimated_pack_weight_kg ) if package_weight_kg: - pertinent_loc_storagetype_domain += [ + pertinent_category_domain += [ "|", - ("storage_category_id.max_weight_in_kg", "=", 0), - ("storage_category_id.max_weight_in_kg", ">=", package_weight_kg), + ("max_weight_in_kg", "=", 0), + ("max_weight_in_kg", ">=", package_weight_kg), ] _logger.debug( - "pertinent storage type domain: %s", pertinent_loc_storagetype_domain + "pertinent storage category domain: %s", pertinent_category_domain ) - return pertinent_loc_storagetype_domain + return pertinent_category_domain - def _allowed_locations_for_location_storage_types( - self, location_storage_types, quants, products + def _allowed_locations_for_storage_categories( + self, categories, quants, products, package_type ): valid_location_ids = set() - for loc_storage_type in location_storage_types: - location_domain = loc_storage_type._domain_location_storage_type( - self, quants, products + for category in categories: + location_domain = category._domain_location_storage_category( + self, quants, products, package_type ) _logger.debug("pertinent location domain: %s", location_domain) locations = self.search(location_domain) @@ -581,7 +574,7 @@ def _select_final_valid_putaway_locations(self, limit=None): return self[:limit] def select_allowed_locations(self, package_type, quants, products, limit=None): - """Filter allowed locations for a storage type + """Filter allowed locations for a package type. ``self`` contains locations already ordered according to the putaway strategy, so beware of the return that must keep the @@ -589,45 +582,31 @@ def select_allowed_locations(self, package_type, quants, products, limit=None): """ # We have package who may be placed in a stock.location # - # 1. On the stock.location there are location_storage_type and on the - # packages there are package_storage_type. Between both, there's a m2m - # who says which package ST can be placed in which location ST + # 1. On the location there is a storage category that defines which + # package type is allowed. This is given by the storage capacities. # - # 2. On a location_ST there are some additional restrictions: a - - # capacity (volume / height / weight) and b - properties (boolean + # 2. On a storage category there are some additional restrictions: + # a - capacity (volume / height / weight) and b - properties (boolean # flags: only empty, don't mix lots, don't mix products) - Capacity = self.env["stock.storage.category.capacity"] _logger.debug( - "select allowed location for package storage type %s (q=%s, p=%s)", + "select allowed location for package type %s (q=%s, p=%s)", package_type.name, quants, products.mapped("name"), ) - # 1: filter locations on compatible storage type - compatible_locations = self.search( - [ - ("id", "in", self.ids), - ( - "computed_storage_category_id.capacity_ids", - "in", - package_type.storage_category_capacity_ids.ids, - ), - ] - ) - pertinent_loc_s_t_domain = ( - compatible_locations._domain_location_storage_type_constraints( - package_type, quants, products - ) + # 1: filter pertinent storage categories + pertinent_category_domain = self._domain_storage_category_constraints( + package_type, quants, products ) - pertinent_loc_storage_types = Capacity.search(pertinent_loc_s_t_domain) + pertinent_categories = self.env["stock.storage.category"].search( + pertinent_category_domain + ) - # now loop over the pertinent location storage types (there should be + # now loop over the pertinent categories (there should be # few of them) and check for properties to find suitable locations - valid_locations = ( - compatible_locations._allowed_locations_for_location_storage_types( - pertinent_loc_storage_types, quants, products - ) + valid_locations = self._allowed_locations_for_storage_categories( + pertinent_categories, quants, products, package_type ) valid_locations = self._order_allowed_locations(valid_locations) diff --git a/stock_storage_type/models/stock_package_type.py b/stock_storage_type/models/stock_package_type.py index ab93b299c33..d099958cd71 100644 --- a/stock_storage_type/models/stock_package_type.py +++ b/stock_storage_type/models/stock_package_type.py @@ -16,7 +16,7 @@ class StockPackageType(models.Model): storage_type_message = fields.Html(compute="_compute_storage_type_message") height_required = fields.Boolean( string="Height required for packages", - help=("Height is mandatory for packages configured with this storage type."), + help=("Height is mandatory for packages configured with this package type."), default=False, ) barcode = fields.Char(copy=False) @@ -36,10 +36,10 @@ def _compute_storage_type_message(self): if sl == storage_locations[-1]: last = True formatted_storage_locations_msgs.append( - sl._format_package_storage_type_message(last=last) + sl._format_package_type_message(last=last) ) msg = _( - "When a package with storage type {name} is put away, the " + "When a package with type {name} is put away, the " "strategy will look for an allowed location in the " "following locations:

    " "{message}

    " @@ -56,7 +56,7 @@ def _compute_storage_type_message(self): msg = _( 'The "Put-Away sequence" ' "must be defined in order to put away packages using " - "this package storage type ({storage})." + "this package type ({storage})." ).format(storage=package_type.name) package_type.storage_type_message = msg diff --git a/stock_storage_type/models/stock_quant.py b/stock_storage_type/models/stock_quant.py index ce08ee4ef23..effebd99c38 100644 --- a/stock_storage_type/models/stock_quant.py +++ b/stock_storage_type/models/stock_quant.py @@ -30,7 +30,6 @@ def _check_storage_capacities(self): "Location {location}" ).format(storage=package_type.name, location=location.name) ) - allowed = False package_weight_kg = ( quant.package_id.pack_weight_in_kg or quant.package_id.estimated_pack_weight_kg @@ -47,94 +46,80 @@ def _check_storage_capacities(self): ) products_in_location = other_quants_in_location.mapped("product_id") lots_in_location = other_quants_in_location.mapped("lot_id") - capacity_fails = [] - for capacity in allowed_capacities: - # Check content constraints - if capacity.allow_new_product == "empty" and other_quants_in_location: - capacity_fails.append( - _( - "Storage Capacity {storage_capacity} is flagged " - "'only empty'" - " with other quants in location." - ).format(storage_capacity=capacity.display_name) - ) - continue - if capacity.allow_new_product == "same" and ( - len(package_products) > 1 - or len(products_in_location) >= 1 - and package_products != products_in_location - ): - capacity_fails.append( - _( - "Storage Capacity {storage_capacity} is flagged 'do not mix" - " products' but there are other products in " - "location." - ).format(storage_capacity=capacity.display_name) - ) - continue - if capacity.allow_new_product == "same_lot" and ( - len(package_lots) > 1 - or len(lots_in_location) >= 1 - and package_lots != lots_in_location - ): - capacity_fails.append( - _( - "Storage Capacity {storage_capacity} is flagged 'do not mix" - " lots' but there are other lots in " - "location." - ).format(storage_capacity=capacity.display_name) - ) - continue - # Check size constraint - if ( - capacity.storage_category_id.max_height_in_m - and quant.package_id.height_in_m - > capacity.storage_category_id.max_height_in_m - ): - capacity_fails.append( - _( - "Storage Category {storage_category} defines " - "max height of {max_h} but the package is bigger: " - "{height}." - ).format( - storage_category=capacity.storage_category_id.display_name, - max_h=capacity.storage_category_id.max_height_in_m, - height=quant.package_id.height_in_m, - ) - ) - continue - if ( - capacity.storage_category_id.max_weight_in_kg - and package_weight_kg - > capacity.storage_category_id.max_weight_in_kg - ): - capacity_fails.append( - _( - "Storage Category {storage_category} defines " - "max weight of {max_w} but the package is heavier: " - "{weight_kg}." - ).format( - storage_category=capacity.storage_category_id.display_name, - max_w=capacity.storage_category_id.max_weight_in_kg, - weight_kg=package_weight_kg, - ) - ) - continue - # If we get here, it means there is a location storage type - # allowing the package into the location - allowed = True - break - if not allowed: + error = None + category = location.computed_storage_category_id + allow_new_product = category.get_allow_new_product( + product=quant.product_id, + package_type=package_type, + package=quant.package_id, + quants=quant, + ) + # Check content constraints + if allow_new_product == "empty" and other_quants_in_location: + error = _( + "Storage Category {category} is flagged " + "'only empty' with other quants in location." + ).format(category=category.display_name) + elif allow_new_product == "same" and ( + len(package_products) > 1 + or len(products_in_location) >= 1 + and package_products != products_in_location + ): + error = _( + "Storage Category {category} is flagged 'do not mix" + " products' but there are other products in " + "location." + ).format(category=category.display_name) + elif allow_new_product == "same_lot" and ( + len(package_lots) > 1 + or len(lots_in_location) >= 1 + and package_lots != lots_in_location + ): + error = _( + "Storage Category {category} is flagged 'do not mix" + " lots' but there are other lots in " + "location." + ).format(category=category.display_name) + # Check size constraint + elif ( + category.max_height_in_m + and quant.package_id.height_in_m > category.max_height_in_m + ): + error = _( + "Storage Category {category} defines " + "max height of {max_h} but the package is bigger: " + "{height}." + ).format( + category=category.display_name, + max_h=category.max_height_in_m, + height=quant.package_id.height_in_m, + ) + elif ( + category.max_weight_in_kg + and package_weight_kg > category.max_weight_in_kg + ): + error = _( + "Storage Category {category} defines " + "max weight of {max_w} but the package is heavier: " + "{weight_kg}." + ).format( + category=category.display_name, + max_w=category.max_weight_in_kg, + weight_kg=package_weight_kg, + ) + # If we get here, it means there is a storage category + # allowing the package into the location + if error: raise ValidationError( _( "Package {package} is not allowed into location {location}," - " because there isn't any storage capacity that allows" - " package type {type} into it:\n\n{fails}" + " because there isn't any rules that allows" + " package type {type} into it:\n\n{error}" ).format( package=quant.package_id.name, location=location.complete_name, type=package_type.name, - fails="\n".join(capacity_fails), + error=error, ) ) diff --git a/stock_storage_type/models/stock_quant_package.py b/stock_storage_type/models/stock_quant_package.py index 3967e8ef59a..8c72ec8c829 100644 --- a/stock_storage_type/models/stock_quant_package.py +++ b/stock_storage_type/models/stock_quant_package.py @@ -51,14 +51,14 @@ def auto_assign_packaging(self): res = super().auto_assign_packaging() for package in self: if not package.package_type_id: - # if no storage type could be set by auto assign, - # fallback on the default product's storage type (if any) + # if no package type could be set by auto assign, + # fallback on the default product's package type (if any) package._sync_package_type_from_single_product() return res @api.model_create_multi - def create(self, vals): - records = super().create(vals) + def create(self, vals_list): + records = super().create(vals_list) records._sync_package_type_from_packaging() return records @@ -71,7 +71,7 @@ def write(self, vals): def _sync_package_type_from_packaging(self): for package in self: if package.package_type_id: - # Do not set package storage type for delivery packages + # Do not set package type for delivery packages # to not trigger constraint like height requirement # (we are delivering them, not storing them) continue diff --git a/stock_storage_type/models/stock_storage_category.py b/stock_storage_type/models/stock_storage_category.py index 63b794c38f5..0196e1cc1a8 100644 --- a/stock_storage_type/models/stock_storage_category.py +++ b/stock_storage_type/models/stock_storage_category.py @@ -15,6 +15,11 @@ class StockStorageCategory(models.Model): computed_location_ids = fields.One2many( comodel_name="stock.location", inverse_name="computed_storage_category_id" ) + allow_new_product_ids = fields.One2many( + comodel_name="stock.storage.category.allow_new_product", + inverse_name="storage_category_id", + string="Allow New Product Rules", + ) # TODO: Move these fields in another module ? max_height = fields.Float( @@ -51,7 +56,6 @@ class StockStorageCategory(models.Model): compute="_compute_max_weight_in_kg", store=True, ) - length_uom_id = fields.Many2one( # Same as product.packing "uom.uom", @@ -64,6 +68,10 @@ class StockStorageCategory(models.Model): "product.template" ]._get_length_uom_id_from_ir_config_parameter(), ) + has_restrictions = fields.Boolean( + compute="_compute_has_restrictions", + help="Technical: This is used to check if we need to display warning message", + ) _sql_constraints = [ ( @@ -92,3 +100,113 @@ def _compute_max_weight_in_kg(self): to_unit=uom_kg, round=False, ) + + @api.depends( + "allow_new_product", + "allow_new_product_ids.allow_new_product", + "max_height", + "max_weight", + ) + def _compute_has_restrictions(self): + """ + A storage category has restrictions when it: + - does not accept mixed products + - or does not accept mixed lots + - or do have a maximum height set on its category + - or do have a maximum weight set on its category + """ + for rec in self: + rec.has_restrictions = any( + [ + rec.allow_new_product != "mixed", + any( + rule.allow_new_product != "mixed" + for rule in rec.allow_new_product_ids + ), + rec.max_height, + rec.max_weight, + ] + ) + + def _get_product_location_domain(self, products): + """ + Helper to get products location domain + """ + return [ + "|", + # Ideally, we would like a domain which is a strict comparison: + # if we do not mix products, we should be able to filter on == + # product.id. Here, if we can create a move for product B and + # set it's destination in a location already used by product A, + # then all the new moves for product B will be allowed in the + # location. + ("location_will_contain_product_ids", "in", products.ids), + ("location_will_contain_product_ids", "=", False), + ] + + def _domain_location_storage_category( + self, candidate_locations, quants, products, package_type + ): + """ + Compute a domain which applies the constraint of the + Stock Storage Category to select locations among candidate locations. + """ + self.ensure_one() + location_domain = [ + ("id", "in", candidate_locations.ids), + ("computed_storage_category_id", "in", self.ids), + ] + # Build the domain using the 'allow_new_product' field + allow_new_product = self.get_allow_new_product( + product=products, + package_type=package_type, + package=quants.package_id, + quants=quants, + ) + if allow_new_product == "empty": + location_domain.append(("location_is_empty", "=", True)) + elif allow_new_product == "same": + location_domain += self._get_product_location_domain(products) + elif allow_new_product == "same_lot": + lots = quants.mapped("lot_id") + # As same lot should filter also on same product + location_domain += self._get_product_location_domain(products) + location_domain += [ + "|", + # same comment as for the products + ("location_will_contain_lot_ids", "in", lots.ids), + ("location_will_contain_lot_ids", "=", False), + ] + return location_domain + + def get_allow_new_product( + self, + product, + package_type=None, + package=None, + quants=None, + ): + """Return the `allow_new_product` option value. + + It first evaluates the conditions based on different criteria, and if no + value can be found among them it fallbacks on the category option value. + """ + self.ensure_one() + for rule in self.allow_new_product_ids: + res = True + for condition in rule.condition_ids: + res = condition.evaluate( + self, + product, + package_type, + package, + quants, + ) + if not res: + # Go to next rule + break + # All conditions are matching + if res: + return rule.allow_new_product + # Fallback on category option value + return self.allow_new_product diff --git a/stock_storage_type/models/stock_storage_category_allow_new_product.py b/stock_storage_type/models/stock_storage_category_allow_new_product.py new file mode 100644 index 00000000000..fa2f1ac004f --- /dev/null +++ b/stock_storage_type/models/stock_storage_category_allow_new_product.py @@ -0,0 +1,31 @@ +# Copyright 2025 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import fields, models + + +class StockStorageCategoryAllowNewProduct(models.Model): + _name = "stock.storage.category.allow_new_product" + _description = "Storage Category Allow New Product Rule" + _order = "storage_category_id, sequence" + + def _selection_allow_new_product(self): + return self.env["stock.storage.category"]._fields["allow_new_product"].selection + + storage_category_id = fields.Many2one( + comodel_name="stock.storage.category", + ondelete="cascade", + required=True, + index=True, + ) + condition_ids = fields.Many2many( + comodel_name="stock.storage.category.allow_new_product.cond", + relation="stock_storage_category_allow_new_product_cond_rel", + string="Conditions", + required=True, + help="All conditions have to match to apply the Allow New Product policy.", + ) + allow_new_product = fields.Selection( + selection=_selection_allow_new_product, default="mixed", required=True + ) + sequence = fields.Integer(index=True) diff --git a/stock_storage_type/models/stock_storage_category_allow_new_product_cond.py b/stock_storage_type/models/stock_storage_category_allow_new_product_cond.py new file mode 100644 index 00000000000..50a105c9b1e --- /dev/null +++ b/stock_storage_type/models/stock_storage_category_allow_new_product_cond.py @@ -0,0 +1,138 @@ +# Copyright 2025 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) +import logging + +from odoo import _, exceptions, models +from odoo.tools import safe_eval + +_logger = logging.getLogger(__name__) + + +class StockStorageCategoryAllowNewProductCond(models.Model): + _inherit = "stock.storage.condition.mixin" + _name = "stock.storage.category.allow_new_product.cond" + _description = "Stock Storage Category Allow New Product Condition" + + _sql_constraints = [ + ( + "name", + "EXCLUDE (name WITH =) WHERE (active = True)", + "Storage Category Allow New Product Condition name must be unique", + ) + ] + + def _default_code_snippet_docs(self): + return """ + Available vars: + * condition (recordset) + * storage_category (recordset) + * product (recordset) + * package_type (recordset) + * package (recordset) + * quants (recordset) + * env + * datetime + * dateutil + * time + * user + * exceptions + + Must initialize a boolean 'result' variable set to True when condition is met + + """ + + def _get_code_snippet_eval_context( + self, + storage_category, + product, + package_type, + package, + quants, + ): + """Prepare the context used when evaluating python code + + :returns: dict -- evaluation context given to safe_eval + """ + self.ensure_one() + return { + "env": self.env, + "user": self.env.user, + "condition": self, + "storage_category": storage_category, + "product": product, + "package_type": package_type, + "package": package, + "quants": quants, + "datetime": safe_eval.datetime, + "dateutil": safe_eval.dateutil, + "time": safe_eval.time, + "exceptions": safe_eval.wrap_module( + exceptions, ["UserError", "ValidationError"] + ), + } + + def _exec_code( + self, + storage_category, + product, + package_type, + package, + quants, + ): + self.ensure_one() + if not self._code_snippet_valued(): + return False + eval_ctx = self._get_code_snippet_eval_context( + storage_category, + product, + package_type, + package, + quants, + ) + snippet = self.code_snippet + safe_eval.safe_eval(snippet, eval_ctx, mode="exec", nocopy=True) + result = eval_ctx.get("result") + if not isinstance(result, bool): + raise exceptions.UserError( + _("code_snippet should return boolean value into `result` variable.") + ) + if not result: + _logger.debug( + "Condition %s not met:\n" + "* storage_category: %s\n" + "* product: %s\n" + "* package_type: %s\n" + "* package: %s\n" + "* quants: %s\n" + % ( + self.name, + storage_category.ids, + package_type and package_type.id or None, + package and package.id or None, + product.id, + quants and quants.ids or None, + ) + ) + return result + + def evaluate( + self, + storage_category, + product, + package_type, + package, + quants, + ): + self.ensure_one() + if self.condition_type == "code": + return self._exec_code( + storage_category, + product, + package_type, + package, + quants, + ) + condition_type = self.condition_type + raise exceptions.UserError( + _(f"Not able to evaluate condition of type {condition_type}") + ) diff --git a/stock_storage_type/models/stock_storage_category_capacity.py b/stock_storage_type/models/stock_storage_category_capacity.py deleted file mode 100644 index e007d398bf1..00000000000 --- a/stock_storage_type/models/stock_storage_category_capacity.py +++ /dev/null @@ -1,111 +0,0 @@ -# Copyright 2022 ACSONE SA -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -from odoo import _, api, fields, models - - -class StorageCategoryProductCapacity(models.Model): - - _inherit = "stock.storage.category.capacity" - - allow_new_product = fields.Selection( - selection=[ - ("empty", "If the location is empty"), - ("same", "If all products are same"), - ("mixed", "Allow mixed products"), - ("same_lot", "If all lots are the same"), - ], - default="mixed", - required=True, - ) - computed_location_ids = fields.One2many( - comodel_name="stock.location", - related="storage_category_id.computed_location_ids", - ) - has_restrictions = fields.Boolean( - compute="_compute_has_restrictions", - help="Technical: This is used to check if we need to display warning message", - ) - - @api.model - def _get_display_name_attributes(self): - """ - Adds the storage capacity attributes to compose the display name - """ - attributes = super()._get_display_name_attributes() - value = self._fields["allow_new_product"].convert_to_export( - self.allow_new_product, self - ) - attributes.append(_("Allow New Product: ") + value) - return attributes - - @api.model - def _compute_display_name_depends(self): - depends = super()._compute_display_name_depends() - depends.append("allow_new_product") - return depends - - @api.depends( - "allow_new_product", - "storage_category_id.max_height", - "storage_category_id.max_weight", - ) - def _compute_has_restrictions(self): - """ - A storage capacity has restrictions when it: - - does not accept mixed products - - or does not accept mixed lots - - or do have a maximum height set on its category - - or do have a maximum weight set on its category - """ - for capacity in self: - capacity.has_restrictions = any( - [ - capacity.allow_new_product != "mixed", - capacity.storage_category_id.max_height, - capacity.storage_category_id.max_weight, - ] - ) - - def _get_product_location_domain(self, products): - """ - Helper to get products location domain - """ - return [ - "|", - # Ideally, we would like a domain which is a strict comparison: - # if we do not mix products, we should be able to filter on == - # product.id. Here, if we can create a move for product B and - # set it's destination in a location already used by product A, - # then all the new moves for product B will be allowed in the - # location. - ("location_will_contain_product_ids", "in", products.ids), - ("location_will_contain_product_ids", "=", False), - ] - - def _domain_location_storage_type(self, candidate_locations, quants, products): - """ - Compute a domain which applies the constraint of the - Stock Storage Category Capacities to select locations among candidate - locations. - """ - self.ensure_one() - location_domain = [ - ("id", "in", candidate_locations.ids), - ("computed_storage_category_id.capacity_ids", "in", self.ids), - ] - # Build the domain using the 'allow_new_product' field - if self.allow_new_product == "empty": - location_domain.append(("location_is_empty", "=", True)) - elif self.allow_new_product == "same": - location_domain += self._get_product_location_domain(products) - elif self.allow_new_product == "same_lot": - lots = quants.mapped("lot_id") - # As same lot should filter also on same product - location_domain += self._get_product_location_domain(products) - location_domain += [ - "|", - # same comment as for the products - ("location_will_contain_lot_ids", "in", lots.ids), - ("location_will_contain_lot_ids", "=", False), - ] - return location_domain diff --git a/stock_storage_type/models/stock_storage_condition_mixin.py b/stock_storage_type/models/stock_storage_condition_mixin.py new file mode 100644 index 00000000000..aa7b7d11e3c --- /dev/null +++ b/stock_storage_type/models/stock_storage_condition_mixin.py @@ -0,0 +1,65 @@ +# Copyright 2025 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +import textwrap + +from odoo import _, api, exceptions, fields, models + + +class StockStorageConditionMixin(models.AbstractModel): + _name = "stock.storage.condition.mixin" + _description = "Mixin to implement storage condition." + + name = fields.Char(required=True) + condition_type = fields.Selection( + selection=[("code", "Execute code")], default="code", required=True + ) + code_snippet = fields.Text(required=True) + code_snippet_docs = fields.Text( + compute="_compute_code_snippet_docs", + default=lambda self: self._default_code_snippet_docs(), + ) + active = fields.Boolean(default=True) + + @api.constrains("condition_type", "code_snippet") + def _check_condition_type_code(self): + for rec in self.filtered(lambda c: c.condition_type == "code"): + if not rec._code_snippet_valued(): + raise exceptions.UserError( + _( + "Condition type is set to `Code`: you must provide a piece of code" + ) + ) + + def _code_snippet_valued(self): + self.ensure_one() + snippet = self.code_snippet or "" + return bool( + [ + not line.startswith("#") + for line in (snippet.splitlines()) + if line.strip("") + ] + ) + + def _compute_code_snippet_docs(self): + for rec in self: + rec.code_snippet_docs = textwrap.dedent(rec._default_code_snippet_docs()) + + def _default_code_snippet_docs(self): + """Return the documentation (e.g. available variables) for `code_snippet`.""" + raise NotImplementedError + + def _get_code_snippet_eval_context(self, *args, **kwargs): + """Prepare the context used when evaluating python code + + :returns: dict -- evaluation context given to safe_eval + """ + raise NotImplementedError + + def _exec_code(self, *args, **kwargs): + raise NotImplementedError + + def evaluate(self, *args, **kwargs): + """Evaluate and return the result of the condition.""" + raise NotImplementedError diff --git a/stock_storage_type/models/stock_storage_location_sequence.py b/stock_storage_type/models/stock_storage_location_sequence.py index f789317c1f8..1dabc98d14a 100644 --- a/stock_storage_type/models/stock_storage_location_sequence.py +++ b/stock_storage_type/models/stock_storage_location_sequence.py @@ -6,7 +6,7 @@ class StockStorageLocationSequence(models.Model): _name = "stock.storage.location.sequence" - _description = "Sequence of locations to put-away the package storage type" + _description = "Sequence of locations to put-away the package type" _order = "sequence" package_type_id = fields.Many2one("stock.package.type", required=True) @@ -22,9 +22,10 @@ class StockStorageLocationSequence(models.Model): string="Conditions", comodel_name="stock.storage.location.sequence.cond", relation="stock_location_sequence_cond_rel", + help="All conditions have to match to apply the put-away strategy.", ) - def _format_package_storage_type_message(self, last=False): + def _format_package_type_message(self, last=False): self.ensure_one() # TODO improve ugly code type_matching_locations = self.location_id.get_storage_locations().filtered( @@ -47,25 +48,25 @@ def _format_package_storage_type_message(self, last=False): ) if last: # If last, we want to check if restrictions are defined on - # location storage types accepting this package storage type + # capacities accepting this package type # TODO improve ugly code capacities = type_matching_locations.mapped( "computed_storage_category_id.capacity_ids" ).filtered( lambda lst, package_type=self.package_type_id: package_type == lst.package_type_id - and not lst.has_restrictions + and not lst.storage_category_id.has_restrictions ) if not capacities: msg = _( ' * {location} (WARNING: ' - "restrictions are active on location storage types " - "matching this package storage type)" + "restrictions are active on storage categories " + "matching this package type)" ).format(location=self.location_id.name) else: msg = _( ' * {location} ' - "(WARNING: no suitable location matching storage type)" + "(WARNING: no suitable location matching package type)" ).format(location=self.location_id.name) return msg diff --git a/stock_storage_type/models/stock_storage_location_sequence_cond.py b/stock_storage_type/models/stock_storage_location_sequence_cond.py index 4a66115f119..f155c2048db 100644 --- a/stock_storage_type/models/stock_storage_location_sequence_cond.py +++ b/stock_storage_type/models/stock_storage_location_sequence_cond.py @@ -1,34 +1,18 @@ # Copyright 2022 ACSONE SA/NV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). import logging -import textwrap -from odoo import _, api, exceptions, fields, models +from odoo import _, exceptions, models from odoo.tools import safe_eval _logger = logging.getLogger(__name__) class StockStorageLocationSequenceCond(models.Model): - + _inherit = "stock.storage.condition.mixin" _name = "stock.storage.location.sequence.cond" _description = "Stock Storage Location Sequence Condition" - name = fields.Char(required=True) - - condition_type = fields.Selection( - selection=[("code", "Execute code")], default="code", required=True - ) - code_snippet = fields.Text(required=True) - code_snippet_docs = fields.Text( - compute="_compute_code_snippet_docs", - default=lambda self: self._default_code_snippet_docs(), - ) - - active = fields.Boolean( - default=True, - ) - _sql_constraints = [ ( "name", @@ -37,20 +21,6 @@ class StockStorageLocationSequenceCond(models.Model): ) ] - def _compute_code_snippet_docs(self): - for rec in self: - rec.code_snippet_docs = textwrap.dedent(rec._default_code_snippet_docs()) - - @api.constrains("condition_type", "code_snippet") - def _check_condition_type_code(self): - for rec in self.filtered(lambda c: c.condition_type == "code"): - if not rec._code_snippet_valued(): - raise exceptions.UserError( - _( - "Condition type is set to `Code`: you must provide a piece of code" - ) - ) - def _default_code_snippet_docs(self): return """ Available vars: @@ -124,17 +94,6 @@ def _exec_code(self, storage_location_sequence, putaway_location, quant, product ) return result - def _code_snippet_valued(self): - self.ensure_one() - snippet = self.code_snippet or "" - return bool( - [ - not line.startswith("#") - for line in (snippet.splitlines()) - if line.strip("") - ] - ) - def evaluate(self, storage_location_sequence, putaway_location, quant, product): self.ensure_one() if self.condition_type == "code": diff --git a/stock_storage_type/readme/CONFIGURATION.rst b/stock_storage_type/readme/CONFIGURATION.rst index 00a0c3f399c..b16e7a780fd 100644 --- a/stock_storage_type/readme/CONFIGURATION.rst +++ b/stock_storage_type/readme/CONFIGURATION.rst @@ -1,11 +1,11 @@ -Got to "Inventory > Settings > Storage Types", to define Package Storage Types -and Location Storage Types. +Go to "Inventory > Settings > Package Types" and +"Inventory > Settings > Storage Categories", to define Package Types and Storage +Categories. -Package Storage Type can be defined on Product Packaging form view from the -product form view. +Package Type can be set on Product and Product Packaging. -Location Storage Type can be added to any stock location and will be computed -automatically as Allowed Locations Storage Types on said stock location's +Storage Category can be added to any stock location and will be computed +automatically as Allowed Storage Category on said stock location's children location. @@ -13,16 +13,16 @@ children location. On stock locations, you can define a "Pack put-away strategy" as "Ordered bins", so that any move, having this locations as its destination, will be put-away -on a children location, according to the restrictions from storage types. +on a children location, according to the restrictions from package types. - Put-away sequence -For any package storage types, you must define a Put-away sequence (i.e. stock +For any package types, you must define a Put-away sequence (i.e. stock location to search) where such a package is allowed to be put-away. Locations will be processed sequentially and the first one having an allowed child location (according to restrictions) will be used to put away. -A good practice here, is to set a location accepting this storage type without +A good practice here, is to set a location accepting this package type without any restriction as the last location in the sequence, to act as a fallback if no other location could be found before. diff --git a/stock_storage_type/readme/DESCRIPTION.rst b/stock_storage_type/readme/DESCRIPTION.rst index bd8d800bb2a..ecd00d730a4 100644 --- a/stock_storage_type/readme/DESCRIPTION.rst +++ b/stock_storage_type/readme/DESCRIPTION.rst @@ -1,51 +1,33 @@ -This module introduces two new models in order to manage stock moves with - packages according to the packaging and stock location properties. +This module extends package types Odoo feature in order to better manage stock +moves with packages according to the packaging and stock location properties +(like height, weight or any customized conditions). -* Stock package storage type (`stock.package.storage.type`) +Moreover, this module implements "package type put-away strategy" in order to +compute a put-away location using package types. - This model is linked to product.packaging and defines the type of storage - related to a specific packaging. - -* Stock location storage type (`stock.location.storage.type`) - - This models is linked to stock.location and defines the types of storage - that are allowed for a specific location. - -Therefore a Stock location storage type can include different Stock package -storage type in order to validate the destination of a move with package into a -stock location. -Moreover Stock location storage type can include product, size or lot -restrictions for the stock locations it's defined on, so that a move with -package will only be allowed if it doesn't violate the restrictions defined -(cf stock_location_storage_type_strategy). - -Moreover, this module implements "storage type put-away strategy" in order to compute a -put-away location using storage types. - -The standard put-away strategy is applied *before* the storage type put-away +The standard put-away strategy is applied *before* the package type put-away strategy as the former relies on product or product category and the latter relies on stock packages. -In other words, when a move is assigned, Odoo standard put-away strategy will be +In other words, when a move is reserved, Odoo standard put-away strategy will be applied to compute a new destination on the stock move lines, according to the product. -After this first "put-away computation", the "storage type" put-away strategy -is applied, if the reserved quant is linked to a package defining a package -storage type. +After this first "put-away computation", the "package type" put-away strategy +is applied, if the reserved quant is linked to a package defining a package type. -Storage locations linked to the package storage are processed sequentially, if +Storage locations linked to the package type are processed sequentially, if said storage location is a child of the move line's destination location (i.e either the put-away location or the move's destination location). -For each location, their packs storage strategy is applied as well as the -restrictions defined on the stock location storage types. +For each location, their package type strategy is applied as well as the +restrictions defined on the storage category. If no suitable location is found, the next location in the sequence will be searched and so on. -For the packs putaway strategy "none", the location is considered as is. For +For the package type putaway strategy "None", the location is considered as is. For the "ordered children" strategy, children locations are sorted by first by max height which is a physical constraint to respect, then pack putaway sequence which allow to favor for example some level or corridor, and finally by name. At the end, if found location is not the same as the original destination location, -the putaway strategies are applied (e.g.: A "none" pack putaway strategy is set on +the putaway strategies are applied (e.g.: A "None" pack putaway strategy is set on computed location and a putaway rule exists on that one). diff --git a/stock_storage_type/security/ir.model.access.csv b/stock_storage_type/security/ir.model.access.csv index ed6a03817a1..7fa2b5e7312 100644 --- a/stock_storage_type/security/ir.model.access.csv +++ b/stock_storage_type/security/ir.model.access.csv @@ -3,3 +3,7 @@ access_stock_storage_location_sequence_user,access_stock_storage_location_sequen access_stock_storage_location_sequence_manager,access_stock_storage_location_sequence_manager,model_stock_storage_location_sequence,stock.group_stock_manager,1,1,1,1 access_stock_storage_location_sequence_cond_user,access_stock_storage_location_sequence_cond_user,model_stock_storage_location_sequence_cond,base.group_user,1,0,0,0 access_stock_storage_location_sequence_cond_manager,access_stock_storage_location_sequence_cond_manager,model_stock_storage_location_sequence_cond,stock.group_stock_manager,1,1,1,1 +access_stock_storage_category_allow_new_product_user,access_stock_storage_category_allow_new_product_user,model_stock_storage_category_allow_new_product,base.group_user,1,0,0,0 +access_stock_storage_category_allow_new_product_manager,access_stock_storage_category_allow_new_product_manager,model_stock_storage_category_allow_new_product,stock.group_stock_manager,1,1,1,1 +access_stock_storage_category_allow_new_product_cond_user,access_stock_storage_category_allow_new_product_cond_user,model_stock_storage_category_allow_new_product_cond,base.group_user,1,0,0,0 +access_stock_storage_category_allow_new_product_cond_manager,access_stock_storage_category_allow_new_product_cond_manager,model_stock_storage_category_allow_new_product_cond,stock.group_stock_manager,1,1,1,1 diff --git a/stock_storage_type/static/description/index.html b/stock_storage_type/static/description/index.html index 5debf7b67f0..4cdf66d916d 100644 --- a/stock_storage_type/static/description/index.html +++ b/stock_storage_type/static/description/index.html @@ -3,7 +3,7 @@ -Stock Storage Type +README.rst -
    -

    Stock Storage Type

    +
    + + +Odoo Community Association + +
    +

    Stock Storage Type

    -

    Beta License: AGPL-3 OCA/wms Translate me on Weblate Try me on Runboat

    -
    -
    This module introduces two new models in order to manage stock moves with
    -
    packages according to the packaging and stock location properties.
    -
    -
      -
    • Stock package storage type (stock.package.storage.type)

      -

      This model is linked to product.packaging and defines the type of storage -related to a specific packaging.

      -
    • -
    • Stock location storage type (stock.location.storage.type)

      -

      This models is linked to stock.location and defines the types of storage -that are allowed for a specific location.

      -
    • -
    -

    Therefore a Stock location storage type can include different Stock package -storage type in order to validate the destination of a move with package into a -stock location. -Moreover Stock location storage type can include product, size or lot -restrictions for the stock locations it’s defined on, so that a move with -package will only be allowed if it doesn’t violate the restrictions defined -(cf stock_location_storage_type_strategy).

    -

    Moreover, this module implements “storage type put-away strategy” in order to compute a -put-away location using storage types.

    -

    The standard put-away strategy is applied before the storage type put-away +

    Beta License: AGPL-3 OCA/wms Translate me on Weblate Try me on Runboat

    +

    This module extends package types Odoo feature in order to better manage stock +moves with packages according to the packaging and stock location properties +(like height, weight or any customized conditions).

    +

    Moreover, this module implements “package type put-away strategy” in order to +compute a put-away location using package types.

    +

    The standard put-away strategy is applied before the package type put-away strategy as the former relies on product or product category and the latter relies on stock packages.

    -

    In other words, when a move is assigned, Odoo standard put-away strategy will be +

    In other words, when a move is reserved, Odoo standard put-away strategy will be applied to compute a new destination on the stock move lines, according to the product. -After this first “put-away computation”, the “storage type” put-away strategy -is applied, if the reserved quant is linked to a package defining a package -storage type.

    -

    Storage locations linked to the package storage are processed sequentially, if +After this first “put-away computation”, the “package type” put-away strategy +is applied, if the reserved quant is linked to a package defining a package type.

    +

    Storage locations linked to the package type are processed sequentially, if said storage location is a child of the move line’s destination location (i.e either the put-away location or the move’s destination location). -For each location, their packs storage strategy is applied as well as the -restrictions defined on the stock location storage types. +For each location, their package type strategy is applied as well as the +restrictions defined on the storage category. If no suitable location is found, the next location in the sequence will be searched and so on.

    -

    For the packs putaway strategy “none”, the location is considered as is. For +

    For the package type putaway strategy “None”, the location is considered as is. For the “ordered children” strategy, children locations are sorted by first by max height which is a physical constraint to respect, then pack putaway sequence which allow to favor for example some level or corridor, and finally by name.

    At the end, if found location is not the same as the original destination location, -the putaway strategies are applied (e.g.: A “none” pack putaway strategy is set on +the putaway strategies are applied (e.g.: A “None” pack putaway strategy is set on computed location and a putaway rule exists on that one).

    Table of contents

    @@ -431,7 +417,7 @@

    Stock Storage Type

    -

    Known issues / Roadmap

    +

    Known issues / Roadmap

    Currently, the module supports only strategies applied on packages (stock.quant.package). For implementations that do not use packages, it would be possible to add compatibility with product packaging.

    @@ -458,7 +444,7 @@

    Known issues / Roadmap

    like max_weight_in_kg in order make simple and efficient computations.

    -

    Limitation

    +

    Limitation

    If the locations structure is using views intensively in order to separate storage types kindly (not mixing them), Odoo standard method to get putaway strategy is returning the first child if a move location destination is a view.

    @@ -470,7 +456,7 @@

    Limitation

    apply standard child location selection’ could help filtering view candidates.

    -

    Bug Tracker

    +

    Bug Tracker

    Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed @@ -478,16 +464,16 @@

    Bug Tracker

    Do not contact contributors directly about support or help with technical issues.

    -

    Credits

    +

    Credits

    -

    Authors

    +

    Authors

    • Camptocamp
    • BCIM
    -

    Contributors

    +

    Contributors

    -

    Maintainers

    +

    Maintainers

    This module is maintained by the OCA.

    Odoo Community Association @@ -514,5 +500,6 @@

    Maintainers

    +
    diff --git a/stock_storage_type/tests/__init__.py b/stock_storage_type/tests/__init__.py index 06655c55be3..ba7aa132776 100644 --- a/stock_storage_type/tests/__init__.py +++ b/stock_storage_type/tests/__init__.py @@ -6,4 +6,5 @@ test_storage_type, test_storage_type_move, test_storage_type_putaway_strategy, + test_storage_category_allow_new_product, ) diff --git a/stock_storage_type/tests/test_auto_assign_storage_type.py b/stock_storage_type/tests/test_auto_assign_storage_type.py index bd83034e14a..f80562ca2af 100644 --- a/stock_storage_type/tests/test_auto_assign_storage_type.py +++ b/stock_storage_type/tests/test_auto_assign_storage_type.py @@ -19,7 +19,7 @@ def setUpClass(cls): def test_auto_assign_package_storage_type_without_packaging_id(self): """Packages without `packaging_id` are internal packages and they are intended to be stored in the warehouse. - On such packages storage type is automatically defined. + On such packages, a package type is automatically defined. """ package = self.env["stock.quant.package"].create( {"name": "TEST", "product_packaging_id": self.product_packaging.id} diff --git a/stock_storage_type/tests/test_stock_location.py b/stock_storage_type/tests/test_stock_location.py index 370f3295d95..6381ee18195 100644 --- a/stock_storage_type/tests/test_stock_location.py +++ b/stock_storage_type/tests/test_stock_location.py @@ -50,7 +50,7 @@ def test_get_ordered_leaf_locations(self): | self.pallets_reserve_bin_4_location ).ids, ) - # Set the max_height on pallets storage type higher than the others + # Set the max_height on pallets storage category higher than the others self.pallets_location_storage_type.storage_category_id.max_height = 2 self.cardboxes_location_storage_type.storage_category_id.max_height = 1 ordered_locations = sublocation.get_storage_locations(self.product) @@ -71,7 +71,7 @@ def test_get_ordered_leaf_locations(self): | self.pallets_reserve_bin_4_location ).ids, ) - # Set the max_height on cardboxes storage type higher than the others + # Set the max_height on cardboxes storage category higher than the others self.pallets_location_storage_type.storage_category_id.max_height = 1 self.cardboxes_location_storage_type.storage_category_id.max_height = 2 ordered_locations = sublocation.get_storage_locations(self.product) @@ -283,11 +283,12 @@ def test_location_is_empty(self): self._update_qty_in_location(location, self.product, 10) self.assertFalse(location.location_is_empty) - # When the location has no "only_empty" storage type, we don't + # When the location has no "only_empty" rule, we don't # care about if it is empty or not, we keep it as True so we # can always put things inside. Not computing it prevents # useless race conditions on concurrent writes. - location.computed_storage_category_id.capacity_ids.filtered( - lambda c: c.allow_new_product == "empty" + category = location.computed_storage_category_id + category.allow_new_product_ids.filtered( + lambda rule: rule.allow_new_product == "empty" ).allow_new_product = "mixed" self.assertTrue(location.location_is_empty) diff --git a/stock_storage_type/tests/test_storage_category_allow_new_product.py b/stock_storage_type/tests/test_storage_category_allow_new_product.py new file mode 100644 index 00000000000..47f43acb63d --- /dev/null +++ b/stock_storage_type/tests/test_storage_category_allow_new_product.py @@ -0,0 +1,159 @@ +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +from .common import TestStorageTypeCommon + + +class TestStorageCategoryAllowNewProduct(TestStorageTypeCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.areas.write({"pack_putaway_strategy": "ordered_locations"}) + cls.category = cls.pallets_location_storage_type.storage_category_id + # Configure the rule matching Pallets to allow the same product on locations + cls.category.allow_new_product_ids.allow_new_product = "same" + + def test_storage_category_allow_new_product(self): + self.category.allow_new_product = "empty" + self.assertEqual(self.category.get_allow_new_product(self.product), "empty") + self.category.allow_new_product = "same_lot" + self.assertEqual(self.category.get_allow_new_product(self.product), "same_lot") + # Create a quant with a package of type Pallet to check the + # allow_new_product rule result + package_type_pallets = self.env.ref( + "stock_storage_type.package_storage_type_pallets" + ) + package = self.env["stock.quant.package"].create( + { + "name": "TEST PKG", + "package_type_id": package_type_pallets.id, + } + ) + self.env["stock.quant"]._update_available_quantity( + self.product, + self.pallets_bin_2_location, + 1.0, + package_id=package, + ) + quant = self.env["stock.quant"].search( + [ + ("location_id", "=", self.pallets_bin_2_location.id), + ("product_id", "=", self.product.id), + ("package_id", "=", package.id), + ] + ) + self.assertEqual( + self.category.get_allow_new_product( + self.product, + quants=quant, + package_type=package_type_pallets, + package=package, + ), + "same", + ) + + def test_storage_strategy_with_allow_new_product_rule(self): + # Set pallets location type as only empty, while it also has a rule + # that will force the 'allow_new_product' to 'same' + self.pallets_location_storage_type.storage_category_id.write( + {"allow_new_product": "empty"} + ) + # Create picking + in_picking = self.env["stock.picking"].create( + { + "picking_type_id": self.receipts_picking_type.id, + "location_id": self.suppliers_location.id, + "location_dest_id": self.input_location.id, + "move_ids": [ + ( + 0, + 0, + { + "name": self.product.name, + "location_id": self.suppliers_location.id, + "location_dest_id": self.input_location.id, + "product_id": self.product.id, + "product_uom_qty": 96.0, + "product_uom": self.product.uom_id.id, + }, + ) + ], + } + ) + # Mark as todo + in_picking.action_confirm() + # Put in pack + in_picking.move_line_ids.qty_done = 48.0 + first_package = in_picking.action_put_in_pack() + # Ensure packaging is set properly on pack + first_package.product_packaging_id = self.product_pallet_product_packaging + # Put in pack again + ml_without_package = in_picking.move_line_ids.filtered( + lambda ml: not ml.result_package_id + ) + ml_without_package.qty_done = 48.0 + second_pack = in_picking.action_put_in_pack() + # Ensure packaging is set properly on pack + second_pack.product_packaging_id = self.product_pallet_product_packaging + + # Validate picking + in_picking.button_validate() + # Assign internal picking + int_picking = in_picking.move_ids.move_dest_ids.picking_id + int_picking.action_assign() + self.assertEqual(int_picking.location_dest_id, self.stock_location) + self.assertEqual( + int_picking.move_ids.mapped("location_dest_id"), self.stock_location + ) + # First & second move lines goes into pallets bin 1, as forced by the rule + self.assertEqual( + int_picking.move_line_ids.mapped("location_dest_id"), + self.pallets_bin_1_location, + ) + + def test_storage_category_mixed_allow_new_product(self): + self.category.allow_new_product = "mixed" + self.assertEqual(self.category.get_allow_new_product(self.product), "mixed") + + # Create a quant with a package of type Pallet to check the + # allow_new_product rule result + package_type_pallets = self.env.ref( + "stock_storage_type.package_storage_type_pallets" + ) + package_type_pallets_uk = self.env.ref( + "stock_storage_type.package_storage_type_pallets_uk" + ) + self.product2.package_type_id = package_type_pallets_uk + package = self.env["stock.quant.package"].create( + { + "name": "TEST PKG", + "package_type_id": package_type_pallets.id, + } + ) + package_uk = self.env["stock.quant.package"].create( + { + "name": "TEST PKG", + "package_type_id": package_type_pallets_uk.id, + } + ) + self.env["stock.quant"]._update_available_quantity( + self.product, + self.pallets_bin_2_location, + 1.0, + package_id=package, + ) + quant = self.env["stock.quant"].search( + [ + ("location_id", "=", self.pallets_bin_2_location.id), + ("product_id", "=", self.product.id), + ("package_id", "=", package.id), + ] + ) + self.assertEqual( + self.category.get_allow_new_product( + self.product2, + quants=quant, + package_type=package_type_pallets_uk, + package=package_uk, + ), + "same", + ) diff --git a/stock_storage_type/tests/test_storage_type.py b/stock_storage_type/tests/test_storage_type.py index a7747c71eda..aa6be654e5e 100644 --- a/stock_storage_type/tests/test_storage_type.py +++ b/stock_storage_type/tests/test_storage_type.py @@ -37,7 +37,7 @@ def setUpClass(cls): ) def test_location_allowed_storage_types(self): - # As cardboxes location storage type is defined on parent stock + # As cardboxes capacity is defined on parent stock # location_storage_type_ids self.assertEqual( self.cardboxes_stock.computed_storage_category_id.capacity_ids, @@ -89,7 +89,7 @@ def test_location_allowed_storage_types(self): self.cardboxes_location_storage_type, ) # If I create a child bin on cardboxes bin 1, it will use the first - # parent's storage type + # parent's capacity bin_1_child = self.env["stock.location"].create( {"name": "Carboxes bin 1 child", "location_id": self.cardboxes_bin_1.id} ) @@ -163,16 +163,23 @@ def test_package_message(self): Test for the message displayed on Stock Package Type forms """ pallets = self.env.ref("stock_storage_type.package_storage_type_pallets") - message = "When a package with storage type Pallets is put away, the " + category = pallets.storage_category_capacity_ids.storage_category_id + message = "When a package with type Pallets is put away, the " message += "strategy will look for an allowed location in the " message += "following locations:" self.assertIn(message, pallets.storage_type_message) + category.allow_new_product_ids.allow_new_product = "empty" + pallets._compute_storage_type_message() message = ( "Pallets reserve storage area (WARNING: restrictions are active on " - "location storage types matching this package storage type)" + "storage categories matching this package type)" ) + self.assertIn(message, pallets.storage_type_message) + category.allow_new_product_ids.allow_new_product = "mixed" + pallets._compute_storage_type_message() + message = "Pallets reserve storage area (Ordered Children Locations)" self.assertIn(message, pallets.storage_type_message) def test_sequence_to_location_menu(self): @@ -185,9 +192,3 @@ def test_sequence_to_location_menu(self): ), action["domain"], ) - - def test_storage_capacity_display(self): - self.assertEqual( - self.cardboxes_stock.computed_storage_category_id.capacity_ids.display_name, - "Cardboxes x 1.0 (Package: Cardboxes - Allow New Product: Allow mixed products)", - ) diff --git a/stock_storage_type/tests/test_storage_type_move.py b/stock_storage_type/tests/test_storage_type_move.py index 86e5d1aa7b7..1915122a3a6 100644 --- a/stock_storage_type/tests/test_storage_type_move.py +++ b/stock_storage_type/tests/test_storage_type_move.py @@ -38,28 +38,33 @@ def _test_confirmed_move(self, product=None): return move_to_assign def test_not_only_empty_confirmed_move(self): - self.pallets_location_storage_type.write({"allow_new_product": "mixed"}) + category = self.pallets_location_storage_type.storage_category_id + category.allow_new_product_ids.allow_new_product = "mixed" move = self._test_confirmed_move() self.assertEqual( move.move_line_ids.location_dest_id, self.pallets_bin_1_location ) def test_only_empty_confirmed_move(self): - self.pallets_location_storage_type.write({"allow_new_product": "empty"}) + category = self.pallets_location_storage_type.storage_category_id + category.allow_new_product_ids.allow_new_product = "empty" move = self._test_confirmed_move() self.assertNotEqual( move.move_line_ids.location_dest_id, self.pallets_bin_1_location ) def test_do_not_mix_products_confirmed_move_ok(self): - self.pallets_location_storage_type.write({"allow_new_product": "same"}) + category = self.pallets_location_storage_type.storage_category_id + category.allow_new_product_ids.allow_new_product = "same" move = self._test_confirmed_move() self.assertEqual( move.move_line_ids.location_dest_id, self.pallets_bin_1_location ) def test_do_not_mix_products_confirmed_move_nok(self): - self.pallets_location_storage_type.write({"allow_new_product": "same"}) + self.pallets_location_storage_type.storage_category_id.write( + {"allow_new_product": "same"} + ) move_other_product = self._test_confirmed_move( self.env.ref("product.product_product_10") ) @@ -70,7 +75,9 @@ def test_do_not_mix_products_confirmed_move_nok(self): def test_package_level_location_dest_domain_only_empty(self): # Set pallets location type as only empty - self.pallets_location_storage_type.write({"allow_new_product": "empty"}) + self.pallets_location_storage_type.storage_category_id.write( + {"allow_new_product": "empty"} + ) # Create picking in_picking = self.env["stock.picking"].create( { @@ -202,7 +209,9 @@ def test_package_level_location_dest_domain_mixed(self): # Mark picking to allow creation and use of existing lots in order # to register two times the same lot in different packages self.receipts_picking_type.use_existing_lots = True - self.cardboxes_location_storage_type.write({"allow_new_product": "same_lot"}) + self.cardboxes_location_storage_type.storage_category_id.write( + {"allow_new_product": "same_lot"} + ) # Create picking in_picking = self.env["stock.picking"].create( { @@ -381,7 +390,9 @@ def test_stock_move_no_package(self): Check that lot restriction is well applied """ # Constrain Cardbox Capacity to accept same lots only - self.cardboxes_location_storage_type.write({"allow_new_product": "same_lot"}) + self.cardboxes_location_storage_type.storage_category_id.write( + {"allow_new_product": "same_lot"} + ) # Set a quantity in cardbox bin 2 to make sure constraint is applied self.env["stock.quant"]._update_available_quantity( self.env.ref("product.product_product_10"), diff --git a/stock_storage_type/tests/test_storage_type_putaway_strategy.py b/stock_storage_type/tests/test_storage_type_putaway_strategy.py index 3f2956576c6..a2e2f82b120 100644 --- a/stock_storage_type/tests/test_storage_type_putaway_strategy.py +++ b/stock_storage_type/tests/test_storage_type_putaway_strategy.py @@ -81,7 +81,9 @@ def test_storage_strategy_ordered_locations_cardboxes(self): def test_storage_strategy_only_empty_ordered_locations_pallets(self): # Set pallets location type as only empty - self.pallets_location_storage_type.write({"allow_new_product": "empty"}) + self.pallets_location_storage_type.storage_category_id.write( + {"allow_new_product": "empty"} + ) # Set a quantity in pallet bin 2 to make sure constraint is applied self.env["stock.quant"]._update_available_quantity( self.product, self.pallets_bin_2_location, 1.0 @@ -142,14 +144,25 @@ def test_storage_strategy_only_empty_ordered_locations_pallets(self): ) def test_storage_strategy_max_weight_ordered_locations_pallets(self): + """Test pallet max weight constraint on a location. + + Configure 'Pallets storage area/Pallets Bin 2' with a max weight to 50kg. + Reception of two pallets of 60kg suggests to put them into Bin 1 and 3, + skipping Bin 2 that doesn't match anymore. + """ + self.pallets_location.storage_category_id.allow_new_product = "empty" # Add a category for max_weight 50 category_50 = self.env["stock.storage.category"].create( - {"name": "Pallets max 50 kg", "max_weight": 50} + { + "name": "Pallets max 50 kg", + "max_weight": 50, + "allow_new_product": "empty", + } ) # Define new pallets location type with a max weight on bin 2 light_location_storage_type = self.pallets_location_storage_type.copy( - {"allow_new_product": "empty", "storage_category_id": category_50.id} + {"storage_category_id": category_50.id} ) self.pallets_bin_2_location.write({"storage_category_id": category_50.id}) self.assertEqual( @@ -215,7 +228,9 @@ def test_storage_strategy_max_weight_ordered_locations_pallets(self): ) def test_storage_strategy_no_products_lots_mix_ordered_locations_cardboxes(self): - self.cardboxes_location_storage_type.write({"allow_new_product": "same_lot"}) + self.cardboxes_location_storage_type.storage_category_id.write( + {"allow_new_product": "same_lot"} + ) # Set a quantity in cardbox bin 2 to make sure constraint is applied self.env["stock.quant"]._update_available_quantity( self.env.ref("product.product_product_10"), @@ -398,7 +413,9 @@ def test_storage_strategy_do_not_mix_products_reuse_location(self): (less qty first). """ StockLocation = self.env["stock.location"] - self.cardboxes_location_storage_type.write({"allow_new_product": "same"}) + self.cardboxes_location_storage_type.storage_category_id.write( + {"allow_new_product": "same"} + ) product = self.product packaging = self.product_cardbox_product_packaging dest_location = self.cardboxes_location diff --git a/stock_storage_type/views/stock_package_type.xml b/stock_storage_type/views/stock_package_type.xml index 7fc87b9c726..e688068d398 100644 --- a/stock_storage_type/views/stock_package_type.xml +++ b/stock_storage_type/views/stock_package_type.xml @@ -32,12 +32,6 @@ - - - diff --git a/stock_storage_type/views/stock_storage_category.xml b/stock_storage_type/views/stock_storage_category.xml index 4665528732a..f0ee3bb7e6e 100644 --- a/stock_storage_type/views/stock_storage_category.xml +++ b/stock_storage_type/views/stock_storage_category.xml @@ -1,5 +1,6 @@ @@ -7,9 +8,26 @@ stock.storage.category - - - + + + + + + + + + + + + + diff --git a/stock_storage_type/views/stock_storage_category_allow_new_product_cond.xml b/stock_storage_type/views/stock_storage_category_allow_new_product_cond.xml new file mode 100644 index 00000000000..b67aba6b640 --- /dev/null +++ b/stock_storage_type/views/stock_storage_category_allow_new_product_cond.xml @@ -0,0 +1,54 @@ + + + + + + stock.storage.category.allow_new_product.cond.form + stock.storage.category.allow_new_product.cond + + + + + +
    +
    + + + + + +
    + +
    +
    + + + Storage Category Allow New Product Conditions + stock.storage.category.allow_new_product.cond + tree,form + + +
    diff --git a/stock_storage_type/views/stock_storage_category_capacity.xml b/stock_storage_type/views/stock_storage_category_capacity.xml deleted file mode 100644 index e7db3cead26..00000000000 --- a/stock_storage_type/views/stock_storage_category_capacity.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - stock.storage.category.capacity.tree (in stock_storage_type) - stock.storage.category.capacity - - - - - - - - - diff --git a/stock_storage_type/views/storage_type_menus.xml b/stock_storage_type/views/storage_type_menus.xml index 8300eef602f..6ec04679ee5 100644 --- a/stock_storage_type/views/storage_type_menus.xml +++ b/stock_storage_type/views/storage_type_menus.xml @@ -7,6 +7,13 @@ sequence="9" groups="stock.group_adv_location" /> + \n" "Language-Team: none\n" "Language: it\n" @@ -14,7 +14,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.17\n" +"X-Generator: Weblate 5.10.4\n" #. module: stock_storage_type_putaway_abc #: model_terms:ir.ui.view,arch_db:stock_storage_type_putaway_abc.product_template_form_view_inherit @@ -61,12 +61,30 @@ msgid "" "None: when moved to this location, it will not be put away any further.\n" "Ordered Children Locations: when moved to this location, a suitable location " "will be searched in its children locations according to the restrictions " -"defined on their respective location storage types." +"defined on their respective storage category." msgstr "" "Questo definisce la strategia di stoccaggio in base al tipo di collo da " "utilizzare quanto un prodotto o un collo è depositato in questa ubicazione.\n" -"Nessuna: quanto movimentato in questa ubicazione, non verrà movimentato " +"Nessuna: quando movimentato in questa ubicazione, non verrà movimentato " "ulteriormente.\n" -"Ordinamento per ubicazioni figlie: quanto movimentato in questa ubicazione, " +"Ordinamento per ubicazioni figlie: quando movimentato in questa ubicazione, " "verrà cercata una ubicazione idonea nelle ubicazioni figlie in accordo con " -"le restrizioni definite nei rispettivi tipi stoccaggio ubicazione." +"le restrizioni definite nelle rispettive categorie stoccaggio." + +#~ msgid "" +#~ "This defines the storage strategy based on package type to use when a " +#~ "product or package is put away in this location.\n" +#~ "None: when moved to this location, it will not be put away any further.\n" +#~ "Ordered Children Locations: when moved to this location, a suitable " +#~ "location will be searched in its children locations according to the " +#~ "restrictions defined on their respective location storage types." +#~ msgstr "" +#~ "Questo definisce la strategia di stoccaggio in base al tipo di collo da " +#~ "utilizzare quanto un prodotto o un collo è depositato in questa " +#~ "ubicazione.\n" +#~ "Nessuna: quanto movimentato in questa ubicazione, non verrà movimentato " +#~ "ulteriormente.\n" +#~ "Ordinamento per ubicazioni figlie: quanto movimentato in questa " +#~ "ubicazione, verrà cercata una ubicazione idonea nelle ubicazioni figlie " +#~ "in accordo con le restrizioni definite nei rispettivi tipi stoccaggio " +#~ "ubicazione." diff --git a/stock_storage_type_putaway_abc/i18n/stock_storage_type_putaway_abc.pot b/stock_storage_type_putaway_abc/i18n/stock_storage_type_putaway_abc.pot index d7f55aad187..a0448a5172b 100644 --- a/stock_storage_type_putaway_abc/i18n/stock_storage_type_putaway_abc.pot +++ b/stock_storage_type_putaway_abc/i18n/stock_storage_type_putaway_abc.pot @@ -55,5 +55,5 @@ msgstr "" msgid "" "This defines the storage strategy based on package type to use when a product or package is put away in this location.\n" "None: when moved to this location, it will not be put away any further.\n" -"Ordered Children Locations: when moved to this location, a suitable location will be searched in its children locations according to the restrictions defined on their respective location storage types." +"Ordered Children Locations: when moved to this location, a suitable location will be searched in its children locations according to the restrictions defined on their respective storage category." msgstr "" diff --git a/stock_warehouse_flow/README.rst b/stock_warehouse_flow/README.rst index 3d7a4f7ba9d..693493df2f2 100644 --- a/stock_warehouse_flow/README.rst +++ b/stock_warehouse_flow/README.rst @@ -7,7 +7,7 @@ Stock Warehouse Flow !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:e6b0c95d50cd99f1b0d53c1d3a68163d86befc2775d968185c538d7815ceced3 + !! source digest: sha256:c82fff4e498effcb90a8f7a92adccc461a6ada468f5a82079759bc844272f3c1 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png diff --git a/stock_warehouse_flow/__manifest__.py b/stock_warehouse_flow/__manifest__.py index babd3d2542a..64fa9ab475a 100644 --- a/stock_warehouse_flow/__manifest__.py +++ b/stock_warehouse_flow/__manifest__.py @@ -6,7 +6,7 @@ "author": "Camptocamp, BCIM, Odoo Community Association (OCA)", "website": "https://github.com/OCA/wms", "category": "Warehouse Management", - "version": "16.0.1.0.2", + "version": "16.0.1.1.0", "license": "AGPL-3", "depends": [ # core diff --git a/stock_warehouse_flow/i18n/es.po b/stock_warehouse_flow/i18n/es.po index 8b26769f442..8b668222c02 100644 --- a/stock_warehouse_flow/i18n/es.po +++ b/stock_warehouse_flow/i18n/es.po @@ -36,6 +36,11 @@ msgstr "Activo" msgid "Applicable Flows" msgstr "Flujos aplicables" +#. module: stock_warehouse_flow +#: model:ir.model.fields,field_description:stock_warehouse_flow.field_stock_route__apply_flow_on +msgid "Apply Flow On" +msgstr "" + #. module: stock_warehouse_flow #: model_terms:ir.ui.view,arch_db:stock_warehouse_flow.stock_warehouse_flow_view_form #: model_terms:ir.ui.view,arch_db:stock_warehouse_flow.stock_warehouse_flow_view_search @@ -62,6 +67,11 @@ msgstr "Creado por" msgid "Created on" msgstr "Creado el" +#. module: stock_warehouse_flow +#: model:ir.model.fields,field_description:stock_warehouse_flow.field_stock_move__default_picking_type_id +msgid "Default Picking Type" +msgstr "" + #. module: stock_warehouse_flow #: model:ir.model.fields.selection,name:stock_warehouse_flow.selection__stock_warehouse_flow__delivery_steps__ship_only msgid "Deliver goods directly (1 step)" @@ -400,6 +410,13 @@ msgstr "A ubicación de salida" msgid "Uom" msgstr "UdM" +#. module: stock_warehouse_flow +#: model:ir.model.fields,help:stock_warehouse_flow.field_stock_move__default_picking_type_id +msgid "" +"Used as a backup to save picking type set by odoo, before a new flow is " +"applied." +msgstr "" + #. module: stock_warehouse_flow #: model:ir.model.fields,help:stock_warehouse_flow.field_stock_warehouse_flow__sequence_prefix msgid "" @@ -426,6 +443,11 @@ msgstr "Flujo de Almacén" msgid "Warning" msgstr "Aviso" +#. module: stock_warehouse_flow +#: model:ir.model.fields.selection,name:stock_warehouse_flow.selection__stock_route__apply_flow_on__on_confirm +msgid "When move is confirmed" +msgstr "" + #. module: stock_warehouse_flow #: model:ir.model.fields,field_description:stock_warehouse_flow.field_stock_warehouse_flow__carrier_ids msgid "With carriers" diff --git a/stock_warehouse_flow/i18n/it.po b/stock_warehouse_flow/i18n/it.po index 9141300982b..3c903b6ed83 100644 --- a/stock_warehouse_flow/i18n/it.po +++ b/stock_warehouse_flow/i18n/it.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0\n" "Report-Msgid-Bugs-To: \n" -"PO-Revision-Date: 2023-12-24 18:43+0000\n" +"PO-Revision-Date: 2025-05-03 14:48+0000\n" "Last-Translator: mymage \n" "Language-Team: none\n" "Language: it\n" @@ -14,7 +14,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.17\n" +"X-Generator: Weblate 5.10.4\n" #. module: stock_warehouse_flow #: model_terms:ir.ui.view,arch_db:stock_warehouse_flow.stock_warehouse_flow_view_form @@ -36,6 +36,11 @@ msgstr "Attivo" msgid "Applicable Flows" msgstr "Flussi applicabili" +#. module: stock_warehouse_flow +#: model:ir.model.fields,field_description:stock_warehouse_flow.field_stock_route__apply_flow_on +msgid "Apply Flow On" +msgstr "Applica flusso a" + #. module: stock_warehouse_flow #: model_terms:ir.ui.view,arch_db:stock_warehouse_flow.stock_warehouse_flow_view_form #: model_terms:ir.ui.view,arch_db:stock_warehouse_flow.stock_warehouse_flow_view_search @@ -62,6 +67,11 @@ msgstr "Creato da" msgid "Created on" msgstr "Creato il" +#. module: stock_warehouse_flow +#: model:ir.model.fields,field_description:stock_warehouse_flow.field_stock_move__default_picking_type_id +msgid "Default Picking Type" +msgstr "Tipo predefinito prelievo" + #. module: stock_warehouse_flow #: model:ir.model.fields.selection,name:stock_warehouse_flow.selection__stock_warehouse_flow__delivery_steps__ship_only msgid "Deliver goods directly (1 step)" @@ -398,6 +408,15 @@ msgstr "A ubicazione uscita" msgid "Uom" msgstr "UdM" +#. module: stock_warehouse_flow +#: model:ir.model.fields,help:stock_warehouse_flow.field_stock_move__default_picking_type_id +msgid "" +"Used as a backup to save picking type set by odoo, before a new flow is " +"applied." +msgstr "" +"Utilizzato come backup per salvare il tipo prelievo impostato da Odoo, prima " +"che sia applicato un nuovo flusso." + #. module: stock_warehouse_flow #: model:ir.model.fields,help:stock_warehouse_flow.field_stock_warehouse_flow__sequence_prefix msgid "" @@ -424,6 +443,11 @@ msgstr "Flusso di magazzino" msgid "Warning" msgstr "Attenzione" +#. module: stock_warehouse_flow +#: model:ir.model.fields.selection,name:stock_warehouse_flow.selection__stock_route__apply_flow_on__on_confirm +msgid "When move is confirmed" +msgstr "Quando il movimento è confermato" + #. module: stock_warehouse_flow #: model:ir.model.fields,field_description:stock_warehouse_flow.field_stock_warehouse_flow__carrier_ids msgid "With carriers" diff --git a/stock_warehouse_flow/i18n/stock_warehouse_flow.pot b/stock_warehouse_flow/i18n/stock_warehouse_flow.pot index f151a64490d..35710954779 100644 --- a/stock_warehouse_flow/i18n/stock_warehouse_flow.pot +++ b/stock_warehouse_flow/i18n/stock_warehouse_flow.pot @@ -33,6 +33,11 @@ msgstr "" msgid "Applicable Flows" msgstr "" +#. module: stock_warehouse_flow +#: model:ir.model.fields,field_description:stock_warehouse_flow.field_stock_route__apply_flow_on +msgid "Apply Flow On" +msgstr "" + #. module: stock_warehouse_flow #: model_terms:ir.ui.view,arch_db:stock_warehouse_flow.stock_warehouse_flow_view_form #: model_terms:ir.ui.view,arch_db:stock_warehouse_flow.stock_warehouse_flow_view_search @@ -59,6 +64,11 @@ msgstr "" msgid "Created on" msgstr "" +#. module: stock_warehouse_flow +#: model:ir.model.fields,field_description:stock_warehouse_flow.field_stock_move__default_picking_type_id +msgid "Default Picking Type" +msgstr "" + #. module: stock_warehouse_flow #: model:ir.model.fields.selection,name:stock_warehouse_flow.selection__stock_warehouse_flow__delivery_steps__ship_only msgid "Deliver goods directly (1 step)" @@ -380,6 +390,13 @@ msgstr "" msgid "Uom" msgstr "" +#. module: stock_warehouse_flow +#: model:ir.model.fields,help:stock_warehouse_flow.field_stock_move__default_picking_type_id +msgid "" +"Used as a backup to save picking type set by odoo, before a new flow is " +"applied." +msgstr "" + #. module: stock_warehouse_flow #: model:ir.model.fields,help:stock_warehouse_flow.field_stock_warehouse_flow__sequence_prefix msgid "" @@ -404,6 +421,11 @@ msgstr "" msgid "Warning" msgstr "" +#. module: stock_warehouse_flow +#: model:ir.model.fields.selection,name:stock_warehouse_flow.selection__stock_route__apply_flow_on__on_confirm +msgid "When move is confirmed" +msgstr "" + #. module: stock_warehouse_flow #: model:ir.model.fields,field_description:stock_warehouse_flow.field_stock_warehouse_flow__carrier_ids msgid "With carriers" diff --git a/stock_warehouse_flow/models/stock_move.py b/stock_warehouse_flow/models/stock_move.py index 826b18a5624..5110c498d54 100644 --- a/stock_warehouse_flow/models/stock_move.py +++ b/stock_warehouse_flow/models/stock_move.py @@ -2,12 +2,25 @@ # Copyright 2023 Michael Tietz (MT Software) # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) -from odoo import models +from odoo import fields, models class StockMove(models.Model): _inherit = "stock.move" + default_picking_type_id = fields.Many2one( + "stock.picking.type", + help=( + "Used as a backup to save picking type set by odoo, " + "before a new flow is applied." + ), + ) + + def _apply_flow_on_action_confirm(self): + if self.rule_id.route_id.apply_flow_on != "on_confirm": + return False + return self.picking_type_id.code == "outgoing" + def _action_confirm(self, merge=True, merge_into=False): # Apply the flow configuration on the move before it generates # its chained moves (if any) @@ -26,6 +39,3 @@ def _action_confirm(self, merge=True, merge_into=False): return super(StockMove, moves_to_confirm)._action_confirm( merge=merge, merge_into=merge_into ) - - def _apply_flow_on_action_confirm(self): - return self.picking_type_id.code == "outgoing" diff --git a/stock_warehouse_flow/models/stock_route.py b/stock_warehouse_flow/models/stock_route.py index f5185927eab..745bb7b3e16 100644 --- a/stock_warehouse_flow/models/stock_route.py +++ b/stock_warehouse_flow/models/stock_route.py @@ -17,6 +17,9 @@ class StockRoute(models.Model): compute="_compute_applicable_flow_ids", string="Applicable Flows", ) + apply_flow_on = fields.Selection( + [("on_confirm", "When move is confirmed")], default="on_confirm" + ) @api.depends("rule_ids.picking_type_id") def _compute_applicable_flow_ids(self): diff --git a/stock_warehouse_flow/models/stock_warehouse_flow.py b/stock_warehouse_flow/models/stock_warehouse_flow.py index 9ce50b2d5fe..1ea1966780d 100644 --- a/stock_warehouse_flow/models/stock_warehouse_flow.py +++ b/stock_warehouse_flow/models/stock_warehouse_flow.py @@ -379,9 +379,17 @@ def action_generate_route(self): def _search_for_move_domain(self, move): domain = [ - ("from_picking_type_id", "=", move.picking_type_id.id), ("delivery_route_id", "!=", False), ] + domain_picking_type = [("from_picking_type_id", "=", move.picking_type_id.id)] + if move.default_picking_type_id: + domain_picking_type = expression.OR( + [ + domain_picking_type, + [("from_picking_type_id", "=", move.default_picking_type_id.id)], + ] + ) + domain = expression.AND([domain, domain_picking_type]) if move.group_id.carrier_id: domain.append(("carrier_ids", "in", move.group_id.carrier_id.ids)) else: @@ -527,9 +535,18 @@ def _apply_on_move(self, move, assign_picking=True): """Apply the flow configuration on the move.""" if not self: return False - logger.info("Applying flow '%s' on '%s'", self.name, move) rule = self._get_rule_from_delivery_route() + # If new rule hasn't changed, do nothing + if move.rule_id == rule: + return + logger.info("Applying flow '%s' on '%s'", self.name, move) + # Backup old picking + old_picking = move.picking_id move.picking_id = False + # Backup default type, as we want to always lookup for rules valid + # for default type, and current type + if not move.default_picking_type_id: + move.default_picking_type_id = move.picking_type_id move.picking_type_id = self.to_picking_type_id move.location_id = ( self.to_output_stock_loc_id @@ -539,6 +556,9 @@ def _apply_on_move(self, move, assign_picking=True): move.rule_id = rule if assign_picking: move._assign_picking() + # If all moves are moved to another picking, we can unlink the old one. + if not old_picking.move_ids: + old_picking.state = "cancel" def write(self, vals): res = super().write(vals) diff --git a/stock_warehouse_flow/static/description/index.html b/stock_warehouse_flow/static/description/index.html index ec0d8500a5d..04faf101c48 100644 --- a/stock_warehouse_flow/static/description/index.html +++ b/stock_warehouse_flow/static/description/index.html @@ -3,7 +3,7 @@ -Stock Warehouse Flow +README.rst -
    -

    Stock Warehouse Flow

    - - -

    Alpha License: AGPL-3 OCA/wms Translate me on Weblate Try me on Runboat

    -

    This module introduces the concept of routing flows in order to manage -different delivery routes for a warehouse.

    -

    The default behavior of Odoo allows you to have only one delivery route per -warehouse (with one, two or three steps). -With this module, you are now able to manage multiple delivery routes (having -their own rules and operation types), the right one being selected automatically -based on some criterias, like the carrier and any attribute of the stock move -to process.

    -

    This allows you to define a delivery route based on the type of goods to ship, -for instance:

    -
      -
    • whole pallet (pick + ship)
    • -
    • cold chain goods
    • -
    • dangerous goods
    • -
    -https://raw.githubusercontent.com/OCA/wms/14.0/stock_warehouse_flow/static/description/flow.png -
    -

    Important

    -

    This is an alpha version, the data model and design can change at any time without warning. -Only for development or testing purpose, do not use in production. -More details on development status

    -
    -

    Table of contents

    - -
    -

    Configuration

    -

    Got to “Inventory > Settings > Routing Flows”.

    -

    A routing flow can be seen as a helper to generate a delivery route (like the -warehouse is doing automatically). The new route will get its own rules and -operation types that doesn’t overlap with the default ones of the warehouse.

    -

    A routing flow is responsible to change the warehouse delivery route of a move -by another one depending on some criterias:

    -
      -
    • the initial outgoing operation type (usually the default one)
    • -
    • the carrier
    • -
    • a custom domain (applied on the move)
    • -
    -

    This way you are able to change the route a move will take depending on its -carrier and, for instance, the type or the packaging of the product -you want to ship.

    -https://raw.githubusercontent.com/OCA/wms/14.0/stock_warehouse_flow/static/description/config.png -
    -
    -

    Usage

    -

    When a stock move is confirmed, if a flow is matching all the criteria then -the new delivery route will be automatically applied.

    -
    -
    -

    Known issues / Roadmap

    -

    Currently, the module supports only delivery routes, but it could improved to -support reception routes as well.

    -
    -
    -

    Bug Tracker

    -

    Bugs are tracked on GitHub Issues. -In case of trouble, please check there if your issue has already been reported. -If you spotted it first, help us to smash it by providing a detailed and welcomed -feedback.

    -

    Do not contact contributors directly about support or help with technical issues.

    -
    -
    -

    Credits

    -
    -

    Authors

    -
      -
    • Camptocamp
    • -
    • BCIM
    • -
    -
    -
    -

    Contributors

    - -
    -
    -

    Maintainers

    -

    This module is maintained by the OCA.

    -Odoo Community Association -

    OCA, or the Odoo Community Association, is a nonprofit organization whose -mission is to support the collaborative development of Odoo features and -promote its widespread use.

    -

    This module is part of the OCA/wms project on GitHub.

    -

    You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

    -
    -
    +
    + + +
    diff --git a/stock_warehouse_flow/tests/test_warehouse_flow.py b/stock_warehouse_flow/tests/test_warehouse_flow.py index 3a446e2fddd..a2e1edae8c7 100644 --- a/stock_warehouse_flow/tests/test_warehouse_flow.py +++ b/stock_warehouse_flow/tests/test_warehouse_flow.py @@ -8,6 +8,31 @@ class TestWarehouseFlow(common.CommonFlow): + def test_flow_search_after_applied(self): + flow_ship_only = self._get_flow("ship_only") + flow_pick_ship = self._get_flow("pick_ship") + moves_before = self.env["stock.move"].search([]) + self._run_procurement(self.product, 10, flow_pick_ship.carrier_ids) + moves = self.env["stock.move"].search([("id", "not in", moves_before.ids)]) + move_ship = moves.filtered(lambda m: m.picking_type_id.code == "outgoing") + # When flow was applied to move, picking type set by odoo was saved + # in default_picking_type_id, and flow.to_picking_type_id is set + self.assertEqual(move_ship.picking_type_id, flow_pick_ship.to_picking_type_id) + default_type = self.env.ref("stock.picking_type_out") + self.assertEqual(move_ship.default_picking_type_id, default_type) + # Only pick_ship matches, because there's a constraint on the carrier + matching_flows = self.env["stock.warehouse.flow"]._search_for_move(move_ship) + self.assertEqual(matching_flows, flow_pick_ship) + # Drop the carrier from the procurement, only ship only flow should match + move_ship.group_id.carrier_id = False + matching_flows = self.env["stock.warehouse.flow"]._search_for_move(move_ship) + self.assertEqual(matching_flows, flow_ship_only) + # Drop default_picking_type_id, no flow is found, because + # now flow has Delivery Orders POST as from_picking_type_id + move_ship.default_picking_type_id = False + matching_flows = self.env["stock.warehouse.flow"]._search_for_move(move_ship) + self.assertFalse(matching_flows) + def test_flow_ship_only(self): """Replace the initial move by a 'ship_only' move.""" # NOTE: use the recorder when migrating to 15.0 to catch created moves @@ -65,6 +90,17 @@ def test_flow_pick_ship(self): self.assertEqual(move_ship.state, "assigned") self._validate_picking(move_ship.picking_id) + def test_disable_flow_at_move_confirm(self): + flow = self._get_flow("pick_ship") + # It is False by default + flow.impacted_route_ids.apply_flow_on = False + moves_before = self.env["stock.move"].search([]) + self._run_procurement(self.product, 10, flow.carrier_ids) + moves = self.env["stock.move"].search([("id", "not in", moves_before.ids)]) + move_ship = moves.filtered(lambda m: m.picking_type_id.code == "outgoing") + # ensure new move doesn't have flow.to_picking_type_id as picking type + self.assertNotEqual(move_ship.picking_type_id, flow.to_picking_type_id) + def test_no_rule_found_on_delivery_route(self): flow = self._get_flow("pick_ship") # Remove the rule diff --git a/stock_warehouse_flow/views/stock_route.xml b/stock_warehouse_flow/views/stock_route.xml index f2802dc7866..3be9e0e66c5 100644 --- a/stock_warehouse_flow/views/stock_route.xml +++ b/stock_warehouse_flow/views/stock_route.xml @@ -14,4 +14,15 @@ + + stock.route.form.inherit + stock.route + + + + + + + + diff --git a/stock_warehouse_flow_delivery_refresh/README.rst b/stock_warehouse_flow_delivery_refresh/README.rst new file mode 100644 index 00000000000..131d52c7eaa --- /dev/null +++ b/stock_warehouse_flow_delivery_refresh/README.rst @@ -0,0 +1,85 @@ +===================================== +Stock Warehouse Flow Delivery Refresh +===================================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:2391de51e04af41e8c5acfcd570c0a8d2ccfeef45975cb932f12686e45873281 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png + :target: https://odoo-community.org/page/development-status + :alt: Alpha +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fwms-lightgray.png?logo=github + :target: https://github.com/OCA/wms/tree/16.0/stock_warehouse_flow_delivery_refresh + :alt: OCA/wms +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/wms-16-0/wms-16-0-stock_warehouse_flow_delivery_refresh + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/wms&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This is a glue module between Stock Warehouse Flow and Stock Available On Premise Release. + +This module applies a new Stock Warehouse Flow on Stock Moves waiting for release when carrier changes on a Stock Picking + +.. IMPORTANT:: + This is an alpha version, the data model and design can change at any time without warning. + Only for development or testing purpose, do not use in production. + `More details on development status `_ + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Camptocamp +* BCIM + +Contributors +~~~~~~~~~~~~ + +* Jacques-Etienne Baudoux +* Matthieu Méquignon + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/wms `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/stock_warehouse_flow_delivery_refresh/__init__.py b/stock_warehouse_flow_delivery_refresh/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/stock_warehouse_flow_delivery_refresh/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/stock_warehouse_flow_delivery_refresh/__manifest__.py b/stock_warehouse_flow_delivery_refresh/__manifest__.py new file mode 100644 index 00000000000..a054ac48181 --- /dev/null +++ b/stock_warehouse_flow_delivery_refresh/__manifest__.py @@ -0,0 +1,15 @@ +{ + "name": "Stock Warehouse Flow Delivery Refresh", + "summary": "Allow to refresh delivery flow when carrier changes", + "version": "16.0.1.0.0", + "category": "Warehouse Management", + "website": "https://github.com/OCA/wms", + "author": "Camptocamp, BCIM, Odoo Community Association (OCA)", + "license": "AGPL-3", + "installable": True, + "depends": [ + "stock_warehouse_flow", + "stock_available_to_promise_release", + ], + "development_status": "Alpha", +} diff --git a/stock_warehouse_flow_delivery_refresh/i18n/it.po b/stock_warehouse_flow_delivery_refresh/i18n/it.po new file mode 100644 index 00000000000..f4e7cc63246 --- /dev/null +++ b/stock_warehouse_flow_delivery_refresh/i18n/it.po @@ -0,0 +1,27 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * stock_warehouse_flow_delivery_refresh +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2025-05-03 17:24+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.10.4\n" + +#. module: stock_warehouse_flow_delivery_refresh +#: model:ir.model,name:stock_warehouse_flow_delivery_refresh.model_stock_move +msgid "Stock Move" +msgstr "Movimento di magazzino" + +#. module: stock_warehouse_flow_delivery_refresh +#: model:ir.model,name:stock_warehouse_flow_delivery_refresh.model_stock_picking +msgid "Transfer" +msgstr "Trasferimento" diff --git a/stock_warehouse_flow_delivery_refresh/i18n/stock_warehouse_flow_delivery_refresh.pot b/stock_warehouse_flow_delivery_refresh/i18n/stock_warehouse_flow_delivery_refresh.pot new file mode 100644 index 00000000000..c57f505fff8 --- /dev/null +++ b/stock_warehouse_flow_delivery_refresh/i18n/stock_warehouse_flow_delivery_refresh.pot @@ -0,0 +1,24 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * stock_warehouse_flow_delivery_refresh +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: stock_warehouse_flow_delivery_refresh +#: model:ir.model,name:stock_warehouse_flow_delivery_refresh.model_stock_move +msgid "Stock Move" +msgstr "" + +#. module: stock_warehouse_flow_delivery_refresh +#: model:ir.model,name:stock_warehouse_flow_delivery_refresh.model_stock_picking +msgid "Transfer" +msgstr "" diff --git a/stock_warehouse_flow_delivery_refresh/models/__init__.py b/stock_warehouse_flow_delivery_refresh/models/__init__.py new file mode 100644 index 00000000000..a33bde1e878 --- /dev/null +++ b/stock_warehouse_flow_delivery_refresh/models/__init__.py @@ -0,0 +1,2 @@ +from . import stock_move +from . import stock_picking diff --git a/stock_warehouse_flow_delivery_refresh/models/stock_move.py b/stock_warehouse_flow_delivery_refresh/models/stock_move.py new file mode 100644 index 00000000000..ac9c2944bb8 --- /dev/null +++ b/stock_warehouse_flow_delivery_refresh/models/stock_move.py @@ -0,0 +1,15 @@ +# Copyright 2025 Camptocamp SA, BCIM +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import models + + +class StockMove(models.Model): + _inherit = "stock.move" + + def _refresh_warehouse_flow(self): + flow_model = self.env["stock.warehouse.flow"] + for move in self: + if not move.need_release: + continue + flow_model._search_and_apply_for_move(move, assign_picking=True) diff --git a/stock_warehouse_flow_delivery_refresh/models/stock_picking.py b/stock_warehouse_flow_delivery_refresh/models/stock_picking.py new file mode 100644 index 00000000000..075bacc06fa --- /dev/null +++ b/stock_warehouse_flow_delivery_refresh/models/stock_picking.py @@ -0,0 +1,17 @@ +# Copyright 2025 Camptocamp SA, BCIM +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import models + + +class StockPicking(models.Model): + _inherit = "stock.picking" + + def write(self, values): + picking_carrier_mapping = {p.id: p.carrier_id for p in self} + res = super().write(values) + pickings_to_update = self.filtered( + lambda p: picking_carrier_mapping.get(p.id) != p.carrier_id + ) + pickings_to_update.move_ids._refresh_warehouse_flow() + return res diff --git a/stock_warehouse_flow_delivery_refresh/readme/CONTRIBUTORS.rst b/stock_warehouse_flow_delivery_refresh/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000000..c37569c4e3b --- /dev/null +++ b/stock_warehouse_flow_delivery_refresh/readme/CONTRIBUTORS.rst @@ -0,0 +1,2 @@ +* Jacques-Etienne Baudoux +* Matthieu Méquignon diff --git a/stock_warehouse_flow_delivery_refresh/readme/DESCRIPTION.rst b/stock_warehouse_flow_delivery_refresh/readme/DESCRIPTION.rst new file mode 100644 index 00000000000..f411e9e27f7 --- /dev/null +++ b/stock_warehouse_flow_delivery_refresh/readme/DESCRIPTION.rst @@ -0,0 +1,3 @@ +This is a glue module between Stock Warehouse Flow and Stock Available On Premise Release. + +This module applies a new Stock Warehouse Flow on Stock Moves waiting for release when carrier changes on a Stock Picking diff --git a/stock_warehouse_flow_delivery_refresh/static/description/icon.png b/stock_warehouse_flow_delivery_refresh/static/description/icon.png new file mode 100644 index 00000000000..1dcc49c24f3 Binary files /dev/null and b/stock_warehouse_flow_delivery_refresh/static/description/icon.png differ diff --git a/stock_warehouse_flow_delivery_refresh/static/description/index.html b/stock_warehouse_flow_delivery_refresh/static/description/index.html new file mode 100644 index 00000000000..04faf101c48 --- /dev/null +++ b/stock_warehouse_flow_delivery_refresh/static/description/index.html @@ -0,0 +1,368 @@ + + + + + +README.rst + + + +
    + + + +
    + + diff --git a/stock_warehouse_flow_delivery_refresh/tests/__init__.py b/stock_warehouse_flow_delivery_refresh/tests/__init__.py new file mode 100644 index 00000000000..6be097995a5 --- /dev/null +++ b/stock_warehouse_flow_delivery_refresh/tests/__init__.py @@ -0,0 +1 @@ +from . import test_reapply_flow diff --git a/stock_warehouse_flow_delivery_refresh/tests/test_reapply_flow.py b/stock_warehouse_flow_delivery_refresh/tests/test_reapply_flow.py new file mode 100644 index 00000000000..53cee536627 --- /dev/null +++ b/stock_warehouse_flow_delivery_refresh/tests/test_reapply_flow.py @@ -0,0 +1,52 @@ +# Copyright 2025 Camptocamp SA, BCIM +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo.tests import tagged +from odoo.tests.common import Form + +from odoo.addons.stock_warehouse_flow.tests.common import CommonFlow + + +@tagged("-at_install", "post_install") +class TestDeliveryRefresh(CommonFlow): + @classmethod + def setUpClass(cls): + super().setUpClass() + # Set outgoing rules as + out_rules = cls.env["stock.rule"].search( + [("picking_type_id.code", "=", "outgoing")] + ) + out_rules.route_id.available_to_promise_defer_pull = True + cls.customer = cls.env["res.partner"].create({"name": "Bob the customer"}) + + @classmethod + def create_sale_order(cls, product_qty, carrier=None): + with Form(cls.env["sale.order"]) as sale_form: + sale_form.partner_id = cls.customer + for product, qty in product_qty: + with sale_form.order_line.new() as line: + line.product_id = product + line.product_uom_qty = qty + order = sale_form.save() + if carrier: + cls.update_carrier_on_order(order, carrier) + return order + + @classmethod + def update_carrier_on_order(cls, order, carrier): + order.set_delivery_line(carrier, 1) + + def test_flow_refresh_after_delivery_change(self): + self.env.ref("stock.stock_location_stock") + post_carrier_flow = self._get_flow("pick_ship") + normal_carrier_flow = self._get_flow("pick_pack_ship") + order = self.create_sale_order( + [(self.product, 10)], carrier=post_carrier_flow.carrier_ids + ) + order.action_confirm() + picking = order.picking_ids + move = picking.move_ids + self.assertEqual(move.picking_type_id, post_carrier_flow.to_picking_type_id) + # # Updating the carrier on the order should update flow used by moves + self.update_carrier_on_order(order, normal_carrier_flow.carrier_ids) + self.assertEqual(move.picking_type_id, normal_carrier_flow.to_picking_type_id) diff --git a/stock_warehouse_flow_release/README.rst b/stock_warehouse_flow_release/README.rst index f85fff8b830..a8429f6440d 100644 --- a/stock_warehouse_flow_release/README.rst +++ b/stock_warehouse_flow_release/README.rst @@ -7,7 +7,7 @@ Stock Warehouse Flow (release integration) !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:d715d0b96733f62db800de351ef5f56c41e8847d4b1d4e07062731036b96e53c + !! source digest: sha256:2c7e4ab3d551fde61622d6cdeec3d4ffb1668ac80ab33ed08546ef85365535e8 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png @@ -30,7 +30,7 @@ Stock Warehouse Flow (release integration) Integrate the **Stock Warehouse Flow** module with **Stock Available to Promise Release**, so the flow is applied -when a move is released (not anymore when it is confirmed). +when a move is released. .. IMPORTANT:: This is an alpha version, the data model and design can change at any time without warning. diff --git a/stock_warehouse_flow_release/__manifest__.py b/stock_warehouse_flow_release/__manifest__.py index e07d031f812..2b071b0dc3b 100644 --- a/stock_warehouse_flow_release/__manifest__.py +++ b/stock_warehouse_flow_release/__manifest__.py @@ -6,7 +6,7 @@ "author": "Camptocamp, BCIM, Odoo Community Association (OCA)", "website": "https://github.com/OCA/wms", "category": "Warehouse Management", - "version": "16.0.1.0.0", + "version": "16.0.1.1.0", "license": "AGPL-3", "depends": [ # OCA/wms @@ -14,7 +14,6 @@ "stock_available_to_promise_release", ], "data": [], - "auto_install": True, "installable": True, "development_status": "Alpha", } diff --git a/stock_warehouse_flow_release/i18n/it.po b/stock_warehouse_flow_release/i18n/it.po index d73f5d6efcf..f7bd4256c5e 100644 --- a/stock_warehouse_flow_release/i18n/it.po +++ b/stock_warehouse_flow_release/i18n/it.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 14.0\n" "Report-Msgid-Bugs-To: \n" -"PO-Revision-Date: 2023-09-20 04:45+0000\n" +"PO-Revision-Date: 2025-05-03 14:48+0000\n" "Last-Translator: mymage \n" "Language-Team: none\n" "Language: it\n" @@ -14,24 +14,33 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.17\n" +"X-Generator: Weblate 5.10.4\n" #. module: stock_warehouse_flow_release -#: model:ir.model.fields,field_description:stock_warehouse_flow_release.field_stock_move__display_name -msgid "Display Name" -msgstr "Nome visualizzato" +#: model:ir.model.fields,field_description:stock_warehouse_flow_release.field_stock_route__apply_flow_on +msgid "Apply Flow On" +msgstr "Applica flusso a" #. module: stock_warehouse_flow_release -#: model:ir.model.fields,field_description:stock_warehouse_flow_release.field_stock_move__id -msgid "ID" -msgstr "ID" - -#. module: stock_warehouse_flow_release -#: model:ir.model.fields,field_description:stock_warehouse_flow_release.field_stock_move____last_update -msgid "Last Modified on" -msgstr "Ultima modifica il" +#: model:ir.model,name:stock_warehouse_flow_release.model_stock_route +msgid "Inventory Routes" +msgstr "Percorsi di inventario" #. module: stock_warehouse_flow_release #: model:ir.model,name:stock_warehouse_flow_release.model_stock_move msgid "Stock Move" msgstr "Movimento di magazzino" + +#. module: stock_warehouse_flow_release +#: model:ir.model.fields.selection,name:stock_warehouse_flow_release.selection__stock_route__apply_flow_on__on_release +msgid "When move is released" +msgstr "Quando il movimento è rilasciato" + +#~ msgid "Display Name" +#~ msgstr "Nome visualizzato" + +#~ msgid "ID" +#~ msgstr "ID" + +#~ msgid "Last Modified on" +#~ msgstr "Ultima modifica il" diff --git a/stock_warehouse_flow_release/i18n/stock_warehouse_flow_release.pot b/stock_warehouse_flow_release/i18n/stock_warehouse_flow_release.pot index a1c307ca235..57fdfc3854f 100644 --- a/stock_warehouse_flow_release/i18n/stock_warehouse_flow_release.pot +++ b/stock_warehouse_flow_release/i18n/stock_warehouse_flow_release.pot @@ -13,7 +13,22 @@ msgstr "" "Content-Transfer-Encoding: \n" "Plural-Forms: \n" +#. module: stock_warehouse_flow_release +#: model:ir.model.fields,field_description:stock_warehouse_flow_release.field_stock_route__apply_flow_on +msgid "Apply Flow On" +msgstr "" + +#. module: stock_warehouse_flow_release +#: model:ir.model,name:stock_warehouse_flow_release.model_stock_route +msgid "Inventory Routes" +msgstr "" + #. module: stock_warehouse_flow_release #: model:ir.model,name:stock_warehouse_flow_release.model_stock_move msgid "Stock Move" msgstr "" + +#. module: stock_warehouse_flow_release +#: model:ir.model.fields.selection,name:stock_warehouse_flow_release.selection__stock_route__apply_flow_on__on_release +msgid "When move is released" +msgstr "" diff --git a/stock_warehouse_flow_release/migrations/16.0.1.1.0/post-migrate.py b/stock_warehouse_flow_release/migrations/16.0.1.1.0/post-migrate.py new file mode 100644 index 00000000000..ca58f3756b0 --- /dev/null +++ b/stock_warehouse_flow_release/migrations/16.0.1.1.0/post-migrate.py @@ -0,0 +1,25 @@ +# Copyright 2025 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +import logging + +from odoo import SUPERUSER_ID, api + +_logger = logging.getLogger(__name__) + + +def migrate(cr, version): + if not version: + return + env = api.Environment(cr, SUPERUSER_ID, {}) + outgoing_rules = env["stock.rule"].search( + [("picking_type_id.code", "=", "outgoing")] + ) + routes_to_update = env["stock.route"].search( + [ + ("available_to_promise_defer_pull", "=", True), + ("rule_ids", "in", outgoing_rules.ids), + ] + ) + _logger.info(f"disabling flow at confirm on {len(routes_to_update)} routes") + routes_to_update.apply_flow_on = "on_release" diff --git a/stock_warehouse_flow_release/models/__init__.py b/stock_warehouse_flow_release/models/__init__.py index 6bda2d2428e..6ea87dcbcef 100644 --- a/stock_warehouse_flow_release/models/__init__.py +++ b/stock_warehouse_flow_release/models/__init__.py @@ -1 +1,2 @@ from . import stock_move +from . import stock_route diff --git a/stock_warehouse_flow_release/models/stock_move.py b/stock_warehouse_flow_release/models/stock_move.py index 153eb00da91..3a9d54231bf 100644 --- a/stock_warehouse_flow_release/models/stock_move.py +++ b/stock_warehouse_flow_release/models/stock_move.py @@ -8,12 +8,10 @@ class StockMove(models.Model): _inherit = "stock.move" - def _apply_flow_on_action_confirm(self): - # Override to not apply the flow configuration on moves to release. - # The flow will be applied on the release, not before. - if self.rule_id.route_id.available_to_promise_defer_pull: + def _apply_flow_on_release(self): + if self.rule_id.route_id.apply_flow_on != "on_release": return False - return super()._apply_flow_on_action_confirm() + return self.picking_type_id.code == "outgoing" def _before_release(self): # Apply the flow when releasing the move @@ -21,6 +19,8 @@ def _before_release(self): FLOW = self.env["stock.warehouse.flow"] move_ids_to_release = [] for move in self: + if not move._apply_flow_on_release(): + continue _move_ids_to_release = FLOW._search_and_apply_for_move(move).ids _move_ids_to_release.remove(move.id) move_ids_to_release += _move_ids_to_release diff --git a/stock_warehouse_flow_release/models/stock_route.py b/stock_warehouse_flow_release/models/stock_route.py new file mode 100644 index 00000000000..a67ff6952c4 --- /dev/null +++ b/stock_warehouse_flow_release/models/stock_route.py @@ -0,0 +1,12 @@ +# Copyright 2025 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import fields, models + + +class StockRoute(models.Model): + _inherit = "stock.route" + + apply_flow_on = fields.Selection( + selection_add=[("on_release", "When move is released")] + ) diff --git a/stock_warehouse_flow_release/readme/DESCRIPTION.rst b/stock_warehouse_flow_release/readme/DESCRIPTION.rst index 3af6a51009a..120c7affd6e 100644 --- a/stock_warehouse_flow_release/readme/DESCRIPTION.rst +++ b/stock_warehouse_flow_release/readme/DESCRIPTION.rst @@ -1,3 +1,3 @@ Integrate the **Stock Warehouse Flow** module with **Stock Available to Promise Release**, so the flow is applied -when a move is released (not anymore when it is confirmed). +when a move is released. diff --git a/stock_warehouse_flow_release/tests/test_warehouse_flow_release.py b/stock_warehouse_flow_release/tests/test_warehouse_flow_release.py index 11d38fb5981..0a3d2062a38 100644 --- a/stock_warehouse_flow_release/tests/test_warehouse_flow_release.py +++ b/stock_warehouse_flow_release/tests/test_warehouse_flow_release.py @@ -12,8 +12,25 @@ def setUpClass(cls): # Set the default delivery route as pick+ship to make releasing working # (there is no need to release a 'ship_only' move) cls.wh.delivery_steps = "pick_ship" - # Enable the operation release on the default delivery route - cls.wh.delivery_route_id.available_to_promise_defer_pull = True + cls.out_rules = cls.env["stock.rule"].search( + [("picking_type_id.code", "=", "outgoing")] + ) + cls.out_rules.route_id.available_to_promise_defer_pull = True + cls.out_rules.route_id.apply_flow_on = "on_release" + + def test_flow_pick_ship_flow_at_confirm(self): + self.out_rules.route_id.apply_flow_on = "on_confirm" + # enable flow assigning at confirm + flow = self._get_flow("pick_ship") + to_picking_type = flow.to_picking_type_id + # NOTE: use the recorder when migrating to 15.0 to catch created moves + moves_before = self.env["stock.move"].search([]) + self._run_procurement(self.product, 10, flow.carrier_ids) + moves_after = self.env["stock.move"].search([]) + move = moves_after - moves_before + self.assertEqual(len(move), 1) + self.assertTrue(move.need_release) + self.assertEqual(move.picking_type_id, to_picking_type) def test_flow_pick_ship_on_release(self): """Replace the initial 'ship_only' move by pick+ship chained moves.