Initial commit: OCA Warehouse packages (12 packages)

This commit is contained in:
Ernad Husremovic 2025-08-29 15:43:06 +02:00
commit af1eea7692
627 changed files with 55555 additions and 0 deletions

View file

@ -0,0 +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

View file

@ -0,0 +1,148 @@
# Copyright 2019 Sergio Teruel <sergio.teruel@tecnativa.com>
# 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()

View file

@ -0,0 +1,905 @@
# 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,
)

View file

@ -0,0 +1,162 @@
# Copyright 2023 Tecnativa - Sergio Teruel
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo import _, api, fields, models
class WizStockBarcodesReadInventory(models.TransientModel):
_name = "wiz.stock.barcodes.read.inventory"
_inherit = "wiz.stock.barcodes.read"
_description = "Wizard to read barcode on inventory"
_allowed_product_types = ["product"]
# Overwrite is needed to take into account new domain values
product_id = fields.Many2one(domain=[("type", "in", _allowed_product_types)])
inventory_product_qty = fields.Float(
string="Inventory quantities", digits="Product Unit of Measure", readonly=True
)
inventory_quant_ids = fields.Many2many(
comodel_name="stock.quant", compute="_compute_inventory_quant_ids"
)
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):
for wiz in self:
domain = [
("user_id", "=", self.env.user.id),
("inventory_date", "<=", fields.Date.context_today(self)),
]
if wiz.display_read_quant:
domain.append(("inventory_quantity_set", "=", True))
order = "write_date DESC"
else:
domain.append(("inventory_quantity_set", "=", False))
order = None
quants = self.env["stock.quant"].search(domain, order=order)
if order is None:
quants = quants.sorted(
lambda q: (
q.location_id.posx,
q.location_id.posy,
q.location_id.posz,
q.location_id.name,
)
)
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,
"location_id": self.location_id.id,
"inventory_quantity": self.product_qty,
"lot_id": self.lot_id.id,
"package_id": self.package_id.id,
}
def _inventory_quant_domain(self):
return [
("user_id", "=", self.env.user.id),
(
"inventory_date",
"<=",
fields.Date.context_today(self).strftime("%Y-%m-%d"),
),
("product_id", "=", self.product_id.id),
("location_id", "=", self.location_id.id),
("lot_id", "=", self.lot_id.id),
("package_id", "=", self.package_id.id),
]
def _add_inventory_quant(self):
StockQuant = self.env["stock.quant"]
quant = StockQuant.search(self._inventory_quant_domain(), limit=1)
quant = quant.with_context(inventory_mode=True)
if quant:
if self.product_id.tracking == "serial" and (
quant.inventory_quantity > 0.0 or self.product_qty != 1
):
self._serial_tracking_message_fail()
return False
if self.option_group_id.accumulate_read_quantity:
quant.inventory_quantity += self.product_qty
else:
quant.inventory_quantity = self.product_qty
else:
if self.product_id.tracking == "serial" and self.product_qty != 1:
self._serial_tracking_message_fail()
return False
quant = StockQuant.with_context(inventory_mode=True).create(
self._prepare_stock_quant_values()
)
self.inventory_product_qty = quant.quantity
return True
def _serial_tracking_message_fail(self):
self._set_messagge_info(
"more_match",
_("Inventory line with more than one unit in serial tracked product"),
)
def action_done(self):
result = super().action_done()
if result:
result = self._add_inventory_quant()
if result:
self.action_clean_values()
return result
def action_manual_entry(self):
result = super().action_manual_entry()
if result:
self.action_done()
return result
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")
def _onchange_product_id(self):
if self.product_id != self.lot_id.product_id:
self.lot_id = False
@api.onchange("lot_id")
def _onchange_lot_id(self):
if self.lot_id and not self.env.context.get("keep_auto_lot"):
self.auto_lot = False
def apply_inventory(self):
action = self.env["ir.actions.actions"]._for_xml_id(
"stock.action_stock_inventory_adjustement_name"
)
action["context"] = {"default_quant_ids": self.inventory_quant_ids.ids}
return action

View file

