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:
2026-06-09 07:47:42 +00:00
parent 5ea667b80e
commit ea29cc31c0
53 changed files with 7727 additions and 548 deletions

View File

@@ -12,6 +12,7 @@ dashboard_stats_bp = Blueprint('dashboard_stats', __name__, url_prefix='/pos/api
from middleware import require_auth
from tenant_db import get_tenant_conn
class DecimalEncoder(json.JSONEncoder):
@@ -25,83 +26,95 @@ class DecimalEncoder(json.JSONEncoder):
@require_auth()
def get_stats():
"""Summary stats for today and this month."""
from tenant_db import get_tenant_db
db = get_tenant_db()
conn = get_tenant_conn(g.tenant_id)
cur = conn.cursor()
today = datetime.utcnow().date()
month_start = today.replace(day=1)
# Sales today
today_sales = db.execute(
"""SELECT COUNT(*) as count, COALESCE(SUM(total), 0) as total
FROM sales WHERE DATE(created_at) = %s""", (today,)
).fetchone()
try:
# Sales today
cur.execute(
"""SELECT COUNT(*) as count, COALESCE(SUM(total), 0) as total
FROM sales WHERE DATE(created_at) = %s""", (today,)
)
today_sales = cur.fetchone()
# Sales this month
month_sales = db.execute(
"""SELECT COUNT(*) as count, COALESCE(SUM(total), 0) as total
FROM sales WHERE DATE(created_at) >= %s""", (month_start,)
).fetchone()
# Sales this month
cur.execute(
"""SELECT COUNT(*) as count, COALESCE(SUM(total), 0) as total
FROM sales WHERE DATE(created_at) >= %s""", (month_start,)
)
month_sales = cur.fetchone()
# Top 5 products today
top_products = db.execute(
"""SELECT p.name, SUM(si.quantity) as qty, SUM(si.total) 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
WHERE DATE(s.created_at) = %s
GROUP BY p.name
ORDER BY revenue DESC
LIMIT 5""", (today,)
).fetchall()
# Top 5 products today
cur.execute(
"""SELECT si.name, SUM(si.quantity) as qty, SUM(si.subtotal) as revenue
FROM sale_items si
JOIN sales s ON si.sale_id = s.id
WHERE DATE(s.created_at) = %s
GROUP BY si.name
ORDER BY revenue DESC
LIMIT 5""", (today,)
)
top_products = cur.fetchall()
# Hourly sales today (0-23)
hourly = db.execute(
"""SELECT EXTRACT(HOUR FROM created_at)::int as hour,
COUNT(*) as count, COALESCE(SUM(total), 0) as total
FROM sales WHERE DATE(created_at) = %s
GROUP BY hour ORDER BY hour""", (today,)
).fetchall()
hourly_map = {row['hour']: {'count': row['count'], 'total': row['total']} for row in hourly}
# Hourly sales today (0-23)
cur.execute(
"""SELECT EXTRACT(HOUR FROM created_at)::int as hour,
COUNT(*) as count, COALESCE(SUM(total), 0) as total
FROM sales WHERE DATE(created_at) = %s
GROUP BY hour ORDER BY hour""", (today,)
)
hourly = cur.fetchall()
hourly_map = {row[0]: {'count': row[1], 'total': row[2]} for row in hourly}
return jsonify({
'today': {
'sales_count': today_sales['count'],
'sales_total': today_sales['total'],
},
'month': {
'sales_count': month_sales['count'],
'sales_total': month_sales['total'],
},
'top_products': [
{'name': row['name'], 'quantity': row['qty'], 'revenue': row['revenue']}
for row in top_products
],
'hourly_sales': [
{'hour': h, 'count': hourly_map.get(h, {}).get('count', 0),
'total': hourly_map.get(h, {}).get('total', 0)}
for h in range(24)
],
}, cls=DecimalEncoder)
return jsonify({
'today': {
'sales_count': today_sales[0],
'sales_total': float(today_sales[1]) if today_sales[1] is not None else 0,
},
'month': {
'sales_count': month_sales[0],
'sales_total': float(month_sales[1]) if month_sales[1] is not None else 0,
},
'top_products': [
{'name': row[0], 'quantity': row[1], 'revenue': float(row[2]) if row[2] is not None else 0}
for row in top_products
],
'hourly_sales': [
{'hour': h, 'count': hourly_map.get(h, {}).get('count', 0),
'total': float(hourly_map.get(h, {}).get('total', 0))}
for h in range(24)
],
})
finally:
cur.close()
conn.close()
@dashboard_stats_bp.route('/stats/employees', methods=['GET'])
@require_auth()
def get_employee_stats():
"""Sales per employee today."""
from tenant_db import get_tenant_db
db = get_tenant_db()
conn = get_tenant_conn(g.tenant_id)
cur = conn.cursor()
today = datetime.utcnow().date()
rows = db.execute(
"""SELECT e.name, COUNT(s.id_sale) as sales, COALESCE(SUM(s.total), 0) as total
FROM sales s
JOIN employees e ON s.employee_id = e.id_employee
WHERE DATE(s.created_at) = %s
GROUP BY e.name
ORDER BY total DESC""", (today,)
).fetchall()
return jsonify({
'employees': [
{'name': row['name'], 'sales': row['sales'], 'total': row['total']}
for row in rows
]
}, cls=DecimalEncoder)
try:
cur.execute(
"""SELECT e.name, COUNT(s.id) as sales, COALESCE(SUM(s.total), 0) as total
FROM sales s
JOIN employees e ON s.employee_id = e.id
WHERE DATE(s.created_at) = %s
GROUP BY e.name
ORDER BY total DESC""", (today,)
)
rows = cur.fetchall()
return jsonify({
'employees': [
{'name': row[0], 'sales': row[1], 'total': float(row[2]) if row[2] is not None else 0}
for row in rows
]
})
finally:
cur.close()
conn.close()