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%
This commit is contained in:
@@ -12,6 +12,7 @@ supplier_portal_bp = Blueprint('supplier_portal', __name__, url_prefix='/pos/api
|
||||
|
||||
|
||||
from middleware import require_auth
|
||||
from tenant_db import get_tenant_conn
|
||||
|
||||
|
||||
class DecimalEncoder(json.JSONEncoder):
|
||||
@@ -26,48 +27,47 @@ class DecimalEncoder(json.JSONEncoder):
|
||||
def get_demand():
|
||||
"""Aggregated demand by zone, part group, and time range."""
|
||||
days = request.args.get('days', 30, type=int)
|
||||
group_id = request.args.get('group_id', type=int)
|
||||
branch_id = request.args.get('branch_id', type=int)
|
||||
|
||||
from tenant_db import get_tenant_db
|
||||
db = get_tenant_db()
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
cur = conn.cursor()
|
||||
since = datetime.utcnow() - timedelta(days=days)
|
||||
|
||||
params = [since]
|
||||
filters = "s.created_at >= %s"
|
||||
if group_id:
|
||||
filters += " AND p.group_id = %s"
|
||||
params.append(group_id)
|
||||
if branch_id:
|
||||
filters += " AND s.branch_id = %s"
|
||||
params.append(branch_id)
|
||||
try:
|
||||
params = [since]
|
||||
filters = "s.created_at >= %s"
|
||||
if branch_id:
|
||||
filters += " AND s.branch_id = %s"
|
||||
params.append(branch_id)
|
||||
|
||||
rows = db.execute(
|
||||
f"""SELECT g.name as group_name, b.name as branch_name,
|
||||
COUNT(DISTINCT s.id_sale) as orders,
|
||||
SUM(si.quantity) as qty_requested,
|
||||
COALESCE(SUM(si.total), 0) as revenue
|
||||
FROM sale_items si
|
||||
JOIN sales s ON si.sale_id = s.id_sale
|
||||
JOIN parts p ON si.part_id = p.id_part
|
||||
JOIN part_groups g ON p.group_id = g.id_group
|
||||
LEFT JOIN branches b ON s.branch_id = b.id_branch
|
||||
WHERE {filters}
|
||||
GROUP BY g.name, b.name
|
||||
ORDER BY revenue DESC
|
||||
LIMIT 100""", tuple(params)
|
||||
).fetchall()
|
||||
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': [
|
||||
{'group': row['group_name'], 'branch': row['branch_name'],
|
||||
'orders': row['orders'], 'quantity': row['qty_requested'],
|
||||
'revenue': row['revenue']}
|
||||
for row in rows
|
||||
]
|
||||
}, cls=DecimalEncoder)
|
||||
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'])
|
||||
@@ -75,31 +75,31 @@ def get_demand():
|
||||
def get_top_parts():
|
||||
"""Top moving parts for suppliers to restock."""
|
||||
days = request.args.get('days', 30, type=int)
|
||||
from tenant_db import get_tenant_db
|
||||
db = get_tenant_db()
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
cur = conn.cursor()
|
||||
since = datetime.utcnow() - timedelta(days=days)
|
||||
|
||||
rows = db.execute(
|
||||
"""SELECT p.oem_part_number, p.name, g.name as group_name,
|
||||
SUM(si.quantity) as sold, COALESCE(SUM(si.total), 0) as revenue,
|
||||
COALESCE(SUM(wi.stock_quantity), 0) as current_stock
|
||||
FROM sale_items si
|
||||
JOIN sales s ON si.sale_id = s.id_sale
|
||||
JOIN parts p ON si.part_id = p.id_part
|
||||
JOIN part_groups g ON p.group_id = g.id_group
|
||||
LEFT JOIN warehouse_inventory wi ON p.id_part = wi.part_id
|
||||
WHERE s.created_at >= %s
|
||||
GROUP BY p.oem_part_number, p.name, g.name
|
||||
ORDER BY sold DESC
|
||||
LIMIT 50""", (since,)
|
||||
).fetchall()
|
||||
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': [
|
||||
{'oem': row['oem_part_number'], 'name': row['name'],
|
||||
'group': row['group_name'], 'sold': row['sold'],
|
||||
'revenue': row['revenue'], 'stock': row['current_stock']}
|
||||
for row in rows
|
||||
]
|
||||
}, cls=DecimalEncoder)
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user