@ -0,0 +1,204 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="view_stock_barcodes_read_inventory_form" model="ir.ui.view">
<field name="name">stock.barcodes.read.inventory.form</field>
<field name="model">wiz.stock.barcodes.read.inventory</field>
<field name="inherit_id" ref="stock_barcodes.view_stock_barcodes_read_form" />
<field name="mode">primary</field>
<field name="priority">99</field>
<field name="arch" type="xml">
<field name="product_qty" position="after">
<field name="inventory_product_qty" invisible="1" />
</field>
<!-- hide result package from inventory -->
<group name="quant_package" position="replace">
<div class="mt4">
<strong class="d-none d-sm-block">Package</strong>
<span
class="fa fa-inbox fa-2x d-sm-none oe_span_small_icon"
title="package"
/>
<field
name="package_id"
options="{'no_create': True}"
attrs="{'readonly': [('manual_entry', '=', False)]}"
force_save="1"
style="width:85%"
class="h5"
/>
</div>
</group>
<field name="location_id" position="attributes">
<attribute name="domain">[('usage', 'in', ['internal', 'transit'])]
</attribute>
</field>
<group name="scan_fields" position="after">
<group
string="Inventory quants"
attrs="{'invisible': [('inventory_quant_ids', '=', [])]}"
col="2"
class="px-3"
>
<field
name="inventory_quant_ids"
options="{'no_open': True}"
nolabel="1"
colspan="2"
mode="kanban"
>
<kanban
class="o_kanban_mobile"
js_class="stock_barcodes_kanban"
>
<field name="product_id" />
<field name="inventory_quantity" />
<field name="product_uom_id" />
<templates>
<t t-name="kanban-box">
<div t-on-click="onCustomGlobalClick">
<div class="row w-auto">
<div class="col-8 col-md-10">
<field name="product_id" class="h2" />
</div>
<div
class="col-4 col-md-2 d-flex justify-content-end align-items-center"
>
<img
t-att-src="kanban_image('product.product', 'image_128', record.product_id.raw_value)"
role="img"
t-att-title="record.product_id.value"
height="40"
width="40"
t-att-alt="record.product_id.value"
/>
<button
name="action_barcode_inventory_quant_edit"
type="object"
class="btn mt0"
context="{'wiz_barcode_id': parent.id}"
>
<i
class="fa fa-pencil"
title="Edit inventory quantity"
/>
</button>
</div>
</div>
<div class="row">
<div
class="col-8 col-md-10 d-flex justify-content-start align-items-center"
>
<span
t-if="record.lot_id.raw_value"
class="h3"
>Lot S/N:&#32;&#32;</span>
<field
t-if="record.lot_id.raw_value"
name="lot_id"
class="h2"
/>
</div>
<div
class="col-4 col-md-2 d-flex justify-content-end align-items-center"
>
<span class="fw-bold mx-3">
<field name="inventory_quantity" />
</span>
<button
name="action_barcode_inventory_quant_unlink"
type="object"
class="btn mt0"
context="{'wiz_barcode_id': parent.id}"
>
<i
class="fa fa-trash"
title="Reset inventory quantity"
/>
</button>
</div>
</div>
<div
t-attf-class="row oe_kanban_operations oe_kanban_operations-#{record.id.raw_value} d-none"
>
<div
class="d-flex justify-content-end align-items-center"
>
<button
name="operation_quantities_rest"
type="object"
class="btn btn-lg btn-op-rest text-white ms-2 ms-sm-4 mx-2"
>-1
</button>
<button
name="operation_quantities"
type="object"
class="btn btn-lg btn-op-sum text-white"
>+1
</button>
</div>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</group>
</group>
<xpath
expr="//div[hasclass('oe_stock_barcodes_bottombar')]//div[hasclass('dropup')]"
position="inside"
>
<field
name="display_read_quant"
widget="barcode_boolean_toggle"
class="d-none"
/>
<button
name="action_display_read_quant"
type="object"
title="Read items"
icon="fa-eye fa-2x"
attrs="{'invisible': [('display_read_quant', '=', True)]}"
/>
<button
name="action_display_read_quant"
type="object"
title="Read items"
icon="fa-eye-slash fa-2x"
attrs="{'invisible': [('display_read_quant', '=', False)]}"
/>
</xpath>
<button name="action_clean_values" position="before">
<button
name="apply_inventory"
type="object"
icon="fa-check fa-2x"
class="btn-primary w-100 oe_kanban_action_button btn-sm d-flex justify-content-center align-items-center fs-1"
attrs="{'invisible': ['|', '|',('display_menu', '=', True), ('inventory_quant_ids', '=', []), ('display_read_quant', '=', False)]}"
data-hotkey="7"
groups="stock.group_stock_manager"
>
<span class="d-none d-lg-block">Apply</span>
(<span class="count_apply_inventory" />)
</button>
</button>
</field>
</record>
<!--
Open wizard in current target option to avoid that the wizard is
closed after any button click,
-->
<record id="action_stock_barcodes_read_inventory" model="ir.actions.act_window">
<field name="res_model">wiz.stock.barcodes.read.inventory</field>
<field name="name">Barcodes Read</field>
<field name="view_mode">form</field>
<field name="context">{"control_panel_hidden": True,
"form_view_initial_mode": "edit",
"inventory_mode": True,
}
</field>
<field name="view_id" ref="view_stock_barcodes_read_inventory_form" />
<field name="target">fullscreen</field>
</record>
</odoo>

View file

