import re from odoo import api, fields, models, _ from odoo.tools import frozendict from odoo.addons.stock_barcode.models.epc_encoder import EpcScheme class StockMoveLine(models.Model): _inherit = 'stock.move.line' product_barcode = fields.Char(related='product_id.barcode') formatted_product_barcode = fields.Char(compute="_compute_product_barcode") location_processed = fields.Boolean() dummy_id = fields.Char(compute='_compute_dummy_id', inverse='_inverse_dummy_id') parent_location_id = fields.Many2one('stock.location', compute='_compute_parent_location_id') parent_location_dest_id = fields.Many2one('stock.location', compute='_compute_parent_location_id') product_stock_quant_ids = fields.One2many('stock.quant', compute='_compute_product_stock_quant_ids') lot_properties = fields.Properties(related='lot_id.lot_properties', definition='product_id.lot_properties_definition', readonly=True) hide_lot_name = fields.Boolean(compute='_compute_hide_lot_name', default=True) hide_lot = fields.Boolean(compute='_compute_hide_lot_name', default=True) image_1920 = fields.Image(related="product_id.image_1920") product_reference_code = fields.Char(related="product_id.code", string="Product Reference Code") qty_done = fields.Float(compute='_compute_qty_done', inverse='_inverse_qty_done', digits='Product Unit') # Dummy field electronic_product_code = fields.Char(compute='_compute_electronic_product_code') packaging_uom_id = fields.Many2one('uom.uom', related='move_id.packaging_uom_id', string='Packaging Unit of Measure') packaging_uom_qty = fields.Float(related='move_id.packaging_uom_qty', string='Packaging Quantity') outermost_result_package_id = fields.Many2one('stock.package', compute="_compute_outermost_result_package_id", inverse="_inverse_outermost_result_package_id") @api.depends('result_package_id') def _compute_outermost_result_package_id(self): for line in self: outermost_package = line.result_package_id.outermost_package_id line.outermost_result_package_id = outermost_package if outermost_package != line.result_package_id else False @api.depends('tracking', 'picking_type_use_existing_lots', 'picking_type_use_create_lots', 'lot_name') def _compute_hide_lot_name(self): for line in self: if line.tracking not in ('lot', 'serial'): line.hide_lot_name = True line.hide_lot = True continue line.hide_lot_name = not line.picking_type_use_create_lots or (line.picking_type_use_existing_lots and not line.lot_name) line.hide_lot = not line.picking_type_use_existing_lots or (line.picking_type_use_create_lots and line.lot_name) @api.depends('picking_id') def _compute_parent_location_id(self): for line in self: line.parent_location_id = line.picking_id.location_id line.parent_location_dest_id = line.picking_id.location_dest_id @api.depends('product_id', 'parent_location_id') def _compute_product_stock_quant_ids(self): domain = [ ('product_id', 'in', self.product_id.ids), ('company_id', 'in', self.env.companies.ids), ('location_id.usage', '=', 'internal'), ('quantity', '>', '0'), ('location_id', 'child_of', self.parent_location_id.ids), ] quant_ids = dict(self.env['stock.quant']._read_group(domain, groupby=['product_id'], aggregates=['id:recordset'])) for line in self: line.product_stock_quant_ids = quant_ids.get(line.product_id, self.env['stock.quant']) def _compute_dummy_id(self): self.dummy_id = '' def _compute_qty_done(self): for line in self: line.qty_done = line.quantity if line.picked else 0 @api.depends('product_barcode') def _compute_product_barcode(self): for line in self: line.formatted_product_barcode = f'[{line.product_barcode}]' if line.product_barcode else _('No Barcode') def _inverse_dummy_id(self): pass def _inverse_outermost_result_package_id(self): def fetch_before_outermost(package): if package.package_dest_id.package_dest_id: return fetch_before_outermost(package.package_dest_id) return package for line in self: if not line.result_package_id: if line.outermost_result_package_id: # A package was set as outermost while the line itself isn't packed yet -> Pack the line line.action_put_in_pack(package_id=line.outermost_result_package_id.id) continue package = fetch_before_outermost(line.result_package_id) package.package_dest_id = False if line.outermost_result_package_id: # A package was set as outermost while the line itself is already in a pack -> Pack the pack package.action_put_in_pack(package_id=line.outermost_result_package_id.id) def _inverse_qty_done(self): for line in self.with_context({'preserve_state': True}): qty_done = line.qty_done line.quantity = qty_done if qty_done: line.picked = True @api.model_create_multi def create(self, vals_list): model = self for vals in vals_list: # To avoid a write on `quantity` at the creation of the record (in the `qty_done` # inverse, when the line's move is not created yet), we set the `quantity` directly at # the creation and remove `qty_done` in the meantime. if 'qty_done' in vals: vals['quantity'] = vals['qty_done'] vals['picked'] = vals['qty_done'] > 0 del vals['qty_done'] # Also delete the default value in the context. if 'default_qty_done' in model.env.context: model = model.with_env(model.env(context={k: v for k, v in self.env.context.items() if k != 'default_qty_done'})) return super(StockMoveLine, model).create(vals_list) def _get_fields_stock_barcode(self): return [ 'product_id', 'product_category_name', 'location_id', 'location_dest_id', 'move_id', 'qty_done', 'quantity', 'display_name', 'product_uom_id', 'product_barcode', 'owner_id', 'lot_id', 'lot_name', 'package_id', 'result_package_id', 'dummy_id', 'picked', 'move_id', 'outermost_result_package_id', 'packaging_uom_id', 'packaging_uom_qty', 'description_picking', 'is_entire_pack', ] def _compute_electronic_product_code(self): movelines_per_product = self.grouped('product_id') epc_sequence = self.env['ir.sequence'].search([('code', '=', 'stock_barcode.epc.serial')], limit=1) for product_id, move_line_ids in movelines_per_product.items(): if not product_id.barcode: move_line_ids.electronic_product_code = self.env._("Error: We can't generate an Electronic Product Code for a product without a barcode.") continue if not re.match(r"^\d+$", product_id.barcode): # length and check digit do not matter move_line_ids.electronic_product_code = self.env._("Error: The product barcode is not a valid Global Trade Item Number, we can't generate an Electronic Product Code for it.") continue if product_id.tracking == 'none': alphanumeric_tracking = False start_number = int(epc_sequence.next_by_id()) tracking_number_list = [i for i in range(start_number, start_number + len(move_line_ids), 1)] epc_sequence.write({'number_next_actual': start_number + len(move_line_ids)}) else: untracked_move_lines = move_line_ids.filtered(lambda m: not m.lot_id and not m.lot_name) if untracked_move_lines: untracked_move_lines.electronic_product_code = self.env._("Error: We can't generate an Electronic Product Code for a tracked product without a tracking number.") move_line_ids -= untracked_move_lines if not move_line_ids: continue tracking_number_list = [m.lot_id.name if m.lot_id else m.lot_name for m in move_line_ids] alphanumeric_tracking = any(re.search(r'[^\d]', tracking_number) for tracking_number in tracking_number_list) # NOTE: In the future, obtain the filter & company prefix length rather than providing them explicitly gtin = product_id.barcode filter_value = 1 # POS Item company_prefix_length = 7 scheme_name, field_name = ('sgtin-198', 'serial_string') if alphanumeric_tracking else ('sgtin-96', 'serial_integer') scheme = EpcScheme(scheme_name) element_string = f"(01) {gtin} (21) {tracking_number_list[0]}" try: move_line_ids[0].electronic_product_code = scheme.encode(element_string, filter_value, company_prefix_length) except Exception as e: # When an error is explicitely returned as an exception, we can't generate an EPC for the whole product, so we skip other tracking numbers move_line_ids.electronic_product_code = str(e) continue # Since we operate on a single product, we can reuse the base encoding for all the tracking numbers to fasten the process for i in range(1, len(move_line_ids)): tracking_number = tracking_number_list[i] move_line_ids[i].electronic_product_code = scheme.encode_partial(field_name, tracking_number) def _check_destinations(self): res = super()._check_destinations() from_barcode = self.env.context.get('barcode_view') if res and from_barcode: res['context'] = {**res.get('context', {}), 'barcode_view': True} return res