diff --git a/BOM/ELP1000037326_Offre Elemaster_2023 12 26.xlsx b/BOM/ELP1000037326_Offre Elemaster_2023 12 26.xlsx new file mode 100644 index 0000000..5bfb613 Binary files /dev/null and b/BOM/ELP1000037326_Offre Elemaster_2023 12 26.xlsx differ diff --git a/nexarxclaude.py b/nexarxclaude.py index 9fbe40f..c211621 100644 --- a/nexarxclaude.py +++ b/nexarxclaude.py @@ -252,9 +252,40 @@ def normalise(s: str) -> str: return re.sub(r"[^a-z0-9]", "", s.lower()) -def _prefix_match(a: str, b: str) -> bool: - """True if normalised strings match or one is a prefix of the other.""" - na, nb = normalise(a), normalise(b) + +_MFR_ALIASES = { + "ti": "texasinstruments", + "texas": "texasinstruments", + "texasinstruments": "texasinstruments", + "onsemi": "onsemi", + "onsemiconductor": "onsemi", + "onsemin": "onsemi", + "nxp": "nxp", + "nxpsemiconductors": "nxp", + "st": "stmicro", + "stmicro": "stmicro", + "stmicroelectronics": "stmicro", + "abracon": "abracon", + "abraconcorporation": "abracon", + "ck": "ck", + "ckcomponents": "ck", + "murata": "murata", + "muratamfg": "murata", + "muratamanufacturing": "murata", + "kingbright": "kingbright", + "kingbight": "kingbright", # known Nexar typo +} + + +def _norm_mfr(s: str) -> str: + """Normalise a manufacturer name, resolving known abbreviations and aliases.""" + n = normalise(s) + return _MFR_ALIASES.get(n, n) + + +def _mfr_match(a: str, b: str) -> bool: + """True if two manufacturer names refer to the same company (alias-aware, prefix-tolerant).""" + na, nb = _norm_mfr(a), _norm_mfr(b) return bool(na and nb and (na == nb or na.startswith(nb) or nb.startswith(na))) @@ -290,6 +321,8 @@ def _elec_match(a: str, b: str) -> bool: return na == nb or nb.startswith(na) or na.startswith(nb) + + def load_bom_data(bom_path: str) -> list | None: """Load BOM xlsx and return a list of entry dicts. Returns None if no 'bom' tab is found.""" @@ -350,27 +383,29 @@ def _find_bom_match(bom_data: list, mfr_name: str, mpn: str) -> dict | None: """Return the highest unit cost BOM entry that prefix-matches both manufacturer and MPN.""" candidates = [ e for e in bom_data - if _prefix_match(e["manufacturer"], mfr_name) and _mpn_match(e["mpn"], mpn) + if _mfr_match(e["manufacturer"], mfr_name) and _mpn_match(e["mpn"], mpn) ] return max(candidates, key=lambda e: e["unit_cost"]) if candidates else None def _extract_r_vals(s: str) -> tuple: """Extract (resistance_norm, footprint) from a description string using word boundaries.""" + s = re.sub(r'(\d),(\d)', r'\1.\2', s) # normalise European decimal comma rm = re.search(r'\b(\d+\.?\d*\s*(?:kohms?|mohms?|k[\s\-]?ohms?|ohms?|k|m|r))\b', s, re.IGNORECASE) resistance = _norm_elec(rm.group(1)) if rm else "" - fm = re.search(r'\b(0\d{3})\b', s) + fm = re.search(r'\b(\d{4})\b', s) footprint = fm.group(1) if fm else "" return resistance, footprint def _extract_c_vals(s: str) -> tuple: """Extract (capacitance_norm, voltage_norm, case) from a description string.""" + s = re.sub(r'(\d),(\d)', r'\1.\2', s) # normalise European decimal comma cm = re.search(r'\b(\d+\.?\d*\s*(?:p|n|u|µ|μ|m)\s*f)\b', s, re.IGNORECASE) capacitance = _norm_elec(cm.group(1)) if cm else "" vm = re.search(r'\b(\d+\.?\d*\s*v)\b', s, re.IGNORECASE) voltage = _norm_elec(vm.group(1)) if vm else "" - fm = re.search(r'\b(0\d{3})\b', s) + fm = re.search(r'\b(\d{4})\b', s) case = fm.group(1) if fm else "" return capacitance, voltage, case @@ -435,13 +470,11 @@ def _find_passive_bom_match(bom_data: list, designator: str, nexar_params: dict, desc = e.get("description", "") if not desc or "cap" not in desc.lower(): continue - bom_c, bom_v, bom_case = _extract_c_vals(desc) + bom_c, _, bom_case = _extract_c_vals(desc) if not bom_c or not _elec_match(bom_c, capacitance): continue if not bom_case or bom_case.lower() != case.lower().strip(): continue - if voltage and bom_v and not _elec_match(bom_v, voltage): - continue candidates.append(e) return max(candidates, key=lambda e: e["unit_cost"]) if candidates else None @@ -528,22 +561,10 @@ def export_to_xlsx(project_name: str, variant_name: str, schematics: list, bom_d f"(BOM: '{mpn_in_bom['manufacturer']}' vs Nexar: '{valid_pairs[0][0]}')" ) else: - # Check if BOM has the manufacturer but with a different MPN - mfr_in_bom = next( - (e for e in bom_data for mfr, _ in valid_pairs if _prefix_match(e["manufacturer"], mfr)), - None + print( + f" NO MATCH [{designator}] {ref}: " + f"Not on Manufacturer's BoM" ) - if mfr_in_bom: - print( - f" NO MATCH [{designator}] {ref}: " - f"manufacturer '{mfr_in_bom['manufacturer']}' found in BOM " - f"but MPN differs (BOM: '{mfr_in_bom['mpn']}' vs Nexar: {tried_mpns})" - ) - else: - print( - f" NO MATCH [{designator}] {ref}: " - f"Nexar MPN(s) {tried_mpns} not found in BOM at all" - ) if match: row += [match["manufacturer"], match["mpn"], match["unit_cost"]]