@ -0,0 +1,415 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="view_stock_barcodes_read_picking_form" model="ir.ui.view">
<field name="name">stock.barcodes.read.picking.form</field>
<field name="model">wiz.stock.barcodes.read.picking</field>
<field name="inherit_id" ref="stock_barcodes.view_stock_barcodes_read_form" />
<field name="mode">primary</field>
<field name="arch" type="xml">
<xpath expr="//button[@name='open_actions']" position="after">
<button
name="action_open_picking"
type="object"
class="btn btn-primary"
data-hotkey="'shift+o'"
>
<field name="picking_id" />
</button>
<t t-if="partner_id">
[<field
name="partner_id"
invisible="context.get('hide_partner', False)"
/>]
</t>
<field name="candidate_picking_id" invisible="1" />
<button
name="action_unlock_picking"
type="object"
title="unlock picking"
attrs="{'invisible': [('candidate_picking_id', '=', 'picking_id')]}"
class="float-end"
>
<span class="fa-stack fa-lg">
<!-- FIXME: Use fa-thumbtack fa-stack-2x on v13 with FA v5.4 -->
<i class="fa fa-thumb-tack fa-stack-1x" />
<!-- FIXME: Use fa-slash on v13 with FA v5.4 -->
<i class="fa fa-ban fa-stack-2x" />
</span>
</button>
<button
name="action_lock_picking"
type="object"
title="lock picking"
attrs="{'invisible': [('candidate_picking_id', '!=', 'picking_id')]}"
class="fa fa-thumb-tack fa-2x float-end"
/>
</xpath>
<xpath expr="//field[@name='message_type']" position="before">
<field
name="candidate_picking_ids"
attrs="{'invisible': [('candidate_picking_ids', '=', [])]}"
mode="kanban"
nolabel="1"
force_save="1"
class="o_x2m_control_panel"
options="{'always_reload': True}"
>
<kanban>
<field name="name" />
<field name="partner_id" />
<field name="date" />
<field name="state" />
<field name="picking_id" />
<field name="wiz_picking_id" />
<field name="product_qty_reserved" />
<field name="product_uom_qty" />
<field name="product_qty_done" />
<field name="scan_count" />
<field name="is_pending" />
<field name="note" />
<templates>
<t t-name="kanban-box">
<div
t-if="!widget.isHtmlEmpty(record.note.raw_value)"
t-att-class="'oe_kanban_color_alert' + (record.is_pending.raw_value == false ? ' bg-success' : '')"
>
<div class="oe_kanban_details p-2">
<field
name="scan_count"
invisible="1"
force_save="1"
/>
<div
class="fw-bold text-center text-danger fst-italic"
>
<t
t-out="record.note.value"
invisible="context.get('hide_note', False)"
/>
</div>
</div>
</div>
</t>
</templates>
</kanban>
</field>
<field
name="todo_line_display_ids"
mode="kanban"
force_save="1"
attrs="{'invisible': [('todo_line_display_ids', '=', [])]}"
/>
</xpath>
<field name="location_id" position="before">
<field name="picking_type_code" invisible="1" force_save="1" />
<field name="picking_id" invisible="1" force_save="1" />
<field name="show_detailed_operations" invisible="1" />
<field name="picking_location_id" invisible="1" />
<field name="picking_location_dest_id" invisible="1" />
<field name="company_id" invisible="1" />
<field name="todo_line_is_extra_line" invisible="1" />
<field name="qty_available" invisible="1" />
</field>
<field name="location_id" position="attributes">
<attribute
name="domain"
>[('id', 'child_of', picking_location_id), '|', ('company_id', '=', False), ('company_id', '=',
company_id), ('usage', '!=', 'view')]
</attribute>
</field>
<group name="location" position="attributes">
<attribute
name="attrs"
>{'readonly': [('manual_entry', '=', False)], 'invisible': [('picking_type_code', '=', 'incoming')]}
</attribute>
</group>
<group name="location" position="after">
<div attrs="{'invisible': [('picking_type_code', '=', 'outgoing')]}">
<strong class=" d-none d-sm-block">Dest. Location</strong>
<span
class="fa fa-2x fa-share text-center d-sm-none oe_span_small_icon"
title="Destination Location"
/>
<field
name="location_dest_id"
options="{'no_create': True, 'no_open': True}"
attrs="{'readonly': [('manual_entry', '=', False)]}"
force_save="1"
nolabel="1"
style="width:90%"
class="h5"
domain="[('id', 'child_of', picking_location_dest_id), '|', ('company_id', '=', False), ('company_id', '=', company_id), ('usage', '!=', 'view')]"
/>
</div>
</group>
<group name="scan_fields" position="attributes">
<!-- hide group scan_fields for extra todo lines -->
<attribute
name="attrs"
>{'invisible': [('todo_line_is_extra_line', '!=', False)]}
</attribute>
</group>
<group name="scan_fields" position="after">
<div
attrs="{'invisible': [('picking_state', '!=', 'done')]}"
class="oe_kanban_picking_done bg-dark d-flex flex-column justify-content-center align-items-center text-white"
>
<span class="fa fa-6x mb-4 fa-exclamation-triangle" />
<span class="fa fa-2x fa-exclamation-triangle">
This picking is already done
</span>
</div>
<group
string="Pending moves"
attrs="{'invisible': ['|', ('picking_state', '=', 'done'), ('pending_move_ids', '=', [])]}"
col="2"
>
<field
name="pending_move_ids"
options="{'no_open': True, 'always_reload': True}"
nolabel="1"
colspan="2"
force_save="1"
mode="kanban"
>
<kanban class="o_kanban_mobile">
<field name="id" />
<field name="state" />
<field name="product_id" />
<field name="product_uom_qty" />
<field name="qty_done" />
<field name="qty_done_rest" />
<field name="uom_id" />
<field name="product_qty_reserved" />
<field name="picking_state" />
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_global_click">
<div class="row">
<div class="col-8 col-md-8">
<field name="product_id" class="h2" />
<div
class="d-flex justify-content-start align-items-center indent h4 pt-3"
>
<span
t-esc="record.qty_done.raw_value"
/>
&#32;/&#32;
<span
t-esc="record.product_uom_qty.raw_value"
/>
<span
t-if="record.uom_id"
t-esc="record.uom_id.value"
/>
</div>
</div>
<div
class="col-4 col-md-4 d-flex justify-content-end align-items-center"
>
<button
name="action_barcode_inventory_quant_edit"
type="object"
class="btn mt0"
context="{'wiz_barcode_id': parent.id}"
>
<i
class="fa fa-pencil"
title="Edit inventory quantity"
/>
</button>
<button
name="operation_quantities"
type="object"
class="btn btn-lg btn-primary btn-op-sum"
t-if="record.qty_done_rest.raw_value > 0"
>
+
<span
class="text-white"
t-esc="record.qty_done_rest.raw_value"
/>
</button>
</div>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</group>
<h3
class="mt-4 w-100 px-4"
attrs="{'invisible': ['|', ('move_line_ids', '=', []), ('show_detailed_operations', '=', False)]}"
>
Detailed operations
</h3>
<group
string="Detailed operations"
col="2"
attrs="{'invisible': ['|', ('move_line_ids', '=', []), ('show_detailed_operations', '=', False)]}"
>
<field
name="move_line_ids"
options="{'no_open': True, 'always_reload': True}"
nolabel="1"
colspan="2"
force_save="1"
mode="kanban"
>
<kanban class="o_kanban_mobile">
<field name="product_id" />
<field name="qty_done" />
<field name="product_uom_id" />
<field name="lot_id" />
<field name="result_package_id" />
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_global_click">
<div class="row">
<div class="col-8 col-md-8">
<field name="product_id" class="h2" />
<div
class="d-flex justify-content-start align-items-center indent h4 pt-3"
>
<t t-if="record.lot_id.raw_value">
Lot:
<span
t-esc="record.lot_id.raw_value"
/>
</t>
<t
t-if="record.result_package_id.raw_value"
>
Package:
<span
t-esc="record.result_package_id.raw_value"
/>
</t>
</div>
</div>
<div
class="col-4 col-md-4 d-flex justify-content-end align-items-center"
>
<span
class="h3"
t-esc="record.qty_done.raw_value"
/>
<button
name="action_barcode_detailed_operation_unlink"
type="object"
class="btn mt0"
context="{'wiz_barcode_id': parent.id}"
>
<i
class="fa fa-trash"
title="Remove"
/>
</button>
</div>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</group>
</group>
<xpath expr="//button[@id='btn_create_lot']" position="after">
<field name="display_assign_serial" invisible="1" />
<button
name="action_assign_serial"
type="object"
string="Range"
title="Assign Serial Numbers"
attrs="{'invisible': [('display_assign_serial', '=', False)]}"
class="btn btn-secondary btn-sm"
/>
</xpath>
<xpath expr="//button[@name='action_clean_values']" position="before">
<field name="picking_state" invisible="1" />
<button
name="action_put_in_pack"
help="Put in pack"
type="object"
icon="fa-cube fa-2x"
title="Put in Pack"
attrs="{'invisible': ['|', ('picking_state', 'in', ('draft', 'done', 'cancel')), ('display_menu', '=', True)]}"
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"
>
<span class="d-none d-lg-block">Put in back</span>
</button>
<!-- t-att-class="'btn float-end' + (record.is_pending.raw_value == false ? ' btn-primary' : ' btn-secondary border')"-->
<button
name="action_validate_picking"
type="object"
icon="fa-check fa-2x"
class="btn btn-secondary w-100 oe_kanban_action_button btn-sm text-uppercase
d-flex justify-content-center align-items-center fs-2"
attrs="{'invisible': [('picking_state', 'not in', ['draft', 'assigned', 'confirmed'])]}"
confirm="Are you sure to validate the picking ?"
data-hotkey="'shift+v'"
>
<span class="d-none d-lg-block">Validate</span>
</button>
</xpath>
<xpath
expr="//div[hasclass('oe_stock_barcodes_bottombar')]//div[hasclass('dropup')]"
position="inside"
>
<field
name="show_detailed_operations"
widget="barcode_boolean_toggle"
class="d-none"
/>
<button
name="action_show_detailed_operations"
type="object"
title="Detailed operations"
icon="fa-eye fa-2x"
attrs="{'invisible': ['|', ('move_line_ids', '=', []), ('show_detailed_operations', '=', True)]}"
/>
<button
name="action_show_detailed_operations"
type="object"
title="Detailed operations"
icon="fa-eye-slash fa-2x"
attrs="{'invisible': ['|', ('move_line_ids', '=', []), ('show_detailed_operations', '=', False)]}"
/>
</xpath>
</field>
</record>
<!--
Open wizard in current target option to avoid that the wizard is
closed after any button click,
-->
<record model="ir.actions.act_window" id="action_stock_barcodes_read_picking">
<field name="res_model">wiz.stock.barcodes.read.picking</field>
<field name="name">Barcodes Read</field>
<field name="view_mode">form</field>
<field name="context">{"control_panel_hidden": True,
"form_view_initial_mode": "edit"}
</field>
<field name="view_id" ref="view_stock_barcodes_read_picking_form" />
<field name="target">fullscreen</field>
</record>
<record model="ir.actions.act_window" id="action_stock_barcodes_menu">
<field name="res_model">wiz.stock.barcodes.read.picking</field>
<field name="name">Barcodes menu</field>
<field name="view_mode">form</field>
<field
name="context"
>{'control_panel_hidden': True, "default_display_menu": True}
</field>
<field name="view_id" ref="view_stock_barcodes_read_picking_form" />
<field name="target">current</field>
</record>
</odoo>

