# Part of Odoo. See LICENSE file for full copyright and licensing details. import re from collections import defaultdict from odoo import fields, http, _ from odoo.http import request from odoo.exceptions import UserError, ValidationError from odoo.fields import Domain from odoo.tools import pdf, split_every from odoo.tools.misc import file_open from odoo.addons.stock_barcode.models.epc_encoder import EpcScheme class StockBarcodeController(http.Controller): @http.route('/stock_barcode/scan_from_main_menu', type='jsonrpc', auth='user') def main_menu(self, barcode): """ Receive a barcode scanned from the main menu and return the appropriate action (open an existing / new picking) or warning. """ barcode_type = None request.update_context(allowed_company_ids=self._get_allowed_company_ids()) nomenclature = request.env.company.nomenclature_id try: parsed_results = nomenclature.parse_barcode(barcode) except ValidationError: parsed_results = False if parsed_results and nomenclature.is_gs1_nomenclature: # search with the last feasible rule for result in parsed_results[::-1]: if result['type'] in ['product', 'package', 'location', 'location_dest']: barcode_type = result['type'] break # Alias support elif parsed_results: for res in parsed_results if isinstance(parsed_results, list) else [parsed_results]: if res['type'] in ['price', 'weight']: barcode = res['base_code'] barcode_type = 'product' break barcode = res.get('code', barcode) break if not barcode_type: ret_open_picking = self._try_open_picking(barcode) if ret_open_picking: return ret_open_picking ret_open_picking_type = self._try_open_picking_type(barcode) if ret_open_picking_type: return ret_open_picking_type if request.env.user.has_group('stock.group_stock_multi_locations') and \ (not barcode_type or barcode_type in ['location', 'location_dest']): ret_new_internal_picking = self._try_new_internal_picking(barcode) if ret_new_internal_picking: return ret_new_internal_picking if not barcode_type or barcode_type == 'product': ret_open_product_location = self._try_open_product_location(barcode) if ret_open_product_location: return ret_open_product_location if request.env.user.has_group('stock.group_production_lot') and \ (not barcode_type or barcode_type == 'lot'): ret_open_lot = self._try_open_lot(barcode) if ret_open_lot: return ret_open_lot if request.env.user.has_group('stock.group_tracking_lot') and \ (not barcode_type or barcode_type == 'package'): ret_open_package = self._try_open_package(barcode) if ret_open_package: return ret_open_package if request.env.user.has_group('stock.group_stock_multi_locations'): return {'warning': _('No picking or location or product corresponding to barcode %(barcode)s', barcode=barcode)} else: return {'warning': _('No picking or product corresponding to barcode %(barcode)s', barcode=barcode)} @http.route('/stock_barcode/save_barcode_data', type='jsonrpc', auth='user') def save_barcode_data(self, model, res_id, write_field, write_vals): if not res_id: return request.env[model].barcode_write(write_vals) target_record = request.env[model].browse(res_id) target_record.with_context(avoid_putaway_rules=True).write({write_field: write_vals}) return target_record._get_stock_barcode_data() @http.route('/stock_barcode/get_barcode_data', type='jsonrpc', auth='user') def get_barcode_data(self, model, res_id): """ Returns a dict with values used by the barcode client: { "data": {'records' : {'model': [{}, ... ]}, 'other_infos':...}, _get_barcode_data_prefetch "groups": , self._get_groups_data } """ if not res_id: target_record = request.env[model].with_context(allowed_company_ids=self._get_allowed_company_ids()) else: target_record = request.env[model].browse(res_id).with_context(allowed_company_ids=self._get_allowed_company_ids()) data = target_record._get_stock_barcode_data() data['records'].update(self._get_barcode_nomenclature()) data['precision'] = request.env['decimal.precision'].precision_get('Product Unit') mute_sound = request.env['ir.config_parameter'].sudo().get_param('stock_barcode.mute_sound_notifications') data['config'] = data.get('config', {}) data['config']['play_sound'] = bool(not mute_sound or mute_sound == "False") data['config']['barcode_separator_regex'] = request.env['ir.config_parameter'].sudo().get_param('stock_barcode.barcode_separator_regex', '.^') data['config']['barcode_rfid_batch_time'] = int(request.env['ir.config_parameter'].sudo().get_param('stock_barcode.barcode_rfid_batch_time', 1000)) delay_between_scan = request.env['ir.config_parameter'].sudo().get_param('stock_barcode.delay_between_scan') if delay_between_scan and delay_between_scan.isnumeric(): data['config']['delay_between_scan'] = int(delay_between_scan) return { 'data': data, 'groups': self._get_groups_data(), } @http.route('/stock_barcode/get_main_menu_data', type='jsonrpc', auth='user') def get_main_menu_data(self): user = request.env.user groups = { 'locations': user.has_group('stock.group_stock_multi_locations'), 'package': user.has_group('stock.group_tracking_lot'), 'tracking': user.has_group('stock.group_production_lot'), } quant_count = request.env['stock.quant'].search_count([ '|', ('user_id', '=', user.id), ('user_id', '=', False), ("company_id", "=", self._get_allowed_company_ids()[0]), ("location_id.usage", "in", ["internal", "transit"]), ("inventory_date", "<=", fields.Date.context_today(user)), ("inventory_quantity_set", "=", False), ]) mute_sound = request.env['ir.config_parameter'].sudo().get_param('stock_barcode.mute_sound_notifications') play_sound = bool(not mute_sound or mute_sound == "False") return { 'groups': groups, 'play_sound': play_sound, 'quant_count': quant_count, } @http.route('/stock_barcode/get_specific_barcode_data', type='jsonrpc', auth='user') def get_specific_barcode_data(self, **kwargs): """ This method gets multiple records data from different models for the given barcode(s). The goal is to do one search by model (plus the additional record, e.g. the UOM records when fetching product's records.) :param kwargs: ''barcode'': a single barcode (string), used when not knowing which model is linked. ''barcodes_by_model'': a dict of model_name -> barcode list ''context'' ''domains_by_model'': a dict of model_name -> domain ''fetch_quant'': Fetch extra quants based on products (used in inventory) """ context = kwargs.get('context', {}) context.update(request.env.context) context['display_default_code'] = False request.update_env(context=context) barcodes_by_model = kwargs.get('barcodes_by_model') domains_by_model = kwargs.get('domains_by_model', {}) universal_domain = Domain(domains_by_model.get('all') or Domain.TRUE) fetch_quant = kwargs.get('fetch_quants') nomenclature = request.env.company.nomenclature_id result = defaultdict(list) product_ids = set() if barcodes_by_model and barcodes_by_model.get('product.product') and not barcodes_by_model.get('stock.lot'): barcodes_by_model['stock.lot'] = [] # If a barcode was given but no model was specified, search for it for all relevant models. if not barcodes_by_model: barcode_field_by_model = self._get_barcode_field_by_model() barcodes = kwargs.get('barcodes') or [kwargs.get('barcode')] barcodes_by_model = {model_name: barcodes for model_name in barcode_field_by_model.keys()} for model_name, barcodes in barcodes_by_model.items(): if not barcodes: continue barcode_field = request.env[model_name]._barcode_field domain = Domain(barcode_field, 'in', barcodes) if nomenclature.is_gs1_nomenclature: # If we use GS1 nomenclature, the domain might need some adjustments. converted_barcodes_domains = [] unconverted_barcodes = [] for barcode in set(barcodes): try: # If barcode is digits only, cut off the padding to keep the original barcode only. barcode = str(int(barcode)) converted_barcodes_domains.append(Domain(barcode_field, 'ilike', barcode)) except ValueError: unconverted_barcodes.append(barcode) pass # Barcode isn't digits only. if converted_barcodes_domains: domain = Domain.OR(converted_barcodes_domains) if unconverted_barcodes: domain |= Domain(barcode_field, 'in', unconverted_barcodes) # Adds additionnal domain if applicable. domain_for_this_model = domains_by_model.get(model_name) if domain_for_this_model: domain &= Domain(domain_for_this_model) if universal_domain: domain &= universal_domain # Search for barcodes' records. records = request.env[model_name].search(domain) fetched_data = self._get_records_fields_stock_barcode(records) if fetch_quant and model_name == 'product.product': product_ids = records.ids for f_model_name in fetched_data: result[f_model_name] = result[f_model_name] + fetched_data[f_model_name] if fetch_quant and product_ids: quants = request.env['stock.quant'].search([ ('product_id', 'in', product_ids), ('location_id.usage', '=', 'internal'), ]) fetched_data = self._get_records_fields_stock_barcode(quants) for f_model_name in fetched_data: result[f_model_name] = result[f_model_name] + fetched_data[f_model_name] return result def _get_records_fields_stock_barcode(self, records): result = defaultdict(list) result[records._name] = records.read(records._get_fields_stock_barcode(), load=False) if hasattr(records, '_get_stock_barcode_specific_data'): records_data_by_model = records._get_stock_barcode_specific_data() for res_model in records_data_by_model: result[res_model] += records_data_by_model[res_model] return result @http.route('/stock_barcode/get_quants', type='jsonrpc', auth='user') def get_existing_quant_and_related_data(self, domain): quants = request.env['stock.quant'].search(domain) return quants.get_stock_barcode_data_records() @http.route('/stock_barcode/rid_of_message_demo_barcodes', type='jsonrpc', auth='user') def rid_of_message_demo_barcodes(self, **kw): """ Edit the main_menu client action so that it doesn't display the 'print demo barcodes sheet' message """ if not request.env.user.has_group('stock.group_stock_user'): return request.not_found() action = request.env.ref('stock_barcode.stock_barcode_action_main_menu') action and action.sudo().write({'params': {'message_demo_barcodes': False}}) @http.route('/stock_barcode/print_inventory_commands', type='http', auth='user') def print_inventory_commands(self, barcode_type=False): if not request.env.user.has_group('stock.group_stock_user'): return request.not_found() # make sure we use the selected company if possible allowed_company_ids = self._get_allowed_company_ids() # same domain conditions for picking types and locations domain = self._get_picking_type_domain(barcode_type, allowed_company_ids) # get fixed command barcodes barcode_pdfs = self._get_barcode_pdfs(barcode_type, domain) if not barcode_pdfs: raise UserError(_("Barcodes are not available.")) merged_pdf = pdf.merge_pdf(barcode_pdfs) pdfhttpheaders = [ ('Content-Type', 'application/pdf'), ('Content-Length', len(merged_pdf)) ] return request.make_response(merged_pdf, headers=pdfhttpheaders) def _try_open_lot(self, barcode): """ If barcode represent a lot, open a form view to show all the details of this lot. """ result = request.env['stock.lot'].search_read([ ('name', '=', barcode), ], ['id', 'display_name'], limit=1) if result: return { 'action': { 'name': 'Open lot', 'res_model': 'stock.lot', 'views': [(request.env.ref('stock.view_production_lot_form').id, 'form')], 'type': 'ir.actions.act_window', 'res_id': result[0]['id'], } } return False def _try_open_product_location(self, barcode): """ If the barcode represents a product or its UoM, open a list/kanban view to show all the locations of this product. """ if result := request.env['product.product'].search_read( [('barcode', '=', barcode)], ['id', 'display_name'], limit=1): product_id, product_display_name = result[0]['id'], result[0]['display_name'] # Ignore UoM barcode if the UoM setting is toggled off. elif result := self.env.user.has_group('uom.group_uom') and \ request.env['product.uom'].search_read( [('barcode', '=', barcode)], ['product_id'], limit=1): product_id, product_display_name = result[0]['product_id'] if result: tree_view_id = request.env.ref('stock.view_stock_quant_tree').id kanban_view_id = request.env.ref('stock_barcode.stock_quant_barcode_kanban_2').id return { 'action': { 'name': product_display_name, 'res_model': 'stock.quant', 'views': [(tree_view_id, 'list'), (kanban_view_id, 'kanban')], 'type': 'ir.actions.act_window', 'domain': [('product_id', '=', product_id)], 'context': { 'search_default_internal_loc': True, }, 'mobile_view_mode': 'kanban', } } def _try_open_picking_type(self, barcode): """ If barcode represent a picking type, open a new picking with this type """ picking_type = request.env['stock.picking.type'].search([ ('barcode', '=', barcode), ('company_id', 'in', [False, *self._get_allowed_company_ids()]), ], limit=1) if picking_type: picking = request.env['stock.picking']._create_new_picking(picking_type) action = picking.action_open_picking_client_action() return {'action': action} return False def _try_open_picking(self, barcode): """ If barcode represents a picking, open it """ corresponding_picking = request.env['stock.picking'].search([ ('name', '=', barcode), ], limit=1) if corresponding_picking: action = corresponding_picking.action_open_picking_client_action() return {'action': action} return False def _try_open_package(self, barcode): """ If barcode represents a package, open it. """ package = request.env['stock.package'].search([('name', '=', barcode)], limit=1) if package: view_id = request.env.ref('stock.stock_package_view_form').id return { 'action': { 'name': 'Open package', 'res_model': 'stock.package', 'views': [(view_id, 'form')], 'type': 'ir.actions.act_window', 'res_id': package.id, 'context': {'active_id': package.id} } } return False def _try_new_internal_picking(self, barcode): """ If barcode represents a location, open a new picking from this location """ corresponding_location = request.env['stock.location'].search([ ('barcode', '=', barcode), ('usage', '=', 'internal'), ("company_id", "=", self._get_allowed_company_ids()[0]) ], limit=1) if corresponding_location: internal_picking_type = request.env['stock.picking.type'].search([('code', '=', 'internal')]) warehouse = corresponding_location.warehouse_id if warehouse: internal_picking_type = internal_picking_type.filtered(lambda r: r.warehouse_id == warehouse) dest_loc = corresponding_location while dest_loc.location_id and dest_loc.location_id.usage == 'internal': dest_loc = dest_loc.location_id if internal_picking_type: # Create and confirm an internal picking picking = request.env['stock.picking'].create({ 'picking_type_id': internal_picking_type[0].id, 'user_id': False, 'location_id': corresponding_location.id, 'location_dest_id': dest_loc.id, }) picking.action_confirm() action = picking.action_open_picking_client_action() return {'action': action} else: return {'warning': _('No internal operation type. Please configure one in warehouse settings.')} return False def _get_allowed_company_ids(self): """ Return the allowed_company_ids based on cookies. Currently request.env.company returns the current user's company when called within a controller rather than the selected company in the company switcher and request.env.companies lists the current user's allowed companies rather than the selected companies. :returns: List of active companies. The first company id in the returned list is the selected company. """ cids = request.cookies.get('cids', str(request.env.user.company_id.id)) return [int(cid) for cid in cids.split('-')] def _get_picking_type_domain(self, barcode_type, allowed_company_ids): return [ ('active', '=', 'True'), ('barcode', '!=', ''), ('company_id', 'in', allowed_company_ids) ] def _get_barcode_pdfs(self, barcode_type, domain): barcode_pdfs = [] if barcode_type == 'barcode_commands_and_operation_types': with file_open('stock_barcode/static/img/barcodes_actions.pdf', "rb") as commands_file: barcode_pdfs.append(commands_file.read()) if 'operation_types' in barcode_type: # get picking types barcodes picking_type_ids = request.env['stock.picking.type'].search(domain) for picking_type_batch in split_every(112, picking_type_ids.ids): picking_types_pdf, _content_type = request.env['ir.actions.report']._render_qweb_pdf('stock.action_report_picking_type_label', picking_type_batch) if picking_types_pdf: barcode_pdfs.append(picking_types_pdf) # get locations barcodes if barcode_type == 'locations' and request.env.user.has_group('stock.group_stock_multi_locations'): locations_ids = request.env['stock.location'].search(domain) for location_ids_batch in split_every(112, locations_ids.ids): locations_pdf, _content_type = request.env['ir.actions.report']._render_qweb_pdf('stock.action_report_location_barcode', location_ids_batch) if locations_pdf: barcode_pdfs.append(locations_pdf) return barcode_pdfs def _get_groups_data(self): return { 'group_stock_multi_locations': request.env.user.has_group('stock.group_stock_multi_locations'), 'group_tracking_owner': request.env.user.has_group('stock.group_tracking_owner'), 'group_tracking_lot': request.env.user.has_group('stock.group_tracking_lot'), 'group_production_lot': request.env.user.has_group('stock.group_production_lot'), 'group_uom': request.env.user.has_group('uom.group_uom'), 'group_stock_sign_delivery': request.env.user.has_group('stock.group_stock_sign_delivery'), } def _get_barcode_nomenclature(self): company = request.env['res.company'].browse(self._get_allowed_company_ids()[0]) nomenclature = company.nomenclature_id return { "barcode.nomenclature": nomenclature.read(load=False), "barcode.rule": nomenclature.rule_ids.read(load=False) } def _get_barcode_field_by_model(self): list_model = [ 'stock.location', 'product.product', 'uom.uom', 'product.uom', 'stock.picking', 'stock.lot', 'stock.package', 'stock.package.type', ] return {model: request.env[model]._barcode_field for model in list_model if hasattr(request.env[model], '_barcode_field')} @http.route('/stock_barcode/get_epc', type='jsonrpc', auth='user') def get_epc(self, scheme_name, element_string_list, company_prefix_length=None, filter=None): scheme = EpcScheme(scheme_name) return {element_string: scheme.encode(element_string, filter, company_prefix_length) for element_string in element_string_list} @http.route('/stock_barcode/get_epc_sgtin', type='jsonrpc', auth='user') def get_epc_sgtin(self, gtin, tracking_number_list, filter, company_prefix_length, alphanumeric_tracking=False): """Get a batch of SGTIN EPC for a defined product with a list of tracking numbers :param gtin: GTIN of the product :param tracking_number_list: List of tracking numbers :param alphanumeric_tracking: True if any tracking number is alphanumeric :param filter: Filter value for SGTIN EPC as defined in [TDS2.1]ยง10.2 :param company_prefix_length: Length of the company prefix """ if not re.match(r"^\d+$", gtin): raise Exception(_("A GTIN can only contains digits")) scheme_name, field_name = ('sgtin-198', 'serial_string') if alphanumeric_tracking else ('sgtin-96', 'serial_integer') scheme = EpcScheme(scheme_name) results = {} element_string = f"(01) {gtin} (21) {tracking_number_list[0]}" # First call to encode may raise an exception if technical fields (header, partition, filter...) are invalid results[tracking_number_list[0]] = scheme.encode(element_string, filter, company_prefix_length) # Since we operate on a single product, we can reuse the base encoding for all the tracking numbers to fasten the process for tracking_number in tracking_number_list[1:]: results[tracking_number] = scheme.encode_partial(field_name, tracking_number) return results