FASE 4-5-6: Infraestructura, CRM, Service Orders, Notificaciones, Ahorro, Logistica, API Publica
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
This commit is contained in:
136
pos/blueprints/image_bp.py
Normal file
136
pos/blueprints/image_bp.py
Normal file
@@ -0,0 +1,136 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user