View file

@ -0,0 +1,223 @@
# Copyright 2019 Sergio Teruel <sergio.teruel@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from odoo import api, fields, models
from odoo.tools.float_utils import float_compare
class WizStockBarcodesReadTodo(models.TransientModel):
_name = "wiz.stock.barcodes.read.todo"
_description = "Wizard to read barcode todo"
# To prevent remove the record wizard until 2 days old
_transient_max_hours = 48
name = fields.Char()
wiz_barcode_id = fields.Many2one(comodel_name="wiz.stock.barcodes.read.picking")
picking_state = fields.Selection(related="wiz_barcode_id.picking_state")
partner_id = fields.Many2one(
comodel_name="res.partner",
readonly=True,
string="Partner",
)
state = fields.Selection(
[("pending", "Pending"), ("done", "Done"), ("done_forced", "Done forced")],
string="Scan State",
default="pending",
compute="_compute_state",
readonly=False,
)
product_qty_reserved = fields.Float(
"Reserved",
digits="Product Unit of Measure",
readonly=True,
)
product_uom_qty = fields.Float(
"Demand",
digits="Product Unit of Measure",
readonly=True,
)
qty_done = fields.Float(
"Done",
digits="Product Unit of Measure",
compute="_compute_qty_done",
)
qty_done_rest = fields.Float(compute="_compute_qty_done_rest", store=True)
location_id = fields.Many2one(comodel_name="stock.location")
location_name = fields.Char(related="location_id.name")
location_dest_id = fields.Many2one(comodel_name="stock.location")
location_dest_name = fields.Char(
string="Destinatino Name", related="location_dest_id.name"
)
product_id = fields.Many2one(comodel_name="product.product")
lot_id = fields.Many2one(comodel_name="stock.lot")
uom_id = fields.Many2one(comodel_name="uom.uom")
package_id = fields.Many2one(comodel_name="stock.quant.package")
result_package_id = fields.Many2one(comodel_name="stock.quant.package")
package_product_qty = fields.Float()
res_model_id = fields.Many2one(comodel_name="ir.model")
res_ids = fields.Char()
line_ids = fields.Many2many(comodel_name="stock.move.line")
stock_move_ids = fields.Many2many(comodel_name="stock.move")
position_index = fields.Integer()
picking_code = fields.Char("Type of Operation")
is_extra_line = fields.Boolean()
# Used in kanban view
is_stock_move_line_origin = fields.Boolean()
@api.depends("qty_done", "product_uom_qty")
def _compute_qty_done_rest(self):
for rec in self:
rec.qty_done_rest = rec.product_uom_qty - rec.qty_done
def action_todo_next(self):
self.state = "done_forced"
self.line_ids.barcode_scan_state = "done_forced"
for sml in self.line_ids:
if (
float_compare(
sml.reserved_uom_qty,
sml.qty_done,
precision_rounding=sml.product_uom_id.rounding,
)
== 0
):
continue
if sml.move_id.state == "confirmed" and sml.qty_done:
sml.move_id.state = "partially_available"
if sml.move_id.state in ["partially_available", "assigned"]:
sml.reserved_uom_qty = sml.qty_done
if self.is_extra_line or not self.is_stock_move_line_origin:
barcode_backorder_action = self.env.context.get(
"barcode_backorder_action", "create_backorder"
)
self.stock_move_ids.barcode_backorder_action = barcode_backorder_action
if barcode_backorder_action == "pending":
self.stock_move_ids.move_line_ids.unlink()
self.stock_move_ids._action_assign()
wiz_barcode = self.wiz_barcode_id
self.wiz_barcode_id.fill_todo_records()
self.wiz_barcode_id = wiz_barcode
self.wiz_barcode_id.determine_todo_action()
def action_reset_lines(self):
self.state = "pending"
self.line_ids.barcode_scan_state = "pending"
self.line_ids.qty_done = 0.0
self.wiz_barcode_id.action_clean_values()
self.wiz_barcode_id.fill_todo_records()
self.wiz_barcode_id.determine_todo_action()
def action_back_line(self):
if self.position_index > 0:
record = self.wiz_barcode_id.todo_line_ids[self.position_index - 1]
self.wiz_barcode_id.determine_todo_action(forced_todo_line=record)
def action_next_line(self):
if self.position_index < len(self.wiz_barcode_id.todo_line_ids) - 1:
record = self.wiz_barcode_id.todo_line_ids[self.position_index + 1]
self.wiz_barcode_id.determine_todo_action(forced_todo_line=record)
@api.depends("line_ids.qty_done")
def _compute_qty_done(self):
for rec in self:
rec.qty_done = sum(ln.qty_done for ln in rec.line_ids)
@api.depends(
"line_ids",
"line_ids.qty_done",
"line_ids.reserved_uom_qty",
"line_ids.barcode_scan_state",
"qty_done",
"product_uom_qty",
)
def _compute_state(self):
for rec in self:
if float_compare(
rec.qty_done,
rec.product_uom_qty,
precision_rounding=rec.uom_id.rounding,
) > -1 or (
rec.wiz_barcode_id.option_group_id.source_pending_moves
== "move_line_ids"
and rec.line_ids
and (
sum(rec.stock_move_ids.mapped("quantity_done"))
>= sum(rec.stock_move_ids.mapped("product_uom_qty"))
or not any(
ln.barcode_scan_state == "pending" for ln in rec.line_ids
)
)
):
rec.state = "done"
else:
rec.state = "pending"
@api.model
def fields_to_fill_from_pending_line(self):
res = [
"location_id",
"location_dest_id",
"product_id",
"lot_id",
"package_id",
]
if not self.wiz_barcode_id.keep_result_package:
res.append("result_package_id")
return res
def fill_from_pending_line(self):
self.wiz_barcode_id.selected_pending_move_id = self
self.wiz_barcode_id.determine_todo_action(forced_todo_line=self)
for field in self.fields_to_fill_from_pending_line():
self.wiz_barcode_id[field] = self[field]
# Force fill product_qty if filled_default is set
if self.wiz_barcode_id.option_group_id.get_option_value(
"product_qty", "filled_default"
):
self.wiz_barcode_id.product_qty = self.product_uom_qty - sum(
self.line_ids.mapped("qty_done")
)
self.wiz_barcode_id.product_uom_id = self.uom_id
self.wiz_barcode_id.action_show_step()
self.wiz_barcode_id._set_focus_on_qty_input()
def operation_quantities(self):
self.wiz_barcode_id.manual_entry = True
self.wiz_barcode_id.product_qty = self.product_qty_reserved
self.wiz_barcode_id.product_id = self.product_id.id
if self.wiz_barcode_id.picking_id.picking_type_id.code != "incoming":
self.wiz_barcode_id.qty_available = self.product_qty_reserved
self.wiz_barcode_id.product_id = self.product_id.id
self.wiz_barcode_id.location_id = self.location_id.id
self.wiz_barcode_id.with_context(manual_picking=True).action_confirm()
def _get_fields_to_edit(self):
return [
"location_dest_id",
"location_id",
"product_id",
"lot_id",
"package_id",
]
def action_barcode_inventory_quant_edit(self):
wiz_barcode_id = self.env.context.get("wiz_barcode_id", False)
wiz_barcode = self.env["wiz.stock.barcodes.read.picking"].browse(wiz_barcode_id)
for quant in self:
# Try to assign fields with the same name between quant and the scan wizard
for fname in self._get_fields_to_edit():
if hasattr(wiz_barcode, fname):
wiz_barcode[fname] = quant[fname]
wiz_barcode.product_qty = quant.qty_done
wiz_barcode.manual_entry = True
self.env["bus.bus"]._sendone(
"stock_barcodes_scan",
"stock_barcodes_edit_manual",
{
"manual_entry": True,
},
)

