diff --git a/setup/stock_move_location/odoo/addons/stock_move_location b/setup/stock_move_location/odoo/addons/stock_move_location new file mode 120000 index 000000000..c0bc16074 --- /dev/null +++ b/setup/stock_move_location/odoo/addons/stock_move_location @@ -0,0 +1 @@ +../../../../stock_move_location \ No newline at end of file diff --git a/setup/stock_move_location/setup.py b/setup/stock_move_location/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/stock_move_location/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/stock_move_location/README.rst b/stock_move_location/README.rst new file mode 100644 index 000000000..bdf72e0bf --- /dev/null +++ b/stock_move_location/README.rst @@ -0,0 +1,119 @@ +=================== +Move Stock Location +=================== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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%2Fstock--logistics--warehouse-lightgray.png?logo=github + :target: https://github.com/OCA/stock-logistics-warehouse/tree/14.0/stock_move_location + :alt: OCA/stock-logistics-warehouse +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/stock-logistics-warehouse-14-0/stock-logistics-warehouse-14-0-stock_move_location + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/153/14.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module allows to move entire location of products from one place to +another and move only selected quantities. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +* A new menu item Stock > Move from location... opens a wizard + where 2 locations can be specified. +* Select origin and destination locations and press "IMMEDIATE TRANSFER" or "PLANNED TRANSFER" +* Press `ADD ALL` button to add all products available +* Those lines can be edited. Move quantity can't be more than a max available quantity +* Move doesn't care about the reservations and will move stuff anyway +* If during your operation with the wizard the real quantity will change + it will move only the available quantity at the button press +* Products will be moved and a form view of picking that did that will show up +* If "PLANNED TRANSFER" is used - the picking won't be validated automatically + +If you want to transfer a full quant: + +* Go to `Inventory > Master Data > Products` and click "On hand" smart button + or `Inventory > Reporting > Inventory`, the quants view will be + opened. + +* Select the quantities which you want move to another location + +If you go to the Inventory Dashboard you can see the button "Move from location" +in each of the picking types (only applicable to internal transfers). Press it +and you will be directed to the wizard. + +Known issues / Roadmap +====================== + +Change the current implementation (suggested by Denis Roussel from ACSONE): + +* A new parameter on stock picking types : 'Product Change Location' (with a little help). +* With this, go to the dashboard, create a picking with that type. +* Add a button on the picking form which is visible with that type that fill in the picking as now +* Nice to have: add a magic button on locations that with context creates a new picking of that type with the origin location already filled in. + +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 smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Julius Network Solutions + +Contributors +~~~~~~~~~~~~ + +* Mathieu Vatel +* Mykhailo Panarin +* Joan Sisquella +* Jordi Ballester Alomar +* Lois Rilo +* Héctor Villarreal +* Tecnativa + + * Sergio Teruel + * João Marques + +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/stock-logistics-warehouse `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/stock_move_location/__init__.py b/stock_move_location/__init__.py new file mode 100644 index 000000000..d1dcf0d2a --- /dev/null +++ b/stock_move_location/__init__.py @@ -0,0 +1,7 @@ +# Copyright (C) 2011 Julius Network Solutions SARL +# Copyright 2018 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from . import wizard +from . import models +from .init_hook import enable_multi_locations diff --git a/stock_move_location/__manifest__.py b/stock_move_location/__manifest__.py new file mode 100644 index 000000000..43d006fe6 --- /dev/null +++ b/stock_move_location/__manifest__.py @@ -0,0 +1,23 @@ +# Copyright (C) 2011 Julius Network Solutions SARL +# Copyright 2018 Camptocamp SA +# Copyright 2020 Tecnativa - João Marques +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +{ + "name": "Move Stock Location", + "version": "14.0.1.0.0", + "author": "Julius Network Solutions, Odoo Community Association (OCA)", + "summary": "This module allows to move all stock " + "in a stock location to an other one.", + "website": "https://github.com/OCA/stock-logistics-warehouse", + "license": "AGPL-3", + "depends": ["stock"], + "category": "Stock", + "data": [ + "data/stock_quant_view.xml", + "security/ir.model.access.csv", + "views/stock_picking_type_views.xml", + "wizard/stock_move_location.xml", + ], + "post_init_hook": "enable_multi_locations", +} diff --git a/stock_move_location/data/stock_quant_view.xml b/stock_move_location/data/stock_quant_view.xml new file mode 100644 index 000000000..868858787 --- /dev/null +++ b/stock_move_location/data/stock_quant_view.xml @@ -0,0 +1,13 @@ + + + + + Move to location... + wiz.stock.move.location + + form + + new + + + diff --git a/stock_move_location/i18n/de.po b/stock_move_location/i18n/de.po new file mode 100644 index 000000000..71d0dc541 --- /dev/null +++ b/stock_move_location/i18n/de.po @@ -0,0 +1,264 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * stock_move_location +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 11.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2019-10-25 14:33+0000\n" +"Last-Translator: Stefan Wild \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 3.8\n" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location__apply_putaway_strategy +msgid "Apply putaway strategy" +msgstr "" + +#. module: stock_move_location +#: model_terms:ir.ui.view,arch_db:stock_move_location.view_wiz_stock_move_location_form_stock_move_location +msgid "Apply putaway strategy for moving products" +msgstr "" + +#. module: stock_move_location +#: model_terms:ir.ui.view,arch_db:stock_move_location.view_wiz_stock_move_location_form_stock_move_location +msgid "Cancel" +msgstr "Abbrechen" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location__picking_id +msgid "Connected Picking" +msgstr "Verbundene Bewegung" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location__create_uid +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location_line__create_uid +msgid "Created by" +msgstr "Erstellt von" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location__create_date +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location_line__create_date +msgid "Created on" +msgstr "Erstellt am" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location_line__custom +msgid "Custom line" +msgstr "Angepasste Zeile" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location__destination_location_id +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location_line__destination_location_id +msgid "Destination Location" +msgstr "Ziellagerort" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location__destination_location_disable +#, fuzzy +msgid "Destination Location Disable" +msgstr "Ziellagerort" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location__display_name +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location_line__display_name +msgid "Display Name" +msgstr "Anzeigename" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location__edit_locations +#: model_terms:ir.ui.view,arch_db:stock_move_location.view_wiz_stock_move_location_form_stock_move_location +#, fuzzy +msgid "Edit Locations" +msgstr "Quelllagerort" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location__id +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location_line__id +msgid "ID" +msgstr "ID" + +#. module: stock_move_location +#: model_terms:ir.ui.view,arch_db:stock_move_location.view_wiz_stock_move_location_form_stock_move_location +msgid "Immediate Transfer" +msgstr "Sofortige Lieferung" + +#. module: stock_move_location +#: model_terms:ir.ui.view,arch_db:stock_move_location.view_wiz_stock_move_location_form_stock_move_location +msgid "Inventory Details" +msgstr "Bestand Details" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location____last_update +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location_line____last_update +msgid "Last Modified on" +msgstr "Zuletzt geändert am" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location__write_uid +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location_line__write_uid +msgid "Last Updated by" +msgstr "Zuletzt geändert von" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location__write_date +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location_line__write_date +msgid "Last Updated on" +msgstr "Zuletzt geändert am" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location_line__lot_id +msgid "Lot/Serial Number" +msgstr "Lot/Seriennummer" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location_line__max_quantity +msgid "Maximum available quantity" +msgstr "Maximal verfügbare Menge" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location__stock_move_location_line_ids +msgid "Move Location lines" +msgstr "Lagerort Buchungszeilen" + +#. module: stock_move_location +#: model_terms:ir.ui.view,arch_db:stock_move_location.stock_picking_type_kanban +msgid "Move On Hand" +msgstr "" + +#. module: stock_move_location +#: model:ir.actions.act_window,name:stock_move_location.wiz_stock_move_location_action +#: model:ir.ui.menu,name:stock_move_location.menuitem_move_location +msgid "Move from location..." +msgstr "Von Lagerort bewegen..." + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location_line__move_location_wizard_id +msgid "Move location Wizard" +msgstr "Umlagerungsassistent" + +#. module: stock_move_location +#: code:addons/stock_move_location/wizard/stock_move_location_line.py:0 +#, python-format +msgid "Move quantity can not exceed max quantity or be negative" +msgstr "Die Menge darf nicht die Bestandsmenge überschreiten oder negativ sein" + +#. module: stock_move_location +#: model:ir.actions.act_window,name:stock_move_location.wiz_stock_quant_location_action +msgid "Move to location..." +msgstr "Zu Lagerort bewegen..." + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location__origin_location_id +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location_line__origin_location_id +msgid "Origin Location" +msgstr "Quelllagerort" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location__origin_location_disable +#, fuzzy +msgid "Origin Location Disable" +msgstr "Quelllagerort" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_stock_move__location_move +msgid "Part of move location" +msgstr "Teil der Lagerortbewegung" + +#. module: stock_move_location +#: model:ir.model,name:stock_move_location.model_stock_picking_type +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location__picking_type_id +msgid "Picking Type" +msgstr "" + +#. module: stock_move_location +#: model_terms:ir.ui.view,arch_db:stock_move_location.view_wiz_stock_move_location_form_stock_move_location +msgid "Planned Transfer" +msgstr "Geplante Bewegung" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location_line__product_id +msgid "Product" +msgstr "Produkt" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location_line__product_uom_id +msgid "Product Unit of Measure" +msgstr "Produkt Maßeinheit" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location_line__move_quantity +msgid "Quantity to move" +msgstr "Anzahl" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location_line__reserved_quantity +msgid "Reserved quantity" +msgstr "" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_stock_picking_type__show_move_onhand +msgid "Show Move On hand stock" +msgstr "" + +#. module: stock_move_location +#: model:ir.model.fields,help:stock_move_location.field_stock_picking_type__show_move_onhand +msgid "" +"Show a button 'Move On Hand' in the Inventory Dashboard to initiate the " +"process to move the products in stock at the origin location." +msgstr "" + +#. module: stock_move_location +#: model:ir.model,name:stock_move_location.model_stock_move +msgid "Stock Move" +msgstr "Lagerbewegung" + +#. module: stock_move_location +#: model_terms:ir.ui.view,arch_db:stock_move_location.view_wiz_stock_move_location_form_stock_move_location +msgid "UoM" +msgstr "Einheit" + +#. module: stock_move_location +#: model:ir.model.fields,help:stock_move_location.field_stock_move__location_move +msgid "Whether this move is a part of stock_location moves" +msgstr "Ist diese Bewegung Teil der Lagerortbewegungen" + +#. module: stock_move_location +#: model:ir.model,name:stock_move_location.model_wiz_stock_move_location +#, fuzzy +msgid "Wizard move location" +msgstr "Teil der Lagerortbewegung" + +#. module: stock_move_location +#: model:ir.model,name:stock_move_location.model_wiz_stock_move_location_line +#, fuzzy +msgid "Wizard move location line" +msgstr "Lagerort Buchungszeilen" + +#. module: stock_move_location +#: model:ir.model.fields,help:stock_move_location.field_wiz_stock_move_location__destination_location_disable +msgid "technical field to disable the edition of destination location." +msgstr "" + +#. module: stock_move_location +#: model:ir.model.fields,help:stock_move_location.field_wiz_stock_move_location__origin_location_disable +msgid "technical field to disable the edition of origin location." +msgstr "" + +#~ msgid "Add all" +#~ msgstr "Alle hinzufügen" + +#~ msgid "Clear all" +#~ msgstr "Alle entfernen" + +#~ msgid "wiz.stock.move.location" +#~ msgstr "wiz.stock.move.location" + +#~ msgid "wiz.stock.move.location.line" +#~ msgstr "wiz.stock.move.location.line" diff --git a/stock_move_location/i18n/es.po b/stock_move_location/i18n/es.po new file mode 100644 index 000000000..2feee6714 --- /dev/null +++ b/stock_move_location/i18n/es.po @@ -0,0 +1,250 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * stock_move_location +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 11.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2020-11-02 20:30+0000\n" +"PO-Revision-Date: 2020-11-02 21:37+0100\n" +"Last-Translator: Sergio Teruel \n" +"Language-Team: none\n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Poedit 2.3\n" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location__apply_putaway_strategy +msgid "Apply putaway strategy" +msgstr "Aplicar estrategia de traslado" + +#. module: stock_move_location +#: model_terms:ir.ui.view,arch_db:stock_move_location.view_wiz_stock_move_location_form_stock_move_location +msgid "Apply putaway strategy for moving products" +msgstr "Aplicar estrategia de traslado para los movimientos de producto" + +#. module: stock_move_location +#: model_terms:ir.ui.view,arch_db:stock_move_location.view_wiz_stock_move_location_form_stock_move_location +msgid "Cancel" +msgstr "Cancelar" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location__picking_id +msgid "Connected Picking" +msgstr "Operación relacionada" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location__create_uid +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location_line__create_uid +msgid "Created by" +msgstr "Creado por" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location__create_date +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location_line__create_date +msgid "Created on" +msgstr "Creado el" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location_line__custom +msgid "Custom line" +msgstr "Línea personalizada" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location__destination_location_id +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location_line__destination_location_id +msgid "Destination Location" +msgstr "Ubicación de destino" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location__destination_location_disable +msgid "Destination Location Disable" +msgstr "Ubicación de destino desactivada" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location__display_name +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location_line__display_name +msgid "Display Name" +msgstr "Nombre mostrado" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location__edit_locations +#: model_terms:ir.ui.view,arch_db:stock_move_location.view_wiz_stock_move_location_form_stock_move_location +msgid "Edit Locations" +msgstr "(editar)" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location__id +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location_line__id +msgid "ID" +msgstr "ID" + +#. module: stock_move_location +#: model_terms:ir.ui.view,arch_db:stock_move_location.view_wiz_stock_move_location_form_stock_move_location +msgid "Immediate Transfer" +msgstr "Transferencia Inmediata" + +#. module: stock_move_location +#: model_terms:ir.ui.view,arch_db:stock_move_location.view_wiz_stock_move_location_form_stock_move_location +msgid "Inventory Details" +msgstr "Detalles de Inventario" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location____last_update +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location_line____last_update +msgid "Last Modified on" +msgstr "Última modificación en" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location__write_uid +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location_line__write_uid +msgid "Last Updated by" +msgstr "Última actualización por" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location__write_date +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location_line__write_date +msgid "Last Updated on" +msgstr "Última actualización el" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location_line__lot_id +msgid "Lot/Serial Number" +msgstr "Lote/Número de serie" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location_line__max_quantity +msgid "Maximum available quantity" +msgstr "Cantidad máxima disponible" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location__stock_move_location_line_ids +msgid "Move Location lines" +msgstr "Líneas de movimiento de Ubicación" + +#. module: stock_move_location +#: model_terms:ir.ui.view,arch_db:stock_move_location.stock_picking_type_kanban +msgid "Move On Hand" +msgstr "Disponible" + +#. module: stock_move_location +#: model:ir.actions.act_window,name:stock_move_location.wiz_stock_move_location_action +#: model:ir.ui.menu,name:stock_move_location.menuitem_move_location +msgid "Move from location..." +msgstr "Mover desde ubicación..." + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location_line__move_location_wizard_id +msgid "Move location Wizard" +msgstr "Asistente para mover desde ubicación" + +#. module: stock_move_location +#: code:addons/stock_move_location/wizard/stock_move_location_line.py:0 +#, python-format +msgid "Move quantity can not exceed max quantity or be negative" +msgstr "La cantidad movida no puede superar la cantidad máxima o ser negativo" + +#. module: stock_move_location +#: model:ir.actions.act_window,name:stock_move_location.wiz_stock_quant_location_action +msgid "Move to location..." +msgstr "Mover a ubicación..." + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location__origin_location_id +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location_line__origin_location_id +msgid "Origin Location" +msgstr "Ubicación de origen" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location__origin_location_disable +msgid "Origin Location Disable" +msgstr "Ubicación origen desactivada" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_stock_move__location_move +msgid "Part of move location" +msgstr "Parte de un movimiento entre ubicaciones" + +#. module: stock_move_location +#: model:ir.model,name:stock_move_location.model_stock_picking_type +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location__picking_type_id +msgid "Picking Type" +msgstr "Tipo de operación" + +#. module: stock_move_location +#: model_terms:ir.ui.view,arch_db:stock_move_location.view_wiz_stock_move_location_form_stock_move_location +msgid "Planned Transfer" +msgstr "Transferencia Planificada" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location_line__product_id +msgid "Product" +msgstr "Producto" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location_line__product_uom_id +msgid "Product Unit of Measure" +msgstr "Unidad de medida" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location_line__move_quantity +msgid "Quantity to move" +msgstr "Cantidad a mover" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location_line__reserved_quantity +msgid "Reserved quantity" +msgstr "Cantidad Reservada" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_stock_picking_type__show_move_onhand +msgid "Show Move On hand stock" +msgstr "Mostrar cantidad disponible" + +#. module: stock_move_location +#: model:ir.model.fields,help:stock_move_location.field_stock_picking_type__show_move_onhand +msgid "" +"Show a button 'Move On Hand' in the Inventory Dashboard to initiate the " +"process to move the products in stock at the origin location." +msgstr "" + +#. module: stock_move_location +#: model:ir.model,name:stock_move_location.model_stock_move +msgid "Stock Move" +msgstr "Movimiento de existencias" + +#. module: stock_move_location +#: model_terms:ir.ui.view,arch_db:stock_move_location.view_wiz_stock_move_location_form_stock_move_location +msgid "UoM" +msgstr "UdM" + +#. module: stock_move_location +#: model:ir.model.fields,help:stock_move_location.field_stock_move__location_move +msgid "Whether this move is a part of stock_location moves" +msgstr "Si este movimiento es parte de movimiento de ubicaciones" + +#. module: stock_move_location +#: model:ir.model,name:stock_move_location.model_wiz_stock_move_location +#, fuzzy +msgid "Wizard move location" +msgstr "Asistente para mover desde ubicación" + +#. module: stock_move_location +#: model:ir.model,name:stock_move_location.model_wiz_stock_move_location_line +#, fuzzy +msgid "Wizard move location line" +msgstr "Asistente para mover desde ubicación" + +#. module: stock_move_location +#: model:ir.model.fields,help:stock_move_location.field_wiz_stock_move_location__destination_location_disable +msgid "technical field to disable the edition of destination location." +msgstr "" + +#. module: stock_move_location +#: model:ir.model.fields,help:stock_move_location.field_wiz_stock_move_location__origin_location_disable +msgid "technical field to disable the edition of origin location." +msgstr "" diff --git a/stock_move_location/i18n/stock_move_location.pot b/stock_move_location/i18n/stock_move_location.pot new file mode 100644 index 000000000..bf553833e --- /dev/null +++ b/stock_move_location/i18n/stock_move_location.pot @@ -0,0 +1,244 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * stock_move_location +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 13.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_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location__apply_putaway_strategy +msgid "Apply putaway strategy" +msgstr "" + +#. module: stock_move_location +#: model_terms:ir.ui.view,arch_db:stock_move_location.view_wiz_stock_move_location_form_stock_move_location +msgid "Apply putaway strategy for moving products" +msgstr "" + +#. module: stock_move_location +#: model_terms:ir.ui.view,arch_db:stock_move_location.view_wiz_stock_move_location_form_stock_move_location +msgid "Cancel" +msgstr "" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location__picking_id +msgid "Connected Picking" +msgstr "" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location__create_uid +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location_line__create_uid +msgid "Created by" +msgstr "" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location__create_date +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location_line__create_date +msgid "Created on" +msgstr "" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location_line__custom +msgid "Custom line" +msgstr "" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location__destination_location_id +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location_line__destination_location_id +msgid "Destination Location" +msgstr "" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location__destination_location_disable +msgid "Destination Location Disable" +msgstr "" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location__display_name +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location_line__display_name +msgid "Display Name" +msgstr "" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location__edit_locations +#: model_terms:ir.ui.view,arch_db:stock_move_location.view_wiz_stock_move_location_form_stock_move_location +msgid "Edit Locations" +msgstr "" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location__id +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location_line__id +msgid "ID" +msgstr "" + +#. module: stock_move_location +#: model_terms:ir.ui.view,arch_db:stock_move_location.view_wiz_stock_move_location_form_stock_move_location +msgid "Immediate Transfer" +msgstr "" + +#. module: stock_move_location +#: model_terms:ir.ui.view,arch_db:stock_move_location.view_wiz_stock_move_location_form_stock_move_location +msgid "Inventory Details" +msgstr "" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location____last_update +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location_line____last_update +msgid "Last Modified on" +msgstr "" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location__write_uid +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location_line__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location__write_date +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location_line__write_date +msgid "Last Updated on" +msgstr "" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location_line__lot_id +msgid "Lot/Serial Number" +msgstr "" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location_line__max_quantity +msgid "Maximum available quantity" +msgstr "" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location__stock_move_location_line_ids +msgid "Move Location lines" +msgstr "" + +#. module: stock_move_location +#: model_terms:ir.ui.view,arch_db:stock_move_location.stock_picking_type_kanban +msgid "Move On Hand" +msgstr "" + +#. module: stock_move_location +#: model:ir.actions.act_window,name:stock_move_location.wiz_stock_move_location_action +#: model:ir.ui.menu,name:stock_move_location.menuitem_move_location +msgid "Move from location..." +msgstr "" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location_line__move_location_wizard_id +msgid "Move location Wizard" +msgstr "" + +#. module: stock_move_location +#: code:addons/stock_move_location/wizard/stock_move_location_line.py:0 +#, python-format +msgid "Move quantity can not exceed max quantity or be negative" +msgstr "" + +#. module: stock_move_location +#: model:ir.actions.act_window,name:stock_move_location.wiz_stock_quant_location_action +msgid "Move to location..." +msgstr "" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location__origin_location_id +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location_line__origin_location_id +msgid "Origin Location" +msgstr "" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location__origin_location_disable +msgid "Origin Location Disable" +msgstr "" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_stock_move__location_move +msgid "Part of move location" +msgstr "" + +#. module: stock_move_location +#: model:ir.model,name:stock_move_location.model_stock_picking_type +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location__picking_type_id +msgid "Picking Type" +msgstr "" + +#. module: stock_move_location +#: model_terms:ir.ui.view,arch_db:stock_move_location.view_wiz_stock_move_location_form_stock_move_location +msgid "Planned Transfer" +msgstr "" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location_line__product_id +msgid "Product" +msgstr "" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location_line__product_uom_id +msgid "Product Unit of Measure" +msgstr "" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location_line__move_quantity +msgid "Quantity to move" +msgstr "" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_wiz_stock_move_location_line__reserved_quantity +msgid "Reserved quantity" +msgstr "" + +#. module: stock_move_location +#: model:ir.model.fields,field_description:stock_move_location.field_stock_picking_type__show_move_onhand +msgid "Show Move On hand stock" +msgstr "" + +#. module: stock_move_location +#: model:ir.model.fields,help:stock_move_location.field_stock_picking_type__show_move_onhand +msgid "" +"Show a button 'Move On Hand' in the Inventory Dashboard to initiate the " +"process to move the products in stock at the origin location." +msgstr "" + +#. module: stock_move_location +#: model:ir.model,name:stock_move_location.model_stock_move +msgid "Stock Move" +msgstr "" + +#. module: stock_move_location +#: model_terms:ir.ui.view,arch_db:stock_move_location.view_wiz_stock_move_location_form_stock_move_location +msgid "UoM" +msgstr "" + +#. module: stock_move_location +#: model:ir.model.fields,help:stock_move_location.field_stock_move__location_move +msgid "Whether this move is a part of stock_location moves" +msgstr "" + +#. module: stock_move_location +#: model:ir.model,name:stock_move_location.model_wiz_stock_move_location +msgid "Wizard move location" +msgstr "" + +#. module: stock_move_location +#: model:ir.model,name:stock_move_location.model_wiz_stock_move_location_line +msgid "Wizard move location line" +msgstr "" + +#. module: stock_move_location +#: model:ir.model.fields,help:stock_move_location.field_wiz_stock_move_location__destination_location_disable +msgid "technical field to disable the edition of destination location." +msgstr "" + +#. module: stock_move_location +#: model:ir.model.fields,help:stock_move_location.field_wiz_stock_move_location__origin_location_disable +msgid "technical field to disable the edition of origin location." +msgstr "" diff --git a/stock_move_location/init_hook.py b/stock_move_location/init_hook.py new file mode 100644 index 000000000..0402025ba --- /dev/null +++ b/stock_move_location/init_hook.py @@ -0,0 +1,10 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import SUPERUSER_ID, api + + +def enable_multi_locations(cr, registry): + env = api.Environment(cr, SUPERUSER_ID, {}) + ResConfig = env["res.config.settings"] + default_values = ResConfig.default_get(list(ResConfig.fields_get())) + default_values.update({"group_stock_multi_locations": True}) + ResConfig.create(default_values).execute() diff --git a/stock_move_location/models/__init__.py b/stock_move_location/models/__init__.py new file mode 100644 index 000000000..58ff9b56b --- /dev/null +++ b/stock_move_location/models/__init__.py @@ -0,0 +1,5 @@ +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from . import stock_move +from . import stock_picking_type diff --git a/stock_move_location/models/stock_move.py b/stock_move_location/models/stock_move.py new file mode 100644 index 000000000..437c679a5 --- /dev/null +++ b/stock_move_location/models/stock_move.py @@ -0,0 +1,19 @@ +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import api, fields, models + + +class StockMove(models.Model): + _inherit = "stock.move" + + location_move = fields.Boolean( + string="Part of move location", + help="Whether this move is a part of stock_location moves", + ) + + @api.depends("location_move") + def _compute_show_details_visible(self): + super()._compute_show_details_visible() + for move in self.filtered(lambda x: x.location_move): + move.show_details_visible = True diff --git a/stock_move_location/models/stock_picking_type.py b/stock_move_location/models/stock_picking_type.py new file mode 100644 index 000000000..0bb437fd0 --- /dev/null +++ b/stock_move_location/models/stock_picking_type.py @@ -0,0 +1,26 @@ +# Copyright 2019 Sergio Teruel +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from odoo import fields, models + + +class StockPickingType(models.Model): + _inherit = "stock.picking.type" + + show_move_onhand = fields.Boolean( + string="Show Move On hand stock", + help="Show a button 'Move On Hand' in the Inventory Dashboard " + "to initiate the process to move the products in stock " + "at the origin location.", + ) + + def action_move_location(self): + action = self.env.ref( + "stock_move_location.wiz_stock_move_location_action" + ).read()[0] + action["context"] = { + "default_origin_location_id": self.default_location_src_id.id, + "default_destination_location_id": self.default_location_dest_id.id, + "default_picking_type_id": self.id, + "default_edit_locations": False, + } + return action diff --git a/stock_move_location/readme/CONTRIBUTORS.rst b/stock_move_location/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..d20d856f0 --- /dev/null +++ b/stock_move_location/readme/CONTRIBUTORS.rst @@ -0,0 +1,10 @@ +* Mathieu Vatel +* Mykhailo Panarin +* Joan Sisquella +* Jordi Ballester Alomar +* Lois Rilo +* Héctor Villarreal +* Tecnativa + + * Sergio Teruel + * João Marques diff --git a/stock_move_location/readme/DESCRIPTION.rst b/stock_move_location/readme/DESCRIPTION.rst new file mode 100644 index 000000000..40d1bdefc --- /dev/null +++ b/stock_move_location/readme/DESCRIPTION.rst @@ -0,0 +1,2 @@ +This module allows to move entire location of products from one place to +another and move only selected quantities. diff --git a/stock_move_location/readme/ROADMAP.rst b/stock_move_location/readme/ROADMAP.rst new file mode 100644 index 000000000..0b83df767 --- /dev/null +++ b/stock_move_location/readme/ROADMAP.rst @@ -0,0 +1,6 @@ +Change the current implementation (suggested by Denis Roussel from ACSONE): + +* A new parameter on stock picking types : 'Product Change Location' (with a little help). +* With this, go to the dashboard, create a picking with that type. +* Add a button on the picking form which is visible with that type that fill in the picking as now +* Nice to have: add a magic button on locations that with context creates a new picking of that type with the origin location already filled in. diff --git a/stock_move_location/readme/USAGE.rst b/stock_move_location/readme/USAGE.rst new file mode 100644 index 000000000..53763e43d --- /dev/null +++ b/stock_move_location/readme/USAGE.rst @@ -0,0 +1,22 @@ +* A new menu item Stock > Move from location... opens a wizard + where 2 locations can be specified. +* Select origin and destination locations and press "IMMEDIATE TRANSFER" or "PLANNED TRANSFER" +* Press `ADD ALL` button to add all products available +* Those lines can be edited. Move quantity can't be more than a max available quantity +* Move doesn't care about the reservations and will move stuff anyway +* If during your operation with the wizard the real quantity will change + it will move only the available quantity at the button press +* Products will be moved and a form view of picking that did that will show up +* If "PLANNED TRANSFER" is used - the picking won't be validated automatically + +If you want to transfer a full quant: + +* Go to `Inventory > Master Data > Products` and click "On hand" smart button + or `Inventory > Reporting > Inventory`, the quants view will be + opened. + +* Select the quantities which you want move to another location + +If you go to the Inventory Dashboard you can see the button "Move from location" +in each of the picking types (only applicable to internal transfers). Press it +and you will be directed to the wizard. diff --git a/stock_move_location/security/ir.model.access.csv b/stock_move_location/security/ir.model.access.csv new file mode 100644 index 000000000..b41b028f1 --- /dev/null +++ b/stock_move_location/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_wiz_stock_move_location,access wiz.stock.move.location,model_wiz_stock_move_location,stock.group_stock_user,1,1,1,1 +access_wiz_stock_move_location_line,access wiz.stock.move.location.line,model_wiz_stock_move_location_line,stock.group_stock_user,1,1,1,1 diff --git a/stock_move_location/static/description/icon.png b/stock_move_location/static/description/icon.png new file mode 100644 index 000000000..3a0328b51 Binary files /dev/null and b/stock_move_location/static/description/icon.png differ diff --git a/stock_move_location/static/description/index.html b/stock_move_location/static/description/index.html new file mode 100644 index 000000000..eda83735c --- /dev/null +++ b/stock_move_location/static/description/index.html @@ -0,0 +1,467 @@ + + + + + + +Move Stock Location + + + +
+

