Initial commit: OCA Warehouse packages (12 packages)

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

View file

@ -0,0 +1,8 @@
from . import stock_barcodes_action
from . import stock_barcodes_option
from . import stock_move
from . import stock_move_line
from . import stock_picking
from . import stock_picking_type
from . import stock_quant
from . import barcode_events_mixin

View file

@ -0,0 +1,11 @@
# Copyright 2019 Sergio Teruel <sergio.teruel@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from odoo import models
class BarcodesEventsMixin(models.AbstractModel):
_inherit = "barcodes.barcode_events_mixin"
def send_bus_done(self, channel, type_channel, data=None):
self.env["bus.bus"]._sendone(channel, type_channel, data or {})

View file

@ -0,0 +1,189 @@
# Copyright 2019 Sergio Teruel <sergio.teruel@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import base64
import re
from io import BytesIO
import barcode
from barcode.writer import ImageWriter
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
from odoo.tools.safe_eval import safe_eval
REGEX = {
"context": r"^[^\s].*[^\s]$|^$",
"barcode": "^[a-zA-Z0-9-]+$",
}
FIELDS_NAME = {"barcode_options": "barcode_option_group_id"}
class StockBarcodesAction(models.Model):
_name = "stock.barcodes.action"
_description = "Actions for barcode interface"
_order = "sequence, id"
name = fields.Char(translate=True)
active = fields.Boolean(default=True)
sequence = fields.Integer(default=100)
action_window_id = fields.Many2one(
comodel_name="ir.actions.act_window", string="Action window"
)
context = fields.Char()
key_shortcut = fields.Integer()
key_char_shortcut = fields.Char()
icon_class = fields.Char()
barcode = fields.Char()
barcode_image = fields.Image(
"Barcode image",
readonly=True,
compute="_compute_barcode_image",
attachment=True,
)
count_elements = fields.Integer(default=0, compute="_compute_count_elements")
@api.constrains("barcode")
def _constrains_barcode(self):
for action in self:
if not re.match(REGEX.get("barcode", False), action.barcode):
raise ValidationError(
_(
" The barcode {} is not correct."
"Use numbers, letters and dashes, without spaces."
"E.g. 15753, BC-5789,er-56 "
""
).format(action.barcode)
)
all_barcode = [bar for bar in action.mapped("barcode") if bar]
domain = [("barcode", "in", all_barcode)]
matched_actions = self.sudo().search(domain, order="id")
if len(matched_actions) > len(all_barcode):
raise ValidationError(
_(
""" Barcode has already been assigned to the action(s): {}."""
).format(", ".join(matched_actions.mapped("name")))
)
def _generate_barcode(self):
barcode_type = barcode.get_barcode_class("code128")
buffer = BytesIO()
barcode_instance = barcode_type(self.barcode, writer=ImageWriter())
barcode_instance.write(buffer)
buffer.seek(0)
image_base64 = base64.b64encode(buffer.getvalue())
return image_base64
@api.depends("barcode")
def _compute_barcode_image(self):
for action in self:
if action.barcode:
action.barcode_image = action._generate_barcode()
else:
action.barcode_image = False
@api.constrains("context")
def _constrains_context(self):
if self.context and not bool(
re.match(REGEX.get("context", False), self.context)
):
raise ValidationError(_("There can be no spaces at the beginning or end."))
def _count_elements(self):
domain = []
if self.context:
context_values = self.context.strip("{}").split(",")
def _map_context_values(x):
field_values = x.split(":")
field_name = field_values[0].split("search_default_")
if len(field_name) > 1:
field_name = field_name[1].strip("'")
field_value_format = field_values[1].replace("'", "").strip()
field_value = (
int(field_value_format)
if field_value_format.isdigit()
else field_value_format
)
if hasattr(
self.action_window_id.res_model,
FIELDS_NAME.get(field_name, field_name),
):
return (
"{}".format(FIELDS_NAME.get(field_name, field_name)),
"=",
field_value,
)
else:
return False
else:
return ()
domain = [
val_domain
for val_domain in list(
map(lambda x: _map_context_values(x), context_values)
)
]
search_count = (
list(filter(lambda x: x, domain))
if all(val_d is True for val_d in domain)
else []
)
return (
self.env[self.action_window_id.res_model].search_count(search_count)
if self.action_window_id.res_model
else 0
)
return 0
@api.depends("context")
def _compute_count_elements(self):
for barcode_action in self:
barcode_action.count_elements = (
barcode_action._count_elements()
if "search_default_" in barcode_action.context
else 0
)
def open_action(self):
action = self.action_window_id.sudo().read()[0]
action_context = safe_eval(action["context"])
ctx = self.env.context.copy()
if action_context:
ctx.update(action_context)
if self.context:
ctx.update(safe_eval(self.context))
if action_context.get("inventory_mode", False):
action = self.open_inventory_action(ctx)
else:
action["context"] = ctx
return action
def open_inventory_action(self, ctx):
option_group = self.env.ref(
"stock_barcodes.stock_barcodes_option_group_inventory"
)
vals = {
"option_group_id": option_group.id,
"manual_entry": option_group.manual_entry,
"display_read_quant": option_group.display_read_quant,
}
if option_group.get_option_value("location_id", "filled_default"):
vals["location_id"] = (
self.env["stock.warehouse"].search([], limit=1).lot_stock_id.id
)
wiz = self.env["wiz.stock.barcodes.read.inventory"].create(vals)
action = self.env["ir.actions.actions"]._for_xml_id(
"stock_barcodes.action_stock_barcodes_read_inventory"
)
action["res_id"] = wiz.id
action["context"] = ctx
return action
def print_barcodes(self):
report_action = self.env.ref(
"stock_barcodes.action_report_barcode_actions"
).report_action(None, data={})
return report_action