View file

@ -0,0 +1,214 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="view_stock_barcodes_todo_kanban" model="ir.ui.view">
<field name="name">stock.barcodes.todo.kanban</field>
<field name="model">wiz.stock.barcodes.read.todo</field>
<field name="arch" type="xml">
<kanban class="o_kanban_mobile">
<field name="picking_code" />
<field name="location_id" />
<field name="location_name" />
<field name="location_dest_id" />
<field name="location_dest_name" />
<field name="product_id" />
<field name="lot_id" />
<field name="uom_id" />
<field name="package_id" />
<field name="result_package_id" />
<field name="package_product_qty" />
<field name="product_uom_qty" />
<field name="qty_done" />
<field name="line_ids" invisible="1" />
<field name="state" />
<field name="is_extra_line" />
<field name="is_stock_move_line_origin" />
<templates>
<t t-name="kanban-box">
<div
t-attf-class="oe_kanban_content "
t-attf-style="background-color: {{record.is_extra_line.raw_value == true and '#ffd683' or '#f0f9fb'}};"
>
<div class="row">
<div class="col">
<span
class="fa fa-map-marker"
title="Location name"
/>
<strong>
<span
attrs="{'invisible': [('picking_code', '!=', 'incoming')]}"
>
<field name="location_dest_name" />
</span>
<span
attrs="{'invisible': [('picking_code', '!=', 'internal')]}"
>
<field name="location_name" /><field
name="location_dest_name"
/>
</span>
<span
attrs="{'invisible': [('picking_code', '!=', 'outgoing')]}"
>
<field name="location_name" />
</span>
</strong>
</div>
</div>
<div class="row">
<div class="col">
<strong>
<field name="product_id" />
</strong>
</div>
</div>
<t
t-if="record.lot_id or record.package_id or record.result_package_id"
>
<table>
<tr>
<td>
<span>
<span
class="fa fa-tags"
title="Lot S/N"
/>
<field
name="lot_id"
options="{'no_open': True}"
/>
</span>
</td>
<td class="text-end">
<span class="fa fa-dropbox" />
<span>
<field name="package_id" />
<t t-if="record.package_id">(<span
t-esc="record.package_product_qty.value"
/> <t
t-esc="record.uom_id.value.slice(0,3)"
/>)</t>
</span>
<span>
<field name="result_package_id" />
</span>
</td>
</tr>
</table>
</t>
<div class="row">
<div class="col-12">
<span>
<span class="fw-bold">
<t t-esc="record.qty_done.value" />
</span> / <t
t-esc="record.product_uom_qty.value"
/> <t t-esc="record.uom_id.value.slice(0,3)" />
</span>
<strong
class="bg-danger"
t-if="record.is_extra_line.raw_value == true"
>NOT AVAILABLE</strong>
</div>
</div>
<div name="action" class="row">
<div class="col-2">
<button
name="action_back_line"
type="object"
icon="fa-step-backward"
title="Previous"
class="btn-sm float-start btn btn-primary"
context="{'wiz_barcode_id': parent.id}"
data-hotkey="1"
/>
</div>
<div class="col-4 p-0">
<button
name="action_reset_lines"
type="object"
icon="fa-trash"
title="Clean"
class="btn-sm btn mx-auto d-block btn-warning"
context="{'wiz_barcode_id': parent.id}"
data-hotkey="2"
/>
</div>
<div class="col-4 p-0">
<button
name="action_todo_next"
type="object"
class="btn-sm mx-auto d-block btn-danger btn"
context="{'wiz_barcode_id': parent.id}"
data-hotkey="3"
attrs="{'invisible': ['|','|', ('qty_done', '=', 0.0), ('is_extra_line', '=', True), ('is_stock_move_line_origin', '=', False)]} "
>
Ignore rest
</button>
<!-- Ask for confirmation when we've got done quantities to avoid squashing quantities -->
<button
name="action_todo_next"
type="object"
class="btn-sm mx-auto d-block btn-danger btn"
context="{'wiz_barcode_id': parent.id}"
data-hotkey="3"
attrs="{'invisible': ['|', '|', ('qty_done', '!=', 0.0), ('is_extra_line', '=', True), ('is_stock_move_line_origin', '=', False)]} "
confirm="You have not set any quantity to this operation and it will be removed from pending moves. Are you sure?"
>Ignore rest
</button>
</div>
<div class="col-2">
<button
name="action_next_line"
type="object"
icon="fa-step-forward"
title="Next"
class="btn-sm float-end btn btn-primary"
context="{'wiz_barcode_id': parent.id}"
data-hotkey="4"
/>
</div>
</div>
<div name="action_extra" class="row mt-2">
<div class="col-12">
<button
name="action_todo_next"
type="object"
class="btn btn-warning float-end btn-sm"
context="{'wiz_barcode_id': parent.id, 'barcode_backorder_action': 'pending'}"
data-hotkey="3"
attrs="{'invisible': [('is_extra_line', '=', False), ('is_stock_move_line_origin', '=', True)]} "
confirm="This move will be set to pending. Are you sure?"
>
Restore to pending
</button>
<button
name="action_todo_next"
type="object"
class="btn btn-danger float-end btn-sm me-5"
context="{'wiz_barcode_id': parent.id, 'barcode_backorder_action': 'skip_backorder'}"
data-hotkey="3"
attrs="{'invisible': [('is_extra_line', '=', False), ('is_stock_move_line_origin', '=', True)]} "
confirm="Odoo will not create a backorder for this move. Are you sure?"
>
No Backorder
</button>
<button
name="action_todo_next"
type="object"
class="btn btn-primary float-end btn-sm me-3"
context="{'wiz_barcode_id': parent.id, 'barcode_backorder_action': 'create_backorder'}"
data-hotkey="3"
attrs="{'invisible': [('is_extra_line', '=', False), ('is_stock_move_line_origin', '=', True)]} "
>
Create Backorder
</button>
</div>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
</odoo>

