oca-warehouse/odoo-bringout-oca-stock-logistics-barcode-stock_barcodes/stock_barcodes/wizard/stock_barcodes_read.py
2025-08-29 15:43:06 +02:00

905 lines
34 KiB
Python

# Copyright 2019 Sergio Teruel <sergio.teruel@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import logging
from odoo import _, api, fields, models
_logger = logging.getLogger(__name__)
TYPE_ERROR = ["more_match", "not_found"]
class WizStockBarcodesRead(models.AbstractModel):
_name = "wiz.stock.barcodes.read"
_inherit = "barcodes.barcode_events_mixin"
_description = "Wizard to read barcode"
# 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)
res_id = fields.Integer(index=True)
product_id = fields.Many2one(
comodel_name="product.product", domain=[("type", "in", _allowed_product_types)]
)
product_uom_id = fields.Many2one(comodel_name="uom.uom")
product_tracking = fields.Selection(related="product_id.tracking", readonly=True)
lot_id = fields.Many2one(comodel_name="stock.lot")
lot_name = fields.Char(
"Lot/Serial Number Name",
compute="_compute_lot_name",
readonly=False,
store=True,
)
location_id = fields.Many2one(comodel_name="stock.location")
location_dest_id = fields.Many2one(
comodel_name="stock.location", string="Location dest."
)
packaging_id = fields.Many2one(comodel_name="product.packaging")
product_packaging_ids = fields.One2many(related="product_id.packaging_ids")
package_id = fields.Many2one(comodel_name="stock.quant.package")
result_package_id = fields.Many2one(comodel_name="stock.quant.package")
owner_id = fields.Many2one(comodel_name="res.partner")
packaging_qty = fields.Float(string="Package Qty", digits="Product Unit of Measure")
product_qty = fields.Float(digits="Product Unit of Measure")
manual_entry = fields.Boolean(string="Manual", help="Entry manual data")
confirmed_moves = fields.Boolean(
string="Confirmed moves", related="option_group_id.confirmed_moves"
)
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"),
],
readonly=True,
)
message = fields.Char(readonly=True)
message_step = fields.Char(readonly=True)
guided_product_id = fields.Many2one(comodel_name="product.product")
guided_location_id = fields.Many2one(comodel_name="stock.location")
guided_location_dest_id = fields.Many2one(comodel_name="stock.location")
guided_lot_id = fields.Many2one(comodel_name="stock.lot")
action_ids = fields.Many2many(
comodel_name="stock.barcodes.action", compute="_compute_action_ids"
)
option_group_id = fields.Many2one(comodel_name="stock.barcodes.option.group")
visible_force_done = fields.Boolean()
step = fields.Integer()
is_manual_qty = fields.Boolean(compute="_compute_is_manual_qty")
is_manual_confirm = fields.Boolean(compute="_compute_is_manual_qty")
# Technical field to allow use in attrs
display_menu = fields.Boolean()
auto_lot = fields.Boolean(
string="Get lots automatically",
help="If checked the lot will be set automatically with the same "
"removal startegy",
compute="_compute_auto_lot",
store=True,
readonly=False,
)
create_lot = fields.Boolean(
string="Allow create lot",
help="Show lot name field",
compute="_compute_create_lot",
)
display_assign_serial = fields.Boolean(compute="_compute_display_assign_serial")
keep_result_package = fields.Boolean()
total_product_uom_qty = fields.Float(
string="Product Demand", digits="Product Unit of Measure", store=False
)
total_product_qty_done = fields.Float(
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(
[("action_window_id", "!=", False)]
)
self.action_ids = actions
@api.depends("option_group_id")
def _compute_is_manual_qty(self):
for rec in self:
rec.is_manual_qty = rec.option_group_id.is_manual_qty
rec.is_manual_confirm = rec.option_group_id.is_manual_confirm
rec.auto_lot = rec.option_group_id.auto_lot
@api.depends("option_group_id")
def _compute_auto_lot(self):
for rec in self:
rec.auto_lot = rec.option_group_id.auto_lot
@api.depends("option_group_id")
def _compute_create_lot(self):
for rec in self:
rec.create_lot = rec.option_group_id.create_lot
@api.depends("product_id")
def _compute_display_assign_serial(self):
for rec in self:
rec.display_assign_serial = rec.product_id.tracking == "serial"
@api.depends("lot_id")
def _compute_lot_name(self):
for rec in self:
rec.lot_name = rec.lot_id.name
@api.onchange("packaging_qty")
def onchange_packaging_qty(self):
if self.packaging_id:
self.product_qty = self.packaging_qty * self.packaging_id.qty
@api.onchange(
"product_id",
"lot_id",
"package_id",
"result_package_id",
"packaging_qty",
"product_qty",
)
def onchange_visible_force_done(self):
self.visible_force_done = False
def _set_messagge_info(self, message_type, message):
"""
Set message type and message description.
For manual entry mode barcode is not set so is not displayed
"""
self.message_type = message_type
# 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:
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))
if location:
self.location_id = location
return True
return False
def process_barcode_location_dest_id(self):
location = self.env["stock.location"].search(self._barcode_domain(self.barcode))
if location:
self.location_dest_id = location
return True
return False
def process_barcode_product_id(self):
domain = self._barcode_domain(self.barcode)
product = self.env["product.product"].search(domain)
if product:
if len(product) > 1:
self._set_messagge_info("more_match", _("More than one product found"))
return False
elif product.type not in self._allowed_product_types:
self._set_messagge_info(
"not_found", _("The product type is not allowed")
)
return False
self.action_product_scaned_post(product)
if (
self.option_group_id.fill_fields_from_lot
and self.location_id
and self.product_id
):
quant_domain = [
("location_id", "=", self.location_id.id),
("product_id", "=", product.id),
]
if self.lot_id:
quant_domain.append(("lot_id", "=", self.lot_id.id))
if self.package_id:
quant_domain.append(("package_id", "=", self.package_id.id))
if self.owner_id:
quant_domain.append(("owner_id", "=", self.owner_id.id))
quants = self.env["stock.quant"].search(quant_domain)
if quants:
self.set_info_from_quants(quants)
return True
return False
def process_barcode_lot_id(self):
if self.env.user.has_group("stock.group_production_lot"):
lot_domain = [("name", "=", self.barcode)]
if self.product_id:
lot_domain.append(("product_id", "=", self.product_id.id))
lot = self.env["stock.lot"].search(lot_domain)
if len(lot) == 1:
if self.option_group_id.fill_fields_from_lot:
quant_domain = [
("lot_id.name", "=", self.barcode),
("product_id", "=", lot.product_id.id),
("quantity", ">", 0.0),
]
if self.location_id:
quant_domain.append(("location_id", "=", self.location_id.id))
else:
quant_domain.append(("location_id.usage", "=", "internal"))
if self.owner_id:
quant_domain.append(("owner_id", "=", self.owner_id.id))
quants = self.env["stock.quant"].search(quant_domain)
if (
not self._name == "wiz.stock.barcodes.read.inventory"
and not quants
and not self.option_group_id.allow_negative_quant
):
self._set_messagge_info(
"more_match",
_("No stock available for this lot with screen values"),
)
self.lot_id = False
self.lot_name = False
return False
if quants:
self.set_info_from_quants(quants)
else:
self.product_id = lot.product_id
self.action_lot_scaned_post(lot)
return True
else:
self.product_id = lot.product_id
self.action_lot_scaned_post(lot)
return True
elif lot:
self._set_messagge_info(
"more_match", _("More than one lot found\nScan product before")
)
elif (
self.product_id
and self.product_id.tracking != "none"
and self.option_group_id.create_lot
):
self.lot_name = self.barcode
self.action_lot_scaned_post(self.lot_name)
return True
return False
def process_barcode_package_id(self):
if not self.env.user.has_group("stock.group_tracking_lot"):
return False
quant_domain = [
("package_id.name", "=", self.barcode),
("quantity", ">", 0.0),
]
if self.option_group_id.get_option_value("location_id", "forced"):
quant_domain.append(("location_id", "=", self.location_id.id))
if self.owner_id:
quant_domain.append(("owner_id", "=", self.owner_id.id))
quants = self.env["stock.quant"].search(quant_domain)
internal_quants = quants.filtered(lambda q: q.location_id.usage == "internal")
if internal_quants:
quants = internal_quants
elif quants:
self = self.with_context(ignore_quant_location=True)
# self._set_messagge_info("more_match", _("Package located external location"))
else:
# self._set_messagge_info("more_match", _("Package not fount or empty"))
return False
self.set_info_from_quants(quants)
return True
def process_barcode_result_package_id(self):
if not self.env.user.has_group("stock.group_tracking_lot"):
return False
domain = [("name", "=", self.barcode)]
package = self.env["stock.quant.package"].search(domain)
if package:
self.result_package_id = package[:1]
return True
return False
def set_info_from_quants(self, quants):
"""
Fill wizard fields from stock quants
"""
if self.env.context.get("skip_set_info_from_quants"):
return
ignore_quant_location = self.env.context.get(
"ignore_quant_location", self.option_group_id.ignore_quant_location
)
if len(quants) == 1:
# All ok
self.action_product_scaned_post(quants.product_id)
self.package_id = quants.package_id
self.result_package_id = quants.package_id
if quants.lot_id:
self.action_lot_scaned_post(quants.lot_id)
if quants.owner_id:
self.owner_id = quants.owner_id
# Review conditions
if (
not ignore_quant_location
and not self.option_group_id.get_option_value("location_id", "forced")
and self.option_group_id.code != "IN"
):
self.location_id = quants.location_id
if self.option_group_id.code != "OUT" and not self.env.context.get(
"skip_update_quantity_from_lot", False
):
self.product_qty = quants.quantity
elif len(quants) > 1:
# More than one record found with same barcode.
# Could be half lot in two distinct locations.
# Empty location field to force a location barcode scan
products = quants.mapped("product_id")
if len(products) == 1:
self.action_product_scaned_post(products[0])
package = quants[0].package_id
if not quants.filtered(lambda q: q.package_id != package):
self.package_id = package
lots = quants.mapped("lot_id")
if len(lots) == 1:
self.action_lot_scaned_post(lots[0])
owner = quants[0].owner_id
if not quants.filtered(lambda q: q.owner_id != owner):
self.owner_id = owner
if not ignore_quant_location:
locations = quants.mapped("location_id")
if len(locations) == 1:
if not self.location_id and self.option_group_id.code != "IN":
self.location_id = locations
def process_barcode_packaging_id(self):
domain = self._barcode_domain(self.barcode)
if self.env.user.has_group("product.group_stock_packaging"):
domain.append(("product_id", "!=", False))
packaging = self.env["product.packaging"].search(domain)
if packaging:
if len(packaging) > 1:
self._set_messagge_info(
"more_match", _("More than one package found")
)
self.packaging_id = False
return False
self.action_packaging_scaned_post(packaging)
return True
return False
def process_barcode(self, barcode):
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
)
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,
)
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
options_required = options.filtered("required")
for option in options_required:
if not getattr(self, option.field_name, False):
if self.is_manual_qty and option.field_name in [
"product_qty",
"packaging_qty",
]:
self._set_focus_on_qty_input("product_qty")
if option.field_name == "lot_id" and (
self.product_id.tracking == "none"
or self.auto_lot
or (self.lot_name and self.create_lot)
):
continue
if self._option_required_hook(option):
continue
self.display_notification(
_("{name} is required").format(name=option.name),
message_type="danger",
title=_("Empty field"),
sticky=False,
)
self.action_show_step()
return False
return True
def _option_required_hook(self, option_required):
"""Hook to evaluate is an option is required"""
return False
def _scanned_location(self, barcode):
location = self.env["stock.location"].search(self._barcode_domain(barcode))
if location:
self.location_id = location
self._set_messagge_info("info", _("Waiting product"))
return True
else:
return False
def _barcode_domain(self, barcode):
field_name = self.env.context.get("barcode_domain_field", "barcode")
return [(field_name, "=", barcode)]
def _clean_barcode_scanned(self, barcode):
return barcode.rstrip()
def on_barcode_scanned(self, barcode):
self.barcode = self._clean_barcode_scanned(barcode)
def dummy_on_barcode_scanned(self):
"""To avoid execute operations in onchange environment"""
self.process_barcode(self.barcode)
def check_location_contidion(self):
if not self.location_id:
self._set_messagge_info("info", _("Waiting location"))
# Remove product when no location has been scanned
self.product_id = False
return False
return True
def check_lot_contidion(self):
if self.product_id.tracking != "none" and not self.lot_id and not self.lot_name:
self._set_messagge_info("info", _("Waiting lot"))
return False
return True
def check_done_conditions(self):
result_ok = self.check_location_contidion()
if not result_ok:
return False
if not self.product_id:
self._set_messagge_info("info", _("Waiting product"))
return False
result_ok = self.check_lot_contidion()
if not result_ok:
return False
if (
not self.product_qty
and not self._name == "wiz.stock.barcodes.read.inventory"
):
self._set_messagge_info("info", _("Waiting quantities"))
return False
if (
self.option_group_id.barcode_guided_mode == "guided"
and not self._check_guided_values()
):
return False
if self.manual_entry:
self._set_messagge_info("success", _("Manual entry OK"))
return True
def _check_guided_values(self):
if (
self.product_id != self.guided_product_id
and self.option_group_id.get_option_value("product_id", "forced")
):
self._set_messagge_info("more_match", _("Wrong product"))
self.product_qty = 0.0
return False
if (
self.guided_product_id.tracking != "none"
and self.lot_id != self.guided_lot_id
and self.option_group_id.get_option_value("lot_id", "forced")
):
self._set_messagge_info("more_match", _("Wrong lot"))
return False
if (
self.location_id != self.guided_location_id
and self.option_group_id.get_option_value("location_id", "forced")
):
self._set_messagge_info("more_match", _("Wrong location"))
return False
if (
self.location_dest_id != self.guided_location_dest_id
and self.option_group_id.get_option_value("location_dest_id", "forced")
):
self._set_messagge_info("more_match", _("Wrong location dest"))
return False
return True
def action_done(self):
if not self.manual_entry and not self.product_qty and not self.is_manual_qty:
self.product_qty = 1.0
limit_product_qty = float(
self.env["ir.config_parameter"]
.sudo()
.get_param("stock_barcodes.limit_product_qty", "999999")
)
if self.product_qty > limit_product_qty:
# HACK: Some times users scan a barcode into input element.
# At this time, to prevent this we check that the quantity be realistic.
self._set_messagge_info("more_match", _("The quantity is huge"))
return False
if not self.check_done_conditions():
return False
self.process_lot_before_done()
return True
def action_cancel(self):
return True
def action_product_scaned_post(self, product):
self.package_id = False
if self.product_id != product and self.lot_id.product_id != product:
self.lot_id = False
self.product_id = product
self.product_uom_id = self.product_id.uom_id
self.set_product_qty()
def action_packaging_scaned_post(self, packaging):
self.packaging_id = packaging
if (
self.product_id != packaging.product_id
and self.lot_id.product_id != packaging.product_id
):
self.lot_id = False
self.product_id = packaging.product_id
self.set_product_qty()
def action_lot_scaned_post(self, lot):
if isinstance(lot, str):
self.lot_name = lot
else:
self.lot_id = lot
self.set_product_qty()
def set_product_qty(self):
if (
self.manual_entry
or self.is_manual_qty
or self.option_group_id.get_option_value("product_qty", "filled_default")
):
return
elif self.packaging_id:
self.packaging_qty = 1.0
self.product_qty = self.packaging_id.qty * self.packaging_qty
else:
self.packaging_qty = 0.0
self.product_qty = 1.0
def action_clean_lot(self):
self.lot_id = False
self.lot_name = False
self.action_show_step()
def action_clean_product(self):
self.product_id = False
self.action_show_step()
def action_clean_package(self):
self.package_id = False
self.result_package_id = False
self.action_show_step()
def action_create_package(self):
self.result_package_id = self.env["stock.quant.package"].create({})
def action_clean_values(self):
options = self.option_group_id.option_ids
options_to_clean = options.filtered(
lambda op: op.clean_after_done and op.field_name in self
)
for option in options_to_clean:
if option.field_name == "result_package_id" and self.keep_result_package:
continue
if option.field_name:
setattr(self, option.field_name, False)
self.action_show_step()
self.product_qty = 0.0
self.packaging_qty = 0.0
self.lot_name = False
def action_manual_entry(self):
return True
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_client"
).read()[0]
def action_back(self):
return self.env.ref("stock.stock_picking_type_action").read()[0]
def open_records(self):
action = self.action_ids
return action
def get_option_value(self, field_name, attribute):
option = self.option_group_id.option_ids.filtered(
lambda op: op.field_name == field_name
)[:1]
return option[attribute]
def action_force_done(self):
res = self.with_context(force_create_move=True).action_confirm()
self.visible_force_done = False
return res
@api.model_create_multi
def create(self, vals_list):
wizards = super().create(vals_list)
for wiz in wizards:
wiz.action_show_step()
return wizards
def action_manual_quantity(self):
action = self.get_formview_action()
form_view = self.env.ref(
"stock_barcodes.view_stock_barcodes_read_form_manual_qty"
)
action["views"] = [(form_view.id, "form")]
action["res_id"] = self.ids[0]
return action
def action_reopen_wizard(self):
return self.get_formview_action()
@api.onchange("step")
def action_show_step(self):
options_required = self.option_group_id.option_ids.filtered("required")
self.step = 0
for option in options_required:
if not getattr(self, option.field_name, False):
if option.field_name == "lot_id" and self.product_id.tracking == "none":
continue
self.step = option.step
break
if not self.step:
self.step = options_required[:1].step
options = self.option_group_id.option_ids.filtered(
lambda op: op.step == self.step and op.to_scan
)
self._set_messagge_info(
"info_page", _("Scan {}").format(", ".join(options.mapped("name")))
)
@api.onchange("package_id")
def onchange_package_id(self):
if self.manual_entry:
self.barcode = self.package_id.name
self.process_barcode_package_id()
def action_confirm(self):
if not self.check_option_required():
self.play_sounds(False)
return False
record = self.browse(self.ids)
record.write(self._convert_to_write(self._cache))
self = record
no_increase_qty_done, force_create_move = False, False
context = dict(self.env.context)
if self._name == "wiz.stock.barcodes.read.picking":
no_increase_qty_done = (
context.get("no_increase_qty_done", False) or self.manual_entry
)
force_create_move = context.get("force_create_move", False)
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()
if force_create_move:
# 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
and self.lot_name
and self.product_id
and self.product_id.tracking != "none"
and self.option_group_id.create_lot
):
self.lot_id = self._create_new_lot()
return True
def play_sounds(self, res):
if res:
self.send_bus_done(
"stock_barcodes_scan",
"stock_barcodes_sound",
{"sound": "ok", "res_model": self._name, "res_id": self.ids[0]},
)
else:
self.send_bus_done(
"stock_barcodes_scan",
"stock_barcodes_sound",
{"sound": "ko", "res_model": self._name, "res_id": self.ids[0]},
)
def _set_focus_on_qty_input(self, field_name=None):
if field_name is None:
field_name = "product_qty"
if field_name == "product_qty" and self.packaging_id:
field_name = "packaging_qty"
self.send_bus_done(
"stock_barcodes_scan",
"stock_barcodes_focus",
{
"action": "focus",
"field_name": field_name,
"res_model": self._name,
"res_id": self.ids[0],
},
)
@api.onchange("product_id")
def onchange_product_id(self):
self.product_uom_id = self.product_id.uom_id
@api.onchange("manual_entry")
def onchange_manual_entry(self):
if self.manual_entry and self.option_group_id.manual_entry_field_focus:
self._set_focus_on_qty_input(self.option_group_id.manual_entry_field_focus)
def _prepare_lot_vals(self):
return {
"name": self.lot_name,
"product_id": self.product_id.id,
"company_id": self.env.company.id,
}
def _create_new_lot(self):
StockProductionLot = self.env["stock.lot"]
lot_domain = [
("name", "=", self.lot_name),
("product_id", "=", self.product_id.id),
]
new_lot = StockProductionLot.search(lot_domain)
if not new_lot:
new_lot = StockProductionLot.create(self._prepare_lot_vals())
return new_lot
def action_clean_message(self):
self.message = False
self.check_option_required()
def action_keep_result_package(self):
self.keep_result_package = not self.keep_result_package
def display_notification(
self, message, message_type="warning", title=False, sticky=True
):
"""Send notifications to web client
message_type:
[options.type='warning'] 'info', 'success', 'warning', 'danger' or ''
sticky: Permanent notification until user removes it
"""
if self.option_group_id.display_notification and not self.env.context.get(
"skip_display_notification", False
):
message = {
"message": message,
"type": message_type,
"sticky": sticky,
"res_model": self._name,
"res_id": self.ids[0],
}
if title:
message["title"] = title
self.send_bus_done(
"stock_barcodes-{}".format(self.ids[0]),
"stock_barcodes_notify-{}".format(self.ids[0]),
message,
)