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 @@ 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()
|
||||
|
||||
Reference in New Issue
Block a user