View file

@ -0,0 +1,489 @@
<odoo>
<record id="view_stock_barcodes_read_form" model="ir.ui.view">
<field name="name">stock.barcodes.read.form</field>
<field name="model">wiz.stock.barcodes.read</field>
<field name="arch" type="xml">
<form
string="Barcodes"
class="oe_stock_barcordes_form h-100"
js_class="stock_barcodes_form"
>
<div
class="d-flex flex-column h-100"
attrs="{'invisible': [('display_menu', '=', True)]}"
>
<div
attrs="{'invisible': [('display_menu', '=', True)]}"
class="flex-fill oe_stock_barcordes_content"
>
<div name="info" class="text-center h3 mb-2">
<div
class="alert barcode-info text-white mb-0 d-flex"
role="status"
>
<button
name="open_actions"
type="object"
class="ms-auto oe_kanban_action_button btn-sm"
title="Open actions"
attrs="{'invisible': [('display_menu', '=', True)]}"
data-hotkey="0"
>
<i class="fa fa-chevron-left fa-2x text-white" />
</button>
<field name="message" class="mt-3 h3 text-white" />
<span class="fa fa-barcode fa-2x mt-2 mx-3" />
</div>
</div>
<field name="message_type" invisible="1" />
<field name="barcode" invisible="1" force_save="1" />
<field name="step" invisible="1" force_save="1" />
<field name="is_manual_qty" invisible="1" />
<field name="is_manual_confirm" invisible="1" />
<field name="auto_lot" invisible="1" />
<field name="product_tracking" invisible="1" force_save="1" />
<field name="guided_product_id" invisible="1" force_save="1" />
<field name="guided_location_id" invisible="1" force_save="1" />
<field
name="guided_location_dest_id"
invisible="1"
force_save="1"
/>
<field name="guided_lot_id" invisible="1" force_save="1" />
<field name="visible_force_done" invisible="1" force_save="1" />
<field name="res_model_id" invisible="1" />
<field name="res_id" invisible="1" />
<field name="option_group_id" invisible="1" force_save="1" />
<field name="confirmed_moves" invisible="1" force_save="1" />
<field name="owner_id" invisible="1" force_save="1" />
<field name="keep_result_package" invisible="1" />
<field name="create_lot" invisible="1" />
<field name="lot_id" invisible="1" />
<field
name="_barcode_scanned"
widget="barcode_handler"
invisible="0"
/>
<group
name="scan_fields"
class="bg-light scan_fields p-2 d-none"
>
<group name="location" col="1">
<div class="mt-1" colspan="2">
<strong class=" d-none d-sm-block">Source Location
</strong>
<span
class="fa fa-map-marker fa-2x d-sm-none oe_span_small_icon"
title="Source Location"
/>
<field
name="location_id"
options="{'no_create': True, 'no_open': True}"
attrs="{'readonly': [('manual_entry', '=', False)]}"
force_save="1"
nolabel="1"
style="width:90%"
class="h5"
/>
</div>
</group>
<group
name="quant_package"
groups="stock.group_tracking_lot"
col="2"
>
<div class="m-0" colspan="2">
<strong
class="d-none d-sm-block"
>Source Package -&gt; Result Package
</strong>
<span
class="fa fa-cubes d-sm-none oe_span_small_icon"
title="Source Package to Result Package"
/>
<field
name="package_id"
options="{'no_create': True}"
attrs="{'readonly': [('manual_entry', '=', False)]}"
force_save="1"
class="h5 oe_inline"
style="width: 35% !important"
/>
<span
attrs="{'invisible': [('result_package_id', '=', False)]}"
>-&gt;
</span>
<field
name="result_package_id"
options="{'no_create': True, 'no_open': True}"
attrs="{'readonly': [('manual_entry', '=', False)]}"
force_save="1"
class="h5 oe_inline ms-1"
style="width: 35% !important"
/>
<!-- Double button to display open or closed padlock -->
<button
id="btn_keep_result_package_lock"
class="btn-sm btn-danger oe_kanban_action_button btn boder ms-1"
type="object"
name="action_keep_result_package"
title="If locked keep result package"
icon="fa-lock"
attrs="{'invisible': [('keep_result_package', '=', False)]}"
/>
<button
id="btn_keep_result_package_unlock"
class="btn-sm oe_kanban_action_button btn btn-secondary border ms-1"
type="object"
name="action_keep_result_package"
title="If locked keep result package"
icon="fa-unlock"
attrs="{'invisible': [('keep_result_package', '=', True)]}"
/>
<!-- End padlock -->
<button
class="btn-sm btn-warning oe_kanban_action_button btn border ms-1"
name="action_clean_package"
type="object"
icon="fa-trash fa-1x"
attrs="{'invisible': [('package_id', '=', False), ('result_package_id', '=', False)]}"
title="Clean package info"
/>
<button
id="btn_create_package"
class="btn-sm oe_kanban_action_button btn btn-secondary border ms-1"
type="object"
name="action_create_package"
icon="fa-plus fa-1x"
title="Create new package"
/>
</div>
</group>
<group col="2">
<div class="m-0" colspan="2">
<strong class="d-none d-sm-block">Product</strong>
<span
class="fa fa-th-list fa-2x d-sm-none oe_span_small_icon"
title="Product"
/>
<field
name="product_id"
options="{'no_create': True, 'no_open': True}"
attrs="{'readonly': [('manual_entry', '=', False)]}"
force_save="1"
style="width:80%"
class="h5"
/>
<button
class="btn-sm float-end btn-warning oe_kanban_action_button btn mr4"
name="action_clean_product"
type="object"
icon="fa-trash fa-1x"
title="Clean product"
attrs="{'invisible': [('product_id', '=', False)]}"
/>
</div>
</group>
<group
groups="stock.group_production_lot"
col="2"
attrs="{'invisible': [('product_tracking', 'in', [False, 'none'])]}"
>
<div class="m-0" colspan="2">
<strong class="d-none d-sm-block">Lot S/N</strong>
<span
class="fa fa-tags fa-2x d-sm-none oe_span_small_icon"
title="Lot S/N"
/>
<field
name="lot_name"
attrs="{'invisible': [('create_lot', '=', False)]}"
style="width:60%"
class="h5"
/>
<field
name="lot_id"
options="{'no_create': True, 'no_open': True}"
domain="[('product_id', '=', product_id)]"
context="{'default_product_id': product_id}"
attrs="{'readonly': [('manual_entry', '=', False)], 'invisible': [('create_lot', '!=', False)]}"
force_save="1"
class="h5"
/>
<button
class="btn-sm float-end btn-warning oe_kanban_action_button btn mr4"
name="action_clean_lot"
type="object"
icon="fa-trash fa-1x ml-2"
attrs="{'invisible': [('lot_id', '=', False), ('lot_name', '=', False)]}"
title="Clean lot"
data-hotkey="G"
/>
<button
id="btn_create_lot"
class="btn-sm float-end oe_kanban_action_button btn btn-secondary border"
type="action"
name="%(action_stock_barcodes_new_lot)d"
icon="fa-plus fa-1x"
title="Create lot"
context="{'default_product_id': product_id}"
attrs="{'invisible': [('create_lot', '=', False)]}"
help="Create new lot"
/>
</div>
</group>
<group
name="option_qty_info"
attrs="{'invisible': ['|', ('product_id', '=', False),'|', ('is_manual_qty', '=', True), ('manual_entry', '=', True)]}"
col="2"
>
<div class="m-0" colspan="2">
<strong class="d-none d-sm-block">Total Qty</strong>
<span
class="fa fa-hashtag d-sm-none oe_span_small_icon"
title="Total Quantity"
/>
<field
name="product_qty"
attrs="{'readonly': [('manual_entry', '=', False)]}"
force_save="1"
style="width:85%"
class="h5"
/>
</div>
</group>
<group
name="option_qty"
attrs="{'invisible': ['|', ('product_id', '=', False),'&amp;', ('is_manual_qty', '=', False), ('manual_entry', '=', False)]}"
col="2"
>
<div
class="row mt8"
name="option_qty_header"
colspan="2"
>
<div
class="text-center col"
name="total_qty_header"
>
<div
attrs="{'invisible': [('total_product_uom_qty', '=', 0.0)]}"
>
<span>(
<field
name="total_product_qty_done"
class="oe_inline"
readonly="1"
/>
/<field
name="total_product_uom_qty"
class="oe_inline"
readonly="1"
/>)
<field
name="product_uom_id"
class="oe_inline"
options="{'no_open': True}"
readonly="1"
widget="selection"
/>
</span>
</div>
</div>
</div>
<div
class="col text-center"
name="total_qty_field"
colspan="2"
>
<field
name="product_qty"
force_save="1"
nolabel="1"
widget="numeric_step"
options="{'auto_select': True}"
/>
</div>
</group>
</group>
</div>
<div
class="oe_stock_barcodes_bottombar d-flex"
attrs="{'invisible': ['|',('display_menu', '=', True), ('enable_add_product', '=', False)]}"
>
<field name="display_menu" invisible="1" />
<field name="enable_add_product" invisible="1" />
<field
name="manual_entry"
widget="barcode_boolean_toggle"
class="d-none"
/>
<div
class="btn-group dropup"
attrs="{'invisible': [('display_menu', '=', True)]}"
/>
<!-- HACK: To avoid inheritance crash -->
<button name="action_manual_entry" invisible="1" />
<!-- Hide button in view to allow do onclick in JS. Use d-none instead of
invisible attribute to be allocated by jquery selector
-->
<button
name="dummy_on_barcode_scanned"
id="dummy_on_barcode_scanned"
type="object"
data-hotkey="99"
invisible="0"
class="d-none"
/>
<button
name="action_add_scan_manual"
type="object"
title="ADD PRODUCT"
icon="fa-plus fa-2x"
class="btn-secondary w-100 oe_kanban_action_button btn-sm text-uppercase
d-flex justify-content-center align-items-center"
attrs="{'invisible': ['|',('display_menu', '=', True), ('enable_add_product', '=', False)]}"
style="width: 50px"
data-hotkey="8"
>
<span class="fs-1 d-none d-lg-block">Product</span>
</button>
<!-- // -->
<button
name="action_clean_values"
type="object"
icon="fa-trash-o fa-2x"
title="Clean Values"
class="btn-warning w-100 oe_kanban_action_button btn-sm ps-3 pe-3 mx-1
text-uppercase d-flex justify-content-center align-items-center"
attrs="{'invisible': ['|',('display_menu', '=', True), ('manual_entry', '=', False)]}"
data-hotkey="7"
>
<span class="fs-1 d-none d-lg-block">Clean Values</span>
</button>
<button
name="action_confirm"
type="object"
icon="fa-check fa-2x"
title="Confirm"
class="btn-success w-100 oe_kanban_action_button btn-sm ps-3 pe-3 text-uppercase d-flex justify-content-center align-items-center"
attrs="{'invisible': ['|', '|','&amp;', ('is_manual_confirm', '=', False), ('manual_entry', '=', False), ('display_menu', '=', True), ('visible_force_done', '=', True)]}"
data-hotkey="8"
>
<span class="fs-1 d-none d-lg-block">Confirm</span>
</button>
<button
name="action_force_done"
type="object"
icon="fa-check fa-2x"
title="Force done"
attrs="{'invisible': [('visible_force_done', '=', False)]}"
class="btn-danger w-100 oe_kanban_action_button btn-sm ps-3 pe-3 text-uppercase d-flex justify-content-center align-items-center"
style="width: 50px"
data-hotkey="8"
>
<span class="fs-1 d-none d-lg-block">Confirm</span>
</button>
</div>
</div>
</form>
</field>
</record>
<record id="view_stock_barcodes_read_packaging_form" model="ir.ui.view">
<field name="name">stock.barcodes.read.packaging.form</field>
<field name="model">wiz.stock.barcodes.read</field>
<field name="inherit_id" ref="stock_barcodes.view_stock_barcodes_read_form" />
<field name="arch" type="xml">
<group name="option_qty" position="before">
<group name="packaging" col="1">
<div colspan="2" groups="product.group_stock_packaging">
<field name="product_packaging_ids" invisible="1" />
<span
class="fa fa-archive d-sm-none oe_span_small_icon"
title="Source Location"
attrs="{'invisible': [('product_packaging_ids', '=', [])]}"
/>
<field
name="packaging_id"
options="{'no_open': True, 'no_create': True}"
domain="[('product_id', '=', product_id)]"
force_save="1"
style="width: 85%"
class="h5"
placeholder="Packaging"
nolabel="1"
attrs="{'invisible': [('product_packaging_ids', '=', [])]}"
/>
</div>
</group>
</group>
<xpath
expr="//div[@name='total_qty_field']//field[@name='product_qty']"
position="before"
>
<field
name="packaging_qty"
force_save="1"
widget="numeric_step"
options="{'auto_select': True}"
groups="product.group_stock_packaging"
attrs="{'invisible': [('product_packaging_ids', '=', [])]}"
/>
</xpath>
<!-- <div name="total_qty_field" position="before">
<div class="col text-center" groups="product.group_stock_packaging">
</div>
</div> -->
</field>
</record>
<!--
Open wizard in current target option to avoid that the wizard is
closed after any button click,
-->
<record id="action_stock_barcodes_read" model="ir.actions.act_window">
<field name="res_model">wiz.stock.barcodes.read</field>
<field name="name">Barcodes Read</field>
<field name="view_mode">form</field>
<field name="context">{}</field>
<field name="view_id" ref="view_stock_barcodes_read_form" />
<field name="target">fullscreen</field>
</record>
<record id="view_stock_barcodes_read_form_manual_qty" model="ir.ui.view">
<field name="name">stock.barcodes.read.form.manual_qty</field>
<field name="model">wiz.stock.barcodes.read</field>
<field name="priority" eval="999" />
<field name="arch" type="xml">
<form string="Barcodes manual quantities">
<sheet>
<field name="packaging_id" invisible="1" />
<field
name="packaging_qty"
attrs="{'invisible': [('packaging_id', '=', False)]}"
force_save="1"
nolabel="1"
widget="numeric_step"
options="{'auto_select': True}"
/>
<field
name="product_qty"
force_save="1"
nolabel="1"
widget="numeric_step"
options="{'auto_select': True}"
/>
<button
name="action_reopen_wizard"
type="object"
icon="fa-check"
title="Reopen"
class="btn-success"
/>
</sheet>
</form>
</field>
</record>
</odoo>

