Files
Autoparts-DB/pos/blueprints/supplier_portal_bp.py
consultoria-as ea29cc31c0 feat(catalog): supplier catalog cleanup, fuzzy matching, and navigation fixes
- Cleaned 137+ fake engine-displacement models from supplier imports
  (v3/v4 scripts: Chevrolet, Ford, Chrysler, Dodge, Jeep, Nissan, etc.)
- Removed 1,251+ corrupted models (INT. prefixes, year-suffix, torque specs,
  empty names, trailing-year variants)
- Migrated supplier tables to master DB (supplier_catalog,
  supplier_catalog_compat, supplier_catalog_interchange)
- Fixed _get_mye_ids_with_parts() to query supplier_catalog_compat from
  master DB so supplier-only vehicles appear for all tenants
- Added fuzzy model matcher with parenthesis stripping, noise suffix removal,
  compact matching, prefix/substring fallback, model aliases, and ±3 year
  proximity
- Matched compat rows: KEEP GREEN +14,152, KNADIAN +3,021, VAZLO +127,500,
  LUK +477, RAYBESTOS +1,743
- Added KNADIAN catalog importer with year-range expansion and future-year
  filtering
- Added VAZLO catalog importer with position parsing and SKU-in-model cleanup
- Added Keep Green, LUK, Yokomitsu, Raybestos catalog importers
- Cache clearing after cleanups (_classify_cache_*, nexus:mye_ids:*,
  nexus:brand_mye_counts:*)

Final match rates:
- KEEP GREEN: 90.3%
- VAZLO: 93.6%
- YOKOMITSU: 100.0%
- KNADIAN: 57.4%
- LUK: 51.0%
- RAYBESTOS: 55.9%
2026-06-09 07:47:42 +00:00

106 lines
3.2 KiB
Python

"""Supplier Portal Blueprint — Demand insights for vendors.
Allows suppliers to view demand by zone, part type, and branch.
"""
from flask import Blueprint, request, jsonify, g
from functools import wraps
from datetime import datetime, timedelta
from decimal import Decimal
import json
supplier_portal_bp = Blueprint('supplier_portal', __name__, url_prefix='/pos/api/supplier-portal')
from middleware import require_auth
from tenant_db import get_tenant_conn
class DecimalEncoder(json.JSONEncoder):
def default(self, o):
if isinstance(o, Decimal):
return float(o)
return super().default(o)
@supplier_portal_bp.route('/demand', methods=['GET'])
@require_auth()
def get_demand():
"""Aggregated demand by zone, part group, and time range."""
days = request.args.get('days', 30, type=int)
branch_id = request.args.get('branch_id', type=int)
conn = get_tenant_conn(g.tenant_id)
cur = conn.cursor()
since = datetime.utcnow() - timedelta(days=days)
try:
params = [since]
filters = "s.created_at >= %s"
if branch_id:
filters += " AND s.branch_id = %s"
params.append(branch_id)
cur.execute(
f"""SELECT b.name as branch_name,
COUNT(DISTINCT s.id) as orders,
SUM(si.quantity) as qty_requested,
COALESCE(SUM(si.subtotal), 0) as revenue
FROM sale_items si
JOIN sales s ON si.sale_id = s.id
LEFT JOIN branches b ON s.branch_id = b.id
WHERE {filters}
GROUP BY b.name
ORDER BY revenue DESC
LIMIT 100""", tuple(params)
)
rows = cur.fetchall()
return jsonify({
'since': since.isoformat(),
'days': days,
'demand': [
{'branch': row[0] or 'Sin sucursal',
'orders': row[1], 'quantity': row[2],
'revenue': float(row[3]) if row[3] is not None else 0}
for row in rows
]
})
finally:
cur.close()
conn.close()
@supplier_portal_bp.route('/top-parts', methods=['GET'])
@require_auth()
def get_top_parts():
"""Top moving parts for suppliers to restock."""
days = request.args.get('days', 30, type=int)
conn = get_tenant_conn(g.tenant_id)
cur = conn.cursor()
since = datetime.utcnow() - timedelta(days=days)
try:
cur.execute(
"""SELECT si.part_number, si.name,
SUM(si.quantity) as sold, COALESCE(SUM(si.subtotal), 0) as revenue
FROM sale_items si
JOIN sales s ON si.sale_id = s.id
WHERE s.created_at >= %s
GROUP BY si.part_number, si.name
ORDER BY sold DESC
LIMIT 50""", (since,)
)
rows = cur.fetchall()
return jsonify({
'since': since.isoformat(),
'parts': [
{'part_number': row[0], 'name': row[1],
'sold': row[2], 'revenue': float(row[3]) if row[3] is not None else 0}
for row in rows
]
})
finally:
cur.close()
conn.close()