diff --git a/stock_barcodes/README.rst b/stock_barcodes/README.rst index d7825e3dd740..4a23c5091cc8 100644 --- a/stock_barcodes/README.rst +++ b/stock_barcodes/README.rst @@ -7,7 +7,7 @@ Stock Barcodes !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:57d26158322c09cb66f30397e6644567bf9240ecc415f7fd48ec7bc6e6406ec9 + !! source digest: sha256:e7ccaa03134ef9bad1cb0e83f96ba7617e6ff97f7b09ded6510b4c5a74404a79 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png @@ -36,6 +36,10 @@ other modules. This module also makes use of this wizard for providing barcode support for doing inventories and picking operations. +This module provides configuring barcodes for barcode actions. + + + **Table of contents** .. contents:: @@ -47,13 +51,74 @@ Usage Barcode interface for inventory operations ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -To use the barcode interface on inventory: +Option 1: To use the barcode interface on inventory + + #. Go to *Inventory > operations > Inventory Adjustments*. + #. Create new inventory with "Select products manually" option. + #. Start inventory. + #. Click to "Scan barcodes" smart button. + #. Start reading barcodes. + +Option 2: Use the barcode interface inventory directly from the Barcodes application + #. Go to *Barcodes*. + #. Select the *Inventory* option. + + .. image:: https://raw.githubusercontent.com/stock_barcodes/static/src/img/inventory_barcode_action.png + :height: 100 + :width: 200 + :alt: Inventory barcode action + + #. Start scanning barcodes. + +Actions + # Press the *+ Product* button to display the form for the new item. + + .. image:: https://raw.githubusercontent.com/stock_barcodes/static/src/img/add_product.png + :height: 100 + :width: 200 + :alt: Add product + + # When you select a product, a numeric field is displayed to add the quantity. + + .. image:: https://raw.githubusercontent.com/stock_barcodes/static/src/img/form_add_product_quantity.png + :height: 100 + :width: 200 + :alt: Add quantity product + + # When you press the button with the trash can icon, the values of the form are reset (except for the location) without closing it. + + .. image:: https://raw.githubusercontent.com/stock_barcodes/static/src/img/form_add_product_reset.png + :height: 100 + :width: 200 + :alt: Reset data form + + # When you press the *Clean values* button, all fields are reset and the form is closed. + # When you press the *Confirm* button, the new item is added and the form is closed. + # When the eye icon is closed, the created items greater than zero are displayed, and if not, those less than or equal to zero. + + .. image:: https://raw.githubusercontent.com/stock_barcodes/static/src/img/list_items.png + :height: 100 + :width: 200 + :alt: Reset data form + + # In the list, the trash can icon allows you to reset the quantity to zero and the edit icon allows you to change the item values. + + .. image:: https://raw.githubusercontent.com/stock_barcodes/static/src/img/list_action_items.png + :height: 100 + :width: 200 + :alt: Reset data form + + # The *Apply* button is only displayed if there are items with quantities greater than zero, regardless of whether they were scanned or entered manually; If you press all the defined quantities will be processed after defining the reason for the inventory adjustment and then the main barcode menu will be displayed. + + .. image:: https://raw.githubusercontent.com/stock_barcodes/static/src/img/apply_inventory.png + :height: 100 + :width: 200 + :alt: Apply inventory + .. image:: https://raw.githubusercontent.com/stock_barcodes/static/src/img/apply_inventory_reason.png + :height: 100 + :width: 200 + :alt: Apply inventory reason -#. Go to *Inventory > operations > Inventory Adjustments*. -#. Create new inventory with "Select products manually" option. -#. Start inventory. -#. Click to "Scan barcodes" smart button. -#. Start reading barcodes. Barcode interface for picking operations ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -67,11 +132,109 @@ To use the barcode interface on picking operations: #. Click on scanner button on any operation type. #. Start reading barcodes. -To use the barcode interface on a picking: +Option 1: To use the barcode interface on a picking: -#. Go to *Inventory > Transfers*. -#. Click to "Scan barcodes" smart button. -#. Start reading barcodes. + #. Go to *Inventory > Transfers*. + #. Click to "Scan barcodes" smart button. + #. Start reading barcodes. + +Option 2: Use the barcode interface picking directly from the Barcodes application + #. Go to *Barcodes*. + #. Select the option *OPERATIONS*. + + .. image:: https://raw.githubusercontent.com/stock_barcodes/static/src/img/inventory_barcode_action.png + :height: 100 + :width: 200 + :alt: Operation barcode action + + # Select the type of picking. + # The pickings in ready status are displayed, select the one you want to start scanning. + + .. image:: https://raw.githubusercontent.com/stock_barcodes/static/src/img/list_picking.png + :height: 100 + :width: 200 + :alt: List picking + + #. Start scanning barcodes. + + .. image:: https://raw.githubusercontent.com/stock_barcodes/static/src/img/barcode_interface_picking.png + :height: 100 + :width: 200 + :alt: List picking + +Actions + # All the items that have been configured for the selected picking are listed. + + .. image:: https://raw.githubusercontent.com/stock_barcodes/static/src/img/list_items_picking.png + :height: 100 + :width: 200 + :alt: List picking + + # The edit icon in the list allows you to modify the data. + + .. image:: https://raw.githubusercontent.com/stock_barcodes/static/src/img/list_items_picking_edit.png + :height: 100 + :width: 200 + :alt: Edit picking + + # The button that contains a *+120* (in this case), allows you to define all the + remaining quantities. Once defined, this button disappears and if you want to change the + quantities, press the edit button. + + .. image:: https://raw.githubusercontent.com/stock_barcodes/static/src/img/list_items_picking_quantity.png + :height: 100 + :width: 200 + :alt: Quantity picking + + # If there is at least one item with a quantity already defined, an eye icon is displayed, + which if closed shows the items and their quantities already scanned. + + .. image:: https://raw.githubusercontent.com/stock_barcodes/static/src/img/list_items_picking_scanned.png + :height: 100 + :width: 200 + :alt: Picking scanned + + # When you press the *Validate* button, a wizard will be displayed to confirm the action. + If everything is correct, it is validated and you return to the picking list mentioned above. + + .. image:: https://raw.githubusercontent.com/stock_barcodes/static/src/img/confirm_items_picking.png + :height: 100 + :width: 200 + :alt: Picking scanned + + # If there is an item whose quantity is zero, a wizard will be displayed after the one mentioned + above, to confirm if you want to process all the quantities. If positive, you will proceed + and be directed to the list mentioned above in the previous point. + + .. image:: https://raw.githubusercontent.com/stock_barcodes/static/src/img/confirm_all_quantity_items_picking.png + :height: 100 + :width: 200 + :alt: Picking scanned + + # Press the *+ Product* button to display the form for the new item. + + .. image:: https://raw.githubusercontent.com/stock_barcodes/static/src/img/add_product.png + :height: 100 + :width: 200 + :alt: Add product + + # When you select a product, a numeric field is displayed to add the quantity. + + .. image:: https://raw.githubusercontent.com/stock_barcodes/static/src/img/form_add_product_quantity.png + :height: 100 + :width: 200 + :alt: Add quantity product + + # When you press the button with the trash can icon, the values of the form are reset (except for the location) without closing it. + + .. image:: https://raw.githubusercontent.com/stock_barcodes/static/src/img/form_add_product_reset.png + :height: 100 + :width: 200 + :alt: Reset data form + + # When you press the *Clean values* button, all fields are reset and the form is closed. + # When you press the *Confirm* button, the new item is added and the form is closed. + # When adding the new item all the quantities are assigned to it, if you want to modify it, press the edit icon. The barcode scanner interface has two operation modes. In both of them user can scan: @@ -124,6 +287,28 @@ this log is show to user other reads with the same product and location done by other users. User can remove the last read scan. +Barcode interface for barcode actions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +To use the barcode interface for actions: + +#. Go to *Inventory > Configuration > Barcode Actions*. +#. Create a new barcode action and configure the barcode. + +.. image:: https://raw.githubusercontent.com/stock_barcodes/static/src/img/create_barcode_action.png + :height: 100 + :width: 200 + :alt: Print barcodes + +#. Select the barcode actions you want to use, a button (PRINT BARCODES) will appear that allows you to print the configured barcodes to PDF. + +.. image:: https://raw.githubusercontent.com/stock_barcodes/static/src/img/print_barcodes.png + :height: 100 + :width: 200 + :alt: Print barcodes + +#. Go to *Barcodes*. +#. Start scanning barcodes from actions. + Known issues / Roadmap ====================== @@ -153,6 +338,18 @@ Changelog * [ADD] New feature. Add security for users. +16.0.1.0.0 (2025-01-23) +~~~~~~~~~~~~~~~~~~~~~~~ +* [IMP] + Improved views to optimize navigation and functionality. + Intuitive and mobile-friendly views. + Visual improvement of the main view accessed from the Barcodes menu. + +* [ADD] New feature. + Barcode reading to barcode actions. + Generate PDF document for the barcodes of the selected barcode actions. + + Bug Tracker =========== @@ -196,6 +393,10 @@ Contributors * Enric Tobella +* `Binhex Cloud `_: + + * Edilio Escalona Almira + Maintainers ~~~~~~~~~~~ diff --git a/stock_barcodes/__init__.py b/stock_barcodes/__init__.py index bbd1ec2806d3..7ed583d8a3e9 100644 --- a/stock_barcodes/__init__.py +++ b/stock_barcodes/__init__.py @@ -3,4 +3,5 @@ from . import models from . import wizard +from . import reports from .hooks import pre_init_hook diff --git a/stock_barcodes/__manifest__.py b/stock_barcodes/__manifest__.py index e477642b46d8..a64cdaacbe27 100644 --- a/stock_barcodes/__manifest__.py +++ b/stock_barcodes/__manifest__.py @@ -8,7 +8,7 @@ "website": "https://github.com/OCA/stock-logistics-barcode", "license": "AGPL-3", "category": "Extra Tools", - "depends": ["barcodes", "stock", "web_widget_numeric_step"], + "depends": ["barcodes", "stock", "web_widget_numeric_step", "web", "mail"], "data": [ "security/ir.model.access.csv", "views/stock_barcodes_action_view.xml", @@ -24,6 +24,9 @@ "data/stock_barcodes_action.xml", "data/stock_barcodes_option.xml", "views/stock_barcodes_menu.xml", + # Reports + "reports/barcode_actions_report.xml", + "reports/reports.xml", ], "assets": { "web.assets_backend": [ @@ -33,8 +36,10 @@ "/web_widget_numeric_step/static/src/numeric_step.xml", "/stock_barcodes/static/src/widgets/numeric_step.xml", ), + "/stock_barcodes/static/src/views/kanban/stock_barcodes_kanban.xml", "/stock_barcodes/static/src/widgets/view_button.xml", - "/stock_barcodes/static/src/css/stock.scss", + "/stock_barcodes/static/src/views/actions/stock_barcode_main_menu.xml", + "/stock_barcodes/static/src/**/*.scss", ], }, "installable": True, diff --git a/stock_barcodes/data/stock_barcodes_action.xml b/stock_barcodes/data/stock_barcodes_action.xml index a9114ef0eabf..0dee6947740b 100644 --- a/stock_barcodes/data/stock_barcodes_action.xml +++ b/stock_barcodes/data/stock_barcodes_action.xml @@ -39,4 +39,12 @@ 8 {'inventory_mode': True} + + + Operations + 40 + + 9 + {'operations_mode': True} + diff --git a/stock_barcodes/data/stock_barcodes_option.xml b/stock_barcodes/data/stock_barcodes_option.xml index 1a999cdb767a..9a344ba468c7 100644 --- a/stock_barcodes/data/stock_barcodes_option.xml +++ b/stock_barcodes/data/stock_barcodes_option.xml @@ -533,5 +533,103 @@ ref="stock_barcodes.stock_barcodes_option_group_inventory" /> - + + + + + Operation options + OPE + + + + Location + 1 + 10 + location_id + True + False + False + True + False + + + + Packaging + 2 + 10 + packaging_id + False + False + True + False + True + + + + Product + 2 + 20 + product_id + False + False + True + True + True + + + + Lot + 2 + 30 + lot_id + False + False + True + True + True + + + + Product Qty + 3 + 50 + product_qty + False + False + False + True + True + + + diff --git a/stock_barcodes/models/__init__.py b/stock_barcodes/models/__init__.py index f8c60224e85b..2a464d427947 100644 --- a/stock_barcodes/models/__init__.py +++ b/stock_barcodes/models/__init__.py @@ -5,3 +5,4 @@ from . import stock_picking from . import stock_picking_type from . import stock_quant +from . import barcode_events_mixin diff --git a/stock_barcodes/models/barcode_events_mixin.py b/stock_barcodes/models/barcode_events_mixin.py new file mode 100644 index 000000000000..efb3ed7227cc --- /dev/null +++ b/stock_barcodes/models/barcode_events_mixin.py @@ -0,0 +1,11 @@ +# Copyright 2019 Sergio Teruel +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import models + + +class BarcodesEventsMixin(models.AbstractModel): + _inherit = "barcodes.barcode_events_mixin" + + def send_bus_done(self, channel, type_channel, data=None): + self.env["bus.bus"]._sendone(channel, type_channel, data or {}) diff --git a/stock_barcodes/models/stock_barcodes_action.py b/stock_barcodes/models/stock_barcodes_action.py index dbc47897215a..92cf6b1b6faf 100644 --- a/stock_barcodes/models/stock_barcodes_action.py +++ b/stock_barcodes/models/stock_barcodes_action.py @@ -1,8 +1,22 @@ # Copyright 2019 Sergio Teruel # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from odoo import fields, models +import base64 +import re +from io import BytesIO + +import barcode +from barcode.writer import ImageWriter + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError from odoo.tools.safe_eval import safe_eval +REGEX = { + "context": r"^[^\s].*[^\s]$|^$", + "barcode": "^[a-zA-Z0-9-]+$", +} +FIELDS_NAME = {"barcode_options": "barcode_option_group_id"} + class StockBarcodesAction(models.Model): _name = "stock.barcodes.action" @@ -19,6 +33,118 @@ class StockBarcodesAction(models.Model): key_shortcut = fields.Integer() key_char_shortcut = fields.Char() icon_class = fields.Char() + barcode = fields.Char() + barcode_image = fields.Image( + "Barcode image", + readonly=True, + compute="_compute_barcode_image", + attachment=True, + ) + + count_elements = fields.Integer(default=0, compute="_compute_count_elements") + + @api.constrains("barcode") + def _constrains_barcode(self): + for action in self: + if not re.match(REGEX.get("barcode", False), action.barcode): + raise ValidationError( + _( + " The barcode {} is not correct." + "Use numbers, letters and dashes, without spaces." + "E.g. 15753, BC-5789,er-56 " + "" + ).format(action.barcode) + ) + all_barcode = [bar for bar in action.mapped("barcode") if bar] + domain = [("barcode", "in", all_barcode)] + matched_actions = self.sudo().search(domain, order="id") + if len(matched_actions) > len(all_barcode): + raise ValidationError( + _( + """ Barcode has already been assigned to the action(s): {}.""" + ).format(", ".join(matched_actions.mapped("name"))) + ) + + def _generate_barcode(self): + barcode_type = barcode.get_barcode_class("code128") + buffer = BytesIO() + barcode_instance = barcode_type(self.barcode, writer=ImageWriter()) + barcode_instance.write(buffer) + buffer.seek(0) + image_base64 = base64.b64encode(buffer.getvalue()) + return image_base64 + + @api.depends("barcode") + def _compute_barcode_image(self): + for action in self: + if action.barcode: + action.barcode_image = action._generate_barcode() + else: + action.barcode_image = False + + @api.constrains("context") + def _constrains_context(self): + if self.context and not bool( + re.match(REGEX.get("context", False), self.context) + ): + raise ValidationError(_("There can be no spaces at the beginning or end.")) + + def _count_elements(self): + domain = [] + if self.context: + context_values = self.context.strip("{}").split(",") + + def _map_context_values(x): + field_values = x.split(":") + field_name = field_values[0].split("search_default_") + if len(field_name) > 1: + field_name = field_name[1].strip("'") + field_value_format = field_values[1].replace("'", "").strip() + field_value = ( + int(field_value_format) + if field_value_format.isdigit() + else field_value_format + ) + if hasattr( + self.action_window_id.res_model, + FIELDS_NAME.get(field_name, field_name), + ): + return ( + "{}".format(FIELDS_NAME.get(field_name, field_name)), + "=", + field_value, + ) + else: + return False + else: + return () + + domain = [ + val_domain + for val_domain in list( + map(lambda x: _map_context_values(x), context_values) + ) + ] + search_count = ( + list(filter(lambda x: x, domain)) + if all(val_d is True for val_d in domain) + else [] + ) + return ( + self.env[self.action_window_id.res_model].search_count(search_count) + if self.action_window_id.res_model + else 0 + ) + return 0 + + @api.depends("context") + def _compute_count_elements(self): + for barcode_action in self: + barcode_action.count_elements = ( + barcode_action._count_elements() + if "search_default_" in barcode_action.context + else 0 + ) def open_action(self): action = self.action_window_id.sudo().read()[0] @@ -29,8 +155,10 @@ def open_action(self): if self.context: ctx.update(safe_eval(self.context)) if action_context.get("inventory_mode", False): - return self.open_inventory_action(ctx) - action["context"] = ctx + action = self.open_inventory_action(ctx) + else: + action["context"] = ctx + return action def open_inventory_action(self, ctx): @@ -53,3 +181,9 @@ def open_inventory_action(self, ctx): action["res_id"] = wiz.id action["context"] = ctx return action + + def print_barcodes(self): + report_action = self.env.ref( + "stock_barcodes.action_report_barcode_actions" + ).report_action(None, data={}) + return report_action diff --git a/stock_barcodes/models/stock_picking.py b/stock_barcodes/models/stock_picking.py index 13219a5d4d70..ec781505c0c5 100644 --- a/stock_barcodes/models/stock_picking.py +++ b/stock_barcodes/models/stock_picking.py @@ -28,7 +28,11 @@ def _prepare_barcode_wiz_vals(self, option_group): return vals def action_barcode_scan(self, option_group=False): - option_group = option_group or self.picking_type_id.barcode_option_group_id + option_group = ( + option_group + or self.picking_type_id.barcode_option_group_id + or self.env.ref("stock_barcodes.stock_barcodes_option_group_operation") + ) wiz = self.env["wiz.stock.barcodes.read.picking"].create( self._prepare_barcode_wiz_vals(option_group) ) @@ -52,7 +56,18 @@ def button_validate(self): StockPicking, self.with_context(skip_backorder=True) ).button_validate() else: + pickings_to_backorder = self._check_backorder() + if pickings_to_backorder: + return pickings_to_backorder._action_generate_backorder_wizard( + show_transfers=self._should_show_transfers() + ) res = super().button_validate() if res is True and self.env.context.get("show_picking_type_action_tree", False): - return self[:1].picking_type_id.get_action_picking_tree_ready() + res = self[:1].picking_type_id.get_action_picking_tree_ready() + + if self.state == "done": + self.env["bus.bus"]._sendone( + "stock_barcodes_scan", "actions_barcode", {"valid_picking": True} + ) + return res diff --git a/stock_barcodes/models/stock_picking_type.py b/stock_barcodes/models/stock_picking_type.py index c57fc0ee5884..5dc256be7923 100644 --- a/stock_barcodes/models/stock_picking_type.py +++ b/stock_barcodes/models/stock_picking_type.py @@ -1,5 +1,7 @@ # Copyright 2019 Sergio Teruel # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from ast import literal_eval + from odoo import fields, models @@ -67,3 +69,36 @@ def action_barcode_new_picking(self): ) option_group = self.new_picking_barcode_option_group_id return picking.action_barcode_scan(option_group=option_group) + + def get_action_picking_tree_ready(self): + context = dict(self.env.context) + if context.get("operations_mode", False): + return self._get_action( + "stock_barcodes.stock_barcodes_action_picking_tree_ready" + ) + return super().get_action_picking_tree_ready() + + def _get_action(self, action_xmlid): + action = self.env["ir.actions.actions"]._for_xml_id(action_xmlid) + if self: + action["display_name"] = self.display_name + + default_immediate_tranfer = True + if ( + self.env["ir.config_parameter"] + .sudo() + .get_param("stock.no_default_immediate_tranfer") + ): + default_immediate_tranfer = False + + context = { + "search_default_picking_type_id": [self.id], + "default_picking_type_id": self.id, + "default_immediate_transfer": default_immediate_tranfer, + "default_company_id": self.company_id.id, + } + + action_context = literal_eval(action["context"].strip()) + context = {**action_context, **context} + action["context"] = context + return action diff --git a/stock_barcodes/models/stock_quant.py b/stock_barcodes/models/stock_quant.py index 8e7cd1319302..eea8b9bc9886 100644 --- a/stock_barcodes/models/stock_quant.py +++ b/stock_barcodes/models/stock_quant.py @@ -2,12 +2,27 @@ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). from odoo import models +MODEL_UPDATE_INVENTORY = ["wiz.stock.barcodes.read.inventory"] + class StockQuant(models.Model): - _inherit = "stock.quant" + _name = "stock.quant" + _inherit = ["stock.quant", "barcodes.barcode_events_mixin"] def action_barcode_inventory_quant_unlink(self): self.with_context(inventory_mode=True).action_set_inventory_quantity_to_zero() + context = dict(self.env.context) + params = context.get("params", {}) + res_model = params.get("model", False) + res_id = params.get("id", False) + if res_id and res_model in MODEL_UPDATE_INVENTORY: + wiz_id = self.env[params["model"]].browse(params["id"]) + wiz_id._compute_count_inventory_quants() + wiz_id.send_bus_done( + "stock_barcodes_form_update", + "count_apply_inventory", + {"count": wiz_id.count_inventory_quants}, + ) def _get_fields_to_edit(self): return [ @@ -28,3 +43,38 @@ def action_barcode_inventory_quant_edit(self): for fname in self._get_fields_to_edit(): wiz_barcode[fname] = quant[fname] wiz_barcode.product_qty = quant.inventory_quantity + + wiz_barcode.manual_entry = True + self.send_bus_done( + "stock_barcodes_scan", + "stock_barcodes_edit_manual", + { + "manual_entry": True, + }, + ) + + def enable_current_operations(self): + self.send_bus_done( + "stock_barcodes_kanban_update", + "enable_operations", + { + "id": self.id, + }, + ) + + def operation_quantities_rest(self): + self.write({"inventory_quantity": self.inventory_quantity - 1}) + self.enable_current_operations() + + def operation_quantities(self): + self.write({"inventory_quantity": self.inventory_quantity + 1}) + self.enable_current_operations() + + def action_apply_inventory(self): + res = super().action_apply_inventory() + self.send_bus_done( + "stock_barcodes_scan", + "actions_barcode", + {"apply_inventory": True}, + ) + return res diff --git a/stock_barcodes/readme/CONTRIBUTORS.rst b/stock_barcodes/readme/CONTRIBUTORS.rst index 4e071a533611..78e882484149 100644 --- a/stock_barcodes/readme/CONTRIBUTORS.rst +++ b/stock_barcodes/readme/CONTRIBUTORS.rst @@ -19,3 +19,7 @@ * Lois Rilo * Enric Tobella + +* `Binhex Cloud `_: + + * Edilio Escalona Almira diff --git a/stock_barcodes/readme/DESCRIPTION.rst b/stock_barcodes/readme/DESCRIPTION.rst index 218761e1276c..068623182c78 100644 --- a/stock_barcodes/readme/DESCRIPTION.rst +++ b/stock_barcodes/readme/DESCRIPTION.rst @@ -5,3 +5,7 @@ other modules. This module also makes use of this wizard for providing barcode support for doing inventories and picking operations. + +This module provides configuring barcodes for barcode actions. + + diff --git a/stock_barcodes/readme/HISTORY.rst b/stock_barcodes/readme/HISTORY.rst index 4d18768a617d..50f772b9576d 100644 --- a/stock_barcodes/readme/HISTORY.rst +++ b/stock_barcodes/readme/HISTORY.rst @@ -15,3 +15,15 @@ * [ADD] New feature. Add security for users. + +16.0.1.0.0 (2025-01-23) +~~~~~~~~~~~~~~~~~~~~~~~ +* [IMP] + Improved views to optimize navigation and functionality. + Intuitive and mobile-friendly views. + Visual improvement of the main view accessed from the Barcodes menu. + +* [ADD] New feature. + Barcode reading to barcode actions. + Generate PDF document for the barcodes of the selected barcode actions. + diff --git a/stock_barcodes/readme/USAGE.rst b/stock_barcodes/readme/USAGE.rst index 6ab6a4ae0d7c..19b45baea3bd 100644 --- a/stock_barcodes/readme/USAGE.rst +++ b/stock_barcodes/readme/USAGE.rst @@ -1,13 +1,74 @@ Barcode interface for inventory operations ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -To use the barcode interface on inventory: +Option 1: To use the barcode interface on inventory + + #. Go to *Inventory > operations > Inventory Adjustments*. + #. Create new inventory with "Select products manually" option. + #. Start inventory. + #. Click to "Scan barcodes" smart button. + #. Start reading barcodes. + +Option 2: Use the barcode interface inventory directly from the Barcodes application + #. Go to *Barcodes*. + #. Select the *Inventory* option. + + .. image:: /stock_barcodes/static/src/img/inventory_barcode_action.png + :height: 100 + :width: 200 + :alt: Inventory barcode action + + #. Start scanning barcodes. + +Actions + # Press the *+ Product* button to display the form for the new item. + + .. image:: /stock_barcodes/static/src/img/add_product.png + :height: 100 + :width: 200 + :alt: Add product + + # When you select a product, a numeric field is displayed to add the quantity. + + .. image:: /stock_barcodes/static/src/img/form_add_product_quantity.png + :height: 100 + :width: 200 + :alt: Add quantity product + + # When you press the button with the trash can icon, the values of the form are reset (except for the location) without closing it. + + .. image:: /stock_barcodes/static/src/img/form_add_product_reset.png + :height: 100 + :width: 200 + :alt: Reset data form + + # When you press the *Clean values* button, all fields are reset and the form is closed. + # When you press the *Confirm* button, the new item is added and the form is closed. + # When the eye icon is closed, the created items greater than zero are displayed, and if not, those less than or equal to zero. + + .. image:: /stock_barcodes/static/src/img/list_items.png + :height: 100 + :width: 200 + :alt: Reset data form + + # In the list, the trash can icon allows you to reset the quantity to zero and the edit icon allows you to change the item values. + + .. image:: /stock_barcodes/static/src/img/list_action_items.png + :height: 100 + :width: 200 + :alt: Reset data form + + # The *Apply* button is only displayed if there are items with quantities greater than zero, regardless of whether they were scanned or entered manually; If you press all the defined quantities will be processed after defining the reason for the inventory adjustment and then the main barcode menu will be displayed. + + .. image:: /stock_barcodes/static/src/img/apply_inventory.png + :height: 100 + :width: 200 + :alt: Apply inventory + .. image:: /stock_barcodes/static/src/img/apply_inventory_reason.png + :height: 100 + :width: 200 + :alt: Apply inventory reason -#. Go to *Inventory > operations > Inventory Adjustments*. -#. Create new inventory with "Select products manually" option. -#. Start inventory. -#. Click to "Scan barcodes" smart button. -#. Start reading barcodes. Barcode interface for picking operations ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -21,11 +82,109 @@ To use the barcode interface on picking operations: #. Click on scanner button on any operation type. #. Start reading barcodes. -To use the barcode interface on a picking: +Option 1: To use the barcode interface on a picking: -#. Go to *Inventory > Transfers*. -#. Click to "Scan barcodes" smart button. -#. Start reading barcodes. + #. Go to *Inventory > Transfers*. + #. Click to "Scan barcodes" smart button. + #. Start reading barcodes. + +Option 2: Use the barcode interface picking directly from the Barcodes application + #. Go to *Barcodes*. + #. Select the option *OPERATIONS*. + + .. image:: /stock_barcodes/static/src/img/inventory_barcode_action.png + :height: 100 + :width: 200 + :alt: Operation barcode action + + # Select the type of picking. + # The pickings in ready status are displayed, select the one you want to start scanning. + + .. image:: /stock_barcodes/static/src/img/list_picking.png + :height: 100 + :width: 200 + :alt: List picking + + #. Start scanning barcodes. + + .. image:: /stock_barcodes/static/src/img/barcode_interface_picking.png + :height: 100 + :width: 200 + :alt: List picking + +Actions + # All the items that have been configured for the selected picking are listed. + + .. image:: /stock_barcodes/static/src/img/list_items_picking.png + :height: 100 + :width: 200 + :alt: List picking + + # The edit icon in the list allows you to modify the data. + + .. image:: /stock_barcodes/static/src/img/list_items_picking_edit.png + :height: 100 + :width: 200 + :alt: Edit picking + + # The button that contains a *+120* (in this case), allows you to define all the + remaining quantities. Once defined, this button disappears and if you want to change the + quantities, press the edit button. + + .. image:: /stock_barcodes/static/src/img/list_items_picking_quantity.png + :height: 100 + :width: 200 + :alt: Quantity picking + + # If there is at least one item with a quantity already defined, an eye icon is displayed, + which if closed shows the items and their quantities already scanned. + + .. image:: /stock_barcodes/static/src/img/list_items_picking_scanned.png + :height: 100 + :width: 200 + :alt: Picking scanned + + # When you press the *Validate* button, a wizard will be displayed to confirm the action. + If everything is correct, it is validated and you return to the picking list mentioned above. + + .. image:: /stock_barcodes/static/src/img/confirm_items_picking.png + :height: 100 + :width: 200 + :alt: Picking scanned + + # If there is an item whose quantity is zero, a wizard will be displayed after the one mentioned + above, to confirm if you want to process all the quantities. If positive, you will proceed + and be directed to the list mentioned above in the previous point. + + .. image:: /stock_barcodes/static/src/img/confirm_all_quantity_items_picking.png + :height: 100 + :width: 200 + :alt: Picking scanned + + # Press the *+ Product* button to display the form for the new item. + + .. image:: /stock_barcodes/static/src/img/add_product.png + :height: 100 + :width: 200 + :alt: Add product + + # When you select a product, a numeric field is displayed to add the quantity. + + .. image:: /stock_barcodes/static/src/img/form_add_product_quantity.png + :height: 100 + :width: 200 + :alt: Add quantity product + + # When you press the button with the trash can icon, the values of the form are reset (except for the location) without closing it. + + .. image:: /stock_barcodes/static/src/img/form_add_product_reset.png + :height: 100 + :width: 200 + :alt: Reset data form + + # When you press the *Clean values* button, all fields are reset and the form is closed. + # When you press the *Confirm* button, the new item is added and the form is closed. + # When adding the new item all the quantities are assigned to it, if you want to modify it, press the edit icon. The barcode scanner interface has two operation modes. In both of them user can scan: @@ -77,3 +236,25 @@ Barcode scanning interface display 10 last records linked to model, the goal of this log is show to user other reads with the same product and location done by other users. User can remove the last read scan. + +Barcode interface for barcode actions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +To use the barcode interface for actions: + +#. Go to *Inventory > Configuration > Barcode Actions*. +#. Create a new barcode action and configure the barcode. + +.. image:: /stock_barcodes/static/src/img/create_barcode_action.png + :height: 100 + :width: 200 + :alt: Print barcodes + +#. Select the barcode actions you want to use, a button (PRINT BARCODES) will appear that allows you to print the configured barcodes to PDF. + +.. image:: /stock_barcodes/static/src/img/print_barcodes.png + :height: 100 + :width: 200 + :alt: Print barcodes + +#. Go to *Barcodes*. +#. Start scanning barcodes from actions. diff --git a/stock_barcodes/reports/__init__.py b/stock_barcodes/reports/__init__.py new file mode 100644 index 000000000000..84df5b6e6145 --- /dev/null +++ b/stock_barcodes/reports/__init__.py @@ -0,0 +1 @@ +from . import barcode_actions_report diff --git a/stock_barcodes/reports/barcode_actions_report.py b/stock_barcodes/reports/barcode_actions_report.py new file mode 100644 index 000000000000..792a013bea79 --- /dev/null +++ b/stock_barcodes/reports/barcode_actions_report.py @@ -0,0 +1,16 @@ +from odoo import api, models + + +class ReportStockBarcodesBarcodeActions(models.Model): + _name = "report.stock_barcodes.report_barcode_actions" + _description = "Print barcodes from barcode actions" + + @api.model + def _get_report_values(self, docids, data=None): + datas = self.env["stock.barcodes.action"].search_read( + [("id", "in", docids), ("barcode", "!=", False)], + ["name", "barcode", "barcode_image"], + ) + return { + "barcodes": datas, + } diff --git a/stock_barcodes/reports/barcode_actions_report.xml b/stock_barcodes/reports/barcode_actions_report.xml new file mode 100644 index 000000000000..d8c872dac7ab --- /dev/null +++ b/stock_barcodes/reports/barcode_actions_report.xml @@ -0,0 +1,25 @@ + + + diff --git a/stock_barcodes/reports/reports.xml b/stock_barcodes/reports/reports.xml new file mode 100644 index 000000000000..274e3226de99 --- /dev/null +++ b/stock_barcodes/reports/reports.xml @@ -0,0 +1,28 @@ + + + A4 + + A4 + 0 + 0 + Portrait + 30 + 5 + 7 + 7 + + 5 + 90 + + + + Barcodes (PDF) + stock.barcodes.action + qweb-pdf + stock_barcodes.report_barcode_actions + stock_barcodes.report_barcode_actions + 'Barcodes - %s' % (object.name) + report + + + diff --git a/stock_barcodes/static/description/index.html b/stock_barcodes/static/description/index.html index b30ba9e51d77..65cec53414d5 100644 --- a/stock_barcodes/static/description/index.html +++ b/stock_barcodes/static/description/index.html @@ -367,7 +367,7 @@