View file

@ -0,0 +1,127 @@
# 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 StockBarcodesOptionGroup(models.Model):
_name = "stock.barcodes.option.group"
_description = "Options group for barcode interface"
name = fields.Char()
code = fields.Char()
option_ids = fields.One2many(
comodel_name="stock.barcodes.option", inverse_name="option_group_id", copy=True
)
barcode_guided_mode = fields.Selection(
[("guided", "Guided")],
string="Mode",
help="When guided mode is selected, information will appear with the "
"movement to be processed",
)
manual_entry = fields.Boolean(
string="Manual entry",
help="Default value when open scan interface",
)
manual_entry_field_focus = fields.Char(
help="Set field to set focus when manual entry mode is enabled",
default="location_id",
)
confirmed_moves = fields.Boolean(
string="Confirmed moves",
help="It allows to work with movements without reservation "
"(Without detailed operations)",
)
show_pending_moves = fields.Boolean(
string="Show pending moves", help="Shows a list of movements to process"
)
source_pending_moves = fields.Selection(
[("move_line_ids", "Detailed operations"), ("move_ids", "Operations")],
default="move_line_ids",
help="Origin of the data to generate the movements to process",
)
ignore_filled_fields = fields.Boolean(
string="Ignore filled fields",
)
auto_put_in_pack = fields.Boolean(
string="Auto put in pack", help="Auto put in pack before picking validation"
)
is_manual_qty = fields.Boolean(
help="If it is checked, it always shows the product quantity field in edit mode"
)
is_manual_confirm = fields.Boolean(
help="If it is marked, the movement must always be confirmed from a button"
)
allow_negative_quant = fields.Boolean(
help="If it is checked, it will allow the creation of movements that "
"generate negative stock"
)
fill_fields_from_lot = fields.Boolean(
help="If checked, the fields in the interface will be filled from "
"the scanned lot"
)
ignore_quant_location = fields.Boolean(
help="If it is checked, quant location will be ignored when reading lot/package",
)
group_key_for_todo_records = fields.Char(
help="You can establish a list of fields that will act as a grouping "
"key to generate the movements to be process.\n"
"The object variable is used to refer to the source record\n"
"For example, object.location_id,object.product_id,object.lot_id"
)
auto_lot = fields.Boolean(
string="Get lots automatically",
help="If checked the lot will be set automatically with the same "
"removal startegy",
)
create_lot = fields.Boolean(
string="Create lots if not match",
help="If checked the lot will created automatically with the scanned barcode "
"if not exists ",
)
show_detailed_operations = fields.Boolean(
help="If checked the picking detailed operations are displayed",
)
keep_screen_values = fields.Boolean(
help="If checked the wizard values are kept until the pending move is completed",
)
accumulate_read_quantity = fields.Boolean(
help="If checked quantity will be accumulated to the existing record instead of "
"overwrite it with the new quantity value",
)
display_notification = fields.Boolean(
string="Display Odoo notifications",
)
use_location_dest_putaway = fields.Boolean(
string="Use location dest. putaway",
)
location_field_to_sort = fields.Selection(
selection=[
("location_id", "Origin Location"),
("location_dest_id", "Destination Location"),
]
)
display_read_quant = fields.Boolean(string="Read items on inventory mode")
def get_option_value(self, field_name, attribute):
option = self.option_ids.filtered(lambda op: op.field_name == field_name)[:1]
return option[attribute]
class StockBarcodesOption(models.Model):
_name = "stock.barcodes.option"
_description = "Options for barcode interface"
_order = "step, sequence, id"
sequence = fields.Integer(default=100)
name = fields.Char()
option_group_id = fields.Many2one(
comodel_name="stock.barcodes.option.group", ondelete="cascade"
)
field_name = fields.Char()
filled_default = fields.Boolean()
forced = fields.Boolean()
to_scan = fields.Boolean()
required = fields.Boolean()
clean_after_done = fields.Boolean()
message = fields.Char()
step = fields.Integer()

