- bnpl_bp.py: APLAZO/Kueski/Clip application workflow (mock) - erp_bp.py: Aspel/CONTPAQi/SAP/Odoo sync jobs (mock) - whatsapp_cloud_bp.py: Meta Cloud API webhook, messages, templates - supplier_portal_bp.py: demand by zone/branch and top-parts analytics - app.py: register all new blueprints
106 lines
3.5 KiB
Python
106 lines
3.5 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
|
|
|
|
|
|
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)
|
|
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()
|
|
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)
|
|
|
|
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()
|
|
|
|
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)
|
|
|
|
|
|
@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)
|
|
from tenant_db import get_tenant_db
|
|
db = get_tenant_db()
|
|
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()
|
|
|
|
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)
|