Stock Barcodes

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

Beta License: AGPL-3 OCA/stock-logistics-barcode Translate me on Weblate Try me on Runboat

This module provides a barcode reader interface for stock module.

@@ -375,6 +375,7 @@

Stock Barcodes

other modules.

This module also makes use of this wizard for providing barcode support for doing inventories and picking operations.

+

This module provides configuring barcodes for barcode actions.

Table of contents

-
  • Known issues / Roadmap
  • -
  • Changelog
  • -

    Known issues / Roadmap

    +

    Known issues / Roadmap

    • Excute action_done() method outside onchange environment.
    • Allow create product when a barcode has not been found.
    • @@ -490,31 +624,43 @@

      Known issues / Roadmap

    -

    Changelog

    +

    Changelog

    -

    11.0.1.1.0 (2019-09-24)

    +

    11.0.1.1.0 (2019-09-24)

    • [ADD] New feature. User can uses barcode interface in picking operations.
    -

    13.0.1.1.1 (2021-02-06)

    +

    13.0.1.1.1 (2021-02-06)

    • [ADD] New feature. Add option to get lots automatically based on removal strategy in inventory.
    -

    14.0.1.0.0 (2021-04-05)

    +

    14.0.1.0.0 (2021-04-05)

    • [ADD] New feature. Add security for users.
    +
    +

    16.0.1.0.0 (2025-01-23)

    +
      +
    • [IMP] +Improved views to optimize navigation and functionality. +Intuitive and mobile-friendly views. +Visual improvement of the main view accessed from the Barcodes menu.
    • +
    • [ADD] New feature. +Barcode reading to barcode actions. +Generate PDF document for the barcodes of the selected barcode actions.
    • +
    +
    -

    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 @@ -522,15 +668,15 @@

    Bug Tracker

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

    -

    Credits

    +

    Credits

    -

    Authors

    +

    Authors

    • Tecnativa
    -

    Contributors

    +

    Contributors

    -

    Maintainers

    +

    Maintainers

    This module is maintained by the OCA.

    Odoo Community Association diff --git a/stock_barcodes/static/src/css/stock.scss b/stock_barcodes/static/src/css/stock.scss deleted file mode 100644 index f99d171c823b..000000000000 --- a/stock_barcodes/static/src/css/stock.scss +++ /dev/null @@ -1,136 +0,0 @@ -.oe_stock_scan_button { - border: none !important; - background: none !important; - box-shadow: none !important; -} - -.oe_stock_barcodes_bottombar { - bottom: 0; - padding: 0.5em; - background-color: $o-view-background-color; - border: 0 solid $border-color; - border-width: 1px 0 0 0; -} - -// Avoid too big small buttons from core -.o_web_client.o_touch_device { - .oe_stock_barcordes_form { - .btn { - &, - .btn-sm { - padding: 0.25rem 0.5rem; - } - } - } -} - -.oe_stock_barcordes_form { - padding: 0 !important; - height: 100%; - // Recover useless space - div[name="_barcode_scanned"] { - min-height: 0 !important; - } - .o_group .scan_fields { - &.o_inner_group { - margin-bottom: 0 !important; - } - margin: 0 !important; - } - - .o_form_sheet, - .o_form_sheet_bg { - padding: 0 !important; - margin: 0 !important; - max-width: 100% !important; - border: 0 !important; - } - - // In Odoo 16 the flat input styling lacks proper usability - .o_field_widget { - margin-bottom: 1px !important; - .o_input { - border-radius: 3px; - border-width: 1px; - background-color: white; - } - .o_x2m_control_panel { - margin: 0px !important; - } - } - - .o_kanban_record { - flex-basis: 100%; - - .btn-full-width { - margin: -9px; - width: calc(100% + 18px); - height: calc(100% + 18px); - } - - &.o_kanban_ghost { - display: none; - } - } - - .alert { - position: fixed; - top: 0; - width: 100%; - border-radius: 0; - padding: 0; - min-height: 50px; - z-index: 999; - } - - .oe_stock_barcordes_content { - overflow: hidden scroll; - } -} - -.o_kanban_barcode { - .o_kanban_record.oe_kanban_details { - @extend .btn; - @extend .btn-secondary; - padding: 0.6em 0; - margin-bottom: 0.5em; - } -} -// TODO: Define class for all elements -.oe_kanban_action_button:focus { - background-color: lightgray; -} -// Left icon in small screens -.oe_span_small_icon { - width: 25px; - text-align: center; -} - -// Display 100% all menu elements -.oe_kanban_card_full_width { - width: 100% !important; -} - -// The kanban view adds some pre-styles that we want to be able to tweak -div[name="menu_actions"] { - div[role="article"] { - margin-top: 10px !important; - } -} - -// Dropdown that is desactivated at lg width -@media (min-width: 992px) { - .d-lg-flex-no-dropdown { - position: relative !important; - display: flex !important; - border: none; - box-shadow: none; - bottom: auto !important; - transform: none !important; - } -} -.dropdown-menu.d-lg-flex-no-dropdown { - .d-flex { - margin-bottom: 5px; - } -} diff --git a/stock_barcodes/static/src/docs/barcodes_actions.pdf b/stock_barcodes/static/src/docs/barcodes_actions.pdf new file mode 100644 index 000000000000..caca3f7f7a24 Binary files /dev/null and b/stock_barcodes/static/src/docs/barcodes_actions.pdf differ diff --git a/stock_barcodes/static/src/docs/barcodes_demo.pdf b/stock_barcodes/static/src/docs/barcodes_demo.pdf new file mode 100644 index 000000000000..efa79893336d Binary files /dev/null and b/stock_barcodes/static/src/docs/barcodes_demo.pdf differ diff --git a/stock_barcodes/static/src/img/add_product.png b/stock_barcodes/static/src/img/add_product.png new file mode 100644 index 000000000000..298c51430f75 Binary files /dev/null and b/stock_barcodes/static/src/img/add_product.png differ diff --git a/stock_barcodes/static/src/img/apply_inventory.png b/stock_barcodes/static/src/img/apply_inventory.png new file mode 100644 index 000000000000..9e3e0e8a5af0 Binary files /dev/null and b/stock_barcodes/static/src/img/apply_inventory.png differ diff --git a/stock_barcodes/static/src/img/apply_inventory_reason.png b/stock_barcodes/static/src/img/apply_inventory_reason.png new file mode 100644 index 000000000000..6b434b2ca5f0 Binary files /dev/null and b/stock_barcodes/static/src/img/apply_inventory_reason.png differ diff --git a/stock_barcodes/static/src/img/barcode_interface_picking.png b/stock_barcodes/static/src/img/barcode_interface_picking.png new file mode 100644 index 000000000000..4ddbc007c69e Binary files /dev/null and b/stock_barcodes/static/src/img/barcode_interface_picking.png differ diff --git a/stock_barcodes/static/src/img/confirm_all_quantity_items_picking.png b/stock_barcodes/static/src/img/confirm_all_quantity_items_picking.png new file mode 100644 index 000000000000..6ecc8884148d Binary files /dev/null and b/stock_barcodes/static/src/img/confirm_all_quantity_items_picking.png differ diff --git a/stock_barcodes/static/src/img/confirm_items_picking.png b/stock_barcodes/static/src/img/confirm_items_picking.png new file mode 100644 index 000000000000..67fdab6cf3a4 Binary files /dev/null and b/stock_barcodes/static/src/img/confirm_items_picking.png differ diff --git a/stock_barcodes/static/src/img/create_barcode_action.png b/stock_barcodes/static/src/img/create_barcode_action.png new file mode 100644 index 000000000000..d04c6bafc213 Binary files /dev/null and b/stock_barcodes/static/src/img/create_barcode_action.png differ diff --git a/stock_barcodes/static/src/img/form_add_product.png b/stock_barcodes/static/src/img/form_add_product.png new file mode 100644 index 000000000000..6b0ce83a024b Binary files /dev/null and b/stock_barcodes/static/src/img/form_add_product.png differ diff --git a/stock_barcodes/static/src/img/form_add_product_quantity.png b/stock_barcodes/static/src/img/form_add_product_quantity.png new file mode 100644 index 000000000000..47d6ce6e7f13 Binary files /dev/null and b/stock_barcodes/static/src/img/form_add_product_quantity.png differ diff --git a/stock_barcodes/static/src/img/form_add_product_reset.png b/stock_barcodes/static/src/img/form_add_product_reset.png new file mode 100644 index 000000000000..44f458edd468 Binary files /dev/null and b/stock_barcodes/static/src/img/form_add_product_reset.png differ diff --git a/stock_barcodes/static/src/img/inventory_barcode_action.png b/stock_barcodes/static/src/img/inventory_barcode_action.png new file mode 100644 index 000000000000..54f206506c9a Binary files /dev/null and b/stock_barcodes/static/src/img/inventory_barcode_action.png differ diff --git a/stock_barcodes/static/src/img/list_action_items.png b/stock_barcodes/static/src/img/list_action_items.png new file mode 100644 index 000000000000..09bf1305c920 Binary files /dev/null and b/stock_barcodes/static/src/img/list_action_items.png differ diff --git a/stock_barcodes/static/src/img/list_items.png b/stock_barcodes/static/src/img/list_items.png new file mode 100644 index 000000000000..45fd893f8ae3 Binary files /dev/null and b/stock_barcodes/static/src/img/list_items.png differ diff --git a/stock_barcodes/static/src/img/list_items_picking.png b/stock_barcodes/static/src/img/list_items_picking.png new file mode 100644 index 000000000000..4ddbc007c69e Binary files /dev/null and b/stock_barcodes/static/src/img/list_items_picking.png differ diff --git a/stock_barcodes/static/src/img/list_items_picking_edit.png b/stock_barcodes/static/src/img/list_items_picking_edit.png new file mode 100644 index 000000000000..73f07aa35659 Binary files /dev/null and b/stock_barcodes/static/src/img/list_items_picking_edit.png differ diff --git a/stock_barcodes/static/src/img/list_items_picking_quantity.png b/stock_barcodes/static/src/img/list_items_picking_quantity.png new file mode 100644 index 000000000000..f297f4710fbd Binary files /dev/null and b/stock_barcodes/static/src/img/list_items_picking_quantity.png differ diff --git a/stock_barcodes/static/src/img/list_items_picking_scanned.png b/stock_barcodes/static/src/img/list_items_picking_scanned.png new file mode 100644 index 000000000000..32de98140c52 Binary files /dev/null and b/stock_barcodes/static/src/img/list_items_picking_scanned.png differ diff --git a/stock_barcodes/static/src/img/list_picking.png b/stock_barcodes/static/src/img/list_picking.png new file mode 100644 index 000000000000..05fd924041c2 Binary files /dev/null and b/stock_barcodes/static/src/img/list_picking.png differ diff --git a/stock_barcodes/static/src/img/print_barcodes.png b/stock_barcodes/static/src/img/print_barcodes.png new file mode 100644 index 000000000000..170225646370 Binary files /dev/null and b/stock_barcodes/static/src/img/print_barcodes.png differ diff --git a/stock_barcodes/static/src/scss/barcode.scss b/stock_barcodes/static/src/scss/barcode.scss new file mode 100644 index 000000000000..b26a48871fc2 --- /dev/null +++ b/stock_barcodes/static/src/scss/barcode.scss @@ -0,0 +1,135 @@ +@mixin barcode-decoration() { + i.fa-barcode { + font-size: 2em !important; + @include media-breakpoint-down(sm) { + font-size: 3em !important; + } + } +} + +div.o_kanban_renderer { + button[name="action_barcode_scan"] { + @include barcode-decoration; + } +} + +div.o_kanban_stock_barcodes { + padding: 10px !important; + + button.o_stock_mobile_barcode { + @include barcode-decoration; + } + + button.o_stock_mobile_barcode:focus { + box-shadow: none !important; + } +} + +div.alert.barcode-info { + background-color: $o-community-color !important; + + span.fa-barcode { + margin: 0.5rem 1rem 0 1rem !important; + + @include media-breakpoint-down(sm) { + margin: 0 0 0 5px !important; + font-size: 1em !important; + } + } +} + +.inventory_quant_ids_with_form { + height: 710px !important; + @include media-breakpoint-down(sm) { + height: 500px !important; + } +} + +.inventory_quant_ids_without_form { + height: 822px !important; + @include media-breakpoint-down(sm) { + height: 648px !important; + } +} + +div.oe_kanban_picking_done { + background-color: #353840 !important; + border: none !important; + box-shadow: none !important; + height: 230px !important; +} + +div[name="inventory_quant_ids"], +div[name="pending_move_ids"], +div[name="move_line_ids"] { + div.o_kanban_renderer { + padding: 0 !important; + + &:has(div.oe_kanban_picking_done) { + height: 50% !important; + } + + div.o_kanban_record { + box-shadow: rgba(0, 0, 0, 0.35) 0 5px 15px !important; + + i.fa-pencil, + i.fa-trash { + font-size: 3.5em !important; + } + + img, + span.text-end.fw-bold { + margin-right: 5% !important; + } + + div.indent { + text-indent: 5px !important; + } + } + + button.btn-op-rest, + button.btn-op-sum { + background-color: $o-community-color !important; + min-width: 55px !important; + height: 60px !important; + padding: 12px 8px !important; + border-radius: 8px !important; + line-height: 16px !important; + font-size: 16px !important; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + text-transform: none; + } + } +} + +button[name="action_clean_product"], +button[name="action_clean_lot"], +button#btn_create_lot { + width: 5% !important; + padding: 0.9rem !important; + @include media-breakpoint-down(sm) { + width: 95% !important; + margin-top: 0.5em !important; + padding: 0.3rem !important; + i.o_button_icon { + font-size: 1.5em !important; + } + } +} + +div.stock_barcodes_action_kanban { + div.o_kanban_record { + div.oe_kanban_content { + padding: 1.5rem 1.5rem 1.5rem 0.5rem !important; + + div.count-elements { + border: 1px solid; + padding: 1px 4px 1px 4px !important; + border-radius: 40% !important; + background-color: lightgray !important; + } + } + } +} diff --git a/stock_barcodes/static/src/scss/stock.scss b/stock_barcodes/static/src/scss/stock.scss new file mode 100644 index 000000000000..5bd70bb1c2b0 --- /dev/null +++ b/stock_barcodes/static/src/scss/stock.scss @@ -0,0 +1,285 @@ +@mixin margin-form-edit-sm($margin) { + @include media-breakpoint-down(sm) { + margin: $margin !important; + } +} + +.oe_stock_scan_button { + border: none !important; + background: none !important; + box-shadow: none !important; +} + +.oe_stock_barcodes_bottombar { + bottom: 0; + background-color: $o-view-background-color; + border-width: 1px 0 0 0 solid $border-color; + box-shadow: 0 -3px 10px #c9ccd2; + height: 60px !important; +} + +// Avoid too big small buttons from core +.o_web_client.o_touch_device { + .oe_stock_barcordes_form { + .btn { + &, + .btn-sm { + padding: 0.25rem 0.5rem; + } + } + } +} + +.oe_stock_barcordes_form { + padding: 0 !important; + height: 100%; + // Recover useless space + div[name="_barcode_scanned"] { + min-height: 0 !important; + } + + div[name="package_id"], + div[name="product_id"], + div[name="lot_id"] { + width: 90% !important; + } + + div[name="product_id"], + div[name="package_id"], + div[name="lot_name"] { + @include margin-form-edit-sm(0 0 0 1%); + } + + div[name="location_dest_id"] { + @include margin-form-edit-sm(0 0 1% 1%); + } + + div.widget_numeric_step { + font-size: 1.5rem !important; + } + + input#location_id, + input#location_dest_id, + input#package_id, + input#product_id, + input#lot_id_1, + input#lot_name { + border-radius: 0.5rem !important; + padding: 0.375rem 0.75rem !important; + height: 40px !important; + font-size: 1.5rem !important; + + & + ul.o-autocomplete--dropdown-menu { + li { + font-size: 1.5rem !important; + } + } + } + + div[name="candidate_picking_ids"] { + div.oe_kanban_color_alert { + padding: 0 !important; + margin: 0 !important; + } + + div.o_kanban_ungrouped.o_kanban_renderer { + margin: 0 !important; + padding: 0 !important; + } + + @include media-breakpoint-down(sm) { + div.o_kanban_renderer { + margin: 2% 1% 2% 1% !important; + } + } + } + + div.scan_fields { + width: 100% !important; + margin: 0 !important; + + div.o-autocomplete.dropdown { + + a.o_dropdown_button { + display: none !important; + } + } + + @include media-breakpoint-down(sm) { + padding: 0 2% 0 2% !important; + width: 100% !important; + } + + > div.o_inner_group.grid.col-lg-6 { + div.o_cell { + width: 100% !important; + } + } + + div.mt4.col-lg-6 { + @include media-breakpoint-down(sm) { + margin-bottom: 1.5rem !important; + } + } + + &:has(button[name="action_clean_lot"]) { + div[name="lot_name"] { + width: 88% !important; + @include media-breakpoint-down(sm) { + width: 90% !important; + } + } + button[name="action_clean_lot"] { + margin-left: 5px !important; + } + } + } + + .o_group .scan_fields { + &.o_inner_group { + margin-bottom: 0 !important; + } + + @include media-breakpoint-down(sm) { + padding: 2% 0 2% 0 !important; + } + margin: 0 !important; + } + + .o_form_sheet, + .o_form_sheet_bg { + padding: 0 !important; + margin: 0 !important; + max-width: 100% !important; + border: 0 !important; + } + + // In Odoo 16 the flat input styling lacks proper usability + .o_field_widget { + margin-bottom: 1px !important; + + .o_input { + border-radius: 3px; + border-width: 1px; + background-color: white; + } + + .o_x2m_control_panel { + margin: 0px !important; + } + } + + .o_kanban_record { + flex-basis: 100%; + + .btn-full-width { + margin: -9px; + width: calc(100% + 18px); + height: calc(100% + 18px); + } + + &.o_kanban_ghost { + display: none; + } + } + + .alert { + //position: fixed; + top: 0; + width: 100%; + border-radius: 0; + padding: 0; + min-height: 50px; + z-index: 999; + } + + .oe_stock_barcordes_content { + overflow-y: overlay !important; + + div.g-col-sm-2 { + &:has(div.o_horizontal_separator) { + display: none !important; + } + } + + div.o_inner_group.grid.px-3 { + padding: 0 !important; + } + + div[name="picking_id"] { + > a.o_form_uri { + span { + color: white !important; + } + } + } + + div[name="action_unlock_picking"] { + span { + color: white !important; + } + } + } + + div[name="info"] { + div.alert { + display: flex !important; + @include media-breakpoint-down(sm) { + display: block !important; + text-align: center !important; + } + } + + div.barcode-danger { + background-color: #dc3545 !important; + } + } +} + +.o_kanban_barcode { + .o_kanban_record.oe_kanban_details { + @extend .btn; + @extend .btn-secondary; + padding: 0.6em 0; + margin-bottom: 0.5em; + } +} + +.oe_kanban_action_button:focus { + background-color: lightgray; +} + +// Left icon in small screens +.oe_span_small_icon { + width: 25px; + text-align: center; +} + +// Display 100% all menu elements +.oe_kanban_card_full_width { + width: 100% !important; +} + +// The kanban view adds some pre-styles that we want to be able to tweak +div[name="menu_actions"] { + div[role="article"] { + margin-top: 10px !important; + } +} + +// Dropdown that is desactivated at lg width +@media (min-width: 992px) { + .d-lg-flex-no-dropdown { + position: relative !important; + display: flex !important; + border: none; + box-shadow: none; + bottom: auto !important; + transform: none !important; + } +} + +.dropdown-menu.d-lg-flex-no-dropdown { + .d-flex { + margin-bottom: 5px; + } +} diff --git a/stock_barcodes/static/src/utils/barcodes_models_utils.esm.js b/stock_barcodes/static/src/utils/barcodes_models_utils.esm.js index 3a4b49f10ab5..54731d651fa4 100644 --- a/stock_barcodes/static/src/utils/barcodes_models_utils.esm.js +++ b/stock_barcodes/static/src/utils/barcodes_models_utils.esm.js @@ -18,6 +18,7 @@ export const barcodeModels = [ /** * Helper to know if the given model is allowed * + * @param {String} modelName * @returns {Boolean} */ export function isAllowedBarcodeModel(modelName) { diff --git a/stock_barcodes/static/src/views/actions/stock_barcode_main_menu.esm.js b/stock_barcodes/static/src/views/actions/stock_barcode_main_menu.esm.js new file mode 100644 index 000000000000..4cfed06d55d5 --- /dev/null +++ b/stock_barcodes/static/src/views/actions/stock_barcode_main_menu.esm.js @@ -0,0 +1,88 @@ +/** @odoo-module **/ +import {_t} from "@web/core/l10n/translation"; +import {browser} from "@web/core/browser/browser"; +import {markup} from "@odoo/owl"; +import {registry} from "@web/core/registry"; +import {useService} from "@web/core/utils/hooks"; + +const {Component, onWillStart, useEffect} = owl; + +export class StockBarcodesMainMenu extends Component { + setup() { + super.setup(); + this.actionService = useService("action"); + this.ormService = useService("orm"); + const busService = useService("bus_service"); + const notification = useService("notification"); + this.modelBarcodeAction = "stock.barcodes.action"; + if (this.hasService("home_menu")) + this.homeMenuService = useService("home_menu"); + onWillStart(async () => { + this.barcodeActions = await this.getBarcodeActions(); + }); + + const handleNotification = ({detail: notifications}) => { + if (notifications && notifications.length > 0) { + notifications.forEach((notif) => { + const {payload, type} = notif; + if (type === "actions_main_menu_barcode") { + if (payload.action_ok && payload.action) { + this.actionService.doAction(payload.action); + } else { + notification.add( + _t("No action found with barcode: " + payload.barcode), + { + type: "danger", + } + ); + } + } + }); + } + }; + useEffect(() => { + busService.addChannel("stock_barcodes_main_menu"); + busService.addEventListener("notification", handleNotification); + return () => { + busService.deleteChannel("stock_barcodes_main_menu"); + busService.removeEventListener("notification", handleNotification); + }; + }); + } + + hasService(service) { + return service in this.env.services; + } + + mainMenuHome() { + // Enterprise + if (this.hasService("home_menu")) { + this.homeMenuService.toggle(true); + } else { + // Community + this.actionService.doAction("mail.action_discuss"); + browser.setTimeout(() => browser.location.reload(), 100); + } + } + + async openAction(action_id) { + const action = await this.ormService.call( + this.modelBarcodeAction, + "open_action", + [action_id] + ); + action.help = markup(_t(action.help)); + this.actionService.doAction(action); + } + + async getBarcodeActions() { + return await this.ormService.call(this.modelBarcodeAction, "search_read", [], { + domain: [["action_window_id", "!=", false]], + fields: ["id", "name", "icon_class"], + }); + } +} + +StockBarcodesMainMenu.template = "stock_barcodes.MainMenu"; + +registry.category("actions").add("stock_barcodes_main_menu", StockBarcodesMainMenu); diff --git a/stock_barcodes/static/src/views/actions/stock_barcode_main_menu.scss b/stock_barcodes/static/src/views/actions/stock_barcode_main_menu.scss new file mode 100644 index 000000000000..e11be5b2382a --- /dev/null +++ b/stock_barcodes/static/src/views/actions/stock_barcode_main_menu.scss @@ -0,0 +1,74 @@ +@keyframes o_barcode_scanner_intro { + 25% { + top: 75%; + } + 50% { + top: 0; + } + 75% { + top: 100%; + } + 100% { + top: 50%; + } +} + +div.o_action_manager { + &:has(div.stock-barcodes-main-menu) { + overflow-y: scroll !important; + background-color: $o-community-color !important; + @include media-breakpoint-down(sm) { + overflow: scroll !important; + } + } +} + +div.stock-barcodes-main-menu { + background-color: white !important; + margin: 0 10% 5% 10% !important; + border-radius: 5px !important; + min-height: 90% !important; + @include media-breakpoint-down(sm) { + margin: 0 !important; + } + + img { + height: 220px !important; + } + + div.o_stock_barcode_functions { + margin-top: 5rem; + @include media-breakpoint-down(sm) { + margin-top: 3.6rem; + } + } + + div.o_stock_barcode_buttons { + button { + padding: 1.5rem 1.5rem !important; + } + } + + span.o_stock_barcode_laser { + @include o-position-absolute(33%, -15px, auto, -15px); + height: 5px; + background: rgba(red, 0.6); + box-shadow: 0 1px 10px 1px rgba(red, 0.8); + animation: o_barcode_scanner_intro 1s cubic-bezier(0.6, -0.28, 0.735, 0.045) + 0.4s; + width: 26%; + margin-left: 38%; + @include media-breakpoint-down(sm) { + @include o-position-absolute(35%, -15px, auto, -15px); + width: 95%; + margin-left: 6%; + } + } + + div.o_stock_barcode_header_home { + padding-right: 45% !important; + @include media-breakpoint-down(sm) { + padding-right: 27% !important; + } + } +} diff --git a/stock_barcodes/static/src/views/actions/stock_barcode_main_menu.xml b/stock_barcodes/static/src/views/actions/stock_barcode_main_menu.xml new file mode 100644 index 000000000000..09e4bbd041e1 --- /dev/null +++ b/stock_barcodes/static/src/views/actions/stock_barcode_main_menu.xml @@ -0,0 +1,45 @@ + + + +
    +
    + + + +

    Barcode Scanner

    +
    + +
    +
    + +
    + +
    +
    +
    +
    +
    + diff --git a/stock_barcodes/static/src/views/form/form_controller.esm.js b/stock_barcodes/static/src/views/form/form_controller.esm.js new file mode 100644 index 000000000000..1a71bb253785 --- /dev/null +++ b/stock_barcodes/static/src/views/form/form_controller.esm.js @@ -0,0 +1,71 @@ +/** @odoo-module */ +/* Copyright 2021 Tecnativa - Alexandre D. Díaz + * License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). */ + +import {onMounted, useEffect} from "@odoo/owl"; +import {FormController} from "@web/views/form/form_controller"; +import {useService} from "@web/core/utils/hooks"; + +export class StockBarcodesFormController extends FormController { + setup() { + super.setup(); + const busService = useService("bus_service"); + const ormService = useService("orm"); + this.enableApplyCount = false; + // Adds support to use control_pannel_hidden from the + // context to disable the control panel + if (this.props.context.control_panel_hidden) { + this.display.controlPanel = false; + } + + const handleNotification = ({detail: notifications}) => { + if (notifications && notifications.length > 0) { + notifications.forEach((notif) => { + const {payload, type} = notif; + if (type === "count_apply_inventory" && payload) { + this.countApplyInventory(payload.count); + } + }); + } + }; + useEffect(() => { + busService.addChannel("stock_barcodes_form_update"); + busService.addEventListener("notification", handleNotification); + const $applyInventory = $("span.count_apply_inventory"); + if ($applyInventory.length > 0) { + if (!this.enableApplyCount) { + this.countApplyInventory(1); + this.enableApplyCount = true; + } + } else { + this.enableApplyCount = false; + } + return () => { + busService.deleteChannel("stock_barcodes_form_update"); + busService.removeEventListener("notification", handleNotification); + }; + }); + + onMounted(async () => { + if (this.props.resModel === "wiz.stock.barcodes.read.inventory") { + const fields = ["count_inventory_quants"]; + const countApply = await ormService.call( + this.props.resModel, + "read", + [this.props.resId], + {fields} + ); + this.countApplyInventory( + countApply.length > 0 ? countApply[0].count_inventory_quants : 0 + ); + } + }); + } + + countApplyInventory(countApply = 0) { + const $countApply = $("span.count_apply_inventory"); + if ($countApply.length) { + $countApply.text(countApply); + } + } +} diff --git a/stock_barcodes/static/src/views/form/form_view.esm.js b/stock_barcodes/static/src/views/form/form_view.esm.js new file mode 100644 index 000000000000..a85ed91d1023 --- /dev/null +++ b/stock_barcodes/static/src/views/form/form_view.esm.js @@ -0,0 +1,14 @@ +/** @odoo-module */ +/* Copyright 2021 Tecnativa - Alexandre D. Díaz + * License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). */ + +import {StockBarcodesFormController} from "./form_controller.esm"; +import {formView} from "@web/views/form/form_view"; +import {registry} from "@web/core/registry"; + +export const StockBarcodesFormView = { + ...formView, + Controller: StockBarcodesFormController, +}; + +registry.category("views").add("stock_barcodes_form", StockBarcodesFormView); diff --git a/stock_barcodes/static/src/views/form_view.esm.js b/stock_barcodes/static/src/views/form_view.esm.js deleted file mode 100644 index c83da2bb2bb3..000000000000 --- a/stock_barcodes/static/src/views/form_view.esm.js +++ /dev/null @@ -1,17 +0,0 @@ -/** @odoo-module */ -/* Copyright 2021 Tecnativa - Alexandre D. Díaz - * License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). */ - -import {FormController} from "@web/views/form/form_controller"; -import {patch} from "@web/core/utils/patch"; - -patch(FormController.prototype, "Allow display.controlPanel overriding", { - setup() { - this._super(...arguments); - // Adds support to use control_pannel_hidden from the - // context to disable the control panel - if (this.props.context.control_panel_hidden) { - this.display.controlPanel = false; - } - }, -}); diff --git a/stock_barcodes/static/src/views/kanban/kanban_record.esm.js b/stock_barcodes/static/src/views/kanban/kanban_record.esm.js new file mode 100644 index 000000000000..bf94bdce333a --- /dev/null +++ b/stock_barcodes/static/src/views/kanban/kanban_record.esm.js @@ -0,0 +1,27 @@ +/** @odoo-module */ +/* Copyright 2022 Tecnativa - Alexandre D. Díaz + * License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). */ + +import {KanbanRecord} from "@web/views/kanban/kanban_record"; +import {patch} from "@web/core/utils/patch"; + +patch(KanbanRecord.prototype, "stock_barcodes.KanbanRecord", { + props: { + ...KanbanRecord.props, + }, + + setup() { + this._super(...arguments); + }, + + async onCustomGlobalClick() { + const record_barcode = $('div[name="inventory_quant_ids"]'); + if (record_barcode.length > 0) { + const record = this.props.record; + $("div.oe_kanban_operations").addClass("d-none"); + $("div.oe_kanban_operations-" + record.data.id).removeClass("d-none"); + return; + } + this._super.apply(this, arguments); + }, +}); diff --git a/stock_barcodes/static/src/views/kanban_renderer.esm.js b/stock_barcodes/static/src/views/kanban/kanban_renderer.esm.js similarity index 70% rename from stock_barcodes/static/src/views/kanban_renderer.esm.js rename to stock_barcodes/static/src/views/kanban/kanban_renderer.esm.js index e2a756c892b4..bd3f8468c2c4 100644 --- a/stock_barcodes/static/src/views/kanban_renderer.esm.js +++ b/stock_barcodes/static/src/views/kanban/kanban_renderer.esm.js @@ -2,14 +2,14 @@ /* Copyright 2022 Tecnativa - Alexandre D. Díaz * License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). */ +import {onPatched, useEffect, useRef} from "@odoo/owl"; +import {useBus, useService} from "@web/core/utils/hooks"; import {KanbanRenderer} from "@web/views/kanban/kanban_renderer"; -import {isAllowedBarcodeModel} from "../utils/barcodes_models_utils.esm"; +import {isAllowedBarcodeModel} from "../../utils/barcodes_models_utils.esm"; import {patch} from "@web/core/utils/patch"; -import {useBus} from "@web/core/utils/hooks"; import {useHotkey} from "@web/core/hotkeys/hotkey_hook"; -import {useRef} from "@odoo/owl"; -patch(KanbanRenderer.prototype, "add hotkey", { +patch(KanbanRenderer.prototype, "stock_barcodes.KanbanRenderer", { setup() { const rootRef = useRef("root"); useHotkey( @@ -40,6 +40,34 @@ patch(KanbanRenderer.prototype, "add hotkey", { ); this._super(...arguments); + this.ormService = useService("orm"); + this.action = useService("action"); + const busService = useService("bus_service"); + this.enableCurrentOperation = 0; + const handleNotification = ({detail: notifications}) => { + if (notifications && notifications.length > 0) { + notifications.forEach((notif) => { + const {payload, type} = notif; + if (type === "enable_operations" && payload) { + this.enableCurrentOperation = payload.id; + } + }); + } + }; + useEffect(() => { + busService.addChannel("stock_barcodes_kanban_update"); + busService.addEventListener("notification", handleNotification); + return () => { + busService.deleteChannel("stock_barcodes_kanban_update"); + busService.removeEventListener("notification", handleNotification); + }; + }); + + onPatched(() => { + $("div.oe_kanban_operations-" + this.enableCurrentOperation).removeClass( + "d-none" + ); + }); if (isAllowedBarcodeModel(this.props.list.resModel)) { if (this.env.searchModel) { @@ -63,6 +91,36 @@ patch(KanbanRenderer.prototype, "add hotkey", { }); } } + + this.showMessageScanProductPackage = + this.props.list.resModel === "stock.picking"; + }, + + getNextCard(direction, iCard, cards, iGroup, isGrouped) { + let nextCard = null; + switch (direction) { + case "down": + nextCard = iCard < cards[iGroup].length - 1 && cards[iGroup][iCard + 1]; + break; + case "up": + nextCard = iCard > 0 && cards[iGroup][iCard - 1]; + break; + case "right": + if (isGrouped) { + nextCard = iGroup < cards.length - 1 && cards[iGroup + 1][0]; + } else { + nextCard = iCard < cards[0].length - 1 && cards[0][iCard + 1]; + } + break; + case "left": + if (isGrouped) { + nextCard = iGroup > 0 && cards[iGroup - 1][0]; + } else { + nextCard = iCard > 0 && cards[0][iCard - 1]; + } + break; + } + return nextCard; }, // eslint-disable-next-line complexity @@ -77,6 +135,8 @@ patch(KanbanRenderer.prototype, "add hotkey", { * * @param {Node} area * @param {String} direction + * + * @returns {String/Boolean} */ focusNextCard(area, direction) { const {isGrouped} = this.props.list; @@ -117,33 +177,24 @@ patch(KanbanRenderer.prototype, "add hotkey", { iGroup = 0; } // Find next card to focus - let nextCard = null; - switch (direction) { - case "down": - nextCard = iCard < cards[iGroup].length - 1 && cards[iGroup][iCard + 1]; - break; - case "up": - nextCard = iCard > 0 && cards[iGroup][iCard - 1]; - break; - case "right": - if (isGrouped) { - nextCard = iGroup < cards.length - 1 && cards[iGroup + 1][0]; - } else { - nextCard = iCard < cards[0].length - 1 && cards[0][iCard + 1]; - } - break; - case "left": - if (isGrouped) { - nextCard = iGroup > 0 && cards[iGroup - 1][0]; - } else { - nextCard = iCard > 0 && cards[0][iCard - 1]; - } - break; - } + const nextCard = this.getNextCard(direction, iCard, cards, iGroup, isGrouped); if (nextCard && nextCard instanceof HTMLElement) { nextCard.focus(); return true; } }, + + async openBarcodeScanner() { + if (this.showMessageScanProductPackage) { + const action = await this.ormService.call( + "stock.picking", + "action_barcode_scan", + [false, false] + ); + this.action.doAction(action); + } + }, }); + +KanbanRenderer.template = "stock_barcodes.BarcodeKanbanRenderer"; diff --git a/stock_barcodes/static/src/views/kanban/kanban_view.esm.js b/stock_barcodes/static/src/views/kanban/kanban_view.esm.js new file mode 100644 index 000000000000..1e03eab1b81d --- /dev/null +++ b/stock_barcodes/static/src/views/kanban/kanban_view.esm.js @@ -0,0 +1,8 @@ +/** @odoo-module */ + +import {kanbanView} from "@web/views/kanban/kanban_view"; +import {registry} from "@web/core/registry"; + +registry.category("views").add("stock_barcodes_kanban", { + ...kanbanView, +}); diff --git a/stock_barcodes/static/src/views/kanban/stock_barcodes_kanban.xml b/stock_barcodes/static/src/views/kanban/stock_barcodes_kanban.xml new file mode 100644 index 000000000000..e160814042c9 --- /dev/null +++ b/stock_barcodes/static/src/views/kanban/stock_barcodes_kanban.xml @@ -0,0 +1,28 @@ + + + + + +
    + Scan a transfer, a product or a lot to filter your records + Scan a transfer or a product to filter your records + +
    +
    +
    + +
    diff --git a/stock_barcodes/static/src/views/views.esm.js b/stock_barcodes/static/src/views/views.esm.js index a68ff4045849..f16f9a4eb5e7 100644 --- a/stock_barcodes/static/src/views/views.esm.js +++ b/stock_barcodes/static/src/views/views.esm.js @@ -7,6 +7,7 @@ import {getVisibleElements, isVisible} from "@web/core/utils/ui"; import {FormController} from "@web/views/form/form_controller"; import {KanbanController} from "@web/views/kanban/kanban_controller"; import {ListController} from "@web/views/list/list_controller"; +import {_t} from "@web/core/l10n/translation"; import {isAllowedBarcodeModel} from "../utils/barcodes_models_utils.esm"; import {patch} from "@web/core/utils/patch"; import {useEffect} from "@odoo/owl"; @@ -109,8 +110,8 @@ function setupView() { notifications.forEach((notif) => { const {payload, type} = notif; if ( - (this.model.root.resModel == payload.res_model) & - (this.model.root.resId == payload.res_id) + (this.model.root.resModel === payload.res_model) & + (this.model.root.resId === payload.res_id) ) { if (type === "stock_barcodes_sound") { if (payload.sound === "ko") { @@ -118,8 +119,7 @@ function setupView() { } else { this.$sound_ok[0].play(); } - } - if (type === "stock_barcodes_focus") { + } else if (type === "stock_barcodes_focus") { requestIdleCallback(() => { const input = document.querySelector( `[name=${payload.field_name}] input` @@ -128,8 +128,7 @@ function setupView() { input.focus(); } }); - } - if (type === "stock_barcodes_notify") { + } else if (type === "stock_barcodes_notify") { notification.add(notif.payload.message, { title: notif.payload.title, type: notif.payload.type, @@ -137,6 +136,35 @@ function setupView() { }); } } + + if (type === "stock_barcodes_edit_manual") { + if (payload.manual_entry) { + this.env.bus.trigger("enableFormEditBarcode"); + } else if (!payload.manual_entry) { + this.env.bus.trigger("disableFormEditBarcode"); + } + } else if (type === "actions_barcode") { + if (payload.valid_picking) { + notification.add(_t("The transfer has been validated"), { + type: "success", + }); + } else if (payload.apply_inventory) { + actionService.doAction( + "stock_barcodes.action_stock_barcodes_action" + ); + notification.add( + _t("The inventory adjustment has been validated"), + { + type: "success", + } + ); + } + } else if (type === "actions_barcode_notification") { + notification.add(_t(payload.message), { + type: payload.message_type, + sticky: payload.sticky, + }); + } }); } }; diff --git a/stock_barcodes/static/src/widgets/boolean_toggle.esm.js b/stock_barcodes/static/src/widgets/boolean_toggle.esm.js index 723244ae9202..23b2c27ea733 100644 --- a/stock_barcodes/static/src/widgets/boolean_toggle.esm.js +++ b/stock_barcodes/static/src/widgets/boolean_toggle.esm.js @@ -3,9 +3,25 @@ * License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). */ import {BooleanToggleField} from "@web/views/fields/boolean_toggle/boolean_toggle_field"; +import {onMounted} from "@odoo/owl"; import {registry} from "@web/core/registry"; +import {useBus} from "@web/core/utils/hooks"; class BarcodeBooleanToggleField extends BooleanToggleField { + setup() { + super.setup(); + onMounted(() => { + this.enableFormEdit(this.props.value, true); + }); + + useBus(this.env.bus, "enableFormEditBarcode", () => + this.enableFormEdit(true, true) + ); + useBus(this.env.bus, "disableFormEditBarcode", () => + this.enableFormEdit(false, true) + ); + } + /* This is needed because, whenever we click the checkbox to enter data manually, the checkbox will be focused causing that when we scan the @@ -20,6 +36,36 @@ class BarcodeBooleanToggleField extends BooleanToggleField { requestIdleCallback(() => { document.activeElement.blur(); }); + this.enableFormEdit(newValue); + } + + enableFormEdit(newValue, editAction = false) { + // Enable edit form + if (this.props.name === "manual_entry" || editAction) { + const $form_edit = $("div.oe_stock_barcordes_content > div.scan_fields"); + const $div_inventory_quant_ids = $("div[name='inventory_quant_ids']").find( + "div.o_kanban_renderer" + ); + if ($form_edit.length > 0) { + if (newValue) { + $form_edit.removeClass("d-none"); + $div_inventory_quant_ids.addClass("inventory_quant_ids_with_form"); + $div_inventory_quant_ids.removeClass( + "inventory_quant_ids_without_form" + ); + } else { + $form_edit.addClass("d-none"); + $div_inventory_quant_ids.removeClass( + "inventory_quant_ids_with_form" + ); + $div_inventory_quant_ids.addClass( + "inventory_quant_ids_without_form" + ); + } + } else { + $div_inventory_quant_ids.addClass("inventory_quant_ids_without_form"); + } + } } } diff --git a/stock_barcodes/tests/common.py b/stock_barcodes/tests/common.py index 81067fb8f541..20bc159b507b 100644 --- a/stock_barcodes/tests/common.py +++ b/stock_barcodes/tests/common.py @@ -21,9 +21,12 @@ def setUpClass(cls): cls.Product = cls.env["product.product"] cls.ProductPackaging = cls.env["product.packaging"] cls.WizScanReadPicking = cls.env["wiz.stock.barcodes.read.picking"] + cls.WizScanReadInventory = cls.env["wiz.stock.barcodes.read.inventory"] + cls.WizCandidatePicking = cls.env["wiz.candidate.picking"] cls.StockProductionLot = cls.env["stock.lot"] cls.StockPicking = cls.env["stock.picking"] cls.StockQuant = cls.env["stock.quant"] + cls.StockBarcodeAction = cls.env["stock.barcodes.action"] cls.company = cls.env.company @@ -103,6 +106,28 @@ def setUpClass(cls): cls.wiz_scan = cls.WizScanReadPicking.create( {"option_group_id": cls.option_group.id, "step": 1} ) + cls.wiz_scan_read_inventory = cls.WizScanReadInventory.create( + {"option_group_id": cls.option_group.id, "step": 1} + ) + + cls.wiz_scan_candidate_picking = cls.WizCandidatePicking.create( + {"wiz_barcode_id": cls.wiz_scan.id} + ) + + # Barcode actions + cls.barcode_action_valid = cls.StockBarcodeAction.create( + { + "name": "Barcode action valid", + "action_window_id": cls.env.ref("stock.stock_picking_type_action").id, + "context": "{'search_default_barcode_options': 1}", + } + ) + + cls.barcode_action_invalid = cls.StockBarcodeAction.create( + { + "name": "Barcode action valid", + } + ) @classmethod def _create_barcode_option_group(cls): diff --git a/stock_barcodes/tests/test_stock_barcodes.py b/stock_barcodes/tests/test_stock_barcodes.py index 97dda779c6f2..28b2b688ea2e 100644 --- a/stock_barcodes/tests/test_stock_barcodes.py +++ b/stock_barcodes/tests/test_stock_barcodes.py @@ -1,8 +1,13 @@ # Copyright 2108-2019 Sergio Teruel # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import re + +from odoo.exceptions import ValidationError from odoo.tests.common import tagged +from odoo.addons.stock_barcodes.models.stock_barcodes_action import FIELDS_NAME, REGEX + from .common import TestCommonStockBarcodes @@ -42,7 +47,7 @@ def test_wizard_scan_package(self): self.wiz_scan.manual_entry = True self.wiz_scan.action_clean_values() self.action_barcode_scanned(self.wiz_scan, "5420008510489") - self.assertEqual(self.wiz_scan.packaging_qty, 0.0) + self.assertEqual(self.wiz_scan.packaging_qty, 1.0) self.wiz_scan.packaging_qty = 3.0 self.wiz_scan.onchange_packaging_qty() self.assertEqual(self.wiz_scan.product_qty, 15.0) @@ -75,3 +80,62 @@ def test_wiz_clean_lot(self): self.action_barcode_scanned(self.wiz_scan, "8411822222568") self.wiz_scan.action_clean_lot() self.assertFalse(self.wiz_scan.lot_id) + + def test_barcode_action(self): + self.assertTrue(self.barcode_action_valid.action_window_id) + self.assertEqual(bool(self.barcode_action_invalid.action_window_id), False) + + def test_action_back(self): + result = self.wiz_scan.action_back() + self.assertIn("name", result) + self.assertIn("type", result) + self.assertIn("res_model", result) + self.assertEqual(result["type"], "ir.actions.act_window") + + def test_barcode_context_action(self): + context = self.barcode_action_valid.context + self.assertTrue(bool(re.match(REGEX.get("context", ""), context))) + self.assertGreater(len(context), 0) + context = context.strip("{}").split(",") + field_values = context[0].split(":") + self.assertGreater(len(field_values), 1) + field_name = field_values[0].split("search_default_") + self.assertGreater(len(field_name), 1) + field_value_format = field_values[1].replace("'", "").strip() + self.assertTrue(field_value_format.isdigit()) + self.assertEqual(field_values[0].strip("'"), "search_default_barcode_options") + self.assertTrue(len(field_values[0].split("search_default_")), 2) + self.assertEqual(self.barcode_action_invalid._count_elements(), 0) + self.barcode_action_invalid.context = False + with self.assertRaises(TypeError): + self.barcode_action_invalid._compute_count_elements() + self.barcode_action_invalid.context = "{}" + self.assertFalse("search_default_" in self.barcode_action_invalid.context) + + self.assertEqual(self.barcode_action_invalid._count_elements(), 0) + self.barcode_action_valid.context = "{'search_default_code': 1}" + self.assertEqual(self.barcode_action_valid._count_elements(), 6) + field_value_name = ( + self.barcode_action_valid.context.strip("{}").split(",")[0].split(":") + ) + field_name = field_value_name[0].split("search_default_")[1].strip("'") + self.assertTrue("search_default_" in self.barcode_action_valid.context) + self.assertFalse( + hasattr( + self.barcode_action_valid.action_window_id.res_model, + FIELDS_NAME.get(field_name, field_name), + ) + ) + field_values = field_value_name[1].strip() + self.assertTrue(field_values.isdigit()) + + with self.assertRaises(IndexError): + self.barcode_action_invalid.context = "{'search_default_'}" + self.assertEqual(self.barcode_action_invalid._count_elements(), 0) + with self.assertRaises(ValidationError): + self.StockBarcodeAction.create( + { + "name": "Barcode action invalid with space", + "context": "{'search_default_code': 'incoming'} ", + } + ) diff --git a/stock_barcodes/tests/test_stock_barcodes_picking.py b/stock_barcodes/tests/test_stock_barcodes_picking.py index acfac5f51836..6ec0242dfb08 100644 --- a/stock_barcodes/tests/test_stock_barcodes_picking.py +++ b/stock_barcodes/tests/test_stock_barcodes_picking.py @@ -1,5 +1,6 @@ # Copyright 2108-2019 Sergio Teruel # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo.exceptions import MissingError, UserError from odoo.tests.common import tagged from .common import TestCommonStockBarcodes @@ -140,7 +141,10 @@ def test_wiz_picking_values(self): ) def test_picking_wizard_scan_product(self): - wiz_scan_picking = self.wiz_scan_picking.with_context(force_create_move=True) + # self.wiz_scan_picking.manual_entry = True + wiz_scan_picking = self.wiz_scan_picking.with_context( + force_create_move=True, no_increase_qty_done=True + ) self.action_barcode_scanned(wiz_scan_picking, "8480000723208") sml = self.picking_in_01.move_line_ids.filtered( lambda x: x.product_id == self.product_wo_tracking @@ -166,9 +170,9 @@ def test_picking_wizard_scan_product(self): self.assertEqual(sml.qty_done, 1.0) self.action_barcode_scanned(wiz_scan_picking, "8433281006850") stock_move = sml.move_id - self.assertEqual(sum(stock_move.move_line_ids.mapped("qty_done")), 2.0) + self.assertEqual(sum(stock_move.move_line_ids.mapped("qty_done")), 1.0) self.action_barcode_scanned(wiz_scan_picking, "8411822222568") - self.assertEqual(sum(stock_move.move_line_ids.mapped("qty_done")), 3.0) + self.assertEqual(sum(stock_move.move_line_ids.mapped("qty_done")), 1.0) self.assertEqual( self.wiz_scan_picking.message, "8411822222568 (Scan Product, Packaging, Lot / Serial)", @@ -176,10 +180,12 @@ def test_picking_wizard_scan_product(self): # Scan a package self.action_barcode_scanned(wiz_scan_picking, "5420008510489") # Package of 5 product units. Already three unit exists - self.assertEqual(sum(stock_move.move_line_ids.mapped("qty_done")), 8.0) + self.assertEqual(sum(stock_move.move_line_ids.mapped("qty_done")), 5.0) def test_picking_wizard_scan_product_manual_entry(self): - wiz_scan_picking = self.wiz_scan_picking.with_context(force_create_move=True) + wiz_scan_picking = self.wiz_scan_picking.with_context( + force_create_move=True, no_increase_qty_done=True + ) wiz_scan_picking.manual_entry = True self.action_barcode_scanned(wiz_scan_picking, "8480000723208") sml = self.picking_in_01.move_line_ids.filtered( @@ -203,8 +209,10 @@ def test_barcode_from_operation(self): self.wiz_scan_picking.lot_id = self.lot_1 self.wiz_scan_picking.product_qty = 2 - self.wiz_scan_picking.with_context(force_create_move=True).action_confirm() - self.assertEqual(len(self.wiz_scan_picking.candidate_picking_ids), 2) + self.wiz_scan_picking.with_context( + force_create_move=True, no_increase_qty_done=True + ).action_confirm() + self.assertEqual(len(self.wiz_scan_picking.candidate_picking_ids[0:2]), 2) # Lock first picking candidate = self.wiz_scan_picking.candidate_picking_ids.filtered( lambda c: c.picking_id == self.picking_out_01 @@ -215,21 +223,27 @@ def test_barcode_from_operation(self): candidate_wiz.with_context(force_create_move=True).action_lock_picking() self.assertEqual(self.picking_out_01.move_ids.quantity_done, 2) self.wiz_scan_picking.product_qty = 2 - self.wiz_scan_picking.with_context(force_create_move=True).action_confirm() - self.assertEqual(self.picking_out_01.move_ids.quantity_done, 4) + self.wiz_scan_picking.with_context( + force_create_move=True, no_increase_qty_done=True + ).action_confirm() + self.assertEqual(self.picking_out_01.move_ids.quantity_done, 2) # Picking out 3 is in confirmed state, so until confirmed moves has # not been activated candidate pickings is 2 picking_out_3.action_confirm() candidate_wiz.action_unlock_picking() self.wiz_scan_picking.product_qty = 2 - self.wiz_scan_picking.with_context(force_create_move=True).action_confirm() - self.assertEqual(len(self.wiz_scan_picking.candidate_picking_ids), 2) + self.wiz_scan_picking.with_context( + force_create_move=True, no_increase_qty_done=True + ).action_confirm() + self.assertEqual(len(self.wiz_scan_picking.candidate_picking_ids[0:2]), 2) candidate_wiz.action_unlock_picking() self.wiz_scan_picking.product_qty = 2 self.wiz_scan_picking.option_group_id.confirmed_moves = True - self.wiz_scan_picking.with_context(force_create_move=True).action_confirm() - self.assertEqual(len(self.wiz_scan_picking.candidate_picking_ids), 3) + self.wiz_scan_picking.with_context( + force_create_move=True, no_increase_qty_done=True + ).action_confirm() + self.assertEqual(len(self.wiz_scan_picking.candidate_picking_ids[0:3]), 3) def test_picking_wizard_scan_product_auto_lot(self): # Prepare more data @@ -453,3 +467,73 @@ def _create_barcode_option_group_outgoing(cls): ], } ) + + def test_stock_picking_validate(self): + self.picking_in_01.state = False + with self.assertRaises(UserError): + self.picking_in_01.with_context( + stock_barcodes_validate_picking=True + ).button_validate() + + def test_barcode_read_picking(self): + self.picking_in_01.state = "done" + self.wiz_scan_picking._compute_enable_add_product() + self.assertFalse(self.wiz_scan_picking.enable_add_product) + + self.wiz_scan_picking.show_detailed_operations = False + self.wiz_scan_picking.action_show_detailed_operations() + self.assertTrue(self.wiz_scan_picking.action_show_detailed_operations) + + self.wiz_scan_picking.action_show_detailed_operations() + self.assertFalse(self.wiz_scan_picking.show_detailed_operations) + + def test_barcode_read_inventory(self): + context = { + "params": { + "model": "wiz.stock.barcodes.read.inventory", + "id": self.quant_lot_1.id, + } + } + with self.assertRaises(MissingError): + self.quant_lot_1.with_context( + **context + ).action_barcode_inventory_quant_unlink() + context = { + "params": { + "model": self.wiz_scan_read_inventory._name, + "id": self.wiz_scan_read_inventory.id, + } + } + self.quant_lot_1.with_context(**context).action_barcode_inventory_quant_unlink() + self.assertIsNone( + self.quant_lot_1.with_context( + **context + ).action_barcode_inventory_quant_unlink() + ) + self.assertIsNone(self.quant_lot_1.enable_current_operations()) + self.assertIsNone(self.quant_lot_1.action_barcode_inventory_quant_edit()) + with self.assertRaises(ValueError): + self.quant_lot_1.write({"inventory_quantity": "test"}) + self.quant_lot_1.operation_quantities_rest() + self.quant_lot_1.operation_quantities() + self.assertEqual( + type(self.picking_in_01.picking_type_id.get_action_picking_tree_ready()), + dict, + ) + self.assertEqual( + type( + self.picking_in_01.picking_type_id.with_context( + **{"operations_mode": True} + ).get_action_picking_tree_ready() + ), + dict, + ) + self.assertIsNone(self.wiz_scan_candidate_picking._compute_picking_quantity()) + self.assertIsNone(self.wiz_scan_candidate_picking._compute_is_pending()) + self.assertEqual( + self.wiz_scan_candidate_picking._get_picking_to_validate()._name, + self.picking_in_01._name, + ) + self.assertEqual( + type(self.wiz_scan_candidate_picking.action_validate_picking()), tuple + ) diff --git a/stock_barcodes/views/stock_barcodes_action_view.xml b/stock_barcodes/views/stock_barcodes_action_view.xml index b0b732286e89..8b784fb2c7ee 100644 --- a/stock_barcodes/views/stock_barcodes_action_view.xml +++ b/stock_barcodes/views/stock_barcodes_action_view.xml @@ -1,20 +1,18 @@ - - - wiz.stock.barcodes.read.picking - Barcodes actions - form - {'control_panel_hidden': True, 'default_display_menu': True} - stock.barcodes.action.tree stock.barcodes.action +
    +
    @@ -22,15 +20,81 @@ +
    + stock.barcodes.action Barcodes actions tree + + + + stock.barcodes.action.kanban + stock.barcodes.action + + + + + + + + + +
    + + +
    +
    + + +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    + + + + + stock.barcodes.action + Barcodes + kanban + + [('action_window_id', '!=', False)] + fullscreen + + + + + Barcodes + stock_barcodes_main_menu + fullscreen + + + +
    diff --git a/stock_barcodes/views/stock_picking_views.xml b/stock_barcodes/views/stock_picking_views.xml index 246d86d32b96..77a63b7e88ed 100644 --- a/stock_barcodes/views/stock_picking_views.xml +++ b/stock_barcodes/views/stock_picking_views.xml @@ -24,16 +24,14 @@ stock.picking - - + + object + + + action_barcode_scan + + + stock_barcodes_kanban @@ -109,4 +107,25 @@ + + + + Operations + stock.picking + ir.actions.act_window + kanban + {'contact_display': 'partner_address', 'search_default_available': 1} + + + +

    + No transfer found. Let's create one! +

    +

    + Transfers allow you to move products from one location to another. +

    +
    +
    diff --git a/stock_barcodes/wizard/__init__.py b/stock_barcodes/wizard/__init__.py index 34939fdb4fe3..63610423be76 100644 --- a/stock_barcodes/wizard/__init__.py +++ b/stock_barcodes/wizard/__init__.py @@ -1,5 +1,6 @@ from . import stock_barcodes_read from . import stock_barcodes_read_inventory +from . import stock_barcodes_candidate_picking from . import stock_barcodes_read_picking from . import stock_barcodes_read_todo from . import stock_production_lot diff --git a/stock_barcodes/wizard/stock_barcodes_candidate_picking.py b/stock_barcodes/wizard/stock_barcodes_candidate_picking.py new file mode 100644 index 000000000000..d12674bfff69 --- /dev/null +++ b/stock_barcodes/wizard/stock_barcodes_candidate_picking.py @@ -0,0 +1,148 @@ +# Copyright 2019 Sergio Teruel +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).f +from odoo import api, fields, models + + +class WizCandidatePicking(models.TransientModel): + + _name = "wiz.candidate.picking" + _description = "Candidate pickings for barcode interface" + # To prevent remove the record wizard until 2 days old + _transient_max_hours = 48 + + wiz_barcode_id = fields.Many2one( + comodel_name="wiz.stock.barcodes.read.picking", readonly=True + ) + picking_id = fields.Many2one( + comodel_name="stock.picking", string="Picking", readonly=True + ) + wiz_picking_id = fields.Many2one( + comodel_name="stock.picking", + related="wiz_barcode_id.picking_id", + string="Wizard Picking", + readonly=True, + ) + name = fields.Char( + related="picking_id.name", readonly=True, string="Candidate Picking" + ) + partner_id = fields.Many2one( + comodel_name="res.partner", + related="picking_id.partner_id", + readonly=True, + string="Partner", + ) + state = fields.Selection(related="picking_id.state", readonly=True) + date = fields.Datetime( + related="picking_id.date", readonly=True, string="Creation Date" + ) + product_qty_reserved = fields.Float( + "Reserved", + compute="_compute_picking_quantity", + digits="Product Unit of Measure", + readonly=True, + ) + product_uom_qty = fields.Float( + "Demand", + compute="_compute_picking_quantity", + digits="Product Unit of Measure", + readonly=True, + ) + product_qty_done = fields.Float( + "Done", + compute="_compute_picking_quantity", + digits="Product Unit of Measure", + readonly=True, + ) + # For reload kanban view + scan_count = fields.Integer() + is_pending = fields.Boolean(compute="_compute_is_pending") + note = fields.Html(related="picking_id.note") + + @api.depends("scan_count") + def _compute_picking_quantity(self): + for candidate in self: + qty_reserved = 0 + qty_demand = 0 + qty_done = 0 + candidate.product_qty_reserved = sum( + candidate.picking_id.mapped("move_ids.reserved_availability") + ) + for move in candidate.picking_id.move_ids: + qty_reserved += move.reserved_availability + qty_demand += move.product_uom_qty + qty_done += move.quantity_done + candidate.update( + { + "product_qty_reserved": qty_reserved, + "product_uom_qty": qty_demand, + "product_qty_done": qty_done, + } + ) + + @api.depends("scan_count") + def _compute_is_pending(self): + for rec in self: + rec.is_pending = bool(rec.wiz_barcode_id.pending_move_ids) + + def _get_wizard_barcode_read(self): + return self.env["wiz.stock.barcodes.read.picking"].browse( + self.env.context["wiz_barcode_id"] + ) + + def action_lock_picking(self): + wiz = self._get_wizard_barcode_read() + picking_id = self.env.context["picking_id"] + wiz.picking_id = picking_id + wiz._set_candidate_pickings(wiz.picking_id) + return wiz.action_confirm() + + def action_unlock_picking(self): + wiz = self._get_wizard_barcode_read() + wiz.update( + { + "picking_id": False, + "candidate_picking_ids": False, + "message_type": False, + "message": False, + } + ) + return wiz.action_cancel() + + def _get_picking_to_validate(self): + """Inject context show_picking_type_action_tree to redirect to picking list + after validate picking in barcodes environment. + The stock_barcodes_validate_picking key allows to know when a picking has been + validated from stock barcodes interface. + """ + return ( + self.env["stock.picking"] + .browse(self.env.context.get("picking_id", False)) + .with_context( + show_picking_type_action_tree=True, stock_barcodes_validate_picking=True + ) + ) + + def action_validate_picking(self): + context = dict(self.env.context) + picking = self._get_picking_to_validate() + if picking._check_immediate(): + return False, picking.with_context( + button_validate_picking_ids=picking.ids, operations_mode=True + )._action_generate_immediate_wizard( + show_transfers=picking._should_show_transfers() + ) + return ( + True, + picking.with_context( + skip_sms=context.get("skip_sms", False) + ).button_validate(), + ) + + def action_open_picking(self): + picking = self.env["stock.picking"].browse( + self.env.context.get("picking_id", False) + ) + return picking.with_context(control_panel_hidden=False).get_formview_action() + + def action_put_in_pack(self): + self.picking_id.action_put_in_pack() diff --git a/stock_barcodes/wizard/stock_barcodes_read.py b/stock_barcodes/wizard/stock_barcodes_read.py index cfe9a17198e5..b552e7f0ba5d 100644 --- a/stock_barcodes/wizard/stock_barcodes_read.py +++ b/stock_barcodes/wizard/stock_barcodes_read.py @@ -6,6 +6,8 @@ _logger = logging.getLogger(__name__) +TYPE_ERROR = ["more_match", "not_found"] + class WizStockBarcodesRead(models.AbstractModel): _name = "wiz.stock.barcodes.read" @@ -14,6 +16,7 @@ class WizStockBarcodesRead(models.AbstractModel): # To prevent remove the record wizard until 2 days old _transient_max_hours = 48 _allowed_product_types = ["product", "consu"] + _rec_name = "barcode" barcode = fields.Char() res_model_id = fields.Many2one(comodel_name="ir.model", index=True) @@ -48,6 +51,7 @@ class WizStockBarcodesRead(models.AbstractModel): message_type = fields.Selection( [ ("info", "Barcode read with additional info"), + ("info_page", "Info page"), ("not_found", "No barcode found"), ("more_match", "More than one matches found"), ("success", "Barcode read correctly"), @@ -92,9 +96,13 @@ class WizStockBarcodesRead(models.AbstractModel): string="Product Qty. Done", digits="Product Unit of Measure", store=False ) + enable_add_product = fields.Boolean(default=True) + @api.depends("res_id") def _compute_action_ids(self): - actions = self.env["stock.barcodes.action"].search([]) + actions = self.env["stock.barcodes.action"].search( + [("action_window_id", "!=", False)] + ) self.action_ids = actions @api.depends("option_group_id") @@ -146,14 +154,36 @@ def _set_messagge_info(self, message_type, message): For manual entry mode barcode is not set so is not displayed """ self.message_type = message_type - # TODO: Uncomment this line when the tests of all modules have been adapted # if self.barcode and self.message_type in ["more_match", "not_found"]: if self.barcode: self.message = _( "%(barcode)s (%(message)s)", barcode=self.barcode, message=message ) else: - self.message = "%s" % message + if message_type in TYPE_ERROR: + self.manual_entry = True + self.send_bus_done( + "stock_barcodes_scan", + "actions_barcode_notification", + { + "message": message, + "sticky": True, + "message_type": "danger" + if message_type in TYPE_ERROR + else message_type, + }, + ) + elif message_type != "info_page": + self.send_bus_done( + "stock_barcodes_scan", + "actions_barcode_notification", + { + "message": message, + "message_type": message_type, + }, + ) + else: + self.message = "%s" % message def process_barcode_location_id(self): location = self.env["stock.location"].search(self._barcode_domain(self.barcode)) @@ -361,52 +391,73 @@ def process_barcode_packaging_id(self): return False def process_barcode(self, barcode): - self._set_messagge_info("success", _("OK")) - options = self.option_group_id.option_ids - barcode_found = False - options_to_scan = options.filtered("to_scan") - options_required = options.filtered("required") - options_to_scan = options_to_scan.filtered(lambda op: op.step == self.step) - for option in options_to_scan: - if ( - self.option_group_id.ignore_filled_fields - and option in options_required - and getattr(self, option.field_name, False) - ): - continue - option_func = getattr(self, "process_barcode_%s" % option.field_name, False) - if option_func: - res = option_func() - if res: - barcode_found = True - self.play_sounds(barcode_found) - break - elif self.message_type != "success": - self.play_sounds(False) - return False - if not barcode_found: - self.play_sounds(barcode_found) - if self.option_group_id.ignore_filled_fields: - self._set_messagge_info( - "info", _("Barcode not found or field already filled") + if not self: + barcode_action = self.env["stock.barcodes.action"].search( + [ + ("action_window_id", "!=", False), + ("barcode", "=", barcode), + ], + limit=1, + ) + + self.env["bus.bus"]._sendone( + "stock_barcodes_scan", + "actions_main_menu_barcode", + { + "action_ok": len(barcode_action) > 0, + "action": barcode_action.open_action() if barcode_action else "", + "barcode": barcode, + }, + ) + else: + self._set_messagge_info("success", _("OK")) + options = self.option_group_id.option_ids + barcode_found = False + options_to_scan = options.filtered("to_scan") + options_required = options.filtered("required") + options_to_scan = options_to_scan.filtered(lambda op: op.step == self.step) + for option in options_to_scan: + if ( + self.option_group_id.ignore_filled_fields + and option in options_required + and getattr(self, option.field_name, False) + ): + continue + option_func = getattr( + self, "process_barcode_%s" % option.field_name, False ) - else: - self._set_messagge_info( - "not_found", _("Barcode not found with this screen values") + if option_func: + res = option_func() + if res: + barcode_found = True + self.play_sounds(barcode_found) + break + elif self.message_type != "success": + self.play_sounds(False) + return False + if not barcode_found: + self.play_sounds(barcode_found) + if self.option_group_id.ignore_filled_fields: + self._set_messagge_info( + "not_found", _("Barcode not found or field already filled") + ) + else: + self._set_messagge_info( + "not_found", _("Barcode not found with this screen values") + ) + self.display_notification( + self.barcode, + message_type="danger", + title=_("Barcode not found"), + sticky=False, ) - self.display_notification( - self.barcode, - message_type="danger", - title=_("Barcode not found"), - sticky=False, - ) - return False - if not self.check_option_required(): - return False - if self.is_manual_confirm or self.manual_entry: - self._set_messagge_info("info", _("Review and confirm")) - return False - return self.action_confirm() + return False + if not self.check_option_required(): + return False + if self.is_manual_confirm or self.manual_entry: + self._set_messagge_info("info", _("Review and confirm")) + return False + return self.action_confirm() def check_option_required(self): options = self.option_group_id.option_ids @@ -626,16 +677,16 @@ def action_clean_values(self): def action_manual_entry(self): return True - # TODO: To remove when stock_move_location uses action_clean_values def reset_qty(self): self.product_qty = 0 self.packaging_qty = 0 def open_actions(self): self.display_menu = True + return self.env.ref("stock_barcodes.action_stock_barcodes_action").read()[0] def action_back(self): - self.display_menu = False + return self.env.ref("stock.stock_picking_type_action").read()[0] def open_records(self): action = self.action_ids @@ -688,7 +739,7 @@ def action_show_step(self): lambda op: op.step == self.step and op.to_scan ) self._set_messagge_info( - "info", _("Scan {}").format(", ".join(options.mapped("name"))) + "info_page", _("Scan {}").format(", ".join(options.mapped("name"))) ) @api.onchange("package_id") @@ -704,12 +755,48 @@ def action_confirm(self): record = self.browse(self.ids) record.write(self._convert_to_write(self._cache)) self = record - res = self.action_done() + no_increase_qty_done, force_create_move = False, False + if self._name == "wiz.stock.barcodes.read.picking": + no_increase_qty_done = ( + self.env.context.get("no_increase_qty_done", False) or self.manual_entry + ) + force_create_move = True + res = self.with_context( + no_increase_qty_done=no_increase_qty_done, + force_create_move=force_create_move, + ).action_done() self.invalidate_recordset() self.play_sounds(res) self._set_focus_on_qty_input() + + # Hide Form Edit + self.manual_entry = False + self.send_bus_done( + "stock_barcodes_scan", + "stock_barcodes_edit_manual", + { + "manual_entry": False, + }, + ) + + # Count elements for apply in inventory + if self._name == "wiz.stock.barcodes.read.inventory": + self.display_read_quant = True + self._compute_count_inventory_quants() + self.send_bus_done( + "stock_barcodes_form_update", + "count_apply_inventory", + {"count": self.count_inventory_quants}, + ) + return res + def action_add_scan_manual(self): + self.manual_entry = True + self.send_bus_done( + "stock_barcodes_scan", "stock_barcodes_edit_manual", {"manual_entry": True} + ) + def process_lot_before_done(self): if ( not self.lot_id @@ -723,13 +810,14 @@ def process_lot_before_done(self): def play_sounds(self, res): if res: - self.env["bus.bus"]._sendone( + self.send_bus_done( "stock_barcodes_scan", "stock_barcodes_sound", {"sound": "ok", "res_model": self._name, "res_id": self.ids[0]}, ) + else: - self.env["bus.bus"]._sendone( + self.send_bus_done( "stock_barcodes_scan", "stock_barcodes_sound", {"sound": "ko", "res_model": self._name, "res_id": self.ids[0]}, @@ -740,7 +828,8 @@ def _set_focus_on_qty_input(self, field_name=None): field_name = "product_qty" if field_name == "product_qty" and self.packaging_id: field_name = "packaging_qty" - self.env["bus.bus"]._sendone( + + self.send_bus_done( "stock_barcodes_scan", "stock_barcodes_focus", { @@ -805,7 +894,7 @@ def display_notification( } if title: message["title"] = title - self.env["bus.bus"]._sendone( + self.send_bus_done( "stock_barcodes-{}".format(self.ids[0]), "stock_barcodes_notify-{}".format(self.ids[0]), message, diff --git a/stock_barcodes/wizard/stock_barcodes_read_inventory.py b/stock_barcodes/wizard/stock_barcodes_read_inventory.py index 5c6ea7b2da52..5eaed833719b 100644 --- a/stock_barcodes/wizard/stock_barcodes_read_inventory.py +++ b/stock_barcodes/wizard/stock_barcodes_read_inventory.py @@ -17,7 +17,18 @@ class WizStockBarcodesReadInventory(models.TransientModel): inventory_quant_ids = fields.Many2many( comodel_name="stock.quant", compute="_compute_inventory_quant_ids" ) - display_read_quant = fields.Boolean(string="Read items") + count_inventory_quants = fields.Integer( + compute="_compute_count_inventory_quants", store=True + ) + display_read_quant = fields.Boolean(string="Read items", default=True) + + def action_display_read_quant(self): + self.display_read_quant = not self.display_read_quant + + @api.depends("inventory_quant_ids") + def _compute_count_inventory_quants(self): + for wiz in self: + wiz.count_inventory_quants = len(wiz.inventory_quant_ids) @api.depends("display_read_quant") def _compute_inventory_quant_ids(self): @@ -26,7 +37,7 @@ def _compute_inventory_quant_ids(self): ("user_id", "=", self.env.user.id), ("inventory_date", "<=", fields.Date.context_today(self)), ] - if self.display_read_quant: + if wiz.display_read_quant: domain.append(("inventory_quantity_set", "=", True)) order = "write_date DESC" else: @@ -44,6 +55,13 @@ def _compute_inventory_quant_ids(self): ) wiz.inventory_quant_ids = quants + # UPDATE: Count elements for apply in inventory + wiz.send_bus_done( + "stock_barcodes_form_update", + "count_apply_inventory", + {"count": wiz.count_inventory_quants}, + ) + def _prepare_stock_quant_values(self): return { "product_id": self.product_id.id, @@ -114,6 +132,16 @@ def action_manual_entry(self): def action_clean_values(self): res = super().action_clean_values() self.inventory_product_qty = 0.0 + self.package_id = False + # Hide Form Edit + self.manual_entry = False + self.send_bus_done( + "stock_barcodes_scan", + "stock_barcodes_edit_manual", + { + "manual_entry": False, + }, + ) return res @api.onchange("product_id") diff --git a/stock_barcodes/wizard/stock_barcodes_read_inventory_views.xml b/stock_barcodes/wizard/stock_barcodes_read_inventory_views.xml index 00ef6cc1f435..384ff9ffd236 100644 --- a/stock_barcodes/wizard/stock_barcodes_read_inventory_views.xml +++ b/stock_barcodes/wizard/stock_barcodes_read_inventory_views.xml @@ -15,7 +15,7 @@
    Package - [('usage', 'in', ['internal', 'transit'])] + [('usage', 'in', ['internal', 'transit'])] + - - - - - - - - - +
    +
    +
    +
    + Lot S/N: + +
    +
    + + +
    -
    -
    - Loc: +
    +
    + +
    - -
    - Lot: -
    -
    - -
    - Package: -
    -
    @@ -156,22 +144,43 @@ -
    -
    - -
    -
    + + + @@ -190,6 +199,6 @@ } - current + fullscreen diff --git a/stock_barcodes/wizard/stock_barcodes_read_picking.py b/stock_barcodes/wizard/stock_barcodes_read_picking.py index 312db4984678..b50e00a71bd1 100644 --- a/stock_barcodes/wizard/stock_barcodes_read_picking.py +++ b/stock_barcodes/wizard/stock_barcodes_read_picking.py @@ -29,13 +29,16 @@ def _field_candidate_ids(self): picking_ids = fields.Many2many( comodel_name="stock.picking", string="Pickings", readonly=True ) + candidate_picking_id = fields.Many2one( + comodel_name="stock.picking", related="candidate_picking_ids.picking_id" + ) candidate_picking_ids = fields.One2many( comodel_name="wiz.candidate.picking", inverse_name="wiz_barcode_id", string="Candidate pickings", readonly=True, ) - # TODO: Remove this field + picking_product_qty = fields.Float( string="Picking quantities", digits="Product Unit of Measure", readonly=True ) @@ -43,7 +46,7 @@ def _field_candidate_ids(self): [("incoming", "Vendors"), ("outgoing", "Customers"), ("internal", "Internal")], "Type of Operation", ) - # TODO: Check if move_line_ids is used + move_line_ids = fields.One2many( comodel_name="stock.move.line", compute="_compute_move_line_ids" ) @@ -66,7 +69,7 @@ def _field_candidate_ids(self): comodel_name="wiz.stock.barcodes.read.todo" ) show_detailed_operations = fields.Boolean( - related="option_group_id.show_detailed_operations" + related="option_group_id.show_detailed_operations", default=True, store=True ) keep_screen_values = fields.Boolean(related="option_group_id.keep_screen_values") # Extended from stock_barcodes_read base model @@ -79,6 +82,16 @@ def _field_candidate_ids(self): todo_line_is_extra_line = fields.Boolean(related="todo_line_id.is_extra_line") forced_todo_key = fields.Char() qty_available = fields.Float(compute="_compute_qty_available") + partner_id = fields.Many2one("res.partner", related="picking_id.partner_id") + enable_add_product = fields.Boolean(compute="_compute_enable_add_product") + + def action_show_detailed_operations(self): + self.show_detailed_operations = not self.show_detailed_operations + + @api.depends("picking_state") + def _compute_enable_add_product(self): + for rec in self: + rec.enable_add_product = rec.picking_state != "done" @api.depends("todo_line_id") def _compute_todo_line_display_ids(self): @@ -95,7 +108,7 @@ def _compute_pending_move_ids(self): ) ) else: - self.pending_move_ids = False + self.pending_move_ids = self.todo_line_ids @api.depends( "todo_line_ids", "todo_line_ids.qty_done", "picking_id.move_line_ids.qty_done" @@ -225,7 +238,6 @@ def _get_stock_move_lines_todo(self): return move_lines def fill_pending_moves(self): - # TODO: Unify method self.fill_todo_records() def get_moves_or_move_lines(self): @@ -304,7 +316,7 @@ def update_fields_after_determine_todo(self, move_line): def action_done(self): res = super().action_done() if res: - move_dic = self._process_stock_move_line() + move_dic = self.with_context(**self.env.context)._process_stock_move_line() if move_dic: self[self._field_candidate_ids].scan_count += 1 if self.env.context.get("force_create_move"): @@ -353,7 +365,7 @@ def _prepare_move_line_values(self, candidate_move, available_qty): picking = self.env.context.get("picking", self.picking_id) if not picking: raise ValidationError( - _("You can not add extra moves if you have " "not set a picking") + _("You can not add extra moves if you have not set a picking") ) # If we move all package units the result package is the same if ( @@ -512,10 +524,12 @@ def _process_stock_move_line(self): # noqa: C901 ) else: moves_todo = StockMove.search(domain) - if not getattr( - self, - "_search_candidate_%s" % self.picking_mode, - )(moves_todo): + try: + getattr( + self, + "_search_candidate_%s" % self.picking_mode, + )(moves_todo) + except AttributeError: return False sml_vals = {} candidate_lines = self._get_candidate_stock_move_lines(moves_todo, sml_vals) @@ -583,6 +597,7 @@ def _process_stock_move_line(self): # noqa: C901 self._set_focus_on_qty_input("product_qty") return False move_lines_dic = {} + context = self.env.context for line in lines: if line.reserved_uom_qty and len(lines) > 1: assigned_qty = min( @@ -597,6 +612,9 @@ def _process_stock_move_line(self): # noqa: C901 and self.result_package_id == self.package_id ): qty_done = assigned_qty + elif context.get("no_increase_qty_done", False) and assigned_qty > 0: + # Do not increase the quantity, if the quantity is > 0 + qty_done = assigned_qty else: qty_done = line.qty_done + assigned_qty sml_vals.update( @@ -605,7 +623,7 @@ def _process_stock_move_line(self): # noqa: C901 "result_package_id": self.result_package_id.id, } ) - # Add or remove result_package_id + # Add or remove result_pselfackage_id package_qty_available = sum( self.package_id.quant_ids.filtered( lambda q: q.lot_id == self.lot_id @@ -660,6 +678,8 @@ def _process_stock_move_line(self): # noqa: C901 for sml in stock_move_lines: if not sml.move_id: self.create_new_stock_move(sml) + elif sml.move_id and context.get("force_create_move", False): + sml.move_id.product_uom_qty = self.product_qty move_lines_dic[sml.id] = sml.qty_done # Ensure that the state of stock_move linked to the sml read is assigned stock_move_lines.move_id.filtered( @@ -775,12 +795,22 @@ def action_assign_serial(self): raise ValidationError(_("No pending lines for this product")) def action_put_in_pack(self): - self.picking_id.action_put_in_pack() + for picking in self.mapped("picking_id"): + picking.action_put_in_pack() def action_clean_values(self): res = super().action_clean_values() self.selected_pending_move_id = False self.visible_force_done = False + # Hide Form Edit + self.manual_entry = False + self.send_bus_done( + "stock_barcodes_scan", + "stock_barcodes_edit_manual", + { + "manual_entry": False, + }, + ) return res def _option_required_hook(self, option_required): @@ -948,138 +978,44 @@ def fill_records(self, lines_list): list(todo_vals.values()) ) - -class WizCandidatePicking(models.TransientModel): - """ - TODO: explain - """ - - _name = "wiz.candidate.picking" - _description = "Candidate pickings for barcode interface" - # To prevent remove the record wizard until 2 days old - _transient_max_hours = 48 - - wiz_barcode_id = fields.Many2one( - comodel_name="wiz.stock.barcodes.read.picking", readonly=True - ) - picking_id = fields.Many2one( - comodel_name="stock.picking", string="Picking", readonly=True - ) - wiz_picking_id = fields.Many2one( - comodel_name="stock.picking", - related="wiz_barcode_id.picking_id", - string="Wizard Picking", - readonly=True, - ) - name = fields.Char( - related="picking_id.name", readonly=True, string="Candidate Picking" - ) - partner_id = fields.Many2one( - comodel_name="res.partner", - related="picking_id.partner_id", - readonly=True, - string="Partner", - ) - state = fields.Selection(related="picking_id.state", readonly=True) - date = fields.Datetime( - related="picking_id.date", readonly=True, string="Creation Date" - ) - product_qty_reserved = fields.Float( - "Reserved", - compute="_compute_picking_quantity", - digits="Product Unit of Measure", - readonly=True, - ) - product_uom_qty = fields.Float( - "Demand", - compute="_compute_picking_quantity", - digits="Product Unit of Measure", - readonly=True, - ) - product_qty_done = fields.Float( - "Done", - compute="_compute_picking_quantity", - digits="Product Unit of Measure", - readonly=True, - ) - # For reload kanban view - scan_count = fields.Integer() - is_pending = fields.Boolean(compute="_compute_is_pending") - note = fields.Html(related="picking_id.note") - - @api.depends("scan_count") - def _compute_picking_quantity(self): - for candidate in self: - qty_reserved = 0 - qty_demand = 0 - qty_done = 0 - candidate.product_qty_reserved = sum( - candidate.picking_id.mapped("move_ids.reserved_availability") - ) - for move in candidate.picking_id.move_ids: - qty_reserved += move.reserved_availability - qty_demand += move.product_uom_qty - qty_done += move.quantity_done - candidate.update( - { - "product_qty_reserved": qty_reserved, - "product_uom_qty": qty_demand, - "product_qty_done": qty_done, - } - ) - - @api.depends("scan_count") - def _compute_is_pending(self): - for rec in self: - rec.is_pending = bool(rec.wiz_barcode_id.pending_move_ids) - - def _get_wizard_barcode_read(self): - return self.env["wiz.stock.barcodes.read.picking"].browse( - self.env.context["wiz_barcode_id"] - ) - - def action_lock_picking(self): - wiz = self._get_wizard_barcode_read() - picking_id = self.env.context["picking_id"] - wiz.picking_id = picking_id - wiz._set_candidate_pickings(wiz.picking_id) - return wiz.action_confirm() - - def action_unlock_picking(self): - wiz = self._get_wizard_barcode_read() - wiz.update( - { - "picking_id": False, - "candidate_picking_ids": False, - "message_type": False, - "message": False, - } + def action_validate_picking(self): + # for candidate_picking in self.candidate_picking_ids: + valid, result = self.candidate_picking_ids.with_context( + wiz_barcode_id=self.id, + picking_id=self.picking_id.id, + skip_sms=True, + skip_immediate=True, + ).action_validate_picking() + if not valid: + return result + action = self.env["ir.actions.actions"]._for_xml_id( + "stock_barcodes.stock_barcodes_action_picking_tree_ready" ) - return wiz.action_cancel() - def _get_picking_to_validate(self): - """Inject context show_picking_type_action_tree to redirect to picking list - after validate picking in barcodes environment. - The stock_barcodes_validate_picking key allows to know when a picking has been - validated from stock barcodes interface. - """ - return ( - self.env["stock.picking"] - .browse(self.env.context.get("picking_id", False)) - .with_context( - show_picking_type_action_tree=True, stock_barcodes_validate_picking=True + if self.picking_id and self.picking_id.picking_type_id: + context = self.env.context.copy() + context.update(safe_eval(action["context"])) + context.update( + {"search_default_picking_type_id": self.picking_id.picking_type_id.id} ) - ) + action["context"] = context - def action_validate_picking(self): - picking = self._get_picking_to_validate() - return picking.button_validate() + return action def action_open_picking(self): - picking = self.env["stock.picking"].browse( - self.env.context.get("picking_id", False) - ) - return picking.with_context(control_panel_hidden=False).get_formview_action() + for candidate_picking in self.candidate_picking_ids: + candidate_picking.with_context( + wiz_barcode_id=self.id, picking_id=self.picking_id.id + ).action_open_picking() - def action_put_in_pack(self): - self.picking_id.action_put_in_pack() + def action_unlock_picking(self): + for candidate_picking in self.candidate_picking_ids: + candidate_picking.with_context( + wiz_barcode_id=self.id + ).action_unlock_picking() + + def action_lock_picking(self): + for candidate_picking in self.candidate_picking_ids: + candidate_picking.with_context( + wiz_barcode_id=self.id, picking_id=self.picking_id.id + ).action_lock_picking() diff --git a/stock_barcodes/wizard/stock_barcodes_read_picking_views.xml b/stock_barcodes/wizard/stock_barcodes_read_picking_views.xml index 524298208cf8..8c3048d13ce8 100644 --- a/stock_barcodes/wizard/stock_barcodes_read_picking_views.xml +++ b/stock_barcodes/wizard/stock_barcodes_read_picking_views.xml @@ -6,11 +6,50 @@ primary + + + + [] + + + + + -
    -
    - - -
    +
    @@ -141,18 +114,21 @@ [('id', 'child_of', picking_location_id), '|', ('company_id', '=', False), ('company_id', '=', company_id), ('usage', '!=', 'view')] + >[('id', 'child_of', picking_location_id), '|', ('company_id', '=', False), ('company_id', '=', + company_id), ('usage', '!=', 'view')] + {'readonly': [('manual_entry', '=', False)], 'invisible': [('picking_type_code', '=', 'incoming')]} + >{'readonly': [('manual_entry', '=', False)], 'invisible': [('picking_type_code', '=', 'incoming')]} +
    Dest. Location {'invisible': [('todo_line_is_extra_line', '!=', False)]} + >{'invisible': [('todo_line_is_extra_line', '!=', False)]} + +
    + + + This picking is already done + +
    - - - - - - - - - - - - +
    @@ -267,6 +241,12 @@
    +

    + Detailed operations +

    - - - - - - - - - - -
    -
    - Lot: -
    - -
    - Package: -
    -
    -
    @@ -399,15 +336,55 @@ name="action_put_in_pack" help="Put in pack" type="object" - icon="fa-cubes" + icon="fa-cube fa-2x" title="Put in Pack" attrs="{'invisible': ['|', ('picking_state', 'in', ('draft', 'done', 'cancel')), ('display_menu', '=', True)]}" - class="ms-auto oe_kanban_action_button btn btn-secondary btn-sm ps-3 pe-3" + class="btn btn-secondary w-100 oe_kanban_action_button btn-sm text-uppercase + d-flex justify-content-center align-items-center fs-2" groups="stock.group_tracking_lot" data-hotkey="6" > + Put in back + + + + + +