"""Supplier Catalog Blueprint — parts from suppliers with vehicle compatibility. Independent from inventory. Supports: - Browse by supplier/category - Search by text or vehicle (MYE or make/model/year) - Part detail with compatibilities and interchanges - Bulk import via Excel """ from flask import Blueprint, request, jsonify, g, render_template from psycopg2.extras import RealDictCursor from tenant_db import get_master_conn from middleware import require_auth supplier_catalog_bp = Blueprint('supplier_catalog', __name__, url_prefix='/pos/api/supplier-catalog') # ─── Helpers ─────────────────────────────────────────────────────────────── def _get_master_conn(): return get_master_conn() def _json_response(data, status=200): return jsonify(data), status # ─── Brands ──────────────────────────────────────────────────────────────── @supplier_catalog_bp.route('/brands', methods=['GET']) @require_auth('catalog.view') def list_brands(): """Return distinct makes (vehicle brands) present in the supplier catalog.""" conn = _get_master_conn() cur = conn.cursor() cur.execute(""" SELECT DISTINCT make, COUNT(*) as cnt FROM supplier_catalog_compat WHERE make IS NOT NULL AND make != '' GROUP BY make ORDER BY make ASC """) rows = cur.fetchall() cur.close(); conn.close() return jsonify({'brands': [{'name': r[0], 'count': r[1]} for r in rows]}) # ─── Search ──────────────────────────────────────────────────────────────── @supplier_catalog_bp.route('/search', methods=['GET']) @require_auth('catalog.view') def search_items(): """Search supplier catalog by text and/or vehicle.""" q = (request.args.get('q') or '').strip() mye_id = request.args.get('mye_id', type=int) make = (request.args.get('make') or '').strip() model = (request.args.get('model') or '').strip() year = request.args.get('year', type=int) supplier = (request.args.get('supplier') or '').strip() category = (request.args.get('category') or '').strip() page = max(1, request.args.get('page', 1, type=int)) per_page = min(100, request.args.get('per_page', 30, type=int)) offset = (page - 1) * per_page conn = _get_master_conn() cur = conn.cursor() # Build query dynamically where_parts = ["sc.is_active = true"] params = [] if supplier: where_parts.append("sc.supplier_name = %s") params.append(supplier) if category: where_parts.append("sc.category = %s") params.append(category) # Text search on SKU, name, or interchange part_number if q: where_parts.append(""" (sc.sku ILIKE %s OR sc.name ILIKE %s OR EXISTS ( SELECT 1 FROM supplier_catalog_interchange sci2 WHERE sci2.catalog_id = sc.id AND sci2.part_number ILIKE %s )) """) like_q = f'%{q}%' params.extend([like_q, like_q, like_q]) # Vehicle filter vehicle_join = "" if mye_id: vehicle_join = "JOIN supplier_catalog_compat scc ON scc.catalog_id = sc.id" where_parts.append("scc.model_year_engine_id = %s") params.append(mye_id) elif make or model or year: vehicle_join = "JOIN supplier_catalog_compat scc ON scc.catalog_id = sc.id" if make: where_parts.append("scc.make ILIKE %s") params.append(f'%{make}%') if model: where_parts.append("scc.model ILIKE %s") params.append(f'%{model}%') if year: where_parts.append("scc.year = %s") params.append(year) where_sql = " AND ".join(where_parts) # Count total count_sql = f""" SELECT COUNT(DISTINCT sc.id) FROM supplier_catalog sc {vehicle_join} WHERE {where_sql} """ cur.execute(count_sql, params) total = cur.fetchone()[0] # Fetch page fetch_sql = f""" SELECT DISTINCT sc.id, sc.supplier_name, sc.sku, sc.name, sc.category, sc.description, sc.image_url FROM supplier_catalog sc {vehicle_join} WHERE {where_sql} ORDER BY sc.name ASC LIMIT %s OFFSET %s """ cur.execute(fetch_sql, params + [per_page, offset]) rows = cur.fetchall() items = [] for r in rows: items.append({ 'id': r[0], 'supplier_name': r[1], 'sku': r[2], 'name': r[3], 'category': r[4], 'description': r[5], 'image_url': r[6], }) cur.close(); conn.close() return jsonify({ 'data': items, 'pagination': { 'page': page, 'per_page': per_page, 'total': total, 'total_pages': (total + per_page - 1) // per_page, } }) # ─── Item Detail ─────────────────────────────────────────────────────────── @supplier_catalog_bp.route('/items/', methods=['GET']) @require_auth('catalog.view') def get_item_detail(item_id): """Return full detail for a supplier catalog item including compat + interchanges.""" conn = _get_master_conn() cur = conn.cursor() cur.execute(""" SELECT id, supplier_name, sku, name, category, description, image_url, created_at FROM supplier_catalog WHERE id = %s AND is_active = true """, (item_id,)) row = cur.fetchone() if not row: cur.close(); conn.close() return jsonify({'error': 'Item not found'}), 404 item = { 'id': row[0], 'supplier_name': row[1], 'sku': row[2], 'name': row[3], 'category': row[4], 'description': row[5], 'image_url': row[6], 'created_at': str(row[7]) if row[7] else None, } # Compatibilities — deduplicate by (make, model, year, engine) because # the same vehicle may map to multiple MYE ids (especially when engine # text is empty from the supplier catalog). cur.execute(""" SELECT make, model, year, engine, model_year_engine_id, source FROM supplier_catalog_compat WHERE catalog_id = %s ORDER BY make, model, year, engine """, (item_id,)) seen_compat = set() compatibilities = [] for r in cur.fetchall(): key = (r[0], r[1], r[2], r[3]) if key in seen_compat: continue seen_compat.add(key) compatibilities.append({ 'make': r[0], 'model': r[1], 'year': r[2], 'engine': r[3], 'model_year_engine_id': r[4], 'source': r[5] }) item['compatibilities'] = compatibilities # Interchanges cur.execute(""" SELECT brand, part_number FROM supplier_catalog_interchange WHERE catalog_id = %s ORDER BY brand, part_number """, (item_id,)) item['interchanges'] = [ {'brand': r[0], 'part_number': r[1]} for r in cur.fetchall() ] cur.close(); conn.close() return jsonify(item) # ─── Categories ──────────────────────────────────────────────────────────── @supplier_catalog_bp.route('/categories', methods=['GET']) @require_auth('catalog.view') def list_categories(): """Return distinct categories with counts.""" conn = _get_master_conn() cur = conn.cursor() cur.execute(""" SELECT category, COUNT(*) as cnt FROM supplier_catalog WHERE is_active = true GROUP BY category ORDER BY cnt DESC """) rows = cur.fetchall() cur.close(); conn.close() return jsonify({'categories': [{'name': r[0], 'count': r[1]} for r in rows]}) # ─── Suppliers ───────────────────────────────────────────────────────────── @supplier_catalog_bp.route('/suppliers', methods=['GET']) @require_auth('catalog.view') def list_suppliers(): """Return distinct suppliers with counts.""" conn = _get_master_conn() cur = conn.cursor() cur.execute(""" SELECT supplier_name, COUNT(*) as cnt FROM supplier_catalog WHERE is_active = true GROUP BY supplier_name ORDER BY supplier_name ASC """) rows = cur.fetchall() cur.close(); conn.close() return jsonify({'suppliers': [{'name': r[0], 'count': r[1]} for r in rows]}) # ─── Delete ──────────────────────────────────────────────────────────────── @supplier_catalog_bp.route('/items/', methods=['DELETE']) @require_auth('inventory.edit') def delete_item(item_id): """Soft-delete a supplier catalog item.""" conn = _get_master_conn() cur = conn.cursor() cur.execute("UPDATE supplier_catalog SET is_active = false WHERE id = %s", (item_id,)) conn.commit() cur.close(); conn.close() return jsonify({'success': True})