Move Stock Location

+ + +

Beta License: AGPL-3 OCA/stock-logistics-warehouse Translate me on Weblate Try me on Runbot

+

This module allows to move entire location of products from one place to +another and move only selected quantities.

+

Table of contents

+ +
+

Usage

+
    +
  • A new menu item Stock > Move from location… opens a wizard +where 2 locations can be specified.
  • +
  • Select origin and destination locations and press “IMMEDIATE TRANSFER” or “PLANNED TRANSFER”
  • +
  • Press ADD ALL button to add all products available
  • +
  • Those lines can be edited. Move quantity can’t be more than a max available quantity
  • +
  • Move doesn’t care about the reservations and will move stuff anyway
  • +
  • If during your operation with the wizard the real quantity will change +it will move only the available quantity at the button press
  • +
  • Products will be moved and a form view of picking that did that will show up
  • +
  • If “PLANNED TRANSFER” is used - the picking won’t be validated automatically
  • +
+

If you want to transfer a full quant:

+
    +
  • Go to Inventory > Master Data > Products and click “On hand” smart button +or Inventory > Reporting > Inventory, the quants view will be +opened.
  • +
  • Select the quantities which you want move to another location
  • +
+

If you go to the Inventory Dashboard you can see the button “Move from location” +in each of the picking types (only applicable to internal transfers). Press it +and you will be directed to the wizard.

