494 lines
24 KiB
Python
494 lines
24 KiB
Python
# 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": <data used by the stock barcode> {'records' : {'model': [{<record>}, ... ]}, 'other_infos':...}, _get_barcode_data_prefetch
|
|
"groups": <security group>, 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
|