- /pos/login redirige a /pos/login2 (no-store). - /pos/login2 sirve la plantilla actualizada que envia employee_id. - nginx redirige / a /pos/login2. Esto evita que navegadores/PWA con cache antigua sigan usando la version que no enviaba el empleado seleccionado.
331 lines
11 KiB
Python
331 lines
11 KiB
Python
from flask import Flask, request, redirect, make_response
|
|
from json_provider import OrjsonProvider
|
|
|
|
|
|
def create_app():
|
|
app = Flask(__name__)
|
|
app.json = OrjsonProvider(app)
|
|
|
|
# Tenant subdomain resolver (before every request)
|
|
from middleware_tenant import resolve_tenant
|
|
app.before_request(resolve_tenant)
|
|
|
|
# ─── Server-side guard: workshop/mechanic users only see /pos/workshop ──────
|
|
@app.before_request
|
|
def restrict_workshop_users():
|
|
path = request.path
|
|
if not path.startswith('/pos/'):
|
|
return
|
|
if path == '/pos/workshop' or path.startswith('/pos/static/') or path.startswith('/pos/api/') or path == '/pos/sw.js' or path == '/pos/login' or path.startswith('/pos/login'):
|
|
return
|
|
role = request.cookies.get('pos_role', '').lower()
|
|
if role in ('workshop', 'mechanic'):
|
|
return redirect('/pos/workshop')
|
|
|
|
# ─── PWA: Service Worker must be served from /pos/ scope ──────
|
|
@app.route('/pos/sw.js')
|
|
def pos_sw():
|
|
from flask import send_from_directory
|
|
return send_from_directory('static/pwa', 'sw.js',
|
|
mimetype='application/javascript')
|
|
|
|
# Register blueprints
|
|
from blueprints.auth_bp import auth_bp
|
|
app.register_blueprint(auth_bp)
|
|
|
|
from blueprints.config_bp import config_bp
|
|
app.register_blueprint(config_bp)
|
|
|
|
from blueprints.inventory_bp import inventory_bp
|
|
app.register_blueprint(inventory_bp)
|
|
|
|
from blueprints.catalog_bp import catalog_bp
|
|
app.register_blueprint(catalog_bp)
|
|
|
|
from blueprints.pos_bp import pos_bp
|
|
app.register_blueprint(pos_bp)
|
|
|
|
from blueprints.public_bp import public_bp
|
|
app.register_blueprint(public_bp)
|
|
|
|
from blueprints.customers_bp import customers_bp
|
|
app.register_blueprint(customers_bp)
|
|
|
|
from blueprints.cashregister_bp import cashregister_bp
|
|
app.register_blueprint(cashregister_bp)
|
|
|
|
from blueprints.invoicing_bp import invoicing_bp
|
|
app.register_blueprint(invoicing_bp)
|
|
|
|
from blueprints.accounting_bp import accounting_bp
|
|
app.register_blueprint(accounting_bp)
|
|
|
|
from blueprints.chat_bp import chat_bp
|
|
app.register_blueprint(chat_bp)
|
|
|
|
from blueprints.fleet_bp import fleet_bp
|
|
app.register_blueprint(fleet_bp)
|
|
|
|
from blueprints.whatsapp_bp import whatsapp_bp
|
|
app.register_blueprint(whatsapp_bp)
|
|
|
|
from blueprints.marketplace_bp import marketplace_bp
|
|
app.register_blueprint(marketplace_bp)
|
|
|
|
from blueprints.marketplace_external_bp import marketplace_ext_bp
|
|
app.register_blueprint(marketplace_ext_bp)
|
|
|
|
from blueprints.dropshipping_bp import dropship_bp
|
|
app.register_blueprint(dropship_bp)
|
|
|
|
from blueprints.peer_bp import peer_bp
|
|
app.register_blueprint(peer_bp)
|
|
|
|
from blueprints.supplier_bp import supplier_bp
|
|
app.register_blueprint(supplier_bp)
|
|
|
|
from blueprints.warranty_bp import warranty_bp
|
|
app.register_blueprint(warranty_bp)
|
|
|
|
from blueprints.crm_bp import crm_bp
|
|
app.register_blueprint(crm_bp)
|
|
|
|
from blueprints.service_order_bp import service_order_bp
|
|
app.register_blueprint(service_order_bp)
|
|
|
|
from blueprints.image_bp import image_bp
|
|
app.register_blueprint(image_bp)
|
|
|
|
from blueprints.notification_bp import notification_bp
|
|
app.register_blueprint(notification_bp)
|
|
|
|
from blueprints.savings_bp import savings_bp
|
|
app.register_blueprint(savings_bp)
|
|
|
|
from blueprints.logistics_bp import logistics_bp
|
|
app.register_blueprint(logistics_bp)
|
|
|
|
from blueprints.public_api_bp import public_api_bp
|
|
app.register_blueprint(public_api_bp)
|
|
|
|
from blueprints.tasks_bp import tasks_bp
|
|
app.register_blueprint(tasks_bp)
|
|
|
|
from blueprints.bnpl_bp import bnpl_bp
|
|
app.register_blueprint(bnpl_bp)
|
|
|
|
from blueprints.erp_bp import erp_bp
|
|
app.register_blueprint(erp_bp)
|
|
|
|
from blueprints.whatsapp_cloud_bp import whatsapp_cloud_bp
|
|
app.register_blueprint(whatsapp_cloud_bp)
|
|
|
|
from blueprints.dashboard_stats_bp import dashboard_stats_bp
|
|
app.register_blueprint(dashboard_stats_bp)
|
|
|
|
from blueprints.supplier_portal_bp import supplier_portal_bp
|
|
app.register_blueprint(supplier_portal_bp)
|
|
|
|
from blueprints.supplier_catalog_bp import supplier_catalog_bp
|
|
app.register_blueprint(supplier_catalog_bp)
|
|
|
|
from blueprints.internal_bp import internal_bp
|
|
app.register_blueprint(internal_bp)
|
|
|
|
# Health check
|
|
@app.route('/pos/health')
|
|
def health():
|
|
return {'status': 'ok'}
|
|
|
|
from flask import g, jsonify, render_template, send_from_directory
|
|
|
|
@app.route('/favicon.ico')
|
|
def favicon():
|
|
return send_from_directory('static/pwa', 'icon-192.png', mimetype='image/png')
|
|
|
|
@app.route('/pos/login')
|
|
def pos_login_legacy():
|
|
# Redirect to the new login path to bypass any stale browser/SW cache
|
|
# of the old /pos/login page that did not send employee_id.
|
|
response = make_response(redirect('/pos/login2'))
|
|
response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0'
|
|
return response
|
|
|
|
@app.route('/pos/login2')
|
|
def pos_login():
|
|
response = make_response(render_template('login.html',
|
|
tenant_id=getattr(g, 'tenant_id', None),
|
|
tenant_name=getattr(g, 'tenant_name', None),
|
|
tenant_subdomain=getattr(g, 'tenant_subdomain', None)))
|
|
response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0'
|
|
response.headers['Pragma'] = 'no-cache'
|
|
response.headers['Expires'] = '0'
|
|
return response
|
|
|
|
@app.route('/pos/supplier-catalog')
|
|
def supplier_catalog_page():
|
|
return render_template('supplier_catalog.html')
|
|
|
|
@app.route('/pos/catalog')
|
|
def pos_catalog():
|
|
return render_template('catalog.html')
|
|
|
|
@app.route('/pos/inventory')
|
|
def pos_inventory():
|
|
return render_template('inventory.html')
|
|
|
|
@app.route('/pos/sale')
|
|
def pos_sale():
|
|
return render_template('pos.html')
|
|
|
|
@app.route('/pos/customers')
|
|
def pos_customers():
|
|
return render_template('customers.html')
|
|
|
|
@app.route('/pos/invoicing')
|
|
def pos_invoicing():
|
|
return render_template('invoicing.html')
|
|
|
|
@app.route('/pos/accounting')
|
|
def pos_accounting():
|
|
return render_template('accounting.html')
|
|
|
|
@app.route('/pos/dashboard')
|
|
def pos_dashboard():
|
|
return render_template('dashboard.html')
|
|
|
|
@app.route('/pos/config')
|
|
def pos_config():
|
|
return render_template('config.html')
|
|
|
|
@app.route('/pos/reports')
|
|
def pos_reports():
|
|
return render_template('reports.html')
|
|
|
|
@app.route('/pos/fleet')
|
|
def pos_fleet():
|
|
return render_template('fleet.html')
|
|
|
|
@app.route('/pos/workshop')
|
|
def pos_workshop():
|
|
return render_template('workshop.html')
|
|
|
|
@app.route('/pos/quotations')
|
|
def pos_quotations():
|
|
return render_template('quotations.html')
|
|
|
|
@app.route('/pos/whatsapp')
|
|
def pos_whatsapp():
|
|
return render_template('whatsapp.html')
|
|
|
|
@app.route('/pos/marketplace')
|
|
def pos_marketplace():
|
|
return render_template('marketplace.html')
|
|
|
|
@app.route('/pos/marketplace-external')
|
|
def pos_marketplace_external():
|
|
return render_template('marketplace_external.html')
|
|
|
|
@app.route('/pos/marketplace-external/callback')
|
|
def pos_marketplace_external_callback():
|
|
return render_template('marketplace_external.html')
|
|
|
|
@app.route('/pos/historical-sales')
|
|
def pos_historical_sales():
|
|
return render_template('historical_sales.html')
|
|
|
|
@app.route('/pos/static/<path:filename>')
|
|
def pos_static(filename):
|
|
return send_from_directory('static', filename)
|
|
|
|
# ─── Sync: full inventory for offline cache ───────────────────
|
|
from middleware import require_auth as _require_auth
|
|
|
|
@app.route('/pos/api/sync/inventory', methods=['GET'])
|
|
@_require_auth()
|
|
def sync_full_inventory():
|
|
"""Download full inventory for offline cache."""
|
|
from tenant_db import get_tenant_conn
|
|
conn = get_tenant_conn(g.tenant_id)
|
|
cur = conn.cursor()
|
|
branch_id = g.branch_id
|
|
|
|
cur.execute("""
|
|
SELECT i.id, i.part_number, i.barcode, i.name, i.brand,
|
|
i.unit, i.price_1, i.price_2, i.price_3, i.tax_rate,
|
|
COALESCE(s.stock, 0) AS stock
|
|
FROM inventory i
|
|
LEFT JOIN (
|
|
SELECT inventory_id, COALESCE(SUM(quantity), 0) AS stock
|
|
FROM inventory_operations GROUP BY inventory_id
|
|
) s ON s.inventory_id = i.id
|
|
WHERE i.is_active = true AND i.branch_id = %s
|
|
ORDER BY i.name
|
|
""", [branch_id])
|
|
|
|
items = []
|
|
for r in cur.fetchall():
|
|
items.append({
|
|
'item_id': r[0], 'sku': r[1], 'barcode': r[2],
|
|
'name': r[3], 'brand': r[4], 'unit': r[5],
|
|
'price_1': float(r[6]) if r[6] else 0,
|
|
'price_2': float(r[7]) if r[7] else 0,
|
|
'price_3': float(r[8]) if r[8] else 0,
|
|
'tax_rate': float(r[9]) if r[9] else 0.16,
|
|
'stock': r[10]
|
|
})
|
|
cur.close()
|
|
conn.close()
|
|
return jsonify({'items': items, 'count': len(items)})
|
|
|
|
@app.route('/pos/api/sync/top-parts', methods=['GET'])
|
|
@_require_auth()
|
|
def sync_top_parts():
|
|
"""Get top 500 most-sold parts for offline catalog cache."""
|
|
from tenant_db import get_tenant_conn
|
|
conn = get_tenant_conn(g.tenant_id)
|
|
cur = conn.cursor()
|
|
branch_id = g.branch_id
|
|
|
|
cur.execute("""
|
|
SELECT i.part_number, i.name, i.brand, i.price_1, i.tax_rate,
|
|
i.category, COALESCE(s.stock, 0) AS stock,
|
|
COALESCE(sv.total_sold, 0) AS total_sold
|
|
FROM inventory i
|
|
LEFT JOIN (
|
|
SELECT inventory_id, COALESCE(SUM(quantity), 0) AS stock
|
|
FROM inventory_operations GROUP BY inventory_id
|
|
) s ON s.inventory_id = i.id
|
|
LEFT JOIN (
|
|
SELECT si.inventory_id, SUM(si.quantity) AS total_sold
|
|
FROM sale_items si
|
|
JOIN sales sa ON si.sale_id = sa.id
|
|
WHERE sa.status IN ('completed', 'partially_returned')
|
|
GROUP BY si.inventory_id
|
|
) sv ON sv.inventory_id = i.id
|
|
WHERE i.is_active = true AND i.branch_id = %s
|
|
ORDER BY COALESCE(sv.total_sold, 0) DESC
|
|
LIMIT 500
|
|
""", [branch_id])
|
|
|
|
parts = []
|
|
for r in cur.fetchall():
|
|
parts.append({
|
|
'part_number': r[0], 'name': r[1], 'brand': r[2],
|
|
'price': float(r[3]) if r[3] else 0,
|
|
'tax_rate': float(r[4]) if r[4] else 0.16,
|
|
'category': r[5] or '',
|
|
'stock': r[6], 'total_sold': r[7]
|
|
})
|
|
cur.close()
|
|
conn.close()
|
|
return jsonify({'parts': parts, 'count': len(parts)})
|
|
|
|
return app
|
|
|
|
# Expose at module level for Gunicorn: gunicorn -w 2 -b 0.0.0.0:5001 app:app
|
|
app = create_app()
|
|
|
|
if __name__ == '__main__':
|
|
app.run(host='0.0.0.0', port=5001, debug=True)
|