View file

@ -0,0 +1,60 @@
# Copyright 2019 Sergio Teruel <sergio.teruel@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from odoo import fields, models
class WizStockBarcodesNewLot(models.TransientModel):
_inherit = "barcodes.barcode_events_mixin"
_name = "wiz.stock.barcodes.new.lot"
_description = "Wizard to create new lot from barcode scanner"
product_id = fields.Many2one(comodel_name="product.product", required=True)
lot_name = fields.Char(string="Lot name")
def on_barcode_scanned(self, barcode):
product = self.env["product.product"].search([("barcode", "=", barcode)])[:1]
if product and not self.product_id:
self.product_id = product
return
self.lot_name = barcode
def _prepare_lot_values(self):
return {
"product_id": self.product_id.id,
"name": self.lot_name,
"company_id": self.env.company.id,
}
def get_scan_wizard(self):
return self.env[self.env.context["active_model"]].browse(
self.env.context["active_id"]
)
def scan_wizard_action(self):
if self.env.context.get("active_model") == "wiz.stock.barcodes.read.inventory":
action = self.env["ir.actions.actions"]._for_xml_id(
"stock_barcodes.action_stock_barcodes_read_inventory"
)
else:
action = self.env["ir.actions.actions"]._for_xml_id(
"stock_barcodes.action_stock_barcodes_read_picking"
)
wiz_id = self.get_scan_wizard()
action["res_id"] = wiz_id.id
return action
def confirm(self):
ProductionLot = self.env["stock.lot"]
lot = ProductionLot.search(
[("product_id", "=", self.product_id.id), ("name", "=", self.lot_name)]
)
if not lot:
lot = self.env["stock.lot"].create(self._prepare_lot_values())
# Assign lot created or found to wizard scanning barcode lot_id field
wiz = self.get_scan_wizard()
if wiz:
wiz.lot_id = lot
return self.scan_wizard_action()
def cancel(self):
return self.scan_wizard_action()

View file

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="view_stock_barcodes_new_lot" model="ir.ui.view">
<field name="name">stock.barcodes.new.lot.form</field>
<field name="model">wiz.stock.barcodes.new.lot</field>
<field name="arch" type="xml">
<form>
<sheet>
<div class="alert alert-info text-center" role="alert">
<p>1 - Scan or input product barcode</p>
<p>2 - Scan or input product lot barcode</p>
</div>
<group>
<field
name="_barcode_scanned"
widget="barcode_handler"
invisible="0"
/>
<field name="product_id" />
<field name="lot_name" />
</group>
<div>
<button
string="Confirm"
name="confirm"
type="object"
class="btn-primary oe_kanban_action_button"
/>
<button
string="Cancel"
name="cancel"
class="btn-default oe_kanban_action_button"
type="object"
/>
</div>
</sheet>
</form>
</field>
</record>
<record id="action_stock_barcodes_new_lot" model="ir.actions.act_window">
<field name="res_model">wiz.stock.barcodes.new.lot</field>
<field name="name">New Lot</field>
<field name="view_mode">form</field>
<field name="context">{}</field>
<field name="view_id" ref="view_stock_barcodes_new_lot" />
<field name="target">inline</field>
</record>
</odoo>