+
+
+

Known issues / Roadmap

+

Change the current implementation (suggested by Denis Roussel from ACSONE):

+
    +
  • A new parameter on stock picking types : ‘Product Change Location’ (with a little help).
  • +
  • With this, go to the dashboard, create a picking with that type.
  • +
  • Add a button on the picking form which is visible with that type that fill in the picking as now
  • +
  • Nice to have: add a magic button on locations that with context creates a new picking of that type with the origin location already filled in.
  • +
+
+
+

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 smashing it by providing a detailed and welcomed +feedback.

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • Julius Network Solutions
  • +
+
+
+

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/stock-logistics-warehouse project on GitHub.

+

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

+
+
+
+ + diff --git a/stock_move_location/tests/__init__.py b/stock_move_location/tests/__init__.py new file mode 100644 index 000000000..00527a8b0 --- /dev/null +++ b/stock_move_location/tests/__init__.py @@ -0,0 +1,6 @@ +# Copyright (C) 2011 Julius Network Solutions SARL +# Copyright 2018 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from . import test_common +from . import test_move_location diff --git a/stock_move_location/tests/test_common.py b/stock_move_location/tests/test_common.py new file mode 100644 index 000000000..8877f2537 --- /dev/null +++ b/stock_move_location/tests/test_common.py @@ -0,0 +1,115 @@ +# Copyright (C) 2011 Julius Network Solutions SARL +# Copyright 2018 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo.tests import common + + +class TestsCommon(common.SavepointCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls.location_obj = cls.env["stock.location"] + product_obj = cls.env["product.product"] + cls.wizard_obj = cls.env["wiz.stock.move.location"] + cls.quant_obj = cls.env["stock.quant"] + cls.company = cls.env.ref("base.main_company") + + cls.internal_loc_1 = cls.location_obj.create( + { + "name": "INT_1", + "usage": "internal", + "active": True, + "company_id": cls.company.id, + } + ) + cls.internal_loc_2 = cls.location_obj.create( + { + "name": "INT_2", + "usage": "internal", + "active": True, + "company_id": cls.company.id, + } + ) + cls.internal_loc_2_shelf = cls.location_obj.create( + { + "name": "Shelf", + "usage": "internal", + "active": True, + "company_id": cls.company.id, + "location_id": cls.internal_loc_2.id, + } + ) + cls.uom_unit = cls.env.ref("uom.product_uom_unit") + cls.product_no_lots = product_obj.create( + {"name": "Pineapple", "type": "product", "tracking": "none"} + ) + cls.product_lots = product_obj.create( + {"name": "Apple", "type": "product", "tracking": "lot"} + ) + cls.lot1 = cls.env["stock.production.lot"].create( + { + "name": "lot1", + "product_id": cls.product_lots.id, + "company_id": cls.company.id, + } + ) + cls.lot2 = cls.env["stock.production.lot"].create( + { + "name": "lot2", + "product_id": cls.product_lots.id, + "company_id": cls.company.id, + } + ) + cls.lot3 = cls.env["stock.production.lot"].create( + { + "name": "lot3", + "product_id": cls.product_lots.id, + "company_id": cls.company.id, + } + ) + + def setup_product_amounts(self): + self.set_product_amount(self.product_no_lots, self.internal_loc_1, 123) + self.set_product_amount( + self.product_lots, self.internal_loc_1, 1.0, lot_id=self.lot1 + ) + self.set_product_amount( + self.product_lots, self.internal_loc_1, 1.0, lot_id=self.lot2 + ) + self.set_product_amount( + self.product_lots, self.internal_loc_1, 1.0, lot_id=self.lot3 + ) + + def set_product_amount(self, product, location, amount, lot_id=None): + self.env["stock.quant"]._update_available_quantity( + product, location, amount, lot_id=lot_id + ) + + def check_product_amount(self, product, location, amount, lot_id=None): + self.assertEqual( + self.env["stock.quant"]._get_available_quantity( + product, location, lot_id=lot_id + ), + amount, + ) + + def _create_wizard(self, origin_location, destination_location): + move_location_wizard = self.env["wiz.stock.move.location"] + return move_location_wizard.create( + { + "origin_location_id": origin_location.id, + "destination_location_id": destination_location.id, + } + ) + + def _create_putaway_for_product(self, product, loc_in, loc_out): + putaway = self.env["stock.putaway.rule"].create( + { + "product_id": product.id, + "location_in_id": loc_in.id, + "location_out_id": loc_out.id, + } + ) + loc_in.write({"putaway_rule_ids": [(4, putaway.id, 0)]}) diff --git a/stock_move_location/tests/test_move_location.py b/stock_move_location/tests/test_move_location.py new file mode 100644 index 000000000..357cce30d --- /dev/null +++ b/stock_move_location/tests/test_move_location.py @@ -0,0 +1,167 @@ +# Copyright (C) 2011 Julius Network Solutions SARL +# Copyright 2018 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo.exceptions import ValidationError + +from .test_common import TestsCommon + + +class TestMoveLocation(TestsCommon): + def setUp(self): + super().setUp() + self.setup_product_amounts() + + def test_move_location_wizard(self): + """Test a simple move.""" + wizard = self._create_wizard(self.internal_loc_1, self.internal_loc_2) + wizard.onchange_origin_location() + wizard.action_move_location() + self.check_product_amount(self.product_no_lots, self.internal_loc_1, 0) + self.check_product_amount(self.product_lots, self.internal_loc_1, 0, self.lot1) + self.check_product_amount(self.product_lots, self.internal_loc_1, 0, self.lot2) + self.check_product_amount(self.product_lots, self.internal_loc_1, 0, self.lot3) + self.check_product_amount(self.product_no_lots, self.internal_loc_2, 123) + self.check_product_amount(self.product_lots, self.internal_loc_2, 1, self.lot1) + self.check_product_amount(self.product_lots, self.internal_loc_2, 1, self.lot2) + self.check_product_amount(self.product_lots, self.internal_loc_2, 1, self.lot3) + + def test_move_location_wizard_amount(self): + """Can't move more than exists.""" + wizard = self._create_wizard(self.internal_loc_1, self.internal_loc_2) + wizard.onchange_origin_location() + with self.assertRaises(ValidationError): + wizard.stock_move_location_line_ids[0].move_quantity += 1 + + def test_move_location_wizard_ignore_reserved(self): + """Can't move more than exists.""" + wizard = self._create_wizard(self.internal_loc_1, self.internal_loc_2) + wizard.onchange_origin_location() + # reserve some quants + self.quant_obj._update_reserved_quantity( + self.product_no_lots, self.internal_loc_1, 50 + ) + self.quant_obj._update_reserved_quantity( + self.product_lots, self.internal_loc_1, 1, lot_id=self.lot1 + ) + # doesn't care about reservations, everything is moved + wizard.action_move_location() + self.check_product_amount(self.product_no_lots, self.internal_loc_1, 0) + self.check_product_amount(self.product_no_lots, self.internal_loc_2, 123) + self.check_product_amount(self.product_lots, self.internal_loc_2, 1, self.lot1) + + def test_wizard_clear_lines(self): + """Test lines getting cleared properly.""" + wizard = self._create_wizard(self.internal_loc_1, self.internal_loc_2) + wizard.onchange_origin_location() + self.assertEqual(len(wizard.stock_move_location_line_ids), 4) + wizard._onchange_destination_location_id() + self.assertEqual(len(wizard.stock_move_location_line_ids), 4) + dest_location_line = wizard.stock_move_location_line_ids.mapped( + "destination_location_id" + ) + self.assertEqual(dest_location_line, wizard.destination_location_id) + wizard._onchange_origin_location_id() + self.assertEqual(len(wizard.stock_move_location_line_ids), 0) + + def test_planned_transfer(self): + """Test planned transfer.""" + wizard = self._create_wizard(self.internal_loc_1, self.internal_loc_2) + wizard.onchange_origin_location() + wizard = wizard.with_context({"planned": True}) + wizard.action_move_location() + picking = wizard.picking_id + self.assertEqual(picking.state, "assigned") + self.assertEqual(len(picking.move_line_ids), 4) + self.assertEqual( + sorted(picking.move_line_ids.mapped("product_uom_qty")), [1, 1, 1, 123] + ) + + def test_quant_transfer(self): + """Test quants transfer.""" + quants = self.product_lots.stock_quant_ids + wizard = self.wizard_obj.with_context( + active_model="stock.quant", + active_ids=quants.ids, + origin_location_disable=True, + ).create( + { + "origin_location_id": quants[:1].location_id.id, + "destination_location_id": self.internal_loc_2.id, + } + ) + lines = wizard.stock_move_location_line_ids + self.assertEqual(len(lines), 3) + wizard.onchange_origin_location() + self.assertEqual(len(lines), 3) + wizard.destination_location_id = self.internal_loc_1 + wizard._onchange_destination_location_id() + self.assertEqual(lines.mapped("destination_location_id"), self.internal_loc_1) + wizard.origin_location_id = self.internal_loc_2 + wizard._onchange_destination_location_id() + self.assertEqual(len(lines), 3) + + def test_readonly_location_computation(self): + """Test that origin_location_disable and destination_location_disable + are computed correctly.""" + wizard = self._create_wizard(self.internal_loc_1, self.internal_loc_2) + # locations are editable. + self.assertFalse(wizard.origin_location_disable) + self.assertFalse(wizard.destination_location_disable) + # Disable edit mode: + wizard.edit_locations = False + self.assertTrue(wizard.origin_location_disable) + self.assertTrue(wizard.destination_location_disable) + + def test_picking_type_action_dummy(self): + """Test that no error is raised from actions.""" + pick_type = self.env.ref("stock.picking_type_internal") + pick_type.action_move_location() + + def test_wizard_with_putaway_strategy(self): + """Test that Putaway strategies are being applied.""" + self._create_putaway_for_product( + self.product_no_lots, self.internal_loc_2, self.internal_loc_2_shelf + ) + wizard = self._create_wizard(self.internal_loc_1, self.internal_loc_2) + wizard.apply_putaway_strategy = True + wizard.onchange_origin_location() + putaway_line = wizard.stock_move_location_line_ids.filtered( + lambda p: p.product_id == self.product_no_lots + )[0] + self.assertEqual( + putaway_line.destination_location_id, self.internal_loc_2_shelf + ) + + def test_inmediate_transfer_reserved_quantity(self): + """ + Unreserve quantities in old location and reserve the same items on + new location + """ + # Create some quants + self.set_product_amount( + self.product_lots, self.internal_loc_1, 100, lot_id=self.lot1 + ) + # Reserve some quantities + stock_move = self.env["stock.move"].create( + { + "name": "Move for test", + "product_id": self.product_lots.id, + "product_uom_qty": 20.0, + "product_uom": self.product_lots.uom_id.id, + "location_id": self.internal_loc_1.id, + "location_dest_id": self.internal_loc_2.id, + } + ) + stock_move._action_confirm() + stock_move._action_assign() + # Move all quantities to other location + wizard = self._create_wizard(self.internal_loc_1, self.internal_loc_2_shelf) + wizard.onchange_origin_location() + wizard.action_move_location() + # The old reserved quantities must be in new location after confirm wizard + self.assertEqual(len(stock_move.move_line_ids), 1) + self.assertEqual(stock_move.move_line_ids.product_uom_qty, 20.0) + self.assertEqual( + stock_move.move_line_ids.location_id, self.internal_loc_2_shelf + ) diff --git a/stock_move_location/views/stock_picking_type_views.xml b/stock_move_location/views/stock_picking_type_views.xml new file mode 100644 index 000000000..ed0a6d6db --- /dev/null +++ b/stock_move_location/views/stock_picking_type_views.xml @@ -0,0 +1,37 @@ + + + + Operation Types + stock.picking.type + + + + + + + + + stock.picking.type + + + + + + +
+ +
+
+
+
+
diff --git a/stock_move_location/wizard/__init__.py b/stock_move_location/wizard/__init__.py new file mode 100644 index 000000000..d9fdbf21c --- /dev/null +++ b/stock_move_location/wizard/__init__.py @@ -0,0 +1,6 @@ +# Copyright (C) 2011 Julius Network Solutions SARL +# Copyright 2018 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from . import stock_move_location +from . import stock_move_location_line diff --git a/stock_move_location/wizard/stock_move_location.py b/stock_move_location/wizard/stock_move_location.py new file mode 100644 index 000000000..f6461ed59 --- /dev/null +++ b/stock_move_location/wizard/stock_move_location.py @@ -0,0 +1,297 @@ +# Copyright (C) 2011 Julius Network Solutions SARL +# Copyright 2018 Camptocamp SA +# Copyright 2019 Sergio Teruel - Tecnativa +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import api, fields, models +from odoo.fields import first + + +class StockMoveLocationWizard(models.TransientModel): + _name = "wiz.stock.move.location" + _description = "Wizard move location" + + def _get_default_picking_type_id(self): + company_id = self.env.context.get("company_id") or self.env.user.company_id.id + return ( + self.env["stock.picking.type"] + .search( + [ + ("code", "=", "internal"), + ("warehouse_id.company_id", "=", company_id), + ], + limit=1, + ) + .id + ) + + origin_location_disable = fields.Boolean( + compute="_compute_readonly_locations", + help="technical field to disable the edition of origin location.", + ) + origin_location_id = fields.Many2one( + string="Origin Location", + comodel_name="stock.location", + required=True, + domain=lambda self: self._get_locations_domain(), + ) + destination_location_disable = fields.Boolean( + compute="_compute_readonly_locations", + help="technical field to disable the edition of destination location.", + ) + destination_location_id = fields.Many2one( + string="Destination Location", + comodel_name="stock.location", + required=True, + domain=lambda self: self._get_locations_domain(), + ) + stock_move_location_line_ids = fields.Many2many( + string="Move Location lines", + comodel_name="wiz.stock.move.location.line", + column1="move_location_wiz_id", + column2="move_location_line_wiz_id", + ) + picking_type_id = fields.Many2one( + comodel_name="stock.picking.type", default=_get_default_picking_type_id + ) + picking_id = fields.Many2one( + string="Connected Picking", comodel_name="stock.picking" + ) + edit_locations = fields.Boolean(string="Edit Locations", default=True) + apply_putaway_strategy = fields.Boolean(string="Apply putaway strategy") + + @api.depends("edit_locations") + def _compute_readonly_locations(self): + for rec in self: + rec.origin_location_disable = self.env.context.get( + "origin_location_disable", False + ) + rec.destination_location_disable = self.env.context.get( + "destination_location_disable", False + ) + if not rec.edit_locations: + rec.origin_location_disable = True + rec.destination_location_disable = True + + @api.model + def default_get(self, fields): + res = super().default_get(fields) + if self.env.context.get("active_model", False) != "stock.quant": + return res + # Load data directly from quants + quants = self.env["stock.quant"].browse( + self.env.context.get("active_ids", False) + ) + res["stock_move_location_line_ids"] = [ + ( + 0, + 0, + { + "product_id": quant.product_id.id, + "move_quantity": quant.quantity, + "max_quantity": quant.quantity, + "reserved_quantity": quant.reserved_quantity, + "origin_location_id": quant.location_id.id, + "lot_id": quant.lot_id.id, + "product_uom_id": quant.product_uom_id.id, + "custom": False, + }, + ) + for quant in quants + ] + res["origin_location_id"] = first(quants).location_id.id + return res + + @api.onchange("origin_location_id") + def _onchange_origin_location_id(self): + if not self.env.context.get("origin_location_disable", False): + self._clear_lines() + + @api.onchange("destination_location_id") + def _onchange_destination_location_id(self): + for line in self.stock_move_location_line_ids: + line.destination_location_id = self.destination_location_id + + def _clear_lines(self): + self.stock_move_location_line_ids = False + + def _get_locations_domain(self): + return [ + "|", + ("company_id", "=", self.env.user.company_id.id), + ("company_id", "=", False), + ] + + def _create_picking(self): + return self.env["stock.picking"].create( + { + "picking_type_id": self.picking_type_id.id, + "location_id": self.origin_location_id.id, + "location_dest_id": self.destination_location_id.id, + } + ) + + def group_lines(self): + lines_grouped = {} + for line in self.stock_move_location_line_ids: + lines_grouped.setdefault( + line.product_id.id, self.env["wiz.stock.move.location.line"].browse() + ) + lines_grouped[line.product_id.id] |= line + return lines_grouped + + def _create_moves(self, picking): + self.ensure_one() + groups = self.group_lines() + moves = self.env["stock.move"] + for lines in groups.values(): + move = self._create_move(picking, lines) + moves |= move + return moves + + def _get_move_values(self, picking, lines): + # locations are same for the products + location_from_id = lines[0].origin_location_id.id + location_to_id = lines[0].destination_location_id.id + product = lines[0].product_id + product_uom_id = lines[0].product_uom_id.id + qty = sum([x.move_quantity for x in lines]) + return { + "name": product.display_name, + "location_id": location_from_id, + "location_dest_id": location_to_id, + "product_id": product.id, + "product_uom": product_uom_id, + "product_uom_qty": qty, + "picking_id": picking.id, + "location_move": True, + } + + def _create_move(self, picking, lines): + self.ensure_one() + move = self.env["stock.move"].create(self._get_move_values(picking, lines)) + if not self.env.context.get("planned"): + for line in lines: + line.create_move_lines(picking, move) + return move + + def _unreserve_moves(self): + """ + Try to unreserve moves that they has reserved quantity before user + moves products from a location to other one and change move origin + location to the new location to assign later. + :return moves unreserved + """ + moves_to_reassign = self.env["stock.move"] + lines_to_ckeck_reverve = self.stock_move_location_line_ids.filtered( + lambda l: ( + l.move_quantity > l.max_quantity - l.reserved_quantity + and not l.origin_location_id.should_bypass_reservation() + ) + ) + for line in lines_to_ckeck_reverve: + move_lines = self.env["stock.move.line"].search( + [ + ("state", "=", "assigned"), + ("product_id", "=", line.product_id.id), + ("location_id", "=", line.origin_location_id.id), + ("lot_id", "=", line.lot_id.id), + ("product_uom_qty", ">", 0.0), + ] + ) + moves_to_unreserve = move_lines.mapped("move_id") + # Unreserve in old location + moves_to_unreserve._do_unreserve() + # Change location in move with the new one + moves_to_unreserve.write({"location_id": line.destination_location_id.id}) + moves_to_reassign |= moves_to_unreserve + return moves_to_reassign + + def action_move_location(self): + self.ensure_one() + picking = self._create_picking() + self._create_moves(picking) + if not self.env.context.get("planned"): + moves_to_reassign = self._unreserve_moves() + picking.button_validate() + moves_to_reassign._action_assign() + else: + picking.action_confirm() + picking.action_assign() + self.picking_id = picking + return self._get_picking_action(picking.id) + + def _get_picking_action(self, pickinig_id): + action = self.env.ref("stock.action_picking_tree_all").read()[0] + form_view = self.env.ref("stock.view_picking_form").id + action.update( + {"view_mode": "form", "views": [(form_view, "form")], "res_id": pickinig_id} + ) + return action + + def _get_group_quants(self): + location_id = self.origin_location_id + # Using sql as search_group doesn't support aggregation functions + # leading to overhead in queries to DB + query = """ + SELECT product_id, lot_id, SUM(quantity) AS quantity, + SUM(reserved_quantity) AS reserved_quantity + FROM stock_quant + WHERE location_id = %s + GROUP BY product_id, lot_id + """ + self.env.cr.execute(query, (location_id.id,)) + return self.env.cr.dictfetchall() + + def _get_stock_move_location_lines_values(self): + product_obj = self.env["product.product"] + product_data = [] + for group in self._get_group_quants(): + product = product_obj.browse(group.get("product_id")).exists() + # Apply the putaway strategy + location_dest_id = ( + self.apply_putaway_strategy + and self.destination_location_id._get_putaway_strategy(product).id + or self.destination_location_id.id + ) + product_data.append( + { + "product_id": product.id, + "move_quantity": group.get("quantity"), + "max_quantity": group.get("quantity"), + "reserved_quantity": group.get("reserved_quantity"), + "origin_location_id": self.origin_location_id.id, + "destination_location_id": location_dest_id, + # cursor returns None instead of False + "lot_id": group.get("lot_id") or False, + "product_uom_id": product.uom_id.id, + "custom": False, + } + ) + return product_data + + @api.onchange("origin_location_id") + def onchange_origin_location(self): + # Get origin_location_disable context key to prevent load all origin + # location products when user opens the wizard from stock quants to + # move it to other location. + if ( + not self.env.context.get("origin_location_disable") + and self.origin_location_id + ): + lines = [] + line_model = self.env["wiz.stock.move.location.line"] + for line_val in self._get_stock_move_location_lines_values(): + if line_val.get("max_quantity") <= 0: + continue + line = line_model.create(line_val) + line.max_quantity = line.get_max_quantity() + line.reserved_quantity = line.reserved_quantity + lines.append(line) + self.update( + {"stock_move_location_line_ids": [(6, 0, [line.id for line in lines])]} + ) + + def clear_lines(self): + self._clear_lines() + return {"type": "ir.action.do_nothing"} diff --git a/stock_move_location/wizard/stock_move_location.xml b/stock_move_location/wizard/stock_move_location.xml new file mode 100644 index 000000000..4c022a469 --- /dev/null +++ b/stock_move_location/wizard/stock_move_location.xml @@ -0,0 +1,154 @@ + + + + wiz.stock.move.location.form.stock_move_location + wiz.stock.move.location + +
+ +
+
+ + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+
+
+ + + +
+
+
+
+
+
+
+
+
+
+ + + + + + Move from location... + wiz.stock.move.location + form + new + {} + + + diff --git a/stock_move_location/wizard/stock_move_location_line.py b/stock_move_location/wizard/stock_move_location_line.py new file mode 100644 index 000000000..527343d9d --- /dev/null +++ b/stock_move_location/wizard/stock_move_location_line.py @@ -0,0 +1,139 @@ +# Copyright (C) 2011 Julius Network Solutions SARL +# Copyright 2018 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError +from odoo.tools import float_compare + + +class StockMoveLocationWizardLine(models.TransientModel): + _name = "wiz.stock.move.location.line" + _description = "Wizard move location line" + + move_location_wizard_id = fields.Many2many( + string="Move location Wizard", + comodel_name="wiz.stock.move.location", + column1="move_location_line_wiz_id", + column2="move_location_wiz_id", + readonly=True, + ) + product_id = fields.Many2one( + string="Product", comodel_name="product.product", required=True + ) + origin_location_id = fields.Many2one( + string="Origin Location", comodel_name="stock.location" + ) + destination_location_id = fields.Many2one( + string="Destination Location", comodel_name="stock.location" + ) + product_uom_id = fields.Many2one( + string="Product Unit of Measure", comodel_name="uom.uom" + ) + lot_id = fields.Many2one( + string="Lot/Serial Number", + comodel_name="stock.production.lot", + domain="[('product_id','=',product_id)]", + ) + move_quantity = fields.Float( + string="Quantity to move", digits="Product Unit of Measure" + ) + max_quantity = fields.Float( + string="Maximum available quantity", digits="Product Unit of Measure" + ) + reserved_quantity = fields.Float( + string="Reserved quantity", digits="Product Unit of Measure" + ) + custom = fields.Boolean(string="Custom line", default=True) + + @staticmethod + def _compare(qty1, qty2, precision_rounding): + return float_compare(qty1, qty2, precision_rounding=precision_rounding) + + @api.constrains("max_quantity", "move_quantity") + def _constraint_max_move_quantity(self): + for record in self: + rounding = record.product_uom_id.rounding + move_qty_gt_max_qty = ( + self._compare(record.move_quantity, record.max_quantity, rounding) == 1 + ) + move_qty_lt_0 = self._compare(record.move_quantity, 0.0, rounding) == -1 + if move_qty_gt_max_qty or move_qty_lt_0: + raise ValidationError( + _("Move quantity can not exceed max quantity or be negative") + ) + + def get_max_quantity(self): + self.product_uom_id = self.product_id.uom_id + search_args = [ + ("location_id", "=", self.origin_location_id.id), + ("product_id", "=", self.product_id.id), + ] + if self.lot_id: + search_args.append(("lot_id", "=", self.lot_id.id)) + else: + search_args.append(("lot_id", "=", False)) + res = self.env["stock.quant"].read_group(search_args, ["quantity"], []) + max_quantity = res[0]["quantity"] + return max_quantity + + def create_move_lines(self, picking, move): + for line in self: + values = line._get_move_line_values(picking, move) + if not self.env.context.get("planned") and values.get("qty_done") <= 0: + continue + self.env["stock.move.line"].create(values) + return True + + def _get_move_line_values(self, picking, move): + self.ensure_one() + location_dest_id = ( + self.move_location_wizard_id.apply_putaway_strategy + and self.destination_location_id.get_putaway_strategy(self.product_id).id + or self.destination_location_id.id + ) + qty_todo, qty_done = self._get_available_quantity() + return { + "product_id": self.product_id.id, + "lot_id": self.lot_id.id, + "location_id": self.origin_location_id.id, + "location_dest_id": location_dest_id, + "product_uom_qty": qty_todo, + "qty_done": qty_done, + "product_uom_id": self.product_uom_id.id, + "picking_id": picking.id, + "move_id": move.id, + } + + def _get_available_quantity(self): + """We check here if the actual amount changed in the stock. + + We don't care about the reservations but we do care about not moving + more than exists.""" + self.ensure_one() + if not self.product_id: + return 0 + if self.env.context.get("planned"): + # for planned transfer we don't care about the amounts at all + return self.move_quantity, 0 + search_args = [ + ("location_id", "=", self.origin_location_id.id), + ("product_id", "=", self.product_id.id), + ] + if self.lot_id: + search_args.append(("lot_id", "=", self.lot_id.id)) + else: + search_args.append(("lot_id", "=", False)) + res = self.env["stock.quant"].read_group(search_args, ["quantity"], []) + available_qty = res[0]["quantity"] + if not available_qty: + # if it is immediate transfer and product doesn't exist in that + # location -> make the transfer of 0. + return 0 + rounding = self.product_uom_id.rounding + available_qty_lt_move_qty = ( + self._compare(available_qty, self.move_quantity, rounding) == -1 + ) + if available_qty_lt_move_qty: + return available_qty + return 0, self.move_quantity