View file

@ -0,0 +1,37 @@
# Copyright 2024 Tecnativa - Sergio Teruel
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo import fields, models
class StockMove(models.Model):
_inherit = "stock.move"
barcode_backorder_action = fields.Selection(
[
("pending", "Pending"),
("create_backorder", "Create Backorder"),
("skip_backorder", "No Backorder"),
],
string="Backorder action",
default="pending",
)
def _action_done(self, cancel_backorder=False):
moves_cancel_backorder = self.browse()
if not cancel_backorder:
moves_cancel_backorder = self.filtered(
lambda sm: sm.barcode_backorder_action == "skip_backorder"
)
super(StockMove, moves_cancel_backorder)._action_done(cancel_backorder=True)
moves_backorder = self - moves_cancel_backorder
moves_backorder.barcode_backorder_action = "pending"
return super(StockMove, moves_backorder)._action_done(
cancel_backorder=cancel_backorder
)
def copy_data(self, default=None):
vals_list = super().copy_data(default=default)
for vals in vals_list:
vals.pop("barcode_backorder_action", None)
return vals_list

View file

@ -0,0 +1,40 @@
# 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
class StockMoveLine(models.Model):
_inherit = "stock.move.line"
barcode_scan_state = fields.Selection(
[("pending", "Pending"), ("done", "Done"), ("done_forced", "Done forced")],
string="Scan State",
default="pending",
compute="_compute_barcode_scan_state",
readonly=False,
store=True,
)
@api.depends("qty_done", "reserved_uom_qty")
def _compute_barcode_scan_state(self):
for line in self:
if line.qty_done >= line.reserved_uom_qty:
line.barcode_scan_state = "done"
else:
line.barcode_scan_state = "pending"
def _barcodes_process_line_to_unlink(self):
self.qty_done = 0.0
def action_barcode_detailed_operation_unlink(self):
for sml in self:
stock_move = sml.move_id
stock_move.barcode_backorder_action = "pending"
sml.unlink()
# HACK: To force refresh wizard values
wiz_barcode = self.env["wiz.stock.barcodes.read.picking"].browse(
self.env.context.get("wiz_barcode_id", False)
)
stock_move._action_assign()
wiz_barcode.fill_todo_records()
wiz_barcode.determine_todo_action()

