mirror of
https://github.com/bringout/oca-warehouse.git
synced 2026-04-25 01:21:59 +02:00
Initial commit: OCA Warehouse packages (12 packages)
This commit is contained in:
commit
af1eea7692
627 changed files with 55555 additions and 0 deletions
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
@ -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:  </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>
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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"
|
||||
/>
|
||||
 / 
|
||||
<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>
|
||||
|
|
@ -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,
|
||||
},
|
||||
)
|
||||
|
|
@ -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>
|
||||
|
|
@ -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 -> 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)]}"
|
||||
>->
|
||||
</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),'&', ('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': ['|', '|','&', ('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>
|
||||
|
|
@ -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()
|
||||
|
|
@ -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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue