FASE 4: - Redis cache de stock con fallback graceful - Multi-moneda (MXN/USD) con contabilidad en MXN - Proveedores y ordenes de compra completo - Meilisearch 1.5M+ partes indexadas - Metabase KPIs con dashboard auto-generado FASE 5: - CRM mejorado: activities, tags, loyalty program, analytics - Imagenes de partes: upload, resize, thumbnails WebP - Ordenes de servicio Kanban: received->diagnosis->repair->ready->delivered - Garantias/RMA, alertas de reorden, multi-sucursal - Stubs BNPL (APLAZO) y ERP Sync (Aspel/Contpaqi) FASE 6: - Notificaciones automaticas: push/WhatsApp/email/in-app - Reportes de ahorro vs retail_price - Logistica + tracking: DHL, FedEx, Estafeta, 99min, Uber - API Publica: API keys, rate limiting, catalog search Migraciones: v1.9-v3.0 Tests: 93/93 pasando Backup: nexus_backup_20260427_045859.tar.gz
137 lines
4.4 KiB
Python
137 lines
4.4 KiB
Python
"""Image Blueprint: part image upload and management.
|
|
|
|
Prefix: /pos/api/inventory
|
|
"""
|
|
|
|
from flask import Blueprint, request, jsonify, g, send_from_directory
|
|
from middleware import require_auth
|
|
from tenant_db import get_tenant_conn
|
|
from services.image_service import save_image, delete_image, get_image_info
|
|
import os
|
|
|
|
image_bp = Blueprint('images', __name__, url_prefix='/pos/api/inventory')
|
|
|
|
|
|
@image_bp.route('/items/<int:item_id>/image', methods=['POST'])
|
|
@require_auth()
|
|
def upload_item_image(item_id):
|
|
"""Upload an image for an inventory item.
|
|
|
|
Supports multipart/form-data with 'image' file, or JSON with 'image_url'.
|
|
"""
|
|
tenant_id = g.tenant_id
|
|
|
|
# Check if item exists
|
|
conn = get_tenant_conn(tenant_id)
|
|
try:
|
|
cur = conn.cursor()
|
|
cur.execute("SELECT id FROM inventory WHERE id = %s", (item_id,))
|
|
if not cur.fetchone():
|
|
cur.close()
|
|
return jsonify({'error': 'Inventory item not found'}), 404
|
|
cur.close()
|
|
|
|
file_obj = None
|
|
image_url = None
|
|
filename_hint = None
|
|
|
|
if 'image' in request.files:
|
|
file = request.files['image']
|
|
if file.filename:
|
|
file_obj = file.stream
|
|
filename_hint = file.filename
|
|
elif request.is_json:
|
|
data = request.get_json() or {}
|
|
image_url = data.get('image_url')
|
|
|
|
if not file_obj and not image_url:
|
|
return jsonify({'error': 'No image provided. Upload via multipart "image" field or JSON "image_url"'}), 400
|
|
|
|
result = save_image(tenant_id, item_id, file_obj=file_obj,
|
|
image_url=image_url, filename_hint=filename_hint)
|
|
|
|
# Update inventory.image_url
|
|
cur = conn.cursor()
|
|
cur.execute("""
|
|
UPDATE inventory SET image_url = %s WHERE id = %s
|
|
""", (result['image_url'], item_id))
|
|
conn.commit()
|
|
cur.close()
|
|
|
|
return jsonify(result), 201
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
@image_bp.route('/items/<int:item_id>/image', methods=['GET'])
|
|
@require_auth()
|
|
def get_item_image(item_id):
|
|
"""Get image info for an inventory item."""
|
|
tenant_id = g.tenant_id
|
|
info = get_image_info(tenant_id, item_id)
|
|
return jsonify(info)
|
|
|
|
|
|
@image_bp.route('/items/<int:item_id>/image', methods=['DELETE'])
|
|
@require_auth()
|
|
def delete_item_image(item_id):
|
|
"""Delete the image for an inventory item."""
|
|
tenant_id = g.tenant_id
|
|
result = delete_image(tenant_id, item_id)
|
|
|
|
conn = get_tenant_conn(tenant_id)
|
|
try:
|
|
cur = conn.cursor()
|
|
cur.execute("UPDATE inventory SET image_url = NULL WHERE id = %s", (item_id,))
|
|
conn.commit()
|
|
cur.close()
|
|
return jsonify({'message': 'Image deleted', 'deleted': result['deleted']})
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
@image_bp.route('/images/bulk', methods=['POST'])
|
|
@require_auth()
|
|
def bulk_import_images():
|
|
"""Bulk import images from a list of {item_id, image_url} objects.
|
|
|
|
Body: {"items": [{"item_id": 1, "image_url": "https://..."}, ...]}
|
|
"""
|
|
data = request.get_json() or {}
|
|
items = data.get('items', [])
|
|
if not items:
|
|
return jsonify({'error': 'items array is required'}), 400
|
|
|
|
tenant_id = g.tenant_id
|
|
results = {'successful': [], 'failed': []}
|
|
|
|
conn = get_tenant_conn(tenant_id)
|
|
try:
|
|
cur = conn.cursor()
|
|
for item in items:
|
|
item_id = item.get('item_id')
|
|
image_url = item.get('image_url')
|
|
if not item_id or not image_url:
|
|
results['failed'].append({'item_id': item_id, 'error': 'Missing item_id or image_url'})
|
|
continue
|
|
|
|
# Verify item exists
|
|
cur.execute("SELECT id FROM inventory WHERE id = %s", (item_id,))
|
|
if not cur.fetchone():
|
|
results['failed'].append({'item_id': item_id, 'error': 'Item not found'})
|
|
continue
|
|
|
|
try:
|
|
result = save_image(tenant_id, item_id, image_url=image_url)
|
|
cur.execute("UPDATE inventory SET image_url = %s WHERE id = %s",
|
|
(result['image_url'], item_id))
|
|
results['successful'].append({'item_id': item_id, 'image_url': result['image_url']})
|
|
except Exception as e:
|
|
results['failed'].append({'item_id': item_id, 'error': str(e)})
|
|
|
|
conn.commit()
|
|
cur.close()
|
|
return jsonify(results)
|
|
finally:
|
|
conn.close()
|