View file

@ -0,0 +1,73 @@
# Copyright 2019 Sergio Teruel <sergio.teruel@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from odoo import models
class StockPicking(models.Model):
_inherit = "stock.picking"
def _prepare_barcode_wiz_vals(self, option_group):
vals = {
"picking_id": self.id,
"res_model_id": self.env.ref("stock.model_stock_picking").id,
"res_id": self.id,
"picking_type_code": self.picking_type_code,
"option_group_id": option_group.id,
"manual_entry": option_group.manual_entry,
"picking_mode": "picking",
}
if self.picking_type_id.code == "outgoing":
vals["location_dest_id"] = self.location_dest_id.id
elif self.picking_type_id.code == "incoming":
vals["location_id"] = self.location_id.id
if option_group.get_option_value("location_id", "filled_default"):
vals["location_id"] = self.location_id.id
if option_group.get_option_value("location_dest_id", "filled_default"):
vals["location_dest_id"] = self.location_dest_id.id
return vals
def action_barcode_scan(self, option_group=False):
option_group = (
option_group
or self.picking_type_id.barcode_option_group_id
or self.env.ref("stock_barcodes.stock_barcodes_option_group_operation")
)
wiz = self.env["wiz.stock.barcodes.read.picking"].create(
self._prepare_barcode_wiz_vals(option_group)
)
wiz.fill_pending_moves()
wiz.determine_todo_action()
action = self.env["ir.actions.actions"]._for_xml_id(
"stock_barcodes.action_stock_barcodes_read_picking"
)
action["res_id"] = wiz.id
return action
def button_validate(self):
put_in_pack_picks = self.filtered(
lambda p: p.picking_type_id.barcode_option_group_id.auto_put_in_pack
and not p.move_line_ids.result_package_id
)
if put_in_pack_picks:
put_in_pack_picks.action_put_in_pack()
if self.env.context.get("stock_barcodes_validate_picking", False):
res = super(
StockPicking, self.with_context(skip_backorder=True)
).button_validate()
else:
pickings_to_backorder = self._check_backorder()
if pickings_to_backorder:
return pickings_to_backorder._action_generate_backorder_wizard(
show_transfers=self._should_show_transfers()
)
res = super().button_validate()
if res is True and self.env.context.get("show_picking_type_action_tree", False):
res = self[:1].picking_type_id.get_action_picking_tree_ready()
if self.state == "done":
self.env["bus.bus"]._sendone(
"stock_barcodes_scan", "actions_barcode", {"valid_picking": True}
)
return res

View file

