# -*- coding: utf-8 -*- # Copyright 2026 Nexa Systems Inc. # License OPL-1 (Odoo Proprietary License v1.0) import re import logging from odoo import models, fields, api _logger = logging.getLogger(__name__) SKIP_WORDS = frozenset({ 'total', 'price', 'quantity', 'subtotal', 'discount', 'amount', 'invoice', 'order', 'product', 'delivery', 'shipping', 'tracking', 'payment', 'customer', 'vendor', 'notes', 'description', 'reference', 'number', 'date', 'name', 'address', 'phone', 'email', 'unit', 'piece', 'each', 'item', 'line', 'from', 'with', 'that', 'this', 'have', 'will', 'been', 'were', 'would', 'could', 'should', }) SERIAL_PATTERN = re.compile(r'(?]+>', ' ', text or '') candidates = SERIAL_PATTERN.findall(clean) for candidate in candidates: if len(candidate) < 5: continue key = candidate.upper() if key in seen or candidate.lower() in SKIP_WORDS: continue seen.add(key) if not re.search(r'\d', candidate): continue lots = self.env['stock.lot'].search([ ('name', '=ilike', candidate), ], limit=5) matched_product = lots.filtered( lambda l: l.product_id.id in product_ids) results.append({ 'wizard_id': self.id, 'serial_text': candidate, 'source': source_label, 'found_in_system': bool(lots), 'matched_product': bool(matched_product), 'lot_id': matched_product[:1].id if matched_product else ( lots[:1].id if lots else False), 'product_id': (matched_product[:1].product_id.id if matched_product else (lots[:1].product_id.id if lots else False)), 'lot_product_name': (matched_product[:1].product_id.name if matched_product else (lots[:1].product_id.name if lots else '')), }) if results: self.env['fusion.serial.scan.line'].create(results) found = sum(1 for r in results if r['found_in_system']) matched = sum(1 for r in results if r['matched_product']) self.scan_summary = ( f'Scanned {len(text_sources)} text sources. ' f'Found {len(results)} potential serial numbers: ' f'{found} exist in system, {matched} match products in this transfer.' ) def _collect_text_sources(self, so): """Gather all text from SO lines, notes, and related invoices.""" sources = [] for line in so.order_line: if line.name: sources.append((f'SO Line: {line.product_id.name}', line.name)) if so.note: sources.append(('SO Notes', so.note)) if so.internal_note if hasattr(so, 'internal_note') else False: sources.append(('SO Internal Note', so.internal_note)) for inv in so.invoice_ids.filtered(lambda m: m.state == 'posted'): for line in inv.invoice_line_ids: if line.name: sources.append(( f'Invoice {inv.name}: {line.product_id.name if line.product_id else ""}', line.name)) if inv.narration: sources.append((f'Invoice {inv.name} Notes', inv.narration)) return sources class FusionSerialScanLine(models.TransientModel): _name = 'fusion.serial.scan.line' _description = 'Serial Scan Result Line' wizard_id = fields.Many2one( 'fusion.serial.scan.wizard', ondelete='cascade', required=True) serial_text = fields.Char(string='Serial Number', readonly=True) source = fields.Char(string='Found In', readonly=True) found_in_system = fields.Boolean(string='Exists in System', readonly=True) matched_product = fields.Boolean( string='Matches Transfer Product', readonly=True) lot_id = fields.Many2one('stock.lot', string='Matched Lot', readonly=True) product_id = fields.Many2one( 'product.product', string='Lot Product', readonly=True) lot_product_name = fields.Char(string='Lot Product Name', readonly=True)