@ -0,0 +1,104 @@
# Copyright 2019 Sergio Teruel <sergio.teruel@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from ast import literal_eval
from odoo import fields, models
class StockPickingType(models.Model):
_inherit = "stock.picking.type"
barcode_option_group_id = fields.Many2one(
comodel_name="stock.barcodes.option.group"
)
new_picking_barcode_option_group_id = fields.Many2one(
comodel_name="stock.barcodes.option.group",
help="This Barcode Option Group will be selected when clicking the 'New' button"
" in an operation type. It will be used to create a non planned picking.",
)
def action_barcode_scan(self):
vals = {
"res_model_id": self.env.ref("stock.model_stock_picking_type").id,
"res_id": self.id,
"picking_type_code": self.code,
"option_group_id": self.barcode_option_group_id.id,
"manual_entry": self.barcode_option_group_id.manual_entry,
"picking_mode": "picking",
}
if self.code == "outgoing":
vals["location_dest_id"] = (
self.default_location_dest_id.id
or self.env.ref("stock.stock_location_customers").id
)
elif self.code == "incoming":
vals["location_id"] = (
self.default_location_src_id.id
or self.env.ref("stock.stock_location_suppliers").id
)
if self.barcode_option_group_id.get_option_value(
"location_id", "filled_default"
):
vals["location_id"] = self.default_location_src_id.id
if self.barcode_option_group_id.get_option_value(
"location_dest_id", "filled_default"
):
vals["location_dest_id"] = self.default_location_dest_id.id
wiz = self.env["wiz.stock.barcodes.read.picking"].create(vals)
wiz.fill_pending_moves()
wiz.determine_todo_action()
action = self.env["ir.actions.actions"]._for_xml_id(
"stock_barcodes.action_stock_barcodes_read_picking"
)
action["res_id"] = wiz.id
return action
def action_barcode_new_picking(self):
self.ensure_one()
picking = (
self.env["stock.picking"]
.with_context(default_immediate_transfer=True)
.create(
{
"picking_type_id": self.id,
"location_id": self.default_location_src_id.id,
"location_dest_id": self.default_location_dest_id.id,
}
)
)
option_group = self.new_picking_barcode_option_group_id
return picking.action_barcode_scan(option_group=option_group)
def get_action_picking_tree_ready(self):
context = dict(self.env.context)
if context.get("operations_mode", False):
return self._get_action(
"stock_barcodes.stock_barcodes_action_picking_tree_ready"
)
return super().get_action_picking_tree_ready()
def _get_action(self, action_xmlid):
action = self.env["ir.actions.actions"]._for_xml_id(action_xmlid)
if self:
action["display_name"] = self.display_name
default_immediate_tranfer = True
if (
self.env["ir.config_parameter"]
.sudo()
.get_param("stock.no_default_immediate_tranfer")
):
default_immediate_tranfer = False
context = {
"search_default_picking_type_id": [self.id],
"default_picking_type_id": self.id,
"default_immediate_transfer": default_immediate_tranfer,
"default_company_id": self.company_id.id,
}
action_context = literal_eval(action["context"].strip())
context = {**action_context, **context}
action["context"] = context
return action

View file

@ -0,0 +1,80 @@
# Copyright 2023 Tecnativa - Sergio Teruel
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo import models
MODEL_UPDATE_INVENTORY = ["wiz.stock.barcodes.read.inventory"]
class StockQuant(models.Model):
_name = "stock.quant"
_inherit = ["stock.quant", "barcodes.barcode_events_mixin"]
def action_barcode_inventory_quant_unlink(self):
self.with_context(inventory_mode=True).action_set_inventory_quantity_to_zero()
context = dict(self.env.context)
params = context.get("params", {})
res_model = params.get("model", False)
res_id = params.get("id", False)
if res_id and res_model in MODEL_UPDATE_INVENTORY:
wiz_id = self.env[params["model"]].browse(params["id"])
wiz_id._compute_count_inventory_quants()
wiz_id.send_bus_done(
"stock_barcodes_form_update",
"count_apply_inventory",
{"count": wiz_id.count_inventory_quants},
)
def _get_fields_to_edit(self):
return [
"location_id",
"product_id",
"product_uom_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.inventory"].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():
wiz_barcode[fname] = quant[fname]
wiz_barcode.product_qty = quant.inventory_quantity
wiz_barcode.manual_entry = True
self.send_bus_done(
"stock_barcodes_scan",
"stock_barcodes_edit_manual",
{
"manual_entry": True,
},
)
def enable_current_operations(self):
self.send_bus_done(
"stock_barcodes_kanban_update",
"enable_operations",
{
"id": self.id,
},
)
def operation_quantities_rest(self):
self.write({"inventory_quantity": self.inventory_quantity - 1})
self.enable_current_operations()
def operation_quantities(self):
self.write({"inventory_quantity": self.inventory_quantity + 1})
self.enable_current_operations()
def action_apply_inventory(self):
res = super().action_apply_inventory()
self.send_bus_done(
"stock_barcodes_scan",
"actions_barcode",
{"apply_inventory": True},
)
return res